mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2025-09-14 06:17:14 +02:00
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
This commit is contained in:
254
cli/deploy.py
254
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<key>[A-Z0-9_]+)\s*:\s*(?P<value>.+?)\s*(?:#\s*(?P<cmt>.*))?\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': <argparse_dest>, 'default': <bool/None>, '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,
|
||||
)
|
||||
|
||||
|
||||
|
@@ -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.
|
||||
|
Reference in New Issue
Block a user