feat: 重构项目并添加新功能
- 移除 defaultenv 文件和未使用的模板文件 - 重构 main.py,添加新的 Docker Compose 配置解析逻辑 - 新增 Nginx 配置生成和安全重启功能 - 更新 requirements.txt,替换 jinja2 为 pyyaml - 删除未使用的 src/config_reader.py 和 src/jinja2_render.py 文件
This commit is contained in:
parent
a4ebe33fca
commit
9297f9170a
@ -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
215
main.py
@ -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()
|
||||||
|
@ -1,2 +1,2 @@
|
|||||||
python-dotenv
|
python-dotenv
|
||||||
jinja2
|
pyyaml
|
||||||
|
@ -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, {})
|
|
@ -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 %}
|
|
@ -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
34
src/logger.py
Normal 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
|
@ -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
202
src/nginx.py
Normal 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()
|
Loading…
x
Reference in New Issue
Block a user