From 7e5990aa16335516d8d9c55603626a90cd8878de Mon Sep 17 00:00:00 2001 From: Kevin Veen-Birkenbach Date: Fri, 12 Sep 2025 03:08:18 +0200 Subject: [PATCH] deploy(cli): auto-generate MODE_* flags from 01_modes.yml; remove legacy skip flags/params; drive cleanup via MODE_CLEANUP; validation via MODE_ASSERT; tests via MODE_TEST; drop MODE_BACKUP from 01_modes.yml. Ref: https://chatgpt.com/share/68c3725f-43a0-800f-9bb0-eb7cbf77ac24 --- cli/deploy.py | 254 ++++++++++++++++++++++++------------ group_vars/all/01_modes.yml | 1 - 2 files changed, 171 insertions(+), 84 deletions(-) diff --git a/cli/deploy.py b/cli/deploy.py index 459c262d..d7a26b40 100644 --- a/cli/deploy.py +++ b/cli/deploy.py @@ -5,6 +5,9 @@ import subprocess import os import datetime import sys +import re +from typing import Optional, Dict, Any, List + def run_ansible_playbook( inventory, @@ -13,21 +16,19 @@ def run_ansible_playbook( allowed_applications=None, password_file=None, verbose=0, - skip_tests=False, - skip_validation=False, skip_build=False, - cleanup=False, logs=False ): start_time = datetime.datetime.now() print(f"\n▶️ Script started at: {start_time.isoformat()}\n") - if cleanup: + # Cleanup is now handled via MODE_CLEANUP + if modes.get("MODE_CLEANUP", False): cleanup_command = ["make", "clean-keep-logs"] if logs else ["make", "clean"] - print("\n🧹 Cleaning up project (" + " ".join(cleanup_command) +")...\n") + print("\n🧹 Cleaning up project (" + " ".join(cleanup_command) + ")...\n") subprocess.run(cleanup_command, check=True) else: - print("\n⚠️ Skipping build as requested.\n") + print("\n⚠️ Skipping cleanup as requested.\n") if not skip_build: print("\n🛠️ Building project (make messy-build)...\n") @@ -38,27 +39,26 @@ def run_ansible_playbook( script_dir = os.path.dirname(os.path.realpath(__file__)) playbook = os.path.join(os.path.dirname(script_dir), "playbook.yml") - # Inventory validation step - if not skip_validation: + # Inventory validation is controlled via MODE_ASSERT + if modes.get("MODE_ASSERT", None) is False: + print("\n⚠️ Skipping inventory validation as requested.\n") + elif "MODE_ASSERT" not in modes or modes["MODE_ASSERT"] is True: print("\n🔍 Validating inventory before deployment...\n") try: subprocess.run( - [sys.executable, - os.path.join(script_dir, "validate/inventory.py"), - os.path.dirname(inventory) + [ + sys.executable, + os.path.join(script_dir, "validate", "inventory.py"), + os.path.dirname(inventory), ], - check=True + check=True, ) except subprocess.CalledProcessError: - print( - "\n❌ Inventory validation failed. Deployment aborted.\n", - file=sys.stderr - ) + print("\n❌ Inventory validation failed. Deployment aborted.\n", file=sys.stderr) sys.exit(1) - else: - print("\n⚠️ Skipping inventory validation as requested.\n") - if not skip_tests: + # Tests are controlled via MODE_TEST + if modes.get("MODE_TEST", False): print("\n🧪 Running tests (make messy-test)...\n") subprocess.run(["make", "messy-test"], check=True) @@ -93,25 +93,136 @@ def run_ansible_playbook( duration = end_time - start_time print(f"⏱️ Total execution time: {duration}\n") + def validate_application_ids(inventory, app_ids): """ Abort the script if any application IDs are invalid, with detailed reasons. """ from module_utils.valid_deploy_id import ValidDeployId + validator = ValidDeployId() invalid = validator.validate(inventory, app_ids) if invalid: print("\n❌ Detected invalid application_id(s):\n") for app_id, status in invalid.items(): reasons = [] - if not status['in_roles']: + if not status["in_roles"]: reasons.append("not defined in roles (infinito)") - if not status['in_inventory']: + if not status["in_inventory"]: reasons.append("not found in inventory file") print(f" - {app_id}: " + ", ".join(reasons)) sys.exit(1) +MODE_LINE_RE = re.compile( + r"""^\s*(?P[A-Z0-9_]+)\s*:\s*(?P.+?)\s*(?:#\s*(?P.*))?\s*$""" +) + + +def _parse_bool_literal(text: str) -> Optional[bool]: + t = text.strip().lower() + if t in ("true", "yes", "on"): + return True + if t in ("false", "no", "off"): + return False + return None + + +def load_modes_from_yaml(modes_yaml_path: str) -> List[Dict[str, Any]]: + """ + Parse group_vars/all/01_modes.yml line-by-line to recover: + - name (e.g., MODE_TEST) + - default (True/False/None if templated/unknown) + - help (from trailing # comment, if present) + """ + modes = [] + if not os.path.exists(modes_yaml_path): + raise FileNotFoundError(f"Modes file not found: {modes_yaml_path}") + + with open(modes_yaml_path, "r", encoding="utf-8") as fh: + for line in fh: + line = line.rstrip() + if not line or line.lstrip().startswith("#"): + continue + m = MODE_LINE_RE.match(line) + if not m: + continue + key = m.group("key") + val = m.group("value").strip() + cmt = (m.group("cmt") or "").strip() + + if not key.startswith("MODE_"): + continue + + default_bool = _parse_bool_literal(val) + modes.append( + { + "name": key, + "default": default_bool, + "help": cmt or f"Toggle {key}", + } + ) + return modes + + +def add_dynamic_mode_args( + parser: argparse.ArgumentParser, modes_meta: List[Dict[str, Any]] +) -> Dict[str, Dict[str, Any]]: + """ + Add argparse options based on modes metadata. + Returns a dict mapping mode name -> { 'dest': , 'default': , 'kind': 'bool_true'|'bool_false'|'explicit' }. + """ + spec: Dict[str, Dict[str, Any]] = {} + for m in modes_meta: + name = m["name"] + default = m["default"] + desc = m["help"] + short = name.replace("MODE_", "").lower() + + if default is True: + opt = f"--skip-{short}" + dest = f"skip_{short}" + help_txt = desc or f"Skip/disable {short} (default: enabled)" + parser.add_argument(opt, action="store_true", help=help_txt, dest=dest) + spec[name] = {"dest": dest, "default": True, "kind": "bool_true"} + elif default is False: + opt = f"--{short}" + dest = short + help_txt = desc or f"Enable {short} (default: disabled)" + parser.add_argument(opt, action="store_true", help=help_txt, dest=dest) + spec[name] = {"dest": dest, "default": False, "kind": "bool_false"} + else: + opt = f"--{short}" + dest = short + help_txt = desc or f"Set {short} explicitly (true/false). If omitted, keep inventory default." + parser.add_argument(opt, choices=["true", "false"], help=help_txt, dest=dest) + spec[name] = {"dest": dest, "default": None, "kind": "explicit"} + + return spec + + +def build_modes_from_args( + spec: Dict[str, Dict[str, Any]], args_namespace: argparse.Namespace +) -> Dict[str, Any]: + """ + Using the argparse results and the spec, compute the `modes` dict to pass to Ansible. + """ + modes: Dict[str, Any] = {} + for mode_name, info in spec.items(): + dest = info["dest"] + kind = info["kind"] + val = getattr(args_namespace, dest, None) + + if kind == "bool_true": + modes[mode_name] = False if val else True + elif kind == "bool_false": + modes[mode_name] = True if val else False + else: + if val is not None: + modes[mode_name] = True if val == "true" else False + return modes + + def main(): parser = argparse.ArgumentParser( description="Run the central Ansible deployment script to manage infrastructure, updates, and tests." @@ -119,88 +230,68 @@ def main(): parser.add_argument( "inventory", - help="Path to the inventory file (INI or YAML) containing hosts and variables." + help="Path to the inventory file (INI or YAML) containing hosts and variables.", ) parser.add_argument( - "-l", "--limit", - help="Restrict execution to a specific host or host group from the inventory." + "-l", + "--limit", + help="Restrict execution to a specific host or host group from the inventory.", ) parser.add_argument( - "-T", "--host-type", + "-T", + "--host-type", choices=["server", "desktop"], default="server", - help="Specify whether the target is a server or a personal computer. Affects role selection and variables." + help="Specify whether the target is a server or a personal computer. Affects role selection and variables.", ) parser.add_argument( - "-r", "--reset", action="store_true", - help="Reset all Infinito.Nexus files and configurations, and run the entire playbook (not just individual roles)." + "-p", + "--password-file", + help="Path to the file containing the Vault password. If not provided, prompts for the password interactively.", ) parser.add_argument( - "-t", "--test", action="store_true", - help="Run test routines instead of production tasks. Useful for local testing and CI pipelines." + "-B", + "--skip-build", + action="store_true", + help="Skip running 'make build' before deployment.", ) parser.add_argument( - "-u", "--update", action="store_true", - help="Enable the update procedure to bring software and roles up to date." - ) - parser.add_argument( - "-b", "--backup", action="store_true", - help="Perform a full backup of critical data and configurations before the update process." - ) - parser.add_argument( - "-c", "--cleanup", action="store_true", - help="Clean up unused files and outdated configurations after all tasks are complete. Also cleans up the repository before the deployment procedure." - ) - parser.add_argument( - "-d", "--debug", action="store_true", - help="Enable detailed debug output for Ansible and this script." - ) - parser.add_argument( - "-p", "--password-file", - help="Path to the file containing the Vault password. If not provided, prompts for the password interactively." - ) - parser.add_argument( - "-s", "--skip-tests", action="store_true", - help="Skip running 'make test' even if tests are normally enabled." - ) - parser.add_argument( - "-V", "--skip-validation", action="store_true", - help="Skip inventory validation before deployment." - ) - parser.add_argument( - "-B", "--skip-build", action="store_true", - help="Skip running 'make build' before deployment." - ) - parser.add_argument( - "-i", "--id", + "-i", + "--id", nargs="+", default=[], dest="id", - help="List of application_id's for partial deploy. If not set, all application IDs defined in the inventory will be executed." + help="List of application_id's for partial deploy. If not set, all application IDs defined in the inventory will be executed.", ) parser.add_argument( - "-v", "--verbose", action="count", default=0, - help="Increase verbosity level. Multiple -v flags increase detail (e.g., -vvv for maximum log output)." + "-v", + "--verbose", + action="count", + default=0, + help="Increase verbosity level. Multiple -v flags increase detail (e.g., -vvv for maximum log output).", ) parser.add_argument( - "--logs", action="store_true", - help="Keep the CLI logs during cleanup command" + "--logs", + action="store_true", + help="Keep the CLI logs during cleanup command", ) + # ---- Dynamically add mode flags from group_vars/all/01_modes.yml ---- + script_dir = os.path.dirname(os.path.realpath(__file__)) + repo_root = os.path.dirname(script_dir) + modes_yaml_path = os.path.join(repo_root, "group_vars", "all", "01_modes.yml") + modes_meta = load_modes_from_yaml(modes_yaml_path) + modes_spec = add_dynamic_mode_args(parser, modes_meta) + args = parser.parse_args() validate_application_ids(args.inventory, args.id) - modes = { - "MODE_RESET": args.reset, - "MODE_TEST": args.test, - "MODE_UPDATE": args.update, - "MODE_BACKUP": args.backup, - "MODE_CLEANUP": args.cleanup, - "MODE_LOGS": args.logs, - "MODE_DEBUG": args.debug, - "MODE_ASSERT": not args.skip_validation, - "host_type": args.host_type - } + # Build modes from dynamic args + modes = build_modes_from_args(modes_spec, args) + + # Additional non-dynamic flags + modes["MODE_LOGS"] = args.logs + modes["host_type"] = args.host_type run_ansible_playbook( inventory=args.inventory, @@ -209,11 +300,8 @@ def main(): allowed_applications=args.id, password_file=args.password_file, verbose=args.verbose, - skip_tests=args.skip_tests, - skip_validation=args.skip_validation, skip_build=args.skip_build, - cleanup=args.cleanup, - logs=args.logs + logs=args.logs, ) diff --git a/group_vars/all/01_modes.yml b/group_vars/all/01_modes.yml index 11b863fc..0a0714b3 100644 --- a/group_vars/all/01_modes.yml +++ b/group_vars/all/01_modes.yml @@ -5,6 +5,5 @@ MODE_TEST: false # Executes test routines instead of pr MODE_UPDATE: true # Executes updates MODE_DEBUG: false # This enables debugging in ansible and in the apps, You SHOULD NOT enable this on production servers MODE_RESET: false # Cleans up all Infinito.Nexus files. It's necessary to run to whole playbook and not particial roles when using this function. -MODE_BACKUP: "{{ MODE_UPDATE | bool }}" # Activates the backup before the update procedure MODE_CLEANUP: "{{ MODE_DEBUG | bool }}" # Cleanup unused files and configurations MODE_ASSERT: "{{ MODE_DEBUG | bool }}" # Executes validation tasks during the run.