"""Add domain servers to nginx.conf executable script.""" from __future__ import annotations import argparse import os from dataclasses import dataclass from typing import Any import jinja2 import yaml class Server: """Server entry for nginx.conf file.""" DEFAULT_HTTP_PORT = 80 DEFAULT_HTTPS_PORT = 443 def __init__( self, http_port: int = ..., https_port: int = ..., **kwargs: dict[str, Any], ): if http_port is ...: http_port = Server.DEFAULT_HTTP_PORT if https_port is ...: https_port = Server.DEFAULT_HTTPS_PORT self._params = dict(kwargs) for additional_name, additional_value in zip( ["http_port", "https_port"], [http_port, https_port], ): self._params[additional_name] = additional_value def __getattr__(self, name) -> Any: return self._params.get(name) def to_dict(self) -> dict: """Convert server class to dict for nginx.conf.j2 jinja2-template""" return dict(self._params) @dataclass class CLIParams: """add_servers CLI parameters""" nginx_template: str domains_list_txt: str servers_config: str default_http_port: str default_https_port: str certificates_path: str http_only: bool output: str | None def main() -> None: """Parse arguments and add domains to nginx.conf""" parser = argparse.ArgumentParser("add-servers", description="Add domain servers to a given nginx.conf file") parser.add_argument("--nginx-template", "-f", required=True, help="Path to nginx.conf.j2 template file") parser.add_argument( "--domains-list-txt", "-d", required=True, help="Path to file with domains which have ssl certificates", ) parser.add_argument( "--servers-config", "-s", required=False, default=None, help="Path to servers configuration yaml file", ) parser.add_argument( "--default-http-port", required=False, default=80, help="Default port for https protocol", ) parser.add_argument( "--default-https-port", required=False, default=443, help="Default port for https protocol", ) parser.add_argument( "--certificates-path", "-c", required=False, default="/etc/letsencrypt/live", help="Path to a directory containing certificates", ) parser.add_argument( "--http-only", action="store_true", help="Remove certificates usage from servers section", ) parser.add_argument("--output", "-o", help="Path to nginx.conf output file") args: CLIParams = parser.parse_args() Server.DEFAULT_HTTP_PORT = args.default_http_port Server.DEFAULT_HTTPS_PORT = args.default_https_port if args.domains_list_txt is not None: with open(args.domains_list_txt, "r", encoding="utf-8") as file: domains_with_certs = [ domain.strip() for domain in file.readlines() if domain.strip() != "" and not domain.startswith("#") ] else: domains_with_certs = [] nginx_servers: list[Server] = [] resolver: str = "127.0.0.1" acme_challenge_location: str | None = None if args.servers_config is not None: with open(args.servers_config, "r", encoding="utf-8") as file: data: dict = yaml.safe_load(file) resolver: str = data.get("resolver", resolver) acme_challenge_location = data.get("acme_challenge_location") servers: dict[str, dict[str, Any]] = data["servers"] for server_name, params in servers.items(): params_part = dict(params) all_names = params.get( "all_names", None if "*" not in server_name else f"{server_name} {server_name.replace('*.', '', 1)}", ) certificate_dir = _get_certificate_path( http_only=args.http_only, domains_with_certs=domains_with_certs, base_certs_path=args.certificates_path, server_name=params.get("certificate_name") or server_name, ) params_part.pop("all_names", None) params_part.pop("certificate_name", None) nginx_servers.append( Server(name=server_name, all_names=all_names, certificate_dir=certificate_dir, **params_part) ) for domain in domains_with_certs: if not any( ( (server.all_names is None and domain == server.name) or ( server.all_names is not None and (f" {domain}" in server.all_names or server.all_names.startswith(domain)) ) ) for server in nginx_servers ): nginx_servers.append(Server(name=domain)) with open(args.nginx_template, "r", encoding="utf-8") as file: template = jinja2.Environment().from_string(file.read()) result = template.render( resolver=resolver, acme_challenge_location=acme_challenge_location, servers=[server.to_dict() for server in nginx_servers], ) print(result) if args.output is not None: with open(args.output, "w", encoding="utf-8") as file: file.write(result) def _get_certificate_path( http_only: bool, domains_with_certs: list[str], base_certs_path: str, server_name: str ) -> str | None: if http_only: return None if server_name in domains_with_certs: return os.path.join(base_certs_path, server_name.replace("*.", "")) if "." in server_name and f"*.{server_name[server_name.find('.') + 1:]}" in domains_with_certs: return os.path.join(base_certs_path, server_name[server_name.find(".") + 1 :]) return None if __name__ == "__main__": main()