[Feat] Refactor manager API and add some test scripts
This commit is contained in:
138
main.py
138
main.py
@ -1,93 +1,53 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
from src.manager import *
|
import argparse
|
||||||
import logging
|
import json
|
||||||
import colorlog
|
from src.manager import ServiceFactory, ServiceRepository, Manager
|
||||||
import sys
|
from src.services import DockerServiceStrategy, SystemServiceStrategy
|
||||||
|
|
||||||
def initialize_logger():
|
def main():
|
||||||
"""Initialize and return a color logger."""
|
parser = argparse.ArgumentParser(description="服务管理工具")
|
||||||
handler = colorlog.StreamHandler()
|
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||||
handler.setFormatter(colorlog.ColoredFormatter(
|
|
||||||
'%(log_color)s%(asctime)s - %(levelname)s - %(message)s',
|
# 注册服务命令
|
||||||
log_colors={
|
register_parser = subparsers.add_parser("register", help="注册新服务")
|
||||||
'DEBUG': 'cyan',
|
register_parser.add_argument("tag", choices=["sys", "docker"], help="服务类型")
|
||||||
'INFO': 'green',
|
register_parser.add_argument("name", help="服务名称")
|
||||||
'WARNING': 'yellow',
|
register_parser.add_argument("--path", help="Docker服务路径(仅docker类型需要)")
|
||||||
'ERROR': 'red',
|
|
||||||
'CRITICAL': 'bold_red',
|
# 列出服务命令
|
||||||
}
|
subparsers.add_parser("list", help="列出所有服务")
|
||||||
))
|
|
||||||
|
# 执行操作命令
|
||||||
|
operate_parser = subparsers.add_parser("operate", help="执行服务操作")
|
||||||
|
operate_parser.add_argument("index", type=int, help="服务索引")
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# 初始化仓库和管理器
|
||||||
|
repo = ServiceRepository("services.json")
|
||||||
|
manager = Manager(repo)
|
||||||
|
|
||||||
|
if args.command == "register":
|
||||||
|
# 创建服务
|
||||||
|
service = manager.register_service(args.tag, args.name, args.path)
|
||||||
|
print(f"服务注册成功: {service.name}")
|
||||||
|
|
||||||
|
elif args.command == "list":
|
||||||
|
# 列出所有服务
|
||||||
|
services = manager.list_services()
|
||||||
|
for i, service in enumerate(services):
|
||||||
|
print(f"{i}: [{service.tag}] {service.name} {f'(path: {service.path})' if service.path else ''}")
|
||||||
|
|
||||||
|
elif args.command == "operate":
|
||||||
|
# 执行服务操作
|
||||||
|
try:
|
||||||
|
manager.execute_service_operation(args.index)
|
||||||
|
print("操作执行成功")
|
||||||
|
except IndexError:
|
||||||
|
print("错误:无效的服务索引")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"操作失败: {str(e)}")
|
||||||
|
|
||||||
logger = colorlog.getLogger(__name__)
|
if __name__ == "__main__":
|
||||||
logger.setLevel(logging.INFO)
|
main()
|
||||||
logger.addHandler(handler)
|
|
||||||
return logger
|
|
||||||
|
|
||||||
def initialize_service_manager():
|
|
||||||
"""Initialize and return the service manager."""
|
|
||||||
return Manager()
|
|
||||||
|
|
||||||
logger = initialize_logger()
|
|
||||||
service_manager = initialize_service_manager()
|
|
||||||
|
|
||||||
def start_menu():
|
|
||||||
logger.info(f"=======================================")
|
|
||||||
logger.info(f"========== Services Manager ===========")
|
|
||||||
logger.info(f"=======================================")
|
|
||||||
logger.info(f"Please choose a operation to carry out:")
|
|
||||||
logger.info(f"=====> 1. Register a new service <=====")
|
|
||||||
logger.info(f"=====> 2. Manage exsist services <=====")
|
|
||||||
logger.info(f"=====> c. Count the number of services")
|
|
||||||
logger.info(f"=====> q. Quit ")
|
|
||||||
|
|
||||||
def main_menu():
|
|
||||||
"""Display the main menu and handle user input."""
|
|
||||||
|
|
||||||
while True:
|
|
||||||
start_menu()
|
|
||||||
|
|
||||||
start_choice = input("Enter your choice: ").strip()
|
|
||||||
if start_choice == '1':
|
|
||||||
service_tag = input("Please input the deploy way of service you want to register('docker' | 'sys'): ")
|
|
||||||
service_name = input("Please input the name of the service: ")
|
|
||||||
if service_tag == "docker":
|
|
||||||
service_path = input("Please input the config path of the service where 'docker-compose.yml' located: ")
|
|
||||||
service_manager.register_service(service_tag, service_name, service_path)
|
|
||||||
else:
|
|
||||||
service_manager.register_service(service_tag, service_name)
|
|
||||||
if start_choice == '2':
|
|
||||||
if not service_manager.services_list:
|
|
||||||
logger.error("No services registered.")
|
|
||||||
continue
|
|
||||||
while True:
|
|
||||||
logger.info(f"=====> Exsist Services Management <=====")
|
|
||||||
service_code = 1
|
|
||||||
for service in service_manager.services_list:
|
|
||||||
logger.info(f"=====> {service_code}: {service._name}")
|
|
||||||
service_code += 1
|
|
||||||
# logger.info("=====> a. Select All")
|
|
||||||
logger.info("=====> q. Quit")
|
|
||||||
|
|
||||||
manage_choice = input("Please select a choice to carry out: ").strip()
|
|
||||||
if manage_choice == 'q':
|
|
||||||
logger.info("Exit main manager")
|
|
||||||
break
|
|
||||||
# if manage_choice == 'a':
|
|
||||||
# pass
|
|
||||||
try:
|
|
||||||
code = int(manage_choice)
|
|
||||||
if 0 <= code < len(service_manager.services_list):
|
|
||||||
service_manager.services_list[code].service_operation()
|
|
||||||
else:
|
|
||||||
logger.error("Invalid service code.")
|
|
||||||
except ValueError:
|
|
||||||
logger.error("Please enter a valid number.")
|
|
||||||
if start_choice == 'c':
|
|
||||||
service_manager.list_services()
|
|
||||||
if start_choice == 'q':
|
|
||||||
logger.info(f"Exit the manager process")
|
|
||||||
break
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
sys.exit(main_menu())
|
|
||||||
|
7
services.json
Normal file
7
services.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"tag": "sys",
|
||||||
|
"name": "nginx",
|
||||||
|
"path": null
|
||||||
|
}
|
||||||
|
]
|
212
src/manager.py
212
src/manager.py
@ -1,8 +1,11 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
from src.services import *
|
from src.services import Service
|
||||||
import logging
|
import logging
|
||||||
|
import json
|
||||||
|
import os
|
||||||
import colorlog
|
import colorlog
|
||||||
|
from typing import Optional, List, Dict
|
||||||
|
|
||||||
# Initialize color logging
|
# Initialize color logging
|
||||||
handler = colorlog.StreamHandler()
|
handler = colorlog.StreamHandler()
|
||||||
@ -20,68 +23,163 @@ handler.setFormatter(colorlog.ColoredFormatter(
|
|||||||
logger = colorlog.getLogger(__name__)
|
logger = colorlog.getLogger(__name__)
|
||||||
logger.setLevel(logging.INFO)
|
logger.setLevel(logging.INFO)
|
||||||
logger.addHandler(handler)
|
logger.addHandler(handler)
|
||||||
|
class ServiceFactory:
|
||||||
|
"""Factory for creating Service instances based on tag.
|
||||||
# nginx = Service(tag="sys", name="nginx")
|
|
||||||
# minecraft = Service(tag="sys", name="minecraft", path="~/web/minecraftService")
|
|
||||||
# homepage = Service(tag="docker", name="docker", path="~/web/homepageService")
|
|
||||||
# status = Service(tag="docker", name="status", path="~/web/statusService")
|
|
||||||
# gitea = Service(tag="docker", name="gitea", path="~/web/giteaService")
|
|
||||||
|
|
||||||
class Manager:
|
|
||||||
"""Interface set of services management operations.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
service_list (list): List use for storing service instances.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
@staticmethod
|
||||||
"""Initialize the Manager with an empty services list."""
|
def create_service(tag: str, name: str, path: Optional[str] = None) -> Service:
|
||||||
self.services_list = []
|
"""Create a Service instance with validation.
|
||||||
|
|
||||||
def append_service(self, service_instance: Service) -> None:
|
|
||||||
"""Append a service instance to the services list.
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
service_instance (Service): The service instance to append.
|
tag: Service type ('sys' or 'docker')
|
||||||
"""
|
name: Service name
|
||||||
self.services_list.append(service_instance)
|
path: Configuration path (required for docker services)
|
||||||
|
|
||||||
def register_service(self, service_tag: str, service_name: str, service_path=None) -> Service:
|
|
||||||
"""Register a new service.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
service_tag ('sys' | 'docker'): The tag that marks the service instance to use which way to deploy ('sys' or 'docker').
|
|
||||||
service_name (str): The name of the service instance.
|
|
||||||
service_path (str): The configuration and data path of the service instance.
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
service: A web service instance.
|
Service instance
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: For invalid inputs
|
||||||
"""
|
"""
|
||||||
|
if tag == "docker":
|
||||||
|
if not path:
|
||||||
|
raise ValueError("Path is required for docker services")
|
||||||
|
if not os.path.exists(path):
|
||||||
|
raise ValueError(f"Invalid service path: {path}")
|
||||||
|
if not os.path.isdir(path):
|
||||||
|
raise ValueError(f"Service path must be a directory: {path}")
|
||||||
|
logger.info(f"Validated docker service path: {path}")
|
||||||
|
return Service(tag=tag, name=name, path=path)
|
||||||
|
|
||||||
if service_tag == "docker":
|
|
||||||
assert service_path != None, """Can not leave a empty path while you register a docker-deploy service."""
|
class ServiceRepository:
|
||||||
if not os.path.exists(service_path):
|
"""Repository for service persistence.
|
||||||
raise ValueError(f"Invalid service path: {service_path}")
|
|
||||||
service = Service(tag=service_tag, name=service_name, path=service_path)
|
"""
|
||||||
# service.code = len(self.services_list) + 1
|
|
||||||
self.append_service(service_instance=service)
|
def __init__(self, file_path: str = '../data/services.json'):
|
||||||
|
self.file_path = file_path
|
||||||
|
|
||||||
|
def save(self, service: Service) -> None:
|
||||||
|
"""Save a service to the repository.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
service: Service instance to save
|
||||||
|
"""
|
||||||
|
# Load existing services
|
||||||
|
services = self.load_all()
|
||||||
|
|
||||||
|
# Add new service
|
||||||
|
services.append(service.to_dict())
|
||||||
|
|
||||||
|
# Save back to file
|
||||||
|
with open(self.file_path, 'w') as file:
|
||||||
|
json.dump(services, file, indent=4)
|
||||||
|
logger.info(f"Saved service to repository: {service.name}")
|
||||||
|
|
||||||
|
def load_all(self) -> List[Dict]:
|
||||||
|
"""Load all services from repository.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of service dictionaries
|
||||||
|
"""
|
||||||
|
if os.path.exists(self.file_path):
|
||||||
|
try:
|
||||||
|
with open(self.file_path, 'r') as file:
|
||||||
|
return json.load(file)
|
||||||
|
except (json.JSONDecodeError, FileNotFoundError):
|
||||||
|
logger.warning("Service repository file corrupted or missing")
|
||||||
|
return []
|
||||||
|
logger.info("Service repository file not found, starting fresh")
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
class Manager:
|
||||||
|
"""Interface for services management operations.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
repository: Optional[ServiceRepository] = None,
|
||||||
|
factory: Optional[ServiceFactory] = None):
|
||||||
|
"""Initialize Manager with dependencies.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
repository: Service repository instance (optional)
|
||||||
|
factory: Service factory instance (optional)
|
||||||
|
"""
|
||||||
|
self.repository = repository or ServiceRepository()
|
||||||
|
self.factory = factory or ServiceFactory()
|
||||||
|
logger.info("Manager initialized")
|
||||||
|
|
||||||
|
def register_service(self,
|
||||||
|
service_tag: str,
|
||||||
|
service_name: str,
|
||||||
|
service_path: Optional[str] = None) -> Service:
|
||||||
|
"""Register and persist a new service.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
service_tag: Service type ('sys' or 'docker')
|
||||||
|
service_name: Service name
|
||||||
|
service_path: Configuration path (required for docker)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Registered service instance
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If service registration fails
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Create service via factory
|
||||||
|
service = self.factory.create_service(service_tag, service_name, service_path)
|
||||||
|
|
||||||
|
# Persist service
|
||||||
|
self.repository.save(service)
|
||||||
|
|
||||||
|
logger.info(f"Successfully registered service: {service_name} ({service_tag})")
|
||||||
return service
|
return service
|
||||||
service = Service(tag=service_tag, name=service_name)
|
|
||||||
# service.code = len(self.services_list) + 1
|
except ValueError as e:
|
||||||
self.append_service(service_instance=service)
|
logger.error(f"Service registration failed: {str(e)}")
|
||||||
return service
|
raise
|
||||||
|
|
||||||
# TODO: Add the feature which can store the registered services into a JSON file whose path with ./data/services.json.
|
|
||||||
|
|
||||||
def list_services(self) -> None:
|
|
||||||
"""Method use for count the num of service instances and list the names of them.
|
|
||||||
|
|
||||||
|
def list_services(self) -> List[Service]:
|
||||||
|
"""List all registered services.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of Service objects
|
||||||
"""
|
"""
|
||||||
|
services_data = self.repository.load_all()
|
||||||
sum_of_service = 0
|
if not services_data:
|
||||||
service_name_list = []
|
logger.info("No services registered")
|
||||||
for services in self.services_list:
|
return []
|
||||||
service_name_list.append(services._name)
|
|
||||||
sum_of_service += 1
|
services = []
|
||||||
logger.info(f"The manager has registered {sum_of_service} services: {', '.join(service_name_list)}")
|
for s in services_data:
|
||||||
|
try:
|
||||||
|
service = Service(tag=s["tag"], name=s["name"], path=s["path"])
|
||||||
|
services.append(service)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Invalid service data: {s}, error: {str(e)}")
|
||||||
|
|
||||||
|
service_names = [s.name for s in services]
|
||||||
|
logger.info(f"Registered services ({len(services)}): {', '.join(service_names)}")
|
||||||
|
return services
|
||||||
|
|
||||||
|
def execute_service_operation(self, index: int) -> None:
|
||||||
|
"""Execute service operation for the service at the given index.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
index: Index of the service in the list returned by list_services()
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
IndexError: If index is out of bounds
|
||||||
|
"""
|
||||||
|
services = self.list_services()
|
||||||
|
if index < 0 or index >= len(services):
|
||||||
|
raise IndexError(f"Invalid service index: {index}")
|
||||||
|
|
||||||
|
service = services[index]
|
||||||
|
service.service_operation()
|
||||||
|
212
src/services.py
212
src/services.py
@ -4,6 +4,8 @@ import subprocess
|
|||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
import colorlog
|
import colorlog
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from typing import Optional, Tuple, List, Callable
|
||||||
|
|
||||||
# Initialize color logging
|
# Initialize color logging
|
||||||
handler = colorlog.StreamHandler()
|
handler = colorlog.StreamHandler()
|
||||||
@ -23,11 +25,16 @@ logger.setLevel(logging.INFO)
|
|||||||
logger.addHandler(handler)
|
logger.addHandler(handler)
|
||||||
|
|
||||||
|
|
||||||
def get_operation():
|
def get_operation() -> Optional[int]:
|
||||||
|
"""Get service operation from user input.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
0: stop, 1: restart, None: quit
|
||||||
|
"""
|
||||||
while True:
|
while True:
|
||||||
choice = input("Please select an operation (0 to stop, 1 to restart, q to quit): ").strip().lower()
|
choice = input("Please select an operation (0 to stop, 1 to restart, q to quit): ").strip().lower()
|
||||||
if choice == 'q':
|
if choice == 'q':
|
||||||
return 'q'
|
return None
|
||||||
try:
|
try:
|
||||||
op = int(choice)
|
op = int(choice)
|
||||||
if op in (0, 1):
|
if op in (0, 1):
|
||||||
@ -37,80 +44,151 @@ def get_operation():
|
|||||||
logger.warning("Invalid input, please enter 0, 1, or q.")
|
logger.warning("Invalid input, please enter 0, 1, or q.")
|
||||||
|
|
||||||
|
|
||||||
class Service:
|
class ServiceStrategy(ABC):
|
||||||
"""A template management for web services
|
"""Abstract base class for service operation strategies."""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def generate_command(self, operation: int, service_name: str, path: Optional[str] = None) -> List[str]:
|
||||||
|
"""Generate command for service operation.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
operation: 0=stop, 1=restart
|
||||||
|
service_name: Name of the service
|
||||||
|
path: Service path (for docker)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Command list for subprocess
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def execute(self, command: List[str]) -> None:
|
||||||
|
"""Execute the service command.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
command: Command to execute
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
subprocess.CalledProcessError: If command fails
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class SystemServiceStrategy(ServiceStrategy):
|
||||||
|
"""Strategy for systemd services."""
|
||||||
|
|
||||||
|
def __init__(self, path: Optional[str] = None):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def generate_command(self, operation: int, service_name: str, path: Optional[str] = None) -> List[str]:
|
||||||
|
if operation == 0:
|
||||||
|
return ["sudo", "systemctl", "stop", service_name]
|
||||||
|
elif operation == 1:
|
||||||
|
return ["sudo", "systemctl", "restart", service_name]
|
||||||
|
raise ValueError(f"Invalid operation for system service: {operation}")
|
||||||
|
|
||||||
|
def execute(self, command: List[str]) -> None:
|
||||||
|
subprocess.run(command, check=True)
|
||||||
|
|
||||||
|
|
||||||
|
class DockerServiceStrategy(ServiceStrategy):
|
||||||
|
"""Strategy for docker-compose services."""
|
||||||
|
|
||||||
|
def __init__(self, path: str):
|
||||||
|
self.path = path
|
||||||
|
|
||||||
|
def generate_command(self, operation: int, service_name: str, path: Optional[str] = None) -> List[str]:
|
||||||
|
if not self.path:
|
||||||
|
raise ValueError("Docker service requires a path")
|
||||||
|
|
||||||
|
expanded_path = os.path.expanduser(self.path)
|
||||||
|
if not os.path.exists(expanded_path):
|
||||||
|
raise FileNotFoundError(f"Docker path not found: {expanded_path}")
|
||||||
|
if not os.path.isdir(expanded_path):
|
||||||
|
raise NotADirectoryError(f"Docker path must be a directory: {expanded_path}")
|
||||||
|
|
||||||
|
if operation == 0:
|
||||||
|
return ["docker", "compose", "down"]
|
||||||
|
elif operation == 1:
|
||||||
|
return ["docker", "compose", "up", "-d"]
|
||||||
|
raise ValueError(f"Invalid operation for docker service: {operation}")
|
||||||
|
|
||||||
|
def execute(self, command: List[str]) -> None:
|
||||||
|
expanded_path = os.path.expanduser(self.path)
|
||||||
|
if not os.path.exists(expanded_path):
|
||||||
|
raise FileNotFoundError(f"Path not found: {expanded_path}")
|
||||||
|
if not os.path.isdir(expanded_path):
|
||||||
|
raise NotADirectoryError(f"Docker path must be directory: {expanded_path}")
|
||||||
|
subprocess.run(command, check=True, cwd=expanded_path)
|
||||||
|
|
||||||
|
|
||||||
|
class Service:
|
||||||
|
"""Management interface for web services.
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
_tag ('sys' | 'docker'): The tag that marks the service instance to use which way to deploy.
|
tag: Service type ('sys' or 'docker')
|
||||||
_name (str): The name of the service instance.
|
name: Service name
|
||||||
_path (str): The configuration and data path of the service instance.
|
path: Configuration path (for docker services)
|
||||||
|
strategy: Service operation strategy
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, tag: str, name: str, path=None):
|
STRATEGIES = {
|
||||||
self._tag = tag
|
'sys': SystemServiceStrategy,
|
||||||
self._name = name
|
'docker': DockerServiceStrategy
|
||||||
self._path = path
|
}
|
||||||
|
|
||||||
def command_gen(self):
|
|
||||||
"""Generate service management commands based on the service's tag.
|
|
||||||
|
|
||||||
|
def __init__(self, tag: str, name: str, path: Optional[str] = None):
|
||||||
|
"""Initialize a service instance.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tag: Service type ('sys' or 'docker')
|
||||||
|
name: Service name
|
||||||
|
path: Configuration path (required for docker)
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: For invalid tag
|
||||||
"""
|
"""
|
||||||
|
if tag not in self.STRATEGIES:
|
||||||
|
raise ValueError(f"Invalid service tag: {tag}")
|
||||||
|
|
||||||
|
self.tag = tag
|
||||||
|
self.name = name
|
||||||
|
self.path = path
|
||||||
|
self.strategy = self.STRATEGIES[tag](path=self.path)
|
||||||
|
logger.info(f"Initialized {tag} service: {name}")
|
||||||
|
|
||||||
|
def to_dict(self) -> dict:
|
||||||
|
"""Return dictionary representation of the service."""
|
||||||
|
return {
|
||||||
|
"tag": self.tag,
|
||||||
|
"name": self.name,
|
||||||
|
"path": self.path
|
||||||
|
}
|
||||||
|
|
||||||
system_services_command = "sudo systemctl"
|
def service_operation(self, operation_getter: Callable[[], Optional[int]] = get_operation) -> None:
|
||||||
docker_services_command = f"cd {os.path.expanduser(f'{self._path}')} && docker compose"
|
"""Perform service operation based on user input.
|
||||||
if self._tag == "sys":
|
|
||||||
return system_services_command
|
Args:
|
||||||
elif self._tag == "docker":
|
operation_getter: Function to get operation choice
|
||||||
return docker_services_command
|
|
||||||
else:
|
|
||||||
raise ValueError(f"The service tag {self._tag} was not included.")
|
|
||||||
|
|
||||||
def service_operation(self):
|
|
||||||
"""Manage the service based on user input.
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
operation = operation_getter()
|
||||||
operation = get_operation()
|
if operation is None:
|
||||||
if operation == 'q':
|
logger.info("Operation cancelled by user")
|
||||||
logger.info("User chose to quit the service management.")
|
|
||||||
return
|
return
|
||||||
|
|
||||||
command = self.command_gen()
|
try:
|
||||||
full_command = None # Initialize full_command
|
# Generate and execute command
|
||||||
|
command = self.strategy.generate_command(operation, self.name, self.path)
|
||||||
if self._tag == "sys":
|
logger.info(f"Executing: {' '.join(command)}")
|
||||||
if operation == 0:
|
self.strategy.execute(command)
|
||||||
# Stop the service
|
logger.info(f"Service {self.name} operation completed")
|
||||||
full_command = f"{command} stop {self._name}"
|
except (ValueError, FileNotFoundError, NotADirectoryError, subprocess.CalledProcessError) as e:
|
||||||
logger.info(f"Stopping service: {self._name}")
|
logger.error(f"Service operation failed: {str(e)}")
|
||||||
elif operation == 1:
|
|
||||||
# Restart the service
|
|
||||||
full_command = f"{command} restart {self._name}"
|
|
||||||
logger.info(f"Restarting service: {self._name}")
|
|
||||||
else:
|
|
||||||
logger.warning("Invalid operation; no service management executed.")
|
|
||||||
return # Exit if the operation is invalid
|
|
||||||
if self._tag == "docker":
|
|
||||||
if operation == 0:
|
|
||||||
# Stop the service
|
|
||||||
full_command = f"{command} down"
|
|
||||||
logger.info(f"Stopping service: {self._name}")
|
|
||||||
elif operation == 1:
|
|
||||||
# Restart the service
|
|
||||||
full_command = f"{command} up -d"
|
|
||||||
logger.info(f"Restarting service: {self._name}")
|
|
||||||
else:
|
|
||||||
logger.warning("Invalid operation; no service management executed.")
|
|
||||||
return # Exit if the operation is invalid
|
|
||||||
|
|
||||||
if full_command: # Ensure full_command is defined
|
|
||||||
try:
|
|
||||||
subprocess.run(full_command, check=True, shell=True)
|
|
||||||
logger.info(f"Service {self._name} operation completed successfully.")
|
|
||||||
except subprocess.CalledProcessError as e:
|
|
||||||
logger.error(f"Failed to manage service {self._name}: {e}")
|
|
||||||
|
|
||||||
# Example call
|
# Example usage
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
service = Service(tag="docker", name="homepage", path="~/web/homepageService")
|
try:
|
||||||
service.service_operation()
|
service = Service(tag="docker", name="homepage", path="~/projects/webservices/homepage")
|
||||||
|
service.service_operation()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Unhandled exception: {str(e)}")
|
||||||
|
93
test/main.py
93
test/main.py
@ -1,93 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
from src.manager import *
|
|
||||||
import logging
|
|
||||||
import colorlog
|
|
||||||
import sys
|
|
||||||
|
|
||||||
def initialize_logger():
|
|
||||||
"""Initialize and return a color logger."""
|
|
||||||
handler = colorlog.StreamHandler()
|
|
||||||
handler.setFormatter(colorlog.ColoredFormatter(
|
|
||||||
'%(log_color)s%(asctime)s - %(levelname)s - %(message)s',
|
|
||||||
log_colors={
|
|
||||||
'DEBUG': 'cyan',
|
|
||||||
'INFO': 'green',
|
|
||||||
'WARNING': 'yellow',
|
|
||||||
'ERROR': 'red',
|
|
||||||
'CRITICAL': 'bold_red',
|
|
||||||
}
|
|
||||||
))
|
|
||||||
|
|
||||||
logger = colorlog.getLogger(__name__)
|
|
||||||
logger.setLevel(logging.INFO)
|
|
||||||
logger.addHandler(handler)
|
|
||||||
return logger
|
|
||||||
|
|
||||||
def initialize_service_manager():
|
|
||||||
"""Initialize and return the service manager."""
|
|
||||||
return Manager()
|
|
||||||
|
|
||||||
logger = initialize_logger()
|
|
||||||
service_manager = initialize_service_manager()
|
|
||||||
|
|
||||||
def start_menu():
|
|
||||||
logger.info(f"=======================================")
|
|
||||||
logger.info(f"========== Services Manager ===========")
|
|
||||||
logger.info(f"=======================================")
|
|
||||||
logger.info(f"Please choose a operation to carry out:")
|
|
||||||
logger.info(f"=====> 1. Register a new service <=====")
|
|
||||||
logger.info(f"=====> 2. Manage exsist services <=====")
|
|
||||||
logger.info(f"=====> c. Count the number of services")
|
|
||||||
logger.info(f"=====> q. Quit ")
|
|
||||||
|
|
||||||
def main_menu():
|
|
||||||
"""Display the main menu and handle user input."""
|
|
||||||
|
|
||||||
while True:
|
|
||||||
start_menu()
|
|
||||||
|
|
||||||
start_choice = input("Enter your choice: ").strip()
|
|
||||||
if start_choice == '1':
|
|
||||||
service_tag = input("Please input the deploy way of service you want to register('docker' | 'sys'): ")
|
|
||||||
service_name = input("Please input the name of the service: ")
|
|
||||||
if service_tag == "docker":
|
|
||||||
service_path = input("Please input the config path of the service where 'docker-compose.yml' located: ")
|
|
||||||
service_manager.register_service(service_tag, service_name, service_path)
|
|
||||||
else:
|
|
||||||
service_manager.register_service(service_tag, service_name)
|
|
||||||
if start_choice == '2':
|
|
||||||
if not service_manager.services_list:
|
|
||||||
logger.error("No services registered.")
|
|
||||||
continue
|
|
||||||
while True:
|
|
||||||
logger.info(f"=====> Exsist Services Management <=====")
|
|
||||||
service_code = 0
|
|
||||||
for service in service_manager.services_list:
|
|
||||||
logger.info(f"=====> {service_code}: {service._name}")
|
|
||||||
service_code += 1
|
|
||||||
# logger.info("=====> a. Select All")
|
|
||||||
logger.info("=====> q. Quit")
|
|
||||||
|
|
||||||
manage_choice = input("Please select a choice to carry out: ").strip()
|
|
||||||
if manage_choice == 'q':
|
|
||||||
logger.info("Exit main manager")
|
|
||||||
break
|
|
||||||
# if manage_choice == 'a':
|
|
||||||
# pass
|
|
||||||
try:
|
|
||||||
code = int(manage_choice)
|
|
||||||
if 0 <= code < len(service_manager.services_list):
|
|
||||||
service_manager.services_list[code].service_operation()
|
|
||||||
else:
|
|
||||||
logger.error("Invalid service code.")
|
|
||||||
except ValueError:
|
|
||||||
logger.error("Please enter a valid number.")
|
|
||||||
if start_choice == 'c':
|
|
||||||
service_manager.list_services()
|
|
||||||
if start_choice == 'q':
|
|
||||||
logger.info(f"Exit the manager process")
|
|
||||||
break
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
sys.exit(main_menu())
|
|
143
test/test_manager.py
Normal file
143
test/test_manager.py
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
import unittest
|
||||||
|
from unittest.mock import patch, MagicMock
|
||||||
|
from src.manager import ServiceFactory, ServiceRepository, Manager
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
class TestServiceFactory(unittest.TestCase):
|
||||||
|
def test_create_valid_service(self):
|
||||||
|
"""测试创建有效的服务"""
|
||||||
|
factory = ServiceFactory()
|
||||||
|
service = factory.create_service("sys", "nginx")
|
||||||
|
self.assertEqual(service.tag, "sys")
|
||||||
|
self.assertEqual(service.name, "nginx")
|
||||||
|
self.assertIsNone(service.path)
|
||||||
|
|
||||||
|
def test_create_invalid_tag(self):
|
||||||
|
"""测试创建无效标签的服务"""
|
||||||
|
factory = ServiceFactory()
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
factory.create_service("invalid", "invalid_service")
|
||||||
|
|
||||||
|
class TestServiceRepository(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.temp_dir = tempfile.TemporaryDirectory()
|
||||||
|
self.repo_path = os.path.join(self.temp_dir.name, "services.json")
|
||||||
|
self.repo = ServiceRepository(self.repo_path)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
self.temp_dir.cleanup()
|
||||||
|
|
||||||
|
def test_save_and_load(self):
|
||||||
|
"""测试服务保存和加载"""
|
||||||
|
# 创建模拟服务
|
||||||
|
mock_service = MagicMock()
|
||||||
|
mock_service.to_dict.return_value = {
|
||||||
|
"tag": "sys",
|
||||||
|
"name": "nginx",
|
||||||
|
"path": None
|
||||||
|
}
|
||||||
|
|
||||||
|
# 保存服务
|
||||||
|
self.repo.save(mock_service)
|
||||||
|
|
||||||
|
# 加载服务
|
||||||
|
services = self.repo.load_all()
|
||||||
|
|
||||||
|
# 验证加载结果
|
||||||
|
self.assertEqual(len(services), 1)
|
||||||
|
self.assertEqual(services[0]["tag"], "sys")
|
||||||
|
self.assertEqual(services[0]["name"], "nginx")
|
||||||
|
|
||||||
|
def test_load_invalid_file(self):
|
||||||
|
"""测试加载无效JSON文件"""
|
||||||
|
with open(self.repo_path, "w") as f:
|
||||||
|
f.write("invalid json")
|
||||||
|
|
||||||
|
services = self.repo.load_all()
|
||||||
|
self.assertEqual(len(services), 0)
|
||||||
|
|
||||||
|
class TestManager(unittest.TestCase):
|
||||||
|
@patch("src.manager.ServiceRepository")
|
||||||
|
@patch("src.manager.ServiceFactory")
|
||||||
|
def test_register_service(self, mock_factory, mock_repo):
|
||||||
|
"""测试服务注册"""
|
||||||
|
# 准备模拟对象
|
||||||
|
mock_service = MagicMock()
|
||||||
|
mock_factory.return_value.create_service.return_value = mock_service
|
||||||
|
mock_repo_instance = MagicMock()
|
||||||
|
|
||||||
|
# 创建管理器
|
||||||
|
manager = Manager(mock_repo.return_value)
|
||||||
|
|
||||||
|
# 注册服务
|
||||||
|
service = manager.register_service("sys", "nginx")
|
||||||
|
|
||||||
|
# 验证调用
|
||||||
|
mock_factory.return_value.create_service.assert_called_with("sys", "nginx", None)
|
||||||
|
mock_repo.return_value.save.assert_called_with(mock_service)
|
||||||
|
self.assertEqual(service, mock_service)
|
||||||
|
|
||||||
|
@patch("src.manager.ServiceRepository")
|
||||||
|
def test_list_services(self, mock_repo):
|
||||||
|
"""测试服务列表"""
|
||||||
|
# 准备模拟服务
|
||||||
|
mock_service1 = MagicMock()
|
||||||
|
mock_service1.name = "nginx"
|
||||||
|
mock_service2 = MagicMock()
|
||||||
|
mock_service2.name = "postgres"
|
||||||
|
|
||||||
|
# 创建管理器
|
||||||
|
manager = Manager(mock_repo.return_value)
|
||||||
|
mock_repo.return_value.load_all.return_value = [
|
||||||
|
{"tag": "sys", "name": "nginx", "path": None},
|
||||||
|
{"tag": "docker", "name": "postgres", "path": "/path/to/docker"}
|
||||||
|
]
|
||||||
|
|
||||||
|
# 获取服务列表
|
||||||
|
services = manager.list_services()
|
||||||
|
|
||||||
|
# 验证结果
|
||||||
|
# 修复预期服务数量
|
||||||
|
self.assertEqual(len(services), 2)
|
||||||
|
self.assertEqual(services[0].name, "nginx")
|
||||||
|
self.assertEqual(services[1].name, "postgres")
|
||||||
|
|
||||||
|
@patch("src.manager.Service")
|
||||||
|
@patch("src.services.get_operation", return_value=1)
|
||||||
|
def test_execute_service_operation(self, mock_get_operation, mock_service, mock_repo):
|
||||||
|
"""测试服务操作执行"""
|
||||||
|
# 准备模拟对象
|
||||||
|
manager = Manager(mock_repo.return_value)
|
||||||
|
mock_service_instance = MagicMock()
|
||||||
|
mock_service.return_value = mock_service_instance
|
||||||
|
|
||||||
|
# 模拟服务列表
|
||||||
|
with patch.object(manager, 'list_services', return_value=[mock_service_instance]):
|
||||||
|
# 执行服务操作
|
||||||
|
manager.execute_service_operation(0) # 执行索引0的服务
|
||||||
|
|
||||||
|
# 验证调用
|
||||||
|
mock_service_instance.service_operation.assert_called_once()
|
||||||
|
|
||||||
|
def test_register_invalid_service(self):
|
||||||
|
"""测试注册无效服务"""
|
||||||
|
manager = Manager(MagicMock())
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
manager.register_service("invalid", "invalid_service")
|
||||||
|
|
||||||
|
@patch("src.manager.logger")
|
||||||
|
def test_execute_service_operation_invalid_index(self, mock_repo, mock_logger):
|
||||||
|
"""测试无效服务索引"""
|
||||||
|
manager = Manager(mock_repo.return_value)
|
||||||
|
|
||||||
|
# 模拟空服务列表
|
||||||
|
with patch.object(manager, 'list_services', return_value=[]):
|
||||||
|
with self.assertRaises(IndexError):
|
||||||
|
manager.execute_service_operation(0)
|
||||||
|
|
||||||
|
mock_logger.error.assert_called()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
150
test/test_services.py
Normal file
150
test/test_services.py
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
import unittest
|
||||||
|
from unittest.mock import patch, MagicMock
|
||||||
|
from src.services import Service, SystemServiceStrategy, DockerServiceStrategy, get_operation
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
class TestService(unittest.TestCase):
|
||||||
|
def test_service_initialization(self):
|
||||||
|
"""测试服务初始化"""
|
||||||
|
service = Service(tag="sys", name="nginx")
|
||||||
|
self.assertEqual(service.tag, "sys")
|
||||||
|
self.assertEqual(service.name, "nginx")
|
||||||
|
self.assertIsNone(service.path)
|
||||||
|
|
||||||
|
def test_docker_service_initialization(self):
|
||||||
|
"""测试Docker服务初始化"""
|
||||||
|
service = Service(tag="docker", name="homepage", path="/path/to/docker")
|
||||||
|
self.assertEqual(service.tag, "docker")
|
||||||
|
self.assertEqual(service.name, "homepage")
|
||||||
|
self.assertEqual(service.path, "/path/to/docker")
|
||||||
|
|
||||||
|
def test_invalid_tag(self):
|
||||||
|
"""测试无效标签"""
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
Service(tag="invalid", name="invalid")
|
||||||
|
|
||||||
|
def test_to_dict(self):
|
||||||
|
"""测试字典转换"""
|
||||||
|
service = Service(tag="sys", name="nginx")
|
||||||
|
result = service.to_dict()
|
||||||
|
self.assertEqual(result, {
|
||||||
|
"tag": "sys",
|
||||||
|
"name": "nginx",
|
||||||
|
"path": None
|
||||||
|
})
|
||||||
|
|
||||||
|
class TestSystemServiceStrategy(unittest.TestCase):
|
||||||
|
def test_generate_stop_command(self):
|
||||||
|
"""测试生成停止命令"""
|
||||||
|
strategy = SystemServiceStrategy()
|
||||||
|
command = strategy.generate_command(0, "nginx")
|
||||||
|
self.assertEqual(command, ["sudo", "systemctl", "stop", "nginx"])
|
||||||
|
|
||||||
|
def test_generate_restart_command(self):
|
||||||
|
"""测试生成重启命令"""
|
||||||
|
strategy = SystemServiceStrategy()
|
||||||
|
command = strategy.generate_command(1, "nginx")
|
||||||
|
self.assertEqual(command, ["sudo", "systemctl", "restart", "nginx"])
|
||||||
|
|
||||||
|
def test_invalid_operation(self):
|
||||||
|
"""测试无效操作"""
|
||||||
|
strategy = SystemServiceStrategy()
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
strategy.generate_command(2, "nginx")
|
||||||
|
|
||||||
|
@patch("subprocess.run")
|
||||||
|
def test_execute_command(self, mock_run):
|
||||||
|
"""测试命令执行"""
|
||||||
|
strategy = SystemServiceStrategy()
|
||||||
|
command = ["sudo", "systemctl", "stop", "nginx"]
|
||||||
|
strategy.execute(command)
|
||||||
|
mock_run.assert_called_with(command, check=True)
|
||||||
|
|
||||||
|
class TestDockerServiceStrategy(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.path = "/path/to/docker"
|
||||||
|
self.strategy = DockerServiceStrategy(self.path)
|
||||||
|
os.path.exists = MagicMock(return_value=True)
|
||||||
|
os.path.isdir = MagicMock(return_value=True)
|
||||||
|
|
||||||
|
def test_generate_down_command(self):
|
||||||
|
"""测试生成停止命令"""
|
||||||
|
command = self.strategy.generate_command(0, "homepage")
|
||||||
|
self.assertEqual(command, ["docker", "compose", "down"])
|
||||||
|
|
||||||
|
def test_generate_up_command(self):
|
||||||
|
"""测试生成启动命令"""
|
||||||
|
command = self.strategy.generate_command(1, "homepage")
|
||||||
|
self.assertEqual(command, ["docker", "compose", "up", "-d"])
|
||||||
|
|
||||||
|
def test_missing_path(self):
|
||||||
|
"""测试路径为空字符串"""
|
||||||
|
strategy = DockerServiceStrategy("")
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
strategy.generate_command(0, "homepage")
|
||||||
|
|
||||||
|
@patch("os.path.exists", return_value=False)
|
||||||
|
def test_path_not_found(self, mock_exists):
|
||||||
|
"""测试路径不存在"""
|
||||||
|
with self.assertRaises(FileNotFoundError):
|
||||||
|
self.strategy.generate_command(0, "homepage")
|
||||||
|
|
||||||
|
@patch("subprocess.run")
|
||||||
|
@patch("os.path.exists", return_value=True)
|
||||||
|
@patch("os.path.isdir", return_value=True)
|
||||||
|
def test_execute_command(self, mock_isdir, mock_exists, mock_run):
|
||||||
|
"""测试命令执行"""
|
||||||
|
command = ["docker", "compose", "down"]
|
||||||
|
self.strategy.execute(command)
|
||||||
|
mock_run.assert_called_with(command, check=True, cwd="/path/to/docker")
|
||||||
|
|
||||||
|
class TestServiceOperation(unittest.TestCase):
|
||||||
|
@patch("builtins.input", side_effect=["0"])
|
||||||
|
def test_get_operation_stop(self, mock_input):
|
||||||
|
"""测试获取停止操作"""
|
||||||
|
result = get_operation()
|
||||||
|
self.assertEqual(result, 0)
|
||||||
|
|
||||||
|
@patch("builtins.input", side_effect=["1"])
|
||||||
|
def test_get_operation_restart(self, mock_input):
|
||||||
|
"""测试获取重启操作"""
|
||||||
|
result = get_operation()
|
||||||
|
self.assertEqual(result, 1)
|
||||||
|
|
||||||
|
@patch("builtins.input", side_effect=["q"])
|
||||||
|
def test_get_operation_quit(self, mock_input):
|
||||||
|
"""测试退出操作"""
|
||||||
|
result = get_operation()
|
||||||
|
self.assertIsNone(result)
|
||||||
|
|
||||||
|
@patch("builtins.input", side_effect=["invalid", "0"])
|
||||||
|
@patch("src.services.logger.warning")
|
||||||
|
def test_invalid_input(self, mock_warning, mock_input):
|
||||||
|
"""测试无效输入"""
|
||||||
|
result = get_operation()
|
||||||
|
self.assertEqual(result, 0)
|
||||||
|
mock_warning.assert_called()
|
||||||
|
|
||||||
|
@patch("src.services.get_operation", return_value=0)
|
||||||
|
@patch("src.services.SystemServiceStrategy.generate_command", return_value=["sudo", "systemctl", "stop", "nginx"])
|
||||||
|
@patch("src.services.SystemServiceStrategy.execute")
|
||||||
|
@patch("src.services.logger.info")
|
||||||
|
@patch("src.services.SystemServiceStrategy.execute")
|
||||||
|
@patch("src.services.SystemServiceStrategy.generate_command")
|
||||||
|
@patch("src.services.get_operation", return_value=0)
|
||||||
|
def test_service_operation(self, mock_get_operation, mock_generate, mock_execute, mock_info):
|
||||||
|
"""测试服务操作流程"""
|
||||||
|
service = Service(tag="sys", name="nginx")
|
||||||
|
service.service_operation()
|
||||||
|
|
||||||
|
# 修复:generate_command 需要 self 参数
|
||||||
|
mock_generate.assert_called_with(0, "nginx", None)
|
||||||
|
mock_execute.assert_called_with(["sudo", "systemctl", "stop", "nginx"])
|
||||||
|
mock_info.assert_any_call("Executing: sudo systemctl stop nginx")
|
||||||
|
mock_info.assert_any_call("Service nginx operation completed")
|
||||||
|
mock_info.assert_any_call("Executing: sudo systemctl stop nginx")
|
||||||
|
mock_info.assert_any_call("Service nginx operation completed")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
Reference in New Issue
Block a user