Source code for controller.templating

"""
Utilities to work with jinja2 templates
"""

import filecmp
import os
import random
import string
from pathlib import Path
from typing import Optional, TypedDict

from jinja2 import DebugUndefined, Environment, FileSystemLoader
from jinja2.exceptions import TemplateNotFound, UndefinedError
from zxcvbn import zxcvbn  # type: ignore

from controller import TEMPLATE_DIR, log, print_and_exit


[docs] class TemplateDataType(TypedDict, total=False): name: str version: str project: str extend: Optional[str] auth_service: str frontend: str hostname: str swarm: bool production: bool testing: bool services: list[str] env_variables: dict[str, str] enable_postgres: bool enable_neo4j: bool enable_rabbit: bool enable_redis: bool enable_celery: bool enable_flower: bool enable_fail2ban: bool enable_ftp: bool celery_broker: Optional[str] celery_backend: Optional[str]
[docs] def username(param_not_used: str, length: int = 8) -> str: rand = random.SystemRandom() charset = string.ascii_lowercase random_string = rand.choice(charset) charset += string.digits for _ in range(length - 1): random_string += rand.choice(charset) return random_string
[docs] def password(param_not_used: str, length: int = 12, symbols: str = "") -> str: rand = random.SystemRandom() charset = string.ascii_lowercase + string.ascii_uppercase + string.digits random_string = rand.choice(charset) charset += symbols for _ in range(length - 1): random_string += rand.choice(charset) return random_string
[docs] def get_strong_password() -> str: p = password(length=16, param_not_used="", symbols="%*,-.=^_~") result = zxcvbn(p) score = result["score"] # Should never happens since 16 characters with symbols is very unlikely to be weak if score < 4: # pragma: no cover log.warning("Generated password is not strong enough, sampling again") return get_strong_password() return p
[docs] class Templating:
[docs] def __init__(self) -> None: self.template_dir = Path(__file__).resolve().parent.joinpath(TEMPLATE_DIR) if not self.template_dir.is_dir(): print_and_exit("Template folder not found: {}", self.template_dir) log.debug("Template folder: {}", self.template_dir) loader = FileSystemLoader([TEMPLATE_DIR, self.template_dir]) self.env = Environment( loader=loader, undefined=DebugUndefined, autoescape=True, keep_trailing_newline=True, ) self.env.filters["password"] = password self.env.filters["username"] = username
[docs] @staticmethod def get_template_name(filename: str) -> str: if filename.startswith("."): filename = filename[1:] return f"{filename}.j2"
[docs] def get_template(self, filename: str, data: TemplateDataType) -> str: try: template_name = self.get_template_name(filename) template = self.env.get_template(template_name) content = str(template.render(**data)) return content except TemplateNotFound as e: print_and_exit("Template {} not found in: {}", str(e), self.template_dir) except UndefinedError as e: # pragma: no cover print_and_exit(str(e))
[docs] def save_template(self, filename: Path, content: str, force: bool = False) -> None: if filename.exists(): if force: self.make_backup(filename) # It is always verified before calling save_template from app, create & add else: # pragma: no cover print_and_exit("File {} already exists", filename) with open(filename, "w+") as fh: fh.write(content)
[docs] @staticmethod def make_backup(filename: Path) -> None: backup_filename = f"{filename}.bak" os.rename(filename, backup_filename) log.info("A backup of {} is saved as {}", filename, backup_filename)
[docs] def file_changed(self, filename: str) -> bool: template = self.get_template_name(filename) return not filecmp.cmp( filename, os.path.join(self.template_dir, template), shallow=True )