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 logging
import argparse
from pathlib import Path
from dotenv import load_dotenv
from src.jinja2_render import render_dataclass
from src.config_reader import nginx_config, docker_config, docker2nginx
import yaml
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:
result = subprocess.run(command, shell=True, check=True,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
print(result.stdout.decode())
return True
except subprocess.CalledProcessError as e:
print(f"Command failed with error: {e.stderr.decode()}")
result = subprocess.run(
["docker", "network", "inspect", NETWORK_NAME],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
check=True
)
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
def docker_compose_action(config_path, output_path):
"读取并渲染配置文件"
(auto_config, other) = docker_config(config_path)
render_dataclass(auto_config, Path('./src/docker-compose.j2'), output_path, other=other)
def parse_compose_config(file_path: Path) -> list[NginxConfig]:
"""解析Docker Compose文件"""
try:
with open(file_path, 'r', encoding='utf-8') as f:
config = yaml.safe_load(f)
def inner_nginx_action(config_path, output_path):
"读取并渲染配置文件"
(auto_config, other) = docker2nginx(config_path)
render_dataclass(auto_config, Path('./src/nginx-conf.j2'), output_path, other=other)
services:dict = config['services']
def outer_nginx_action(config_path, output_path):
"读取并渲染配置文件"
(auto_config, other) = nginx_config(config_path)
render_dataclass(auto_config, Path('./src/nginx-conf.j2'), output_path, other=other)
# networks = config['networks'][NETWORK_NAME]
# if not networks['external']:
# raise ValueError("network 必须为external")
def nginx_reload():
"""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")
nginx_configs:list[NginxConfig] = []
def docker_compose_restart():
"docker compose restart"
run_command("docker compose restart")
for name, service in services.items():
try:
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():
"list running docker containers by my format"
cmd = "docker ps --format "\
"\"table {{.Names}}\t{{.Image}}\t{{.Command}}\t{{.CreatedAt}}\t{{.Status}}\""
run_command(cmd)
nginx_configs.append(conf)
except Exception as e:
logger.error("解析服务 %s 配置失败: %s{str(e)}", name, str(e))
continue
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():
"main function"
parser = argparse.ArgumentParser(description="Automate Docker and Nginx configuration.")
parser.add_argument('-a', '--all', action='store_true', default=True,
help='Perform all actions')
parser.add_argument('-c', '--docker-compose', action='store_true',
help='Generate Docker Compose')
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')
"""main"""
parser = argparse.ArgumentParser(description="Nginx自动化配置工具")
parser.add_argument('--compose-file', type=str, default=DOCKER_COMPOSE_FILE,
help="Docker Compose文件路径")
parser.add_argument('--debug', action='store_true',
help="启用调试模式")
args = parser.parse_args()
if args.all:
docker_compose_action(args.docker_config_path, args.docker_output_path)
inner_nginx_action(args.inner_config_path, args.inner_output_path)
outer_nginx_action(args.outer_config_path, args.outer_output_path)
nginx_reload()
else:
if args.docker_compose:
docker_compose_action(args.docker_config_path, args.docker_output_path)
if args.inner_nginx:
inner_nginx_action(args.inner_config_path, args.inner_output_path)
if args.outer_nginx:
outer_nginx_action(args.outer_config_path, args.outer_output_path)
if args.nginx_reload:
nginx_reload()
if args.docker_compose_restart:
docker_compose_restart()
if args.docker_ps:
docker_ps()
if args.debug:
logger.setLevel(logging.DEBUG)
logger.debug("调试模式已启用")
# 前置检查
if not validate_docker_network():
logger.error("请先创建Docker网络: docker network create %s", NETWORK_NAME)
return
# 配置生成
try:
logger.info("开始解析Docker Compose配置...")
compose_files:str = args.compose_file
configs = []
for file in compose_files.split(','):
logger.info("parse %s", file)
config = parse_compose_config(file)
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__":
main()

View File

@ -1,2 +1,2 @@
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()