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