feat: 重构项目并添加新功能

- 移除 defaultenv 文件和未使用的模板文件
- 重构 main.py,添加新的 Docker Compose 配置解析逻辑
- 新增 Nginx 配置生成和安全重启功能
- 更新 requirements.txt,替换 jinja2 为 pyyaml
- 删除未使用的 src/config_reader.py 和 src/jinja2_render.py 文件
This commit is contained in:
ZZY 2025-04-26 16:56:38 +08:00
parent a4ebe33fca
commit 9297f9170a
9 changed files with 357 additions and 405 deletions

View File

@ -1,6 +0,0 @@
DOCKER_INIPATH = docker.ini
NGINX_INIPATH = nginx.ini
DOCKER_COMPOSE_OUTPATH = docker-compose.yml
INNER_NGINX_OUTPATH = inner-nginx.conf
OUTER_NGINX_OUTPATH = outer-nginx.conf

215
main.py
View File

@ -1,116 +1,141 @@
""" """
this is the main file main
""" """
import argparse
import os
import subprocess import subprocess
import logging
import argparse
from pathlib import Path from pathlib import Path
from dotenv import load_dotenv import yaml
from src.jinja2_render import render_dataclass
from src.config_reader import nginx_config, docker_config, docker2nginx
load_dotenv() from src.logger import get_logger
from src.nginx import NginxConfig, NginxConfigurator
logger = get_logger(__name__)
def run_command(command): # 配置常量
"run a command and use stdout" FILE_PATH = Path(__file__).parent
DOCKER_COMPOSE_FILE = Path(FILE_PATH, "docker-compose.yml")
NETWORK_NAME = "nginx-net"
def validate_docker_network():
"""检查Docker网络是否存在"""
try: try:
result = subprocess.run(command, shell=True, check=True, result = subprocess.run(
stdout=subprocess.PIPE, stderr=subprocess.PIPE) ["docker", "network", "inspect", NETWORK_NAME],
print(result.stdout.decode()) stdout=subprocess.DEVNULL,
return True stderr=subprocess.DEVNULL,
except subprocess.CalledProcessError as e: check=True
print(f"Command failed with error: {e.stderr.decode()}") )
return result.returncode == 0
except subprocess.CalledProcessError:
logger.error("Docker网络 %s 不存在", NETWORK_NAME)
return False
except Exception as e:
logger.error("网络检查异常: %s", str(e), exc_info=True)
return False return False
def docker_compose_action(config_path, output_path): def parse_compose_config(file_path: Path) -> list[NginxConfig]:
"读取并渲染配置文件" """解析Docker Compose文件"""
(auto_config, other) = docker_config(config_path) try:
render_dataclass(auto_config, Path('./src/docker-compose.j2'), output_path, other=other) with open(file_path, 'r', encoding='utf-8') as f:
config = yaml.safe_load(f)
def inner_nginx_action(config_path, output_path): services:dict = config['services']
"读取并渲染配置文件"
(auto_config, other) = docker2nginx(config_path)
render_dataclass(auto_config, Path('./src/nginx-conf.j2'), output_path, other=other)
def outer_nginx_action(config_path, output_path): # networks = config['networks'][NETWORK_NAME]
"读取并渲染配置文件" # if not networks['external']:
(auto_config, other) = nginx_config(config_path) # raise ValueError("network 必须为external")
render_dataclass(auto_config, Path('./src/nginx-conf.j2'), output_path, other=other)
def nginx_reload(): nginx_configs:list[NginxConfig] = []
"""reload nginx config"""
res = True
if res:
res = run_command("docker exec -it nginx nginx -t")
if res:
res = run_command("docker exec -it nginx nginx -s reload")
def docker_compose_restart(): for name, service in services.items():
"docker compose restart" try:
run_command("docker compose restart") labels = service.get('labels')
if labels is not None:
n = labels.get('nginx_prefix_name')
if n is not None:
name = n
conf = NginxConfig(
name = f"{name}.zzyxyz.com",
host = service['container_name'],
port = service['expose'][0]
)
if len(service['expose']) != 1:
raise ValueError("expose 必须为一项")
networks = service.get('networks')
# TODO
if networks is None or networks[0] != NETWORK_NAME:
raise ValueError(f"networks 需要设置为{NETWORK_NAME}")
# required_fields = ['com.lingma.nginx.domain', 'com.lingma.nginx.port']
# for field in required_fields:
# if field not in labels:
# raise ValueError(f"服务 {name} 缺少必要标签: {field}")
def docker_ps(): nginx_configs.append(conf)
"list running docker containers by my format" except Exception as e:
cmd = "docker ps --format "\ logger.error("解析服务 %s 配置失败: %s{str(e)}", name, str(e))
"\"table {{.Names}}\t{{.Image}}\t{{.Command}}\t{{.CreatedAt}}\t{{.Status}}\"" continue
run_command(cmd)
return nginx_configs
except FileNotFoundError:
logger.error("文件不存在: %s", file_path)
return False
except PermissionError:
logger.error("无权限访问文件: %s", file_path)
return False
except yaml.YAMLError as e:
logger.error("YAML解析错误: %s", str(e))
return False
except Exception as e:
logger.error("未知错误: %s", str(e), exc_info=True)
return False
def main(): def main():
"main function" """main"""
parser = argparse.ArgumentParser(description="Automate Docker and Nginx configuration.") parser = argparse.ArgumentParser(description="Nginx自动化配置工具")
parser.add_argument('-a', '--all', action='store_true', default=True, parser.add_argument('--compose-file', type=str, default=DOCKER_COMPOSE_FILE,
help='Perform all actions') help="Docker Compose文件路径")
parser.add_argument('-c', '--docker-compose', action='store_true', parser.add_argument('--debug', action='store_true',
help='Generate Docker Compose') help="启用调试模式")
parser.add_argument('-i', '--inner-nginx', action='store_true',
help='Generate Inner Nginx (docker network)')
parser.add_argument('-o', '--outer-nginx', action='store_true',
help='Generate Outer Nginx (host machine)')
parser.add_argument('-r', '--nginx-reload', action='store_true',
help='Reload Nginx configuration')
parser.add_argument('-s', '--docker-compose-restart', action='store_true',
help='Restart Docker Compose services')
parser.add_argument('-p', '--docker-ps', action='store_true',
help='List running Docker containers')
parser.add_argument('--docker-config-path', type=Path,
default=Path(os.getenv('DOCKER_INIPATH', 'docker.ini')),
help='Path to the Docker INI configuration file')
parser.add_argument('--docker-output-path', type=Path,
default=Path(os.getenv('DOCKER_COMPOSE_OUTPATH', 'docker-compose.conf')),
help='Path to the output Docker Compose configuration file')
parser.add_argument('--inner-config-path', type=Path,
default=Path(os.getenv('DOCKER_INIPATH', 'docker.ini')),
help='Path to the Docker INI configuration file for inner Nginx')
parser.add_argument('--inner-output-path', type=Path,
default=Path(os.getenv('INNER_NGINX_OUTPATH', 'inner-nginx.conf')),
help='Path to the output Inner Nginx configuration file')
parser.add_argument('--outer-config-path', type=Path,
default=Path(os.getenv('NGINX_INIPATH', 'nginx.ini')),
help='Path to the Nginx INI configuration file for outer Nginx')
parser.add_argument('--outer-output-path', type=Path,
default=Path(os.getenv('OUTER_NGINX_OUTPATH', 'outer-nginx.conf')),
help='Path to the output Outer Nginx configuration file')
args = parser.parse_args() args = parser.parse_args()
if args.all: if args.debug:
docker_compose_action(args.docker_config_path, args.docker_output_path) logger.setLevel(logging.DEBUG)
inner_nginx_action(args.inner_config_path, args.inner_output_path) logger.debug("调试模式已启用")
outer_nginx_action(args.outer_config_path, args.outer_output_path)
nginx_reload() # 前置检查
else: if not validate_docker_network():
if args.docker_compose: logger.error("请先创建Docker网络: docker network create %s", NETWORK_NAME)
docker_compose_action(args.docker_config_path, args.docker_output_path) return
if args.inner_nginx:
inner_nginx_action(args.inner_config_path, args.inner_output_path) # 配置生成
if args.outer_nginx: try:
outer_nginx_action(args.outer_config_path, args.outer_output_path) logger.info("开始解析Docker Compose配置...")
if args.nginx_reload: compose_files:str = args.compose_file
nginx_reload() configs = []
if args.docker_compose_restart: for file in compose_files.split(','):
docker_compose_restart() logger.info("parse %s", file)
if args.docker_ps: config = parse_compose_config(file)
docker_ps() configs.extend(config)
logger.info("发现 %d 个需要代理的服务", len(configs))
conf = NginxConfigurator()
conf.backup_config()
ret = conf.gen_and_save_config(configs)
if not ret:
return
ret = conf.safe_reload()
if ret:
logger.info("全流程完成")
else:
logger.error("流程未完成,请检查错误日志")
except Exception as e:
logger.critical("主流程异常终止: %s", str(e), exc_info=args.debug)
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View File

@ -1,2 +1,2 @@
python-dotenv python-dotenv
jinja2 pyyaml

View File

@ -1,201 +0,0 @@
"""
config_reader.py
"""
import configparser
from dataclasses import dataclass
from typing import Optional, List, Tuple
from pathlib import Path
def read_config(file_path: Path) -> configparser.ConfigParser:
"""
读取并解析指定路径的 INI 配置文件
参数:
file_path (Path): 配置文件的路径
返回:
configparser.ConfigParser: 解析后的配置对象
"""
config = configparser.ConfigParser()
config.read(file_path, encoding='utf-8')
return config
def config_get(config: configparser.ConfigParser, section: str, key: str,
default: str = None) -> str:
"""
从配置文件中获取指定键的值支持默认值
如果在指定的 section 中找不到键则尝试从默认 section 中获取
如果最终值为None则会抛出异常
参数:
config (configparser.ConfigParser): 配置对象
section (str): 要查询的 section 名称
key (str): 要查询的键名称
default (str): 最终配置文件没有时硬编码的默认值
返回:
str: 查询到的值或默认值
"""
res = config.get(section, key, fallback=
config.get(config.default_section, key, fallback=default))
# if is_raise and res is None:
# raise ValueError(f"Key '{key}' not found in section '{section}' or "\
# "default section or default value.")
return res
@dataclass
class NginxConfig:
"""
表示一个 Nginx 配置项的数据类
属性:
name (str): 配置项名称默认为 "unknown"
host (str): 主机地址默认为 "localhost"
port (int): 端口号默认为 80
output_path (Optional[str]): 输出路径默认为 None
tcp_nopush (str): TCP 推送选项默认为 "off"
sendfile (str): Sendfile 选项默认为 "off"
client_max_body_size (str): 客户端最大请求体大小默认为 "100m"
proxy_connect_timeout (str): 代理连接超时时间默认为 "60s"
proxy_send_timeout (str): 代理发送超时时间默认为 "60s"
proxy_read_timeout (str): 代理读取超时时间默认为 "60s"
web_socket_proxy (bool): 是否启用 WebSocket 代理默认为 False
"""
name: str = "unknown"
host: str = "localhost"
port: int = 80
output_path: Optional[str] = None
tcp_nopush: str = "off"
sendfile: str = "off"
client_max_body_size: str = "100m"
proxy_connect_timeout: str = "60s"
proxy_send_timeout: str = "60s"
proxy_read_timeout: str = "60s"
web_socket_proxy: bool = False
def nginx_config(file_path: Path) -> Tuple[List[NginxConfig], dict]:
"""
读取 INI 配置文件并解析为 NginxConfig 对象列表
参数:
file_path (Path): 配置文件的路径
返回:
Tuple[List[NginxConfig], dict]: 包含 NginxConfig 对象的列表和其他相关信息的字典
"""
config = read_config(file_path)
configs = []
g_section = config.default_section
def _config_get(name, default = None):
return config_get(config, g_section, name, default)
for section in config.sections():
g_section = section
configs.append(NginxConfig(
name=section,
host=_config_get('host'),
port=int(_config_get('port')), # 将端口转换为整数类型
web_socket_proxy=_config_get('web_socket_proxy') == 'True',
client_max_body_size=_config_get('client_max_body_size',
NginxConfig.client_max_body_size)
))
return (configs, {})
@dataclass
class DockerComposeConfig:
"""
表示一个 Docker Compose 配置项的数据类
属性:
name (str): 服务名称
container_name (str): 容器名称
image (str): 使用的镜像
ports (Optional[list[str]]): 映射的端口列表默认为 None
volumes (Optional[list[str]]): 挂载的卷列表默认为 None
environment (Optional[list[str]]): 环境变量列表默认为 None
extra_hosts (Optional[list[str]]): 额外的主机映射列表默认为 None
export (Optional[str]): 导出的端口默认为 None
network (Optional[str]): 所属网络默认为 None
"""
name: str
container_name: str
image: str
ports: Optional[List[str]] = None
volumes: Optional[List[str]] = None
environment: Optional[List[str]] = None
extra_hosts: Optional[List[str]] = None
export: Optional[str] = None
network: Optional[str] = None
def docker_config(file_path: Path) -> Tuple[List[DockerComposeConfig], dict]:
"""
读取 INI 配置文件并解析为 DockerComposeConfig 对象列表
参数:
file_path (Path): 配置文件的路径
返回:
Tuple[List[DockerComposeConfig], dict]: 包含 DockerComposeConfig 对象的列表和其他相关信息的字典
"""
config = read_config(file_path)
configs = []
g_section = ''
def _config_get(name, default = None):
return config_get(config, g_section, name, default)
def _get_item(name) -> List[str]:
"""将逗号分隔的字符串转换为列表,并去除空格和空字符串。"""
return [i.strip() for i in _config_get(name, '').split(',') if i.strip()]
network = set()
for section in config.sections():
g_section = section
configs.append(DockerComposeConfig(
name=section,
container_name=_config_get('container_name'),
image=_config_get('image'),
volumes=_get_item('volumes'),
ports=_get_item('ports'),
environment=_get_item('environment'),
extra_hosts=_get_item('extra_hosts'),
export=_config_get('export'),
network=_config_get('network'),
))
network.add(_config_get('network'))
return (configs, {'networks': list(network)})
def docker2nginx(file_path: Path) -> Tuple[List[NginxConfig], dict]:
"""
Docker Compose 服务配置转换为 Nginx 代理配置
参数:
file_path (Path): 配置文件的路径
返回:
Tuple[List[NginxConfig], dict]: 包含 NginxConfig 对象的列表和其他相关信息的字典
"""
config = read_config(file_path)
configs = []
g_section = ''
def _config_get(name):
return config_get(config, g_section, name)
def _get_item(name) -> List[str]:
"""将逗号分隔的字符串转换为列表,并去除空格和空字符串。"""
return [i.strip() for i in _config_get(name).split(',') if i.strip()]
for section in config.sections():
if section == 'nginx':
continue
g_section = section
configs.append(NginxConfig(
name=config.get(section, 'nginx_name', fallback=section),
host=_config_get('container_name'),
port=_config_get('export'),
))
return (configs, {})

View File

@ -1,55 +0,0 @@
# docker-compose.j2
version: '3'
services:
{%- for config in autoconfigs %}
{{ config.name }}:
container_name: {{ config.container_name }}
image: {{ config.image }}
deploy:
update_config:
parallelism: 1
delay: 10s
restart_policy:
condition: on-failure
max_attempts: 3
{%- if config.ports %}
ports:
{%- for port in config.ports %}
- {{ port }}
{%- endfor %}
{%- endif %}
{%- if config.volumes %}
volumes:
{%- for volume in config.volumes %}
- {{ volume }}
{%- endfor %}
{%- endif %}
{%- if config.environment %}
environment:
{%- for env in config.environment %}
- {{ env }}
{%- endfor %}
{%- endif %}
{%- if config.export %}
expose:
- {{ config.export }}
{%- endif %}
{%- if config.extra_hosts %}
extra_hosts:
{%- for host in config.extra_hosts %}
- {{ host }}
{%- endfor %}
{%- endif %}
{%- if config.network %}
networks:
- {{ config.network }}
{%- endif %}
{% endfor %}
{%- if networks %}
networks:
{%- for network in networks %}
{{ network }}:
{%- endfor %}
{%- endif %}

View File

@ -1,19 +0,0 @@
"""
jinja2_render.py
"""
from pathlib import Path
from dataclasses import asdict, dataclass
from jinja2 import Environment, FileSystemLoader, Template
def render_dataclass(configs: list[dataclass], tmp_path: Path, out_path: Path,
verbose :bool = True, other = None) -> str:
"from dataclasses to jinja template"
env = Environment(loader=FileSystemLoader(str(tmp_path.parent),
encoding='utf-8'))
template: Template = env.get_template(str(tmp_path.name))
data = [asdict(config) for config in configs]
res = template.render({'autoconfigs': data}, **other)
out_path.write_text(res, encoding='utf-8')
if verbose:
print(f"{out_path.absolute()} is rendered")
return res

34
src/logger.py Normal file
View File

@ -0,0 +1,34 @@
"""
统一日志工具
"""
import logging
class ColorFormatter(logging.Formatter):
"""
using terminal color to print colorful log
"""
COLORS = {
'WARNING': '\033[93m',
'ERROR': '\033[91m',
'CRITICAL': '\033[91m',
'INFO': '\033[94m',
'DEBUG': '\033[92m',
'ENDC': '\033[0m'
}
def format(self, record):
color = self.COLORS.get(record.levelname, '')
message = super().format(record)
return f"{color}{message}{self.COLORS['ENDC']}" if color else message
def get_logger(name: str | None = None):
"""
using ColorFormatter to print colorful log
"""
logger = logging.getLogger(name)
logger.setLevel(logging.INFO)
handler = logging.StreamHandler()
handler.setFormatter(ColorFormatter('[%(asctime)s] %(levelname)s: %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'))
logger.addHandler(handler)
return logger

View File

@ -1,28 +0,0 @@
{%- for config in autoconfigs %}
server {
listen 80;
listen 443 ssl;
server_name {{ config.name }};
if ($scheme = http) {
return 301 https://$server_name$request_uri;
}
tcp_nopush {{ config.tcp_nopush }};
sendfile {{ config.sendfile }};
client_max_body_size {{ config.client_max_body_size }};
location / {
proxy_pass http://{{ config.host }}:{{ config.port }};
proxy_connect_timeout {{ config.proxy_connect_timeout}};
proxy_send_timeout {{ config.proxy_send_timeout }};
proxy_read_timeout {{ config.proxy_read_timeout }};
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
{%- if config.web_socket_proxy %}
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
{%- endif %}
}
}
{% endfor %}

202
src/nginx.py Normal file
View File

@ -0,0 +1,202 @@
"""
nginx 配置生成工具
"""
from dataclasses import asdict, dataclass
from datetime import datetime
import logging
from pathlib import Path
import shutil
import subprocess
from logger import get_logger
logger = get_logger(__name__)
NGINX_CONF_DIR = Path("nginx/conf/conf.d")
BACKUP_DIR = Path("nginx/conf/backups")
CONTAINER_NAME = "nginx"
@dataclass
class NginxConfig:
"""
表示一个 Nginx 配置项的数据类
属性:
name (str): 配置项名称默认为 "unknown"
host (str): 主机地址默认为 "localhost"
port (int): 端口号默认为 80
output_path (Optional[str]): 输出路径默认为 None
tcp_nopush (str): TCP 推送选项默认为 "off"
sendfile (str): Sendfile 选项默认为 "off"
client_max_body_size (str): 客户端最大请求体大小默认为 "100m"
proxy_connect_timeout (str): 代理连接超时时间默认为 "60s"
proxy_send_timeout (str): 代理发送超时时间默认为 "60s"
proxy_read_timeout (str): 代理读取超时时间默认为 "60s"
web_socket_proxy (bool): 是否启用 WebSocket 代理默认为 False
"""
name: str = "unknown"
host: str = "localhost"
port: int = 80
output_path: str | None = None
tcp_nopush: str = "off"
sendfile: str = "off"
client_max_body_size: str = "100m"
proxy_connect_timeout: str = "60s"
proxy_send_timeout: str = "60s"
proxy_read_timeout: str = "60s"
web_socket_proxy: bool = False
@dataclass
class NginxConfigurator:
"""
Nginx配置管理类封装配置生成备份重载等操作
属性:
compose_file (Path): Docker Compose文件路径
output_dir (Path): 配置文件输出目录
backup_dir (Path): 配置备份目录
logger (logging.Logger): 日志记录器
"""
output_dir: Path = NGINX_CONF_DIR
backup_dir: Path = BACKUP_DIR
logger: logging.Logger = logger
def gen_all_config(self, configs: list[NginxConfig]) -> str:
"""生成Nginx配置内容"""
return "\n".join([self.gen_once_config(conf) for conf in configs])
def gen_once_config(self, conf: NginxConfig) -> str:
"""构建单个server配置块"""
template = \
"""
server {{
# # 必须添加DNS解析器配置
# resolver 127.0.0.11 valid=10s; # Docker内置DNS服务
# upstream {name}_upstream {{
# zone upstream_{name} 64k; # 共享内存区
# server {host}:{port} resolve max_fails=3 fail_timeout=30s;
# server 0.0.0.0:65535 backup; # 占位无效地址
# }}
listen 80;
listen 443 ssl;
server_name {name};
if ($scheme = http) {{
return 301 https://$server_name$request_uri;
}}
tcp_nopush {tcp_nopush};
sendfile {sendfile};
client_max_body_size {client_max_body_size};
location / {{
proxy_pass http://{host}:{port};
proxy_connect_timeout {proxy_connect_timeout};
proxy_send_timeout {proxy_send_timeout};
proxy_read_timeout {proxy_read_timeout};
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
{_web_socket_proxy}
}}
}}
"""
conf_dict = {
"_web_socket_proxy":
"""
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
""".strip() if conf.web_socket_proxy else "",
}
return template.format_map(asdict(conf) | conf_dict)
def backup_config(self, filename: Path = Path("generated.conf")) -> Path | None:
"""执行配置文件备份"""
try:
self.backup_dir.mkdir(exist_ok=True, parents=True)
backup_file = self.backup_dir / f"{filename}.bak.{datetime.now():%Y%m%d-%H%M%S}"
if not (self.output_dir / filename).exists():
self.logger.warning("未找到源文件,无法备份")
return None
shutil.copy(self.output_dir / filename, backup_file)
self.logger.info("配置已备份至 %s", backup_file)
return backup_file
except Exception as e:
self.logger.error("备份失败: %s", str(e))
return None
def gen_and_save_config(self, configs: list[NginxConfig],
filename: Path = Path("generated.conf")) -> bool:
"""生成配置文件并保存到文件里"""
try:
output_file = self.output_dir / filename
output_file.parent.mkdir(exist_ok=True, parents=True)
with open(output_file, 'w', encoding='utf-8') as f:
res = self.gen_all_config(configs)
f.write(res)
logger.info("配置已写入 %s", output_file)
return True
except Exception as e:
self.logger.error(f"生成文件失败: {str(e)}")
return False
def safe_reload(self) -> bool:
"""安全重载Nginx配置"""
if self.test_config() and self.reload():
return True
return False
def test_config(self) -> bool:
"""测试Nginx配置"""
result = subprocess.run(
["docker", "exec", CONTAINER_NAME, "nginx", "-t"],
capture_output=True, stderr=None, stdout=None, check=False
)
if result.returncode != 0:
self.logger.error("配置测试失败: %s", result.stderr)
return False
else:
return True
def reload(self) -> bool:
"""执行配置重载"""
try:
result = subprocess.run(
["docker", "exec", CONTAINER_NAME, "nginx", "-s", "reload"],
capture_output=True, stderr=None, stdout=None, check=False
)
if result.returncode != 0:
self.logger.error("配置重载失败: %s", result.stderr)
return False
else:
self.logger.info("配置重载成功")
return True
except subprocess.CalledProcessError as e:
self.logger.error("重载失败: %s", str(e))
return False
# def rollback(self) -> bool:
# """回滚到最近的有效配置"""
# backups = sorted(self.backup_dir.glob("*.bak.*"),
# key=os.path.getmtime, reverse=True)
# if not backups:
# self.logger.error("无可用的备份配置")
# return False
# try:
# shutil.copy(backups[0], self.output_dir / "generated.conf")
# self.logger.warning(f"已回滚到备份 {backups[0].name}")
# return self.safe_reload()
# except Exception as e:
# self.logger.error(f"回滚失败: {str(e)}")
# return False
if __name__ == '__main__':
config = NginxConfigurator()
config.safe_reload()
# conf = NginxConfig()
# conf.web_socket_proxy = True
# ret = config.gen_config([conf, conf])
# config.backup_config()