Files
ssl-nginx/nginx/add_servers.py
Aleksei Sokol 3f56d6d16a Version 0.6.0 (2026-04-01)
Changes:
- add config validation option via Makefile
- add `default-http{,s}-port` commands to add_servers.py
- update add_servers.py to pass generic parameters to servers in templates
- add nginx_conf.d directory usage for more than one custom nginx configurations
- rename `port` and `ssl_port` to `http{,s}_port` for templates
- add `http{,s}_custom_params` to templates
2026-04-01 13:21:16 +03:00

187 lines
5.8 KiB
Python

"""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()