From 06849a5927a774915bfcc3fa017a0ef45772e952 Mon Sep 17 00:00:00 2001 From: Mike Kell Date: Mon, 21 Apr 2025 17:07:56 +0000 Subject: [PATCH] updated initialization steps - added ansible role audit --- ansible/roles/preflight/README.md | 39 ++++++++++ ansible/roles/preflight/defaults/main.yml | 2 + ansible/roles/preflight/meta/main.yml | 10 +++ ansible/roles/preflight/tasks/main.yml | 3 + .../roles/preflight/tasks/validate_vars.yml | 67 ++++++++++++++++ audit_roles.py | 76 +++++++++++++++++++ deployment/opencmmc_deploy.py | 59 ++++++++++++++ evidence/99_preflight/validation_checks.md | 21 +++++ 8 files changed, 277 insertions(+) create mode 100644 ansible/roles/preflight/README.md create mode 100644 ansible/roles/preflight/defaults/main.yml create mode 100644 ansible/roles/preflight/meta/main.yml create mode 100644 ansible/roles/preflight/tasks/main.yml create mode 100644 ansible/roles/preflight/tasks/validate_vars.yml create mode 100644 audit_roles.py create mode 100644 deployment/opencmmc_deploy.py create mode 100644 evidence/99_preflight/validation_checks.md diff --git a/ansible/roles/preflight/README.md b/ansible/roles/preflight/README.md new file mode 100644 index 0000000..57015ae --- /dev/null +++ b/ansible/roles/preflight/README.md @@ -0,0 +1,39 @@ +# šŸ›« Preflight Role + +This Ansible role performs pre-deployment checks to ensure that all required configuration values and infrastructure prerequisites are present and valid before continuing with the OpenCMMC Stack deployment. + +## āœ… Features + +- Validates required variables are defined +- Ensures SSH public key format is correct +- Checks email formatting using regex +- Verifies DNS resolution for target domain +- Logs all validation steps to the `evidence/99_preflight/` directory + +## šŸ“‚ Evidence Artifacts + +Validation checks are written to: + +``` +evidence/ +└── 99_preflight/ + └── validation_checks.md +``` + +## šŸ” Tags + +Use with: +```bash +ansible-playbook site.yml --tags preflight +``` + +## šŸ”§ Variables Checked + +- `default_user` +- `ssh_authorized_key` +- `domain_name` +- `hostname` +- `mailcow_admin_user` +- `mailcow_admin_password` +- `mailcow_fqdn` +- `mailcow_letsencrypt_email` diff --git a/ansible/roles/preflight/defaults/main.yml b/ansible/roles/preflight/defaults/main.yml new file mode 100644 index 0000000..88845ca --- /dev/null +++ b/ansible/roles/preflight/defaults/main.yml @@ -0,0 +1,2 @@ +--- +evidence_path: "../../evidence" diff --git a/ansible/roles/preflight/meta/main.yml b/ansible/roles/preflight/meta/main.yml new file mode 100644 index 0000000..dab057a --- /dev/null +++ b/ansible/roles/preflight/meta/main.yml @@ -0,0 +1,10 @@ +--- +galaxy_info: + author: OpenCMMC Team + description: Validates required deployment inputs before running any roles + license: MIT + min_ansible_version: "2.10" + platforms: + - name: Ubuntu + versions: + - 22.04 diff --git a/ansible/roles/preflight/tasks/main.yml b/ansible/roles/preflight/tasks/main.yml new file mode 100644 index 0000000..20e3486 --- /dev/null +++ b/ansible/roles/preflight/tasks/main.yml @@ -0,0 +1,3 @@ +--- +- name: Run preflight variable validation + include_tasks: validate_vars.yml diff --git a/ansible/roles/preflight/tasks/validate_vars.yml b/ansible/roles/preflight/tasks/validate_vars.yml new file mode 100644 index 0000000..2cfe760 --- /dev/null +++ b/ansible/roles/preflight/tasks/validate_vars.yml @@ -0,0 +1,67 @@ +--- +- name: Assert required variables are set + assert: + that: + - default_user is defined + - ssh_authorized_key is defined + - domain_name is defined + - hostname is defined + - mailcow_admin_user is defined + - mailcow_admin_password is defined + - mailcow_fqdn is defined + fail_msg: "One or more required variables are missing. Check deployment_config.yml or group_vars/all.yml" + success_msg: "All required variables are present" + +- name: Validate SSH public key format + assert: + that: + - ssh_authorized_key is match("^(ssh-(rsa|ed25519)|ecdsa-sha2-nistp[0-9]+) [A-Za-z0-9+/=]+( .*)?$") + fail_msg: "SSH public key format is invalid" + success_msg: "SSH public key format appears valid" + +- name: Validate email format + assert: + that: + - mailcow_letsencrypt_email is match("^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$") + fail_msg: "Mailcow Let's Encrypt email address is not valid" + success_msg: "Mailcow Let's Encrypt email format is valid" + +- name: Check if domain_name resolves + command: "getent hosts {{ domain_name }}" + register: domain_lookup + ignore_errors: yes + +- name: Assert domain_name resolves in DNS + assert: + that: + - domain_lookup.rc == 0 + fail_msg: "Domain name {{ domain_name }} does not resolve. Verify DNS or /etc/hosts." + success_msg: "Domain name {{ domain_name }} resolves successfully" + +- name: Log preflight validation results + copy: + dest: "{{ evidence_path }}/99_preflight/validation_checks.md" + content: | + # āœ… Preflight Validation Checks + + **Status:** PASSED + Timestamp: {{ ansible_date_time.iso8601 }} + + ## Variables + - default_user: `{{ default_user }}` + - domain_name: `{{ domain_name }}` + - hostname: `{{ hostname }}` + - mailcow_admin_user: `{{ mailcow_admin_user }}` + - mailcow_fqdn: `{{ mailcow_fqdn }}` + - mailcow_letsencrypt_email: `{{ mailcow_letsencrypt_email }}` + + ## DNS Resolution + - Domain `{{ domain_name }}` resolved to: `{{ domain_lookup.stdout | default('N/A') }}` + + ## SSH Key Format + - SSH key validated against standard format + + ## Email Format + - Email passed regex validation + + mode: '0644' diff --git a/audit_roles.py b/audit_roles.py new file mode 100644 index 0000000..47eff77 --- /dev/null +++ b/audit_roles.py @@ -0,0 +1,76 @@ +import os +from pathlib import Path + +EXPECTED_ROLE_FILES = [ + "tasks/main.yml", + "defaults/main.yml", + "meta/main.yml", +] + +OPTIONAL_FILES = [ + "handlers/main.yml", + "templates/", + "files/", + "vars/main.yml", + "molecule/default/converge.yml", + "molecule/default/verify.yml", +] + +PLACEHOLDER_TERMS = ["TODO", "FILL_ME_IN", "REPLACE_ME"] + +def is_placeholder(file_path): + try: + with open(file_path, "r", encoding="utf-8") as f: + content = f.read() + return any(term in content for term in PLACEHOLDER_TERMS) + except Exception: + return False + +def is_non_empty(file_path): + try: + return os.path.getsize(file_path) > 0 + except Exception: + return False + +def audit_role(role_path): + role_name = os.path.basename(role_path) + issues = [] + + for rel_path in EXPECTED_ROLE_FILES: + full_path = os.path.join(role_path, rel_path) + if not os.path.isfile(full_path): + issues.append(f"āŒ MISSING: {rel_path}") + elif not is_non_empty(full_path): + issues.append(f"āš ļø EMPTY: {rel_path}") + elif is_placeholder(full_path): + issues.append(f"āš ļø PLACEHOLDER: {rel_path}") + + for rel_path in OPTIONAL_FILES: + full_path = os.path.join(role_path, rel_path) + if os.path.exists(full_path) and os.path.isfile(full_path): + if not is_non_empty(full_path): + issues.append(f"āš ļø OPTIONAL EMPTY: {rel_path}") + elif is_placeholder(full_path): + issues.append(f"āš ļø OPTIONAL PLACEHOLDER: {rel_path}") + + return role_name, issues + +def main(): + base_path = Path("ansible/roles/") + if not base_path.exists(): + print("ā— 'ansible/roles/' directory not found.") + return + + print("šŸ” Auditing roles in:", base_path) + for role_dir in base_path.iterdir(): + if role_dir.is_dir(): + role_name, issues = audit_role(role_dir) + if issues: + print(f"\nšŸ”Ž Role: {role_name}") + for issue in issues: + print(" ", issue) + else: + print(f"āœ… Role: {role_name} — All checks passed.") + +if __name__ == "__main__": + main() diff --git a/deployment/opencmmc_deploy.py b/deployment/opencmmc_deploy.py new file mode 100644 index 0000000..78c2e9e --- /dev/null +++ b/deployment/opencmmc_deploy.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +import os +import yaml +import subprocess + +CONFIG_PATH = "deployment/deployment_config.yml" + +def prompt_input(prompt, default=None, required=True): + while True: + val = input(f"{prompt}{' [' + default + ']' if default else ''}: ").strip() + if val: + return val + elif default is not None: + return default + elif not required: + return '' + else: + print("This field is required.") + +def write_config(config, path=CONFIG_PATH): + os.makedirs(os.path.dirname(path), exist_ok=True) + with open(path, "w") as f: + yaml.dump(config, f) + print(f"\nāœ… Config written to {path}") + +def run_terraform(): + print("\nšŸš€ Running Terraform provisioning...\n") + subprocess.run(["terraform", "init"], cwd="terraform") + subprocess.run(["terraform", "apply", "-auto-approve"], cwd="terraform") + +def run_ansible(): + print("\nšŸ”§ Running Ansible deployment...\n") + subprocess.run([ + "ansible-playbook", "-i", "inventory/terraform_inventory.yml", "site.yml" + ]) + +def main(): + print("šŸ›”ļø OpenCMMC Stack: Guided Deployment") + config = { + "cloud_provider": prompt_input("Cloud Provider (aws, azure, gcp, do, proxmox, bare-metal)"), + "fqdn": prompt_input("Root Fully Qualified Domain Name (e.g., open-cmmc.example.com)"), + "domain_name": prompt_input("Internal Domain Name (e.g., example.cmmc.local)"), + "admin_email": prompt_input("Administrator email (for certs and contact)"), + "admin_user": prompt_input("Admin system username", default="cmmcadmin"), + "ssh_pubkey_path": prompt_input("Path to SSH public key", default="~/.ssh/id_rsa.pub"), + "keycloak_realm": prompt_input("Keycloak Realm", default="OpenCMMC"), + "tailscale_key": prompt_input("Tailscale Auth Key", required=False), + } + + write_config(config) + + if prompt_input("Proceed with Terraform provisioning?", default="y").lower() == "y": + run_terraform() + + if prompt_input("Proceed with Ansible deployment?", default="y").lower() == "y": + run_ansible() + +if __name__ == "__main__": + main() diff --git a/evidence/99_preflight/validation_checks.md b/evidence/99_preflight/validation_checks.md new file mode 100644 index 0000000..732cd61 --- /dev/null +++ b/evidence/99_preflight/validation_checks.md @@ -0,0 +1,21 @@ +# āœ… Preflight Validation Checks + +**Status:** PASSED +Timestamp: {{ ansible_date_time.iso8601 }} + +## Variables +- default_user: `{{ default_user }}` +- domain_name: `{{ domain_name }}` +- hostname: `{{ hostname }}` +- mailcow_admin_user: `{{ mailcow_admin_user }}` +- mailcow_fqdn: `{{ mailcow_fqdn }}` +- mailcow_letsencrypt_email: `{{ mailcow_letsencrypt_email }}` + +## DNS Resolution +- Domain `{{ domain_name }}` resolved to: `{{ domain_lookup.stdout | default('N/A') }}` + +## SSH Key Format +- SSH key validated against standard format + +## Email Format +- Email passed regex validation