diff --git a/.ansible-lint b/.ansible-lint new file mode 100644 index 00000000..2ff7ff37 --- /dev/null +++ b/.ansible-lint @@ -0,0 +1,47 @@ +profile: basic + +skip_list: + # SCREAMING_SNAKE is the documented convention for infrastructure-wide vars + - var-naming[pattern] + # noun.verb role naming is the documented convention + - role-name + # generated scenario playbooks do not require play-level names + - name[play] + # pre-existing task naming convention: lowercase names throughout bundles + - name[casing] + # generated bundles include tasks without explicit names + - name[missing] + # Jinja template syntax in task names is intentional (loop items, vars) + - name[template] + # pre-existing {{ item.ID}} spacing in generated bundles — cosmetic only + - jinja[spacing] + +warn_list: + - yaml[line-length] + - yaml[empty-lines] + - yaml[trailing-spaces] + - risky-shell-pipe + +# catalog and cross-repo roles not checked out in CI — stub so syntax-check passes +mock_roles: + - range42-ansible_roles-proxmox_controller + - ansible.utils + - indexer + - software.configure.docker-compose + - software.configure.firewalls + - software.configure.tailscale_disable_nftables + - software.install.nodejs_app_systemd + - software.install.symlinks_farm + - software.install.tailscale + - software.install.warmup.basic_packages + - software.install.warmup.dot_files + - software.install.wazuh-dashboard + - software.install.wazuh-filebeat-oss + - software.install.wazuh-indexer + - software.install.wazuh-manager + - systems.configure.add_user + - software.install.wazuh-agent + +# vars defined at runtime via inventory/group_vars — stub for CI syntax-check +extra_vars: + global_vm_ssh_name: ci_mock diff --git a/.ansible/roles/software.configure.firewalls/tasks/main.yml b/.ansible/roles/software.configure.firewalls/tasks/main.yml new file mode 100644 index 00000000..ed97d539 --- /dev/null +++ b/.ansible/roles/software.configure.firewalls/tasks/main.yml @@ -0,0 +1 @@ +--- diff --git a/.ansible/roles/software.configure.tailscale_disable_nftables/tasks/main.yml b/.ansible/roles/software.configure.tailscale_disable_nftables/tasks/main.yml new file mode 100644 index 00000000..ed97d539 --- /dev/null +++ b/.ansible/roles/software.configure.tailscale_disable_nftables/tasks/main.yml @@ -0,0 +1 @@ +--- diff --git a/.ansible/roles/software.install.nodejs_app_systemd/tasks/main.yml b/.ansible/roles/software.install.nodejs_app_systemd/tasks/main.yml new file mode 100644 index 00000000..ed97d539 --- /dev/null +++ b/.ansible/roles/software.install.nodejs_app_systemd/tasks/main.yml @@ -0,0 +1 @@ +--- diff --git a/.ansible/roles/software.install.symlinks_farm/tasks/main.yml b/.ansible/roles/software.install.symlinks_farm/tasks/main.yml new file mode 100644 index 00000000..ed97d539 --- /dev/null +++ b/.ansible/roles/software.install.symlinks_farm/tasks/main.yml @@ -0,0 +1 @@ +--- diff --git a/.ansible/roles/software.install.tailscale/tasks/main.yml b/.ansible/roles/software.install.tailscale/tasks/main.yml new file mode 100644 index 00000000..ed97d539 --- /dev/null +++ b/.ansible/roles/software.install.tailscale/tasks/main.yml @@ -0,0 +1 @@ +--- diff --git a/.ansible/roles/systems.configure.add_user/tasks/main.yml b/.ansible/roles/systems.configure.add_user/tasks/main.yml new file mode 100644 index 00000000..ed97d539 --- /dev/null +++ b/.ansible/roles/systems.configure.add_user/tasks/main.yml @@ -0,0 +1 @@ +--- diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..8d1b0a07 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +version: 2 +updates: + - package-ecosystem: pip + directory: / + schedule: + interval: weekly + labels: [dependencies] + - package-ecosystem: github-actions + directory: / + schedule: + interval: monthly + labels: [dependencies, ci] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..fa4a423e --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,34 @@ +name: CI +on: + push: + branches: [main, dev, 'feat/**', 'fix/**'] + pull_request: + branches: [main, dev] +jobs: + ansible-lint: + name: Ansible Lint + runs-on: ubuntu-latest + container: + image: python:3.13-slim + steps: + - run: apt-get update && apt-get install -y --no-install-recommends git + - uses: actions/checkout@v6 + - run: pip install --no-cache-dir ansible-lint + - run: ansible-lint bundles/ + + python: + name: Ruff + Pytest + runs-on: ubuntu-latest + container: + image: python:3.13-slim + steps: + - run: apt-get update && apt-get install -y --no-install-recommends git + - uses: actions/checkout@v6 + - id: pycheck + run: test -f pyproject.toml && echo "exists=true" >> "$GITHUB_OUTPUT" || echo "exists=false" >> "$GITHUB_OUTPUT" + - if: "steps.pycheck.outputs.exists == 'true'" + run: pip install --no-cache-dir -e ".[dev]" + - if: "steps.pycheck.outputs.exists == 'true'" + run: ruff check --select ALL r42playbooks/ + - if: "steps.pycheck.outputs.exists == 'true'" + run: pytest tests/ --tb=short -q diff --git a/.gitignore b/.gitignore index 9b718e0c..63b4ada7 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,23 @@ **/secret/ CLAUDE.md **/CLAUDE.md + +# Python (r42playbooks module) +.venv/ +__pycache__/ +*.py[cod] +*.egg-info/ +.pytest_cache/ +.ruff_cache/ +.coverage +htmlcov/ +dist/ +build/ + +# Claude session files (local only — handoffs, design notes) +.claude/ + +# Scratch compiler artifacts (flat-workspace outputs, never committed) +/topology.json +/hosts.yml +/expanded.json diff --git a/bundles/core/linux/debian/README.md b/bundles/core/linux/debian/README.md new file mode 100644 index 00000000..22d3eddc --- /dev/null +++ b/bundles/core/linux/debian/README.md @@ -0,0 +1,18 @@ +# bundles/core/linux/debian + +Debian-targeted **example bundles** for composing scenarios — the siblings of +`../ubuntu/`. + +The warmup/configure roles (`software.install.warmup.*`, `software.configure.*`, +`systems.configure.*`) dispatch per OS **at runtime** via +`ansible_facts.distribution` (ubuntu / debian / fedora — see the catalog's +`feat/local-apt-mirror`). Each bundle here therefore has the **same** `main.yml` +as `../ubuntu/` (it runs `hosts: all` + the OS-agnostic role); only the `test.sh` +target host differs (a Debian box, e.g. `r42.debian-jump-00`). + +Pair with the catalog `debian-jump` box template (`image: debian_trixie`, +Debian 13) to deploy a Debian target these bundles can run against. + +> If the roles stay fully OS-agnostic, a future cleanup may collapse +> `ubuntu/`+`debian/` into a single `linux/` set; kept split here to mirror the +> existing layout. diff --git a/bundles/core/linux/debian/configure/add-user/main.yml b/bundles/core/linux/debian/configure/add-user/main.yml new file mode 100644 index 00000000..4772898e --- /dev/null +++ b/bundles/core/linux/debian/configure/add-user/main.yml @@ -0,0 +1,19 @@ +# Debian example bundle. The role self-detects the OS at runtime via +# ansible_facts.distribution (ubuntu/debian/fedora), so this mirrors the +# ubuntu/ bundle — it is the Debian-targeted invocation + test host. + +- name: add user + hosts: all + become: true + roles: + - systems.configure.add_user + + vars: + # + # variables will be overwrited by values coming from API + # + TARGET_USER: student + TARGET_PASSWORD: "boj6q-uien3-ieda-h3Yiesi-li-uYi" # test password - will not be use in production. + CHANGE_PWD_AT_LOGON: false + TARGET_SHELL_PATH: "/bin/sh" + diff --git a/bundles/core/linux/debian/configure/add-user/test.sh b/bundles/core/linux/debian/configure/add-user/test.sh new file mode 100644 index 00000000..65901a52 --- /dev/null +++ b/bundles/core/linux/debian/configure/add-user/test.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +# echo $TMP_RANGE42_ANSIBLE_INVENTORY_DIR ; exit 1 + +ansible-playbook -i "$TMP_RANGE42_ANSIBLE_INVENTORY_DIR/inventory_default.yml" \ + -l "r42.debian-jump-00" \ + "./main.yml" \ + --vault-password-file /tmp/vault/vault_pass.txt diff --git a/bundles/core/linux/debian/install/basic-packages/main.yml b/bundles/core/linux/debian/install/basic-packages/main.yml new file mode 100644 index 00000000..d13753fb --- /dev/null +++ b/bundles/core/linux/debian/install/basic-packages/main.yml @@ -0,0 +1,35 @@ +# Debian example bundle. The role self-detects the OS at runtime via +# ansible_facts.distribution (ubuntu/debian/fedora), so this mirrors the +# ubuntu/ bundle — it is the Debian-targeted invocation + test host. + +# + +- name: install basics packages + hosts: all + become: true + roles: + # - software.install.warmup.dot_files + - software.install.warmup.basic_packages + + # + # variables will be overwrited by values coming from API + # + vars: + # FOR - software.install.warmup.dot_files + # OPERATOR_USER: jane + # INSTALL_VIM_DOTFILES: "NO" # accept also true false + # INSTALL_ZSH_DOTFILES: "NO" # accept also true false + + # FOR - software.install.warmup.basic_packages + INSTALL_PACKAGES_BASICS: "YES" + INSTALL_PACKAGES_FIREWALLS: "YES" + # + INSTALL_PACKAGES_DOCKER: "NO" + INSTALL_PACKAGES_DOCKER_COMPOSE: "NO" + # + INSTALL_PACKAGES_UTILS_JSON: "NO" + INSTALL_PACKAGES_UTILS_NETWORK: "NO" + + INSTALL_PACKAGES_NTP_AND_UPDATE_TIME: "YES" + + SPECIFIC_PACKAGES_CLEANING: "NO" diff --git a/bundles/core/linux/debian/install/basic-packages/test.sh b/bundles/core/linux/debian/install/basic-packages/test.sh new file mode 100644 index 00000000..65901a52 --- /dev/null +++ b/bundles/core/linux/debian/install/basic-packages/test.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +# echo $TMP_RANGE42_ANSIBLE_INVENTORY_DIR ; exit 1 + +ansible-playbook -i "$TMP_RANGE42_ANSIBLE_INVENTORY_DIR/inventory_default.yml" \ + -l "r42.debian-jump-00" \ + "./main.yml" \ + --vault-password-file /tmp/vault/vault_pass.txt diff --git a/bundles/core/linux/debian/install/docker-compose/main.yml b/bundles/core/linux/debian/install/docker-compose/main.yml new file mode 100644 index 00000000..122836eb --- /dev/null +++ b/bundles/core/linux/debian/install/docker-compose/main.yml @@ -0,0 +1,35 @@ +# Debian example bundle. The role self-detects the OS at runtime via +# ansible_facts.distribution (ubuntu/debian/fedora), so this mirrors the +# ubuntu/ bundle — it is the Debian-targeted invocation + test host. + +# + +- name: install basics packages + hosts: all + become: true + roles: + # - software.install.warmup.dot_files + - software.install.warmup.basic_packages + + # + # variables will be overwrited by values coming from API + # + vars: + # FOR - software.install.warmup.dot_files + # OPERATOR_USER: jane + # INSTALL_VIM_DOTFILES: "NO" # accept also true false + # INSTALL_ZSH_DOTFILES: "NO" # accept also true false + + # FOR - software.install.warmup.basic_packages + INSTALL_PACKAGES_BASICS: "YES" + INSTALL_PACKAGES_FIREWALLS: "NO" + # + INSTALL_PACKAGES_DOCKER: "YES" + INSTALL_PACKAGES_DOCKER_COMPOSE: "YES" + # + INSTALL_PACKAGES_UTILS_JSON: "NO" + INSTALL_PACKAGES_UTILS_NETWORK: "NO" + + INSTALL_PACKAGES_NTP_AND_UPDATE_TIME: "YES" + + SPECIFIC_PACKAGES_CLEANING: "NO" diff --git a/bundles/core/linux/debian/install/docker-compose/test.sh b/bundles/core/linux/debian/install/docker-compose/test.sh new file mode 100644 index 00000000..65901a52 --- /dev/null +++ b/bundles/core/linux/debian/install/docker-compose/test.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +# echo $TMP_RANGE42_ANSIBLE_INVENTORY_DIR ; exit 1 + +ansible-playbook -i "$TMP_RANGE42_ANSIBLE_INVENTORY_DIR/inventory_default.yml" \ + -l "r42.debian-jump-00" \ + "./main.yml" \ + --vault-password-file /tmp/vault/vault_pass.txt diff --git a/bundles/core/linux/debian/install/docker/main.yml b/bundles/core/linux/debian/install/docker/main.yml new file mode 100644 index 00000000..49a2245a --- /dev/null +++ b/bundles/core/linux/debian/install/docker/main.yml @@ -0,0 +1,35 @@ +# Debian example bundle. The role self-detects the OS at runtime via +# ansible_facts.distribution (ubuntu/debian/fedora), so this mirrors the +# ubuntu/ bundle — it is the Debian-targeted invocation + test host. + +# + +- name: install basics packages + hosts: all + become: true + roles: + # - software.install.warmup.dot_files + - software.install.warmup.basic_packages + + # + # variables will be overwrited by values coming from API + # + vars: + # FOR - software.install.warmup.dot_files + # OPERATOR_USER: jane + # INSTALL_VIM_DOTFILES: "NO" # accept also true false + # INSTALL_ZSH_DOTFILES: "NO" # accept also true false + + # FOR - software.install.warmup.basic_packages + INSTALL_PACKAGES_BASICS: "YES" + INSTALL_PACKAGES_FIREWALLS: "NO" + # + INSTALL_PACKAGES_DOCKER: "YES" + INSTALL_PACKAGES_DOCKER_COMPOSE: "NO" + # + INSTALL_PACKAGES_UTILS_JSON: "NO" + INSTALL_PACKAGES_UTILS_NETWORK: "NO" + + INSTALL_PACKAGES_NTP_AND_UPDATE_TIME: "YES" + + SPECIFIC_PACKAGES_CLEANING: "NO" diff --git a/bundles/core/linux/debian/install/docker/test.sh b/bundles/core/linux/debian/install/docker/test.sh new file mode 100644 index 00000000..65901a52 --- /dev/null +++ b/bundles/core/linux/debian/install/docker/test.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +# echo $TMP_RANGE42_ANSIBLE_INVENTORY_DIR ; exit 1 + +ansible-playbook -i "$TMP_RANGE42_ANSIBLE_INVENTORY_DIR/inventory_default.yml" \ + -l "r42.debian-jump-00" \ + "./main.yml" \ + --vault-password-file /tmp/vault/vault_pass.txt diff --git a/bundles/core/linux/debian/install/dot-files/main.yml b/bundles/core/linux/debian/install/dot-files/main.yml new file mode 100644 index 00000000..a626bece --- /dev/null +++ b/bundles/core/linux/debian/install/dot-files/main.yml @@ -0,0 +1,21 @@ +# Debian example bundle. The role self-detects the OS at runtime via +# ansible_facts.distribution (ubuntu/debian/fedora), so this mirrors the +# ubuntu/ bundle — it is the Debian-targeted invocation + test host. + +# + +- name: install dot files + hosts: all + become: true + roles: + - software.install.warmup.dot_files + + vars: + # + # variables will be overwrited by values coming from API + # + OPERATOR_USER: jane + INSTALL_VIM_DOTFILES: "NO" # accept also true false + INSTALL_ZSH_DOTFILES: "NO" # accept also true false + # + APPLY_FOR_ROOT: "NO" # accept also true false diff --git a/bundles/core/linux/debian/install/dot-files/test.sh b/bundles/core/linux/debian/install/dot-files/test.sh new file mode 100644 index 00000000..65901a52 --- /dev/null +++ b/bundles/core/linux/debian/install/dot-files/test.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +# echo $TMP_RANGE42_ANSIBLE_INVENTORY_DIR ; exit 1 + +ansible-playbook -i "$TMP_RANGE42_ANSIBLE_INVENTORY_DIR/inventory_default.yml" \ + -l "r42.debian-jump-00" \ + "./main.yml" \ + --vault-password-file /tmp/vault/vault_pass.txt diff --git a/bundles/core/proxmox/configure/default/vms/create-vms-admin/stage_00/builder_api_devkit.yml b/bundles/core/proxmox/configure/default/vms/create-vms-admin/stage_00/builder_api_devkit.yml index 6cc3d41e..59a43c0d 100644 --- a/bundles/core/proxmox/configure/default/vms/create-vms-admin/stage_00/builder_api_devkit.yml +++ b/bundles/core/proxmox/configure/default/vms/create-vms-admin/stage_00/builder_api_devkit.yml @@ -21,7 +21,7 @@ vars_files: - "../../secrets/default_vault.yml" - vars: + vars: {} # # variables will be overwrited by values in _main.yml # diff --git a/bundles/core/proxmox/configure/default/vms/create-vms-admin/stage_00/builder_docker_registry.yml b/bundles/core/proxmox/configure/default/vms/create-vms-admin/stage_00/builder_docker_registry.yml index 0b0e4a98..3e6505f6 100644 --- a/bundles/core/proxmox/configure/default/vms/create-vms-admin/stage_00/builder_docker_registry.yml +++ b/bundles/core/proxmox/configure/default/vms/create-vms-admin/stage_00/builder_docker_registry.yml @@ -21,7 +21,7 @@ vars_files: - "../../secrets/default_vault.yml" - vars: + vars: {} # # variables will be overwrited by values in _main.yml # diff --git a/bundles/core/proxmox/configure/default/vms/create-vms-admin/stage_00/deployer_api_backend.yml b/bundles/core/proxmox/configure/default/vms/create-vms-admin/stage_00/deployer_api_backend.yml index b4e7d901..a70e6f3d 100644 --- a/bundles/core/proxmox/configure/default/vms/create-vms-admin/stage_00/deployer_api_backend.yml +++ b/bundles/core/proxmox/configure/default/vms/create-vms-admin/stage_00/deployer_api_backend.yml @@ -21,7 +21,7 @@ vars_files: - "../../secrets/default_vault.yml" - vars: + vars: {} # # variables will be overwrited by values in _main.yml # diff --git a/bundles/core/proxmox/configure/default/vms/create-vms-admin/stage_00/deployer_api_gateway.yml b/bundles/core/proxmox/configure/default/vms/create-vms-admin/stage_00/deployer_api_gateway.yml index 61f0342c..342bbc92 100644 --- a/bundles/core/proxmox/configure/default/vms/create-vms-admin/stage_00/deployer_api_gateway.yml +++ b/bundles/core/proxmox/configure/default/vms/create-vms-admin/stage_00/deployer_api_gateway.yml @@ -21,7 +21,7 @@ vars_files: - "../../secrets/default_vault.yml" - vars: + vars: {} # # variables will be overwrited by values in _main.yml # diff --git a/bundles/core/proxmox/configure/default/vms/create-vms-admin/stage_00/deployer_ui.yml b/bundles/core/proxmox/configure/default/vms/create-vms-admin/stage_00/deployer_ui.yml index a0b92918..5d7562c1 100644 --- a/bundles/core/proxmox/configure/default/vms/create-vms-admin/stage_00/deployer_ui.yml +++ b/bundles/core/proxmox/configure/default/vms/create-vms-admin/stage_00/deployer_ui.yml @@ -21,7 +21,7 @@ vars_files: - "../../secrets/default_vault.yml" - vars: + vars: {} # # variables will be overwrited by values in _main.yml # diff --git a/bundles/core/proxmox/configure/default/vms/create-vms-admin/stage_00/mon_wazuh.yml b/bundles/core/proxmox/configure/default/vms/create-vms-admin/stage_00/mon_wazuh.yml index e4f71374..405dfd49 100644 --- a/bundles/core/proxmox/configure/default/vms/create-vms-admin/stage_00/mon_wazuh.yml +++ b/bundles/core/proxmox/configure/default/vms/create-vms-admin/stage_00/mon_wazuh.yml @@ -19,9 +19,9 @@ - hosts: proxmox gather_facts: false vars_files: - - "../../secrets/default_vault.yml" + - "../../secrets/default_vault.yml" - vars: + vars: {} # # variables will be overwrited by values in _main.yml # diff --git a/bundles/core/proxmox/configure/default/vms/create-vms-admin/stage_01/_r42_admin.yml b/bundles/core/proxmox/configure/default/vms/create-vms-admin/stage_01/_r42_admin.yml index 313de6fe..46f5c5a9 100644 --- a/bundles/core/proxmox/configure/default/vms/create-vms-admin/stage_01/_r42_admin.yml +++ b/bundles/core/proxmox/configure/default/vms/create-vms-admin/stage_01/_r42_admin.yml @@ -112,10 +112,9 @@ INSTALL_TAILSCALE: "YES" # variables defined in main tasks: - - block: + - when: INSTALL_TAILSCALE == "YES" + block: - include_role: name: software.install.tailscale - include_role: name: software.configure.tailscale_disable_nftables - - when: INSTALL_TAILSCALE == "YES" diff --git a/bundles/core/proxmox/configure/default/vms/create-vms-admin/stage_01/deployer_ui.yml b/bundles/core/proxmox/configure/default/vms/create-vms-admin/stage_01/deployer_ui.yml index 45fc3a10..3b9d30e7 100644 --- a/bundles/core/proxmox/configure/default/vms/create-vms-admin/stage_01/deployer_ui.yml +++ b/bundles/core/proxmox/configure/default/vms/create-vms-admin/stage_01/deployer_ui.yml @@ -2,8 +2,7 @@ # # -- name: - become: true +- become: true hosts: r42.admin-deployer-ui roles: - software.install.nodejs_app_systemd diff --git a/bundles/core/proxmox/configure/default/vms/create-vms-admin/stage_01/mon_wazuh.yml b/bundles/core/proxmox/configure/default/vms/create-vms-admin/stage_01/mon_wazuh.yml index 8da7f4da..f86bacb8 100644 --- a/bundles/core/proxmox/configure/default/vms/create-vms-admin/stage_01/mon_wazuh.yml +++ b/bundles/core/proxmox/configure/default/vms/create-vms-admin/stage_01/mon_wazuh.yml @@ -35,7 +35,7 @@ # #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - hosts: r42.admin-wazuh - become: yes + become: true become_user: root roles: - role: software.install.wazuh-indexer @@ -63,7 +63,7 @@ # # #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### - hosts: r42.admin-wazuh - become: yes + become: true vars_files: - "../../secrets/default_vault.yml" vars: @@ -81,7 +81,7 @@ - name: EXEC wazuh-passwords-tool.sh - if CHANGE_WAZUH_PASSWORD is true hosts: r42.admin-wazuh - become: yes + become: true become_user: root vars_files: - "../../secrets/default_vault.yml" @@ -110,7 +110,7 @@ # wazuh deploy agent - hosts: r42_admin_wazuh_clients - become: yes + become: true become_user: root roles: - software.install.wazuh-agent diff --git a/bundles/core/proxmox/configure/default/vms/create-vms-student/stage_00/student_box_01.yml b/bundles/core/proxmox/configure/default/vms/create-vms-student/stage_00/student_box_01.yml index 33be476f..ea1c0684 100644 --- a/bundles/core/proxmox/configure/default/vms/create-vms-student/stage_00/student_box_01.yml +++ b/bundles/core/proxmox/configure/default/vms/create-vms-student/stage_00/student_box_01.yml @@ -18,7 +18,7 @@ vars_files: - "../../secrets/default_vault.yml" - vars: + vars: {} # # variables will be overwrited by values in _main.yml # diff --git a/bundles/core/proxmox/configure/default/vms/create-vms-student/stage_01/_r42_student_box_group.yml b/bundles/core/proxmox/configure/default/vms/create-vms-student/stage_01/_r42_student_box_group.yml index 85f02823..9720484f 100644 --- a/bundles/core/proxmox/configure/default/vms/create-vms-student/stage_01/_r42_student_box_group.yml +++ b/bundles/core/proxmox/configure/default/vms/create-vms-student/stage_01/_r42_student_box_group.yml @@ -98,10 +98,9 @@ INSTALL_TAILSCALE: "YES" # variables defined in main tasks: - - block: + - when: INSTALL_TAILSCALE == "YES" + block: - include_role: name: software.install.tailscale - include_role: name: software.configure.tailscale_disable_nftables - - when: INSTALL_TAILSCALE == "YES" diff --git a/bundles/core/proxmox/configure/default/vms/create-vms-vuln/stage_00/vuln_box_00.yml b/bundles/core/proxmox/configure/default/vms/create-vms-vuln/stage_00/vuln_box_00.yml index 0b59797a..ee37f64c 100644 --- a/bundles/core/proxmox/configure/default/vms/create-vms-vuln/stage_00/vuln_box_00.yml +++ b/bundles/core/proxmox/configure/default/vms/create-vms-vuln/stage_00/vuln_box_00.yml @@ -19,7 +19,7 @@ vars_files: - "../../secrets/default_vault.yml" - vars: + vars: {} # # variables will be overwrited by values in _main.yml diff --git a/bundles/core/proxmox/configure/default/vms/create-vms-vuln/stage_00/vuln_box_01.yml b/bundles/core/proxmox/configure/default/vms/create-vms-vuln/stage_00/vuln_box_01.yml index aa71b41c..470cc2dd 100644 --- a/bundles/core/proxmox/configure/default/vms/create-vms-vuln/stage_00/vuln_box_01.yml +++ b/bundles/core/proxmox/configure/default/vms/create-vms-vuln/stage_00/vuln_box_01.yml @@ -20,7 +20,7 @@ vars_files: - "../../secrets/default_vault.yml" - vars: + vars: {} # # variables will be overwrited by values in _main.yml diff --git a/bundles/core/proxmox/configure/default/vms/create-vms-vuln/stage_00/vuln_box_02.yml b/bundles/core/proxmox/configure/default/vms/create-vms-vuln/stage_00/vuln_box_02.yml index aa71b41c..470cc2dd 100644 --- a/bundles/core/proxmox/configure/default/vms/create-vms-vuln/stage_00/vuln_box_02.yml +++ b/bundles/core/proxmox/configure/default/vms/create-vms-vuln/stage_00/vuln_box_02.yml @@ -20,7 +20,7 @@ vars_files: - "../../secrets/default_vault.yml" - vars: + vars: {} # # variables will be overwrited by values in _main.yml diff --git a/bundles/core/proxmox/configure/default/vms/create-vms-vuln/stage_00/vuln_box_03.yml b/bundles/core/proxmox/configure/default/vms/create-vms-vuln/stage_00/vuln_box_03.yml index aa71b41c..470cc2dd 100644 --- a/bundles/core/proxmox/configure/default/vms/create-vms-vuln/stage_00/vuln_box_03.yml +++ b/bundles/core/proxmox/configure/default/vms/create-vms-vuln/stage_00/vuln_box_03.yml @@ -20,7 +20,7 @@ vars_files: - "../../secrets/default_vault.yml" - vars: + vars: {} # # variables will be overwrited by values in _main.yml diff --git a/bundles/core/proxmox/configure/default/vms/create-vms-vuln/stage_00/vuln_box_04.yml b/bundles/core/proxmox/configure/default/vms/create-vms-vuln/stage_00/vuln_box_04.yml index aa71b41c..470cc2dd 100644 --- a/bundles/core/proxmox/configure/default/vms/create-vms-vuln/stage_00/vuln_box_04.yml +++ b/bundles/core/proxmox/configure/default/vms/create-vms-vuln/stage_00/vuln_box_04.yml @@ -20,7 +20,7 @@ vars_files: - "../../secrets/default_vault.yml" - vars: + vars: {} # # variables will be overwrited by values in _main.yml diff --git a/bundles/core/proxmox/configure/default/vms/create-vms-vuln/stage_01/_r42_vuln_box_group.yml b/bundles/core/proxmox/configure/default/vms/create-vms-vuln/stage_01/_r42_vuln_box_group.yml index a6edaab8..768cdc71 100644 --- a/bundles/core/proxmox/configure/default/vms/create-vms-vuln/stage_01/_r42_vuln_box_group.yml +++ b/bundles/core/proxmox/configure/default/vms/create-vms-vuln/stage_01/_r42_vuln_box_group.yml @@ -102,10 +102,9 @@ INSTALL_TAILSCALE: "NO" # variables defined in main tasks: - - block: + - when: INSTALL_TAILSCALE == "YES" + block: - include_role: name: software.install.tailscale - include_role: name: software.configure.tailscale_disable_nftables - - when: INSTALL_TAILSCALE == "YES" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..1dcf0307 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,36 @@ +[build-system] +requires = ["setuptools>=68"] +build-backend = "setuptools.build_meta" + +[project] +name = "r42playbooks" +version = "0.1.0" +description = "range42 scenario generator — compose labs from the catalog into deployable scenarios/" +readme = "r42playbooks/README.md" +requires-python = ">=3.11" +license = { text = "GPL-3.0-only" } +dependencies = [ + "pydantic>=2,<3", + "pyyaml>=6", +] + +[project.optional-dependencies] +cli = ["typer>=0.12"] +tui = ["textual>=0.60"] +dev = ["pytest>=8.3", "pytest-cov"] + +[project.scripts] +r42playbooks = "r42playbooks.cli:app" +r42playbooks-tui = "r42playbooks.tui.app:main" + +[tool.setuptools.packages.find] +include = ["r42playbooks*"] + +[tool.setuptools.package-data] +# Verbatim scenario boilerplate copied into every generated tree (e.g. the +# 01_init_proxmox/ template-creation subtree — see render.py / plan H3). +r42playbooks = ["assets/**/*"] + +[tool.pytest.ini_options] +testpaths = ["tests"] +addopts = "-q" diff --git a/r42playbooks/README.md b/r42playbooks/README.md new file mode 100644 index 00000000..baa16dae --- /dev/null +++ b/r42playbooks/README.md @@ -0,0 +1,104 @@ +# r42playbooks + +**Scenario generator for range42 — compose labs from the catalog.** + +`r42playbooks` lists the composable modules in `range42-catalog`, lets you compose a lab +(CLI flags, a `scenario.r42.yml` spec, or the TUI), and **generates a real +`scenarios//` directory** in the existing `demo_lab` format — deployable through the +normal range42 flow (`range42-context deploy`) with no changes elsewhere. + +It is **not** the canonical topology engine for the gamenet "universal" playbook (that +convergence is parked on a separate branch, issue #67). A generated scenario references +catalog content **by name only** — Ansible roles resolve via `ANSIBLE_ROLES_PATH`, container +stacks via `RANGE42_INVENTORY__DOCKER__*` — so the generated tree stays a thin recipe of names, +never a copy of role code. + +## Install + +```bash +pip install r42playbooks # pure core only (pydantic + pyyaml + stdlib) +pip install "r42playbooks[cli]" # + Typer CLI +pip install "r42playbooks[tui]" # + Textual TUI +``` + +## CLI (`list` / `show` / `new` / `validate`) + +```bash +# explore the catalog +r42playbooks list boxes --catalog ../range42-catalog +r42playbooks list subnets --catalog ../range42-catalog +r42playbooks list policies --catalog ../range42-catalog +r42playbooks list roles --catalog ../range42-catalog +r42playbooks list containers --catalog ../range42-catalog +r42playbooks show vuln-box --catalog ../range42-catalog + +# compose + generate a scenario from flags +r42playbooks new my_lab \ + --subnet default-3zone --policy air-gap-ctf \ + --box admin-wazuh --box vuln-box:count=5 \ + --catalog ../range42-catalog -o scenarios/ + +# or regenerate from a saved composition (written into every scenario) +r42playbooks new my_lab --spec scenarios/my_lab/scenario.r42.yml \ + --catalog ../range42-catalog -o scenarios/ +``` + +## TUI + +```bash +r42playbooks-tui ../range42-catalog +``` + +Pick a name, subnet layout, and network policy; add boxes (with a count); preview the +allocation; generate. + +## Library (`import r42playbooks`) + +The stable surface is re-exported at the package root so a downstream tool +(`r42deploy`/`r42runtime`, the range42 deployment CLI/TUI) can drive generation by import alone: + +```python +import r42playbooks as r + +catalog = r.load_catalog("/path/to/range42-catalog") +spec = r.load_spec("scenario.r42.yml") # or r.ScenarioSpec.model_validate({...}) + +problems = r.validate_refs(spec, catalog) # typo guard — [] means every ref resolves +if not problems: + root = r.render_scenario(spec, catalog=catalog, dest="scenarios/") + print(f"generated {root}") +``` + +Everything raises the `r42playbooks.core.errors` hierarchy (`TopologyError` and subclasses) — +never a framework type. Frozen entry points: `load_catalog`, `list_roles`, `list_containers`, +`validate_refs`, `load_spec`, `allocate`, `render_scenario(spec, *, catalog, dest, reserved=None)`. + +## What gets generated + +``` +scenarios// + scenario.r42.yml # the composition (re-runnable) + main.yml / main_vms_only.yml # import skeleton (only the emitted sections) + 01_init_proxmox/templates/ # 9xxx Proxmox template creation (vendored, verbatim) + NN_
/ # 02_admin / 03_student / 04_ctf, per composed box role + _main.yml # stage imports + per-VM global_* overrides + stage_00/.yml # clone Proxmox template -> create VM + stage_01/.yml # roles: [] + .devkit/ + manifest/scenario_vms.json # allocated vm_id / ip (octet rule, _reserved.json aware) + templates/ # ansible-inventory.j2, ssh-config.j2, ansible-vars.yml, vault-example.yml + .setup.sh / .delete_all.sh / reset scripts +``` + +`secrets/` is **not** generated — it is a deploy-time symlink created by `range42-context use`. + +## Develop + +```bash +python3 -m venv .venv && . .venv/bin/activate +pip install -e ".[dev]" +.venv/bin/python -m pytest -q +``` + +See [`docs/r42playbooks-plan.md`](../docs/r42playbooks-plan.md) for the full build plan and status. + +GPL-3.0. diff --git a/r42playbooks/__init__.py b/r42playbooks/__init__.py new file mode 100644 index 00000000..86911477 --- /dev/null +++ b/r42playbooks/__init__.py @@ -0,0 +1,61 @@ +"""r42playbooks — range42 scenario generator. + +Lists composable ``range42-catalog`` modules, lets a user compose a lab (flags, +a ``scenario.r42.yml`` spec, the CLI, or the TUI), and generates a real +``scenarios//`` directory in the existing demo_lab format — deployable +through the normal range42 flow with no changes elsewhere. + +The pure core (``r42playbooks.core``) is framework-agnostic; the stable surface +(``r42playbooks.api``) is re-exported here so a downstream tool can drive +generation by import alone:: + + import r42playbooks as r + + catalog = r.load_catalog("/path/to/range42-catalog") + spec = r.load_spec("scenario.r42.yml") # or r.ScenarioSpec.model_validate(...) + if not r.validate_refs(spec, catalog): + root = r.render_scenario(spec, catalog=catalog, dest="scenarios/") + +Everything raises the ``r42playbooks.core.errors`` hierarchy — never a framework +type. CLI/TUI deps are optional extras (``pip install r42playbooks[cli]``, ``[tui]``). +""" + +from r42playbooks.api import ( + Allocation, + Catalog, + ReservedIndex, + ScenarioExistsError, + ScenarioSpec, + allocate, + dump_spec_atomic, + list_containers, + list_images, + list_roles, + load_catalog, + load_spec, + render_scenario, + validate_refs, +) +from r42playbooks.core.models import Topology + +__all__ = [ + # generator surface (the stable import-only contract) + "load_catalog", + "list_images", + "list_roles", + "list_containers", + "validate_refs", + "load_spec", + "dump_spec_atomic", + "allocate", + "render_scenario", + "ScenarioSpec", + "Allocation", + "Catalog", + "ReservedIndex", + "ScenarioExistsError", + # legacy topology model (pre-pivot, kept for back-compat) + "Topology", + "__version__", +] +__version__ = "0.1.0" diff --git a/r42playbooks/api.py b/r42playbooks/api.py new file mode 100644 index 00000000..ffc5edb5 --- /dev/null +++ b/r42playbooks/api.py @@ -0,0 +1,109 @@ +"""Importable adapter — the surface every frontend calls. + +Consumed by the range42-backend-api (FastAPI), r42playbooks's own CLI/TUI, and the +range42 deployment CLI/TUI. Pure: returns plain models / dicts / lists and +raises the ``r42playbooks.core.errors`` hierarchy — never framework types. Each +consumer maps these into its own surface (HTTP envelopes, exit codes, dialogs). +""" + +from pathlib import Path + +from pydantic import ValidationError as _PydanticValidationError + +from r42playbooks.core.allocate import Allocation, allocate +from r42playbooks.core.compiler.services import resolve_services as _resolve_services +from r42playbooks.core.catalog import ( + Catalog, + list_containers, + list_images, + list_roles, + load_catalog, + validate_refs, +) +from r42playbooks.core.compiler import CompileResult, compile_topology +from r42playbooks.core.errors import ScenarioExistsError, ValidationError +from r42playbooks.core.extravars import resolve_universal_extravars +from r42playbooks.core.idalloc import ReservedIndex, validate_allocation +from r42playbooks.core.models import Topology +from r42playbooks.core.render import render_scenario as _render_scenario +from r42playbooks.core.spec import ScenarioSpec, dump_spec_atomic, load_spec +from r42playbooks.core.validate import semantic_problems + +__all__ = [ + # generator surface (FROZEN at S5a — consumed by the CLI/TUI/backend) + "load_catalog", + "list_images", + "list_roles", + "list_containers", + "validate_refs", + "load_spec", + "dump_spec_atomic", + "allocate", + "render_scenario", + "ScenarioSpec", + "Allocation", + "Catalog", + "ReservedIndex", + "ScenarioExistsError", + # legacy topology-compiler surface (pre-pivot; kept for compatibility) + "author_topology", + "validate_topology", + "compile_topology", + "resolve_universal_extravars", + "CompileResult", + "Topology", +] + + +def render_scenario( + spec: ScenarioSpec, + *, + catalog: Catalog, + dest: Path, + reserved: ReservedIndex | None = None, + overwrite: bool = False, +) -> Path: + """Allocate *spec* against *catalog* and render a ``scenarios//`` tree. + + The single entry point a frontend calls to go from a composition spec to a + deployable scenario directory. Returns the scenario root path. + + Note: this resolves box templates but does NOT pre-validate role/container + attachment names — call :func:`validate_refs` first (the CLI/TUI do) to + typo-guard ``attachments_add`` / ``default_attachments`` before generating. + Deny-list guards on every field still apply at schema-validation time. + + :param overwrite: if False (default) and the target dir exists, raise + :class:`~r42playbooks.core.errors.ScenarioExistsError` instead of clobbering. + :raises CatalogNotFoundError: a referenced subnet layout / box template is unknown. + :raises CompileError: a box cannot be placed (no subnet, exhausted ids/octets). + :raises ScenarioExistsError: target exists and ``overwrite`` is False. + """ + alloc = allocate(spec, catalog, reserved) + alloc = _resolve_services(alloc, spec, catalog) + return _render_scenario(alloc, spec, catalog=catalog, dest=Path(dest), overwrite=overwrite) + + +def author_topology(spec: dict, *, catalog: Catalog) -> Topology: + """Validate a raw author spec into a Topology, rejecting dangling catalog refs. + + :raises ValidationError: on schema failure or a reference the catalog can't resolve. + """ + try: + topology = Topology.model_validate(spec) + except _PydanticValidationError as exc: + raise ValidationError(f"invalid topology: {exc}") from exc + + problems = semantic_problems(topology, catalog) + if problems: + raise ValidationError("topology references unresolved items: " + "; ".join(problems)) + return topology + + +def validate_topology( + topology: Topology, *, catalog: Catalog, reserved: ReservedIndex +) -> list[str]: + """Return all problems (semantic + allocation) with a topology ([] == valid).""" + problems = list(semantic_problems(topology, catalog)) + problems.extend(validate_allocation(topology, reserved).errors) + return problems diff --git a/r42playbooks/cli.py b/r42playbooks/cli.py new file mode 100644 index 00000000..388aac1a --- /dev/null +++ b/r42playbooks/cli.py @@ -0,0 +1,367 @@ +"""Typer CLI — the scenario generator frontend. + +Commands: + list enumerate catalog modules (boxes/subnets/policies/roles/ + containers/images) or existing generated scenarios + show describe one catalog module (box, subnet, policy, image…) + new compose a ScenarioSpec (from flags or --spec) and render a + deployable scenarios// tree + validate typo-check a scenario.r42.yml against the catalog + +A thin shell over the frozen ``r42playbooks.api``: it parses args, prints, and +maps core errors to exit codes. No business logic lives here. +""" + +from enum import Enum +from pathlib import Path +from typing import NoReturn + +import typer +from pydantic import ValidationError as _PydValidationError + +from r42playbooks import api +from r42playbooks.core.catalog import find_template_vm +from r42playbooks.core.errors import ScenarioExistsError, TopologyError +from r42playbooks.core.idalloc import ReservedIndex +from r42playbooks.core.spec import ScenarioSpec + +app = typer.Typer( + help="range42 scenario generator (compose labs from the catalog)", + no_args_is_help=True, +) + +_CatalogOpt = typer.Option( + Path("../range42-catalog"), + "--catalog", + help="Path to the range42-catalog checkout", +) +_OutputOpt = typer.Option( + Path("scenarios"), "-o", "--output", help="Scenarios output dir" +) +_ReservedOpt = typer.Option( + None, "--reserved", + help="Path to _reserved.json (auto-detected from output dir if present)", +) + + +class ListKind(str, Enum): + boxes = "boxes" + subnets = "subnets" + policies = "policies" + roles = "roles" + containers = "containers" + images = "images" + scenarios = "scenarios" + + +def _fail(message: str) -> NoReturn: + typer.secho(message, fg=typer.colors.RED, err=True) + raise typer.Exit(code=1) + + +def _load_catalog(catalog: Path | None) -> api.Catalog: + if catalog is None: + _fail("error: --catalog is required") + try: + return api.load_catalog(catalog) + except TopologyError as exc: + _fail(f"error: {exc}") + + +def _reserved(path: Path | None) -> ReservedIndex | None: + if path is None: + return None + try: + return ReservedIndex.from_file(path) + except TopologyError as exc: + _fail(f"error: {exc}") + + +# --- list ------------------------------------------------------------------ + + +@app.command("list") +def list_cmd( + kind: ListKind = typer.Argument(..., help="What to enumerate"), + catalog: Path = _CatalogOpt, + output: Path = _OutputOpt, +) -> None: + """List pickable catalog modules, or existing generated scenarios.""" + if kind is ListKind.scenarios: + paths = sorted(Path(output).glob("*/scenario.r42.yml")) + if not paths: + return + for path in paths: + try: + spec = api.load_spec(path) + boxes_summary = ", ".join( + f"{b.template}" + (f"×{b.count}" if b.count > 1 else "") + for b in spec.boxes + ) + typer.echo(f"{spec.name}\t{spec.subnet_layout}\t[{boxes_summary}]") + except Exception as exc: + typer.secho(f"warning: could not parse {path}: {exc}", fg=typer.colors.YELLOW, err=True) + typer.echo(path.parent.name) + return + + cat = _load_catalog(catalog) + if kind is ListKind.boxes: + rows = [(name, cat.box_templates[name].template_vm) + for name in sorted(cat.box_templates)] + w0 = max(len(r[0]) for r in rows) + for name, tvm in rows: + typer.echo(f"{name:<{w0}} {tvm}") + elif kind is ListKind.subnets: + for name in sorted(cat.subnet_layouts): + typer.echo(name) + elif kind is ListKind.policies: + for name in sorted(cat.network_policies): + typer.echo(name) + elif kind is ListKind.roles: + for name in sorted(cat.roles): + typer.echo(name) + elif kind is ListKind.containers: + for name in sorted(cat.containers): + typer.echo(name) + elif kind is ListKind.images: + rows_img = [(image_id, f"{cat.images[image_id].distro}/{cat.images[image_id].codename}", + f"{len(cat.images[image_id].proxmox_templates)} template(s)") + for image_id in sorted(cat.images)] + w0 = max(len(r[0]) for r in rows_img) + w1 = max(len(r[1]) for r in rows_img) + for image_id, distro_codename, tpl_count in rows_img: + typer.echo(f"{image_id:<{w0}} {distro_codename:<{w1}} {tpl_count}") + + +# --- show ------------------------------------------------------------------ + + +@app.command() +def show( + module: str = typer.Argument( + ..., help="A box / subnet / policy / role / container name" + ), + catalog: Path = _CatalogOpt, +) -> None: + """Describe a single catalog module (auto-detects its kind).""" + cat = _load_catalog(catalog) + + if module in cat.box_templates: + bt = cat.box_templates[module] + typer.secho(f"box-template: {bt.id}", bold=True) + if bt.description: + typer.echo(f" {bt.description}") + resolved = find_template_vm(cat, bt.template_vm) + if resolved: + image_id, tpl = resolved + typer.echo( + f" template_vm: {bt.template_vm} [{image_id} vm_id={tpl.vm_id} {tpl.spec}]" + ) + else: + typer.echo(f" template_vm: {bt.template_vm}") + attachments = bt.default_attachments or [] + typer.echo( + f" default roles: {', '.join(a.catalog_ref for a in attachments) or '(none)'}" + ) + elif module in cat.subnet_layouts: + sl = cat.subnet_layouts[module] + typer.secho(f"subnet-layout: {sl.id}", bold=True) + if sl.description: + typer.echo(f" {sl.description}") + for s in sl.subnets: + typer.echo(f" - {s.name}={s.cidr}@{s.bridge}") + if sl.template_subnet: + ts = sl.template_subnet + typer.echo(f" template_subnet: {ts.cidr}@{ts.bridge}") + elif module in cat.network_policies: + pol = cat.network_policies[module] + typer.secho(f"network-policy: {pol.id}", bold=True) + typer.echo(f" zones: {', '.join(z.name for z in pol.zones)}") + typer.echo(f" services: {len(pol.services)} matrix rules: {len(pol.matrix)}") + elif module in cat.images: + img = cat.images[module] + typer.secho(f"image: {img.id}", bold=True) + if img.description: + typer.echo(f" {img.description}") + typer.echo(f" distro/codename: {img.distro}/{img.codename}") + if img.cloud_image: + typer.echo(f" cloud_image: {img.cloud_image.filename}") + if img.proxmox_templates: + typer.echo(f" proxmox_templates ({len(img.proxmox_templates)}):") + for tpl in img.proxmox_templates: + typer.echo(f" vm_id={tpl.vm_id} {tpl.vm_name} {tpl.spec}") + elif module in cat.roles: + typer.secho(f"role: {module}", bold=True) + elif module in cat.containers: + typer.secho(f"container: {module}", bold=True) + else: + _fail(f"error: no catalog module named {module!r}") + + +# --- new ------------------------------------------------------------------- + + +def _print_manifest_summary(root: Path) -> None: + """Print a compact VM/template table from the generated manifest.""" + import json + + manifest_path = root / "manifest" / "scenario_vms.json" + if not manifest_path.is_file(): + return + data = json.loads(manifest_path.read_text()) + templates = data.get("templates", []) + vms = data.get("vms", []) + if templates: + typer.echo(f"\n templates ({len(templates)}):") + for t in templates: + typer.echo(f" {t['vm_id']} {t['vm_name']} [{t['image']} {t['spec']}]") + if vms: + typer.echo(f"\n boxes ({len(vms)}):") + col = max(len(v["vm_name"]) for v in vms) + for v in vms: + typer.echo( + f" {v['vm_id']} {v['vm_name']:<{col}} {v['subnet']:<8} {v['ip']}" + ) + + +def _parse_box(raw: str) -> dict: + """Parse a ``--box`` flag: ``template`` or ``template:subnet=admin,count=5``.""" + template, _, rest = raw.partition(":") + box: dict = {"template": template} + if rest: + for pair in rest.split(","): + key, _, value = pair.partition("=") + key = key.strip() + if key == "subnet": + box[key] = value.strip() + elif key in ("count", "template_vm_id", "octet"): + try: + box[key] = int(value) + except ValueError: + _fail(f"error: --box {raw!r}: {key} must be an integer") + else: + _fail(f"error: unknown box option {key!r} in --box {raw!r}") + return box + + +def _build_spec( + name: str, + subnet: str | None, + policy: str | None, + boxes: list[str], + spec_file: Path | None, + proxmox_node: str | None, + notes: str | None, +) -> ScenarioSpec: + """Build a ScenarioSpec from --spec (name-overridden) or from flags.""" + if spec_file is not None: + try: + loaded = api.load_spec(spec_file) + except TopologyError as exc: + _fail(f"error: {exc}") + else: + data = loaded.model_dump(mode="json") + data["name"] = name # positional name wins, so the output dir matches + else: + if not subnet: + _fail("error: --subnet is required (or pass --spec)") + if not boxes: + _fail("error: at least one --box is required (or pass --spec)") + data = { + "name": name, + "subnet_layout": subnet, + "boxes": [_parse_box(b) for b in boxes], + } + if policy: + data["network_policy"] = policy + if proxmox_node: + data["proxmox_node"] = proxmox_node + if notes: + data["notes"] = notes + try: + return ScenarioSpec.model_validate(data) + except _PydValidationError as exc: + _fail(f"error: invalid composition: {exc}") + + +@app.command() +def new( + name: str = typer.Argument(..., help="Scenario name (no dots)"), + subnet: str = typer.Option(None, "--subnet", help="subnet_layout template id"), + policy: str = typer.Option( + None, + "--policy", + help="network_policy id — emits 05_network_isolation/ iptables playbook", + ), + box: list[str] = typer.Option( + None, "--box", help="template[:count=N,template_vm_id=ID]" + ), + spec: Path = typer.Option( + None, "--spec", help="Load a scenario.r42.yml instead of flags" + ), + proxmox_node: str = typer.Option( + None, "--proxmox-node", help="Target Proxmox node" + ), + notes: str = typer.Option(None, "--notes", help="Free-text notes"), + force: bool = typer.Option( + False, "--force", help="Overwrite an existing scenario dir" + ), + catalog: Path = _CatalogOpt, + output: Path = _OutputOpt, + reserved: Path = _ReservedOpt, +) -> None: + """Compose a scenario and render a deployable scenarios// tree.""" + cat = _load_catalog(catalog) + composed = _build_spec(name, subnet, policy, box or [], spec, proxmox_node, notes) + + problems = api.validate_refs(composed, cat) + if problems: + for p in problems: + typer.secho(f" ✗ {p}", fg=typer.colors.RED) + _fail(f"{len(problems)} unknown catalog reference(s)") + + # Auto-detect _reserved.json in the output dir when not explicitly passed. + auto = output / "_reserved.json" + effective_reserved = reserved if reserved is not None else (auto if auto.is_file() else None) + + try: + root = api.render_scenario( + composed, + catalog=cat, + dest=output, + reserved=_reserved(effective_reserved), + overwrite=force, + ) + except ScenarioExistsError as exc: + _fail(f"{exc}\n pass --force to overwrite it") + except TopologyError as exc: + _fail(f"generate failed: {exc}") + typer.secho(f"✓ generated {root}", fg=typer.colors.GREEN) + _print_manifest_summary(root) + + +@app.command() +def validate( + spec: Path = typer.Argument(..., help="Path to a scenario.r42.yml spec file"), + catalog: Path = _CatalogOpt, +) -> None: + """Validate a scenario.r42.yml spec against the catalog (no files written).""" + cat = _load_catalog(catalog) + try: + composed = api.load_spec(spec) + except TopologyError as exc: + _fail(f"error: {exc}") + + problems = api.validate_refs(composed, cat) + if problems: + for p in problems: + typer.secho(f" ✗ {p}", fg=typer.colors.RED) + _fail(f"{len(problems)} problem(s) found in {spec}") + typer.secho( + f"✓ {spec.name} — spec valid ({len(composed.boxes)} box(es), {composed.subnet_layout})", + fg=typer.colors.GREEN, + ) + + +if __name__ == "__main__": + app() diff --git a/r42playbooks/core/__init__.py b/r42playbooks/core/__init__.py new file mode 100644 index 00000000..c1ad7b9a --- /dev/null +++ b/r42playbooks/core/__init__.py @@ -0,0 +1,6 @@ +"""Pure core for r42playbooks: pydantic models, validation, IO, compiler. + +This sub-package imports only pydantic, pyyaml, and the standard library. +It must remain free of CLI/TUI/web-framework imports so every consumer +(backend-api, CLI, TUI, deployment tooling) can import it cheaply. +""" diff --git a/r42playbooks/core/allocate.py b/r42playbooks/core/allocate.py new file mode 100644 index 00000000..86331657 --- /dev/null +++ b/r42playbooks/core/allocate.py @@ -0,0 +1,323 @@ +"""Turn a ``ScenarioSpec`` into a concrete, deterministic VM allocation. + +Renderer-A of the generator: place each composed box in its role's subnet, +assign a ``vm_id``/IP that honours the project **octet rule** (vm_id last 3 +digits == IP last octet for single-subnet VMs) and **global uniqueness** via +``_reserved.json``, resolve each box's Proxmox clone template (§7.1 / H2), +expand ``count>1`` boxes, and emit the demo_lab ``scenario_vms.json`` shape with +a populated ``templates[]`` (H1). + +Pure and read-only: it never writes ``_reserved.json`` (claiming reservations +with a lock is the deploy side's job — see ``idalloc`` docstring). +""" + +import json +from dataclasses import dataclass +from types import MappingProxyType +from typing import Any, Mapping + +from r42playbooks.core import constants as C +from r42playbooks.core.catalog import Catalog, find_template_vm +from r42playbooks.core.errors import CompileError, TopologyError, ValidationError +from r42playbooks.core.idalloc import ReservedIndex +from r42playbooks.core.models import Attachment, Subnet +from r42playbooks.core.spec import BoxSpec, ScenarioSpec + + +@dataclass(frozen=True) +class ResolvedTemplate: + """A concrete Proxmox template VM resolved from the catalog for this scenario. + + Contains only the template VMs actually referenced by boxes in the scenario + (selective — not the full image table). The renderer uses this to emit only + the needed ``stage_01-create_templates`` create plays. + """ + + vm_id: int + vm_name: str + spec: str + ip: str + bridge: str + image: str # image_id this template belongs to (e.g. "ubuntu_noble") + + +@dataclass(frozen=True) +class AllocatedBox: + """One concrete VM: allocation result + everything the renderer needs.""" + + vm_id: int + vm_name: str + ip: str + bridge: str + subnet_name: str + section: str # playbook directory (e.g. "02_admin_infrastructure") + label: str # human task label (e.g. "ADMIN INFRASTRUCTURE INIT") + gateway: str | None + inventory_group: str + box_template: str # the catalog box-template id (e.g. "vuln-box") + image: str # versioned base image (e.g. ubuntu_noble, debian_trixie) + template_vm_id: int # the 9xxx clone source (global_template_vm_id) + template_name: str + attachments: tuple[Attachment, ...] + box_vars: Mapping[str, Any] # read-only (MappingProxyType) — frozen-dataclass safe + + +@dataclass(frozen=True) +class Allocation: + """The fully-resolved composition the renderer turns into a scenario tree.""" + + scenario: str + description: str + boxes: tuple[AllocatedBox, ...] + templates: tuple[ResolvedTemplate, ...] # only the VMs this scenario actually needs + subnets: tuple[Subnet, ...] + + +_DEFAULT_OCTET = 10 # starting octet for auto-allocated boxes (no explicit octet set) +_TEMPLATE_IP_BASE = 2 # first host octet for template build IPs on the template subnet (gw is .1) + + +def _subnet_prefix(cidr: str) -> str: + """Return the leading 3 octets of a /24 CIDR (host octet stripped).""" + return cidr.split("/", 1)[0].rsplit(".", 1)[0] + + +def _expand_names(subnet_name: str, template_id: str, count: int) -> list[str]: + """Always produce {subnet}-{template}-{index:02d}, keeping names unique across subnets.""" + return [f"{subnet_name}-{template_id}-{i:0{C.REPLICA_PAD}d}" for i in range(count)] + + +def _next_free_octet(base: int, prefix: str, taken_ips: set[str]) -> int: + """Lowest host octet >= base whose IP in *prefix* is not already taken.""" + for octet in range(base, C.HOST_OCTET_MAX + 1): + if f"{prefix}.{octet}" not in taken_ips: + return octet + raise CompileError(f"subnet {prefix}.0/24 exhausted from octet {base}") + + +def _next_free_vm_id(octet: int, taken_ids: set[int]) -> int: + """Lowest vm_id (band*1000+octet, bands 1..8) not already claimed. + + Every candidate satisfies the octet rule by construction (vm_id % 1000 == + octet); bumping the band keeps the rule while dodging a global collision. + """ + for band in range(C.VM_ID_BAND_MIN, C.VM_ID_BAND_MAX + 1): + vm_id = band * 1000 + octet + if vm_id not in taken_ids: + return vm_id + raise CompileError(f"no free vm_id band for octet {octet} (bands 1..{C.VM_ID_BAND_MAX} taken)") + + +def _blocked(reserved: ReservedIndex, scenario: str) -> tuple[set[int], set[str]]: + """vm_ids/IPs owned by *other* scenarios (our own rows do not block a re-run).""" + ids: set[int] = set() + ips: set[str] = set() + for e in reserved.entries: + if e.get("scenario") == scenario: + continue + if "vm_id" in e: + try: + ids.add(int(e["vm_id"])) + except (TypeError, ValueError) as exc: + raise TopologyError( + f"_reserved.json has invalid vm_id {e['vm_id']!r}: {exc}" + ) from exc + if "ip" in e: + ips.add(str(e["ip"])) + return ids, ips + + +def _subnet_section(index: int, name: str) -> str: + """Playbook dir for a subnet: 01_init_proxmox is always slot 0, subnets start at 02.""" + return f"{index + 2:02d}_{name}_infrastructure" + + +def _subnet_label(name: str) -> str: + return f"{name.upper()} INFRASTRUCTURE INIT" + + +def _allocate_box( + box: BoxSpec, + catalog: Catalog, + subnets_by_name: dict[str, Subnet], + subnet_index: dict[str, int], + spec_name: str, + taken_ids: set[int], + taken_ips: set[str], +) -> list[AllocatedBox]: + """Resolve and place every replica of one composed box.""" + bt = catalog.resolve_box_template(box.template) # raises CatalogNotFoundError + + subnet_name = box.subnet + subnet = subnets_by_name.get(subnet_name) if subnet_name else None + if subnet is None: + raise CompileError( + f"subnet layout has no {subnet_name!r} subnet " + f"(box {box.template!r})" + ) + + prefix = _subnet_prefix(subnet.cidr) + + if box.octet is not None and subnet.gateway is not None: + if f"{prefix}.{box.octet}" == subnet.gateway: + raise CompileError( + f"box {box.template!r} octet {box.octet} on subnet {subnet_name!r} " + f"conflicts with the subnet gateway {subnet.gateway}" + ) + + # Resolve template VM from catalog (globally unique vm_name → image + spec). + resolved = find_template_vm(catalog, bt.template_vm) + if resolved is None: + raise CompileError( + f"box template {box.template!r} references unknown template_vm " + f"{bt.template_vm!r} — add it to the catalog's 01_image_layer" + ) + image_id, tpl_spec = resolved + # BoxSpec.template_vm_id overrides the catalog reference for ad-hoc pinning. + # image_id is kept from the catalog-resolved template_vm; only the numeric id changes. + if box.template_vm_id is not None: + tmpl_vm_id = box.template_vm_id + tmpl_vm_name = bt.template_vm # best-effort: catalog name may differ from override id + else: + tmpl_vm_id = tpl_spec.vm_id + tmpl_vm_name = tpl_spec.vm_name + + attachments = tuple(bt.default_attachments) + tuple(box.attachments_add) + start_octet = box.octet if box.octet is not None else _DEFAULT_OCTET + + placed: list[AllocatedBox] = [] + for name in _expand_names(subnet_name, box.template, box.count): + octet = _next_free_octet(start_octet, prefix, taken_ips) + vm_id = _next_free_vm_id(octet, taken_ids) + ip = f"{prefix}.{octet}" + taken_ips.add(ip) + taken_ids.add(vm_id) + placed.append(AllocatedBox( + vm_id=vm_id, + vm_name=name, + ip=ip, + bridge=subnet.bridge, + subnet_name=subnet.name, + section=_subnet_section(subnet_index[subnet_name], subnet_name), + label=_subnet_label(subnet_name), + gateway=subnet.gateway, + inventory_group=f"r42_{subnet_name}_group", + box_template=box.template, + image=image_id, + template_vm_id=tmpl_vm_id, + template_name=tmpl_vm_name, + attachments=attachments, + box_vars=MappingProxyType(dict(box.vars)), + )) + return placed + + +def allocate(spec: ScenarioSpec, catalog: Catalog, reserved: ReservedIndex | None = None) -> Allocation: + """Resolve *spec* against *catalog* into a deterministic ``Allocation``. + + :raises CatalogNotFoundError: a referenced subnet layout / box template is unknown. + :raises CompileError: a box cannot be placed (no subnet, exhausted ids/octets). + """ + if reserved is None: + reserved = ReservedIndex(entries=()) + + layout = catalog.resolve_subnet_layout(spec.subnet_layout) + subnets_by_name = {s.name: s for s in layout.subnets} + subnet_index = {s.name: i for i, s in enumerate(layout.subnets)} + + taken_ids, taken_ips = _blocked(reserved, spec.name) + + # Reserve gateway IPs — they belong to the Proxmox host, never to VMs. + for s in layout.subnets: + if s.gateway: + taken_ips.add(s.gateway) + + boxes: list[AllocatedBox] = [] + for box in spec.boxes: + boxes.extend( + _allocate_box(box, catalog, subnets_by_name, subnet_index, spec.name, taken_ids, taken_ips) + ) + + + # Resolve template subnet for IP/bridge derivation. + tpl_subnet = layout.template_subnet + if tpl_subnet is None: + raise CompileError( + f"subnet layout {spec.subnet_layout!r} has no template_subnet — add " + f"template_subnet: {{cidr: '192.168.140.0/24', bridge: vmbr140}} to it" + ) + tpl_prefix = _subnet_prefix(tpl_subnet.cidr) + tpl_bridge = tpl_subnet.bridge + + # Build the deduplicated set of template VMs actually needed by this scenario. + # Each distinct template needs a UNIQUE IP on the (single) template subnet: + # the templates are started concurrently during 01_init_proxmox, so a shared + # IP collides on the bridge and the loser's cloud-init can't reach the network + # (apt-get update stalls → it never auto-powers-off → deploy hangs). Deriving + # the template IP from the box octet is unsafe because two boxes in *different* + # lab subnets can legitimately share an octet (e.g. dual-lan .150.2 + .151.2), + # which would map their two distinct templates onto the same template-subnet + # IP. Allocate sequentially on the template subnet instead, skipping the gw. + tpl_gateway = f"{tpl_prefix}.1" + taken_tpl_ips: set[str] = {tpl_gateway} + seen_tpl_ids: set[int] = set() + resolved_templates: list[ResolvedTemplate] = [] + for box in boxes: + if box.template_vm_id in seen_tpl_ids: + continue + result = find_template_vm(catalog, box.template_name) + if result is None: + continue # template_vm_id override without catalog entry — skip manifest entry + image_id, tpl_spec = result + seen_tpl_ids.add(box.template_vm_id) + tpl_octet = _next_free_octet(_TEMPLATE_IP_BASE, tpl_prefix, taken_tpl_ips) + tpl_ip = f"{tpl_prefix}.{tpl_octet}" + taken_tpl_ips.add(tpl_ip) + resolved_templates.append(ResolvedTemplate( + vm_id=box.template_vm_id, + vm_name=tpl_spec.vm_name, + spec=tpl_spec.spec, + ip=tpl_ip, + bridge=tpl_bridge, + image=image_id, + )) + + return Allocation( + scenario=spec.name, + description=spec.notes, + boxes=tuple(boxes), + templates=tuple(sorted(resolved_templates, key=lambda t: t.vm_id)), + subnets=tuple(layout.subnets), + ) + + +def manifest_dict(alloc: Allocation) -> dict[str, Any]: + """Project an Allocation into the demo_lab ``scenario_vms.json`` dict.""" + vms = sorted( + ( + {"vm_id": b.vm_id, "vm_name": b.vm_name, "ip": b.ip, "subnet": b.subnet_name, + "bridge": b.bridge, "image": b.image} + for b in alloc.boxes + ), + key=lambda v: v["vm_id"], + ) + templates = sorted( + [ + {"vm_id": t.vm_id, "vm_name": t.vm_name, "spec": t.spec, "ip": t.ip, + "bridge": t.bridge, "image": t.image} + for t in alloc.templates + ], + key=lambda t: t["vm_id"], + ) + return { + "scenario": alloc.scenario, + "version": 2, + "description": alloc.description, + "vms": vms, + "templates": templates, + } + + +def manifest_json(alloc: Allocation) -> str: + """Serialize the manifest as deterministic, sorted JSON.""" + return json.dumps(manifest_dict(alloc), indent=2, sort_keys=True) + "\n" diff --git a/r42playbooks/core/catalog.py b/r42playbooks/core/catalog.py new file mode 100644 index 00000000..f998513b --- /dev/null +++ b/r42playbooks/core/catalog.py @@ -0,0 +1,366 @@ +"""Catalog loader: resolve + validate 05_topology_layer templates from disk. + +Layout (directory-per-version): + /05_topology_layer///v../template.yml + +For each (category, id) the highest version is selected, parsed, validated +against its pydantic model, and content-hashed (sha256 of the raw file) so a +compile can record exactly which template version produced an artifact. + +Security posture mirrors the backend's checks_playbooks.py: template ids are +regex-validated, resolved with strict=True, asserted to stay inside the layer +root, and symlinks escaping the root are rejected. +""" + +import hashlib +from dataclasses import dataclass, field +from pathlib import Path +from typing import TYPE_CHECKING + +import yaml +from pydantic import ValidationError as _PydValidationError + +from r42playbooks.core import constants as C +from r42playbooks.core.catalog_models import ( + BoxTemplate, + ImageDef, + NetworkPolicyTemplate, + ProxmoxTemplateSpec, + SubnetLayout, +) +from r42playbooks.core.errors import CatalogNotFoundError, ValidationError + +if TYPE_CHECKING: + from r42playbooks.core.spec import ScenarioSpec + +_CATEGORY_MODEL = { + C.CATEGORY_BOX_TEMPLATES: BoxTemplate, + C.CATEGORY_NETWORK_POLICIES: NetworkPolicyTemplate, + C.CATEGORY_SUBNET_LAYOUTS: SubnetLayout, +} + + +@dataclass(frozen=True) +class _Resolved: + """A loaded template plus the version + content hash it came from.""" + + model: object + version: str + sha256: str + + +@dataclass +class Catalog: + """In-memory index of validated catalog templates + pickable refs.""" + + # 01_image_layer — base VM image descriptors (optional; empty when layer absent) + images: dict[str, ImageDef] = field(default_factory=dict) + # 05_topology_layer categories + box_templates: dict[str, BoxTemplate] = field(default_factory=dict) + network_policies: dict[str, NetworkPolicyTemplate] = field(default_factory=dict) + subnet_layouts: dict[str, SubnetLayout] = field(default_factory=dict) + # name-referenced modules from 02_/03_ (not pydantic templates) — see S3. + roles: set[str] = field(default_factory=set) + containers: set[str] = field(default_factory=set) + _resolved: dict[tuple[str, str], _Resolved] = field( + default_factory=dict, init=False, repr=False + ) + + # -- resolution helpers (raise CatalogNotFoundError on miss) -- + + def resolve_box_template(self, ref: str) -> BoxTemplate: + return self._resolve(C.CATEGORY_BOX_TEMPLATES, ref, self.box_templates) + + def resolve_network_policy(self, ref: str) -> NetworkPolicyTemplate: + return self._resolve(C.CATEGORY_NETWORK_POLICIES, ref, self.network_policies) + + def resolve_subnet_layout(self, ref: str) -> SubnetLayout: + return self._resolve(C.CATEGORY_SUBNET_LAYOUTS, ref, self.subnet_layouts) + + def resolved_version(self, category: str, template_id: str) -> str: + return self._resolved_entry(category, template_id).version + + def resolved_hash(self, category: str, template_id: str) -> str: + return self._resolved_entry(category, template_id).sha256 + + def _resolved_entry(self, category: str, template_id: str) -> _Resolved: + try: + return self._resolved[(category, template_id)] + except KeyError: + raise CatalogNotFoundError( + f"{category} template not in resolved index: {template_id!r}" + ) from None + + def _resolve(self, category: str, ref: str, index: dict): + # ref is a bare template id (version pinning via @range is a later refinement) + if not C.TEMPLATE_ID_RE.fullmatch(ref): + raise CatalogNotFoundError(f"invalid template id: {ref!r}") + try: + return index[ref] + except KeyError: + raise CatalogNotFoundError( + f"{category} template not found: {ref!r}" + ) from None + + +def _highest_version_dir(template_dir: Path) -> tuple[Path, tuple[int, int, int]]: + """Return (version_dir, parsed_semver) for the highest vX.Y.Z under a template.""" + best: tuple[tuple[int, int, int], Path] | None = None + for child in template_dir.iterdir(): + if not child.is_dir(): + continue + m = C.VERSION_DIR_RE.fullmatch(child.name) + if not m: + continue + sem = (int(m.group(1)), int(m.group(2)), int(m.group(3))) + if best is None or sem > best[0]: + best = (sem, child) + if best is None: + raise CatalogNotFoundError(f"no versioned template under {template_dir}") + return best[1], best[0] + + +def _load_category(layer_root: Path, category: str, catalog: Catalog) -> None: + """Load every template id under a category into the catalog index.""" + category_dir = (layer_root / category).resolve() + if not category_dir.is_dir(): + return # category optional + if not category_dir.is_relative_to(layer_root): # symlink escape guard + raise CatalogNotFoundError(f"category escapes layer root: {category}") + + model_cls = _CATEGORY_MODEL[category] + index = { + C.CATEGORY_BOX_TEMPLATES: catalog.box_templates, + C.CATEGORY_NETWORK_POLICIES: catalog.network_policies, + C.CATEGORY_SUBNET_LAYOUTS: catalog.subnet_layouts, + }[category] + + for template_dir in sorted(category_dir.iterdir()): + if not template_dir.is_dir(): + continue + template_id = template_dir.name + if not C.TEMPLATE_ID_RE.fullmatch(template_id): + raise ValidationError(f"invalid template id directory: {template_id!r}") + + version_dir, sem = _highest_version_dir(template_dir) + template_file = (version_dir / "template.yml").resolve(strict=True) + if not template_file.is_relative_to(layer_root): + raise CatalogNotFoundError(f"template escapes layer root: {template_file}") + + raw = template_file.read_bytes() + sha = hashlib.sha256(raw).hexdigest() + data = yaml.safe_load(raw.decode("utf-8")) + try: + model = model_cls.model_validate(data) + except _PydValidationError as exc: # pydantic ValidationError -> our ValidationError + raise ValidationError( + f"invalid {category} template {template_id!r}: {exc}" + ) from exc + + if model.id != template_id: + raise ValidationError( + f"template id {model.id!r} does not match directory {template_id!r}" + ) + + index[template_id] = model + catalog._resolved[(category, template_id)] = _Resolved( + model=model, version=f"{sem[0]}.{sem[1]}.{sem[2]}", sha256=sha + ) + + +def _load_image_layer(catalog_root: Path, catalog: Catalog) -> None: + """Load every image descriptor from ``01_image_layer/`` into *catalog*. + + The layer is optional — absent dirs are silently skipped so old/minimal + catalogs remain loadable. Each ``/v*/image.yml`` file is validated + against :class:`~r42playbooks.core.catalog_models.ImageDef`. + """ + layer_root = (Path(catalog_root) / C.IMAGE_LAYER_DIR).resolve() + if not layer_root.is_dir(): + return + + for image_dir in sorted(layer_root.iterdir()): + if not image_dir.is_dir(): + continue + image_id = image_dir.name + if not C.IMAGE_RE.fullmatch(image_id): + raise ValidationError(f"invalid image id directory: {image_id!r}") + + version_dir, _ = _highest_version_dir(image_dir) + image_file = (version_dir / "image.yml").resolve(strict=True) + if not image_file.is_relative_to(layer_root): + raise CatalogNotFoundError(f"image escapes layer root: {image_file}") + + raw = image_file.read_bytes() + data = yaml.safe_load(raw.decode("utf-8")) + try: + model = ImageDef.model_validate(data) + except _PydValidationError as exc: + raise ValidationError(f"invalid image {image_id!r}: {exc}") from exc + + if model.id != image_id: + raise ValidationError( + f"image id {model.id!r} does not match directory {image_id!r}" + ) + + catalog.images[image_id] = model + + # Enforce global uniqueness of template vm_names across all images. + seen: dict[str, str] = {} # vm_name -> image_id + for image_id, img_def in catalog.images.items(): + for tpl in img_def.proxmox_templates: + if tpl.vm_name in seen: + raise ValidationError( + f"template vm_name {tpl.vm_name!r} appears in both " + f"{seen[tpl.vm_name]!r} and {image_id!r} — vm_names must be " + f"globally unique so box_template.template_vm can resolve unambiguously" + ) + seen[tpl.vm_name] = image_id + + +def find_template_vm( + catalog: "Catalog", vm_name: str +) -> "tuple[str, ProxmoxTemplateSpec] | None": + """Return ``(image_id, ProxmoxTemplateSpec)`` for *vm_name*, or ``None``. + + vm_names are globally unique across all images (enforced by ``_load_image_layer``), + so the first match is the only match. + """ + for image_id, img_def in catalog.images.items(): + for tpl in img_def.proxmox_templates: + if tpl.vm_name == vm_name: + return image_id, tpl + return None + + +def list_images(catalog_root: Path) -> list[str]: + """Enumerate base image ids from ``01_image_layer/``. + + Returns a sorted list of image id strings (e.g. ``["debian_trixie", + "ubuntu_noble"]``). Returns an empty list when the layer is absent. + """ + layer_root = (Path(catalog_root) / C.IMAGE_LAYER_DIR).resolve() + if not layer_root.is_dir(): + return [] + ids: list[str] = [] + for image_dir in sorted(layer_root.iterdir()): + if not image_dir.is_dir(): + continue + if C.IMAGE_RE.fullmatch(image_dir.name): + ids.append(image_dir.name) + return ids + + +def list_roles(catalog_root: Path) -> list[str]: + """Enumerate reusable Ansible role names under ``02_ansible_layer/**/roles/``. + + Roles are referenced by name (``..``) and resolve at + deploy time via ``ANSIBLE_ROLES_PATH`` — this is a read-only name scan, never a + copy. Returns a sorted, de-duplicated list. Empty if the layer is absent. + """ + layer_root = (Path(catalog_root) / C.ANSIBLE_LAYER_DIR).resolve() + if not layer_root.is_dir(): + return [] + names: set[str] = set() + for roles_dir in layer_root.rglob(C.ROLES_DIR_NAME): + if not roles_dir.is_dir() or not roles_dir.resolve().is_relative_to(layer_root): + continue # skip symlink escapes + for child in roles_dir.iterdir(): + if child.is_dir() and C.CATALOG_REF_RE.fullmatch(child.name): + names.add(child.name) + return sorted(names) + + +def list_containers(catalog_root: Path) -> list[str]: + """Enumerate CTF docker stacks under ``03_container_layer/docker/_ctf/``. + + A container ref is the POSIX path, relative to ``_ctf/``, of a directory that + holds a compose file (e.g. ``cve/web/dvwa``). Returns a sorted list. Empty if + the layer is absent. + """ + ctf_root = (Path(catalog_root) / C.CONTAINER_LAYER_DIR / C.CTF_REL_DIR).resolve() + if not ctf_root.is_dir(): + return [] + refs: set[str] = set() + for filename in C.COMPOSE_FILENAMES: + for compose in ctf_root.rglob(filename): + stack_dir = compose.parent.resolve() + if not stack_dir.is_relative_to(ctf_root): + continue # skip symlink escapes + rel = stack_dir.relative_to(ctf_root).as_posix() + if rel and rel != ".": + refs.add(rel) + return sorted(refs) + + +def load_catalog(catalog_root: Path) -> Catalog: + """Load + validate all catalog layers from a catalog checkout. + + Loads (in order): 01_image_layer (optional), 05_topology_layer (required), + 02_ansible_layer roles, 03_container_layer containers. + + :param catalog_root: path that contains ``05_topology_layer/``. + :raises CatalogNotFoundError: if ``05_topology_layer`` is absent. + :raises ValidationError: if any template file fails schema validation. + """ + layer_root = (Path(catalog_root) / C.TOPOLOGY_LAYER_DIR).resolve() + if not layer_root.is_dir(): + raise CatalogNotFoundError(f"missing {C.TOPOLOGY_LAYER_DIR} under {catalog_root}") + + catalog = Catalog() + _load_image_layer(catalog_root, catalog) + for category in _CATEGORY_MODEL: + _load_category(layer_root, category, catalog) + catalog.roles = set(list_roles(catalog_root)) + catalog.containers = set(list_containers(catalog_root)) + return catalog + + +def validate_refs(spec: "ScenarioSpec", catalog: Catalog) -> list[str]: + """Return human-readable messages for every spec ref missing from *catalog*. + + A typo guard for ``scenario.r42.yml``: checks the subnet layout, network + policy, each box template, its base image (when 01_image_layer is loaded), + and every attachment that becomes a generated role name — both the box's + catalog ``default_attachments`` and the spec's ``attachments_add`` (the + renderer emits both, so both must resolve). An empty list means every + referenced module exists. ``gamification`` attachments are not enumerated + here and are skipped (cannot be validated by name yet). + """ + problems: list[str] = [] + if spec.subnet_layout not in catalog.subnet_layouts: + problems.append(f"unknown subnet_layout: {spec.subnet_layout!r}") + # network_policy is optional + ignored by the generator; typo-guard only if set. + if spec.network_policy is not None and spec.network_policy not in catalog.network_policies: + problems.append(f"unknown network_policy: {spec.network_policy!r}") + for box in spec.boxes: + bt = catalog.box_templates.get(box.template) + if bt is None: + problems.append(f"unknown box template: {box.template!r}") + default_attachments = bt.default_attachments if bt else [] + # Validate template_vm resolves when 01_image_layer is loaded (optional layer). + if bt is not None and catalog.images and find_template_vm(catalog, bt.template_vm) is None: + problems.append( + f"unknown template_vm {bt.template_vm!r} (box template {box.template!r})" + ) + for att in list(default_attachments) + list(box.attachments_add): + if att.kind == "role" and att.catalog_ref not in catalog.roles: + problems.append(f"unknown role: {att.catalog_ref!r}") + elif att.kind == "container" and att.catalog_ref not in catalog.containers: + problems.append(f"unknown container: {att.catalog_ref!r}") + if spec.services is not None and spec.services.apt is not None: + apt_svc = spec.services.apt + box_templates = {b.template for b in spec.boxes} + if apt_svc.box not in box_templates: + problems.append( + f"services.apt.box {apt_svc.box!r} is not in spec boxes" + ) + if isinstance(apt_svc.wire_to, list): + for ref in apt_svc.wire_to: + if ref not in box_templates: + problems.append( + f"services.apt.wire_to ref {ref!r} is not in spec boxes" + ) + if "software.configure.apt_mirror_client" not in catalog.roles: + problems.append("unknown role: 'software.configure.apt_mirror_client'") + return problems diff --git a/r42playbooks/core/catalog_models.py b/r42playbooks/core/catalog_models.py new file mode 100644 index 00000000..4a30f5cf --- /dev/null +++ b/r42playbooks/core/catalog_models.py @@ -0,0 +1,211 @@ +"""Pydantic models for catalog layers. + +Loaded artifacts: + - ImageDef (01_image_layer/) base VM image descriptors + - BoxTemplate (box_templates/) VM/box archetypes + - NetworkPolicyTemplate (network_policies/) symbolic isolation policies + - SubnetLayout (subnet_layouts/) subnet/bridge layouts + +Templates carry *symbolic* structure only (zone names, service ports) — no +concrete per-scenario IPs beyond declared params/defaults. The compiler (P3) +binds symbols to a topology's concrete subnets/IPs. +""" + +from typing import Any, Literal, Self + +from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator + +from r42playbooks.core import constants as C +from r42playbooks.core.models import Attachment, Subnet + +_STRICT = ConfigDict(extra="forbid") + + +# --- base images (01_image_layer) ----------------------------------------- + +class CloudImageSpec(BaseModel): + """A cloud image download target: URL to fetch + filename on Proxmox local storage.""" + + model_config = _STRICT + + url: str = Field(min_length=1) + filename: str = Field(min_length=1) + + +class ProxmoxTemplateSpec(BaseModel): + """One Proxmox 9xxx template VM entry — the render source for stage_01 create plays. + + Lives in the catalog so the generator renders + ``stage_01-create_templates/templates//.yml`` without + any hardcoded data in the playbooks repo. + + The template VM IP is resolved at allocation time from the first allocated + box that uses this template: its last octet is mirrored onto the + ``template_subnet`` prefix. No octet is stored here. + """ + + model_config = _STRICT + + vm_id: int + vm_name: str = Field(min_length=1) + spec: str = Field(min_length=1) # "Xcpu/Ygb/Zgb" + + +class ImageDef(BaseModel): + """Base VM image descriptor — the canonical name + distro metadata. + + id matches ``_`` (IMAGE_RE), e.g. ``ubuntu_noble``. + Box templates reference images by id; validate_refs checks that the id + exists in the catalog's 01_image_layer. + cloud_image carries the download coordinates used by the generator to + render stage_00-download_cloudinit_files/.yml. + proxmox_templates carries the per-VM create coordinates used to render + stage_01-create_templates/templates//.yml. + """ + + model_config = _STRICT + + id: str = Field(pattern=C.IMAGE_RE.pattern) + api_version: int = 1 + distro: str = Field(pattern=r"^[a-z0-9]+$", min_length=1) + codename: str = Field(pattern=r"^[a-z0-9]+$", min_length=1) + description: str = "" + cloud_image: CloudImageSpec | None = None + proxmox_templates: list[ProxmoxTemplateSpec] = Field(default_factory=list) + + +# --- box templates --------------------------------------------------------- + +class BoxTemplate(BaseModel): + """A VM/box archetype: template VM reference and attachments. + + ``template_vm`` is the ``vm_name`` of a :class:`ProxmoxTemplateSpec` entry in + the catalog's ``01_image_layer``. It uniquely identifies both the clone source + and its base image (vm_names are globally unique across all images). The + generator resolves it at allocation time to derive the image, vm_id, and spec. + The Ansible inventory group is derived at allocation time from the subnet name. + """ + + model_config = _STRICT + + id: str = Field(pattern=C.TEMPLATE_ID_RE.pattern) + api_version: int = 1 + description: str = "" + template_vm: str = Field(pattern=C.TEMPLATE_ID_RE.pattern) + default_attachments: list[Attachment] = Field(default_factory=list) + + +# --- subnet layouts -------------------------------------------------------- + +class TemplateSubnet(BaseModel): + """The Proxmox infrastructure subnet used for template VM creation. + + Separate from lab ``subnets`` (which define the scenario's zone topology). + The generator reads ``cidr`` and ``bridge`` here to derive template VM IPs + (``{prefix}.{first_box_octet}``) and network attachments. + """ + + model_config = _STRICT + + cidr: str = Field(pattern=C.IPV4_CIDR_RE.pattern) + bridge: str = Field(pattern=C.BRIDGE_RE.pattern) + + +class SubnetLayout(BaseModel): + """A reusable set of subnet/bridge declarations a topology can adopt. + + ``subnets`` lists the lab zones (admin, student, ctf …). + ``template_subnet`` defines the separate Proxmox infrastructure subnet + for template VM creation — not counted as a lab zone. + """ + + model_config = _STRICT + + id: str = Field(pattern=C.TEMPLATE_ID_RE.pattern) + api_version: int = 1 + description: str = "" + subnets: list[Subnet] = Field(min_length=1) + template_subnet: TemplateSubnet | None = None + + +# --- network policy templates --------------------------------------------- + +class PortSpec(BaseModel): + """A protocol + optional port (range). port is None for icmp/all-ports.""" + + model_config = _STRICT + + proto: Literal["tcp", "udp", "icmp"] + port: int | None = Field(default=None, ge=1, le=65535) + port_end: int | None = Field(default=None, ge=1, le=65535) + + @model_validator(mode="after") + def _port_range_valid(self) -> Self: + if self.port is not None and self.port_end is not None: + if self.port_end < self.port: + raise ValueError( + f"port_end ({self.port_end}) must be >= port ({self.port})" + ) + return self + + +class ZoneDecl(BaseModel): + """A symbolic zone; `wan: true` marks the uplink/internet pseudo-zone.""" + + model_config = _STRICT + + name: str = Field(pattern=C.VM_NAME_RE.pattern) + wan: bool = False + + +class ServiceEndpoint(BaseModel): + """A named service inside a zone, addressable in the matrix as `svc:`.""" + + model_config = _STRICT + + name: str = Field(pattern=C.VM_NAME_RE.pattern) + zone: str = Field(pattern=C.VM_NAME_RE.pattern) + ports: list[PortSpec] = Field(min_length=1) + + +class MatrixRule(BaseModel): + """One intent cell: src zone -> dst (zone | `svc:`) with an action.""" + + model_config = _STRICT + + src: str = Field(pattern=C.MATRIX_SRC_RE.pattern) + dst: str = Field(pattern=C.MATRIX_DST_RE.pattern) + action: Literal["accept", "drop", "reject"] + ports: list[PortSpec] = Field(default_factory=list) + comment: str | None = Field(default=None, max_length=200) + + _guard_comment = field_validator("comment")( + lambda v: C.reject_injection(v) if v is not None else v + ) + + +class PolicyDefaults(BaseModel): + """Catch-all behaviour the compiler synthesizes around explicit matrix rules.""" + + model_config = _STRICT + + default_action: Literal["drop", "reject", "accept"] = "drop" + accept_established_related: bool = True + allow_intra_zone: bool = True + airgap_zones: list[str] = Field(default_factory=list) + + +class NetworkPolicyTemplate(BaseModel): + """A symbolic, parametric isolation policy (generalizes 05_network_isolation).""" + + model_config = _STRICT + + id: str = Field(pattern=C.TEMPLATE_ID_RE.pattern) + api_version: int = 1 + kind: Literal["isolation-policy"] + description: str = "" + params: dict[str, Any] = Field(default_factory=dict) + zones: list[ZoneDecl] = Field(min_length=1) + services: list[ServiceEndpoint] = Field(default_factory=list) + matrix: list[MatrixRule] = Field(default_factory=list) + defaults: PolicyDefaults = Field(default_factory=PolicyDefaults) diff --git a/r42playbooks/core/compiler/__init__.py b/r42playbooks/core/compiler/__init__.py new file mode 100644 index 00000000..73a59184 --- /dev/null +++ b/r42playbooks/core/compiler/__init__.py @@ -0,0 +1,102 @@ +"""Topology compiler: expand a validated Topology into deploy artifacts. + +``compile_topology`` is the orchestrator. It validates (semantics + allocation), +resolves catalog templates, builds the artifacts in an isolated workspace, runs +the network-segmentation linter, and returns a :class:`CompileResult` of paths. +Artifacts are written under:: + + /project/topology.json + /project/manifest/scenario_vms.json + /project/network_policy.json + /project/stages.json + /inventory/hosts.yml +""" + +from dataclasses import dataclass +from pathlib import Path + +from r42playbooks.core.catalog import Catalog +from r42playbooks.core.compiler import inventory as _inventory +from r42playbooks.core.compiler import network_policy as _netpol +from r42playbooks.core.compiler import scenario_vms as _scenario_vms +from r42playbooks.core.compiler import stages as _stages +from r42playbooks.core.errors import CompileError +from r42playbooks.core.idalloc import ReservedIndex, validate_allocation +from r42playbooks.core.io import atomic_write_text, dump_topology +from r42playbooks.core.models import Topology +from r42playbooks.core.validate import semantic_problems + + +@dataclass(frozen=True) +class CompileResult: + """Absolute paths to the artifacts produced by ``compile_topology``.""" + + workspace: Path + topology_path: Path + inventory_path: Path + scenario_vms_path: Path + network_policy_path: Path + stages_path: Path + + +def _write_json(path: Path, text: str) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + atomic_write_text(text, path) + + +def compile_topology( + topology: Topology, + *, + workspace: Path, + catalog: Catalog, + reserved: ReservedIndex, +) -> CompileResult: + """Compile *topology* into deploy artifacts under *workspace*. + + :raises CompileError: on any semantic, allocation, or segmentation failure. + """ + # 1. semantics + problems = semantic_problems(topology, catalog) + if problems: + raise CompileError("topology has semantic errors: " + "; ".join(problems)) + + # 2. allocation (octet rule, duplicates, cross-scenario collisions) + report = validate_allocation(topology, reserved) + if report.errors: + raise CompileError("allocation errors: " + "; ".join(report.errors)) + + # 3. resolve the network policy + compile its rule table + policy = catalog.resolve_network_policy(topology.network_policy.template) + version = catalog.resolved_version("network_policies", policy.id) + compiled_policy = _netpol.compile_network_policy(topology, policy, version=version) + + # 4. segmentation linter — fail closed + lint = _netpol.lint_segmentation(compiled_policy, policy, topology) + if lint: + raise CompileError("network segmentation linter: " + "; ".join(lint)) + + # 5. build artifacts + workspace = Path(workspace) + project = workspace / "project" + inv_dir = workspace / "inventory" + + topology_path = project / "topology.json" + inventory_path = inv_dir / "hosts.yml" + scenario_vms_path = project / "manifest" / "scenario_vms.json" + network_policy_path = project / "network_policy.json" + stages_path = project / "stages.json" + + dump_topology(topology, topology_path) + _write_json(inventory_path, _inventory.build_inventory_yaml(topology)) + _write_json(scenario_vms_path, _scenario_vms.build_scenario_vms_json(topology)) + _write_json(network_policy_path, compiled_policy.model_dump_json(indent=2) + "\n") + _write_json(stages_path, _stages.build_stages_json(topology, catalog)) + + return CompileResult( + workspace=workspace, + topology_path=topology_path, + inventory_path=inventory_path, + scenario_vms_path=scenario_vms_path, + network_policy_path=network_policy_path, + stages_path=stages_path, + ) diff --git a/r42playbooks/core/compiler/inventory.py b/r42playbooks/core/compiler/inventory.py new file mode 100644 index 00000000..b138b3cc --- /dev/null +++ b/r42playbooks/core/compiler/inventory.py @@ -0,0 +1,49 @@ +"""Compile a topology into an Ansible YAML inventory (hosts.yml). + +Reproduces range42's nested layout:: + + all.children.range42_infrastructure.children..hosts.r42. + +Only the scenario's VM groups are emitted; the ``proxmox`` / ``proxmox-cli`` +host groups are workspace-level (provided by the deployer base inventory), so +the host-level network-policy play targets those, merged at deploy time. + +Group names come from each box's validated ``inventory_group`` (snake_case); +host keys are ``r42.``. Emitted via ``yaml.safe_dump`` — topology +strings are data, never templated, so there is no SSTI surface. +""" + +import yaml + +from r42playbooks.core.models import Topology + + +def build_inventory(topology: Topology) -> dict: + """Return the inventory as a plain dict (groups -> hosts -> host vars).""" + groups: dict[str, dict] = {} + zone_role = {z.name: z.role for z in topology.zones} + + for box in topology.boxes: + group = groups.setdefault(box.inventory_group, {"hosts": {}}) + group["hosts"][f"r42.{box.vm_name}"] = { + "ansible_host": box.ip, + "r42_vm_id": box.vm_id, + "r42_vm_name": box.vm_name, + "r42_zone": box.zone, + "r42_role": zone_role.get(box.zone), + } + + return { + "all": { + "children": { + "range42_infrastructure": { + "children": {name: groups[name] for name in sorted(groups)}, + }, + }, + }, + } + + +def build_inventory_yaml(topology: Topology) -> str: + """Serialize the inventory as deterministic YAML.""" + return yaml.safe_dump(build_inventory(topology), sort_keys=True, default_flow_style=False) diff --git a/r42playbooks/core/compiler/network_policy.py b/r42playbooks/core/compiler/network_policy.py new file mode 100644 index 00000000..66784b65 --- /dev/null +++ b/r42playbooks/core/compiler/network_policy.py @@ -0,0 +1,434 @@ +"""Compile a symbolic network policy + topology bindings into ordered FORWARD rules. + +iptables FORWARD is first-match-wins, so rule ORDER is correctness, not style. +Each emitted rule gets a deterministic ``weight`` from a fixed band table; the +final list is stable-sorted by (weight, source, destination, proto, port, jump) +so identical inputs yield byte-identical output. The deploy playbook (Plan B) +flush-rebuilds a dedicated ``R42-FORWARD`` chain from this list in order, which +is naturally idempotent and never touches the host INPUT chain (SSH stays up). + +Two compile paths exist: + - ``compile_network_policy(topology, policy, version)`` — original path, uses + the Topology model with explicit zone/subnet bindings and overrides. + - ``compile_network_policy_from_alloc(alloc, policy, version)`` — generator + path, resolves zone→subnet from Allocation.subnets by matching names. + Used by render.py to emit ``05_network_isolation/`` when spec.network_policy + is set. +""" + +import ipaddress +from typing import TYPE_CHECKING, Callable, Literal + +from pydantic import BaseModel, ConfigDict, Field + +from r42playbooks.core import constants as C +from r42playbooks.core.catalog_models import MatrixRule, NetworkPolicyTemplate, PortSpec +from r42playbooks.core.errors import CompileError +from r42playbooks.core.models import Topology +from r42playbooks.core.validate import zone_bridge_map, zone_subnet_map + +if TYPE_CHECKING: + from r42playbooks.core.allocate import Allocation + +# weight bands — lower weight is evaluated earlier in FORWARD +W_ESTABLISHED = 0 +W_SERVICE_ACCEPT = 100 +W_ZONE_ACCEPT = 200 +W_INTRA = 300 +W_ZONE_DROP = 500 +W_AIRGAP = 600 +# W_EGRESS_ACCEPT: used when dst resolves to None (wan/unbound zone). +# Placed AFTER W_ZONE_DROP (500) and W_AIRGAP (600) so explicit zone DROPs +# and airgap rules fire first, then remaining traffic is allowed to egress. +# This enables deny-by-default with explicit internet-egress ACCEPT rows +# without shadowing cross-zone DROP rules. +W_EGRESS_ACCEPT = 700 +W_DEFAULT = 900 + +_ACTION_JUMP = {"accept": "ACCEPT", "drop": "DROP", "reject": "REJECT"} +_SVC = "svc:" + + +class CompiledRule(BaseModel): + """A single iptables FORWARD rule, fully resolved to concrete values.""" + + model_config = ConfigDict(extra="forbid") + + weight: int + chain: Literal["FORWARD"] = "FORWARD" + proto: Literal["tcp", "udp", "icmp", "all"] = "all" + source: str | None = None + destination: str | None = None + destination_port: str | None = Field(default=None, pattern=C.PORT_SPEC_RE.pattern) + in_interface: str | None = Field(default=None, pattern=C.IFACE_RE.pattern) + out_interface: str | None = Field(default=None, pattern=C.IFACE_RE.pattern) + ctstate: str | None = None + jump: Literal["ACCEPT", "DROP", "REJECT"] + comment: str = Field(max_length=200) + + +class CompiledNetworkPolicy(BaseModel): + """The ordered FORWARD rule table produced from a policy + topology.""" + + model_config = ConfigDict(extra="forbid") + + chain: Literal["FORWARD"] = "FORWARD" + policy_id: str + policy_version: str + wan_interface: str + rules: list[CompiledRule] = Field(default_factory=list) + + +def _sort_key(r: CompiledRule) -> tuple: + return (r.weight, r.source or "", r.destination or "", + r.in_interface or "", r.proto, r.destination_port or "", r.jump) + + +def _port_str(p: PortSpec) -> str | None: + if p.port is None: + return None + return f"{p.port}:{p.port_end}" if p.port_end else str(p.port) + + +def _param(policy: NetworkPolicyTemplate, topology: Topology, key: str, default): + """Resolve a policy param: topology override wins over template default.""" + return topology.network_policy.overrides.get(key, policy.params.get(key, default)) + + +def _require_ip(value, what: str) -> str: + """Validate that *value* is a concrete IPv4/IPv6 address, else CompileError.""" + try: + ipaddress.ip_address(str(value)) + except ValueError: + raise CompileError(f"{what} must be a valid IP address, got {value!r}") from None + return str(value) + + +def compile_network_policy( + topology: Topology, policy: NetworkPolicyTemplate, *, version: str +) -> CompiledNetworkPolicy: + """Compile *policy* against *topology*'s zone/subnet bindings into FORWARD rules.""" + zsubnet = zone_subnet_map(topology) + zbridge = zone_bridge_map(topology) + services = {s.name: s for s in policy.services} + wan_interface = str(_param(policy, topology, "wan_interface", "vmbr0")) + if not C.IFACE_RE.fullmatch(wan_interface): + raise CompileError(f"invalid wan_interface: {wan_interface!r}") + defaults = policy.defaults + + params_fn: Callable[[str, object], object] = lambda k, d: _param(policy, topology, k, d) + return _compile_rules( + policy=policy, version=version, zsubnet=zsubnet, zbridge=zbridge, + services=services, wan_interface=wan_interface, defaults=defaults, + params_fn=params_fn, + ) + + +def compile_network_policy_from_alloc( + alloc: "Allocation", + policy: NetworkPolicyTemplate, + *, + version: str, + wan_interface: str = "vmbr0", +) -> CompiledNetworkPolicy: + """Compile *policy* against *alloc*'s subnets into FORWARD rules. + + Maps policy zone names to subnets by matching ``zone.name == subnet.name``. + Zones marked ``wan: true`` are not mapped to a subnet — they are represented + by *wan_interface*. Zones with no matching subnet in the layout are silently + skipped (the compiled policy is narrower but still valid). + + Service IPs are resolved from ``policy.params`` (no topology-level overrides). + """ + if not C.IFACE_RE.fullmatch(wan_interface): + raise CompileError(f"invalid wan_interface: {wan_interface!r}") + + subnet_by_name = {s.name: s for s in alloc.subnets} + zsubnet: dict[str, str] = {} + zbridge: dict[str, str] = {} + for zone in policy.zones: + if zone.wan: + continue + subnet = subnet_by_name.get(zone.name) + if subnet is None: + continue + zsubnet[zone.name] = subnet.cidr + zbridge[zone.name] = subnet.bridge + + services = {s.name: s for s in policy.services} + defaults = policy.defaults + params_fn: Callable[[str, object], object] = lambda k, d: policy.params.get(k, d) + return _compile_rules( + policy=policy, version=version, zsubnet=zsubnet, zbridge=zbridge, + services=services, wan_interface=wan_interface, defaults=defaults, + params_fn=params_fn, + ) + + +def _compile_rules( + *, + policy: NetworkPolicyTemplate, + version: str, + zsubnet: dict[str, str], + zbridge: dict[str, str], + services: dict, + wan_interface: str, + defaults, + params_fn: Callable[[str, object], object], +) -> CompiledNetworkPolicy: + """Shared rule-building logic for both compile paths.""" + rules: list[CompiledRule] = [] + + # band 0 — return traffic + if defaults.accept_established_related: + rules.append(CompiledRule( + weight=W_ESTABLISHED, ctstate="ESTABLISHED,RELATED", jump="ACCEPT", + comment="r42: allow established", + )) + + # explicit matrix rules + for mr in policy.matrix: + rules.extend(_compile_matrix_rule(mr, zsubnet, services, params_fn)) + + # band 300 — intra-zone accept + if defaults.allow_intra_zone: + for zname, cidr in sorted(zsubnet.items()): + rules.append(CompiledRule( + weight=W_INTRA, source=cidr, destination=cidr, jump="ACCEPT", + comment=f"r42: intra-zone {zname}", + )) + + # band 600 — air-gap (zone bridge -> wan drop) + for zname in defaults.airgap_zones: + bridge = zbridge.get(zname) + if bridge is None: + continue + rules.append(CompiledRule( + weight=W_AIRGAP, in_interface=bridge, out_interface=wan_interface, + source=zsubnet.get(zname), jump="DROP", + comment=f"r42: air-gap {zname}", + )) + + # band 900 — terminal default + rules.append(CompiledRule( + weight=W_DEFAULT, jump=_ACTION_JUMP[defaults.default_action], + comment=f"r42: default {defaults.default_action}", + )) + + rules.sort(key=_sort_key) + return CompiledNetworkPolicy( + policy_id=policy.id, policy_version=version, + wan_interface=wan_interface, rules=rules, + ) + + +def _compile_matrix_rule( + mr: MatrixRule, + zsubnet: dict[str, str], + services: dict, + params_fn: Callable[[str, object], object], +) -> list[CompiledRule]: + jump = _ACTION_JUMP[mr.action] + src = None if mr.src == "*" else zsubnet.get(mr.src) + comment = f"r42: {mr.comment}" if mr.comment else f"r42: {mr.src}->{mr.dst}" + + # service destination (svc:) -> one rule per service port + if mr.dst.startswith(_SVC): + svc_name = mr.dst[len(_SVC):] + svc = services.get(svc_name) + if svc is None: + raise CompileError(f"matrix references unknown service {svc_name!r}") + # a missing/invalid service IP would silently broaden the rule to "any dest" + svc_ip = _require_ip(params_fn(f"{svc_name}_ip", None), + f"service {svc_name!r} ip ({svc_name}_ip)") + out: list[CompiledRule] = [] + for p in (mr.ports or svc.ports): + out.append(CompiledRule( + weight=W_SERVICE_ACCEPT if mr.action == "accept" else W_ZONE_DROP, + proto=p.proto, source=src, destination=svc_ip, + destination_port=_port_str(p), jump=jump, comment=comment, + )) + return out + + # zone destination (dst=None when zone is wan/unbound — use egress band) + dst = zsubnet.get(mr.dst) + if mr.action == "accept": + weight = W_EGRESS_ACCEPT if dst is None else W_ZONE_ACCEPT + else: + weight = W_ZONE_DROP + if mr.ports: + return [CompiledRule(weight=weight, proto=p.proto, source=src, destination=dst, + destination_port=_port_str(p), jump=jump, comment=comment) + for p in mr.ports] + return [CompiledRule(weight=weight, source=src, destination=dst, jump=jump, + comment=comment)] + + +def lint_segmentation( + compiled: CompiledNetworkPolicy, policy: NetworkPolicyTemplate, topology: Topology +) -> list[str]: + """Assert the compiled rule table is safe. Returns problems ([] == safe).""" + problems: list[str] = [] + rules = compiled.rules + + # 1. FORWARD only — host INPUT (SSH/mgmt) must never be touched + if any(r.chain != "FORWARD" for r in rules): + problems.append("non-FORWARD rule present (host INPUT must not be modified)") + + drop_weights = [r.weight for r in rules if r.jump in ("DROP", "REJECT")] + first_drop = min(drop_weights) if drop_weights else None + + # 2. established accept must exist and precede any drop + if policy.defaults.accept_established_related: + est = [r for r in rules if r.ctstate and "ESTABLISHED" in r.ctstate and r.jump == "ACCEPT"] + if not est: + problems.append("missing ESTABLISHED,RELATED ACCEPT") + elif first_drop is not None and min(r.weight for r in est) > first_drop: + problems.append("ESTABLISHED accept does not precede DROP rules") + + # 3. no ACCEPT may shadow an earlier-evaluated DROP/REJECT for overlapping + # traffic. Considers ALL compiled drops (including wildcard-source ones), + # not just exact matrix zone-pairs — this closes the src="*" bypass. + # The terminal default catch-all (no match fields) is excluded: specific + # ACCEPTs are *supposed* to precede it. Interface-scoped rules (air-gap) + # only conflict with other interface-scoped rules, so they are matched + # on interface too, avoiding false positives against CIDR/service rules. + accepts = [r for r in rules if r.jump == "ACCEPT"] + for d in rules: + if d.jump not in ("DROP", "REJECT") or _is_catch_all(d): + continue + for a in accepts: + if a.weight < d.weight and _shadows(a, d): + problems.append( + f"ACCEPT (w{a.weight}) shadows DROP (w{d.weight}) for " + f"{a.source or '*'}->{a.destination or a.out_interface or '*'} " + f"({d.comment})" + ) + break + + # also assert every explicit zone->zone deny actually produced a DROP rule + zsubnet = zone_subnet_map(topology) + for mr in policy.matrix: + if mr.action not in ("drop", "reject") or mr.dst.startswith(_SVC) or mr.src == "*": + continue + src, dst = zsubnet.get(mr.src), zsubnet.get(mr.dst) + if src is None or dst is None: + continue + if not any(r.source == src and r.destination == dst and r.jump in ("DROP", "REJECT") + for r in rules): + problems.append(f"deny {mr.src}->{mr.dst} produced no DROP rule") + + # 4. each air-gap zone must exist in the topology AND have a wan-drop rule + zbridge = zone_bridge_map(topology) + for zname in policy.defaults.airgap_zones: + bridge = zbridge.get(zname) + if bridge is None: + problems.append(f"air-gap zone {zname!r} not bound in topology — air-gap absent") + elif not any(r.in_interface == bridge and r.out_interface and r.jump == "DROP" + for r in rules): + problems.append(f"air-gap zone {zname!r} missing wan DROP") + + # 5. terminal rule must be the deny catch-all when default denies + if policy.defaults.default_action in ("drop", "reject"): + if not rules or rules[-1].jump not in ("DROP", "REJECT"): + problems.append("default-deny is not the terminal rule") + + return problems + + +def lint_compiled_policy_from_alloc( + compiled: CompiledNetworkPolicy, + policy: NetworkPolicyTemplate, + alloc: "Allocation", +) -> list[str]: + """Topology-free lint for the generator (alloc) path. Returns problems ([] == safe). + + Equivalent to ``lint_segmentation`` but derives zone→subnet/bridge maps from + ``alloc.subnets`` instead of a ``Topology`` object (not available in render path). + """ + subnet_by_name = {s.name: s for s in alloc.subnets} + zsubnet: dict[str, str] = {} + zbridge: dict[str, str] = {} + for zone in policy.zones: + if zone.wan: + continue + subnet = subnet_by_name.get(zone.name) + if subnet is None: + continue + zsubnet[zone.name] = subnet.cidr + zbridge[zone.name] = subnet.bridge + + problems: list[str] = [] + rules = compiled.rules + + if any(r.chain != "FORWARD" for r in rules): + problems.append("non-FORWARD rule present (host INPUT must not be modified)") + + drop_weights = [r.weight for r in rules if r.jump in ("DROP", "REJECT")] + first_drop = min(drop_weights) if drop_weights else None + + if policy.defaults.accept_established_related: + est = [r for r in rules if r.ctstate and "ESTABLISHED" in r.ctstate and r.jump == "ACCEPT"] + if not est: + problems.append("missing ESTABLISHED,RELATED ACCEPT") + elif first_drop is not None and min(r.weight for r in est) > first_drop: + problems.append("ESTABLISHED accept does not precede DROP rules") + + accepts = [r for r in rules if r.jump == "ACCEPT"] + for d in rules: + if d.jump not in ("DROP", "REJECT") or _is_catch_all(d): + continue + for a in accepts: + if a.weight < d.weight and _shadows(a, d): + problems.append( + f"ACCEPT (w{a.weight}) shadows DROP (w{d.weight}) for " + f"{a.source or '*'}->{a.destination or a.out_interface or '*'} " + f"({d.comment})" + ) + break + + for mr in policy.matrix: + if mr.action not in ("drop", "reject") or mr.dst.startswith(_SVC) or mr.src == "*": + continue + src, dst = zsubnet.get(mr.src), zsubnet.get(mr.dst) + if src is None or dst is None: + continue + if not any(r.source == src and r.destination == dst and r.jump in ("DROP", "REJECT") + for r in rules): + problems.append(f"deny {mr.src}->{mr.dst} produced no DROP rule") + + for zname in policy.defaults.airgap_zones: + bridge = zbridge.get(zname) + if bridge is None: + problems.append(f"air-gap zone {zname!r} not bound in alloc — air-gap absent") + elif not any(r.in_interface == bridge and r.out_interface and r.jump == "DROP" + for r in rules): + problems.append(f"air-gap zone {zname!r} missing wan DROP") + + if policy.defaults.default_action in ("drop", "reject"): + if not rules or rules[-1].jump not in ("DROP", "REJECT"): + problems.append("default-deny is not the terminal rule") + + return problems + + +def _is_catch_all(rule: CompiledRule) -> bool: + """True for a rule with no match fields — the intended terminal default.""" + return (rule.source is None and rule.destination is None + and rule.in_interface is None and rule.out_interface is None + and rule.destination_port is None and rule.ctstate is None) + + +def _shadows(accept: CompiledRule, drop: CompiledRule) -> bool: + """True if *accept* would match traffic the later *drop* intends to block. + + A None match field means "any" for that dimension. Interface-scoped drops + (air-gap) only conflict with interface-scoped accepts on the same interface, + so they never false-positive against CIDR/service ACCEPT rules. + """ + if drop.in_interface or drop.out_interface: + return (accept.in_interface == drop.in_interface + and accept.out_interface == drop.out_interface) + src_overlap = drop.source is None or accept.source == drop.source + dst_overlap = drop.destination is None or accept.destination == drop.destination + return src_overlap and dst_overlap diff --git a/r42playbooks/core/compiler/scenario_vms.py b/r42playbooks/core/compiler/scenario_vms.py new file mode 100644 index 00000000..3de3f499 --- /dev/null +++ b/r42playbooks/core/compiler/scenario_vms.py @@ -0,0 +1,45 @@ +"""Compile a topology into manifest/scenario_vms.json (the demo_lab shape). + +range42-context reads this manifest to know a scenario's VMs (e.g. to flush +their SSH known_hosts on redeploy). Role comes from the box's zone; bridge +comes from the zone's subnet. Templates (the 9xxx clone sources) are not part +of a topology and are emitted as an empty list for now. +""" + +import json + +from r42playbooks.core.models import Topology +from r42playbooks.core.validate import zone_bridge_map + + +def build_scenario_vms(topology: Topology) -> dict: + """Return the scenario_vms manifest as a plain dict.""" + zone_role = {z.name: z.role for z in topology.zones} + zbridge = zone_bridge_map(topology) + + vms = sorted( + ( + { + "vm_id": box.vm_id, + "vm_name": box.vm_name, + "ip": box.ip, + "role": zone_role.get(box.zone), + "bridge": zbridge.get(box.zone), + } + for box in topology.boxes + ), + key=lambda v: v["vm_id"], + ) + + return { + "scenario": topology.scenario, + "version": 2, + "description": topology.description, + "vms": vms, + "templates": [], + } + + +def build_scenario_vms_json(topology: Topology) -> str: + """Serialize the manifest as deterministic JSON.""" + return json.dumps(build_scenario_vms(topology), indent=2, sort_keys=True) + "\n" diff --git a/r42playbooks/core/compiler/services.py b/r42playbooks/core/compiler/services.py new file mode 100644 index 00000000..5a46111e --- /dev/null +++ b/r42playbooks/core/compiler/services.py @@ -0,0 +1,118 @@ +"""Post-allocation services wiring pass. + +Resolves ``ScenarioSpec.services.apt`` after IP addresses are known: + - injects ``software.configure.apt_mirror_client`` into every wired client box; + - for mirror mode: auto-detects which suite flags to enable on the server + by inspecting client image distro/codename, then merges them into the + server's ``software.install.apt_mirror`` attachment params. + +Pure: returns a new Allocation (never mutates the input). +""" + +import dataclasses +from typing import TYPE_CHECKING + +from r42playbooks.core.allocate import Allocation, AllocatedBox +from r42playbooks.core.errors import CompileError +from r42playbooks.core.models import Attachment + +if TYPE_CHECKING: + from r42playbooks.core.catalog import Catalog + from r42playbooks.core.spec import ScenarioSpec + +_APT_MIRROR_ROLE = "software.install.apt_mirror" +_APT_CLIENT_ROLE = "software.configure.apt_mirror_client" + + +def resolve_services( + alloc: Allocation, + spec: "ScenarioSpec", + catalog: "Catalog", +) -> Allocation: + """Apply services wiring to *alloc* and return the patched allocation. + + If ``spec.services`` is None (the common case) the original allocation is + returned unchanged — zero overhead for scenarios that don't use services. + + :raises CompileError: the declared server box template is not in the scenario. + """ + if spec.services is None or spec.services.apt is None: + return alloc + + apt_svc = spec.services.apt + boxes: list[AllocatedBox] = list(alloc.boxes) + + # Locate the server box (first match by box_template id). + server_idx = next( + (i for i, b in enumerate(boxes) if b.box_template == apt_svc.box), + None, + ) + if server_idx is None: + raise CompileError( + f"services.apt.box {apt_svc.box!r} is not present in the scenario — " + f"add a box with template: {apt_svc.box!r} to the scenario spec" + ) + server = boxes[server_idx] + + # Determine client box indices. + if apt_svc.wire_to == "all": + client_indices = [i for i in range(len(boxes)) if i != server_idx] + else: + wire_set = set(apt_svc.wire_to) + client_indices = [ + i for i, b in enumerate(boxes) + if b.box_template in wire_set and i != server_idx + ] + + if not client_indices: + return alloc + + # Mirror mode: derive suite flags from client images and patch server attachment. + if apt_svc.mode == "mirror" and client_indices: + needed: dict[str, bool] = {} + for i in client_indices: + img_def = catalog.images.get(boxes[i].image) + if img_def is None: + continue + needed[f"apt_mirror_{img_def.distro}_{img_def.codename}"] = True + + if needed: + new_atts = list(server.attachments) + patched = False + for j, att in enumerate(new_atts): + if att.kind == "role" and att.catalog_ref == _APT_MIRROR_ROLE: + # Merge: spec-authored params take precedence over auto-detected flags. + merged = {**needed, **att.params} + new_atts[j] = att.model_copy(update={"params": merged}) + patched = True + break + if patched: + boxes[server_idx] = dataclasses.replace( + server, attachments=tuple(new_atts) + ) + + # Build and inject client attachment using the resolved server IP. + if client_indices: + if apt_svc.mode == "proxy": + client_params: dict = { + "apt_mirror_enabled": True, + "apt_proxy_url": f"http://{server.ip}:3142", + } + else: + client_params = { + "apt_mirror_enabled": True, + "apt_mirror_airgapped": True, + "apt_mirror_vm_ip": server.ip, + "apt_mirror_http_port": 80, + } + + client_att = Attachment( + kind="role", + catalog_ref=_APT_CLIENT_ROLE, + params=client_params, + ) + for i in client_indices: + b = boxes[i] + boxes[i] = dataclasses.replace(b, attachments=b.attachments + (client_att,)) + + return dataclasses.replace(alloc, boxes=tuple(boxes)) diff --git a/r42playbooks/core/compiler/stages.py b/r42playbooks/core/compiler/stages.py new file mode 100644 index 00000000..b87709b9 --- /dev/null +++ b/r42playbooks/core/compiler/stages.py @@ -0,0 +1,71 @@ +"""Compile a topology into stages.json — the per-zone box/attachment dispatch. + +This is the data the ``_universal`` Plan B playbook iterates: for each zone (in +deploy order), the boxes to create and, per box, the ordered catalog +attachments to apply. Each box's effective attachments are the box_template's +``default_attachments`` first, then the box's own ``attachments``, de-duplicated +by (kind, catalog_ref) keeping first occurrence. +""" + +import json + +from r42playbooks.core.catalog import Catalog +from r42playbooks.core.models import Box, Topology + +# deploy order for zone roles (admin infra before students before ctf targets) +_ROLE_ORDER = {"template": 0, "admin": 1, "student": 2, "team": 3, "ctf": 4} + + +def _effective_attachments(box: Box, catalog: Catalog) -> list[dict]: + template = catalog.box_templates.get(box.box_template) + merged: list[dict] = [] + seen: set[tuple[str, str]] = set() + source = list(template.default_attachments) if template else [] + source += list(box.attachments) + for att in source: + key = (att.kind, att.catalog_ref) + if key in seen: + continue + seen.add(key) + merged.append({"kind": att.kind, "catalog_ref": att.catalog_ref, "params": att.params}) + return merged + + +def build_stages(topology: Topology, catalog: Catalog) -> dict: + """Return the stages dispatch as a plain dict.""" + zones_by_name = {z.name: z for z in topology.zones} + boxes_by_zone: dict[str, list[Box]] = {} + for box in topology.boxes: + boxes_by_zone.setdefault(box.zone, []).append(box) + + ordered_zone_names = sorted( + boxes_by_zone, + key=lambda zn: (_ROLE_ORDER.get(zones_by_name[zn].role, 99), zn) + if zn in zones_by_name else (99, zn), + ) + + zones_out = [] + for zname in ordered_zone_names: + zone = zones_by_name.get(zname) + zones_out.append({ + "name": zname, + "role": zone.role if zone else None, + "boxes": [ + { + "vm_name": box.vm_name, + "vm_id": box.vm_id, + "ip": box.ip, + "box_template": box.box_template, + "inventory_group": box.inventory_group, + "attachments": _effective_attachments(box, catalog), + } + for box in sorted(boxes_by_zone[zname], key=lambda b: b.vm_id) + ], + }) + + return {"scenario": topology.scenario, "zones": zones_out} + + +def build_stages_json(topology: Topology, catalog: Catalog) -> str: + """Serialize the stages dispatch as deterministic JSON.""" + return json.dumps(build_stages(topology, catalog), indent=2, sort_keys=True) + "\n" diff --git a/r42playbooks/core/constants.py b/r42playbooks/core/constants.py new file mode 100644 index 00000000..96cc9dab --- /dev/null +++ b/r42playbooks/core/constants.py @@ -0,0 +1,143 @@ +"""Regexes, limits, and naming rules shared across the core. + +Single source of truth for the patterns that several modules validate against, +so the scenario-name rule (must match the backend resolver) and the security +deny-list never drift between models, the compiler, and the catalog loader. +""" + +import re + +# Scenario name — mirrors range42-backend-api resolve_scenarios_playbook(): +# /scenarios//main.yml, name segments are [A-Za-z0-9_-], +# slash-joined, NO dots, no leading/trailing slash. +SCENARIO_NAME_RE = re.compile(r"^[A-Za-z0-9_-]+(?:/[A-Za-z0-9_-]+)*$") + +# vm_name / inventory host leaf — lowercase kebab, bounded length. +VM_NAME_RE = re.compile(r"^[a-z0-9-]{1,40}$") + +# Ansible inventory group — lowercase snake (used by the legacy Box model). +INVENTORY_GROUP_RE = re.compile(r"^[a-z0-9_]+$") + +# Catalog reference — role/stack ids MAY contain dots (e.g. software.install.wazuh), +# unlike scenario names. Kept deliberately separate from SCENARIO_NAME_RE. +CATALOG_REF_RE = re.compile(r"^[A-Za-z0-9_.-]+(?:/[A-Za-z0-9_.-]+)*$") + +# Proxmox bridge interface. +BRIDGE_RE = re.compile(r"^vmbr[0-9]+$") + +# IPv4 address and CIDR (syntactic; semantic checks live in the compiler). +IPV4_RE = re.compile(r"^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$") +IPV4_CIDR_RE = re.compile(r"^(?:[0-9]{1,3}\.){3}[0-9]{1,3}/[0-9]{1,2}$") + +# Proxmox node name — requires a leading alphanumeric (no empty, no leading dash +# which a subprocess could read as a flag), bounded length per RFC-952/Proxmox. +PROXMOX_NODE_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9-]{0,62}$") + +# Network-policy matrix endpoints (catalog-authored): src is a zone name or "*"; +# dst is a zone name, "svc:", or "*". +MATRIX_SRC_RE = re.compile(r"^(\*|[a-z0-9-]{1,40})$") +MATRIX_DST_RE = re.compile(r"^(\*|svc:[a-z0-9-]{1,40}|[a-z0-9-]{1,40})$") + +# Interface name for compiled iptables rules (bridges, uplink NICs). +IFACE_RE = re.compile(r"^[A-Za-z0-9._-]{1,32}$") +# iptables destination port or inclusive range, e.g. "1514" or "1514:1515". +PORT_SPEC_RE = re.compile(r"^[0-9]{1,5}(:[0-9]{1,5})?$") + +# vm_id bounds: 4-digit ids in the project's allocated band. +VM_ID_MIN = 1000 +VM_ID_MAX = 9999 + +# Upper bound on how many VMs a single composed box may expand to (count>1). +# Bounds runaway allocation from a hostile/typo'd scenario.r42.yml. +BOX_COUNT_MAX = 99 + +# --- allocation conventions (S4) ------------------------------------------- +# vm_id = band*1000 + octet. Bands 1..8 for placed boxes; 9xxx is the template band. +VM_ID_BAND_MIN = 1 +VM_ID_BAND_MAX = 8 +# Highest usable host octet in a /24 (last is broadcast-ish; stay <= 254). +HOST_OCTET_MAX = 254 +# Zero-pad width for expanded replica names (vuln-box-00..); supports BOX_COUNT_MAX. +REPLICA_PAD = 2 + +# Catalog template id — dotless kebab (distinct from dotted role refs and from +# the scenario-name rule). Used for directory names under 05_topology_layer/. +TEMPLATE_ID_RE = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$") + +# Base-image set name — ``_`` (e.g. ubuntu_noble, debian_trixie). +# Matches the 01_init_proxmox/.../templates// dir a box clones from. +IMAGE_RE = re.compile(r"^[a-z0-9]+(?:_[a-z0-9]+)+$") + +# Version directory under a template id, e.g. "v1.0.0". +VERSION_DIR_RE = re.compile(r"^v(\d+)\.(\d+)\.(\d+)$") + +# Catalog layout: numbered layers (foundational → composed). +# 01_image_layer — base VM images (ubuntu_noble, debian_trixie …) +# 02_ansible_layer — Ansible roles +# 03_container_layer — Docker stacks + LXC +# 05_topology_layer — box_templates / subnet_layouts / network_policies +IMAGE_LAYER_DIR = "01_image_layer" +TOPOLOGY_LAYER_DIR = "05_topology_layer" +CATEGORY_BOX_TEMPLATES = "box_templates" +CATEGORY_NETWORK_POLICIES = "network_policies" +CATEGORY_SUBNET_LAYOUTS = "subnet_layouts" + +# Catalog layers scanned for pickable, name-referenced modules (S3). +# roles -> 02_ansible_layer/**/roles/../ +# containers -> 03_container_layer/docker/_ctf/**/ (dir holding a compose file) +ANSIBLE_LAYER_DIR = "02_ansible_layer" +CONTAINER_LAYER_DIR = "03_container_layer" +CTF_REL_DIR = "docker/_ctf" +ROLES_DIR_NAME = "roles" +COMPOSE_FILENAMES: tuple[str, ...] = ( + "docker-compose.yml", "docker-compose.yaml", "compose.yml", "compose.yaml", +) + +# Security deny-list: substrings that must never appear in a free-text topology +# field. Blocks Jinja/SSTI (`{{ }}`, `{% %}`, `${`), shell metacharacters, +# path traversal, and argv-flag injection. Fields are rejected, never sanitized. +DENYLIST_SUBSTRINGS: tuple[str, ...] = ( + "{{", "}}", "{%", "%}", "${", "`", ";", "|", "&", "\n", "\r", "\x00", "..", +) + + +def violates_denylist(value: str) -> bool: + """Return True if *value* contains any denied substring or a leading dash.""" + if value.startswith("-"): + return True + return any(token in value for token in DENYLIST_SUBSTRINGS) + + +def reject_injection(value: str) -> str: + """Field-validator helper: raise ValueError if *value* is deny-listed.""" + if violates_denylist(value): + raise ValueError("value contains a forbidden character or pattern") + return value + + +def reject_injection_nested(obj): + """Recursively deny-list-check every string key/value in a dict/list/scalar. + + Used for free-form ``params`` / ``overrides`` dicts whose values can flow + into Ansible variables (Jinja2 render surface) or compiled rule fields. + """ + if isinstance(obj, str): + reject_injection(obj) + elif isinstance(obj, dict): + for key, val in obj.items(): + if isinstance(key, str): + reject_injection(key) + reject_injection_nested(val) + elif isinstance(obj, (list, tuple)): + for item in obj: + reject_injection_nested(item) + return obj + + +def octet_matches_vm_id(vm_id: int, ip: str) -> bool: + """Project rule: a single-subnet VM's vm_id last 3 digits == IP last octet. + + Enforced as an error for newly authored boxes; legacy ``_reserved.json`` + rows that predate the rule are only warned about (handled in idalloc). + """ + return vm_id % 1000 == int(ip.rsplit(".", 1)[-1]) diff --git a/r42playbooks/core/errors.py b/r42playbooks/core/errors.py new file mode 100644 index 00000000..0459ab0d --- /dev/null +++ b/r42playbooks/core/errors.py @@ -0,0 +1,31 @@ +"""Framework-free exception hierarchy for the r42playbooks core. + +Consumers (backend-api, CLI, TUI) translate these into their own surface +(HTTP envelopes, exit codes, dialog text). The core never raises framework +exceptions such as fastapi.HTTPException. +""" + + +class TopologyError(Exception): + """Base class for all r42playbooks core errors.""" + + +class ValidationError(TopologyError): + """A topology or template failed semantic validation (beyond schema).""" + + +class CatalogNotFoundError(TopologyError): + """A referenced catalog template id/version could not be resolved.""" + + +class CompileError(TopologyError): + """Compilation failed (reservation conflict, segmentation invariant, etc.).""" + + +class ScenarioExistsError(TopologyError): + """The target ``scenarios//`` directory already exists. + + Raised by the renderer when ``overwrite=False`` (the default) so a generate + never silently clobbers an existing scenario. Callers pass ``overwrite=True`` + (CLI ``--force``) to replace it. + """ diff --git a/r42playbooks/core/extravars.py b/r42playbooks/core/extravars.py new file mode 100644 index 00000000..764eab9e --- /dev/null +++ b/r42playbooks/core/extravars.py @@ -0,0 +1,50 @@ +"""Build the extravars dict the ``_universal`` scenario consumes. + +The backend calls ``run_playbook_core(scenarios/_universal/main.yml, inventory, +extravars=...)``; ``_universal`` asserts ``r42_topology_path`` is set and points +at a real file. This builder emits ONLY the allow-listed ``r42_*`` keys — no +``ansible_*`` or arbitrary keys can leak into the playbook's variable space. +Identifier values are deny-list checked (no injection into templated vars). +""" + +from r42playbooks.core import constants as C +from r42playbooks.core.compiler import CompileResult +from r42playbooks.core.errors import ValidationError + +# the exact contract keys _universal/main.yml expects +_ALLOWED_KEYS = ( + "r42_topology_path", "r42_inventory_dir", "r42_deployment_id", + "r42_attempt_id", "r42_scope", "r42_team_id", +) + + +def _safe_id(name: str, value: str) -> str: + if C.violates_denylist(value): + raise ValidationError(f"{name} contains a forbidden character or pattern") + return value + + +def resolve_universal_extravars( + result: CompileResult, + *, + deployment_id: str, + attempt_id: str, + scope: str, + team_id: str | None = None, +) -> dict: + """Return the typed, allow-listed extravars for the ``_universal`` playbook.""" + extravars = { + "r42_topology_path": str(result.topology_path), + "r42_inventory_dir": str(result.workspace / "inventory"), + "r42_deployment_id": _safe_id("deployment_id", deployment_id), + "r42_attempt_id": _safe_id("attempt_id", attempt_id), + "r42_scope": _safe_id("scope", scope), + "r42_team_id": _safe_id("team_id", team_id) if team_id else "", + } + # defensive: guarantee no key escaped the allow-list (explicit, not assert — + # assert is stripped under `python -O`) + if set(extravars) != set(_ALLOWED_KEYS): + raise ValidationError( + f"extravars key mismatch: {set(extravars) ^ set(_ALLOWED_KEYS)}" + ) + return extravars diff --git a/r42playbooks/core/idalloc.py b/r42playbooks/core/idalloc.py new file mode 100644 index 00000000..4647ee67 --- /dev/null +++ b/r42playbooks/core/idalloc.py @@ -0,0 +1,132 @@ +"""vm_id / IP allocation checks against the global reservation registry. + +`scenarios/_reserved.json` is a JSONL file (one JSON object per line) that +records every vm_id/IP claimed across all scenarios. This module validates a +topology's boxes against it and against the project's allocation rules: + + - octet rule: a box's vm_id last 3 digits must equal its IP last octet + (enforced as an error for newly authored boxes); + - no duplicate vm_id / IP within the topology; + - no vm_id / IP collision with a *different* scenario already in the registry + (re-deploying the topology's own scenario is fine). + +Read-only and pure: claiming/writing reservations (with a file lock) is a +separate concern handled by the deploy side, not here. + +TOCTOU: ``validate_allocation`` reads the registry at compile time; there is an +inherent race between this check and the deploy-side reservation write. Callers +that compile concurrently must serialize the validate→deploy→record sequence +with an external lock (file lock / DB advisory lock) to avoid two scenarios +claiming the same vm_id/IP. +""" + +import json +from dataclasses import dataclass, field +from pathlib import Path + +from r42playbooks.core import constants as C +from r42playbooks.core.errors import TopologyError +from r42playbooks.core.models import Topology + + +@dataclass(frozen=True) +class ReservedIndex: + """Parsed view of a JSONL ``_reserved.json`` reservation registry.""" + + entries: tuple[dict, ...] + + @classmethod + def from_file(cls, path: Path) -> "ReservedIndex": + path = Path(path) + try: + text = path.read_text(encoding="utf-8") + except OSError as exc: + raise TopologyError(f"cannot read reservation file: {path}") from exc + rows: list[dict] = [] + for lineno, line in enumerate(text.splitlines(), start=1): + line = line.strip() + if not line: + continue + try: + rows.append(json.loads(line)) + except json.JSONDecodeError as exc: + raise TopologyError( + f"invalid JSON on line {lineno} of {path}" + ) from exc + return cls(entries=tuple(rows)) + + def used_vm_ids(self) -> set[int]: + return {int(e["vm_id"]) for e in self.entries if "vm_id" in e} + + def used_ips(self) -> set[str]: + return {str(e["ip"]) for e in self.entries if "ip" in e} + + def owner_of_vm_id(self, vm_id: int) -> str | None: + for e in self.entries: + if int(e.get("vm_id", -1)) == vm_id: + return e.get("scenario") + return None + + def owner_of_ip(self, ip: str) -> str | None: + for e in self.entries: + if str(e.get("ip", "")) == ip: + return e.get("scenario") + return None + + +@dataclass +class AllocationReport: + """Result of validate_allocation: human-readable errors + warnings.""" + + errors: list[str] = field(default_factory=list) + warnings: list[str] = field(default_factory=list) + + @property + def ok(self) -> bool: + return not self.errors + + +def validate_allocation(topology: Topology, reserved: ReservedIndex) -> AllocationReport: + """Validate a topology's vm_id/IP allocation. Returns a report (never raises).""" + report = AllocationReport() + scenario = topology.scenario + + seen_vm_ids: dict[int, str] = {} + seen_ips: dict[str, str] = {} + + for box in topology.boxes: + # octet rule (new boxes must comply) + if not C.octet_matches_vm_id(box.vm_id, box.ip): + report.errors.append( + f"box {box.vm_name}: octet rule violated — vm_id {box.vm_id} " + f"last 3 digits != IP last octet ({box.ip})" + ) + + # intra-topology duplicates + if box.vm_id in seen_vm_ids: + report.errors.append( + f"box {box.vm_name}: duplicate vm_id {box.vm_id} " + f"(also {seen_vm_ids[box.vm_id]})" + ) + else: + seen_vm_ids[box.vm_id] = box.vm_name + if box.ip in seen_ips: + report.errors.append( + f"box {box.vm_name}: duplicate IP {box.ip} (also {seen_ips[box.ip]})" + ) + else: + seen_ips[box.ip] = box.vm_name + + # cross-scenario collisions in the registry + owner = reserved.owner_of_vm_id(box.vm_id) + if owner is not None and owner != scenario: + report.errors.append( + f"box {box.vm_name}: vm_id {box.vm_id} reserved by scenario {owner!r}" + ) + ip_owner = reserved.owner_of_ip(box.ip) + if ip_owner is not None and ip_owner != scenario: + report.errors.append( + f"box {box.vm_name}: IP {box.ip} reserved by scenario {ip_owner!r}" + ) + + return report diff --git a/r42playbooks/core/io.py b/r42playbooks/core/io.py new file mode 100644 index 00000000..5f66a857 --- /dev/null +++ b/r42playbooks/core/io.py @@ -0,0 +1,66 @@ +"""Topology load/dump helpers — deterministic, atomic JSON IO. + +``dump_topology`` writes sorted-key, stable JSON so re-compiling an unchanged +topology yields byte-identical output (clean diffs, reproducible builds). +Writes are atomic (temp file + os.replace) to avoid half-written artifacts. +""" + +import json +import os +import tempfile +from pathlib import Path + +from pydantic import ValidationError as _PydanticValidationError + +from r42playbooks.core.errors import TopologyError +from r42playbooks.core.models import Topology + + +def load_topology(path: Path) -> Topology: + """Load and validate a topology.json from *path*. + + :raises TopologyError: if the file is missing or not valid JSON. + """ + path = Path(path) + try: + raw = path.read_text(encoding="utf-8") + except OSError as exc: + raise TopologyError(f"cannot read topology file: {path}") from exc + try: + data = json.loads(raw) + except json.JSONDecodeError as exc: + raise TopologyError(f"invalid JSON in topology file: {path}") from exc + try: + return Topology.model_validate(data) + except _PydanticValidationError as exc: + raise TopologyError(f"topology schema error in {path}: {exc}") from exc + + +def dumps_topology(topology: Topology) -> str: + """Serialize a Topology to canonical, sorted, newline-terminated JSON.""" + payload = topology.model_dump(mode="json") + return json.dumps(payload, indent=2, sort_keys=True, ensure_ascii=False) + "\n" + + +def atomic_write_text(text: str, path: Path) -> Path: + """Atomically write *text* to *path* (temp file + os.replace). Returns the path. + + Shared by every artifact writer in the core (topology JSON, scenario specs, + rendered scenario files) so atomicity lives in exactly one place. + """ + path = Path(path) + path.parent.mkdir(parents=True, exist_ok=True) + fd, tmp = tempfile.mkstemp(dir=path.parent, prefix=".r42-", suffix=".tmp") + try: + with os.fdopen(fd, "w", encoding="utf-8") as fh: + fh.write(text) + os.replace(tmp, path) + finally: + if os.path.exists(tmp): + os.unlink(tmp) + return path + + +def dump_topology(topology: Topology, path: Path) -> Path: + """Atomically write *topology* to *path* as canonical JSON. Returns the path.""" + return atomic_write_text(dumps_topology(topology), Path(path)) diff --git a/r42playbooks/core/models.py b/r42playbooks/core/models.py new file mode 100644 index 00000000..aef28d3b --- /dev/null +++ b/r42playbooks/core/models.py @@ -0,0 +1,104 @@ +"""Pydantic v2 models for topology.json — the authored source of truth. + +Schema-level validation only (shapes, patterns, the security deny-list). +Cross-field semantics (zone/subnet referential integrity, IP-in-subnet, +reservation uniqueness) live in the compiler/idalloc layer, not here. +""" + +from typing import Any, Literal + +from pydantic import BaseModel, ConfigDict, Field, field_validator + +from r42playbooks.core import constants as C + +_STRICT = ConfigDict(extra="forbid") + +# free-text injection guard (shared with catalog_models via constants) +_no_injection = C.reject_injection + + +class Attachment(BaseModel): + """A catalog item dispatched onto a box (role / container / gamification).""" + + model_config = _STRICT + + kind: Literal["role", "container", "gamification"] + catalog_ref: str = Field(pattern=C.CATALOG_REF_RE.pattern) + params: dict[str, Any] = Field(default_factory=dict) + + # CATALOG_REF_RE permits dots/slashes (role names, container paths) — so it + # also admits '..'. Deny-list-guard the ref: it is written verbatim as an + # Ansible role name / resolved against ANSIBLE_ROLES_PATH at deploy. + _guard_catalog_ref = field_validator("catalog_ref")(_no_injection) + _guard_params = field_validator("params")(C.reject_injection_nested) + + +class Subnet(BaseModel): + """A concrete L3 subnet bound to a Proxmox bridge.""" + + model_config = _STRICT + + name: str = Field(pattern=C.VM_NAME_RE.pattern) + cidr: str = Field(pattern=C.IPV4_CIDR_RE.pattern) + bridge: str = Field(pattern=C.BRIDGE_RE.pattern) + gateway: str | None = Field(default=None, pattern=C.IPV4_RE.pattern) + + _guard_name = field_validator("name")(_no_injection) + + +class Zone(BaseModel): + """A logical isolation zone mapped onto one subnet.""" + + model_config = _STRICT + + name: str = Field(pattern=C.VM_NAME_RE.pattern) + subnet: str = Field(pattern=C.VM_NAME_RE.pattern) # FK -> Subnet.name + role: Literal["admin", "ctf", "team", "student", "template"] + + _guard_name = field_validator("name")(_no_injection) + + +class Box(BaseModel): + """A single VM to create and configure.""" + + model_config = _STRICT + + vm_name: str = Field(pattern=C.VM_NAME_RE.pattern) + vm_id: int = Field(ge=C.VM_ID_MIN, le=C.VM_ID_MAX) + ip: str = Field(pattern=C.IPV4_RE.pattern) + zone: str = Field(pattern=C.VM_NAME_RE.pattern) # FK -> Zone.name + box_template: str = Field(pattern=C.CATALOG_REF_RE.pattern) + inventory_group: str = Field(pattern=C.INVENTORY_GROUP_RE.pattern) + attachments: list[Attachment] = Field(default_factory=list) + + _guard_vm_name = field_validator("vm_name")(_no_injection) + + +class NetworkPolicyRef(BaseModel): + """Reference to a catalog network-isolation policy template + overrides.""" + + model_config = _STRICT + + template: str = Field(pattern=C.CATALOG_REF_RE.pattern) + overrides: dict[str, Any] = Field(default_factory=dict) + + _guard_overrides = field_validator("overrides")(C.reject_injection_nested) + + +class Topology(BaseModel): + """Top-level authored topology — the source of truth the compiler expands.""" + + model_config = _STRICT + + schema_version: Literal[1] = 1 + scenario: str = Field(pattern=C.SCENARIO_NAME_RE.pattern) + description: str = Field(default="", max_length=255) + proxmox_node: str = Field(pattern=C.PROXMOX_NODE_RE.pattern) + subnets: list[Subnet] = Field(min_length=1) + zones: list[Zone] = Field(min_length=1) + boxes: list[Box] = Field(min_length=1) + network_policy: NetworkPolicyRef + + _guard_scenario = field_validator("scenario")(_no_injection) + _guard_description = field_validator("description")(_no_injection) + _guard_proxmox_node = field_validator("proxmox_node")(_no_injection) diff --git a/r42playbooks/core/render.py b/r42playbooks/core/render.py new file mode 100644 index 00000000..59cc555d --- /dev/null +++ b/r42playbooks/core/render.py @@ -0,0 +1,632 @@ +"""Renderer (part 1, S5b): a composed ``Allocation`` -> ``scenarios//``. + +Emits the **class-(B)** files (plan §4.1): boilerplate copied from +``_init_lab``/``demo_lab`` with placeholders filled — per-box ``stage_00`` clone +playbooks, ``stage_01`` plays that list catalog roles **by name** (§2), per-box +devkits, per-section reinstall scripts, the top-level scripts, the class-B +``templates/`` files, the vendored ``01_init_proxmox/`` template-creation subtree +(plan H3), and the originating ``scenario.r42.yml`` (reproducibility). + +What this part does NOT emit (left to S5a, the class-(A) manifest-derived +artifacts): each section's ``_main.yml``, ``templates/*.j2``, and +``manifest/scenario_vms.json``. It also does NOT generate ``secrets/`` — that is +a deploy-time symlink (plan §4.2). And per the §7.2 richness decision it emits a +**uniform per-box** structure only (no group playbooks / group devkits / +``_testing`` / ``builder_*``). + +Pure + deterministic: every write goes through ``core.io.atomic_write_text`` and +output depends only on the ``Allocation`` + ``ScenarioSpec`` (no clock/randomness). +""" + +import os +import shutil +from collections.abc import Mapping +from pathlib import Path + +import yaml + +from r42playbooks.core import constants as C +from r42playbooks.core import render_assets as A +from r42playbooks.core.allocate import ( + Allocation, + AllocatedBox, + ResolvedTemplate, + manifest_json, +) +from r42playbooks.core.catalog import Catalog +from r42playbooks.core.compiler.network_policy import ( + CompiledRule, + compile_network_policy_from_alloc, + lint_compiled_policy_from_alloc, +) +from r42playbooks.core.errors import CompileError, ScenarioExistsError, ValidationError +from r42playbooks.core.io import atomic_write_text +from r42playbooks.core.models import Attachment +from r42playbooks.core.spec import ScenarioSpec, dumps_spec + +_DEFAULT_PROXMOX_NODE = "pve" +_NETMASK = "24" +_EXEC_MODE = 0o755 + +# 01_init_proxmox uses the _init_lab STAGED layout, keyed by versioned image name +# (``_``), which IS the templates// dir: +# _main.yml -> imports stage_00 + stage_01 +# stage_00-download_cloudinit_files/ cloudinit_.yml (rendered from catalog) +# stage_01-create_templates/ templates//
+ per-size files +# A scenario only downloads/creates the image sets its composition uses +# (image-selective): the two stage _main.yml are generated with just those +# imports, and only the matching cloudinit_.yml + templates// are +# emitted. Per image: "main" is the stage_01 orchestrator (vendored asset). +# The download play (stage_00) is rendered from the catalog's cloud_image spec — +# NOT copied from a static asset file. +_INIT_MAIN_IMPORT = "- import_playbook: ./01_init_proxmox/_main.yml" +_DOWNLOAD_DIR = "stage_00-download_cloudinit_files" +_TEMPLATES_STAGE = "stage_01-create_templates" + + +def _parse_spec(spec: str) -> tuple[int, int, str]: + """Parse ``'Xcpu/Ygb/Zgb'`` into ``(cores, memory_mb, disk_size)``. + + ``memory_mb`` is ``Y * 1024``; ``disk_size`` is ``'Zg'`` (qemu-img / Proxmox format). + """ + parts = spec.split("/") + if len(parts) != 3: + raise CompileError( + f"malformed template spec {spec!r}: expected format Xcpu/Ygb/Zgb" + ) + try: + cores = int(parts[0].replace("cpu", "")) + ram_mb = int(parts[1].replace("gb", "")) * 1024 + disk = parts[2].replace("gb", "g") + except ValueError as exc: + raise CompileError(f"malformed template spec {spec!r}: {exc}") from exc + return cores, ram_mb, disk + + +def _write(text: str, path: Path, *, executable: bool = False) -> Path: + """Atomic write; ``chmod +x`` for generated shell scripts.""" + atomic_write_text(text, path) + if executable: + os.chmod(path, _EXEC_MODE) + return path + + +def _ssh_host(vm_name: str) -> str: + """M5 naming contract: SSH/inventory host id for a VM.""" + return f"r42.{vm_name}" + + +def _file_prefix(scenario: str) -> str: + """Filename-safe scenario prefix (SCENARIO_NAME_RE permits '/' for nesting). + + The directory tree keeps the full ``scenario`` name (so ``ctf/web1`` nests), + but per-file prefixes use only the leaf so a slash never spawns extra dirs in + a script/devkit filename (e.g. ``ctf/web1.setup.sh``). + """ + return scenario.rsplit("/", 1)[-1] + + +def _vars_block(box_vars: Mapping[str, object]) -> str: + """Render a box's free-form vars as an indented ``vars:`` block (or '').""" + if not box_vars: + return "" + dumped = yaml.safe_dump(dict(box_vars), default_flow_style=False, sort_keys=True) + indented = "\n".join(" " + line for line in dumped.rstrip("\n").splitlines()) + return f" vars:\n{indented}\n" + + +def _gateway(box: AllocatedBox) -> str: + """Box subnet gateway, or the conventional ``.1`` host if the layout omits it.""" + if box.gateway: + return box.gateway + prefix = box.ip.rsplit(".", 1)[0] + return f"{prefix}.1" + + +def _attachment_role(att: Attachment) -> str: + """The catalog role an attachment applies (container -> shared docker-compose).""" + return ( + "software.configure.docker-compose" + if att.kind == "container" + else att.catalog_ref + ) + + +def _attachment_vars(att: Attachment) -> dict: + """The vars a stage_01 play passes to the attachment's role. + + role/gamification → the attachment's ``params`` verbatim. container → docker + wiring derived from the stack path (env-based project dir, labels), overlaid + with any explicit ``params`` (mirrors the demo_lab / bundle docker plays). + """ + if att.kind != "container": + return dict(att.params) + name = att.catalog_ref.rsplit("/", 1)[-1] + wiring = { + "LABEL_PROJECT_TYPE": "CTF", + "LABEL_PROJECT_NAME": name, + "LOCAL__PROJECT_DIR": "{{ lookup('env', 'RANGE42_INVENTORY__DOCKER__CTF') }}/" + + att.catalog_ref + + "/", + "REMOTE_PROJECT_DIR": f"/tmp/deploy-{name}", + "OPERATOR_USER": "{{ default_admin_vm_ci_user }}", + } + wiring.update(att.params) + return wiring + + +def _render_stage01(box: AllocatedBox) -> str: + """One play per attachment (role + its params), or a no-op placeholder.""" + ssh = _ssh_host(box.vm_name) + box_vars = dict(box.box_vars) + plays = [ + A.fill( + A.STAGE01_PLAY, + VM_NAME=box.vm_name, + SSH_HOST=ssh, + ROLE=_attachment_role(att), + VARS_BLOCK=_vars_block({**box_vars, **_attachment_vars(att)}), + ) + for att in box.attachments + ] + if not plays: + return A.fill( + A.STAGE01_PLACEHOLDER, + VM_NAME=box.vm_name, + SSH_HOST=ssh, + VARS_BLOCK=_vars_block(box_vars), + ) + return A.fill(A.STAGE01_HEADER, VM_NAME=box.vm_name) + "\n" + "\n".join(plays) + + +# --- per-box ----------------------------------------------------------------- + + +def _render_box( + box: AllocatedBox, section_dir: Path, scenario: str, proxmox_node: str +) -> None: + """Emit stage_00 + stage_01 + devkit for one VM.""" + label = box.label + + stage00 = A.fill( + A.STAGE00_CLONE, + SECTION_LABEL=label, + VM_NAME=box.vm_name, + VM_ID=box.vm_id, + IP=box.ip, + TEMPLATE_NAME=box.template_name, + NETMASK=_NETMASK, + GATEWAY=_gateway(box), + BRIDGE=box.bridge, + ) + _write(stage00, section_dir / "stage_00" / f"{box.vm_name}.yml") + + _write(_render_stage01(box), section_dir / "stage_01" / f"{box.vm_name}.yml") + + _render_devkit( + box, section_dir / "stage_01" / f"{box.vm_name}.devkit", scenario, proxmox_node + ) + + +def _render_devkit( + box: AllocatedBox, devkit_dir: Path, scenario: str, proxmox_node: str +) -> None: + """Emit the per-box install / snapshot / revert scripts.""" + prefix = f"{_file_prefix(scenario)}.{box.vm_name}" + scripts = { + f"{prefix}.install.sh": A.fill(A.DEVKIT_INSTALL, VM_NAME=box.vm_name), + f"{prefix}.snapshot.sh": A.fill( + A.DEVKIT_SNAPSHOT, VM_NAME=box.vm_name, PROXMOX_NODE=proxmox_node + ), + f"{prefix}.revert.sh": A.fill( + A.DEVKIT_REVERT, VM_NAME=box.vm_name, PROXMOX_NODE=proxmox_node + ), + } + for name, body in scripts.items(): + _write(body, devkit_dir / name, executable=True) + + +# --- sections ---------------------------------------------------------------- + + +def _sections_for(alloc: Allocation) -> dict[str, list[AllocatedBox]]: + """Group placed boxes by their emitted section dir (sorted by section number).""" + grouped: dict[str, list[AllocatedBox]] = {} + for box in alloc.boxes: + section = box.section + grouped.setdefault(section, []).append(box) + return {name: grouped[name] for name in sorted(grouped)} + + +def _render_sections( + alloc: Allocation, root: Path, scenario: str, proxmox_node: str +) -> list[str]: + """Emit every section dir + its boxes + reinstall script. Returns section names.""" + sections = _sections_for(alloc) + for section, boxes in sections.items(): + section_dir = root / section + for box in boxes: + _render_box(box, section_dir, scenario, proxmox_node) + _write( + A.fill(A.SECTION_REINSTALL, SECTION=section), + section_dir / "_main.reinstall.sh", + executable=True, + ) + return list(sections) + + +# --- top level --------------------------------------------------------------- + + +def _render_init_proxmox( + root: Path, + alloc_templates: tuple[ResolvedTemplate, ...], + catalog: Catalog, + proxmox_node: str, +) -> None: + """Emit 01_init_proxmox for ONLY the template VMs the scenario actually needs. + + ``alloc_templates`` is the selective set from the allocation — a scenario with + one ``debian-jump`` box only creates ``template-vm-debian-small`` (9321), not + all 14 ubuntu_noble templates. Both images share their stage_00 download play; + stage_01 renders only the referenced template VM create plays. + """ + from collections import defaultdict + + # Group needed template VMs by image. + by_image: dict[str, list[ResolvedTemplate]] = defaultdict(list) + for tpl in alloc_templates: + by_image[tpl.image].append(tpl) + used_images = sorted(by_image.keys()) + + init = root / "01_init_proxmox" + + # top orchestrator + reinstall helper + _write(A.INIT_PROXMOX_MAIN_YML, init / "_main.yml") + _write(A.INIT_REINSTALL_SH, init / "_main.reinstall.sh", executable=True) + + # stage_00: download — one cloudinit_.yml per used image + dl = init / _DOWNLOAD_DIR + cloudinit_filename = "cloudinit_{image}.yml" + dl_imports = "".join( + f"- import_playbook: ./{cloudinit_filename.format(image=i)}\n" + for i in used_images + ) + _write(A.fill(A.STAGE_DOWNLOAD_MAIN, IMPORTS=dl_imports), dl / "_main.yml") + for image_id in used_images: + img_def = catalog.images.get(image_id) + if img_def is None or img_def.cloud_image is None: + raise ValidationError( + f"image {image_id!r} has no cloud_image in the catalog's 01_image_layer" + ) + ci = img_def.cloud_image + _write( + A.fill( + A.CLOUDINIT_DOWNLOAD_YML, + IMAGE_ID=image_id, + ISO_URL=ci.url, + ISO_FILE_NAME=ci.filename, + ), + dl / cloudinit_filename.format(image=image_id), + ) + + # stage_01: create templates — ONLY the VMs this scenario references + stage = init / _TEMPLATES_STAGE + tpl_stage_imports = "".join( + f"- import_playbook: ./templates/{i}/_main_{i}.yml\n" + for i in used_images + ) + _write( + A.fill(A.STAGE_TEMPLATES_MAIN, IMPORTS=tpl_stage_imports), stage / "_main.yml" + ) + + for image_id in used_images: + img_def = catalog.images.get(image_id) + if img_def is None or img_def.cloud_image is None: + raise ValidationError( + f"image {image_id!r} has no cloud_image in the catalog's 01_image_layer" + ) + img_dir = stage / "templates" / image_id + image_path = f"/var/lib/vz/template/iso/{img_def.cloud_image.filename}" + needed_templates = by_image[image_id] + + # render one play per NEEDED template VM (not the full image set) + tpl_imports_img = "" + for tpl in needed_templates: + cores, memory_mb, disk_size = _parse_spec(tpl.spec) + ip_prefix = ".".join(tpl.ip.split(".")[:3]) + _write( + A.fill( + A.TEMPLATE_VM_YML, + IMAGE_ID=image_id, + VM_ID=str(tpl.vm_id), + VM_NAME=tpl.vm_name, + VM_CORES=str(cores), + VM_MEMORY_MB=str(memory_mb), + VM_NET_BRIDGE=tpl.bridge, + VM_DISK_SIZE=disk_size, + CLOUDINIT_IMAGE_PATH=image_path, + VM_CI_IP=tpl.ip, + VM_CI_IP_GW=f"{ip_prefix}.1", + PROXMOX_NODE=proxmox_node, + ), + img_dir / f"{tpl.vm_name}.yml", + ) + tpl_imports_img += f"- import_playbook: ./{tpl.vm_name}.yml\n" + + # _apply_apt_proxy only for the needed template VMs + id_list = "".join( + f" - {t.vm_id} # {t.vm_name}\n" for t in needed_templates + ) + _write( + A.fill(A.APPLY_APT_PROXY_YML, IMAGE_ID=image_id, VM_ID_LIST=id_list), + img_dir / "_apply_apt_proxy.yml", + ) + + # _main_.yml imports only the needed template plays + _write( + A.fill( + A.MAIN_IMAGE_YML, IMAGE_ID=image_id, TEMPLATE_IMPORTS=tpl_imports_img + ), + img_dir / f"_main_{image_id}.yml", + ) + + # static helpers (read from manifest at runtime / pure Jinja2 — no ids) + _write( + A.fill(A.UPDATE_TEMPLATES_YML, IMAGE_ID=image_id), + img_dir / "_update_templates.yml", + ) + _write( + A.TEMPLATE_BOOTSTRAP_YAML_J2, img_dir / "range42-template-bootstrap.yaml.j2" + ) + + +def _iptables_task(rule: CompiledRule, idx: int) -> str: + """Render one CompiledRule as a 4-space-indented ansible.builtin.iptables task.""" + ind = " " + lines = [ + f'{ind}- name: "NETWORK ISOLATION - rule w={rule.weight:04d}:{idx:03d} — {rule.comment}"', + f'{ind} ansible.builtin.iptables:', + f'{ind} chain: R42-FORWARD', + f'{ind} table: filter', + ] + if rule.proto != "all": + lines.append(f'{ind} protocol: {rule.proto}') + if rule.source: + lines.append(f'{ind} source: "{rule.source}"') + if rule.destination: + lines.append(f'{ind} destination: "{rule.destination}"') + if rule.destination_port: + lines.append(f'{ind} destination_port: "{rule.destination_port}"') + if rule.in_interface: + lines.append(f'{ind} in_interface: {rule.in_interface}') + if rule.out_interface: + lines.append(f'{ind} out_interface: {rule.out_interface}') + if rule.ctstate: + lines.append(f'{ind} ctstate:') + for state in rule.ctstate.split(","): + lines.append(f'{ind} - {state.strip()}') + lines.append(f'{ind} jump: {rule.jump}') + if rule.comment: + safe = rule.comment.replace('"', "'") + lines.append(f'{ind} comment: "{safe}"') + return "\n".join(lines) + + +def _render_network_isolation( + alloc: Allocation, spec: ScenarioSpec, root: Path, catalog: Catalog +) -> bool: + """Emit 05_network_isolation/_main.yml when spec.network_policy is set. + + Compiles the policy against the allocation's subnets and generates an + idempotent Ansible playbook that flush-rebuilds the R42-FORWARD iptables + chain on proxmox-cli. Returns True when the section was written. + """ + if spec.network_policy is None: + return False + + policy = catalog.resolve_network_policy(spec.network_policy) + version = catalog.resolved_version(C.CATEGORY_NETWORK_POLICIES, spec.network_policy) + wan_interface = str(policy.params.get("wan_interface", "vmbr0")) + + compiled = compile_network_policy_from_alloc( + alloc, policy, version=version, wan_interface=wan_interface + ) + problems = lint_compiled_policy_from_alloc(compiled, policy, alloc) + if problems: + raise CompileError( + f"network policy {spec.network_policy!r} failed safety lint: " + + "; ".join(problems) + ) + + rule_tasks = "\n\n".join( + _iptables_task(r, i) for i, r in enumerate(compiled.rules) + ) + body = A.fill( + A.NETWORK_ISOLATION_MAIN, + SCENARIO=spec.name, + POLICY_ID=compiled.policy_id, + POLICY_VERSION=compiled.policy_version, + RULE_TASKS=rule_tasks, + ) + _write(body, root / "05_network_isolation" / "_main.yml") + teardown_body = A.fill(A.NETWORK_ISOLATION_TEARDOWN, SCENARIO=spec.name) + _write(teardown_body, root / "05_network_isolation" / "teardown.yml") + return True + + +def _render_main_playbooks( + root: Path, scenario: str, sections: list[str], *, network_isolation: bool = False +) -> None: + """Top-level main.yml / main_vms_only.yml (only emitted sections).""" + section_imports = "".join(f"- import_playbook: ./{s}/_main.yml\n" for s in sections) + isolation_import = ( + "- import_playbook: ./05_network_isolation/_main.yml\n" + if network_isolation else "" + ) + header = A.fill(A.MAIN_HEADER, SCENARIO=scenario) + # main.yml: create the template images (01_init_proxmox) then deploy the sections. + _write( + header + _INIT_MAIN_IMPORT + "\n\n" + section_imports + isolation_import, + root / "main.yml", + ) + # main_vms_only.yml: skip 01_init_proxmox (templates already exist). + _write( + A.fill(A.MAIN_VMS_ONLY_HEADER, SCENARIO=scenario) + section_imports + isolation_import, + root / "main_vms_only.yml", + ) + + +def _render_top_level_scripts(root: Path, scenario: str) -> None: + """Emit the activate + setup/delete/reset + inventory scripts (all executable).""" + prefix = _file_prefix(scenario) + scripts = { + "_activate.sh": A.ACTIVATE_SH, + f"{prefix}.setup.sh": A.SETUP_SH, + f"{prefix}.setup_vms_only.sh": A.SETUP_VMS_ONLY_SH, + f"{prefix}.delete_all.sh": A.DELETE_ALL_SH, + f"{prefix}.delete_vms_only.sh": A.DELETE_VMS_ONLY_SH, + f"{prefix}.reset.setup.sh": A.RESET_SETUP_SH, + f"{prefix}.reset.ssh_keys.sh": A.RESET_SSH_KEYS_SH, + "devkit_ansible.show_ansible_inventory.to.text.sh": A.SHOW_INVENTORY_SH, + } + for name, body in scripts.items(): + _write(body, root / name, executable=True) + + +def _render_templates_class_b(root: Path, scenario: str) -> None: + """Emit the class-B templates/ files (the .j2 inventory/ssh-config are S5a).""" + _write( + A.fill(A.ANSIBLE_VARS_YML, SCENARIO=scenario), + root / "templates" / "ansible-vars.yml", + ) + _write( + A.fill(A.VAULT_EXAMPLE_YML, SCENARIO=scenario), + root / "templates" / "vault-example.yml", + ) + + +def _render_readme(root: Path, spec: ScenarioSpec, alloc: Allocation) -> None: + rows = ["| box | subnet | image | vm_id | ip |", "|---|---|---|---|---|"] + rows += [ + f"| `{b.vm_name}` | {b.subnet_name} | {b.image} | {b.vm_id} | {b.ip} |" + for b in alloc.boxes + ] + notes_line = f"\n> {spec.notes}\n" if spec.notes else "" + _write( + A.fill( + A.README_MD, + SCENARIO=spec.name, + SUBNET_LAYOUT=spec.subnet_layout, + NOTES_LINE=notes_line, + BOX_TABLE="\n".join(rows), + ), + root / "README.md", + ) + + +# --- class-A: manifest-derived artifacts (reflect THIS composition) ---------- + + +def _render_manifest(alloc: Allocation, root: Path) -> None: + """Write manifest/scenario_vms.json (vms[] + populated templates[], H1).""" + _write(manifest_json(alloc), root / "manifest" / "scenario_vms.json") + + +def _inventory_groups(alloc: Allocation) -> str: + """Build the per-composition inventory group blocks (8/10/12-space indent).""" + groups: dict[str, list[str]] = {} + for box in alloc.boxes: + groups.setdefault(box.inventory_group, []).append(box.vm_name) + blocks: list[str] = [] + for group, names in groups.items(): + lines = [f" {group}:", " hosts:"] + lines += [f" {_ssh_host(n)}:" for n in names] + blocks.append("\n".join(lines)) + return "\n\n".join(blocks) + "\n" + + +def _render_inventory_j2(alloc: Allocation, root: Path) -> None: + """templates/ansible-inventory.j2 — groups + member hosts from the manifest.""" + body = A.fill(A.INVENTORY_J2, GROUPS=_inventory_groups(alloc)) + _write(body, root / "templates" / "ansible-inventory.j2") + + +def _render_ssh_config_j2(alloc: Allocation, root: Path) -> None: + """templates/ssh-config.j2 — one Host/Hostname block per VM (M5 naming).""" + blocks = "\n\n".join( + f"Host {_ssh_host(b.vm_name)}\n Hostname {b.ip}" for b in alloc.boxes + ) + _write( + A.fill(A.SSHCONFIG_J2, VM_BLOCKS=blocks), root / "templates" / "ssh-config.j2" + ) + + +def _stage00_import(box: AllocatedBox) -> str: + """One section-_main.yml stage_00 import + its per-VM global_* overrides.""" + return A.fill( + A.SECTION_MAIN_STAGE00, + VM_NAME=box.vm_name, + VM_ID=box.vm_id, + IP=box.ip, + TAG=box.subnet_name, + DESCRIPTION=box.box_template, + TEMPLATE_VM_ID=box.template_vm_id, + TEMPLATE_NAME=box.template_name, + ) + + +def _render_section_mains(alloc: Allocation, root: Path) -> None: + """Each section's _main.yml: stage_00 imports (+global_*) then stage_01 imports.""" + for section, boxes in _sections_for(alloc).items(): + parts = [A.fill(A.SECTION_MAIN_HEADER, SECTION=section), "#### STAGE 00 ####\n"] + parts += [_stage00_import(b) for b in boxes] + parts.append("\n#### STAGE 01 ####\n") + parts += [f"- import_playbook: ./stage_01/{b.vm_name}.yml\n" for b in boxes] + _write("\n".join(parts), root / section / "_main.yml") + + +def render_scenario( + alloc: Allocation, + spec: ScenarioSpec, + *, + catalog: Catalog, + dest: Path, + overwrite: bool = False, +) -> Path: + """Render *alloc*/*spec* into ``dest//`` and return that path. + + Emits both class-(B) boilerplate (S5b) and the class-(A) manifest-derived + artifacts (S5a: manifest, inventory/ssh-config templates, section _main.yml). + Never creates ``secrets/`` (deploy-time symlink, §4.2). Deterministic. + + :param catalog: the loaded catalog; its ``01_image_layer`` cloud_image specs + are used to render ``stage_00-download_cloudinit_files/.yml``. + :param overwrite: if False (default) and ``dest//`` already exists, + raise :class:`ScenarioExistsError` rather than silently clobbering it. + :raises ScenarioExistsError: target exists and ``overwrite`` is False. + :raises ValidationError: if a used image has no cloud_image in the catalog. + """ + root = Path(dest) / spec.name + if root.exists(): + if not overwrite: + raise ScenarioExistsError(f"scenario already exists at {root}") + shutil.rmtree(root) + proxmox_node = spec.proxmox_node or _DEFAULT_PROXMOX_NODE + + # class B — verbatim-with-param boilerplate + _render_init_proxmox(root, alloc.templates, catalog, proxmox_node) + sections = _render_sections(alloc, root, spec.name, proxmox_node) + has_isolation = _render_network_isolation(alloc, spec, root, catalog) + _render_main_playbooks(root, spec.name, sections, network_isolation=has_isolation) + _render_top_level_scripts(root, spec.name) + _render_templates_class_b(root, spec.name) + _render_readme(root, spec, alloc) + _write(dumps_spec(spec), root / "scenario.r42.yml") + + # class A — generated from the allocation (reflect THIS composition) + _render_manifest(alloc, root) + _render_inventory_j2(alloc, root) + _render_ssh_config_j2(alloc, root) + _render_section_mains(alloc, root) + return root diff --git a/r42playbooks/core/render_assets.py b/r42playbooks/core/render_assets.py new file mode 100644 index 00000000..dfcbd142 --- /dev/null +++ b/r42playbooks/core/render_assets.py @@ -0,0 +1,1258 @@ +"""Verbatim-with-param boilerplate strings for the S5b renderer (class B, §4.1). + +These mirror the ``demo_lab`` / ``_init_lab`` shapes; the renderer fills the +``@@PLACEHOLDER@@`` sentinels with values from the composed ``Allocation``. +Sentinels are ``@@NAME@@`` (not ``str.format``) on purpose: the bodies are full +of Ansible Jinja ``{{ }}`` / ``{% %}`` that ``.format`` would choke on. + +Every string is static; the only per-scenario content is what the renderer +substitutes. No catalog *role code* is ever embedded here — ``stage_01`` lists +role NAMES only (the catalog↔playbooks name-reference contract, plan §2). +""" + +import re as _re + +_SENTINEL_RE = _re.compile(r"@@[A-Z_]+@@") + + +def fill(template: str, **values: object) -> str: + """Replace every ``@@KEY@@`` sentinel in *template* with ``str(value)``. + + :raises RuntimeError: if any ``@@KEY@@`` sentinel is left unfilled (a + programming error — a call site misspelled or forgot a key). This keeps + an unsubstituted sentinel from silently shipping into a generated file. + """ + out = template + for key, value in values.items(): + out = out.replace(f"@@{key}@@", str(value)) + leftover = _SENTINEL_RE.findall(out) + if leftover: + raise RuntimeError(f"unfilled render sentinel(s): {sorted(set(leftover))}") + return out + + +# --- stage_00: parametrised Proxmox clone playbook ------------------------- + +STAGE00_CLONE = """\ +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# +# @@SECTION_LABEL@@ - CLONE + CLOUD-INIT +# +# hostname : @@VM_NAME@@ +# vm_id : @@VM_ID@@ +# ip : @@IP@@ +# template : @@TEMPLATE_NAME@@ +# +# global_* vars are supplied by ../_main.yml (kept here as Jinja references). +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- hosts: proxmox + gather_facts: false + vars_files: + - "../../secrets/default_vault.yml" + + tasks: + - name: "@@SECTION_LABEL@@ - {{ global_vm_name }} - CLONE TEMPLATE - {{ global_template_name }}" + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "vm_clone" + vm_id: "{{ global_template_vm_id }}" + vm_new_id: "{{ global_vm_id }}" + vm_name: "{{ global_vm_name }}" + vm_description: "{{ global_vm_description }}" + + - name: "@@SECTION_LABEL@@ - {{ global_vm_name }} - WAIT FOR PROXMOX UNLOCK" + ansible.builtin.shell: qm config {{ global_vm_id }} | grep -q '^lock:' + register: clone_lock_check + until: clone_lock_check.rc != 0 + retries: 60 + delay: 5 + failed_when: false + changed_when: false + delegate_to: "{{ inventory_hostname }}-cli" + + - name: "@@SECTION_LABEL@@ - {{ global_vm_name }} - SET PROXMOX TAG" + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "vm_set_tag" + vm_id: "{{ global_vm_id }}" + vm_tag_name: "{{ global_vm_tag_name }}" + + - name: "@@SECTION_LABEL@@ - {{ global_vm_name }} - SET CLOUD-INIT VARIABLES" + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "cloudinit_set_variables" + vm_id: "{{ global_vm_id }}" + vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" + vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" + vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" + vm_ci_ip: "{{ global_vm_ci_ip }}" + vm_ci_netmask: "@@NETMASK@@" + vm_ci_ip_gw: "@@GATEWAY@@" + vm_net_virtio_bridge: "@@BRIDGE@@" + + - name: "@@SECTION_LABEL@@ - {{ global_vm_name }} - START VM" + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "vm_start" + vm_id: "{{ global_vm_id }}" + +- hosts: "{{ global_vm_ssh_name }}" + gather_facts: false + vars_files: + - "../../secrets/default_vault.yml" + vars: + ARG_vm_ssh_name: "{{ global_vm_ssh_name }}" + requested_tasks: + - wait/openssh_server/is_reachable.yml + - wait/cloudinit/is_boot_finished.yml + tasks: + - name: "@@SECTION_LABEL@@ - {{ global_vm_name }} - WAIT FOR SSH + CLOUD-INIT" + include_role: + name: ansible.utils +""" + + +# --- stage_01: software install — catalog roles BY NAME (§2) --------------- + +STAGE01_HEADER = """\ +## +## stage_01 — software install for @@VM_NAME@@ +## catalog roles are referenced BY NAME (resolved via ANSIBLE_ROLES_PATH at deploy); +## one play per attachment so each carries its own params (firewall_rules, docker …). +## +""" + +# One play per box attachment — its role + its params (merged box vars + attachment +# params) as a vars block. Containers map to the docker-compose role (see render). +STAGE01_PLAY = """\ +- name: "@@VM_NAME@@ — @@ROLE@@" + hosts: @@SSH_HOST@@ + become: true + vars_files: + - "../../secrets/default_vault.yml" +@@VARS_BLOCK@@ roles: + - @@ROLE@@ +""" + +# A valid no-op play (NOT a bare `[]`, which `import_playbook` rejects with +# "A play definition must contain exactly one of hosts/import_playbook/roles/tasks"). +STAGE01_PLACEHOLDER = """\ +## +## stage_01 — @@VM_NAME@@ : placeholder (no catalog roles attached) +## This box clones + boots but installs nothing. Attach roles via the box's +## `attachments_add` in scenario.r42.yml, then re-generate. +## + +- name: "@@VM_NAME@@ — placeholder (no catalog roles attached)" + hosts: @@SSH_HOST@@ + gather_facts: false +@@VARS_BLOCK@@ tasks: [] +""" + + +# --- per-box devkit scripts ------------------------------------------------ + +DEVKIT_INSTALL = """\ +#!/bin/bash +## +## reinstall software on @@VM_NAME@@ only (fast single-VM iteration) +## + +ansible-playbook -i "${RANGE42_ANSIBLE_ROLES__INVENTORY_DIR}/inventory_default.yml" \\ + -l "all" \\ + "../@@VM_NAME@@.yml" --vault-password-file "${RANGE42_VAULT_PASSWORD_FILE:?RANGE42_VAULT_PASSWORD_FILE is not set — run: range42-context use }" +""" + +DEVKIT_SNAPSHOT = """\ +#!/bin/bash +## +## snapshot @@VM_NAME@@ as "base" +## + +VM_ID=$(devkit_manifest.find_vm_id.to.text.sh "$0" "@@VM_NAME@@") || exit 1 +echo "{\\"proxmox_node\\":\\"@@PROXMOX_NODE@@\\",\\"vm_id\\":${VM_ID},\\"vm_snapshot_description\\":\\"base\\"}" | proxmox_snapshot_vm.vm_id.create_snapshot.to.jsons.sh +""" + +DEVKIT_REVERT = """\ +#!/bin/bash +## +## revert @@VM_NAME@@ to its latest snapshot and restart it +## + +VM_ID=$(devkit_manifest.find_vm_id.to.text.sh "$0" "@@VM_NAME@@") || exit 1 +echo "{\\"proxmox_node\\":\\"@@PROXMOX_NODE@@\\",\\"vm_id\\":${VM_ID} }" | proxmox_snapshot_vm.vm_id.revert_snapshot.to.jsons.sh +echo "{\\"proxmox_node\\":\\"@@PROXMOX_NODE@@\\",\\"vm_id\\":${VM_ID} }" | proxmox_vm.vm_id.start.to.jsons.sh +""" + + +# --- per-section reinstall ------------------------------------------------- + +SECTION_REINSTALL = """\ +#!/bin/bash +## +## re-run this section's playbooks (@@SECTION@@) +## + +ansible-playbook -i "${RANGE42_ANSIBLE_ROLES__INVENTORY_DIR}/inventory_default.yml" \\ + -l "all" \\ + "./_main.yml" --vault-password-file "${RANGE42_VAULT_PASSWORD_FILE:?RANGE42_VAULT_PASSWORD_FILE is not set — run: range42-context use }" +""" + + +# --- top-level scripts ----------------------------------------------------- + +ACTIVATE_SH = """\ +#!/bin/bash + +# deprecation algo issue with paramiko and ubuntu 24.04.2 LTS +# https://github.com/paramiko/paramiko/issues/2419 + +set -euo pipefail + +VIRTUAL_ENV_DIR="$HOME/ansible_fix/venv" + +if [ ! -d "$VIRTUAL_ENV_DIR" ]; then + + mkdir -p "$HOME/ansible_fix" + python3 -m venv "$VIRTUAL_ENV_DIR" + + # shellcheck disable=SC1091 + source "$VIRTUAL_ENV_DIR/bin/activate" + + pip install --upgrade pip setuptools wheel + pip install ansible paramiko cryptography +fi + +# shellcheck disable=SC1091 +source "$VIRTUAL_ENV_DIR/bin/activate" + +echo "source \\"$VIRTUAL_ENV_DIR/bin/activate\\"" +""" + +SETUP_SH = """\ +#!/bin/bash + +ansible-playbook -i "${RANGE42_ANSIBLE_ROLES__INVENTORY_DIR}/inventory_default.yml" \\ + -l "all" \\ + "./main.yml" --vault-password-file "${RANGE42_VAULT_PASSWORD_FILE:?RANGE42_VAULT_PASSWORD_FILE is not set — run: range42-context use }" +""" + +SETUP_VMS_ONLY_SH = """\ +#!/bin/bash + +## +## deploy VMs only — skip template download and creation +## faster redeploy when templates already exist on proxmox +## + +ansible-playbook -i "${RANGE42_ANSIBLE_ROLES__INVENTORY_DIR}/inventory_default.yml" \\ + -l "all" \\ + "./main_vms_only.yml" --vault-password-file "${RANGE42_VAULT_PASSWORD_FILE:?RANGE42_VAULT_PASSWORD_FILE is not set — run: range42-context use }" +""" + +# delete_all / delete_vms_only / reset are manifest-driven and scenario-generic +# (they read vm_id / ip from manifest/scenario_vms.json — no hard-coded VMs). + +DELETE_ALL_SH = """\ +#!/bin/bash + +## +## delete all — VMs + this scenario's ubuntu_noble templates +## +## ⚠ templates (9xxx) are shared across scenarios on the same Proxmox. Run this +## only when no other scenario relies on them, or use delete_vms_only.sh instead. +## + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +MANIFEST="$SCRIPT_DIR/manifest/scenario_vms.json" + +if [[ ! -f "$MANIFEST" ]]; then + echo "ERROR: manifest not found: $MANIFEST" >&2 + exit 1 +fi + +mapfile -t SCENARIO_VM_IDS < <(jq -r '.vms[].vm_id' "$MANIFEST") +mapfile -t TEMPLATE_VM_IDS < <(jq -r '.templates[].vm_id' "$MANIFEST") +mapfile -t INFRASTRUCTURE_IP < <(jq -r '.vms[].ip' "$MANIFEST") + +ALL_IDS=("${SCENARIO_VM_IDS[@]}" "${TEMPLATE_VM_IDS[@]}") +ID_REGEX=$(printf '|%s' "${ALL_IDS[@]}" | sed 's/^|//') + +echo ":: stopping and deleting VMs + templates" +VM_LIST_JSON=$(proxmox_vm.list.to.jsons.sh 2>&1 | grep '"vm_id":[0-9]') +if [ -z "$VM_LIST_JSON" ]; then + echo "ERROR: proxmox_vm.list.to.jsons.sh returned no VM data — aborting" >&2 + exit 1 +fi +echo "$VM_LIST_JSON" | jq -c | grep -E "\\"vm_id\\":($ID_REGEX)([^0-9]|$)" | proxmox_vm.vm_id.stop_force.to.jsons.sh +echo "$VM_LIST_JSON" | jq -c | grep -E "\\"vm_id\\":($ID_REGEX)([^0-9]|$)" | proxmox_vm.vm_id.delete.to.jsons.sh + +for ip in "${INFRASTRUCTURE_IP[@]}"; do + echo ":: REMOVE SSH KEY FOR : $ip" + ssh-keygen -f "$HOME/.ssh/known_hosts" -R "$ip" +done + +echo ":: done — VMs and templates removed" +""" + +DELETE_VMS_ONLY_SH = """\ +#!/bin/bash + +## +## delete VMs only — scenario VMs (filter by vm_id), keep templates +## + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +MANIFEST="$SCRIPT_DIR/manifest/scenario_vms.json" + +if [[ ! -f "$MANIFEST" ]]; then + echo "ERROR: manifest not found: $MANIFEST" >&2 + exit 1 +fi + +mapfile -t SCENARIO_VM_IDS < <(jq -r '.vms[].vm_id' "$MANIFEST") +mapfile -t INFRASTRUCTURE_IP < <(jq -r '.vms[].ip' "$MANIFEST") +ID_REGEX=$(printf '|%s' "${SCENARIO_VM_IDS[@]}" | sed 's/^|//') + +echo ":: stopping and deleting scenario VMs (keeping templates)..." +VM_LIST_JSON=$(proxmox_vm.list.to.jsons.sh 2>&1 | grep '"vm_id":[0-9]') +if [ -z "$VM_LIST_JSON" ]; then + echo "ERROR: proxmox_vm.list.to.jsons.sh returned no VM data — aborting" >&2 + exit 1 +fi +echo "$VM_LIST_JSON" | jq -c | grep -E "\\"vm_id\\":($ID_REGEX)([^0-9]|$)" | proxmox_vm.vm_id.stop_force.to.jsons.sh +echo "$VM_LIST_JSON" | jq -c | grep -E "\\"vm_id\\":($ID_REGEX)([^0-9]|$)" | proxmox_vm.vm_id.delete.to.jsons.sh + +for ip in "${INFRASTRUCTURE_IP[@]}"; do + echo ":: REMOVE SSH KEY FOR : $ip" + ssh-keygen -f "$HOME/.ssh/known_hosts" -R "$ip" +done + +echo ":: done — templates preserved" +""" + +RESET_SETUP_SH = """\ +#!/bin/bash + +## +## reset — delete this scenario's VMs (from manifest) then re-deploy +## + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +MANIFEST="$SCRIPT_DIR/manifest/scenario_vms.json" + +if [[ ! -f "$MANIFEST" ]]; then + echo "ERROR: manifest not found: $MANIFEST" >&2 + exit 1 +fi + +mapfile -t SCENARIO_VM_IDS < <(jq -r '.vms[].vm_id' "$MANIFEST") +mapfile -t INFRASTRUCTURE_IP < <(jq -r '.vms[].ip' "$MANIFEST") +ID_REGEX=$(printf '|%s' "${SCENARIO_VM_IDS[@]}" | sed 's/^|//') + +echo ":: stopping and deleting scenario VMs (vm_ids: ${SCENARIO_VM_IDS[*]})..." +VM_LIST_JSON=$(proxmox_vm.list.to.jsons.sh 2>&1 | grep '"vm_id":[0-9]') +if [ -z "$VM_LIST_JSON" ]; then + echo "ERROR: proxmox_vm.list.to.jsons.sh returned no VM data — aborting" >&2 + exit 1 +fi +echo "$VM_LIST_JSON" | jq -c | grep -E "\\"vm_id\\":($ID_REGEX)([^0-9]|$)" | proxmox_vm.vm_id.stop_force.to.jsons.sh +echo "$VM_LIST_JSON" | jq -c | grep -E "\\"vm_id\\":($ID_REGEX)([^0-9]|$)" | proxmox_vm.vm_id.delete.to.jsons.sh + +for ip in "${INFRASTRUCTURE_IP[@]}"; do + echo ":: REMOVE SSH KEY FOR : $ip" + ssh-keygen -f "$HOME/.ssh/known_hosts" -R "$ip" +done + +ansible-playbook -i "${RANGE42_ANSIBLE_ROLES__INVENTORY_DIR}/inventory_default.yml" \\ + -l "all" \\ + "./main.yml" --vault-password-file "${RANGE42_VAULT_PASSWORD_FILE:?RANGE42_VAULT_PASSWORD_FILE is not set — run: range42-context use }" +""" + +RESET_SSH_KEYS_SH = """\ +#!/bin/bash + +## +## remove known_hosts entries for every VM IP in this scenario (from manifest) +## + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +MANIFEST="$SCRIPT_DIR/manifest/scenario_vms.json" + +if [[ ! -f "$MANIFEST" ]]; then + echo "ERROR: manifest not found: $MANIFEST" >&2 + exit 1 +fi + +mapfile -t INFRASTRUCTURE_IP < <(jq -r '.vms[].ip' "$MANIFEST") + +for ip in "${INFRASTRUCTURE_IP[@]}"; do + echo ":: REMOVE SSH KEY FOR : $ip" + ssh-keygen -f "$HOME/.ssh/known_hosts" -R "$ip" +done +""" + +SHOW_INVENTORY_SH = """\ +#!/bin/bash + +ansible-inventory -i "${RANGE42_ANSIBLE_ROLES__INVENTORY_DIR}/inventory_default.yml" --graph +""" + + +# --- templates/ (class-B parametrised) ------------------------------------- + +ANSIBLE_VARS_YML = """\ +--- +################################################################################ +# range42 — scenario-specific variables (@@SCENARIO@@) +# +# Generated by r42playbooks. Shared variables -> group_vars/all/vars.yml, +# secrets -> vault (see vault-example.yml). +################################################################################ + + +#### SCENARIO IDENTIFICATION #### + +INFRASTRUCTURE_SCENARIO: "@@SCENARIO@@" + + +#### CREDENTIAL GENERATION #### + +context_auto_generate_ssh_keys: "YES" +context_auto_generate_vm_passwords: "YES" + +student_additionnal_keys_count: 5 + + +#### CLOUD-INIT USERNAMES #### + +default_admin_vm_ci_user: "alice" +default_trainee_vm_ci_user: "bob" + + +#### CLOUD-INIT DNS #### +# default resolver for lab VMs; range42-init.py substitutes the Proxmox node +# resolver here at workspace creation. 1.1.1.1 is the portable fallback. +default_vm_ci_dns_ips: "1.1.1.1" +""" + +VAULT_EXAMPLE_YML = """\ +--- +################################################################################ +# range42 — vault secrets (@@SCENARIO@@) +# +# Copy to vault.yml, fill real values, then: ansible-vault encrypt vault.yml +# Never commit the unencrypted version. +################################################################################ + + +#### PROXMOX API SECRET #### +proxmox_api_token_secret: "REPLACE_ME" + + +#### JUMP HOST #### +jump_password: "REPLACE_ME" + + +#### CLOUD-INIT PASSWORDS #### +default_admin_vm_ci_password: "REPLACE_ME" +default_admin_vm_ci_ssh_key: "ssh-ed25519 AAAA... alice CODENAME-SCENARIO" +default_trainee_vm_ci_password: "REPLACE_ME" +default_trainee_vm_ci_ssh_key: "ssh-ed25519 AAAA... bob CODENAME-SCENARIO" + + +#### CLOUD-INIT DNS #### +# default resolver for lab VMs; range42-init.py substitutes the Proxmox node +# resolver here at workspace creation. 1.1.1.1 is the portable fallback. +default_vm_ci_dns_ips: "1.1.1.1" + + +#### TAILSCALE #### +infrastructure_tailscale_authkey: "tskey-auth-REPLACE_ME" +infrastructure_tailscale_apikey: "tskey-api-REPLACE_ME" + + +#### MISC #### +deployer_cli_user_ssh_known_hosts: "/home/your_deployer_cli_username/.ssh/known_hosts" +""" + + +# --- main.yml / main_vms_only.yml import skeleton -------------------------- + +MAIN_HEADER = """\ +--- +## +## @@SCENARIO@@ — generated by r42playbooks +## + +""" + +MAIN_VMS_ONLY_HEADER = """\ +--- +## +## @@SCENARIO@@ — deploy VMs only (templates already exist; skip 01_init_proxmox) +## + +""" + +# Staged 01_init_proxmox sub-orchestrators, generated per scenario so each stage +# imports ONLY the OS families the composition uses (@@IMPORTS@@ = the import +# lines). The cloudinit_.yml downloads and templates// trees are copied +# from the vendored asset; these two files just wire up the used ones. + +# Ansible plays for the bootstrap snippet sent to the Proxmox host. +# Written as-is (no @@placeholders@@) into each image's templates/ dir. +TEMPLATE_BOOTSTRAP_YAML_J2 = """\ +#cloud-config +# range42 - TEMPORARY bootstrap snippet for templates +# Triggers apt update + dist-upgrade then poweroff. +# Auto-generated by _update_templates.yml. Detached after the template is +# converted, replaced by range42-apt-proxy.yaml (persistent). +# +# NOTE: cloud-init clean was previously here in `runcmd` - REMOVED because +# it ran in cloud_config stage, before cloud_final, and broke +# package-update-upgrade-install. Clones get a new vmid -> new instance-id, +# so cloud-init re-processes naturally on each clone. No clean needed. +# +{% if (apt_proxy_url | default('')) | length > 0 %} +apt: + http_proxy: "{{ apt_proxy_url }}" +{% endif %} +package_update: true +package_upgrade: true +package_reboot_if_required: false +power_state: + mode: poweroff + timeout: 60 + condition: True +""" + +# @@IMAGE_ID@@ — image id (e.g. ubuntu_noble) +UPDATE_TEMPLATES_YML = """\ +## +## @@IMAGE_ID@@ - pre-update templates +## +## After all templates are created (but BEFORE convert to template), this playbook: +## 1. renders a temporary bootstrap snippet directly on the Proxmox host +## (apt update + apt dist-upgrade + cloud-init clean + auto-poweroff) +## 2. attaches it as cicustom vendor on each template VM +## 3. starts all templates +## 4. polls qm status until each VM is stopped (cloud-init auto-poweroff signals end) +## 5. detaches the bootstrap snippet, re-attaches the persistent apt-proxy snippet +## 6. converts each VM to a template (qm template ) +## +## Reads VM IDs from the scenario manifest (manifest/scenario_vms.json). +## Runs entirely on `proxmox-cli` (= the Proxmox host itself), no SSH involved. +## + +- hosts: proxmox-cli + gather_facts: false + vars_files: + - "../../../../secrets/default_vault.yml" + + vars: + manifest_path: "{{ playbook_dir }}/../../../../manifest/scenario_vms.json" + bootstrap_snippet_template: "{{ playbook_dir }}/range42-template-bootstrap.yaml.j2" + bootstrap_snippet_path: "/var/lib/vz/snippets/range42-template-bootstrap.yaml" + + tasks: + + #### #### #### debug apt_proxy_url visibility - fully safe vs undefined #### #### #### + + - name: TEMPLATES UPDATE - DEBUG apt_proxy_url visibility + ansible.builtin.debug: + msg: | + apt_proxy_url defined : {{ apt_proxy_url is defined }} + apt_proxy_url value : {{ apt_proxy_url | default('(undefined)') }} + apt_proxy_url length : {{ (apt_proxy_url | default('')) | length }} + -> snippet WILL include apt section : {{ (apt_proxy_url | default('')) | length > 0 }} + + #### #### #### load manifest #### #### #### + + - name: TEMPLATES UPDATE - LOAD MANIFEST + ansible.builtin.set_fact: + templates: "{{ (lookup('file', manifest_path) | from_json).templates }}" + + #### #### #### IDEMPOTENCE - probe each template's current state #### #### #### + # 3 possible states per VM ID : + # - "template" : already converted, skip update entirely + # - "vm" : exists as a regular VM, needs update + convert + # - "missing" : VM doesn't exist on Proxmox, skip (templates not created yet) + # Only "vm" state proceeds through the start/update/shutdown/convert pipeline. + + - name: TEMPLATES UPDATE - PROBE state of each template VM + ansible.builtin.shell: | + if qm config {{ item.vm_id }} 2>/dev/null | grep -q '^template:'; then + echo "template" + elif qm config {{ item.vm_id }} >/dev/null 2>&1; then + echo "vm" + else + echo "missing" + fi + register: tpl_state + loop: "{{ templates }}" + loop_control: + label: "vm_id={{ item.vm_id }}" + changed_when: false + + - name: TEMPLATES UPDATE - BUILD list of templates needing update (state == 'vm') + ansible.builtin.set_fact: + templates_to_update: >- + {{ templates | zip(tpl_state.results) + | selectattr('1.stdout', 'eq', 'vm') + | map(attribute='0') | list }} + + - name: TEMPLATES UPDATE - DISPLAY PLAN + ansible.builtin.debug: + msg: | + total templates in manifest : {{ templates | length }} + already converted (skip) : {{ tpl_state.results | selectattr('stdout', 'eq', 'template') | list | length }} + missing (skip) : {{ tpl_state.results | selectattr('stdout', 'eq', 'missing') | list | length }} + to update + convert : {{ templates_to_update | length }} + {% if templates_to_update | length > 0 %} + targets : + {% for t in templates_to_update %} - {{ t.vm_id }} {{ t.vm_name }} ({{ t.ip }}) + {% endfor %} + {% endif %} + + - name: TEMPLATES UPDATE - SHORT-CIRCUIT if nothing to update + ansible.builtin.meta: end_play + when: templates_to_update | length == 0 + + #### #### #### render bootstrap snippet directly on the Proxmox host #### #### #### + + - name: TEMPLATES UPDATE - RENDER BOOTSTRAP SNIPPET ON PROXMOX + ansible.builtin.template: + src: "{{ bootstrap_snippet_template }}" + dest: "{{ bootstrap_snippet_path }}" + mode: "0644" + + #### #### #### attach bootstrap snippet on every template #### #### #### + + - name: TEMPLATES UPDATE - ATTACH bootstrap cicustom + ansible.builtin.command: + cmd: qm set {{ item.vm_id }} --cicustom "vendor=local:snippets/range42-template-bootstrap.yaml" + loop: "{{ templates_to_update }}" + loop_control: + label: "vm_id={{ item.vm_id }}" + + #### #### #### start all templates #### #### #### + + - name: TEMPLATES UPDATE - START all templates + ansible.builtin.command: + cmd: qm start {{ item.vm_id }} + register: start_result + failed_when: + - start_result.rc != 0 + - "'already running' not in start_result.stderr" + - "'MAX' not in start_result.stderr" + changed_when: start_result.rc == 0 + loop: "{{ templates_to_update }}" + loop_control: + label: "vm_id={{ item.vm_id }}" + + # VMs that failed with "MAX X vcpus" couldn't start - exclude them from + # the wait/convert pipeline but keep the play running for the others. + - name: TEMPLATES UPDATE - BUILD started list (exclude CPU-limited VMs) + ansible.builtin.set_fact: + templates_started: >- + {{ templates_to_update | zip(start_result.results) + | rejectattr('1.stderr', 'search', 'MAX') + | map(attribute='0') | list }} + templates_skipped_cpu: >- + {{ templates_to_update | zip(start_result.results) + | selectattr('1.stderr', 'search', 'MAX') + | map(attribute='0') | list }} + + - name: TEMPLATES UPDATE - WARN skipped templates (host CPU limit) + ansible.builtin.debug: + msg: | + WARNING: {{ templates_skipped_cpu | length }} template(s) skipped - host CPU limit: + {% for t in templates_skipped_cpu %} + - {{ t.vm_id }} {{ t.vm_name }} (spec: {{ t.spec }}) + {% endfor %} + These templates require more vCPUs than this host provides. + They will NOT be converted. Run on a host with enough physical + CPUs, or lower vm_cores in the template playbook if intentional. + when: templates_skipped_cpu | length > 0 + + #### #### #### wait until each VM auto-poweroffs (cloud-init done) #### #### #### + # the loop is sequential per-vm, but since all started in parallel above, + # total time is bounded by the slowest template (~3-15 min depending on cache) + + - name: TEMPLATES UPDATE - WAIT for cloud-init to auto-poweroff + ansible.builtin.shell: | + qm status {{ item.vm_id }} | grep -q 'status: stopped' + register: stop_check + retries: 360 # safety cap : 360 x 5s = 30 min max per VM + delay: 5 + until: stop_check.rc == 0 + loop: "{{ templates_started }}" + loop_control: + label: "vm_id={{ item.vm_id }}" + changed_when: false + + #### #### #### detach bootstrap, re-attach apt-proxy, convert to template #### #### #### + + - name: TEMPLATES UPDATE - DETACH bootstrap cicustom + ansible.builtin.command: + cmd: qm set {{ item.vm_id }} --delete cicustom + loop: "{{ templates_started }}" + loop_control: + label: "vm_id={{ item.vm_id }}" + + - name: TEMPLATES UPDATE - RE-ATTACH apt-proxy cicustom (persistent for clones) + ansible.builtin.command: + cmd: qm set {{ item.vm_id }} --cicustom "vendor=local:snippets/range42-apt-proxy.yaml" + loop: "{{ templates_started }}" + loop_control: + label: "vm_id={{ item.vm_id }}" + when: (apt_proxy_url | default('')) | length > 0 + + - name: TEMPLATES UPDATE - CONVERT VM to template + ansible.builtin.command: + cmd: qm template {{ item.vm_id }} + loop: "{{ templates_started }}" + loop_control: + label: "vm_id={{ item.vm_id }}" + + #### #### #### cleanup #### #### #### + + - name: TEMPLATES UPDATE - REMOVE BOOTSTRAP SNIPPET FROM PROXMOX + ansible.builtin.file: + path: "{{ bootstrap_snippet_path }}" + state: absent + + - name: TEMPLATES UPDATE - DISPLAY DONE + ansible.builtin.debug: + msg: | + {{ templates_started | length }} templates updated and converted + apt-proxy cicustom re-attached (if apt_proxy_url set) + {% if templates_skipped_cpu | length > 0 %} + {{ templates_skipped_cpu | length }} skipped (host CPU limit) - see warning above + {% endif %} +""" + +INIT_PROXMOX_MAIN_YML = """\ +- import_playbook: ./stage_00-download_cloudinit_files/_main.yml +- import_playbook: ./stage_01-create_templates/_main.yml +""" + +STAGE_DOWNLOAD_MAIN = """\ +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# PROMOX INIT - download cloud-init base images (only the OS families used) +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +@@IMPORTS@@""" + +# Rendered per image from the catalog's 01_image_layer cloud_image spec. +# @@IMAGE_ID@@ — the image id (e.g. ubuntu_noble) +# @@ISO_URL@@ — download URL +# @@ISO_FILE_NAME@@ — filename on Proxmox local storage (/var/lib/vz/template/iso/) +CLOUDINIT_DOWNLOAD_YML = """\ +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# +# PROXMOX INIT - download cloud init image for @@IMAGE_ID@@ +# +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- hosts: proxmox + gather_facts: false + vars_files: + - "../../secrets/default_vault.yml" + + tasks: + - name: PROMOX INIT - DOWNLOAD - CLOUD INIT images + include_role: + name: range42-ansible_roles-proxmox_controller + + vars: + proxmox_vm_action: "storage_download_iso" + proxmox_storage: "local" + iso_file_content_type: "iso" + iso_url: "@@ISO_URL@@" + iso_file_name: "@@ISO_FILE_NAME@@" + +# The download-url API is async (returns a UPID immediately). Poll on +# proxmox-cli until the file is fully on disk before stage_01 imports it. +- hosts: proxmox-cli + gather_facts: false + vars_files: + - "../../secrets/default_vault.yml" + + tasks: + - name: PROMOX INIT - WAIT for @@ISO_FILE_NAME@@ to be fully downloaded + ansible.builtin.stat: + path: "/var/lib/vz/template/iso/@@ISO_FILE_NAME@@" + register: _cloudinit_img + until: _cloudinit_img.stat.exists and _cloudinit_img.stat.size > 100000000 + retries: 120 # 120 x 10 s = 20 min max + delay: 10 + changed_when: false +""" + +STAGE_TEMPLATES_MAIN = """\ +--- +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# PROMOX INIT - create the Proxmox template images (only the OS families used) +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +@@IMPORTS@@""" + +# --- stage_01 per-template-VM create play (rendered from catalog) --- +# One file per ProxmoxTemplateSpec entry in 01_image_layer//image.yml. +# Placeholders: +# @@IMAGE_ID@@ — image id (e.g. ubuntu_noble) +# @@VM_ID@@ — Proxmox vm_id (e.g. 9221) +# @@VM_NAME@@ — vm_name (e.g. template-vm-small-01-4g-32g) +# @@VM_CORES@@ — cpu cores (parsed from spec) +# @@VM_MEMORY_MB@@ — memory in MB (parsed from spec) +# @@VM_NET_BRIDGE@@ — network bridge (e.g. vmbr140) +# @@VM_DISK_SIZE@@ — disk size string for qemu-img (e.g. 32g) +# @@CLOUDINIT_IMAGE_PATH@@ — full path on Proxmox local storage +# @@VM_CI_IP@@ — cloud-init IP +# @@VM_CI_IP_GW@@ — cloud-init gateway (first 3 octets + .1) +TEMPLATE_VM_YML = """\ +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# +# PROXMOX INIT - create @@IMAGE_ID@@ template VM @@VM_NAME@@ (id @@VM_ID@@) +# +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- hosts: proxmox + gather_facts: false + vars_files: + - "../../../../secrets/default_vault.yml" + + tasks: + #### IDEMPOTENCE - skip if already finalized as template #### + - name: TEMPLATE @@VM_ID@@ - CHECK if already finalized as template + ansible.builtin.shell: qm config @@VM_ID@@ 2>/dev/null | grep -q '^template:' + register: tpl_@@VM_ID@@_is_template + failed_when: false + changed_when: false + delegate_to: "{{ inventory_hostname }}-cli" + + - name: TEMPLATE @@VM_ID@@ - WAIT for unlock if a parallel deploy holds the VM + ansible.builtin.shell: qm config @@VM_ID@@ | grep -q '^lock:' + register: tpl_@@VM_ID@@_lock + until: tpl_@@VM_ID@@_lock.rc != 0 + retries: 30 + delay: 10 + failed_when: false + changed_when: false + delegate_to: "{{ inventory_hostname }}-cli" + when: tpl_@@VM_ID@@_is_template.rc != 0 + + - name: TEMPLATE @@VM_ID@@ - SKIP all (already finalized as template) + ansible.builtin.debug: + msg: "Template @@VM_ID@@ is already a template - skipping. Run delete-everything to recreate." + when: tpl_@@VM_ID@@_is_template.rc == 0 + + #### vm_create idempotence: separate exists check #### + - name: TEMPLATE @@VM_ID@@ - CHECK if VM exists (vm_create idempotence) + ansible.builtin.shell: qm config @@VM_ID@@ 2>/dev/null + register: tpl_@@VM_ID@@_exists + failed_when: false + changed_when: false + delegate_to: "{{ inventory_hostname }}-cli" + when: tpl_@@VM_ID@@_is_template.rc != 0 + + - name: PROXMOX INIT - TEMPLATES - CREATE TEMPLATE - @@VM_NAME@@ + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "vm_create" + vm_id: @@VM_ID@@ + vm_name: "@@VM_NAME@@" + vm_cpu: "host" + vm_cores: @@VM_CORES@@ + vm_sockets: 1 + vm_memory: @@VM_MEMORY_MB@@ + vm_net_virtio_bridge: "@@VM_NET_BRIDGE@@" + when: + - tpl_@@VM_ID@@_is_template.rc != 0 + - tpl_@@VM_ID@@_exists.rc != 0 + + - name: PROXMOX INIT - TEMPLATES - WAIT for vm_create to finish (lock-aware) + ansible.builtin.shell: qm config @@VM_ID@@ | grep -q '^lock:' + register: tpl_@@VM_ID@@_post_create_lock + until: tpl_@@VM_ID@@_post_create_lock.rc != 0 + retries: 60 + delay: 5 + failed_when: false + changed_when: false + delegate_to: "{{ inventory_hostname }}-cli" + when: + - tpl_@@VM_ID@@_is_template.rc != 0 + - tpl_@@VM_ID@@_exists.rc != 0 + +- hosts: proxmox-cli + gather_facts: false + vars_files: + - "../../../../secrets/default_vault.yml" + + tasks: + - name: TEMPLATE @@VM_ID@@ - CHECK if already template (proxmox-cli play) + ansible.builtin.shell: qm config @@VM_ID@@ 2>/dev/null | grep -q '^template:' + register: tpl_@@VM_ID@@_is_template + failed_when: false + changed_when: false + + - name: PROXMOX INIT - TEMPLATES - IMPORT CLOUD INIT DISK - @@VM_NAME@@ + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "template_cloudinit_import_disk" + proxmox_node: "@@PROXMOX_NODE@@" + cloudinit_image_full_path: "@@CLOUDINIT_IMAGE_PATH@@" + vm_id: @@VM_ID@@ + vm_disk_size: "@@VM_DISK_SIZE@@" + proxmox_dest_vm_storage_name: "local-lvm" + when: tpl_@@VM_ID@@_is_template.rc != 0 + +- hosts: proxmox + gather_facts: false + vars_files: + - "../../../../secrets/default_vault.yml" + + tasks: + - name: TEMPLATE @@VM_ID@@ - CHECK if already template (third play) + ansible.builtin.shell: qm config @@VM_ID@@ 2>/dev/null | grep -q '^template:' + register: tpl_@@VM_ID@@_is_template + failed_when: false + changed_when: false + delegate_to: "{{ inventory_hostname }}-cli" + + - name: PROXMOX INIT - TEMPLATES - SET CLOUD DEFAULT INIT VARIABLE - @@VM_NAME@@ + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "cloudinit_set_variables" + vm_id: @@VM_ID@@ + vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" + vm_ci_password: "{{ default_trainee_vm_ci_password | default('supersecret') }}" + vm_ci_ssh_key: "{{ default_trainee_vm_ci_ssh_key }}" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" + vm_ci_ip: "@@VM_CI_IP@@" + vm_ci_netmask: "24" + vm_ci_ip_gw: "@@VM_CI_IP_GW@@" + when: tpl_@@VM_ID@@_is_template.rc != 0 + + - name: PROXMOX INIT - TEMPLATES - SET PROXMOX TAG + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "vm_set_tag" + vm_id: @@VM_ID@@ + vm_tag_name: "template" + when: tpl_@@VM_ID@@_is_template.rc != 0 +""" + +# Apt proxy cicustom play — rendered per image with its full vm_id list. +# @@VM_ID_LIST@@ — YAML sequence items, 6-space indent, e.g.: +# " - 9221 # template-vm-small-01-4g-32g\n - 9222 # ..." +APPLY_APT_PROXY_YML = """\ +## +## @@IMAGE_ID@@ - apt proxy via cloud-init cicustom (optional) +## +## Attaches /var/lib/vz/snippets/range42-apt-proxy.yaml as cloud-init vendor-data +## to every template VM. All VMs cloned from these templates inherit the apt +## proxy automatically via cloud-init at first boot. +## Skipped (no-op) if apt_proxy_url is empty/unset. Idempotent. +## + +- hosts: proxmox-cli + gather_facts: false + vars_files: + - "../../../../secrets/default_vault.yml" + + vars: + bs2_template_vm_ids: +@@VM_ID_LIST@@ + tasks: + - name: APT-PROXY - SKIP (apt_proxy_url is empty) + ansible.builtin.debug: + msg: "apt_proxy_url is empty - cloud-init cicustom not applied to templates" + when: apt_proxy_url is not defined or (apt_proxy_url | length) == 0 + + - name: APT-PROXY - ATTACH cicustom vendor TO TEMPLATES + ansible.builtin.command: + cmd: qm set {{ item }} --cicustom "vendor=local:snippets/range42-apt-proxy.yaml" + loop: "{{ bs2_template_vm_ids }}" + register: qm_result + changed_when: qm_result.rc == 0 + failed_when: + - qm_result.rc != 0 + - "'does not exist' not in qm_result.stderr" + when: apt_proxy_url is defined and (apt_proxy_url | length) > 0 + + - name: APT-PROXY - DETACH cicustom vendor FROM TEMPLATES (apt_proxy_url unset) + ansible.builtin.command: + cmd: qm set {{ item }} --delete cicustom + loop: "{{ bs2_template_vm_ids }}" + register: qm_unset + changed_when: qm_unset.rc == 0 + failed_when: + - qm_unset.rc != 0 + - "'does not exist' not in qm_unset.stderr" + when: apt_proxy_url is not defined or (apt_proxy_url | length) == 0 + + - name: APT-PROXY - DISPLAY STATUS + ansible.builtin.debug: + msg: | + apt proxy: {{ apt_proxy_url if (apt_proxy_url is defined and (apt_proxy_url | length) > 0) else 'DISABLED' }} + cicustom : {{ 'attached to ' + (bs2_template_vm_ids | length | string) + ' templates' if (apt_proxy_url is defined and (apt_proxy_url | length) > 0) else 'detached from templates' }} +""" + +# Per-image stage_01 orchestrator — imports all template VM plays + proxy + update. +# @@IMAGE_ID@@ — image id +# @@TEMPLATE_IMPORTS@@ — "- import_playbook: ./template-vm-nano.yml\n..." lines +MAIN_IMAGE_YML = """\ +## +## @@IMAGE_ID@@ — create + configure all Proxmox template VMs for this image +## + +@@TEMPLATE_IMPORTS@@ +# apt proxy cicustom (no-op if apt_proxy_url empty) +- import_playbook: ./_apply_apt_proxy.yml + +# pre-update: start each VM, run apt update + dist-upgrade via cloud-init poweroff, +# then convert to template. +- import_playbook: ./_update_templates.yml +""" + +# Reinstall helper for the 01_init_proxmox stage (re-run template creation). +INIT_REINSTALL_SH = """\ +#!/bin/bash +## +## re-run 01_init_proxmox (download images + create templates) +## + +ansible-playbook -i "${RANGE42_ANSIBLE_ROLES__INVENTORY_DIR}/inventory_default.yml" \\ + -l "proxmox" \\ + "./_main.yml" --vault-password-file "${RANGE42_VAULT_PASSWORD_FILE:?RANGE42_VAULT_PASSWORD_FILE is not set — run: range42-context use }" +""" + +# --- class-A: ansible-inventory.j2 (groups + member hosts, manifest-derived) --- +# @@GROUPS@@ is built per-composition; the proxmox/-cli groups stay verbatim +# (range42-context fills INFRASTRUCTURE_CODENAME / _PROXMOX_ADDRESS at deploy). + +INVENTORY_J2 = """\ +all: + children: + range42_infrastructure: + children: +@@GROUPS@@ + proxmox: + hosts: + {{ INFRASTRUCTURE_CODENAME }}: + ansible_host: {{ INFRASTRUCTURE_PROXMOX_ADDRESS | mandatory }}:8006 + ansible_connection: local + ansible_python_interpreter: /usr/bin/python3 + + proxmox-cli: + hosts: + {{ INFRASTRUCTURE_CODENAME }}-cli: +""" + + +# --- class-A: ssh-config.j2 (one Host block per VM, manifest-derived) ------- +# @@VM_BLOCKS@@ is the per-VM Host/Hostname list; a single r42.* wildcard block +# carries the shared user / key / ProxyJump (all generated VMs are equivalent). + +SSHCONFIG_J2 = """\ +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# infrastructure code name : {{ INFRASTRUCTURE_CODENAME }} +# infrastructure proxmox address : {{ INFRASTRUCTURE_PROXMOX_ADDRESS }} +# scenario : {{ INFRASTRUCTURE_SCENARIO }} +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +#### PROXMOX WEB UI (port forward) #### +Host px.{{ INFRASTRUCTURE_CODENAME }}.redirect_www.proxmox + RequestTTY no + RemoteCommand none + localforward localhost:18042 {{ INFRASTRUCTURE_PROXMOX_ADDRESS | mandatory }}:8006 + +#### PX ROOT USER SSH ACCESS #### +Host px.{{ INFRASTRUCTURE_CODENAME }}-ssh_cli.root {{ INFRASTRUCTURE_CODENAME }}-cli + Hostname {{ INFRASTRUCTURE_PROXMOX_ADDRESS | mandatory }} + User root + IdentityFile {{ DEPLOYER_CLI__DST_SSH_KEYS_JUMP_DEST_DIR }}/px.{{ INFRASTRUCTURE_CODENAME }}-{{ INFRASTRUCTURE_SCENARIO }}-ssh_cli.root + Port 22 + +#### SSH JUMPER #### +Host px.{{ INFRASTRUCTURE_CODENAME }}.jumper + Hostname {{ INFRASTRUCTURE_PROXMOX_ADDRESS | mandatory }} + User jump_user + IdentityFile {{ DEPLOYER_CLI__DST_SSH_KEYS_JUMP_DEST_DIR }}/px.{{ INFRASTRUCTURE_CODENAME }}-{{ INFRASTRUCTURE_SCENARIO }}-ssh_cli.jump_user + Port 22 + +#### SCENARIO VMs #### +@@VM_BLOCKS@@ + +Host r42.* + User alice + IdentityFile {{ DEPLOYER_CLI__DST_SSH_KEYS_BACKEND_DEST_DIR }}/r42.{{ INFRASTRUCTURE_CODENAME }}-{{ INFRASTRUCTURE_SCENARIO }}-deployer-key_alice + Port 22 + ProxyJump px.{{ INFRASTRUCTURE_CODENAME }}.jumper +""" + + +# --- class-A: section _main.yml (stage imports + per-VM global_* overrides) - + +SECTION_MAIN_HEADER = """\ +--- +## +## @@SECTION@@ — generated by r42playbooks +## stage_00 clones VMs (per-VM global_* below); stage_01 installs software. +## +""" + +# One stage_00 import with its global_* override block. Filled per box. +SECTION_MAIN_STAGE00 = """\ +- import_playbook: ./stage_00/@@VM_NAME@@.yml + vars: + global_vm_name: "@@VM_NAME@@" + global_vm_ssh_name: "r42.@@VM_NAME@@" + global_vm_id: @@VM_ID@@ + global_vm_description: "@@DESCRIPTION@@" + global_vm_tag_name: "@@TAG@@" + global_vm_ci_ip: "@@IP@@" + global_template_vm_id: @@TEMPLATE_VM_ID@@ + global_template_name: "@@TEMPLATE_NAME@@ - id @@TEMPLATE_VM_ID@@" +""" + + +# --- 05_network_isolation/_main.yml ------------------------------------------ +# Generated playbook that flush-rebuilds the R42-FORWARD iptables chain on +# proxmox-cli from the compiled policy rules. Idempotent: chain is created if +# absent, flushed, rules re-inserted in weight order, then hooked into FORWARD. +# +# @@SCENARIO@@ — scenario name +# @@POLICY_ID@@ — resolved policy id (e.g. air-gap-ctf) +# @@POLICY_VERSION@@ — resolved semver (e.g. 1.1.0) +# @@RULE_TASKS@@ — per-rule ansible.builtin.iptables task blocks (4-space indent) + +NETWORK_ISOLATION_MAIN = """\ +--- +## +## @@SCENARIO@@ — 05_network_isolation +## network policy: @@POLICY_ID@@ v@@POLICY_VERSION@@ +## +## Generated by r42playbooks. Do not edit — regenerated on each run. +## Flush-rebuilds R42-FORWARD chain on proxmox-cli. Idempotent. +## SSH/management traffic is never touched (FORWARD only, not INPUT). +## + +- hosts: proxmox-cli + gather_facts: false + vars_files: + - "../../secrets/default_vault.yml" + + tasks: + + - name: "NETWORK ISOLATION - CREATE R42-FORWARD chain" + ansible.builtin.iptables: + chain: R42-FORWARD + chain_management: true + state: present + table: filter + + - name: "NETWORK ISOLATION - FLUSH R42-FORWARD" + ansible.builtin.iptables: + chain: R42-FORWARD + flush: true + table: filter + + #### FORWARD rules — @@POLICY_ID@@ v@@POLICY_VERSION@@ #### + +@@RULE_TASKS@@ + + #### HOOK R42-FORWARD into kernel FORWARD chain (idempotent) #### + + - name: "NETWORK ISOLATION - HOOK R42-FORWARD into FORWARD" + ansible.builtin.iptables: + chain: FORWARD + jump: R42-FORWARD + action: insert + rule_num: 1 + table: filter + state: present +""" + +# --- 05_network_isolation/teardown.yml ---------------------------------------- +# Companion teardown: unhooks and destroys the R42-FORWARD chain. +# Safe to run even if the chain was never created (ignore_errors on each task). +# Called by range42-context delete-everything after all VMs are removed. +# +# @@SCENARIO@@ — scenario name (for the comment header) + +NETWORK_ISOLATION_TEARDOWN = """\ +--- +## +## @@SCENARIO@@ — 05_network_isolation — teardown +## +## Unhooks and destroys the R42-FORWARD iptables chain on proxmox-cli. +## Generated by r42playbooks. Safe to run even if chain was never deployed. +## + +- hosts: proxmox-cli + gather_facts: false + vars_files: + - "../../secrets/default_vault.yml" + + tasks: + + - name: "NETWORK ISOLATION TEARDOWN - unhook R42-FORWARD from FORWARD" + ansible.builtin.iptables: + chain: FORWARD + jump: R42-FORWARD + state: absent + table: filter + ignore_errors: true + + - name: "NETWORK ISOLATION TEARDOWN - flush R42-FORWARD chain" + ansible.builtin.iptables: + chain: R42-FORWARD + flush: true + table: filter + ignore_errors: true + + - name: "NETWORK ISOLATION TEARDOWN - delete R42-FORWARD chain" + ansible.builtin.iptables: + chain: R42-FORWARD + chain_management: true + state: absent + table: filter + ignore_errors: true +""" + + +README_MD = """\ +# @@SCENARIO@@ + +Generated by **r42playbooks**. + +- subnet layout: `@@SUBNET_LAYOUT@@` +@@NOTES_LINE@@ +## Boxes + +@@BOX_TABLE@@ + +## Deploy + +```bash +range42-context use @@SCENARIO@@ # creates secrets/ symlink + vault +./@@SCENARIO@@.setup.sh # templates + VMs + software +``` + +The composition is reproduced in `scenario.r42.yml` — re-run +`r42playbooks new --spec scenario.r42.yml` to regenerate this tree. +""" diff --git a/r42playbooks/core/scaffold.py b/r42playbooks/core/scaffold.py new file mode 100644 index 00000000..1e2cb347 --- /dev/null +++ b/r42playbooks/core/scaffold.py @@ -0,0 +1,106 @@ +"""Generate a starter Topology from catalog templates (pure, reused by CLI + TUI). + +``scaffold_topology`` turns a subnet-layout + policy choice into a minimal, +valid, deployable topology: one zone per subnet and one box per zone whose +template id best matches the subnet name. vm_id/IP are auto-assigned to satisfy +the project octet rule (vm_id last 3 digits == IP last octet). The result is a +starting point an operator refines in the CLI/TUI. +""" + +import ipaddress + +from r42playbooks.core.catalog import Catalog +from r42playbooks.core.errors import ValidationError +from r42playbooks.core.models import Box, NetworkPolicyRef, Topology, Zone + +_DEFAULT_OCTET = 10 # starting octet for scaffolded boxes + +# Subnet names to skip (infrastructure subnets, not lab zones) +_SKIP_SUBNETS = {"template"} + + +def _first_template_for_subnet(catalog: Catalog, subnet_name: str, used: set): + """Pick the best-matching unused box template for *subnet_name*. + + Preference order: + 1. template id starts with the subnet name (e.g. "admin-wazuh" for "admin") + 2. template id contains the subnet name + 3. alphabetically first unused template (fallback) + """ + candidates = [t for t in catalog.box_templates.values() if t.id not in used] + if not candidates: + return None + # prefer id starts with subnet_name + for t in sorted(candidates, key=lambda x: x.id): + if t.id.startswith(subnet_name): + return t + # then prefer id contains subnet_name + for t in sorted(candidates, key=lambda x: x.id): + if subnet_name in t.id: + return t + # fallback: alphabetically first unused + return sorted(candidates, key=lambda x: x.id)[0] + + +def _ip_with_octet(cidr: str, octet: int) -> str: + net = ipaddress.ip_network(cidr, strict=True) + base = str(net.network_address).rsplit(".", 1)[0] + return f"{base}.{octet}" + + +def scaffold_topology( + catalog: Catalog, + *, + scenario: str, + layout_id: str, + policy_id: str, + proxmox_node: str = "pve", + description: str = "", +) -> Topology: + """Build a minimal valid Topology from a subnet layout + network policy. + + :raises ValidationError: if no box template matches any zone role. + """ + layout = catalog.resolve_subnet_layout(layout_id) + catalog.resolve_network_policy(policy_id) # fail fast if missing + + zones, boxes = [], [] + subnet_seen: dict[str, int] = {} # per-subnet count -> distinct octet/vm_id per box + used_templates: set = set() # track globally-used template ids for uniqueness + + for subnet in layout.subnets: + base = _DEFAULT_OCTET + role = subnet.name if subnet.name in ("admin", "ctf", "student", "team", "template") else "team" + zones.append(Zone(name=subnet.name, subnet=subnet.name, role=role)) + if subnet.name in _SKIP_SUBNETS: + continue + template = _first_template_for_subnet(catalog, subnet.name, used_templates) + if template is None: + continue # no archetype for this subnet — leave the zone box-less + used_templates.add(template.id) + nth = subnet_seen.get(subnet.name, 0) + subnet_seen[subnet.name] = nth + 1 + octet = base + nth # avoid dup vm_id/IP per subnet + boxes.append(Box( + vm_name=f"{template.id}-{nth:02d}" if nth else template.id, + vm_id=1000 + octet, + ip=_ip_with_octet(subnet.cidr, octet), + zone=subnet.name, + box_template=template.id, + inventory_group=f"r42_{subnet.name}_group", + )) + + if not boxes: + raise ValidationError( + f"layout {layout_id!r} produced no boxes — no box template matched any zone role" + ) + + return Topology( + scenario=scenario, + description=description, + proxmox_node=proxmox_node, + subnets=list(layout.subnets), # pydantic accepts model instances directly + zones=zones, + boxes=boxes, + network_policy=NetworkPolicyRef(template=policy_id), + ) diff --git a/r42playbooks/core/spec.py b/r42playbooks/core/spec.py new file mode 100644 index 00000000..e95ad684 --- /dev/null +++ b/r42playbooks/core/spec.py @@ -0,0 +1,164 @@ +"""The ``scenario.r42.yml`` composition spec — the "options". + +A ``ScenarioSpec`` is the reproducible artifact a user composes: which catalog +modules (subnet layout, network policy, box templates) make up a lab. It is +written verbatim into every generated ``scenarios//`` so ``new`` is +re-runnable. + +Schema-level validation only (shapes, patterns, the security deny-list). Whether +a referenced module *exists* in the catalog is a separate concern (``catalog.py`` +``validate_refs``); allocation/rendering happen later still. This layer never +touches the filesystem beyond the load/dump helpers, which reuse ``core.io``'s +atomic writer. +""" + +from pathlib import Path +from typing import Annotated, Any, Literal + +from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator +from pydantic import ValidationError as _PydanticValidationError + +import yaml + +from r42playbooks.core import constants as C +from r42playbooks.core import io +from r42playbooks.core.errors import TopologyError +from r42playbooks.core.models import Attachment + +_STRICT = ConfigDict(extra="forbid") +_no_injection = C.reject_injection + + +class AptServiceSpec(BaseModel): + """Wire an apt-cache or apt-mirror server box to a set of client boxes. + + The compiler resolves the server IP post-allocation and injects + ``software.configure.apt_mirror_client`` into each client box's attachments. + Suite flags (apt_mirror_ubuntu_2404 etc.) are auto-detected from client images + and merged into the server box's apt_mirror attachment params (mirror mode only). + """ + + model_config = _STRICT + + # Box template id of the deployed apt server (e.g. "apt-cache" or "apt-mirror"). + box: str = Field(pattern=C.TEMPLATE_ID_RE.pattern) + # "all" wires every box except the server; list of template ids to target a subset. + wire_to: list[Annotated[str, Field(pattern=C.TEMPLATE_ID_RE.pattern)]] | Literal["all"] = "all" + # proxy → apt-cacher-ng (port 3142); mirror → apt-mirror nginx (port 80, airgapped). + mode: Literal["proxy", "mirror"] = "proxy" + + +class ServicesSpec(BaseModel): + model_config = ConfigDict(extra="forbid") + + apt: AptServiceSpec | None = None + + +class BoxSpec(BaseModel): + """One composed box: a catalog box-template pick plus optional tweaks.""" + + model_config = _STRICT + + # box_template id (dotless kebab dir under 05_topology_layer/box_templates/). + template: str = Field(pattern=C.TEMPLATE_ID_RE.pattern) + # how many VMs this box expands to (vuln-box:count=5 -> vuln-box-00..04). + count: int = Field(default=1, ge=1, le=C.BOX_COUNT_MAX) + # subnet name (e.g. "ctf"); must be explicitly set — no role-based fallback. + subnet: str = Field(pattern=C.TEMPLATE_ID_RE.pattern) + # explicit last IP octet within the subnet (1-254); None → sequential from .10. + octet: int | None = Field(default=None, ge=1, le=254) + # extra catalog attachments layered on top of the template's defaults. + attachments_add: list[Attachment] = Field(default_factory=list) + # free-form Ansible vars merged into the box (Jinja render surface -> guarded). + vars: dict[str, Any] = Field(default_factory=dict) + # §7.1 override: pin the Proxmox template vm_id instead of auto-selecting it. + template_vm_id: int | None = Field(default=None, ge=C.VM_ID_MIN, le=C.VM_ID_MAX) + + _guard_vars = field_validator("vars")(C.reject_injection_nested) + + +# Known apt-provider box template IDs and their service mode. +# When a spec contains exactly one of these and services is not explicitly set, +# services.apt is auto-injected (wire_to=all) so callers don't need to set it. +_APT_PROVIDER_MODES: dict[str, str] = { + "apt-cache": "proxy", + "apt-mirror": "mirror", + "apt-mirror-airgapped": "mirror", +} + + +class ScenarioSpec(BaseModel): + """The composed lab: catalog picks the renderer turns into a scenario tree.""" + + model_config = _STRICT + + schema_version: Literal[1] = 1 + name: str = Field(pattern=C.SCENARIO_NAME_RE.pattern) + subnet_layout: str = Field(pattern=C.TEMPLATE_ID_RE.pattern) + # Optional catalog network policy id. When set, the renderer emits + # 05_network_isolation/_main.yml targeting proxmox-cli that flush-rebuilds + # the R42-FORWARD iptables chain from the compiled policy rules. + network_policy: str | None = Field(default=None, pattern=C.TEMPLATE_ID_RE.pattern) + boxes: list[BoxSpec] = Field(min_length=1) + proxmox_node: str | None = Field(default=None, pattern=C.PROXMOX_NODE_RE.pattern) + notes: str = Field(default="", max_length=255) + services: ServicesSpec | None = Field(default=None) + + _guard_name = field_validator("name")(_no_injection) + _guard_notes = field_validator("notes")(_no_injection) + + @model_validator(mode="after") + def _auto_wire_apt_services(self) -> "ScenarioSpec": + """Auto-inject services.apt when exactly one apt-provider box is present. + + Only fires when services is None (not set). Explicit services: {apt: null} + is respected as an intentional opt-out. + """ + if self.services is not None: + return self + providers = [b.template for b in self.boxes if b.template in _APT_PROVIDER_MODES] + if len(providers) != 1: + return self + tpl = providers[0] + self.services = ServicesSpec(apt=AptServiceSpec( + box=tpl, wire_to="all", mode=_APT_PROVIDER_MODES[tpl] + )) + return self + + +def dumps_spec(spec: ScenarioSpec) -> str: + """Serialize *spec* to canonical, sorted, newline-terminated YAML.""" + payload = spec.model_dump(mode="json") + return yaml.safe_dump( + payload, + sort_keys=True, + default_flow_style=False, + allow_unicode=True, + ) + + +def dump_spec_atomic(spec: ScenarioSpec, path: Path | str) -> Path: + """Atomically write *spec* to *path* as canonical YAML. Returns the path.""" + return io.atomic_write_text(dumps_spec(spec), path) + + +def load_spec(path: Path | str) -> ScenarioSpec: + """Load and validate a ``scenario.r42.yml`` from *path*. + + :raises TopologyError: if the file is missing, not valid YAML, or fails schema. + """ + path = Path(path) + try: + raw = path.read_text(encoding="utf-8") + except OSError as exc: + raise TopologyError(f"cannot read scenario spec: {path}") from exc + try: + data = yaml.safe_load(raw) + except yaml.YAMLError as exc: + raise TopologyError(f"invalid YAML in scenario spec: {path}") from exc + if not isinstance(data, dict): + raise TopologyError(f"scenario spec must be a mapping: {path}") + try: + return ScenarioSpec.model_validate(data) + except _PydanticValidationError as exc: + raise TopologyError(f"scenario spec schema error in {path}: {exc}") from exc diff --git a/r42playbooks/core/validate.py b/r42playbooks/core/validate.py new file mode 100644 index 00000000..b81f6dc7 --- /dev/null +++ b/r42playbooks/core/validate.py @@ -0,0 +1,79 @@ +"""Semantic validation of a topology against itself and the catalog. + +Schema-level constraints (patterns, ranges) are handled by the pydantic models. +This module covers cross-field semantics that pydantic cannot express alone: +referential integrity (zone->subnet, box->zone), IP-in-subnet, and dangling +catalog references. Returns a list of human-readable problems ([] == valid); +never raises. +""" + +import ipaddress + +from r42playbooks.core.catalog import Catalog +from r42playbooks.core.models import Topology + + +def zone_subnet_map(topology: Topology) -> dict[str, str]: + """Map zone name -> subnet cidr (only for zones whose subnet exists).""" + subnet_cidr = {s.name: s.cidr for s in topology.subnets} + return { + z.name: subnet_cidr[z.subnet] + for z in topology.zones + if z.subnet in subnet_cidr + } + + +def zone_bridge_map(topology: Topology) -> dict[str, str]: + """Map zone name -> bridge (only for zones whose subnet exists).""" + subnet_bridge = {s.name: s.bridge for s in topology.subnets} + return { + z.name: subnet_bridge[z.subnet] + for z in topology.zones + if z.subnet in subnet_bridge + } + + +def semantic_problems(topology: Topology, catalog: Catalog) -> list[str]: + """Return human-readable semantic problems with a topology ([] if clean).""" + problems: list[str] = [] + + subnet_names = {s.name for s in topology.subnets} + zone_names = {z.name for z in topology.zones} + + # zones must reference an existing subnet + for z in topology.zones: + if z.subnet not in subnet_names: + problems.append(f"zone {z.name!r} references unknown subnet {z.subnet!r}") + + zsubnet = zone_subnet_map(topology) + + # boxes: zone must exist; ip must fall inside the zone's subnet + for box in topology.boxes: + if box.zone not in zone_names: + problems.append(f"box {box.vm_name!r} references unknown zone {box.zone!r}") + continue + cidr = zsubnet.get(box.zone) + if cidr is None: + continue # zone's subnet already reported missing + try: + net = ipaddress.ip_network(cidr, strict=True) + if ipaddress.ip_address(box.ip) not in net: + problems.append( + f"box {box.vm_name!r} ip {box.ip} not in zone {box.zone!r} subnet {cidr}" + ) + except ValueError as exc: + problems.append(f"box {box.vm_name!r}: {exc}") + + # box_template must resolve in the catalog + if box.box_template not in catalog.box_templates: + problems.append( + f"box {box.vm_name!r} references unknown box_template {box.box_template!r}" + ) + + # network policy template must resolve + if topology.network_policy.template not in catalog.network_policies: + problems.append( + f"unknown network_policy template {topology.network_policy.template!r}" + ) + + return problems diff --git a/r42playbooks/tui/__init__.py b/r42playbooks/tui/__init__.py new file mode 100644 index 00000000..b735a8eb --- /dev/null +++ b/r42playbooks/tui/__init__.py @@ -0,0 +1,6 @@ +"""Textual TUI for r42playbooks. + +The interactive authoring frontend. All non-view logic lives in +``TuiController`` (pure, framework-light) so it can be unit-tested without the +event loop and shared with the range42 deployment TUI rewrite. +""" diff --git a/r42playbooks/tui/app.py b/r42playbooks/tui/app.py new file mode 100644 index 00000000..58a9eebf --- /dev/null +++ b/r42playbooks/tui/app.py @@ -0,0 +1,218 @@ +"""Textual TUI view — thin shell over ScenarioComposerController. + +Compose a lab: name it, pick a subnet layout + network policy, add boxes (with a +count), preview the allocation, and generate a deployable ``scenarios//`` +tree. All logic is delegated to ScenarioComposerController (tested separately). + +Operated the standard way — **mouse or keyboard**: click a button, or Tab between +fields and press Enter/Space on the focused button; type in the inputs; use the +arrow keys to open and choose in the dropdowns. No custom key shortcuts. +""" + +from pathlib import Path + +from textual.app import App, ComposeResult +from textual.containers import Horizontal, VerticalScroll +from textual.widgets import Button, Footer, Header, Input, Label, Select, Static + +from r42playbooks.core.errors import ScenarioExistsError, TopologyError +from r42playbooks.tui.controller import ScenarioComposerController + +_DEFAULT_COUNT = "1" + + +class ScenarioComposerApp(App): + """Interactive scenario composer for r42playbooks.""" + + CSS = """ + #body { height: 1fr; } + #form { width: 60; height: 1fr; padding: 1; } + #form Button { height: 1; min-height: 1; border: none; margin: 0; width: 100%; } + #output { width: 1fr; height: 1fr; padding: 1; border: round $accent; } + Select, Input { width: 1fr; } + .row { height: auto; } + #count { width: 8; } + #octet { width: 10; } + """ + + def __init__( + self, controller: ScenarioComposerController, *, out_dir: Path | None = None + ) -> None: + super().__init__() + self.controller = controller + self.out_dir = out_dir or Path("scenarios") + self._pending_overwrite = ( + False # set after an exists-warning; next Generate overwrites + ) + + def _select(self, options: list[str], widget_id: str) -> Select: + """A Select that defaults to its first option (no blank/prompt state).""" + opts = [(x, x) for x in options] + if opts: + return Select(opts, id=widget_id, allow_blank=False, value=opts[0][1]) + return Select(opts, id=widget_id) + + def _policy_select(self) -> Select: + """Build the policy Select (optional — first option is blank/none).""" + opts = [("(none — no isolation)", "")] + [ + (p, p) for p in self.controller.policies() + ] + return Select(opts, id="policy", allow_blank=False, value="") + + def _subnet_select(self) -> Select: + """Build the subnet Select populated from the current layout selection.""" + layout = self._current_layout_id() + subnets = self.controller.subnets_for_layout(layout) if layout else [] + opts = [(f"{name} {cidr}", name) for name, cidr in subnets] + if opts: + return Select(opts, id="subnet", allow_blank=False, value=opts[0][1]) + return Select(opts, id="subnet", allow_blank=True) + + def _current_layout_id(self) -> str | None: + try: + value = self.query_one("#layout", Select).value + return None if value is Select.BLANK else str(value) + except Exception: + return None + + def _refresh_subnet_options(self) -> None: + """Repopulate #subnet when the chosen layout changes.""" + layout = self._current_layout_id() + subnets = self.controller.subnets_for_layout(layout) if layout else [] + opts = [(f"{name} {cidr}", name) for name, cidr in subnets] + sel = self.query_one("#subnet", Select) + sel.set_options(opts) + if opts: + sel.value = opts[0][1] + + def compose(self) -> ComposeResult: + yield Header() + with Horizontal(id="body"): + with VerticalScroll(id="form"): + yield Label("Scenario name") + yield Input(placeholder="my_lab", id="scenario") + yield Label("Subnet layout") + yield self._select(self.controller.layouts(), "layout") + yield Label("Network policy (optional)") + yield self._policy_select() + yield Label("Box (template · count · start octet · subnet)") + with Horizontal(classes="row"): + yield self._select(self.controller.box_templates(), "box") + yield Input(value=_DEFAULT_COUNT, id="count") + yield Input(placeholder="octet", id="octet") + yield self._subnet_select() + yield Button("Add box", id="add", variant="primary") + yield Button("Preview", id="preview") + yield Button("Generate", id="generate", variant="success") + yield Button("Clear boxes", id="clear", variant="warning") + yield Button("Quit", id="quit", variant="error") + yield Static( + "Click a button, or Tab between fields and press Enter on a button.", + id="output", + ) + yield Footer() + + def on_mount(self) -> None: + # Focus the name field so typing works immediately. + self.query_one("#scenario", Input).focus() + # Populate subnet options from the initial layout selection. + self._refresh_subnet_options() + + # -- helpers -- + + def _set_output(self, text: str) -> None: + self.query_one("#output", Static).update(text) + + def _selected(self, widget_id: str) -> str | None: + value = self.query_one(f"#{widget_id}", Select).value + return None if value is Select.BLANK else str(value) + + def _count(self) -> int: + try: + return max(1, int(self.query_one("#count", Input).value or _DEFAULT_COUNT)) + except ValueError: + return 1 + + def _octet(self) -> int | None: + raw = self.query_one("#octet", Input).value.strip() + try: + v = int(raw) + return v if 1 <= v <= 254 else None + except ValueError: + return None + + def _sync_header_fields(self) -> None: + """Push the name/layout/policy widgets into the controller.""" + self.controller.set_name(self.query_one("#scenario", Input).value) + if (layout := self._selected("layout")) is not None: + self.controller.set_subnet(layout) + policy = self._selected("policy") + self.controller.set_policy(policy or "") + + # -- events -- + + def on_select_changed(self, event: Select.Changed) -> None: + if event.select.id == "layout": + self._refresh_subnet_options() + + def on_button_pressed(self, event: Button.Pressed) -> None: + """Handle every button — fired by a mouse click or Enter/Space when focused.""" + handlers = { + "add": self._do_add, + "preview": self._do_preview, + "generate": self._do_generate, + "clear": self._do_clear, + "quit": self.exit, + } + handler = handlers.get(event.button.id) + if handler is None: + return + try: + handler() + except Exception as exc: # never let a handler silently kill the app + self._set_output(f"✗ unexpected error: {exc!r}") + + def _do_add(self) -> None: + template = self._selected("box") + if template is None: + self._set_output("⚠ pick a box template first") + return + subnet = self._selected("subnet") + octet = self._octet() + self.controller.add_box(template, self._count(), subnet, octet) + self._do_preview() + + def _do_clear(self) -> None: + self.controller.clear_boxes() + self._do_preview() + + def _do_preview(self) -> None: + self._sync_header_fields() + self._set_output(self.controller.preview()) + + def _do_generate(self) -> None: + self._sync_header_fields() + try: + root = self.controller.generate( + self.out_dir, overwrite=self._pending_overwrite + ) + except ScenarioExistsError as exc: + self._pending_overwrite = True + self._set_output(f"⚠ {exc}\n press Generate again to overwrite it.") + return + except TopologyError as exc: + self._set_output(f"✗ {exc}") + return + self._pending_overwrite = False + self._set_output(f"✓ generated {root}") + + +def main() -> None: # pragma: no cover - manual entry point + import sys + + catalog = Path(sys.argv[1]) if len(sys.argv) > 1 else Path("../range42-catalog") + ScenarioComposerApp(ScenarioComposerController(catalog)).run() + + +if __name__ == "__main__": # pragma: no cover + main() diff --git a/r42playbooks/tui/controller.py b/r42playbooks/tui/controller.py new file mode 100644 index 00000000..d4da892d --- /dev/null +++ b/r42playbooks/tui/controller.py @@ -0,0 +1,188 @@ +"""Pure controller behind the Textual TUI — no Textual imports here. + +Wraps the frozen ``r42playbooks.api`` so the view stays a thin shell and the +compose→generate logic is unit-testable (and reusable by the range42 deployment +TUI). Holds the in-progress composition: a scenario name, a subnet layout, a +network policy, and an ordered list of boxes (template + count). +""" + +from pathlib import Path + +from pydantic import ValidationError as _PydValidationError + +from r42playbooks import api +from r42playbooks.core.allocate import allocate +from r42playbooks.core.errors import TopologyError +from r42playbooks.core.idalloc import ReservedIndex +from r42playbooks.core.spec import ScenarioSpec + + +class ScenarioComposerController: + """Stateful façade the TUI drives: pick layout/policy, add boxes, generate.""" + + def __init__(self, catalog_root: Path, reserved_path: Path | None = None) -> None: + self.catalog = api.load_catalog(catalog_root) + self.reserved: ReservedIndex | None = ( + ReservedIndex.from_file(reserved_path) if reserved_path else None + ) + self.name: str = "" + self.subnet_layout: str = "" + self.network_policy: str = "" + self._boxes: list[tuple[str, int, str | None, int | None]] = [] + + # -- catalog choices -- + + def layouts(self) -> list[str]: + return sorted(self.catalog.subnet_layouts) + + def policies(self) -> list[str]: + return sorted(self.catalog.network_policies) + + def box_templates(self) -> list[str]: + return sorted(self.catalog.box_templates) + + def roles(self) -> list[str]: + return sorted(self.catalog.roles) + + def containers(self) -> list[str]: + return sorted(self.catalog.containers) + + def subnets_for_layout(self, layout_id: str) -> list[tuple[str, str]]: + """Return (name, cidr) pairs for subnets in *layout_id* (empty if unknown).""" + layout = self.catalog.subnet_layouts.get(layout_id) + return [(s.name, s.cidr) for s in layout.subnets] if layout else [] + + # -- composition state -- + + @property + def boxes(self) -> list[tuple[str, int, str | None, int | None]]: + """The composed (template, count, subnet, octet) 4-tuples, in insertion order (a copy).""" + return list(self._boxes) + + def set_name(self, name: str) -> None: + self.name = name.strip() + + def set_subnet(self, layout_id: str) -> None: + self.subnet_layout = layout_id + + def set_policy(self, policy_id: str) -> None: + self.network_policy = policy_id + + def add_box(self, template: str, count: int = 1, subnet: str | None = None, octet: int | None = None) -> None: + self._boxes.append((template, count, subnet, octet)) + + def remove_box(self, index: int) -> None: + self._boxes = [b for i, b in enumerate(self._boxes) if i != index] + + def clear_boxes(self) -> None: + self._boxes = [] + + # -- spec / validation -- + + def _missing(self) -> list[str]: + """Structural gaps that stop a spec from being built (pre-schema).""" + problems: list[str] = [] + if not self.name: + problems.append("scenario name is required") + if not self.subnet_layout: + problems.append("a subnet layout must be selected") + if not self._boxes: + problems.append("add at least one box") + return problems + + def build_spec(self) -> ScenarioSpec: + """Assemble the in-progress composition into a ``ScenarioSpec``. + + :raises TopologyError: if the composition is incomplete or schema-invalid. + """ + missing = self._missing() + if missing: + raise TopologyError("; ".join(missing)) + boxes = [] + for t, c, s, o in self._boxes: + entry: dict = {"template": t, "count": c} + if s: + entry["subnet"] = s + if o is not None: + entry["octet"] = o + boxes.append(entry) + data = { + "name": self.name, + "subnet_layout": self.subnet_layout, + "boxes": boxes, + } + if self.network_policy: # optional + ignored by the generator + data["network_policy"] = self.network_policy + try: + return ScenarioSpec.model_validate(data) + except _PydValidationError as exc: + raise TopologyError(f"invalid composition: {exc}") from exc + + def validate(self) -> list[str]: + """Return all problems with the current composition ([] == ready).""" + missing = self._missing() + if missing: + return missing + try: + spec = self.build_spec() + except TopologyError as exc: + return [str(exc)] + return api.validate_refs(spec, self.catalog) + + # -- preview / generate -- + + def preview(self) -> str: + """A plain-text preview of the current composition. + + Always shows the picked header + the boxes added so far (so every + ``add_box`` gives visible feedback), then the readiness verdict or the + allocated VMs. Never raises: validation gaps and allocation errors are + returned as text so the TUI shows them in-pane instead of crashing. + """ + total_vms = sum(count for _t, count, _s, _o in self._boxes) + lines = [ + f"name: {self.name or '(unset)'}", + f"subnet layout: {self.subnet_layout or '(unset)'}", + f"network policy:{' ' + self.network_policy if self.network_policy else ' (none)'}", + f"boxes: {total_vms} VM(s) from {len(self._boxes)} pick(s)", + ] + lines += [ + f" - {t} ×{c}" + (f" → {s}" if s else "") + (f" @.{o}" if o is not None else "") + for t, c, s, o in self._boxes + ] + + problems = self.validate() + if problems: + lines += ["", "not ready:"] + [f" ✗ {p}" for p in problems] + return "\n".join(lines) + + try: + spec = self.build_spec() + alloc = allocate(spec, self.catalog, self.reserved) + except TopologyError as exc: + lines += ["", f"✗ cannot allocate: {exc}"] + return "\n".join(lines) + + lines += ["", "ready — allocation:"] + lines += [ + f" - {b.vm_name} id={b.vm_id} ip={b.ip} subnet={b.subnet_name}" for b in alloc.boxes + ] + return "\n".join(lines) + + def generate(self, dest: Path, *, overwrite: bool = False) -> Path: + """Render the composition into ``dest//`` and return that path. + + :param overwrite: replace an existing ``dest//`` (default False). + :raises ScenarioExistsError: target exists and ``overwrite`` is False. + :raises TopologyError: if the composition is incomplete/invalid or a ref + cannot be resolved/placed. + """ + spec = self.build_spec() + dest = Path(dest) + reserved = self.reserved + if reserved is None: + auto = dest / "_reserved.json" + if auto.is_file(): + reserved = ReservedIndex.from_file(auto) + return api.render_scenario(spec, catalog=self.catalog, dest=dest, + reserved=reserved, overwrite=overwrite) diff --git a/scenarios/_reserved.json b/scenarios/_reserved.json index 8111e470..9f39649d 100644 --- a/scenarios/_reserved.json +++ b/scenarios/_reserved.json @@ -118,6 +118,27 @@ {"vm_id":9244,"vm_name":"template-vm-large-04-8g-64g","spec":"4cpu/8gb/64gb","ip":"192.168.140.244","bridge":"vmbr140","scenario":"demo_lab","role":"template"} {"vm_id":9246,"vm_name":"template-vm-large-06-8g-64g","spec":"6cpu/8gb/64gb","ip":"192.168.140.246","bridge":"vmbr140","scenario":"demo_lab","role":"template"} {"vm_id":9248,"vm_name":"template-vm-large-08-8g-64g","spec":"8cpu/8gb/64gb","ip":"192.168.140.248","bridge":"vmbr140","scenario":"demo_lab","role":"template"} +{"vm_id":1000,"vm_name":"admin-wazuh","ip":"192.168.142.100","role":"admin","bridge":"vmbr142","scenario":"demo_lab_network"} +{"vm_id":1020,"vm_name":"admin-deployer-api-gateway","ip":"192.168.142.120","role":"admin","bridge":"vmbr142","scenario":"demo_lab_network"} +{"vm_id":1021,"vm_name":"admin-deployer-api-backend","ip":"192.168.142.121","role":"admin","bridge":"vmbr142","scenario":"demo_lab_network"} +{"vm_id":1023,"vm_name":"admin-deployer-ui","ip":"192.168.142.123","role":"admin","bridge":"vmbr142","scenario":"demo_lab_network"} +{"vm_id":4000,"vm_name":"vuln-box-00","ip":"192.168.144.170","role":"ctf","bridge":"vmbr144","scenario":"demo_lab_network"} +{"vm_id":4001,"vm_name":"vuln-box-01","ip":"192.168.144.171","role":"ctf","bridge":"vmbr144","scenario":"demo_lab_network"} +{"vm_id":4002,"vm_name":"vuln-box-02","ip":"192.168.144.172","role":"ctf","bridge":"vmbr144","scenario":"demo_lab_network"} +{"vm_id":4003,"vm_name":"vuln-box-03","ip":"192.168.144.173","role":"ctf","bridge":"vmbr144","scenario":"demo_lab_network"} +{"vm_id":4004,"vm_name":"vuln-box-04","ip":"192.168.144.174","role":"ctf","bridge":"vmbr144","scenario":"demo_lab_network"} +{"vm_id":9901,"vm_name":"template-vm-nano","spec":"1cpu/1gb/16gb","ip":"192.168.140.201","bridge":"vmbr140","scenario":"demo_lab_network","role":"template"} +{"vm_id":9211,"vm_name":"template-vm-micro-01-2g-24g","spec":"1cpu/2gb/24gb","ip":"192.168.140.211","bridge":"vmbr140","scenario":"demo_lab_network","role":"template"} +{"vm_id":9212,"vm_name":"template-vm-micro-02-2g-24g","spec":"1cpu/2gb/24gb","ip":"192.168.140.212","bridge":"vmbr140","scenario":"demo_lab_network","role":"template"} +{"vm_id":9221,"vm_name":"template-vm-small-01-4g-32g","spec":"1cpu/4gb/32gb","ip":"192.168.140.221","bridge":"vmbr140","scenario":"demo_lab_network","role":"template"} +{"vm_id":9222,"vm_name":"template-vm-small-02-4g-32g","spec":"1cpu/4gb/32gb","ip":"192.168.140.222","bridge":"vmbr140","scenario":"demo_lab_network","role":"template"} +{"vm_id":9224,"vm_name":"template-vm-small-04-4g-32g","spec":"1cpu/4gb/32gb","ip":"192.168.140.224","bridge":"vmbr140","scenario":"demo_lab_network","role":"template"} +{"vm_id":9232,"vm_name":"template-vm-medium-02-8g-64g","spec":"2cpu/8gb/64gb","ip":"192.168.140.232","bridge":"vmbr140","scenario":"demo_lab_network","role":"template"} +{"vm_id":9234,"vm_name":"template-vm-medium-04-8g-64g","spec":"4cpu/8gb/64gb","ip":"192.168.140.234","bridge":"vmbr140","scenario":"demo_lab_network","role":"template"} +{"vm_id":9236,"vm_name":"template-vm-medium-06-8g-64g","spec":"6cpu/8gb/64gb","ip":"192.168.140.236","bridge":"vmbr140","scenario":"demo_lab_network","role":"template"} +{"vm_id":9244,"vm_name":"template-vm-large-04-8g-64g","spec":"4cpu/8gb/64gb","ip":"192.168.140.244","bridge":"vmbr140","scenario":"demo_lab_network","role":"template"} +{"vm_id":9246,"vm_name":"template-vm-large-06-8g-64g","spec":"6cpu/8gb/64gb","ip":"192.168.140.246","bridge":"vmbr140","scenario":"demo_lab_network","role":"template"} +{"vm_id":9248,"vm_name":"template-vm-large-08-8g-64g","spec":"8cpu/8gb/64gb","ip":"192.168.140.248","bridge":"vmbr140","scenario":"demo_lab_network","role":"template"} {"vm_id":1190,"vm_name":"dev-deployer-ui","ip":"192.168.142.190","role":"dev-deployer-ui-lab","bridge":"vmbr142","scenario":"dev_deployer_ui_lab"} {"vm_id":1191,"vm_name":"dev-backend","ip":"192.168.142.191","role":"dev-deployer-ui-lab","bridge":"vmbr142","scenario":"dev_deployer_ui_lab"} {"vm_id":1192,"vm_name":"dev-kong","ip":"192.168.142.192","role":"dev-deployer-ui-lab","bridge":"vmbr142","scenario":"dev_deployer_ui_lab"} @@ -148,3 +169,22 @@ {"vm_id":9232,"vm_name":"template-vm-medium-02-8g-64g","spec":"2cpu/8gb/64gb","ip":"192.168.140.232","bridge":"vmbr140","scenario":"kunai_lab","role":"template"} {"vm_id":1180,"vm_name":"admin-misp-standalone","ip":"192.168.142.180","role":"misp-lab","bridge":"vmbr142","scenario":"misp_lab"} {"vm_id":9232,"vm_name":"template-vm-medium-02-8g-64g","spec":"2cpu/8gb/64gb","ip":"192.168.140.232","bridge":"vmbr140","scenario":"misp_lab","role":"template"} +{"bridge":"vmbr151","image":"debian_trixie","ip":"192.168.151.12","subnet":"lan2","vm_id":1012,"vm_name":"lan2-debian-jump-00","scenario":"test_apt_cacher"} +{"bridge":"vmbr151","image":"ubuntu_resolute","ip":"192.168.151.13","subnet":"lan2","vm_id":1013,"vm_name":"lan2-ubuntu-jump-00","scenario":"test_apt_cacher"} +{"bridge":"vmbr150","image":"debian_trixie","ip":"192.168.150.11","subnet":"lan1","vm_id":2011,"vm_name":"lan1-apt-cache-00","scenario":"test_apt_cacher"} +{"bridge":"vmbr140","image":"debian_trixie","ip":"192.168.140.2","spec":"1cpu/4gb/32gb","vm_id":9321,"vm_name":"template-vm-debian-trixie-small","scenario":"test_apt_cacher","role":"template"} +{"bridge":"vmbr140","image":"ubuntu_resolute","ip":"192.168.140.3","spec":"1cpu/4gb/32gb","vm_id":9521,"vm_name":"template-vm-ubuntu-resolute-small-01-4g-32g","scenario":"test_apt_cacher","role":"template"} +{"bridge":"vmbr151","image":"debian_trixie","ip":"192.168.151.12","subnet":"lan2","vm_id":1012,"vm_name":"lan2-debian-jump-00","scenario":"test_apt_mirror"} +{"bridge":"vmbr151","image":"ubuntu_resolute","ip":"192.168.151.13","subnet":"lan2","vm_id":1013,"vm_name":"lan2-ubuntu-jump-00","scenario":"test_apt_mirror"} +{"bridge":"vmbr150","image":"debian_trixie","ip":"192.168.150.11","subnet":"lan1","vm_id":2011,"vm_name":"lan1-apt-mirror-00","scenario":"test_apt_mirror"} +{"bridge":"vmbr140","image":"debian_trixie","ip":"192.168.140.3","spec":"1cpu/4gb/32gb","vm_id":9321,"vm_name":"template-vm-debian-trixie-small","scenario":"test_apt_mirror","role":"template"} +{"bridge":"vmbr140","image":"debian_trixie","ip":"192.168.140.2","spec":"4cpu/16gb/500gb","vm_id":9341,"vm_name":"template-vm-debian-trixie-large","scenario":"test_apt_mirror","role":"template"} +{"bridge":"vmbr140","image":"ubuntu_resolute","ip":"192.168.140.4","spec":"1cpu/4gb/32gb","vm_id":9521,"vm_name":"template-vm-ubuntu-resolute-small-01-4g-32g","scenario":"test_apt_mirror","role":"template"} +{"bridge":"vmbr150","image":"debian_trixie","ip":"192.168.150.2","subnet":"lan1","vm_id":1002,"vm_name":"lan1-admin-rocketchat-00","scenario":"test_rocketchat"} +{"bridge":"vmbr151","image":"debian_trixie","ip":"192.168.151.2","subnet":"lan2","vm_id":3002,"vm_name":"lan2-debian-jump-00","scenario":"test_rocketchat"} +{"bridge":"vmbr140","image":"debian_trixie","ip":"192.168.140.3","spec":"1cpu/4gb/32gb","vm_id":9321,"vm_name":"template-vm-debian-trixie-small","scenario":"test_rocketchat","role":"template"} +{"bridge":"vmbr140","image":"debian_trixie","ip":"192.168.140.2","spec":"2cpu/8gb/64gb","vm_id":9331,"vm_name":"template-vm-debian-trixie-medium","scenario":"test_rocketchat","role":"template"} +{"bridge":"vmbr150","image":"debian_trixie","ip":"192.168.150.3","subnet":"lan1","vm_id":1003,"vm_name":"lan1-admin-mattermost-00","scenario":"test_mattermost"} +{"bridge":"vmbr151","image":"debian_trixie","ip":"192.168.151.3","subnet":"lan2","vm_id":3003,"vm_name":"lan2-debian-jump-00","scenario":"test_mattermost"} +{"bridge":"vmbr140","image":"debian_trixie","ip":"192.168.140.3","spec":"1cpu/4gb/32gb","vm_id":9321,"vm_name":"template-vm-debian-trixie-small","scenario":"test_mattermost","role":"template"} +{"bridge":"vmbr140","image":"debian_trixie","ip":"192.168.140.2","spec":"2cpu/8gb/64gb","vm_id":9331,"vm_name":"template-vm-debian-trixie-medium","scenario":"test_mattermost","role":"template"} diff --git a/scenarios/blank_scenario_2_subnets/01_admin_infrastructure/stage_00/bs2_admin_deployer_api_backend.yml b/scenarios/blank_scenario_2_subnets/01_admin_infrastructure/stage_00/bs2_admin_deployer_api_backend.yml index 96fbb276..292027f7 100644 --- a/scenarios/blank_scenario_2_subnets/01_admin_infrastructure/stage_00/bs2_admin_deployer_api_backend.yml +++ b/scenarios/blank_scenario_2_subnets/01_admin_infrastructure/stage_00/bs2_admin_deployer_api_backend.yml @@ -85,7 +85,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "{{ global_vm_ci_ip }}" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.142.1" diff --git a/scenarios/blank_scenario_2_subnets/01_admin_infrastructure/stage_00/bs2_admin_deployer_api_gateway.yml b/scenarios/blank_scenario_2_subnets/01_admin_infrastructure/stage_00/bs2_admin_deployer_api_gateway.yml index 19821319..d9795d8b 100644 --- a/scenarios/blank_scenario_2_subnets/01_admin_infrastructure/stage_00/bs2_admin_deployer_api_gateway.yml +++ b/scenarios/blank_scenario_2_subnets/01_admin_infrastructure/stage_00/bs2_admin_deployer_api_gateway.yml @@ -85,7 +85,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "{{ global_vm_ci_ip }}" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.142.1" diff --git a/scenarios/blank_scenario_2_subnets/01_admin_infrastructure/stage_00/bs2_admin_deployer_ui.yml b/scenarios/blank_scenario_2_subnets/01_admin_infrastructure/stage_00/bs2_admin_deployer_ui.yml index 39467841..e6bb2972 100644 --- a/scenarios/blank_scenario_2_subnets/01_admin_infrastructure/stage_00/bs2_admin_deployer_ui.yml +++ b/scenarios/blank_scenario_2_subnets/01_admin_infrastructure/stage_00/bs2_admin_deployer_ui.yml @@ -85,7 +85,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "{{ global_vm_ci_ip }}" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.142.1" diff --git a/scenarios/blank_scenario_2_subnets/01_admin_infrastructure/stage_00/bs2_admin_wazuh.yml b/scenarios/blank_scenario_2_subnets/01_admin_infrastructure/stage_00/bs2_admin_wazuh.yml index 78ad9a24..29b1fa2e 100644 --- a/scenarios/blank_scenario_2_subnets/01_admin_infrastructure/stage_00/bs2_admin_wazuh.yml +++ b/scenarios/blank_scenario_2_subnets/01_admin_infrastructure/stage_00/bs2_admin_wazuh.yml @@ -85,7 +85,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "{{ global_vm_ci_ip }}" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.142.1" diff --git a/scenarios/blank_scenario_2_subnets/01_init_proxmox/templates/ubuntu_noble/00-template-vm-nano.yml b/scenarios/blank_scenario_2_subnets/01_init_proxmox/templates/ubuntu_noble/00-template-vm-nano.yml index f35071f0..94a73508 100644 --- a/scenarios/blank_scenario_2_subnets/01_init_proxmox/templates/ubuntu_noble/00-template-vm-nano.yml +++ b/scenarios/blank_scenario_2_subnets/01_init_proxmox/templates/ubuntu_noble/00-template-vm-nano.yml @@ -137,7 +137,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_trainee_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_trainee_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "192.168.140.201" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.140.1" diff --git a/scenarios/blank_scenario_2_subnets/01_init_proxmox/templates/ubuntu_noble/01-template-vm-micro-01-2g-24g.yml b/scenarios/blank_scenario_2_subnets/01_init_proxmox/templates/ubuntu_noble/01-template-vm-micro-01-2g-24g.yml index 3fcef71d..1b84e401 100644 --- a/scenarios/blank_scenario_2_subnets/01_init_proxmox/templates/ubuntu_noble/01-template-vm-micro-01-2g-24g.yml +++ b/scenarios/blank_scenario_2_subnets/01_init_proxmox/templates/ubuntu_noble/01-template-vm-micro-01-2g-24g.yml @@ -137,7 +137,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_trainee_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_trainee_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "192.168.140.211" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.140.1" diff --git a/scenarios/blank_scenario_2_subnets/01_init_proxmox/templates/ubuntu_noble/01-template-vm-micro-02-2g-24g.yml b/scenarios/blank_scenario_2_subnets/01_init_proxmox/templates/ubuntu_noble/01-template-vm-micro-02-2g-24g.yml index 6c962ee4..1e0a5694 100644 --- a/scenarios/blank_scenario_2_subnets/01_init_proxmox/templates/ubuntu_noble/01-template-vm-micro-02-2g-24g.yml +++ b/scenarios/blank_scenario_2_subnets/01_init_proxmox/templates/ubuntu_noble/01-template-vm-micro-02-2g-24g.yml @@ -137,7 +137,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_trainee_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_trainee_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "192.168.140.212" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.140.1" diff --git a/scenarios/blank_scenario_2_subnets/01_init_proxmox/templates/ubuntu_noble/02-template-vm-small-01-4g-32g.yml b/scenarios/blank_scenario_2_subnets/01_init_proxmox/templates/ubuntu_noble/02-template-vm-small-01-4g-32g.yml index 046a2234..dde76262 100644 --- a/scenarios/blank_scenario_2_subnets/01_init_proxmox/templates/ubuntu_noble/02-template-vm-small-01-4g-32g.yml +++ b/scenarios/blank_scenario_2_subnets/01_init_proxmox/templates/ubuntu_noble/02-template-vm-small-01-4g-32g.yml @@ -137,7 +137,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_trainee_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_trainee_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "192.168.140.221" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.140.1" diff --git a/scenarios/blank_scenario_2_subnets/01_init_proxmox/templates/ubuntu_noble/02-template-vm-small-02-4g-32g.yml b/scenarios/blank_scenario_2_subnets/01_init_proxmox/templates/ubuntu_noble/02-template-vm-small-02-4g-32g.yml index 7bb1ee66..f5dd439b 100644 --- a/scenarios/blank_scenario_2_subnets/01_init_proxmox/templates/ubuntu_noble/02-template-vm-small-02-4g-32g.yml +++ b/scenarios/blank_scenario_2_subnets/01_init_proxmox/templates/ubuntu_noble/02-template-vm-small-02-4g-32g.yml @@ -137,7 +137,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_trainee_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_trainee_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "192.168.140.222" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.140.1" diff --git a/scenarios/blank_scenario_2_subnets/01_init_proxmox/templates/ubuntu_noble/02-template-vm-small-04-4g-32g.yml b/scenarios/blank_scenario_2_subnets/01_init_proxmox/templates/ubuntu_noble/02-template-vm-small-04-4g-32g.yml index 25e53e40..57c434ac 100644 --- a/scenarios/blank_scenario_2_subnets/01_init_proxmox/templates/ubuntu_noble/02-template-vm-small-04-4g-32g.yml +++ b/scenarios/blank_scenario_2_subnets/01_init_proxmox/templates/ubuntu_noble/02-template-vm-small-04-4g-32g.yml @@ -137,7 +137,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_trainee_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_trainee_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "192.168.140.224" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.140.1" diff --git a/scenarios/blank_scenario_2_subnets/01_init_proxmox/templates/ubuntu_noble/03-template-vm-medium-02-8g-64g.yml b/scenarios/blank_scenario_2_subnets/01_init_proxmox/templates/ubuntu_noble/03-template-vm-medium-02-8g-64g.yml index a6f7c449..0f2339c6 100644 --- a/scenarios/blank_scenario_2_subnets/01_init_proxmox/templates/ubuntu_noble/03-template-vm-medium-02-8g-64g.yml +++ b/scenarios/blank_scenario_2_subnets/01_init_proxmox/templates/ubuntu_noble/03-template-vm-medium-02-8g-64g.yml @@ -137,7 +137,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_trainee_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_trainee_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "192.168.140.232" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.140.1" diff --git a/scenarios/blank_scenario_2_subnets/01_init_proxmox/templates/ubuntu_noble/03-template-vm-medium-04-8g-64g.yml b/scenarios/blank_scenario_2_subnets/01_init_proxmox/templates/ubuntu_noble/03-template-vm-medium-04-8g-64g.yml index ffceb4bc..e33d27fe 100644 --- a/scenarios/blank_scenario_2_subnets/01_init_proxmox/templates/ubuntu_noble/03-template-vm-medium-04-8g-64g.yml +++ b/scenarios/blank_scenario_2_subnets/01_init_proxmox/templates/ubuntu_noble/03-template-vm-medium-04-8g-64g.yml @@ -137,7 +137,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_trainee_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_trainee_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "192.168.140.234" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.140.1" diff --git a/scenarios/blank_scenario_2_subnets/01_init_proxmox/templates/ubuntu_noble/03-template-vm-medium-06-8g-64g.yml b/scenarios/blank_scenario_2_subnets/01_init_proxmox/templates/ubuntu_noble/03-template-vm-medium-06-8g-64g.yml index a08ac96e..19713c1c 100644 --- a/scenarios/blank_scenario_2_subnets/01_init_proxmox/templates/ubuntu_noble/03-template-vm-medium-06-8g-64g.yml +++ b/scenarios/blank_scenario_2_subnets/01_init_proxmox/templates/ubuntu_noble/03-template-vm-medium-06-8g-64g.yml @@ -137,7 +137,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_trainee_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_trainee_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "192.168.140.236" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.140.1" diff --git a/scenarios/blank_scenario_2_subnets/01_init_proxmox/templates/ubuntu_noble/04-template-vm-large-04-8g-64g.yml b/scenarios/blank_scenario_2_subnets/01_init_proxmox/templates/ubuntu_noble/04-template-vm-large-04-8g-64g.yml index 630d82fd..ea62d6fb 100644 --- a/scenarios/blank_scenario_2_subnets/01_init_proxmox/templates/ubuntu_noble/04-template-vm-large-04-8g-64g.yml +++ b/scenarios/blank_scenario_2_subnets/01_init_proxmox/templates/ubuntu_noble/04-template-vm-large-04-8g-64g.yml @@ -137,7 +137,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_trainee_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_trainee_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "192.168.140.244" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.140.1" diff --git a/scenarios/blank_scenario_2_subnets/01_init_proxmox/templates/ubuntu_noble/04-template-vm-large-06-8g-64g.yml b/scenarios/blank_scenario_2_subnets/01_init_proxmox/templates/ubuntu_noble/04-template-vm-large-06-8g-64g.yml index 60622fc5..02e1e257 100644 --- a/scenarios/blank_scenario_2_subnets/01_init_proxmox/templates/ubuntu_noble/04-template-vm-large-06-8g-64g.yml +++ b/scenarios/blank_scenario_2_subnets/01_init_proxmox/templates/ubuntu_noble/04-template-vm-large-06-8g-64g.yml @@ -137,7 +137,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_trainee_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_trainee_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "192.168.140.246" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.140.1" diff --git a/scenarios/blank_scenario_2_subnets/01_init_proxmox/templates/ubuntu_noble/04-template-vm-large-08-8g-64g.yml b/scenarios/blank_scenario_2_subnets/01_init_proxmox/templates/ubuntu_noble/04-template-vm-large-08-8g-64g.yml index b464a6ab..3c2dd4fe 100644 --- a/scenarios/blank_scenario_2_subnets/01_init_proxmox/templates/ubuntu_noble/04-template-vm-large-08-8g-64g.yml +++ b/scenarios/blank_scenario_2_subnets/01_init_proxmox/templates/ubuntu_noble/04-template-vm-large-08-8g-64g.yml @@ -137,7 +137,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_trainee_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_trainee_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "192.168.140.248" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.140.1" diff --git a/scenarios/blank_scenario_2_subnets/01_init_proxmox/templates/ubuntu_noble/test_setup_templates.yml b/scenarios/blank_scenario_2_subnets/01_init_proxmox/templates/ubuntu_noble/test_setup_templates.yml index 74956397..e6aaee03 100644 --- a/scenarios/blank_scenario_2_subnets/01_init_proxmox/templates/ubuntu_noble/test_setup_templates.yml +++ b/scenarios/blank_scenario_2_subnets/01_init_proxmox/templates/ubuntu_noble/test_setup_templates.yml @@ -83,7 +83,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_trainee_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_trainee_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "192.168.142.55" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.142.1" diff --git a/scenarios/blank_scenario_2_subnets/02_network_lab/stage_00/bs2_team_143_01.yml b/scenarios/blank_scenario_2_subnets/02_network_lab/stage_00/bs2_team_143_01.yml index e33d9f03..4383274a 100644 --- a/scenarios/blank_scenario_2_subnets/02_network_lab/stage_00/bs2_team_143_01.yml +++ b/scenarios/blank_scenario_2_subnets/02_network_lab/stage_00/bs2_team_143_01.yml @@ -54,7 +54,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "{{ global_vm_ci_ip }}" vm_ci_netmask: "24" vm_ci_ip_gw: "{{ global_vm_ci_ip_gw }}" diff --git a/scenarios/blank_scenario_2_subnets/02_network_lab/stage_00/bs2_team_143_02.yml b/scenarios/blank_scenario_2_subnets/02_network_lab/stage_00/bs2_team_143_02.yml index 49602aeb..1e7ca08c 100644 --- a/scenarios/blank_scenario_2_subnets/02_network_lab/stage_00/bs2_team_143_02.yml +++ b/scenarios/blank_scenario_2_subnets/02_network_lab/stage_00/bs2_team_143_02.yml @@ -54,7 +54,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "{{ global_vm_ci_ip }}" vm_ci_netmask: "24" vm_ci_ip_gw: "{{ global_vm_ci_ip_gw }}" diff --git a/scenarios/blank_scenario_2_subnets/02_network_lab/stage_00/bs2_team_144_01.yml b/scenarios/blank_scenario_2_subnets/02_network_lab/stage_00/bs2_team_144_01.yml index 935e26ae..edde634e 100644 --- a/scenarios/blank_scenario_2_subnets/02_network_lab/stage_00/bs2_team_144_01.yml +++ b/scenarios/blank_scenario_2_subnets/02_network_lab/stage_00/bs2_team_144_01.yml @@ -54,7 +54,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "{{ global_vm_ci_ip }}" vm_ci_netmask: "24" vm_ci_ip_gw: "{{ global_vm_ci_ip_gw }}" diff --git a/scenarios/blank_scenario_2_subnets/02_network_lab/stage_00/bs2_team_144_02.yml b/scenarios/blank_scenario_2_subnets/02_network_lab/stage_00/bs2_team_144_02.yml index 89c69f3e..31db3908 100644 --- a/scenarios/blank_scenario_2_subnets/02_network_lab/stage_00/bs2_team_144_02.yml +++ b/scenarios/blank_scenario_2_subnets/02_network_lab/stage_00/bs2_team_144_02.yml @@ -54,7 +54,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "{{ global_vm_ci_ip }}" vm_ci_netmask: "24" vm_ci_ip_gw: "{{ global_vm_ci_ip_gw }}" diff --git a/scenarios/blank_scenario_2_subnets/templates/ansible-vars.yml b/scenarios/blank_scenario_2_subnets/templates/ansible-vars.yml index 78512b30..fd052812 100644 --- a/scenarios/blank_scenario_2_subnets/templates/ansible-vars.yml +++ b/scenarios/blank_scenario_2_subnets/templates/ansible-vars.yml @@ -33,3 +33,8 @@ default_admin_vm_ci_user: "alice" # student vm user default_trainee_vm_ci_user: "bob" + +#### CLOUD-INIT DNS #### +# default resolver for lab VMs; range42-init.py substitutes the Proxmox node +# resolver here at workspace creation. 1.1.1.1 is the portable fallback. +default_vm_ci_dns_ips: "1.1.1.1" diff --git a/scenarios/blank_scenario_2_subnets/templates/vault-example.yml b/scenarios/blank_scenario_2_subnets/templates/vault-example.yml index 934e85d2..34de8d4c 100644 --- a/scenarios/blank_scenario_2_subnets/templates/vault-example.yml +++ b/scenarios/blank_scenario_2_subnets/templates/vault-example.yml @@ -63,3 +63,8 @@ WAZUH_PASSWORD: "REPLACE_ME" # deployer-cli known_hosts path (not really secret, but stored in vault historically) deployer_cli_user_ssh_known_hosts: "/home/your_deployer_cli_username/.ssh/known_hosts" + +#### CLOUD-INIT DNS #### +# default resolver for lab VMs; range42-init.py substitutes the Proxmox node +# resolver here at workspace creation. 1.1.1.1 is the portable fallback. +default_vm_ci_dns_ips: "1.1.1.1" diff --git a/scenarios/blank_scenario_4_subnets/01_admin_infrastructure/stage_00/bs4_admin_deployer_api_backend.yml b/scenarios/blank_scenario_4_subnets/01_admin_infrastructure/stage_00/bs4_admin_deployer_api_backend.yml index ee3a5ba1..29ad5dde 100644 --- a/scenarios/blank_scenario_4_subnets/01_admin_infrastructure/stage_00/bs4_admin_deployer_api_backend.yml +++ b/scenarios/blank_scenario_4_subnets/01_admin_infrastructure/stage_00/bs4_admin_deployer_api_backend.yml @@ -85,7 +85,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "{{ global_vm_ci_ip }}" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.142.1" diff --git a/scenarios/blank_scenario_4_subnets/01_admin_infrastructure/stage_00/bs4_admin_deployer_api_gateway.yml b/scenarios/blank_scenario_4_subnets/01_admin_infrastructure/stage_00/bs4_admin_deployer_api_gateway.yml index f4ebe0e6..208b8dc3 100644 --- a/scenarios/blank_scenario_4_subnets/01_admin_infrastructure/stage_00/bs4_admin_deployer_api_gateway.yml +++ b/scenarios/blank_scenario_4_subnets/01_admin_infrastructure/stage_00/bs4_admin_deployer_api_gateway.yml @@ -85,7 +85,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "{{ global_vm_ci_ip }}" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.142.1" diff --git a/scenarios/blank_scenario_4_subnets/01_admin_infrastructure/stage_00/bs4_admin_deployer_ui.yml b/scenarios/blank_scenario_4_subnets/01_admin_infrastructure/stage_00/bs4_admin_deployer_ui.yml index ffca13cb..bdc1834d 100644 --- a/scenarios/blank_scenario_4_subnets/01_admin_infrastructure/stage_00/bs4_admin_deployer_ui.yml +++ b/scenarios/blank_scenario_4_subnets/01_admin_infrastructure/stage_00/bs4_admin_deployer_ui.yml @@ -85,7 +85,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "{{ global_vm_ci_ip }}" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.142.1" diff --git a/scenarios/blank_scenario_4_subnets/01_admin_infrastructure/stage_00/bs4_admin_wazuh.yml b/scenarios/blank_scenario_4_subnets/01_admin_infrastructure/stage_00/bs4_admin_wazuh.yml index 87d6e721..eb53ca5c 100644 --- a/scenarios/blank_scenario_4_subnets/01_admin_infrastructure/stage_00/bs4_admin_wazuh.yml +++ b/scenarios/blank_scenario_4_subnets/01_admin_infrastructure/stage_00/bs4_admin_wazuh.yml @@ -85,7 +85,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "{{ global_vm_ci_ip }}" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.142.1" diff --git a/scenarios/blank_scenario_4_subnets/01_init_proxmox/templates/ubuntu_noble/00-template-vm-nano.yml b/scenarios/blank_scenario_4_subnets/01_init_proxmox/templates/ubuntu_noble/00-template-vm-nano.yml index f35071f0..94a73508 100644 --- a/scenarios/blank_scenario_4_subnets/01_init_proxmox/templates/ubuntu_noble/00-template-vm-nano.yml +++ b/scenarios/blank_scenario_4_subnets/01_init_proxmox/templates/ubuntu_noble/00-template-vm-nano.yml @@ -137,7 +137,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_trainee_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_trainee_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "192.168.140.201" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.140.1" diff --git a/scenarios/blank_scenario_4_subnets/01_init_proxmox/templates/ubuntu_noble/01-template-vm-micro-01-2g-24g.yml b/scenarios/blank_scenario_4_subnets/01_init_proxmox/templates/ubuntu_noble/01-template-vm-micro-01-2g-24g.yml index 3fcef71d..1b84e401 100644 --- a/scenarios/blank_scenario_4_subnets/01_init_proxmox/templates/ubuntu_noble/01-template-vm-micro-01-2g-24g.yml +++ b/scenarios/blank_scenario_4_subnets/01_init_proxmox/templates/ubuntu_noble/01-template-vm-micro-01-2g-24g.yml @@ -137,7 +137,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_trainee_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_trainee_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "192.168.140.211" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.140.1" diff --git a/scenarios/blank_scenario_4_subnets/01_init_proxmox/templates/ubuntu_noble/01-template-vm-micro-02-2g-24g.yml b/scenarios/blank_scenario_4_subnets/01_init_proxmox/templates/ubuntu_noble/01-template-vm-micro-02-2g-24g.yml index 6c962ee4..1e0a5694 100644 --- a/scenarios/blank_scenario_4_subnets/01_init_proxmox/templates/ubuntu_noble/01-template-vm-micro-02-2g-24g.yml +++ b/scenarios/blank_scenario_4_subnets/01_init_proxmox/templates/ubuntu_noble/01-template-vm-micro-02-2g-24g.yml @@ -137,7 +137,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_trainee_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_trainee_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "192.168.140.212" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.140.1" diff --git a/scenarios/blank_scenario_4_subnets/01_init_proxmox/templates/ubuntu_noble/02-template-vm-small-01-4g-32g.yml b/scenarios/blank_scenario_4_subnets/01_init_proxmox/templates/ubuntu_noble/02-template-vm-small-01-4g-32g.yml index 046a2234..dde76262 100644 --- a/scenarios/blank_scenario_4_subnets/01_init_proxmox/templates/ubuntu_noble/02-template-vm-small-01-4g-32g.yml +++ b/scenarios/blank_scenario_4_subnets/01_init_proxmox/templates/ubuntu_noble/02-template-vm-small-01-4g-32g.yml @@ -137,7 +137,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_trainee_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_trainee_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "192.168.140.221" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.140.1" diff --git a/scenarios/blank_scenario_4_subnets/01_init_proxmox/templates/ubuntu_noble/02-template-vm-small-02-4g-32g.yml b/scenarios/blank_scenario_4_subnets/01_init_proxmox/templates/ubuntu_noble/02-template-vm-small-02-4g-32g.yml index 7bb1ee66..f5dd439b 100644 --- a/scenarios/blank_scenario_4_subnets/01_init_proxmox/templates/ubuntu_noble/02-template-vm-small-02-4g-32g.yml +++ b/scenarios/blank_scenario_4_subnets/01_init_proxmox/templates/ubuntu_noble/02-template-vm-small-02-4g-32g.yml @@ -137,7 +137,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_trainee_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_trainee_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "192.168.140.222" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.140.1" diff --git a/scenarios/blank_scenario_4_subnets/01_init_proxmox/templates/ubuntu_noble/02-template-vm-small-04-4g-32g.yml b/scenarios/blank_scenario_4_subnets/01_init_proxmox/templates/ubuntu_noble/02-template-vm-small-04-4g-32g.yml index 25e53e40..57c434ac 100644 --- a/scenarios/blank_scenario_4_subnets/01_init_proxmox/templates/ubuntu_noble/02-template-vm-small-04-4g-32g.yml +++ b/scenarios/blank_scenario_4_subnets/01_init_proxmox/templates/ubuntu_noble/02-template-vm-small-04-4g-32g.yml @@ -137,7 +137,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_trainee_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_trainee_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "192.168.140.224" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.140.1" diff --git a/scenarios/blank_scenario_4_subnets/01_init_proxmox/templates/ubuntu_noble/03-template-vm-medium-02-8g-64g.yml b/scenarios/blank_scenario_4_subnets/01_init_proxmox/templates/ubuntu_noble/03-template-vm-medium-02-8g-64g.yml index a6f7c449..0f2339c6 100644 --- a/scenarios/blank_scenario_4_subnets/01_init_proxmox/templates/ubuntu_noble/03-template-vm-medium-02-8g-64g.yml +++ b/scenarios/blank_scenario_4_subnets/01_init_proxmox/templates/ubuntu_noble/03-template-vm-medium-02-8g-64g.yml @@ -137,7 +137,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_trainee_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_trainee_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "192.168.140.232" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.140.1" diff --git a/scenarios/blank_scenario_4_subnets/01_init_proxmox/templates/ubuntu_noble/03-template-vm-medium-04-8g-64g.yml b/scenarios/blank_scenario_4_subnets/01_init_proxmox/templates/ubuntu_noble/03-template-vm-medium-04-8g-64g.yml index ffceb4bc..e33d27fe 100644 --- a/scenarios/blank_scenario_4_subnets/01_init_proxmox/templates/ubuntu_noble/03-template-vm-medium-04-8g-64g.yml +++ b/scenarios/blank_scenario_4_subnets/01_init_proxmox/templates/ubuntu_noble/03-template-vm-medium-04-8g-64g.yml @@ -137,7 +137,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_trainee_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_trainee_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "192.168.140.234" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.140.1" diff --git a/scenarios/blank_scenario_4_subnets/01_init_proxmox/templates/ubuntu_noble/03-template-vm-medium-06-8g-64g.yml b/scenarios/blank_scenario_4_subnets/01_init_proxmox/templates/ubuntu_noble/03-template-vm-medium-06-8g-64g.yml index a08ac96e..19713c1c 100644 --- a/scenarios/blank_scenario_4_subnets/01_init_proxmox/templates/ubuntu_noble/03-template-vm-medium-06-8g-64g.yml +++ b/scenarios/blank_scenario_4_subnets/01_init_proxmox/templates/ubuntu_noble/03-template-vm-medium-06-8g-64g.yml @@ -137,7 +137,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_trainee_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_trainee_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "192.168.140.236" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.140.1" diff --git a/scenarios/blank_scenario_4_subnets/01_init_proxmox/templates/ubuntu_noble/04-template-vm-large-04-8g-64g.yml b/scenarios/blank_scenario_4_subnets/01_init_proxmox/templates/ubuntu_noble/04-template-vm-large-04-8g-64g.yml index 630d82fd..ea62d6fb 100644 --- a/scenarios/blank_scenario_4_subnets/01_init_proxmox/templates/ubuntu_noble/04-template-vm-large-04-8g-64g.yml +++ b/scenarios/blank_scenario_4_subnets/01_init_proxmox/templates/ubuntu_noble/04-template-vm-large-04-8g-64g.yml @@ -137,7 +137,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_trainee_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_trainee_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "192.168.140.244" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.140.1" diff --git a/scenarios/blank_scenario_4_subnets/01_init_proxmox/templates/ubuntu_noble/04-template-vm-large-06-8g-64g.yml b/scenarios/blank_scenario_4_subnets/01_init_proxmox/templates/ubuntu_noble/04-template-vm-large-06-8g-64g.yml index 60622fc5..02e1e257 100644 --- a/scenarios/blank_scenario_4_subnets/01_init_proxmox/templates/ubuntu_noble/04-template-vm-large-06-8g-64g.yml +++ b/scenarios/blank_scenario_4_subnets/01_init_proxmox/templates/ubuntu_noble/04-template-vm-large-06-8g-64g.yml @@ -137,7 +137,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_trainee_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_trainee_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "192.168.140.246" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.140.1" diff --git a/scenarios/blank_scenario_4_subnets/01_init_proxmox/templates/ubuntu_noble/04-template-vm-large-08-8g-64g.yml b/scenarios/blank_scenario_4_subnets/01_init_proxmox/templates/ubuntu_noble/04-template-vm-large-08-8g-64g.yml index b464a6ab..3c2dd4fe 100644 --- a/scenarios/blank_scenario_4_subnets/01_init_proxmox/templates/ubuntu_noble/04-template-vm-large-08-8g-64g.yml +++ b/scenarios/blank_scenario_4_subnets/01_init_proxmox/templates/ubuntu_noble/04-template-vm-large-08-8g-64g.yml @@ -137,7 +137,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_trainee_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_trainee_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "192.168.140.248" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.140.1" diff --git a/scenarios/blank_scenario_4_subnets/01_init_proxmox/templates/ubuntu_noble/test_setup_templates.yml b/scenarios/blank_scenario_4_subnets/01_init_proxmox/templates/ubuntu_noble/test_setup_templates.yml index 74956397..e6aaee03 100644 --- a/scenarios/blank_scenario_4_subnets/01_init_proxmox/templates/ubuntu_noble/test_setup_templates.yml +++ b/scenarios/blank_scenario_4_subnets/01_init_proxmox/templates/ubuntu_noble/test_setup_templates.yml @@ -83,7 +83,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_trainee_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_trainee_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "192.168.142.55" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.142.1" diff --git a/scenarios/blank_scenario_4_subnets/02_network_lab/stage_00/bs4_team_143_01.yml b/scenarios/blank_scenario_4_subnets/02_network_lab/stage_00/bs4_team_143_01.yml index d3e0d94f..55395ebd 100644 --- a/scenarios/blank_scenario_4_subnets/02_network_lab/stage_00/bs4_team_143_01.yml +++ b/scenarios/blank_scenario_4_subnets/02_network_lab/stage_00/bs4_team_143_01.yml @@ -54,7 +54,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "{{ global_vm_ci_ip }}" vm_ci_netmask: "24" vm_ci_ip_gw: "{{ global_vm_ci_ip_gw }}" diff --git a/scenarios/blank_scenario_4_subnets/02_network_lab/stage_00/bs4_team_143_02.yml b/scenarios/blank_scenario_4_subnets/02_network_lab/stage_00/bs4_team_143_02.yml index f27d8afc..67656fe2 100644 --- a/scenarios/blank_scenario_4_subnets/02_network_lab/stage_00/bs4_team_143_02.yml +++ b/scenarios/blank_scenario_4_subnets/02_network_lab/stage_00/bs4_team_143_02.yml @@ -54,7 +54,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "{{ global_vm_ci_ip }}" vm_ci_netmask: "24" vm_ci_ip_gw: "{{ global_vm_ci_ip_gw }}" diff --git a/scenarios/blank_scenario_4_subnets/02_network_lab/stage_00/bs4_team_143_03.yml b/scenarios/blank_scenario_4_subnets/02_network_lab/stage_00/bs4_team_143_03.yml index f2a96bee..162b2791 100644 --- a/scenarios/blank_scenario_4_subnets/02_network_lab/stage_00/bs4_team_143_03.yml +++ b/scenarios/blank_scenario_4_subnets/02_network_lab/stage_00/bs4_team_143_03.yml @@ -54,7 +54,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "{{ global_vm_ci_ip }}" vm_ci_netmask: "24" vm_ci_ip_gw: "{{ global_vm_ci_ip_gw }}" diff --git a/scenarios/blank_scenario_4_subnets/02_network_lab/stage_00/bs4_team_143_04.yml b/scenarios/blank_scenario_4_subnets/02_network_lab/stage_00/bs4_team_143_04.yml index 800ee4eb..414b7e86 100644 --- a/scenarios/blank_scenario_4_subnets/02_network_lab/stage_00/bs4_team_143_04.yml +++ b/scenarios/blank_scenario_4_subnets/02_network_lab/stage_00/bs4_team_143_04.yml @@ -54,7 +54,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "{{ global_vm_ci_ip }}" vm_ci_netmask: "24" vm_ci_ip_gw: "{{ global_vm_ci_ip_gw }}" diff --git a/scenarios/blank_scenario_4_subnets/02_network_lab/stage_00/bs4_team_144_01.yml b/scenarios/blank_scenario_4_subnets/02_network_lab/stage_00/bs4_team_144_01.yml index 62087c0c..28ded597 100644 --- a/scenarios/blank_scenario_4_subnets/02_network_lab/stage_00/bs4_team_144_01.yml +++ b/scenarios/blank_scenario_4_subnets/02_network_lab/stage_00/bs4_team_144_01.yml @@ -54,7 +54,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "{{ global_vm_ci_ip }}" vm_ci_netmask: "24" vm_ci_ip_gw: "{{ global_vm_ci_ip_gw }}" diff --git a/scenarios/blank_scenario_4_subnets/02_network_lab/stage_00/bs4_team_144_02.yml b/scenarios/blank_scenario_4_subnets/02_network_lab/stage_00/bs4_team_144_02.yml index 29be2803..85dfd197 100644 --- a/scenarios/blank_scenario_4_subnets/02_network_lab/stage_00/bs4_team_144_02.yml +++ b/scenarios/blank_scenario_4_subnets/02_network_lab/stage_00/bs4_team_144_02.yml @@ -54,7 +54,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "{{ global_vm_ci_ip }}" vm_ci_netmask: "24" vm_ci_ip_gw: "{{ global_vm_ci_ip_gw }}" diff --git a/scenarios/blank_scenario_4_subnets/02_network_lab/stage_00/bs4_team_144_03.yml b/scenarios/blank_scenario_4_subnets/02_network_lab/stage_00/bs4_team_144_03.yml index 535d43d9..9ca7aa2f 100644 --- a/scenarios/blank_scenario_4_subnets/02_network_lab/stage_00/bs4_team_144_03.yml +++ b/scenarios/blank_scenario_4_subnets/02_network_lab/stage_00/bs4_team_144_03.yml @@ -54,7 +54,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "{{ global_vm_ci_ip }}" vm_ci_netmask: "24" vm_ci_ip_gw: "{{ global_vm_ci_ip_gw }}" diff --git a/scenarios/blank_scenario_4_subnets/02_network_lab/stage_00/bs4_team_144_04.yml b/scenarios/blank_scenario_4_subnets/02_network_lab/stage_00/bs4_team_144_04.yml index f004d274..3737ec53 100644 --- a/scenarios/blank_scenario_4_subnets/02_network_lab/stage_00/bs4_team_144_04.yml +++ b/scenarios/blank_scenario_4_subnets/02_network_lab/stage_00/bs4_team_144_04.yml @@ -54,7 +54,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "{{ global_vm_ci_ip }}" vm_ci_netmask: "24" vm_ci_ip_gw: "{{ global_vm_ci_ip_gw }}" diff --git a/scenarios/blank_scenario_4_subnets/02_network_lab/stage_00/bs4_team_145_01.yml b/scenarios/blank_scenario_4_subnets/02_network_lab/stage_00/bs4_team_145_01.yml index f358f6ba..df752455 100644 --- a/scenarios/blank_scenario_4_subnets/02_network_lab/stage_00/bs4_team_145_01.yml +++ b/scenarios/blank_scenario_4_subnets/02_network_lab/stage_00/bs4_team_145_01.yml @@ -54,7 +54,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "{{ global_vm_ci_ip }}" vm_ci_netmask: "24" vm_ci_ip_gw: "{{ global_vm_ci_ip_gw }}" diff --git a/scenarios/blank_scenario_4_subnets/02_network_lab/stage_00/bs4_team_145_02.yml b/scenarios/blank_scenario_4_subnets/02_network_lab/stage_00/bs4_team_145_02.yml index d44e4f74..25626b42 100644 --- a/scenarios/blank_scenario_4_subnets/02_network_lab/stage_00/bs4_team_145_02.yml +++ b/scenarios/blank_scenario_4_subnets/02_network_lab/stage_00/bs4_team_145_02.yml @@ -54,7 +54,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "{{ global_vm_ci_ip }}" vm_ci_netmask: "24" vm_ci_ip_gw: "{{ global_vm_ci_ip_gw }}" diff --git a/scenarios/blank_scenario_4_subnets/02_network_lab/stage_00/bs4_team_145_03.yml b/scenarios/blank_scenario_4_subnets/02_network_lab/stage_00/bs4_team_145_03.yml index feee9420..334de79c 100644 --- a/scenarios/blank_scenario_4_subnets/02_network_lab/stage_00/bs4_team_145_03.yml +++ b/scenarios/blank_scenario_4_subnets/02_network_lab/stage_00/bs4_team_145_03.yml @@ -54,7 +54,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "{{ global_vm_ci_ip }}" vm_ci_netmask: "24" vm_ci_ip_gw: "{{ global_vm_ci_ip_gw }}" diff --git a/scenarios/blank_scenario_4_subnets/02_network_lab/stage_00/bs4_team_145_04.yml b/scenarios/blank_scenario_4_subnets/02_network_lab/stage_00/bs4_team_145_04.yml index adfcbb4e..0895b7fc 100644 --- a/scenarios/blank_scenario_4_subnets/02_network_lab/stage_00/bs4_team_145_04.yml +++ b/scenarios/blank_scenario_4_subnets/02_network_lab/stage_00/bs4_team_145_04.yml @@ -54,7 +54,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "{{ global_vm_ci_ip }}" vm_ci_netmask: "24" vm_ci_ip_gw: "{{ global_vm_ci_ip_gw }}" diff --git a/scenarios/blank_scenario_4_subnets/02_network_lab/stage_00/bs4_team_146_01.yml b/scenarios/blank_scenario_4_subnets/02_network_lab/stage_00/bs4_team_146_01.yml index 5ddfa396..c6a6fe17 100644 --- a/scenarios/blank_scenario_4_subnets/02_network_lab/stage_00/bs4_team_146_01.yml +++ b/scenarios/blank_scenario_4_subnets/02_network_lab/stage_00/bs4_team_146_01.yml @@ -54,7 +54,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "{{ global_vm_ci_ip }}" vm_ci_netmask: "24" vm_ci_ip_gw: "{{ global_vm_ci_ip_gw }}" diff --git a/scenarios/blank_scenario_4_subnets/02_network_lab/stage_00/bs4_team_146_02.yml b/scenarios/blank_scenario_4_subnets/02_network_lab/stage_00/bs4_team_146_02.yml index 75a0c8b9..bef41ff0 100644 --- a/scenarios/blank_scenario_4_subnets/02_network_lab/stage_00/bs4_team_146_02.yml +++ b/scenarios/blank_scenario_4_subnets/02_network_lab/stage_00/bs4_team_146_02.yml @@ -54,7 +54,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "{{ global_vm_ci_ip }}" vm_ci_netmask: "24" vm_ci_ip_gw: "{{ global_vm_ci_ip_gw }}" diff --git a/scenarios/blank_scenario_4_subnets/02_network_lab/stage_00/bs4_team_146_03.yml b/scenarios/blank_scenario_4_subnets/02_network_lab/stage_00/bs4_team_146_03.yml index 6c6b9177..785f7771 100644 --- a/scenarios/blank_scenario_4_subnets/02_network_lab/stage_00/bs4_team_146_03.yml +++ b/scenarios/blank_scenario_4_subnets/02_network_lab/stage_00/bs4_team_146_03.yml @@ -54,7 +54,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "{{ global_vm_ci_ip }}" vm_ci_netmask: "24" vm_ci_ip_gw: "{{ global_vm_ci_ip_gw }}" diff --git a/scenarios/blank_scenario_4_subnets/02_network_lab/stage_00/bs4_team_146_04.yml b/scenarios/blank_scenario_4_subnets/02_network_lab/stage_00/bs4_team_146_04.yml index 68a52c1a..74574804 100644 --- a/scenarios/blank_scenario_4_subnets/02_network_lab/stage_00/bs4_team_146_04.yml +++ b/scenarios/blank_scenario_4_subnets/02_network_lab/stage_00/bs4_team_146_04.yml @@ -54,7 +54,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "{{ global_vm_ci_ip }}" vm_ci_netmask: "24" vm_ci_ip_gw: "{{ global_vm_ci_ip_gw }}" diff --git a/scenarios/blank_scenario_4_subnets/templates/ansible-vars.yml b/scenarios/blank_scenario_4_subnets/templates/ansible-vars.yml index 2a46df28..56bd64be 100644 --- a/scenarios/blank_scenario_4_subnets/templates/ansible-vars.yml +++ b/scenarios/blank_scenario_4_subnets/templates/ansible-vars.yml @@ -33,3 +33,8 @@ default_admin_vm_ci_user: "alice" # student vm user default_trainee_vm_ci_user: "bob" + +#### CLOUD-INIT DNS #### +# default resolver for lab VMs; range42-init.py substitutes the Proxmox node +# resolver here at workspace creation. 1.1.1.1 is the portable fallback. +default_vm_ci_dns_ips: "1.1.1.1" diff --git a/scenarios/blank_scenario_4_subnets/templates/vault-example.yml b/scenarios/blank_scenario_4_subnets/templates/vault-example.yml index d49bed40..25a4c436 100644 --- a/scenarios/blank_scenario_4_subnets/templates/vault-example.yml +++ b/scenarios/blank_scenario_4_subnets/templates/vault-example.yml @@ -63,3 +63,8 @@ WAZUH_PASSWORD: "REPLACE_ME" # deployer-cli known_hosts path (not really secret, but stored in vault historically) deployer_cli_user_ssh_known_hosts: "/home/your_deployer_cli_username/.ssh/known_hosts" + +#### CLOUD-INIT DNS #### +# default resolver for lab VMs; range42-init.py substitutes the Proxmox node +# resolver here at workspace creation. 1.1.1.1 is the portable fallback. +default_vm_ci_dns_ips: "1.1.1.1" diff --git a/scenarios/blank_scenario_6_subnets/01_admin_infrastructure/stage_00/bs6_admin_deployer_api_backend.yml b/scenarios/blank_scenario_6_subnets/01_admin_infrastructure/stage_00/bs6_admin_deployer_api_backend.yml index 39815889..bf5c4bc3 100644 --- a/scenarios/blank_scenario_6_subnets/01_admin_infrastructure/stage_00/bs6_admin_deployer_api_backend.yml +++ b/scenarios/blank_scenario_6_subnets/01_admin_infrastructure/stage_00/bs6_admin_deployer_api_backend.yml @@ -85,7 +85,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "{{ global_vm_ci_ip }}" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.142.1" diff --git a/scenarios/blank_scenario_6_subnets/01_admin_infrastructure/stage_00/bs6_admin_deployer_api_gateway.yml b/scenarios/blank_scenario_6_subnets/01_admin_infrastructure/stage_00/bs6_admin_deployer_api_gateway.yml index 792f0ea1..04f4c0ee 100644 --- a/scenarios/blank_scenario_6_subnets/01_admin_infrastructure/stage_00/bs6_admin_deployer_api_gateway.yml +++ b/scenarios/blank_scenario_6_subnets/01_admin_infrastructure/stage_00/bs6_admin_deployer_api_gateway.yml @@ -85,7 +85,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "{{ global_vm_ci_ip }}" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.142.1" diff --git a/scenarios/blank_scenario_6_subnets/01_admin_infrastructure/stage_00/bs6_admin_deployer_ui.yml b/scenarios/blank_scenario_6_subnets/01_admin_infrastructure/stage_00/bs6_admin_deployer_ui.yml index fd37f9b4..71cdee16 100644 --- a/scenarios/blank_scenario_6_subnets/01_admin_infrastructure/stage_00/bs6_admin_deployer_ui.yml +++ b/scenarios/blank_scenario_6_subnets/01_admin_infrastructure/stage_00/bs6_admin_deployer_ui.yml @@ -85,7 +85,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "{{ global_vm_ci_ip }}" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.142.1" diff --git a/scenarios/blank_scenario_6_subnets/01_admin_infrastructure/stage_00/bs6_admin_wazuh.yml b/scenarios/blank_scenario_6_subnets/01_admin_infrastructure/stage_00/bs6_admin_wazuh.yml index da1dcd7b..d924eacd 100644 --- a/scenarios/blank_scenario_6_subnets/01_admin_infrastructure/stage_00/bs6_admin_wazuh.yml +++ b/scenarios/blank_scenario_6_subnets/01_admin_infrastructure/stage_00/bs6_admin_wazuh.yml @@ -85,7 +85,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "{{ global_vm_ci_ip }}" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.142.1" diff --git a/scenarios/blank_scenario_6_subnets/01_init_proxmox/templates/ubuntu_noble/00-template-vm-nano.yml b/scenarios/blank_scenario_6_subnets/01_init_proxmox/templates/ubuntu_noble/00-template-vm-nano.yml index f35071f0..94a73508 100644 --- a/scenarios/blank_scenario_6_subnets/01_init_proxmox/templates/ubuntu_noble/00-template-vm-nano.yml +++ b/scenarios/blank_scenario_6_subnets/01_init_proxmox/templates/ubuntu_noble/00-template-vm-nano.yml @@ -137,7 +137,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_trainee_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_trainee_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "192.168.140.201" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.140.1" diff --git a/scenarios/blank_scenario_6_subnets/01_init_proxmox/templates/ubuntu_noble/01-template-vm-micro-01-2g-24g.yml b/scenarios/blank_scenario_6_subnets/01_init_proxmox/templates/ubuntu_noble/01-template-vm-micro-01-2g-24g.yml index 3fcef71d..1b84e401 100644 --- a/scenarios/blank_scenario_6_subnets/01_init_proxmox/templates/ubuntu_noble/01-template-vm-micro-01-2g-24g.yml +++ b/scenarios/blank_scenario_6_subnets/01_init_proxmox/templates/ubuntu_noble/01-template-vm-micro-01-2g-24g.yml @@ -137,7 +137,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_trainee_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_trainee_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "192.168.140.211" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.140.1" diff --git a/scenarios/blank_scenario_6_subnets/01_init_proxmox/templates/ubuntu_noble/01-template-vm-micro-02-2g-24g.yml b/scenarios/blank_scenario_6_subnets/01_init_proxmox/templates/ubuntu_noble/01-template-vm-micro-02-2g-24g.yml index 6c962ee4..1e0a5694 100644 --- a/scenarios/blank_scenario_6_subnets/01_init_proxmox/templates/ubuntu_noble/01-template-vm-micro-02-2g-24g.yml +++ b/scenarios/blank_scenario_6_subnets/01_init_proxmox/templates/ubuntu_noble/01-template-vm-micro-02-2g-24g.yml @@ -137,7 +137,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_trainee_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_trainee_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "192.168.140.212" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.140.1" diff --git a/scenarios/blank_scenario_6_subnets/01_init_proxmox/templates/ubuntu_noble/02-template-vm-small-01-4g-32g.yml b/scenarios/blank_scenario_6_subnets/01_init_proxmox/templates/ubuntu_noble/02-template-vm-small-01-4g-32g.yml index 046a2234..dde76262 100644 --- a/scenarios/blank_scenario_6_subnets/01_init_proxmox/templates/ubuntu_noble/02-template-vm-small-01-4g-32g.yml +++ b/scenarios/blank_scenario_6_subnets/01_init_proxmox/templates/ubuntu_noble/02-template-vm-small-01-4g-32g.yml @@ -137,7 +137,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_trainee_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_trainee_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "192.168.140.221" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.140.1" diff --git a/scenarios/blank_scenario_6_subnets/01_init_proxmox/templates/ubuntu_noble/02-template-vm-small-02-4g-32g.yml b/scenarios/blank_scenario_6_subnets/01_init_proxmox/templates/ubuntu_noble/02-template-vm-small-02-4g-32g.yml index 7bb1ee66..f5dd439b 100644 --- a/scenarios/blank_scenario_6_subnets/01_init_proxmox/templates/ubuntu_noble/02-template-vm-small-02-4g-32g.yml +++ b/scenarios/blank_scenario_6_subnets/01_init_proxmox/templates/ubuntu_noble/02-template-vm-small-02-4g-32g.yml @@ -137,7 +137,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_trainee_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_trainee_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "192.168.140.222" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.140.1" diff --git a/scenarios/blank_scenario_6_subnets/01_init_proxmox/templates/ubuntu_noble/02-template-vm-small-04-4g-32g.yml b/scenarios/blank_scenario_6_subnets/01_init_proxmox/templates/ubuntu_noble/02-template-vm-small-04-4g-32g.yml index 25e53e40..57c434ac 100644 --- a/scenarios/blank_scenario_6_subnets/01_init_proxmox/templates/ubuntu_noble/02-template-vm-small-04-4g-32g.yml +++ b/scenarios/blank_scenario_6_subnets/01_init_proxmox/templates/ubuntu_noble/02-template-vm-small-04-4g-32g.yml @@ -137,7 +137,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_trainee_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_trainee_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "192.168.140.224" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.140.1" diff --git a/scenarios/blank_scenario_6_subnets/01_init_proxmox/templates/ubuntu_noble/03-template-vm-medium-02-8g-64g.yml b/scenarios/blank_scenario_6_subnets/01_init_proxmox/templates/ubuntu_noble/03-template-vm-medium-02-8g-64g.yml index a6f7c449..0f2339c6 100644 --- a/scenarios/blank_scenario_6_subnets/01_init_proxmox/templates/ubuntu_noble/03-template-vm-medium-02-8g-64g.yml +++ b/scenarios/blank_scenario_6_subnets/01_init_proxmox/templates/ubuntu_noble/03-template-vm-medium-02-8g-64g.yml @@ -137,7 +137,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_trainee_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_trainee_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "192.168.140.232" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.140.1" diff --git a/scenarios/blank_scenario_6_subnets/01_init_proxmox/templates/ubuntu_noble/03-template-vm-medium-04-8g-64g.yml b/scenarios/blank_scenario_6_subnets/01_init_proxmox/templates/ubuntu_noble/03-template-vm-medium-04-8g-64g.yml index ffceb4bc..e33d27fe 100644 --- a/scenarios/blank_scenario_6_subnets/01_init_proxmox/templates/ubuntu_noble/03-template-vm-medium-04-8g-64g.yml +++ b/scenarios/blank_scenario_6_subnets/01_init_proxmox/templates/ubuntu_noble/03-template-vm-medium-04-8g-64g.yml @@ -137,7 +137,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_trainee_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_trainee_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "192.168.140.234" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.140.1" diff --git a/scenarios/blank_scenario_6_subnets/01_init_proxmox/templates/ubuntu_noble/03-template-vm-medium-06-8g-64g.yml b/scenarios/blank_scenario_6_subnets/01_init_proxmox/templates/ubuntu_noble/03-template-vm-medium-06-8g-64g.yml index a08ac96e..19713c1c 100644 --- a/scenarios/blank_scenario_6_subnets/01_init_proxmox/templates/ubuntu_noble/03-template-vm-medium-06-8g-64g.yml +++ b/scenarios/blank_scenario_6_subnets/01_init_proxmox/templates/ubuntu_noble/03-template-vm-medium-06-8g-64g.yml @@ -137,7 +137,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_trainee_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_trainee_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "192.168.140.236" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.140.1" diff --git a/scenarios/blank_scenario_6_subnets/01_init_proxmox/templates/ubuntu_noble/04-template-vm-large-04-8g-64g.yml b/scenarios/blank_scenario_6_subnets/01_init_proxmox/templates/ubuntu_noble/04-template-vm-large-04-8g-64g.yml index 630d82fd..ea62d6fb 100644 --- a/scenarios/blank_scenario_6_subnets/01_init_proxmox/templates/ubuntu_noble/04-template-vm-large-04-8g-64g.yml +++ b/scenarios/blank_scenario_6_subnets/01_init_proxmox/templates/ubuntu_noble/04-template-vm-large-04-8g-64g.yml @@ -137,7 +137,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_trainee_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_trainee_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "192.168.140.244" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.140.1" diff --git a/scenarios/blank_scenario_6_subnets/01_init_proxmox/templates/ubuntu_noble/04-template-vm-large-06-8g-64g.yml b/scenarios/blank_scenario_6_subnets/01_init_proxmox/templates/ubuntu_noble/04-template-vm-large-06-8g-64g.yml index 60622fc5..02e1e257 100644 --- a/scenarios/blank_scenario_6_subnets/01_init_proxmox/templates/ubuntu_noble/04-template-vm-large-06-8g-64g.yml +++ b/scenarios/blank_scenario_6_subnets/01_init_proxmox/templates/ubuntu_noble/04-template-vm-large-06-8g-64g.yml @@ -137,7 +137,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_trainee_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_trainee_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "192.168.140.246" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.140.1" diff --git a/scenarios/blank_scenario_6_subnets/01_init_proxmox/templates/ubuntu_noble/04-template-vm-large-08-8g-64g.yml b/scenarios/blank_scenario_6_subnets/01_init_proxmox/templates/ubuntu_noble/04-template-vm-large-08-8g-64g.yml index b464a6ab..3c2dd4fe 100644 --- a/scenarios/blank_scenario_6_subnets/01_init_proxmox/templates/ubuntu_noble/04-template-vm-large-08-8g-64g.yml +++ b/scenarios/blank_scenario_6_subnets/01_init_proxmox/templates/ubuntu_noble/04-template-vm-large-08-8g-64g.yml @@ -137,7 +137,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_trainee_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_trainee_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "192.168.140.248" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.140.1" diff --git a/scenarios/blank_scenario_6_subnets/01_init_proxmox/templates/ubuntu_noble/test_setup_templates.yml b/scenarios/blank_scenario_6_subnets/01_init_proxmox/templates/ubuntu_noble/test_setup_templates.yml index 74956397..e6aaee03 100644 --- a/scenarios/blank_scenario_6_subnets/01_init_proxmox/templates/ubuntu_noble/test_setup_templates.yml +++ b/scenarios/blank_scenario_6_subnets/01_init_proxmox/templates/ubuntu_noble/test_setup_templates.yml @@ -83,7 +83,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_trainee_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_trainee_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "192.168.142.55" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.142.1" diff --git a/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_143_01.yml b/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_143_01.yml index 625f316b..2f05fd2f 100644 --- a/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_143_01.yml +++ b/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_143_01.yml @@ -54,7 +54,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "{{ global_vm_ci_ip }}" vm_ci_netmask: "24" vm_ci_ip_gw: "{{ global_vm_ci_ip_gw }}" diff --git a/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_143_02.yml b/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_143_02.yml index 11251b7c..19c763b0 100644 --- a/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_143_02.yml +++ b/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_143_02.yml @@ -54,7 +54,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "{{ global_vm_ci_ip }}" vm_ci_netmask: "24" vm_ci_ip_gw: "{{ global_vm_ci_ip_gw }}" diff --git a/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_143_03.yml b/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_143_03.yml index 9c395b45..c447c9c3 100644 --- a/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_143_03.yml +++ b/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_143_03.yml @@ -54,7 +54,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "{{ global_vm_ci_ip }}" vm_ci_netmask: "24" vm_ci_ip_gw: "{{ global_vm_ci_ip_gw }}" diff --git a/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_143_04.yml b/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_143_04.yml index 36bc2add..87c7cd04 100644 --- a/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_143_04.yml +++ b/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_143_04.yml @@ -54,7 +54,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "{{ global_vm_ci_ip }}" vm_ci_netmask: "24" vm_ci_ip_gw: "{{ global_vm_ci_ip_gw }}" diff --git a/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_144_01.yml b/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_144_01.yml index 66774b11..79111eb7 100644 --- a/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_144_01.yml +++ b/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_144_01.yml @@ -54,7 +54,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "{{ global_vm_ci_ip }}" vm_ci_netmask: "24" vm_ci_ip_gw: "{{ global_vm_ci_ip_gw }}" diff --git a/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_144_02.yml b/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_144_02.yml index b98d3027..8f88c03d 100644 --- a/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_144_02.yml +++ b/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_144_02.yml @@ -54,7 +54,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "{{ global_vm_ci_ip }}" vm_ci_netmask: "24" vm_ci_ip_gw: "{{ global_vm_ci_ip_gw }}" diff --git a/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_144_03.yml b/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_144_03.yml index 32f42c56..e47dbe36 100644 --- a/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_144_03.yml +++ b/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_144_03.yml @@ -54,7 +54,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "{{ global_vm_ci_ip }}" vm_ci_netmask: "24" vm_ci_ip_gw: "{{ global_vm_ci_ip_gw }}" diff --git a/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_144_04.yml b/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_144_04.yml index ab508354..3f2cb290 100644 --- a/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_144_04.yml +++ b/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_144_04.yml @@ -54,7 +54,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "{{ global_vm_ci_ip }}" vm_ci_netmask: "24" vm_ci_ip_gw: "{{ global_vm_ci_ip_gw }}" diff --git a/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_145_01.yml b/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_145_01.yml index 6ee940f3..65411c0c 100644 --- a/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_145_01.yml +++ b/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_145_01.yml @@ -54,7 +54,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "{{ global_vm_ci_ip }}" vm_ci_netmask: "24" vm_ci_ip_gw: "{{ global_vm_ci_ip_gw }}" diff --git a/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_145_02.yml b/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_145_02.yml index a3b7d6bd..bc6dc533 100644 --- a/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_145_02.yml +++ b/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_145_02.yml @@ -54,7 +54,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "{{ global_vm_ci_ip }}" vm_ci_netmask: "24" vm_ci_ip_gw: "{{ global_vm_ci_ip_gw }}" diff --git a/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_145_03.yml b/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_145_03.yml index d9c5bd65..51f319ac 100644 --- a/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_145_03.yml +++ b/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_145_03.yml @@ -54,7 +54,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "{{ global_vm_ci_ip }}" vm_ci_netmask: "24" vm_ci_ip_gw: "{{ global_vm_ci_ip_gw }}" diff --git a/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_145_04.yml b/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_145_04.yml index 355f16d3..59960562 100644 --- a/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_145_04.yml +++ b/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_145_04.yml @@ -54,7 +54,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "{{ global_vm_ci_ip }}" vm_ci_netmask: "24" vm_ci_ip_gw: "{{ global_vm_ci_ip_gw }}" diff --git a/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_146_01.yml b/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_146_01.yml index 77c4cab6..3b2e65d7 100644 --- a/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_146_01.yml +++ b/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_146_01.yml @@ -54,7 +54,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "{{ global_vm_ci_ip }}" vm_ci_netmask: "24" vm_ci_ip_gw: "{{ global_vm_ci_ip_gw }}" diff --git a/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_146_02.yml b/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_146_02.yml index c01ceccf..ed6e3107 100644 --- a/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_146_02.yml +++ b/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_146_02.yml @@ -54,7 +54,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "{{ global_vm_ci_ip }}" vm_ci_netmask: "24" vm_ci_ip_gw: "{{ global_vm_ci_ip_gw }}" diff --git a/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_146_03.yml b/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_146_03.yml index 837b7124..bc9f1b5d 100644 --- a/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_146_03.yml +++ b/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_146_03.yml @@ -54,7 +54,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "{{ global_vm_ci_ip }}" vm_ci_netmask: "24" vm_ci_ip_gw: "{{ global_vm_ci_ip_gw }}" diff --git a/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_146_04.yml b/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_146_04.yml index 779aeaa4..90692d71 100644 --- a/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_146_04.yml +++ b/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_146_04.yml @@ -54,7 +54,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "{{ global_vm_ci_ip }}" vm_ci_netmask: "24" vm_ci_ip_gw: "{{ global_vm_ci_ip_gw }}" diff --git a/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_147_01.yml b/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_147_01.yml index 1614fbd6..6e41c486 100644 --- a/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_147_01.yml +++ b/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_147_01.yml @@ -54,7 +54,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "{{ global_vm_ci_ip }}" vm_ci_netmask: "24" vm_ci_ip_gw: "{{ global_vm_ci_ip_gw }}" diff --git a/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_147_02.yml b/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_147_02.yml index 6e96db3a..4b3d13b1 100644 --- a/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_147_02.yml +++ b/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_147_02.yml @@ -54,7 +54,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "{{ global_vm_ci_ip }}" vm_ci_netmask: "24" vm_ci_ip_gw: "{{ global_vm_ci_ip_gw }}" diff --git a/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_147_03.yml b/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_147_03.yml index cbb6fe1a..2ff989e6 100644 --- a/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_147_03.yml +++ b/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_147_03.yml @@ -54,7 +54,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "{{ global_vm_ci_ip }}" vm_ci_netmask: "24" vm_ci_ip_gw: "{{ global_vm_ci_ip_gw }}" diff --git a/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_147_04.yml b/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_147_04.yml index b022a74a..50cc7641 100644 --- a/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_147_04.yml +++ b/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_147_04.yml @@ -54,7 +54,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "{{ global_vm_ci_ip }}" vm_ci_netmask: "24" vm_ci_ip_gw: "{{ global_vm_ci_ip_gw }}" diff --git a/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_148_01.yml b/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_148_01.yml index dd984343..bd8427d3 100644 --- a/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_148_01.yml +++ b/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_148_01.yml @@ -54,7 +54,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "{{ global_vm_ci_ip }}" vm_ci_netmask: "24" vm_ci_ip_gw: "{{ global_vm_ci_ip_gw }}" diff --git a/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_148_02.yml b/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_148_02.yml index e8485c9e..be1d7bab 100644 --- a/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_148_02.yml +++ b/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_148_02.yml @@ -54,7 +54,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "{{ global_vm_ci_ip }}" vm_ci_netmask: "24" vm_ci_ip_gw: "{{ global_vm_ci_ip_gw }}" diff --git a/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_148_03.yml b/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_148_03.yml index 87dea3b5..1619b358 100644 --- a/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_148_03.yml +++ b/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_148_03.yml @@ -54,7 +54,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "{{ global_vm_ci_ip }}" vm_ci_netmask: "24" vm_ci_ip_gw: "{{ global_vm_ci_ip_gw }}" diff --git a/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_148_04.yml b/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_148_04.yml index d46f370e..f5faa75e 100644 --- a/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_148_04.yml +++ b/scenarios/blank_scenario_6_subnets/02_network_lab/stage_00/bs6_team_148_04.yml @@ -54,7 +54,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "{{ global_vm_ci_ip }}" vm_ci_netmask: "24" vm_ci_ip_gw: "{{ global_vm_ci_ip_gw }}" diff --git a/scenarios/blank_scenario_6_subnets/templates/ansible-vars.yml b/scenarios/blank_scenario_6_subnets/templates/ansible-vars.yml index 09b83f96..b208371e 100644 --- a/scenarios/blank_scenario_6_subnets/templates/ansible-vars.yml +++ b/scenarios/blank_scenario_6_subnets/templates/ansible-vars.yml @@ -33,3 +33,8 @@ default_admin_vm_ci_user: "alice" # student vm user default_trainee_vm_ci_user: "bob" + +#### CLOUD-INIT DNS #### +# default resolver for lab VMs; range42-init.py substitutes the Proxmox node +# resolver here at workspace creation. 1.1.1.1 is the portable fallback. +default_vm_ci_dns_ips: "1.1.1.1" diff --git a/scenarios/blank_scenario_6_subnets/templates/vault-example.yml b/scenarios/blank_scenario_6_subnets/templates/vault-example.yml index 68784a07..3e8c035c 100644 --- a/scenarios/blank_scenario_6_subnets/templates/vault-example.yml +++ b/scenarios/blank_scenario_6_subnets/templates/vault-example.yml @@ -63,3 +63,8 @@ WAZUH_PASSWORD: "REPLACE_ME" # deployer-cli known_hosts path (not really secret, but stored in vault historically) deployer_cli_user_ssh_known_hosts: "/home/your_deployer_cli_username/.ssh/known_hosts" + +#### CLOUD-INIT DNS #### +# default resolver for lab VMs; range42-init.py substitutes the Proxmox node +# resolver here at workspace creation. 1.1.1.1 is the portable fallback. +default_vm_ci_dns_ips: "1.1.1.1" diff --git a/scenarios/catalog_try/01_init_proxmox/templates/ubuntu_noble/02-template-vm-small-01-4g-32g.yml b/scenarios/catalog_try/01_init_proxmox/templates/ubuntu_noble/02-template-vm-small-01-4g-32g.yml index 9210bc55..c3a4dc8f 100644 --- a/scenarios/catalog_try/01_init_proxmox/templates/ubuntu_noble/02-template-vm-small-01-4g-32g.yml +++ b/scenarios/catalog_try/01_init_proxmox/templates/ubuntu_noble/02-template-vm-small-01-4g-32g.yml @@ -83,7 +83,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "192.168.142.221" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.142.1" diff --git a/scenarios/catalog_try/02_catalog-try_infrastructure/stage_00/catalog_try_vm.yml b/scenarios/catalog_try/02_catalog-try_infrastructure/stage_00/catalog_try_vm.yml index 67386d04..50dd96e3 100644 --- a/scenarios/catalog_try/02_catalog-try_infrastructure/stage_00/catalog_try_vm.yml +++ b/scenarios/catalog_try/02_catalog-try_infrastructure/stage_00/catalog_try_vm.yml @@ -81,7 +81,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "{{ global_vm_ci_ip }}" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.142.1" diff --git a/scenarios/catalog_try/templates/ansible-vars.yml b/scenarios/catalog_try/templates/ansible-vars.yml index 0be97f43..14e0c966 100644 --- a/scenarios/catalog_try/templates/ansible-vars.yml +++ b/scenarios/catalog_try/templates/ansible-vars.yml @@ -31,3 +31,8 @@ student_additionnal_keys_count: 0 # admin vm user (not secret - password and ssh key are in vault) default_admin_vm_ci_user: "alice" + +#### CLOUD-INIT DNS #### +# default resolver for lab VMs; range42-init.py substitutes the Proxmox node +# resolver here at workspace creation. 1.1.1.1 is the portable fallback. +default_vm_ci_dns_ips: "1.1.1.1" diff --git a/scenarios/catalog_try/templates/vault-example.yml b/scenarios/catalog_try/templates/vault-example.yml index 84fe9d16..2af0e8c9 100644 --- a/scenarios/catalog_try/templates/vault-example.yml +++ b/scenarios/catalog_try/templates/vault-example.yml @@ -43,3 +43,8 @@ default_admin_vm_ci_ssh_key: "ssh-ed25519 AAAA... alice CODENAME-SCENARIO" # operator-side known_hosts snapshot used by the wait-for-SSH task deployer_cli_user_ssh_known_hosts: "REPLACE_ME" + +#### CLOUD-INIT DNS #### +# default resolver for lab VMs; range42-init.py substitutes the Proxmox node +# resolver here at workspace creation. 1.1.1.1 is the portable fallback. +default_vm_ci_dns_ips: "1.1.1.1" diff --git a/scenarios/debug_scenario_a/01_init_proxmox/templates/alpine/00-template-vm-alpine-nano.yml b/scenarios/debug_scenario_a/01_init_proxmox/templates/alpine/00-template-vm-alpine-nano.yml index f43b8e3f..f76d3fd8 100644 --- a/scenarios/debug_scenario_a/01_init_proxmox/templates/alpine/00-template-vm-alpine-nano.yml +++ b/scenarios/debug_scenario_a/01_init_proxmox/templates/alpine/00-template-vm-alpine-nano.yml @@ -80,7 +80,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_trainee_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_trainee_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "192.168.142.203" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.142.1" diff --git a/scenarios/debug_scenario_a/01_init_proxmox/templates/debian/00-template-vm-debian-nano.yml b/scenarios/debug_scenario_a/01_init_proxmox/templates/debian/00-template-vm-debian-nano.yml index 59681220..5a35db7f 100644 --- a/scenarios/debug_scenario_a/01_init_proxmox/templates/debian/00-template-vm-debian-nano.yml +++ b/scenarios/debug_scenario_a/01_init_proxmox/templates/debian/00-template-vm-debian-nano.yml @@ -80,7 +80,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_trainee_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_trainee_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "192.168.142.202" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.142.1" diff --git a/scenarios/debug_scenario_a/01_init_proxmox/templates/ubuntu_noble/00-template-vm-nano.yml b/scenarios/debug_scenario_a/01_init_proxmox/templates/ubuntu_noble/00-template-vm-nano.yml index a6b2edde..58c7db66 100644 --- a/scenarios/debug_scenario_a/01_init_proxmox/templates/ubuntu_noble/00-template-vm-nano.yml +++ b/scenarios/debug_scenario_a/01_init_proxmox/templates/ubuntu_noble/00-template-vm-nano.yml @@ -80,7 +80,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_trainee_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_trainee_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "192.168.142.201" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.142.1" diff --git a/scenarios/debug_scenario_a/02_network_lab/stage_00/dsa_vm_01.yml b/scenarios/debug_scenario_a/02_network_lab/stage_00/dsa_vm_01.yml index e01c677c..4bec6dad 100644 --- a/scenarios/debug_scenario_a/02_network_lab/stage_00/dsa_vm_01.yml +++ b/scenarios/debug_scenario_a/02_network_lab/stage_00/dsa_vm_01.yml @@ -54,7 +54,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "{{ global_vm_ci_ip }}" vm_ci_netmask: "24" vm_ci_ip_gw: "{{ global_vm_ci_ip_gw }}" diff --git a/scenarios/debug_scenario_a/templates/ansible-vars.yml b/scenarios/debug_scenario_a/templates/ansible-vars.yml index 0a5fe95c..1e52cac0 100644 --- a/scenarios/debug_scenario_a/templates/ansible-vars.yml +++ b/scenarios/debug_scenario_a/templates/ansible-vars.yml @@ -33,3 +33,8 @@ default_admin_vm_ci_user: "alice" # student vm user default_trainee_vm_ci_user: "bob" + +#### CLOUD-INIT DNS #### +# default resolver for lab VMs; range42-init.py substitutes the Proxmox node +# resolver here at workspace creation. 1.1.1.1 is the portable fallback. +default_vm_ci_dns_ips: "1.1.1.1" diff --git a/scenarios/debug_scenario_a/templates/vault-example.yml b/scenarios/debug_scenario_a/templates/vault-example.yml index 87c9daef..db6a9886 100644 --- a/scenarios/debug_scenario_a/templates/vault-example.yml +++ b/scenarios/debug_scenario_a/templates/vault-example.yml @@ -63,3 +63,8 @@ WAZUH_PASSWORD: "REPLACE_ME" # deployer-cli known_hosts path (not really secret, but stored in vault historically) deployer_cli_user_ssh_known_hosts: "/home/your_deployer_cli_username/.ssh/known_hosts" + +#### CLOUD-INIT DNS #### +# default resolver for lab VMs; range42-init.py substitutes the Proxmox node +# resolver here at workspace creation. 1.1.1.1 is the portable fallback. +default_vm_ci_dns_ips: "1.1.1.1" diff --git a/scenarios/debug_scenario_b/01_init_proxmox/templates/alpine/00-template-vm-alpine-nano.yml b/scenarios/debug_scenario_b/01_init_proxmox/templates/alpine/00-template-vm-alpine-nano.yml index f43b8e3f..f76d3fd8 100644 --- a/scenarios/debug_scenario_b/01_init_proxmox/templates/alpine/00-template-vm-alpine-nano.yml +++ b/scenarios/debug_scenario_b/01_init_proxmox/templates/alpine/00-template-vm-alpine-nano.yml @@ -80,7 +80,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_trainee_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_trainee_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "192.168.142.203" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.142.1" diff --git a/scenarios/debug_scenario_b/01_init_proxmox/templates/debian/00-template-vm-debian-nano.yml b/scenarios/debug_scenario_b/01_init_proxmox/templates/debian/00-template-vm-debian-nano.yml index 59681220..5a35db7f 100644 --- a/scenarios/debug_scenario_b/01_init_proxmox/templates/debian/00-template-vm-debian-nano.yml +++ b/scenarios/debug_scenario_b/01_init_proxmox/templates/debian/00-template-vm-debian-nano.yml @@ -80,7 +80,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_trainee_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_trainee_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "192.168.142.202" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.142.1" diff --git a/scenarios/debug_scenario_b/01_init_proxmox/templates/ubuntu_noble/00-template-vm-nano.yml b/scenarios/debug_scenario_b/01_init_proxmox/templates/ubuntu_noble/00-template-vm-nano.yml index a6b2edde..58c7db66 100644 --- a/scenarios/debug_scenario_b/01_init_proxmox/templates/ubuntu_noble/00-template-vm-nano.yml +++ b/scenarios/debug_scenario_b/01_init_proxmox/templates/ubuntu_noble/00-template-vm-nano.yml @@ -80,7 +80,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_trainee_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_trainee_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "192.168.142.201" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.142.1" diff --git a/scenarios/debug_scenario_b/02_network_lab/stage_00/dsb_vm_01.yml b/scenarios/debug_scenario_b/02_network_lab/stage_00/dsb_vm_01.yml index 6210a4e8..fbd44f38 100644 --- a/scenarios/debug_scenario_b/02_network_lab/stage_00/dsb_vm_01.yml +++ b/scenarios/debug_scenario_b/02_network_lab/stage_00/dsb_vm_01.yml @@ -54,7 +54,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "{{ global_vm_ci_ip }}" vm_ci_netmask: "24" vm_ci_ip_gw: "{{ global_vm_ci_ip_gw }}" diff --git a/scenarios/debug_scenario_b/templates/ansible-vars.yml b/scenarios/debug_scenario_b/templates/ansible-vars.yml index 5066474a..659d7d25 100644 --- a/scenarios/debug_scenario_b/templates/ansible-vars.yml +++ b/scenarios/debug_scenario_b/templates/ansible-vars.yml @@ -33,3 +33,8 @@ default_admin_vm_ci_user: "alice" # student vm user default_trainee_vm_ci_user: "bob" + +#### CLOUD-INIT DNS #### +# default resolver for lab VMs; range42-init.py substitutes the Proxmox node +# resolver here at workspace creation. 1.1.1.1 is the portable fallback. +default_vm_ci_dns_ips: "1.1.1.1" diff --git a/scenarios/debug_scenario_b/templates/vault-example.yml b/scenarios/debug_scenario_b/templates/vault-example.yml index 87c9daef..db6a9886 100644 --- a/scenarios/debug_scenario_b/templates/vault-example.yml +++ b/scenarios/debug_scenario_b/templates/vault-example.yml @@ -63,3 +63,8 @@ WAZUH_PASSWORD: "REPLACE_ME" # deployer-cli known_hosts path (not really secret, but stored in vault historically) deployer_cli_user_ssh_known_hosts: "/home/your_deployer_cli_username/.ssh/known_hosts" + +#### CLOUD-INIT DNS #### +# default resolver for lab VMs; range42-init.py substitutes the Proxmox node +# resolver here at workspace creation. 1.1.1.1 is the portable fallback. +default_vm_ci_dns_ips: "1.1.1.1" diff --git a/scenarios/demo_lab/01_init_proxmox/templates/ubuntu_noble/00-template-vm-nano.yml b/scenarios/demo_lab/01_init_proxmox/templates/ubuntu_noble/00-template-vm-nano.yml index 17d60402..c4044fec 100644 --- a/scenarios/demo_lab/01_init_proxmox/templates/ubuntu_noble/00-template-vm-nano.yml +++ b/scenarios/demo_lab/01_init_proxmox/templates/ubuntu_noble/00-template-vm-nano.yml @@ -83,7 +83,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_trainee_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_trainee_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "192.168.142.201" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.142.1" diff --git a/scenarios/demo_lab/01_init_proxmox/templates/ubuntu_noble/01-template-vm-micro-01-2g-24g.yml b/scenarios/demo_lab/01_init_proxmox/templates/ubuntu_noble/01-template-vm-micro-01-2g-24g.yml index c18c5266..d7b8292d 100644 --- a/scenarios/demo_lab/01_init_proxmox/templates/ubuntu_noble/01-template-vm-micro-01-2g-24g.yml +++ b/scenarios/demo_lab/01_init_proxmox/templates/ubuntu_noble/01-template-vm-micro-01-2g-24g.yml @@ -83,7 +83,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_trainee_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_trainee_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "192.168.142.211" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.142.1" diff --git a/scenarios/demo_lab/01_init_proxmox/templates/ubuntu_noble/01-template-vm-micro-02-2g-24g.yml b/scenarios/demo_lab/01_init_proxmox/templates/ubuntu_noble/01-template-vm-micro-02-2g-24g.yml index 297db933..900105fb 100644 --- a/scenarios/demo_lab/01_init_proxmox/templates/ubuntu_noble/01-template-vm-micro-02-2g-24g.yml +++ b/scenarios/demo_lab/01_init_proxmox/templates/ubuntu_noble/01-template-vm-micro-02-2g-24g.yml @@ -83,7 +83,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_trainee_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_trainee_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "192.168.142.212" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.142.1" diff --git a/scenarios/demo_lab/01_init_proxmox/templates/ubuntu_noble/02-template-vm-small-01-4g-32g.yml b/scenarios/demo_lab/01_init_proxmox/templates/ubuntu_noble/02-template-vm-small-01-4g-32g.yml index 6a32c72a..78b43a27 100644 --- a/scenarios/demo_lab/01_init_proxmox/templates/ubuntu_noble/02-template-vm-small-01-4g-32g.yml +++ b/scenarios/demo_lab/01_init_proxmox/templates/ubuntu_noble/02-template-vm-small-01-4g-32g.yml @@ -83,7 +83,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_trainee_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_trainee_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "192.168.142.221" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.142.1" diff --git a/scenarios/demo_lab/01_init_proxmox/templates/ubuntu_noble/02-template-vm-small-02-4g-32g.yml b/scenarios/demo_lab/01_init_proxmox/templates/ubuntu_noble/02-template-vm-small-02-4g-32g.yml index bd2e3b67..a892cc56 100644 --- a/scenarios/demo_lab/01_init_proxmox/templates/ubuntu_noble/02-template-vm-small-02-4g-32g.yml +++ b/scenarios/demo_lab/01_init_proxmox/templates/ubuntu_noble/02-template-vm-small-02-4g-32g.yml @@ -83,7 +83,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_trainee_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_trainee_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "192.168.142.222" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.142.1" diff --git a/scenarios/demo_lab/01_init_proxmox/templates/ubuntu_noble/02-template-vm-small-04-4g-32g.yml b/scenarios/demo_lab/01_init_proxmox/templates/ubuntu_noble/02-template-vm-small-04-4g-32g.yml index 6451b800..c90ffebe 100644 --- a/scenarios/demo_lab/01_init_proxmox/templates/ubuntu_noble/02-template-vm-small-04-4g-32g.yml +++ b/scenarios/demo_lab/01_init_proxmox/templates/ubuntu_noble/02-template-vm-small-04-4g-32g.yml @@ -83,7 +83,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_trainee_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_trainee_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "192.168.142.224" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.142.1" diff --git a/scenarios/demo_lab/01_init_proxmox/templates/ubuntu_noble/03-template-vm-medium-02-8g-64g.yml b/scenarios/demo_lab/01_init_proxmox/templates/ubuntu_noble/03-template-vm-medium-02-8g-64g.yml index 4897f948..fa82e03b 100644 --- a/scenarios/demo_lab/01_init_proxmox/templates/ubuntu_noble/03-template-vm-medium-02-8g-64g.yml +++ b/scenarios/demo_lab/01_init_proxmox/templates/ubuntu_noble/03-template-vm-medium-02-8g-64g.yml @@ -83,7 +83,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_trainee_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_trainee_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "192.168.142.232" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.142.1" diff --git a/scenarios/demo_lab/01_init_proxmox/templates/ubuntu_noble/03-template-vm-medium-04-8g-64g.yml b/scenarios/demo_lab/01_init_proxmox/templates/ubuntu_noble/03-template-vm-medium-04-8g-64g.yml index 815fcda4..d6e6dd3c 100644 --- a/scenarios/demo_lab/01_init_proxmox/templates/ubuntu_noble/03-template-vm-medium-04-8g-64g.yml +++ b/scenarios/demo_lab/01_init_proxmox/templates/ubuntu_noble/03-template-vm-medium-04-8g-64g.yml @@ -83,7 +83,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_trainee_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_trainee_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "192.168.142.234" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.142.1" diff --git a/scenarios/demo_lab/01_init_proxmox/templates/ubuntu_noble/03-template-vm-medium-06-8g-64g.yml b/scenarios/demo_lab/01_init_proxmox/templates/ubuntu_noble/03-template-vm-medium-06-8g-64g.yml index eb42f32a..e67394f4 100644 --- a/scenarios/demo_lab/01_init_proxmox/templates/ubuntu_noble/03-template-vm-medium-06-8g-64g.yml +++ b/scenarios/demo_lab/01_init_proxmox/templates/ubuntu_noble/03-template-vm-medium-06-8g-64g.yml @@ -83,7 +83,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_trainee_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_trainee_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "192.168.142.236" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.142.1" diff --git a/scenarios/demo_lab/01_init_proxmox/templates/ubuntu_noble/04-template-vm-large-04-8g-64g.yml b/scenarios/demo_lab/01_init_proxmox/templates/ubuntu_noble/04-template-vm-large-04-8g-64g.yml index d0c1e6c9..f4532159 100644 --- a/scenarios/demo_lab/01_init_proxmox/templates/ubuntu_noble/04-template-vm-large-04-8g-64g.yml +++ b/scenarios/demo_lab/01_init_proxmox/templates/ubuntu_noble/04-template-vm-large-04-8g-64g.yml @@ -83,7 +83,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_trainee_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_trainee_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "192.168.142.244" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.142.1" diff --git a/scenarios/demo_lab/01_init_proxmox/templates/ubuntu_noble/04-template-vm-large-06-8g-64g.yml b/scenarios/demo_lab/01_init_proxmox/templates/ubuntu_noble/04-template-vm-large-06-8g-64g.yml index 4978e397..b7328661 100644 --- a/scenarios/demo_lab/01_init_proxmox/templates/ubuntu_noble/04-template-vm-large-06-8g-64g.yml +++ b/scenarios/demo_lab/01_init_proxmox/templates/ubuntu_noble/04-template-vm-large-06-8g-64g.yml @@ -83,7 +83,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_trainee_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_trainee_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "192.168.142.246" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.142.1" diff --git a/scenarios/demo_lab/01_init_proxmox/templates/ubuntu_noble/04-template-vm-large-08-8g-64g.yml b/scenarios/demo_lab/01_init_proxmox/templates/ubuntu_noble/04-template-vm-large-08-8g-64g.yml index 27914033..cef77cf9 100644 --- a/scenarios/demo_lab/01_init_proxmox/templates/ubuntu_noble/04-template-vm-large-08-8g-64g.yml +++ b/scenarios/demo_lab/01_init_proxmox/templates/ubuntu_noble/04-template-vm-large-08-8g-64g.yml @@ -83,7 +83,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_trainee_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_trainee_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "192.168.142.248" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.142.1" diff --git a/scenarios/demo_lab/01_init_proxmox/templates/ubuntu_noble/test_setup_templates.yml b/scenarios/demo_lab/01_init_proxmox/templates/ubuntu_noble/test_setup_templates.yml index 74956397..e6aaee03 100644 --- a/scenarios/demo_lab/01_init_proxmox/templates/ubuntu_noble/test_setup_templates.yml +++ b/scenarios/demo_lab/01_init_proxmox/templates/ubuntu_noble/test_setup_templates.yml @@ -83,7 +83,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_trainee_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_trainee_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "192.168.142.55" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.142.1" diff --git a/scenarios/demo_lab/02_admin_infrastructure/stage_00/_testing/wazuh_client_01.yml b/scenarios/demo_lab/02_admin_infrastructure/stage_00/_testing/wazuh_client_01.yml index 15ecb3d1..8d7bdd97 100644 --- a/scenarios/demo_lab/02_admin_infrastructure/stage_00/_testing/wazuh_client_01.yml +++ b/scenarios/demo_lab/02_admin_infrastructure/stage_00/_testing/wazuh_client_01.yml @@ -83,7 +83,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "{{ global_vm_ci_ip }}" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.142.1" diff --git a/scenarios/demo_lab/02_admin_infrastructure/stage_00/builder_api_devkit.yml b/scenarios/demo_lab/02_admin_infrastructure/stage_00/builder_api_devkit.yml index feb318c8..a091c13b 100644 --- a/scenarios/demo_lab/02_admin_infrastructure/stage_00/builder_api_devkit.yml +++ b/scenarios/demo_lab/02_admin_infrastructure/stage_00/builder_api_devkit.yml @@ -85,7 +85,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "{{ global_vm_ci_ip }}" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.142.1" diff --git a/scenarios/demo_lab/02_admin_infrastructure/stage_00/builder_docker_registry.yml b/scenarios/demo_lab/02_admin_infrastructure/stage_00/builder_docker_registry.yml index ec9b7090..1a815280 100644 --- a/scenarios/demo_lab/02_admin_infrastructure/stage_00/builder_docker_registry.yml +++ b/scenarios/demo_lab/02_admin_infrastructure/stage_00/builder_docker_registry.yml @@ -85,7 +85,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "{{ global_vm_ci_ip }}" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.142.1" diff --git a/scenarios/demo_lab/02_admin_infrastructure/stage_00/deployer_api_backend.yml b/scenarios/demo_lab/02_admin_infrastructure/stage_00/deployer_api_backend.yml index b2045cba..0ad1513d 100644 --- a/scenarios/demo_lab/02_admin_infrastructure/stage_00/deployer_api_backend.yml +++ b/scenarios/demo_lab/02_admin_infrastructure/stage_00/deployer_api_backend.yml @@ -85,7 +85,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "{{ global_vm_ci_ip }}" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.142.1" diff --git a/scenarios/demo_lab/02_admin_infrastructure/stage_00/deployer_api_gateway.yml b/scenarios/demo_lab/02_admin_infrastructure/stage_00/deployer_api_gateway.yml index b401adcb..a0b9cd60 100644 --- a/scenarios/demo_lab/02_admin_infrastructure/stage_00/deployer_api_gateway.yml +++ b/scenarios/demo_lab/02_admin_infrastructure/stage_00/deployer_api_gateway.yml @@ -85,7 +85,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "{{ global_vm_ci_ip }}" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.142.1" diff --git a/scenarios/demo_lab/02_admin_infrastructure/stage_00/deployer_ui.yml b/scenarios/demo_lab/02_admin_infrastructure/stage_00/deployer_ui.yml index cde461ef..32d0c35d 100644 --- a/scenarios/demo_lab/02_admin_infrastructure/stage_00/deployer_ui.yml +++ b/scenarios/demo_lab/02_admin_infrastructure/stage_00/deployer_ui.yml @@ -85,7 +85,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "{{ global_vm_ci_ip }}" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.142.1" diff --git a/scenarios/demo_lab/02_admin_infrastructure/stage_00/mon_wazuh.yml b/scenarios/demo_lab/02_admin_infrastructure/stage_00/mon_wazuh.yml index f3e2b796..78c81743 100644 --- a/scenarios/demo_lab/02_admin_infrastructure/stage_00/mon_wazuh.yml +++ b/scenarios/demo_lab/02_admin_infrastructure/stage_00/mon_wazuh.yml @@ -85,7 +85,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "{{ global_vm_ci_ip }}" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.142.1" diff --git a/scenarios/demo_lab/03_student_infrastructure/stage_00/student_box_01.yml b/scenarios/demo_lab/03_student_infrastructure/stage_00/student_box_01.yml index 3f6202ef..ac7458cc 100644 --- a/scenarios/demo_lab/03_student_infrastructure/stage_00/student_box_01.yml +++ b/scenarios/demo_lab/03_student_infrastructure/stage_00/student_box_01.yml @@ -77,7 +77,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "{{ global_vm_ci_ip }}" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.143.1" diff --git a/scenarios/demo_lab/04_ctf_infrastructure/stage_00/vuln_box_00.yml b/scenarios/demo_lab/04_ctf_infrastructure/stage_00/vuln_box_00.yml index d79b8683..db2e822f 100644 --- a/scenarios/demo_lab/04_ctf_infrastructure/stage_00/vuln_box_00.yml +++ b/scenarios/demo_lab/04_ctf_infrastructure/stage_00/vuln_box_00.yml @@ -78,7 +78,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "{{ global_vm_ci_ip }}" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.144.1" diff --git a/scenarios/demo_lab/04_ctf_infrastructure/stage_00/vuln_box_01.yml b/scenarios/demo_lab/04_ctf_infrastructure/stage_00/vuln_box_01.yml index 168d1fc0..e68c2ba5 100644 --- a/scenarios/demo_lab/04_ctf_infrastructure/stage_00/vuln_box_01.yml +++ b/scenarios/demo_lab/04_ctf_infrastructure/stage_00/vuln_box_01.yml @@ -78,7 +78,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "{{ global_vm_ci_ip }}" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.144.1" diff --git a/scenarios/demo_lab/04_ctf_infrastructure/stage_00/vuln_box_02.yml b/scenarios/demo_lab/04_ctf_infrastructure/stage_00/vuln_box_02.yml index d67df58b..0d32c680 100644 --- a/scenarios/demo_lab/04_ctf_infrastructure/stage_00/vuln_box_02.yml +++ b/scenarios/demo_lab/04_ctf_infrastructure/stage_00/vuln_box_02.yml @@ -78,7 +78,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "{{ global_vm_ci_ip }}" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.144.1" diff --git a/scenarios/demo_lab/04_ctf_infrastructure/stage_00/vuln_box_03.yml b/scenarios/demo_lab/04_ctf_infrastructure/stage_00/vuln_box_03.yml index b5f7fa11..10b30ea1 100644 --- a/scenarios/demo_lab/04_ctf_infrastructure/stage_00/vuln_box_03.yml +++ b/scenarios/demo_lab/04_ctf_infrastructure/stage_00/vuln_box_03.yml @@ -78,7 +78,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "{{ global_vm_ci_ip }}" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.144.1" diff --git a/scenarios/demo_lab/04_ctf_infrastructure/stage_00/vuln_box_04.yml b/scenarios/demo_lab/04_ctf_infrastructure/stage_00/vuln_box_04.yml index 4821ec3c..04edc819 100644 --- a/scenarios/demo_lab/04_ctf_infrastructure/stage_00/vuln_box_04.yml +++ b/scenarios/demo_lab/04_ctf_infrastructure/stage_00/vuln_box_04.yml @@ -78,7 +78,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "{{ global_vm_ci_ip }}" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.144.1" diff --git a/scenarios/demo_lab/templates/ansible-vars.yml b/scenarios/demo_lab/templates/ansible-vars.yml index fa9e83c8..03655639 100644 --- a/scenarios/demo_lab/templates/ansible-vars.yml +++ b/scenarios/demo_lab/templates/ansible-vars.yml @@ -33,3 +33,8 @@ default_admin_vm_ci_user: "alice" # student vm user default_trainee_vm_ci_user: "bob" + +#### CLOUD-INIT DNS #### +# default resolver for lab VMs; range42-init.py substitutes the Proxmox node +# resolver here at workspace creation. 1.1.1.1 is the portable fallback. +default_vm_ci_dns_ips: "1.1.1.1" diff --git a/scenarios/demo_lab/templates/vault-example.yml b/scenarios/demo_lab/templates/vault-example.yml index 87c9daef..db6a9886 100644 --- a/scenarios/demo_lab/templates/vault-example.yml +++ b/scenarios/demo_lab/templates/vault-example.yml @@ -63,3 +63,8 @@ WAZUH_PASSWORD: "REPLACE_ME" # deployer-cli known_hosts path (not really secret, but stored in vault historically) deployer_cli_user_ssh_known_hosts: "/home/your_deployer_cli_username/.ssh/known_hosts" + +#### CLOUD-INIT DNS #### +# default resolver for lab VMs; range42-init.py substitutes the Proxmox node +# resolver here at workspace creation. 1.1.1.1 is the portable fallback. +default_vm_ci_dns_ips: "1.1.1.1" diff --git a/scenarios/demo_lab_network/01_init_proxmox/templates/_main_download_cloudinit_files.yml b/scenarios/demo_lab_network/01_init_proxmox/templates/_main_download_cloudinit_files.yml new file mode 100644 index 00000000..1a6ba6c0 --- /dev/null +++ b/scenarios/demo_lab_network/01_init_proxmox/templates/_main_download_cloudinit_files.yml @@ -0,0 +1,41 @@ +## +## + + +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# +# PROMOX INIT - download cloud init files +# +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- hosts: proxmox + gather_facts: false + vars_files: + - "../../secrets/default_vault.yml" + + tasks: + - name: PROMOX INIT - DOWNLOAD - CLOUD INIT images + include_role: + name: range42-ansible_roles-proxmox_controller + + vars: + proxmox_vm_action: "storage_download_iso" + # proxmox_node: "px-testing" + proxmox_storage: "local" + iso_file_content_type: "iso" + iso_url: "{{ item.iso_url }}" + iso_file_name: "{{ item.iso_file_name }}" + + loop: + - { + iso_url: "https://cloud-images.ubuntu.com/minimal/daily/noble/current/noble-minimal-cloudimg-amd64.img", + iso_file_name: "noble-minimal-cloudimg-amd64.img", + } + - { + iso_url: "https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img", + iso_file_name: "noble-server-cloudimg-amd64.img", + } + - { + iso_url: "https://cloud.debian.org/images/cloud/bookworm/latest/debian-12-genericcloud-amd64.raw", + iso_file_name: "debian-12-genericcloud-amd64.img", + } diff --git a/scenarios/demo_lab_network/01_init_proxmox/templates/alpine/place_holder b/scenarios/demo_lab_network/01_init_proxmox/templates/alpine/place_holder new file mode 100644 index 00000000..e69de29b diff --git a/scenarios/demo_lab_network/01_init_proxmox/templates/debian/place_holder b/scenarios/demo_lab_network/01_init_proxmox/templates/debian/place_holder new file mode 100644 index 00000000..e69de29b diff --git a/scenarios/demo_lab_network/01_init_proxmox/templates/ubuntu_noble/00-template-vm-nano.yml b/scenarios/demo_lab_network/01_init_proxmox/templates/ubuntu_noble/00-template-vm-nano.yml new file mode 100644 index 00000000..4cef26e9 --- /dev/null +++ b/scenarios/demo_lab_network/01_init_proxmox/templates/ubuntu_noble/00-template-vm-nano.yml @@ -0,0 +1,99 @@ +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# +# PROMOX INIT - create ubuntu noble template : +# +# vm_id : 9901 +# cpu core : 1 +# ram : 1g +# disk : 16g +# ip : 192.168.142.201 +# +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +## +## + +- hosts: proxmox + gather_facts: false + vars_files: + - "../../../secrets/default_vault.yml" + + tasks: + # + - name: PROMOX INIT - TEMPLATES - CREATE TEMPLATE - template-vm-nano + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "vm_create" + # proxmox_node: "px-testing" + vm_id: 9901 + vm_name: "template-vm-nano" + vm_cpu: "host" + vm_cores: 1 + vm_sockets: 1 + vm_memory: 1024 + vm_net_virtio_bridge: "vmbr140" + + # + - name: PROMOX INIT - TEMPLATES - CONVERT VM TO TEMPLATE - template-vm-nano + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "template_convert_vm_to_template" + # proxmox_node: "px-testing" + proxmox_storage: "local" + vm_id: 9901 + +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- hosts: proxmox-cli + gather_facts: false + vars_files: + - "../../../secrets/default_vault.yml" + + tasks: + # + - name: PROMOX INIT - TEMPLATES - IMPORT CLOUD INIT DISK - template-vm-nano + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "template_cloudinit_import_disk" + proxmox_node: "px-testing-cli" + cloudinit_image_full_path: "/var/lib/vz/template/iso/noble-minimal-cloudimg-amd64.img" + vm_id: 9901 + vm_disk_size: "16g" + proxmox_dest_vm_storage_name: "local-lvm" + +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- hosts: proxmox + gather_facts: false + vars_files: + - "../../../secrets/default_vault.yml" + + tasks: + # + - name: PROMOX INIT - TEMPLATES - SET CLOUD DEFAULT INIT VARIABLE - template-vm-nano + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "cloudinit_set_variables" + # proxmox_node: "px-testing" + vm_id: 9901 + vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" + vm_ci_password: "{{ default_trainee_vm_ci_password | default('supersecret') }}" + vm_ci_ssh_key: "{{ default_trainee_vm_ci_ssh_key }}" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" + vm_ci_ip: "192.168.142.201" + vm_ci_netmask: "24" + vm_ci_ip_gw: "192.168.142.1" + + # + - name: PROMOX INIT - TEMPLATES - SET PROMOX TAG + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "vm_set_tag" + # proxmox_node: "px-testing" + vm_id: 9901 + vm_tag_name: "template" diff --git a/scenarios/demo_lab_network/01_init_proxmox/templates/ubuntu_noble/01-template-vm-micro-01-2g-24g.yml b/scenarios/demo_lab_network/01_init_proxmox/templates/ubuntu_noble/01-template-vm-micro-01-2g-24g.yml new file mode 100644 index 00000000..9c626206 --- /dev/null +++ b/scenarios/demo_lab_network/01_init_proxmox/templates/ubuntu_noble/01-template-vm-micro-01-2g-24g.yml @@ -0,0 +1,99 @@ +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# +# PROMOX INIT - create ubuntu noble template : +# +# vm_id : 9211 +# cpu core : 1 +# ram : 2g +# disk : 24g +# ip : 192.168.142.211 +# +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +## +## + +- hosts: proxmox + gather_facts: false + vars_files: + - "../../../secrets/default_vault.yml" + + tasks: + # + - name: PROMOX INIT - TEMPLATES - CREATE TEMPLATE - template-vm-micro-01-2g-24g + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "vm_create" + # proxmox_node: "px-testing" + vm_id: 9211 + vm_name: "template-vm-micro-01-2g-24g" + vm_cpu: "host" + vm_cores: 1 + vm_sockets: 1 + vm_memory: 2048 + vm_net_virtio_bridge: "vmbr140" + + # + - name: PROMOX INIT - TEMPLATES - CONVERT VM TO TEMPLATE - template-vm-micro-01-2g-24g + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "template_convert_vm_to_template" + # proxmox_node: "px-testing" + proxmox_storage: "local" + vm_id: 9211 + +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- hosts: proxmox-cli + gather_facts: false + vars_files: + - "../../../secrets/default_vault.yml" + + tasks: + # + - name: PROMOX INIT - TEMPLATES - IMPORT CLOUD INIT DISK - template-vm-micro-01-2g-24g + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "template_cloudinit_import_disk" + proxmox_node: "px-testing-cli" + cloudinit_image_full_path: "/var/lib/vz/template/iso/noble-minimal-cloudimg-amd64.img" + vm_id: 9211 + vm_disk_size: "24g" + proxmox_dest_vm_storage_name: "local-lvm" + +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- hosts: proxmox + gather_facts: false + vars_files: + - "../../../secrets/default_vault.yml" + + tasks: + # + - name: PROMOX INIT - TEMPLATES - SET CLOUD DEFAULT INIT VARIABLE - template-vm-micro-01-2g-24g + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "cloudinit_set_variables" + # proxmox_node: "px-testing" + vm_id: 9211 + vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" + vm_ci_password: "{{ default_trainee_vm_ci_password | default('supersecret') }}" + vm_ci_ssh_key: "{{ default_trainee_vm_ci_ssh_key }}" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" + vm_ci_ip: "192.168.142.211" + vm_ci_netmask: "24" + vm_ci_ip_gw: "192.168.142.1" + + # + - name: PROMOX INIT - TEMPLATES - SET PROMOX TAG - template-vm-micro-01-2g-24g + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "vm_set_tag" + # proxmox_node: "px-testing" + vm_id: 9211 + vm_tag_name: "template" diff --git a/scenarios/demo_lab_network/01_init_proxmox/templates/ubuntu_noble/01-template-vm-micro-02-2g-24g.yml b/scenarios/demo_lab_network/01_init_proxmox/templates/ubuntu_noble/01-template-vm-micro-02-2g-24g.yml new file mode 100644 index 00000000..b534faae --- /dev/null +++ b/scenarios/demo_lab_network/01_init_proxmox/templates/ubuntu_noble/01-template-vm-micro-02-2g-24g.yml @@ -0,0 +1,99 @@ +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# +# PROMOX INIT - create ubuntu noble template : +# +# vm_id : 92212 +# cpu core : 2 +# ram : 2g +# disk : 24g +# ip : 192.168.142.212 +# +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +## +## + +- hosts: proxmox + gather_facts: false + vars_files: + - "../../../secrets/default_vault.yml" + + tasks: + # + - name: PROMOX INIT - TEMPLATES - CREATE TEMPLATE - template-vm-micro-02-2g-24g + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "vm_create" + # proxmox_node: "px-testing" + vm_id: 9212 + vm_name: "template-vm-micro-02-2g-24g" + vm_cpu: "host" + vm_cores: 2 + vm_sockets: 1 + vm_memory: 2048 + vm_net_virtio_bridge: "vmbr140" + + # + - name: PROMOX INIT - TEMPLATES - CONVERT VM TO TEMPLATE - template-vm-micro-02-2g-24g + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "template_convert_vm_to_template" + # proxmox_node: "px-testing" + proxmox_storage: "local" + vm_id: 9212 + +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- hosts: proxmox-cli + gather_facts: false + vars_files: + - "../../../secrets/default_vault.yml" + + tasks: + # + - name: PROMOX INIT - TEMPLATES - IMPORT CLOUD INIT DISK - template-vm-micro-02-2g-24g + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "template_cloudinit_import_disk" + proxmox_node: "px-testing-cli" + cloudinit_image_full_path: "/var/lib/vz/template/iso/noble-minimal-cloudimg-amd64.img" + vm_id: 9212 + vm_disk_size: "24g" + proxmox_dest_vm_storage_name: "local-lvm" + +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- hosts: proxmox + gather_facts: false + vars_files: + - "../../../secrets/default_vault.yml" + + tasks: + # + - name: PROMOX INIT - TEMPLATES - SET CLOUD DEFAULT INIT VARIABLE - template-vm-micro-02-2g-24g + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "cloudinit_set_variables" + # proxmox_node: "px-testing" + vm_id: 9212 + vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" + vm_ci_password: "{{ default_trainee_vm_ci_password | default('supersecret') }}" + vm_ci_ssh_key: "{{ default_trainee_vm_ci_ssh_key }}" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" + vm_ci_ip: "192.168.142.212" + vm_ci_netmask: "24" + vm_ci_ip_gw: "192.168.142.1" + + # + - name: PROMOX INIT - TEMPLATES - SET PROMOX TAG - template-vm-micro-02-2g-24g + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "vm_set_tag" + # proxmox_node: "px-testing" + vm_id: 9212 + vm_tag_name: "template" diff --git a/scenarios/demo_lab_network/01_init_proxmox/templates/ubuntu_noble/02-template-vm-small-01-4g-32g.yml b/scenarios/demo_lab_network/01_init_proxmox/templates/ubuntu_noble/02-template-vm-small-01-4g-32g.yml new file mode 100644 index 00000000..b69cec20 --- /dev/null +++ b/scenarios/demo_lab_network/01_init_proxmox/templates/ubuntu_noble/02-template-vm-small-01-4g-32g.yml @@ -0,0 +1,99 @@ +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# +# PROMOX INIT - create ubuntu noble template : +# +# vm_id : 9221 +# cpu core : 1 +# ram : 4g +# disk : 32g +# ip : 192.168.142.221 +# +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +## +## + +- hosts: proxmox + gather_facts: false + vars_files: + - "../../../secrets/default_vault.yml" + + tasks: + # + - name: PROMOX INIT - TEMPLATES - CREATE TEMPLATE - template-vm-small-01-4g-32g + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "vm_create" + # proxmox_node: "px-testing" + vm_id: 9221 + vm_name: "template-vm-small-01-4g-32g" + vm_cpu: "host" + vm_cores: 1 + vm_sockets: 1 + vm_memory: 4096 + vm_net_virtio_bridge: "vmbr140" + + # + - name: PROMOX INIT - TEMPLATES - CONVERT VM TO TEMPLATE - template-vm-small-01-4g-32g + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "template_convert_vm_to_template" + # proxmox_node: "px-testing" + proxmox_storage: "local" + vm_id: 9221 + +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- hosts: proxmox-cli + gather_facts: false + vars_files: + - "../../../secrets/default_vault.yml" + + tasks: + # + - name: PROMOX INIT - TEMPLATES - IMPORT CLOUD INIT DISK - template-vm-small-01-4g-32g + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "template_cloudinit_import_disk" + proxmox_node: "px-testing-cli" + cloudinit_image_full_path: "/var/lib/vz/template/iso/noble-minimal-cloudimg-amd64.img" + vm_id: 9221 + vm_disk_size: "32g" + proxmox_dest_vm_storage_name: "local-lvm" + +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- hosts: proxmox + gather_facts: false + vars_files: + - "../../../secrets/default_vault.yml" + + tasks: + # + - name: PROMOX INIT - TEMPLATES - SET CLOUD DEFAULT INIT VARIABLE - template-vm-small-01-4g-32g + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "cloudinit_set_variables" + # proxmox_node: "px-testing" + vm_id: 9221 + vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" + vm_ci_password: "{{ default_trainee_vm_ci_password | default('supersecret') }}" + vm_ci_ssh_key: "{{ default_trainee_vm_ci_ssh_key }}" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" + vm_ci_ip: "192.168.142.221" + vm_ci_netmask: "24" + vm_ci_ip_gw: "192.168.142.1" + + # + - name: PROMOX INIT - TEMPLATES - SET PROMOX TAG - template-vm-small-01-4g-32g + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "vm_set_tag" + # proxmox_node: "px-testing" + vm_id: 9221 + vm_tag_name: "template" diff --git a/scenarios/demo_lab_network/01_init_proxmox/templates/ubuntu_noble/02-template-vm-small-02-4g-32g.yml b/scenarios/demo_lab_network/01_init_proxmox/templates/ubuntu_noble/02-template-vm-small-02-4g-32g.yml new file mode 100644 index 00000000..94b7a52f --- /dev/null +++ b/scenarios/demo_lab_network/01_init_proxmox/templates/ubuntu_noble/02-template-vm-small-02-4g-32g.yml @@ -0,0 +1,99 @@ +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# +# PROMOX INIT - create ubuntu noble template : +# +# vm_id : 9222 +# cpu core : 2 +# ram : 4g +# disk : 32g +# ip : 192.168.142.222 +# +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +## +## + +- hosts: proxmox + gather_facts: false + vars_files: + - "../../../secrets/default_vault.yml" + + tasks: + # + - name: PROMOX INIT - TEMPLATES - CREATE TEMPLATE - template-vm-small-02-4g-32g + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "vm_create" + # proxmox_node: "px-testing" + vm_id: 9222 + vm_name: "template-vm-small-02-4g-32g" + vm_cpu: "host" + vm_cores: 2 + vm_sockets: 1 + vm_memory: 4096 + vm_net_virtio_bridge: "vmbr140" + + # + - name: PROMOX INIT - TEMPLATES - CONVERT VM TO TEMPLATE - template-vm-small-02-4g-32g + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "template_convert_vm_to_template" + # proxmox_node: "px-testing" + proxmox_storage: "local" + vm_id: 9222 + +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- hosts: proxmox-cli + gather_facts: false + vars_files: + - "../../../secrets/default_vault.yml" + + tasks: + # + - name: PROMOX INIT - TEMPLATES - IMPORT CLOUD INIT DISK - template-vm-small-02-4g-32g + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "template_cloudinit_import_disk" + proxmox_node: "px-testing-cli" + cloudinit_image_full_path: "/var/lib/vz/template/iso/noble-minimal-cloudimg-amd64.img" + vm_id: 9222 + vm_disk_size: "32g" + proxmox_dest_vm_storage_name: "local-lvm" + +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- hosts: proxmox + gather_facts: false + vars_files: + - "../../../secrets/default_vault.yml" + + tasks: + # + - name: PROMOX INIT - TEMPLATES - SET CLOUD DEFAULT INIT VARIABLE - template-vm-small-02-4g-32g + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "cloudinit_set_variables" + # proxmox_node: "px-testing" + vm_id: 9222 + vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" + vm_ci_password: "{{ default_trainee_vm_ci_password | default('supersecret') }}" + vm_ci_ssh_key: "{{ default_trainee_vm_ci_ssh_key }}" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" + vm_ci_ip: "192.168.142.222" + vm_ci_netmask: "24" + vm_ci_ip_gw: "192.168.142.1" + + # + - name: PROMOX INIT - TEMPLATES - SET PROMOX TAG - template-vm-small-02-4g-32g + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "vm_set_tag" + # proxmox_node: "px-testing" + vm_id: 9222 + vm_tag_name: "template" diff --git a/scenarios/demo_lab_network/01_init_proxmox/templates/ubuntu_noble/02-template-vm-small-04-4g-32g.yml b/scenarios/demo_lab_network/01_init_proxmox/templates/ubuntu_noble/02-template-vm-small-04-4g-32g.yml new file mode 100644 index 00000000..40002d62 --- /dev/null +++ b/scenarios/demo_lab_network/01_init_proxmox/templates/ubuntu_noble/02-template-vm-small-04-4g-32g.yml @@ -0,0 +1,99 @@ +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# +# PROMOX INIT - create ubuntu noble template : +# +# vm_id : 9224 +# cpu core : 4 +# ram : 4g +# disk : 32g +# ip : 192.168.142.224 +# +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +## +## + +- hosts: proxmox + gather_facts: false + vars_files: + - "../../../secrets/default_vault.yml" + + tasks: + # + - name: PROMOX INIT - TEMPLATES - CREATE TEMPLATE - template-vm-small-04-4g-32g + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "vm_create" + # proxmox_node: "px-testing" + vm_id: 9224 + vm_name: "template-vm-small-04-4g-32g" + vm_cpu: "host" + vm_cores: 4 + vm_sockets: 1 + vm_memory: 4096 + vm_net_virtio_bridge: "vmbr140" + + # + - name: PROMOX INIT - TEMPLATES - CONVERT VM TO TEMPLATE - template-vm-small-04-4g-32g + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "template_convert_vm_to_template" + # proxmox_node: "px-testing" + proxmox_storage: "local" + vm_id: 9224 + +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- hosts: proxmox-cli + gather_facts: false + vars_files: + - "../../../secrets/default_vault.yml" + + tasks: + # + - name: PROMOX INIT - TEMPLATES - IMPORT CLOUD INIT DISK - template-vm-small-04-4g-32g + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "template_cloudinit_import_disk" + proxmox_node: "px-testing-cli" + cloudinit_image_full_path: "/var/lib/vz/template/iso/noble-minimal-cloudimg-amd64.img" + vm_id: 9224 + vm_disk_size: "32g" + proxmox_dest_vm_storage_name: "local-lvm" + +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- hosts: proxmox + gather_facts: false + vars_files: + - "../../../secrets/default_vault.yml" + + tasks: + # + - name: PROMOX INIT - TEMPLATES - SET CLOUD DEFAULT INIT VARIABLE - template-vm-small-04-4g-32g + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "cloudinit_set_variables" + # proxmox_node: "px-testing" + vm_id: 9224 + vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" + vm_ci_password: "{{ default_trainee_vm_ci_password | default('supersecret') }}" + vm_ci_ssh_key: "{{ default_trainee_vm_ci_ssh_key }}" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" + vm_ci_ip: "192.168.142.224" + vm_ci_netmask: "24" + vm_ci_ip_gw: "192.168.142.1" + + # + - name: PROMOX INIT - TEMPLATES - SET PROMOX TAG - template-vm-small-04-4g-32g + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "vm_set_tag" + # proxmox_node: "px-testing" + vm_id: 9224 + vm_tag_name: "template" diff --git a/scenarios/demo_lab_network/01_init_proxmox/templates/ubuntu_noble/03-template-vm-medium-02-8g-64g.yml b/scenarios/demo_lab_network/01_init_proxmox/templates/ubuntu_noble/03-template-vm-medium-02-8g-64g.yml new file mode 100644 index 00000000..a385c1c7 --- /dev/null +++ b/scenarios/demo_lab_network/01_init_proxmox/templates/ubuntu_noble/03-template-vm-medium-02-8g-64g.yml @@ -0,0 +1,99 @@ +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# +# PROMOX INIT - create ubuntu noble template : +# +# vm_id : 9232 +# cpu core : 2 +# ram : 8g +# disk : 64g +# ip : 192.168.142.232 +# +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +## +## + +- hosts: proxmox + gather_facts: false + vars_files: + - "../../../secrets/default_vault.yml" + + tasks: + # + - name: PROMOX INIT - TEMPLATES - CREATE TEMPLATE - template-vm-medium-02-8g-64g + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "vm_create" + # proxmox_node: "px-testing" + vm_id: 9232 + vm_name: "template-vm-medium-02-8g-64g" + vm_cpu: "host" + vm_cores: 2 + vm_sockets: 1 + vm_memory: 8192 + vm_net_virtio_bridge: "vmbr140" + + # + - name: PROMOX INIT - TEMPLATES - CONVERT VM TO TEMPLATE - template-vm-medium-02-8g-64g + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "template_convert_vm_to_template" + # proxmox_node: "px-testing" + proxmox_storage: "local" + vm_id: 9232 + +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- hosts: proxmox-cli + gather_facts: false + vars_files: + - "../../../secrets/default_vault.yml" + + tasks: + # + - name: PROMOX INIT - TEMPLATES - IMPORT CLOUD INIT DISK - template-vm-medium-02-8g-64g + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "template_cloudinit_import_disk" + proxmox_node: "px-testing-cli" + cloudinit_image_full_path: "/var/lib/vz/template/iso/noble-minimal-cloudimg-amd64.img" + vm_id: 9232 + vm_disk_size: "64g" + proxmox_dest_vm_storage_name: "local-lvm" + +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- hosts: proxmox + gather_facts: false + vars_files: + - "../../../secrets/default_vault.yml" + + tasks: + # + - name: PROMOX INIT - TEMPLATES - SET CLOUD DEFAULT INIT VARIABLE - template-vm-medium-02-8g-64g + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "cloudinit_set_variables" + # proxmox_node: "px-testing" + vm_id: 9232 + vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" + vm_ci_password: "{{ default_trainee_vm_ci_password | default('supersecret') }}" + vm_ci_ssh_key: "{{ default_trainee_vm_ci_ssh_key }}" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" + vm_ci_ip: "192.168.142.232" + vm_ci_netmask: "24" + vm_ci_ip_gw: "192.168.142.1" + + # + - name: PROMOX INIT - TEMPLATES - SET PROMOX TAG - template-vm-medium-02-8g-64g + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "vm_set_tag" + # proxmox_node: "px-testing" + vm_id: 9232 + vm_tag_name: "template" diff --git a/scenarios/demo_lab_network/01_init_proxmox/templates/ubuntu_noble/03-template-vm-medium-04-8g-64g.yml b/scenarios/demo_lab_network/01_init_proxmox/templates/ubuntu_noble/03-template-vm-medium-04-8g-64g.yml new file mode 100644 index 00000000..ed863fa5 --- /dev/null +++ b/scenarios/demo_lab_network/01_init_proxmox/templates/ubuntu_noble/03-template-vm-medium-04-8g-64g.yml @@ -0,0 +1,99 @@ +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# +# PROMOX INIT - create ubuntu noble template : +# +# vm_id : 9234 +# cpu core : 4 +# ram : 8g +# disk : 64g +# ip : 192.168.142.234 +# +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +## +## + +- hosts: proxmox + gather_facts: false + vars_files: + - "../../../secrets/default_vault.yml" + + tasks: + # + - name: PROMOX INIT - TEMPLATES - CREATE TEMPLATE - template-vm-medium-04-8g-64g + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "vm_create" + # proxmox_node: "px-testing" + vm_id: 9234 + vm_name: "template-vm-medium-04-8g-64g" + vm_cpu: "host" + vm_cores: 4 + vm_sockets: 1 + vm_memory: 8192 + vm_net_virtio_bridge: "vmbr140" + + # + - name: PROMOX INIT - TEMPLATES - CONVERT VM TO TEMPLATE - template-vm-medium-04-8g-64g + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "template_convert_vm_to_template" + # proxmox_node: "px-testing" + proxmox_storage: "local" + vm_id: 9234 + +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- hosts: proxmox-cli + gather_facts: false + vars_files: + - "../../../secrets/default_vault.yml" + + tasks: + # + - name: PROMOX INIT - TEMPLATES - IMPORT CLOUD INIT DISK - template-vm-medium-04-8g-64g + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "template_cloudinit_import_disk" + proxmox_node: "px-testing-cli" + cloudinit_image_full_path: "/var/lib/vz/template/iso/noble-minimal-cloudimg-amd64.img" + vm_id: 9234 + vm_disk_size: "64g" + proxmox_dest_vm_storage_name: "local-lvm" + +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- hosts: proxmox + gather_facts: false + vars_files: + - "../../../secrets/default_vault.yml" + + tasks: + # + - name: PROMOX INIT - TEMPLATES - SET CLOUD DEFAULT INIT VARIABLE - template-vm-medium-04-8g-64g + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "cloudinit_set_variables" + # proxmox_node: "px-testing" + vm_id: 9234 + vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" + vm_ci_password: "{{ default_trainee_vm_ci_password | default('supersecret') }}" + vm_ci_ssh_key: "{{ default_trainee_vm_ci_ssh_key }}" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" + vm_ci_ip: "192.168.142.234" + vm_ci_netmask: "24" + vm_ci_ip_gw: "192.168.142.1" + + # + - name: PROMOX INIT - TEMPLATES - SET PROMOX TAG - template-vm-medium-04-8g-64g + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "vm_set_tag" + # proxmox_node: "px-testing" + vm_id: 9234 + vm_tag_name: "template" diff --git a/scenarios/demo_lab_network/01_init_proxmox/templates/ubuntu_noble/03-template-vm-medium-06-8g-64g.yml b/scenarios/demo_lab_network/01_init_proxmox/templates/ubuntu_noble/03-template-vm-medium-06-8g-64g.yml new file mode 100644 index 00000000..33d9dafe --- /dev/null +++ b/scenarios/demo_lab_network/01_init_proxmox/templates/ubuntu_noble/03-template-vm-medium-06-8g-64g.yml @@ -0,0 +1,99 @@ +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# +# PROMOX INIT - create ubuntu noble template : +# +# vm_id : 9236 +# cpu core : 6 +# ram : 8g +# disk : 64g +# ip : 192.168.142.236 +# +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +## +## + +- hosts: proxmox + gather_facts: false + vars_files: + - "../../../secrets/default_vault.yml" + + tasks: + # + - name: PROMOX INIT - TEMPLATES - CREATE TEMPLATE - template-vm-medium-06-8g-64g + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "vm_create" + # proxmox_node: "px-testing" + vm_id: 9236 + vm_name: "template-vm-medium-06-8g-64g" + vm_cpu: "host" + vm_cores: 6 + vm_sockets: 1 + vm_memory: 8192 + vm_net_virtio_bridge: "vmbr140" + + # + - name: PROMOX INIT - TEMPLATES - CONVERT VM TO TEMPLATE - template-vm-medium-06-8g-64g + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "template_convert_vm_to_template" + # proxmox_node: "px-testing" + proxmox_storage: "local" + vm_id: 9236 + +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- hosts: proxmox-cli + gather_facts: false + vars_files: + - "../../../secrets/default_vault.yml" + + tasks: + # + - name: PROMOX INIT - TEMPLATES - IMPORT CLOUD INIT DISK - template-vm-medium-06-8g-64g + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "template_cloudinit_import_disk" + proxmox_node: "px-testing-cli" + cloudinit_image_full_path: "/var/lib/vz/template/iso/noble-minimal-cloudimg-amd64.img" + vm_id: 9236 + vm_disk_size: "64g" + proxmox_dest_vm_storage_name: "local-lvm" + +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- hosts: proxmox + gather_facts: false + vars_files: + - "../../../secrets/default_vault.yml" + + tasks: + # + - name: PROMOX INIT - TEMPLATES - SET CLOUD DEFAULT INIT VARIABLE - template-vm-medium-06-8g-64g + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "cloudinit_set_variables" + # proxmox_node: "px-testing" + vm_id: 9236 + vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" + vm_ci_password: "{{ default_trainee_vm_ci_password | default('supersecret') }}" + vm_ci_ssh_key: "{{ default_trainee_vm_ci_ssh_key }}" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" + vm_ci_ip: "192.168.142.236" + vm_ci_netmask: "24" + vm_ci_ip_gw: "192.168.142.1" + + # + - name: PROMOX INIT - TEMPLATES - SET PROMOX TAG - template-vm-medium-06-8g-64g + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "vm_set_tag" + # proxmox_node: "px-testing" + vm_id: 9236 + vm_tag_name: "template" diff --git a/scenarios/demo_lab_network/01_init_proxmox/templates/ubuntu_noble/04-template-vm-large-04-8g-64g.yml b/scenarios/demo_lab_network/01_init_proxmox/templates/ubuntu_noble/04-template-vm-large-04-8g-64g.yml new file mode 100644 index 00000000..bc8d6f46 --- /dev/null +++ b/scenarios/demo_lab_network/01_init_proxmox/templates/ubuntu_noble/04-template-vm-large-04-8g-64g.yml @@ -0,0 +1,99 @@ +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# +# PROMOX INIT - create ubuntu noble template : +# +# vm_id : 9244 +# cpu core : 4 +# ram : 16g +# disk : 100g +# ip : 192.168.142.244 +# +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +## +## + +- hosts: proxmox + gather_facts: false + vars_files: + - "../../../secrets/default_vault.yml" + + tasks: + # + - name: PROMOX INIT - TEMPLATES - CREATE TEMPLATE - template-vm-large-04-16g-100g + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "vm_create" + # proxmox_node: "px-testing" + vm_id: 9244 + vm_name: "template-vm-large-04-16g-100g" + vm_cpu: "host" + vm_cores: 4 + vm_sockets: 1 + vm_memory: 16384 + vm_net_virtio_bridge: "vmbr140" + + # + - name: PROMOX INIT - TEMPLATES - CONVERT VM TO TEMPLATE - template-vm-large-04-16g-100g + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "template_convert_vm_to_template" + # proxmox_node: "px-testing" + proxmox_storage: "local" + vm_id: 9244 + +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- hosts: proxmox-cli + gather_facts: false + vars_files: + - "../../../secrets/default_vault.yml" + + tasks: + # + - name: PROMOX INIT - TEMPLATES - IMPORT CLOUD INIT DISK - template-vm-large-04-16g-100g + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "template_cloudinit_import_disk" + proxmox_node: "px-testing-cli" + cloudinit_image_full_path: "/var/lib/vz/template/iso/noble-minimal-cloudimg-amd64.img" + vm_id: 9244 + vm_disk_size: "100g" + proxmox_dest_vm_storage_name: "local-lvm" + +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- hosts: proxmox + gather_facts: false + vars_files: + - "../../../secrets/default_vault.yml" + + tasks: + # + - name: PROMOX INIT - TEMPLATES - SET CLOUD DEFAULT INIT VARIABLE - template-vm-large-04-16g-100g + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "cloudinit_set_variables" + # proxmox_node: "px-testing" + vm_id: 9244 + vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" + vm_ci_password: "{{ default_trainee_vm_ci_password | default('supersecret') }}" + vm_ci_ssh_key: "{{ default_trainee_vm_ci_ssh_key }}" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" + vm_ci_ip: "192.168.142.244" + vm_ci_netmask: "24" + vm_ci_ip_gw: "192.168.142.1" + + # + - name: PROMOX INIT - TEMPLATES - SET PROMOX TAG - template-vm-large-04-16g-100g + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "vm_set_tag" + # proxmox_node: "px-testing" + vm_id: 9244 + vm_tag_name: "template" diff --git a/scenarios/demo_lab_network/01_init_proxmox/templates/ubuntu_noble/04-template-vm-large-06-8g-64g.yml b/scenarios/demo_lab_network/01_init_proxmox/templates/ubuntu_noble/04-template-vm-large-06-8g-64g.yml new file mode 100644 index 00000000..0e12fef5 --- /dev/null +++ b/scenarios/demo_lab_network/01_init_proxmox/templates/ubuntu_noble/04-template-vm-large-06-8g-64g.yml @@ -0,0 +1,99 @@ +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# +# PROMOX INIT - create ubuntu noble template : +# +# vm_id : 9246 +# cpu core : 6 +# ram : 16g +# disk : 100g +# ip : 192.168.142.246 +# +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +## +## + +- hosts: proxmox + gather_facts: false + vars_files: + - "../../../secrets/default_vault.yml" + + tasks: + # + - name: PROMOX INIT - TEMPLATES - CREATE TEMPLATE - template-vm-large-06-16g-100g + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "vm_create" + # proxmox_node: "px-testing" + vm_id: 9246 + vm_name: "template-vm-large-06-16g-100g" + vm_cpu: "host" + vm_cores: 6 + vm_sockets: 1 + vm_memory: 16384 + vm_net_virtio_bridge: "vmbr140" + + # + - name: PROMOX INIT - TEMPLATES - CONVERT VM TO TEMPLATE - template-vm-large-06-16g-100g + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "template_convert_vm_to_template" + # proxmox_node: "px-testing" + proxmox_storage: "local" + vm_id: 9246 + +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- hosts: proxmox-cli + gather_facts: false + vars_files: + - "../../../secrets/default_vault.yml" + + tasks: + # + - name: PROMOX INIT - TEMPLATES - IMPORT CLOUD INIT DISK - template-vm-large-06-16g-100g + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "template_cloudinit_import_disk" + proxmox_node: "px-testing-cli" + cloudinit_image_full_path: "/var/lib/vz/template/iso/noble-minimal-cloudimg-amd64.img" + vm_id: 9246 + vm_disk_size: "100g" + proxmox_dest_vm_storage_name: "local-lvm" + +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- hosts: proxmox + gather_facts: false + vars_files: + - "../../../secrets/default_vault.yml" + + tasks: + # + - name: PROMOX INIT - TEMPLATES - SET CLOUD DEFAULT INIT VARIABLE - template-vm-large-06-16g-100g + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "cloudinit_set_variables" + # proxmox_node: "px-testing" + vm_id: 9246 + vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" + vm_ci_password: "{{ default_trainee_vm_ci_password | default('supersecret') }}" + vm_ci_ssh_key: "{{ default_trainee_vm_ci_ssh_key }}" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" + vm_ci_ip: "192.168.142.246" + vm_ci_netmask: "24" + vm_ci_ip_gw: "192.168.142.1" + + # + - name: PROMOX INIT - TEMPLATES - SET PROMOX TAG - template-vm-large-06-16g-100g + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "vm_set_tag" + # proxmox_node: "px-testing" + vm_id: 9246 + vm_tag_name: "template" diff --git a/scenarios/demo_lab_network/01_init_proxmox/templates/ubuntu_noble/04-template-vm-large-08-8g-64g.yml b/scenarios/demo_lab_network/01_init_proxmox/templates/ubuntu_noble/04-template-vm-large-08-8g-64g.yml new file mode 100644 index 00000000..630e056a --- /dev/null +++ b/scenarios/demo_lab_network/01_init_proxmox/templates/ubuntu_noble/04-template-vm-large-08-8g-64g.yml @@ -0,0 +1,99 @@ +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# +# PROMOX INIT - create ubuntu noble template : +# +# vm_id : 9248 +# cpu core : 8 +# ram : 16g +# disk : 100g +# ip : 192.168.142.248 +# +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + + ## + ## + +- hosts: proxmox + gather_facts: false + vars_files: + - "../../../secrets/default_vault.yml" + + tasks: + # + - name: PROMOX INIT - TEMPLATES - CREATE TEMPLATE - template-vm-large-08-16g-100g + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "vm_create" + # proxmox_node: "px-testing" + vm_id: 9248 + vm_name: "template-vm-large-06-16g-100g" + vm_cpu: "host" + vm_cores: 8 + vm_sockets: 1 + vm_memory: 16384 + vm_net_virtio_bridge: "vmbr140" + + # + - name: PROMOX INIT - TEMPLATES - CONVERT VM TO TEMPLATE - template-vm-large-08-16g-100g + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "template_convert_vm_to_template" + # proxmox_node: "px-testing" + proxmox_storage: "local" + vm_id: 9248 + +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- hosts: proxmox-cli + gather_facts: false + vars_files: + - "../../../secrets/default_vault.yml" + + tasks: + # + - name: PROMOX INIT - TEMPLATES - IMPORT CLOUD INIT DISK - template-vm-large-08-16g-100g + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "template_cloudinit_import_disk" + proxmox_node: "px-testing-cli" + cloudinit_image_full_path: "/var/lib/vz/template/iso/noble-minimal-cloudimg-amd64.img" + vm_id: 9248 + vm_disk_size: "100g" + proxmox_dest_vm_storage_name: "local-lvm" + +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- hosts: proxmox + gather_facts: false + vars_files: + - "../../../secrets/default_vault.yml" + + tasks: + # + - name: PROMOX INIT - TEMPLATES - SET CLOUD DEFAULT INIT VARIABLE - template-vm-large-08-16g-100g + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "cloudinit_set_variables" + # proxmox_node: "px-testing" + vm_id: 9248 + vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" + vm_ci_password: "{{ default_trainee_vm_ci_password | default('supersecret') }}" + vm_ci_ssh_key: "{{ default_trainee_vm_ci_ssh_key }}" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" + vm_ci_ip: "192.168.142.248" + vm_ci_netmask: "24" + vm_ci_ip_gw: "192.168.142.1" + + # + - name: PROMOX INIT - TEMPLATES - SET PROMOX TAG - template-vm-large-08-16g-100g + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "vm_set_tag" + # proxmox_node: "px-testing" + vm_id: 9248 + vm_tag_name: "template" diff --git a/scenarios/demo_lab_network/01_init_proxmox/templates/ubuntu_noble/_main_ubuntu_noble.yml b/scenarios/demo_lab_network/01_init_proxmox/templates/ubuntu_noble/_main_ubuntu_noble.yml new file mode 100644 index 00000000..486c5b44 --- /dev/null +++ b/scenarios/demo_lab_network/01_init_proxmox/templates/ubuntu_noble/_main_ubuntu_noble.yml @@ -0,0 +1,15 @@ +## +## + +- import_playbook: ./00-template-vm-nano.yml +- import_playbook: ./01-template-vm-micro-01-2g-24g.yml +- import_playbook: ./01-template-vm-micro-02-2g-24g.yml +- import_playbook: ./02-template-vm-small-01-4g-32g.yml +- import_playbook: ./02-template-vm-small-02-4g-32g.yml +- import_playbook: ./02-template-vm-small-04-4g-32g.yml +- import_playbook: ./03-template-vm-medium-02-8g-64g.yml +- import_playbook: ./03-template-vm-medium-04-8g-64g.yml +- import_playbook: ./03-template-vm-medium-06-8g-64g.yml +- import_playbook: ./04-template-vm-large-04-8g-64g.yml +- import_playbook: ./04-template-vm-large-06-8g-64g.yml +- import_playbook: ./04-template-vm-large-08-8g-64g.yml diff --git a/scenarios/demo_lab_network/01_init_proxmox/templates/ubuntu_noble/test_setup_templates.sh b/scenarios/demo_lab_network/01_init_proxmox/templates/ubuntu_noble/test_setup_templates.sh new file mode 100755 index 00000000..fd1b2542 --- /dev/null +++ b/scenarios/demo_lab_network/01_init_proxmox/templates/ubuntu_noble/test_setup_templates.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +## +## + +ansible-playbook -i "${RANGE42_ANSIBLE_ROLES__INVENTORY_DIR}/inventory_default.yml" \ + -l "all" \ + "test_setup_templates.yml" --ask-vault-pass diff --git a/scenarios/demo_lab_network/01_init_proxmox/templates/ubuntu_noble/test_setup_templates.yml b/scenarios/demo_lab_network/01_init_proxmox/templates/ubuntu_noble/test_setup_templates.yml new file mode 100644 index 00000000..36db525f --- /dev/null +++ b/scenarios/demo_lab_network/01_init_proxmox/templates/ubuntu_noble/test_setup_templates.yml @@ -0,0 +1,99 @@ +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# +# PROMOX INIT - create ubuntu noble template : +# +# vm_id : +# cpu core : +# ram : +# disk : +# ip : 192.168.142. +# +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +## +## + +- hosts: proxmox + gather_facts: false + vars_files: + - "../../../secrets/default_vault.yml" + + tasks: + # + - name: PROMOX INIT - TEMPLATES - CREATE TEMPLATE - XXXXXX + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "vm_create" + # proxmox_node: "px-testing" + vm_id: 8001 + vm_name: "brol1" + vm_cpu: "host" + vm_cores: 1 + vm_sockets: 1 + vm_memory: 2048 + vm_net_virtio_bridge: "vmbr140" + + # + - name: PROMOX INIT - TEMPLATES - CONVERT VM TO TEMPLATE + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "template_convert_vm_to_template" + # proxmox_node: "px-testing" + proxmox_storage: "local" + vm_id: 8001 + +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- hosts: proxmox-cli + gather_facts: false + vars_files: + - "../../../secrets/default_vault.yml" + + tasks: + # + - name: PROMOX INIT - TEMPLATES - IMPORT CLOUD INIT DISK + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "template_cloudinit_import_disk" + proxmox_node: "px-testing-cli" + cloudinit_image_full_path: "/var/lib/vz/template/iso/noble-minimal-cloudimg-amd64.img" + vm_id: 8001 + vm_disk_size: "103g" + proxmox_dest_vm_storage_name: "local-lvm" + +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- hosts: proxmox + gather_facts: false + vars_files: + - "../../../secrets/default_vault.yml" + + tasks: + # + - name: PROMOX INIT - TEMPLATES - SET CLOUD DEFAULT INIT VARIABLE + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "cloudinit_set_variables" + # proxmox_node: "px-testing" + vm_id: 8001 + vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" + vm_ci_password: "{{ default_trainee_vm_ci_password | default('supersecret') }}" + vm_ci_ssh_key: "{{ default_trainee_vm_ci_ssh_key }}" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" + vm_ci_ip: "192.168.142.55" + vm_ci_netmask: "24" + vm_ci_ip_gw: "192.168.142.1" + + # + - name: PROMOX INIT - TEMPLATES - SET PROMOX TAG + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "vm_set_tag" + # proxmox_node: "px-testing" + vm_id: 8001 + vm_tag_name: "template" diff --git a/scenarios/demo_lab_network/02_admin_infrastructure/_main.reinstall.sh b/scenarios/demo_lab_network/02_admin_infrastructure/_main.reinstall.sh new file mode 100755 index 00000000..6e9083ef --- /dev/null +++ b/scenarios/demo_lab_network/02_admin_infrastructure/_main.reinstall.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +## +## + +./stage_00/r42_admin_group.devkit/demo_lab_network.r42_admin_group.delete.sh + +ansible-playbook -i "${RANGE42_ANSIBLE_ROLES__INVENTORY_DIR}/inventory_default.yml" \ + -l "all" \ + "./_main.yml" --vault-password-file "${RANGE42_VAULT_PASSWORD_FILE:?RANGE42_VAULT_PASSWORD_FILE is not set — run: range42-context use }" diff --git a/scenarios/demo_lab_network/02_admin_infrastructure/_main.yml b/scenarios/demo_lab_network/02_admin_infrastructure/_main.yml new file mode 100644 index 00000000..1c25e8b9 --- /dev/null +++ b/scenarios/demo_lab_network/02_admin_infrastructure/_main.yml @@ -0,0 +1,148 @@ +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +# +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +#### #### #### #### #### #### #### #### STAGE 00 #### #### #### #### #### #### #### #### #### #### #### #### #### #### +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# + +# disabled — wazuh client test VM, re-enable when wazuh config is validated +# - import_playbook: ./stage_00/_testing/wazuh_client_01.yml +# +# vars: +# global_vm_name: "testing-wazuh-client" +# global_vm_ssh_name: "r42.testing-wazuh-client" +# global_vm_id: 1111 +# global_vm_description: "wazuh - client test" +# global_vm_tag_name: "admin" +# global_vm_ci_ip: "192.168.142.111" +# global_template_vm_id: 9211 +# global_template_name: "01-template-vm-micro-01-2g-24g - id 9211" + +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + +- import_playbook: ./stage_00/mon_wazuh.yml + + vars: + global_vm_name: "admin-wazuh" + global_vm_ssh_name: "r42.admin-wazuh" + # global_vm_ssh_name: "r42.{{ global_vm_name }}" + + global_vm_id: 1000 + global_vm_description: "" + global_vm_tag_name: "admin" + global_vm_ci_ip: "192.168.142.100" + # + global_template_vm_id: 9232 + global_template_name: "03-template-vm-medium-02-8g-64g - 9232" # debug + # global_operator_ssh_config_known_hosts: "global_operator_ssh_config_known_hosts" # move this one to vault ! + +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + +# disabled — docker registry, re-enable when needed +# - import_playbook: ./stage_00/builder_docker_registry.yml +# +# vars: +# global_vm_name: "admin-builder-docker-registry" +# global_vm_ssh_name: "r42.admin-builder-docker-registry" +# global_vm_id: 1001 +# global_vm_description: "docker registry" +# global_vm_tag_name: "admin" +# global_vm_ci_ip: "192.168.142.101" +# global_template_vm_id: 9232 +# global_template_name: "03-template-vm-medium-02-8g-64g - 9232" + +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + +- import_playbook: ./stage_00/deployer_api_gateway.yml + + vars: + global_vm_name: "admin-deployer-api-gateway" + global_vm_ssh_name: "r42.admin-deployer-api-gateway" + # global_vm_ssh_name: "r42.{{ global_vm_name }}" + global_vm_id: 1020 + global_vm_description: "API gateway" + global_vm_tag_name: "admin" + global_vm_ci_ip: "192.168.142.120" + # + global_template_vm_id: 9221 + global_template_name: "template-vm-small-01-4g-32g - id : 9221" # debug + # + +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + +- import_playbook: ./stage_00/deployer_api_backend.yml + + vars: + global_vm_name: "admin-deployer-api-backend" + global_vm_ssh_name: "r42.admin-deployer-api-backend" + # global_vm_ssh_name: "r42.{{ global_vm_name }}" + global_vm_id: 1021 + global_vm_description: "www - backend API " + global_vm_tag_name: "admin" + global_vm_ci_ip: "192.168.142.121" + # + global_template_vm_id: 9221 + global_template_name: "template-vm-small-01-4g-32g - id : 9221" # debug + # + +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- import_playbook: ./stage_00/deployer_ui.yml + + vars: + global_vm_name: "admin-deployer-ui" + global_vm_ssh_name: "r42.admin-deployer-ui" + global_vm_id: 1023 + global_vm_description: "www - front end - r42 - deployer -ui" + global_vm_tag_name: "admin" + global_vm_ci_ip: "192.168.142.123" + # + global_template_vm_id: 9221 + global_template_name: "template-vm-small-01-4g-32g - id : 9221" # debug + +# +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +#### #### #### #### #### #### #### #### STAGE 01 #### #### #### #### #### #### #### #### #### #### #### #### #### #### +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# # + +- import_playbook: ./stage_01/_r42_admin.yml + vars: + OPERATOR_USER: alice + + INSTALL_VIM_DOTFILES: "YES" + INSTALL_ZSH_DOTFILES: "YES" + + INSTALL_PACKAGES_BASICS: "YES" + INSTALL_PACKAGES_FIREWALLS: "YES" + # + INSTALL_PACKAGES_DOCKER: "NO" + INSTALL_PACKAGES_DOCKER_COMPOSE: "NO" + # + INSTALL_PACKAGES_UTILS_JSON: "NO" + INSTALL_PACKAGES_UTILS_NETWORK: "YES" + + INSTALL_PACKAGES_NTP_AND_UPDATE_TIME: "YES" + # # + + INSTALL_TAILSCALE: "NO" + + # SPECIFIC_PACKAGES_CLEANING: "NO" + + firewall_rules: + - ip: "all" + port: 22 + protocol: "tcp" + +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + +- import_playbook: ./stage_01/deployer_ui.yml + +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + +- import_playbook: ./stage_01/mon_wazuh.yml +# +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### diff --git a/scenarios/demo_lab_network/02_admin_infrastructure/stage_00/_r42_admin_group.yml b/scenarios/demo_lab_network/02_admin_infrastructure/stage_00/_r42_admin_group.yml new file mode 100644 index 00000000..ffa141e9 --- /dev/null +++ b/scenarios/demo_lab_network/02_admin_infrastructure/stage_00/_r42_admin_group.yml @@ -0,0 +1,131 @@ +# +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +#### #### #### #### #### #### #### #### STAGE 00 #### #### #### #### #### #### #### #### #### #### #### #### #### #### +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# + +## +## + +# - import_playbook: ./_testing/wazuh_client_01.yml + +# vars: +# global_vm_name: "testing-wazuh-client" +# global_vm_ssh_name: "r42.testing-wazuh-client" +# # global_vm_ssh_name: "r42.{{ global_vm_name }}" +# global_vm_id: 1111 +# global_vm_description: "wazuh - client test" +# global_vm_tag_name: "admin" +# global_vm_ci_ip: "192.168.142.111" +# # +# global_template_vm_id: 9211 +# global_template_name: "01-template-vm-micro-01-2g-24g - id 9211" # debug +# # + +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + +- import_playbook: ./mon_wazuh.yml + + vars: + global_vm_name: "admin-wazuh" + global_vm_ssh_name: "r42.admin-wazuh" + # global_vm_ssh_name: "r42.{{ global_vm_name }}" + + global_vm_id: 1000 + global_vm_description: "" + global_vm_tag_name: "admin" + global_vm_ci_ip: "192.168.142.100" + # + global_template_vm_id: 9232 + global_template_name: "03-template-vm-medium-02-8g-64g - 9232" # debug + # global_operator_ssh_config_known_hosts: "global_operator_ssh_config_known_hosts" # move this one to vault ! + +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + +# - import_playbook: ./builder_docker_registry.yml + +# vars: +# global_vm_name: "admin-builder-docker-registry" +# global_vm_ssh_name: "r42.admin-builder-docker-registry" +# # global_vm_ssh_name: "r42.{{ global_vm_name }}" + +# global_vm_id: 1001 +# global_vm_description: "docker registry" +# global_vm_tag_name: "admin" +# global_vm_ci_ip: "192.168.142.101" +# # +# global_template_vm_id: 9232 +# global_template_name: "03-template-vm-medium-02-8g-64g - 9232" # debug +# # + +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + +# - import_playbook: ./builder_api_devkit.yml + +# vars: +# global_vm_name: "admin-builder-api-devkit" +# global_vm_ssh_name: "r42.admin-builder-api-devkit" +# # global_vm_ssh_name: "r42.{{ global_vm_name }}" +# global_vm_id: 1002 +# global_vm_description: "devkit tooling" +# global_vm_tag_name: "admin" +# global_vm_ci_ip: "192.168.142.102" +# # +# global_template_vm_id: 9232 +# global_template_name: "01-template-vm-micro-01-2g-24g - id 9232" # debug +# # + +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + +- import_playbook: ./deployer_api_gateway.yml + + vars: + global_vm_name: "admin-deployer-api-gateway" + global_vm_ssh_name: "r42.admin-deployer-api-gateway" + # global_vm_ssh_name: "r42.{{ global_vm_name }}" + global_vm_id: 1020 + global_vm_description: "API gateway" + global_vm_tag_name: "admin" + global_vm_ci_ip: "192.168.142.120" + # + global_template_vm_id: 9221 + global_template_name: "template-vm-small-01-4g-32g - id : 9221" # debug + # + +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + +- import_playbook: ./deployer_api_backend.yml + + vars: + global_vm_name: "admin-deployer-api-backend" + global_vm_ssh_name: "r42.admin-deployer-api-backend" + # global_vm_ssh_name: "r42.{{ global_vm_name }}" + global_vm_id: 1021 + global_vm_description: "www - backend API " + global_vm_tag_name: "admin" + global_vm_ci_ip: "192.168.142.121" + # + global_template_vm_id: 9221 + global_template_name: "template-vm-small-01-4g-32g - id : 9221" # debug + # + +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- import_playbook: ./deployer_ui.yml + + vars: + global_vm_name: "admin-deployer-ui" + global_vm_ssh_name: "r42.admin-deployer-ui" + + global_vm_id: 1023 + global_vm_description: "www - front end - r42 - deployer -ui " + global_vm_tag_name: "admin" + global_vm_ci_ip: "192.168.142.123" + # + global_template_vm_id: 9221 + global_template_name: "template-vm-small-01-4g-32g - id : 9221" # debug + + + # diff --git a/scenarios/demo_lab_network/02_admin_infrastructure/stage_00/_testing/wazuh_client_01.yml b/scenarios/demo_lab_network/02_admin_infrastructure/stage_00/_testing/wazuh_client_01.yml new file mode 100644 index 00000000..4883c86a --- /dev/null +++ b/scenarios/demo_lab_network/02_admin_infrastructure/stage_00/_testing/wazuh_client_01.yml @@ -0,0 +1,116 @@ +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# +# PROMOX INIT INFRASTRUCTURE - ADMIN +# +# hostname : testing-wazuh-client +# vm_id : 1100 +# cpu core : 1 +# ram : 2g +# disk : 24g +# ip : 192.168.142.110 +# +# template : template-vm-micro-01-2g-24g - id 9211 +# +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +# TEST FILE - issue #10 + +- hosts: proxmox + gather_facts: false + vars_files: + - "../../../secrets/default_vault.yml" + + vars: + # global_vm_ssh_name: "cr42.testing-wazuh-client" + # # + # global_vm_id: 1110 + # global_vm_name: "testing-wazuh-client" + # global_vm_description: "" + # global_vm_tag_name: "admin" + # global_vm_ci_ip: "192.168.142.110" + # # + # global_template_vm_id: 9211 + # global_template_name: "01-template-vm-micro-01-2g-24g - id 9211" # debug + + tasks: + #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + + - name: ADMIN INFRASTRUCTURE INIT - "{{ global_vm_name }}" - CLONE TEMPLATE - "{{ global_template_name }}" + + include_role: + name: range42-ansible_roles-proxmox_controller + + vars: + proxmox_vm_action: "vm_clone" + vm_id: "{{ global_template_vm_id }}" # TEMPLATE VM_ID - vars should be rename to vm_id_src / vm_id_dst + vm_new_id: "{{ global_vm_id }}" + vm_name: "{{ global_vm_name }}" + vm_description: "{{ global_vm_description }}" + + #### + - name: INFRASTRUCTURE INIT - WAITING FOR PROXMOX UNLOCK TO CLEAR (slow setup) + ansible.builtin.pause: + seconds: 20 + prompt: "Waiting for 20 seconds during clone..." + #### + + - name: ADMIN INFRASTRUCTURE INIT - "{{ global_vm_name }}" - SET PROMOX TAG + + include_role: + name: range42-ansible_roles-proxmox_controller + + vars: + proxmox_vm_action: "vm_set_tag" + vm_id: "{{ global_vm_id }}" + vm_tag_name: "{{ global_vm_tag_name }}" + + #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + + - name: ADMIN INFRASTRUCTURE INIT - "{{ global_vm_name }}" - SET CLOUD DEFAULT INIT VARIABLE + + include_role: + name: range42-ansible_roles-proxmox_controller + + vars: + proxmox_vm_action: "cloudinit_set_variables" + + vm_id: "{{ global_vm_id }}" + vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" + vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" + vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" + vm_ci_ip: "{{ global_vm_ci_ip }}" + vm_ci_netmask: "24" + vm_ci_ip_gw: "192.168.142.1" + vm_net_virtio_bridge: "vmbr142" + + # + - name: ADMIN INFRASTRUCTURE INIT - "{{ global_vm_name }}" - CLONE TEMPLATE - START VM + + include_role: + name: range42-ansible_roles-proxmox_controller + + vars: + proxmox_vm_action: "vm_start" + vm_id: "{{ global_vm_id }}" + + # #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- hosts: "{{ global_vm_ssh_name }}" + gather_facts: false + vars_files: + - "../../../secrets/default_vault.yml" + + vars: + deployer_cli_user_ssh_known_hosts: "{{ deployer_cli_user_ssh_known_hosts }}" + ARG_vm_ssh_name: "{{ global_vm_ssh_name }}" + + requested_tasks: + - wait/openssh_server/is_reachable.yml + - wait/cloudinit/is_boot_finished.yml + tasks: + #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + + - name: TASK DYNAMIC INCLUDE FROM ansible.utils + include_role: + name: ansible.utils diff --git a/scenarios/demo_lab_network/02_admin_infrastructure/stage_00/builder_api_devkit.yml b/scenarios/demo_lab_network/02_admin_infrastructure/stage_00/builder_api_devkit.yml new file mode 100644 index 00000000..02f38524 --- /dev/null +++ b/scenarios/demo_lab_network/02_admin_infrastructure/stage_00/builder_api_devkit.yml @@ -0,0 +1,118 @@ +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# +# PROMOX INIT INFRASTRUCTURE - ADMIN +# +# hostname : admin-builder-api-devkit +# vm_id : 1002 +# cpu core : 1 +# ram : 2g +# disk : 24g +# ip : 192.168.142.102 +# +# template : 01-template-vm-micro-01-2g-24g - id 9232 +# +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +## +## + +- hosts: proxmox + gather_facts: false + vars_files: + - "../../secrets/default_vault.yml" + + vars: + # + # variables will be overwrited by values in _main.yml + # + # global_vm_id: 1002 + # global_vm_name: "admin-builder-api-devkit" + # global_vm_description: "" + # global_vm_tag_name: "admin" + # global_vm_ci_ip: "192.168.142.102" + # # + # global_template_vm_id: 9232 + # global_template_name: "01-template-vm-micro-01-2g-24g - id 9232" # debug + + tasks: + #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + + - name: ADMIN INFRASTRUCTURE INIT - "{{ global_vm_name }}" - CLONE TEMPLATE - "{{ global_template_name }}" + + include_role: + name: range42-ansible_roles-proxmox_controller + + vars: + proxmox_vm_action: "vm_clone" + vm_id: "{{ global_template_vm_id }}" # TEMPLATE VM_ID - vars should be rename to vm_id_src / vm_id_dst + vm_new_id: "{{ global_vm_id }}" + vm_name: "{{ global_vm_name }}" + vm_description: "{{ global_vm_description }}" + + #### + - name: INFRASTRUCTURE INIT - WAITING FOR PROXMOX UNLOCK TO CLEAR (slow setup) + ansible.builtin.pause: + seconds: 20 + prompt: "Waiting for 20 seconds during clone..." + #### + + - name: ADMIN INFRASTRUCTURE INIT - "{{ global_vm_name }}" - SET PROMOX TAG + + include_role: + name: range42-ansible_roles-proxmox_controller + + vars: + proxmox_vm_action: "vm_set_tag" + vm_id: "{{ global_vm_id }}" + vm_tag_name: "{{ global_vm_tag_name }}" + + #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + + - name: ADMIN INFRASTRUCTURE INIT - "{{ global_vm_name }}" - SET CLOUD DEFAULT INIT VARIABLE + + include_role: + name: range42-ansible_roles-proxmox_controller + + vars: + proxmox_vm_action: "cloudinit_set_variables" + + vm_id: "{{ global_vm_id }}" + vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" + vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" + vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" + vm_ci_ip: "{{ global_vm_ci_ip }}" + vm_ci_netmask: "24" + vm_ci_ip_gw: "192.168.142.1" + vm_net_virtio_bridge: "vmbr142" + + # + - name: ADMIN INFRASTRUCTURE INIT - "{{ global_vm_name }}" - CLONE TEMPLATE - START VM + + include_role: + name: range42-ansible_roles-proxmox_controller + + vars: + proxmox_vm_action: "vm_start" + vm_id: "{{ global_vm_id }}" + + # #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- hosts: "{{ global_vm_ssh_name }}" + gather_facts: false + vars_files: + - "../../secrets/default_vault.yml" + + vars: + deployer_cli_user_ssh_known_hosts: "{{ deployer_cli_user_ssh_known_hosts }}" + ARG_vm_ssh_name: "{{ global_vm_ssh_name }}" + + requested_tasks: + - wait/openssh_server/is_reachable.yml + - wait/cloudinit/is_boot_finished.yml + tasks: + #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + + - name: TASK DYNAMIC INCLUDE FROM ansible.utils + include_role: + name: ansible.utils diff --git a/scenarios/demo_lab_network/02_admin_infrastructure/stage_00/builder_docker_registry.yml b/scenarios/demo_lab_network/02_admin_infrastructure/stage_00/builder_docker_registry.yml new file mode 100644 index 00000000..dea2f232 --- /dev/null +++ b/scenarios/demo_lab_network/02_admin_infrastructure/stage_00/builder_docker_registry.yml @@ -0,0 +1,118 @@ +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# +# PROMOX INIT INFRASTRUCTURE - ADMIN +# +# hostname : admin-builder-docker-registry +# vm_id : 1001 +# cpu core : 1 +# ram : 2g +# disk : 24g +# ip : 192.168.142.101 +# +# template : 03-template-vm-medium-02-8g-64g - 9232 +# +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +## +## + +- hosts: proxmox + gather_facts: false + vars_files: + - "../../secrets/default_vault.yml" + + vars: + # + # variables will be overwrited by values in _main.yml + # + # global_vm_id: 1001 + # global_vm_name: "admin-builder-docker-registry" + # global_vm_description: "" + # global_vm_tag_name: "admin" + # global_vm_ci_ip: "192.168.142.101" + # # + # global_template_vm_id: 9232 + # global_template_name: "03-template-vm-medium-02-8g-64g - 9232" # debug + + tasks: + #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + + - name: ADMIN INFRASTRUCTURE INIT - "{{ global_vm_name }}" - CLONE TEMPLATE - "{{ global_template_name }}" + + include_role: + name: range42-ansible_roles-proxmox_controller + + vars: + proxmox_vm_action: "vm_clone" + vm_id: "{{ global_template_vm_id }}" # TEMPLATE VM_ID - vars should be rename to vm_id_src / vm_id_dst + vm_new_id: "{{ global_vm_id }}" + vm_name: "{{ global_vm_name }}" + vm_description: "{{ global_vm_description }}" + + #### + - name: INFRASTRUCTURE INIT - WAITING FOR PROXMOX UNLOCK TO CLEAR (slow setup) + ansible.builtin.pause: + seconds: 20 + prompt: "Waiting for 20 seconds during clone..." + #### + + - name: ADMIN INFRASTRUCTURE INIT - "{{ global_vm_name }}" - SET PROMOX TAG + + include_role: + name: range42-ansible_roles-proxmox_controller + + vars: + proxmox_vm_action: "vm_set_tag" + vm_id: "{{ global_vm_id }}" + vm_tag_name: "{{ global_vm_tag_name }}" + + #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + + - name: ADMIN INFRASTRUCTURE INIT - "{{ global_vm_name }}" - SET CLOUD DEFAULT INIT VARIABLE + + include_role: + name: range42-ansible_roles-proxmox_controller + + vars: + proxmox_vm_action: "cloudinit_set_variables" + + vm_id: "{{ global_vm_id }}" + vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" + vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" + vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" + vm_ci_ip: "{{ global_vm_ci_ip }}" + vm_ci_netmask: "24" + vm_ci_ip_gw: "192.168.142.1" + vm_net_virtio_bridge: "vmbr142" + + # + - name: ADMIN INFRASTRUCTURE INIT - "{{ global_vm_name }}" - CLONE TEMPLATE - START VM + + include_role: + name: range42-ansible_roles-proxmox_controller + + vars: + proxmox_vm_action: "vm_start" + vm_id: "{{ global_vm_id }}" + + # #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- hosts: "{{ global_vm_ssh_name }}" + gather_facts: false + vars_files: + - "../../secrets/default_vault.yml" + + vars: + deployer_cli_user_ssh_known_hosts: "{{ deployer_cli_user_ssh_known_hosts }}" + ARG_vm_ssh_name: "{{ global_vm_ssh_name }}" + + requested_tasks: + - wait/openssh_server/is_reachable.yml + - wait/cloudinit/is_boot_finished.yml + tasks: + #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + + - name: TASK DYNAMIC INCLUDE FROM ansible.utils + include_role: + name: ansible.utils diff --git a/scenarios/demo_lab_network/02_admin_infrastructure/stage_00/deployer_api_backend.yml b/scenarios/demo_lab_network/02_admin_infrastructure/stage_00/deployer_api_backend.yml new file mode 100644 index 00000000..b36fb2a0 --- /dev/null +++ b/scenarios/demo_lab_network/02_admin_infrastructure/stage_00/deployer_api_backend.yml @@ -0,0 +1,118 @@ +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# +# PROMOX INIT INFRASTRUCTURE - ADMIN +# +# hostname : admin-deployer-api-backend +# vm_id : 1021 +# cpu core : 1 +# ram : 4g +# disk : 32g +# ip : 192.168.142.121 +# +# template : template-vm-small-01-4g-32g - id : 9221 +# +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +## +## + +- hosts: proxmox + gather_facts: false + vars_files: + - "../../secrets/default_vault.yml" + + vars: + # + # variables will be overwrited by values in _main.yml + # + # global_vm_id: 1021 + # global_vm_name: "admin-deployer-api-backend" + # global_vm_description: "" + # global_vm_tag_name: "admin" + # global_vm_ci_ip: "192.168.142.121" + # # + # global_template_vm_id: 9221 + # global_template_name: "template-vm-small-01-4g-32g - id : 9221" # debug + + tasks: + #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + + - name: ADMIN INFRASTRUCTURE INIT - "{{ global_vm_name }}" - CLONE TEMPLATE - "{{ global_template_name }}" + + include_role: + name: range42-ansible_roles-proxmox_controller + + vars: + proxmox_vm_action: "vm_clone" + vm_id: "{{ global_template_vm_id }}" # TEMPLATE VM_ID - vars should be rename to vm_id_src / vm_id_dst + vm_new_id: "{{ global_vm_id }}" + vm_name: "{{ global_vm_name }}" + vm_description: "{{ global_vm_description }}" + + #### + - name: INFRASTRUCTURE INIT - WAITING FOR PROXMOX UNLOCK TO CLEAR (slow setup) + ansible.builtin.pause: + seconds: 20 + prompt: "Waiting for 20 seconds during clone..." + #### + + - name: ADMIN INFRASTRUCTURE INIT - "{{ global_vm_name }}" - SET PROMOX TAG + + include_role: + name: range42-ansible_roles-proxmox_controller + + vars: + proxmox_vm_action: "vm_set_tag" + vm_id: "{{ global_vm_id }}" + vm_tag_name: "{{ global_vm_tag_name }}" + + #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + + - name: ADMIN INFRASTRUCTURE INIT - "{{ global_vm_name }}" - SET CLOUD DEFAULT INIT VARIABLE + + include_role: + name: range42-ansible_roles-proxmox_controller + + vars: + proxmox_vm_action: "cloudinit_set_variables" + + vm_id: "{{ global_vm_id }}" + vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" + vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" + vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" + vm_ci_ip: "{{ global_vm_ci_ip }}" + vm_ci_netmask: "24" + vm_ci_ip_gw: "192.168.142.1" + vm_net_virtio_bridge: "vmbr142" + + # + - name: ADMIN INFRASTRUCTURE INIT - "{{ global_vm_name }}" - CLONE TEMPLATE - START VM + + include_role: + name: range42-ansible_roles-proxmox_controller + + vars: + proxmox_vm_action: "vm_start" + vm_id: "{{ global_vm_id }}" + + # #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- hosts: "{{ global_vm_ssh_name }}" + gather_facts: false + vars_files: + - "../../secrets/default_vault.yml" + + vars: + deployer_cli_user_ssh_known_hosts: "{{ deployer_cli_user_ssh_known_hosts }}" + ARG_vm_ssh_name: "{{ global_vm_ssh_name }}" + + requested_tasks: + - wait/openssh_server/is_reachable.yml + - wait/cloudinit/is_boot_finished.yml + tasks: + #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + + - name: TASK DYNAMIC INCLUDE FROM ansible.utils + include_role: + name: ansible.utils diff --git a/scenarios/demo_lab_network/02_admin_infrastructure/stage_00/deployer_api_gateway.yml b/scenarios/demo_lab_network/02_admin_infrastructure/stage_00/deployer_api_gateway.yml new file mode 100644 index 00000000..3e4de986 --- /dev/null +++ b/scenarios/demo_lab_network/02_admin_infrastructure/stage_00/deployer_api_gateway.yml @@ -0,0 +1,123 @@ +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# +# PROMOX INIT INFRASTRUCTURE - ADMIN +# +# hostname : admin-deployer-api-gateway +# vm_id : 1020 +# cpu core : 1 +# ram : 4g +# disk : 32g +# ip : 192.168.142.120 +# +# template : template-vm-small-01-4g-32g - id : 9221 +# +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +## +## + +- hosts: proxmox + gather_facts: false + vars_files: + - "../../secrets/default_vault.yml" + + vars: + # + # variables will be overwrited by values in _main.yml + # + # global_vm_id: 1020 + # global_vm_name: "admin-deployer-api-gateway" + # global_vm_description: "" + # global_vm_tag_name: "admin" + # global_vm_ci_ip: "192.168.142.120" + # # + # global_template_vm_id: 9221 + # global_template_name: "template-vm-small-01-4g-32g - id : 9221" # debug + + tasks: + #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + + - name: ADMIN INFRASTRUCTURE INIT - "{{ global_vm_name }}" - CLONE TEMPLATE - "{{ global_template_name }}" + + include_role: + name: range42-ansible_roles-proxmox_controller + + vars: + proxmox_vm_action: "vm_clone" + vm_id: "{{ global_template_vm_id }}" # TEMPLATE VM_ID - vars should be rename to vm_id_src / vm_id_dst + vm_new_id: "{{ global_vm_id }}" + vm_name: "{{ global_vm_name }}" + vm_description: "{{ global_vm_description }}" + + #### + - name: INFRASTRUCTURE INIT - WAITING FOR PROXMOX UNLOCK TO CLEAR (slow setup) + ansible.builtin.pause: + seconds: 20 + prompt: "Waiting for 20 seconds during clone..." + #### + + - name: ADMIN INFRASTRUCTURE INIT - "{{ global_vm_name }}" - SET PROMOX TAG + + include_role: + name: range42-ansible_roles-proxmox_controller + + vars: + proxmox_vm_action: "vm_set_tag" + vm_id: "{{ global_vm_id }}" + vm_tag_name: "{{ global_vm_tag_name }}" + + #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + + - name: ADMIN INFRASTRUCTURE INIT - "{{ global_vm_name }}" - SET CLOUD DEFAULT INIT VARIABLE + + include_role: + name: range42-ansible_roles-proxmox_controller + + vars: + proxmox_vm_action: "cloudinit_set_variables" + + vm_id: "{{ global_vm_id }}" + vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" + vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" + vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" + vm_ci_ip: "{{ global_vm_ci_ip }}" + vm_ci_netmask: "24" + vm_ci_ip_gw: "192.168.142.1" + vm_net_virtio_bridge: "vmbr142" + + # + - name: ADMIN INFRASTRUCTURE INIT - "{{ global_vm_name }}" - CLONE TEMPLATE - START VM + + include_role: + name: range42-ansible_roles-proxmox_controller + + vars: + proxmox_vm_action: "vm_start" + vm_id: "{{ global_vm_id }}" + + # #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- hosts: "{{ global_vm_ssh_name }}" + gather_facts: false + vars_files: + - "../../secrets/default_vault.yml" + + vars: + # ARG_global_operator_ssh_config_known_hosts: "{{ global_operator_ssh_config_known_hosts }}" + # ARG_global_vm_ssh_name: "{{ global_vm_ssh_name }}" + # ARG_global_operator_ssh_config_known_hosts: "{{ global_operator_ssh_config_known_hosts }}" + # ARG_global_vm_ssh_name: "{{ global_vm_ssh_name }}" + + deployer_cli_user_ssh_known_hosts: "{{ deployer_cli_user_ssh_known_hosts }}" + ARG_vm_ssh_name: "{{ global_vm_ssh_name }}" + + requested_tasks: + - wait/openssh_server/is_reachable.yml + - wait/cloudinit/is_boot_finished.yml + tasks: + #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + + - name: TASK DYNAMIC INCLUDE FROM ansible.utils + include_role: + name: ansible.utils diff --git a/scenarios/demo_lab_network/02_admin_infrastructure/stage_00/deployer_ui.yml b/scenarios/demo_lab_network/02_admin_infrastructure/stage_00/deployer_ui.yml new file mode 100644 index 00000000..6b0aec9c --- /dev/null +++ b/scenarios/demo_lab_network/02_admin_infrastructure/stage_00/deployer_ui.yml @@ -0,0 +1,118 @@ +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# +# PROMOX INIT INFRASTRUCTURE - ADMIN +# +# hostname : admin-deployer-ui +# vm_id : 1023 +# cpu core : 1 +# ram : 4g +# disk : 32g +# ip : 192.168.142.122 +# +# template : template-vm-small-01-4g-32g - id : 9221 +# +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +## +## + +- hosts: proxmox + gather_facts: false + vars_files: + - "../../secrets/default_vault.yml" + + vars: + # + # variables will be overwrited by values in _main.yml + # + # global_vm_id: 1023 + # global_vm_name: "admin-deployer-ui" + # global_vm_description: "" + # global_vm_tag_name: "admin" + # global_vm_ci_ip: "192.168.142.123" + # # + # global_template_vm_id: 9221 + # global_template_name: "template-vm-small-01-4g-32g - id : 9221" # debug + + tasks: + #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + + - name: ADMIN INFRASTRUCTURE INIT - "{{ global_vm_name }}" - CLONE TEMPLATE - "{{ global_template_name }}" + + include_role: + name: range42-ansible_roles-proxmox_controller + + vars: + proxmox_vm_action: "vm_clone" + vm_id: "{{ global_template_vm_id }}" # TEMPLATE VM_ID - vars should be rename to vm_id_src / vm_id_dst + vm_new_id: "{{ global_vm_id }}" + vm_name: "{{ global_vm_name }}" + vm_description: "{{ global_vm_description }}" + + #### + - name: INFRASTRUCTURE INIT - WAITING FOR PROXMOX UNLOCK TO CLEAR (slow setup) + ansible.builtin.pause: + seconds: 20 + prompt: "Waiting for 20 seconds during clone..." + #### + + - name: ADMIN INFRASTRUCTURE INIT - "{{ global_vm_name }}" - SET PROMOX TAG + + include_role: + name: range42-ansible_roles-proxmox_controller + + vars: + proxmox_vm_action: "vm_set_tag" + vm_id: "{{ global_vm_id }}" + vm_tag_name: "{{ global_vm_tag_name }}" + + #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + + - name: ADMIN INFRASTRUCTURE INIT - "{{ global_vm_name }}" - SET CLOUD DEFAULT INIT VARIABLE + + include_role: + name: range42-ansible_roles-proxmox_controller + + vars: + proxmox_vm_action: "cloudinit_set_variables" + + vm_id: "{{ global_vm_id }}" + vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" + vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" + vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" + vm_ci_ip: "{{ global_vm_ci_ip }}" + vm_ci_netmask: "24" + vm_ci_ip_gw: "192.168.142.1" + vm_net_virtio_bridge: "vmbr142" + + # + - name: ADMIN INFRASTRUCTURE INIT - "{{ global_vm_name }}" - CLONE TEMPLATE - START VM + + include_role: + name: range42-ansible_roles-proxmox_controller + + vars: + proxmox_vm_action: "vm_start" + vm_id: "{{ global_vm_id }}" + + # #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- hosts: "{{ global_vm_ssh_name }}" + gather_facts: false + vars_files: + - "../../secrets/default_vault.yml" + + vars: + deployer_cli_user_ssh_known_hosts: "{{ deployer_cli_user_ssh_known_hosts }}" + ARG_vm_ssh_name: "{{ global_vm_ssh_name }}" + + requested_tasks: + - wait/openssh_server/is_reachable.yml + - wait/cloudinit/is_boot_finished.yml + tasks: + #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + + - name: TASK DYNAMIC INCLUDE FROM ansible.utils + include_role: + name: ansible.utils diff --git a/scenarios/demo_lab_network/02_admin_infrastructure/stage_00/mon_wazuh.yml b/scenarios/demo_lab_network/02_admin_infrastructure/stage_00/mon_wazuh.yml new file mode 100644 index 00000000..6ae63931 --- /dev/null +++ b/scenarios/demo_lab_network/02_admin_infrastructure/stage_00/mon_wazuh.yml @@ -0,0 +1,123 @@ +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# +# PROMOX INIT INFRASTRUCTURE - ADMIN +# +# hostname : admin-wazuh +# vm_id : 1000 +# cpu core : 1 +# ram : 2g +# disk : 24g +# ip : 192.168.142.100 +# +# template : 03-template-vm-medium-02-8g-64g - 9232 +# +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +## +## + +- hosts: proxmox + gather_facts: false + vars_files: + - "../../secrets/default_vault.yml" + + vars: + # + # variables will be overwrited by values in _main.yml + # + # global_vm_id: 1000 + # global_vm_name: "admin-wazuh" + # global_vm_description: "" + # global_vm_tag_name: "admin" + # global_vm_ci_ip: "192.168.142.100" + # # + # global_template_vm_id: 9232 + # global_template_name: "03-template-vm-medium-02-8g-64g - 9232" # debug + + tasks: + #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + + - name: ADMIN INFRASTRUCTURE INIT - "{{ global_vm_name }}" - CLONE TEMPLATE - "{{ global_template_name }}" + + include_role: + name: range42-ansible_roles-proxmox_controller + + vars: + proxmox_vm_action: "vm_clone" + vm_id: "{{ global_template_vm_id }}" # TEMPLATE VM_ID - vars should be rename to vm_id_src / vm_id_dst + vm_new_id: "{{ global_vm_id }}" + vm_name: "{{ global_vm_name }}" + vm_description: "{{ global_vm_description }}" + + #### + - name: INFRASTRUCTURE INIT - WAITING FOR PROXMOX UNLOCK TO CLEAR (slow setup) + ansible.builtin.pause: + seconds: 20 + prompt: "Waiting for 20 seconds during clone..." + #### + + - name: ADMIN INFRASTRUCTURE INIT - "{{ global_vm_name }}" - SET PROMOX TAG + + include_role: + name: range42-ansible_roles-proxmox_controller + + vars: + proxmox_vm_action: "vm_set_tag" + vm_id: "{{ global_vm_id }}" + vm_tag_name: "{{ global_vm_tag_name }}" + + #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + + - name: ADMIN INFRASTRUCTURE INIT - "{{ global_vm_name }}" - SET CLOUD DEFAULT INIT VARIABLE + + include_role: + name: range42-ansible_roles-proxmox_controller + + vars: + proxmox_vm_action: "cloudinit_set_variables" + + vm_id: "{{ global_vm_id }}" + vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" + vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" + vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" + vm_ci_ip: "{{ global_vm_ci_ip }}" + vm_ci_netmask: "24" + vm_ci_ip_gw: "192.168.142.1" + vm_net_virtio_bridge: "vmbr142" + + # + - name: ADMIN INFRASTRUCTURE INIT - "{{ global_vm_name }}" - CLONE TEMPLATE - START VM + + include_role: + name: range42-ansible_roles-proxmox_controller + + vars: + proxmox_vm_action: "vm_start" + vm_id: "{{ global_vm_id }}" + + # #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- hosts: "{{ global_vm_ssh_name }}" + gather_facts: false + vars_files: + - "../../secrets/default_vault.yml" + + vars: + # ARG_global_operator_ssh_config_known_hosts: "{{ global_operator_ssh_config_known_hosts }}" + # ARG_global_vm_ssh_name: "{{ global_vm_ssh_name }}" + # ARG_global_operator_ssh_config_known_hosts: "{{ global_operator_ssh_config_known_hosts }}" + # ARG_global_vm_ssh_name: "{{ global_vm_ssh_name }}" + + deployer_cli_user_ssh_known_hosts: "{{ deployer_cli_user_ssh_known_hosts }}" + ARG_vm_ssh_name: "{{ global_vm_ssh_name }}" + + requested_tasks: + - wait/openssh_server/is_reachable.yml + - wait/cloudinit/is_boot_finished.yml + tasks: + #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + + - name: TASK DYNAMIC INCLUDE FROM ansible.utils + include_role: + name: ansible.utils diff --git a/scenarios/demo_lab_network/02_admin_infrastructure/stage_00/r42_admin_group.devkit/demo_lab_network.r42_admin_group.delete.sh b/scenarios/demo_lab_network/02_admin_infrastructure/stage_00/r42_admin_group.devkit/demo_lab_network.r42_admin_group.delete.sh new file mode 100755 index 00000000..13ad6c00 --- /dev/null +++ b/scenarios/demo_lab_network/02_admin_infrastructure/stage_00/r42_admin_group.devkit/demo_lab_network.r42_admin_group.delete.sh @@ -0,0 +1,38 @@ +#!/bin/bash + +## +## + +VULN_BOX=( + + "192.168.142.111" # testing-wazuh-client + # + "192.168.142.102" # admin-builder-api-devkit + "192.168.142.101" # admin-builder-docker-registry + "192.168.142.100" # admin-wazuh + "192.168.142.120" # admin-deployer-api-gateway + "192.168.142.121" # admin-deployer-api-backend + "192.168.142.123" # admin-deployer-ui + # +) + +for ip in "${VULN_BOX[@]}"; do + echo ":: REMOVE SSH KEY FOR : $ip" + ssh-keygen -f "$HOME/.ssh/known_hosts" -R "$ip" +done + +for line in $(proxmox_vm.list.to.jsons.sh | grep -i -E "(admin|testing-wazuh)" | + jq -c "."); do + + printf "%s\n" "$line" | proxmox_vm.vm_id.stop.to.jsons.sh + + sleep 2 +done + +for line in $(proxmox_vm.list.to.jsons.sh | grep -i -E "(admin|testing-wazuh)" | + jq -c "."); do + + printf "%s\n" "$line" | proxmox_vm.vm_id.delete.to.jsons.sh + + sleep 1 +done diff --git a/scenarios/demo_lab_network/02_admin_infrastructure/stage_00/r42_admin_group.devkit/demo_lab_network.r42_admin_group.install.sh b/scenarios/demo_lab_network/02_admin_infrastructure/stage_00/r42_admin_group.devkit/demo_lab_network.r42_admin_group.install.sh new file mode 100755 index 00000000..4d705468 --- /dev/null +++ b/scenarios/demo_lab_network/02_admin_infrastructure/stage_00/r42_admin_group.devkit/demo_lab_network.r42_admin_group.install.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +## +## + +ansible-playbook -i "${RANGE42_ANSIBLE_ROLES__INVENTORY_DIR}/inventory_default.yml" \ + -l "all" \ + "../_r42_admin_group.yml" --vault-password-file "${RANGE42_VAULT_PASSWORD_FILE:?RANGE42_VAULT_PASSWORD_FILE is not set — run: range42-context use }" diff --git a/scenarios/demo_lab_network/02_admin_infrastructure/stage_01/_r42_admin.devkit/demo_lab_network.r42_admin.install.sh b/scenarios/demo_lab_network/02_admin_infrastructure/stage_01/_r42_admin.devkit/demo_lab_network.r42_admin.install.sh new file mode 100755 index 00000000..93a9ff00 --- /dev/null +++ b/scenarios/demo_lab_network/02_admin_infrastructure/stage_01/_r42_admin.devkit/demo_lab_network.r42_admin.install.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +## +## + +ansible-playbook -i "${RANGE42_ANSIBLE_ROLES__INVENTORY_DIR}/inventory_default.yml" \ + -l "all" \ + "../_r42_admin.yml" --vault-password-file "${RANGE42_VAULT_PASSWORD_FILE:?RANGE42_VAULT_PASSWORD_FILE is not set — run: range42-context use }" diff --git a/scenarios/demo_lab_network/02_admin_infrastructure/stage_01/_r42_admin.devkit/demo_lab_network.r42_admin.revert.sh b/scenarios/demo_lab_network/02_admin_infrastructure/stage_01/_r42_admin.devkit/demo_lab_network.r42_admin.revert.sh new file mode 100755 index 00000000..ab1bae52 --- /dev/null +++ b/scenarios/demo_lab_network/02_admin_infrastructure/stage_01/_r42_admin.devkit/demo_lab_network.r42_admin.revert.sh @@ -0,0 +1,38 @@ +#!/bin/bash + +## +## + +# proxmox_vm.list.to.jsons.sh | +# grep -vi template | +# grep -vi group | +# grep -iE "(admin-)|(testing-)" | +# jq -c | +# proxmox_snapshot_vm.vm_id.revert_snapshot.to.jsons.sh + +# proxmox_vm.list.to.jsons.sh | +# grep -vi template | +# grep -vi group | +# grep -iE "(admin-)|(testing-)" | +# jq -c | +# proxmox_vm.vm_id.start.to.jsons.sh + +for line in $(proxmox_vm.list.to.jsons.sh | grep -vi template | + grep -vi group | + grep -iE "(admin-)|(testing-)" | + jq -c "."); do + + printf "%s\n" "$line" | proxmox_snapshot_vm.vm_id.revert_snapshot.to.jsons.sh + + sleep 2 +done + +for line in $(proxmox_vm.list.to.jsons.sh | grep -vi template | + grep -vi group | + grep -iE "(admin-)|(testing-)" | + jq -c "."); do + + printf "%s\n" "$line" | proxmox_vm.vm_id.start.to.jsons.sh + + sleep 1 +done diff --git a/scenarios/demo_lab_network/02_admin_infrastructure/stage_01/_r42_admin.devkit/demo_lab_network.r42_admin.snapshot.sh b/scenarios/demo_lab_network/02_admin_infrastructure/stage_01/_r42_admin.devkit/demo_lab_network.r42_admin.snapshot.sh new file mode 100755 index 00000000..9bb5c485 --- /dev/null +++ b/scenarios/demo_lab_network/02_admin_infrastructure/stage_01/_r42_admin.devkit/demo_lab_network.r42_admin.snapshot.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +## +## + +# +# proxmox_vm.list.to.jsons.sh | +# grep -vi template | +# grep -vi group | +# grep -iE "(admin-)|(testing-)" | +# jq -c "." | +# proxmox_snapshot_vm.vm_id.create_snapshot.to.jsons.sh + +for line in $(proxmox_vm.list.to.jsons.sh | grep -vi template | + grep -vi group | + grep -iE "(admin-)|(testing-)" | + jq -c "."); do + + printf "%s\n" "$line" | proxmox_snapshot_vm.vm_id.create_snapshot.to.jsons.sh + + sleep 2 +done diff --git a/scenarios/demo_lab_network/02_admin_infrastructure/stage_01/_r42_admin.yml b/scenarios/demo_lab_network/02_admin_infrastructure/stage_01/_r42_admin.yml new file mode 100644 index 00000000..2de3bde7 --- /dev/null +++ b/scenarios/demo_lab_network/02_admin_infrastructure/stage_01/_r42_admin.yml @@ -0,0 +1,120 @@ +# +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +#### #### #### #### #### #### #### #### STAGE 01 #### #### #### #### #### #### #### #### #### #### #### #### #### #### +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# + +## +## + +- name: install basics packages + hosts: r42_admin + become: true + roles: + - software.install.warmup.basic_packages + + # + # variables will be overwrited by values in _main.yml + # + + vars: + INSTALL_PACKAGES_BASICS: "YES" + INSTALL_PACKAGES_FIREWALLS: "YES" + # + INSTALL_PACKAGES_DOCKER: "NO" + INSTALL_PACKAGES_DOCKER_COMPOSE: "NO" + # + INSTALL_PACKAGES_UTILS_JSON: "NO" + INSTALL_PACKAGES_UTILS_NETWORK: "YES" + + INSTALL_PACKAGES_NTP_AND_UPDATE_TIME: "YES" + # + # SPECIFIC_PACKAGES_CLEANING: "NO" +# # +# #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# # + +- name: install dot files + hosts: r42_admin + become: true + roles: + - software.install.warmup.dot_files + + vars: + # + # variables will be overwrited by values in _main.yml + # + OPERATOR_USER: alice + INSTALL_VIM_DOTFILES: "YES" + INSTALL_ZSH_DOTFILES: "YES" + +- name: configure firewall - all + become: true + hosts: r42_admin + roles: + - software.configure.firewalls + + # + # variables will be overwrited by values in _main.yml + + vars: + firewall_rules: + - ip: "all" + port: 22 + protocol: "tcp" +# +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# + +## +## + +- hosts: r42.admin-wazuh + gather_facts: false + vars_files: + - "../../secrets/default_vault.yml" + + vars: + tailscale_hostnames: + - "admin-wazuh" + # - "admin-builder-docker-registry" # disabled — re-enable when needed + - "admin-deployer-api-gateway" + - "admin-deployer-api-backend" + - "admin-deployer-ui" + # - "testing-wazuh-client" # disabled — re-enable when wazuh config is validated + + requested_tasks: + - delete/tailscale_client.yml + # + tasks: + # + # + - name: TASK DYNAMIC INCLUDE FROM ansible.utils + include_role: + name: ansible.utils + loop: "{{ tailscale_hostnames }}" + loop_control: + loop_var: tailscale_hostname + when: INSTALL_TAILSCALE == "YES" + +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- hosts: r42_admin + become: true + vars_files: + - "../../secrets/default_vault.yml" + vars: + # + # variables defined in vault + # + tailscale_authkey: "{{ infrastructure_tailscale_authkey }}" + INSTALL_TAILSCALE: "YES" # variables defined in main + + tasks: + - block: + - include_role: + name: software.install.tailscale + - include_role: + name: software.configure.tailscale_disable_nftables + + when: INSTALL_TAILSCALE == "YES" diff --git a/scenarios/demo_lab_network/02_admin_infrastructure/stage_01/deployer_api_gateway.devkit/demo_lab_network.deployer_api_gateway.install.sh b/scenarios/demo_lab_network/02_admin_infrastructure/stage_01/deployer_api_gateway.devkit/demo_lab_network.deployer_api_gateway.install.sh new file mode 100755 index 00000000..5e9511c2 --- /dev/null +++ b/scenarios/demo_lab_network/02_admin_infrastructure/stage_01/deployer_api_gateway.devkit/demo_lab_network.deployer_api_gateway.install.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +## +## + +ansible-playbook -i "${RANGE42_ANSIBLE_ROLES__INVENTORY_DIR}/inventory_default.yml" \ + -l "all" \ + "../deployer_api_gateway.yml" --vault-password-file "${RANGE42_VAULT_PASSWORD_FILE:?RANGE42_VAULT_PASSWORD_FILE is not set — run: range42-context use }" diff --git a/scenarios/demo_lab_network/02_admin_infrastructure/stage_01/deployer_api_gateway.devkit/demo_lab_network.deployer_api_gateway.revert.sh b/scenarios/demo_lab_network/02_admin_infrastructure/stage_01/deployer_api_gateway.devkit/demo_lab_network.deployer_api_gateway.revert.sh new file mode 100755 index 00000000..6aa5d622 --- /dev/null +++ b/scenarios/demo_lab_network/02_admin_infrastructure/stage_01/deployer_api_gateway.devkit/demo_lab_network.deployer_api_gateway.revert.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +## +## + +echo '{"proxmox_node":"px-testing","vm_id":1020 }' | proxmox_snapshot_vm.vm_id.revert_snapshot.to.jsons.sh +echo '{"proxmox_node":"px-testing","vm_id":1020 }' | proxmox_vm.vm_id.start.to.jsons.sh diff --git a/scenarios/demo_lab_network/02_admin_infrastructure/stage_01/deployer_api_gateway.devkit/demo_lab_network.deployer_api_gateway.snapshot.sh b/scenarios/demo_lab_network/02_admin_infrastructure/stage_01/deployer_api_gateway.devkit/demo_lab_network.deployer_api_gateway.snapshot.sh new file mode 100755 index 00000000..770251d6 --- /dev/null +++ b/scenarios/demo_lab_network/02_admin_infrastructure/stage_01/deployer_api_gateway.devkit/demo_lab_network.deployer_api_gateway.snapshot.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +## +## + +echo '{"proxmox_node":"px-testing","vm_id":1020,"vm_snapshot_description":"base"}' | proxmox_snapshot_vm.vm_id.create_snapshot.to.jsons.sh diff --git a/scenarios/demo_lab_network/02_admin_infrastructure/stage_01/deployer_api_gateway.yml b/scenarios/demo_lab_network/02_admin_infrastructure/stage_01/deployer_api_gateway.yml new file mode 100644 index 00000000..615c128f --- /dev/null +++ b/scenarios/demo_lab_network/02_admin_infrastructure/stage_01/deployer_api_gateway.yml @@ -0,0 +1,71 @@ +## +## +## + +- name: install basics packages + hosts: r42_admin + become: true + roles: + - software.install.warmup.basic_packages + + vars: + INSTALL_PACKAGES_BASICS: "YES" + INSTALL_PACKAGES_FIREWALLS: "YES" + # + INSTALL_PACKAGES_DOCKER: "YES" + INSTALL_PACKAGES_DOCKER_COMPOSE: "YES" + # + INSTALL_PACKAGES_UTILS_JSON: "NO" + INSTALL_PACKAGES_UTILS_NETWORK: "YES" + + INSTALL_PACKAGES_NTP_AND_UPDATE_TIME: "YES" + # # + + # SPECIFIC_PACKAGES_CLEANING: "NO" +# # +# #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# # + +- name: install dot files + hosts: r42_admin + become: true + roles: + - software.install.warmup.dot_files + + vars: + OPERATOR_USER: alice + + INSTALL_VIM_DOTFILES: "YES" + INSTALL_ZSH_DOTFILES: "YES" + +- name: configure firewall - all + become: true + hosts: r42_admin + roles: + - software.configure.firewalls + vars: + firewall_rules: + - ip: "all" + port: 22 + protocol: "tcp" +# +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# + +# - hosts: r42_admin +# become: true + +# vars_files: +# - "../../secrets/default_vault.yml" + +# vars: +# tailscale_authkey: "{{ infrastructure_tailscale_authkey }}" + +# roles: +# - software.install.tailscale + +# - hosts: r42_admin +# become: true +# roles: +# - software.configure.tailscale_disable_nftables +# vars: diff --git a/scenarios/demo_lab_network/02_admin_infrastructure/stage_01/deployer_ui.devkit/demo_lab_network.deployer_ui.install.sh b/scenarios/demo_lab_network/02_admin_infrastructure/stage_01/deployer_ui.devkit/demo_lab_network.deployer_ui.install.sh new file mode 100755 index 00000000..889d32dd --- /dev/null +++ b/scenarios/demo_lab_network/02_admin_infrastructure/stage_01/deployer_ui.devkit/demo_lab_network.deployer_ui.install.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +## +## + +ansible-playbook -i "${RANGE42_ANSIBLE_ROLES__INVENTORY_DIR}/inventory_default.yml" \ + -l "all" \ + "../deployer_ui.yml" --vault-password-file "${RANGE42_VAULT_PASSWORD_FILE:?RANGE42_VAULT_PASSWORD_FILE is not set — run: range42-context use }" diff --git a/scenarios/demo_lab_network/02_admin_infrastructure/stage_01/deployer_ui.devkit/demo_lab_network.deployer_ui.revert.sh b/scenarios/demo_lab_network/02_admin_infrastructure/stage_01/deployer_ui.devkit/demo_lab_network.deployer_ui.revert.sh new file mode 100755 index 00000000..ff6147b2 --- /dev/null +++ b/scenarios/demo_lab_network/02_admin_infrastructure/stage_01/deployer_ui.devkit/demo_lab_network.deployer_ui.revert.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +## +## + +echo '{"proxmox_node":"px-testing","vm_id":1023 }' | proxmox_snapshot_vm.vm_id.revert_snapshot.to.jsons.sh +echo '{"proxmox_node":"px-testing","vm_id":1023 }' | proxmox_vm.vm_id.start.to.jsons.sh diff --git a/scenarios/demo_lab_network/02_admin_infrastructure/stage_01/deployer_ui.devkit/demo_lab_network.deployer_ui.snapshot.sh b/scenarios/demo_lab_network/02_admin_infrastructure/stage_01/deployer_ui.devkit/demo_lab_network.deployer_ui.snapshot.sh new file mode 100755 index 00000000..733e0c52 --- /dev/null +++ b/scenarios/demo_lab_network/02_admin_infrastructure/stage_01/deployer_ui.devkit/demo_lab_network.deployer_ui.snapshot.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +## +## + +echo '{"proxmox_node":"px-testing","vm_id":1023,"vm_snapshot_description":"base"}' | proxmox_snapshot_vm.vm_id.create_snapshot.to.jsons.sh diff --git a/scenarios/demo_lab_network/02_admin_infrastructure/stage_01/deployer_ui.yml b/scenarios/demo_lab_network/02_admin_infrastructure/stage_01/deployer_ui.yml new file mode 100644 index 00000000..45fc3a10 --- /dev/null +++ b/scenarios/demo_lab_network/02_admin_infrastructure/stage_01/deployer_ui.yml @@ -0,0 +1,31 @@ +# +# +# + +- name: + become: true + hosts: r42.admin-deployer-ui + roles: + - software.install.nodejs_app_systemd + vars_files: + - "../../secrets/default_vault.yml" + vars: + # + NODE_VERSION: "22" + # + LOCAL_CODE_PATH: "{{ lookup('env', 'RANGE42_GITDIR__ROOT_DIR') }}/range42-deployer-ui/" + + REMOTE_PROJECT_DIR: /var/www/range42_deployer_ui + # + NPM_COMMAND: dev + # + OPERATOR_USER: "{{ default_admin_vm_ci_user }}" + # + APP_USER: webapp_user + APP_USER_SHELL: "/usr/sbin/nologin" + APP_NAME_SYSTEMD: range42_deployer_ui + + # to avoid home dir warning in case of no login user + # ansible_remote_tmp: "/tmp/.ansible-{{ APP_USER }}/tmp" +# +# diff --git a/scenarios/demo_lab_network/02_admin_infrastructure/stage_01/mon_wazuh.devkit/demo_lab_network.mon_wazuh.install.sh b/scenarios/demo_lab_network/02_admin_infrastructure/stage_01/mon_wazuh.devkit/demo_lab_network.mon_wazuh.install.sh new file mode 100755 index 00000000..bd26415d --- /dev/null +++ b/scenarios/demo_lab_network/02_admin_infrastructure/stage_01/mon_wazuh.devkit/demo_lab_network.mon_wazuh.install.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +## +## + +ansible-playbook -i "${RANGE42_ANSIBLE_ROLES__INVENTORY_DIR}/inventory_default.yml" \ + -l "all" \ + "../mon_wazuh.yml" --vault-password-file "${RANGE42_VAULT_PASSWORD_FILE:?RANGE42_VAULT_PASSWORD_FILE is not set — run: range42-context use }" diff --git a/scenarios/demo_lab_network/02_admin_infrastructure/stage_01/mon_wazuh.devkit/demo_lab_network.mon_wazuh.revert.sh b/scenarios/demo_lab_network/02_admin_infrastructure/stage_01/mon_wazuh.devkit/demo_lab_network.mon_wazuh.revert.sh new file mode 100755 index 00000000..97859219 --- /dev/null +++ b/scenarios/demo_lab_network/02_admin_infrastructure/stage_01/mon_wazuh.devkit/demo_lab_network.mon_wazuh.revert.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +## +## + +echo '{"proxmox_node":"px-testing","vm_id":1000 }' | proxmox_snapshot_vm.vm_id.revert_snapshot.to.jsons.sh +echo '{"proxmox_node":"px-testing","vm_id":1000 }' | proxmox_vm.vm_id.start.to.jsons.sh diff --git a/scenarios/demo_lab_network/02_admin_infrastructure/stage_01/mon_wazuh.devkit/demo_lab_network.mon_wazuh.snapshot.sh b/scenarios/demo_lab_network/02_admin_infrastructure/stage_01/mon_wazuh.devkit/demo_lab_network.mon_wazuh.snapshot.sh new file mode 100755 index 00000000..60a34a6c --- /dev/null +++ b/scenarios/demo_lab_network/02_admin_infrastructure/stage_01/mon_wazuh.devkit/demo_lab_network.mon_wazuh.snapshot.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +## +## + +echo '{"proxmox_node":"px-testing","vm_id":1000,"vm_snapshot_description":"base"}' | proxmox_snapshot_vm.vm_id.create_snapshot.to.jsons.sh diff --git a/scenarios/demo_lab_network/02_admin_infrastructure/stage_01/mon_wazuh.yml b/scenarios/demo_lab_network/02_admin_infrastructure/stage_01/mon_wazuh.yml new file mode 100644 index 00000000..bc514291 --- /dev/null +++ b/scenarios/demo_lab_network/02_admin_infrastructure/stage_01/mon_wazuh.yml @@ -0,0 +1,195 @@ + +- name: configure firewall - r42.admin-wazuh + become: true + hosts: r42.admin-wazuh + roles: + - software.configure.firewalls + vars_files: + - "../../secrets/default_vault.yml" + vars: + firewall_rules: + - ip: "all" + port: 22 + protocol: "tcp" + - ip: "all" + port: 443 + protocol: "tcp" + - ip: "all" + port: 1515 + protocol: "tcp" + - ip: "all" + port: 1514 + protocol: "tcp" + +# disabled — testing-wazuh-client, re-enable when wazuh config is validated +# - name: configure firewall - r42.testing-wazuh-client +# become: true +# hosts: r42.testing-wazuh-client +# roles: +# - software.configure.firewalls +# vars: +# firewall_rules: +# - ip: "all" +# port: 22 +# protocol: "tcp" + +# #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- name: Install wazuh-indexer + hosts: r42.admin-wazuh + become: yes + become_user: root + roles: + - role: software.install.wazuh-indexer + + vars_files: + - "../../secrets/default_vault.yml" + + vars: + local_certs_path: "/tmp/certificates/" + WAZUH_SERVER_IP: 192.168.142.100 # MOVE TO VAULT ? + single_node: true + indexer_network_host: "{{ WAZUH_SERVER_IP }}" + indexer_cluster_nodes: + - "{{ WAZUH_SERVER_IP }}" + indexer_jvm_xms: 4096 + ansible_shell_allow_world_readable_temp: true + instances: + node1: + name: node-1 + ip: "{{ WAZUH_SERVER_IP }}" + role: indexer + +# # #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- name: Wait for wazuh-indexer to be ready + hosts: r42.admin-wazuh + become: yes + tasks: + - name: Wait for wazuh-indexer API on port 9200 + ansible.builtin.wait_for: + port: 9200 + host: 192.168.142.100 + delay: 5 + timeout: 120 + +# # #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- name: Install wazuh-dashboard + hosts: r42.admin-wazuh + become: yes + become_user: root + roles: + - role: software.install.wazuh-dashboard + + vars_files: + - "../../secrets/default_vault.yml" + + vars: + local_certs_path: "/tmp/certificates/" + WAZUH_SERVER_IP: 192.168.142.100 # MOVE TO VAULT ? + single_node: true + indexer_network_host: "{{ WAZUH_SERVER_IP }}" + indexer_cluster_nodes: + - "{{ WAZUH_SERVER_IP }}" + ansible_shell_allow_world_readable_temp: true + instances: + node1: + name: node-1 + ip: "{{ WAZUH_SERVER_IP }}" + role: indexer + +# # #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- hosts: r42.admin-wazuh + become: yes + vars_files: + - "../../secrets/default_vault.yml" + vars: + WAZUH_SERVER_IP: 192.168.142.100 # MOVE TO VAULT ? + local_certs_path: "/tmp/certificates/" + + roles: + - role: software.install.wazuh-manager + - role: software.install.wazuh-filebeat-oss + filebeat_node_name: node-1 + filebeat_output_indexer_hosts: + - "{{ WAZUH_SERVER_IP }}:9200" + +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- name: Wait for wazuh-manager API before password change + hosts: r42.admin-wazuh + become: yes + tasks: + - name: Wait for wazuh-manager API on port 55000 + ansible.builtin.wait_for: + port: 55000 + host: 127.0.0.1 + delay: 10 + timeout: 120 + + - name: Wait for wazuh-manager daemons to be ready + ansible.builtin.command: /var/ossec/bin/wazuh-control status + register: _wazuh_status + failed_when: "'wazuh-modulesd is running' not in _wazuh_status.stdout" + until: "'wazuh-modulesd is running' in _wazuh_status.stdout" + retries: 12 + delay: 10 + changed_when: false + +# # #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- name: EXEC wazuh-passwords-tool.sh - if CHANGE_WAZUH_PASSWORD is true + hosts: r42.admin-wazuh + become: yes + become_user: root + vars_files: + - "../../secrets/default_vault.yml" + + vars: + CHANGE_WAZUH_PASSWORD: true + WAZUH_ANSIBLE_HOSTNAME: "r42.admin-wazuh" ######### IF NOT IN TAILSCALE ! + # WAZUH_ANSIBLE_HOSTNAME: "xxx.yyyy.zzzz.edge.wazuh" # IF TAILSCALE ! + # WAZUH_ANSIBLE_HOSTNAME: "192.168.99.11" + + #### MOVED TO VAULT :: + #### + # WAZUH_PASSWORD: "***********" # moved to vault ! + tasks: + - name: Run wazuh-passwords-tool.sh + ansible.builtin.shell: > + /usr/share/wazuh-indexer/plugins/opensearch-security/tools/wazuh-passwords-tool.sh + -u admin -p "{{ infrastructure_wazuh_admin_password }}" + register: password_output + delegate_to: "{{ WAZUH_ANSIBLE_HOSTNAME }}" + when: CHANGE_WAZUH_PASSWORD + retries: 2 + delay: 15 + until: password_output.rc == 0 + # no_log: true + no_log: false + +############################### +# wazuh deploy agent + +- hosts: r42_admin_wazuh_clients + become: yes + become_user: root + roles: + - software.install.wazuh-agent + + vars: + WAZUH_SERVER_IP: 192.168.142.100 # MOVE TO VAULT !! + wazuh_managers: + - address: "{{ WAZUH_SERVER_IP }}" + port: 1514 + protocol: tcp + api_port: 55000 + api_proto: "https" + api_user: wazuh + max_retries: 5 + retry_interval: 5 +# +############################### + diff --git a/scenarios/demo_lab_network/04_ctf_infrastructure/_main.reinstall.sh b/scenarios/demo_lab_network/04_ctf_infrastructure/_main.reinstall.sh new file mode 100755 index 00000000..7b1a2433 --- /dev/null +++ b/scenarios/demo_lab_network/04_ctf_infrastructure/_main.reinstall.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +## +## + +./stage_00/r42_vuln_box_group.devkit/demo_lab_network.vuln_box_01.delete.sh + +ansible-playbook -i "${RANGE42_ANSIBLE_ROLES__INVENTORY_DIR}/inventory_default.yml" \ + -l "all" \ + "./_main.yml" --vault-password-file "${RANGE42_VAULT_PASSWORD_FILE:?RANGE42_VAULT_PASSWORD_FILE is not set — run: range42-context use }" diff --git a/scenarios/demo_lab_network/04_ctf_infrastructure/_main.yml b/scenarios/demo_lab_network/04_ctf_infrastructure/_main.yml new file mode 100644 index 00000000..9207cdea --- /dev/null +++ b/scenarios/demo_lab_network/04_ctf_infrastructure/_main.yml @@ -0,0 +1,159 @@ +## +## issue 13 +## + +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +#### #### #### #### #### #### #### #### STAGE 00 #### #### #### #### #### #### #### #### #### #### #### #### #### #### +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# + +- import_playbook: ./stage_00/vuln_box_00.yml + + vars: + global_vm_name: "vuln-box-00" + global_vm_ssh_name: "r42.vuln-box-00" + # global_vm_ssh_name: "r42.{{ global_vm_name }}" + global_vm_id: 4000 + global_vm_description: "demo - vuln-box-00" + global_vm_tag_name: "admin" + global_vm_ci_ip: "192.168.144.170" + # + global_template_vm_id: 9221 + global_template_name: "template-vm-small-01-4g-32g - id : 9221" # debug + # + +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- import_playbook: ./stage_00/vuln_box_01.yml + + vars: + global_vm_name: "vuln-box-01" + global_vm_ssh_name: "r42.vuln-box-01" + # global_vm_ssh_name: "r42.{{ global_vm_name }}" + global_vm_id: 4001 + global_vm_description: "demo - vuln-box-01" + global_vm_tag_name: "admin" + global_vm_ci_ip: "192.168.144.171" + # + global_template_vm_id: 9221 + global_template_name: "template-vm-small-01-4g-32g - id : 9221" # debug + # + +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- import_playbook: ./stage_00/vuln_box_02.yml + + vars: + global_vm_name: "vuln-box-02" + global_vm_ssh_name: "r42.vuln-box-02" + # global_vm_ssh_name: "r42.{{ global_vm_name }}" + global_vm_id: 4002 + global_vm_description: "demo - vuln-box-02" + global_vm_tag_name: "admin" + global_vm_ci_ip: "192.168.144.172" + # + global_template_vm_id: 9221 + global_template_name: "template-vm-small-01-4g-32g - id : 9221" # debug + # + +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- import_playbook: ./stage_00/vuln_box_03.yml + + vars: + global_vm_name: "vuln-box-03" + global_vm_ssh_name: "r42.vuln-box-03" + # global_vm_ssh_name: "r42.{{ global_vm_name }}" + global_vm_id: 4003 + global_vm_description: "demo - vuln-box-03" + global_vm_tag_name: "admin" + global_vm_ci_ip: "192.168.144.173" + # + global_template_vm_id: 9221 + global_template_name: "template-vm-small-01-4g-32g - id : 9221" # debug + # + +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- import_playbook: ./stage_00/vuln_box_04.yml + + vars: + global_vm_name: "vuln-box-04" + global_vm_ssh_name: "r42.vuln-box-04" + # global_vm_ssh_name: "r42.{{ global_vm_name }}" + global_vm_id: 4004 + global_vm_description: "demo - vuln-box-04" + global_vm_tag_name: "admin" + global_vm_ci_ip: "192.168.144.174" + # + global_template_vm_id: 9221 + global_template_name: "template-vm-small-01-4g-32g - id : 9221" # debug + # + +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +# +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +#### #### #### #### #### #### #### #### STAGE 01 #### #### #### #### #### #### #### #### #### #### #### #### #### #### +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# + +- import_playbook: ./stage_01/_r42_vuln_box_group.yml + + vars: + OPERATOR_USER: alice + + INSTALL_VIM_DOTFILES: "YES" + INSTALL_ZSH_DOTFILES: "YES" + + INSTALL_PACKAGES_BASICS: "YES" + INSTALL_PACKAGES_FIREWALLS: "YES" + # + INSTALL_PACKAGES_DOCKER: "YES" + INSTALL_PACKAGES_DOCKER_COMPOSE: "YES" + # + INSTALL_PACKAGES_UTILS_JSON: "NO" + INSTALL_PACKAGES_UTILS_NETWORK: "YES" + + INSTALL_PACKAGES_NTP_AND_UPDATE_TIME: "YES" + # # + + INSTALL_TAILSCALE: "NO" + + # SPECIFIC_PACKAGES_CLEANING: "NO" + + firewall_rules: + - ip: "all" + port: 22 + protocol: "tcp" +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # +# - import_playbook: ./stage_01/vuln_box_00.yml +# vars: +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + +# - import_playbook: ./stage_01/vuln_box_01.yml +# vars: + +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + +# - import_playbook: ./stage_01/vuln_box_02.yml +# vars: + +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + +# - import_playbook: ./stage_01/vuln_box_03.yml +# vars: + +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + +# - import_playbook: ./stage_01/vuln_box_04.yml +# vars: +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + +# +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +#### #### #### #### #### #### #### #### STAGE 02 #### #### #### #### #### #### #### #### #### #### #### #### #### #### +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# +# +# - import_playbook: ./stage_02/_vuln_box.yml diff --git a/scenarios/demo_lab_network/04_ctf_infrastructure/stage_00/_r42_vuln_box_group.yml b/scenarios/demo_lab_network/04_ctf_infrastructure/stage_00/_r42_vuln_box_group.yml new file mode 100644 index 00000000..ffdf912c --- /dev/null +++ b/scenarios/demo_lab_network/04_ctf_infrastructure/stage_00/_r42_vuln_box_group.yml @@ -0,0 +1,90 @@ +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +#### #### #### #### #### #### #### #### STAGE 00 #### #### #### #### #### #### #### #### #### #### #### #### #### #### +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# + +## +## + +- import_playbook: ./vuln_box_00.yml + + vars: + global_vm_name: "vuln-box-00" + global_vm_ssh_name: "r42.vuln-box-00" + # global_vm_ssh_name: "r42.{{ global_vm_name }}" + global_vm_id: 4000 + global_vm_description: "demo - vuln-box-00" + global_vm_tag_name: "admin" + global_vm_ci_ip: "192.168.144.170" + # + global_template_vm_id: 9221 + global_template_name: "template-vm-small-01-4g-32g - id : 9221" # debug + # + +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- import_playbook: ./vuln_box_01.yml + + vars: + global_vm_name: "vuln-box-01" + global_vm_ssh_name: "r42.vuln-box-01" + # global_vm_ssh_name: "r42.{{ global_vm_name }}" + global_vm_id: 4001 + global_vm_description: "demo - vuln-box-01" + global_vm_tag_name: "admin" + global_vm_ci_ip: "192.168.144.171" + # + global_template_vm_id: 9221 + global_template_name: "template-vm-small-01-4g-32g - id : 9221" # debug + # + +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- import_playbook: ./vuln_box_02.yml + + vars: + global_vm_name: "vuln-box-02" + global_vm_ssh_name: "r42.vuln-box-02" + # global_vm_ssh_name: "r42.{{ global_vm_name }}" + global_vm_id: 4002 + global_vm_description: "demo - vuln-box-02" + global_vm_tag_name: "admin" + global_vm_ci_ip: "192.168.144.172" + # + global_template_vm_id: 9221 + global_template_name: "template-vm-small-01-4g-32g - id : 9221" # debug + # + +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- import_playbook: ./vuln_box_03.yml + + vars: + global_vm_name: "vuln-box-03" + global_vm_ssh_name: "r42.vuln-box-03" + # global_vm_ssh_name: "r42.{{ global_vm_name }}" + global_vm_id: 4003 + global_vm_description: "demo - vuln-box-03" + global_vm_tag_name: "admin" + global_vm_ci_ip: "192.168.144.173" + # + global_template_vm_id: 9221 + global_template_name: "template-vm-small-01-4g-32g - id : 9221" # debug + # + +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- import_playbook: ./vuln_box_04.yml + + vars: + global_vm_name: "vuln-box-04" + global_vm_ssh_name: "r42.vuln-box-04" + # global_vm_ssh_name: "r42.{{ global_vm_name }}" + global_vm_id: 4004 + global_vm_description: "demo - vuln-box-04" + global_vm_tag_name: "admin" + global_vm_ci_ip: "192.168.144.174" + # + global_template_vm_id: 9221 + global_template_name: "template-vm-small-01-4g-32g - id : 9221" # debug + # diff --git a/scenarios/demo_lab_network/04_ctf_infrastructure/stage_00/r42_vuln_box_group.devkit/demo_lab_network.vuln_box_01.delete.sh b/scenarios/demo_lab_network/04_ctf_infrastructure/stage_00/r42_vuln_box_group.devkit/demo_lab_network.vuln_box_01.delete.sh new file mode 100755 index 00000000..e9898fd7 --- /dev/null +++ b/scenarios/demo_lab_network/04_ctf_infrastructure/stage_00/r42_vuln_box_group.devkit/demo_lab_network.vuln_box_01.delete.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +## +## + +VULN_BOX=( + + # + "192.168.144.170" # vuln-box-00 + "192.168.144.171" # vuln-box-01 + "192.168.144.172" # vuln-box-02 + "192.168.144.173" # vuln-box-03 + "192.168.144.174" # vuln-box-04 +) + +for ip in "${VULN_BOX[@]}"; do + echo ":: REMOVE SSH KEY FOR : $ip" + ssh-keygen -f "$HOME/.ssh/known_hosts" -R "$ip" +done + +for line in $(proxmox_vm.list.to.jsons.sh | grep -i "vuln-box" | + jq -c "."); do + + printf "%s\n" "$line" | proxmox_vm.vm_id.stop.to.jsons.sh + + sleep 2 +done + +for line in $(proxmox_vm.list.to.jsons.sh | grep -i "vuln-box" | + jq -c "."); do + + printf "%s\n" "$line" | proxmox_vm.vm_id.delete.to.jsons.sh + + sleep 1 +done diff --git a/scenarios/demo_lab_network/04_ctf_infrastructure/stage_00/r42_vuln_box_group.devkit/demo_lab_network.vuln_box_01.install.sh b/scenarios/demo_lab_network/04_ctf_infrastructure/stage_00/r42_vuln_box_group.devkit/demo_lab_network.vuln_box_01.install.sh new file mode 100755 index 00000000..f1416832 --- /dev/null +++ b/scenarios/demo_lab_network/04_ctf_infrastructure/stage_00/r42_vuln_box_group.devkit/demo_lab_network.vuln_box_01.install.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +## +## + +ansible-playbook -i "${RANGE42_ANSIBLE_ROLES__INVENTORY_DIR}/inventory_default.yml" \ + -l "all" \ + "../_r42_vuln_box_group.yml" --vault-password-file "${RANGE42_VAULT_PASSWORD_FILE:?RANGE42_VAULT_PASSWORD_FILE is not set — run: range42-context use }" diff --git a/scenarios/demo_lab_network/04_ctf_infrastructure/stage_00/vuln_box_00.yml b/scenarios/demo_lab_network/04_ctf_infrastructure/stage_00/vuln_box_00.yml new file mode 100644 index 00000000..67211982 --- /dev/null +++ b/scenarios/demo_lab_network/04_ctf_infrastructure/stage_00/vuln_box_00.yml @@ -0,0 +1,116 @@ +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# +# PROMOX INIT INFRASTRUCTURE - VULN-BOX +# +# hostname : vuln-box-00 +# vm_id : 4000 +# cpu core : 1 +# ram : 4g +# disk : 32g +# ip : +# +# template : template-vm-small-01-4g-32g - id : 9221 +# +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +## +## + +- hosts: proxmox + gather_facts: false + vars_files: + - "../../secrets/default_vault.yml" + + vars: + + # + # variables will be overwrited by values in _main.yml + # + + tasks: + #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + + - name: ADMIN INFRASTRUCTURE INIT - "{{ global_vm_name }}" - CLONE TEMPLATE - "{{ global_template_name }}" + + include_role: + name: range42-ansible_roles-proxmox_controller + + vars: + proxmox_vm_action: "vm_clone" + vm_id: "{{ global_template_vm_id }}" # TEMPLATE VM_ID - vars should be rename to vm_id_src / vm_id_dst + vm_new_id: "{{ global_vm_id }}" + vm_name: "{{ global_vm_name }}" + vm_description: "{{ global_vm_description }}" + + #### + - name: INFRASTRUCTURE INIT - WAITING FOR PROXMOX UNLOCK TO CLEAR (slow setup) + ansible.builtin.pause: + seconds: 20 + prompt: "Waiting for 20 seconds during clone..." + #### + + - name: ADMIN INFRASTRUCTURE INIT - "{{ global_vm_name }}" - SET PROMOX TAG + + include_role: + name: range42-ansible_roles-proxmox_controller + + vars: + proxmox_vm_action: "vm_set_tag" + vm_id: "{{ global_vm_id }}" + vm_tag_name: "{{ global_vm_tag_name }}" + + #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + + - name: ADMIN INFRASTRUCTURE INIT - "{{ global_vm_name }}" - SET CLOUD DEFAULT INIT VARIABLE + + include_role: + name: range42-ansible_roles-proxmox_controller + + vars: + proxmox_vm_action: "cloudinit_set_variables" + + vm_id: "{{ global_vm_id }}" + vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" + vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" + vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" + vm_ci_ip: "{{ global_vm_ci_ip }}" + vm_ci_netmask: "24" + vm_ci_ip_gw: "192.168.144.1" + vm_net_virtio_bridge: "vmbr144" + + # + - name: ADMIN INFRASTRUCTURE INIT - "{{ global_vm_name }}" - CLONE TEMPLATE - START VM + + include_role: + name: range42-ansible_roles-proxmox_controller + + vars: + proxmox_vm_action: "vm_start" + vm_id: "{{ global_vm_id }}" + + # #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- hosts: "{{ global_vm_ssh_name }}" + gather_facts: false + vars_files: + - "../../secrets/default_vault.yml" + + vars: + # ARG_global_operator_ssh_config_known_hosts: "{{ global_operator_ssh_config_known_hosts }}" + # ARG_global_vm_ssh_name: "{{ global_vm_ssh_name }}" + # ARG_global_operator_ssh_config_known_hosts: "{{ global_operator_ssh_config_known_hosts }}" + # ARG_global_vm_ssh_name: "{{ global_vm_ssh_name }}" + + deployer_cli_user_ssh_known_hosts: "{{ deployer_cli_user_ssh_known_hosts }}" + ARG_vm_ssh_name: "{{ global_vm_ssh_name }}" + + requested_tasks: + - wait/openssh_server/is_reachable.yml + - wait/cloudinit/is_boot_finished.yml + tasks: + #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + + - name: TASK DYNAMIC INCLUDE FROM ansible.utils + include_role: + name: ansible.utils diff --git a/scenarios/demo_lab_network/04_ctf_infrastructure/stage_00/vuln_box_01.yml b/scenarios/demo_lab_network/04_ctf_infrastructure/stage_00/vuln_box_01.yml new file mode 100644 index 00000000..db072091 --- /dev/null +++ b/scenarios/demo_lab_network/04_ctf_infrastructure/stage_00/vuln_box_01.yml @@ -0,0 +1,116 @@ +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# +# PROMOX INIT INFRASTRUCTURE - VULN-BOX +# +# hostname : vuln-box-00 +# vm_id : 4000 +# cpu core : 1 +# ram : 4g +# disk : 32g +# ip : +# +# template : template-vm-small-01-4g-32g - id : 9221 +# +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +# +## + +- hosts: proxmox + gather_facts: false + vars_files: + - "../../secrets/default_vault.yml" + + vars: + + # + # variables will be overwrited by values in _main.yml + # + + tasks: + #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + + - name: ADMIN INFRASTRUCTURE INIT - "{{ global_vm_name }}" - CLONE TEMPLATE - "{{ global_template_name }}" + + include_role: + name: range42-ansible_roles-proxmox_controller + + vars: + proxmox_vm_action: "vm_clone" + vm_id: "{{ global_template_vm_id }}" # TEMPLATE VM_ID - vars should be rename to vm_id_src / vm_id_dst + vm_new_id: "{{ global_vm_id }}" + vm_name: "{{ global_vm_name }}" + vm_description: "{{ global_vm_description }}" + + #### + - name: INFRASTRUCTURE INIT - WAITING FOR PROXMOX UNLOCK TO CLEAR (slow setup) + ansible.builtin.pause: + seconds: 20 + prompt: "Waiting for 20 seconds during clone..." + #### + + - name: ADMIN INFRASTRUCTURE INIT - "{{ global_vm_name }}" - SET PROMOX TAG + + include_role: + name: range42-ansible_roles-proxmox_controller + + vars: + proxmox_vm_action: "vm_set_tag" + vm_id: "{{ global_vm_id }}" + vm_tag_name: "{{ global_vm_tag_name }}" + + #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + + - name: ADMIN INFRASTRUCTURE INIT - "{{ global_vm_name }}" - SET CLOUD DEFAULT INIT VARIABLE + + include_role: + name: range42-ansible_roles-proxmox_controller + + vars: + proxmox_vm_action: "cloudinit_set_variables" + + vm_id: "{{ global_vm_id }}" + vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" + vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" + vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" + vm_ci_ip: "{{ global_vm_ci_ip }}" + vm_ci_netmask: "24" + vm_ci_ip_gw: "192.168.144.1" + vm_net_virtio_bridge: "vmbr144" + + # + - name: ADMIN INFRASTRUCTURE INIT - "{{ global_vm_name }}" - CLONE TEMPLATE - START VM + + include_role: + name: range42-ansible_roles-proxmox_controller + + vars: + proxmox_vm_action: "vm_start" + vm_id: "{{ global_vm_id }}" + + # #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- hosts: "{{ global_vm_ssh_name }}" + gather_facts: false + vars_files: + - "../../secrets/default_vault.yml" + + vars: + # ARG_global_operator_ssh_config_known_hosts: "{{ global_operator_ssh_config_known_hosts }}" + # ARG_global_vm_ssh_name: "{{ global_vm_ssh_name }}" + # ARG_global_operator_ssh_config_known_hosts: "{{ global_operator_ssh_config_known_hosts }}" + # ARG_global_vm_ssh_name: "{{ global_vm_ssh_name }}" + + deployer_cli_user_ssh_known_hosts: "{{ deployer_cli_user_ssh_known_hosts }}" + ARG_vm_ssh_name: "{{ global_vm_ssh_name }}" + + requested_tasks: + - wait/openssh_server/is_reachable.yml + - wait/cloudinit/is_boot_finished.yml + tasks: + #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + + - name: TASK DYNAMIC INCLUDE FROM ansible.utils + include_role: + name: ansible.utils diff --git a/scenarios/demo_lab_network/04_ctf_infrastructure/stage_00/vuln_box_02.yml b/scenarios/demo_lab_network/04_ctf_infrastructure/stage_00/vuln_box_02.yml new file mode 100644 index 00000000..db072091 --- /dev/null +++ b/scenarios/demo_lab_network/04_ctf_infrastructure/stage_00/vuln_box_02.yml @@ -0,0 +1,116 @@ +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# +# PROMOX INIT INFRASTRUCTURE - VULN-BOX +# +# hostname : vuln-box-00 +# vm_id : 4000 +# cpu core : 1 +# ram : 4g +# disk : 32g +# ip : +# +# template : template-vm-small-01-4g-32g - id : 9221 +# +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +# +## + +- hosts: proxmox + gather_facts: false + vars_files: + - "../../secrets/default_vault.yml" + + vars: + + # + # variables will be overwrited by values in _main.yml + # + + tasks: + #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + + - name: ADMIN INFRASTRUCTURE INIT - "{{ global_vm_name }}" - CLONE TEMPLATE - "{{ global_template_name }}" + + include_role: + name: range42-ansible_roles-proxmox_controller + + vars: + proxmox_vm_action: "vm_clone" + vm_id: "{{ global_template_vm_id }}" # TEMPLATE VM_ID - vars should be rename to vm_id_src / vm_id_dst + vm_new_id: "{{ global_vm_id }}" + vm_name: "{{ global_vm_name }}" + vm_description: "{{ global_vm_description }}" + + #### + - name: INFRASTRUCTURE INIT - WAITING FOR PROXMOX UNLOCK TO CLEAR (slow setup) + ansible.builtin.pause: + seconds: 20 + prompt: "Waiting for 20 seconds during clone..." + #### + + - name: ADMIN INFRASTRUCTURE INIT - "{{ global_vm_name }}" - SET PROMOX TAG + + include_role: + name: range42-ansible_roles-proxmox_controller + + vars: + proxmox_vm_action: "vm_set_tag" + vm_id: "{{ global_vm_id }}" + vm_tag_name: "{{ global_vm_tag_name }}" + + #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + + - name: ADMIN INFRASTRUCTURE INIT - "{{ global_vm_name }}" - SET CLOUD DEFAULT INIT VARIABLE + + include_role: + name: range42-ansible_roles-proxmox_controller + + vars: + proxmox_vm_action: "cloudinit_set_variables" + + vm_id: "{{ global_vm_id }}" + vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" + vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" + vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" + vm_ci_ip: "{{ global_vm_ci_ip }}" + vm_ci_netmask: "24" + vm_ci_ip_gw: "192.168.144.1" + vm_net_virtio_bridge: "vmbr144" + + # + - name: ADMIN INFRASTRUCTURE INIT - "{{ global_vm_name }}" - CLONE TEMPLATE - START VM + + include_role: + name: range42-ansible_roles-proxmox_controller + + vars: + proxmox_vm_action: "vm_start" + vm_id: "{{ global_vm_id }}" + + # #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- hosts: "{{ global_vm_ssh_name }}" + gather_facts: false + vars_files: + - "../../secrets/default_vault.yml" + + vars: + # ARG_global_operator_ssh_config_known_hosts: "{{ global_operator_ssh_config_known_hosts }}" + # ARG_global_vm_ssh_name: "{{ global_vm_ssh_name }}" + # ARG_global_operator_ssh_config_known_hosts: "{{ global_operator_ssh_config_known_hosts }}" + # ARG_global_vm_ssh_name: "{{ global_vm_ssh_name }}" + + deployer_cli_user_ssh_known_hosts: "{{ deployer_cli_user_ssh_known_hosts }}" + ARG_vm_ssh_name: "{{ global_vm_ssh_name }}" + + requested_tasks: + - wait/openssh_server/is_reachable.yml + - wait/cloudinit/is_boot_finished.yml + tasks: + #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + + - name: TASK DYNAMIC INCLUDE FROM ansible.utils + include_role: + name: ansible.utils diff --git a/scenarios/demo_lab_network/04_ctf_infrastructure/stage_00/vuln_box_03.yml b/scenarios/demo_lab_network/04_ctf_infrastructure/stage_00/vuln_box_03.yml new file mode 100644 index 00000000..db072091 --- /dev/null +++ b/scenarios/demo_lab_network/04_ctf_infrastructure/stage_00/vuln_box_03.yml @@ -0,0 +1,116 @@ +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# +# PROMOX INIT INFRASTRUCTURE - VULN-BOX +# +# hostname : vuln-box-00 +# vm_id : 4000 +# cpu core : 1 +# ram : 4g +# disk : 32g +# ip : +# +# template : template-vm-small-01-4g-32g - id : 9221 +# +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +# +## + +- hosts: proxmox + gather_facts: false + vars_files: + - "../../secrets/default_vault.yml" + + vars: + + # + # variables will be overwrited by values in _main.yml + # + + tasks: + #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + + - name: ADMIN INFRASTRUCTURE INIT - "{{ global_vm_name }}" - CLONE TEMPLATE - "{{ global_template_name }}" + + include_role: + name: range42-ansible_roles-proxmox_controller + + vars: + proxmox_vm_action: "vm_clone" + vm_id: "{{ global_template_vm_id }}" # TEMPLATE VM_ID - vars should be rename to vm_id_src / vm_id_dst + vm_new_id: "{{ global_vm_id }}" + vm_name: "{{ global_vm_name }}" + vm_description: "{{ global_vm_description }}" + + #### + - name: INFRASTRUCTURE INIT - WAITING FOR PROXMOX UNLOCK TO CLEAR (slow setup) + ansible.builtin.pause: + seconds: 20 + prompt: "Waiting for 20 seconds during clone..." + #### + + - name: ADMIN INFRASTRUCTURE INIT - "{{ global_vm_name }}" - SET PROMOX TAG + + include_role: + name: range42-ansible_roles-proxmox_controller + + vars: + proxmox_vm_action: "vm_set_tag" + vm_id: "{{ global_vm_id }}" + vm_tag_name: "{{ global_vm_tag_name }}" + + #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + + - name: ADMIN INFRASTRUCTURE INIT - "{{ global_vm_name }}" - SET CLOUD DEFAULT INIT VARIABLE + + include_role: + name: range42-ansible_roles-proxmox_controller + + vars: + proxmox_vm_action: "cloudinit_set_variables" + + vm_id: "{{ global_vm_id }}" + vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" + vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" + vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" + vm_ci_ip: "{{ global_vm_ci_ip }}" + vm_ci_netmask: "24" + vm_ci_ip_gw: "192.168.144.1" + vm_net_virtio_bridge: "vmbr144" + + # + - name: ADMIN INFRASTRUCTURE INIT - "{{ global_vm_name }}" - CLONE TEMPLATE - START VM + + include_role: + name: range42-ansible_roles-proxmox_controller + + vars: + proxmox_vm_action: "vm_start" + vm_id: "{{ global_vm_id }}" + + # #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- hosts: "{{ global_vm_ssh_name }}" + gather_facts: false + vars_files: + - "../../secrets/default_vault.yml" + + vars: + # ARG_global_operator_ssh_config_known_hosts: "{{ global_operator_ssh_config_known_hosts }}" + # ARG_global_vm_ssh_name: "{{ global_vm_ssh_name }}" + # ARG_global_operator_ssh_config_known_hosts: "{{ global_operator_ssh_config_known_hosts }}" + # ARG_global_vm_ssh_name: "{{ global_vm_ssh_name }}" + + deployer_cli_user_ssh_known_hosts: "{{ deployer_cli_user_ssh_known_hosts }}" + ARG_vm_ssh_name: "{{ global_vm_ssh_name }}" + + requested_tasks: + - wait/openssh_server/is_reachable.yml + - wait/cloudinit/is_boot_finished.yml + tasks: + #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + + - name: TASK DYNAMIC INCLUDE FROM ansible.utils + include_role: + name: ansible.utils diff --git a/scenarios/demo_lab_network/04_ctf_infrastructure/stage_00/vuln_box_04.yml b/scenarios/demo_lab_network/04_ctf_infrastructure/stage_00/vuln_box_04.yml new file mode 100644 index 00000000..db072091 --- /dev/null +++ b/scenarios/demo_lab_network/04_ctf_infrastructure/stage_00/vuln_box_04.yml @@ -0,0 +1,116 @@ +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# +# PROMOX INIT INFRASTRUCTURE - VULN-BOX +# +# hostname : vuln-box-00 +# vm_id : 4000 +# cpu core : 1 +# ram : 4g +# disk : 32g +# ip : +# +# template : template-vm-small-01-4g-32g - id : 9221 +# +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +# +## + +- hosts: proxmox + gather_facts: false + vars_files: + - "../../secrets/default_vault.yml" + + vars: + + # + # variables will be overwrited by values in _main.yml + # + + tasks: + #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + + - name: ADMIN INFRASTRUCTURE INIT - "{{ global_vm_name }}" - CLONE TEMPLATE - "{{ global_template_name }}" + + include_role: + name: range42-ansible_roles-proxmox_controller + + vars: + proxmox_vm_action: "vm_clone" + vm_id: "{{ global_template_vm_id }}" # TEMPLATE VM_ID - vars should be rename to vm_id_src / vm_id_dst + vm_new_id: "{{ global_vm_id }}" + vm_name: "{{ global_vm_name }}" + vm_description: "{{ global_vm_description }}" + + #### + - name: INFRASTRUCTURE INIT - WAITING FOR PROXMOX UNLOCK TO CLEAR (slow setup) + ansible.builtin.pause: + seconds: 20 + prompt: "Waiting for 20 seconds during clone..." + #### + + - name: ADMIN INFRASTRUCTURE INIT - "{{ global_vm_name }}" - SET PROMOX TAG + + include_role: + name: range42-ansible_roles-proxmox_controller + + vars: + proxmox_vm_action: "vm_set_tag" + vm_id: "{{ global_vm_id }}" + vm_tag_name: "{{ global_vm_tag_name }}" + + #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + + - name: ADMIN INFRASTRUCTURE INIT - "{{ global_vm_name }}" - SET CLOUD DEFAULT INIT VARIABLE + + include_role: + name: range42-ansible_roles-proxmox_controller + + vars: + proxmox_vm_action: "cloudinit_set_variables" + + vm_id: "{{ global_vm_id }}" + vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" + vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" + vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" + vm_ci_ip: "{{ global_vm_ci_ip }}" + vm_ci_netmask: "24" + vm_ci_ip_gw: "192.168.144.1" + vm_net_virtio_bridge: "vmbr144" + + # + - name: ADMIN INFRASTRUCTURE INIT - "{{ global_vm_name }}" - CLONE TEMPLATE - START VM + + include_role: + name: range42-ansible_roles-proxmox_controller + + vars: + proxmox_vm_action: "vm_start" + vm_id: "{{ global_vm_id }}" + + # #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- hosts: "{{ global_vm_ssh_name }}" + gather_facts: false + vars_files: + - "../../secrets/default_vault.yml" + + vars: + # ARG_global_operator_ssh_config_known_hosts: "{{ global_operator_ssh_config_known_hosts }}" + # ARG_global_vm_ssh_name: "{{ global_vm_ssh_name }}" + # ARG_global_operator_ssh_config_known_hosts: "{{ global_operator_ssh_config_known_hosts }}" + # ARG_global_vm_ssh_name: "{{ global_vm_ssh_name }}" + + deployer_cli_user_ssh_known_hosts: "{{ deployer_cli_user_ssh_known_hosts }}" + ARG_vm_ssh_name: "{{ global_vm_ssh_name }}" + + requested_tasks: + - wait/openssh_server/is_reachable.yml + - wait/cloudinit/is_boot_finished.yml + tasks: + #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + + - name: TASK DYNAMIC INCLUDE FROM ansible.utils + include_role: + name: ansible.utils diff --git a/scenarios/demo_lab_network/04_ctf_infrastructure/stage_01/_r42_vuln_box_group.devkit/demo_lab_network.r42_vuln_box.install.sh b/scenarios/demo_lab_network/04_ctf_infrastructure/stage_01/_r42_vuln_box_group.devkit/demo_lab_network.r42_vuln_box.install.sh new file mode 100755 index 00000000..f1416832 --- /dev/null +++ b/scenarios/demo_lab_network/04_ctf_infrastructure/stage_01/_r42_vuln_box_group.devkit/demo_lab_network.r42_vuln_box.install.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +## +## + +ansible-playbook -i "${RANGE42_ANSIBLE_ROLES__INVENTORY_DIR}/inventory_default.yml" \ + -l "all" \ + "../_r42_vuln_box_group.yml" --vault-password-file "${RANGE42_VAULT_PASSWORD_FILE:?RANGE42_VAULT_PASSWORD_FILE is not set — run: range42-context use }" diff --git a/scenarios/demo_lab_network/04_ctf_infrastructure/stage_01/_r42_vuln_box_group.devkit/demo_lab_network.r42_vuln_box.revert.sh b/scenarios/demo_lab_network/04_ctf_infrastructure/stage_01/_r42_vuln_box_group.devkit/demo_lab_network.r42_vuln_box.revert.sh new file mode 100755 index 00000000..3e89ba61 --- /dev/null +++ b/scenarios/demo_lab_network/04_ctf_infrastructure/stage_01/_r42_vuln_box_group.devkit/demo_lab_network.r42_vuln_box.revert.sh @@ -0,0 +1,34 @@ +#!/bin/bash + +## +## + +# proxmox_vm.list.to.jsons.sh | +# grep -vi template | +# grep -vi group | +# grep -iE "(admin-)|(testing-)" | +# jq -c | +# proxmox_snapshot_vm.vm_id.revert_snapshot.to.jsons.sh + +# proxmox_vm.list.to.jsons.sh | +# grep -vi template | +# grep -vi group | +# grep -iE "(admin-)|(testing-)" | +# jq -c | +# proxmox_vm.vm_id.start.to.jsons.sh + +for line in $(proxmox_vm.list.to.jsons.sh | grep -i "vuln-box" | + jq -c "."); do + + printf "%s\n" "$line" | proxmox_snapshot_vm.vm_id.revert_snapshot.to.jsons.sh + + sleep 2 +done + +for line in $(proxmox_vm.list.to.jsons.sh | grep -i "vuln-box" | + jq -c "."); do + + printf "%s\n" "$line" | proxmox_vm.vm_id.start.to.jsons.sh + + sleep 1 +done diff --git a/scenarios/demo_lab_network/04_ctf_infrastructure/stage_01/_r42_vuln_box_group.devkit/demo_lab_network.r42_vuln_box.snapshot.sh b/scenarios/demo_lab_network/04_ctf_infrastructure/stage_01/_r42_vuln_box_group.devkit/demo_lab_network.r42_vuln_box.snapshot.sh new file mode 100755 index 00000000..86183aa3 --- /dev/null +++ b/scenarios/demo_lab_network/04_ctf_infrastructure/stage_01/_r42_vuln_box_group.devkit/demo_lab_network.r42_vuln_box.snapshot.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +## +## + +# +# proxmox_vm.list.to.jsons.sh | +# grep -vi template | +# grep -vi group | +# grep -iE "(admin-)|(testing-)" | +# jq -c "." | +# proxmox_snapshot_vm.vm_id.create_snapshot.to.jsons.sh + +for line in $(proxmox_vm.list.to.jsons.sh | grep -i "vuln-box" | + jq -c "."); do + + printf "%s\n" "$line" | proxmox_snapshot_vm.vm_id.create_snapshot.to.jsons.sh + + sleep 2 +done diff --git a/scenarios/demo_lab_network/04_ctf_infrastructure/stage_01/_r42_vuln_box_group.yml b/scenarios/demo_lab_network/04_ctf_infrastructure/stage_01/_r42_vuln_box_group.yml new file mode 100644 index 00000000..24cc62e5 --- /dev/null +++ b/scenarios/demo_lab_network/04_ctf_infrastructure/stage_01/_r42_vuln_box_group.yml @@ -0,0 +1,115 @@ +## +## issue 13 +## + +- name: install basics packages + hosts: r42_vuln_box_group + become: true + roles: + - software.install.warmup.basic_packages + + # + # variables will be overwrited by values in _main.yml + # + vars: + INSTALL_PACKAGES_BASICS: "YES" + INSTALL_PACKAGES_FIREWALLS: "YES" + # + INSTALL_PACKAGES_DOCKER: "YES" + INSTALL_PACKAGES_DOCKER_COMPOSE: "YES" + # + INSTALL_PACKAGES_UTILS_JSON: "NO" + INSTALL_PACKAGES_UTILS_NETWORK: "YES" + + INSTALL_PACKAGES_NTP_AND_UPDATE_TIME: "YES" + + SPECIFIC_PACKAGES_CLEANING: "NO" + +# #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- name: install dot files + hosts: r42_vuln_box_group + become: true + roles: + - software.install.warmup.dot_files + + vars: + # + # variables will be overwrited by values in _main.yml + # + OPERATOR_USER: alice + INSTALL_VIM_DOTFILES: "YES" + INSTALL_ZSH_DOTFILES: "YES" + +- name: configure firewall - all + become: true + hosts: r42_vuln_box_group + roles: + - software.configure.firewalls + + vars: + # + # variables will be overwrited by values in _main.yml + # + + firewall_rules: + - ip: "all" + port: 22 + protocol: "tcp" + +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- hosts: r42.admin-wazuh + gather_facts: false + vars_files: + - "../../secrets/default_vault.yml" + + vars: + tailscale_hostnames: + - "vuln-box-00" + - "vuln-box-01" + - "vuln-box-02" + - "vuln-box-03" + - "vuln-box-04" + + INSTALL_TAILSCALE: "NO" # variables defined in main + + requested_tasks: + - delete/tailscale_client.yml + # + tasks: + # + # + - name: TASK DYNAMIC INCLUDE FROM ansible.utils + include_role: + name: ansible.utils + loop: "{{ tailscale_hostnames }}" + loop_control: + loop_var: tailscale_hostname + when: INSTALL_TAILSCALE == "YES" + +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +## +## issue 11 +## + +- hosts: r42_vuln_box_group + become: true + vars_files: + - "../../secrets/default_vault.yml" + vars: + # + # variables defined in vault + # + tailscale_authkey: "{{ infrastructure_tailscale_authkey }}" + INSTALL_TAILSCALE: "NO" # variables defined in main + + tasks: + - block: + - include_role: + name: software.install.tailscale + - include_role: + name: software.configure.tailscale_disable_nftables + + when: INSTALL_TAILSCALE == "YES" diff --git a/scenarios/demo_lab_network/04_ctf_infrastructure/stage_01/vuln_box_00.devkit/demo_lab_network.vuln_box_00.install.sh b/scenarios/demo_lab_network/04_ctf_infrastructure/stage_01/vuln_box_00.devkit/demo_lab_network.vuln_box_00.install.sh new file mode 100755 index 00000000..af1511f8 --- /dev/null +++ b/scenarios/demo_lab_network/04_ctf_infrastructure/stage_01/vuln_box_00.devkit/demo_lab_network.vuln_box_00.install.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +## +## + +ansible-playbook -i "${RANGE42_ANSIBLE_ROLES__INVENTORY_DIR}/inventory_default.yml" \ + -l "all" \ + "../vuln_box_00.yml" --vault-password-file "${RANGE42_VAULT_PASSWORD_FILE:?RANGE42_VAULT_PASSWORD_FILE is not set — run: range42-context use }" diff --git a/scenarios/demo_lab_network/04_ctf_infrastructure/stage_01/vuln_box_00.devkit/demo_lab_network.vuln_box_00.revert.sh b/scenarios/demo_lab_network/04_ctf_infrastructure/stage_01/vuln_box_00.devkit/demo_lab_network.vuln_box_00.revert.sh new file mode 100755 index 00000000..777d2020 --- /dev/null +++ b/scenarios/demo_lab_network/04_ctf_infrastructure/stage_01/vuln_box_00.devkit/demo_lab_network.vuln_box_00.revert.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +## +## + +proxmox_vm.list.to.jsons.sh | grep -i vuln-box-00 | proxmox_snapshot_vm.vm_id.revert_snapshot.to.jsons.sh +proxmox_vm.list.to.jsons.sh | grep -i vuln-box-00 | proxmox_vm.vm_id.start.to.jsons.sh diff --git a/scenarios/demo_lab_network/04_ctf_infrastructure/stage_01/vuln_box_00.devkit/demo_lab_network.vuln_box_00.snapshot.sh b/scenarios/demo_lab_network/04_ctf_infrastructure/stage_01/vuln_box_00.devkit/demo_lab_network.vuln_box_00.snapshot.sh new file mode 100755 index 00000000..78d6c18c --- /dev/null +++ b/scenarios/demo_lab_network/04_ctf_infrastructure/stage_01/vuln_box_00.devkit/demo_lab_network.vuln_box_00.snapshot.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +## +## + +# proxmox_vm.list.to.jsons.sh | grep -i vuln-box-00 | proxmox_snapshot_vm.vm_id.create_snapshot.to.jsons.sh +echo '{"proxmox_node":"px-testing","vm_id":4000,"vm_snapshot_description":"base"}' | proxmox_snapshot_vm.vm_id.create_snapshot.to.jsons.sh diff --git a/scenarios/demo_lab_network/04_ctf_infrastructure/stage_01/vuln_box_00.yml b/scenarios/demo_lab_network/04_ctf_infrastructure/stage_01/vuln_box_00.yml new file mode 100644 index 00000000..97bc22fd --- /dev/null +++ b/scenarios/demo_lab_network/04_ctf_infrastructure/stage_01/vuln_box_00.yml @@ -0,0 +1,67 @@ +## +## + +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- name: configure firewall - all + become: true + hosts: vuln-box-00 + roles: + - software.configure.firewalls + + vars: + # variables will be overwrited by values in _main.yml + firewall_rules: + - ip: "all" + port: 22 + protocol: "tcp" + - ip: "all" + port: 21 + protocol: "tcp" + - ip: "all" + port: 8080 + protocol: "tcp" + +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- name: DOCKER - uwsgi-php-cve-2018-7490 - exposed_port:8080 + hosts: r42.vuln-box-00 + become: true + roles: + - software.configure.docker-compose + vars_files: + - "../../secrets/default_vault.yml" + vars: + LABEL_PROJECT_TYPE: "CTF" + LABEL_PROJET_NAME: "uwsgi-php-cve-2018-7490" + # + LOCAL__PROJECT_DIR: "{{ lookup('env', 'RANGE42_INVENTORY__DOCKER__CTF') }}/cve/web/uwsg_php/CVE-2018-7490/" + REMOTE_PROJECT_DIR: "/tmp/deploy-{{ LABEL_PROJET_NAME }}" + # + OPERATOR_USER: "{{ default_admin_vm_ci_user }}" + # + CLEAN_UP_DEPLOY_DIR: "NO" + SEND_POC_DIR: "NO" + +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- name: DOCKER FTP ANONYMOUS SERVER - exposed_port:21 + hosts: r42.vuln-box-00 + become: true + roles: + - software.configure.docker-compose + vars_files: + - "../../secrets/default_vault.yml" + vars: + LABEL_PROJECT_TYPE: "CTF" + LABEL_PROJET_NAME: "ftp_anon_server" + + # + LOCAL__PROJECT_DIR: "{{ lookup('env', 'RANGE42_INVENTORY__DOCKER__CTF') }}/misconfiguration/network/vsftpd/ftp_anon_server/" + REMOTE_PROJECT_DIR: "/tmp/deploy-{{ LABEL_PROJET_NAME }}" + # + + OPERATOR_USER: "{{ default_admin_vm_ci_user }}" + # + CLEAN_UP_DEPLOY_DIR: "NO" + SEND_POC_DIR: "NO" diff --git a/scenarios/demo_lab_network/04_ctf_infrastructure/stage_01/vuln_box_01.devkit/demo_lab_network.vuln_box_01.install.sh b/scenarios/demo_lab_network/04_ctf_infrastructure/stage_01/vuln_box_01.devkit/demo_lab_network.vuln_box_01.install.sh new file mode 100755 index 00000000..2ae455f5 --- /dev/null +++ b/scenarios/demo_lab_network/04_ctf_infrastructure/stage_01/vuln_box_01.devkit/demo_lab_network.vuln_box_01.install.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +## +## + +ansible-playbook -i "${RANGE42_ANSIBLE_ROLES__INVENTORY_DIR}/inventory_default.yml" \ + -l "all" \ + "../vuln_box_01.yml" --vault-password-file "${RANGE42_VAULT_PASSWORD_FILE:?RANGE42_VAULT_PASSWORD_FILE is not set — run: range42-context use }" diff --git a/scenarios/demo_lab_network/04_ctf_infrastructure/stage_01/vuln_box_01.devkit/demo_lab_network.vuln_box_01.revert.sh b/scenarios/demo_lab_network/04_ctf_infrastructure/stage_01/vuln_box_01.devkit/demo_lab_network.vuln_box_01.revert.sh new file mode 100755 index 00000000..51472a78 --- /dev/null +++ b/scenarios/demo_lab_network/04_ctf_infrastructure/stage_01/vuln_box_01.devkit/demo_lab_network.vuln_box_01.revert.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +## +## + +proxmox_vm.list.to.jsons.sh | grep -i vuln-box-01 | proxmox_snapshot_vm.vm_id.revert_snapshot.to.jsons.sh +proxmox_vm.list.to.jsons.sh | grep -i vuln-box-01 | proxmox_vm.vm_id.start.to.jsons.sh diff --git a/scenarios/demo_lab_network/04_ctf_infrastructure/stage_01/vuln_box_01.devkit/demo_lab_network.vuln_box_01.snapshot.sh b/scenarios/demo_lab_network/04_ctf_infrastructure/stage_01/vuln_box_01.devkit/demo_lab_network.vuln_box_01.snapshot.sh new file mode 100755 index 00000000..040c92d1 --- /dev/null +++ b/scenarios/demo_lab_network/04_ctf_infrastructure/stage_01/vuln_box_01.devkit/demo_lab_network.vuln_box_01.snapshot.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +## +## + +# proxmox_vm.list.to.jsons.sh | grep -i vuln-box-00 | proxmox_snapshot_vm.vm_id.create_snapshot.to.jsons.sh +echo '{"proxmox_node":"px-testing","vm_id":4001,"vm_snapshot_description":"base"}' | proxmox_snapshot_vm.vm_id.create_snapshot.to.jsons.sh diff --git a/scenarios/demo_lab_network/04_ctf_infrastructure/stage_01/vuln_box_01.yml b/scenarios/demo_lab_network/04_ctf_infrastructure/stage_01/vuln_box_01.yml new file mode 100644 index 00000000..2247611d --- /dev/null +++ b/scenarios/demo_lab_network/04_ctf_infrastructure/stage_01/vuln_box_01.yml @@ -0,0 +1,145 @@ +## +## + +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- name: configure firewall - all + become: true + hosts: r42.vuln-box-01 + roles: + - software.configure.firewalls + + vars: + # variables will be overwrited by values in _main.yml + firewall_rules: + - ip: "all" + port: 22 + protocol: "tcp" + - ip: "all" + port: 8443 + protocol: "tcp" + - ip: "all" + port: 9443 + protocol: "tcp" + - ip: "all" + port: 2218 + protocol: "tcp" + - ip: "all" + port: 2224 + protocol: "tcp" + - ip: "all" + port: 2225 + protocol: "tcp" + +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- name: DEPLOY - cve/crypto/openssl/CVE-2014-0160/" - exposed_port:8443 + hosts: r42.vuln-box-01 + become: true + roles: + - software.configure.docker-compose + + vars_files: + - "../../secrets/default_vault.yml" + vars: + LABEL_PROJECT_TYPE: "CTF" + LABEL_PROJET_NAME: "crypto-openssl-cve-2014-0160" + # + LOCAL__PROJECT_DIR: "{{ lookup('env', 'RANGE42_INVENTORY__DOCKER__CTF') }}/cve/crypto/openssl/CVE-2014-0160/" + # REMOTE_PROJECT_DIR: "/tmp/deploy" + REMOTE_PROJECT_DIR: "/tmp/deploy-{{ LABEL_PROJET_NAME }}" + # + OPERATOR_USER: "{{ default_admin_vm_ci_user }}" + # + CLEAN_UP_DEPLOY_DIR: "NO" + SEND_POC_DIR: "YES" + +# ### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- name: DEPLOY - crypto-openssl-cve-2022-0778 - exposed_port:9443 + hosts: r42.vuln-box-01 + become: true + roles: + - software.configure.docker-compose + + vars_files: + - "../../secrets/default_vault.yml" + vars: + LABEL_PROJECT_TYPE: "CTF" + LABEL_PROJET_NAME: "crypto-openssl-cve-2022-0778" + # + LOCAL__PROJECT_DIR: "{{ lookup('env', 'RANGE42_INVENTORY__DOCKER__CTF') }}/cve/crypto/openssl/CVE-2022-0778/" + REMOTE_PROJECT_DIR: "/tmp/deploy-{{ LABEL_PROJET_NAME }}" + # + OPERATOR_USER: "{{ default_admin_vm_ci_user }}" + # + CLEAN_UP_DEPLOY_DIR: "NO" + SEND_POC_DIR: "YES" + +# ### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- name: DEPLOY - erlang-ssh-CVE-2025-32433 - exposed_port:2225 + hosts: r42.vuln-box-01 + become: true + roles: + - software.configure.docker-compose + + vars_files: + - "../../secrets/default_vault.yml" + vars: + LABEL_PROJECT_TYPE: "CTF" + LABEL_PROJET_NAME: "erlang-ssh-CVE-2025-32433" + # + + LOCAL__PROJECT_DIR: "{{ lookup('env', 'RANGE42_INVENTORY__DOCKER__CTF') }}/cve/network/erlang-ssh/CVE-2025-32433" + REMOTE_PROJECT_DIR: "/tmp/deploy-{{ LABEL_PROJET_NAME }}" + # + OPERATOR_USER: "{{ default_admin_vm_ci_user }}" + # + CLEAN_UP_DEPLOY_DIR: "NO" + SEND_POC_DIR: "YES" + +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- name: DEPLOY - openssh-cve-2018-15473 - exposed_port:2218 + hosts: r42.vuln-box-01 + become: true + roles: + - software.configure.docker-compose + + vars_files: + - "../../secrets/default_vault.yml" + vars: + LABEL_PROJECT_TYPE: "CTF" + LABEL_PROJET_NAME: "openssh-cve-2018-15473" + # + LOCAL__PROJECT_DIR: "{{ lookup('env', 'RANGE42_INVENTORY__DOCKER__CTF') }}/cve/network/openssh/CVE-2018-15473" + REMOTE_PROJECT_DIR: "/tmp/deploy-{{ LABEL_PROJET_NAME }}" + # + OPERATOR_USER: "{{ default_admin_vm_ci_user }}" + # + CLEAN_UP_DEPLOY_DIR: "NO" + SEND_POC_DIR: "YES" + +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- name: DEPLOY - openssh-cve-2024-6387 - exposed_port:2224 + hosts: r42.vuln-box-01 + become: true + roles: + - software.configure.docker-compose + + vars_files: + - "../../secrets/default_vault.yml" + vars: + LABEL_PROJECT_TYPE: "CTF" + LABEL_PROJET_NAME: "openssh-cve-2024-6387" + # + LOCAL__PROJECT_DIR: "{{ lookup('env', 'RANGE42_INVENTORY__DOCKER__CTF') }}/cve/network/openssh/CVE-2024-6387" + REMOTE_PROJECT_DIR: "/tmp/deploy-{{ LABEL_PROJET_NAME }}" + # + OPERATOR_USER: "{{ default_admin_vm_ci_user }}" + # + CLEAN_UP_DEPLOY_DIR: "NO" + SEND_POC_DIR: "YES" +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### diff --git a/scenarios/demo_lab_network/04_ctf_infrastructure/stage_01/vuln_box_02.devkit/demo_lab_network.vuln_box_02.install.sh b/scenarios/demo_lab_network/04_ctf_infrastructure/stage_01/vuln_box_02.devkit/demo_lab_network.vuln_box_02.install.sh new file mode 100755 index 00000000..7972f8d8 --- /dev/null +++ b/scenarios/demo_lab_network/04_ctf_infrastructure/stage_01/vuln_box_02.devkit/demo_lab_network.vuln_box_02.install.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +## +## + +ansible-playbook -i "${RANGE42_ANSIBLE_ROLES__INVENTORY_DIR}/inventory_default.yml" \ + -l "all" \ + "../vuln_box_02.yml" --vault-password-file "${RANGE42_VAULT_PASSWORD_FILE:?RANGE42_VAULT_PASSWORD_FILE is not set — run: range42-context use }" diff --git a/scenarios/demo_lab_network/04_ctf_infrastructure/stage_01/vuln_box_02.devkit/demo_lab_network.vuln_box_02.revert.sh b/scenarios/demo_lab_network/04_ctf_infrastructure/stage_01/vuln_box_02.devkit/demo_lab_network.vuln_box_02.revert.sh new file mode 100755 index 00000000..6f3f1175 --- /dev/null +++ b/scenarios/demo_lab_network/04_ctf_infrastructure/stage_01/vuln_box_02.devkit/demo_lab_network.vuln_box_02.revert.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +## +## + +proxmox_vm.list.to.jsons.sh | grep -i vuln-box-02 | proxmox_snapshot_vm.vm_id.revert_snapshot.to.jsons.sh +proxmox_vm.list.to.jsons.sh | grep -i vuln-box-02 | proxmox_vm.vm_id.start.to.jsons.sh diff --git a/scenarios/demo_lab_network/04_ctf_infrastructure/stage_01/vuln_box_02.devkit/demo_lab_network.vuln_box_02.snapshot.sh b/scenarios/demo_lab_network/04_ctf_infrastructure/stage_01/vuln_box_02.devkit/demo_lab_network.vuln_box_02.snapshot.sh new file mode 100755 index 00000000..e8d50d92 --- /dev/null +++ b/scenarios/demo_lab_network/04_ctf_infrastructure/stage_01/vuln_box_02.devkit/demo_lab_network.vuln_box_02.snapshot.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +## +## + +echo '{"proxmox_node":"px-testing","vm_id":4002,"vm_snapshot_description":"base"}' | proxmox_snapshot_vm.vm_id.create_snapshot.to.jsons.sh diff --git a/scenarios/demo_lab_network/04_ctf_infrastructure/stage_01/vuln_box_02.yml b/scenarios/demo_lab_network/04_ctf_infrastructure/stage_01/vuln_box_02.yml new file mode 100644 index 00000000..afa2b6fe --- /dev/null +++ b/scenarios/demo_lab_network/04_ctf_infrastructure/stage_01/vuln_box_02.yml @@ -0,0 +1,169 @@ +## +## + +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- name: configure firewall - all + become: true + hosts: r42.vuln-box-02 + roles: + - software.configure.firewalls + + vars: + # variables will be overwrited by values in _main.yml + firewall_rules: + - ip: "all" + port: 22 + protocol: "tcp" + - ip: "all" + port: 21 + protocol: "tcp" + - ip: "all" + port: 8888 + protocol: "tcp" + - ip: "all" + port: 8080 + protocol: "tcp" + - ip: "all" + port: 8081 + protocol: "tcp" + - ip: "all" + port: 8082 + protocol: "tcp" + - ip: "all" + port: 4444 + protocol: "tcp" + +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- name: DOCKER FTP ANONYMOUS SERVER - exposed_port:21 + hosts: r42.vuln-box-02 + become: true + roles: + - software.configure.docker-compose + vars_files: + - "../../secrets/default_vault.yml" + vars: + LABEL_PROJECT_TYPE: "CTF" + LABEL_PROJET_NAME: "ftp_anon_server" + + # + LOCAL__PROJECT_DIR: "{{ lookup('env', 'RANGE42_INVENTORY__DOCKER__CTF') }}/misconfiguration/network/vsftpd/ftp_anon_server/" + REMOTE_PROJECT_DIR: "/tmp/deploy-{{ LABEL_PROJET_NAME }}" + # + + OPERATOR_USER: "{{ default_admin_vm_ci_user }}" + # + CLEAN_UP_DEPLOY_DIR: "NO" + SEND_POC_DIR: "NO" + +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- name: DEPLOY - apache-cve-2021-42013 - exposed_port:8888 + hosts: r42.vuln-box-02 + become: true + roles: + - software.configure.docker-compose + + vars_files: + - "../../secrets/default_vault.yml" + vars: + LABEL_PROJECT_TYPE: "CTF" + LABEL_PROJET_NAME: "apache-cve-2021-42013" + # + LOCAL__PROJECT_DIR: "{{ lookup('env', 'RANGE42_INVENTORY__DOCKER__CTF') }}/cve/web/apache/CVE-2021-42013/" + REMOTE_PROJECT_DIR: "/tmp/deploy-{{ LABEL_PROJET_NAME }}" + # + OPERATOR_USER: "{{ default_admin_vm_ci_user }}" + # + CLEAN_UP_DEPLOY_DIR: "NO" + SEND_POC_DIR: "YES" + +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- name: DEPLOY - pdfjs-cve-2024-4367 - exposed_port:4444 + hosts: r42.vuln-box-02 + become: true + roles: + - software.configure.docker-compose + + vars_files: + - "../../secrets/default_vault.yml" + vars: + LABEL_PROJECT_TYPE: "CTF" + LABEL_PROJET_NAME: "pdfjs-cve-2024-4367" + # + + LOCAL__PROJECT_DIR: "{{ lookup('env', 'RANGE42_INVENTORY__DOCKER__CTF') }}/cve/web/pdfjs/CVE-2024-4367" + REMOTE_PROJECT_DIR: "/tmp/deploy-{{ LABEL_PROJET_NAME }}" + # + OPERATOR_USER: "{{ default_admin_vm_ci_user }}" + # + CLEAN_UP_DEPLOY_DIR: "NO" + SEND_POC_DIR: "YES" +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- name: DEPLOY - tomcat-cve-2025-24813 - exposed_port:8081 + hosts: r42.vuln-box-02 + become: true + roles: + - software.configure.docker-compose + + vars_files: + - "../../secrets/default_vault.yml" + vars: + LABEL_PROJECT_TYPE: "CTF" + LABEL_PROJET_NAME: "tomcat-cve-2025-24813" + # + LOCAL__PROJECT_DIR: "{{ lookup('env', 'RANGE42_INVENTORY__DOCKER__CTF') }}/cve/web/tomcat/CVE-2025-24813/" + REMOTE_PROJECT_DIR: "/tmp/deploy-{{ LABEL_PROJET_NAME }}" + # + OPERATOR_USER: "{{ default_admin_vm_ci_user }}" + # + CLEAN_UP_DEPLOY_DIR: "NO" + SEND_POC_DIR: "YES" + +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- name: DOCKER - uwsgi-php-cve-2018-7490 - exposed_port:8080 + hosts: r42.vuln-box-02 + become: true + roles: + - software.configure.docker-compose + vars_files: + - "../../secrets/default_vault.yml" + vars: + LABEL_PROJECT_TYPE: "CTF" + LABEL_PROJET_NAME: "uwsgi-php-cve-2018-7490" + # + LOCAL__PROJECT_DIR: "{{ lookup('env', 'RANGE42_INVENTORY__DOCKER__CTF') }}/cve/web/uwsg_php/CVE-2018-7490/" + REMOTE_PROJECT_DIR: "/tmp/deploy-{{ LABEL_PROJET_NAME }}" + # + OPERATOR_USER: "{{ default_admin_vm_ci_user }}" + # + CLEAN_UP_DEPLOY_DIR: "NO" + SEND_POC_DIR: "NO" +# +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +# - name: DEPLOY - php-CVE-2019-11043 - 8082 +# hosts: r42.vuln-box-02 +# become: true +# roles: +# - software.configure.docker-compose + +# vars_files: +# - "../../secrets/default_vault.yml" +# vars: +# LABEL_PROJECT_TYPE: "CTF" +# LABEL_PROJET_NAME: "php-cve-2019-11043" +# # +# LOCAL__PROJECT_DIR: "{{ lookup('env', 'RANGE42_INVENTORY__DOCKER__CTF') }}/cve/web/php/CVE-2019-11043" +# REMOTE_PROJECT_DIR: "/tmp/deploy-{{ LABEL_PROJET_NAME }}" +# # +# OPERATOR_USER: "{{ default_admin_vm_ci_user }}" +# # +# CLEAN_UP_DEPLOY_DIR: "NO" +# SEND_POC_DIR: "YES" + +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### diff --git a/scenarios/demo_lab_network/04_ctf_infrastructure/stage_01/vuln_box_03.devkit/demo_lab_network.vuln_box_03.install.sh b/scenarios/demo_lab_network/04_ctf_infrastructure/stage_01/vuln_box_03.devkit/demo_lab_network.vuln_box_03.install.sh new file mode 100755 index 00000000..e9af8e78 --- /dev/null +++ b/scenarios/demo_lab_network/04_ctf_infrastructure/stage_01/vuln_box_03.devkit/demo_lab_network.vuln_box_03.install.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +## +## + +ansible-playbook -i "${RANGE42_ANSIBLE_ROLES__INVENTORY_DIR}/inventory_default.yml" \ + -l "all" \ + "../vuln_box_03.yml" --vault-password-file "${RANGE42_VAULT_PASSWORD_FILE:?RANGE42_VAULT_PASSWORD_FILE is not set — run: range42-context use }" diff --git a/scenarios/demo_lab_network/04_ctf_infrastructure/stage_01/vuln_box_03.devkit/demo_lab_network.vuln_box_03.revert.sh b/scenarios/demo_lab_network/04_ctf_infrastructure/stage_01/vuln_box_03.devkit/demo_lab_network.vuln_box_03.revert.sh new file mode 100755 index 00000000..1295a4bb --- /dev/null +++ b/scenarios/demo_lab_network/04_ctf_infrastructure/stage_01/vuln_box_03.devkit/demo_lab_network.vuln_box_03.revert.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +## +## + +proxmox_vm.list.to.jsons.sh | grep -i vuln-box-03 | proxmox_snapshot_vm.vm_id.revert_snapshot.to.jsons.sh +proxmox_vm.list.to.jsons.sh | grep -i vuln-box-03 | proxmox_vm.vm_id.start.to.jsons.sh diff --git a/scenarios/demo_lab_network/04_ctf_infrastructure/stage_01/vuln_box_03.devkit/demo_lab_network.vuln_box_03.snapshot.sh b/scenarios/demo_lab_network/04_ctf_infrastructure/stage_01/vuln_box_03.devkit/demo_lab_network.vuln_box_03.snapshot.sh new file mode 100755 index 00000000..f705d27b --- /dev/null +++ b/scenarios/demo_lab_network/04_ctf_infrastructure/stage_01/vuln_box_03.devkit/demo_lab_network.vuln_box_03.snapshot.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +## +## + +# proxmox_vm.list.to.jsons.sh | grep -i vuln-box-00 | proxmox_snapshot_vm.vm_id.create_snapshot.to.jsons.sh +echo '{"proxmox_node":"px-testing","vm_id":4003,"vm_snapshot_description":"base"}' | proxmox_snapshot_vm.vm_id.create_snapshot.to.jsons.sh diff --git a/scenarios/demo_lab_network/04_ctf_infrastructure/stage_01/vuln_box_03.yml b/scenarios/demo_lab_network/04_ctf_infrastructure/stage_01/vuln_box_03.yml new file mode 100644 index 00000000..379b0073 --- /dev/null +++ b/scenarios/demo_lab_network/04_ctf_infrastructure/stage_01/vuln_box_03.yml @@ -0,0 +1,178 @@ +## +## + +- name: configure firewall - all + become: true + hosts: r42.vuln-box-03 + roles: + - software.configure.firewalls + + vars: + # variables will be overwrited by values in _main.yml + firewall_rules: + - ip: "all" + port: 22 + protocol: "tcp" + - ip: "all" + port: 443 + protocol: "tcp" + +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- name: DOCKER FTP ANONYMOUS SERVER + hosts: r42.vuln-box-03 + become: true + roles: + - software.configure.docker-compose + vars_files: + - "../../secrets/default_vault.yml" + vars: + LABEL_PROJECT_TYPE: "CTF" + LABEL_PROJET_NAME: "lpe-01" + + # + LOCAL__PROJECT_DIR: "{{ lookup('env', 'RANGE42_INVENTORY__DOCKER__CTF') }}/misconfiguration/system/lpe-01/" + REMOTE_PROJECT_DIR: "/tmp/deploy-{{ LABEL_PROJET_NAME }}" + # + + OPERATOR_USER: "{{ default_admin_vm_ci_user }}" + # + CLEAN_UP_DEPLOY_DIR: "NO" + SEND_POC_DIR: "NO" + +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- name: DEPLOY - sudo-cve-2025-32463 exposed_port:1113 + hosts: r42.vuln-box-03 + become: true + roles: + - software.configure.docker-compose + + vars_files: + - "../../secrets/default_vault.yml" + vars: + LABEL_PROJECT_TYPE: "CTF" + LABEL_PROJET_NAME: "sudo-cve-2025-32463" + # + # LOCAL__PROJECT_DIR: "{{ lookup('env', 'RANGE42_INVENTORY__DOCKER__CTF') }}/cve/system/sudo/CVE-2025-32463" + LOCAL__PROJECT_DIR: "{{ lookup('env', 'RANGE42_INVENTORY__DOCKER__CTF') }}/cve/system/sudo/CVE-2025-32463" + REMOTE_PROJECT_DIR: "/tmp/deploy-{{ LABEL_PROJET_NAME }}" + # + OPERATOR_USER: "{{ default_admin_vm_ci_user }}" + # + CLEAN_UP_DEPLOY_DIR: "NO" + SEND_POC_DIR: "YES" + +### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- name: DEPLOY - sudo-cve-2025-32462 exposed_port:1112 + hosts: r42.vuln-box-03 + become: true + roles: + - software.configure.docker-compose + + vars_files: + - "../../secrets/default_vault.yml" + vars: + LABEL_PROJECT_TYPE: "CTF" + LABEL_PROJET_NAME: "sudo-cve-2025-32462" + # + + LOCAL__PROJECT_DIR: "{{ lookup('env', 'RANGE42_INVENTORY__DOCKER__CTF') }}/cve/system/sudo/CVE-2025-32462" + REMOTE_PROJECT_DIR: "/tmp/deploy-{{ LABEL_PROJET_NAME }}" + # + OPERATOR_USER: "{{ default_admin_vm_ci_user }}" + # + CLEAN_UP_DEPLOY_DIR: "NO" + SEND_POC_DIR: "YES" + +### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- name: DEPLOY - sudo-cve-2023-22809 exposed_port:1111 + hosts: r42.vuln-box-03 + become: true + roles: + - software.configure.docker-compose + + vars_files: + - "../../secrets/default_vault.yml" + vars: + LABEL_PROJECT_TYPE: "CTF" + LABEL_PROJET_NAME: "sudo-cve-2023-22809" + # + + LOCAL__PROJECT_DIR: "{{ lookup('env', 'RANGE42_INVENTORY__DOCKER__CTF') }}/cve/system/sudo/CVE-2023-22809" + REMOTE_PROJECT_DIR: "/tmp/deploy-{{ LABEL_PROJET_NAME }}" + # + OPERATOR_USER: "{{ default_admin_vm_ci_user }}" + # + CLEAN_UP_DEPLOY_DIR: "NO" + SEND_POC_DIR: "YES" + +### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- name: DEPLOY - vite-cve-2025-30208 - exposed_port:7001 + hosts: r42.vuln-box-03 + become: true + roles: + - software.configure.docker-compose + + vars_files: + - "../../secrets/default_vault.yml" + vars: + LABEL_PROJECT_TYPE: "CTF" + LABEL_PROJET_NAME: "vite-cve-2025-30208" + # + + LOCAL__PROJECT_DIR: "{{ lookup('env', 'RANGE42_INVENTORY__DOCKER__CTF') }}/cve/web/vite/CVE-2025-30208" + REMOTE_PROJECT_DIR: "/tmp/deploy-{{ LABEL_PROJET_NAME }}" + # + OPERATOR_USER: "{{ default_admin_vm_ci_user }}" + # + CLEAN_UP_DEPLOY_DIR: "NO" + SEND_POC_DIR: "YES" + +### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- name: DEPLOY - vite-cve-2022-44615 - exposed_port:7000 + hosts: r42.vuln-box-03 + become: true + roles: + - software.configure.docker-compose + + vars_files: + - "../../secrets/default_vault.yml" + vars: + LABEL_PROJECT_TYPE: "CTF" + LABEL_PROJET_NAME: "vite-cve-2022-44615" + # + + LOCAL__PROJECT_DIR: "{{ lookup('env', 'RANGE42_INVENTORY__DOCKER__CTF') }}/cve/web/vite/CVE-2022-44615" + REMOTE_PROJECT_DIR: "/tmp/deploy-{{ LABEL_PROJET_NAME }}" + # + OPERATOR_USER: "{{ default_admin_vm_ci_user }}" + # + CLEAN_UP_DEPLOY_DIR: "NO" + SEND_POC_DIR: "YES" + +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- name: DEPLOY - php-CVE-2019-11043 - 8082 + hosts: r42.vuln-box-03 + become: true + roles: + - software.configure.docker-compose + + vars_files: + - "../../secrets/default_vault.yml" + vars: + LABEL_PROJECT_TYPE: "CTF" + LABEL_PROJET_NAME: "php-cve-2019-11043" + # + LOCAL__PROJECT_DIR: "{{ lookup('env', 'RANGE42_INVENTORY__DOCKER__CTF') }}/cve/web/php/CVE-2019-11043" + REMOTE_PROJECT_DIR: "/tmp/deploy-{{ LABEL_PROJET_NAME }}" + # + OPERATOR_USER: "{{ default_admin_vm_ci_user }}" + # + CLEAN_UP_DEPLOY_DIR: "NO" + SEND_POC_DIR: "YES" diff --git a/scenarios/demo_lab_network/04_ctf_infrastructure/stage_01/vuln_box_04.devkit/demo_lab_network.vuln_box_04.install.sh b/scenarios/demo_lab_network/04_ctf_infrastructure/stage_01/vuln_box_04.devkit/demo_lab_network.vuln_box_04.install.sh new file mode 100755 index 00000000..c52d2133 --- /dev/null +++ b/scenarios/demo_lab_network/04_ctf_infrastructure/stage_01/vuln_box_04.devkit/demo_lab_network.vuln_box_04.install.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +## +## + +ansible-playbook -i "${RANGE42_ANSIBLE_ROLES__INVENTORY_DIR}/inventory_default.yml" \ + -l "all" \ + "../vuln_box_04.yml" --vault-password-file "${RANGE42_VAULT_PASSWORD_FILE:?RANGE42_VAULT_PASSWORD_FILE is not set — run: range42-context use }" diff --git a/scenarios/demo_lab_network/04_ctf_infrastructure/stage_01/vuln_box_04.devkit/demo_lab_network.vuln_box_04.revert.sh b/scenarios/demo_lab_network/04_ctf_infrastructure/stage_01/vuln_box_04.devkit/demo_lab_network.vuln_box_04.revert.sh new file mode 100755 index 00000000..cb7ce4b0 --- /dev/null +++ b/scenarios/demo_lab_network/04_ctf_infrastructure/stage_01/vuln_box_04.devkit/demo_lab_network.vuln_box_04.revert.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +## +## + +proxmox_vm.list.to.jsons.sh | grep -i vuln-box-04 | proxmox_snapshot_vm.vm_id.revert_snapshot.to.jsons.sh +proxmox_vm.list.to.jsons.sh | grep -i vuln-box-04 | proxmox_vm.vm_id.start.to.jsons.sh diff --git a/scenarios/demo_lab_network/04_ctf_infrastructure/stage_01/vuln_box_04.devkit/demo_lab_network.vuln_box_04.snapshot.sh b/scenarios/demo_lab_network/04_ctf_infrastructure/stage_01/vuln_box_04.devkit/demo_lab_network.vuln_box_04.snapshot.sh new file mode 100755 index 00000000..17f6510e --- /dev/null +++ b/scenarios/demo_lab_network/04_ctf_infrastructure/stage_01/vuln_box_04.devkit/demo_lab_network.vuln_box_04.snapshot.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +## +## + +# proxmox_vm.list.to.jsons.sh | grep -i vuln-box-00 | proxmox_snapshot_vm.vm_id.create_snapshot.to.jsons.sh +echo '{"proxmox_node":"px-testing","vm_id":4004,"vm_snapshot_description":"base"}' | proxmox_snapshot_vm.vm_id.create_snapshot.to.jsons.sh diff --git a/scenarios/demo_lab_network/04_ctf_infrastructure/stage_01/vuln_box_04.yml b/scenarios/demo_lab_network/04_ctf_infrastructure/stage_01/vuln_box_04.yml new file mode 100644 index 00000000..601b5c3d --- /dev/null +++ b/scenarios/demo_lab_network/04_ctf_infrastructure/stage_01/vuln_box_04.yml @@ -0,0 +1,361 @@ +## +## + +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +# - name: configure firewall - all +# become: true +# hosts: r42.vuln-box-04 +# roles: +# - software.configure.firewalls + +# vars: +# # variables will be overwrited by values in _main.yml +# firewall_rules: +# - ip: "all" +# port: 22 +# protocol: "tcp" +# - ip: "all" +# port: 443 +# protocol: "tcp" +# - ip: "all" +# port: 8082 # php cve-2019-11043 +# protocol: "tcp" +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- name: DEPLOY - cve/web/php/CVE-2019-11043/ + hosts: r42.vuln-box-04 + become: true + roles: + - software.configure.docker-compose + + vars_files: + - "../../secrets/default_vault.yml" + vars: + LABEL_PROJECT_TYPE: "CTF" + LABEL_PROJET_NAME: "php-cve-2019-11043" + # + LOCAL__PROJECT_DIR: "{{ lookup('env', 'RANGE42_INVENTORY__DOCKER__CTF') }}/cve/web/php/CVE-2019-11043/" + # REMOTE_PROJECT_DIR: "/tmp/deploy" + REMOTE_PROJECT_DIR: "/tmp/deploy-{{ LABEL_PROJET_NAME }}" + # + OPERATOR_USER: "{{ default_admin_vm_ci_user }}" + # + CLEAN_UP_DEPLOY_DIR: "NO" + SEND_POC_DIR: "YES" +# +# +# +# - name: DEPLOY - cve/crypto/openssl/CVE-2014-0160/" +# hosts: r42.vuln-box-04 +# become: true +# roles: +# - software.configure.docker-compose + +# vars_files: +# - "../../secrets/default_vault.yml" +# vars: +# LABEL_PROJECT_TYPE: "CTF" +# LABEL_PROJET_NAME: "crypto-openssl-cve-2014-0160" +# # +# LOCAL__PROJECT_DIR: "{{ lookup('env', 'RANGE42_INVENTORY__DOCKER__CTF') }}/cve/crypto/openssl/CVE-2014-0160/" +# # REMOTE_PROJECT_DIR: "/tmp/deploy" +# REMOTE_PROJECT_DIR: "/tmp/deploy-{{ LABEL_PROJET_NAME }}" +# # +# OPERATOR_USER: "{{ default_admin_vm_ci_user }}" +# # +# CLEAN_UP_DEPLOY_DIR: "NO" +# SEND_POC_DIR: "YES" + +# ### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +# - name: DEPLOY - crypto-openssl-cve-2022-0778 +# hosts: r42.vuln-box-04 +# become: true +# roles: +# - software.configure.docker-compose + +# vars_files: +# - "../../secrets/default_vault.yml" +# vars: +# LABEL_PROJECT_TYPE: "CTF" +# LABEL_PROJET_NAME: "crypto-openssl-cve-2022-0778" +# # +# LOCAL__PROJECT_DIR: "{{ lookup('env', 'RANGE42_INVENTORY__DOCKER__CTF') }}/cve/crypto/openssl/CVE-2022-0778/" +# REMOTE_PROJECT_DIR: "/tmp/deploy-{{ LABEL_PROJET_NAME }}" +# # +# OPERATOR_USER: "{{ default_admin_vm_ci_user }}" +# # +# CLEAN_UP_DEPLOY_DIR: "NO" +# SEND_POC_DIR: "YES" + +# ### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +# - name: DEPLOY - tomcat-cve-2025-24813 +# hosts: r42.vuln-box-04 +# become: true +# roles: +# - software.configure.docker-compose + +# vars_files: +# - "../../secrets/default_vault.yml" +# vars: +# LABEL_PROJECT_TYPE: "CTF" +# LABEL_PROJET_NAME: "tomcat-cve-2025-24813" +# # +# LOCAL__PROJECT_DIR: "{{ lookup('env', 'RANGE42_INVENTORY__DOCKER__CTF') }}/cve/web/tomcat/CVE-2025-24813/" +# REMOTE_PROJECT_DIR: "/tmp/deploy-{{ LABEL_PROJET_NAME }}" +# # +# OPERATOR_USER: "{{ default_admin_vm_ci_user }}" +# # +# CLEAN_UP_DEPLOY_DIR: "NO" +# SEND_POC_DIR: "YES" + +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +# - name: DEPLOY - apache-cve-2021-42013 +# hosts: r42.vuln-box-04 +# become: true +# roles: +# - software.configure.docker-compose + +# vars_files: +# - "../../secrets/default_vault.yml" +# vars: +# LABEL_PROJECT_TYPE: "CTF" +# LABEL_PROJET_NAME: "apache-cve-2021-42013" +# # +# LOCAL__PROJECT_DIR: "{{ lookup('env', 'RANGE42_INVENTORY__DOCKER__CTF') }}/cve/web/apache/CVE-2021-42013/" +# REMOTE_PROJECT_DIR: "/tmp/deploy-{{ LABEL_PROJET_NAME }}" +# # +# OPERATOR_USER: "{{ default_admin_vm_ci_user }}" +# # +# CLEAN_UP_DEPLOY_DIR: "NO" +# SEND_POC_DIR: "YES" + +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +# - name: DEPLOY - openssh-cve-2018-15473 +# hosts: r42.vuln-box-04 +# become: true +# roles: +# - software.configure.docker-compose + +# vars_files: +# - "../../secrets/default_vault.yml" +# vars: +# LABEL_PROJECT_TYPE: "CTF" +# LABEL_PROJET_NAME: "openssh-cve-2018-15473" +# # +# LOCAL__PROJECT_DIR: "{{ lookup('env', 'RANGE42_INVENTORY__DOCKER__CTF') }}/cve/network/openssh/CVE-2018-15473" +# REMOTE_PROJECT_DIR: "/tmp/deploy-{{ LABEL_PROJET_NAME }}" +# # +# OPERATOR_USER: "{{ default_admin_vm_ci_user }}" +# # +# CLEAN_UP_DEPLOY_DIR: "NO" +# SEND_POC_DIR: "YES" + +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +# - name: DEPLOY - sudo-cve-2025-32463 +# hosts: r42.vuln-box-04 +# become: true +# roles: +# - software.configure.docker-compose + +# vars_files: +# - "../../secrets/default_vault.yml" +# vars: +# LABEL_PROJECT_TYPE: "CTF" +# LABEL_PROJET_NAME: "sudo-cve-2025-32463" +# # +# # LOCAL__PROJECT_DIR: "{{ lookup('env', 'RANGE42_INVENTORY__DOCKER__CTF') }}/cve/system/sudo/CVE-2025-32463" +# LOCAL__PROJECT_DIR: "{{ lookup('env', 'RANGE42_INVENTORY__DOCKER__CTF') }}/cve/system/sudo/CVE-2025-32463" +# REMOTE_PROJECT_DIR: "/tmp/deploy-{{ LABEL_PROJET_NAME }}" +# # +# OPERATOR_USER: "{{ default_admin_vm_ci_user }}" +# # +# CLEAN_UP_DEPLOY_DIR: "NO" +# SEND_POC_DIR: "YES" + +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +# - name: DEPLOY - sudo-cve-2025-32462 +# hosts: r42.vuln-box-04 +# become: true +# roles: +# - software.configure.docker-compose + +# vars_files: +# - "../../secrets/default_vault.yml" +# vars: +# LABEL_PROJECT_TYPE: "CTF" +# LABEL_PROJET_NAME: "sudo-cve-2025-32462" +# # + +# LOCAL__PROJECT_DIR: "{{ lookup('env', 'RANGE42_INVENTORY__DOCKER__CTF') }}/cve/system/sudo/CVE-2025-32462" +# REMOTE_PROJECT_DIR: "/tmp/deploy-{{ LABEL_PROJET_NAME }}" +# # +# OPERATOR_USER: "{{ default_admin_vm_ci_user }}" +# # +# CLEAN_UP_DEPLOY_DIR: "NO" +# SEND_POC_DIR: "YES" +# +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +# - name: DEPLOY - sudo-cve-2023-22809 +# hosts: r42.vuln-box-04 +# become: true +# roles: +# - software.configure.docker-compose + +# vars_files: +# - "../../secrets/default_vault.yml" +# vars: +# LABEL_PROJECT_TYPE: "CTF" +# LABEL_PROJET_NAME: "sudo-cve-2023-22809" +# # + +# LOCAL__PROJECT_DIR: "{{ lookup('env', 'RANGE42_INVENTORY__DOCKER__CTF') }}/cve/system/sudo/CVE-2023-22809" +# REMOTE_PROJECT_DIR: "/tmp/deploy-{{ LABEL_PROJET_NAME }}" +# # +# OPERATOR_USER: "{{ default_admin_vm_ci_user }}" +# # +# CLEAN_UP_DEPLOY_DIR: "NO" +# SEND_POC_DIR: "YES" +# +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +# - name: DEPLOY - pdfjs-cve-2024-4367 +# hosts: r42.vuln-box-04 +# become: true +# roles: +# - software.configure.docker-compose + +# vars_files: +# - "../../secrets/default_vault.yml" +# vars: +# LABEL_PROJECT_TYPE: "CTF" +# LABEL_PROJET_NAME: "pdfjs-cve-2024-4367" +# # + +# LOCAL__PROJECT_DIR: "{{ lookup('env', 'RANGE42_INVENTORY__DOCKER__CTF') }}/cve/web/pdfjs/CVE-2024-4367" +# REMOTE_PROJECT_DIR: "/tmp/deploy-{{ LABEL_PROJET_NAME }}" +# # +# OPERATOR_USER: "{{ default_admin_vm_ci_user }}" +# # +# CLEAN_UP_DEPLOY_DIR: "NO" +# SEND_POC_DIR: "YES" +# +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +# - name: DEPLOY - vite-cve-2025-30208 +# hosts: r42.vuln-box-04 +# become: true +# roles: +# - software.configure.docker-compose + +# vars_files: +# - "../../secrets/default_vault.yml" +# vars: +# LABEL_PROJECT_TYPE: "CTF" +# LABEL_PROJET_NAME: "vite-cve-2025-30208" +# # + +# LOCAL__PROJECT_DIR: "{{ lookup('env', 'RANGE42_INVENTORY__DOCKER__CTF') }}/cve/web/vite/CVE-2025-30208" +# REMOTE_PROJECT_DIR: "/tmp/deploy-{{ LABEL_PROJET_NAME }}" +# # +# OPERATOR_USER: "{{ default_admin_vm_ci_user }}" +# # +# CLEAN_UP_DEPLOY_DIR: "NO" +# SEND_POC_DIR: "YES" +# # +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +# - name: DEPLOY - vite-cve-2022-44615 +# hosts: r42.vuln-box-04 +# become: true +# roles: +# - software.configure.docker-compose + +# vars_files: +# - "../../secrets/default_vault.yml" +# vars: +# LABEL_PROJECT_TYPE: "CTF" +# LABEL_PROJET_NAME: "vite-cve-2022-44615" +# # + +# LOCAL__PROJECT_DIR: "{{ lookup('env', 'RANGE42_INVENTORY__DOCKER__CTF') }}/cve/web/vite/CVE-2022-44615" +# REMOTE_PROJECT_DIR: "/tmp/deploy-{{ LABEL_PROJET_NAME }}" +# # +# OPERATOR_USER: "{{ default_admin_vm_ci_user }}" +# # +# CLEAN_UP_DEPLOY_DIR: "NO" +# SEND_POC_DIR: "YES" +# # +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +# - name: DEPLOY - erlang-ssh-CVE-2025-32433 +# hosts: r42.vuln-box-04 +# become: true +# roles: +# - software.configure.docker-compose + +# vars_files: +# - "../../secrets/default_vault.yml" +# vars: +# LABEL_PROJECT_TYPE: "CTF" +# LABEL_PROJET_NAME: "erlang-ssh-CVE-2025-32433" +# # + +# LOCAL__PROJECT_DIR: "{{ lookup('env', 'RANGE42_INVENTORY__DOCKER__CTF') }}/cve/network/erlang-ssh/CVE-2025-32433" +# REMOTE_PROJECT_DIR: "/tmp/deploy-{{ LABEL_PROJET_NAME }}" +# # +# OPERATOR_USER: "{{ default_admin_vm_ci_user }}" +# # +# CLEAN_UP_DEPLOY_DIR: "NO" +# SEND_POC_DIR: "YES" + +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +# - name: DEPLOY - openssh-cve-2024-6387 +# hosts: r42.vuln-box-04 +# become: true +# roles: +# - software.configure.docker-compose + +# vars_files: +# - "../../secrets/default_vault.yml" +# vars: +# LABEL_PROJECT_TYPE: "CTF" +# LABEL_PROJET_NAME: "openssh-cve-2024-6387" +# # +# LOCAL__PROJECT_DIR: "{{ lookup('env', 'RANGE42_INVENTORY__DOCKER__CTF') }}/cve/network/openssh/CVE-2024-6387" +# REMOTE_PROJECT_DIR: "/tmp/deploy-{{ LABEL_PROJET_NAME }}" +# # +# OPERATOR_USER: "{{ default_admin_vm_ci_user }}" +# # +# CLEAN_UP_DEPLOY_DIR: "NO" +# SEND_POC_DIR: "YES" + +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +# - name: DEPLOY - php-CVE-2019-11043 +# hosts: r42.vuln-box-04 +# become: true +# roles: +# - software.configure.docker-compose + +# vars_files: +# - "../../secrets/default_vault.yml" +# vars: +# LABEL_PROJECT_TYPE: "CTF" +# LABEL_PROJET_NAME: "php-cve-2019-11043" +# # +# LOCAL__PROJECT_DIR: "{{ lookup('env', 'RANGE42_INVENTORY__DOCKER__CTF') }}/cve/web/php/CVE-2019-11043" +# REMOTE_PROJECT_DIR: "/tmp/deploy-{{ LABEL_PROJET_NAME }}" +# # +# OPERATOR_USER: "{{ default_admin_vm_ci_user }}" +# # +# CLEAN_UP_DEPLOY_DIR: "NO" +# SEND_POC_DIR: "YES" diff --git a/scenarios/demo_lab_network/05_network_isolation/_main.yml b/scenarios/demo_lab_network/05_network_isolation/_main.yml new file mode 100644 index 00000000..1e88423a --- /dev/null +++ b/scenarios/demo_lab_network/05_network_isolation/_main.yml @@ -0,0 +1,8 @@ +## +## demo_lab_network — 05_network_isolation +## +## Apply FORWARD chain iptables rules on the Proxmox host to enforce +## zone isolation between the admin (vmbr142) and CTF (vmbr144) bridges. +## + +- import_playbook: ./stage_00/proxmox_forward_rules.yml diff --git a/scenarios/demo_lab_network/05_network_isolation/stage_00/proxmox_forward_rules.yml b/scenarios/demo_lab_network/05_network_isolation/stage_00/proxmox_forward_rules.yml new file mode 100644 index 00000000..2d1e5ad9 --- /dev/null +++ b/scenarios/demo_lab_network/05_network_isolation/stage_00/proxmox_forward_rules.yml @@ -0,0 +1,113 @@ +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# +# NETWORK ISOLATION — PROXMOX HOST FORWARD RULES +# +# Enforces zone isolation between the admin and CTF bridges by installing +# iptables FORWARD chain rules directly on the Proxmox host via SSH (proxmox-cli). +# +# Rule table (evaluated in order): +# +# ESTABLISHED,RELATED → FORWARD ACCEPT (return traffic for existing flows) +# admin (vmbr142) → ctf ACCEPT (admin manages vuln boxes) +# ctf (vmbr144) → wazuh :1514 ACCEPT (agent event reporting) +# ctf (vmbr144) → wazuh :1515 ACCEPT (agent enrollment) +# ctf (vmbr144) → admin DROP (zone isolation) +# ctf (vmbr144) → WAN DROP (air-gap — no internet from vuln boxes during lab) +# +# Rules are persisted via iptables-persistent / netfilter-persistent. +# Re-running this playbook is idempotent (ansible.builtin.iptables checks +# before inserting). +# +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- hosts: proxmox-cli + gather_facts: false + vars: + ansible_python_interpreter: /usr/bin/python3 + vars_files: + - "../../secrets/default_vault.yml" + + vars: + admin_subnet: "192.168.142.0/24" + ctf_subnet: "192.168.144.0/24" + wazuh_ip: "192.168.142.100" + ctf_bridge: "vmbr144" + wan_interface: "{{ infrastructure_proxmox_default_network_card_interface | default('vmbr0') }}" + + handlers: + - name: persist iptables rules + ansible.builtin.command: netfilter-persistent save + changed_when: true + + tasks: + + #### #### PREREQ #### + + - name: NETWORK ISOLATION - Install iptables-persistent + ansible.builtin.apt: + name: iptables-persistent + state: present + update_cache: false + + #### #### FORWARD ACCEPT rules (must precede DROP rules) #### + + - name: NETWORK ISOLATION - Allow established/related connections (FORWARD) + ansible.builtin.iptables: + chain: FORWARD + ctstate: ESTABLISHED,RELATED + jump: ACCEPT + action: insert + rule_num: 1 + comment: "r42: allow established" + notify: persist iptables rules + + - name: NETWORK ISOLATION - Allow Admin (vmbr142) → CTF (vmbr144) + ansible.builtin.iptables: + chain: FORWARD + source: "{{ admin_subnet }}" + destination: "{{ ctf_subnet }}" + jump: ACCEPT + comment: "r42: admin to ctf" + notify: persist iptables rules + + - name: NETWORK ISOLATION - Allow CTF → Wazuh TCP 1514 (agent events) + ansible.builtin.iptables: + chain: FORWARD + source: "{{ ctf_subnet }}" + destination: "{{ wazuh_ip }}" + protocol: tcp + destination_port: "1514" + jump: ACCEPT + comment: "r42: ctf wazuh events" + notify: persist iptables rules + + - name: NETWORK ISOLATION - Allow CTF → Wazuh TCP 1515 (agent enrollment) + ansible.builtin.iptables: + chain: FORWARD + source: "{{ ctf_subnet }}" + destination: "{{ wazuh_ip }}" + protocol: tcp + destination_port: "1515" + jump: ACCEPT + comment: "r42: ctf wazuh enrollment" + notify: persist iptables rules + + #### #### FORWARD DROP rules #### + + - name: NETWORK ISOLATION - Drop CTF (vmbr144) → Admin (vmbr142) — zone isolation + ansible.builtin.iptables: + chain: FORWARD + source: "{{ ctf_subnet }}" + destination: "{{ admin_subnet }}" + jump: DROP + comment: "r42: block ctf to admin" + notify: persist iptables rules + + - name: NETWORK ISOLATION - Drop CTF (vmbr144) → WAN — air-gap + ansible.builtin.iptables: + chain: FORWARD + in_interface: "{{ ctf_bridge }}" + out_interface: "{{ wan_interface }}" + jump: DROP + comment: "r42: ctf air-gap" + notify: persist iptables rules diff --git a/scenarios/demo_lab_network/05_network_isolation/stage_pre/lift_ctf_airgap.yml b/scenarios/demo_lab_network/05_network_isolation/stage_pre/lift_ctf_airgap.yml new file mode 100644 index 00000000..24cdd53a --- /dev/null +++ b/scenarios/demo_lab_network/05_network_isolation/stage_pre/lift_ctf_airgap.yml @@ -0,0 +1,65 @@ +## +## demo_lab_network — lift CTF air-gap before setup +## +## Removes the CTF (vmbr144) → WAN FORWARD blocking rule so CTF VMs can +## reach the internet during cloud-init and package installation. +## +## Called at the start of the CTF infrastructure deployment. +## The full air-gap is restored afterwards by 05_network_isolation/_main.yml. +## +## Idempotent: uses state=absent — no-op if the rule is not present. +## + +- hosts: proxmox-cli + gather_facts: false + vars: + ansible_python_interpreter: /usr/bin/python3 + vars_files: + - "../../secrets/default_vault.yml" + + vars: + ctf_bridge: "vmbr144" + wan_interface: "{{ infrastructure_proxmox_default_network_card_interface | default('vmbr0') }}" + + tasks: + + - name: NETWORK ISOLATION - Lift CTF air-gap (REJECT variant) + ansible.builtin.iptables: + chain: FORWARD + in_interface: "{{ ctf_bridge }}" + out_interface: "{{ wan_interface }}" + jump: REJECT + reject_with: icmp-net-unreachable + comment: "r42: ctf air-gap" + state: absent + ignore_errors: true + + - name: NETWORK ISOLATION - Lift CTF air-gap (DROP variant — legacy) + ansible.builtin.iptables: + chain: FORWARD + in_interface: "{{ ctf_bridge }}" + out_interface: "{{ wan_interface }}" + jump: DROP + comment: "r42: ctf air-gap" + state: absent + ignore_errors: true + + - name: NETWORK ISOLATION - Lift CTF air-gap (DROP variant — second pass, handles duplicates) + ansible.builtin.iptables: + chain: FORWARD + in_interface: "{{ ctf_bridge }}" + out_interface: "{{ wan_interface }}" + jump: DROP + comment: "r42: ctf air-gap" + state: absent + ignore_errors: true + + - name: NETWORK ISOLATION - Lift CTF air-gap (fallback — flush any remaining DROP on ctf→wan by interface) + ansible.builtin.shell: | + while iptables -D FORWARD -i {{ ctf_bridge }} -o {{ wan_interface }} -j DROP 2>/dev/null; do :; done + exit 0 + changed_when: false + + - name: NETWORK ISOLATION - Persist (air-gap lifted) + ansible.builtin.command: netfilter-persistent save + changed_when: true diff --git a/scenarios/demo_lab_network/README.md b/scenarios/demo_lab_network/README.md new file mode 100644 index 00000000..3e3d6149 --- /dev/null +++ b/scenarios/demo_lab_network/README.md @@ -0,0 +1,105 @@ +# demo_lab_network + +`demo_lab` + inter-bridge iptables isolation. Adds FORWARD chain firewall rules on the Proxmox host to enforce zone separation between the admin and CTF subnets. + +> Closes #20 + +## What's new vs demo_lab + +| | demo_lab | demo_lab_network | +|---|---|---| +| VM layout | identical | identical | +| Admin → CTF | allowed (default forward) | allowed (explicit ACCEPT) | +| CTF → Admin | allowed (default forward) | **DROPPED** | +| CTF → Internet | allowed (default forward) | **DROPPED** (air-gap) | +| CTF → Wazuh :1514/:1515 | allowed | **allowed** (explicit ACCEPT) | +| Proxmox iptables rules | none | `05_network_isolation` Ansible step | + +## Network architecture + +``` + ┌─────────────────────────────────────────────────┐ + │ Proxmox Host (pve01) │ + │ ip_forward=1 • iptables FORWARD │ + └───────────────────┬─────────────────┬────────────┘ + │ │ + ┌──────────┘ └──────────┐ + │ vmbr142 vmbr144 │ + ┌────────▼──────────────┐ ┌────────▼──────────────┐ + │ Admin / Deployment │ │ CTF / Vuln (air-gap) │ + │ 192.168.142.0/24 │ │ 192.168.144.0/24 │ + │ │ │ │ + │ admin-wazuh .100 │ │ vuln-box-00 .170 │ + │ api-gateway .120 │ │ vuln-box-01 .171 │ + │ api-backend .121 │ │ vuln-box-02 .172 │ + │ deployer-ui .123 │ │ vuln-box-03 .173 │ + └───────────────────────┘ │ vuln-box-04 .174 │ + └───────────────────────┘ +``` + +### iptables FORWARD rules (applied on Proxmox host) + +| From | To | Port | Action | Reason | +|------|----|------|--------|--------| +| any | any | — | ACCEPT (ESTABLISHED,RELATED) | return traffic | +| Admin (.142/24) | CTF (.144/24) | ALL | ACCEPT | admin manages vuln boxes | +| CTF (.144/24) | Wazuh (.142.100) | 1514 TCP | ACCEPT | Wazuh agent events | +| CTF (.144/24) | Wazuh (.142.100) | 1515 TCP | ACCEPT | Wazuh agent enrollment | +| CTF (.144/24) | Admin (.142/24) | ALL | **DROP** | zone isolation | +| CTF (vmbr144) | WAN (vmbr0) | ALL | **DROP** | air-gap | + +Rules are idempotent (`ansible.builtin.iptables` checks before inserting) and persist across reboots via `iptables-persistent`. + +## Deployed VMs + +### 02_admin_infrastructure (vmbr142) + +| VM | VM ID | IP | +|----|-------|----| +| admin-wazuh | 1000 | 192.168.142.100 | +| admin-deployer-api-gateway | 1020 | 192.168.142.120 | +| admin-deployer-api-backend | 1021 | 192.168.142.121 | +| admin-deployer-ui | 1023 | 192.168.142.123 | + +### 04_ctf_infrastructure (vmbr144) + +| VM | VM ID | IP | +|----|-------|----| +| vuln-box-00 | 4000 | 192.168.144.170 | +| vuln-box-01 | 4001 | 192.168.144.171 | +| vuln-box-02 | 4002 | 192.168.144.172 | +| vuln-box-03 | 4003 | 192.168.144.173 | +| vuln-box-04 | 4004 | 192.168.144.174 | + +## Stages + +| Stage | What it does | +|-------|-------------| +| `01_init_proxmox` | Download cloud-init images, create Ubuntu Noble VM templates | +| `02_admin_infrastructure` | Create + configure admin VMs on vmbr142 | +| `04_ctf_infrastructure` | Create + configure CTF/vuln VMs on vmbr144 | +| `05_network_isolation` | Install `iptables-persistent` on pve01, apply FORWARD rules | + +## Scripts + +| Script | What it does | +|--------|-------------| +| `demo_lab_network.setup.sh` | Full deploy (templates + VMs + network isolation) | +| `demo_lab_network.setup_vms_only.sh` | Fast redeploy (VMs only, skip templates) | +| `demo_lab_network.delete_vms_only.sh` | Destroy VMs, keep templates | +| `demo_lab_network.delete_all.sh` | Destroy everything + clean SSH known_hosts | +| `demo_lab_network.reset.setup.sh` | Delete all + redeploy from scratch | +| `demo_lab_network.reset.ssh_keys.sh` | Reset SSH known_hosts entries only | + +## Notes + +- **VM IDs are the same as demo_lab** — do not run both scenarios on the same Proxmox simultaneously. +- The `05_network_isolation` step runs on the `proxmox` host (not on VMs). Requires SSH root access to pve01. +- If your Proxmox WAN interface is not `vmbr0`, override with `-e wan_interface=` or set `infrastructure_proxmox_default_network_card_interface` in group_vars. +- iptables rules are NOT removed by the delete scripts. Use `iptables -D FORWARD ...` manually if needed. + +## Design notes + +Two-zone design (admin/ctf) was chosen over a three-zone design with a dedicated management bridge because vmbr142/vmbr144 already pre-exist in the `proxmox.init` bridge configuration and Wazuh agent traffic is adequately handled via iptables pinholes on ports 1514/1515. + +A three-zone design (with a dedicated `vmbr145` management bridge and dual NICs on vuln boxes) remains a valid future enhancement — it would eliminate the Wazuh iptables pinholes entirely and provide stronger monitoring isolation. diff --git a/scenarios/demo_lab_network/demo_lab_network.delete_all.sh b/scenarios/demo_lab_network/demo_lab_network.delete_all.sh new file mode 100755 index 00000000..a58b5dec --- /dev/null +++ b/scenarios/demo_lab_network/demo_lab_network.delete_all.sh @@ -0,0 +1,55 @@ +#!/bin/bash + +## +## delete all — VMs + ubuntu_noble templates of THIS scenario +## +## VM IDs and IPs are read from the scenario manifest: +## manifest/scenario_vms.json +## +## WARNING: Templates (9xxx) are deleted by this script. They may be shared with +## other scenarios on the same Proxmox (demo_lab / blank_scenario_* all use the +## same template IDs). Run this only when no other scenario relies on them, or +## run delete_vms_only.sh to keep templates. +## +## NOTE: This script does NOT remove the iptables FORWARD rules applied to the +## Proxmox host by 05_network_isolation. Clean those up manually if needed. +## + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +MANIFEST="$SCRIPT_DIR/manifest/scenario_vms.json" + +if [[ ! -f "$MANIFEST" ]]; then + echo "ERROR: manifest not found: $MANIFEST" >&2 + exit 1 +fi + +mapfile -t SCENARIO_VM_IDS < <(jq -r '.vms[].vm_id' "$MANIFEST") +mapfile -t TEMPLATE_VM_IDS < <(jq -r '.templates[].vm_id' "$MANIFEST") +mapfile -t INFRASTRUCTURE_IP < <(jq -r '.vms[].ip' "$MANIFEST") + +ALL_IDS=("${SCENARIO_VM_IDS[@]}" "${TEMPLATE_VM_IDS[@]}") +ID_REGEX=$(printf '|%s' "${ALL_IDS[@]}" | sed 's/^|//') + +echo ":: stopping and deleting VMs + templates" +echo ":: scenario VMs: ${SCENARIO_VM_IDS[*]}" +echo ":: templates : ${TEMPLATE_VM_IDS[*]}" +echo "" + +VM_LIST_JSON=$(proxmox_vm.list.to.jsons.sh 2>&1) +if ! echo "$VM_LIST_JSON" | jq -e . >/dev/null 2>&1; then + echo "ERROR: proxmox_vm.list.to.jsons.sh returned invalid JSON — aborting" >&2 + printf "output: %.200s\n" "$VM_LIST_JSON" >&2 + exit 1 +fi +echo "$VM_LIST_JSON" | jq -c | grep -E "\"vm_id\":($ID_REGEX)([^0-9]|\$)" | proxmox_vm.vm_id.stop_force.to.jsons.sh +echo "$VM_LIST_JSON" | jq -c | grep -E "\"vm_id\":($ID_REGEX)([^0-9]|\$)" | proxmox_vm.vm_id.delete.to.jsons.sh + +for ip in "${INFRASTRUCTURE_IP[@]}"; do + echo ":: REMOVE SSH KEY FOR : $ip" + ssh-keygen -f "$HOME/.ssh/known_hosts" -R "$ip" +done + +echo "" +echo ":: done — VMs and templates removed" +echo ":: redeploy from scratch with: range42-context deploy" +echo "" diff --git a/scenarios/demo_lab_network/demo_lab_network.delete_vms_only.sh b/scenarios/demo_lab_network/demo_lab_network.delete_vms_only.sh new file mode 100755 index 00000000..32c5aa00 --- /dev/null +++ b/scenarios/demo_lab_network/demo_lab_network.delete_vms_only.sh @@ -0,0 +1,42 @@ +#!/bin/bash + +## +## delete VMs only — keep templates +## faster redeploy: skip 01_init_proxmox (templates already exist) +## + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +MANIFEST="$SCRIPT_DIR/manifest/scenario_vms.json" + +if [[ ! -f "$MANIFEST" ]]; then + echo "ERROR: manifest not found: $MANIFEST" >&2 + exit 1 +fi + +mapfile -t SCENARIO_VM_IDS < <(jq -r '.vms[].vm_id' "$MANIFEST") +mapfile -t INFRASTRUCTURE_IP < <(jq -r '.vms[].ip' "$MANIFEST") + +ID_REGEX=$(printf '|%s' "${SCENARIO_VM_IDS[@]}" | sed 's/^|//') + +echo ":: stopping and deleting VMs (keeping templates)..." +echo ":: scenario VMs: ${SCENARIO_VM_IDS[*]}" +echo "" + +VM_LIST_JSON=$(proxmox_vm.list.to.jsons.sh 2>&1) +if ! echo "$VM_LIST_JSON" | jq -e . >/dev/null 2>&1; then + echo "ERROR: proxmox_vm.list.to.jsons.sh returned invalid JSON — aborting" >&2 + printf "output: %.200s\n" "$VM_LIST_JSON" >&2 + exit 1 +fi +echo "$VM_LIST_JSON" | jq -c | grep -E "\"vm_id\":($ID_REGEX)([^0-9]|\$)" | proxmox_vm.vm_id.stop_force.to.jsons.sh +echo "$VM_LIST_JSON" | jq -c | grep -E "\"vm_id\":($ID_REGEX)([^0-9]|\$)" | proxmox_vm.vm_id.delete.to.jsons.sh + +for ip in "${INFRASTRUCTURE_IP[@]}"; do + echo ":: REMOVE SSH KEY FOR : $ip" + ssh-keygen -f "$HOME/.ssh/known_hosts" -R "$ip" +done + +echo "" +echo ":: done — templates preserved" +echo ":: redeploy with: range42-context deploy" +echo "" diff --git a/scenarios/demo_lab_network/demo_lab_network.reset.setup.sh b/scenarios/demo_lab_network/demo_lab_network.reset.setup.sh new file mode 100755 index 00000000..53774a62 --- /dev/null +++ b/scenarios/demo_lab_network/demo_lab_network.reset.setup.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +## +## reset + full redeploy — delete everything then deploy from scratch +## + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" + +echo ":: reset — deleting all VMs and templates..." +"$SCRIPT_DIR/demo_lab_network.delete_all.sh" || exit 1 + +echo "" +echo ":: redeploying from scratch..." +"$SCRIPT_DIR/demo_lab_network.setup.sh" diff --git a/scenarios/demo_lab_network/demo_lab_network.reset.ssh_keys.sh b/scenarios/demo_lab_network/demo_lab_network.reset.ssh_keys.sh new file mode 100755 index 00000000..1f18b9b8 --- /dev/null +++ b/scenarios/demo_lab_network/demo_lab_network.reset.ssh_keys.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +## +## reset SSH known_hosts entries for all VMs in this scenario +## + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +MANIFEST="$SCRIPT_DIR/manifest/scenario_vms.json" + +if [[ ! -f "$MANIFEST" ]]; then + echo "ERROR: manifest not found: $MANIFEST" >&2 + exit 1 +fi + +mapfile -t INFRASTRUCTURE_IP < <(jq -r '.vms[].ip' "$MANIFEST") + +for ip in "${INFRASTRUCTURE_IP[@]}"; do + echo ":: REMOVE SSH KEY FOR : $ip" + ssh-keygen -f "$HOME/.ssh/known_hosts" -R "$ip" +done + +echo "" +echo ":: done — SSH known_hosts entries removed" +echo "" diff --git a/scenarios/demo_lab_network/demo_lab_network.setup.sh b/scenarios/demo_lab_network/demo_lab_network.setup.sh new file mode 100755 index 00000000..aff48748 --- /dev/null +++ b/scenarios/demo_lab_network/demo_lab_network.setup.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +ansible-playbook -i "${RANGE42_ANSIBLE_ROLES__INVENTORY_DIR}/inventory_default.yml" \ + -l "all" \ + "./main.yml" --vault-password-file "${RANGE42_VAULT_PASSWORD_FILE:?RANGE42_VAULT_PASSWORD_FILE is not set — run: range42-context use }" diff --git a/scenarios/demo_lab_network/demo_lab_network.setup_vms_only.sh b/scenarios/demo_lab_network/demo_lab_network.setup_vms_only.sh new file mode 100755 index 00000000..afb9116a --- /dev/null +++ b/scenarios/demo_lab_network/demo_lab_network.setup_vms_only.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +## +## setup VMs only — skip template creation (templates already exist) +## + +ansible-playbook -i "${RANGE42_ANSIBLE_ROLES__INVENTORY_DIR}/inventory_default.yml" \ + -l "all" \ + "./main_vms_only.yml" --vault-password-file "${RANGE42_VAULT_PASSWORD_FILE:?RANGE42_VAULT_PASSWORD_FILE is not set — run: range42-context use }" diff --git a/scenarios/demo_lab_network/main.yml b/scenarios/demo_lab_network/main.yml new file mode 100644 index 00000000..eecc00c3 --- /dev/null +++ b/scenarios/demo_lab_network/main.yml @@ -0,0 +1,33 @@ +--- +## +## demo_lab_network — demo_lab VMs + inter-bridge iptables isolation +## +## Network zones: +## admin (vmbr142 · 192.168.142.0/24) — deployer platform + wazuh +## ctf (vmbr144 · 192.168.144.0/24) — vulnerable targets, air-gapped +## +## Adds on top of demo_lab: +## FORWARD DROP ctf → admin (zone isolation) +## FORWARD DROP ctf → internet (air-gap) +## FORWARD ACCEPT ctf → wazuh :1514/1515 (agent reporting) +## FORWARD ACCEPT admin → ctf (management access) +## + +- import_playbook: ./01_init_proxmox/templates/_main_download_cloudinit_files.yml +- import_playbook: ./01_init_proxmox/templates/ubuntu_noble/_main_ubuntu_noble.yml + +#### + +- import_playbook: ./02_admin_infrastructure/_main.yml + +#### + +#- import_playbook: ./03_student_infrastructure/_main.yml + +# Lift air-gap so CTF VMs can reach the internet during cloud-init + package install +- import_playbook: ./05_network_isolation/stage_pre/lift_ctf_airgap.yml + +- import_playbook: ./04_ctf_infrastructure/_main.yml + +# Restore air-gap after CTF setup is complete +- import_playbook: ./05_network_isolation/_main.yml diff --git a/scenarios/demo_lab_network/main_vms_only.yml b/scenarios/demo_lab_network/main_vms_only.yml new file mode 100644 index 00000000..27f27774 --- /dev/null +++ b/scenarios/demo_lab_network/main_vms_only.yml @@ -0,0 +1,19 @@ +--- +## +## demo_lab_network — VMs + isolation only (skip template creation) +## Use when templates already exist from a prior full deploy. +## + +- import_playbook: ./02_admin_infrastructure/_main.yml + +#### + +#- import_playbook: ./03_student_infrastructure/_main.yml + +# Lift air-gap so CTF VMs can reach the internet during cloud-init + package install +- import_playbook: ./05_network_isolation/stage_pre/lift_ctf_airgap.yml + +- import_playbook: ./04_ctf_infrastructure/_main.yml + +# Restore air-gap after CTF setup is complete +- import_playbook: ./05_network_isolation/_main.yml diff --git a/scenarios/demo_lab_network/manifest/scenario_vms.json b/scenarios/demo_lab_network/manifest/scenario_vms.json new file mode 100644 index 00000000..aaa0687c --- /dev/null +++ b/scenarios/demo_lab_network/manifest/scenario_vms.json @@ -0,0 +1,30 @@ +{ + "scenario": "demo_lab_network", + "version": 1, + "description": "demo_lab + inter-bridge iptables isolation. Admin (vmbr142/192.168.142.0/24) and CTF (vmbr144/192.168.144.0/24) are network-isolated: CTF→admin is dropped, CTF→internet is dropped, Wazuh agent traffic (1514/1515) is allowed. Same VM IDs as demo_lab.", + "vms": [ + {"vm_id": 1000, "vm_name": "admin-wazuh", "ip": "192.168.142.100", "role": "admin", "bridge": "vmbr142"}, + {"vm_id": 1020, "vm_name": "admin-deployer-api-gateway", "ip": "192.168.142.120", "role": "admin", "bridge": "vmbr142"}, + {"vm_id": 1021, "vm_name": "admin-deployer-api-backend", "ip": "192.168.142.121", "role": "admin", "bridge": "vmbr142"}, + {"vm_id": 1023, "vm_name": "admin-deployer-ui", "ip": "192.168.142.123", "role": "admin", "bridge": "vmbr142"}, + {"vm_id": 4000, "vm_name": "vuln-box-00", "ip": "192.168.144.170", "role": "ctf", "bridge": "vmbr144"}, + {"vm_id": 4001, "vm_name": "vuln-box-01", "ip": "192.168.144.171", "role": "ctf", "bridge": "vmbr144"}, + {"vm_id": 4002, "vm_name": "vuln-box-02", "ip": "192.168.144.172", "role": "ctf", "bridge": "vmbr144"}, + {"vm_id": 4003, "vm_name": "vuln-box-03", "ip": "192.168.144.173", "role": "ctf", "bridge": "vmbr144"}, + {"vm_id": 4004, "vm_name": "vuln-box-04", "ip": "192.168.144.174", "role": "ctf", "bridge": "vmbr144"} + ], + "templates": [ + {"vm_id": 9901, "vm_name": "template-vm-nano", "spec": "1cpu/1gb/16gb", "ip": "192.168.140.201", "bridge": "vmbr140"}, + {"vm_id": 9211, "vm_name": "template-vm-micro-01-2g-24g", "spec": "1cpu/2gb/24gb", "ip": "192.168.140.211", "bridge": "vmbr140"}, + {"vm_id": 9212, "vm_name": "template-vm-micro-02-2g-24g", "spec": "1cpu/2gb/24gb", "ip": "192.168.140.212", "bridge": "vmbr140"}, + {"vm_id": 9221, "vm_name": "template-vm-small-01-4g-32g", "spec": "1cpu/4gb/32gb", "ip": "192.168.140.221", "bridge": "vmbr140"}, + {"vm_id": 9222, "vm_name": "template-vm-small-02-4g-32g", "spec": "1cpu/4gb/32gb", "ip": "192.168.140.222", "bridge": "vmbr140"}, + {"vm_id": 9224, "vm_name": "template-vm-small-04-4g-32g", "spec": "1cpu/4gb/32gb", "ip": "192.168.140.224", "bridge": "vmbr140"}, + {"vm_id": 9232, "vm_name": "template-vm-medium-02-8g-64g", "spec": "2cpu/8gb/64gb", "ip": "192.168.140.232", "bridge": "vmbr140"}, + {"vm_id": 9234, "vm_name": "template-vm-medium-04-8g-64g", "spec": "4cpu/8gb/64gb", "ip": "192.168.140.234", "bridge": "vmbr140"}, + {"vm_id": 9236, "vm_name": "template-vm-medium-06-8g-64g", "spec": "6cpu/8gb/64gb", "ip": "192.168.140.236", "bridge": "vmbr140"}, + {"vm_id": 9244, "vm_name": "template-vm-large-04-8g-64g", "spec": "4cpu/8gb/64gb", "ip": "192.168.140.244", "bridge": "vmbr140"}, + {"vm_id": 9246, "vm_name": "template-vm-large-06-8g-64g", "spec": "6cpu/8gb/64gb", "ip": "192.168.140.246", "bridge": "vmbr140"}, + {"vm_id": 9248, "vm_name": "template-vm-large-08-8g-64g", "spec": "8cpu/8gb/64gb", "ip": "192.168.140.248", "bridge": "vmbr140"} + ] +} diff --git a/scenarios/demo_lab_network/templates/ansible-inventory.j2 b/scenarios/demo_lab_network/templates/ansible-inventory.j2 new file mode 100644 index 00000000..1b2738ab --- /dev/null +++ b/scenarios/demo_lab_network/templates/ansible-inventory.j2 @@ -0,0 +1,62 @@ +all: + children: + range42_infrastructure: + children: + #### + + r42_admin: + hosts: + # r42.testing-wazuh-client: # disabled + # r42.admin-builder-api-devkit: # removed + # r42.admin-builder-docker-registry: # disabled — re-enable when needed + r42.admin-wazuh: + r42.admin-deployer-api-gateway: + r42.admin-deployer-api-backend: + r42.admin-deployer-ui: + + r42_admin_wazuh_clients: + hosts: + # r42.testing-wazuh-client: # disabled + # r42.admin-builder-api-devkit: # removed + # r42.admin-builder-docker-registry: # disabled — re-enable when needed + r42.admin-deployer-api-gateway: + r42.admin-deployer-api-backend: + r42.admin-deployer-ui: + + #### + + r42_student_box_group: + hosts: + r42.student-box-01: + + #### + + r42_vuln_box_group: + hosts: + r42.vuln-box-00: + r42.vuln-box-01: + r42.vuln-box-02: + r42.vuln-box-03: + r42.vuln-box-04: + + # TODO T57: move init_vm_group to scenario-specific config + r42_init_vm_group: + hosts: + r42.init-vm-00: + r42.init-vm-01: + r42.init-vm-02: + r42.init-vm-03: + r42.init-vm-04: + + #### + + proxmox: + hosts: + {{ INFRASTRUCTURE_CODENAME }}: + ansible_host: {{ INFRASTRUCTURE_PROXMOX_ADDRESS | mandatory }}:8006 + ansible_connection: local + ansible_python_interpreter: /usr/bin/python3 + + proxmox-cli: + hosts: + {{ INFRASTRUCTURE_CODENAME }}-cli: diff --git a/scenarios/demo_lab_network/templates/ansible-vars.yml b/scenarios/demo_lab_network/templates/ansible-vars.yml new file mode 100644 index 00000000..8c749f31 --- /dev/null +++ b/scenarios/demo_lab_network/templates/ansible-vars.yml @@ -0,0 +1,40 @@ +--- +################################################################################ +# range42 — scenario-specific variables (demo_lab_network) +# +# These variables are specific to this scenario and may differ +# between scenarios running on the same CODENAME infrastructure. +# +# For shared variables → group_vars/all/vars.yml +# For secrets → vault.yml (see vault.yml.example) +# +################################################################################ + + +#### SCENARIO IDENTIFICATION #### + +INFRASTRUCTURE_SCENARIO: "demo_lab_network" + + +#### CREDENTIAL GENERATION #### + +# auto-generate ssh keys and passwords (YES/NO) +context_auto_generate_ssh_keys: "YES" +context_auto_generate_vm_passwords: "YES" + +# number of additional student ssh keys (bob_1 .. bob_N) +student_additionnal_keys_count: 5 + + +#### CLOUD-INIT USERNAMES #### + +# admin vm user (not secret — password and ssh key are in vault) +default_admin_vm_ci_user: "alice" + +# student vm user +default_trainee_vm_ci_user: "bob" + +#### CLOUD-INIT DNS #### +# default resolver for lab VMs; range42-init.py substitutes the Proxmox node +# resolver here at workspace creation. 1.1.1.1 is the portable fallback. +default_vm_ci_dns_ips: "1.1.1.1" diff --git a/scenarios/demo_lab_network/templates/ssh-config.j2 b/scenarios/demo_lab_network/templates/ssh-config.j2 new file mode 100644 index 00000000..92a014c1 --- /dev/null +++ b/scenarios/demo_lab_network/templates/ssh-config.j2 @@ -0,0 +1,84 @@ +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# +# infrastructure code name : {{ INFRASTRUCTURE_CODENAME }} +# infrastructure proxmox address : {{ INFRASTRUCTURE_PROXMOX_ADDRESS }} +# scenario : {{ INFRASTRUCTURE_SCENARIO }} +# +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# PROXMOX WEB UI (port forward) +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +Host px.{{ INFRASTRUCTURE_CODENAME }}.redirect_www.proxmox + RequestTTY no + RemoteCommand none + localforward localhost:18042 {{ INFRASTRUCTURE_PROXMOX_ADDRESS | mandatory }}:8006 + localcommand sh -c '/usr/bin/chromium-browser --incognito --new-window --disable-translate https://127.0.0.1:18042 &' + +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# PX ROOT USER SSH ACCESS +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +Host px.{{ INFRASTRUCTURE_CODENAME }}-ssh_cli.root {{ INFRASTRUCTURE_CODENAME }}-cli + Hostname {{ INFRASTRUCTURE_PROXMOX_ADDRESS | mandatory }} + User root + IdentityFile {{ DEPLOYER_CLI__DST_SSH_KEYS_JUMP_DEST_DIR }}/px.{{ INFRASTRUCTURE_CODENAME }}-{{ INFRASTRUCTURE_SCENARIO }}-ssh_cli.root + Port 22 + +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# SSH JUMPER +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +Host px.{{ INFRASTRUCTURE_CODENAME }}.jumper + Hostname {{ INFRASTRUCTURE_PROXMOX_ADDRESS | mandatory }} + User jump_user + IdentityFile {{ DEPLOYER_CLI__DST_SSH_KEYS_JUMP_DEST_DIR }}/px.{{ INFRASTRUCTURE_CODENAME }}-{{ INFRASTRUCTURE_SCENARIO }}-ssh_cli.jump_user + Port 22 + +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# ADMIN VMs (vmbr142 · 192.168.142.0/24) +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +Host r42.admin-wazuh + Hostname 192.168.142.100 + +Host r42.admin-deployer-api-gateway + Hostname 192.168.142.120 + +Host r42.admin-deployer-api-backend + Hostname 192.168.142.121 + +Host r42.admin-deployer-ui + Hostname 192.168.142.123 + +Host r42.admin* r42.testing* + User alice + IdentityFile {{ DEPLOYER_CLI__DST_SSH_KEYS_BACKEND_DEST_DIR }}/r42.{{ INFRASTRUCTURE_CODENAME }}-{{ INFRASTRUCTURE_SCENARIO }}-deployer-key_alice + Port 22 + ProxyJump px.{{ INFRASTRUCTURE_CODENAME }}.jumper + +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# CTF / VULN VMs (vmbr144 · 192.168.144.0/24) +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +Host r42.vuln-box-00 + Hostname 192.168.144.170 + +Host r42.vuln-box-01 + Hostname 192.168.144.171 + +Host r42.vuln-box-02 + Hostname 192.168.144.172 + +Host r42.vuln-box-03 + Hostname 192.168.144.173 + +Host r42.vuln-box-04 + Hostname 192.168.144.174 + +Host r42.vuln-box-* + User alice + IdentityFile {{ DEPLOYER_CLI__DST_SSH_KEYS_BACKEND_DEST_DIR }}/r42.{{ INFRASTRUCTURE_CODENAME }}-{{ INFRASTRUCTURE_SCENARIO }}-deployer-key_alice + Port 22 + ProxyJump px.{{ INFRASTRUCTURE_CODENAME }}.jumper diff --git a/scenarios/demo_lab_network/templates/vault-example.yml b/scenarios/demo_lab_network/templates/vault-example.yml new file mode 100644 index 00000000..db6a9886 --- /dev/null +++ b/scenarios/demo_lab_network/templates/vault-example.yml @@ -0,0 +1,70 @@ +--- +################################################################################ +# range42 — vault secrets (demo_lab) +# +# This file contains ALL secret values for this CODENAME-SCENARIO. +# +# Usage: +# 1. Copy this file: cp vault.yml.example vault.yml +# 2. Fill in real values +# 3. Encrypt: ansible-vault encrypt vault.yml +# 4. Never commit the unencrypted version +# +# Password format: use only a-z A-Z 0-9 - _ (20+ characters recommended) +# +################################################################################ + + +#### PROXMOX API SECRET #### + +# api token secret — generated by proxmox.api-token or imported from existing +proxmox_api_token_secret: "REPLACE_ME" + + +#### JUMP HOST #### + +# jump user password (initial password, used for user creation) +jump_password: "REPLACE_ME" + + +#### CLOUD-INIT PASSWORDS #### + +# admin vm (alice) password +default_admin_vm_ci_password: "REPLACE_ME" + +# admin vm (alice) ssh public key — populated by credentials.generate +default_admin_vm_ci_ssh_key: "ssh-ed25519 AAAA... alice CODENAME-SCENARIO" + +# student vm (bob) password +default_trainee_vm_ci_password: "REPLACE_ME" + +# student vm (bob) ssh public key — populated by credentials.generate +default_trainee_vm_ci_ssh_key: "ssh-ed25519 AAAA... bob CODENAME-SCENARIO" + + +#### TAILSCALE #### + +# tailscale auth key (for joining the tailnet) +infrastructure_tailscale_authkey: "tskey-auth-REPLACE_ME" + +# tailscale api key (for managing devices) +infrastructure_tailscale_apikey: "tskey-api-REPLACE_ME" + + +#### WAZUH #### + +# wazuh admin password +# WAZUH_PASSWORD is consumed by credentials.vault template +# and mapped to infrastructure_wazuh_admin_password in the generated vault +WAZUH_PASSWORD: "REPLACE_ME" + + +#### MISC #### + +# deployer-cli known_hosts path (not really secret, but stored in vault historically) +deployer_cli_user_ssh_known_hosts: "/home/your_deployer_cli_username/.ssh/known_hosts" + +#### CLOUD-INIT DNS #### +# default resolver for lab VMs; range42-init.py substitutes the Proxmox node +# resolver here at workspace creation. 1.1.1.1 is the portable fallback. +default_vm_ci_dns_ips: "1.1.1.1" diff --git a/scenarios/dev_deployer_ui_lab/01_init_proxmox/templates/ubuntu_noble/03-template-vm-medium-02-8g-64g.yml b/scenarios/dev_deployer_ui_lab/01_init_proxmox/templates/ubuntu_noble/03-template-vm-medium-02-8g-64g.yml index 914dc58b..9ed82ce8 100644 --- a/scenarios/dev_deployer_ui_lab/01_init_proxmox/templates/ubuntu_noble/03-template-vm-medium-02-8g-64g.yml +++ b/scenarios/dev_deployer_ui_lab/01_init_proxmox/templates/ubuntu_noble/03-template-vm-medium-02-8g-64g.yml @@ -137,7 +137,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "192.168.140.232" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.140.1" diff --git a/scenarios/dev_deployer_ui_lab/02_dev_deployer_ui_lab_infrastructure/stage_00/dev_backend_vm.yml b/scenarios/dev_deployer_ui_lab/02_dev_deployer_ui_lab_infrastructure/stage_00/dev_backend_vm.yml index 80f4b49f..0c8c5c71 100644 --- a/scenarios/dev_deployer_ui_lab/02_dev_deployer_ui_lab_infrastructure/stage_00/dev_backend_vm.yml +++ b/scenarios/dev_deployer_ui_lab/02_dev_deployer_ui_lab_infrastructure/stage_00/dev_backend_vm.yml @@ -82,7 +82,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "{{ global_vm_ci_ip }}" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.142.1" diff --git a/scenarios/dev_deployer_ui_lab/02_dev_deployer_ui_lab_infrastructure/stage_00/dev_deployer_ui_vm.yml b/scenarios/dev_deployer_ui_lab/02_dev_deployer_ui_lab_infrastructure/stage_00/dev_deployer_ui_vm.yml index edc3fdee..3b195074 100644 --- a/scenarios/dev_deployer_ui_lab/02_dev_deployer_ui_lab_infrastructure/stage_00/dev_deployer_ui_vm.yml +++ b/scenarios/dev_deployer_ui_lab/02_dev_deployer_ui_lab_infrastructure/stage_00/dev_deployer_ui_vm.yml @@ -82,7 +82,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "{{ global_vm_ci_ip }}" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.142.1" diff --git a/scenarios/dev_deployer_ui_lab/02_dev_deployer_ui_lab_infrastructure/stage_00/dev_kong_vm.yml b/scenarios/dev_deployer_ui_lab/02_dev_deployer_ui_lab_infrastructure/stage_00/dev_kong_vm.yml index 67ec9443..7b6e761e 100644 --- a/scenarios/dev_deployer_ui_lab/02_dev_deployer_ui_lab_infrastructure/stage_00/dev_kong_vm.yml +++ b/scenarios/dev_deployer_ui_lab/02_dev_deployer_ui_lab_infrastructure/stage_00/dev_kong_vm.yml @@ -82,7 +82,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "{{ global_vm_ci_ip }}" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.142.1" diff --git a/scenarios/dev_deployer_ui_lab/templates/ansible-vars.yml b/scenarios/dev_deployer_ui_lab/templates/ansible-vars.yml index 33d57858..607408a4 100644 --- a/scenarios/dev_deployer_ui_lab/templates/ansible-vars.yml +++ b/scenarios/dev_deployer_ui_lab/templates/ansible-vars.yml @@ -32,3 +32,8 @@ student_additional_keys_count: 0 # admin vm user (not secret - password and ssh key are in vault) default_admin_vm_ci_user: "alice" + +#### CLOUD-INIT DNS #### +# default resolver for lab VMs; range42-init.py substitutes the Proxmox node +# resolver here at workspace creation. 1.1.1.1 is the portable fallback. +default_vm_ci_dns_ips: "1.1.1.1" diff --git a/scenarios/dev_deployer_ui_lab/templates/vault-example.yml b/scenarios/dev_deployer_ui_lab/templates/vault-example.yml index 6bd8d1de..d1f93e0c 100644 --- a/scenarios/dev_deployer_ui_lab/templates/vault-example.yml +++ b/scenarios/dev_deployer_ui_lab/templates/vault-example.yml @@ -43,3 +43,8 @@ default_admin_vm_ci_ssh_key: "ssh-ed25519 AAAA... alice CODENAME-SCENARIO" # operator-side known_hosts snapshot used by the wait-for-SSH task deployer_cli_user_ssh_known_hosts: "REPLACE_ME" + +#### CLOUD-INIT DNS #### +# default resolver for lab VMs; range42-init.py substitutes the Proxmox node +# resolver here at workspace creation. 1.1.1.1 is the portable fallback. +default_vm_ci_dns_ips: "1.1.1.1" diff --git a/scenarios/kunai_lab/01_init_proxmox/templates/ubuntu_noble/03-template-vm-medium-02-8g-64g.yml b/scenarios/kunai_lab/01_init_proxmox/templates/ubuntu_noble/03-template-vm-medium-02-8g-64g.yml index 914dc58b..9ed82ce8 100644 --- a/scenarios/kunai_lab/01_init_proxmox/templates/ubuntu_noble/03-template-vm-medium-02-8g-64g.yml +++ b/scenarios/kunai_lab/01_init_proxmox/templates/ubuntu_noble/03-template-vm-medium-02-8g-64g.yml @@ -137,7 +137,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "192.168.140.232" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.140.1" diff --git a/scenarios/kunai_lab/02_kunai_lab_infrastructure/stage_00/admin_trainer_kunai_vm.yml b/scenarios/kunai_lab/02_kunai_lab_infrastructure/stage_00/admin_trainer_kunai_vm.yml index 7e3db442..77969b73 100644 --- a/scenarios/kunai_lab/02_kunai_lab_infrastructure/stage_00/admin_trainer_kunai_vm.yml +++ b/scenarios/kunai_lab/02_kunai_lab_infrastructure/stage_00/admin_trainer_kunai_vm.yml @@ -81,7 +81,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "{{ global_vm_ci_ip }}" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.142.1" diff --git a/scenarios/kunai_lab/02_kunai_lab_infrastructure/stage_00/student_kunai_01_vm.yml b/scenarios/kunai_lab/02_kunai_lab_infrastructure/stage_00/student_kunai_01_vm.yml index 4df51e38..dc2c7561 100644 --- a/scenarios/kunai_lab/02_kunai_lab_infrastructure/stage_00/student_kunai_01_vm.yml +++ b/scenarios/kunai_lab/02_kunai_lab_infrastructure/stage_00/student_kunai_01_vm.yml @@ -82,7 +82,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "{{ global_vm_ci_ip }}" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.142.1" diff --git a/scenarios/kunai_lab/02_kunai_lab_infrastructure/stage_00/student_kunai_02_vm.yml b/scenarios/kunai_lab/02_kunai_lab_infrastructure/stage_00/student_kunai_02_vm.yml index b6f87e92..e91cd39a 100644 --- a/scenarios/kunai_lab/02_kunai_lab_infrastructure/stage_00/student_kunai_02_vm.yml +++ b/scenarios/kunai_lab/02_kunai_lab_infrastructure/stage_00/student_kunai_02_vm.yml @@ -82,7 +82,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "{{ global_vm_ci_ip }}" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.142.1" diff --git a/scenarios/kunai_lab/02_kunai_lab_infrastructure/stage_00/student_kunai_03_vm.yml b/scenarios/kunai_lab/02_kunai_lab_infrastructure/stage_00/student_kunai_03_vm.yml index 3554729e..62a47af5 100644 --- a/scenarios/kunai_lab/02_kunai_lab_infrastructure/stage_00/student_kunai_03_vm.yml +++ b/scenarios/kunai_lab/02_kunai_lab_infrastructure/stage_00/student_kunai_03_vm.yml @@ -82,7 +82,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "{{ global_vm_ci_ip }}" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.142.1" diff --git a/scenarios/kunai_lab/02_kunai_lab_infrastructure/stage_00/student_kunai_04_vm.yml b/scenarios/kunai_lab/02_kunai_lab_infrastructure/stage_00/student_kunai_04_vm.yml index d95a8334..e3dda926 100644 --- a/scenarios/kunai_lab/02_kunai_lab_infrastructure/stage_00/student_kunai_04_vm.yml +++ b/scenarios/kunai_lab/02_kunai_lab_infrastructure/stage_00/student_kunai_04_vm.yml @@ -82,7 +82,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "{{ global_vm_ci_ip }}" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.142.1" diff --git a/scenarios/kunai_lab/02_kunai_lab_infrastructure/stage_00/student_kunai_05_vm.yml b/scenarios/kunai_lab/02_kunai_lab_infrastructure/stage_00/student_kunai_05_vm.yml index b00e6440..b461d738 100644 --- a/scenarios/kunai_lab/02_kunai_lab_infrastructure/stage_00/student_kunai_05_vm.yml +++ b/scenarios/kunai_lab/02_kunai_lab_infrastructure/stage_00/student_kunai_05_vm.yml @@ -82,7 +82,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "{{ global_vm_ci_ip }}" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.142.1" diff --git a/scenarios/kunai_lab/templates/ansible-vars.yml b/scenarios/kunai_lab/templates/ansible-vars.yml index 1f1175d8..b85db2a5 100644 --- a/scenarios/kunai_lab/templates/ansible-vars.yml +++ b/scenarios/kunai_lab/templates/ansible-vars.yml @@ -35,3 +35,8 @@ student_additionnal_keys_count: 0 # cloud-init user is alice on all 6 VMs (medium template default, project # convention - matches demo_lab and the blank scenarios) default_admin_vm_ci_user: "alice" + +#### CLOUD-INIT DNS #### +# default resolver for lab VMs; range42-init.py substitutes the Proxmox node +# resolver here at workspace creation. 1.1.1.1 is the portable fallback. +default_vm_ci_dns_ips: "1.1.1.1" diff --git a/scenarios/kunai_lab/templates/vault-example.yml b/scenarios/kunai_lab/templates/vault-example.yml index 4955a138..15997a94 100644 --- a/scenarios/kunai_lab/templates/vault-example.yml +++ b/scenarios/kunai_lab/templates/vault-example.yml @@ -43,3 +43,8 @@ default_admin_vm_ci_ssh_key: "ssh-ed25519 AAAA... alice CODENAME-SCENARIO" # operator-side known_hosts snapshot used by the wait-for-SSH task deployer_cli_user_ssh_known_hosts: "REPLACE_ME" + +#### CLOUD-INIT DNS #### +# default resolver for lab VMs; range42-init.py substitutes the Proxmox node +# resolver here at workspace creation. 1.1.1.1 is the portable fallback. +default_vm_ci_dns_ips: "1.1.1.1" diff --git a/scenarios/misp_lab/01_init_proxmox/templates/ubuntu_noble/03-template-vm-medium-02-8g-64g.yml b/scenarios/misp_lab/01_init_proxmox/templates/ubuntu_noble/03-template-vm-medium-02-8g-64g.yml index 914dc58b..9ed82ce8 100644 --- a/scenarios/misp_lab/01_init_proxmox/templates/ubuntu_noble/03-template-vm-medium-02-8g-64g.yml +++ b/scenarios/misp_lab/01_init_proxmox/templates/ubuntu_noble/03-template-vm-medium-02-8g-64g.yml @@ -137,7 +137,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "192.168.140.232" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.140.1" diff --git a/scenarios/misp_lab/02_misp_lab_infrastructure/stage_00/misp_lab_vm.yml b/scenarios/misp_lab/02_misp_lab_infrastructure/stage_00/misp_lab_vm.yml index 468201cb..276d1c46 100644 --- a/scenarios/misp_lab/02_misp_lab_infrastructure/stage_00/misp_lab_vm.yml +++ b/scenarios/misp_lab/02_misp_lab_infrastructure/stage_00/misp_lab_vm.yml @@ -81,7 +81,7 @@ vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" - vm_ci_dns_ips: "1.1.1.1" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" vm_ci_ip: "{{ global_vm_ci_ip }}" vm_ci_netmask: "24" vm_ci_ip_gw: "192.168.142.1" diff --git a/scenarios/misp_lab/templates/ansible-vars.yml b/scenarios/misp_lab/templates/ansible-vars.yml index 75dfdfac..e123e41e 100644 --- a/scenarios/misp_lab/templates/ansible-vars.yml +++ b/scenarios/misp_lab/templates/ansible-vars.yml @@ -31,3 +31,8 @@ student_additionnal_keys_count: 0 # admin vm user (not secret - password and ssh key are in vault) default_admin_vm_ci_user: "alice" + +#### CLOUD-INIT DNS #### +# default resolver for lab VMs; range42-init.py substitutes the Proxmox node +# resolver here at workspace creation. 1.1.1.1 is the portable fallback. +default_vm_ci_dns_ips: "1.1.1.1" diff --git a/scenarios/misp_lab/templates/vault-example.yml b/scenarios/misp_lab/templates/vault-example.yml index 21161dda..5997f231 100644 --- a/scenarios/misp_lab/templates/vault-example.yml +++ b/scenarios/misp_lab/templates/vault-example.yml @@ -43,3 +43,8 @@ default_admin_vm_ci_ssh_key: "ssh-ed25519 AAAA... alice CODENAME-SCENARIO" # operator-side known_hosts snapshot used by the wait-for-SSH task deployer_cli_user_ssh_known_hosts: "REPLACE_ME" + +#### CLOUD-INIT DNS #### +# default resolver for lab VMs; range42-init.py substitutes the Proxmox node +# resolver here at workspace creation. 1.1.1.1 is the portable fallback. +default_vm_ci_dns_ips: "1.1.1.1" diff --git a/scenarios/test_apt_cacher/01_init_proxmox/_main.reinstall.sh b/scenarios/test_apt_cacher/01_init_proxmox/_main.reinstall.sh new file mode 100755 index 00000000..adcd14a3 --- /dev/null +++ b/scenarios/test_apt_cacher/01_init_proxmox/_main.reinstall.sh @@ -0,0 +1,8 @@ +#!/bin/bash +## +## re-run 01_init_proxmox (download images + create templates) +## + +ansible-playbook -i "${RANGE42_ANSIBLE_ROLES__INVENTORY_DIR}/inventory_default.yml" \ + -l "proxmox" \ + "./_main.yml" --vault-password-file "${RANGE42_VAULT_PASSWORD_FILE:?RANGE42_VAULT_PASSWORD_FILE is not set — run: range42-context use }" diff --git a/scenarios/test_apt_cacher/01_init_proxmox/_main.yml b/scenarios/test_apt_cacher/01_init_proxmox/_main.yml new file mode 100644 index 00000000..40a81665 --- /dev/null +++ b/scenarios/test_apt_cacher/01_init_proxmox/_main.yml @@ -0,0 +1,2 @@ +- import_playbook: ./stage_00-download_cloudinit_files/_main.yml +- import_playbook: ./stage_01-create_templates/_main.yml diff --git a/scenarios/test_apt_cacher/01_init_proxmox/stage_00-download_cloudinit_files/_main.yml b/scenarios/test_apt_cacher/01_init_proxmox/stage_00-download_cloudinit_files/_main.yml new file mode 100644 index 00000000..ba0f4ca9 --- /dev/null +++ b/scenarios/test_apt_cacher/01_init_proxmox/stage_00-download_cloudinit_files/_main.yml @@ -0,0 +1,6 @@ +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# PROMOX INIT - download cloud-init base images (only the OS families used) +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- import_playbook: ./cloudinit_debian_trixie.yml +- import_playbook: ./cloudinit_ubuntu_resolute.yml diff --git a/scenarios/test_apt_cacher/01_init_proxmox/stage_00-download_cloudinit_files/cloudinit_debian_trixie.yml b/scenarios/test_apt_cacher/01_init_proxmox/stage_00-download_cloudinit_files/cloudinit_debian_trixie.yml new file mode 100644 index 00000000..633cfec3 --- /dev/null +++ b/scenarios/test_apt_cacher/01_init_proxmox/stage_00-download_cloudinit_files/cloudinit_debian_trixie.yml @@ -0,0 +1,39 @@ +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# +# PROXMOX INIT - download cloud init image for debian_trixie +# +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- hosts: proxmox + gather_facts: false + vars_files: + - "../../secrets/default_vault.yml" + + tasks: + - name: PROMOX INIT - DOWNLOAD - CLOUD INIT images + include_role: + name: range42-ansible_roles-proxmox_controller + + vars: + proxmox_vm_action: "storage_download_iso" + proxmox_storage: "local" + iso_file_content_type: "iso" + iso_url: "https://cloud.debian.org/images/cloud/trixie/latest/debian-13-genericcloud-amd64.raw" + iso_file_name: "debian-13-genericcloud-amd64.img" + +# The download-url API is async (returns a UPID immediately). Poll on +# proxmox-cli until the file is fully on disk before stage_01 imports it. +- hosts: proxmox-cli + gather_facts: false + vars_files: + - "../../secrets/default_vault.yml" + + tasks: + - name: PROMOX INIT - WAIT for debian-13-genericcloud-amd64.img to be fully downloaded + ansible.builtin.stat: + path: "/var/lib/vz/template/iso/debian-13-genericcloud-amd64.img" + register: _cloudinit_img + until: _cloudinit_img.stat.exists and _cloudinit_img.stat.size > 100000000 + retries: 120 # 120 x 10 s = 20 min max + delay: 10 + changed_when: false diff --git a/scenarios/test_apt_cacher/01_init_proxmox/stage_00-download_cloudinit_files/cloudinit_ubuntu_resolute.yml b/scenarios/test_apt_cacher/01_init_proxmox/stage_00-download_cloudinit_files/cloudinit_ubuntu_resolute.yml new file mode 100644 index 00000000..67a10780 --- /dev/null +++ b/scenarios/test_apt_cacher/01_init_proxmox/stage_00-download_cloudinit_files/cloudinit_ubuntu_resolute.yml @@ -0,0 +1,39 @@ +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# +# PROXMOX INIT - download cloud init image for ubuntu_resolute +# +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- hosts: proxmox + gather_facts: false + vars_files: + - "../../secrets/default_vault.yml" + + tasks: + - name: PROMOX INIT - DOWNLOAD - CLOUD INIT images + include_role: + name: range42-ansible_roles-proxmox_controller + + vars: + proxmox_vm_action: "storage_download_iso" + proxmox_storage: "local" + iso_file_content_type: "iso" + iso_url: "https://cloud-images.ubuntu.com/minimal/daily/resolute/current/resolute-minimal-cloudimg-amd64.img" + iso_file_name: "resolute-minimal-cloudimg-amd64.img" + +# The download-url API is async (returns a UPID immediately). Poll on +# proxmox-cli until the file is fully on disk before stage_01 imports it. +- hosts: proxmox-cli + gather_facts: false + vars_files: + - "../../secrets/default_vault.yml" + + tasks: + - name: PROMOX INIT - WAIT for resolute-minimal-cloudimg-amd64.img to be fully downloaded + ansible.builtin.stat: + path: "/var/lib/vz/template/iso/resolute-minimal-cloudimg-amd64.img" + register: _cloudinit_img + until: _cloudinit_img.stat.exists and _cloudinit_img.stat.size > 100000000 + retries: 120 # 120 x 10 s = 20 min max + delay: 10 + changed_when: false diff --git a/scenarios/test_apt_cacher/01_init_proxmox/stage_01-create_templates/_main.yml b/scenarios/test_apt_cacher/01_init_proxmox/stage_01-create_templates/_main.yml new file mode 100644 index 00000000..5796c564 --- /dev/null +++ b/scenarios/test_apt_cacher/01_init_proxmox/stage_01-create_templates/_main.yml @@ -0,0 +1,7 @@ +--- +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# PROMOX INIT - create the Proxmox template images (only the OS families used) +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- import_playbook: ./templates/debian_trixie/_main_debian_trixie.yml +- import_playbook: ./templates/ubuntu_resolute/_main_ubuntu_resolute.yml diff --git a/scenarios/test_apt_cacher/01_init_proxmox/stage_01-create_templates/templates/debian_trixie/_apply_apt_proxy.yml b/scenarios/test_apt_cacher/01_init_proxmox/stage_01-create_templates/templates/debian_trixie/_apply_apt_proxy.yml new file mode 100644 index 00000000..6dae25ca --- /dev/null +++ b/scenarios/test_apt_cacher/01_init_proxmox/stage_01-create_templates/templates/debian_trixie/_apply_apt_proxy.yml @@ -0,0 +1,51 @@ +## +## debian_trixie - apt proxy via cloud-init cicustom (optional) +## +## Attaches /var/lib/vz/snippets/range42-apt-proxy.yaml as cloud-init vendor-data +## to every template VM. All VMs cloned from these templates inherit the apt +## proxy automatically via cloud-init at first boot. +## Skipped (no-op) if apt_proxy_url is empty/unset. Idempotent. +## + +- hosts: proxmox-cli + gather_facts: false + vars_files: + - "../../../../secrets/default_vault.yml" + + vars: + bs2_template_vm_ids: + - 9321 # template-vm-debian-trixie-small + + tasks: + - name: APT-PROXY - SKIP (apt_proxy_url is empty) + ansible.builtin.debug: + msg: "apt_proxy_url is empty - cloud-init cicustom not applied to templates" + when: apt_proxy_url is not defined or (apt_proxy_url | length) == 0 + + - name: APT-PROXY - ATTACH cicustom vendor TO TEMPLATES + ansible.builtin.command: + cmd: qm set {{ item }} --cicustom "vendor=local:snippets/range42-apt-proxy.yaml" + loop: "{{ bs2_template_vm_ids }}" + register: qm_result + changed_when: qm_result.rc == 0 + failed_when: + - qm_result.rc != 0 + - "'does not exist' not in qm_result.stderr" + when: apt_proxy_url is defined and (apt_proxy_url | length) > 0 + + - name: APT-PROXY - DETACH cicustom vendor FROM TEMPLATES (apt_proxy_url unset) + ansible.builtin.command: + cmd: qm set {{ item }} --delete cicustom + loop: "{{ bs2_template_vm_ids }}" + register: qm_unset + changed_when: qm_unset.rc == 0 + failed_when: + - qm_unset.rc != 0 + - "'does not exist' not in qm_unset.stderr" + when: apt_proxy_url is not defined or (apt_proxy_url | length) == 0 + + - name: APT-PROXY - DISPLAY STATUS + ansible.builtin.debug: + msg: | + apt proxy: {{ apt_proxy_url if (apt_proxy_url is defined and (apt_proxy_url | length) > 0) else 'DISABLED' }} + cicustom : {{ 'attached to ' + (bs2_template_vm_ids | length | string) + ' templates' if (apt_proxy_url is defined and (apt_proxy_url | length) > 0) else 'detached from templates' }} diff --git a/scenarios/test_apt_cacher/01_init_proxmox/stage_01-create_templates/templates/debian_trixie/_main_debian_trixie.yml b/scenarios/test_apt_cacher/01_init_proxmox/stage_01-create_templates/templates/debian_trixie/_main_debian_trixie.yml new file mode 100644 index 00000000..f842f74f --- /dev/null +++ b/scenarios/test_apt_cacher/01_init_proxmox/stage_01-create_templates/templates/debian_trixie/_main_debian_trixie.yml @@ -0,0 +1,12 @@ +## +## debian_trixie — create + configure all Proxmox template VMs for this image +## + +- import_playbook: ./template-vm-debian-trixie-small.yml + +# apt proxy cicustom (no-op if apt_proxy_url empty) +- import_playbook: ./_apply_apt_proxy.yml + +# pre-update: start each VM, run apt update + dist-upgrade via cloud-init poweroff, +# then convert to template. +- import_playbook: ./_update_templates.yml diff --git a/scenarios/test_apt_cacher/01_init_proxmox/stage_01-create_templates/templates/debian_trixie/_update_templates.yml b/scenarios/test_apt_cacher/01_init_proxmox/stage_01-create_templates/templates/debian_trixie/_update_templates.yml new file mode 100644 index 00000000..2455c653 --- /dev/null +++ b/scenarios/test_apt_cacher/01_init_proxmox/stage_01-create_templates/templates/debian_trixie/_update_templates.yml @@ -0,0 +1,202 @@ +## +## debian_trixie - pre-update templates +## +## After all templates are created (but BEFORE convert to template), this playbook: +## 1. renders a temporary bootstrap snippet directly on the Proxmox host +## (apt update + apt dist-upgrade + cloud-init clean + auto-poweroff) +## 2. attaches it as cicustom vendor on each template VM +## 3. starts all templates +## 4. polls qm status until each VM is stopped (cloud-init auto-poweroff signals end) +## 5. detaches the bootstrap snippet, re-attaches the persistent apt-proxy snippet +## 6. converts each VM to a template (qm template ) +## +## Reads VM IDs from the scenario manifest (manifest/scenario_vms.json). +## Runs entirely on `proxmox-cli` (= the Proxmox host itself), no SSH involved. +## + +- hosts: proxmox-cli + gather_facts: false + vars_files: + - "../../../../secrets/default_vault.yml" + + vars: + manifest_path: "{{ playbook_dir }}/../../../../manifest/scenario_vms.json" + bootstrap_snippet_template: "{{ playbook_dir }}/range42-template-bootstrap.yaml.j2" + bootstrap_snippet_path: "/var/lib/vz/snippets/range42-template-bootstrap.yaml" + + tasks: + + #### #### #### debug apt_proxy_url visibility - fully safe vs undefined #### #### #### + + - name: TEMPLATES UPDATE - DEBUG apt_proxy_url visibility + ansible.builtin.debug: + msg: | + apt_proxy_url defined : {{ apt_proxy_url is defined }} + apt_proxy_url value : {{ apt_proxy_url | default('(undefined)') }} + apt_proxy_url length : {{ (apt_proxy_url | default('')) | length }} + -> snippet WILL include apt section : {{ (apt_proxy_url | default('')) | length > 0 }} + + #### #### #### load manifest #### #### #### + + - name: TEMPLATES UPDATE - LOAD MANIFEST + ansible.builtin.set_fact: + templates: "{{ (lookup('file', manifest_path) | from_json).templates }}" + + #### #### #### IDEMPOTENCE - probe each template's current state #### #### #### + # 3 possible states per VM ID : + # - "template" : already converted, skip update entirely + # - "vm" : exists as a regular VM, needs update + convert + # - "missing" : VM doesn't exist on Proxmox, skip (templates not created yet) + # Only "vm" state proceeds through the start/update/shutdown/convert pipeline. + + - name: TEMPLATES UPDATE - PROBE state of each template VM + ansible.builtin.shell: | + if qm config {{ item.vm_id }} 2>/dev/null | grep -q '^template:'; then + echo "template" + elif qm config {{ item.vm_id }} >/dev/null 2>&1; then + echo "vm" + else + echo "missing" + fi + register: tpl_state + loop: "{{ templates }}" + loop_control: + label: "vm_id={{ item.vm_id }}" + changed_when: false + + - name: TEMPLATES UPDATE - BUILD list of templates needing update (state == 'vm') + ansible.builtin.set_fact: + templates_to_update: >- + {{ templates | zip(tpl_state.results) + | selectattr('1.stdout', 'eq', 'vm') + | map(attribute='0') | list }} + + - name: TEMPLATES UPDATE - DISPLAY PLAN + ansible.builtin.debug: + msg: | + total templates in manifest : {{ templates | length }} + already converted (skip) : {{ tpl_state.results | selectattr('stdout', 'eq', 'template') | list | length }} + missing (skip) : {{ tpl_state.results | selectattr('stdout', 'eq', 'missing') | list | length }} + to update + convert : {{ templates_to_update | length }} + {% if templates_to_update | length > 0 %} + targets : + {% for t in templates_to_update %} - {{ t.vm_id }} {{ t.vm_name }} ({{ t.ip }}) + {% endfor %} + {% endif %} + + - name: TEMPLATES UPDATE - SHORT-CIRCUIT if nothing to update + ansible.builtin.meta: end_play + when: templates_to_update | length == 0 + + #### #### #### render bootstrap snippet directly on the Proxmox host #### #### #### + + - name: TEMPLATES UPDATE - RENDER BOOTSTRAP SNIPPET ON PROXMOX + ansible.builtin.template: + src: "{{ bootstrap_snippet_template }}" + dest: "{{ bootstrap_snippet_path }}" + mode: "0644" + + #### #### #### attach bootstrap snippet on every template #### #### #### + + - name: TEMPLATES UPDATE - ATTACH bootstrap cicustom + ansible.builtin.command: + cmd: qm set {{ item.vm_id }} --cicustom "vendor=local:snippets/range42-template-bootstrap.yaml" + loop: "{{ templates_to_update }}" + loop_control: + label: "vm_id={{ item.vm_id }}" + + #### #### #### start all templates #### #### #### + + - name: TEMPLATES UPDATE - START all templates + ansible.builtin.command: + cmd: qm start {{ item.vm_id }} + register: start_result + failed_when: + - start_result.rc != 0 + - "'already running' not in start_result.stderr" + - "'MAX' not in start_result.stderr" + changed_when: start_result.rc == 0 + loop: "{{ templates_to_update }}" + loop_control: + label: "vm_id={{ item.vm_id }}" + + # VMs that failed with "MAX X vcpus" couldn't start - exclude them from + # the wait/convert pipeline but keep the play running for the others. + - name: TEMPLATES UPDATE - BUILD started list (exclude CPU-limited VMs) + ansible.builtin.set_fact: + templates_started: >- + {{ templates_to_update | zip(start_result.results) + | rejectattr('1.stderr', 'search', 'MAX') + | map(attribute='0') | list }} + templates_skipped_cpu: >- + {{ templates_to_update | zip(start_result.results) + | selectattr('1.stderr', 'search', 'MAX') + | map(attribute='0') | list }} + + - name: TEMPLATES UPDATE - WARN skipped templates (host CPU limit) + ansible.builtin.debug: + msg: | + WARNING: {{ templates_skipped_cpu | length }} template(s) skipped - host CPU limit: + {% for t in templates_skipped_cpu %} + - {{ t.vm_id }} {{ t.vm_name }} (spec: {{ t.spec }}) + {% endfor %} + These templates require more vCPUs than this host provides. + They will NOT be converted. Run on a host with enough physical + CPUs, or lower vm_cores in the template playbook if intentional. + when: templates_skipped_cpu | length > 0 + + #### #### #### wait until each VM auto-poweroffs (cloud-init done) #### #### #### + # the loop is sequential per-vm, but since all started in parallel above, + # total time is bounded by the slowest template (~3-15 min depending on cache) + + - name: TEMPLATES UPDATE - WAIT for cloud-init to auto-poweroff + ansible.builtin.shell: | + qm status {{ item.vm_id }} | grep -q 'status: stopped' + register: stop_check + retries: 360 # safety cap : 360 x 5s = 30 min max per VM + delay: 5 + until: stop_check.rc == 0 + loop: "{{ templates_started }}" + loop_control: + label: "vm_id={{ item.vm_id }}" + changed_when: false + + #### #### #### detach bootstrap, re-attach apt-proxy, convert to template #### #### #### + + - name: TEMPLATES UPDATE - DETACH bootstrap cicustom + ansible.builtin.command: + cmd: qm set {{ item.vm_id }} --delete cicustom + loop: "{{ templates_started }}" + loop_control: + label: "vm_id={{ item.vm_id }}" + + - name: TEMPLATES UPDATE - RE-ATTACH apt-proxy cicustom (persistent for clones) + ansible.builtin.command: + cmd: qm set {{ item.vm_id }} --cicustom "vendor=local:snippets/range42-apt-proxy.yaml" + loop: "{{ templates_started }}" + loop_control: + label: "vm_id={{ item.vm_id }}" + when: (apt_proxy_url | default('')) | length > 0 + + - name: TEMPLATES UPDATE - CONVERT VM to template + ansible.builtin.command: + cmd: qm template {{ item.vm_id }} + loop: "{{ templates_started }}" + loop_control: + label: "vm_id={{ item.vm_id }}" + + #### #### #### cleanup #### #### #### + + - name: TEMPLATES UPDATE - REMOVE BOOTSTRAP SNIPPET FROM PROXMOX + ansible.builtin.file: + path: "{{ bootstrap_snippet_path }}" + state: absent + + - name: TEMPLATES UPDATE - DISPLAY DONE + ansible.builtin.debug: + msg: | + {{ templates_started | length }} templates updated and converted + apt-proxy cicustom re-attached (if apt_proxy_url set) + {% if templates_skipped_cpu | length > 0 %} + {{ templates_skipped_cpu | length }} skipped (host CPU limit) - see warning above + {% endif %} diff --git a/scenarios/test_apt_cacher/01_init_proxmox/stage_01-create_templates/templates/debian_trixie/range42-template-bootstrap.yaml.j2 b/scenarios/test_apt_cacher/01_init_proxmox/stage_01-create_templates/templates/debian_trixie/range42-template-bootstrap.yaml.j2 new file mode 100644 index 00000000..46990999 --- /dev/null +++ b/scenarios/test_apt_cacher/01_init_proxmox/stage_01-create_templates/templates/debian_trixie/range42-template-bootstrap.yaml.j2 @@ -0,0 +1,22 @@ +#cloud-config +# range42 - TEMPORARY bootstrap snippet for templates +# Triggers apt update + dist-upgrade then poweroff. +# Auto-generated by _update_templates.yml. Detached after the template is +# converted, replaced by range42-apt-proxy.yaml (persistent). +# +# NOTE: cloud-init clean was previously here in `runcmd` - REMOVED because +# it ran in cloud_config stage, before cloud_final, and broke +# package-update-upgrade-install. Clones get a new vmid -> new instance-id, +# so cloud-init re-processes naturally on each clone. No clean needed. +# +{% if (apt_proxy_url | default('')) | length > 0 %} +apt: + http_proxy: "{{ apt_proxy_url }}" +{% endif %} +package_update: true +package_upgrade: true +package_reboot_if_required: false +power_state: + mode: poweroff + timeout: 60 + condition: True diff --git a/scenarios/test_apt_cacher/01_init_proxmox/stage_01-create_templates/templates/debian_trixie/template-vm-debian-trixie-small.yml b/scenarios/test_apt_cacher/01_init_proxmox/stage_01-create_templates/templates/debian_trixie/template-vm-debian-trixie-small.yml new file mode 100644 index 00000000..94ccfbd0 --- /dev/null +++ b/scenarios/test_apt_cacher/01_init_proxmox/stage_01-create_templates/templates/debian_trixie/template-vm-debian-trixie-small.yml @@ -0,0 +1,134 @@ +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# +# PROXMOX INIT - create debian_trixie template VM template-vm-debian-trixie-small (id 9321) +# +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- hosts: proxmox + gather_facts: false + vars_files: + - "../../../../secrets/default_vault.yml" + + tasks: + #### IDEMPOTENCE - skip if already finalized as template #### + - name: TEMPLATE 9321 - CHECK if already finalized as template + ansible.builtin.shell: qm config 9321 2>/dev/null | grep -q '^template:' + register: tpl_9321_is_template + failed_when: false + changed_when: false + delegate_to: "{{ inventory_hostname }}-cli" + + - name: TEMPLATE 9321 - WAIT for unlock if a parallel deploy holds the VM + ansible.builtin.shell: qm config 9321 | grep -q '^lock:' + register: tpl_9321_lock + until: tpl_9321_lock.rc != 0 + retries: 30 + delay: 10 + failed_when: false + changed_when: false + delegate_to: "{{ inventory_hostname }}-cli" + when: tpl_9321_is_template.rc != 0 + + - name: TEMPLATE 9321 - SKIP all (already finalized as template) + ansible.builtin.debug: + msg: "Template 9321 is already a template - skipping. Run delete-everything to recreate." + when: tpl_9321_is_template.rc == 0 + + #### vm_create idempotence: separate exists check #### + - name: TEMPLATE 9321 - CHECK if VM exists (vm_create idempotence) + ansible.builtin.shell: qm config 9321 2>/dev/null + register: tpl_9321_exists + failed_when: false + changed_when: false + delegate_to: "{{ inventory_hostname }}-cli" + when: tpl_9321_is_template.rc != 0 + + - name: PROXMOX INIT - TEMPLATES - CREATE TEMPLATE - template-vm-debian-trixie-small + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "vm_create" + vm_id: 9321 + vm_name: "template-vm-debian-trixie-small" + vm_cpu: "host" + vm_cores: 1 + vm_sockets: 1 + vm_memory: 4096 + vm_net_virtio_bridge: "vmbr140" + when: + - tpl_9321_is_template.rc != 0 + - tpl_9321_exists.rc != 0 + + - name: PROXMOX INIT - TEMPLATES - WAIT for vm_create to finish (lock-aware) + ansible.builtin.shell: qm config 9321 | grep -q '^lock:' + register: tpl_9321_post_create_lock + until: tpl_9321_post_create_lock.rc != 0 + retries: 60 + delay: 5 + failed_when: false + changed_when: false + delegate_to: "{{ inventory_hostname }}-cli" + when: + - tpl_9321_is_template.rc != 0 + - tpl_9321_exists.rc != 0 + +- hosts: proxmox-cli + gather_facts: false + vars_files: + - "../../../../secrets/default_vault.yml" + + tasks: + - name: TEMPLATE 9321 - CHECK if already template (proxmox-cli play) + ansible.builtin.shell: qm config 9321 2>/dev/null | grep -q '^template:' + register: tpl_9321_is_template + failed_when: false + changed_when: false + + - name: PROXMOX INIT - TEMPLATES - IMPORT CLOUD INIT DISK - template-vm-debian-trixie-small + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "template_cloudinit_import_disk" + proxmox_node: "pve" + cloudinit_image_full_path: "/var/lib/vz/template/iso/debian-13-genericcloud-amd64.img" + vm_id: 9321 + vm_disk_size: "32g" + proxmox_dest_vm_storage_name: "local-lvm" + when: tpl_9321_is_template.rc != 0 + +- hosts: proxmox + gather_facts: false + vars_files: + - "../../../../secrets/default_vault.yml" + + tasks: + - name: TEMPLATE 9321 - CHECK if already template (third play) + ansible.builtin.shell: qm config 9321 2>/dev/null | grep -q '^template:' + register: tpl_9321_is_template + failed_when: false + changed_when: false + delegate_to: "{{ inventory_hostname }}-cli" + + - name: PROXMOX INIT - TEMPLATES - SET CLOUD DEFAULT INIT VARIABLE - template-vm-debian-trixie-small + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "cloudinit_set_variables" + vm_id: 9321 + vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" + vm_ci_password: "{{ default_trainee_vm_ci_password | default('supersecret') }}" + vm_ci_ssh_key: "{{ default_trainee_vm_ci_ssh_key }}" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" + vm_ci_ip: "192.168.140.2" + vm_ci_netmask: "24" + vm_ci_ip_gw: "192.168.140.1" + when: tpl_9321_is_template.rc != 0 + + - name: PROXMOX INIT - TEMPLATES - SET PROXMOX TAG + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "vm_set_tag" + vm_id: 9321 + vm_tag_name: "template" + when: tpl_9321_is_template.rc != 0 diff --git a/scenarios/test_apt_cacher/01_init_proxmox/stage_01-create_templates/templates/ubuntu_resolute/_apply_apt_proxy.yml b/scenarios/test_apt_cacher/01_init_proxmox/stage_01-create_templates/templates/ubuntu_resolute/_apply_apt_proxy.yml new file mode 100644 index 00000000..a8925f42 --- /dev/null +++ b/scenarios/test_apt_cacher/01_init_proxmox/stage_01-create_templates/templates/ubuntu_resolute/_apply_apt_proxy.yml @@ -0,0 +1,51 @@ +## +## ubuntu_resolute - apt proxy via cloud-init cicustom (optional) +## +## Attaches /var/lib/vz/snippets/range42-apt-proxy.yaml as cloud-init vendor-data +## to every template VM. All VMs cloned from these templates inherit the apt +## proxy automatically via cloud-init at first boot. +## Skipped (no-op) if apt_proxy_url is empty/unset. Idempotent. +## + +- hosts: proxmox-cli + gather_facts: false + vars_files: + - "../../../../secrets/default_vault.yml" + + vars: + bs2_template_vm_ids: + - 9521 # template-vm-ubuntu-resolute-small-01-4g-32g + + tasks: + - name: APT-PROXY - SKIP (apt_proxy_url is empty) + ansible.builtin.debug: + msg: "apt_proxy_url is empty - cloud-init cicustom not applied to templates" + when: apt_proxy_url is not defined or (apt_proxy_url | length) == 0 + + - name: APT-PROXY - ATTACH cicustom vendor TO TEMPLATES + ansible.builtin.command: + cmd: qm set {{ item }} --cicustom "vendor=local:snippets/range42-apt-proxy.yaml" + loop: "{{ bs2_template_vm_ids }}" + register: qm_result + changed_when: qm_result.rc == 0 + failed_when: + - qm_result.rc != 0 + - "'does not exist' not in qm_result.stderr" + when: apt_proxy_url is defined and (apt_proxy_url | length) > 0 + + - name: APT-PROXY - DETACH cicustom vendor FROM TEMPLATES (apt_proxy_url unset) + ansible.builtin.command: + cmd: qm set {{ item }} --delete cicustom + loop: "{{ bs2_template_vm_ids }}" + register: qm_unset + changed_when: qm_unset.rc == 0 + failed_when: + - qm_unset.rc != 0 + - "'does not exist' not in qm_unset.stderr" + when: apt_proxy_url is not defined or (apt_proxy_url | length) == 0 + + - name: APT-PROXY - DISPLAY STATUS + ansible.builtin.debug: + msg: | + apt proxy: {{ apt_proxy_url if (apt_proxy_url is defined and (apt_proxy_url | length) > 0) else 'DISABLED' }} + cicustom : {{ 'attached to ' + (bs2_template_vm_ids | length | string) + ' templates' if (apt_proxy_url is defined and (apt_proxy_url | length) > 0) else 'detached from templates' }} diff --git a/scenarios/test_apt_cacher/01_init_proxmox/stage_01-create_templates/templates/ubuntu_resolute/_main_ubuntu_resolute.yml b/scenarios/test_apt_cacher/01_init_proxmox/stage_01-create_templates/templates/ubuntu_resolute/_main_ubuntu_resolute.yml new file mode 100644 index 00000000..ca2dd160 --- /dev/null +++ b/scenarios/test_apt_cacher/01_init_proxmox/stage_01-create_templates/templates/ubuntu_resolute/_main_ubuntu_resolute.yml @@ -0,0 +1,12 @@ +## +## ubuntu_resolute — create + configure all Proxmox template VMs for this image +## + +- import_playbook: ./template-vm-ubuntu-resolute-small-01-4g-32g.yml + +# apt proxy cicustom (no-op if apt_proxy_url empty) +- import_playbook: ./_apply_apt_proxy.yml + +# pre-update: start each VM, run apt update + dist-upgrade via cloud-init poweroff, +# then convert to template. +- import_playbook: ./_update_templates.yml diff --git a/scenarios/test_apt_cacher/01_init_proxmox/stage_01-create_templates/templates/ubuntu_resolute/_update_templates.yml b/scenarios/test_apt_cacher/01_init_proxmox/stage_01-create_templates/templates/ubuntu_resolute/_update_templates.yml new file mode 100644 index 00000000..1513b666 --- /dev/null +++ b/scenarios/test_apt_cacher/01_init_proxmox/stage_01-create_templates/templates/ubuntu_resolute/_update_templates.yml @@ -0,0 +1,202 @@ +## +## ubuntu_resolute - pre-update templates +## +## After all templates are created (but BEFORE convert to template), this playbook: +## 1. renders a temporary bootstrap snippet directly on the Proxmox host +## (apt update + apt dist-upgrade + cloud-init clean + auto-poweroff) +## 2. attaches it as cicustom vendor on each template VM +## 3. starts all templates +## 4. polls qm status until each VM is stopped (cloud-init auto-poweroff signals end) +## 5. detaches the bootstrap snippet, re-attaches the persistent apt-proxy snippet +## 6. converts each VM to a template (qm template ) +## +## Reads VM IDs from the scenario manifest (manifest/scenario_vms.json). +## Runs entirely on `proxmox-cli` (= the Proxmox host itself), no SSH involved. +## + +- hosts: proxmox-cli + gather_facts: false + vars_files: + - "../../../../secrets/default_vault.yml" + + vars: + manifest_path: "{{ playbook_dir }}/../../../../manifest/scenario_vms.json" + bootstrap_snippet_template: "{{ playbook_dir }}/range42-template-bootstrap.yaml.j2" + bootstrap_snippet_path: "/var/lib/vz/snippets/range42-template-bootstrap.yaml" + + tasks: + + #### #### #### debug apt_proxy_url visibility - fully safe vs undefined #### #### #### + + - name: TEMPLATES UPDATE - DEBUG apt_proxy_url visibility + ansible.builtin.debug: + msg: | + apt_proxy_url defined : {{ apt_proxy_url is defined }} + apt_proxy_url value : {{ apt_proxy_url | default('(undefined)') }} + apt_proxy_url length : {{ (apt_proxy_url | default('')) | length }} + -> snippet WILL include apt section : {{ (apt_proxy_url | default('')) | length > 0 }} + + #### #### #### load manifest #### #### #### + + - name: TEMPLATES UPDATE - LOAD MANIFEST + ansible.builtin.set_fact: + templates: "{{ (lookup('file', manifest_path) | from_json).templates }}" + + #### #### #### IDEMPOTENCE - probe each template's current state #### #### #### + # 3 possible states per VM ID : + # - "template" : already converted, skip update entirely + # - "vm" : exists as a regular VM, needs update + convert + # - "missing" : VM doesn't exist on Proxmox, skip (templates not created yet) + # Only "vm" state proceeds through the start/update/shutdown/convert pipeline. + + - name: TEMPLATES UPDATE - PROBE state of each template VM + ansible.builtin.shell: | + if qm config {{ item.vm_id }} 2>/dev/null | grep -q '^template:'; then + echo "template" + elif qm config {{ item.vm_id }} >/dev/null 2>&1; then + echo "vm" + else + echo "missing" + fi + register: tpl_state + loop: "{{ templates }}" + loop_control: + label: "vm_id={{ item.vm_id }}" + changed_when: false + + - name: TEMPLATES UPDATE - BUILD list of templates needing update (state == 'vm') + ansible.builtin.set_fact: + templates_to_update: >- + {{ templates | zip(tpl_state.results) + | selectattr('1.stdout', 'eq', 'vm') + | map(attribute='0') | list }} + + - name: TEMPLATES UPDATE - DISPLAY PLAN + ansible.builtin.debug: + msg: | + total templates in manifest : {{ templates | length }} + already converted (skip) : {{ tpl_state.results | selectattr('stdout', 'eq', 'template') | list | length }} + missing (skip) : {{ tpl_state.results | selectattr('stdout', 'eq', 'missing') | list | length }} + to update + convert : {{ templates_to_update | length }} + {% if templates_to_update | length > 0 %} + targets : + {% for t in templates_to_update %} - {{ t.vm_id }} {{ t.vm_name }} ({{ t.ip }}) + {% endfor %} + {% endif %} + + - name: TEMPLATES UPDATE - SHORT-CIRCUIT if nothing to update + ansible.builtin.meta: end_play + when: templates_to_update | length == 0 + + #### #### #### render bootstrap snippet directly on the Proxmox host #### #### #### + + - name: TEMPLATES UPDATE - RENDER BOOTSTRAP SNIPPET ON PROXMOX + ansible.builtin.template: + src: "{{ bootstrap_snippet_template }}" + dest: "{{ bootstrap_snippet_path }}" + mode: "0644" + + #### #### #### attach bootstrap snippet on every template #### #### #### + + - name: TEMPLATES UPDATE - ATTACH bootstrap cicustom + ansible.builtin.command: + cmd: qm set {{ item.vm_id }} --cicustom "vendor=local:snippets/range42-template-bootstrap.yaml" + loop: "{{ templates_to_update }}" + loop_control: + label: "vm_id={{ item.vm_id }}" + + #### #### #### start all templates #### #### #### + + - name: TEMPLATES UPDATE - START all templates + ansible.builtin.command: + cmd: qm start {{ item.vm_id }} + register: start_result + failed_when: + - start_result.rc != 0 + - "'already running' not in start_result.stderr" + - "'MAX' not in start_result.stderr" + changed_when: start_result.rc == 0 + loop: "{{ templates_to_update }}" + loop_control: + label: "vm_id={{ item.vm_id }}" + + # VMs that failed with "MAX X vcpus" couldn't start - exclude them from + # the wait/convert pipeline but keep the play running for the others. + - name: TEMPLATES UPDATE - BUILD started list (exclude CPU-limited VMs) + ansible.builtin.set_fact: + templates_started: >- + {{ templates_to_update | zip(start_result.results) + | rejectattr('1.stderr', 'search', 'MAX') + | map(attribute='0') | list }} + templates_skipped_cpu: >- + {{ templates_to_update | zip(start_result.results) + | selectattr('1.stderr', 'search', 'MAX') + | map(attribute='0') | list }} + + - name: TEMPLATES UPDATE - WARN skipped templates (host CPU limit) + ansible.builtin.debug: + msg: | + WARNING: {{ templates_skipped_cpu | length }} template(s) skipped - host CPU limit: + {% for t in templates_skipped_cpu %} + - {{ t.vm_id }} {{ t.vm_name }} (spec: {{ t.spec }}) + {% endfor %} + These templates require more vCPUs than this host provides. + They will NOT be converted. Run on a host with enough physical + CPUs, or lower vm_cores in the template playbook if intentional. + when: templates_skipped_cpu | length > 0 + + #### #### #### wait until each VM auto-poweroffs (cloud-init done) #### #### #### + # the loop is sequential per-vm, but since all started in parallel above, + # total time is bounded by the slowest template (~3-15 min depending on cache) + + - name: TEMPLATES UPDATE - WAIT for cloud-init to auto-poweroff + ansible.builtin.shell: | + qm status {{ item.vm_id }} | grep -q 'status: stopped' + register: stop_check + retries: 360 # safety cap : 360 x 5s = 30 min max per VM + delay: 5 + until: stop_check.rc == 0 + loop: "{{ templates_started }}" + loop_control: + label: "vm_id={{ item.vm_id }}" + changed_when: false + + #### #### #### detach bootstrap, re-attach apt-proxy, convert to template #### #### #### + + - name: TEMPLATES UPDATE - DETACH bootstrap cicustom + ansible.builtin.command: + cmd: qm set {{ item.vm_id }} --delete cicustom + loop: "{{ templates_started }}" + loop_control: + label: "vm_id={{ item.vm_id }}" + + - name: TEMPLATES UPDATE - RE-ATTACH apt-proxy cicustom (persistent for clones) + ansible.builtin.command: + cmd: qm set {{ item.vm_id }} --cicustom "vendor=local:snippets/range42-apt-proxy.yaml" + loop: "{{ templates_started }}" + loop_control: + label: "vm_id={{ item.vm_id }}" + when: (apt_proxy_url | default('')) | length > 0 + + - name: TEMPLATES UPDATE - CONVERT VM to template + ansible.builtin.command: + cmd: qm template {{ item.vm_id }} + loop: "{{ templates_started }}" + loop_control: + label: "vm_id={{ item.vm_id }}" + + #### #### #### cleanup #### #### #### + + - name: TEMPLATES UPDATE - REMOVE BOOTSTRAP SNIPPET FROM PROXMOX + ansible.builtin.file: + path: "{{ bootstrap_snippet_path }}" + state: absent + + - name: TEMPLATES UPDATE - DISPLAY DONE + ansible.builtin.debug: + msg: | + {{ templates_started | length }} templates updated and converted + apt-proxy cicustom re-attached (if apt_proxy_url set) + {% if templates_skipped_cpu | length > 0 %} + {{ templates_skipped_cpu | length }} skipped (host CPU limit) - see warning above + {% endif %} diff --git a/scenarios/test_apt_cacher/01_init_proxmox/stage_01-create_templates/templates/ubuntu_resolute/range42-template-bootstrap.yaml.j2 b/scenarios/test_apt_cacher/01_init_proxmox/stage_01-create_templates/templates/ubuntu_resolute/range42-template-bootstrap.yaml.j2 new file mode 100644 index 00000000..46990999 --- /dev/null +++ b/scenarios/test_apt_cacher/01_init_proxmox/stage_01-create_templates/templates/ubuntu_resolute/range42-template-bootstrap.yaml.j2 @@ -0,0 +1,22 @@ +#cloud-config +# range42 - TEMPORARY bootstrap snippet for templates +# Triggers apt update + dist-upgrade then poweroff. +# Auto-generated by _update_templates.yml. Detached after the template is +# converted, replaced by range42-apt-proxy.yaml (persistent). +# +# NOTE: cloud-init clean was previously here in `runcmd` - REMOVED because +# it ran in cloud_config stage, before cloud_final, and broke +# package-update-upgrade-install. Clones get a new vmid -> new instance-id, +# so cloud-init re-processes naturally on each clone. No clean needed. +# +{% if (apt_proxy_url | default('')) | length > 0 %} +apt: + http_proxy: "{{ apt_proxy_url }}" +{% endif %} +package_update: true +package_upgrade: true +package_reboot_if_required: false +power_state: + mode: poweroff + timeout: 60 + condition: True diff --git a/scenarios/test_apt_cacher/01_init_proxmox/stage_01-create_templates/templates/ubuntu_resolute/template-vm-ubuntu-resolute-small-01-4g-32g.yml b/scenarios/test_apt_cacher/01_init_proxmox/stage_01-create_templates/templates/ubuntu_resolute/template-vm-ubuntu-resolute-small-01-4g-32g.yml new file mode 100644 index 00000000..0428b4c1 --- /dev/null +++ b/scenarios/test_apt_cacher/01_init_proxmox/stage_01-create_templates/templates/ubuntu_resolute/template-vm-ubuntu-resolute-small-01-4g-32g.yml @@ -0,0 +1,134 @@ +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# +# PROXMOX INIT - create ubuntu_resolute template VM template-vm-ubuntu-resolute-small-01-4g-32g (id 9521) +# +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- hosts: proxmox + gather_facts: false + vars_files: + - "../../../../secrets/default_vault.yml" + + tasks: + #### IDEMPOTENCE - skip if already finalized as template #### + - name: TEMPLATE 9521 - CHECK if already finalized as template + ansible.builtin.shell: qm config 9521 2>/dev/null | grep -q '^template:' + register: tpl_9521_is_template + failed_when: false + changed_when: false + delegate_to: "{{ inventory_hostname }}-cli" + + - name: TEMPLATE 9521 - WAIT for unlock if a parallel deploy holds the VM + ansible.builtin.shell: qm config 9521 | grep -q '^lock:' + register: tpl_9521_lock + until: tpl_9521_lock.rc != 0 + retries: 30 + delay: 10 + failed_when: false + changed_when: false + delegate_to: "{{ inventory_hostname }}-cli" + when: tpl_9521_is_template.rc != 0 + + - name: TEMPLATE 9521 - SKIP all (already finalized as template) + ansible.builtin.debug: + msg: "Template 9521 is already a template - skipping. Run delete-everything to recreate." + when: tpl_9521_is_template.rc == 0 + + #### vm_create idempotence: separate exists check #### + - name: TEMPLATE 9521 - CHECK if VM exists (vm_create idempotence) + ansible.builtin.shell: qm config 9521 2>/dev/null + register: tpl_9521_exists + failed_when: false + changed_when: false + delegate_to: "{{ inventory_hostname }}-cli" + when: tpl_9521_is_template.rc != 0 + + - name: PROXMOX INIT - TEMPLATES - CREATE TEMPLATE - template-vm-ubuntu-resolute-small-01-4g-32g + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "vm_create" + vm_id: 9521 + vm_name: "template-vm-ubuntu-resolute-small-01-4g-32g" + vm_cpu: "host" + vm_cores: 1 + vm_sockets: 1 + vm_memory: 4096 + vm_net_virtio_bridge: "vmbr140" + when: + - tpl_9521_is_template.rc != 0 + - tpl_9521_exists.rc != 0 + + - name: PROXMOX INIT - TEMPLATES - WAIT for vm_create to finish (lock-aware) + ansible.builtin.shell: qm config 9521 | grep -q '^lock:' + register: tpl_9521_post_create_lock + until: tpl_9521_post_create_lock.rc != 0 + retries: 60 + delay: 5 + failed_when: false + changed_when: false + delegate_to: "{{ inventory_hostname }}-cli" + when: + - tpl_9521_is_template.rc != 0 + - tpl_9521_exists.rc != 0 + +- hosts: proxmox-cli + gather_facts: false + vars_files: + - "../../../../secrets/default_vault.yml" + + tasks: + - name: TEMPLATE 9521 - CHECK if already template (proxmox-cli play) + ansible.builtin.shell: qm config 9521 2>/dev/null | grep -q '^template:' + register: tpl_9521_is_template + failed_when: false + changed_when: false + + - name: PROXMOX INIT - TEMPLATES - IMPORT CLOUD INIT DISK - template-vm-ubuntu-resolute-small-01-4g-32g + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "template_cloudinit_import_disk" + proxmox_node: "pve" + cloudinit_image_full_path: "/var/lib/vz/template/iso/resolute-minimal-cloudimg-amd64.img" + vm_id: 9521 + vm_disk_size: "32g" + proxmox_dest_vm_storage_name: "local-lvm" + when: tpl_9521_is_template.rc != 0 + +- hosts: proxmox + gather_facts: false + vars_files: + - "../../../../secrets/default_vault.yml" + + tasks: + - name: TEMPLATE 9521 - CHECK if already template (third play) + ansible.builtin.shell: qm config 9521 2>/dev/null | grep -q '^template:' + register: tpl_9521_is_template + failed_when: false + changed_when: false + delegate_to: "{{ inventory_hostname }}-cli" + + - name: PROXMOX INIT - TEMPLATES - SET CLOUD DEFAULT INIT VARIABLE - template-vm-ubuntu-resolute-small-01-4g-32g + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "cloudinit_set_variables" + vm_id: 9521 + vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" + vm_ci_password: "{{ default_trainee_vm_ci_password | default('supersecret') }}" + vm_ci_ssh_key: "{{ default_trainee_vm_ci_ssh_key }}" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" + vm_ci_ip: "192.168.140.3" + vm_ci_netmask: "24" + vm_ci_ip_gw: "192.168.140.1" + when: tpl_9521_is_template.rc != 0 + + - name: PROXMOX INIT - TEMPLATES - SET PROXMOX TAG + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "vm_set_tag" + vm_id: 9521 + vm_tag_name: "template" + when: tpl_9521_is_template.rc != 0 diff --git a/scenarios/test_apt_cacher/02_lan1_infrastructure/_main.reinstall.sh b/scenarios/test_apt_cacher/02_lan1_infrastructure/_main.reinstall.sh new file mode 100755 index 00000000..87297363 --- /dev/null +++ b/scenarios/test_apt_cacher/02_lan1_infrastructure/_main.reinstall.sh @@ -0,0 +1,8 @@ +#!/bin/bash +## +## re-run this section's playbooks (02_lan1_infrastructure) +## + +ansible-playbook -i "${RANGE42_ANSIBLE_ROLES__INVENTORY_DIR}/inventory_default.yml" \ + -l "all" \ + "./_main.yml" --vault-password-file "${RANGE42_VAULT_PASSWORD_FILE:?RANGE42_VAULT_PASSWORD_FILE is not set — run: range42-context use }" diff --git a/scenarios/test_apt_cacher/02_lan1_infrastructure/_main.yml b/scenarios/test_apt_cacher/02_lan1_infrastructure/_main.yml new file mode 100644 index 00000000..e569da39 --- /dev/null +++ b/scenarios/test_apt_cacher/02_lan1_infrastructure/_main.yml @@ -0,0 +1,23 @@ +--- +## +## 02_lan1_infrastructure — generated by r42playbooks +## stage_00 clones VMs (per-VM global_* below); stage_01 installs software. +## + +#### STAGE 00 #### + +- import_playbook: ./stage_00/lan1-apt-cache-00.yml + vars: + global_vm_name: "lan1-apt-cache-00" + global_vm_ssh_name: "r42.lan1-apt-cache-00" + global_vm_id: 2011 + global_vm_description: "apt-cache" + global_vm_tag_name: "lan1" + global_vm_ci_ip: "192.168.150.11" + global_template_vm_id: 9321 + global_template_name: "template-vm-debian-trixie-small - id 9321" + + +#### STAGE 01 #### + +- import_playbook: ./stage_01/lan1-apt-cache-00.yml diff --git a/scenarios/test_apt_cacher/02_lan1_infrastructure/stage_00/lan1-apt-cache-00.yml b/scenarios/test_apt_cacher/02_lan1_infrastructure/stage_00/lan1-apt-cache-00.yml new file mode 100644 index 00000000..548dbb8c --- /dev/null +++ b/scenarios/test_apt_cacher/02_lan1_infrastructure/stage_00/lan1-apt-cache-00.yml @@ -0,0 +1,81 @@ +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# +# LAN1 INFRASTRUCTURE INIT - CLONE + CLOUD-INIT +# +# hostname : lan1-apt-cache-00 +# vm_id : 2011 +# ip : 192.168.150.11 +# template : template-vm-debian-trixie-small +# +# global_* vars are supplied by ../_main.yml (kept here as Jinja references). +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- hosts: proxmox + gather_facts: false + vars_files: + - "../../secrets/default_vault.yml" + + tasks: + - name: "LAN1 INFRASTRUCTURE INIT - {{ global_vm_name }} - CLONE TEMPLATE - {{ global_template_name }}" + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "vm_clone" + vm_id: "{{ global_template_vm_id }}" + vm_new_id: "{{ global_vm_id }}" + vm_name: "{{ global_vm_name }}" + vm_description: "{{ global_vm_description }}" + + - name: "LAN1 INFRASTRUCTURE INIT - {{ global_vm_name }} - WAIT FOR PROXMOX UNLOCK" + ansible.builtin.shell: qm config {{ global_vm_id }} | grep -q '^lock:' + register: clone_lock_check + until: clone_lock_check.rc != 0 + retries: 60 + delay: 5 + failed_when: false + changed_when: false + delegate_to: "{{ inventory_hostname }}-cli" + + - name: "LAN1 INFRASTRUCTURE INIT - {{ global_vm_name }} - SET PROXMOX TAG" + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "vm_set_tag" + vm_id: "{{ global_vm_id }}" + vm_tag_name: "{{ global_vm_tag_name }}" + + - name: "LAN1 INFRASTRUCTURE INIT - {{ global_vm_name }} - SET CLOUD-INIT VARIABLES" + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "cloudinit_set_variables" + vm_id: "{{ global_vm_id }}" + vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" + vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" + vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" + vm_ci_ip: "{{ global_vm_ci_ip }}" + vm_ci_netmask: "24" + vm_ci_ip_gw: "192.168.150.1" + vm_net_virtio_bridge: "vmbr150" + + - name: "LAN1 INFRASTRUCTURE INIT - {{ global_vm_name }} - START VM" + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "vm_start" + vm_id: "{{ global_vm_id }}" + +- hosts: "{{ global_vm_ssh_name }}" + gather_facts: false + vars_files: + - "../../secrets/default_vault.yml" + vars: + ARG_vm_ssh_name: "{{ global_vm_ssh_name }}" + requested_tasks: + - wait/openssh_server/is_reachable.yml + - wait/cloudinit/is_boot_finished.yml + tasks: + - name: "LAN1 INFRASTRUCTURE INIT - {{ global_vm_name }} - WAIT FOR SSH + CLOUD-INIT" + include_role: + name: ansible.utils diff --git a/scenarios/test_apt_cacher/02_lan1_infrastructure/stage_01/lan1-apt-cache-00.devkit/test_apt_cacher.lan1-apt-cache-00.install.sh b/scenarios/test_apt_cacher/02_lan1_infrastructure/stage_01/lan1-apt-cache-00.devkit/test_apt_cacher.lan1-apt-cache-00.install.sh new file mode 100755 index 00000000..9d1eff68 --- /dev/null +++ b/scenarios/test_apt_cacher/02_lan1_infrastructure/stage_01/lan1-apt-cache-00.devkit/test_apt_cacher.lan1-apt-cache-00.install.sh @@ -0,0 +1,8 @@ +#!/bin/bash +## +## reinstall software on lan1-apt-cache-00 only (fast single-VM iteration) +## + +ansible-playbook -i "${RANGE42_ANSIBLE_ROLES__INVENTORY_DIR}/inventory_default.yml" \ + -l "all" \ + "../lan1-apt-cache-00.yml" --vault-password-file "${RANGE42_VAULT_PASSWORD_FILE:?RANGE42_VAULT_PASSWORD_FILE is not set — run: range42-context use }" diff --git a/scenarios/test_apt_cacher/02_lan1_infrastructure/stage_01/lan1-apt-cache-00.devkit/test_apt_cacher.lan1-apt-cache-00.revert.sh b/scenarios/test_apt_cacher/02_lan1_infrastructure/stage_01/lan1-apt-cache-00.devkit/test_apt_cacher.lan1-apt-cache-00.revert.sh new file mode 100755 index 00000000..0ff5fd90 --- /dev/null +++ b/scenarios/test_apt_cacher/02_lan1_infrastructure/stage_01/lan1-apt-cache-00.devkit/test_apt_cacher.lan1-apt-cache-00.revert.sh @@ -0,0 +1,8 @@ +#!/bin/bash +## +## revert lan1-apt-cache-00 to its latest snapshot and restart it +## + +VM_ID=$(devkit_manifest.find_vm_id.to.text.sh "$0" "lan1-apt-cache-00") || exit 1 +echo "{\"proxmox_node\":\"pve\",\"vm_id\":${VM_ID} }" | proxmox_snapshot_vm.vm_id.revert_snapshot.to.jsons.sh +echo "{\"proxmox_node\":\"pve\",\"vm_id\":${VM_ID} }" | proxmox_vm.vm_id.start.to.jsons.sh diff --git a/scenarios/test_apt_cacher/02_lan1_infrastructure/stage_01/lan1-apt-cache-00.devkit/test_apt_cacher.lan1-apt-cache-00.snapshot.sh b/scenarios/test_apt_cacher/02_lan1_infrastructure/stage_01/lan1-apt-cache-00.devkit/test_apt_cacher.lan1-apt-cache-00.snapshot.sh new file mode 100755 index 00000000..2d859649 --- /dev/null +++ b/scenarios/test_apt_cacher/02_lan1_infrastructure/stage_01/lan1-apt-cache-00.devkit/test_apt_cacher.lan1-apt-cache-00.snapshot.sh @@ -0,0 +1,7 @@ +#!/bin/bash +## +## snapshot lan1-apt-cache-00 as "base" +## + +VM_ID=$(devkit_manifest.find_vm_id.to.text.sh "$0" "lan1-apt-cache-00") || exit 1 +echo "{\"proxmox_node\":\"pve\",\"vm_id\":${VM_ID},\"vm_snapshot_description\":\"base\"}" | proxmox_snapshot_vm.vm_id.create_snapshot.to.jsons.sh diff --git a/scenarios/test_apt_cacher/02_lan1_infrastructure/stage_01/lan1-apt-cache-00.yml b/scenarios/test_apt_cacher/02_lan1_infrastructure/stage_01/lan1-apt-cache-00.yml new file mode 100644 index 00000000..d2ba53e2 --- /dev/null +++ b/scenarios/test_apt_cacher/02_lan1_infrastructure/stage_01/lan1-apt-cache-00.yml @@ -0,0 +1,43 @@ +## +## stage_01 — software install for lan1-apt-cache-00 +## catalog roles are referenced BY NAME (resolved via ANSIBLE_ROLES_PATH at deploy); +## one play per attachment so each carries its own params (firewall_rules, docker …). +## + +- name: "lan1-apt-cache-00 — software.configure.firewalls" + hosts: r42.lan1-apt-cache-00 + become: true + vars_files: + - "../../secrets/default_vault.yml" + vars: + firewall_rules: + - ip: all + port: 22 + protocol: tcp + - ip: all + port: 3142 + protocol: tcp + roles: + - software.configure.firewalls + +- name: "lan1-apt-cache-00 — software.install.warmup.basic_packages" + hosts: r42.lan1-apt-cache-00 + become: true + vars_files: + - "../../secrets/default_vault.yml" + roles: + - software.install.warmup.basic_packages + +- name: "lan1-apt-cache-00 — software.install.apt_cacher_ng" + hosts: r42.lan1-apt-cache-00 + become: true + vars_files: + - "../../secrets/default_vault.yml" + vars: + apt_cacher_bind_address: 0.0.0.0 + apt_cacher_cache_dir: /var/cache/apt-cacher-ng + apt_cacher_log_dir: /var/log/apt-cacher-ng + apt_cacher_passthrough: true + apt_cacher_port: 3142 + roles: + - software.install.apt_cacher_ng diff --git a/scenarios/test_apt_cacher/03_lan2_infrastructure/_main.reinstall.sh b/scenarios/test_apt_cacher/03_lan2_infrastructure/_main.reinstall.sh new file mode 100755 index 00000000..56d65a34 --- /dev/null +++ b/scenarios/test_apt_cacher/03_lan2_infrastructure/_main.reinstall.sh @@ -0,0 +1,8 @@ +#!/bin/bash +## +## re-run this section's playbooks (03_lan2_infrastructure) +## + +ansible-playbook -i "${RANGE42_ANSIBLE_ROLES__INVENTORY_DIR}/inventory_default.yml" \ + -l "all" \ + "./_main.yml" --vault-password-file "${RANGE42_VAULT_PASSWORD_FILE:?RANGE42_VAULT_PASSWORD_FILE is not set — run: range42-context use }" diff --git a/scenarios/test_apt_cacher/03_lan2_infrastructure/_main.yml b/scenarios/test_apt_cacher/03_lan2_infrastructure/_main.yml new file mode 100644 index 00000000..75f08b38 --- /dev/null +++ b/scenarios/test_apt_cacher/03_lan2_infrastructure/_main.yml @@ -0,0 +1,36 @@ +--- +## +## 03_lan2_infrastructure — generated by r42playbooks +## stage_00 clones VMs (per-VM global_* below); stage_01 installs software. +## + +#### STAGE 00 #### + +- import_playbook: ./stage_00/lan2-debian-jump-00.yml + vars: + global_vm_name: "lan2-debian-jump-00" + global_vm_ssh_name: "r42.lan2-debian-jump-00" + global_vm_id: 1012 + global_vm_description: "debian-jump" + global_vm_tag_name: "lan2" + global_vm_ci_ip: "192.168.151.12" + global_template_vm_id: 9321 + global_template_name: "template-vm-debian-trixie-small - id 9321" + +- import_playbook: ./stage_00/lan2-ubuntu-jump-00.yml + vars: + global_vm_name: "lan2-ubuntu-jump-00" + global_vm_ssh_name: "r42.lan2-ubuntu-jump-00" + global_vm_id: 1013 + global_vm_description: "ubuntu-jump" + global_vm_tag_name: "lan2" + global_vm_ci_ip: "192.168.151.13" + global_template_vm_id: 9521 + global_template_name: "template-vm-ubuntu-resolute-small-01-4g-32g - id 9521" + + +#### STAGE 01 #### + +- import_playbook: ./stage_01/lan2-debian-jump-00.yml + +- import_playbook: ./stage_01/lan2-ubuntu-jump-00.yml diff --git a/scenarios/test_apt_cacher/03_lan2_infrastructure/stage_00/lan2-debian-jump-00.yml b/scenarios/test_apt_cacher/03_lan2_infrastructure/stage_00/lan2-debian-jump-00.yml new file mode 100644 index 00000000..66e1c1f2 --- /dev/null +++ b/scenarios/test_apt_cacher/03_lan2_infrastructure/stage_00/lan2-debian-jump-00.yml @@ -0,0 +1,81 @@ +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# +# LAN2 INFRASTRUCTURE INIT - CLONE + CLOUD-INIT +# +# hostname : lan2-debian-jump-00 +# vm_id : 1012 +# ip : 192.168.151.12 +# template : template-vm-debian-trixie-small +# +# global_* vars are supplied by ../_main.yml (kept here as Jinja references). +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- hosts: proxmox + gather_facts: false + vars_files: + - "../../secrets/default_vault.yml" + + tasks: + - name: "LAN2 INFRASTRUCTURE INIT - {{ global_vm_name }} - CLONE TEMPLATE - {{ global_template_name }}" + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "vm_clone" + vm_id: "{{ global_template_vm_id }}" + vm_new_id: "{{ global_vm_id }}" + vm_name: "{{ global_vm_name }}" + vm_description: "{{ global_vm_description }}" + + - name: "LAN2 INFRASTRUCTURE INIT - {{ global_vm_name }} - WAIT FOR PROXMOX UNLOCK" + ansible.builtin.shell: qm config {{ global_vm_id }} | grep -q '^lock:' + register: clone_lock_check + until: clone_lock_check.rc != 0 + retries: 60 + delay: 5 + failed_when: false + changed_when: false + delegate_to: "{{ inventory_hostname }}-cli" + + - name: "LAN2 INFRASTRUCTURE INIT - {{ global_vm_name }} - SET PROXMOX TAG" + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "vm_set_tag" + vm_id: "{{ global_vm_id }}" + vm_tag_name: "{{ global_vm_tag_name }}" + + - name: "LAN2 INFRASTRUCTURE INIT - {{ global_vm_name }} - SET CLOUD-INIT VARIABLES" + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "cloudinit_set_variables" + vm_id: "{{ global_vm_id }}" + vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" + vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" + vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" + vm_ci_ip: "{{ global_vm_ci_ip }}" + vm_ci_netmask: "24" + vm_ci_ip_gw: "192.168.151.1" + vm_net_virtio_bridge: "vmbr151" + + - name: "LAN2 INFRASTRUCTURE INIT - {{ global_vm_name }} - START VM" + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "vm_start" + vm_id: "{{ global_vm_id }}" + +- hosts: "{{ global_vm_ssh_name }}" + gather_facts: false + vars_files: + - "../../secrets/default_vault.yml" + vars: + ARG_vm_ssh_name: "{{ global_vm_ssh_name }}" + requested_tasks: + - wait/openssh_server/is_reachable.yml + - wait/cloudinit/is_boot_finished.yml + tasks: + - name: "LAN2 INFRASTRUCTURE INIT - {{ global_vm_name }} - WAIT FOR SSH + CLOUD-INIT" + include_role: + name: ansible.utils diff --git a/scenarios/test_apt_cacher/03_lan2_infrastructure/stage_00/lan2-ubuntu-jump-00.yml b/scenarios/test_apt_cacher/03_lan2_infrastructure/stage_00/lan2-ubuntu-jump-00.yml new file mode 100644 index 00000000..b3bdcca7 --- /dev/null +++ b/scenarios/test_apt_cacher/03_lan2_infrastructure/stage_00/lan2-ubuntu-jump-00.yml @@ -0,0 +1,81 @@ +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# +# LAN2 INFRASTRUCTURE INIT - CLONE + CLOUD-INIT +# +# hostname : lan2-ubuntu-jump-00 +# vm_id : 1013 +# ip : 192.168.151.13 +# template : template-vm-ubuntu-resolute-small-01-4g-32g +# +# global_* vars are supplied by ../_main.yml (kept here as Jinja references). +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- hosts: proxmox + gather_facts: false + vars_files: + - "../../secrets/default_vault.yml" + + tasks: + - name: "LAN2 INFRASTRUCTURE INIT - {{ global_vm_name }} - CLONE TEMPLATE - {{ global_template_name }}" + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "vm_clone" + vm_id: "{{ global_template_vm_id }}" + vm_new_id: "{{ global_vm_id }}" + vm_name: "{{ global_vm_name }}" + vm_description: "{{ global_vm_description }}" + + - name: "LAN2 INFRASTRUCTURE INIT - {{ global_vm_name }} - WAIT FOR PROXMOX UNLOCK" + ansible.builtin.shell: qm config {{ global_vm_id }} | grep -q '^lock:' + register: clone_lock_check + until: clone_lock_check.rc != 0 + retries: 60 + delay: 5 + failed_when: false + changed_when: false + delegate_to: "{{ inventory_hostname }}-cli" + + - name: "LAN2 INFRASTRUCTURE INIT - {{ global_vm_name }} - SET PROXMOX TAG" + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "vm_set_tag" + vm_id: "{{ global_vm_id }}" + vm_tag_name: "{{ global_vm_tag_name }}" + + - name: "LAN2 INFRASTRUCTURE INIT - {{ global_vm_name }} - SET CLOUD-INIT VARIABLES" + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "cloudinit_set_variables" + vm_id: "{{ global_vm_id }}" + vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" + vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" + vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" + vm_ci_ip: "{{ global_vm_ci_ip }}" + vm_ci_netmask: "24" + vm_ci_ip_gw: "192.168.151.1" + vm_net_virtio_bridge: "vmbr151" + + - name: "LAN2 INFRASTRUCTURE INIT - {{ global_vm_name }} - START VM" + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "vm_start" + vm_id: "{{ global_vm_id }}" + +- hosts: "{{ global_vm_ssh_name }}" + gather_facts: false + vars_files: + - "../../secrets/default_vault.yml" + vars: + ARG_vm_ssh_name: "{{ global_vm_ssh_name }}" + requested_tasks: + - wait/openssh_server/is_reachable.yml + - wait/cloudinit/is_boot_finished.yml + tasks: + - name: "LAN2 INFRASTRUCTURE INIT - {{ global_vm_name }} - WAIT FOR SSH + CLOUD-INIT" + include_role: + name: ansible.utils diff --git a/scenarios/test_apt_cacher/03_lan2_infrastructure/stage_01/lan2-debian-jump-00.devkit/test_apt_cacher.lan2-debian-jump-00.install.sh b/scenarios/test_apt_cacher/03_lan2_infrastructure/stage_01/lan2-debian-jump-00.devkit/test_apt_cacher.lan2-debian-jump-00.install.sh new file mode 100755 index 00000000..01989c0c --- /dev/null +++ b/scenarios/test_apt_cacher/03_lan2_infrastructure/stage_01/lan2-debian-jump-00.devkit/test_apt_cacher.lan2-debian-jump-00.install.sh @@ -0,0 +1,8 @@ +#!/bin/bash +## +## reinstall software on lan2-debian-jump-00 only (fast single-VM iteration) +## + +ansible-playbook -i "${RANGE42_ANSIBLE_ROLES__INVENTORY_DIR}/inventory_default.yml" \ + -l "all" \ + "../lan2-debian-jump-00.yml" --vault-password-file "${RANGE42_VAULT_PASSWORD_FILE:?RANGE42_VAULT_PASSWORD_FILE is not set — run: range42-context use }" diff --git a/scenarios/test_apt_cacher/03_lan2_infrastructure/stage_01/lan2-debian-jump-00.devkit/test_apt_cacher.lan2-debian-jump-00.revert.sh b/scenarios/test_apt_cacher/03_lan2_infrastructure/stage_01/lan2-debian-jump-00.devkit/test_apt_cacher.lan2-debian-jump-00.revert.sh new file mode 100755 index 00000000..6caf2355 --- /dev/null +++ b/scenarios/test_apt_cacher/03_lan2_infrastructure/stage_01/lan2-debian-jump-00.devkit/test_apt_cacher.lan2-debian-jump-00.revert.sh @@ -0,0 +1,8 @@ +#!/bin/bash +## +## revert lan2-debian-jump-00 to its latest snapshot and restart it +## + +VM_ID=$(devkit_manifest.find_vm_id.to.text.sh "$0" "lan2-debian-jump-00") || exit 1 +echo "{\"proxmox_node\":\"pve\",\"vm_id\":${VM_ID} }" | proxmox_snapshot_vm.vm_id.revert_snapshot.to.jsons.sh +echo "{\"proxmox_node\":\"pve\",\"vm_id\":${VM_ID} }" | proxmox_vm.vm_id.start.to.jsons.sh diff --git a/scenarios/test_apt_cacher/03_lan2_infrastructure/stage_01/lan2-debian-jump-00.devkit/test_apt_cacher.lan2-debian-jump-00.snapshot.sh b/scenarios/test_apt_cacher/03_lan2_infrastructure/stage_01/lan2-debian-jump-00.devkit/test_apt_cacher.lan2-debian-jump-00.snapshot.sh new file mode 100755 index 00000000..71fb0c9a --- /dev/null +++ b/scenarios/test_apt_cacher/03_lan2_infrastructure/stage_01/lan2-debian-jump-00.devkit/test_apt_cacher.lan2-debian-jump-00.snapshot.sh @@ -0,0 +1,7 @@ +#!/bin/bash +## +## snapshot lan2-debian-jump-00 as "base" +## + +VM_ID=$(devkit_manifest.find_vm_id.to.text.sh "$0" "lan2-debian-jump-00") || exit 1 +echo "{\"proxmox_node\":\"pve\",\"vm_id\":${VM_ID},\"vm_snapshot_description\":\"base\"}" | proxmox_snapshot_vm.vm_id.create_snapshot.to.jsons.sh diff --git a/scenarios/test_apt_cacher/03_lan2_infrastructure/stage_01/lan2-debian-jump-00.yml b/scenarios/test_apt_cacher/03_lan2_infrastructure/stage_01/lan2-debian-jump-00.yml new file mode 100644 index 00000000..00ef4192 --- /dev/null +++ b/scenarios/test_apt_cacher/03_lan2_infrastructure/stage_01/lan2-debian-jump-00.yml @@ -0,0 +1,37 @@ +## +## stage_01 — software install for lan2-debian-jump-00 +## catalog roles are referenced BY NAME (resolved via ANSIBLE_ROLES_PATH at deploy); +## one play per attachment so each carries its own params (firewall_rules, docker …). +## + +- name: "lan2-debian-jump-00 — software.configure.firewalls" + hosts: r42.lan2-debian-jump-00 + become: true + vars_files: + - "../../secrets/default_vault.yml" + vars: + firewall_rules: + - ip: all + port: 22 + protocol: tcp + roles: + - software.configure.firewalls + +- name: "lan2-debian-jump-00 — software.install.warmup.basic_packages" + hosts: r42.lan2-debian-jump-00 + become: true + vars_files: + - "../../secrets/default_vault.yml" + roles: + - software.install.warmup.basic_packages + +- name: "lan2-debian-jump-00 — software.configure.apt_mirror_client" + hosts: r42.lan2-debian-jump-00 + become: true + vars_files: + - "../../secrets/default_vault.yml" + vars: + apt_mirror_enabled: true + apt_proxy_url: http://192.168.150.11:3142 + roles: + - software.configure.apt_mirror_client diff --git a/scenarios/test_apt_cacher/03_lan2_infrastructure/stage_01/lan2-ubuntu-jump-00.devkit/test_apt_cacher.lan2-ubuntu-jump-00.install.sh b/scenarios/test_apt_cacher/03_lan2_infrastructure/stage_01/lan2-ubuntu-jump-00.devkit/test_apt_cacher.lan2-ubuntu-jump-00.install.sh new file mode 100755 index 00000000..ee33ee9a --- /dev/null +++ b/scenarios/test_apt_cacher/03_lan2_infrastructure/stage_01/lan2-ubuntu-jump-00.devkit/test_apt_cacher.lan2-ubuntu-jump-00.install.sh @@ -0,0 +1,8 @@ +#!/bin/bash +## +## reinstall software on lan2-ubuntu-jump-00 only (fast single-VM iteration) +## + +ansible-playbook -i "${RANGE42_ANSIBLE_ROLES__INVENTORY_DIR}/inventory_default.yml" \ + -l "all" \ + "../lan2-ubuntu-jump-00.yml" --vault-password-file "${RANGE42_VAULT_PASSWORD_FILE:?RANGE42_VAULT_PASSWORD_FILE is not set — run: range42-context use }" diff --git a/scenarios/test_apt_cacher/03_lan2_infrastructure/stage_01/lan2-ubuntu-jump-00.devkit/test_apt_cacher.lan2-ubuntu-jump-00.revert.sh b/scenarios/test_apt_cacher/03_lan2_infrastructure/stage_01/lan2-ubuntu-jump-00.devkit/test_apt_cacher.lan2-ubuntu-jump-00.revert.sh new file mode 100755 index 00000000..fd1327bd --- /dev/null +++ b/scenarios/test_apt_cacher/03_lan2_infrastructure/stage_01/lan2-ubuntu-jump-00.devkit/test_apt_cacher.lan2-ubuntu-jump-00.revert.sh @@ -0,0 +1,8 @@ +#!/bin/bash +## +## revert lan2-ubuntu-jump-00 to its latest snapshot and restart it +## + +VM_ID=$(devkit_manifest.find_vm_id.to.text.sh "$0" "lan2-ubuntu-jump-00") || exit 1 +echo "{\"proxmox_node\":\"pve\",\"vm_id\":${VM_ID} }" | proxmox_snapshot_vm.vm_id.revert_snapshot.to.jsons.sh +echo "{\"proxmox_node\":\"pve\",\"vm_id\":${VM_ID} }" | proxmox_vm.vm_id.start.to.jsons.sh diff --git a/scenarios/test_apt_cacher/03_lan2_infrastructure/stage_01/lan2-ubuntu-jump-00.devkit/test_apt_cacher.lan2-ubuntu-jump-00.snapshot.sh b/scenarios/test_apt_cacher/03_lan2_infrastructure/stage_01/lan2-ubuntu-jump-00.devkit/test_apt_cacher.lan2-ubuntu-jump-00.snapshot.sh new file mode 100755 index 00000000..57ed6153 --- /dev/null +++ b/scenarios/test_apt_cacher/03_lan2_infrastructure/stage_01/lan2-ubuntu-jump-00.devkit/test_apt_cacher.lan2-ubuntu-jump-00.snapshot.sh @@ -0,0 +1,7 @@ +#!/bin/bash +## +## snapshot lan2-ubuntu-jump-00 as "base" +## + +VM_ID=$(devkit_manifest.find_vm_id.to.text.sh "$0" "lan2-ubuntu-jump-00") || exit 1 +echo "{\"proxmox_node\":\"pve\",\"vm_id\":${VM_ID},\"vm_snapshot_description\":\"base\"}" | proxmox_snapshot_vm.vm_id.create_snapshot.to.jsons.sh diff --git a/scenarios/test_apt_cacher/03_lan2_infrastructure/stage_01/lan2-ubuntu-jump-00.yml b/scenarios/test_apt_cacher/03_lan2_infrastructure/stage_01/lan2-ubuntu-jump-00.yml new file mode 100644 index 00000000..b6046f35 --- /dev/null +++ b/scenarios/test_apt_cacher/03_lan2_infrastructure/stage_01/lan2-ubuntu-jump-00.yml @@ -0,0 +1,37 @@ +## +## stage_01 — software install for lan2-ubuntu-jump-00 +## catalog roles are referenced BY NAME (resolved via ANSIBLE_ROLES_PATH at deploy); +## one play per attachment so each carries its own params (firewall_rules, docker …). +## + +- name: "lan2-ubuntu-jump-00 — software.configure.firewalls" + hosts: r42.lan2-ubuntu-jump-00 + become: true + vars_files: + - "../../secrets/default_vault.yml" + vars: + firewall_rules: + - ip: all + port: 22 + protocol: tcp + roles: + - software.configure.firewalls + +- name: "lan2-ubuntu-jump-00 — software.install.warmup.basic_packages" + hosts: r42.lan2-ubuntu-jump-00 + become: true + vars_files: + - "../../secrets/default_vault.yml" + roles: + - software.install.warmup.basic_packages + +- name: "lan2-ubuntu-jump-00 — software.configure.apt_mirror_client" + hosts: r42.lan2-ubuntu-jump-00 + become: true + vars_files: + - "../../secrets/default_vault.yml" + vars: + apt_mirror_enabled: true + apt_proxy_url: http://192.168.150.11:3142 + roles: + - software.configure.apt_mirror_client diff --git a/scenarios/test_apt_cacher/README.md b/scenarios/test_apt_cacher/README.md new file mode 100644 index 00000000..073d54fa --- /dev/null +++ b/scenarios/test_apt_cacher/README.md @@ -0,0 +1,25 @@ +# test_apt_cacher + +Generated by **r42playbooks**. + +- subnet layout: `dual-lan` + +> Test scenario: apt-cacher-ng on lan1, Debian+Ubuntu clients on lan2. No network policy. + +## Boxes + +| box | subnet | image | vm_id | ip | +|---|---|---|---|---| +| `lan1-apt-cache-00` | lan1 | debian_trixie | 2011 | 192.168.150.11 | +| `lan2-debian-jump-00` | lan2 | debian_trixie | 1012 | 192.168.151.12 | +| `lan2-ubuntu-jump-00` | lan2 | ubuntu_resolute | 1013 | 192.168.151.13 | + +## Deploy + +```bash +range42-context use test_apt_cacher # creates secrets/ symlink + vault +./test_apt_cacher.setup.sh # templates + VMs + software +``` + +The composition is reproduced in `scenario.r42.yml` — re-run +`r42playbooks new --spec scenario.r42.yml` to regenerate this tree. diff --git a/scenarios/test_apt_cacher/_activate.sh b/scenarios/test_apt_cacher/_activate.sh new file mode 100755 index 00000000..fbdff157 --- /dev/null +++ b/scenarios/test_apt_cacher/_activate.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +# deprecation algo issue with paramiko and ubuntu 24.04.2 LTS +# https://github.com/paramiko/paramiko/issues/2419 + +set -euo pipefail + +VIRTUAL_ENV_DIR="$HOME/ansible_fix/venv" + +if [ ! -d "$VIRTUAL_ENV_DIR" ]; then + + mkdir -p "$HOME/ansible_fix" + python3 -m venv "$VIRTUAL_ENV_DIR" + + # shellcheck disable=SC1091 + source "$VIRTUAL_ENV_DIR/bin/activate" + + pip install --upgrade pip setuptools wheel + pip install ansible paramiko cryptography +fi + +# shellcheck disable=SC1091 +source "$VIRTUAL_ENV_DIR/bin/activate" + +echo "source \"$VIRTUAL_ENV_DIR/bin/activate\"" diff --git a/scenarios/test_apt_cacher/devkit_ansible.show_ansible_inventory.to.text.sh b/scenarios/test_apt_cacher/devkit_ansible.show_ansible_inventory.to.text.sh new file mode 100755 index 00000000..29f83a48 --- /dev/null +++ b/scenarios/test_apt_cacher/devkit_ansible.show_ansible_inventory.to.text.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +ansible-inventory -i "${RANGE42_ANSIBLE_ROLES__INVENTORY_DIR}/inventory_default.yml" --graph diff --git a/scenarios/test_apt_cacher/main.yml b/scenarios/test_apt_cacher/main.yml new file mode 100644 index 00000000..ee25140c --- /dev/null +++ b/scenarios/test_apt_cacher/main.yml @@ -0,0 +1,9 @@ +--- +## +## test_apt_cacher — generated by r42playbooks +## + +- import_playbook: ./01_init_proxmox/_main.yml + +- import_playbook: ./02_lan1_infrastructure/_main.yml +- import_playbook: ./03_lan2_infrastructure/_main.yml diff --git a/scenarios/test_apt_cacher/main_vms_only.yml b/scenarios/test_apt_cacher/main_vms_only.yml new file mode 100644 index 00000000..ef20bb54 --- /dev/null +++ b/scenarios/test_apt_cacher/main_vms_only.yml @@ -0,0 +1,7 @@ +--- +## +## test_apt_cacher — deploy VMs only (templates already exist; skip 01_init_proxmox) +## + +- import_playbook: ./02_lan1_infrastructure/_main.yml +- import_playbook: ./03_lan2_infrastructure/_main.yml diff --git a/scenarios/test_apt_cacher/manifest/scenario_vms.json b/scenarios/test_apt_cacher/manifest/scenario_vms.json new file mode 100644 index 00000000..0a6ec5d7 --- /dev/null +++ b/scenarios/test_apt_cacher/manifest/scenario_vms.json @@ -0,0 +1,49 @@ +{ + "description": "Test scenario: apt-cacher-ng on lan1, Debian+Ubuntu clients on lan2. No network policy.", + "scenario": "test_apt_cacher", + "templates": [ + { + "bridge": "vmbr140", + "image": "debian_trixie", + "ip": "192.168.140.2", + "spec": "1cpu/4gb/32gb", + "vm_id": 9321, + "vm_name": "template-vm-debian-trixie-small" + }, + { + "bridge": "vmbr140", + "image": "ubuntu_resolute", + "ip": "192.168.140.3", + "spec": "1cpu/4gb/32gb", + "vm_id": 9521, + "vm_name": "template-vm-ubuntu-resolute-small-01-4g-32g" + } + ], + "version": 2, + "vms": [ + { + "bridge": "vmbr151", + "image": "debian_trixie", + "ip": "192.168.151.12", + "subnet": "lan2", + "vm_id": 1012, + "vm_name": "lan2-debian-jump-00" + }, + { + "bridge": "vmbr151", + "image": "ubuntu_resolute", + "ip": "192.168.151.13", + "subnet": "lan2", + "vm_id": 1013, + "vm_name": "lan2-ubuntu-jump-00" + }, + { + "bridge": "vmbr150", + "image": "debian_trixie", + "ip": "192.168.150.11", + "subnet": "lan1", + "vm_id": 2011, + "vm_name": "lan1-apt-cache-00" + } + ] +} diff --git a/scenarios/test_apt_cacher/scenario.r42.yml b/scenarios/test_apt_cacher/scenario.r42.yml new file mode 100644 index 00000000..73b8f9bd --- /dev/null +++ b/scenarios/test_apt_cacher/scenario.r42.yml @@ -0,0 +1,34 @@ +boxes: +- attachments_add: [] + count: 1 + octet: null + subnet: lan1 + template: apt-cache + template_vm_id: null + vars: {} +- attachments_add: [] + count: 1 + octet: null + subnet: lan2 + template: debian-jump + template_vm_id: null + vars: {} +- attachments_add: [] + count: 1 + octet: null + subnet: lan2 + template: ubuntu-jump + template_vm_id: null + vars: {} +name: test_apt_cacher +network_policy: null +notes: 'Test scenario: apt-cacher-ng on lan1, Debian+Ubuntu clients on lan2. No network + policy.' +proxmox_node: null +schema_version: 1 +services: + apt: + box: apt-cache + mode: proxy + wire_to: all +subnet_layout: dual-lan diff --git a/scenarios/test_apt_cacher/templates/ansible-inventory.j2 b/scenarios/test_apt_cacher/templates/ansible-inventory.j2 new file mode 100644 index 00000000..e0307d0b --- /dev/null +++ b/scenarios/test_apt_cacher/templates/ansible-inventory.j2 @@ -0,0 +1,23 @@ +all: + children: + range42_infrastructure: + children: + r42_lan1_group: + hosts: + r42.lan1-apt-cache-00: + + r42_lan2_group: + hosts: + r42.lan2-debian-jump-00: + r42.lan2-ubuntu-jump-00: + + proxmox: + hosts: + {{ INFRASTRUCTURE_CODENAME }}: + ansible_host: {{ INFRASTRUCTURE_PROXMOX_ADDRESS | mandatory }}:8006 + ansible_connection: local + ansible_python_interpreter: /usr/bin/python3 + + proxmox-cli: + hosts: + {{ INFRASTRUCTURE_CODENAME }}-cli: diff --git a/scenarios/test_apt_cacher/templates/ansible-vars.yml b/scenarios/test_apt_cacher/templates/ansible-vars.yml new file mode 100644 index 00000000..a402e7b2 --- /dev/null +++ b/scenarios/test_apt_cacher/templates/ansible-vars.yml @@ -0,0 +1,32 @@ +--- +################################################################################ +# range42 — scenario-specific variables (test_apt_cacher) +# +# Generated by r42playbooks. Shared variables -> group_vars/all/vars.yml, +# secrets -> vault (see vault-example.yml). +################################################################################ + + +#### SCENARIO IDENTIFICATION #### + +INFRASTRUCTURE_SCENARIO: "test_apt_cacher" + + +#### CREDENTIAL GENERATION #### + +context_auto_generate_ssh_keys: "YES" +context_auto_generate_vm_passwords: "YES" + +student_additionnal_keys_count: 5 + + +#### CLOUD-INIT USERNAMES #### + +default_admin_vm_ci_user: "alice" +default_trainee_vm_ci_user: "bob" + + +#### CLOUD-INIT DNS #### +# default resolver for lab VMs; range42-init.py substitutes the Proxmox node +# resolver here at workspace creation. 1.1.1.1 is the portable fallback. +default_vm_ci_dns_ips: "1.1.1.1" diff --git a/scenarios/test_apt_cacher/templates/ssh-config.j2 b/scenarios/test_apt_cacher/templates/ssh-config.j2 new file mode 100644 index 00000000..c7c96e34 --- /dev/null +++ b/scenarios/test_apt_cacher/templates/ssh-config.j2 @@ -0,0 +1,41 @@ +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# infrastructure code name : {{ INFRASTRUCTURE_CODENAME }} +# infrastructure proxmox address : {{ INFRASTRUCTURE_PROXMOX_ADDRESS }} +# scenario : {{ INFRASTRUCTURE_SCENARIO }} +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +#### PROXMOX WEB UI (port forward) #### +Host px.{{ INFRASTRUCTURE_CODENAME }}.redirect_www.proxmox + RequestTTY no + RemoteCommand none + localforward localhost:18042 {{ INFRASTRUCTURE_PROXMOX_ADDRESS | mandatory }}:8006 + +#### PX ROOT USER SSH ACCESS #### +Host px.{{ INFRASTRUCTURE_CODENAME }}-ssh_cli.root {{ INFRASTRUCTURE_CODENAME }}-cli + Hostname {{ INFRASTRUCTURE_PROXMOX_ADDRESS | mandatory }} + User root + IdentityFile {{ DEPLOYER_CLI__DST_SSH_KEYS_JUMP_DEST_DIR }}/px.{{ INFRASTRUCTURE_CODENAME }}-{{ INFRASTRUCTURE_SCENARIO }}-ssh_cli.root + Port 22 + +#### SSH JUMPER #### +Host px.{{ INFRASTRUCTURE_CODENAME }}.jumper + Hostname {{ INFRASTRUCTURE_PROXMOX_ADDRESS | mandatory }} + User jump_user + IdentityFile {{ DEPLOYER_CLI__DST_SSH_KEYS_JUMP_DEST_DIR }}/px.{{ INFRASTRUCTURE_CODENAME }}-{{ INFRASTRUCTURE_SCENARIO }}-ssh_cli.jump_user + Port 22 + +#### SCENARIO VMs #### +Host r42.lan1-apt-cache-00 + Hostname 192.168.150.11 + +Host r42.lan2-debian-jump-00 + Hostname 192.168.151.12 + +Host r42.lan2-ubuntu-jump-00 + Hostname 192.168.151.13 + +Host r42.* + User alice + IdentityFile {{ DEPLOYER_CLI__DST_SSH_KEYS_BACKEND_DEST_DIR }}/r42.{{ INFRASTRUCTURE_CODENAME }}-{{ INFRASTRUCTURE_SCENARIO }}-deployer-key_alice + Port 22 + ProxyJump px.{{ INFRASTRUCTURE_CODENAME }}.jumper diff --git a/scenarios/test_apt_cacher/templates/vault-example.yml b/scenarios/test_apt_cacher/templates/vault-example.yml new file mode 100644 index 00000000..8e622866 --- /dev/null +++ b/scenarios/test_apt_cacher/templates/vault-example.yml @@ -0,0 +1,37 @@ +--- +################################################################################ +# range42 — vault secrets (test_apt_cacher) +# +# Copy to vault.yml, fill real values, then: ansible-vault encrypt vault.yml +# Never commit the unencrypted version. +################################################################################ + + +#### PROXMOX API SECRET #### +proxmox_api_token_secret: "REPLACE_ME" + + +#### JUMP HOST #### +jump_password: "REPLACE_ME" + + +#### CLOUD-INIT PASSWORDS #### +default_admin_vm_ci_password: "REPLACE_ME" +default_admin_vm_ci_ssh_key: "ssh-ed25519 AAAA... alice CODENAME-SCENARIO" +default_trainee_vm_ci_password: "REPLACE_ME" +default_trainee_vm_ci_ssh_key: "ssh-ed25519 AAAA... bob CODENAME-SCENARIO" + + +#### CLOUD-INIT DNS #### +# default resolver for lab VMs; range42-init.py substitutes the Proxmox node +# resolver here at workspace creation. 1.1.1.1 is the portable fallback. +default_vm_ci_dns_ips: "1.1.1.1" + + +#### TAILSCALE #### +infrastructure_tailscale_authkey: "tskey-auth-REPLACE_ME" +infrastructure_tailscale_apikey: "tskey-api-REPLACE_ME" + + +#### MISC #### +deployer_cli_user_ssh_known_hosts: "/home/your_deployer_cli_username/.ssh/known_hosts" diff --git a/scenarios/test_apt_cacher/test_apt_cacher.delete_all.sh b/scenarios/test_apt_cacher/test_apt_cacher.delete_all.sh new file mode 100755 index 00000000..b2d0b680 --- /dev/null +++ b/scenarios/test_apt_cacher/test_apt_cacher.delete_all.sh @@ -0,0 +1,39 @@ +#!/bin/bash + +## +## delete all — VMs + this scenario's ubuntu_noble templates +## +## ⚠ templates (9xxx) are shared across scenarios on the same Proxmox. Run this +## only when no other scenario relies on them, or use delete_vms_only.sh instead. +## + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +MANIFEST="$SCRIPT_DIR/manifest/scenario_vms.json" + +if [[ ! -f "$MANIFEST" ]]; then + echo "ERROR: manifest not found: $MANIFEST" >&2 + exit 1 +fi + +mapfile -t SCENARIO_VM_IDS < <(jq -r '.vms[].vm_id' "$MANIFEST") +mapfile -t TEMPLATE_VM_IDS < <(jq -r '.templates[].vm_id' "$MANIFEST") +mapfile -t INFRASTRUCTURE_IP < <(jq -r '.vms[].ip' "$MANIFEST") + +ALL_IDS=("${SCENARIO_VM_IDS[@]}" "${TEMPLATE_VM_IDS[@]}") +ID_REGEX=$(printf '|%s' "${ALL_IDS[@]}" | sed 's/^|//') + +echo ":: stopping and deleting VMs + templates" +VM_LIST_JSON=$(proxmox_vm.list.to.jsons.sh 2>&1 | grep '"vm_id":[0-9]') +if [ -z "$VM_LIST_JSON" ]; then + echo "ERROR: proxmox_vm.list.to.jsons.sh returned no VM data — aborting" >&2 + exit 1 +fi +echo "$VM_LIST_JSON" | jq -c | grep -E "\"vm_id\":($ID_REGEX)([^0-9]|$)" | proxmox_vm.vm_id.stop_force.to.jsons.sh +echo "$VM_LIST_JSON" | jq -c | grep -E "\"vm_id\":($ID_REGEX)([^0-9]|$)" | proxmox_vm.vm_id.delete.to.jsons.sh + +for ip in "${INFRASTRUCTURE_IP[@]}"; do + echo ":: REMOVE SSH KEY FOR : $ip" + ssh-keygen -f "$HOME/.ssh/known_hosts" -R "$ip" +done + +echo ":: done — VMs and templates removed" diff --git a/scenarios/test_apt_cacher/test_apt_cacher.delete_vms_only.sh b/scenarios/test_apt_cacher/test_apt_cacher.delete_vms_only.sh new file mode 100755 index 00000000..ba2443a3 --- /dev/null +++ b/scenarios/test_apt_cacher/test_apt_cacher.delete_vms_only.sh @@ -0,0 +1,33 @@ +#!/bin/bash + +## +## delete VMs only — scenario VMs (filter by vm_id), keep templates +## + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +MANIFEST="$SCRIPT_DIR/manifest/scenario_vms.json" + +if [[ ! -f "$MANIFEST" ]]; then + echo "ERROR: manifest not found: $MANIFEST" >&2 + exit 1 +fi + +mapfile -t SCENARIO_VM_IDS < <(jq -r '.vms[].vm_id' "$MANIFEST") +mapfile -t INFRASTRUCTURE_IP < <(jq -r '.vms[].ip' "$MANIFEST") +ID_REGEX=$(printf '|%s' "${SCENARIO_VM_IDS[@]}" | sed 's/^|//') + +echo ":: stopping and deleting scenario VMs (keeping templates)..." +VM_LIST_JSON=$(proxmox_vm.list.to.jsons.sh 2>&1 | grep '"vm_id":[0-9]') +if [ -z "$VM_LIST_JSON" ]; then + echo "ERROR: proxmox_vm.list.to.jsons.sh returned no VM data — aborting" >&2 + exit 1 +fi +echo "$VM_LIST_JSON" | jq -c | grep -E "\"vm_id\":($ID_REGEX)([^0-9]|$)" | proxmox_vm.vm_id.stop_force.to.jsons.sh +echo "$VM_LIST_JSON" | jq -c | grep -E "\"vm_id\":($ID_REGEX)([^0-9]|$)" | proxmox_vm.vm_id.delete.to.jsons.sh + +for ip in "${INFRASTRUCTURE_IP[@]}"; do + echo ":: REMOVE SSH KEY FOR : $ip" + ssh-keygen -f "$HOME/.ssh/known_hosts" -R "$ip" +done + +echo ":: done — templates preserved" diff --git a/scenarios/test_apt_cacher/test_apt_cacher.reset.setup.sh b/scenarios/test_apt_cacher/test_apt_cacher.reset.setup.sh new file mode 100755 index 00000000..b70309e0 --- /dev/null +++ b/scenarios/test_apt_cacher/test_apt_cacher.reset.setup.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +## +## reset — delete this scenario's VMs (from manifest) then re-deploy +## + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +MANIFEST="$SCRIPT_DIR/manifest/scenario_vms.json" + +if [[ ! -f "$MANIFEST" ]]; then + echo "ERROR: manifest not found: $MANIFEST" >&2 + exit 1 +fi + +mapfile -t SCENARIO_VM_IDS < <(jq -r '.vms[].vm_id' "$MANIFEST") +mapfile -t INFRASTRUCTURE_IP < <(jq -r '.vms[].ip' "$MANIFEST") +ID_REGEX=$(printf '|%s' "${SCENARIO_VM_IDS[@]}" | sed 's/^|//') + +echo ":: stopping and deleting scenario VMs (vm_ids: ${SCENARIO_VM_IDS[*]})..." +VM_LIST_JSON=$(proxmox_vm.list.to.jsons.sh 2>&1 | grep '"vm_id":[0-9]') +if [ -z "$VM_LIST_JSON" ]; then + echo "ERROR: proxmox_vm.list.to.jsons.sh returned no VM data — aborting" >&2 + exit 1 +fi +echo "$VM_LIST_JSON" | jq -c | grep -E "\"vm_id\":($ID_REGEX)([^0-9]|$)" | proxmox_vm.vm_id.stop_force.to.jsons.sh +echo "$VM_LIST_JSON" | jq -c | grep -E "\"vm_id\":($ID_REGEX)([^0-9]|$)" | proxmox_vm.vm_id.delete.to.jsons.sh + +for ip in "${INFRASTRUCTURE_IP[@]}"; do + echo ":: REMOVE SSH KEY FOR : $ip" + ssh-keygen -f "$HOME/.ssh/known_hosts" -R "$ip" +done + +ansible-playbook -i "${RANGE42_ANSIBLE_ROLES__INVENTORY_DIR}/inventory_default.yml" \ + -l "all" \ + "./main.yml" --vault-password-file "${RANGE42_VAULT_PASSWORD_FILE:?RANGE42_VAULT_PASSWORD_FILE is not set — run: range42-context use }" diff --git a/scenarios/test_apt_cacher/test_apt_cacher.reset.ssh_keys.sh b/scenarios/test_apt_cacher/test_apt_cacher.reset.ssh_keys.sh new file mode 100755 index 00000000..bea1d07b --- /dev/null +++ b/scenarios/test_apt_cacher/test_apt_cacher.reset.ssh_keys.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +## +## remove known_hosts entries for every VM IP in this scenario (from manifest) +## + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +MANIFEST="$SCRIPT_DIR/manifest/scenario_vms.json" + +if [[ ! -f "$MANIFEST" ]]; then + echo "ERROR: manifest not found: $MANIFEST" >&2 + exit 1 +fi + +mapfile -t INFRASTRUCTURE_IP < <(jq -r '.vms[].ip' "$MANIFEST") + +for ip in "${INFRASTRUCTURE_IP[@]}"; do + echo ":: REMOVE SSH KEY FOR : $ip" + ssh-keygen -f "$HOME/.ssh/known_hosts" -R "$ip" +done diff --git a/scenarios/test_apt_cacher/test_apt_cacher.setup.sh b/scenarios/test_apt_cacher/test_apt_cacher.setup.sh new file mode 100755 index 00000000..aff48748 --- /dev/null +++ b/scenarios/test_apt_cacher/test_apt_cacher.setup.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +ansible-playbook -i "${RANGE42_ANSIBLE_ROLES__INVENTORY_DIR}/inventory_default.yml" \ + -l "all" \ + "./main.yml" --vault-password-file "${RANGE42_VAULT_PASSWORD_FILE:?RANGE42_VAULT_PASSWORD_FILE is not set — run: range42-context use }" diff --git a/scenarios/test_apt_cacher/test_apt_cacher.setup_vms_only.sh b/scenarios/test_apt_cacher/test_apt_cacher.setup_vms_only.sh new file mode 100755 index 00000000..f7f81aa4 --- /dev/null +++ b/scenarios/test_apt_cacher/test_apt_cacher.setup_vms_only.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +## +## deploy VMs only — skip template download and creation +## faster redeploy when templates already exist on proxmox +## + +ansible-playbook -i "${RANGE42_ANSIBLE_ROLES__INVENTORY_DIR}/inventory_default.yml" \ + -l "all" \ + "./main_vms_only.yml" --vault-password-file "${RANGE42_VAULT_PASSWORD_FILE:?RANGE42_VAULT_PASSWORD_FILE is not set — run: range42-context use }" diff --git a/scenarios/test_apt_mirror/01_init_proxmox/_main.reinstall.sh b/scenarios/test_apt_mirror/01_init_proxmox/_main.reinstall.sh new file mode 100755 index 00000000..adcd14a3 --- /dev/null +++ b/scenarios/test_apt_mirror/01_init_proxmox/_main.reinstall.sh @@ -0,0 +1,8 @@ +#!/bin/bash +## +## re-run 01_init_proxmox (download images + create templates) +## + +ansible-playbook -i "${RANGE42_ANSIBLE_ROLES__INVENTORY_DIR}/inventory_default.yml" \ + -l "proxmox" \ + "./_main.yml" --vault-password-file "${RANGE42_VAULT_PASSWORD_FILE:?RANGE42_VAULT_PASSWORD_FILE is not set — run: range42-context use }" diff --git a/scenarios/test_apt_mirror/01_init_proxmox/_main.yml b/scenarios/test_apt_mirror/01_init_proxmox/_main.yml new file mode 100644 index 00000000..40a81665 --- /dev/null +++ b/scenarios/test_apt_mirror/01_init_proxmox/_main.yml @@ -0,0 +1,2 @@ +- import_playbook: ./stage_00-download_cloudinit_files/_main.yml +- import_playbook: ./stage_01-create_templates/_main.yml diff --git a/scenarios/test_apt_mirror/01_init_proxmox/stage_00-download_cloudinit_files/_main.yml b/scenarios/test_apt_mirror/01_init_proxmox/stage_00-download_cloudinit_files/_main.yml new file mode 100644 index 00000000..ba0f4ca9 --- /dev/null +++ b/scenarios/test_apt_mirror/01_init_proxmox/stage_00-download_cloudinit_files/_main.yml @@ -0,0 +1,6 @@ +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# PROMOX INIT - download cloud-init base images (only the OS families used) +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- import_playbook: ./cloudinit_debian_trixie.yml +- import_playbook: ./cloudinit_ubuntu_resolute.yml diff --git a/scenarios/test_apt_mirror/01_init_proxmox/stage_00-download_cloudinit_files/cloudinit_debian_trixie.yml b/scenarios/test_apt_mirror/01_init_proxmox/stage_00-download_cloudinit_files/cloudinit_debian_trixie.yml new file mode 100644 index 00000000..633cfec3 --- /dev/null +++ b/scenarios/test_apt_mirror/01_init_proxmox/stage_00-download_cloudinit_files/cloudinit_debian_trixie.yml @@ -0,0 +1,39 @@ +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# +# PROXMOX INIT - download cloud init image for debian_trixie +# +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- hosts: proxmox + gather_facts: false + vars_files: + - "../../secrets/default_vault.yml" + + tasks: + - name: PROMOX INIT - DOWNLOAD - CLOUD INIT images + include_role: + name: range42-ansible_roles-proxmox_controller + + vars: + proxmox_vm_action: "storage_download_iso" + proxmox_storage: "local" + iso_file_content_type: "iso" + iso_url: "https://cloud.debian.org/images/cloud/trixie/latest/debian-13-genericcloud-amd64.raw" + iso_file_name: "debian-13-genericcloud-amd64.img" + +# The download-url API is async (returns a UPID immediately). Poll on +# proxmox-cli until the file is fully on disk before stage_01 imports it. +- hosts: proxmox-cli + gather_facts: false + vars_files: + - "../../secrets/default_vault.yml" + + tasks: + - name: PROMOX INIT - WAIT for debian-13-genericcloud-amd64.img to be fully downloaded + ansible.builtin.stat: + path: "/var/lib/vz/template/iso/debian-13-genericcloud-amd64.img" + register: _cloudinit_img + until: _cloudinit_img.stat.exists and _cloudinit_img.stat.size > 100000000 + retries: 120 # 120 x 10 s = 20 min max + delay: 10 + changed_when: false diff --git a/scenarios/test_apt_mirror/01_init_proxmox/stage_00-download_cloudinit_files/cloudinit_ubuntu_resolute.yml b/scenarios/test_apt_mirror/01_init_proxmox/stage_00-download_cloudinit_files/cloudinit_ubuntu_resolute.yml new file mode 100644 index 00000000..67a10780 --- /dev/null +++ b/scenarios/test_apt_mirror/01_init_proxmox/stage_00-download_cloudinit_files/cloudinit_ubuntu_resolute.yml @@ -0,0 +1,39 @@ +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# +# PROXMOX INIT - download cloud init image for ubuntu_resolute +# +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- hosts: proxmox + gather_facts: false + vars_files: + - "../../secrets/default_vault.yml" + + tasks: + - name: PROMOX INIT - DOWNLOAD - CLOUD INIT images + include_role: + name: range42-ansible_roles-proxmox_controller + + vars: + proxmox_vm_action: "storage_download_iso" + proxmox_storage: "local" + iso_file_content_type: "iso" + iso_url: "https://cloud-images.ubuntu.com/minimal/daily/resolute/current/resolute-minimal-cloudimg-amd64.img" + iso_file_name: "resolute-minimal-cloudimg-amd64.img" + +# The download-url API is async (returns a UPID immediately). Poll on +# proxmox-cli until the file is fully on disk before stage_01 imports it. +- hosts: proxmox-cli + gather_facts: false + vars_files: + - "../../secrets/default_vault.yml" + + tasks: + - name: PROMOX INIT - WAIT for resolute-minimal-cloudimg-amd64.img to be fully downloaded + ansible.builtin.stat: + path: "/var/lib/vz/template/iso/resolute-minimal-cloudimg-amd64.img" + register: _cloudinit_img + until: _cloudinit_img.stat.exists and _cloudinit_img.stat.size > 100000000 + retries: 120 # 120 x 10 s = 20 min max + delay: 10 + changed_when: false diff --git a/scenarios/test_apt_mirror/01_init_proxmox/stage_01-create_templates/_main.yml b/scenarios/test_apt_mirror/01_init_proxmox/stage_01-create_templates/_main.yml new file mode 100644 index 00000000..5796c564 --- /dev/null +++ b/scenarios/test_apt_mirror/01_init_proxmox/stage_01-create_templates/_main.yml @@ -0,0 +1,7 @@ +--- +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# PROMOX INIT - create the Proxmox template images (only the OS families used) +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- import_playbook: ./templates/debian_trixie/_main_debian_trixie.yml +- import_playbook: ./templates/ubuntu_resolute/_main_ubuntu_resolute.yml diff --git a/scenarios/test_apt_mirror/01_init_proxmox/stage_01-create_templates/templates/debian_trixie/_apply_apt_proxy.yml b/scenarios/test_apt_mirror/01_init_proxmox/stage_01-create_templates/templates/debian_trixie/_apply_apt_proxy.yml new file mode 100644 index 00000000..0f49ba86 --- /dev/null +++ b/scenarios/test_apt_mirror/01_init_proxmox/stage_01-create_templates/templates/debian_trixie/_apply_apt_proxy.yml @@ -0,0 +1,52 @@ +## +## debian_trixie - apt proxy via cloud-init cicustom (optional) +## +## Attaches /var/lib/vz/snippets/range42-apt-proxy.yaml as cloud-init vendor-data +## to every template VM. All VMs cloned from these templates inherit the apt +## proxy automatically via cloud-init at first boot. +## Skipped (no-op) if apt_proxy_url is empty/unset. Idempotent. +## + +- hosts: proxmox-cli + gather_facts: false + vars_files: + - "../../../../secrets/default_vault.yml" + + vars: + bs2_template_vm_ids: + - 9321 # template-vm-debian-trixie-small + - 9341 # template-vm-debian-trixie-large + + tasks: + - name: APT-PROXY - SKIP (apt_proxy_url is empty) + ansible.builtin.debug: + msg: "apt_proxy_url is empty - cloud-init cicustom not applied to templates" + when: apt_proxy_url is not defined or (apt_proxy_url | length) == 0 + + - name: APT-PROXY - ATTACH cicustom vendor TO TEMPLATES + ansible.builtin.command: + cmd: qm set {{ item }} --cicustom "vendor=local:snippets/range42-apt-proxy.yaml" + loop: "{{ bs2_template_vm_ids }}" + register: qm_result + changed_when: qm_result.rc == 0 + failed_when: + - qm_result.rc != 0 + - "'does not exist' not in qm_result.stderr" + when: apt_proxy_url is defined and (apt_proxy_url | length) > 0 + + - name: APT-PROXY - DETACH cicustom vendor FROM TEMPLATES (apt_proxy_url unset) + ansible.builtin.command: + cmd: qm set {{ item }} --delete cicustom + loop: "{{ bs2_template_vm_ids }}" + register: qm_unset + changed_when: qm_unset.rc == 0 + failed_when: + - qm_unset.rc != 0 + - "'does not exist' not in qm_unset.stderr" + when: apt_proxy_url is not defined or (apt_proxy_url | length) == 0 + + - name: APT-PROXY - DISPLAY STATUS + ansible.builtin.debug: + msg: | + apt proxy: {{ apt_proxy_url if (apt_proxy_url is defined and (apt_proxy_url | length) > 0) else 'DISABLED' }} + cicustom : {{ 'attached to ' + (bs2_template_vm_ids | length | string) + ' templates' if (apt_proxy_url is defined and (apt_proxy_url | length) > 0) else 'detached from templates' }} diff --git a/scenarios/test_apt_mirror/01_init_proxmox/stage_01-create_templates/templates/debian_trixie/_main_debian_trixie.yml b/scenarios/test_apt_mirror/01_init_proxmox/stage_01-create_templates/templates/debian_trixie/_main_debian_trixie.yml new file mode 100644 index 00000000..547727e3 --- /dev/null +++ b/scenarios/test_apt_mirror/01_init_proxmox/stage_01-create_templates/templates/debian_trixie/_main_debian_trixie.yml @@ -0,0 +1,13 @@ +## +## debian_trixie — create + configure all Proxmox template VMs for this image +## + +- import_playbook: ./template-vm-debian-trixie-small.yml +- import_playbook: ./template-vm-debian-trixie-large.yml + +# apt proxy cicustom (no-op if apt_proxy_url empty) +- import_playbook: ./_apply_apt_proxy.yml + +# pre-update: start each VM, run apt update + dist-upgrade via cloud-init poweroff, +# then convert to template. +- import_playbook: ./_update_templates.yml diff --git a/scenarios/test_apt_mirror/01_init_proxmox/stage_01-create_templates/templates/debian_trixie/_update_templates.yml b/scenarios/test_apt_mirror/01_init_proxmox/stage_01-create_templates/templates/debian_trixie/_update_templates.yml new file mode 100644 index 00000000..2455c653 --- /dev/null +++ b/scenarios/test_apt_mirror/01_init_proxmox/stage_01-create_templates/templates/debian_trixie/_update_templates.yml @@ -0,0 +1,202 @@ +## +## debian_trixie - pre-update templates +## +## After all templates are created (but BEFORE convert to template), this playbook: +## 1. renders a temporary bootstrap snippet directly on the Proxmox host +## (apt update + apt dist-upgrade + cloud-init clean + auto-poweroff) +## 2. attaches it as cicustom vendor on each template VM +## 3. starts all templates +## 4. polls qm status until each VM is stopped (cloud-init auto-poweroff signals end) +## 5. detaches the bootstrap snippet, re-attaches the persistent apt-proxy snippet +## 6. converts each VM to a template (qm template ) +## +## Reads VM IDs from the scenario manifest (manifest/scenario_vms.json). +## Runs entirely on `proxmox-cli` (= the Proxmox host itself), no SSH involved. +## + +- hosts: proxmox-cli + gather_facts: false + vars_files: + - "../../../../secrets/default_vault.yml" + + vars: + manifest_path: "{{ playbook_dir }}/../../../../manifest/scenario_vms.json" + bootstrap_snippet_template: "{{ playbook_dir }}/range42-template-bootstrap.yaml.j2" + bootstrap_snippet_path: "/var/lib/vz/snippets/range42-template-bootstrap.yaml" + + tasks: + + #### #### #### debug apt_proxy_url visibility - fully safe vs undefined #### #### #### + + - name: TEMPLATES UPDATE - DEBUG apt_proxy_url visibility + ansible.builtin.debug: + msg: | + apt_proxy_url defined : {{ apt_proxy_url is defined }} + apt_proxy_url value : {{ apt_proxy_url | default('(undefined)') }} + apt_proxy_url length : {{ (apt_proxy_url | default('')) | length }} + -> snippet WILL include apt section : {{ (apt_proxy_url | default('')) | length > 0 }} + + #### #### #### load manifest #### #### #### + + - name: TEMPLATES UPDATE - LOAD MANIFEST + ansible.builtin.set_fact: + templates: "{{ (lookup('file', manifest_path) | from_json).templates }}" + + #### #### #### IDEMPOTENCE - probe each template's current state #### #### #### + # 3 possible states per VM ID : + # - "template" : already converted, skip update entirely + # - "vm" : exists as a regular VM, needs update + convert + # - "missing" : VM doesn't exist on Proxmox, skip (templates not created yet) + # Only "vm" state proceeds through the start/update/shutdown/convert pipeline. + + - name: TEMPLATES UPDATE - PROBE state of each template VM + ansible.builtin.shell: | + if qm config {{ item.vm_id }} 2>/dev/null | grep -q '^template:'; then + echo "template" + elif qm config {{ item.vm_id }} >/dev/null 2>&1; then + echo "vm" + else + echo "missing" + fi + register: tpl_state + loop: "{{ templates }}" + loop_control: + label: "vm_id={{ item.vm_id }}" + changed_when: false + + - name: TEMPLATES UPDATE - BUILD list of templates needing update (state == 'vm') + ansible.builtin.set_fact: + templates_to_update: >- + {{ templates | zip(tpl_state.results) + | selectattr('1.stdout', 'eq', 'vm') + | map(attribute='0') | list }} + + - name: TEMPLATES UPDATE - DISPLAY PLAN + ansible.builtin.debug: + msg: | + total templates in manifest : {{ templates | length }} + already converted (skip) : {{ tpl_state.results | selectattr('stdout', 'eq', 'template') | list | length }} + missing (skip) : {{ tpl_state.results | selectattr('stdout', 'eq', 'missing') | list | length }} + to update + convert : {{ templates_to_update | length }} + {% if templates_to_update | length > 0 %} + targets : + {% for t in templates_to_update %} - {{ t.vm_id }} {{ t.vm_name }} ({{ t.ip }}) + {% endfor %} + {% endif %} + + - name: TEMPLATES UPDATE - SHORT-CIRCUIT if nothing to update + ansible.builtin.meta: end_play + when: templates_to_update | length == 0 + + #### #### #### render bootstrap snippet directly on the Proxmox host #### #### #### + + - name: TEMPLATES UPDATE - RENDER BOOTSTRAP SNIPPET ON PROXMOX + ansible.builtin.template: + src: "{{ bootstrap_snippet_template }}" + dest: "{{ bootstrap_snippet_path }}" + mode: "0644" + + #### #### #### attach bootstrap snippet on every template #### #### #### + + - name: TEMPLATES UPDATE - ATTACH bootstrap cicustom + ansible.builtin.command: + cmd: qm set {{ item.vm_id }} --cicustom "vendor=local:snippets/range42-template-bootstrap.yaml" + loop: "{{ templates_to_update }}" + loop_control: + label: "vm_id={{ item.vm_id }}" + + #### #### #### start all templates #### #### #### + + - name: TEMPLATES UPDATE - START all templates + ansible.builtin.command: + cmd: qm start {{ item.vm_id }} + register: start_result + failed_when: + - start_result.rc != 0 + - "'already running' not in start_result.stderr" + - "'MAX' not in start_result.stderr" + changed_when: start_result.rc == 0 + loop: "{{ templates_to_update }}" + loop_control: + label: "vm_id={{ item.vm_id }}" + + # VMs that failed with "MAX X vcpus" couldn't start - exclude them from + # the wait/convert pipeline but keep the play running for the others. + - name: TEMPLATES UPDATE - BUILD started list (exclude CPU-limited VMs) + ansible.builtin.set_fact: + templates_started: >- + {{ templates_to_update | zip(start_result.results) + | rejectattr('1.stderr', 'search', 'MAX') + | map(attribute='0') | list }} + templates_skipped_cpu: >- + {{ templates_to_update | zip(start_result.results) + | selectattr('1.stderr', 'search', 'MAX') + | map(attribute='0') | list }} + + - name: TEMPLATES UPDATE - WARN skipped templates (host CPU limit) + ansible.builtin.debug: + msg: | + WARNING: {{ templates_skipped_cpu | length }} template(s) skipped - host CPU limit: + {% for t in templates_skipped_cpu %} + - {{ t.vm_id }} {{ t.vm_name }} (spec: {{ t.spec }}) + {% endfor %} + These templates require more vCPUs than this host provides. + They will NOT be converted. Run on a host with enough physical + CPUs, or lower vm_cores in the template playbook if intentional. + when: templates_skipped_cpu | length > 0 + + #### #### #### wait until each VM auto-poweroffs (cloud-init done) #### #### #### + # the loop is sequential per-vm, but since all started in parallel above, + # total time is bounded by the slowest template (~3-15 min depending on cache) + + - name: TEMPLATES UPDATE - WAIT for cloud-init to auto-poweroff + ansible.builtin.shell: | + qm status {{ item.vm_id }} | grep -q 'status: stopped' + register: stop_check + retries: 360 # safety cap : 360 x 5s = 30 min max per VM + delay: 5 + until: stop_check.rc == 0 + loop: "{{ templates_started }}" + loop_control: + label: "vm_id={{ item.vm_id }}" + changed_when: false + + #### #### #### detach bootstrap, re-attach apt-proxy, convert to template #### #### #### + + - name: TEMPLATES UPDATE - DETACH bootstrap cicustom + ansible.builtin.command: + cmd: qm set {{ item.vm_id }} --delete cicustom + loop: "{{ templates_started }}" + loop_control: + label: "vm_id={{ item.vm_id }}" + + - name: TEMPLATES UPDATE - RE-ATTACH apt-proxy cicustom (persistent for clones) + ansible.builtin.command: + cmd: qm set {{ item.vm_id }} --cicustom "vendor=local:snippets/range42-apt-proxy.yaml" + loop: "{{ templates_started }}" + loop_control: + label: "vm_id={{ item.vm_id }}" + when: (apt_proxy_url | default('')) | length > 0 + + - name: TEMPLATES UPDATE - CONVERT VM to template + ansible.builtin.command: + cmd: qm template {{ item.vm_id }} + loop: "{{ templates_started }}" + loop_control: + label: "vm_id={{ item.vm_id }}" + + #### #### #### cleanup #### #### #### + + - name: TEMPLATES UPDATE - REMOVE BOOTSTRAP SNIPPET FROM PROXMOX + ansible.builtin.file: + path: "{{ bootstrap_snippet_path }}" + state: absent + + - name: TEMPLATES UPDATE - DISPLAY DONE + ansible.builtin.debug: + msg: | + {{ templates_started | length }} templates updated and converted + apt-proxy cicustom re-attached (if apt_proxy_url set) + {% if templates_skipped_cpu | length > 0 %} + {{ templates_skipped_cpu | length }} skipped (host CPU limit) - see warning above + {% endif %} diff --git a/scenarios/test_apt_mirror/01_init_proxmox/stage_01-create_templates/templates/debian_trixie/range42-template-bootstrap.yaml.j2 b/scenarios/test_apt_mirror/01_init_proxmox/stage_01-create_templates/templates/debian_trixie/range42-template-bootstrap.yaml.j2 new file mode 100644 index 00000000..46990999 --- /dev/null +++ b/scenarios/test_apt_mirror/01_init_proxmox/stage_01-create_templates/templates/debian_trixie/range42-template-bootstrap.yaml.j2 @@ -0,0 +1,22 @@ +#cloud-config +# range42 - TEMPORARY bootstrap snippet for templates +# Triggers apt update + dist-upgrade then poweroff. +# Auto-generated by _update_templates.yml. Detached after the template is +# converted, replaced by range42-apt-proxy.yaml (persistent). +# +# NOTE: cloud-init clean was previously here in `runcmd` - REMOVED because +# it ran in cloud_config stage, before cloud_final, and broke +# package-update-upgrade-install. Clones get a new vmid -> new instance-id, +# so cloud-init re-processes naturally on each clone. No clean needed. +# +{% if (apt_proxy_url | default('')) | length > 0 %} +apt: + http_proxy: "{{ apt_proxy_url }}" +{% endif %} +package_update: true +package_upgrade: true +package_reboot_if_required: false +power_state: + mode: poweroff + timeout: 60 + condition: True diff --git a/scenarios/test_apt_mirror/01_init_proxmox/stage_01-create_templates/templates/debian_trixie/template-vm-debian-trixie-large.yml b/scenarios/test_apt_mirror/01_init_proxmox/stage_01-create_templates/templates/debian_trixie/template-vm-debian-trixie-large.yml new file mode 100644 index 00000000..0c40a565 --- /dev/null +++ b/scenarios/test_apt_mirror/01_init_proxmox/stage_01-create_templates/templates/debian_trixie/template-vm-debian-trixie-large.yml @@ -0,0 +1,134 @@ +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# +# PROXMOX INIT - create debian_trixie template VM template-vm-debian-trixie-large (id 9341) +# +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- hosts: proxmox + gather_facts: false + vars_files: + - "../../../../secrets/default_vault.yml" + + tasks: + #### IDEMPOTENCE - skip if already finalized as template #### + - name: TEMPLATE 9341 - CHECK if already finalized as template + ansible.builtin.shell: qm config 9341 2>/dev/null | grep -q '^template:' + register: tpl_9341_is_template + failed_when: false + changed_when: false + delegate_to: "{{ inventory_hostname }}-cli" + + - name: TEMPLATE 9341 - WAIT for unlock if a parallel deploy holds the VM + ansible.builtin.shell: qm config 9341 | grep -q '^lock:' + register: tpl_9341_lock + until: tpl_9341_lock.rc != 0 + retries: 30 + delay: 10 + failed_when: false + changed_when: false + delegate_to: "{{ inventory_hostname }}-cli" + when: tpl_9341_is_template.rc != 0 + + - name: TEMPLATE 9341 - SKIP all (already finalized as template) + ansible.builtin.debug: + msg: "Template 9341 is already a template - skipping. Run delete-everything to recreate." + when: tpl_9341_is_template.rc == 0 + + #### vm_create idempotence: separate exists check #### + - name: TEMPLATE 9341 - CHECK if VM exists (vm_create idempotence) + ansible.builtin.shell: qm config 9341 2>/dev/null + register: tpl_9341_exists + failed_when: false + changed_when: false + delegate_to: "{{ inventory_hostname }}-cli" + when: tpl_9341_is_template.rc != 0 + + - name: PROXMOX INIT - TEMPLATES - CREATE TEMPLATE - template-vm-debian-trixie-large + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "vm_create" + vm_id: 9341 + vm_name: "template-vm-debian-trixie-large" + vm_cpu: "host" + vm_cores: 4 + vm_sockets: 1 + vm_memory: 16384 + vm_net_virtio_bridge: "vmbr140" + when: + - tpl_9341_is_template.rc != 0 + - tpl_9341_exists.rc != 0 + + - name: PROXMOX INIT - TEMPLATES - WAIT for vm_create to finish (lock-aware) + ansible.builtin.shell: qm config 9341 | grep -q '^lock:' + register: tpl_9341_post_create_lock + until: tpl_9341_post_create_lock.rc != 0 + retries: 60 + delay: 5 + failed_when: false + changed_when: false + delegate_to: "{{ inventory_hostname }}-cli" + when: + - tpl_9341_is_template.rc != 0 + - tpl_9341_exists.rc != 0 + +- hosts: proxmox-cli + gather_facts: false + vars_files: + - "../../../../secrets/default_vault.yml" + + tasks: + - name: TEMPLATE 9341 - CHECK if already template (proxmox-cli play) + ansible.builtin.shell: qm config 9341 2>/dev/null | grep -q '^template:' + register: tpl_9341_is_template + failed_when: false + changed_when: false + + - name: PROXMOX INIT - TEMPLATES - IMPORT CLOUD INIT DISK - template-vm-debian-trixie-large + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "template_cloudinit_import_disk" + proxmox_node: "pve" + cloudinit_image_full_path: "/var/lib/vz/template/iso/debian-13-genericcloud-amd64.img" + vm_id: 9341 + vm_disk_size: "500g" + proxmox_dest_vm_storage_name: "local-lvm" + when: tpl_9341_is_template.rc != 0 + +- hosts: proxmox + gather_facts: false + vars_files: + - "../../../../secrets/default_vault.yml" + + tasks: + - name: TEMPLATE 9341 - CHECK if already template (third play) + ansible.builtin.shell: qm config 9341 2>/dev/null | grep -q '^template:' + register: tpl_9341_is_template + failed_when: false + changed_when: false + delegate_to: "{{ inventory_hostname }}-cli" + + - name: PROXMOX INIT - TEMPLATES - SET CLOUD DEFAULT INIT VARIABLE - template-vm-debian-trixie-large + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "cloudinit_set_variables" + vm_id: 9341 + vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" + vm_ci_password: "{{ default_trainee_vm_ci_password | default('supersecret') }}" + vm_ci_ssh_key: "{{ default_trainee_vm_ci_ssh_key }}" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" + vm_ci_ip: "192.168.140.2" + vm_ci_netmask: "24" + vm_ci_ip_gw: "192.168.140.1" + when: tpl_9341_is_template.rc != 0 + + - name: PROXMOX INIT - TEMPLATES - SET PROXMOX TAG + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "vm_set_tag" + vm_id: 9341 + vm_tag_name: "template" + when: tpl_9341_is_template.rc != 0 diff --git a/scenarios/test_apt_mirror/01_init_proxmox/stage_01-create_templates/templates/debian_trixie/template-vm-debian-trixie-small.yml b/scenarios/test_apt_mirror/01_init_proxmox/stage_01-create_templates/templates/debian_trixie/template-vm-debian-trixie-small.yml new file mode 100644 index 00000000..bf774d52 --- /dev/null +++ b/scenarios/test_apt_mirror/01_init_proxmox/stage_01-create_templates/templates/debian_trixie/template-vm-debian-trixie-small.yml @@ -0,0 +1,134 @@ +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# +# PROXMOX INIT - create debian_trixie template VM template-vm-debian-trixie-small (id 9321) +# +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- hosts: proxmox + gather_facts: false + vars_files: + - "../../../../secrets/default_vault.yml" + + tasks: + #### IDEMPOTENCE - skip if already finalized as template #### + - name: TEMPLATE 9321 - CHECK if already finalized as template + ansible.builtin.shell: qm config 9321 2>/dev/null | grep -q '^template:' + register: tpl_9321_is_template + failed_when: false + changed_when: false + delegate_to: "{{ inventory_hostname }}-cli" + + - name: TEMPLATE 9321 - WAIT for unlock if a parallel deploy holds the VM + ansible.builtin.shell: qm config 9321 | grep -q '^lock:' + register: tpl_9321_lock + until: tpl_9321_lock.rc != 0 + retries: 30 + delay: 10 + failed_when: false + changed_when: false + delegate_to: "{{ inventory_hostname }}-cli" + when: tpl_9321_is_template.rc != 0 + + - name: TEMPLATE 9321 - SKIP all (already finalized as template) + ansible.builtin.debug: + msg: "Template 9321 is already a template - skipping. Run delete-everything to recreate." + when: tpl_9321_is_template.rc == 0 + + #### vm_create idempotence: separate exists check #### + - name: TEMPLATE 9321 - CHECK if VM exists (vm_create idempotence) + ansible.builtin.shell: qm config 9321 2>/dev/null + register: tpl_9321_exists + failed_when: false + changed_when: false + delegate_to: "{{ inventory_hostname }}-cli" + when: tpl_9321_is_template.rc != 0 + + - name: PROXMOX INIT - TEMPLATES - CREATE TEMPLATE - template-vm-debian-trixie-small + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "vm_create" + vm_id: 9321 + vm_name: "template-vm-debian-trixie-small" + vm_cpu: "host" + vm_cores: 1 + vm_sockets: 1 + vm_memory: 4096 + vm_net_virtio_bridge: "vmbr140" + when: + - tpl_9321_is_template.rc != 0 + - tpl_9321_exists.rc != 0 + + - name: PROXMOX INIT - TEMPLATES - WAIT for vm_create to finish (lock-aware) + ansible.builtin.shell: qm config 9321 | grep -q '^lock:' + register: tpl_9321_post_create_lock + until: tpl_9321_post_create_lock.rc != 0 + retries: 60 + delay: 5 + failed_when: false + changed_when: false + delegate_to: "{{ inventory_hostname }}-cli" + when: + - tpl_9321_is_template.rc != 0 + - tpl_9321_exists.rc != 0 + +- hosts: proxmox-cli + gather_facts: false + vars_files: + - "../../../../secrets/default_vault.yml" + + tasks: + - name: TEMPLATE 9321 - CHECK if already template (proxmox-cli play) + ansible.builtin.shell: qm config 9321 2>/dev/null | grep -q '^template:' + register: tpl_9321_is_template + failed_when: false + changed_when: false + + - name: PROXMOX INIT - TEMPLATES - IMPORT CLOUD INIT DISK - template-vm-debian-trixie-small + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "template_cloudinit_import_disk" + proxmox_node: "pve" + cloudinit_image_full_path: "/var/lib/vz/template/iso/debian-13-genericcloud-amd64.img" + vm_id: 9321 + vm_disk_size: "32g" + proxmox_dest_vm_storage_name: "local-lvm" + when: tpl_9321_is_template.rc != 0 + +- hosts: proxmox + gather_facts: false + vars_files: + - "../../../../secrets/default_vault.yml" + + tasks: + - name: TEMPLATE 9321 - CHECK if already template (third play) + ansible.builtin.shell: qm config 9321 2>/dev/null | grep -q '^template:' + register: tpl_9321_is_template + failed_when: false + changed_when: false + delegate_to: "{{ inventory_hostname }}-cli" + + - name: PROXMOX INIT - TEMPLATES - SET CLOUD DEFAULT INIT VARIABLE - template-vm-debian-trixie-small + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "cloudinit_set_variables" + vm_id: 9321 + vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" + vm_ci_password: "{{ default_trainee_vm_ci_password | default('supersecret') }}" + vm_ci_ssh_key: "{{ default_trainee_vm_ci_ssh_key }}" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" + vm_ci_ip: "192.168.140.3" + vm_ci_netmask: "24" + vm_ci_ip_gw: "192.168.140.1" + when: tpl_9321_is_template.rc != 0 + + - name: PROXMOX INIT - TEMPLATES - SET PROXMOX TAG + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "vm_set_tag" + vm_id: 9321 + vm_tag_name: "template" + when: tpl_9321_is_template.rc != 0 diff --git a/scenarios/test_apt_mirror/01_init_proxmox/stage_01-create_templates/templates/ubuntu_resolute/_apply_apt_proxy.yml b/scenarios/test_apt_mirror/01_init_proxmox/stage_01-create_templates/templates/ubuntu_resolute/_apply_apt_proxy.yml new file mode 100644 index 00000000..a8925f42 --- /dev/null +++ b/scenarios/test_apt_mirror/01_init_proxmox/stage_01-create_templates/templates/ubuntu_resolute/_apply_apt_proxy.yml @@ -0,0 +1,51 @@ +## +## ubuntu_resolute - apt proxy via cloud-init cicustom (optional) +## +## Attaches /var/lib/vz/snippets/range42-apt-proxy.yaml as cloud-init vendor-data +## to every template VM. All VMs cloned from these templates inherit the apt +## proxy automatically via cloud-init at first boot. +## Skipped (no-op) if apt_proxy_url is empty/unset. Idempotent. +## + +- hosts: proxmox-cli + gather_facts: false + vars_files: + - "../../../../secrets/default_vault.yml" + + vars: + bs2_template_vm_ids: + - 9521 # template-vm-ubuntu-resolute-small-01-4g-32g + + tasks: + - name: APT-PROXY - SKIP (apt_proxy_url is empty) + ansible.builtin.debug: + msg: "apt_proxy_url is empty - cloud-init cicustom not applied to templates" + when: apt_proxy_url is not defined or (apt_proxy_url | length) == 0 + + - name: APT-PROXY - ATTACH cicustom vendor TO TEMPLATES + ansible.builtin.command: + cmd: qm set {{ item }} --cicustom "vendor=local:snippets/range42-apt-proxy.yaml" + loop: "{{ bs2_template_vm_ids }}" + register: qm_result + changed_when: qm_result.rc == 0 + failed_when: + - qm_result.rc != 0 + - "'does not exist' not in qm_result.stderr" + when: apt_proxy_url is defined and (apt_proxy_url | length) > 0 + + - name: APT-PROXY - DETACH cicustom vendor FROM TEMPLATES (apt_proxy_url unset) + ansible.builtin.command: + cmd: qm set {{ item }} --delete cicustom + loop: "{{ bs2_template_vm_ids }}" + register: qm_unset + changed_when: qm_unset.rc == 0 + failed_when: + - qm_unset.rc != 0 + - "'does not exist' not in qm_unset.stderr" + when: apt_proxy_url is not defined or (apt_proxy_url | length) == 0 + + - name: APT-PROXY - DISPLAY STATUS + ansible.builtin.debug: + msg: | + apt proxy: {{ apt_proxy_url if (apt_proxy_url is defined and (apt_proxy_url | length) > 0) else 'DISABLED' }} + cicustom : {{ 'attached to ' + (bs2_template_vm_ids | length | string) + ' templates' if (apt_proxy_url is defined and (apt_proxy_url | length) > 0) else 'detached from templates' }} diff --git a/scenarios/test_apt_mirror/01_init_proxmox/stage_01-create_templates/templates/ubuntu_resolute/_main_ubuntu_resolute.yml b/scenarios/test_apt_mirror/01_init_proxmox/stage_01-create_templates/templates/ubuntu_resolute/_main_ubuntu_resolute.yml new file mode 100644 index 00000000..ca2dd160 --- /dev/null +++ b/scenarios/test_apt_mirror/01_init_proxmox/stage_01-create_templates/templates/ubuntu_resolute/_main_ubuntu_resolute.yml @@ -0,0 +1,12 @@ +## +## ubuntu_resolute — create + configure all Proxmox template VMs for this image +## + +- import_playbook: ./template-vm-ubuntu-resolute-small-01-4g-32g.yml + +# apt proxy cicustom (no-op if apt_proxy_url empty) +- import_playbook: ./_apply_apt_proxy.yml + +# pre-update: start each VM, run apt update + dist-upgrade via cloud-init poweroff, +# then convert to template. +- import_playbook: ./_update_templates.yml diff --git a/scenarios/test_apt_mirror/01_init_proxmox/stage_01-create_templates/templates/ubuntu_resolute/_update_templates.yml b/scenarios/test_apt_mirror/01_init_proxmox/stage_01-create_templates/templates/ubuntu_resolute/_update_templates.yml new file mode 100644 index 00000000..1513b666 --- /dev/null +++ b/scenarios/test_apt_mirror/01_init_proxmox/stage_01-create_templates/templates/ubuntu_resolute/_update_templates.yml @@ -0,0 +1,202 @@ +## +## ubuntu_resolute - pre-update templates +## +## After all templates are created (but BEFORE convert to template), this playbook: +## 1. renders a temporary bootstrap snippet directly on the Proxmox host +## (apt update + apt dist-upgrade + cloud-init clean + auto-poweroff) +## 2. attaches it as cicustom vendor on each template VM +## 3. starts all templates +## 4. polls qm status until each VM is stopped (cloud-init auto-poweroff signals end) +## 5. detaches the bootstrap snippet, re-attaches the persistent apt-proxy snippet +## 6. converts each VM to a template (qm template ) +## +## Reads VM IDs from the scenario manifest (manifest/scenario_vms.json). +## Runs entirely on `proxmox-cli` (= the Proxmox host itself), no SSH involved. +## + +- hosts: proxmox-cli + gather_facts: false + vars_files: + - "../../../../secrets/default_vault.yml" + + vars: + manifest_path: "{{ playbook_dir }}/../../../../manifest/scenario_vms.json" + bootstrap_snippet_template: "{{ playbook_dir }}/range42-template-bootstrap.yaml.j2" + bootstrap_snippet_path: "/var/lib/vz/snippets/range42-template-bootstrap.yaml" + + tasks: + + #### #### #### debug apt_proxy_url visibility - fully safe vs undefined #### #### #### + + - name: TEMPLATES UPDATE - DEBUG apt_proxy_url visibility + ansible.builtin.debug: + msg: | + apt_proxy_url defined : {{ apt_proxy_url is defined }} + apt_proxy_url value : {{ apt_proxy_url | default('(undefined)') }} + apt_proxy_url length : {{ (apt_proxy_url | default('')) | length }} + -> snippet WILL include apt section : {{ (apt_proxy_url | default('')) | length > 0 }} + + #### #### #### load manifest #### #### #### + + - name: TEMPLATES UPDATE - LOAD MANIFEST + ansible.builtin.set_fact: + templates: "{{ (lookup('file', manifest_path) | from_json).templates }}" + + #### #### #### IDEMPOTENCE - probe each template's current state #### #### #### + # 3 possible states per VM ID : + # - "template" : already converted, skip update entirely + # - "vm" : exists as a regular VM, needs update + convert + # - "missing" : VM doesn't exist on Proxmox, skip (templates not created yet) + # Only "vm" state proceeds through the start/update/shutdown/convert pipeline. + + - name: TEMPLATES UPDATE - PROBE state of each template VM + ansible.builtin.shell: | + if qm config {{ item.vm_id }} 2>/dev/null | grep -q '^template:'; then + echo "template" + elif qm config {{ item.vm_id }} >/dev/null 2>&1; then + echo "vm" + else + echo "missing" + fi + register: tpl_state + loop: "{{ templates }}" + loop_control: + label: "vm_id={{ item.vm_id }}" + changed_when: false + + - name: TEMPLATES UPDATE - BUILD list of templates needing update (state == 'vm') + ansible.builtin.set_fact: + templates_to_update: >- + {{ templates | zip(tpl_state.results) + | selectattr('1.stdout', 'eq', 'vm') + | map(attribute='0') | list }} + + - name: TEMPLATES UPDATE - DISPLAY PLAN + ansible.builtin.debug: + msg: | + total templates in manifest : {{ templates | length }} + already converted (skip) : {{ tpl_state.results | selectattr('stdout', 'eq', 'template') | list | length }} + missing (skip) : {{ tpl_state.results | selectattr('stdout', 'eq', 'missing') | list | length }} + to update + convert : {{ templates_to_update | length }} + {% if templates_to_update | length > 0 %} + targets : + {% for t in templates_to_update %} - {{ t.vm_id }} {{ t.vm_name }} ({{ t.ip }}) + {% endfor %} + {% endif %} + + - name: TEMPLATES UPDATE - SHORT-CIRCUIT if nothing to update + ansible.builtin.meta: end_play + when: templates_to_update | length == 0 + + #### #### #### render bootstrap snippet directly on the Proxmox host #### #### #### + + - name: TEMPLATES UPDATE - RENDER BOOTSTRAP SNIPPET ON PROXMOX + ansible.builtin.template: + src: "{{ bootstrap_snippet_template }}" + dest: "{{ bootstrap_snippet_path }}" + mode: "0644" + + #### #### #### attach bootstrap snippet on every template #### #### #### + + - name: TEMPLATES UPDATE - ATTACH bootstrap cicustom + ansible.builtin.command: + cmd: qm set {{ item.vm_id }} --cicustom "vendor=local:snippets/range42-template-bootstrap.yaml" + loop: "{{ templates_to_update }}" + loop_control: + label: "vm_id={{ item.vm_id }}" + + #### #### #### start all templates #### #### #### + + - name: TEMPLATES UPDATE - START all templates + ansible.builtin.command: + cmd: qm start {{ item.vm_id }} + register: start_result + failed_when: + - start_result.rc != 0 + - "'already running' not in start_result.stderr" + - "'MAX' not in start_result.stderr" + changed_when: start_result.rc == 0 + loop: "{{ templates_to_update }}" + loop_control: + label: "vm_id={{ item.vm_id }}" + + # VMs that failed with "MAX X vcpus" couldn't start - exclude them from + # the wait/convert pipeline but keep the play running for the others. + - name: TEMPLATES UPDATE - BUILD started list (exclude CPU-limited VMs) + ansible.builtin.set_fact: + templates_started: >- + {{ templates_to_update | zip(start_result.results) + | rejectattr('1.stderr', 'search', 'MAX') + | map(attribute='0') | list }} + templates_skipped_cpu: >- + {{ templates_to_update | zip(start_result.results) + | selectattr('1.stderr', 'search', 'MAX') + | map(attribute='0') | list }} + + - name: TEMPLATES UPDATE - WARN skipped templates (host CPU limit) + ansible.builtin.debug: + msg: | + WARNING: {{ templates_skipped_cpu | length }} template(s) skipped - host CPU limit: + {% for t in templates_skipped_cpu %} + - {{ t.vm_id }} {{ t.vm_name }} (spec: {{ t.spec }}) + {% endfor %} + These templates require more vCPUs than this host provides. + They will NOT be converted. Run on a host with enough physical + CPUs, or lower vm_cores in the template playbook if intentional. + when: templates_skipped_cpu | length > 0 + + #### #### #### wait until each VM auto-poweroffs (cloud-init done) #### #### #### + # the loop is sequential per-vm, but since all started in parallel above, + # total time is bounded by the slowest template (~3-15 min depending on cache) + + - name: TEMPLATES UPDATE - WAIT for cloud-init to auto-poweroff + ansible.builtin.shell: | + qm status {{ item.vm_id }} | grep -q 'status: stopped' + register: stop_check + retries: 360 # safety cap : 360 x 5s = 30 min max per VM + delay: 5 + until: stop_check.rc == 0 + loop: "{{ templates_started }}" + loop_control: + label: "vm_id={{ item.vm_id }}" + changed_when: false + + #### #### #### detach bootstrap, re-attach apt-proxy, convert to template #### #### #### + + - name: TEMPLATES UPDATE - DETACH bootstrap cicustom + ansible.builtin.command: + cmd: qm set {{ item.vm_id }} --delete cicustom + loop: "{{ templates_started }}" + loop_control: + label: "vm_id={{ item.vm_id }}" + + - name: TEMPLATES UPDATE - RE-ATTACH apt-proxy cicustom (persistent for clones) + ansible.builtin.command: + cmd: qm set {{ item.vm_id }} --cicustom "vendor=local:snippets/range42-apt-proxy.yaml" + loop: "{{ templates_started }}" + loop_control: + label: "vm_id={{ item.vm_id }}" + when: (apt_proxy_url | default('')) | length > 0 + + - name: TEMPLATES UPDATE - CONVERT VM to template + ansible.builtin.command: + cmd: qm template {{ item.vm_id }} + loop: "{{ templates_started }}" + loop_control: + label: "vm_id={{ item.vm_id }}" + + #### #### #### cleanup #### #### #### + + - name: TEMPLATES UPDATE - REMOVE BOOTSTRAP SNIPPET FROM PROXMOX + ansible.builtin.file: + path: "{{ bootstrap_snippet_path }}" + state: absent + + - name: TEMPLATES UPDATE - DISPLAY DONE + ansible.builtin.debug: + msg: | + {{ templates_started | length }} templates updated and converted + apt-proxy cicustom re-attached (if apt_proxy_url set) + {% if templates_skipped_cpu | length > 0 %} + {{ templates_skipped_cpu | length }} skipped (host CPU limit) - see warning above + {% endif %} diff --git a/scenarios/test_apt_mirror/01_init_proxmox/stage_01-create_templates/templates/ubuntu_resolute/range42-template-bootstrap.yaml.j2 b/scenarios/test_apt_mirror/01_init_proxmox/stage_01-create_templates/templates/ubuntu_resolute/range42-template-bootstrap.yaml.j2 new file mode 100644 index 00000000..46990999 --- /dev/null +++ b/scenarios/test_apt_mirror/01_init_proxmox/stage_01-create_templates/templates/ubuntu_resolute/range42-template-bootstrap.yaml.j2 @@ -0,0 +1,22 @@ +#cloud-config +# range42 - TEMPORARY bootstrap snippet for templates +# Triggers apt update + dist-upgrade then poweroff. +# Auto-generated by _update_templates.yml. Detached after the template is +# converted, replaced by range42-apt-proxy.yaml (persistent). +# +# NOTE: cloud-init clean was previously here in `runcmd` - REMOVED because +# it ran in cloud_config stage, before cloud_final, and broke +# package-update-upgrade-install. Clones get a new vmid -> new instance-id, +# so cloud-init re-processes naturally on each clone. No clean needed. +# +{% if (apt_proxy_url | default('')) | length > 0 %} +apt: + http_proxy: "{{ apt_proxy_url }}" +{% endif %} +package_update: true +package_upgrade: true +package_reboot_if_required: false +power_state: + mode: poweroff + timeout: 60 + condition: True diff --git a/scenarios/test_apt_mirror/01_init_proxmox/stage_01-create_templates/templates/ubuntu_resolute/template-vm-ubuntu-resolute-small-01-4g-32g.yml b/scenarios/test_apt_mirror/01_init_proxmox/stage_01-create_templates/templates/ubuntu_resolute/template-vm-ubuntu-resolute-small-01-4g-32g.yml new file mode 100644 index 00000000..0ff9c9e4 --- /dev/null +++ b/scenarios/test_apt_mirror/01_init_proxmox/stage_01-create_templates/templates/ubuntu_resolute/template-vm-ubuntu-resolute-small-01-4g-32g.yml @@ -0,0 +1,134 @@ +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# +# PROXMOX INIT - create ubuntu_resolute template VM template-vm-ubuntu-resolute-small-01-4g-32g (id 9521) +# +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- hosts: proxmox + gather_facts: false + vars_files: + - "../../../../secrets/default_vault.yml" + + tasks: + #### IDEMPOTENCE - skip if already finalized as template #### + - name: TEMPLATE 9521 - CHECK if already finalized as template + ansible.builtin.shell: qm config 9521 2>/dev/null | grep -q '^template:' + register: tpl_9521_is_template + failed_when: false + changed_when: false + delegate_to: "{{ inventory_hostname }}-cli" + + - name: TEMPLATE 9521 - WAIT for unlock if a parallel deploy holds the VM + ansible.builtin.shell: qm config 9521 | grep -q '^lock:' + register: tpl_9521_lock + until: tpl_9521_lock.rc != 0 + retries: 30 + delay: 10 + failed_when: false + changed_when: false + delegate_to: "{{ inventory_hostname }}-cli" + when: tpl_9521_is_template.rc != 0 + + - name: TEMPLATE 9521 - SKIP all (already finalized as template) + ansible.builtin.debug: + msg: "Template 9521 is already a template - skipping. Run delete-everything to recreate." + when: tpl_9521_is_template.rc == 0 + + #### vm_create idempotence: separate exists check #### + - name: TEMPLATE 9521 - CHECK if VM exists (vm_create idempotence) + ansible.builtin.shell: qm config 9521 2>/dev/null + register: tpl_9521_exists + failed_when: false + changed_when: false + delegate_to: "{{ inventory_hostname }}-cli" + when: tpl_9521_is_template.rc != 0 + + - name: PROXMOX INIT - TEMPLATES - CREATE TEMPLATE - template-vm-ubuntu-resolute-small-01-4g-32g + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "vm_create" + vm_id: 9521 + vm_name: "template-vm-ubuntu-resolute-small-01-4g-32g" + vm_cpu: "host" + vm_cores: 1 + vm_sockets: 1 + vm_memory: 4096 + vm_net_virtio_bridge: "vmbr140" + when: + - tpl_9521_is_template.rc != 0 + - tpl_9521_exists.rc != 0 + + - name: PROXMOX INIT - TEMPLATES - WAIT for vm_create to finish (lock-aware) + ansible.builtin.shell: qm config 9521 | grep -q '^lock:' + register: tpl_9521_post_create_lock + until: tpl_9521_post_create_lock.rc != 0 + retries: 60 + delay: 5 + failed_when: false + changed_when: false + delegate_to: "{{ inventory_hostname }}-cli" + when: + - tpl_9521_is_template.rc != 0 + - tpl_9521_exists.rc != 0 + +- hosts: proxmox-cli + gather_facts: false + vars_files: + - "../../../../secrets/default_vault.yml" + + tasks: + - name: TEMPLATE 9521 - CHECK if already template (proxmox-cli play) + ansible.builtin.shell: qm config 9521 2>/dev/null | grep -q '^template:' + register: tpl_9521_is_template + failed_when: false + changed_when: false + + - name: PROXMOX INIT - TEMPLATES - IMPORT CLOUD INIT DISK - template-vm-ubuntu-resolute-small-01-4g-32g + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "template_cloudinit_import_disk" + proxmox_node: "pve" + cloudinit_image_full_path: "/var/lib/vz/template/iso/resolute-minimal-cloudimg-amd64.img" + vm_id: 9521 + vm_disk_size: "32g" + proxmox_dest_vm_storage_name: "local-lvm" + when: tpl_9521_is_template.rc != 0 + +- hosts: proxmox + gather_facts: false + vars_files: + - "../../../../secrets/default_vault.yml" + + tasks: + - name: TEMPLATE 9521 - CHECK if already template (third play) + ansible.builtin.shell: qm config 9521 2>/dev/null | grep -q '^template:' + register: tpl_9521_is_template + failed_when: false + changed_when: false + delegate_to: "{{ inventory_hostname }}-cli" + + - name: PROXMOX INIT - TEMPLATES - SET CLOUD DEFAULT INIT VARIABLE - template-vm-ubuntu-resolute-small-01-4g-32g + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "cloudinit_set_variables" + vm_id: 9521 + vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" + vm_ci_password: "{{ default_trainee_vm_ci_password | default('supersecret') }}" + vm_ci_ssh_key: "{{ default_trainee_vm_ci_ssh_key }}" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" + vm_ci_ip: "192.168.140.4" + vm_ci_netmask: "24" + vm_ci_ip_gw: "192.168.140.1" + when: tpl_9521_is_template.rc != 0 + + - name: PROXMOX INIT - TEMPLATES - SET PROXMOX TAG + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "vm_set_tag" + vm_id: 9521 + vm_tag_name: "template" + when: tpl_9521_is_template.rc != 0 diff --git a/scenarios/test_apt_mirror/02_lan1_infrastructure/_main.reinstall.sh b/scenarios/test_apt_mirror/02_lan1_infrastructure/_main.reinstall.sh new file mode 100755 index 00000000..87297363 --- /dev/null +++ b/scenarios/test_apt_mirror/02_lan1_infrastructure/_main.reinstall.sh @@ -0,0 +1,8 @@ +#!/bin/bash +## +## re-run this section's playbooks (02_lan1_infrastructure) +## + +ansible-playbook -i "${RANGE42_ANSIBLE_ROLES__INVENTORY_DIR}/inventory_default.yml" \ + -l "all" \ + "./_main.yml" --vault-password-file "${RANGE42_VAULT_PASSWORD_FILE:?RANGE42_VAULT_PASSWORD_FILE is not set — run: range42-context use }" diff --git a/scenarios/test_apt_mirror/02_lan1_infrastructure/_main.yml b/scenarios/test_apt_mirror/02_lan1_infrastructure/_main.yml new file mode 100644 index 00000000..b7ad2c42 --- /dev/null +++ b/scenarios/test_apt_mirror/02_lan1_infrastructure/_main.yml @@ -0,0 +1,23 @@ +--- +## +## 02_lan1_infrastructure — generated by r42playbooks +## stage_00 clones VMs (per-VM global_* below); stage_01 installs software. +## + +#### STAGE 00 #### + +- import_playbook: ./stage_00/lan1-apt-mirror-00.yml + vars: + global_vm_name: "lan1-apt-mirror-00" + global_vm_ssh_name: "r42.lan1-apt-mirror-00" + global_vm_id: 2011 + global_vm_description: "apt-mirror" + global_vm_tag_name: "lan1" + global_vm_ci_ip: "192.168.150.11" + global_template_vm_id: 9341 + global_template_name: "template-vm-debian-trixie-large - id 9341" + + +#### STAGE 01 #### + +- import_playbook: ./stage_01/lan1-apt-mirror-00.yml diff --git a/scenarios/test_apt_mirror/02_lan1_infrastructure/stage_00/lan1-apt-mirror-00.yml b/scenarios/test_apt_mirror/02_lan1_infrastructure/stage_00/lan1-apt-mirror-00.yml new file mode 100644 index 00000000..db6d5781 --- /dev/null +++ b/scenarios/test_apt_mirror/02_lan1_infrastructure/stage_00/lan1-apt-mirror-00.yml @@ -0,0 +1,81 @@ +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# +# LAN1 INFRASTRUCTURE INIT - CLONE + CLOUD-INIT +# +# hostname : lan1-apt-mirror-00 +# vm_id : 2011 +# ip : 192.168.150.11 +# template : template-vm-debian-trixie-large +# +# global_* vars are supplied by ../_main.yml (kept here as Jinja references). +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- hosts: proxmox + gather_facts: false + vars_files: + - "../../secrets/default_vault.yml" + + tasks: + - name: "LAN1 INFRASTRUCTURE INIT - {{ global_vm_name }} - CLONE TEMPLATE - {{ global_template_name }}" + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "vm_clone" + vm_id: "{{ global_template_vm_id }}" + vm_new_id: "{{ global_vm_id }}" + vm_name: "{{ global_vm_name }}" + vm_description: "{{ global_vm_description }}" + + - name: "LAN1 INFRASTRUCTURE INIT - {{ global_vm_name }} - WAIT FOR PROXMOX UNLOCK" + ansible.builtin.shell: qm config {{ global_vm_id }} | grep -q '^lock:' + register: clone_lock_check + until: clone_lock_check.rc != 0 + retries: 60 + delay: 5 + failed_when: false + changed_when: false + delegate_to: "{{ inventory_hostname }}-cli" + + - name: "LAN1 INFRASTRUCTURE INIT - {{ global_vm_name }} - SET PROXMOX TAG" + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "vm_set_tag" + vm_id: "{{ global_vm_id }}" + vm_tag_name: "{{ global_vm_tag_name }}" + + - name: "LAN1 INFRASTRUCTURE INIT - {{ global_vm_name }} - SET CLOUD-INIT VARIABLES" + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "cloudinit_set_variables" + vm_id: "{{ global_vm_id }}" + vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" + vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" + vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" + vm_ci_ip: "{{ global_vm_ci_ip }}" + vm_ci_netmask: "24" + vm_ci_ip_gw: "192.168.150.1" + vm_net_virtio_bridge: "vmbr150" + + - name: "LAN1 INFRASTRUCTURE INIT - {{ global_vm_name }} - START VM" + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "vm_start" + vm_id: "{{ global_vm_id }}" + +- hosts: "{{ global_vm_ssh_name }}" + gather_facts: false + vars_files: + - "../../secrets/default_vault.yml" + vars: + ARG_vm_ssh_name: "{{ global_vm_ssh_name }}" + requested_tasks: + - wait/openssh_server/is_reachable.yml + - wait/cloudinit/is_boot_finished.yml + tasks: + - name: "LAN1 INFRASTRUCTURE INIT - {{ global_vm_name }} - WAIT FOR SSH + CLOUD-INIT" + include_role: + name: ansible.utils diff --git a/scenarios/test_apt_mirror/02_lan1_infrastructure/stage_01/lan1-apt-mirror-00.devkit/test_apt_mirror.lan1-apt-mirror-00.install.sh b/scenarios/test_apt_mirror/02_lan1_infrastructure/stage_01/lan1-apt-mirror-00.devkit/test_apt_mirror.lan1-apt-mirror-00.install.sh new file mode 100755 index 00000000..9a6e7928 --- /dev/null +++ b/scenarios/test_apt_mirror/02_lan1_infrastructure/stage_01/lan1-apt-mirror-00.devkit/test_apt_mirror.lan1-apt-mirror-00.install.sh @@ -0,0 +1,8 @@ +#!/bin/bash +## +## reinstall software on lan1-apt-mirror-00 only (fast single-VM iteration) +## + +ansible-playbook -i "${RANGE42_ANSIBLE_ROLES__INVENTORY_DIR}/inventory_default.yml" \ + -l "all" \ + "../lan1-apt-mirror-00.yml" --vault-password-file "${RANGE42_VAULT_PASSWORD_FILE:?RANGE42_VAULT_PASSWORD_FILE is not set — run: range42-context use }" diff --git a/scenarios/test_apt_mirror/02_lan1_infrastructure/stage_01/lan1-apt-mirror-00.devkit/test_apt_mirror.lan1-apt-mirror-00.revert.sh b/scenarios/test_apt_mirror/02_lan1_infrastructure/stage_01/lan1-apt-mirror-00.devkit/test_apt_mirror.lan1-apt-mirror-00.revert.sh new file mode 100755 index 00000000..0f40111e --- /dev/null +++ b/scenarios/test_apt_mirror/02_lan1_infrastructure/stage_01/lan1-apt-mirror-00.devkit/test_apt_mirror.lan1-apt-mirror-00.revert.sh @@ -0,0 +1,8 @@ +#!/bin/bash +## +## revert lan1-apt-mirror-00 to its latest snapshot and restart it +## + +VM_ID=$(devkit_manifest.find_vm_id.to.text.sh "$0" "lan1-apt-mirror-00") || exit 1 +echo "{\"proxmox_node\":\"pve\",\"vm_id\":${VM_ID} }" | proxmox_snapshot_vm.vm_id.revert_snapshot.to.jsons.sh +echo "{\"proxmox_node\":\"pve\",\"vm_id\":${VM_ID} }" | proxmox_vm.vm_id.start.to.jsons.sh diff --git a/scenarios/test_apt_mirror/02_lan1_infrastructure/stage_01/lan1-apt-mirror-00.devkit/test_apt_mirror.lan1-apt-mirror-00.snapshot.sh b/scenarios/test_apt_mirror/02_lan1_infrastructure/stage_01/lan1-apt-mirror-00.devkit/test_apt_mirror.lan1-apt-mirror-00.snapshot.sh new file mode 100755 index 00000000..2ebfe93f --- /dev/null +++ b/scenarios/test_apt_mirror/02_lan1_infrastructure/stage_01/lan1-apt-mirror-00.devkit/test_apt_mirror.lan1-apt-mirror-00.snapshot.sh @@ -0,0 +1,7 @@ +#!/bin/bash +## +## snapshot lan1-apt-mirror-00 as "base" +## + +VM_ID=$(devkit_manifest.find_vm_id.to.text.sh "$0" "lan1-apt-mirror-00") || exit 1 +echo "{\"proxmox_node\":\"pve\",\"vm_id\":${VM_ID},\"vm_snapshot_description\":\"base\"}" | proxmox_snapshot_vm.vm_id.create_snapshot.to.jsons.sh diff --git a/scenarios/test_apt_mirror/02_lan1_infrastructure/stage_01/lan1-apt-mirror-00.yml b/scenarios/test_apt_mirror/02_lan1_infrastructure/stage_01/lan1-apt-mirror-00.yml new file mode 100644 index 00000000..35ee856b --- /dev/null +++ b/scenarios/test_apt_mirror/02_lan1_infrastructure/stage_01/lan1-apt-mirror-00.yml @@ -0,0 +1,48 @@ +## +## stage_01 — software install for lan1-apt-mirror-00 +## catalog roles are referenced BY NAME (resolved via ANSIBLE_ROLES_PATH at deploy); +## one play per attachment so each carries its own params (firewall_rules, docker …). +## + +- name: "lan1-apt-mirror-00 — software.configure.firewalls" + hosts: r42.lan1-apt-mirror-00 + become: true + vars_files: + - "../../secrets/default_vault.yml" + vars: + firewall_rules: + - ip: all + port: 22 + protocol: tcp + - ip: all + port: 80 + protocol: tcp + roles: + - software.configure.firewalls + +- name: "lan1-apt-mirror-00 — software.install.warmup.basic_packages" + hosts: r42.lan1-apt-mirror-00 + become: true + vars_files: + - "../../secrets/default_vault.yml" + roles: + - software.install.warmup.basic_packages + +- name: "lan1-apt-mirror-00 — software.install.apt_mirror" + hosts: r42.lan1-apt-mirror-00 + become: true + vars_files: + - "../../secrets/default_vault.yml" + vars: + apt_mirror_backports: false + apt_mirror_debian_trixie: true + apt_mirror_http_port: 80 + apt_mirror_include_sources: false + apt_mirror_prewarm: true + apt_mirror_root: /var/spool/apt-mirror + apt_mirror_security: true + apt_mirror_threads: 20 + apt_mirror_ubuntu_components: main restricted universe + apt_mirror_ubuntu_resolute: true + roles: + - software.install.apt_mirror diff --git a/scenarios/test_apt_mirror/03_lan2_infrastructure/_main.reinstall.sh b/scenarios/test_apt_mirror/03_lan2_infrastructure/_main.reinstall.sh new file mode 100755 index 00000000..56d65a34 --- /dev/null +++ b/scenarios/test_apt_mirror/03_lan2_infrastructure/_main.reinstall.sh @@ -0,0 +1,8 @@ +#!/bin/bash +## +## re-run this section's playbooks (03_lan2_infrastructure) +## + +ansible-playbook -i "${RANGE42_ANSIBLE_ROLES__INVENTORY_DIR}/inventory_default.yml" \ + -l "all" \ + "./_main.yml" --vault-password-file "${RANGE42_VAULT_PASSWORD_FILE:?RANGE42_VAULT_PASSWORD_FILE is not set — run: range42-context use }" diff --git a/scenarios/test_apt_mirror/03_lan2_infrastructure/_main.yml b/scenarios/test_apt_mirror/03_lan2_infrastructure/_main.yml new file mode 100644 index 00000000..75f08b38 --- /dev/null +++ b/scenarios/test_apt_mirror/03_lan2_infrastructure/_main.yml @@ -0,0 +1,36 @@ +--- +## +## 03_lan2_infrastructure — generated by r42playbooks +## stage_00 clones VMs (per-VM global_* below); stage_01 installs software. +## + +#### STAGE 00 #### + +- import_playbook: ./stage_00/lan2-debian-jump-00.yml + vars: + global_vm_name: "lan2-debian-jump-00" + global_vm_ssh_name: "r42.lan2-debian-jump-00" + global_vm_id: 1012 + global_vm_description: "debian-jump" + global_vm_tag_name: "lan2" + global_vm_ci_ip: "192.168.151.12" + global_template_vm_id: 9321 + global_template_name: "template-vm-debian-trixie-small - id 9321" + +- import_playbook: ./stage_00/lan2-ubuntu-jump-00.yml + vars: + global_vm_name: "lan2-ubuntu-jump-00" + global_vm_ssh_name: "r42.lan2-ubuntu-jump-00" + global_vm_id: 1013 + global_vm_description: "ubuntu-jump" + global_vm_tag_name: "lan2" + global_vm_ci_ip: "192.168.151.13" + global_template_vm_id: 9521 + global_template_name: "template-vm-ubuntu-resolute-small-01-4g-32g - id 9521" + + +#### STAGE 01 #### + +- import_playbook: ./stage_01/lan2-debian-jump-00.yml + +- import_playbook: ./stage_01/lan2-ubuntu-jump-00.yml diff --git a/scenarios/test_apt_mirror/03_lan2_infrastructure/stage_00/lan2-debian-jump-00.yml b/scenarios/test_apt_mirror/03_lan2_infrastructure/stage_00/lan2-debian-jump-00.yml new file mode 100644 index 00000000..66e1c1f2 --- /dev/null +++ b/scenarios/test_apt_mirror/03_lan2_infrastructure/stage_00/lan2-debian-jump-00.yml @@ -0,0 +1,81 @@ +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# +# LAN2 INFRASTRUCTURE INIT - CLONE + CLOUD-INIT +# +# hostname : lan2-debian-jump-00 +# vm_id : 1012 +# ip : 192.168.151.12 +# template : template-vm-debian-trixie-small +# +# global_* vars are supplied by ../_main.yml (kept here as Jinja references). +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- hosts: proxmox + gather_facts: false + vars_files: + - "../../secrets/default_vault.yml" + + tasks: + - name: "LAN2 INFRASTRUCTURE INIT - {{ global_vm_name }} - CLONE TEMPLATE - {{ global_template_name }}" + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "vm_clone" + vm_id: "{{ global_template_vm_id }}" + vm_new_id: "{{ global_vm_id }}" + vm_name: "{{ global_vm_name }}" + vm_description: "{{ global_vm_description }}" + + - name: "LAN2 INFRASTRUCTURE INIT - {{ global_vm_name }} - WAIT FOR PROXMOX UNLOCK" + ansible.builtin.shell: qm config {{ global_vm_id }} | grep -q '^lock:' + register: clone_lock_check + until: clone_lock_check.rc != 0 + retries: 60 + delay: 5 + failed_when: false + changed_when: false + delegate_to: "{{ inventory_hostname }}-cli" + + - name: "LAN2 INFRASTRUCTURE INIT - {{ global_vm_name }} - SET PROXMOX TAG" + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "vm_set_tag" + vm_id: "{{ global_vm_id }}" + vm_tag_name: "{{ global_vm_tag_name }}" + + - name: "LAN2 INFRASTRUCTURE INIT - {{ global_vm_name }} - SET CLOUD-INIT VARIABLES" + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "cloudinit_set_variables" + vm_id: "{{ global_vm_id }}" + vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" + vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" + vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" + vm_ci_ip: "{{ global_vm_ci_ip }}" + vm_ci_netmask: "24" + vm_ci_ip_gw: "192.168.151.1" + vm_net_virtio_bridge: "vmbr151" + + - name: "LAN2 INFRASTRUCTURE INIT - {{ global_vm_name }} - START VM" + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "vm_start" + vm_id: "{{ global_vm_id }}" + +- hosts: "{{ global_vm_ssh_name }}" + gather_facts: false + vars_files: + - "../../secrets/default_vault.yml" + vars: + ARG_vm_ssh_name: "{{ global_vm_ssh_name }}" + requested_tasks: + - wait/openssh_server/is_reachable.yml + - wait/cloudinit/is_boot_finished.yml + tasks: + - name: "LAN2 INFRASTRUCTURE INIT - {{ global_vm_name }} - WAIT FOR SSH + CLOUD-INIT" + include_role: + name: ansible.utils diff --git a/scenarios/test_apt_mirror/03_lan2_infrastructure/stage_00/lan2-ubuntu-jump-00.yml b/scenarios/test_apt_mirror/03_lan2_infrastructure/stage_00/lan2-ubuntu-jump-00.yml new file mode 100644 index 00000000..b3bdcca7 --- /dev/null +++ b/scenarios/test_apt_mirror/03_lan2_infrastructure/stage_00/lan2-ubuntu-jump-00.yml @@ -0,0 +1,81 @@ +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# +# LAN2 INFRASTRUCTURE INIT - CLONE + CLOUD-INIT +# +# hostname : lan2-ubuntu-jump-00 +# vm_id : 1013 +# ip : 192.168.151.13 +# template : template-vm-ubuntu-resolute-small-01-4g-32g +# +# global_* vars are supplied by ../_main.yml (kept here as Jinja references). +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- hosts: proxmox + gather_facts: false + vars_files: + - "../../secrets/default_vault.yml" + + tasks: + - name: "LAN2 INFRASTRUCTURE INIT - {{ global_vm_name }} - CLONE TEMPLATE - {{ global_template_name }}" + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "vm_clone" + vm_id: "{{ global_template_vm_id }}" + vm_new_id: "{{ global_vm_id }}" + vm_name: "{{ global_vm_name }}" + vm_description: "{{ global_vm_description }}" + + - name: "LAN2 INFRASTRUCTURE INIT - {{ global_vm_name }} - WAIT FOR PROXMOX UNLOCK" + ansible.builtin.shell: qm config {{ global_vm_id }} | grep -q '^lock:' + register: clone_lock_check + until: clone_lock_check.rc != 0 + retries: 60 + delay: 5 + failed_when: false + changed_when: false + delegate_to: "{{ inventory_hostname }}-cli" + + - name: "LAN2 INFRASTRUCTURE INIT - {{ global_vm_name }} - SET PROXMOX TAG" + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "vm_set_tag" + vm_id: "{{ global_vm_id }}" + vm_tag_name: "{{ global_vm_tag_name }}" + + - name: "LAN2 INFRASTRUCTURE INIT - {{ global_vm_name }} - SET CLOUD-INIT VARIABLES" + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "cloudinit_set_variables" + vm_id: "{{ global_vm_id }}" + vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" + vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" + vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" + vm_ci_ip: "{{ global_vm_ci_ip }}" + vm_ci_netmask: "24" + vm_ci_ip_gw: "192.168.151.1" + vm_net_virtio_bridge: "vmbr151" + + - name: "LAN2 INFRASTRUCTURE INIT - {{ global_vm_name }} - START VM" + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "vm_start" + vm_id: "{{ global_vm_id }}" + +- hosts: "{{ global_vm_ssh_name }}" + gather_facts: false + vars_files: + - "../../secrets/default_vault.yml" + vars: + ARG_vm_ssh_name: "{{ global_vm_ssh_name }}" + requested_tasks: + - wait/openssh_server/is_reachable.yml + - wait/cloudinit/is_boot_finished.yml + tasks: + - name: "LAN2 INFRASTRUCTURE INIT - {{ global_vm_name }} - WAIT FOR SSH + CLOUD-INIT" + include_role: + name: ansible.utils diff --git a/scenarios/test_apt_mirror/03_lan2_infrastructure/stage_01/lan2-debian-jump-00.devkit/test_apt_mirror.lan2-debian-jump-00.install.sh b/scenarios/test_apt_mirror/03_lan2_infrastructure/stage_01/lan2-debian-jump-00.devkit/test_apt_mirror.lan2-debian-jump-00.install.sh new file mode 100755 index 00000000..01989c0c --- /dev/null +++ b/scenarios/test_apt_mirror/03_lan2_infrastructure/stage_01/lan2-debian-jump-00.devkit/test_apt_mirror.lan2-debian-jump-00.install.sh @@ -0,0 +1,8 @@ +#!/bin/bash +## +## reinstall software on lan2-debian-jump-00 only (fast single-VM iteration) +## + +ansible-playbook -i "${RANGE42_ANSIBLE_ROLES__INVENTORY_DIR}/inventory_default.yml" \ + -l "all" \ + "../lan2-debian-jump-00.yml" --vault-password-file "${RANGE42_VAULT_PASSWORD_FILE:?RANGE42_VAULT_PASSWORD_FILE is not set — run: range42-context use }" diff --git a/scenarios/test_apt_mirror/03_lan2_infrastructure/stage_01/lan2-debian-jump-00.devkit/test_apt_mirror.lan2-debian-jump-00.revert.sh b/scenarios/test_apt_mirror/03_lan2_infrastructure/stage_01/lan2-debian-jump-00.devkit/test_apt_mirror.lan2-debian-jump-00.revert.sh new file mode 100755 index 00000000..6caf2355 --- /dev/null +++ b/scenarios/test_apt_mirror/03_lan2_infrastructure/stage_01/lan2-debian-jump-00.devkit/test_apt_mirror.lan2-debian-jump-00.revert.sh @@ -0,0 +1,8 @@ +#!/bin/bash +## +## revert lan2-debian-jump-00 to its latest snapshot and restart it +## + +VM_ID=$(devkit_manifest.find_vm_id.to.text.sh "$0" "lan2-debian-jump-00") || exit 1 +echo "{\"proxmox_node\":\"pve\",\"vm_id\":${VM_ID} }" | proxmox_snapshot_vm.vm_id.revert_snapshot.to.jsons.sh +echo "{\"proxmox_node\":\"pve\",\"vm_id\":${VM_ID} }" | proxmox_vm.vm_id.start.to.jsons.sh diff --git a/scenarios/test_apt_mirror/03_lan2_infrastructure/stage_01/lan2-debian-jump-00.devkit/test_apt_mirror.lan2-debian-jump-00.snapshot.sh b/scenarios/test_apt_mirror/03_lan2_infrastructure/stage_01/lan2-debian-jump-00.devkit/test_apt_mirror.lan2-debian-jump-00.snapshot.sh new file mode 100755 index 00000000..71fb0c9a --- /dev/null +++ b/scenarios/test_apt_mirror/03_lan2_infrastructure/stage_01/lan2-debian-jump-00.devkit/test_apt_mirror.lan2-debian-jump-00.snapshot.sh @@ -0,0 +1,7 @@ +#!/bin/bash +## +## snapshot lan2-debian-jump-00 as "base" +## + +VM_ID=$(devkit_manifest.find_vm_id.to.text.sh "$0" "lan2-debian-jump-00") || exit 1 +echo "{\"proxmox_node\":\"pve\",\"vm_id\":${VM_ID},\"vm_snapshot_description\":\"base\"}" | proxmox_snapshot_vm.vm_id.create_snapshot.to.jsons.sh diff --git a/scenarios/test_apt_mirror/03_lan2_infrastructure/stage_01/lan2-debian-jump-00.yml b/scenarios/test_apt_mirror/03_lan2_infrastructure/stage_01/lan2-debian-jump-00.yml new file mode 100644 index 00000000..23c552a2 --- /dev/null +++ b/scenarios/test_apt_mirror/03_lan2_infrastructure/stage_01/lan2-debian-jump-00.yml @@ -0,0 +1,39 @@ +## +## stage_01 — software install for lan2-debian-jump-00 +## catalog roles are referenced BY NAME (resolved via ANSIBLE_ROLES_PATH at deploy); +## one play per attachment so each carries its own params (firewall_rules, docker …). +## + +- name: "lan2-debian-jump-00 — software.configure.firewalls" + hosts: r42.lan2-debian-jump-00 + become: true + vars_files: + - "../../secrets/default_vault.yml" + vars: + firewall_rules: + - ip: all + port: 22 + protocol: tcp + roles: + - software.configure.firewalls + +- name: "lan2-debian-jump-00 — software.install.warmup.basic_packages" + hosts: r42.lan2-debian-jump-00 + become: true + vars_files: + - "../../secrets/default_vault.yml" + roles: + - software.install.warmup.basic_packages + +- name: "lan2-debian-jump-00 — software.configure.apt_mirror_client" + hosts: r42.lan2-debian-jump-00 + become: true + vars_files: + - "../../secrets/default_vault.yml" + vars: + apt_mirror_airgapped: true + apt_mirror_enabled: true + apt_mirror_http_port: 80 + apt_mirror_vm_ip: 192.168.150.11 + roles: + - software.configure.apt_mirror_client diff --git a/scenarios/test_apt_mirror/03_lan2_infrastructure/stage_01/lan2-ubuntu-jump-00.devkit/test_apt_mirror.lan2-ubuntu-jump-00.install.sh b/scenarios/test_apt_mirror/03_lan2_infrastructure/stage_01/lan2-ubuntu-jump-00.devkit/test_apt_mirror.lan2-ubuntu-jump-00.install.sh new file mode 100755 index 00000000..ee33ee9a --- /dev/null +++ b/scenarios/test_apt_mirror/03_lan2_infrastructure/stage_01/lan2-ubuntu-jump-00.devkit/test_apt_mirror.lan2-ubuntu-jump-00.install.sh @@ -0,0 +1,8 @@ +#!/bin/bash +## +## reinstall software on lan2-ubuntu-jump-00 only (fast single-VM iteration) +## + +ansible-playbook -i "${RANGE42_ANSIBLE_ROLES__INVENTORY_DIR}/inventory_default.yml" \ + -l "all" \ + "../lan2-ubuntu-jump-00.yml" --vault-password-file "${RANGE42_VAULT_PASSWORD_FILE:?RANGE42_VAULT_PASSWORD_FILE is not set — run: range42-context use }" diff --git a/scenarios/test_apt_mirror/03_lan2_infrastructure/stage_01/lan2-ubuntu-jump-00.devkit/test_apt_mirror.lan2-ubuntu-jump-00.revert.sh b/scenarios/test_apt_mirror/03_lan2_infrastructure/stage_01/lan2-ubuntu-jump-00.devkit/test_apt_mirror.lan2-ubuntu-jump-00.revert.sh new file mode 100755 index 00000000..fd1327bd --- /dev/null +++ b/scenarios/test_apt_mirror/03_lan2_infrastructure/stage_01/lan2-ubuntu-jump-00.devkit/test_apt_mirror.lan2-ubuntu-jump-00.revert.sh @@ -0,0 +1,8 @@ +#!/bin/bash +## +## revert lan2-ubuntu-jump-00 to its latest snapshot and restart it +## + +VM_ID=$(devkit_manifest.find_vm_id.to.text.sh "$0" "lan2-ubuntu-jump-00") || exit 1 +echo "{\"proxmox_node\":\"pve\",\"vm_id\":${VM_ID} }" | proxmox_snapshot_vm.vm_id.revert_snapshot.to.jsons.sh +echo "{\"proxmox_node\":\"pve\",\"vm_id\":${VM_ID} }" | proxmox_vm.vm_id.start.to.jsons.sh diff --git a/scenarios/test_apt_mirror/03_lan2_infrastructure/stage_01/lan2-ubuntu-jump-00.devkit/test_apt_mirror.lan2-ubuntu-jump-00.snapshot.sh b/scenarios/test_apt_mirror/03_lan2_infrastructure/stage_01/lan2-ubuntu-jump-00.devkit/test_apt_mirror.lan2-ubuntu-jump-00.snapshot.sh new file mode 100755 index 00000000..57ed6153 --- /dev/null +++ b/scenarios/test_apt_mirror/03_lan2_infrastructure/stage_01/lan2-ubuntu-jump-00.devkit/test_apt_mirror.lan2-ubuntu-jump-00.snapshot.sh @@ -0,0 +1,7 @@ +#!/bin/bash +## +## snapshot lan2-ubuntu-jump-00 as "base" +## + +VM_ID=$(devkit_manifest.find_vm_id.to.text.sh "$0" "lan2-ubuntu-jump-00") || exit 1 +echo "{\"proxmox_node\":\"pve\",\"vm_id\":${VM_ID},\"vm_snapshot_description\":\"base\"}" | proxmox_snapshot_vm.vm_id.create_snapshot.to.jsons.sh diff --git a/scenarios/test_apt_mirror/03_lan2_infrastructure/stage_01/lan2-ubuntu-jump-00.yml b/scenarios/test_apt_mirror/03_lan2_infrastructure/stage_01/lan2-ubuntu-jump-00.yml new file mode 100644 index 00000000..8ff04c7b --- /dev/null +++ b/scenarios/test_apt_mirror/03_lan2_infrastructure/stage_01/lan2-ubuntu-jump-00.yml @@ -0,0 +1,39 @@ +## +## stage_01 — software install for lan2-ubuntu-jump-00 +## catalog roles are referenced BY NAME (resolved via ANSIBLE_ROLES_PATH at deploy); +## one play per attachment so each carries its own params (firewall_rules, docker …). +## + +- name: "lan2-ubuntu-jump-00 — software.configure.firewalls" + hosts: r42.lan2-ubuntu-jump-00 + become: true + vars_files: + - "../../secrets/default_vault.yml" + vars: + firewall_rules: + - ip: all + port: 22 + protocol: tcp + roles: + - software.configure.firewalls + +- name: "lan2-ubuntu-jump-00 — software.install.warmup.basic_packages" + hosts: r42.lan2-ubuntu-jump-00 + become: true + vars_files: + - "../../secrets/default_vault.yml" + roles: + - software.install.warmup.basic_packages + +- name: "lan2-ubuntu-jump-00 — software.configure.apt_mirror_client" + hosts: r42.lan2-ubuntu-jump-00 + become: true + vars_files: + - "../../secrets/default_vault.yml" + vars: + apt_mirror_airgapped: true + apt_mirror_enabled: true + apt_mirror_http_port: 80 + apt_mirror_vm_ip: 192.168.150.11 + roles: + - software.configure.apt_mirror_client diff --git a/scenarios/test_apt_mirror/README.md b/scenarios/test_apt_mirror/README.md new file mode 100644 index 00000000..b43cba4a --- /dev/null +++ b/scenarios/test_apt_mirror/README.md @@ -0,0 +1,25 @@ +# test_apt_mirror + +Generated by **r42playbooks**. + +- subnet layout: `dual-lan` + +> Test scenario: local apt-mirror on lan1, Debian+Ubuntu clients on lan2. No network policy. + +## Boxes + +| box | subnet | image | vm_id | ip | +|---|---|---|---|---| +| `lan1-apt-mirror-00` | lan1 | debian_trixie | 2011 | 192.168.150.11 | +| `lan2-debian-jump-00` | lan2 | debian_trixie | 1012 | 192.168.151.12 | +| `lan2-ubuntu-jump-00` | lan2 | ubuntu_resolute | 1013 | 192.168.151.13 | + +## Deploy + +```bash +range42-context use test_apt_mirror # creates secrets/ symlink + vault +./test_apt_mirror.setup.sh # templates + VMs + software +``` + +The composition is reproduced in `scenario.r42.yml` — re-run +`r42playbooks new --spec scenario.r42.yml` to regenerate this tree. diff --git a/scenarios/test_apt_mirror/_activate.sh b/scenarios/test_apt_mirror/_activate.sh new file mode 100755 index 00000000..fbdff157 --- /dev/null +++ b/scenarios/test_apt_mirror/_activate.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +# deprecation algo issue with paramiko and ubuntu 24.04.2 LTS +# https://github.com/paramiko/paramiko/issues/2419 + +set -euo pipefail + +VIRTUAL_ENV_DIR="$HOME/ansible_fix/venv" + +if [ ! -d "$VIRTUAL_ENV_DIR" ]; then + + mkdir -p "$HOME/ansible_fix" + python3 -m venv "$VIRTUAL_ENV_DIR" + + # shellcheck disable=SC1091 + source "$VIRTUAL_ENV_DIR/bin/activate" + + pip install --upgrade pip setuptools wheel + pip install ansible paramiko cryptography +fi + +# shellcheck disable=SC1091 +source "$VIRTUAL_ENV_DIR/bin/activate" + +echo "source \"$VIRTUAL_ENV_DIR/bin/activate\"" diff --git a/scenarios/test_apt_mirror/devkit_ansible.show_ansible_inventory.to.text.sh b/scenarios/test_apt_mirror/devkit_ansible.show_ansible_inventory.to.text.sh new file mode 100755 index 00000000..29f83a48 --- /dev/null +++ b/scenarios/test_apt_mirror/devkit_ansible.show_ansible_inventory.to.text.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +ansible-inventory -i "${RANGE42_ANSIBLE_ROLES__INVENTORY_DIR}/inventory_default.yml" --graph diff --git a/scenarios/test_apt_mirror/main.yml b/scenarios/test_apt_mirror/main.yml new file mode 100644 index 00000000..3cbac5bf --- /dev/null +++ b/scenarios/test_apt_mirror/main.yml @@ -0,0 +1,9 @@ +--- +## +## test_apt_mirror — generated by r42playbooks +## + +- import_playbook: ./01_init_proxmox/_main.yml + +- import_playbook: ./02_lan1_infrastructure/_main.yml +- import_playbook: ./03_lan2_infrastructure/_main.yml diff --git a/scenarios/test_apt_mirror/main_vms_only.yml b/scenarios/test_apt_mirror/main_vms_only.yml new file mode 100644 index 00000000..7797cb71 --- /dev/null +++ b/scenarios/test_apt_mirror/main_vms_only.yml @@ -0,0 +1,7 @@ +--- +## +## test_apt_mirror — deploy VMs only (templates already exist; skip 01_init_proxmox) +## + +- import_playbook: ./02_lan1_infrastructure/_main.yml +- import_playbook: ./03_lan2_infrastructure/_main.yml diff --git a/scenarios/test_apt_mirror/manifest/scenario_vms.json b/scenarios/test_apt_mirror/manifest/scenario_vms.json new file mode 100644 index 00000000..783d10e9 --- /dev/null +++ b/scenarios/test_apt_mirror/manifest/scenario_vms.json @@ -0,0 +1,57 @@ +{ + "description": "Test scenario: local apt-mirror on lan1, Debian+Ubuntu clients on lan2. No network policy.", + "scenario": "test_apt_mirror", + "templates": [ + { + "bridge": "vmbr140", + "image": "debian_trixie", + "ip": "192.168.140.3", + "spec": "1cpu/4gb/32gb", + "vm_id": 9321, + "vm_name": "template-vm-debian-trixie-small" + }, + { + "bridge": "vmbr140", + "image": "debian_trixie", + "ip": "192.168.140.2", + "spec": "4cpu/16gb/500gb", + "vm_id": 9341, + "vm_name": "template-vm-debian-trixie-large" + }, + { + "bridge": "vmbr140", + "image": "ubuntu_resolute", + "ip": "192.168.140.4", + "spec": "1cpu/4gb/32gb", + "vm_id": 9521, + "vm_name": "template-vm-ubuntu-resolute-small-01-4g-32g" + } + ], + "version": 2, + "vms": [ + { + "bridge": "vmbr151", + "image": "debian_trixie", + "ip": "192.168.151.12", + "subnet": "lan2", + "vm_id": 1012, + "vm_name": "lan2-debian-jump-00" + }, + { + "bridge": "vmbr151", + "image": "ubuntu_resolute", + "ip": "192.168.151.13", + "subnet": "lan2", + "vm_id": 1013, + "vm_name": "lan2-ubuntu-jump-00" + }, + { + "bridge": "vmbr150", + "image": "debian_trixie", + "ip": "192.168.150.11", + "subnet": "lan1", + "vm_id": 2011, + "vm_name": "lan1-apt-mirror-00" + } + ] +} diff --git a/scenarios/test_apt_mirror/scenario.r42.yml b/scenarios/test_apt_mirror/scenario.r42.yml new file mode 100644 index 00000000..c57335c7 --- /dev/null +++ b/scenarios/test_apt_mirror/scenario.r42.yml @@ -0,0 +1,34 @@ +boxes: +- attachments_add: [] + count: 1 + octet: null + subnet: lan1 + template: apt-mirror + template_vm_id: null + vars: {} +- attachments_add: [] + count: 1 + octet: null + subnet: lan2 + template: debian-jump + template_vm_id: null + vars: {} +- attachments_add: [] + count: 1 + octet: null + subnet: lan2 + template: ubuntu-jump + template_vm_id: null + vars: {} +name: test_apt_mirror +network_policy: null +notes: 'Test scenario: local apt-mirror on lan1, Debian+Ubuntu clients on lan2. No + network policy.' +proxmox_node: null +schema_version: 1 +services: + apt: + box: apt-mirror + mode: mirror + wire_to: all +subnet_layout: dual-lan diff --git a/scenarios/test_apt_mirror/templates/ansible-inventory.j2 b/scenarios/test_apt_mirror/templates/ansible-inventory.j2 new file mode 100644 index 00000000..e5efc64d --- /dev/null +++ b/scenarios/test_apt_mirror/templates/ansible-inventory.j2 @@ -0,0 +1,23 @@ +all: + children: + range42_infrastructure: + children: + r42_lan1_group: + hosts: + r42.lan1-apt-mirror-00: + + r42_lan2_group: + hosts: + r42.lan2-debian-jump-00: + r42.lan2-ubuntu-jump-00: + + proxmox: + hosts: + {{ INFRASTRUCTURE_CODENAME }}: + ansible_host: {{ INFRASTRUCTURE_PROXMOX_ADDRESS | mandatory }}:8006 + ansible_connection: local + ansible_python_interpreter: /usr/bin/python3 + + proxmox-cli: + hosts: + {{ INFRASTRUCTURE_CODENAME }}-cli: diff --git a/scenarios/test_apt_mirror/templates/ansible-vars.yml b/scenarios/test_apt_mirror/templates/ansible-vars.yml new file mode 100644 index 00000000..86eb01fc --- /dev/null +++ b/scenarios/test_apt_mirror/templates/ansible-vars.yml @@ -0,0 +1,32 @@ +--- +################################################################################ +# range42 — scenario-specific variables (test_apt_mirror) +# +# Generated by r42playbooks. Shared variables -> group_vars/all/vars.yml, +# secrets -> vault (see vault-example.yml). +################################################################################ + + +#### SCENARIO IDENTIFICATION #### + +INFRASTRUCTURE_SCENARIO: "test_apt_mirror" + + +#### CREDENTIAL GENERATION #### + +context_auto_generate_ssh_keys: "YES" +context_auto_generate_vm_passwords: "YES" + +student_additionnal_keys_count: 5 + + +#### CLOUD-INIT USERNAMES #### + +default_admin_vm_ci_user: "alice" +default_trainee_vm_ci_user: "bob" + + +#### CLOUD-INIT DNS #### +# default resolver for lab VMs; range42-init.py substitutes the Proxmox node +# resolver here at workspace creation. 1.1.1.1 is the portable fallback. +default_vm_ci_dns_ips: "1.1.1.1" diff --git a/scenarios/test_apt_mirror/templates/ssh-config.j2 b/scenarios/test_apt_mirror/templates/ssh-config.j2 new file mode 100644 index 00000000..a7e76bca --- /dev/null +++ b/scenarios/test_apt_mirror/templates/ssh-config.j2 @@ -0,0 +1,41 @@ +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# infrastructure code name : {{ INFRASTRUCTURE_CODENAME }} +# infrastructure proxmox address : {{ INFRASTRUCTURE_PROXMOX_ADDRESS }} +# scenario : {{ INFRASTRUCTURE_SCENARIO }} +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +#### PROXMOX WEB UI (port forward) #### +Host px.{{ INFRASTRUCTURE_CODENAME }}.redirect_www.proxmox + RequestTTY no + RemoteCommand none + localforward localhost:18042 {{ INFRASTRUCTURE_PROXMOX_ADDRESS | mandatory }}:8006 + +#### PX ROOT USER SSH ACCESS #### +Host px.{{ INFRASTRUCTURE_CODENAME }}-ssh_cli.root {{ INFRASTRUCTURE_CODENAME }}-cli + Hostname {{ INFRASTRUCTURE_PROXMOX_ADDRESS | mandatory }} + User root + IdentityFile {{ DEPLOYER_CLI__DST_SSH_KEYS_JUMP_DEST_DIR }}/px.{{ INFRASTRUCTURE_CODENAME }}-{{ INFRASTRUCTURE_SCENARIO }}-ssh_cli.root + Port 22 + +#### SSH JUMPER #### +Host px.{{ INFRASTRUCTURE_CODENAME }}.jumper + Hostname {{ INFRASTRUCTURE_PROXMOX_ADDRESS | mandatory }} + User jump_user + IdentityFile {{ DEPLOYER_CLI__DST_SSH_KEYS_JUMP_DEST_DIR }}/px.{{ INFRASTRUCTURE_CODENAME }}-{{ INFRASTRUCTURE_SCENARIO }}-ssh_cli.jump_user + Port 22 + +#### SCENARIO VMs #### +Host r42.lan1-apt-mirror-00 + Hostname 192.168.150.11 + +Host r42.lan2-debian-jump-00 + Hostname 192.168.151.12 + +Host r42.lan2-ubuntu-jump-00 + Hostname 192.168.151.13 + +Host r42.* + User alice + IdentityFile {{ DEPLOYER_CLI__DST_SSH_KEYS_BACKEND_DEST_DIR }}/r42.{{ INFRASTRUCTURE_CODENAME }}-{{ INFRASTRUCTURE_SCENARIO }}-deployer-key_alice + Port 22 + ProxyJump px.{{ INFRASTRUCTURE_CODENAME }}.jumper diff --git a/scenarios/test_apt_mirror/templates/vault-example.yml b/scenarios/test_apt_mirror/templates/vault-example.yml new file mode 100644 index 00000000..4f881f72 --- /dev/null +++ b/scenarios/test_apt_mirror/templates/vault-example.yml @@ -0,0 +1,37 @@ +--- +################################################################################ +# range42 — vault secrets (test_apt_mirror) +# +# Copy to vault.yml, fill real values, then: ansible-vault encrypt vault.yml +# Never commit the unencrypted version. +################################################################################ + + +#### PROXMOX API SECRET #### +proxmox_api_token_secret: "REPLACE_ME" + + +#### JUMP HOST #### +jump_password: "REPLACE_ME" + + +#### CLOUD-INIT PASSWORDS #### +default_admin_vm_ci_password: "REPLACE_ME" +default_admin_vm_ci_ssh_key: "ssh-ed25519 AAAA... alice CODENAME-SCENARIO" +default_trainee_vm_ci_password: "REPLACE_ME" +default_trainee_vm_ci_ssh_key: "ssh-ed25519 AAAA... bob CODENAME-SCENARIO" + + +#### CLOUD-INIT DNS #### +# default resolver for lab VMs; range42-init.py substitutes the Proxmox node +# resolver here at workspace creation. 1.1.1.1 is the portable fallback. +default_vm_ci_dns_ips: "1.1.1.1" + + +#### TAILSCALE #### +infrastructure_tailscale_authkey: "tskey-auth-REPLACE_ME" +infrastructure_tailscale_apikey: "tskey-api-REPLACE_ME" + + +#### MISC #### +deployer_cli_user_ssh_known_hosts: "/home/your_deployer_cli_username/.ssh/known_hosts" diff --git a/scenarios/test_apt_mirror/test_apt_mirror.delete_all.sh b/scenarios/test_apt_mirror/test_apt_mirror.delete_all.sh new file mode 100755 index 00000000..b2d0b680 --- /dev/null +++ b/scenarios/test_apt_mirror/test_apt_mirror.delete_all.sh @@ -0,0 +1,39 @@ +#!/bin/bash + +## +## delete all — VMs + this scenario's ubuntu_noble templates +## +## ⚠ templates (9xxx) are shared across scenarios on the same Proxmox. Run this +## only when no other scenario relies on them, or use delete_vms_only.sh instead. +## + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +MANIFEST="$SCRIPT_DIR/manifest/scenario_vms.json" + +if [[ ! -f "$MANIFEST" ]]; then + echo "ERROR: manifest not found: $MANIFEST" >&2 + exit 1 +fi + +mapfile -t SCENARIO_VM_IDS < <(jq -r '.vms[].vm_id' "$MANIFEST") +mapfile -t TEMPLATE_VM_IDS < <(jq -r '.templates[].vm_id' "$MANIFEST") +mapfile -t INFRASTRUCTURE_IP < <(jq -r '.vms[].ip' "$MANIFEST") + +ALL_IDS=("${SCENARIO_VM_IDS[@]}" "${TEMPLATE_VM_IDS[@]}") +ID_REGEX=$(printf '|%s' "${ALL_IDS[@]}" | sed 's/^|//') + +echo ":: stopping and deleting VMs + templates" +VM_LIST_JSON=$(proxmox_vm.list.to.jsons.sh 2>&1 | grep '"vm_id":[0-9]') +if [ -z "$VM_LIST_JSON" ]; then + echo "ERROR: proxmox_vm.list.to.jsons.sh returned no VM data — aborting" >&2 + exit 1 +fi +echo "$VM_LIST_JSON" | jq -c | grep -E "\"vm_id\":($ID_REGEX)([^0-9]|$)" | proxmox_vm.vm_id.stop_force.to.jsons.sh +echo "$VM_LIST_JSON" | jq -c | grep -E "\"vm_id\":($ID_REGEX)([^0-9]|$)" | proxmox_vm.vm_id.delete.to.jsons.sh + +for ip in "${INFRASTRUCTURE_IP[@]}"; do + echo ":: REMOVE SSH KEY FOR : $ip" + ssh-keygen -f "$HOME/.ssh/known_hosts" -R "$ip" +done + +echo ":: done — VMs and templates removed" diff --git a/scenarios/test_apt_mirror/test_apt_mirror.delete_vms_only.sh b/scenarios/test_apt_mirror/test_apt_mirror.delete_vms_only.sh new file mode 100755 index 00000000..ba2443a3 --- /dev/null +++ b/scenarios/test_apt_mirror/test_apt_mirror.delete_vms_only.sh @@ -0,0 +1,33 @@ +#!/bin/bash + +## +## delete VMs only — scenario VMs (filter by vm_id), keep templates +## + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +MANIFEST="$SCRIPT_DIR/manifest/scenario_vms.json" + +if [[ ! -f "$MANIFEST" ]]; then + echo "ERROR: manifest not found: $MANIFEST" >&2 + exit 1 +fi + +mapfile -t SCENARIO_VM_IDS < <(jq -r '.vms[].vm_id' "$MANIFEST") +mapfile -t INFRASTRUCTURE_IP < <(jq -r '.vms[].ip' "$MANIFEST") +ID_REGEX=$(printf '|%s' "${SCENARIO_VM_IDS[@]}" | sed 's/^|//') + +echo ":: stopping and deleting scenario VMs (keeping templates)..." +VM_LIST_JSON=$(proxmox_vm.list.to.jsons.sh 2>&1 | grep '"vm_id":[0-9]') +if [ -z "$VM_LIST_JSON" ]; then + echo "ERROR: proxmox_vm.list.to.jsons.sh returned no VM data — aborting" >&2 + exit 1 +fi +echo "$VM_LIST_JSON" | jq -c | grep -E "\"vm_id\":($ID_REGEX)([^0-9]|$)" | proxmox_vm.vm_id.stop_force.to.jsons.sh +echo "$VM_LIST_JSON" | jq -c | grep -E "\"vm_id\":($ID_REGEX)([^0-9]|$)" | proxmox_vm.vm_id.delete.to.jsons.sh + +for ip in "${INFRASTRUCTURE_IP[@]}"; do + echo ":: REMOVE SSH KEY FOR : $ip" + ssh-keygen -f "$HOME/.ssh/known_hosts" -R "$ip" +done + +echo ":: done — templates preserved" diff --git a/scenarios/test_apt_mirror/test_apt_mirror.reset.setup.sh b/scenarios/test_apt_mirror/test_apt_mirror.reset.setup.sh new file mode 100755 index 00000000..b70309e0 --- /dev/null +++ b/scenarios/test_apt_mirror/test_apt_mirror.reset.setup.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +## +## reset — delete this scenario's VMs (from manifest) then re-deploy +## + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +MANIFEST="$SCRIPT_DIR/manifest/scenario_vms.json" + +if [[ ! -f "$MANIFEST" ]]; then + echo "ERROR: manifest not found: $MANIFEST" >&2 + exit 1 +fi + +mapfile -t SCENARIO_VM_IDS < <(jq -r '.vms[].vm_id' "$MANIFEST") +mapfile -t INFRASTRUCTURE_IP < <(jq -r '.vms[].ip' "$MANIFEST") +ID_REGEX=$(printf '|%s' "${SCENARIO_VM_IDS[@]}" | sed 's/^|//') + +echo ":: stopping and deleting scenario VMs (vm_ids: ${SCENARIO_VM_IDS[*]})..." +VM_LIST_JSON=$(proxmox_vm.list.to.jsons.sh 2>&1 | grep '"vm_id":[0-9]') +if [ -z "$VM_LIST_JSON" ]; then + echo "ERROR: proxmox_vm.list.to.jsons.sh returned no VM data — aborting" >&2 + exit 1 +fi +echo "$VM_LIST_JSON" | jq -c | grep -E "\"vm_id\":($ID_REGEX)([^0-9]|$)" | proxmox_vm.vm_id.stop_force.to.jsons.sh +echo "$VM_LIST_JSON" | jq -c | grep -E "\"vm_id\":($ID_REGEX)([^0-9]|$)" | proxmox_vm.vm_id.delete.to.jsons.sh + +for ip in "${INFRASTRUCTURE_IP[@]}"; do + echo ":: REMOVE SSH KEY FOR : $ip" + ssh-keygen -f "$HOME/.ssh/known_hosts" -R "$ip" +done + +ansible-playbook -i "${RANGE42_ANSIBLE_ROLES__INVENTORY_DIR}/inventory_default.yml" \ + -l "all" \ + "./main.yml" --vault-password-file "${RANGE42_VAULT_PASSWORD_FILE:?RANGE42_VAULT_PASSWORD_FILE is not set — run: range42-context use }" diff --git a/scenarios/test_apt_mirror/test_apt_mirror.reset.ssh_keys.sh b/scenarios/test_apt_mirror/test_apt_mirror.reset.ssh_keys.sh new file mode 100755 index 00000000..bea1d07b --- /dev/null +++ b/scenarios/test_apt_mirror/test_apt_mirror.reset.ssh_keys.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +## +## remove known_hosts entries for every VM IP in this scenario (from manifest) +## + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +MANIFEST="$SCRIPT_DIR/manifest/scenario_vms.json" + +if [[ ! -f "$MANIFEST" ]]; then + echo "ERROR: manifest not found: $MANIFEST" >&2 + exit 1 +fi + +mapfile -t INFRASTRUCTURE_IP < <(jq -r '.vms[].ip' "$MANIFEST") + +for ip in "${INFRASTRUCTURE_IP[@]}"; do + echo ":: REMOVE SSH KEY FOR : $ip" + ssh-keygen -f "$HOME/.ssh/known_hosts" -R "$ip" +done diff --git a/scenarios/test_apt_mirror/test_apt_mirror.setup.sh b/scenarios/test_apt_mirror/test_apt_mirror.setup.sh new file mode 100755 index 00000000..aff48748 --- /dev/null +++ b/scenarios/test_apt_mirror/test_apt_mirror.setup.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +ansible-playbook -i "${RANGE42_ANSIBLE_ROLES__INVENTORY_DIR}/inventory_default.yml" \ + -l "all" \ + "./main.yml" --vault-password-file "${RANGE42_VAULT_PASSWORD_FILE:?RANGE42_VAULT_PASSWORD_FILE is not set — run: range42-context use }" diff --git a/scenarios/test_apt_mirror/test_apt_mirror.setup_vms_only.sh b/scenarios/test_apt_mirror/test_apt_mirror.setup_vms_only.sh new file mode 100755 index 00000000..f7f81aa4 --- /dev/null +++ b/scenarios/test_apt_mirror/test_apt_mirror.setup_vms_only.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +## +## deploy VMs only — skip template download and creation +## faster redeploy when templates already exist on proxmox +## + +ansible-playbook -i "${RANGE42_ANSIBLE_ROLES__INVENTORY_DIR}/inventory_default.yml" \ + -l "all" \ + "./main_vms_only.yml" --vault-password-file "${RANGE42_VAULT_PASSWORD_FILE:?RANGE42_VAULT_PASSWORD_FILE is not set — run: range42-context use }" diff --git a/scenarios/test_mattermost/01_init_proxmox/_main.reinstall.sh b/scenarios/test_mattermost/01_init_proxmox/_main.reinstall.sh new file mode 100644 index 00000000..390e3cda --- /dev/null +++ b/scenarios/test_mattermost/01_init_proxmox/_main.reinstall.sh @@ -0,0 +1,7 @@ +#!/bin/bash +## +## re-run 01_init_proxmox (download images + create templates) +## +ansible-playbook -i "${RANGE42_ANSIBLE_ROLES__INVENTORY_DIR}/inventory_default.yml" \ + -l "proxmox" \ + "./_main.yml" --vault-password-file "${RANGE42_VAULT_PASSWORD_FILE:?RANGE42_VAULT_PASSWORD_FILE is not set — run: range42-context use }" diff --git a/scenarios/test_mattermost/01_init_proxmox/_main.yml b/scenarios/test_mattermost/01_init_proxmox/_main.yml new file mode 100644 index 00000000..40a81665 --- /dev/null +++ b/scenarios/test_mattermost/01_init_proxmox/_main.yml @@ -0,0 +1,2 @@ +- import_playbook: ./stage_00-download_cloudinit_files/_main.yml +- import_playbook: ./stage_01-create_templates/_main.yml diff --git a/scenarios/test_mattermost/01_init_proxmox/stage_00-download_cloudinit_files/_main.yml b/scenarios/test_mattermost/01_init_proxmox/stage_00-download_cloudinit_files/_main.yml new file mode 100644 index 00000000..d9e39ea2 --- /dev/null +++ b/scenarios/test_mattermost/01_init_proxmox/stage_00-download_cloudinit_files/_main.yml @@ -0,0 +1,4 @@ +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# PROMOX INIT - download cloud-init base images (only the OS families used) +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +- import_playbook: ./cloudinit_debian_trixie.yml diff --git a/scenarios/test_mattermost/01_init_proxmox/stage_00-download_cloudinit_files/cloudinit_debian_trixie.yml b/scenarios/test_mattermost/01_init_proxmox/stage_00-download_cloudinit_files/cloudinit_debian_trixie.yml new file mode 100644 index 00000000..633cfec3 --- /dev/null +++ b/scenarios/test_mattermost/01_init_proxmox/stage_00-download_cloudinit_files/cloudinit_debian_trixie.yml @@ -0,0 +1,39 @@ +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# +# PROXMOX INIT - download cloud init image for debian_trixie +# +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- hosts: proxmox + gather_facts: false + vars_files: + - "../../secrets/default_vault.yml" + + tasks: + - name: PROMOX INIT - DOWNLOAD - CLOUD INIT images + include_role: + name: range42-ansible_roles-proxmox_controller + + vars: + proxmox_vm_action: "storage_download_iso" + proxmox_storage: "local" + iso_file_content_type: "iso" + iso_url: "https://cloud.debian.org/images/cloud/trixie/latest/debian-13-genericcloud-amd64.raw" + iso_file_name: "debian-13-genericcloud-amd64.img" + +# The download-url API is async (returns a UPID immediately). Poll on +# proxmox-cli until the file is fully on disk before stage_01 imports it. +- hosts: proxmox-cli + gather_facts: false + vars_files: + - "../../secrets/default_vault.yml" + + tasks: + - name: PROMOX INIT - WAIT for debian-13-genericcloud-amd64.img to be fully downloaded + ansible.builtin.stat: + path: "/var/lib/vz/template/iso/debian-13-genericcloud-amd64.img" + register: _cloudinit_img + until: _cloudinit_img.stat.exists and _cloudinit_img.stat.size > 100000000 + retries: 120 # 120 x 10 s = 20 min max + delay: 10 + changed_when: false diff --git a/scenarios/test_mattermost/01_init_proxmox/stage_01-create_templates/_main.yml b/scenarios/test_mattermost/01_init_proxmox/stage_01-create_templates/_main.yml new file mode 100644 index 00000000..e669287d --- /dev/null +++ b/scenarios/test_mattermost/01_init_proxmox/stage_01-create_templates/_main.yml @@ -0,0 +1,5 @@ +--- +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# PROMOX INIT - create the Proxmox template images (only the OS families used) +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +- import_playbook: ./templates/debian_trixie/_main_debian_trixie.yml diff --git a/scenarios/test_mattermost/01_init_proxmox/stage_01-create_templates/templates/debian_trixie/_apply_apt_proxy.yml b/scenarios/test_mattermost/01_init_proxmox/stage_01-create_templates/templates/debian_trixie/_apply_apt_proxy.yml new file mode 100644 index 00000000..04e2f296 --- /dev/null +++ b/scenarios/test_mattermost/01_init_proxmox/stage_01-create_templates/templates/debian_trixie/_apply_apt_proxy.yml @@ -0,0 +1,52 @@ +## +## debian_trixie - apt proxy via cloud-init cicustom (optional) +## +## Attaches /var/lib/vz/snippets/range42-apt-proxy.yaml as cloud-init vendor-data +## to every template VM. All VMs cloned from these templates inherit the apt +## proxy automatically via cloud-init at first boot. +## Skipped (no-op) if apt_proxy_url is empty/unset. Idempotent. +## + +- hosts: proxmox-cli + gather_facts: false + vars_files: + - "../../../../secrets/default_vault.yml" + + vars: + bs2_template_vm_ids: + - 9321 # template-vm-debian-trixie-small + - 9331 # template-vm-debian-trixie-medium + + tasks: + - name: APT-PROXY - SKIP (apt_proxy_url is empty) + ansible.builtin.debug: + msg: "apt_proxy_url is empty - cloud-init cicustom not applied to templates" + when: apt_proxy_url is not defined or (apt_proxy_url | length) == 0 + + - name: APT-PROXY - ATTACH cicustom vendor TO TEMPLATES + ansible.builtin.command: + cmd: qm set {{ item }} --cicustom "vendor=local:snippets/range42-apt-proxy.yaml" + loop: "{{ bs2_template_vm_ids }}" + register: qm_result + changed_when: qm_result.rc == 0 + failed_when: + - qm_result.rc != 0 + - "'does not exist' not in qm_result.stderr" + when: apt_proxy_url is defined and (apt_proxy_url | length) > 0 + + - name: APT-PROXY - DETACH cicustom vendor FROM TEMPLATES (apt_proxy_url unset) + ansible.builtin.command: + cmd: qm set {{ item }} --delete cicustom + loop: "{{ bs2_template_vm_ids }}" + register: qm_unset + changed_when: qm_unset.rc == 0 + failed_when: + - qm_unset.rc != 0 + - "'does not exist' not in qm_unset.stderr" + when: apt_proxy_url is not defined or (apt_proxy_url | length) == 0 + + - name: APT-PROXY - DISPLAY STATUS + ansible.builtin.debug: + msg: | + apt proxy: {{ apt_proxy_url if (apt_proxy_url is defined and (apt_proxy_url | length) > 0) else 'DISABLED' }} + cicustom : {{ 'attached to ' + (bs2_template_vm_ids | length | string) + ' templates' if (apt_proxy_url is defined and (apt_proxy_url | length) > 0) else 'detached from templates' }} diff --git a/scenarios/test_mattermost/01_init_proxmox/stage_01-create_templates/templates/debian_trixie/_main_debian_trixie.yml b/scenarios/test_mattermost/01_init_proxmox/stage_01-create_templates/templates/debian_trixie/_main_debian_trixie.yml new file mode 100644 index 00000000..ca3944c3 --- /dev/null +++ b/scenarios/test_mattermost/01_init_proxmox/stage_01-create_templates/templates/debian_trixie/_main_debian_trixie.yml @@ -0,0 +1,13 @@ +## +## debian_trixie — create + configure all Proxmox template VMs for this image +## + +- import_playbook: ./template-vm-debian-trixie-small.yml +- import_playbook: ./template-vm-debian-trixie-medium.yml + +# apt proxy cicustom (no-op if apt_proxy_url empty) +- import_playbook: ./_apply_apt_proxy.yml + +# pre-update: start each VM, run apt update + dist-upgrade via cloud-init poweroff, +# then convert to template. +- import_playbook: ./_update_templates.yml diff --git a/scenarios/test_mattermost/01_init_proxmox/stage_01-create_templates/templates/debian_trixie/_update_templates.yml b/scenarios/test_mattermost/01_init_proxmox/stage_01-create_templates/templates/debian_trixie/_update_templates.yml new file mode 100644 index 00000000..2455c653 --- /dev/null +++ b/scenarios/test_mattermost/01_init_proxmox/stage_01-create_templates/templates/debian_trixie/_update_templates.yml @@ -0,0 +1,202 @@ +## +## debian_trixie - pre-update templates +## +## After all templates are created (but BEFORE convert to template), this playbook: +## 1. renders a temporary bootstrap snippet directly on the Proxmox host +## (apt update + apt dist-upgrade + cloud-init clean + auto-poweroff) +## 2. attaches it as cicustom vendor on each template VM +## 3. starts all templates +## 4. polls qm status until each VM is stopped (cloud-init auto-poweroff signals end) +## 5. detaches the bootstrap snippet, re-attaches the persistent apt-proxy snippet +## 6. converts each VM to a template (qm template ) +## +## Reads VM IDs from the scenario manifest (manifest/scenario_vms.json). +## Runs entirely on `proxmox-cli` (= the Proxmox host itself), no SSH involved. +## + +- hosts: proxmox-cli + gather_facts: false + vars_files: + - "../../../../secrets/default_vault.yml" + + vars: + manifest_path: "{{ playbook_dir }}/../../../../manifest/scenario_vms.json" + bootstrap_snippet_template: "{{ playbook_dir }}/range42-template-bootstrap.yaml.j2" + bootstrap_snippet_path: "/var/lib/vz/snippets/range42-template-bootstrap.yaml" + + tasks: + + #### #### #### debug apt_proxy_url visibility - fully safe vs undefined #### #### #### + + - name: TEMPLATES UPDATE - DEBUG apt_proxy_url visibility + ansible.builtin.debug: + msg: | + apt_proxy_url defined : {{ apt_proxy_url is defined }} + apt_proxy_url value : {{ apt_proxy_url | default('(undefined)') }} + apt_proxy_url length : {{ (apt_proxy_url | default('')) | length }} + -> snippet WILL include apt section : {{ (apt_proxy_url | default('')) | length > 0 }} + + #### #### #### load manifest #### #### #### + + - name: TEMPLATES UPDATE - LOAD MANIFEST + ansible.builtin.set_fact: + templates: "{{ (lookup('file', manifest_path) | from_json).templates }}" + + #### #### #### IDEMPOTENCE - probe each template's current state #### #### #### + # 3 possible states per VM ID : + # - "template" : already converted, skip update entirely + # - "vm" : exists as a regular VM, needs update + convert + # - "missing" : VM doesn't exist on Proxmox, skip (templates not created yet) + # Only "vm" state proceeds through the start/update/shutdown/convert pipeline. + + - name: TEMPLATES UPDATE - PROBE state of each template VM + ansible.builtin.shell: | + if qm config {{ item.vm_id }} 2>/dev/null | grep -q '^template:'; then + echo "template" + elif qm config {{ item.vm_id }} >/dev/null 2>&1; then + echo "vm" + else + echo "missing" + fi + register: tpl_state + loop: "{{ templates }}" + loop_control: + label: "vm_id={{ item.vm_id }}" + changed_when: false + + - name: TEMPLATES UPDATE - BUILD list of templates needing update (state == 'vm') + ansible.builtin.set_fact: + templates_to_update: >- + {{ templates | zip(tpl_state.results) + | selectattr('1.stdout', 'eq', 'vm') + | map(attribute='0') | list }} + + - name: TEMPLATES UPDATE - DISPLAY PLAN + ansible.builtin.debug: + msg: | + total templates in manifest : {{ templates | length }} + already converted (skip) : {{ tpl_state.results | selectattr('stdout', 'eq', 'template') | list | length }} + missing (skip) : {{ tpl_state.results | selectattr('stdout', 'eq', 'missing') | list | length }} + to update + convert : {{ templates_to_update | length }} + {% if templates_to_update | length > 0 %} + targets : + {% for t in templates_to_update %} - {{ t.vm_id }} {{ t.vm_name }} ({{ t.ip }}) + {% endfor %} + {% endif %} + + - name: TEMPLATES UPDATE - SHORT-CIRCUIT if nothing to update + ansible.builtin.meta: end_play + when: templates_to_update | length == 0 + + #### #### #### render bootstrap snippet directly on the Proxmox host #### #### #### + + - name: TEMPLATES UPDATE - RENDER BOOTSTRAP SNIPPET ON PROXMOX + ansible.builtin.template: + src: "{{ bootstrap_snippet_template }}" + dest: "{{ bootstrap_snippet_path }}" + mode: "0644" + + #### #### #### attach bootstrap snippet on every template #### #### #### + + - name: TEMPLATES UPDATE - ATTACH bootstrap cicustom + ansible.builtin.command: + cmd: qm set {{ item.vm_id }} --cicustom "vendor=local:snippets/range42-template-bootstrap.yaml" + loop: "{{ templates_to_update }}" + loop_control: + label: "vm_id={{ item.vm_id }}" + + #### #### #### start all templates #### #### #### + + - name: TEMPLATES UPDATE - START all templates + ansible.builtin.command: + cmd: qm start {{ item.vm_id }} + register: start_result + failed_when: + - start_result.rc != 0 + - "'already running' not in start_result.stderr" + - "'MAX' not in start_result.stderr" + changed_when: start_result.rc == 0 + loop: "{{ templates_to_update }}" + loop_control: + label: "vm_id={{ item.vm_id }}" + + # VMs that failed with "MAX X vcpus" couldn't start - exclude them from + # the wait/convert pipeline but keep the play running for the others. + - name: TEMPLATES UPDATE - BUILD started list (exclude CPU-limited VMs) + ansible.builtin.set_fact: + templates_started: >- + {{ templates_to_update | zip(start_result.results) + | rejectattr('1.stderr', 'search', 'MAX') + | map(attribute='0') | list }} + templates_skipped_cpu: >- + {{ templates_to_update | zip(start_result.results) + | selectattr('1.stderr', 'search', 'MAX') + | map(attribute='0') | list }} + + - name: TEMPLATES UPDATE - WARN skipped templates (host CPU limit) + ansible.builtin.debug: + msg: | + WARNING: {{ templates_skipped_cpu | length }} template(s) skipped - host CPU limit: + {% for t in templates_skipped_cpu %} + - {{ t.vm_id }} {{ t.vm_name }} (spec: {{ t.spec }}) + {% endfor %} + These templates require more vCPUs than this host provides. + They will NOT be converted. Run on a host with enough physical + CPUs, or lower vm_cores in the template playbook if intentional. + when: templates_skipped_cpu | length > 0 + + #### #### #### wait until each VM auto-poweroffs (cloud-init done) #### #### #### + # the loop is sequential per-vm, but since all started in parallel above, + # total time is bounded by the slowest template (~3-15 min depending on cache) + + - name: TEMPLATES UPDATE - WAIT for cloud-init to auto-poweroff + ansible.builtin.shell: | + qm status {{ item.vm_id }} | grep -q 'status: stopped' + register: stop_check + retries: 360 # safety cap : 360 x 5s = 30 min max per VM + delay: 5 + until: stop_check.rc == 0 + loop: "{{ templates_started }}" + loop_control: + label: "vm_id={{ item.vm_id }}" + changed_when: false + + #### #### #### detach bootstrap, re-attach apt-proxy, convert to template #### #### #### + + - name: TEMPLATES UPDATE - DETACH bootstrap cicustom + ansible.builtin.command: + cmd: qm set {{ item.vm_id }} --delete cicustom + loop: "{{ templates_started }}" + loop_control: + label: "vm_id={{ item.vm_id }}" + + - name: TEMPLATES UPDATE - RE-ATTACH apt-proxy cicustom (persistent for clones) + ansible.builtin.command: + cmd: qm set {{ item.vm_id }} --cicustom "vendor=local:snippets/range42-apt-proxy.yaml" + loop: "{{ templates_started }}" + loop_control: + label: "vm_id={{ item.vm_id }}" + when: (apt_proxy_url | default('')) | length > 0 + + - name: TEMPLATES UPDATE - CONVERT VM to template + ansible.builtin.command: + cmd: qm template {{ item.vm_id }} + loop: "{{ templates_started }}" + loop_control: + label: "vm_id={{ item.vm_id }}" + + #### #### #### cleanup #### #### #### + + - name: TEMPLATES UPDATE - REMOVE BOOTSTRAP SNIPPET FROM PROXMOX + ansible.builtin.file: + path: "{{ bootstrap_snippet_path }}" + state: absent + + - name: TEMPLATES UPDATE - DISPLAY DONE + ansible.builtin.debug: + msg: | + {{ templates_started | length }} templates updated and converted + apt-proxy cicustom re-attached (if apt_proxy_url set) + {% if templates_skipped_cpu | length > 0 %} + {{ templates_skipped_cpu | length }} skipped (host CPU limit) - see warning above + {% endif %} diff --git a/scenarios/test_mattermost/01_init_proxmox/stage_01-create_templates/templates/debian_trixie/range42-template-bootstrap.yaml.j2 b/scenarios/test_mattermost/01_init_proxmox/stage_01-create_templates/templates/debian_trixie/range42-template-bootstrap.yaml.j2 new file mode 100644 index 00000000..46990999 --- /dev/null +++ b/scenarios/test_mattermost/01_init_proxmox/stage_01-create_templates/templates/debian_trixie/range42-template-bootstrap.yaml.j2 @@ -0,0 +1,22 @@ +#cloud-config +# range42 - TEMPORARY bootstrap snippet for templates +# Triggers apt update + dist-upgrade then poweroff. +# Auto-generated by _update_templates.yml. Detached after the template is +# converted, replaced by range42-apt-proxy.yaml (persistent). +# +# NOTE: cloud-init clean was previously here in `runcmd` - REMOVED because +# it ran in cloud_config stage, before cloud_final, and broke +# package-update-upgrade-install. Clones get a new vmid -> new instance-id, +# so cloud-init re-processes naturally on each clone. No clean needed. +# +{% if (apt_proxy_url | default('')) | length > 0 %} +apt: + http_proxy: "{{ apt_proxy_url }}" +{% endif %} +package_update: true +package_upgrade: true +package_reboot_if_required: false +power_state: + mode: poweroff + timeout: 60 + condition: True diff --git a/scenarios/test_mattermost/01_init_proxmox/stage_01-create_templates/templates/debian_trixie/template-vm-debian-trixie-medium.yml b/scenarios/test_mattermost/01_init_proxmox/stage_01-create_templates/templates/debian_trixie/template-vm-debian-trixie-medium.yml new file mode 100644 index 00000000..7667317f --- /dev/null +++ b/scenarios/test_mattermost/01_init_proxmox/stage_01-create_templates/templates/debian_trixie/template-vm-debian-trixie-medium.yml @@ -0,0 +1,134 @@ +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# +# PROXMOX INIT - create debian_trixie template VM template-vm-debian-trixie-medium (id 9331) +# +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- hosts: proxmox + gather_facts: false + vars_files: + - "../../../../secrets/default_vault.yml" + + tasks: + #### IDEMPOTENCE - skip if already finalized as template #### + - name: TEMPLATE 9331 - CHECK if already finalized as template + ansible.builtin.shell: qm config 9331 2>/dev/null | grep -q '^template:' + register: tpl_9331_is_template + failed_when: false + changed_when: false + delegate_to: "{{ inventory_hostname }}-cli" + + - name: TEMPLATE 9331 - WAIT for unlock if a parallel deploy holds the VM + ansible.builtin.shell: qm config 9331 | grep -q '^lock:' + register: tpl_9331_lock + until: tpl_9331_lock.rc != 0 + retries: 30 + delay: 10 + failed_when: false + changed_when: false + delegate_to: "{{ inventory_hostname }}-cli" + when: tpl_9331_is_template.rc != 0 + + - name: TEMPLATE 9331 - SKIP all (already finalized as template) + ansible.builtin.debug: + msg: "Template 9331 is already a template - skipping. Run delete-everything to recreate." + when: tpl_9331_is_template.rc == 0 + + #### vm_create idempotence: separate exists check #### + - name: TEMPLATE 9331 - CHECK if VM exists (vm_create idempotence) + ansible.builtin.shell: qm config 9331 2>/dev/null + register: tpl_9331_exists + failed_when: false + changed_when: false + delegate_to: "{{ inventory_hostname }}-cli" + when: tpl_9331_is_template.rc != 0 + + - name: PROXMOX INIT - TEMPLATES - CREATE TEMPLATE - template-vm-debian-trixie-medium + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "vm_create" + vm_id: 9331 + vm_name: "template-vm-debian-trixie-medium" + vm_cpu: "host" + vm_cores: 2 + vm_sockets: 1 + vm_memory: 8192 + vm_net_virtio_bridge: "vmbr140" + when: + - tpl_9331_is_template.rc != 0 + - tpl_9331_exists.rc != 0 + + - name: PROXMOX INIT - TEMPLATES - WAIT for vm_create to finish (lock-aware) + ansible.builtin.shell: qm config 9331 | grep -q '^lock:' + register: tpl_9331_post_create_lock + until: tpl_9331_post_create_lock.rc != 0 + retries: 60 + delay: 5 + failed_when: false + changed_when: false + delegate_to: "{{ inventory_hostname }}-cli" + when: + - tpl_9331_is_template.rc != 0 + - tpl_9331_exists.rc != 0 + +- hosts: proxmox-cli + gather_facts: false + vars_files: + - "../../../../secrets/default_vault.yml" + + tasks: + - name: TEMPLATE 9331 - CHECK if already template (proxmox-cli play) + ansible.builtin.shell: qm config 9331 2>/dev/null | grep -q '^template:' + register: tpl_9331_is_template + failed_when: false + changed_when: false + + - name: PROXMOX INIT - TEMPLATES - IMPORT CLOUD INIT DISK - template-vm-debian-trixie-medium + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "template_cloudinit_import_disk" + proxmox_node: "pve" + cloudinit_image_full_path: "/var/lib/vz/template/iso/debian-13-genericcloud-amd64.img" + vm_id: 9331 + vm_disk_size: "64g" + proxmox_dest_vm_storage_name: "local-lvm" + when: tpl_9331_is_template.rc != 0 + +- hosts: proxmox + gather_facts: false + vars_files: + - "../../../../secrets/default_vault.yml" + + tasks: + - name: TEMPLATE 9331 - CHECK if already template (third play) + ansible.builtin.shell: qm config 9331 2>/dev/null | grep -q '^template:' + register: tpl_9331_is_template + failed_when: false + changed_when: false + delegate_to: "{{ inventory_hostname }}-cli" + + - name: PROXMOX INIT - TEMPLATES - SET CLOUD DEFAULT INIT VARIABLE - template-vm-debian-trixie-medium + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "cloudinit_set_variables" + vm_id: 9331 + vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" + vm_ci_password: "{{ default_trainee_vm_ci_password | default('supersecret') }}" + vm_ci_ssh_key: "{{ default_trainee_vm_ci_ssh_key }}" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" + vm_ci_ip: "192.168.140.2" + vm_ci_netmask: "24" + vm_ci_ip_gw: "192.168.140.1" + when: tpl_9331_is_template.rc != 0 + + - name: PROXMOX INIT - TEMPLATES - SET PROXMOX TAG + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "vm_set_tag" + vm_id: 9331 + vm_tag_name: "template" + when: tpl_9331_is_template.rc != 0 diff --git a/scenarios/test_mattermost/01_init_proxmox/stage_01-create_templates/templates/debian_trixie/template-vm-debian-trixie-small.yml b/scenarios/test_mattermost/01_init_proxmox/stage_01-create_templates/templates/debian_trixie/template-vm-debian-trixie-small.yml new file mode 100644 index 00000000..bf774d52 --- /dev/null +++ b/scenarios/test_mattermost/01_init_proxmox/stage_01-create_templates/templates/debian_trixie/template-vm-debian-trixie-small.yml @@ -0,0 +1,134 @@ +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# +# PROXMOX INIT - create debian_trixie template VM template-vm-debian-trixie-small (id 9321) +# +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- hosts: proxmox + gather_facts: false + vars_files: + - "../../../../secrets/default_vault.yml" + + tasks: + #### IDEMPOTENCE - skip if already finalized as template #### + - name: TEMPLATE 9321 - CHECK if already finalized as template + ansible.builtin.shell: qm config 9321 2>/dev/null | grep -q '^template:' + register: tpl_9321_is_template + failed_when: false + changed_when: false + delegate_to: "{{ inventory_hostname }}-cli" + + - name: TEMPLATE 9321 - WAIT for unlock if a parallel deploy holds the VM + ansible.builtin.shell: qm config 9321 | grep -q '^lock:' + register: tpl_9321_lock + until: tpl_9321_lock.rc != 0 + retries: 30 + delay: 10 + failed_when: false + changed_when: false + delegate_to: "{{ inventory_hostname }}-cli" + when: tpl_9321_is_template.rc != 0 + + - name: TEMPLATE 9321 - SKIP all (already finalized as template) + ansible.builtin.debug: + msg: "Template 9321 is already a template - skipping. Run delete-everything to recreate." + when: tpl_9321_is_template.rc == 0 + + #### vm_create idempotence: separate exists check #### + - name: TEMPLATE 9321 - CHECK if VM exists (vm_create idempotence) + ansible.builtin.shell: qm config 9321 2>/dev/null + register: tpl_9321_exists + failed_when: false + changed_when: false + delegate_to: "{{ inventory_hostname }}-cli" + when: tpl_9321_is_template.rc != 0 + + - name: PROXMOX INIT - TEMPLATES - CREATE TEMPLATE - template-vm-debian-trixie-small + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "vm_create" + vm_id: 9321 + vm_name: "template-vm-debian-trixie-small" + vm_cpu: "host" + vm_cores: 1 + vm_sockets: 1 + vm_memory: 4096 + vm_net_virtio_bridge: "vmbr140" + when: + - tpl_9321_is_template.rc != 0 + - tpl_9321_exists.rc != 0 + + - name: PROXMOX INIT - TEMPLATES - WAIT for vm_create to finish (lock-aware) + ansible.builtin.shell: qm config 9321 | grep -q '^lock:' + register: tpl_9321_post_create_lock + until: tpl_9321_post_create_lock.rc != 0 + retries: 60 + delay: 5 + failed_when: false + changed_when: false + delegate_to: "{{ inventory_hostname }}-cli" + when: + - tpl_9321_is_template.rc != 0 + - tpl_9321_exists.rc != 0 + +- hosts: proxmox-cli + gather_facts: false + vars_files: + - "../../../../secrets/default_vault.yml" + + tasks: + - name: TEMPLATE 9321 - CHECK if already template (proxmox-cli play) + ansible.builtin.shell: qm config 9321 2>/dev/null | grep -q '^template:' + register: tpl_9321_is_template + failed_when: false + changed_when: false + + - name: PROXMOX INIT - TEMPLATES - IMPORT CLOUD INIT DISK - template-vm-debian-trixie-small + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "template_cloudinit_import_disk" + proxmox_node: "pve" + cloudinit_image_full_path: "/var/lib/vz/template/iso/debian-13-genericcloud-amd64.img" + vm_id: 9321 + vm_disk_size: "32g" + proxmox_dest_vm_storage_name: "local-lvm" + when: tpl_9321_is_template.rc != 0 + +- hosts: proxmox + gather_facts: false + vars_files: + - "../../../../secrets/default_vault.yml" + + tasks: + - name: TEMPLATE 9321 - CHECK if already template (third play) + ansible.builtin.shell: qm config 9321 2>/dev/null | grep -q '^template:' + register: tpl_9321_is_template + failed_when: false + changed_when: false + delegate_to: "{{ inventory_hostname }}-cli" + + - name: PROXMOX INIT - TEMPLATES - SET CLOUD DEFAULT INIT VARIABLE - template-vm-debian-trixie-small + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "cloudinit_set_variables" + vm_id: 9321 + vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" + vm_ci_password: "{{ default_trainee_vm_ci_password | default('supersecret') }}" + vm_ci_ssh_key: "{{ default_trainee_vm_ci_ssh_key }}" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" + vm_ci_ip: "192.168.140.3" + vm_ci_netmask: "24" + vm_ci_ip_gw: "192.168.140.1" + when: tpl_9321_is_template.rc != 0 + + - name: PROXMOX INIT - TEMPLATES - SET PROXMOX TAG + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "vm_set_tag" + vm_id: 9321 + vm_tag_name: "template" + when: tpl_9321_is_template.rc != 0 diff --git a/scenarios/test_mattermost/02_lan1_infrastructure/_main.reinstall.sh b/scenarios/test_mattermost/02_lan1_infrastructure/_main.reinstall.sh new file mode 100644 index 00000000..f0ede56e --- /dev/null +++ b/scenarios/test_mattermost/02_lan1_infrastructure/_main.reinstall.sh @@ -0,0 +1,7 @@ +#!/bin/bash +## +## re-run this section's playbooks (02_lan1_infrastructure) +## +ansible-playbook -i "${RANGE42_ANSIBLE_ROLES__INVENTORY_DIR}/inventory_default.yml" \ + -l "all" \ + "./_main.yml" --vault-password-file "${RANGE42_VAULT_PASSWORD_FILE:?RANGE42_VAULT_PASSWORD_FILE is not set — run: range42-context use }" diff --git a/scenarios/test_mattermost/02_lan1_infrastructure/_main.yml b/scenarios/test_mattermost/02_lan1_infrastructure/_main.yml new file mode 100644 index 00000000..23e7683a --- /dev/null +++ b/scenarios/test_mattermost/02_lan1_infrastructure/_main.yml @@ -0,0 +1,18 @@ +--- +## +## 02_lan1_infrastructure — generated by r42playbooks +## stage_00 clones VMs (per-VM global_* below); stage_01 installs software. +## +#### STAGE 00 #### +- import_playbook: ./stage_00/lan1-admin-mattermost-00.yml + vars: + global_vm_name: "lan1-admin-mattermost-00" + global_vm_ssh_name: "r42.lan1-admin-mattermost-00" + global_vm_id: 1003 + global_vm_description: "admin-mattermost" + global_vm_tag_name: "lan1" + global_vm_ci_ip: "192.168.150.3" + global_template_vm_id: 9331 + global_template_name: "template-vm-debian-trixie-medium - id 9331" +#### STAGE 01 #### +- import_playbook: ./stage_01/lan1-admin-mattermost-00.yml diff --git a/scenarios/test_mattermost/02_lan1_infrastructure/stage_00/lan1-admin-mattermost-00.yml b/scenarios/test_mattermost/02_lan1_infrastructure/stage_00/lan1-admin-mattermost-00.yml new file mode 100644 index 00000000..97928327 --- /dev/null +++ b/scenarios/test_mattermost/02_lan1_infrastructure/stage_00/lan1-admin-mattermost-00.yml @@ -0,0 +1,74 @@ +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# +# LAN1 INFRASTRUCTURE INIT - CLONE + CLOUD-INIT +# +# hostname : lan1-admin-mattermost-00 +# vm_id : 1002 +# ip : 192.168.150.3 +# template : template-vm-debian-trixie-medium +# +# global_* vars are supplied by ../_main.yml (kept here as Jinja references). +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +- hosts: proxmox + gather_facts: false + vars_files: + - "../../secrets/default_vault.yml" + tasks: + - name: "LAN1 INFRASTRUCTURE INIT - {{ global_vm_name }} - CLONE TEMPLATE - {{ global_template_name }}" + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "vm_clone" + vm_id: "{{ global_template_vm_id }}" + vm_new_id: "{{ global_vm_id }}" + vm_name: "{{ global_vm_name }}" + vm_description: "{{ global_vm_description }}" + - name: "LAN1 INFRASTRUCTURE INIT - {{ global_vm_name }} - WAIT FOR PROXMOX UNLOCK" + ansible.builtin.shell: qm config {{ global_vm_id }} | grep -q '^lock:' + register: clone_lock_check + until: clone_lock_check.rc != 0 + retries: 60 + delay: 5 + failed_when: false + changed_when: false + delegate_to: "{{ inventory_hostname }}-cli" + - name: "LAN1 INFRASTRUCTURE INIT - {{ global_vm_name }} - SET PROXMOX TAG" + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "vm_set_tag" + vm_id: "{{ global_vm_id }}" + vm_tag_name: "{{ global_vm_tag_name }}" + - name: "LAN1 INFRASTRUCTURE INIT - {{ global_vm_name }} - SET CLOUD-INIT VARIABLES" + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "cloudinit_set_variables" + vm_id: "{{ global_vm_id }}" + vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" + vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" + vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" + vm_ci_ip: "{{ global_vm_ci_ip }}" + vm_ci_netmask: "24" + vm_ci_ip_gw: "192.168.150.1" + vm_net_virtio_bridge: "vmbr150" + - name: "LAN1 INFRASTRUCTURE INIT - {{ global_vm_name }} - START VM" + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "vm_start" + vm_id: "{{ global_vm_id }}" +- hosts: "{{ global_vm_ssh_name }}" + gather_facts: false + vars_files: + - "../../secrets/default_vault.yml" + vars: + ARG_vm_ssh_name: "{{ global_vm_ssh_name }}" + requested_tasks: + - wait/openssh_server/is_reachable.yml + - wait/cloudinit/is_boot_finished.yml + tasks: + - name: "LAN1 INFRASTRUCTURE INIT - {{ global_vm_name }} - WAIT FOR SSH + CLOUD-INIT" + include_role: + name: ansible.utils diff --git a/scenarios/test_mattermost/02_lan1_infrastructure/stage_01/lan1-admin-mattermost-00.yml b/scenarios/test_mattermost/02_lan1_infrastructure/stage_01/lan1-admin-mattermost-00.yml new file mode 100644 index 00000000..8b171fa3 --- /dev/null +++ b/scenarios/test_mattermost/02_lan1_infrastructure/stage_01/lan1-admin-mattermost-00.yml @@ -0,0 +1,40 @@ +## +## stage_01 — software install for lan1-admin-mattermost-00 +## catalog roles are referenced BY NAME (resolved via ANSIBLE_ROLES_PATH at deploy); +## one play per attachment so each carries its own params (firewall_rules, docker …). +## +- name: "lan1-admin-mattermost-00 — software.configure.firewalls" + hosts: r42.lan1-admin-mattermost-00 + become: true + vars_files: + - "../../secrets/default_vault.yml" + vars: + firewall_rules: + - ip: all + port: 22 + protocol: tcp + - ip: all + port: 8065 + protocol: tcp + roles: + - software.configure.firewalls +- name: "lan1-admin-mattermost-00 — software.install.warmup.basic_packages" + hosts: r42.lan1-admin-mattermost-00 + become: true + vars_files: + - "../../secrets/default_vault.yml" + vars: + INSTALL_PACKAGES_BASICS: 'YES' + INSTALL_PACKAGES_DOCKER: 'YES' + INSTALL_PACKAGES_DOCKER_COMPOSE: 'YES' + INSTALL_PACKAGES_UTILS_JSON: 'YES' + INSTALL_PACKAGES_UTILS_NETWORK: 'YES' + roles: + - software.install.warmup.basic_packages +- name: "lan1-admin-mattermost-00 — software.install.mattermost" + hosts: r42.lan1-admin-mattermost-00 + become: true + vars_files: + - "../../secrets/default_vault.yml" + roles: + - software.install.mattermost diff --git a/scenarios/test_mattermost/03_lan2_infrastructure/_main.reinstall.sh b/scenarios/test_mattermost/03_lan2_infrastructure/_main.reinstall.sh new file mode 100644 index 00000000..2ac11384 --- /dev/null +++ b/scenarios/test_mattermost/03_lan2_infrastructure/_main.reinstall.sh @@ -0,0 +1,7 @@ +#!/bin/bash +## +## re-run this section's playbooks (03_lan2_infrastructure) +## +ansible-playbook -i "${RANGE42_ANSIBLE_ROLES__INVENTORY_DIR}/inventory_default.yml" \ + -l "all" \ + "./_main.yml" --vault-password-file "${RANGE42_VAULT_PASSWORD_FILE:?RANGE42_VAULT_PASSWORD_FILE is not set — run: range42-context use }" diff --git a/scenarios/test_mattermost/03_lan2_infrastructure/_main.yml b/scenarios/test_mattermost/03_lan2_infrastructure/_main.yml new file mode 100644 index 00000000..73acb225 --- /dev/null +++ b/scenarios/test_mattermost/03_lan2_infrastructure/_main.yml @@ -0,0 +1,18 @@ +--- +## +## 03_lan2_infrastructure — generated by r42playbooks +## stage_00 clones VMs (per-VM global_* below); stage_01 installs software. +## +#### STAGE 00 #### +- import_playbook: ./stage_00/lan2-debian-jump-00.yml + vars: + global_vm_name: "lan2-debian-jump-00" + global_vm_ssh_name: "r42.lan2-debian-jump-00" + global_vm_id: 3003 + global_vm_description: "debian-jump" + global_vm_tag_name: "lan2" + global_vm_ci_ip: "192.168.151.3" + global_template_vm_id: 9321 + global_template_name: "template-vm-debian-trixie-small - id 9321" +#### STAGE 01 #### +- import_playbook: ./stage_01/lan2-debian-jump-00.yml diff --git a/scenarios/test_mattermost/03_lan2_infrastructure/stage_00/lan2-debian-jump-00.yml b/scenarios/test_mattermost/03_lan2_infrastructure/stage_00/lan2-debian-jump-00.yml new file mode 100644 index 00000000..dde902ab --- /dev/null +++ b/scenarios/test_mattermost/03_lan2_infrastructure/stage_00/lan2-debian-jump-00.yml @@ -0,0 +1,81 @@ +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# +# LAN2 INFRASTRUCTURE INIT - CLONE + CLOUD-INIT +# +# hostname : lan2-debian-jump-00 +# vm_id : 3002 +# ip : 192.168.151.3 +# template : template-vm-debian-trixie-small +# +# global_* vars are supplied by ../_main.yml (kept here as Jinja references). +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- hosts: proxmox + gather_facts: false + vars_files: + - "../../secrets/default_vault.yml" + + tasks: + - name: "LAN2 INFRASTRUCTURE INIT - {{ global_vm_name }} - CLONE TEMPLATE - {{ global_template_name }}" + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "vm_clone" + vm_id: "{{ global_template_vm_id }}" + vm_new_id: "{{ global_vm_id }}" + vm_name: "{{ global_vm_name }}" + vm_description: "{{ global_vm_description }}" + + - name: "LAN2 INFRASTRUCTURE INIT - {{ global_vm_name }} - WAIT FOR PROXMOX UNLOCK" + ansible.builtin.shell: qm config {{ global_vm_id }} | grep -q '^lock:' + register: clone_lock_check + until: clone_lock_check.rc != 0 + retries: 60 + delay: 5 + failed_when: false + changed_when: false + delegate_to: "{{ inventory_hostname }}-cli" + + - name: "LAN2 INFRASTRUCTURE INIT - {{ global_vm_name }} - SET PROXMOX TAG" + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "vm_set_tag" + vm_id: "{{ global_vm_id }}" + vm_tag_name: "{{ global_vm_tag_name }}" + + - name: "LAN2 INFRASTRUCTURE INIT - {{ global_vm_name }} - SET CLOUD-INIT VARIABLES" + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "cloudinit_set_variables" + vm_id: "{{ global_vm_id }}" + vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" + vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" + vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" + vm_ci_ip: "{{ global_vm_ci_ip }}" + vm_ci_netmask: "24" + vm_ci_ip_gw: "192.168.151.1" + vm_net_virtio_bridge: "vmbr151" + + - name: "LAN2 INFRASTRUCTURE INIT - {{ global_vm_name }} - START VM" + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "vm_start" + vm_id: "{{ global_vm_id }}" + +- hosts: "{{ global_vm_ssh_name }}" + gather_facts: false + vars_files: + - "../../secrets/default_vault.yml" + vars: + ARG_vm_ssh_name: "{{ global_vm_ssh_name }}" + requested_tasks: + - wait/openssh_server/is_reachable.yml + - wait/cloudinit/is_boot_finished.yml + tasks: + - name: "LAN2 INFRASTRUCTURE INIT - {{ global_vm_name }} - WAIT FOR SSH + CLOUD-INIT" + include_role: + name: ansible.utils diff --git a/scenarios/test_mattermost/03_lan2_infrastructure/stage_01/lan2-debian-jump-00.yml b/scenarios/test_mattermost/03_lan2_infrastructure/stage_01/lan2-debian-jump-00.yml new file mode 100644 index 00000000..749d0a80 --- /dev/null +++ b/scenarios/test_mattermost/03_lan2_infrastructure/stage_01/lan2-debian-jump-00.yml @@ -0,0 +1,24 @@ +## +## stage_01 — software install for lan2-debian-jump-00 +## catalog roles are referenced BY NAME (resolved via ANSIBLE_ROLES_PATH at deploy); +## one play per attachment so each carries its own params (firewall_rules, docker …). +## +- name: "lan2-debian-jump-00 — software.configure.firewalls" + hosts: r42.lan2-debian-jump-00 + become: true + vars_files: + - "../../secrets/default_vault.yml" + vars: + firewall_rules: + - ip: all + port: 22 + protocol: tcp + roles: + - software.configure.firewalls +- name: "lan2-debian-jump-00 — software.install.warmup.basic_packages" + hosts: r42.lan2-debian-jump-00 + become: true + vars_files: + - "../../secrets/default_vault.yml" + roles: + - software.install.warmup.basic_packages diff --git a/scenarios/test_mattermost/README.md b/scenarios/test_mattermost/README.md new file mode 100644 index 00000000..767ee83d --- /dev/null +++ b/scenarios/test_mattermost/README.md @@ -0,0 +1,14 @@ +# test_mattermost +Generated by **r42playbooks**. +- subnet layout: `dual-lan` +## Boxes +| box | subnet | image | vm_id | ip | +|---|---|---|---|---| +| `lan1-admin-mattermost-00` | lan1 | debian_trixie | 1002 | 192.168.150.3 | +| `lan2-debian-jump-00` | lan2 | debian_trixie | 3002 | 192.168.151.3 | +## Deploy +```bash +range42-context use test_mattermost +./test_mattermost.setup.sh +``` +The composition is reproduced in `scenario.r42.yml` — re-run `r42playbooks new --spec scenario.r42.yml` to regenerate this tree. diff --git a/scenarios/test_mattermost/_activate.sh b/scenarios/test_mattermost/_activate.sh new file mode 100644 index 00000000..94de41f3 --- /dev/null +++ b/scenarios/test_mattermost/_activate.sh @@ -0,0 +1,14 @@ +#!/bin/bash +# deprecation algo issue with paramiko and ubuntu 24.04.2 LTS +# https://github.com/paramiko/paramiko/issues/2419 +set -euo pipefail +VIRTUAL_ENV_DIR="$HOME/ansible_fix/venv" +if [ ! -d "$VIRTUAL_ENV_DIR" ]; then + mkdir -p "$HOME/ansible_fix" + python3 -m venv "$VIRTUAL_ENV_DIR" + source "$VIRTUAL_ENV_DIR/bin/activate" + pip install --upgrade pip setuptools wheel + pip install ansible paramiko cryptography +fi +source "$VIRTUAL_ENV_DIR/bin/activate" +echo "source \"$VIRTUAL_ENV_DIR/bin/activate\"" diff --git a/scenarios/test_mattermost/devkit_ansible.show_ansible_inventory.to.text.sh b/scenarios/test_mattermost/devkit_ansible.show_ansible_inventory.to.text.sh new file mode 100644 index 00000000..09a451a0 --- /dev/null +++ b/scenarios/test_mattermost/devkit_ansible.show_ansible_inventory.to.text.sh @@ -0,0 +1,2 @@ +#!/bin/bash +ansible-inventory -i "${RANGE42_ANSIBLE_ROLES__INVENTORY_DIR}/inventory_default.yml" --graph diff --git a/scenarios/test_mattermost/main.yml b/scenarios/test_mattermost/main.yml new file mode 100644 index 00000000..8f3324de --- /dev/null +++ b/scenarios/test_mattermost/main.yml @@ -0,0 +1,7 @@ +--- +## +## test_mattermost — generated by r42playbooks +## +- import_playbook: ./01_init_proxmox/_main.yml +- import_playbook: ./02_lan1_infrastructure/_main.yml +- import_playbook: ./03_lan2_infrastructure/_main.yml diff --git a/scenarios/test_mattermost/main_vms_only.yml b/scenarios/test_mattermost/main_vms_only.yml new file mode 100644 index 00000000..9ca19d0b --- /dev/null +++ b/scenarios/test_mattermost/main_vms_only.yml @@ -0,0 +1,6 @@ +--- +## +## test_mattermost — deploy VMs only (templates already exist; skip 01_init_proxmox) +## +- import_playbook: ./02_lan1_infrastructure/_main.yml +- import_playbook: ./03_lan2_infrastructure/_main.yml diff --git a/scenarios/test_mattermost/manifest/scenario_vms.json b/scenarios/test_mattermost/manifest/scenario_vms.json new file mode 100644 index 00000000..397941cb --- /dev/null +++ b/scenarios/test_mattermost/manifest/scenario_vms.json @@ -0,0 +1,13 @@ +{ + "description": "", + "scenario": "test_mattermost", + "templates": [ + {"bridge": "vmbr140", "image": "debian_trixie", "ip": "192.168.140.3", "spec": "1cpu/4gb/32gb", "vm_id": 9321, "vm_name": "template-vm-debian-trixie-small"}, + {"bridge": "vmbr140", "image": "debian_trixie", "ip": "192.168.140.2", "spec": "2cpu/8gb/64gb", "vm_id": 9331, "vm_name": "template-vm-debian-trixie-medium"} + ], + "version": 2, + "vms": [ + {"bridge": "vmbr150", "image": "debian_trixie", "ip": "192.168.150.3", "subnet": "lan1", "vm_id": 1003, "vm_name": "lan1-admin-mattermost-00"}, + {"bridge": "vmbr151", "image": "debian_trixie", "ip": "192.168.151.3", "subnet": "lan2", "vm_id": 3003, "vm_name": "lan2-debian-jump-00"} + ] +} diff --git a/scenarios/test_mattermost/scenario.r42.yml b/scenarios/test_mattermost/scenario.r42.yml new file mode 100644 index 00000000..6cf9a1b2 --- /dev/null +++ b/scenarios/test_mattermost/scenario.r42.yml @@ -0,0 +1,22 @@ +boxes: +- attachments_add: [] + count: 1 + octet: 2 + subnet: lan1 + template: admin-mattermost + template_vm_id: null + vars: {} +- attachments_add: [] + count: 1 + octet: 2 + subnet: lan2 + template: debian-jump + template_vm_id: null + vars: {} +name: test_mattermost +network_policy: null +notes: '' +proxmox_node: null +schema_version: 1 +services: null +subnet_layout: dual-lan diff --git a/scenarios/test_mattermost/templates/ansible-inventory.j2 b/scenarios/test_mattermost/templates/ansible-inventory.j2 new file mode 100644 index 00000000..52644aeb --- /dev/null +++ b/scenarios/test_mattermost/templates/ansible-inventory.j2 @@ -0,0 +1,19 @@ +all: + children: + range42_infrastructure: + children: + r42_lan1_group: + hosts: + r42.lan1-admin-mattermost-00: + r42_lan2_group: + hosts: + r42.lan2-debian-jump-00: + proxmox: + hosts: + {{ INFRASTRUCTURE_CODENAME }}: + ansible_host: {{ INFRASTRUCTURE_PROXMOX_ADDRESS | mandatory }}:8006 + ansible_connection: local + ansible_python_interpreter: /usr/bin/python3 + proxmox-cli: + hosts: + {{ INFRASTRUCTURE_CODENAME }}-cli: diff --git a/scenarios/test_mattermost/templates/ansible-vars.yml b/scenarios/test_mattermost/templates/ansible-vars.yml new file mode 100644 index 00000000..4a592008 --- /dev/null +++ b/scenarios/test_mattermost/templates/ansible-vars.yml @@ -0,0 +1,20 @@ +--- +################################################################################ +# range42 — scenario-specific variables (test_mattermost) +# +# Generated by r42playbooks. Shared variables -> group_vars/all/vars.yml, +# secrets -> vault (see vault-example.yml). +################################################################################ +#### SCENARIO IDENTIFICATION #### +INFRASTRUCTURE_SCENARIO: "test_mattermost" +#### CREDENTIAL GENERATION #### +context_auto_generate_ssh_keys: "YES" +context_auto_generate_vm_passwords: "YES" +student_additionnal_keys_count: 5 +#### CLOUD-INIT USERNAMES #### +default_admin_vm_ci_user: "alice" +default_trainee_vm_ci_user: "bob" +#### CLOUD-INIT DNS #### +# default resolver for lab VMs; range42-init.py substitutes the Proxmox node +# resolver here at workspace creation. 1.1.1.1 is the portable fallback. +default_vm_ci_dns_ips: "1.1.1.1" diff --git a/scenarios/test_mattermost/templates/ssh-config.j2 b/scenarios/test_mattermost/templates/ssh-config.j2 new file mode 100644 index 00000000..69813907 --- /dev/null +++ b/scenarios/test_mattermost/templates/ssh-config.j2 @@ -0,0 +1,38 @@ +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# infrastructure code name : {{ INFRASTRUCTURE_CODENAME }} +# infrastructure proxmox address : {{ INFRASTRUCTURE_PROXMOX_ADDRESS }} +# scenario : {{ INFRASTRUCTURE_SCENARIO }} +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +#### PROXMOX WEB UI (port forward) #### +Host px.{{ INFRASTRUCTURE_CODENAME }}.redirect_www.proxmox + RequestTTY no + RemoteCommand none + localforward localhost:18042 {{ INFRASTRUCTURE_PROXMOX_ADDRESS | mandatory }}:8006 + +#### PX ROOT USER SSH ACCESS #### +Host px.{{ INFRASTRUCTURE_CODENAME }}-ssh_cli.root {{ INFRASTRUCTURE_CODENAME }}-cli + Hostname {{ INFRASTRUCTURE_PROXMOX_ADDRESS | mandatory }} + User root + IdentityFile {{ DEPLOYER_CLI__DST_SSH_KEYS_JUMP_DEST_DIR }}/px.{{ INFRASTRUCTURE_CODENAME }}-{{ INFRASTRUCTURE_SCENARIO }}-ssh_cli.root + Port 22 + +#### SSH JUMPER #### +Host px.{{ INFRASTRUCTURE_CODENAME }}.jumper + Hostname {{ INFRASTRUCTURE_PROXMOX_ADDRESS | mandatory }} + User jump_user + IdentityFile {{ DEPLOYER_CLI__DST_SSH_KEYS_JUMP_DEST_DIR }}/px.{{ INFRASTRUCTURE_CODENAME }}-{{ INFRASTRUCTURE_SCENARIO }}-ssh_cli.jump_user + Port 22 + +#### SCENARIO VMs #### +Host r42.lan1-admin-mattermost-00 + Hostname 192.168.150.3 + +Host r42.lan2-debian-jump-00 + Hostname 192.168.151.3 + +Host r42.* + User alice + IdentityFile {{ DEPLOYER_CLI__DST_SSH_KEYS_BACKEND_DEST_DIR }}/r42.{{ INFRASTRUCTURE_CODENAME }}-{{ INFRASTRUCTURE_SCENARIO }}-deployer-key_alice + Port 22 + ProxyJump px.{{ INFRASTRUCTURE_CODENAME }}.jumper diff --git a/scenarios/test_mattermost/templates/vault-example.yml b/scenarios/test_mattermost/templates/vault-example.yml new file mode 100644 index 00000000..87397acb --- /dev/null +++ b/scenarios/test_mattermost/templates/vault-example.yml @@ -0,0 +1,37 @@ +--- +################################################################################ +# range42 — vault secrets (test_mattermost) +# +# Copy to vault.yml, fill real values, then: ansible-vault encrypt vault.yml +# Never commit the unencrypted version. +################################################################################ + + +#### PROXMOX API SECRET #### +proxmox_api_token_secret: "REPLACE_ME" + + +#### JUMP HOST #### +jump_password: "REPLACE_ME" + + +#### CLOUD-INIT PASSWORDS #### +default_admin_vm_ci_password: "REPLACE_ME" +default_admin_vm_ci_ssh_key: "ssh-ed25519 AAAA... alice CODENAME-SCENARIO" +default_trainee_vm_ci_password: "REPLACE_ME" +default_trainee_vm_ci_ssh_key: "ssh-ed25519 AAAA... bob CODENAME-SCENARIO" + + +#### CLOUD-INIT DNS #### +# default resolver for lab VMs; range42-init.py substitutes the Proxmox node +# resolver here at workspace creation. 1.1.1.1 is the portable fallback. +default_vm_ci_dns_ips: "1.1.1.1" + + +#### TAILSCALE #### +infrastructure_tailscale_authkey: "tskey-auth-REPLACE_ME" +infrastructure_tailscale_apikey: "tskey-api-REPLACE_ME" + + +#### MISC #### +deployer_cli_user_ssh_known_hosts: "/home/your_deployer_cli_username/.ssh/known_hosts" diff --git a/scenarios/test_mattermost/test_mattermost.delete_all.sh b/scenarios/test_mattermost/test_mattermost.delete_all.sh new file mode 100644 index 00000000..2b27a54c --- /dev/null +++ b/scenarios/test_mattermost/test_mattermost.delete_all.sh @@ -0,0 +1,32 @@ +#!/bin/bash + +## +## delete all — VMs + this scenario's ubuntu_noble templates +## +## ⚠ templates (9xxx) are shared across scenarios on the same Proxmox. Run this +## only when no other scenario relies on them, or use delete_vms_only.sh instead. +## +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +MANIFEST="$SCRIPT_DIR/manifest/scenario_vms.json" +if [[ ! -f "$MANIFEST" ]]; then + echo "ERROR: manifest not found: $MANIFEST" >&2 + exit 1 +fi +mapfile -t SCENARIO_VM_IDS < <(jq -r '.vms[].vm_id' "$MANIFEST") +mapfile -t TEMPLATE_VM_IDS < <(jq -r '.templates[].vm_id' "$MANIFEST") +mapfile -t INFRASTRUCTURE_IP < <(jq -r '.vms[].ip' "$MANIFEST") +ALL_IDS=("${SCENARIO_VM_IDS[@]}" "${TEMPLATE_VM_IDS[@]}") +ID_REGEX=$(printf '|%s' "${ALL_IDS[@]}" | sed 's/^|//') +echo ":: stopping and deleting VMs + templates" +VM_LIST_JSON=$(proxmox_vm.list.to.jsons.sh 2>&1 | grep '"vm_id":[0-9]') +if [ -z "$VM_LIST_JSON" ]; then + echo "ERROR: proxmox_vm.list.to.jsons.sh returned no VM data — aborting" >&2 + exit 1 +fi +echo "$VM_LIST_JSON" | jq -c | grep -E "\"vm_id\":($ID_REGEX)([^0-9]|$)" | proxmox_vm.vm_id.stop_force.to.jsons.sh +echo "$VM_LIST_JSON" | jq -c | grep -E "\"vm_id\":($ID_REGEX)([^0-9]|$)" | proxmox_vm.vm_id.delete.to.jsons.sh +for ip in "${INFRASTRUCTURE_IP[@]}"; do + echo ":: REMOVE SSH KEY FOR : $ip" + ssh-keygen -f "$HOME/.ssh/known_hosts" -R "$ip" +done +echo ":: done — VMs and templates removed" diff --git a/scenarios/test_mattermost/test_mattermost.delete_vms_only.sh b/scenarios/test_mattermost/test_mattermost.delete_vms_only.sh new file mode 100644 index 00000000..ba2443a3 --- /dev/null +++ b/scenarios/test_mattermost/test_mattermost.delete_vms_only.sh @@ -0,0 +1,33 @@ +#!/bin/bash + +## +## delete VMs only — scenario VMs (filter by vm_id), keep templates +## + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +MANIFEST="$SCRIPT_DIR/manifest/scenario_vms.json" + +if [[ ! -f "$MANIFEST" ]]; then + echo "ERROR: manifest not found: $MANIFEST" >&2 + exit 1 +fi + +mapfile -t SCENARIO_VM_IDS < <(jq -r '.vms[].vm_id' "$MANIFEST") +mapfile -t INFRASTRUCTURE_IP < <(jq -r '.vms[].ip' "$MANIFEST") +ID_REGEX=$(printf '|%s' "${SCENARIO_VM_IDS[@]}" | sed 's/^|//') + +echo ":: stopping and deleting scenario VMs (keeping templates)..." +VM_LIST_JSON=$(proxmox_vm.list.to.jsons.sh 2>&1 | grep '"vm_id":[0-9]') +if [ -z "$VM_LIST_JSON" ]; then + echo "ERROR: proxmox_vm.list.to.jsons.sh returned no VM data — aborting" >&2 + exit 1 +fi +echo "$VM_LIST_JSON" | jq -c | grep -E "\"vm_id\":($ID_REGEX)([^0-9]|$)" | proxmox_vm.vm_id.stop_force.to.jsons.sh +echo "$VM_LIST_JSON" | jq -c | grep -E "\"vm_id\":($ID_REGEX)([^0-9]|$)" | proxmox_vm.vm_id.delete.to.jsons.sh + +for ip in "${INFRASTRUCTURE_IP[@]}"; do + echo ":: REMOVE SSH KEY FOR : $ip" + ssh-keygen -f "$HOME/.ssh/known_hosts" -R "$ip" +done + +echo ":: done — templates preserved" diff --git a/scenarios/test_mattermost/test_mattermost.reset.setup.sh b/scenarios/test_mattermost/test_mattermost.reset.setup.sh new file mode 100644 index 00000000..b70309e0 --- /dev/null +++ b/scenarios/test_mattermost/test_mattermost.reset.setup.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +## +## reset — delete this scenario's VMs (from manifest) then re-deploy +## + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +MANIFEST="$SCRIPT_DIR/manifest/scenario_vms.json" + +if [[ ! -f "$MANIFEST" ]]; then + echo "ERROR: manifest not found: $MANIFEST" >&2 + exit 1 +fi + +mapfile -t SCENARIO_VM_IDS < <(jq -r '.vms[].vm_id' "$MANIFEST") +mapfile -t INFRASTRUCTURE_IP < <(jq -r '.vms[].ip' "$MANIFEST") +ID_REGEX=$(printf '|%s' "${SCENARIO_VM_IDS[@]}" | sed 's/^|//') + +echo ":: stopping and deleting scenario VMs (vm_ids: ${SCENARIO_VM_IDS[*]})..." +VM_LIST_JSON=$(proxmox_vm.list.to.jsons.sh 2>&1 | grep '"vm_id":[0-9]') +if [ -z "$VM_LIST_JSON" ]; then + echo "ERROR: proxmox_vm.list.to.jsons.sh returned no VM data — aborting" >&2 + exit 1 +fi +echo "$VM_LIST_JSON" | jq -c | grep -E "\"vm_id\":($ID_REGEX)([^0-9]|$)" | proxmox_vm.vm_id.stop_force.to.jsons.sh +echo "$VM_LIST_JSON" | jq -c | grep -E "\"vm_id\":($ID_REGEX)([^0-9]|$)" | proxmox_vm.vm_id.delete.to.jsons.sh + +for ip in "${INFRASTRUCTURE_IP[@]}"; do + echo ":: REMOVE SSH KEY FOR : $ip" + ssh-keygen -f "$HOME/.ssh/known_hosts" -R "$ip" +done + +ansible-playbook -i "${RANGE42_ANSIBLE_ROLES__INVENTORY_DIR}/inventory_default.yml" \ + -l "all" \ + "./main.yml" --vault-password-file "${RANGE42_VAULT_PASSWORD_FILE:?RANGE42_VAULT_PASSWORD_FILE is not set — run: range42-context use }" diff --git a/scenarios/test_mattermost/test_mattermost.reset.ssh_keys.sh b/scenarios/test_mattermost/test_mattermost.reset.ssh_keys.sh new file mode 100644 index 00000000..bea1d07b --- /dev/null +++ b/scenarios/test_mattermost/test_mattermost.reset.ssh_keys.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +## +## remove known_hosts entries for every VM IP in this scenario (from manifest) +## + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +MANIFEST="$SCRIPT_DIR/manifest/scenario_vms.json" + +if [[ ! -f "$MANIFEST" ]]; then + echo "ERROR: manifest not found: $MANIFEST" >&2 + exit 1 +fi + +mapfile -t INFRASTRUCTURE_IP < <(jq -r '.vms[].ip' "$MANIFEST") + +for ip in "${INFRASTRUCTURE_IP[@]}"; do + echo ":: REMOVE SSH KEY FOR : $ip" + ssh-keygen -f "$HOME/.ssh/known_hosts" -R "$ip" +done diff --git a/scenarios/test_mattermost/test_mattermost.setup.sh b/scenarios/test_mattermost/test_mattermost.setup.sh new file mode 100644 index 00000000..739bddcf --- /dev/null +++ b/scenarios/test_mattermost/test_mattermost.setup.sh @@ -0,0 +1,4 @@ +#!/bin/bash +ansible-playbook -i "${RANGE42_ANSIBLE_ROLES__INVENTORY_DIR}/inventory_default.yml" \ + -l "all" \ + "./main.yml" --vault-password-file "${RANGE42_VAULT_PASSWORD_FILE:?RANGE42_VAULT_PASSWORD_FILE is not set — run: range42-context use }" diff --git a/scenarios/test_mattermost/test_mattermost.setup_vms_only.sh b/scenarios/test_mattermost/test_mattermost.setup_vms_only.sh new file mode 100644 index 00000000..016acd50 --- /dev/null +++ b/scenarios/test_mattermost/test_mattermost.setup_vms_only.sh @@ -0,0 +1,8 @@ +#!/bin/bash +## +## deploy VMs only — skip template download and creation +## faster redeploy when templates already exist on proxmox +## +ansible-playbook -i "${RANGE42_ANSIBLE_ROLES__INVENTORY_DIR}/inventory_default.yml" \ + -l "all" \ + "./main_vms_only.yml" --vault-password-file "${RANGE42_VAULT_PASSWORD_FILE:?RANGE42_VAULT_PASSWORD_FILE is not set — run: range42-context use }" diff --git a/scenarios/test_nextcloud/01_init_proxmox/_main.reinstall.sh b/scenarios/test_nextcloud/01_init_proxmox/_main.reinstall.sh new file mode 100644 index 00000000..390e3cda --- /dev/null +++ b/scenarios/test_nextcloud/01_init_proxmox/_main.reinstall.sh @@ -0,0 +1,7 @@ +#!/bin/bash +## +## re-run 01_init_proxmox (download images + create templates) +## +ansible-playbook -i "${RANGE42_ANSIBLE_ROLES__INVENTORY_DIR}/inventory_default.yml" \ + -l "proxmox" \ + "./_main.yml" --vault-password-file "${RANGE42_VAULT_PASSWORD_FILE:?RANGE42_VAULT_PASSWORD_FILE is not set — run: range42-context use }" diff --git a/scenarios/test_nextcloud/01_init_proxmox/_main.yml b/scenarios/test_nextcloud/01_init_proxmox/_main.yml new file mode 100644 index 00000000..40a81665 --- /dev/null +++ b/scenarios/test_nextcloud/01_init_proxmox/_main.yml @@ -0,0 +1,2 @@ +- import_playbook: ./stage_00-download_cloudinit_files/_main.yml +- import_playbook: ./stage_01-create_templates/_main.yml diff --git a/scenarios/test_nextcloud/01_init_proxmox/stage_00-download_cloudinit_files/_main.yml b/scenarios/test_nextcloud/01_init_proxmox/stage_00-download_cloudinit_files/_main.yml new file mode 100644 index 00000000..d9e39ea2 --- /dev/null +++ b/scenarios/test_nextcloud/01_init_proxmox/stage_00-download_cloudinit_files/_main.yml @@ -0,0 +1,4 @@ +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# PROMOX INIT - download cloud-init base images (only the OS families used) +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +- import_playbook: ./cloudinit_debian_trixie.yml diff --git a/scenarios/test_nextcloud/01_init_proxmox/stage_00-download_cloudinit_files/cloudinit_debian_trixie.yml b/scenarios/test_nextcloud/01_init_proxmox/stage_00-download_cloudinit_files/cloudinit_debian_trixie.yml new file mode 100644 index 00000000..633cfec3 --- /dev/null +++ b/scenarios/test_nextcloud/01_init_proxmox/stage_00-download_cloudinit_files/cloudinit_debian_trixie.yml @@ -0,0 +1,39 @@ +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# +# PROXMOX INIT - download cloud init image for debian_trixie +# +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- hosts: proxmox + gather_facts: false + vars_files: + - "../../secrets/default_vault.yml" + + tasks: + - name: PROMOX INIT - DOWNLOAD - CLOUD INIT images + include_role: + name: range42-ansible_roles-proxmox_controller + + vars: + proxmox_vm_action: "storage_download_iso" + proxmox_storage: "local" + iso_file_content_type: "iso" + iso_url: "https://cloud.debian.org/images/cloud/trixie/latest/debian-13-genericcloud-amd64.raw" + iso_file_name: "debian-13-genericcloud-amd64.img" + +# The download-url API is async (returns a UPID immediately). Poll on +# proxmox-cli until the file is fully on disk before stage_01 imports it. +- hosts: proxmox-cli + gather_facts: false + vars_files: + - "../../secrets/default_vault.yml" + + tasks: + - name: PROMOX INIT - WAIT for debian-13-genericcloud-amd64.img to be fully downloaded + ansible.builtin.stat: + path: "/var/lib/vz/template/iso/debian-13-genericcloud-amd64.img" + register: _cloudinit_img + until: _cloudinit_img.stat.exists and _cloudinit_img.stat.size > 100000000 + retries: 120 # 120 x 10 s = 20 min max + delay: 10 + changed_when: false diff --git a/scenarios/test_nextcloud/01_init_proxmox/stage_01-create_templates/_main.yml b/scenarios/test_nextcloud/01_init_proxmox/stage_01-create_templates/_main.yml new file mode 100644 index 00000000..e669287d --- /dev/null +++ b/scenarios/test_nextcloud/01_init_proxmox/stage_01-create_templates/_main.yml @@ -0,0 +1,5 @@ +--- +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# PROMOX INIT - create the Proxmox template images (only the OS families used) +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +- import_playbook: ./templates/debian_trixie/_main_debian_trixie.yml diff --git a/scenarios/test_nextcloud/01_init_proxmox/stage_01-create_templates/templates/debian_trixie/_apply_apt_proxy.yml b/scenarios/test_nextcloud/01_init_proxmox/stage_01-create_templates/templates/debian_trixie/_apply_apt_proxy.yml new file mode 100644 index 00000000..04e2f296 --- /dev/null +++ b/scenarios/test_nextcloud/01_init_proxmox/stage_01-create_templates/templates/debian_trixie/_apply_apt_proxy.yml @@ -0,0 +1,52 @@ +## +## debian_trixie - apt proxy via cloud-init cicustom (optional) +## +## Attaches /var/lib/vz/snippets/range42-apt-proxy.yaml as cloud-init vendor-data +## to every template VM. All VMs cloned from these templates inherit the apt +## proxy automatically via cloud-init at first boot. +## Skipped (no-op) if apt_proxy_url is empty/unset. Idempotent. +## + +- hosts: proxmox-cli + gather_facts: false + vars_files: + - "../../../../secrets/default_vault.yml" + + vars: + bs2_template_vm_ids: + - 9321 # template-vm-debian-trixie-small + - 9331 # template-vm-debian-trixie-medium + + tasks: + - name: APT-PROXY - SKIP (apt_proxy_url is empty) + ansible.builtin.debug: + msg: "apt_proxy_url is empty - cloud-init cicustom not applied to templates" + when: apt_proxy_url is not defined or (apt_proxy_url | length) == 0 + + - name: APT-PROXY - ATTACH cicustom vendor TO TEMPLATES + ansible.builtin.command: + cmd: qm set {{ item }} --cicustom "vendor=local:snippets/range42-apt-proxy.yaml" + loop: "{{ bs2_template_vm_ids }}" + register: qm_result + changed_when: qm_result.rc == 0 + failed_when: + - qm_result.rc != 0 + - "'does not exist' not in qm_result.stderr" + when: apt_proxy_url is defined and (apt_proxy_url | length) > 0 + + - name: APT-PROXY - DETACH cicustom vendor FROM TEMPLATES (apt_proxy_url unset) + ansible.builtin.command: + cmd: qm set {{ item }} --delete cicustom + loop: "{{ bs2_template_vm_ids }}" + register: qm_unset + changed_when: qm_unset.rc == 0 + failed_when: + - qm_unset.rc != 0 + - "'does not exist' not in qm_unset.stderr" + when: apt_proxy_url is not defined or (apt_proxy_url | length) == 0 + + - name: APT-PROXY - DISPLAY STATUS + ansible.builtin.debug: + msg: | + apt proxy: {{ apt_proxy_url if (apt_proxy_url is defined and (apt_proxy_url | length) > 0) else 'DISABLED' }} + cicustom : {{ 'attached to ' + (bs2_template_vm_ids | length | string) + ' templates' if (apt_proxy_url is defined and (apt_proxy_url | length) > 0) else 'detached from templates' }} diff --git a/scenarios/test_nextcloud/01_init_proxmox/stage_01-create_templates/templates/debian_trixie/_main_debian_trixie.yml b/scenarios/test_nextcloud/01_init_proxmox/stage_01-create_templates/templates/debian_trixie/_main_debian_trixie.yml new file mode 100644 index 00000000..ca3944c3 --- /dev/null +++ b/scenarios/test_nextcloud/01_init_proxmox/stage_01-create_templates/templates/debian_trixie/_main_debian_trixie.yml @@ -0,0 +1,13 @@ +## +## debian_trixie — create + configure all Proxmox template VMs for this image +## + +- import_playbook: ./template-vm-debian-trixie-small.yml +- import_playbook: ./template-vm-debian-trixie-medium.yml + +# apt proxy cicustom (no-op if apt_proxy_url empty) +- import_playbook: ./_apply_apt_proxy.yml + +# pre-update: start each VM, run apt update + dist-upgrade via cloud-init poweroff, +# then convert to template. +- import_playbook: ./_update_templates.yml diff --git a/scenarios/test_nextcloud/01_init_proxmox/stage_01-create_templates/templates/debian_trixie/_update_templates.yml b/scenarios/test_nextcloud/01_init_proxmox/stage_01-create_templates/templates/debian_trixie/_update_templates.yml new file mode 100644 index 00000000..2455c653 --- /dev/null +++ b/scenarios/test_nextcloud/01_init_proxmox/stage_01-create_templates/templates/debian_trixie/_update_templates.yml @@ -0,0 +1,202 @@ +## +## debian_trixie - pre-update templates +## +## After all templates are created (but BEFORE convert to template), this playbook: +## 1. renders a temporary bootstrap snippet directly on the Proxmox host +## (apt update + apt dist-upgrade + cloud-init clean + auto-poweroff) +## 2. attaches it as cicustom vendor on each template VM +## 3. starts all templates +## 4. polls qm status until each VM is stopped (cloud-init auto-poweroff signals end) +## 5. detaches the bootstrap snippet, re-attaches the persistent apt-proxy snippet +## 6. converts each VM to a template (qm template ) +## +## Reads VM IDs from the scenario manifest (manifest/scenario_vms.json). +## Runs entirely on `proxmox-cli` (= the Proxmox host itself), no SSH involved. +## + +- hosts: proxmox-cli + gather_facts: false + vars_files: + - "../../../../secrets/default_vault.yml" + + vars: + manifest_path: "{{ playbook_dir }}/../../../../manifest/scenario_vms.json" + bootstrap_snippet_template: "{{ playbook_dir }}/range42-template-bootstrap.yaml.j2" + bootstrap_snippet_path: "/var/lib/vz/snippets/range42-template-bootstrap.yaml" + + tasks: + + #### #### #### debug apt_proxy_url visibility - fully safe vs undefined #### #### #### + + - name: TEMPLATES UPDATE - DEBUG apt_proxy_url visibility + ansible.builtin.debug: + msg: | + apt_proxy_url defined : {{ apt_proxy_url is defined }} + apt_proxy_url value : {{ apt_proxy_url | default('(undefined)') }} + apt_proxy_url length : {{ (apt_proxy_url | default('')) | length }} + -> snippet WILL include apt section : {{ (apt_proxy_url | default('')) | length > 0 }} + + #### #### #### load manifest #### #### #### + + - name: TEMPLATES UPDATE - LOAD MANIFEST + ansible.builtin.set_fact: + templates: "{{ (lookup('file', manifest_path) | from_json).templates }}" + + #### #### #### IDEMPOTENCE - probe each template's current state #### #### #### + # 3 possible states per VM ID : + # - "template" : already converted, skip update entirely + # - "vm" : exists as a regular VM, needs update + convert + # - "missing" : VM doesn't exist on Proxmox, skip (templates not created yet) + # Only "vm" state proceeds through the start/update/shutdown/convert pipeline. + + - name: TEMPLATES UPDATE - PROBE state of each template VM + ansible.builtin.shell: | + if qm config {{ item.vm_id }} 2>/dev/null | grep -q '^template:'; then + echo "template" + elif qm config {{ item.vm_id }} >/dev/null 2>&1; then + echo "vm" + else + echo "missing" + fi + register: tpl_state + loop: "{{ templates }}" + loop_control: + label: "vm_id={{ item.vm_id }}" + changed_when: false + + - name: TEMPLATES UPDATE - BUILD list of templates needing update (state == 'vm') + ansible.builtin.set_fact: + templates_to_update: >- + {{ templates | zip(tpl_state.results) + | selectattr('1.stdout', 'eq', 'vm') + | map(attribute='0') | list }} + + - name: TEMPLATES UPDATE - DISPLAY PLAN + ansible.builtin.debug: + msg: | + total templates in manifest : {{ templates | length }} + already converted (skip) : {{ tpl_state.results | selectattr('stdout', 'eq', 'template') | list | length }} + missing (skip) : {{ tpl_state.results | selectattr('stdout', 'eq', 'missing') | list | length }} + to update + convert : {{ templates_to_update | length }} + {% if templates_to_update | length > 0 %} + targets : + {% for t in templates_to_update %} - {{ t.vm_id }} {{ t.vm_name }} ({{ t.ip }}) + {% endfor %} + {% endif %} + + - name: TEMPLATES UPDATE - SHORT-CIRCUIT if nothing to update + ansible.builtin.meta: end_play + when: templates_to_update | length == 0 + + #### #### #### render bootstrap snippet directly on the Proxmox host #### #### #### + + - name: TEMPLATES UPDATE - RENDER BOOTSTRAP SNIPPET ON PROXMOX + ansible.builtin.template: + src: "{{ bootstrap_snippet_template }}" + dest: "{{ bootstrap_snippet_path }}" + mode: "0644" + + #### #### #### attach bootstrap snippet on every template #### #### #### + + - name: TEMPLATES UPDATE - ATTACH bootstrap cicustom + ansible.builtin.command: + cmd: qm set {{ item.vm_id }} --cicustom "vendor=local:snippets/range42-template-bootstrap.yaml" + loop: "{{ templates_to_update }}" + loop_control: + label: "vm_id={{ item.vm_id }}" + + #### #### #### start all templates #### #### #### + + - name: TEMPLATES UPDATE - START all templates + ansible.builtin.command: + cmd: qm start {{ item.vm_id }} + register: start_result + failed_when: + - start_result.rc != 0 + - "'already running' not in start_result.stderr" + - "'MAX' not in start_result.stderr" + changed_when: start_result.rc == 0 + loop: "{{ templates_to_update }}" + loop_control: + label: "vm_id={{ item.vm_id }}" + + # VMs that failed with "MAX X vcpus" couldn't start - exclude them from + # the wait/convert pipeline but keep the play running for the others. + - name: TEMPLATES UPDATE - BUILD started list (exclude CPU-limited VMs) + ansible.builtin.set_fact: + templates_started: >- + {{ templates_to_update | zip(start_result.results) + | rejectattr('1.stderr', 'search', 'MAX') + | map(attribute='0') | list }} + templates_skipped_cpu: >- + {{ templates_to_update | zip(start_result.results) + | selectattr('1.stderr', 'search', 'MAX') + | map(attribute='0') | list }} + + - name: TEMPLATES UPDATE - WARN skipped templates (host CPU limit) + ansible.builtin.debug: + msg: | + WARNING: {{ templates_skipped_cpu | length }} template(s) skipped - host CPU limit: + {% for t in templates_skipped_cpu %} + - {{ t.vm_id }} {{ t.vm_name }} (spec: {{ t.spec }}) + {% endfor %} + These templates require more vCPUs than this host provides. + They will NOT be converted. Run on a host with enough physical + CPUs, or lower vm_cores in the template playbook if intentional. + when: templates_skipped_cpu | length > 0 + + #### #### #### wait until each VM auto-poweroffs (cloud-init done) #### #### #### + # the loop is sequential per-vm, but since all started in parallel above, + # total time is bounded by the slowest template (~3-15 min depending on cache) + + - name: TEMPLATES UPDATE - WAIT for cloud-init to auto-poweroff + ansible.builtin.shell: | + qm status {{ item.vm_id }} | grep -q 'status: stopped' + register: stop_check + retries: 360 # safety cap : 360 x 5s = 30 min max per VM + delay: 5 + until: stop_check.rc == 0 + loop: "{{ templates_started }}" + loop_control: + label: "vm_id={{ item.vm_id }}" + changed_when: false + + #### #### #### detach bootstrap, re-attach apt-proxy, convert to template #### #### #### + + - name: TEMPLATES UPDATE - DETACH bootstrap cicustom + ansible.builtin.command: + cmd: qm set {{ item.vm_id }} --delete cicustom + loop: "{{ templates_started }}" + loop_control: + label: "vm_id={{ item.vm_id }}" + + - name: TEMPLATES UPDATE - RE-ATTACH apt-proxy cicustom (persistent for clones) + ansible.builtin.command: + cmd: qm set {{ item.vm_id }} --cicustom "vendor=local:snippets/range42-apt-proxy.yaml" + loop: "{{ templates_started }}" + loop_control: + label: "vm_id={{ item.vm_id }}" + when: (apt_proxy_url | default('')) | length > 0 + + - name: TEMPLATES UPDATE - CONVERT VM to template + ansible.builtin.command: + cmd: qm template {{ item.vm_id }} + loop: "{{ templates_started }}" + loop_control: + label: "vm_id={{ item.vm_id }}" + + #### #### #### cleanup #### #### #### + + - name: TEMPLATES UPDATE - REMOVE BOOTSTRAP SNIPPET FROM PROXMOX + ansible.builtin.file: + path: "{{ bootstrap_snippet_path }}" + state: absent + + - name: TEMPLATES UPDATE - DISPLAY DONE + ansible.builtin.debug: + msg: | + {{ templates_started | length }} templates updated and converted + apt-proxy cicustom re-attached (if apt_proxy_url set) + {% if templates_skipped_cpu | length > 0 %} + {{ templates_skipped_cpu | length }} skipped (host CPU limit) - see warning above + {% endif %} diff --git a/scenarios/test_nextcloud/01_init_proxmox/stage_01-create_templates/templates/debian_trixie/range42-template-bootstrap.yaml.j2 b/scenarios/test_nextcloud/01_init_proxmox/stage_01-create_templates/templates/debian_trixie/range42-template-bootstrap.yaml.j2 new file mode 100644 index 00000000..46990999 --- /dev/null +++ b/scenarios/test_nextcloud/01_init_proxmox/stage_01-create_templates/templates/debian_trixie/range42-template-bootstrap.yaml.j2 @@ -0,0 +1,22 @@ +#cloud-config +# range42 - TEMPORARY bootstrap snippet for templates +# Triggers apt update + dist-upgrade then poweroff. +# Auto-generated by _update_templates.yml. Detached after the template is +# converted, replaced by range42-apt-proxy.yaml (persistent). +# +# NOTE: cloud-init clean was previously here in `runcmd` - REMOVED because +# it ran in cloud_config stage, before cloud_final, and broke +# package-update-upgrade-install. Clones get a new vmid -> new instance-id, +# so cloud-init re-processes naturally on each clone. No clean needed. +# +{% if (apt_proxy_url | default('')) | length > 0 %} +apt: + http_proxy: "{{ apt_proxy_url }}" +{% endif %} +package_update: true +package_upgrade: true +package_reboot_if_required: false +power_state: + mode: poweroff + timeout: 60 + condition: True diff --git a/scenarios/test_nextcloud/01_init_proxmox/stage_01-create_templates/templates/debian_trixie/template-vm-debian-trixie-medium.yml b/scenarios/test_nextcloud/01_init_proxmox/stage_01-create_templates/templates/debian_trixie/template-vm-debian-trixie-medium.yml new file mode 100644 index 00000000..7667317f --- /dev/null +++ b/scenarios/test_nextcloud/01_init_proxmox/stage_01-create_templates/templates/debian_trixie/template-vm-debian-trixie-medium.yml @@ -0,0 +1,134 @@ +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# +# PROXMOX INIT - create debian_trixie template VM template-vm-debian-trixie-medium (id 9331) +# +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- hosts: proxmox + gather_facts: false + vars_files: + - "../../../../secrets/default_vault.yml" + + tasks: + #### IDEMPOTENCE - skip if already finalized as template #### + - name: TEMPLATE 9331 - CHECK if already finalized as template + ansible.builtin.shell: qm config 9331 2>/dev/null | grep -q '^template:' + register: tpl_9331_is_template + failed_when: false + changed_when: false + delegate_to: "{{ inventory_hostname }}-cli" + + - name: TEMPLATE 9331 - WAIT for unlock if a parallel deploy holds the VM + ansible.builtin.shell: qm config 9331 | grep -q '^lock:' + register: tpl_9331_lock + until: tpl_9331_lock.rc != 0 + retries: 30 + delay: 10 + failed_when: false + changed_when: false + delegate_to: "{{ inventory_hostname }}-cli" + when: tpl_9331_is_template.rc != 0 + + - name: TEMPLATE 9331 - SKIP all (already finalized as template) + ansible.builtin.debug: + msg: "Template 9331 is already a template - skipping. Run delete-everything to recreate." + when: tpl_9331_is_template.rc == 0 + + #### vm_create idempotence: separate exists check #### + - name: TEMPLATE 9331 - CHECK if VM exists (vm_create idempotence) + ansible.builtin.shell: qm config 9331 2>/dev/null + register: tpl_9331_exists + failed_when: false + changed_when: false + delegate_to: "{{ inventory_hostname }}-cli" + when: tpl_9331_is_template.rc != 0 + + - name: PROXMOX INIT - TEMPLATES - CREATE TEMPLATE - template-vm-debian-trixie-medium + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "vm_create" + vm_id: 9331 + vm_name: "template-vm-debian-trixie-medium" + vm_cpu: "host" + vm_cores: 2 + vm_sockets: 1 + vm_memory: 8192 + vm_net_virtio_bridge: "vmbr140" + when: + - tpl_9331_is_template.rc != 0 + - tpl_9331_exists.rc != 0 + + - name: PROXMOX INIT - TEMPLATES - WAIT for vm_create to finish (lock-aware) + ansible.builtin.shell: qm config 9331 | grep -q '^lock:' + register: tpl_9331_post_create_lock + until: tpl_9331_post_create_lock.rc != 0 + retries: 60 + delay: 5 + failed_when: false + changed_when: false + delegate_to: "{{ inventory_hostname }}-cli" + when: + - tpl_9331_is_template.rc != 0 + - tpl_9331_exists.rc != 0 + +- hosts: proxmox-cli + gather_facts: false + vars_files: + - "../../../../secrets/default_vault.yml" + + tasks: + - name: TEMPLATE 9331 - CHECK if already template (proxmox-cli play) + ansible.builtin.shell: qm config 9331 2>/dev/null | grep -q '^template:' + register: tpl_9331_is_template + failed_when: false + changed_when: false + + - name: PROXMOX INIT - TEMPLATES - IMPORT CLOUD INIT DISK - template-vm-debian-trixie-medium + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "template_cloudinit_import_disk" + proxmox_node: "pve" + cloudinit_image_full_path: "/var/lib/vz/template/iso/debian-13-genericcloud-amd64.img" + vm_id: 9331 + vm_disk_size: "64g" + proxmox_dest_vm_storage_name: "local-lvm" + when: tpl_9331_is_template.rc != 0 + +- hosts: proxmox + gather_facts: false + vars_files: + - "../../../../secrets/default_vault.yml" + + tasks: + - name: TEMPLATE 9331 - CHECK if already template (third play) + ansible.builtin.shell: qm config 9331 2>/dev/null | grep -q '^template:' + register: tpl_9331_is_template + failed_when: false + changed_when: false + delegate_to: "{{ inventory_hostname }}-cli" + + - name: PROXMOX INIT - TEMPLATES - SET CLOUD DEFAULT INIT VARIABLE - template-vm-debian-trixie-medium + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "cloudinit_set_variables" + vm_id: 9331 + vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" + vm_ci_password: "{{ default_trainee_vm_ci_password | default('supersecret') }}" + vm_ci_ssh_key: "{{ default_trainee_vm_ci_ssh_key }}" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" + vm_ci_ip: "192.168.140.2" + vm_ci_netmask: "24" + vm_ci_ip_gw: "192.168.140.1" + when: tpl_9331_is_template.rc != 0 + + - name: PROXMOX INIT - TEMPLATES - SET PROXMOX TAG + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "vm_set_tag" + vm_id: 9331 + vm_tag_name: "template" + when: tpl_9331_is_template.rc != 0 diff --git a/scenarios/test_nextcloud/01_init_proxmox/stage_01-create_templates/templates/debian_trixie/template-vm-debian-trixie-small.yml b/scenarios/test_nextcloud/01_init_proxmox/stage_01-create_templates/templates/debian_trixie/template-vm-debian-trixie-small.yml new file mode 100644 index 00000000..bf774d52 --- /dev/null +++ b/scenarios/test_nextcloud/01_init_proxmox/stage_01-create_templates/templates/debian_trixie/template-vm-debian-trixie-small.yml @@ -0,0 +1,134 @@ +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# +# PROXMOX INIT - create debian_trixie template VM template-vm-debian-trixie-small (id 9321) +# +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- hosts: proxmox + gather_facts: false + vars_files: + - "../../../../secrets/default_vault.yml" + + tasks: + #### IDEMPOTENCE - skip if already finalized as template #### + - name: TEMPLATE 9321 - CHECK if already finalized as template + ansible.builtin.shell: qm config 9321 2>/dev/null | grep -q '^template:' + register: tpl_9321_is_template + failed_when: false + changed_when: false + delegate_to: "{{ inventory_hostname }}-cli" + + - name: TEMPLATE 9321 - WAIT for unlock if a parallel deploy holds the VM + ansible.builtin.shell: qm config 9321 | grep -q '^lock:' + register: tpl_9321_lock + until: tpl_9321_lock.rc != 0 + retries: 30 + delay: 10 + failed_when: false + changed_when: false + delegate_to: "{{ inventory_hostname }}-cli" + when: tpl_9321_is_template.rc != 0 + + - name: TEMPLATE 9321 - SKIP all (already finalized as template) + ansible.builtin.debug: + msg: "Template 9321 is already a template - skipping. Run delete-everything to recreate." + when: tpl_9321_is_template.rc == 0 + + #### vm_create idempotence: separate exists check #### + - name: TEMPLATE 9321 - CHECK if VM exists (vm_create idempotence) + ansible.builtin.shell: qm config 9321 2>/dev/null + register: tpl_9321_exists + failed_when: false + changed_when: false + delegate_to: "{{ inventory_hostname }}-cli" + when: tpl_9321_is_template.rc != 0 + + - name: PROXMOX INIT - TEMPLATES - CREATE TEMPLATE - template-vm-debian-trixie-small + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "vm_create" + vm_id: 9321 + vm_name: "template-vm-debian-trixie-small" + vm_cpu: "host" + vm_cores: 1 + vm_sockets: 1 + vm_memory: 4096 + vm_net_virtio_bridge: "vmbr140" + when: + - tpl_9321_is_template.rc != 0 + - tpl_9321_exists.rc != 0 + + - name: PROXMOX INIT - TEMPLATES - WAIT for vm_create to finish (lock-aware) + ansible.builtin.shell: qm config 9321 | grep -q '^lock:' + register: tpl_9321_post_create_lock + until: tpl_9321_post_create_lock.rc != 0 + retries: 60 + delay: 5 + failed_when: false + changed_when: false + delegate_to: "{{ inventory_hostname }}-cli" + when: + - tpl_9321_is_template.rc != 0 + - tpl_9321_exists.rc != 0 + +- hosts: proxmox-cli + gather_facts: false + vars_files: + - "../../../../secrets/default_vault.yml" + + tasks: + - name: TEMPLATE 9321 - CHECK if already template (proxmox-cli play) + ansible.builtin.shell: qm config 9321 2>/dev/null | grep -q '^template:' + register: tpl_9321_is_template + failed_when: false + changed_when: false + + - name: PROXMOX INIT - TEMPLATES - IMPORT CLOUD INIT DISK - template-vm-debian-trixie-small + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "template_cloudinit_import_disk" + proxmox_node: "pve" + cloudinit_image_full_path: "/var/lib/vz/template/iso/debian-13-genericcloud-amd64.img" + vm_id: 9321 + vm_disk_size: "32g" + proxmox_dest_vm_storage_name: "local-lvm" + when: tpl_9321_is_template.rc != 0 + +- hosts: proxmox + gather_facts: false + vars_files: + - "../../../../secrets/default_vault.yml" + + tasks: + - name: TEMPLATE 9321 - CHECK if already template (third play) + ansible.builtin.shell: qm config 9321 2>/dev/null | grep -q '^template:' + register: tpl_9321_is_template + failed_when: false + changed_when: false + delegate_to: "{{ inventory_hostname }}-cli" + + - name: PROXMOX INIT - TEMPLATES - SET CLOUD DEFAULT INIT VARIABLE - template-vm-debian-trixie-small + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "cloudinit_set_variables" + vm_id: 9321 + vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" + vm_ci_password: "{{ default_trainee_vm_ci_password | default('supersecret') }}" + vm_ci_ssh_key: "{{ default_trainee_vm_ci_ssh_key }}" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" + vm_ci_ip: "192.168.140.3" + vm_ci_netmask: "24" + vm_ci_ip_gw: "192.168.140.1" + when: tpl_9321_is_template.rc != 0 + + - name: PROXMOX INIT - TEMPLATES - SET PROXMOX TAG + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "vm_set_tag" + vm_id: 9321 + vm_tag_name: "template" + when: tpl_9321_is_template.rc != 0 diff --git a/scenarios/test_nextcloud/02_lan1_infrastructure/_main.reinstall.sh b/scenarios/test_nextcloud/02_lan1_infrastructure/_main.reinstall.sh new file mode 100644 index 00000000..f0ede56e --- /dev/null +++ b/scenarios/test_nextcloud/02_lan1_infrastructure/_main.reinstall.sh @@ -0,0 +1,7 @@ +#!/bin/bash +## +## re-run this section's playbooks (02_lan1_infrastructure) +## +ansible-playbook -i "${RANGE42_ANSIBLE_ROLES__INVENTORY_DIR}/inventory_default.yml" \ + -l "all" \ + "./_main.yml" --vault-password-file "${RANGE42_VAULT_PASSWORD_FILE:?RANGE42_VAULT_PASSWORD_FILE is not set — run: range42-context use }" diff --git a/scenarios/test_nextcloud/02_lan1_infrastructure/_main.yml b/scenarios/test_nextcloud/02_lan1_infrastructure/_main.yml new file mode 100644 index 00000000..45b30164 --- /dev/null +++ b/scenarios/test_nextcloud/02_lan1_infrastructure/_main.yml @@ -0,0 +1,18 @@ +--- +## +## 02_lan1_infrastructure — generated by r42playbooks +## stage_00 clones VMs (per-VM global_* below); stage_01 installs software. +## +#### STAGE 00 #### +- import_playbook: ./stage_00/lan1-admin-nextcloud-00.yml + vars: + global_vm_name: "lan1-admin-nextcloud-00" + global_vm_ssh_name: "r42.lan1-admin-nextcloud-00" + global_vm_id: 1002 + global_vm_description: "admin-nextcloud" + global_vm_tag_name: "lan1" + global_vm_ci_ip: "192.168.150.2" + global_template_vm_id: 9331 + global_template_name: "template-vm-debian-trixie-medium - id 9331" +#### STAGE 01 #### +- import_playbook: ./stage_01/lan1-admin-nextcloud-00.yml diff --git a/scenarios/test_nextcloud/02_lan1_infrastructure/stage_00/lan1-admin-nextcloud-00.yml b/scenarios/test_nextcloud/02_lan1_infrastructure/stage_00/lan1-admin-nextcloud-00.yml new file mode 100644 index 00000000..bc3e9b9b --- /dev/null +++ b/scenarios/test_nextcloud/02_lan1_infrastructure/stage_00/lan1-admin-nextcloud-00.yml @@ -0,0 +1,74 @@ +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# +# LAN1 INFRASTRUCTURE INIT - CLONE + CLOUD-INIT +# +# hostname : lan1-admin-nextcloud-00 +# vm_id : 1002 +# ip : 192.168.150.2 +# template : template-vm-debian-trixie-medium +# +# global_* vars are supplied by ../_main.yml (kept here as Jinja references). +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +- hosts: proxmox + gather_facts: false + vars_files: + - "../../secrets/default_vault.yml" + tasks: + - name: "LAN1 INFRASTRUCTURE INIT - {{ global_vm_name }} - CLONE TEMPLATE - {{ global_template_name }}" + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "vm_clone" + vm_id: "{{ global_template_vm_id }}" + vm_new_id: "{{ global_vm_id }}" + vm_name: "{{ global_vm_name }}" + vm_description: "{{ global_vm_description }}" + - name: "LAN1 INFRASTRUCTURE INIT - {{ global_vm_name }} - WAIT FOR PROXMOX UNLOCK" + ansible.builtin.shell: qm config {{ global_vm_id }} | grep -q '^lock:' + register: clone_lock_check + until: clone_lock_check.rc != 0 + retries: 60 + delay: 5 + failed_when: false + changed_when: false + delegate_to: "{{ inventory_hostname }}-cli" + - name: "LAN1 INFRASTRUCTURE INIT - {{ global_vm_name }} - SET PROXMOX TAG" + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "vm_set_tag" + vm_id: "{{ global_vm_id }}" + vm_tag_name: "{{ global_vm_tag_name }}" + - name: "LAN1 INFRASTRUCTURE INIT - {{ global_vm_name }} - SET CLOUD-INIT VARIABLES" + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "cloudinit_set_variables" + vm_id: "{{ global_vm_id }}" + vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" + vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" + vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" + vm_ci_ip: "{{ global_vm_ci_ip }}" + vm_ci_netmask: "24" + vm_ci_ip_gw: "192.168.150.1" + vm_net_virtio_bridge: "vmbr150" + - name: "LAN1 INFRASTRUCTURE INIT - {{ global_vm_name }} - START VM" + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "vm_start" + vm_id: "{{ global_vm_id }}" +- hosts: "{{ global_vm_ssh_name }}" + gather_facts: false + vars_files: + - "../../secrets/default_vault.yml" + vars: + ARG_vm_ssh_name: "{{ global_vm_ssh_name }}" + requested_tasks: + - wait/openssh_server/is_reachable.yml + - wait/cloudinit/is_boot_finished.yml + tasks: + - name: "LAN1 INFRASTRUCTURE INIT - {{ global_vm_name }} - WAIT FOR SSH + CLOUD-INIT" + include_role: + name: ansible.utils diff --git a/scenarios/test_nextcloud/02_lan1_infrastructure/stage_01/lan1-admin-nextcloud-00.yml b/scenarios/test_nextcloud/02_lan1_infrastructure/stage_01/lan1-admin-nextcloud-00.yml new file mode 100644 index 00000000..6245978d --- /dev/null +++ b/scenarios/test_nextcloud/02_lan1_infrastructure/stage_01/lan1-admin-nextcloud-00.yml @@ -0,0 +1,40 @@ +## +## stage_01 — software install for lan1-admin-nextcloud-00 +## catalog roles are referenced BY NAME (resolved via ANSIBLE_ROLES_PATH at deploy); +## one play per attachment so each carries its own params (firewall_rules, docker …). +## +- name: "lan1-admin-nextcloud-00 — software.configure.firewalls" + hosts: r42.lan1-admin-nextcloud-00 + become: true + vars_files: + - "../../secrets/default_vault.yml" + vars: + firewall_rules: + - ip: all + port: 22 + protocol: tcp + - ip: all + port: 8080 + protocol: tcp + roles: + - software.configure.firewalls +- name: "lan1-admin-nextcloud-00 — software.install.warmup.basic_packages" + hosts: r42.lan1-admin-nextcloud-00 + become: true + vars_files: + - "../../secrets/default_vault.yml" + vars: + INSTALL_PACKAGES_BASICS: 'YES' + INSTALL_PACKAGES_DOCKER: 'YES' + INSTALL_PACKAGES_DOCKER_COMPOSE: 'YES' + INSTALL_PACKAGES_UTILS_JSON: 'YES' + INSTALL_PACKAGES_UTILS_NETWORK: 'YES' + roles: + - software.install.warmup.basic_packages +- name: "lan1-admin-nextcloud-00 — software.install.nextcloud" + hosts: r42.lan1-admin-nextcloud-00 + become: true + vars_files: + - "../../secrets/default_vault.yml" + roles: + - software.install.nextcloud diff --git a/scenarios/test_nextcloud/03_lan2_infrastructure/_main.reinstall.sh b/scenarios/test_nextcloud/03_lan2_infrastructure/_main.reinstall.sh new file mode 100644 index 00000000..2ac11384 --- /dev/null +++ b/scenarios/test_nextcloud/03_lan2_infrastructure/_main.reinstall.sh @@ -0,0 +1,7 @@ +#!/bin/bash +## +## re-run this section's playbooks (03_lan2_infrastructure) +## +ansible-playbook -i "${RANGE42_ANSIBLE_ROLES__INVENTORY_DIR}/inventory_default.yml" \ + -l "all" \ + "./_main.yml" --vault-password-file "${RANGE42_VAULT_PASSWORD_FILE:?RANGE42_VAULT_PASSWORD_FILE is not set — run: range42-context use }" diff --git a/scenarios/test_nextcloud/03_lan2_infrastructure/_main.yml b/scenarios/test_nextcloud/03_lan2_infrastructure/_main.yml new file mode 100644 index 00000000..4fe294b1 --- /dev/null +++ b/scenarios/test_nextcloud/03_lan2_infrastructure/_main.yml @@ -0,0 +1,18 @@ +--- +## +## 03_lan2_infrastructure — generated by r42playbooks +## stage_00 clones VMs (per-VM global_* below); stage_01 installs software. +## +#### STAGE 00 #### +- import_playbook: ./stage_00/lan2-debian-jump-00.yml + vars: + global_vm_name: "lan2-debian-jump-00" + global_vm_ssh_name: "r42.lan2-debian-jump-00" + global_vm_id: 3002 + global_vm_description: "debian-jump" + global_vm_tag_name: "lan2" + global_vm_ci_ip: "192.168.151.2" + global_template_vm_id: 9321 + global_template_name: "template-vm-debian-trixie-small - id 9321" +#### STAGE 01 #### +- import_playbook: ./stage_01/lan2-debian-jump-00.yml diff --git a/scenarios/test_nextcloud/03_lan2_infrastructure/stage_00/lan2-debian-jump-00.yml b/scenarios/test_nextcloud/03_lan2_infrastructure/stage_00/lan2-debian-jump-00.yml new file mode 100644 index 00000000..32d7db5e --- /dev/null +++ b/scenarios/test_nextcloud/03_lan2_infrastructure/stage_00/lan2-debian-jump-00.yml @@ -0,0 +1,81 @@ +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# +# LAN2 INFRASTRUCTURE INIT - CLONE + CLOUD-INIT +# +# hostname : lan2-debian-jump-00 +# vm_id : 3002 +# ip : 192.168.151.2 +# template : template-vm-debian-trixie-small +# +# global_* vars are supplied by ../_main.yml (kept here as Jinja references). +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- hosts: proxmox + gather_facts: false + vars_files: + - "../../secrets/default_vault.yml" + + tasks: + - name: "LAN2 INFRASTRUCTURE INIT - {{ global_vm_name }} - CLONE TEMPLATE - {{ global_template_name }}" + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "vm_clone" + vm_id: "{{ global_template_vm_id }}" + vm_new_id: "{{ global_vm_id }}" + vm_name: "{{ global_vm_name }}" + vm_description: "{{ global_vm_description }}" + + - name: "LAN2 INFRASTRUCTURE INIT - {{ global_vm_name }} - WAIT FOR PROXMOX UNLOCK" + ansible.builtin.shell: qm config {{ global_vm_id }} | grep -q '^lock:' + register: clone_lock_check + until: clone_lock_check.rc != 0 + retries: 60 + delay: 5 + failed_when: false + changed_when: false + delegate_to: "{{ inventory_hostname }}-cli" + + - name: "LAN2 INFRASTRUCTURE INIT - {{ global_vm_name }} - SET PROXMOX TAG" + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "vm_set_tag" + vm_id: "{{ global_vm_id }}" + vm_tag_name: "{{ global_vm_tag_name }}" + + - name: "LAN2 INFRASTRUCTURE INIT - {{ global_vm_name }} - SET CLOUD-INIT VARIABLES" + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "cloudinit_set_variables" + vm_id: "{{ global_vm_id }}" + vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" + vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" + vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" + vm_ci_ip: "{{ global_vm_ci_ip }}" + vm_ci_netmask: "24" + vm_ci_ip_gw: "192.168.151.1" + vm_net_virtio_bridge: "vmbr151" + + - name: "LAN2 INFRASTRUCTURE INIT - {{ global_vm_name }} - START VM" + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "vm_start" + vm_id: "{{ global_vm_id }}" + +- hosts: "{{ global_vm_ssh_name }}" + gather_facts: false + vars_files: + - "../../secrets/default_vault.yml" + vars: + ARG_vm_ssh_name: "{{ global_vm_ssh_name }}" + requested_tasks: + - wait/openssh_server/is_reachable.yml + - wait/cloudinit/is_boot_finished.yml + tasks: + - name: "LAN2 INFRASTRUCTURE INIT - {{ global_vm_name }} - WAIT FOR SSH + CLOUD-INIT" + include_role: + name: ansible.utils diff --git a/scenarios/test_nextcloud/03_lan2_infrastructure/stage_01/lan2-debian-jump-00.yml b/scenarios/test_nextcloud/03_lan2_infrastructure/stage_01/lan2-debian-jump-00.yml new file mode 100644 index 00000000..749d0a80 --- /dev/null +++ b/scenarios/test_nextcloud/03_lan2_infrastructure/stage_01/lan2-debian-jump-00.yml @@ -0,0 +1,24 @@ +## +## stage_01 — software install for lan2-debian-jump-00 +## catalog roles are referenced BY NAME (resolved via ANSIBLE_ROLES_PATH at deploy); +## one play per attachment so each carries its own params (firewall_rules, docker …). +## +- name: "lan2-debian-jump-00 — software.configure.firewalls" + hosts: r42.lan2-debian-jump-00 + become: true + vars_files: + - "../../secrets/default_vault.yml" + vars: + firewall_rules: + - ip: all + port: 22 + protocol: tcp + roles: + - software.configure.firewalls +- name: "lan2-debian-jump-00 — software.install.warmup.basic_packages" + hosts: r42.lan2-debian-jump-00 + become: true + vars_files: + - "../../secrets/default_vault.yml" + roles: + - software.install.warmup.basic_packages diff --git a/scenarios/test_nextcloud/README.md b/scenarios/test_nextcloud/README.md new file mode 100644 index 00000000..a19a75a4 --- /dev/null +++ b/scenarios/test_nextcloud/README.md @@ -0,0 +1,14 @@ +# test_nextcloud +Generated by **r42playbooks**. +- subnet layout: `dual-lan` +## Boxes +| box | subnet | image | vm_id | ip | +|---|---|---|---|---| +| `lan1-admin-nextcloud-00` | lan1 | debian_trixie | 1002 | 192.168.150.2 | +| `lan2-debian-jump-00` | lan2 | debian_trixie | 3002 | 192.168.151.2 | +## Deploy +```bash +range42-context use test_nextcloud +./test_nextcloud.setup.sh +``` +The composition is reproduced in `scenario.r42.yml` — re-run `r42playbooks new --spec scenario.r42.yml` to regenerate this tree. diff --git a/scenarios/test_nextcloud/_activate.sh b/scenarios/test_nextcloud/_activate.sh new file mode 100644 index 00000000..94de41f3 --- /dev/null +++ b/scenarios/test_nextcloud/_activate.sh @@ -0,0 +1,14 @@ +#!/bin/bash +# deprecation algo issue with paramiko and ubuntu 24.04.2 LTS +# https://github.com/paramiko/paramiko/issues/2419 +set -euo pipefail +VIRTUAL_ENV_DIR="$HOME/ansible_fix/venv" +if [ ! -d "$VIRTUAL_ENV_DIR" ]; then + mkdir -p "$HOME/ansible_fix" + python3 -m venv "$VIRTUAL_ENV_DIR" + source "$VIRTUAL_ENV_DIR/bin/activate" + pip install --upgrade pip setuptools wheel + pip install ansible paramiko cryptography +fi +source "$VIRTUAL_ENV_DIR/bin/activate" +echo "source \"$VIRTUAL_ENV_DIR/bin/activate\"" diff --git a/scenarios/test_nextcloud/devkit_ansible.show_ansible_inventory.to.text.sh b/scenarios/test_nextcloud/devkit_ansible.show_ansible_inventory.to.text.sh new file mode 100644 index 00000000..09a451a0 --- /dev/null +++ b/scenarios/test_nextcloud/devkit_ansible.show_ansible_inventory.to.text.sh @@ -0,0 +1,2 @@ +#!/bin/bash +ansible-inventory -i "${RANGE42_ANSIBLE_ROLES__INVENTORY_DIR}/inventory_default.yml" --graph diff --git a/scenarios/test_nextcloud/main.yml b/scenarios/test_nextcloud/main.yml new file mode 100644 index 00000000..3f70fc9a --- /dev/null +++ b/scenarios/test_nextcloud/main.yml @@ -0,0 +1,7 @@ +--- +## +## test_nextcloud — generated by r42playbooks +## +- import_playbook: ./01_init_proxmox/_main.yml +- import_playbook: ./02_lan1_infrastructure/_main.yml +- import_playbook: ./03_lan2_infrastructure/_main.yml diff --git a/scenarios/test_nextcloud/main_vms_only.yml b/scenarios/test_nextcloud/main_vms_only.yml new file mode 100644 index 00000000..8b2edeb8 --- /dev/null +++ b/scenarios/test_nextcloud/main_vms_only.yml @@ -0,0 +1,6 @@ +--- +## +## test_nextcloud — deploy VMs only (templates already exist; skip 01_init_proxmox) +## +- import_playbook: ./02_lan1_infrastructure/_main.yml +- import_playbook: ./03_lan2_infrastructure/_main.yml diff --git a/scenarios/test_nextcloud/manifest/scenario_vms.json b/scenarios/test_nextcloud/manifest/scenario_vms.json new file mode 100644 index 00000000..5862e773 --- /dev/null +++ b/scenarios/test_nextcloud/manifest/scenario_vms.json @@ -0,0 +1,13 @@ +{ + "description": "", + "scenario": "test_nextcloud", + "templates": [ + {"bridge": "vmbr140", "image": "debian_trixie", "ip": "192.168.140.3", "spec": "1cpu/4gb/32gb", "vm_id": 9321, "vm_name": "template-vm-debian-trixie-small"}, + {"bridge": "vmbr140", "image": "debian_trixie", "ip": "192.168.140.2", "spec": "2cpu/8gb/64gb", "vm_id": 9331, "vm_name": "template-vm-debian-trixie-medium"} + ], + "version": 2, + "vms": [ + {"bridge": "vmbr150", "image": "debian_trixie", "ip": "192.168.150.2", "subnet": "lan1", "vm_id": 1002, "vm_name": "lan1-admin-nextcloud-00"}, + {"bridge": "vmbr151", "image": "debian_trixie", "ip": "192.168.151.2", "subnet": "lan2", "vm_id": 3002, "vm_name": "lan2-debian-jump-00"} + ] +} diff --git a/scenarios/test_nextcloud/scenario.r42.yml b/scenarios/test_nextcloud/scenario.r42.yml new file mode 100644 index 00000000..da5892f6 --- /dev/null +++ b/scenarios/test_nextcloud/scenario.r42.yml @@ -0,0 +1,22 @@ +boxes: +- attachments_add: [] + count: 1 + octet: 2 + subnet: lan1 + template: admin-nextcloud + template_vm_id: null + vars: {} +- attachments_add: [] + count: 1 + octet: 2 + subnet: lan2 + template: debian-jump + template_vm_id: null + vars: {} +name: test_nextcloud +network_policy: null +notes: '' +proxmox_node: null +schema_version: 1 +services: null +subnet_layout: dual-lan diff --git a/scenarios/test_nextcloud/templates/ansible-inventory.j2 b/scenarios/test_nextcloud/templates/ansible-inventory.j2 new file mode 100644 index 00000000..42dbed84 --- /dev/null +++ b/scenarios/test_nextcloud/templates/ansible-inventory.j2 @@ -0,0 +1,19 @@ +all: + children: + range42_infrastructure: + children: + r42_lan1_group: + hosts: + r42.lan1-admin-nextcloud-00: + r42_lan2_group: + hosts: + r42.lan2-debian-jump-00: + proxmox: + hosts: + {{ INFRASTRUCTURE_CODENAME }}: + ansible_host: {{ INFRASTRUCTURE_PROXMOX_ADDRESS | mandatory }}:8006 + ansible_connection: local + ansible_python_interpreter: /usr/bin/python3 + proxmox-cli: + hosts: + {{ INFRASTRUCTURE_CODENAME }}-cli: diff --git a/scenarios/test_nextcloud/templates/ansible-vars.yml b/scenarios/test_nextcloud/templates/ansible-vars.yml new file mode 100644 index 00000000..494ac1b6 --- /dev/null +++ b/scenarios/test_nextcloud/templates/ansible-vars.yml @@ -0,0 +1,20 @@ +--- +################################################################################ +# range42 — scenario-specific variables (test_nextcloud) +# +# Generated by r42playbooks. Shared variables -> group_vars/all/vars.yml, +# secrets -> vault (see vault-example.yml). +################################################################################ +#### SCENARIO IDENTIFICATION #### +INFRASTRUCTURE_SCENARIO: "test_nextcloud" +#### CREDENTIAL GENERATION #### +context_auto_generate_ssh_keys: "YES" +context_auto_generate_vm_passwords: "YES" +student_additionnal_keys_count: 5 +#### CLOUD-INIT USERNAMES #### +default_admin_vm_ci_user: "alice" +default_trainee_vm_ci_user: "bob" +#### CLOUD-INIT DNS #### +# default resolver for lab VMs; range42-init.py substitutes the Proxmox node +# resolver here at workspace creation. 1.1.1.1 is the portable fallback. +default_vm_ci_dns_ips: "1.1.1.1" diff --git a/scenarios/test_nextcloud/templates/ssh-config.j2 b/scenarios/test_nextcloud/templates/ssh-config.j2 new file mode 100644 index 00000000..6ff71406 --- /dev/null +++ b/scenarios/test_nextcloud/templates/ssh-config.j2 @@ -0,0 +1,38 @@ +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# infrastructure code name : {{ INFRASTRUCTURE_CODENAME }} +# infrastructure proxmox address : {{ INFRASTRUCTURE_PROXMOX_ADDRESS }} +# scenario : {{ INFRASTRUCTURE_SCENARIO }} +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +#### PROXMOX WEB UI (port forward) #### +Host px.{{ INFRASTRUCTURE_CODENAME }}.redirect_www.proxmox + RequestTTY no + RemoteCommand none + localforward localhost:18042 {{ INFRASTRUCTURE_PROXMOX_ADDRESS | mandatory }}:8006 + +#### PX ROOT USER SSH ACCESS #### +Host px.{{ INFRASTRUCTURE_CODENAME }}-ssh_cli.root {{ INFRASTRUCTURE_CODENAME }}-cli + Hostname {{ INFRASTRUCTURE_PROXMOX_ADDRESS | mandatory }} + User root + IdentityFile {{ DEPLOYER_CLI__DST_SSH_KEYS_JUMP_DEST_DIR }}/px.{{ INFRASTRUCTURE_CODENAME }}-{{ INFRASTRUCTURE_SCENARIO }}-ssh_cli.root + Port 22 + +#### SSH JUMPER #### +Host px.{{ INFRASTRUCTURE_CODENAME }}.jumper + Hostname {{ INFRASTRUCTURE_PROXMOX_ADDRESS | mandatory }} + User jump_user + IdentityFile {{ DEPLOYER_CLI__DST_SSH_KEYS_JUMP_DEST_DIR }}/px.{{ INFRASTRUCTURE_CODENAME }}-{{ INFRASTRUCTURE_SCENARIO }}-ssh_cli.jump_user + Port 22 + +#### SCENARIO VMs #### +Host r42.lan1-admin-nextcloud-00 + Hostname 192.168.150.2 + +Host r42.lan2-debian-jump-00 + Hostname 192.168.151.2 + +Host r42.* + User alice + IdentityFile {{ DEPLOYER_CLI__DST_SSH_KEYS_BACKEND_DEST_DIR }}/r42.{{ INFRASTRUCTURE_CODENAME }}-{{ INFRASTRUCTURE_SCENARIO }}-deployer-key_alice + Port 22 + ProxyJump px.{{ INFRASTRUCTURE_CODENAME }}.jumper diff --git a/scenarios/test_nextcloud/templates/vault-example.yml b/scenarios/test_nextcloud/templates/vault-example.yml new file mode 100644 index 00000000..5d1acdd4 --- /dev/null +++ b/scenarios/test_nextcloud/templates/vault-example.yml @@ -0,0 +1,37 @@ +--- +################################################################################ +# range42 — vault secrets (test_nextcloud) +# +# Copy to vault.yml, fill real values, then: ansible-vault encrypt vault.yml +# Never commit the unencrypted version. +################################################################################ + + +#### PROXMOX API SECRET #### +proxmox_api_token_secret: "REPLACE_ME" + + +#### JUMP HOST #### +jump_password: "REPLACE_ME" + + +#### CLOUD-INIT PASSWORDS #### +default_admin_vm_ci_password: "REPLACE_ME" +default_admin_vm_ci_ssh_key: "ssh-ed25519 AAAA... alice CODENAME-SCENARIO" +default_trainee_vm_ci_password: "REPLACE_ME" +default_trainee_vm_ci_ssh_key: "ssh-ed25519 AAAA... bob CODENAME-SCENARIO" + + +#### CLOUD-INIT DNS #### +# default resolver for lab VMs; range42-init.py substitutes the Proxmox node +# resolver here at workspace creation. 1.1.1.1 is the portable fallback. +default_vm_ci_dns_ips: "1.1.1.1" + + +#### TAILSCALE #### +infrastructure_tailscale_authkey: "tskey-auth-REPLACE_ME" +infrastructure_tailscale_apikey: "tskey-api-REPLACE_ME" + + +#### MISC #### +deployer_cli_user_ssh_known_hosts: "/home/your_deployer_cli_username/.ssh/known_hosts" diff --git a/scenarios/test_nextcloud/test_nextcloud.delete_all.sh b/scenarios/test_nextcloud/test_nextcloud.delete_all.sh new file mode 100644 index 00000000..2b27a54c --- /dev/null +++ b/scenarios/test_nextcloud/test_nextcloud.delete_all.sh @@ -0,0 +1,32 @@ +#!/bin/bash + +## +## delete all — VMs + this scenario's ubuntu_noble templates +## +## ⚠ templates (9xxx) are shared across scenarios on the same Proxmox. Run this +## only when no other scenario relies on them, or use delete_vms_only.sh instead. +## +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +MANIFEST="$SCRIPT_DIR/manifest/scenario_vms.json" +if [[ ! -f "$MANIFEST" ]]; then + echo "ERROR: manifest not found: $MANIFEST" >&2 + exit 1 +fi +mapfile -t SCENARIO_VM_IDS < <(jq -r '.vms[].vm_id' "$MANIFEST") +mapfile -t TEMPLATE_VM_IDS < <(jq -r '.templates[].vm_id' "$MANIFEST") +mapfile -t INFRASTRUCTURE_IP < <(jq -r '.vms[].ip' "$MANIFEST") +ALL_IDS=("${SCENARIO_VM_IDS[@]}" "${TEMPLATE_VM_IDS[@]}") +ID_REGEX=$(printf '|%s' "${ALL_IDS[@]}" | sed 's/^|//') +echo ":: stopping and deleting VMs + templates" +VM_LIST_JSON=$(proxmox_vm.list.to.jsons.sh 2>&1 | grep '"vm_id":[0-9]') +if [ -z "$VM_LIST_JSON" ]; then + echo "ERROR: proxmox_vm.list.to.jsons.sh returned no VM data — aborting" >&2 + exit 1 +fi +echo "$VM_LIST_JSON" | jq -c | grep -E "\"vm_id\":($ID_REGEX)([^0-9]|$)" | proxmox_vm.vm_id.stop_force.to.jsons.sh +echo "$VM_LIST_JSON" | jq -c | grep -E "\"vm_id\":($ID_REGEX)([^0-9]|$)" | proxmox_vm.vm_id.delete.to.jsons.sh +for ip in "${INFRASTRUCTURE_IP[@]}"; do + echo ":: REMOVE SSH KEY FOR : $ip" + ssh-keygen -f "$HOME/.ssh/known_hosts" -R "$ip" +done +echo ":: done — VMs and templates removed" diff --git a/scenarios/test_nextcloud/test_nextcloud.delete_vms_only.sh b/scenarios/test_nextcloud/test_nextcloud.delete_vms_only.sh new file mode 100644 index 00000000..ba2443a3 --- /dev/null +++ b/scenarios/test_nextcloud/test_nextcloud.delete_vms_only.sh @@ -0,0 +1,33 @@ +#!/bin/bash + +## +## delete VMs only — scenario VMs (filter by vm_id), keep templates +## + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +MANIFEST="$SCRIPT_DIR/manifest/scenario_vms.json" + +if [[ ! -f "$MANIFEST" ]]; then + echo "ERROR: manifest not found: $MANIFEST" >&2 + exit 1 +fi + +mapfile -t SCENARIO_VM_IDS < <(jq -r '.vms[].vm_id' "$MANIFEST") +mapfile -t INFRASTRUCTURE_IP < <(jq -r '.vms[].ip' "$MANIFEST") +ID_REGEX=$(printf '|%s' "${SCENARIO_VM_IDS[@]}" | sed 's/^|//') + +echo ":: stopping and deleting scenario VMs (keeping templates)..." +VM_LIST_JSON=$(proxmox_vm.list.to.jsons.sh 2>&1 | grep '"vm_id":[0-9]') +if [ -z "$VM_LIST_JSON" ]; then + echo "ERROR: proxmox_vm.list.to.jsons.sh returned no VM data — aborting" >&2 + exit 1 +fi +echo "$VM_LIST_JSON" | jq -c | grep -E "\"vm_id\":($ID_REGEX)([^0-9]|$)" | proxmox_vm.vm_id.stop_force.to.jsons.sh +echo "$VM_LIST_JSON" | jq -c | grep -E "\"vm_id\":($ID_REGEX)([^0-9]|$)" | proxmox_vm.vm_id.delete.to.jsons.sh + +for ip in "${INFRASTRUCTURE_IP[@]}"; do + echo ":: REMOVE SSH KEY FOR : $ip" + ssh-keygen -f "$HOME/.ssh/known_hosts" -R "$ip" +done + +echo ":: done — templates preserved" diff --git a/scenarios/test_nextcloud/test_nextcloud.reset.setup.sh b/scenarios/test_nextcloud/test_nextcloud.reset.setup.sh new file mode 100644 index 00000000..b70309e0 --- /dev/null +++ b/scenarios/test_nextcloud/test_nextcloud.reset.setup.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +## +## reset — delete this scenario's VMs (from manifest) then re-deploy +## + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +MANIFEST="$SCRIPT_DIR/manifest/scenario_vms.json" + +if [[ ! -f "$MANIFEST" ]]; then + echo "ERROR: manifest not found: $MANIFEST" >&2 + exit 1 +fi + +mapfile -t SCENARIO_VM_IDS < <(jq -r '.vms[].vm_id' "$MANIFEST") +mapfile -t INFRASTRUCTURE_IP < <(jq -r '.vms[].ip' "$MANIFEST") +ID_REGEX=$(printf '|%s' "${SCENARIO_VM_IDS[@]}" | sed 's/^|//') + +echo ":: stopping and deleting scenario VMs (vm_ids: ${SCENARIO_VM_IDS[*]})..." +VM_LIST_JSON=$(proxmox_vm.list.to.jsons.sh 2>&1 | grep '"vm_id":[0-9]') +if [ -z "$VM_LIST_JSON" ]; then + echo "ERROR: proxmox_vm.list.to.jsons.sh returned no VM data — aborting" >&2 + exit 1 +fi +echo "$VM_LIST_JSON" | jq -c | grep -E "\"vm_id\":($ID_REGEX)([^0-9]|$)" | proxmox_vm.vm_id.stop_force.to.jsons.sh +echo "$VM_LIST_JSON" | jq -c | grep -E "\"vm_id\":($ID_REGEX)([^0-9]|$)" | proxmox_vm.vm_id.delete.to.jsons.sh + +for ip in "${INFRASTRUCTURE_IP[@]}"; do + echo ":: REMOVE SSH KEY FOR : $ip" + ssh-keygen -f "$HOME/.ssh/known_hosts" -R "$ip" +done + +ansible-playbook -i "${RANGE42_ANSIBLE_ROLES__INVENTORY_DIR}/inventory_default.yml" \ + -l "all" \ + "./main.yml" --vault-password-file "${RANGE42_VAULT_PASSWORD_FILE:?RANGE42_VAULT_PASSWORD_FILE is not set — run: range42-context use }" diff --git a/scenarios/test_nextcloud/test_nextcloud.reset.ssh_keys.sh b/scenarios/test_nextcloud/test_nextcloud.reset.ssh_keys.sh new file mode 100644 index 00000000..bea1d07b --- /dev/null +++ b/scenarios/test_nextcloud/test_nextcloud.reset.ssh_keys.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +## +## remove known_hosts entries for every VM IP in this scenario (from manifest) +## + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +MANIFEST="$SCRIPT_DIR/manifest/scenario_vms.json" + +if [[ ! -f "$MANIFEST" ]]; then + echo "ERROR: manifest not found: $MANIFEST" >&2 + exit 1 +fi + +mapfile -t INFRASTRUCTURE_IP < <(jq -r '.vms[].ip' "$MANIFEST") + +for ip in "${INFRASTRUCTURE_IP[@]}"; do + echo ":: REMOVE SSH KEY FOR : $ip" + ssh-keygen -f "$HOME/.ssh/known_hosts" -R "$ip" +done diff --git a/scenarios/test_nextcloud/test_nextcloud.setup.sh b/scenarios/test_nextcloud/test_nextcloud.setup.sh new file mode 100644 index 00000000..739bddcf --- /dev/null +++ b/scenarios/test_nextcloud/test_nextcloud.setup.sh @@ -0,0 +1,4 @@ +#!/bin/bash +ansible-playbook -i "${RANGE42_ANSIBLE_ROLES__INVENTORY_DIR}/inventory_default.yml" \ + -l "all" \ + "./main.yml" --vault-password-file "${RANGE42_VAULT_PASSWORD_FILE:?RANGE42_VAULT_PASSWORD_FILE is not set — run: range42-context use }" diff --git a/scenarios/test_nextcloud/test_nextcloud.setup_vms_only.sh b/scenarios/test_nextcloud/test_nextcloud.setup_vms_only.sh new file mode 100644 index 00000000..016acd50 --- /dev/null +++ b/scenarios/test_nextcloud/test_nextcloud.setup_vms_only.sh @@ -0,0 +1,8 @@ +#!/bin/bash +## +## deploy VMs only — skip template download and creation +## faster redeploy when templates already exist on proxmox +## +ansible-playbook -i "${RANGE42_ANSIBLE_ROLES__INVENTORY_DIR}/inventory_default.yml" \ + -l "all" \ + "./main_vms_only.yml" --vault-password-file "${RANGE42_VAULT_PASSWORD_FILE:?RANGE42_VAULT_PASSWORD_FILE is not set — run: range42-context use }" diff --git a/scenarios/test_rocketchat/01_init_proxmox/_main.reinstall.sh b/scenarios/test_rocketchat/01_init_proxmox/_main.reinstall.sh new file mode 100755 index 00000000..adcd14a3 --- /dev/null +++ b/scenarios/test_rocketchat/01_init_proxmox/_main.reinstall.sh @@ -0,0 +1,8 @@ +#!/bin/bash +## +## re-run 01_init_proxmox (download images + create templates) +## + +ansible-playbook -i "${RANGE42_ANSIBLE_ROLES__INVENTORY_DIR}/inventory_default.yml" \ + -l "proxmox" \ + "./_main.yml" --vault-password-file "${RANGE42_VAULT_PASSWORD_FILE:?RANGE42_VAULT_PASSWORD_FILE is not set — run: range42-context use }" diff --git a/scenarios/test_rocketchat/01_init_proxmox/_main.yml b/scenarios/test_rocketchat/01_init_proxmox/_main.yml new file mode 100644 index 00000000..40a81665 --- /dev/null +++ b/scenarios/test_rocketchat/01_init_proxmox/_main.yml @@ -0,0 +1,2 @@ +- import_playbook: ./stage_00-download_cloudinit_files/_main.yml +- import_playbook: ./stage_01-create_templates/_main.yml diff --git a/scenarios/test_rocketchat/01_init_proxmox/stage_00-download_cloudinit_files/_main.yml b/scenarios/test_rocketchat/01_init_proxmox/stage_00-download_cloudinit_files/_main.yml new file mode 100644 index 00000000..9dd4628e --- /dev/null +++ b/scenarios/test_rocketchat/01_init_proxmox/stage_00-download_cloudinit_files/_main.yml @@ -0,0 +1,5 @@ +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# PROMOX INIT - download cloud-init base images (only the OS families used) +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- import_playbook: ./cloudinit_debian_trixie.yml diff --git a/scenarios/test_rocketchat/01_init_proxmox/stage_00-download_cloudinit_files/cloudinit_debian_trixie.yml b/scenarios/test_rocketchat/01_init_proxmox/stage_00-download_cloudinit_files/cloudinit_debian_trixie.yml new file mode 100644 index 00000000..633cfec3 --- /dev/null +++ b/scenarios/test_rocketchat/01_init_proxmox/stage_00-download_cloudinit_files/cloudinit_debian_trixie.yml @@ -0,0 +1,39 @@ +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# +# PROXMOX INIT - download cloud init image for debian_trixie +# +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- hosts: proxmox + gather_facts: false + vars_files: + - "../../secrets/default_vault.yml" + + tasks: + - name: PROMOX INIT - DOWNLOAD - CLOUD INIT images + include_role: + name: range42-ansible_roles-proxmox_controller + + vars: + proxmox_vm_action: "storage_download_iso" + proxmox_storage: "local" + iso_file_content_type: "iso" + iso_url: "https://cloud.debian.org/images/cloud/trixie/latest/debian-13-genericcloud-amd64.raw" + iso_file_name: "debian-13-genericcloud-amd64.img" + +# The download-url API is async (returns a UPID immediately). Poll on +# proxmox-cli until the file is fully on disk before stage_01 imports it. +- hosts: proxmox-cli + gather_facts: false + vars_files: + - "../../secrets/default_vault.yml" + + tasks: + - name: PROMOX INIT - WAIT for debian-13-genericcloud-amd64.img to be fully downloaded + ansible.builtin.stat: + path: "/var/lib/vz/template/iso/debian-13-genericcloud-amd64.img" + register: _cloudinit_img + until: _cloudinit_img.stat.exists and _cloudinit_img.stat.size > 100000000 + retries: 120 # 120 x 10 s = 20 min max + delay: 10 + changed_when: false diff --git a/scenarios/test_rocketchat/01_init_proxmox/stage_01-create_templates/_main.yml b/scenarios/test_rocketchat/01_init_proxmox/stage_01-create_templates/_main.yml new file mode 100644 index 00000000..436d49ee --- /dev/null +++ b/scenarios/test_rocketchat/01_init_proxmox/stage_01-create_templates/_main.yml @@ -0,0 +1,6 @@ +--- +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# PROMOX INIT - create the Proxmox template images (only the OS families used) +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- import_playbook: ./templates/debian_trixie/_main_debian_trixie.yml diff --git a/scenarios/test_rocketchat/01_init_proxmox/stage_01-create_templates/templates/debian_trixie/_apply_apt_proxy.yml b/scenarios/test_rocketchat/01_init_proxmox/stage_01-create_templates/templates/debian_trixie/_apply_apt_proxy.yml new file mode 100644 index 00000000..04e2f296 --- /dev/null +++ b/scenarios/test_rocketchat/01_init_proxmox/stage_01-create_templates/templates/debian_trixie/_apply_apt_proxy.yml @@ -0,0 +1,52 @@ +## +## debian_trixie - apt proxy via cloud-init cicustom (optional) +## +## Attaches /var/lib/vz/snippets/range42-apt-proxy.yaml as cloud-init vendor-data +## to every template VM. All VMs cloned from these templates inherit the apt +## proxy automatically via cloud-init at first boot. +## Skipped (no-op) if apt_proxy_url is empty/unset. Idempotent. +## + +- hosts: proxmox-cli + gather_facts: false + vars_files: + - "../../../../secrets/default_vault.yml" + + vars: + bs2_template_vm_ids: + - 9321 # template-vm-debian-trixie-small + - 9331 # template-vm-debian-trixie-medium + + tasks: + - name: APT-PROXY - SKIP (apt_proxy_url is empty) + ansible.builtin.debug: + msg: "apt_proxy_url is empty - cloud-init cicustom not applied to templates" + when: apt_proxy_url is not defined or (apt_proxy_url | length) == 0 + + - name: APT-PROXY - ATTACH cicustom vendor TO TEMPLATES + ansible.builtin.command: + cmd: qm set {{ item }} --cicustom "vendor=local:snippets/range42-apt-proxy.yaml" + loop: "{{ bs2_template_vm_ids }}" + register: qm_result + changed_when: qm_result.rc == 0 + failed_when: + - qm_result.rc != 0 + - "'does not exist' not in qm_result.stderr" + when: apt_proxy_url is defined and (apt_proxy_url | length) > 0 + + - name: APT-PROXY - DETACH cicustom vendor FROM TEMPLATES (apt_proxy_url unset) + ansible.builtin.command: + cmd: qm set {{ item }} --delete cicustom + loop: "{{ bs2_template_vm_ids }}" + register: qm_unset + changed_when: qm_unset.rc == 0 + failed_when: + - qm_unset.rc != 0 + - "'does not exist' not in qm_unset.stderr" + when: apt_proxy_url is not defined or (apt_proxy_url | length) == 0 + + - name: APT-PROXY - DISPLAY STATUS + ansible.builtin.debug: + msg: | + apt proxy: {{ apt_proxy_url if (apt_proxy_url is defined and (apt_proxy_url | length) > 0) else 'DISABLED' }} + cicustom : {{ 'attached to ' + (bs2_template_vm_ids | length | string) + ' templates' if (apt_proxy_url is defined and (apt_proxy_url | length) > 0) else 'detached from templates' }} diff --git a/scenarios/test_rocketchat/01_init_proxmox/stage_01-create_templates/templates/debian_trixie/_main_debian_trixie.yml b/scenarios/test_rocketchat/01_init_proxmox/stage_01-create_templates/templates/debian_trixie/_main_debian_trixie.yml new file mode 100644 index 00000000..ca3944c3 --- /dev/null +++ b/scenarios/test_rocketchat/01_init_proxmox/stage_01-create_templates/templates/debian_trixie/_main_debian_trixie.yml @@ -0,0 +1,13 @@ +## +## debian_trixie — create + configure all Proxmox template VMs for this image +## + +- import_playbook: ./template-vm-debian-trixie-small.yml +- import_playbook: ./template-vm-debian-trixie-medium.yml + +# apt proxy cicustom (no-op if apt_proxy_url empty) +- import_playbook: ./_apply_apt_proxy.yml + +# pre-update: start each VM, run apt update + dist-upgrade via cloud-init poweroff, +# then convert to template. +- import_playbook: ./_update_templates.yml diff --git a/scenarios/test_rocketchat/01_init_proxmox/stage_01-create_templates/templates/debian_trixie/_update_templates.yml b/scenarios/test_rocketchat/01_init_proxmox/stage_01-create_templates/templates/debian_trixie/_update_templates.yml new file mode 100644 index 00000000..2455c653 --- /dev/null +++ b/scenarios/test_rocketchat/01_init_proxmox/stage_01-create_templates/templates/debian_trixie/_update_templates.yml @@ -0,0 +1,202 @@ +## +## debian_trixie - pre-update templates +## +## After all templates are created (but BEFORE convert to template), this playbook: +## 1. renders a temporary bootstrap snippet directly on the Proxmox host +## (apt update + apt dist-upgrade + cloud-init clean + auto-poweroff) +## 2. attaches it as cicustom vendor on each template VM +## 3. starts all templates +## 4. polls qm status until each VM is stopped (cloud-init auto-poweroff signals end) +## 5. detaches the bootstrap snippet, re-attaches the persistent apt-proxy snippet +## 6. converts each VM to a template (qm template ) +## +## Reads VM IDs from the scenario manifest (manifest/scenario_vms.json). +## Runs entirely on `proxmox-cli` (= the Proxmox host itself), no SSH involved. +## + +- hosts: proxmox-cli + gather_facts: false + vars_files: + - "../../../../secrets/default_vault.yml" + + vars: + manifest_path: "{{ playbook_dir }}/../../../../manifest/scenario_vms.json" + bootstrap_snippet_template: "{{ playbook_dir }}/range42-template-bootstrap.yaml.j2" + bootstrap_snippet_path: "/var/lib/vz/snippets/range42-template-bootstrap.yaml" + + tasks: + + #### #### #### debug apt_proxy_url visibility - fully safe vs undefined #### #### #### + + - name: TEMPLATES UPDATE - DEBUG apt_proxy_url visibility + ansible.builtin.debug: + msg: | + apt_proxy_url defined : {{ apt_proxy_url is defined }} + apt_proxy_url value : {{ apt_proxy_url | default('(undefined)') }} + apt_proxy_url length : {{ (apt_proxy_url | default('')) | length }} + -> snippet WILL include apt section : {{ (apt_proxy_url | default('')) | length > 0 }} + + #### #### #### load manifest #### #### #### + + - name: TEMPLATES UPDATE - LOAD MANIFEST + ansible.builtin.set_fact: + templates: "{{ (lookup('file', manifest_path) | from_json).templates }}" + + #### #### #### IDEMPOTENCE - probe each template's current state #### #### #### + # 3 possible states per VM ID : + # - "template" : already converted, skip update entirely + # - "vm" : exists as a regular VM, needs update + convert + # - "missing" : VM doesn't exist on Proxmox, skip (templates not created yet) + # Only "vm" state proceeds through the start/update/shutdown/convert pipeline. + + - name: TEMPLATES UPDATE - PROBE state of each template VM + ansible.builtin.shell: | + if qm config {{ item.vm_id }} 2>/dev/null | grep -q '^template:'; then + echo "template" + elif qm config {{ item.vm_id }} >/dev/null 2>&1; then + echo "vm" + else + echo "missing" + fi + register: tpl_state + loop: "{{ templates }}" + loop_control: + label: "vm_id={{ item.vm_id }}" + changed_when: false + + - name: TEMPLATES UPDATE - BUILD list of templates needing update (state == 'vm') + ansible.builtin.set_fact: + templates_to_update: >- + {{ templates | zip(tpl_state.results) + | selectattr('1.stdout', 'eq', 'vm') + | map(attribute='0') | list }} + + - name: TEMPLATES UPDATE - DISPLAY PLAN + ansible.builtin.debug: + msg: | + total templates in manifest : {{ templates | length }} + already converted (skip) : {{ tpl_state.results | selectattr('stdout', 'eq', 'template') | list | length }} + missing (skip) : {{ tpl_state.results | selectattr('stdout', 'eq', 'missing') | list | length }} + to update + convert : {{ templates_to_update | length }} + {% if templates_to_update | length > 0 %} + targets : + {% for t in templates_to_update %} - {{ t.vm_id }} {{ t.vm_name }} ({{ t.ip }}) + {% endfor %} + {% endif %} + + - name: TEMPLATES UPDATE - SHORT-CIRCUIT if nothing to update + ansible.builtin.meta: end_play + when: templates_to_update | length == 0 + + #### #### #### render bootstrap snippet directly on the Proxmox host #### #### #### + + - name: TEMPLATES UPDATE - RENDER BOOTSTRAP SNIPPET ON PROXMOX + ansible.builtin.template: + src: "{{ bootstrap_snippet_template }}" + dest: "{{ bootstrap_snippet_path }}" + mode: "0644" + + #### #### #### attach bootstrap snippet on every template #### #### #### + + - name: TEMPLATES UPDATE - ATTACH bootstrap cicustom + ansible.builtin.command: + cmd: qm set {{ item.vm_id }} --cicustom "vendor=local:snippets/range42-template-bootstrap.yaml" + loop: "{{ templates_to_update }}" + loop_control: + label: "vm_id={{ item.vm_id }}" + + #### #### #### start all templates #### #### #### + + - name: TEMPLATES UPDATE - START all templates + ansible.builtin.command: + cmd: qm start {{ item.vm_id }} + register: start_result + failed_when: + - start_result.rc != 0 + - "'already running' not in start_result.stderr" + - "'MAX' not in start_result.stderr" + changed_when: start_result.rc == 0 + loop: "{{ templates_to_update }}" + loop_control: + label: "vm_id={{ item.vm_id }}" + + # VMs that failed with "MAX X vcpus" couldn't start - exclude them from + # the wait/convert pipeline but keep the play running for the others. + - name: TEMPLATES UPDATE - BUILD started list (exclude CPU-limited VMs) + ansible.builtin.set_fact: + templates_started: >- + {{ templates_to_update | zip(start_result.results) + | rejectattr('1.stderr', 'search', 'MAX') + | map(attribute='0') | list }} + templates_skipped_cpu: >- + {{ templates_to_update | zip(start_result.results) + | selectattr('1.stderr', 'search', 'MAX') + | map(attribute='0') | list }} + + - name: TEMPLATES UPDATE - WARN skipped templates (host CPU limit) + ansible.builtin.debug: + msg: | + WARNING: {{ templates_skipped_cpu | length }} template(s) skipped - host CPU limit: + {% for t in templates_skipped_cpu %} + - {{ t.vm_id }} {{ t.vm_name }} (spec: {{ t.spec }}) + {% endfor %} + These templates require more vCPUs than this host provides. + They will NOT be converted. Run on a host with enough physical + CPUs, or lower vm_cores in the template playbook if intentional. + when: templates_skipped_cpu | length > 0 + + #### #### #### wait until each VM auto-poweroffs (cloud-init done) #### #### #### + # the loop is sequential per-vm, but since all started in parallel above, + # total time is bounded by the slowest template (~3-15 min depending on cache) + + - name: TEMPLATES UPDATE - WAIT for cloud-init to auto-poweroff + ansible.builtin.shell: | + qm status {{ item.vm_id }} | grep -q 'status: stopped' + register: stop_check + retries: 360 # safety cap : 360 x 5s = 30 min max per VM + delay: 5 + until: stop_check.rc == 0 + loop: "{{ templates_started }}" + loop_control: + label: "vm_id={{ item.vm_id }}" + changed_when: false + + #### #### #### detach bootstrap, re-attach apt-proxy, convert to template #### #### #### + + - name: TEMPLATES UPDATE - DETACH bootstrap cicustom + ansible.builtin.command: + cmd: qm set {{ item.vm_id }} --delete cicustom + loop: "{{ templates_started }}" + loop_control: + label: "vm_id={{ item.vm_id }}" + + - name: TEMPLATES UPDATE - RE-ATTACH apt-proxy cicustom (persistent for clones) + ansible.builtin.command: + cmd: qm set {{ item.vm_id }} --cicustom "vendor=local:snippets/range42-apt-proxy.yaml" + loop: "{{ templates_started }}" + loop_control: + label: "vm_id={{ item.vm_id }}" + when: (apt_proxy_url | default('')) | length > 0 + + - name: TEMPLATES UPDATE - CONVERT VM to template + ansible.builtin.command: + cmd: qm template {{ item.vm_id }} + loop: "{{ templates_started }}" + loop_control: + label: "vm_id={{ item.vm_id }}" + + #### #### #### cleanup #### #### #### + + - name: TEMPLATES UPDATE - REMOVE BOOTSTRAP SNIPPET FROM PROXMOX + ansible.builtin.file: + path: "{{ bootstrap_snippet_path }}" + state: absent + + - name: TEMPLATES UPDATE - DISPLAY DONE + ansible.builtin.debug: + msg: | + {{ templates_started | length }} templates updated and converted + apt-proxy cicustom re-attached (if apt_proxy_url set) + {% if templates_skipped_cpu | length > 0 %} + {{ templates_skipped_cpu | length }} skipped (host CPU limit) - see warning above + {% endif %} diff --git a/scenarios/test_rocketchat/01_init_proxmox/stage_01-create_templates/templates/debian_trixie/range42-template-bootstrap.yaml.j2 b/scenarios/test_rocketchat/01_init_proxmox/stage_01-create_templates/templates/debian_trixie/range42-template-bootstrap.yaml.j2 new file mode 100644 index 00000000..46990999 --- /dev/null +++ b/scenarios/test_rocketchat/01_init_proxmox/stage_01-create_templates/templates/debian_trixie/range42-template-bootstrap.yaml.j2 @@ -0,0 +1,22 @@ +#cloud-config +# range42 - TEMPORARY bootstrap snippet for templates +# Triggers apt update + dist-upgrade then poweroff. +# Auto-generated by _update_templates.yml. Detached after the template is +# converted, replaced by range42-apt-proxy.yaml (persistent). +# +# NOTE: cloud-init clean was previously here in `runcmd` - REMOVED because +# it ran in cloud_config stage, before cloud_final, and broke +# package-update-upgrade-install. Clones get a new vmid -> new instance-id, +# so cloud-init re-processes naturally on each clone. No clean needed. +# +{% if (apt_proxy_url | default('')) | length > 0 %} +apt: + http_proxy: "{{ apt_proxy_url }}" +{% endif %} +package_update: true +package_upgrade: true +package_reboot_if_required: false +power_state: + mode: poweroff + timeout: 60 + condition: True diff --git a/scenarios/test_rocketchat/01_init_proxmox/stage_01-create_templates/templates/debian_trixie/template-vm-debian-trixie-medium.yml b/scenarios/test_rocketchat/01_init_proxmox/stage_01-create_templates/templates/debian_trixie/template-vm-debian-trixie-medium.yml new file mode 100644 index 00000000..7667317f --- /dev/null +++ b/scenarios/test_rocketchat/01_init_proxmox/stage_01-create_templates/templates/debian_trixie/template-vm-debian-trixie-medium.yml @@ -0,0 +1,134 @@ +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# +# PROXMOX INIT - create debian_trixie template VM template-vm-debian-trixie-medium (id 9331) +# +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- hosts: proxmox + gather_facts: false + vars_files: + - "../../../../secrets/default_vault.yml" + + tasks: + #### IDEMPOTENCE - skip if already finalized as template #### + - name: TEMPLATE 9331 - CHECK if already finalized as template + ansible.builtin.shell: qm config 9331 2>/dev/null | grep -q '^template:' + register: tpl_9331_is_template + failed_when: false + changed_when: false + delegate_to: "{{ inventory_hostname }}-cli" + + - name: TEMPLATE 9331 - WAIT for unlock if a parallel deploy holds the VM + ansible.builtin.shell: qm config 9331 | grep -q '^lock:' + register: tpl_9331_lock + until: tpl_9331_lock.rc != 0 + retries: 30 + delay: 10 + failed_when: false + changed_when: false + delegate_to: "{{ inventory_hostname }}-cli" + when: tpl_9331_is_template.rc != 0 + + - name: TEMPLATE 9331 - SKIP all (already finalized as template) + ansible.builtin.debug: + msg: "Template 9331 is already a template - skipping. Run delete-everything to recreate." + when: tpl_9331_is_template.rc == 0 + + #### vm_create idempotence: separate exists check #### + - name: TEMPLATE 9331 - CHECK if VM exists (vm_create idempotence) + ansible.builtin.shell: qm config 9331 2>/dev/null + register: tpl_9331_exists + failed_when: false + changed_when: false + delegate_to: "{{ inventory_hostname }}-cli" + when: tpl_9331_is_template.rc != 0 + + - name: PROXMOX INIT - TEMPLATES - CREATE TEMPLATE - template-vm-debian-trixie-medium + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "vm_create" + vm_id: 9331 + vm_name: "template-vm-debian-trixie-medium" + vm_cpu: "host" + vm_cores: 2 + vm_sockets: 1 + vm_memory: 8192 + vm_net_virtio_bridge: "vmbr140" + when: + - tpl_9331_is_template.rc != 0 + - tpl_9331_exists.rc != 0 + + - name: PROXMOX INIT - TEMPLATES - WAIT for vm_create to finish (lock-aware) + ansible.builtin.shell: qm config 9331 | grep -q '^lock:' + register: tpl_9331_post_create_lock + until: tpl_9331_post_create_lock.rc != 0 + retries: 60 + delay: 5 + failed_when: false + changed_when: false + delegate_to: "{{ inventory_hostname }}-cli" + when: + - tpl_9331_is_template.rc != 0 + - tpl_9331_exists.rc != 0 + +- hosts: proxmox-cli + gather_facts: false + vars_files: + - "../../../../secrets/default_vault.yml" + + tasks: + - name: TEMPLATE 9331 - CHECK if already template (proxmox-cli play) + ansible.builtin.shell: qm config 9331 2>/dev/null | grep -q '^template:' + register: tpl_9331_is_template + failed_when: false + changed_when: false + + - name: PROXMOX INIT - TEMPLATES - IMPORT CLOUD INIT DISK - template-vm-debian-trixie-medium + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "template_cloudinit_import_disk" + proxmox_node: "pve" + cloudinit_image_full_path: "/var/lib/vz/template/iso/debian-13-genericcloud-amd64.img" + vm_id: 9331 + vm_disk_size: "64g" + proxmox_dest_vm_storage_name: "local-lvm" + when: tpl_9331_is_template.rc != 0 + +- hosts: proxmox + gather_facts: false + vars_files: + - "../../../../secrets/default_vault.yml" + + tasks: + - name: TEMPLATE 9331 - CHECK if already template (third play) + ansible.builtin.shell: qm config 9331 2>/dev/null | grep -q '^template:' + register: tpl_9331_is_template + failed_when: false + changed_when: false + delegate_to: "{{ inventory_hostname }}-cli" + + - name: PROXMOX INIT - TEMPLATES - SET CLOUD DEFAULT INIT VARIABLE - template-vm-debian-trixie-medium + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "cloudinit_set_variables" + vm_id: 9331 + vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" + vm_ci_password: "{{ default_trainee_vm_ci_password | default('supersecret') }}" + vm_ci_ssh_key: "{{ default_trainee_vm_ci_ssh_key }}" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" + vm_ci_ip: "192.168.140.2" + vm_ci_netmask: "24" + vm_ci_ip_gw: "192.168.140.1" + when: tpl_9331_is_template.rc != 0 + + - name: PROXMOX INIT - TEMPLATES - SET PROXMOX TAG + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "vm_set_tag" + vm_id: 9331 + vm_tag_name: "template" + when: tpl_9331_is_template.rc != 0 diff --git a/scenarios/test_rocketchat/01_init_proxmox/stage_01-create_templates/templates/debian_trixie/template-vm-debian-trixie-small.yml b/scenarios/test_rocketchat/01_init_proxmox/stage_01-create_templates/templates/debian_trixie/template-vm-debian-trixie-small.yml new file mode 100644 index 00000000..bf774d52 --- /dev/null +++ b/scenarios/test_rocketchat/01_init_proxmox/stage_01-create_templates/templates/debian_trixie/template-vm-debian-trixie-small.yml @@ -0,0 +1,134 @@ +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# +# PROXMOX INIT - create debian_trixie template VM template-vm-debian-trixie-small (id 9321) +# +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- hosts: proxmox + gather_facts: false + vars_files: + - "../../../../secrets/default_vault.yml" + + tasks: + #### IDEMPOTENCE - skip if already finalized as template #### + - name: TEMPLATE 9321 - CHECK if already finalized as template + ansible.builtin.shell: qm config 9321 2>/dev/null | grep -q '^template:' + register: tpl_9321_is_template + failed_when: false + changed_when: false + delegate_to: "{{ inventory_hostname }}-cli" + + - name: TEMPLATE 9321 - WAIT for unlock if a parallel deploy holds the VM + ansible.builtin.shell: qm config 9321 | grep -q '^lock:' + register: tpl_9321_lock + until: tpl_9321_lock.rc != 0 + retries: 30 + delay: 10 + failed_when: false + changed_when: false + delegate_to: "{{ inventory_hostname }}-cli" + when: tpl_9321_is_template.rc != 0 + + - name: TEMPLATE 9321 - SKIP all (already finalized as template) + ansible.builtin.debug: + msg: "Template 9321 is already a template - skipping. Run delete-everything to recreate." + when: tpl_9321_is_template.rc == 0 + + #### vm_create idempotence: separate exists check #### + - name: TEMPLATE 9321 - CHECK if VM exists (vm_create idempotence) + ansible.builtin.shell: qm config 9321 2>/dev/null + register: tpl_9321_exists + failed_when: false + changed_when: false + delegate_to: "{{ inventory_hostname }}-cli" + when: tpl_9321_is_template.rc != 0 + + - name: PROXMOX INIT - TEMPLATES - CREATE TEMPLATE - template-vm-debian-trixie-small + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "vm_create" + vm_id: 9321 + vm_name: "template-vm-debian-trixie-small" + vm_cpu: "host" + vm_cores: 1 + vm_sockets: 1 + vm_memory: 4096 + vm_net_virtio_bridge: "vmbr140" + when: + - tpl_9321_is_template.rc != 0 + - tpl_9321_exists.rc != 0 + + - name: PROXMOX INIT - TEMPLATES - WAIT for vm_create to finish (lock-aware) + ansible.builtin.shell: qm config 9321 | grep -q '^lock:' + register: tpl_9321_post_create_lock + until: tpl_9321_post_create_lock.rc != 0 + retries: 60 + delay: 5 + failed_when: false + changed_when: false + delegate_to: "{{ inventory_hostname }}-cli" + when: + - tpl_9321_is_template.rc != 0 + - tpl_9321_exists.rc != 0 + +- hosts: proxmox-cli + gather_facts: false + vars_files: + - "../../../../secrets/default_vault.yml" + + tasks: + - name: TEMPLATE 9321 - CHECK if already template (proxmox-cli play) + ansible.builtin.shell: qm config 9321 2>/dev/null | grep -q '^template:' + register: tpl_9321_is_template + failed_when: false + changed_when: false + + - name: PROXMOX INIT - TEMPLATES - IMPORT CLOUD INIT DISK - template-vm-debian-trixie-small + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "template_cloudinit_import_disk" + proxmox_node: "pve" + cloudinit_image_full_path: "/var/lib/vz/template/iso/debian-13-genericcloud-amd64.img" + vm_id: 9321 + vm_disk_size: "32g" + proxmox_dest_vm_storage_name: "local-lvm" + when: tpl_9321_is_template.rc != 0 + +- hosts: proxmox + gather_facts: false + vars_files: + - "../../../../secrets/default_vault.yml" + + tasks: + - name: TEMPLATE 9321 - CHECK if already template (third play) + ansible.builtin.shell: qm config 9321 2>/dev/null | grep -q '^template:' + register: tpl_9321_is_template + failed_when: false + changed_when: false + delegate_to: "{{ inventory_hostname }}-cli" + + - name: PROXMOX INIT - TEMPLATES - SET CLOUD DEFAULT INIT VARIABLE - template-vm-debian-trixie-small + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "cloudinit_set_variables" + vm_id: 9321 + vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" + vm_ci_password: "{{ default_trainee_vm_ci_password | default('supersecret') }}" + vm_ci_ssh_key: "{{ default_trainee_vm_ci_ssh_key }}" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" + vm_ci_ip: "192.168.140.3" + vm_ci_netmask: "24" + vm_ci_ip_gw: "192.168.140.1" + when: tpl_9321_is_template.rc != 0 + + - name: PROXMOX INIT - TEMPLATES - SET PROXMOX TAG + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "vm_set_tag" + vm_id: 9321 + vm_tag_name: "template" + when: tpl_9321_is_template.rc != 0 diff --git a/scenarios/test_rocketchat/02_lan1_infrastructure/_main.reinstall.sh b/scenarios/test_rocketchat/02_lan1_infrastructure/_main.reinstall.sh new file mode 100755 index 00000000..87297363 --- /dev/null +++ b/scenarios/test_rocketchat/02_lan1_infrastructure/_main.reinstall.sh @@ -0,0 +1,8 @@ +#!/bin/bash +## +## re-run this section's playbooks (02_lan1_infrastructure) +## + +ansible-playbook -i "${RANGE42_ANSIBLE_ROLES__INVENTORY_DIR}/inventory_default.yml" \ + -l "all" \ + "./_main.yml" --vault-password-file "${RANGE42_VAULT_PASSWORD_FILE:?RANGE42_VAULT_PASSWORD_FILE is not set — run: range42-context use }" diff --git a/scenarios/test_rocketchat/02_lan1_infrastructure/_main.yml b/scenarios/test_rocketchat/02_lan1_infrastructure/_main.yml new file mode 100644 index 00000000..06b51910 --- /dev/null +++ b/scenarios/test_rocketchat/02_lan1_infrastructure/_main.yml @@ -0,0 +1,23 @@ +--- +## +## 02_lan1_infrastructure — generated by r42playbooks +## stage_00 clones VMs (per-VM global_* below); stage_01 installs software. +## + +#### STAGE 00 #### + +- import_playbook: ./stage_00/lan1-admin-rocketchat-00.yml + vars: + global_vm_name: "lan1-admin-rocketchat-00" + global_vm_ssh_name: "r42.lan1-admin-rocketchat-00" + global_vm_id: 1002 + global_vm_description: "admin-rocketchat" + global_vm_tag_name: "lan1" + global_vm_ci_ip: "192.168.150.2" + global_template_vm_id: 9331 + global_template_name: "template-vm-debian-trixie-medium - id 9331" + + +#### STAGE 01 #### + +- import_playbook: ./stage_01/lan1-admin-rocketchat-00.yml diff --git a/scenarios/test_rocketchat/02_lan1_infrastructure/stage_00/lan1-admin-rocketchat-00.yml b/scenarios/test_rocketchat/02_lan1_infrastructure/stage_00/lan1-admin-rocketchat-00.yml new file mode 100644 index 00000000..76295511 --- /dev/null +++ b/scenarios/test_rocketchat/02_lan1_infrastructure/stage_00/lan1-admin-rocketchat-00.yml @@ -0,0 +1,81 @@ +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# +# LAN1 INFRASTRUCTURE INIT - CLONE + CLOUD-INIT +# +# hostname : lan1-admin-rocketchat-00 +# vm_id : 1002 +# ip : 192.168.150.2 +# template : template-vm-debian-trixie-medium +# +# global_* vars are supplied by ../_main.yml (kept here as Jinja references). +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- hosts: proxmox + gather_facts: false + vars_files: + - "../../secrets/default_vault.yml" + + tasks: + - name: "LAN1 INFRASTRUCTURE INIT - {{ global_vm_name }} - CLONE TEMPLATE - {{ global_template_name }}" + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "vm_clone" + vm_id: "{{ global_template_vm_id }}" + vm_new_id: "{{ global_vm_id }}" + vm_name: "{{ global_vm_name }}" + vm_description: "{{ global_vm_description }}" + + - name: "LAN1 INFRASTRUCTURE INIT - {{ global_vm_name }} - WAIT FOR PROXMOX UNLOCK" + ansible.builtin.shell: qm config {{ global_vm_id }} | grep -q '^lock:' + register: clone_lock_check + until: clone_lock_check.rc != 0 + retries: 60 + delay: 5 + failed_when: false + changed_when: false + delegate_to: "{{ inventory_hostname }}-cli" + + - name: "LAN1 INFRASTRUCTURE INIT - {{ global_vm_name }} - SET PROXMOX TAG" + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "vm_set_tag" + vm_id: "{{ global_vm_id }}" + vm_tag_name: "{{ global_vm_tag_name }}" + + - name: "LAN1 INFRASTRUCTURE INIT - {{ global_vm_name }} - SET CLOUD-INIT VARIABLES" + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "cloudinit_set_variables" + vm_id: "{{ global_vm_id }}" + vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" + vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" + vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" + vm_ci_ip: "{{ global_vm_ci_ip }}" + vm_ci_netmask: "24" + vm_ci_ip_gw: "192.168.150.1" + vm_net_virtio_bridge: "vmbr150" + + - name: "LAN1 INFRASTRUCTURE INIT - {{ global_vm_name }} - START VM" + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "vm_start" + vm_id: "{{ global_vm_id }}" + +- hosts: "{{ global_vm_ssh_name }}" + gather_facts: false + vars_files: + - "../../secrets/default_vault.yml" + vars: + ARG_vm_ssh_name: "{{ global_vm_ssh_name }}" + requested_tasks: + - wait/openssh_server/is_reachable.yml + - wait/cloudinit/is_boot_finished.yml + tasks: + - name: "LAN1 INFRASTRUCTURE INIT - {{ global_vm_name }} - WAIT FOR SSH + CLOUD-INIT" + include_role: + name: ansible.utils diff --git a/scenarios/test_rocketchat/02_lan1_infrastructure/stage_01/lan1-admin-rocketchat-00.devkit/test_rocketchat.lan1-admin-rocketchat-00.install.sh b/scenarios/test_rocketchat/02_lan1_infrastructure/stage_01/lan1-admin-rocketchat-00.devkit/test_rocketchat.lan1-admin-rocketchat-00.install.sh new file mode 100755 index 00000000..6726b6e6 --- /dev/null +++ b/scenarios/test_rocketchat/02_lan1_infrastructure/stage_01/lan1-admin-rocketchat-00.devkit/test_rocketchat.lan1-admin-rocketchat-00.install.sh @@ -0,0 +1,8 @@ +#!/bin/bash +## +## reinstall software on lan1-admin-rocketchat-00 only (fast single-VM iteration) +## + +ansible-playbook -i "${RANGE42_ANSIBLE_ROLES__INVENTORY_DIR}/inventory_default.yml" \ + -l "all" \ + "../lan1-admin-rocketchat-00.yml" --vault-password-file "${RANGE42_VAULT_PASSWORD_FILE:?RANGE42_VAULT_PASSWORD_FILE is not set — run: range42-context use }" diff --git a/scenarios/test_rocketchat/02_lan1_infrastructure/stage_01/lan1-admin-rocketchat-00.devkit/test_rocketchat.lan1-admin-rocketchat-00.revert.sh b/scenarios/test_rocketchat/02_lan1_infrastructure/stage_01/lan1-admin-rocketchat-00.devkit/test_rocketchat.lan1-admin-rocketchat-00.revert.sh new file mode 100755 index 00000000..cbb56940 --- /dev/null +++ b/scenarios/test_rocketchat/02_lan1_infrastructure/stage_01/lan1-admin-rocketchat-00.devkit/test_rocketchat.lan1-admin-rocketchat-00.revert.sh @@ -0,0 +1,8 @@ +#!/bin/bash +## +## revert lan1-admin-rocketchat-00 to its latest snapshot and restart it +## + +VM_ID=$(devkit_manifest.find_vm_id.to.text.sh "$0" "lan1-admin-rocketchat-00") || exit 1 +echo "{\"proxmox_node\":\"pve\",\"vm_id\":${VM_ID} }" | proxmox_snapshot_vm.vm_id.revert_snapshot.to.jsons.sh +echo "{\"proxmox_node\":\"pve\",\"vm_id\":${VM_ID} }" | proxmox_vm.vm_id.start.to.jsons.sh diff --git a/scenarios/test_rocketchat/02_lan1_infrastructure/stage_01/lan1-admin-rocketchat-00.devkit/test_rocketchat.lan1-admin-rocketchat-00.snapshot.sh b/scenarios/test_rocketchat/02_lan1_infrastructure/stage_01/lan1-admin-rocketchat-00.devkit/test_rocketchat.lan1-admin-rocketchat-00.snapshot.sh new file mode 100755 index 00000000..d2649e6b --- /dev/null +++ b/scenarios/test_rocketchat/02_lan1_infrastructure/stage_01/lan1-admin-rocketchat-00.devkit/test_rocketchat.lan1-admin-rocketchat-00.snapshot.sh @@ -0,0 +1,7 @@ +#!/bin/bash +## +## snapshot lan1-admin-rocketchat-00 as "base" +## + +VM_ID=$(devkit_manifest.find_vm_id.to.text.sh "$0" "lan1-admin-rocketchat-00") || exit 1 +echo "{\"proxmox_node\":\"pve\",\"vm_id\":${VM_ID},\"vm_snapshot_description\":\"base\"}" | proxmox_snapshot_vm.vm_id.create_snapshot.to.jsons.sh diff --git a/scenarios/test_rocketchat/02_lan1_infrastructure/stage_01/lan1-admin-rocketchat-00.yml b/scenarios/test_rocketchat/02_lan1_infrastructure/stage_01/lan1-admin-rocketchat-00.yml new file mode 100644 index 00000000..04c322f7 --- /dev/null +++ b/scenarios/test_rocketchat/02_lan1_infrastructure/stage_01/lan1-admin-rocketchat-00.yml @@ -0,0 +1,43 @@ +## +## stage_01 — software install for lan1-admin-rocketchat-00 +## catalog roles are referenced BY NAME (resolved via ANSIBLE_ROLES_PATH at deploy); +## one play per attachment so each carries its own params (firewall_rules, docker …). +## + +- name: "lan1-admin-rocketchat-00 — software.configure.firewalls" + hosts: r42.lan1-admin-rocketchat-00 + become: true + vars_files: + - "../../secrets/default_vault.yml" + vars: + firewall_rules: + - ip: all + port: 22 + protocol: tcp + - ip: all + port: 3000 + protocol: tcp + roles: + - software.configure.firewalls + +- name: "lan1-admin-rocketchat-00 — software.install.warmup.basic_packages" + hosts: r42.lan1-admin-rocketchat-00 + become: true + vars_files: + - "../../secrets/default_vault.yml" + vars: + INSTALL_PACKAGES_BASICS: 'YES' + INSTALL_PACKAGES_DOCKER: 'YES' + INSTALL_PACKAGES_DOCKER_COMPOSE: 'YES' + INSTALL_PACKAGES_UTILS_JSON: 'YES' + INSTALL_PACKAGES_UTILS_NETWORK: 'YES' + roles: + - software.install.warmup.basic_packages + +- name: "lan1-admin-rocketchat-00 — software.install.rocketchat" + hosts: r42.lan1-admin-rocketchat-00 + become: true + vars_files: + - "../../secrets/default_vault.yml" + roles: + - software.install.rocketchat diff --git a/scenarios/test_rocketchat/03_lan2_infrastructure/_main.reinstall.sh b/scenarios/test_rocketchat/03_lan2_infrastructure/_main.reinstall.sh new file mode 100755 index 00000000..56d65a34 --- /dev/null +++ b/scenarios/test_rocketchat/03_lan2_infrastructure/_main.reinstall.sh @@ -0,0 +1,8 @@ +#!/bin/bash +## +## re-run this section's playbooks (03_lan2_infrastructure) +## + +ansible-playbook -i "${RANGE42_ANSIBLE_ROLES__INVENTORY_DIR}/inventory_default.yml" \ + -l "all" \ + "./_main.yml" --vault-password-file "${RANGE42_VAULT_PASSWORD_FILE:?RANGE42_VAULT_PASSWORD_FILE is not set — run: range42-context use }" diff --git a/scenarios/test_rocketchat/03_lan2_infrastructure/_main.yml b/scenarios/test_rocketchat/03_lan2_infrastructure/_main.yml new file mode 100644 index 00000000..2f82659a --- /dev/null +++ b/scenarios/test_rocketchat/03_lan2_infrastructure/_main.yml @@ -0,0 +1,23 @@ +--- +## +## 03_lan2_infrastructure — generated by r42playbooks +## stage_00 clones VMs (per-VM global_* below); stage_01 installs software. +## + +#### STAGE 00 #### + +- import_playbook: ./stage_00/lan2-debian-jump-00.yml + vars: + global_vm_name: "lan2-debian-jump-00" + global_vm_ssh_name: "r42.lan2-debian-jump-00" + global_vm_id: 3002 + global_vm_description: "debian-jump" + global_vm_tag_name: "lan2" + global_vm_ci_ip: "192.168.151.2" + global_template_vm_id: 9321 + global_template_name: "template-vm-debian-trixie-small - id 9321" + + +#### STAGE 01 #### + +- import_playbook: ./stage_01/lan2-debian-jump-00.yml diff --git a/scenarios/test_rocketchat/03_lan2_infrastructure/stage_00/lan2-debian-jump-00.yml b/scenarios/test_rocketchat/03_lan2_infrastructure/stage_00/lan2-debian-jump-00.yml new file mode 100644 index 00000000..32d7db5e --- /dev/null +++ b/scenarios/test_rocketchat/03_lan2_infrastructure/stage_00/lan2-debian-jump-00.yml @@ -0,0 +1,81 @@ +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# +# LAN2 INFRASTRUCTURE INIT - CLONE + CLOUD-INIT +# +# hostname : lan2-debian-jump-00 +# vm_id : 3002 +# ip : 192.168.151.2 +# template : template-vm-debian-trixie-small +# +# global_* vars are supplied by ../_main.yml (kept here as Jinja references). +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +- hosts: proxmox + gather_facts: false + vars_files: + - "../../secrets/default_vault.yml" + + tasks: + - name: "LAN2 INFRASTRUCTURE INIT - {{ global_vm_name }} - CLONE TEMPLATE - {{ global_template_name }}" + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "vm_clone" + vm_id: "{{ global_template_vm_id }}" + vm_new_id: "{{ global_vm_id }}" + vm_name: "{{ global_vm_name }}" + vm_description: "{{ global_vm_description }}" + + - name: "LAN2 INFRASTRUCTURE INIT - {{ global_vm_name }} - WAIT FOR PROXMOX UNLOCK" + ansible.builtin.shell: qm config {{ global_vm_id }} | grep -q '^lock:' + register: clone_lock_check + until: clone_lock_check.rc != 0 + retries: 60 + delay: 5 + failed_when: false + changed_when: false + delegate_to: "{{ inventory_hostname }}-cli" + + - name: "LAN2 INFRASTRUCTURE INIT - {{ global_vm_name }} - SET PROXMOX TAG" + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "vm_set_tag" + vm_id: "{{ global_vm_id }}" + vm_tag_name: "{{ global_vm_tag_name }}" + + - name: "LAN2 INFRASTRUCTURE INIT - {{ global_vm_name }} - SET CLOUD-INIT VARIABLES" + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "cloudinit_set_variables" + vm_id: "{{ global_vm_id }}" + vm_ci_user: "{{ default_admin_vm_ci_user | default('alice') }}" + vm_ci_password: "{{ default_admin_vm_ci_password | default('supersecret') }}" + vm_ci_ssh_key: "{{ default_admin_vm_ci_ssh_key }}" + vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" + vm_ci_ip: "{{ global_vm_ci_ip }}" + vm_ci_netmask: "24" + vm_ci_ip_gw: "192.168.151.1" + vm_net_virtio_bridge: "vmbr151" + + - name: "LAN2 INFRASTRUCTURE INIT - {{ global_vm_name }} - START VM" + include_role: + name: range42-ansible_roles-proxmox_controller + vars: + proxmox_vm_action: "vm_start" + vm_id: "{{ global_vm_id }}" + +- hosts: "{{ global_vm_ssh_name }}" + gather_facts: false + vars_files: + - "../../secrets/default_vault.yml" + vars: + ARG_vm_ssh_name: "{{ global_vm_ssh_name }}" + requested_tasks: + - wait/openssh_server/is_reachable.yml + - wait/cloudinit/is_boot_finished.yml + tasks: + - name: "LAN2 INFRASTRUCTURE INIT - {{ global_vm_name }} - WAIT FOR SSH + CLOUD-INIT" + include_role: + name: ansible.utils diff --git a/scenarios/test_rocketchat/03_lan2_infrastructure/stage_01/lan2-debian-jump-00.devkit/test_rocketchat.lan2-debian-jump-00.install.sh b/scenarios/test_rocketchat/03_lan2_infrastructure/stage_01/lan2-debian-jump-00.devkit/test_rocketchat.lan2-debian-jump-00.install.sh new file mode 100755 index 00000000..01989c0c --- /dev/null +++ b/scenarios/test_rocketchat/03_lan2_infrastructure/stage_01/lan2-debian-jump-00.devkit/test_rocketchat.lan2-debian-jump-00.install.sh @@ -0,0 +1,8 @@ +#!/bin/bash +## +## reinstall software on lan2-debian-jump-00 only (fast single-VM iteration) +## + +ansible-playbook -i "${RANGE42_ANSIBLE_ROLES__INVENTORY_DIR}/inventory_default.yml" \ + -l "all" \ + "../lan2-debian-jump-00.yml" --vault-password-file "${RANGE42_VAULT_PASSWORD_FILE:?RANGE42_VAULT_PASSWORD_FILE is not set — run: range42-context use }" diff --git a/scenarios/test_rocketchat/03_lan2_infrastructure/stage_01/lan2-debian-jump-00.devkit/test_rocketchat.lan2-debian-jump-00.revert.sh b/scenarios/test_rocketchat/03_lan2_infrastructure/stage_01/lan2-debian-jump-00.devkit/test_rocketchat.lan2-debian-jump-00.revert.sh new file mode 100755 index 00000000..6caf2355 --- /dev/null +++ b/scenarios/test_rocketchat/03_lan2_infrastructure/stage_01/lan2-debian-jump-00.devkit/test_rocketchat.lan2-debian-jump-00.revert.sh @@ -0,0 +1,8 @@ +#!/bin/bash +## +## revert lan2-debian-jump-00 to its latest snapshot and restart it +## + +VM_ID=$(devkit_manifest.find_vm_id.to.text.sh "$0" "lan2-debian-jump-00") || exit 1 +echo "{\"proxmox_node\":\"pve\",\"vm_id\":${VM_ID} }" | proxmox_snapshot_vm.vm_id.revert_snapshot.to.jsons.sh +echo "{\"proxmox_node\":\"pve\",\"vm_id\":${VM_ID} }" | proxmox_vm.vm_id.start.to.jsons.sh diff --git a/scenarios/test_rocketchat/03_lan2_infrastructure/stage_01/lan2-debian-jump-00.devkit/test_rocketchat.lan2-debian-jump-00.snapshot.sh b/scenarios/test_rocketchat/03_lan2_infrastructure/stage_01/lan2-debian-jump-00.devkit/test_rocketchat.lan2-debian-jump-00.snapshot.sh new file mode 100755 index 00000000..71fb0c9a --- /dev/null +++ b/scenarios/test_rocketchat/03_lan2_infrastructure/stage_01/lan2-debian-jump-00.devkit/test_rocketchat.lan2-debian-jump-00.snapshot.sh @@ -0,0 +1,7 @@ +#!/bin/bash +## +## snapshot lan2-debian-jump-00 as "base" +## + +VM_ID=$(devkit_manifest.find_vm_id.to.text.sh "$0" "lan2-debian-jump-00") || exit 1 +echo "{\"proxmox_node\":\"pve\",\"vm_id\":${VM_ID},\"vm_snapshot_description\":\"base\"}" | proxmox_snapshot_vm.vm_id.create_snapshot.to.jsons.sh diff --git a/scenarios/test_rocketchat/03_lan2_infrastructure/stage_01/lan2-debian-jump-00.yml b/scenarios/test_rocketchat/03_lan2_infrastructure/stage_01/lan2-debian-jump-00.yml new file mode 100644 index 00000000..396c7216 --- /dev/null +++ b/scenarios/test_rocketchat/03_lan2_infrastructure/stage_01/lan2-debian-jump-00.yml @@ -0,0 +1,26 @@ +## +## stage_01 — software install for lan2-debian-jump-00 +## catalog roles are referenced BY NAME (resolved via ANSIBLE_ROLES_PATH at deploy); +## one play per attachment so each carries its own params (firewall_rules, docker …). +## + +- name: "lan2-debian-jump-00 — software.configure.firewalls" + hosts: r42.lan2-debian-jump-00 + become: true + vars_files: + - "../../secrets/default_vault.yml" + vars: + firewall_rules: + - ip: all + port: 22 + protocol: tcp + roles: + - software.configure.firewalls + +- name: "lan2-debian-jump-00 — software.install.warmup.basic_packages" + hosts: r42.lan2-debian-jump-00 + become: true + vars_files: + - "../../secrets/default_vault.yml" + roles: + - software.install.warmup.basic_packages diff --git a/scenarios/test_rocketchat/README.md b/scenarios/test_rocketchat/README.md new file mode 100644 index 00000000..73a8e9d1 --- /dev/null +++ b/scenarios/test_rocketchat/README.md @@ -0,0 +1,22 @@ +# test_rocketchat + +Generated by **r42playbooks**. + +- subnet layout: `dual-lan` + +## Boxes + +| box | subnet | image | vm_id | ip | +|---|---|---|---|---| +| `lan1-admin-rocketchat-00` | lan1 | debian_trixie | 1002 | 192.168.150.2 | +| `lan2-debian-jump-00` | lan2 | debian_trixie | 3002 | 192.168.151.2 | + +## Deploy + +```bash +range42-context use test_rocketchat # creates secrets/ symlink + vault +./test_rocketchat.setup.sh # templates + VMs + software +``` + +The composition is reproduced in `scenario.r42.yml` — re-run +`r42playbooks new --spec scenario.r42.yml` to regenerate this tree. diff --git a/scenarios/test_rocketchat/_activate.sh b/scenarios/test_rocketchat/_activate.sh new file mode 100755 index 00000000..fbdff157 --- /dev/null +++ b/scenarios/test_rocketchat/_activate.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +# deprecation algo issue with paramiko and ubuntu 24.04.2 LTS +# https://github.com/paramiko/paramiko/issues/2419 + +set -euo pipefail + +VIRTUAL_ENV_DIR="$HOME/ansible_fix/venv" + +if [ ! -d "$VIRTUAL_ENV_DIR" ]; then + + mkdir -p "$HOME/ansible_fix" + python3 -m venv "$VIRTUAL_ENV_DIR" + + # shellcheck disable=SC1091 + source "$VIRTUAL_ENV_DIR/bin/activate" + + pip install --upgrade pip setuptools wheel + pip install ansible paramiko cryptography +fi + +# shellcheck disable=SC1091 +source "$VIRTUAL_ENV_DIR/bin/activate" + +echo "source \"$VIRTUAL_ENV_DIR/bin/activate\"" diff --git a/scenarios/test_rocketchat/devkit_ansible.show_ansible_inventory.to.text.sh b/scenarios/test_rocketchat/devkit_ansible.show_ansible_inventory.to.text.sh new file mode 100755 index 00000000..29f83a48 --- /dev/null +++ b/scenarios/test_rocketchat/devkit_ansible.show_ansible_inventory.to.text.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +ansible-inventory -i "${RANGE42_ANSIBLE_ROLES__INVENTORY_DIR}/inventory_default.yml" --graph diff --git a/scenarios/test_rocketchat/main.yml b/scenarios/test_rocketchat/main.yml new file mode 100644 index 00000000..6e15947e --- /dev/null +++ b/scenarios/test_rocketchat/main.yml @@ -0,0 +1,9 @@ +--- +## +## test_rocketchat — generated by r42playbooks +## + +- import_playbook: ./01_init_proxmox/_main.yml + +- import_playbook: ./02_lan1_infrastructure/_main.yml +- import_playbook: ./03_lan2_infrastructure/_main.yml diff --git a/scenarios/test_rocketchat/main_vms_only.yml b/scenarios/test_rocketchat/main_vms_only.yml new file mode 100644 index 00000000..618bb4f2 --- /dev/null +++ b/scenarios/test_rocketchat/main_vms_only.yml @@ -0,0 +1,7 @@ +--- +## +## test_rocketchat — deploy VMs only (templates already exist; skip 01_init_proxmox) +## + +- import_playbook: ./02_lan1_infrastructure/_main.yml +- import_playbook: ./03_lan2_infrastructure/_main.yml diff --git a/scenarios/test_rocketchat/manifest/scenario_vms.json b/scenarios/test_rocketchat/manifest/scenario_vms.json new file mode 100644 index 00000000..35d44a27 --- /dev/null +++ b/scenarios/test_rocketchat/manifest/scenario_vms.json @@ -0,0 +1,41 @@ +{ + "description": "", + "scenario": "test_rocketchat", + "templates": [ + { + "bridge": "vmbr140", + "image": "debian_trixie", + "ip": "192.168.140.3", + "spec": "1cpu/4gb/32gb", + "vm_id": 9321, + "vm_name": "template-vm-debian-trixie-small" + }, + { + "bridge": "vmbr140", + "image": "debian_trixie", + "ip": "192.168.140.2", + "spec": "2cpu/8gb/64gb", + "vm_id": 9331, + "vm_name": "template-vm-debian-trixie-medium" + } + ], + "version": 2, + "vms": [ + { + "bridge": "vmbr150", + "image": "debian_trixie", + "ip": "192.168.150.2", + "subnet": "lan1", + "vm_id": 1002, + "vm_name": "lan1-admin-rocketchat-00" + }, + { + "bridge": "vmbr151", + "image": "debian_trixie", + "ip": "192.168.151.2", + "subnet": "lan2", + "vm_id": 3002, + "vm_name": "lan2-debian-jump-00" + } + ] +} diff --git a/scenarios/test_rocketchat/scenario.r42.yml b/scenarios/test_rocketchat/scenario.r42.yml new file mode 100644 index 00000000..a2ef840c --- /dev/null +++ b/scenarios/test_rocketchat/scenario.r42.yml @@ -0,0 +1,22 @@ +boxes: +- attachments_add: [] + count: 1 + octet: 2 + subnet: lan1 + template: admin-rocketchat + template_vm_id: null + vars: {} +- attachments_add: [] + count: 1 + octet: 2 + subnet: lan2 + template: debian-jump + template_vm_id: null + vars: {} +name: test_rocketchat +network_policy: null +notes: '' +proxmox_node: null +schema_version: 1 +services: null +subnet_layout: dual-lan diff --git a/scenarios/test_rocketchat/templates/ansible-inventory.j2 b/scenarios/test_rocketchat/templates/ansible-inventory.j2 new file mode 100644 index 00000000..a8c1743c --- /dev/null +++ b/scenarios/test_rocketchat/templates/ansible-inventory.j2 @@ -0,0 +1,22 @@ +all: + children: + range42_infrastructure: + children: + r42_lan1_group: + hosts: + r42.lan1-admin-rocketchat-00: + + r42_lan2_group: + hosts: + r42.lan2-debian-jump-00: + + proxmox: + hosts: + {{ INFRASTRUCTURE_CODENAME }}: + ansible_host: {{ INFRASTRUCTURE_PROXMOX_ADDRESS | mandatory }}:8006 + ansible_connection: local + ansible_python_interpreter: /usr/bin/python3 + + proxmox-cli: + hosts: + {{ INFRASTRUCTURE_CODENAME }}-cli: diff --git a/scenarios/test_rocketchat/templates/ansible-vars.yml b/scenarios/test_rocketchat/templates/ansible-vars.yml new file mode 100644 index 00000000..a8cbca34 --- /dev/null +++ b/scenarios/test_rocketchat/templates/ansible-vars.yml @@ -0,0 +1,32 @@ +--- +################################################################################ +# range42 — scenario-specific variables (test_rocketchat) +# +# Generated by r42playbooks. Shared variables -> group_vars/all/vars.yml, +# secrets -> vault (see vault-example.yml). +################################################################################ + + +#### SCENARIO IDENTIFICATION #### + +INFRASTRUCTURE_SCENARIO: "test_rocketchat" + + +#### CREDENTIAL GENERATION #### + +context_auto_generate_ssh_keys: "YES" +context_auto_generate_vm_passwords: "YES" + +student_additionnal_keys_count: 5 + + +#### CLOUD-INIT USERNAMES #### + +default_admin_vm_ci_user: "alice" +default_trainee_vm_ci_user: "bob" + + +#### CLOUD-INIT DNS #### +# default resolver for lab VMs; range42-init.py substitutes the Proxmox node +# resolver here at workspace creation. 1.1.1.1 is the portable fallback. +default_vm_ci_dns_ips: "1.1.1.1" diff --git a/scenarios/test_rocketchat/templates/ssh-config.j2 b/scenarios/test_rocketchat/templates/ssh-config.j2 new file mode 100644 index 00000000..710fdb97 --- /dev/null +++ b/scenarios/test_rocketchat/templates/ssh-config.j2 @@ -0,0 +1,38 @@ +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### +# infrastructure code name : {{ INFRASTRUCTURE_CODENAME }} +# infrastructure proxmox address : {{ INFRASTRUCTURE_PROXMOX_ADDRESS }} +# scenario : {{ INFRASTRUCTURE_SCENARIO }} +#### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### #### + +#### PROXMOX WEB UI (port forward) #### +Host px.{{ INFRASTRUCTURE_CODENAME }}.redirect_www.proxmox + RequestTTY no + RemoteCommand none + localforward localhost:18042 {{ INFRASTRUCTURE_PROXMOX_ADDRESS | mandatory }}:8006 + +#### PX ROOT USER SSH ACCESS #### +Host px.{{ INFRASTRUCTURE_CODENAME }}-ssh_cli.root {{ INFRASTRUCTURE_CODENAME }}-cli + Hostname {{ INFRASTRUCTURE_PROXMOX_ADDRESS | mandatory }} + User root + IdentityFile {{ DEPLOYER_CLI__DST_SSH_KEYS_JUMP_DEST_DIR }}/px.{{ INFRASTRUCTURE_CODENAME }}-{{ INFRASTRUCTURE_SCENARIO }}-ssh_cli.root + Port 22 + +#### SSH JUMPER #### +Host px.{{ INFRASTRUCTURE_CODENAME }}.jumper + Hostname {{ INFRASTRUCTURE_PROXMOX_ADDRESS | mandatory }} + User jump_user + IdentityFile {{ DEPLOYER_CLI__DST_SSH_KEYS_JUMP_DEST_DIR }}/px.{{ INFRASTRUCTURE_CODENAME }}-{{ INFRASTRUCTURE_SCENARIO }}-ssh_cli.jump_user + Port 22 + +#### SCENARIO VMs #### +Host r42.lan1-admin-rocketchat-00 + Hostname 192.168.150.2 + +Host r42.lan2-debian-jump-00 + Hostname 192.168.151.2 + +Host r42.* + User alice + IdentityFile {{ DEPLOYER_CLI__DST_SSH_KEYS_BACKEND_DEST_DIR }}/r42.{{ INFRASTRUCTURE_CODENAME }}-{{ INFRASTRUCTURE_SCENARIO }}-deployer-key_alice + Port 22 + ProxyJump px.{{ INFRASTRUCTURE_CODENAME }}.jumper diff --git a/scenarios/test_rocketchat/templates/vault-example.yml b/scenarios/test_rocketchat/templates/vault-example.yml new file mode 100644 index 00000000..d36ba359 --- /dev/null +++ b/scenarios/test_rocketchat/templates/vault-example.yml @@ -0,0 +1,37 @@ +--- +################################################################################ +# range42 — vault secrets (test_rocketchat) +# +# Copy to vault.yml, fill real values, then: ansible-vault encrypt vault.yml +# Never commit the unencrypted version. +################################################################################ + + +#### PROXMOX API SECRET #### +proxmox_api_token_secret: "REPLACE_ME" + + +#### JUMP HOST #### +jump_password: "REPLACE_ME" + + +#### CLOUD-INIT PASSWORDS #### +default_admin_vm_ci_password: "REPLACE_ME" +default_admin_vm_ci_ssh_key: "ssh-ed25519 AAAA... alice CODENAME-SCENARIO" +default_trainee_vm_ci_password: "REPLACE_ME" +default_trainee_vm_ci_ssh_key: "ssh-ed25519 AAAA... bob CODENAME-SCENARIO" + + +#### CLOUD-INIT DNS #### +# default resolver for lab VMs; range42-init.py substitutes the Proxmox node +# resolver here at workspace creation. 1.1.1.1 is the portable fallback. +default_vm_ci_dns_ips: "1.1.1.1" + + +#### TAILSCALE #### +infrastructure_tailscale_authkey: "tskey-auth-REPLACE_ME" +infrastructure_tailscale_apikey: "tskey-api-REPLACE_ME" + + +#### MISC #### +deployer_cli_user_ssh_known_hosts: "/home/your_deployer_cli_username/.ssh/known_hosts" diff --git a/scenarios/test_rocketchat/test_rocketchat.delete_all.sh b/scenarios/test_rocketchat/test_rocketchat.delete_all.sh new file mode 100755 index 00000000..b2d0b680 --- /dev/null +++ b/scenarios/test_rocketchat/test_rocketchat.delete_all.sh @@ -0,0 +1,39 @@ +#!/bin/bash + +## +## delete all — VMs + this scenario's ubuntu_noble templates +## +## ⚠ templates (9xxx) are shared across scenarios on the same Proxmox. Run this +## only when no other scenario relies on them, or use delete_vms_only.sh instead. +## + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +MANIFEST="$SCRIPT_DIR/manifest/scenario_vms.json" + +if [[ ! -f "$MANIFEST" ]]; then + echo "ERROR: manifest not found: $MANIFEST" >&2 + exit 1 +fi + +mapfile -t SCENARIO_VM_IDS < <(jq -r '.vms[].vm_id' "$MANIFEST") +mapfile -t TEMPLATE_VM_IDS < <(jq -r '.templates[].vm_id' "$MANIFEST") +mapfile -t INFRASTRUCTURE_IP < <(jq -r '.vms[].ip' "$MANIFEST") + +ALL_IDS=("${SCENARIO_VM_IDS[@]}" "${TEMPLATE_VM_IDS[@]}") +ID_REGEX=$(printf '|%s' "${ALL_IDS[@]}" | sed 's/^|//') + +echo ":: stopping and deleting VMs + templates" +VM_LIST_JSON=$(proxmox_vm.list.to.jsons.sh 2>&1 | grep '"vm_id":[0-9]') +if [ -z "$VM_LIST_JSON" ]; then + echo "ERROR: proxmox_vm.list.to.jsons.sh returned no VM data — aborting" >&2 + exit 1 +fi +echo "$VM_LIST_JSON" | jq -c | grep -E "\"vm_id\":($ID_REGEX)([^0-9]|$)" | proxmox_vm.vm_id.stop_force.to.jsons.sh +echo "$VM_LIST_JSON" | jq -c | grep -E "\"vm_id\":($ID_REGEX)([^0-9]|$)" | proxmox_vm.vm_id.delete.to.jsons.sh + +for ip in "${INFRASTRUCTURE_IP[@]}"; do + echo ":: REMOVE SSH KEY FOR : $ip" + ssh-keygen -f "$HOME/.ssh/known_hosts" -R "$ip" +done + +echo ":: done — VMs and templates removed" diff --git a/scenarios/test_rocketchat/test_rocketchat.delete_vms_only.sh b/scenarios/test_rocketchat/test_rocketchat.delete_vms_only.sh new file mode 100755 index 00000000..ba2443a3 --- /dev/null +++ b/scenarios/test_rocketchat/test_rocketchat.delete_vms_only.sh @@ -0,0 +1,33 @@ +#!/bin/bash + +## +## delete VMs only — scenario VMs (filter by vm_id), keep templates +## + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +MANIFEST="$SCRIPT_DIR/manifest/scenario_vms.json" + +if [[ ! -f "$MANIFEST" ]]; then + echo "ERROR: manifest not found: $MANIFEST" >&2 + exit 1 +fi + +mapfile -t SCENARIO_VM_IDS < <(jq -r '.vms[].vm_id' "$MANIFEST") +mapfile -t INFRASTRUCTURE_IP < <(jq -r '.vms[].ip' "$MANIFEST") +ID_REGEX=$(printf '|%s' "${SCENARIO_VM_IDS[@]}" | sed 's/^|//') + +echo ":: stopping and deleting scenario VMs (keeping templates)..." +VM_LIST_JSON=$(proxmox_vm.list.to.jsons.sh 2>&1 | grep '"vm_id":[0-9]') +if [ -z "$VM_LIST_JSON" ]; then + echo "ERROR: proxmox_vm.list.to.jsons.sh returned no VM data — aborting" >&2 + exit 1 +fi +echo "$VM_LIST_JSON" | jq -c | grep -E "\"vm_id\":($ID_REGEX)([^0-9]|$)" | proxmox_vm.vm_id.stop_force.to.jsons.sh +echo "$VM_LIST_JSON" | jq -c | grep -E "\"vm_id\":($ID_REGEX)([^0-9]|$)" | proxmox_vm.vm_id.delete.to.jsons.sh + +for ip in "${INFRASTRUCTURE_IP[@]}"; do + echo ":: REMOVE SSH KEY FOR : $ip" + ssh-keygen -f "$HOME/.ssh/known_hosts" -R "$ip" +done + +echo ":: done — templates preserved" diff --git a/scenarios/test_rocketchat/test_rocketchat.reset.setup.sh b/scenarios/test_rocketchat/test_rocketchat.reset.setup.sh new file mode 100755 index 00000000..b70309e0 --- /dev/null +++ b/scenarios/test_rocketchat/test_rocketchat.reset.setup.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +## +## reset — delete this scenario's VMs (from manifest) then re-deploy +## + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +MANIFEST="$SCRIPT_DIR/manifest/scenario_vms.json" + +if [[ ! -f "$MANIFEST" ]]; then + echo "ERROR: manifest not found: $MANIFEST" >&2 + exit 1 +fi + +mapfile -t SCENARIO_VM_IDS < <(jq -r '.vms[].vm_id' "$MANIFEST") +mapfile -t INFRASTRUCTURE_IP < <(jq -r '.vms[].ip' "$MANIFEST") +ID_REGEX=$(printf '|%s' "${SCENARIO_VM_IDS[@]}" | sed 's/^|//') + +echo ":: stopping and deleting scenario VMs (vm_ids: ${SCENARIO_VM_IDS[*]})..." +VM_LIST_JSON=$(proxmox_vm.list.to.jsons.sh 2>&1 | grep '"vm_id":[0-9]') +if [ -z "$VM_LIST_JSON" ]; then + echo "ERROR: proxmox_vm.list.to.jsons.sh returned no VM data — aborting" >&2 + exit 1 +fi +echo "$VM_LIST_JSON" | jq -c | grep -E "\"vm_id\":($ID_REGEX)([^0-9]|$)" | proxmox_vm.vm_id.stop_force.to.jsons.sh +echo "$VM_LIST_JSON" | jq -c | grep -E "\"vm_id\":($ID_REGEX)([^0-9]|$)" | proxmox_vm.vm_id.delete.to.jsons.sh + +for ip in "${INFRASTRUCTURE_IP[@]}"; do + echo ":: REMOVE SSH KEY FOR : $ip" + ssh-keygen -f "$HOME/.ssh/known_hosts" -R "$ip" +done + +ansible-playbook -i "${RANGE42_ANSIBLE_ROLES__INVENTORY_DIR}/inventory_default.yml" \ + -l "all" \ + "./main.yml" --vault-password-file "${RANGE42_VAULT_PASSWORD_FILE:?RANGE42_VAULT_PASSWORD_FILE is not set — run: range42-context use }" diff --git a/scenarios/test_rocketchat/test_rocketchat.reset.ssh_keys.sh b/scenarios/test_rocketchat/test_rocketchat.reset.ssh_keys.sh new file mode 100755 index 00000000..bea1d07b --- /dev/null +++ b/scenarios/test_rocketchat/test_rocketchat.reset.ssh_keys.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +## +## remove known_hosts entries for every VM IP in this scenario (from manifest) +## + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +MANIFEST="$SCRIPT_DIR/manifest/scenario_vms.json" + +if [[ ! -f "$MANIFEST" ]]; then + echo "ERROR: manifest not found: $MANIFEST" >&2 + exit 1 +fi + +mapfile -t INFRASTRUCTURE_IP < <(jq -r '.vms[].ip' "$MANIFEST") + +for ip in "${INFRASTRUCTURE_IP[@]}"; do + echo ":: REMOVE SSH KEY FOR : $ip" + ssh-keygen -f "$HOME/.ssh/known_hosts" -R "$ip" +done diff --git a/scenarios/test_rocketchat/test_rocketchat.setup.sh b/scenarios/test_rocketchat/test_rocketchat.setup.sh new file mode 100755 index 00000000..aff48748 --- /dev/null +++ b/scenarios/test_rocketchat/test_rocketchat.setup.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +ansible-playbook -i "${RANGE42_ANSIBLE_ROLES__INVENTORY_DIR}/inventory_default.yml" \ + -l "all" \ + "./main.yml" --vault-password-file "${RANGE42_VAULT_PASSWORD_FILE:?RANGE42_VAULT_PASSWORD_FILE is not set — run: range42-context use }" diff --git a/scenarios/test_rocketchat/test_rocketchat.setup_vms_only.sh b/scenarios/test_rocketchat/test_rocketchat.setup_vms_only.sh new file mode 100755 index 00000000..f7f81aa4 --- /dev/null +++ b/scenarios/test_rocketchat/test_rocketchat.setup_vms_only.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +## +## deploy VMs only — skip template download and creation +## faster redeploy when templates already exist on proxmox +## + +ansible-playbook -i "${RANGE42_ANSIBLE_ROLES__INVENTORY_DIR}/inventory_default.yml" \ + -l "all" \ + "./main_vms_only.yml" --vault-password-file "${RANGE42_VAULT_PASSWORD_FILE:?RANGE42_VAULT_PASSWORD_FILE is not set — run: range42-context use }" diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..b945d805 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,290 @@ +"""Shared test fixtures for r42playbooks.""" + +import copy + +import pytest + + +@pytest.fixture +def valid_topology_dict() -> dict: + """A minimal, valid topology spec (admin + ctf zones, one box each).""" + return { + "schema_version": 1, + "scenario": "demo_lab_network", + "description": "test topology", + "proxmox_node": "px-testing", + "subnets": [ + { + "name": "admin", + "cidr": "192.168.142.0/24", + "bridge": "vmbr142", + "gateway": "192.168.142.1", + }, + {"name": "ctf", "cidr": "192.168.144.0/24", "bridge": "vmbr144"}, + ], + "zones": [ + {"name": "admin", "subnet": "admin", "role": "admin"}, + {"name": "ctf", "subnet": "ctf", "role": "ctf"}, + ], + "boxes": [ + { + "vm_name": "admin-wazuh", + "vm_id": 1100, + "ip": "192.168.142.100", + "zone": "admin", + "box_template": "admin-wazuh", + "inventory_group": "r42_admin_group", + "attachments": [ + { + "kind": "role", + "catalog_ref": "software.install.wazuh", + "params": {}, + }, + ], + }, + { + "vm_name": "vuln-box-00", + "vm_id": 1170, + "ip": "192.168.144.170", + "zone": "ctf", + "box_template": "vuln-box", + "inventory_group": "r42_vuln_box_group", + "attachments": [], + }, + ], + "network_policy": {"template": "air-gap-ctf", "overrides": {}}, + } + + +@pytest.fixture +def valid_spec_dict() -> dict: + """A minimal, valid scenario.r42.yml composition spec.""" + return { + "schema_version": 1, + "name": "my_lab", + "subnet_layout": "default-3zone", + "network_policy": "air-gap-ctf", + "proxmox_node": "px-testing", + "notes": "demo composition", + "boxes": [ + {"template": "admin-wazuh", "subnet": "admin"}, + { + "template": "vuln-box", + "count": 5, + "subnet": "ctf", + "attachments_add": [ + { + "kind": "role", + "catalog_ref": "software.install.extra", + "params": {}, + }, + ], + "vars": {"difficulty": "hard"}, + }, + ], + } + + +@pytest.fixture +def spec_factory(valid_spec_dict): + """Return a deep-copy mutator so tests can tweak one spec field in isolation.""" + + def _make(**overrides) -> dict: + spec = copy.deepcopy(valid_spec_dict) + spec.update(overrides) + return spec + + return _make + + +@pytest.fixture +def topology_factory(valid_topology_dict): + """Return a deep-copy mutator so tests can tweak one field in isolation.""" + + def _make(**overrides) -> dict: + spec = copy.deepcopy(valid_topology_dict) + spec.update(overrides) + return spec + + return _make + + +def _write(path, text: str): + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(text, encoding="utf-8") + + +@pytest.fixture +def fake_catalog(tmp_path): + """Materialize a minimal, valid 05_topology_layer catalog under tmp_path. + + Returns the catalog_root (the dir that *contains* 05_topology_layer), + mirroring how range42-catalog is laid out on disk. + """ + root = tmp_path / "range42-catalog" + layer = root / "05_topology_layer" + + _write( + layer / "subnet_layouts" / "default-3zone" / "v1.0.0" / "template.yml", + """ +id: default-3zone +api_version: 1 +description: admin + ctf + student subnets +subnets: + - {name: admin, cidr: "192.168.142.0/24", bridge: vmbr142, gateway: "192.168.142.1"} + - {name: student, cidr: "192.168.143.0/24", bridge: vmbr143} + - {name: ctf, cidr: "192.168.144.0/24", bridge: vmbr144} +template_subnet: {cidr: "192.168.140.0/24", bridge: vmbr140} +""".lstrip(), + ) + + _write( + layer / "box_templates" / "vuln-box" / "v1.0.0" / "template.yml", + """ +id: vuln-box +api_version: 1 +description: CTF vulnerable target +template_vm: "template-vm-ubuntu-noble-small-01-4g-32g" +default_attachments: + - {kind: role, catalog_ref: software.install.wazuh-agent, params: {}} +""".lstrip(), + ) + + _write( + layer / "box_templates" / "admin-wazuh" / "v1.0.0" / "template.yml", + """ +id: admin-wazuh +api_version: 1 +description: Wazuh SIEM admin box +template_vm: "template-vm-ubuntu-noble-medium-04-8g-64g" +""".lstrip(), + ) + + _write( + layer / "box_templates" / "student-box" / "v1.0.0" / "template.yml", + """ +id: student-box +api_version: 1 +description: student workstation +template_vm: "template-vm-ubuntu-noble-micro-01-2g-24g" +""".lstrip(), + ) + + # 02_ansible_layer: reusable roles, referenced by name (..). + for role in ( + "software.install.wazuh", + "software.install.wazuh-agent", + "software.install.extra", + ): + _write( + root / "02_ansible_layer" / "admin" / "roles" / role / "tasks" / "main.yml", + "---\n[]\n", + ) + + # 03_container_layer: CTF docker stacks, referenced by path under _ctf/. + _write( + root + / "03_container_layer" + / "docker" + / "_ctf" + / "cve" + / "web" + / "dvwa" + / "docker-compose.yml", + "services: {}\n", + ) + _write( + root + / "03_container_layer" + / "docker" + / "_ctf" + / "misconfiguration" + / "network" + / "open-smb" + / "compose.yml", + "services: {}\n", + ) + + # 01_image_layer: base VM image descriptors (ubuntu_noble + debian_trixie) + _write( + root / "01_image_layer" / "ubuntu_noble" / "v1.0.0" / "image.yml", + """ +id: ubuntu_noble +api_version: 1 +distro: ubuntu +codename: noble +description: Ubuntu 24.04 LTS (Noble Numbat) +cloud_image: + url: "https://cloud-images.ubuntu.com/minimal/daily/noble/current/noble-minimal-cloudimg-amd64.img" + filename: "noble-minimal-cloudimg-amd64.img" +proxmox_templates: + - {vm_id: 9211, vm_name: "template-vm-ubuntu-noble-micro-01-2g-24g", spec: "1cpu/2gb/24gb"} + - {vm_id: 9221, vm_name: "template-vm-ubuntu-noble-small-01-4g-32g", spec: "1cpu/4gb/32gb"} + - {vm_id: 9232, vm_name: "template-vm-ubuntu-noble-medium-02-8g-64g", spec: "2cpu/8gb/64gb"} + - {vm_id: 9234, vm_name: "template-vm-ubuntu-noble-medium-04-8g-64g", spec: "4cpu/8gb/64gb"} +""".lstrip(), + ) + _write( + root / "01_image_layer" / "debian_trixie" / "v1.0.0" / "image.yml", + """ +id: debian_trixie +api_version: 1 +distro: debian +codename: trixie +description: Debian 13 (Trixie) +cloud_image: + url: "https://cloud.debian.org/images/cloud/trixie/latest/debian-13-genericcloud-amd64.raw" + filename: "debian-13-genericcloud-amd64.img" +proxmox_templates: + - {vm_id: 9321, vm_name: "template-vm-debian-trixie-small", spec: "1cpu/4gb/32gb"} + - {vm_id: 9331, vm_name: "template-vm-debian-trixie-medium", spec: "2cpu/8gb/64gb"} +""".lstrip(), + ) + + # two versions of a policy — loader must pick the highest (1.1.0) + for ver, comment in (("v1.0.0", "v1"), ("v1.1.0", "v1.1")): + _write( + layer / "network_policies" / "air-gap-ctf" / ver / "template.yml", + f""" +id: air-gap-ctf +api_version: 1 +kind: isolation-policy +description: admin/ctf isolation + ctf air-gap ({comment}) +params: + siem_ip: "192.168.142.100" +zones: + - {{name: admin}} + - {{name: ctf}} + - {{name: wan, wan: true}} +services: + - {{name: siem, zone: admin, ports: [{{proto: tcp, port: 1514}}, {{proto: tcp, port: 1515}}]}} +defaults: + default_action: drop + accept_established_related: true + allow_intra_zone: true + airgap_zones: [ctf] +matrix: + - {{src: admin, dst: ctf, action: accept, comment: "admin manages vuln boxes"}} + - {{src: ctf, dst: "svc:siem", action: accept, comment: "wazuh agent"}} + - {{src: ctf, dst: admin, action: drop, comment: "zone isolation"}} +""".lstrip(), + ) + + return root + + +@pytest.fixture +def reserved_factory(tmp_path): + """Write a JSONL _reserved.json (one object per line) from a list of dicts.""" + import json + + def _make(entries: list[dict]) -> "object": + from pathlib import Path + + path = Path(tmp_path) / "_reserved.json" + path.write_text( + "\n".join(json.dumps(e) for e in entries) + "\n", encoding="utf-8" + ) + return path + + return _make diff --git a/tests/golden/demo_lab.filelist.txt b/tests/golden/demo_lab.filelist.txt new file mode 100644 index 00000000..55f1d4f5 --- /dev/null +++ b/tests/golden/demo_lab.filelist.txt @@ -0,0 +1,116 @@ +# Golden baseline: the complete file list of scenarios/demo_lab (the §4.3 +# reference shape for the S5 renderer). Paths are relative to the scenario root. +# Regenerate: (cd scenarios/demo_lab && find . -type f | sort) +# demo_lab-specific infixes (e.g. 'demo_lab.' in script names, vuln_box_00..04, +# deployer_*/builder_* admin VMs) are demo_lab's OWN composition — the renderer +# maps these to the composed scenario's name + boxes, it does not copy them verbatim. +01_init_proxmox/templates/alpine/place_holder +01_init_proxmox/templates/debian/place_holder +01_init_proxmox/templates/_main_download_cloudinit_files.yml +01_init_proxmox/templates/ubuntu_noble/00-template-vm-nano.yml +01_init_proxmox/templates/ubuntu_noble/01-template-vm-micro-01-2g-24g.yml +01_init_proxmox/templates/ubuntu_noble/01-template-vm-micro-02-2g-24g.yml +01_init_proxmox/templates/ubuntu_noble/02-template-vm-small-01-4g-32g.yml +01_init_proxmox/templates/ubuntu_noble/02-template-vm-small-02-4g-32g.yml +01_init_proxmox/templates/ubuntu_noble/02-template-vm-small-04-4g-32g.yml +01_init_proxmox/templates/ubuntu_noble/03-template-vm-medium-02-8g-64g.yml +01_init_proxmox/templates/ubuntu_noble/03-template-vm-medium-04-8g-64g.yml +01_init_proxmox/templates/ubuntu_noble/03-template-vm-medium-06-8g-64g.yml +01_init_proxmox/templates/ubuntu_noble/04-template-vm-large-04-8g-64g.yml +01_init_proxmox/templates/ubuntu_noble/04-template-vm-large-06-8g-64g.yml +01_init_proxmox/templates/ubuntu_noble/04-template-vm-large-08-8g-64g.yml +01_init_proxmox/templates/ubuntu_noble/_main_ubuntu_noble.yml +01_init_proxmox/templates/ubuntu_noble/test_setup_templates.sh +01_init_proxmox/templates/ubuntu_noble/test_setup_templates.yml +02_admin_infrastructure/_main.reinstall.sh +02_admin_infrastructure/_main.yml +02_admin_infrastructure/stage_00/builder_api_devkit.yml +02_admin_infrastructure/stage_00/builder_docker_registry.yml +02_admin_infrastructure/stage_00/deployer_api_backend.yml +02_admin_infrastructure/stage_00/deployer_api_gateway.yml +02_admin_infrastructure/stage_00/deployer_ui.yml +02_admin_infrastructure/stage_00/mon_wazuh.yml +02_admin_infrastructure/stage_00/r42_admin_group.devkit/demo_lab.r42_admin_group.delete.sh +02_admin_infrastructure/stage_00/r42_admin_group.devkit/demo_lab.r42_admin_group.install.sh +02_admin_infrastructure/stage_00/_r42_admin_group.yml +02_admin_infrastructure/stage_00/_testing/wazuh_client_01.yml +02_admin_infrastructure/stage_01/deployer_api_gateway.devkit/demo_lab.deployer_api_gateway.install.sh +02_admin_infrastructure/stage_01/deployer_api_gateway.devkit/demo_lab.deployer_api_gateway.revert.sh +02_admin_infrastructure/stage_01/deployer_api_gateway.devkit/demo_lab.deployer_api_gateway.snapshot.sh +02_admin_infrastructure/stage_01/deployer_api_gateway.yml +02_admin_infrastructure/stage_01/deployer_ui.devkit/demo_lab.deployer_ui.install.sh +02_admin_infrastructure/stage_01/deployer_ui.devkit/demo_lab.deployer_ui.revert.sh +02_admin_infrastructure/stage_01/deployer_ui.devkit/demo_lab.deployer_ui.snapshot.sh +02_admin_infrastructure/stage_01/deployer_ui.yml +02_admin_infrastructure/stage_01/mon_wazuh.devkit/demo_lab.mon_wazuh.install.sh +02_admin_infrastructure/stage_01/mon_wazuh.devkit/demo_lab.mon_wazuh.revert.sh +02_admin_infrastructure/stage_01/mon_wazuh.devkit/demo_lab.mon_wazuh.snapshot.sh +02_admin_infrastructure/stage_01/mon_wazuh.yml +02_admin_infrastructure/stage_01/_r42_admin.devkit/demo_lab.r42_admin.install.sh +02_admin_infrastructure/stage_01/_r42_admin.devkit/demo_lab.r42_admin.revert.sh +02_admin_infrastructure/stage_01/_r42_admin.devkit/demo_lab.r42_admin.snapshot.sh +02_admin_infrastructure/stage_01/_r42_admin.yml +03_student_infrastructure/_main.reinstall.sh +03_student_infrastructure/_main.yml +03_student_infrastructure/stage_00/_r42_student_box_group.devkit/demo_lab.r42_student_box_group.delete.sh +03_student_infrastructure/stage_00/_r42_student_box_group.devkit/demo_lab.r42_student_box_group.install.sh +03_student_infrastructure/stage_00/_r42_student_box_group.yml +03_student_infrastructure/stage_00/student_box_01.yml +03_student_infrastructure/stage_01/_r42_student_box_group.devkit/demo_lab.r42_student_box_group.install.sh +03_student_infrastructure/stage_01/_r42_student_box_group.devkit/demo_lab.r42_student_box_group.revert.sh +03_student_infrastructure/stage_01/_r42_student_box_group.devkit/demo_lab.r42_student_box_group.snapshot.sh +03_student_infrastructure/stage_01/_r42_student_box_group.yml +03_student_infrastructure/stage_01/student_box_01.devkit/demo_lab.student_box_01.install.sh +03_student_infrastructure/stage_01/student_box_01.devkit/demo_lab.student_box_01.revert.sh +03_student_infrastructure/stage_01/student_box_01.devkit/demo_lab.student_box_01.snapshot.sh +03_student_infrastructure/stage_01/student_box_01.yml +04_ctf_infrastructure/_main.reinstall.sh +04_ctf_infrastructure/_main.yml +04_ctf_infrastructure/stage_00/r42_vuln_box_group.devkit/demo_lab.vuln_box_01.delete.sh +04_ctf_infrastructure/stage_00/r42_vuln_box_group.devkit/demo_lab.vuln_box_01.install.sh +04_ctf_infrastructure/stage_00/_r42_vuln_box_group.yml +04_ctf_infrastructure/stage_00/vuln_box_00.yml +04_ctf_infrastructure/stage_00/vuln_box_01.yml +04_ctf_infrastructure/stage_00/vuln_box_02.yml +04_ctf_infrastructure/stage_00/vuln_box_03.yml +04_ctf_infrastructure/stage_00/vuln_box_04.yml +04_ctf_infrastructure/stage_01/_r42_vuln_box_group.devkit/demo_lab.r42_vuln_box.install.sh +04_ctf_infrastructure/stage_01/_r42_vuln_box_group.devkit/demo_lab.r42_vuln_box.revert.sh +04_ctf_infrastructure/stage_01/_r42_vuln_box_group.devkit/demo_lab.r42_vuln_box.snapshot.sh +04_ctf_infrastructure/stage_01/_r42_vuln_box_group.yml +04_ctf_infrastructure/stage_01/vuln_box_00.devkit/demo_lab.vuln_box_00.install.sh +04_ctf_infrastructure/stage_01/vuln_box_00.devkit/demo_lab.vuln_box_00.revert.sh +04_ctf_infrastructure/stage_01/vuln_box_00.devkit/demo_lab.vuln_box_00.snapshot.sh +04_ctf_infrastructure/stage_01/vuln_box_00.yml +04_ctf_infrastructure/stage_01/vuln_box_01.devkit/demo_lab.vuln_box_01.install.sh +04_ctf_infrastructure/stage_01/vuln_box_01.devkit/demo_lab.vuln_box_01.revert.sh +04_ctf_infrastructure/stage_01/vuln_box_01.devkit/demo_lab.vuln_box_01.snapshot.sh +04_ctf_infrastructure/stage_01/vuln_box_01.yml +04_ctf_infrastructure/stage_01/vuln_box_02.devkit/demo_lab.vuln_box_02.install.sh +04_ctf_infrastructure/stage_01/vuln_box_02.devkit/demo_lab.vuln_box_02.revert.sh +04_ctf_infrastructure/stage_01/vuln_box_02.devkit/demo_lab.vuln_box_02.snapshot.sh +04_ctf_infrastructure/stage_01/vuln_box_02.yml +04_ctf_infrastructure/stage_01/vuln_box_03.devkit/demo_lab.vuln_box_03.install.sh +04_ctf_infrastructure/stage_01/vuln_box_03.devkit/demo_lab.vuln_box_03.revert.sh +04_ctf_infrastructure/stage_01/vuln_box_03.devkit/demo_lab.vuln_box_03.snapshot.sh +04_ctf_infrastructure/stage_01/vuln_box_03.yml +04_ctf_infrastructure/stage_01/vuln_box_04.devkit/demo_lab.vuln_box_04.install.sh +04_ctf_infrastructure/stage_01/vuln_box_04.devkit/demo_lab.vuln_box_04.revert.sh +04_ctf_infrastructure/stage_01/vuln_box_04.devkit/demo_lab.vuln_box_04.snapshot.sh +04_ctf_infrastructure/stage_01/vuln_box_04.yml +_activate.sh +demo_lab.delete_all.sh +demo_lab.delete_vms_only.sh +demo_lab.reset.setup.sh +demo_lab.reset.ssh_keys.sh +demo_lab.setup.sh +demo_lab.setup_vms_only.sh +devkit_ansible.show_ansible_inventory.to.text.sh +main_vms_only.yml +main.yml +manifest/scenario_vms.json +README.md +templates/ansible-inventory.j2 +templates/ansible-vars.yml +templates/ssh-config.j2 +templates/vault-example.yml diff --git a/tests/test_allocate.py b/tests/test_allocate.py new file mode 100644 index 00000000..842b4902 --- /dev/null +++ b/tests/test_allocate.py @@ -0,0 +1,366 @@ +"""Step 4 — allocation + manifest/scenario_vms.json (RED before GREEN). + +Invariants under test (plan §4, §7.1): + - octet rule (vm_id last 3 digits == IP last octet) holds for every placed box; + - templates are NOT placed / never octet-checked; templates[] is the full table; + - vm_names follow {subnet}-{template}-{index:02d} for all boxes; + - lowest-matching template vm_id is selected for a box spec (H2); + - global uniqueness vs _reserved.json (other scenarios block; band bumps); + - manifest matches the demo_lab schema. +All tests use the fake_catalog fixture (real range42-catalog never in checkout). +""" + +import pytest + +from r42playbooks.core import constants as C +from r42playbooks.core.allocate import Allocation, allocate, manifest_dict +from r42playbooks.core.catalog import load_catalog +from r42playbooks.core.errors import CatalogNotFoundError, CompileError, ValidationError +from r42playbooks.core.idalloc import ReservedIndex +from r42playbooks.core.spec import ScenarioSpec + + +def _alloc(catalog, **spec_overrides): + base = { + "name": "gen_lab", + "subnet_layout": "default-3zone", + "network_policy": "air-gap-ctf", + "boxes": [{"template": "admin-wazuh", "subnet": "admin"}], + } + base.update(spec_overrides) + return allocate(ScenarioSpec.model_validate(base), catalog) + + +# --- template VM resolution (catalog-driven) -------------------------------- + +def test_box_image_resolved_from_template_vm(fake_catalog): + """template_vm reference on BoxTemplate drives image resolution.""" + alloc = _alloc(load_catalog(fake_catalog)) + # admin-wazuh → template-vm-ubuntu-noble-medium-04-8g-64g → ubuntu_noble + assert all(b.image == "ubuntu_noble" for b in alloc.boxes) + assert {t.image for t in alloc.templates} == {"ubuntu_noble"} + + +def test_distinct_templates_get_unique_ips_when_boxes_share_octet(fake_catalog): + """Regression: two boxes in different subnets sharing an octet must not make + their distinct templates collide on the single template subnet. + + Template build VMs boot concurrently during 01_init_proxmox; a shared IP + collides on the bridge so the loser's cloud-init can't reach the network and + its apt-get update stalls forever (the VM never auto-powers-off and the + deploy hangs at 'WAIT for cloud-init to auto-poweroff'). Two boxes can + legitimately share an octet across different lab subnets, so the template IP + must not be derived from the box octet. + """ + alloc = _alloc( + load_catalog(fake_catalog), + boxes=[ + {"template": "admin-wazuh", "subnet": "admin", "octet": 2}, # → medium template + {"template": "vuln-box", "subnet": "ctf", "octet": 2}, # → small template + ], + ) + # Two distinct templates resolved (different template_vm ids). + assert len({t.vm_id for t in alloc.templates}) == 2 + tpl_ips = [t.ip for t in alloc.templates] + # Their build IPs are unique — no collision on the template subnet. + assert len(set(tpl_ips)) == len(tpl_ips), f"template IPs collided: {tpl_ips}" + # All on the template subnet (192.168.140.0/24), never the gateway (.1). + assert all(ip.rsplit(".", 1)[0] == "192.168.140" for ip in tpl_ips) + assert "192.168.140.1" not in tpl_ips + + +def _add_box_template(fake_catalog, *, box_id: str, template_vm: str) -> None: + layer = fake_catalog / "05_topology_layer" / "box_templates" / box_id / "v1.0.0" + layer.mkdir(parents=True) + (layer / "template.yml").write_text( + f"id: {box_id}\napi_version: 1\n" + f"template_vm: \"{template_vm}\"\n", + encoding="utf-8", + ) + + +def test_debian_box_selects_a_debian_trixie_template(fake_catalog): + """A box referencing a debian_trixie template_vm clones that image.""" + from r42playbooks.core.catalog import load_catalog as _load + _add_box_template(fake_catalog, box_id="deb-box", + template_vm="template-vm-debian-trixie-small") + catalog = _load(fake_catalog) + spec = ScenarioSpec.model_validate({ + "name": "deb_lab", "subnet_layout": "default-3zone", "boxes": [{"template": "deb-box", "subnet": "student"}], + }) + box = allocate(spec, catalog).boxes[0] + assert box.image == "debian_trixie" + assert box.template_vm_id == 9321 + assert box.template_name == "template-vm-debian-trixie-small" + + +def test_unknown_template_vm_blocks_allocation(fake_catalog): + """A box referencing a template_vm that doesn't exist raises CompileError.""" + from r42playbooks.core.catalog import load_catalog as _load + _add_box_template(fake_catalog, box_id="ghost-vm-box", + template_vm="template-vm-does-not-exist") + catalog = _load(fake_catalog) + spec = ScenarioSpec.model_validate({ + "name": "ghost_lab", "subnet_layout": "default-3zone", + "boxes": [{"template": "ghost-vm-box", "subnet": "student"}], + }) + with pytest.raises(CompileError, match="template-vm-does-not-exist"): + allocate(spec, catalog) + + +# --- octet assignment ----------------------------------------------------- + +def test_auto_box_starts_at_default_octet(fake_catalog): + """Without an explicit octet, allocation starts at .10.""" + alloc = _alloc(load_catalog(fake_catalog)) + box = alloc.boxes[0] + assert box.vm_name == "admin-admin-wazuh-00" + assert box.vm_id == 1010 + assert box.ip == "192.168.142.10" + assert box.bridge == "vmbr142" + + +def test_explicit_octet_pins_ip(fake_catalog): + """BoxSpec.octet pins the starting last octet within the subnet.""" + alloc = _alloc(load_catalog(fake_catalog), boxes=[{"template": "admin-wazuh", "subnet": "admin", "octet": 100}]) + box = alloc.boxes[0] + assert box.ip == "192.168.142.100" + assert box.vm_id == 1100 + assert C.octet_matches_vm_id(box.vm_id, box.ip) + + +def test_explicit_octet_with_count_assigns_sequentially(fake_catalog): + """count>1 with explicit octet: .N, .N+1, .N+2 …""" + alloc = _alloc(load_catalog(fake_catalog), boxes=[{"template": "vuln-box", "count": 3, "subnet": "ctf", "octet": 50}]) + ips = [b.ip for b in alloc.boxes] + ids = [b.vm_id for b in alloc.boxes] + assert ips == ["192.168.144.50", "192.168.144.51", "192.168.144.52"] + assert ids == [1050, 1051, 1052] + + +def test_auto_box_on_any_subnet_starts_at_ten(fake_catalog): + alloc = _alloc(load_catalog(fake_catalog), boxes=[{"template": "student-box", "subnet": "student"}]) + box = alloc.boxes[0] + assert box.vm_id == 1010 + assert box.ip == "192.168.143.10" + + +def test_octet_rule_holds_for_every_placed_box(fake_catalog): + alloc = _alloc( + load_catalog(fake_catalog), + boxes=[{"template": "admin-wazuh", "subnet": "admin"}, {"template": "vuln-box", "count": 5, "subnet": "ctf"}], + ) + for box in alloc.boxes: + assert C.octet_matches_vm_id(box.vm_id, box.ip), box + + +# --- count expansion ------------------------------------------------------- + +def test_count_expands_to_zero_padded_names(fake_catalog): + alloc = _alloc(load_catalog(fake_catalog), boxes=[{"template": "vuln-box", "count": 5, "subnet": "ctf"}]) + names = [b.vm_name for b in alloc.boxes] + assert names == ["ctf-vuln-box-00", "ctf-vuln-box-01", "ctf-vuln-box-02", "ctf-vuln-box-03", "ctf-vuln-box-04"] + ids = [b.vm_id for b in alloc.boxes] + assert ids == [1010, 1011, 1012, 1013, 1014] + ips = [b.ip for b in alloc.boxes] + assert ips == [f"192.168.144.{o}" for o in range(10, 15)] + + +def test_count_one_uses_subnet_prefixed_name(fake_catalog): + alloc = _alloc(load_catalog(fake_catalog), boxes=[{"template": "vuln-box", "subnet": "ctf"}]) + assert alloc.boxes[0].vm_name == "ctf-vuln-box-00" + + +def test_box_resolves_clone_template_vm_id(fake_catalog): + alloc = _alloc(load_catalog(fake_catalog), boxes=[{"template": "vuln-box", "subnet": "ctf"}]) + box = alloc.boxes[0] + assert box.template_vm_id == 9221 # lowest 1cpu/4gb/32gb + + +def test_attachments_merge_template_defaults_and_spec_additions(fake_catalog): + alloc = _alloc( + load_catalog(fake_catalog), + boxes=[{ + "template": "vuln-box", + "subnet": "ctf", + "attachments_add": [ + {"kind": "role", "catalog_ref": "software.install.extra", "params": {}}, + ], + }], + ) + refs = [a.catalog_ref for a in alloc.boxes[0].attachments] + assert "software.install.wazuh-agent" in refs # from template default + assert "software.install.extra" in refs # from spec addition + + +# --- _reserved.json uniqueness -------------------------------------------- + +def test_other_scenario_vm_id_collision_bumps_band(fake_catalog, reserved_factory): + # another scenario owns vm_id 1010, but on a different IP -> octet .10 still + # free for our ctf subnet, so the band bumps (2010) keeping the octet rule. + reserved = ReservedIndex.from_file(reserved_factory([ + {"vm_id": 1010, "ip": "10.9.9.9", "scenario": "other_lab"}, + ])) + spec = ScenarioSpec.model_validate({ + "name": "gen_lab", "subnet_layout": "default-3zone", + "network_policy": "air-gap-ctf", "boxes": [{"template": "vuln-box", "subnet": "ctf"}], + }) + box = allocate(spec, load_catalog(fake_catalog), reserved).boxes[0] + assert box.ip == "192.168.144.10" + assert box.vm_id == 2010 + assert C.octet_matches_vm_id(box.vm_id, box.ip) + + +def test_other_scenario_ip_collision_bumps_octet(fake_catalog, reserved_factory): + reserved = ReservedIndex.from_file(reserved_factory([ + {"vm_id": 1010, "ip": "192.168.144.10", "scenario": "other_lab"}, + ])) + spec = ScenarioSpec.model_validate({ + "name": "gen_lab", "subnet_layout": "default-3zone", + "network_policy": "air-gap-ctf", "boxes": [{"template": "vuln-box", "subnet": "ctf"}], + }) + box = allocate(spec, load_catalog(fake_catalog), reserved).boxes[0] + assert box.ip == "192.168.144.11" + assert box.vm_id == 1011 + + +def test_template_rows_never_reallocated(fake_catalog, reserved_factory): + # _reserved.json carries 9xxx template rows; no placed box may land in 9xxx. + catalog = load_catalog(fake_catalog) + # Use the actual template VMs from the catalog as the reserved set. + template_entries = [ + {"vm_id": t_spec.vm_id, "vm_name": t_spec.vm_name, "bridge": "vmbr140", + "role": "template", "scenario": "other_lab"} + for img in catalog.images.values() + for t_spec in img.proxmox_templates + ] + reserved = ReservedIndex.from_file(reserved_factory(template_entries)) + spec = ScenarioSpec.model_validate({ + "name": "gen_lab", "subnet_layout": "default-3zone", + "network_policy": "air-gap-ctf", "boxes": [{"template": "vuln-box", "count": 3, "subnet": "ctf"}], + }) + alloc = allocate(spec, catalog, reserved) + for box in alloc.boxes: + assert box.vm_id < 9000 + + +# --- manifest -------------------------------------------------------------- + +def test_manifest_matches_demo_lab_schema(fake_catalog): + alloc = _alloc( + load_catalog(fake_catalog), + name="demo_clone", + boxes=[{"template": "admin-wazuh", "subnet": "admin"}, {"template": "vuln-box", "count": 5, "subnet": "ctf"}], + ) + m = manifest_dict(alloc) + assert m["scenario"] == "demo_clone" + assert m["version"] == 2 + assert {"scenario", "version", "description", "vms", "templates"} == set(m) + # vms sorted by vm_id, demo_lab row shape + assert m["vms"] == sorted(m["vms"], key=lambda v: v["vm_id"]) + assert set(m["vms"][0]) == {"vm_id", "vm_name", "ip", "subnet", "bridge", "image"} + assert all(v["image"] == "ubuntu_noble" for v in m["vms"]) # fake_catalog boxes default + + +def test_manifest_templates_selective_not_full_table(fake_catalog): + """templates[] contains only the VMs the scenario actually needs (not the full table).""" + alloc = _alloc(load_catalog(fake_catalog)) + m = manifest_dict(alloc) + # admin-wazuh needs template-vm-ubuntu-noble-medium-04-8g-64g only + assert len(m["templates"]) == 1 + assert m["templates"][0]["vm_id"] == 9234 + assert m["templates"][0]["image"] == "ubuntu_noble" + assert set(m["templates"][0]) == {"vm_id", "vm_name", "spec", "ip", "bridge", "image"} + + +def test_manifest_templates_multi_image(fake_catalog): + """A scenario with both ubuntu and debian boxes has both images in templates[].""" + _add_box_template(fake_catalog, box_id="deb-box2", + template_vm="template-vm-debian-trixie-small") + alloc = _alloc( + load_catalog(fake_catalog), + boxes=[{"template": "admin-wazuh", "subnet": "admin"}, {"template": "deb-box2", "subnet": "student"}], + ) + m = manifest_dict(alloc) + assert {t["image"] for t in m["templates"]} == {"ubuntu_noble", "debian_trixie"} + assert len(m["templates"]) == 2 + + +def test_unknown_box_template_raises(fake_catalog): + catalog = load_catalog(fake_catalog) + spec = ScenarioSpec.model_validate({ + "name": "gen_lab", "subnet_layout": "default-3zone", + "network_policy": "air-gap-ctf", "boxes": [{"template": "ghost-box", "subnet": "admin"}], + }) + with pytest.raises(CatalogNotFoundError): + allocate(spec, catalog) + + +def test_allocation_is_deterministic(fake_catalog): + catalog = load_catalog(fake_catalog) + a1 = _alloc(catalog, boxes=[{"template": "vuln-box", "count": 3, "subnet": "ctf"}]) + a2 = _alloc(catalog, boxes=[{"template": "vuln-box", "count": 3, "subnet": "ctf"}]) + assert isinstance(a1, Allocation) + assert manifest_dict(a1) == manifest_dict(a2) + + +def test_missing_template_subnet_raises(fake_catalog): + """A subnet layout without template_subnet raises CompileError at allocation.""" + layer = fake_catalog / "05_topology_layer" / "subnet_layouts" / "no-tpl-subnet" / "v1.0.0" + layer.mkdir(parents=True) + (layer / "template.yml").write_text( + "id: no-tpl-subnet\napi_version: 1\n" + "subnets:\n - {name: admin, cidr: 192.168.99.0/24, bridge: vmbr99}\n", + encoding="utf-8", + ) + catalog = load_catalog(fake_catalog) + spec = ScenarioSpec.model_validate({ + "name": "bad_lab", "subnet_layout": "no-tpl-subnet", + "boxes": [{"template": "admin-wazuh", "subnet": "admin"}], + }) + with pytest.raises(CompileError, match="no template_subnet"): + allocate(spec, catalog) + + +# --- gateway conflict validation ------------------------------------------- + +def test_explicit_octet_conflicting_with_gateway_raises(fake_catalog): + """Explicit octet that matches the subnet gateway raises CompileError.""" + # admin gateway in the fixture is 192.168.142.1 (octet 1) + with pytest.raises(CompileError, match="gateway"): + _alloc(load_catalog(fake_catalog), + boxes=[{"template": "admin-wazuh", "subnet": "admin", "octet": 1}]) + + +def test_auto_allocation_skips_gateway_ip(fake_catalog): + """Auto-allocation never assigns the gateway IP even when it falls at the start octet.""" + layer = fake_catalog / "05_topology_layer" / "subnet_layouts" / "gw-at-ten" / "v1.0.0" + layer.mkdir(parents=True) + (layer / "template.yml").write_text( + "id: gw-at-ten\napi_version: 1\n" + "subnets:\n" + " - {name: srv, cidr: 192.168.200.0/24, bridge: vmbr200, gateway: 192.168.200.10}\n" + "template_subnet: {cidr: 192.168.140.0/24, bridge: vmbr140}\n", + encoding="utf-8", + ) + catalog = load_catalog(fake_catalog) + spec = ScenarioSpec.model_validate({ + "name": "gw_test", "subnet_layout": "gw-at-ten", + "boxes": [{"template": "admin-wazuh", "subnet": "srv"}], + }) + alloc = allocate(spec, catalog) + box = alloc.boxes[0] + assert box.ip != "192.168.200.10", "gateway IP must not be assigned to a VM" + assert box.ip == "192.168.200.11" # skipped .10 (gateway), landed on .11 + + +# --- subnet-prefixed naming ------------------------------------------------------- + +def test_same_template_on_two_subnets_gets_unique_names(fake_catalog): + """Same template on two subnets produces distinct names: {subnet}-{template}-00.""" + alloc = _alloc(load_catalog(fake_catalog), boxes=[ + {"template": "admin-wazuh", "subnet": "admin"}, + {"template": "admin-wazuh", "subnet": "ctf"}, + ]) + names = [b.vm_name for b in alloc.boxes] + assert names == ["admin-admin-wazuh-00", "ctf-admin-wazuh-00"] diff --git a/tests/test_api.py b/tests/test_api.py new file mode 100644 index 00000000..548672e7 --- /dev/null +++ b/tests/test_api.py @@ -0,0 +1,55 @@ +"""P3 importable-API adapter tests (the surface FastAPI / deployment CLI call).""" + +import pytest + +from r42playbooks import api +from r42playbooks.core.errors import ValidationError +from r42playbooks.core.idalloc import ReservedIndex +from r42playbooks.core.models import Topology + + +def test_load_catalog_reexported(fake_catalog): + cat = api.load_catalog(fake_catalog) + assert "vuln-box" in cat.box_templates + + +def test_author_topology_returns_model(topology_factory, fake_catalog): + cat = api.load_catalog(fake_catalog) + t = api.author_topology(topology_factory(), catalog=cat) + assert isinstance(t, Topology) + + +def test_author_topology_rejects_dangling_ref(topology_factory, fake_catalog): + cat = api.load_catalog(fake_catalog) + spec = topology_factory() + spec["boxes"][0]["box_template"] = "missing-template" + with pytest.raises(ValidationError): + api.author_topology(spec, catalog=cat) + + +def test_validate_topology_clean(topology_factory, fake_catalog, reserved_factory): + cat = api.load_catalog(fake_catalog) + t = Topology.model_validate(topology_factory()) + reserved = ReservedIndex.from_file(reserved_factory([])) + assert api.validate_topology(t, catalog=cat, reserved=reserved) == [] + + +def test_validate_topology_reports_problems(topology_factory, fake_catalog, reserved_factory): + cat = api.load_catalog(fake_catalog) + spec = topology_factory() + spec["boxes"][0]["ip"] = "10.0.0.100" + t = Topology.model_validate(spec) + reserved = ReservedIndex.from_file(reserved_factory([])) + assert api.validate_topology(t, catalog=cat, reserved=reserved) + + +def test_full_author_compile_extravars(topology_factory, fake_catalog, reserved_factory, tmp_path): + cat = api.load_catalog(fake_catalog) + reserved = ReservedIndex.from_file(reserved_factory([])) + t = api.author_topology(topology_factory(), catalog=cat) + assert api.validate_topology(t, catalog=cat, reserved=reserved) == [] + result = api.compile_topology(t, workspace=tmp_path / "ws", catalog=cat, reserved=reserved) + ev = api.resolve_universal_extravars(result, deployment_id="d", attempt_id="a", scope="global") + # the _universal stub asserts this path exists and is a regular file + from pathlib import Path + assert Path(ev["r42_topology_path"]).is_file() diff --git a/tests/test_catalog.py b/tests/test_catalog.py new file mode 100644 index 00000000..4968eb33 --- /dev/null +++ b/tests/test_catalog.py @@ -0,0 +1,104 @@ +"""P2 catalog loader tests — RED before GREEN.""" + +import pytest + +from r42playbooks.core.catalog import list_images, load_catalog +from r42playbooks.core.errors import CatalogNotFoundError + + +def test_load_catalog_indexes_all_categories(fake_catalog): + cat = load_catalog(fake_catalog) + assert set(cat.box_templates) == {"vuln-box", "admin-wazuh", "student-box"} + assert set(cat.network_policies) == {"air-gap-ctf"} + assert set(cat.subnet_layouts) == {"default-3zone"} + assert set(cat.images) == {"ubuntu_noble", "debian_trixie"} + + +def test_box_template_fields_parsed(fake_catalog): + cat = load_catalog(fake_catalog) + vb = cat.box_templates["vuln-box"] + assert vb.default_attachments[0].catalog_ref == "software.install.wazuh-agent" + + +def test_loader_picks_highest_version(fake_catalog): + cat = load_catalog(fake_catalog) + pol = cat.network_policies["air-gap-ctf"] + assert cat.resolved_version("network_policies", "air-gap-ctf") == "1.1.0" + # content hash is recorded for reproducibility + assert len(cat.resolved_hash("network_policies", "air-gap-ctf")) == 64 + + +def test_network_policy_structure(fake_catalog): + cat = load_catalog(fake_catalog) + pol = cat.network_policies["air-gap-ctf"] + assert pol.kind == "isolation-policy" + assert {z.name for z in pol.zones} == {"admin", "ctf", "wan"} + assert any(z.wan for z in pol.zones) + assert pol.defaults.airgap_zones == ["ctf"] + assert any(r.dst == "svc:siem" for r in pol.matrix) + + +def test_resolve_helpers(fake_catalog): + cat = load_catalog(fake_catalog) + assert cat.resolve_box_template("vuln-box").id == "vuln-box" + assert cat.resolve_network_policy("air-gap-ctf").id == "air-gap-ctf" + + +def test_missing_template_raises(fake_catalog): + cat = load_catalog(fake_catalog) + with pytest.raises(CatalogNotFoundError): + cat.resolve_box_template("does-not-exist") + + +def test_missing_layer_raises(tmp_path): + with pytest.raises(CatalogNotFoundError): + load_catalog(tmp_path / "empty") + + +def test_template_id_traversal_rejected(fake_catalog): + cat = load_catalog(fake_catalog) + with pytest.raises(CatalogNotFoundError): + cat.resolve_box_template("../../etc/passwd") + + +def test_shipped_catalog_validates(): + """The real range42-catalog (sibling repo) must load if present.""" + from pathlib import Path + sibling = Path(__file__).resolve().parents[2] / "range42-catalog" + if not (sibling / "05_topology_layer").is_dir(): + pytest.skip("range42-catalog sibling not checked out") + cat = load_catalog(sibling) + assert cat.box_templates and cat.network_policies and cat.subnet_layouts + assert cat.images, "01_image_layer must be present in the shipped catalog" + + +def test_image_fields_parsed(fake_catalog): + cat = load_catalog(fake_catalog) + noble = cat.images["ubuntu_noble"] + assert noble.distro == "ubuntu" + assert noble.codename == "noble" + trixie = cat.images["debian_trixie"] + assert trixie.distro == "debian" + assert trixie.codename == "trixie" + + +def test_image_layer_absent_is_ok(tmp_path): + """load_catalog succeeds when 01_image_layer is absent — layer is optional.""" + from pathlib import Path + # Minimal catalog with only the topology layer + layer = tmp_path / "cat" / "05_topology_layer" + (layer / "subnet_layouts" / "s" / "v1.0.0").mkdir(parents=True) + (layer / "subnet_layouts" / "s" / "v1.0.0" / "template.yml").write_text( + "id: s\napi_version: 1\ndescription: x\nsubnets:\n - {name: a, cidr: 10.0.0.0/24, bridge: vmbr0}\n" + ) + cat = load_catalog(tmp_path / "cat") + assert cat.images == {} + + +def test_list_images(fake_catalog): + imgs = list_images(fake_catalog) + assert imgs == ["debian_trixie", "ubuntu_noble"] # sorted + + +def test_list_images_absent_returns_empty(tmp_path): + assert list_images(tmp_path / "empty") == [] diff --git a/tests/test_catalog_pick.py b/tests/test_catalog_pick.py new file mode 100644 index 00000000..fa8357cd --- /dev/null +++ b/tests/test_catalog_pick.py @@ -0,0 +1,167 @@ +"""Step 3 — catalog pick/validate API (RED before GREEN). + +Enumerate pickable roles (02_ansible_layer) and containers (03_container_layer), +and validate that every ref in a ScenarioSpec resolves in the catalog. +All tests use the fake_catalog fixture — the real range42-catalog is a separate, +gitignored repo never present in this checkout. +""" + +from r42playbooks.core.catalog import ( + Catalog, + list_containers, + list_roles, + load_catalog, + validate_refs, +) +from r42playbooks.core.catalog_models import BoxTemplate +from r42playbooks.core.models import Attachment +from r42playbooks.core.spec import ScenarioSpec + + +def test_validate_refs_flags_unknown_default_attachment_role(): + """A box template's own default_attachments must resolve too (renderer emits them).""" + catalog = Catalog( + subnet_layouts={"layout": object()}, # presence is all validate_refs checks + network_policies={"policy": object()}, + box_templates={ + "leaky-box": BoxTemplate( + id="leaky-box", + template_vm="template-vm-ubuntu-noble-small-01-4g-32g", + default_attachments=[ + Attachment(kind="role", catalog_ref="software.install.ghost"), + ], + ) + }, + roles=set(), # the default role is NOT in the catalog + containers=set(), + ) + spec = ScenarioSpec.model_validate({ + "name": "x", "subnet_layout": "layout", "network_policy": "policy", + "boxes": [{"template": "leaky-box", "subnet": "admin"}], + }) + problems = validate_refs(spec, catalog) + assert any("software.install.ghost" in p for p in problems) + + +def test_list_roles_enumerates_role_dir_names(fake_catalog): + roles = list_roles(fake_catalog) + assert "software.install.wazuh" in roles + assert "software.install.wazuh-agent" in roles + assert roles == sorted(roles) # deterministic ordering + + +def test_list_containers_enumerates_ctf_paths(fake_catalog): + containers = list_containers(fake_catalog) + assert "cve/web/dvwa" in containers + assert "misconfiguration/network/open-smb" in containers + + +def test_list_is_empty_when_layers_absent(tmp_path): + # a catalog_root without 02/03 layers yields empty lists, not an error + assert list_roles(tmp_path) == [] + assert list_containers(tmp_path) == [] + + +def test_load_catalog_populates_roles_and_containers(fake_catalog): + catalog = load_catalog(fake_catalog) + assert "software.install.wazuh" in catalog.roles + assert "cve/web/dvwa" in catalog.containers + + +def test_validate_refs_all_known_returns_empty(fake_catalog, valid_spec_dict): + catalog = load_catalog(fake_catalog) + spec = ScenarioSpec.model_validate(valid_spec_dict) + assert validate_refs(spec, catalog) == [] + + +def test_validate_refs_reports_unknown_box_template(fake_catalog, spec_factory): + catalog = load_catalog(fake_catalog) + data = spec_factory(boxes=[{"template": "ghost-box", "subnet": "admin"}]) + spec = ScenarioSpec.model_validate(data) + problems = validate_refs(spec, catalog) + assert any("ghost-box" in p for p in problems) + + +def test_validate_refs_reports_unknown_subnet_and_policy(fake_catalog, spec_factory): + catalog = load_catalog(fake_catalog) + data = spec_factory(subnet_layout="nope-layout", network_policy="nope-policy") + spec = ScenarioSpec.model_validate(data) + problems = validate_refs(spec, catalog) + assert any("nope-layout" in p for p in problems) + assert any("nope-policy" in p for p in problems) + + +def test_validate_refs_reports_unknown_role(fake_catalog, valid_spec_dict): + catalog = load_catalog(fake_catalog) + valid_spec_dict["boxes"][0]["attachments_add"] = [ + {"kind": "role", "catalog_ref": "software.install.ghost", "params": {}}, + ] + spec = ScenarioSpec.model_validate(valid_spec_dict) + problems = validate_refs(spec, catalog) + assert any("software.install.ghost" in p for p in problems) + + +def test_validate_refs_reports_unknown_container(fake_catalog, valid_spec_dict): + catalog = load_catalog(fake_catalog) + valid_spec_dict["boxes"][0]["attachments_add"] = [ + {"kind": "container", "catalog_ref": "cve/web/ghost", "params": {}}, + ] + spec = ScenarioSpec.model_validate(valid_spec_dict) + problems = validate_refs(spec, catalog) + assert any("cve/web/ghost" in p for p in problems) + + +def test_validate_refs_accepts_known_role_and_container(fake_catalog, valid_spec_dict): + catalog = load_catalog(fake_catalog) + valid_spec_dict["boxes"][0]["attachments_add"] = [ + {"kind": "role", "catalog_ref": "software.install.wazuh", "params": {}}, + {"kind": "container", "catalog_ref": "cve/web/dvwa", "params": {}}, + ] + spec = ScenarioSpec.model_validate(valid_spec_dict) + assert validate_refs(spec, catalog) == [] + + +def test_validate_refs_flags_unknown_template_vm(fake_catalog, spec_factory): + """A box template referencing an unknown template_vm is flagged when 01_image_layer is loaded.""" + catalog = load_catalog(fake_catalog) + catalog.box_templates["ghost-vm-box"] = BoxTemplate( + id="ghost-vm-box", + template_vm="template-vm-does-not-exist", + ) + data = spec_factory(boxes=[{"template": "ghost-vm-box", "subnet": "admin"}]) + spec = ScenarioSpec.model_validate(data) + problems = validate_refs(spec, catalog) + assert any("template-vm-does-not-exist" in p for p in problems) + + +def test_validate_refs_accepts_known_template_vm(fake_catalog, spec_factory): + """A box template referencing a known template_vm resolves cleanly.""" + catalog = load_catalog(fake_catalog) + catalog.box_templates["deb-box"] = BoxTemplate( + id="deb-box", + template_vm="template-vm-debian-trixie-small", + ) + data = spec_factory(boxes=[{"template": "deb-box", "subnet": "student"}]) + spec = ScenarioSpec.model_validate(data) + assert validate_refs(spec, catalog) == [] + + +def test_validate_refs_skips_template_vm_check_when_layer_absent(spec_factory): + """When 01_image_layer is not loaded (images={}), template_vm names are not validated.""" + catalog = Catalog( + subnet_layouts={"layout": object()}, + network_policies={}, + box_templates={ + "any-box": BoxTemplate( + id="any-box", + template_vm="template-vm-unknown", + ) + }, + roles=set(), + containers=set(), + # images is empty — layer not loaded + ) + data = spec_factory(subnet_layout="layout", boxes=[{"template": "any-box", "subnet": "admin"}]) + data.pop("network_policy", None) + spec = ScenarioSpec.model_validate(data) + assert validate_refs(spec, catalog) == [] diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 00000000..15c8bd2f --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,341 @@ +"""S6 Typer CLI tests via CliRunner — the generator frontend. + +The CLI is a thin shell over the frozen ``r42playbooks.api``: ``list`` enumerates +pickable catalog modules, ``show`` describes one, and ``new`` composes a +``ScenarioSpec`` (from flags or ``--spec``) and renders a deployable +``scenarios//`` tree. All business logic lives in the core; the CLI only +parses args, prints, and maps errors to exit codes. +""" + +from typer.testing import CliRunner + +from r42playbooks.cli import app + +runner = CliRunner() + + +# --- list ------------------------------------------------------------------ + + +def test_list_boxes_shows_catalog_box_templates(fake_catalog): + res = runner.invoke(app, ["list", "boxes", "--catalog", str(fake_catalog)]) + assert res.exit_code == 0, res.output + assert "admin-wazuh" in res.output + assert "vuln-box" in res.output + + +def test_list_roles_shows_named_roles(fake_catalog): + res = runner.invoke(app, ["list", "roles", "--catalog", str(fake_catalog)]) + assert res.exit_code == 0, res.output + assert "software.install.wazuh-agent" in res.output + + +def test_list_containers_shows_ctf_stacks(fake_catalog): + res = runner.invoke(app, ["list", "containers", "--catalog", str(fake_catalog)]) + assert res.exit_code == 0, res.output + assert "cve/web/dvwa" in res.output + + +def test_list_subnets_and_policies(fake_catalog): + res = runner.invoke(app, ["list", "subnets", "--catalog", str(fake_catalog)]) + assert res.exit_code == 0 and "default-3zone" in res.output + res = runner.invoke(app, ["list", "policies", "--catalog", str(fake_catalog)]) + assert res.exit_code == 0 and "air-gap-ctf" in res.output + + +def test_list_images_shows_base_images(fake_catalog): + res = runner.invoke(app, ["list", "images", "--catalog", str(fake_catalog)]) + assert res.exit_code == 0, res.output + assert "ubuntu_noble" in res.output and "debian_trixie" in res.output + assert "template(s)" in res.output + + +def test_list_bad_catalog_exits_nonzero(tmp_path): + res = runner.invoke(app, ["list", "boxes", "--catalog", str(tmp_path / "nope")]) + assert res.exit_code != 0 + + +def test_list_scenarios_shows_generated(tmp_path, fake_catalog): + out = tmp_path / "scenarios" + gen = runner.invoke( + app, + [ + "new", + "scen_a", + "--subnet", + "default-3zone", + "--policy", + "air-gap-ctf", + "--box", + "admin-wazuh:subnet=admin", + "--catalog", + str(fake_catalog), + "-o", + str(out), + ], + ) + assert gen.exit_code == 0, gen.output + res = runner.invoke(app, ["list", "scenarios", "-o", str(out)]) + assert res.exit_code == 0, res.output + assert "scen_a" in res.output + assert "default-3zone" in res.output # subnet layout shown in summary + assert "admin-wazuh" in res.output # box template shown in summary + + +# --- show ------------------------------------------------------------------ + + +def test_show_box_template_details(fake_catalog): + res = runner.invoke(app, ["show", "vuln-box", "--catalog", str(fake_catalog)]) + assert res.exit_code == 0, res.output + assert "template-vm-ubuntu-noble-small-01-4g-32g" in res.output # template_vm + assert "ubuntu_noble" in res.output # resolved image + assert "9221" in res.output # resolved vm_id + assert "1cpu/4gb/32gb" in res.output # resolved spec + assert "CTF vulnerable target" in res.output # description + + +def test_show_subnet_layout_details(fake_catalog): + res = runner.invoke(app, ["show", "default-3zone", "--catalog", str(fake_catalog)]) + assert res.exit_code == 0, res.output + assert "admin" in res.output and "192.168.142.0/24" in res.output # lab zones + assert "vmbr140" in res.output # template_subnet bridge + + +def test_show_image_details(fake_catalog): + res = runner.invoke(app, ["show", "ubuntu_noble", "--catalog", str(fake_catalog)]) + assert res.exit_code == 0, res.output + assert "ubuntu/noble" in res.output + assert "noble-minimal-cloudimg" in res.output # cloud_image filename + assert "9221" in res.output # one of the template vm_ids + assert "proxmox_templates" in res.output + + +def test_show_unknown_module_exits_nonzero(fake_catalog): + res = runner.invoke(app, ["show", "does-not-exist", "--catalog", str(fake_catalog)]) + assert res.exit_code != 0 + + +# --- new ------------------------------------------------------------------- + + +def test_new_from_flags_writes_deployable_tree(tmp_path, fake_catalog): + out = tmp_path / "scenarios" + res = runner.invoke( + app, + [ + "new", + "cli_lab", + "--subnet", + "default-3zone", + "--policy", + "air-gap-ctf", + "--box", + "admin-wazuh:subnet=admin", + "--box", + "vuln-box:subnet=ctf,count=3", + "--catalog", + str(fake_catalog), + "-o", + str(out), + ], + ) + assert res.exit_code == 0, res.output + assert ( + "templates" in res.output and "boxes" in res.output + ) # manifest summary printed + root = out / "cli_lab" + assert (root / "main.yml").is_file() + assert (root / "manifest" / "scenario_vms.json").is_file() + assert (root / "scenario.r42.yml").is_file() + # count=3 expanded + assert (root / "04_ctf_infrastructure" / "stage_00" / "ctf-vuln-box-02.yml").is_file() + assert str(root) in res.output + + +def test_new_existing_dir_warns_then_force_overwrites(tmp_path, fake_catalog): + out = tmp_path / "scenarios" + args = [ + "new", + "dup_lab", + "--subnet", + "default-3zone", + "--policy", + "air-gap-ctf", + "--box", + "admin-wazuh:subnet=admin", + "--catalog", + str(fake_catalog), + "-o", + str(out), + ] + assert runner.invoke(app, args).exit_code == 0 + again = runner.invoke(app, args) # second run: refuse + hint + assert again.exit_code != 0 + assert "already exists" in again.output and "--force" in again.output + forced = runner.invoke(app, args + ["--force"]) # --force: overwrite + assert forced.exit_code == 0, forced.output + + +def test_new_bad_box_ref_exits_nonzero(tmp_path, fake_catalog): + out = tmp_path / "scenarios" + res = runner.invoke( + app, + [ + "new", + "bad_lab", + "--subnet", + "default-3zone", + "--policy", + "air-gap-ctf", + "--box", + "no-such-box:subnet=admin", + "--catalog", + str(fake_catalog), + "-o", + str(out), + ], + ) + assert res.exit_code != 0 + assert not (out / "bad_lab").exists() + + +def test_new_from_spec_file_roundtrips(tmp_path, fake_catalog, valid_spec_dict): + import yaml + + spec_path = tmp_path / "compose.r42.yml" + spec_path.write_text(yaml.safe_dump(valid_spec_dict), encoding="utf-8") + out = tmp_path / "scenarios" + res = runner.invoke( + app, + [ + "new", + "from_spec", + "--spec", + str(spec_path), + "--catalog", + str(fake_catalog), + "-o", + str(out), + ], + ) + assert res.exit_code == 0, res.output + # positional name overrides the spec's own name + assert (out / "from_spec" / "main.yml").is_file() + + +def test_new_generated_tree_revalidates(tmp_path, fake_catalog): + """A generated scenario.r42.yml re-loads + re-renders cleanly (no drift).""" + out = tmp_path / "scenarios" + res = runner.invoke( + app, + [ + "new", + "round_lab", + "--subnet", + "default-3zone", + "--policy", + "air-gap-ctf", + "--box", + "admin-wazuh:subnet=admin", + "--catalog", + str(fake_catalog), + "-o", + str(out), + ], + ) + assert res.exit_code == 0, res.output + regen = runner.invoke( + app, + [ + "new", + "round_lab2", + "--spec", + str(out / "round_lab" / "scenario.r42.yml"), + "--catalog", + str(fake_catalog), + "-o", + str(out), + ], + ) + assert regen.exit_code == 0, regen.output + + +# --- validate ------------------------------------------------------------------ + + +def test_validate_valid_spec_exits_zero(tmp_path, fake_catalog, valid_spec_dict): + import yaml + + spec_path = tmp_path / "scenario.r42.yml" + spec_path.write_text(yaml.safe_dump(valid_spec_dict), encoding="utf-8") + res = runner.invoke( + app, ["validate", str(spec_path), "--catalog", str(fake_catalog)] + ) + assert res.exit_code == 0, res.output + assert "valid" in res.output + + +def test_validate_generated_scenario_r42_yml(tmp_path, fake_catalog): + """The scenario.r42.yml written by `new` should pass `validate` cleanly.""" + out = tmp_path / "scenarios" + runner.invoke( + app, + [ + "new", + "val_lab", + "--subnet", + "default-3zone", + "--box", + "admin-wazuh:subnet=admin", + "--catalog", + str(fake_catalog), + "-o", + str(out), + ], + ) + spec_path = out / "val_lab" / "scenario.r42.yml" + res = runner.invoke( + app, ["validate", str(spec_path), "--catalog", str(fake_catalog)] + ) + assert res.exit_code == 0, res.output + + +def test_validate_bad_spec_exits_nonzero(tmp_path, fake_catalog): + import yaml + + bad = { + "schema_version": 1, + "name": "bad", + "subnet_layout": "no-such-layout", + "boxes": [{"template": "vuln-box", "subnet": "ctf"}], + } + spec_path = tmp_path / "bad.r42.yml" + spec_path.write_text(yaml.safe_dump(bad), encoding="utf-8") + res = runner.invoke( + app, ["validate", str(spec_path), "--catalog", str(fake_catalog)] + ) + assert res.exit_code != 0 + assert "no-such-layout" in res.output + + +def test_new_autodetects_reserved_json(tmp_path, fake_catalog): + """When _reserved.json exists in the output dir, `new` uses it without --reserved.""" + import json + + out = tmp_path / "scenarios" + out.mkdir() + entry = { + "vm_id": 1010, "ip": "192.168.144.10", "vm_name": "vuln-box-00", + "subnet": "ctf", "bridge": "vmbr144", "scenario": "other_lab", + } + (out / "_reserved.json").write_text(json.dumps(entry) + "\n", encoding="utf-8") + res = runner.invoke(app, [ + "new", "my_lab", "--subnet", "default-3zone", "--box", "vuln-box:subnet=ctf", + "--catalog", str(fake_catalog), "-o", str(out), + ]) + assert res.exit_code == 0, res.output + manifest = json.loads((out / "my_lab" / "manifest" / "scenario_vms.json").read_text()) + vm_ids = {v["vm_id"] for v in manifest["vms"]} + assert 1010 not in vm_ids, "blocked vm_id 1010 was allocated despite _reserved.json" + assert 1011 in vm_ids diff --git a/tests/test_compiler.py b/tests/test_compiler.py new file mode 100644 index 00000000..e8b51852 --- /dev/null +++ b/tests/test_compiler.py @@ -0,0 +1,140 @@ +"""P3 compiler tests (inventory, scenario_vms, network policy, stages, e2e).""" + +import json + +import pytest +import yaml + +from r42playbooks.core.catalog import load_catalog +from r42playbooks.core.compiler import compile_topology +from r42playbooks.core.compiler.network_policy import compile_network_policy, lint_segmentation +from r42playbooks.core.idalloc import ReservedIndex +from r42playbooks.core.models import Topology + + +@pytest.fixture +def built(topology_factory, fake_catalog, reserved_factory, tmp_path): + t = Topology.model_validate(topology_factory()) + cat = load_catalog(fake_catalog) + reserved = ReservedIndex.from_file(reserved_factory([])) + ws = tmp_path / "ws" + result = compile_topology(t, workspace=ws, catalog=cat, reserved=reserved) + return t, cat, result + + +# --- inventory --- + +def test_inventory_structure(built): + _, _, result = built + inv = yaml.safe_load(result.inventory_path.read_text()) + groups = inv["all"]["children"]["range42_infrastructure"]["children"] + assert "r42_admin_group" in groups + assert "r42_vuln_box_group" in groups + admin_hosts = groups["r42_admin_group"]["hosts"] + assert "r42.admin-wazuh" in admin_hosts + assert admin_hosts["r42.admin-wazuh"]["ansible_host"] == "192.168.142.100" + + +# --- scenario_vms manifest --- + +def test_scenario_vms_manifest(built): + _, _, result = built + man = json.loads(result.scenario_vms_path.read_text()) + assert man["scenario"] == "demo_lab_network" + by_name = {v["vm_name"]: v for v in man["vms"]} + assert by_name["admin-wazuh"]["role"] == "admin" + assert by_name["admin-wazuh"]["bridge"] == "vmbr142" + assert by_name["vuln-box-00"]["role"] == "ctf" + + +# --- network policy compilation --- + +def test_network_policy_ordered_rules(built): + t, cat, _ = built + pol = cat.network_policies["air-gap-ctf"] + compiled = compile_network_policy(t, pol, version="1.1.0") + weights = [r.weight for r in compiled.rules] + assert weights == sorted(weights) # emitted in ascending order + + # established first + assert compiled.rules[0].ctstate and "ESTABLISHED" in compiled.rules[0].ctstate + # admin -> ctf accept + assert any(r.source == "192.168.142.0/24" and r.destination == "192.168.144.0/24" + and r.jump == "ACCEPT" for r in compiled.rules) + # ctf -> siem 1514/1515 accept + siem = [r for r in compiled.rules if r.destination == "192.168.142.100" and r.jump == "ACCEPT"] + assert {r.destination_port for r in siem} == {"1514", "1515"} + # ctf -> admin drop + assert any(r.source == "192.168.144.0/24" and r.destination == "192.168.142.0/24" + and r.jump == "DROP" for r in compiled.rules) + # air-gap: ctf bridge -> wan drop + assert any(r.in_interface == "vmbr144" and r.jump == "DROP" and r.out_interface + for r in compiled.rules) + # terminal default-deny + assert compiled.rules[-1].jump == "DROP" + + +def test_network_policy_deterministic(built): + t, cat, _ = built + pol = cat.network_policies["air-gap-ctf"] + a = compile_network_policy(t, pol, version="1.1.0").model_dump_json() + b = compile_network_policy(t, pol, version="1.1.0").model_dump_json() + assert a == b + + +def test_segmentation_linter_passes_for_air_gap(built): + t, cat, _ = built + pol = cat.network_policies["air-gap-ctf"] + compiled = compile_network_policy(t, pol, version="1.1.0") + assert lint_segmentation(compiled, pol, t) == [] + + +def test_segmentation_linter_flags_shadowed_drop(built): + """An ACCEPT that precedes a same-pair DROP must be flagged (ordering hazard).""" + t, cat, _ = built + pol = cat.network_policies["air-gap-ctf"].model_copy(deep=True) + # add ctf->admin ACCEPT before the existing ctf->admin DROP + from r42playbooks.core.catalog_models import MatrixRule + pol.matrix.insert(0, MatrixRule(src="ctf", dst="admin", action="accept", + comment="bad: opens ctf->admin")) + compiled = compile_network_policy(t, pol, version="1.1.0") + assert lint_segmentation(compiled, pol, t) != [] + + +# --- stages --- + +def test_stages_merge_template_and_box_attachments(built): + _, _, result = built + stages = json.loads(result.stages_path.read_text()) + by_zone = {z["name"]: z for z in stages["zones"]} + ctf_boxes = by_zone["ctf"]["boxes"] + refs = [a["catalog_ref"] for a in ctf_boxes[0]["attachments"]] + # template default for vuln-box includes the wazuh agent + assert "software.install.wazuh-agent" in refs + + +# --- compile_topology orchestration --- + +def test_compile_writes_all_artifacts(built): + _, _, result = built + for p in (result.topology_path, result.inventory_path, + result.scenario_vms_path, result.network_policy_path, result.stages_path): + assert p.exists() + + +def test_compile_topology_roundtrips_topology(built): + t, _, result = built + from r42playbooks.core.io import load_topology + assert load_topology(result.topology_path) == t + + +def test_compile_rejects_allocation_conflict(topology_factory, fake_catalog, + reserved_factory, tmp_path): + from r42playbooks.core.errors import CompileError + t = Topology.model_validate(topology_factory()) + cat = load_catalog(fake_catalog) + reserved = ReservedIndex.from_file(reserved_factory([ + {"vm_id": 1100, "ip": "192.168.142.100", "scenario": "other", "role": "admin"}, + ])) + with pytest.raises(CompileError): + compile_topology(t, workspace=tmp_path / "ws2", catalog=cat, reserved=reserved) diff --git a/tests/test_extravars.py b/tests/test_extravars.py new file mode 100644 index 00000000..c6480f4c --- /dev/null +++ b/tests/test_extravars.py @@ -0,0 +1,47 @@ +"""P3 extravars tests — RED before GREEN.""" + +import pytest + +from r42playbooks.core.catalog import load_catalog +from r42playbooks.core.compiler import compile_topology +from r42playbooks.core.errors import ValidationError +from r42playbooks.core.extravars import resolve_universal_extravars +from r42playbooks.core.idalloc import ReservedIndex +from r42playbooks.core.models import Topology + + +@pytest.fixture +def result(topology_factory, fake_catalog, reserved_factory, tmp_path): + t = Topology.model_validate(topology_factory()) + cat = load_catalog(fake_catalog) + reserved = ReservedIndex.from_file(reserved_factory([])) + return compile_topology(t, workspace=tmp_path / "ws", catalog=cat, reserved=reserved) + + +def test_extravars_has_universal_contract_keys(result): + ev = resolve_universal_extravars(result, deployment_id="dep-1", attempt_id="att-1", + scope="team", team_id="team-a") + assert set(ev) == { + "r42_topology_path", "r42_inventory_dir", "r42_deployment_id", + "r42_attempt_id", "r42_scope", "r42_team_id", + } + assert ev["r42_topology_path"] == str(result.topology_path) + assert ev["r42_inventory_dir"] == str(result.workspace / "inventory") + assert ev["r42_team_id"] == "team-a" + + +def test_extravars_team_id_optional(result): + ev = resolve_universal_extravars(result, deployment_id="d", attempt_id="a", scope="global") + assert ev["r42_team_id"] == "" + + +def test_extravars_rejects_injection_in_ids(result): + with pytest.raises(ValidationError): + resolve_universal_extravars(result, deployment_id="d;rm -rf /", attempt_id="a", + scope="global") + + +def test_extravars_only_allowlisted_keys(result): + # no ansible_* or arbitrary keys can leak through + ev = resolve_universal_extravars(result, deployment_id="d", attempt_id="a", scope="global") + assert not any(k.lower().startswith("ansible") for k in ev) diff --git a/tests/test_idalloc.py b/tests/test_idalloc.py new file mode 100644 index 00000000..fdf20f35 --- /dev/null +++ b/tests/test_idalloc.py @@ -0,0 +1,64 @@ +"""P2 id/IP allocation + reservation tests — RED before GREEN.""" + +from r42playbooks.core.idalloc import ReservedIndex, validate_allocation +from r42playbooks.core.models import Topology + + +def test_valid_topology_has_no_allocation_errors(valid_topology_dict, reserved_factory): + t = Topology.model_validate(valid_topology_dict) + reserved = ReservedIndex.from_file(reserved_factory([])) + report = validate_allocation(t, reserved) + assert report.errors == [] + + +def test_octet_rule_violation_is_error(topology_factory, reserved_factory): + spec = topology_factory() + spec["boxes"][0]["vm_id"] = 1101 # ip ends .100 -> 101 != 100 + t = Topology.model_validate(spec) + report = validate_allocation(t, ReservedIndex.from_file(reserved_factory([]))) + assert any("octet" in e.lower() for e in report.errors) + + +def test_cross_scenario_vm_id_collision_is_error(valid_topology_dict, reserved_factory): + t = Topology.model_validate(valid_topology_dict) + reserved = ReservedIndex.from_file(reserved_factory([ + {"vm_id": 1100, "ip": "192.168.1.1", "scenario": "other_lab", "role": "admin"}, + ])) + report = validate_allocation(t, reserved) + assert any("1100" in e and "other_lab" in e for e in report.errors) + + +def test_same_scenario_reservation_is_not_collision(valid_topology_dict, reserved_factory): + t = Topology.model_validate(valid_topology_dict) # scenario demo_lab_network + reserved = ReservedIndex.from_file(reserved_factory([ + {"vm_id": 1100, "ip": "192.168.142.100", "scenario": "demo_lab_network", "role": "admin"}, + ])) + report = validate_allocation(t, reserved) + assert report.errors == [] # re-deploy of own scenario is fine + + +def test_cross_scenario_ip_collision_is_error(valid_topology_dict, reserved_factory): + t = Topology.model_validate(valid_topology_dict) + reserved = ReservedIndex.from_file(reserved_factory([ + {"vm_id": 5555, "ip": "192.168.142.100", "scenario": "other_lab", "role": "admin"}, + ])) + report = validate_allocation(t, reserved) + assert any("192.168.142.100" in e and "other_lab" in e for e in report.errors) + + +def test_intra_topology_duplicate_vm_id_is_error(topology_factory, reserved_factory): + spec = topology_factory() + spec["boxes"][1]["vm_id"] = spec["boxes"][0]["vm_id"] # dup within topology + spec["boxes"][1]["ip"] = "192.168.144." + str(spec["boxes"][0]["vm_id"] % 1000) + t = Topology.model_validate(spec) + report = validate_allocation(t, ReservedIndex.from_file(reserved_factory([]))) + assert any("duplicate" in e.lower() for e in report.errors) + + +def test_reserved_index_parses_jsonl(reserved_factory): + reserved = ReservedIndex.from_file(reserved_factory([ + {"vm_id": 2001, "ip": "192.168.143.200", "scenario": "a", "role": "team"}, + {"vm_id": 2002, "ip": "192.168.143.201", "scenario": "a", "role": "team"}, + ])) + assert reserved.used_vm_ids() == {2001, 2002} + assert "192.168.143.200" in reserved.used_ips() diff --git a/tests/test_io.py b/tests/test_io.py new file mode 100644 index 00000000..db3ac2fb --- /dev/null +++ b/tests/test_io.py @@ -0,0 +1,32 @@ +"""P1 IO round-trip tests for r42playbooks.core.io — RED before GREEN.""" + +import json + +from r42playbooks.core.io import dump_topology, load_topology +from r42playbooks.core.models import Topology + + +def test_round_trip_preserves_topology(tmp_path, valid_topology_dict): + t = Topology.model_validate(valid_topology_dict) + path = tmp_path / "topology.json" + dump_topology(t, path) + loaded = load_topology(path) + assert loaded == t + + +def test_dump_writes_sorted_deterministic_json(tmp_path, valid_topology_dict): + t = Topology.model_validate(valid_topology_dict) + p1, p2 = tmp_path / "a.json", tmp_path / "b.json" + dump_topology(t, p1) + dump_topology(t, p2) + assert p1.read_text() == p2.read_text() # byte-identical + top = json.loads(p1.read_text()) + assert list(top.keys()) == sorted(top.keys()) # sorted keys + + +def test_load_rejects_unknown_path(tmp_path): + import pytest + + from r42playbooks.core.errors import TopologyError + with pytest.raises(TopologyError): + load_topology(tmp_path / "does-not-exist.json") diff --git a/tests/test_models.py b/tests/test_models.py new file mode 100644 index 00000000..46fb5d02 --- /dev/null +++ b/tests/test_models.py @@ -0,0 +1,62 @@ +"""P1 schema tests for r42playbooks.core.models — RED before GREEN.""" + +import pytest +from pydantic import ValidationError + +from r42playbooks.core.models import Topology + + +def test_valid_topology_parses(valid_topology_dict): + t = Topology.model_validate(valid_topology_dict) + assert t.scenario == "demo_lab_network" + assert len(t.boxes) == 2 + assert t.network_policy.template == "air-gap-ctf" + + +def test_scenario_name_rejects_dots(topology_factory): + # backend resolver regex forbids dots in scenario names + with pytest.raises(ValidationError): + Topology.model_validate(topology_factory(scenario="demo.lab")) + + +def test_scenario_name_rejects_leading_slash(topology_factory): + with pytest.raises(ValidationError): + Topology.model_validate(topology_factory(scenario="/demo_lab")) + + +def test_bridge_pattern_enforced(topology_factory): + spec = topology_factory() + spec["subnets"][0]["bridge"] = "br0" # not vmbrN + with pytest.raises(ValidationError): + Topology.model_validate(spec) + + +def test_vm_id_range_enforced(topology_factory): + spec = topology_factory() + spec["boxes"][0]["vm_id"] = 99 # below 1000 + with pytest.raises(ValidationError): + Topology.model_validate(spec) + + +def test_extra_fields_forbidden(topology_factory): + with pytest.raises(ValidationError): + Topology.model_validate(topology_factory(unexpected="x")) + + +def test_attachment_catalog_ref_allows_dots(valid_topology_dict): + # role names like software.install.wazuh contain dots (unlike scenario names) + t = Topology.model_validate(valid_topology_dict) + assert t.boxes[0].attachments[0].catalog_ref == "software.install.wazuh" + + +@pytest.mark.parametrize("bad", [ + "{{ lookup('pipe','id') }}", + "name;rm -rf /", + "a|b", + "../escape", +]) +def test_denylist_rejects_injection_in_vm_name(topology_factory, bad): + spec = topology_factory() + spec["boxes"][0]["vm_name"] = bad + with pytest.raises(ValidationError): + Topology.model_validate(spec) diff --git a/tests/test_package_api.py b/tests/test_package_api.py new file mode 100644 index 00000000..a7d530b0 --- /dev/null +++ b/tests/test_package_api.py @@ -0,0 +1,53 @@ +"""S8 — the importable package surface. + +A downstream tool (r42deploy/r42runtime) must be able to drive generation by +``import r42playbooks`` alone, without reaching into ``r42playbooks.core`` or +``r42playbooks.api``. These tests pin that top-level re-export contract. +""" + +import r42playbooks as r +from r42playbooks.core.errors import TopologyError + + +def test_package_exports_frozen_generator_surface(): + for name in ( + "render_scenario", "allocate", "load_spec", "dump_spec_atomic", + "load_catalog", "list_images", "list_roles", "list_containers", "validate_refs", + "ScenarioSpec", "Catalog", "Allocation", "ReservedIndex", "__version__", + ): + assert hasattr(r, name), f"r42playbooks.{name} not exported" + + +def test_import_only_generation_roundtrip(fake_catalog, valid_spec_dict, tmp_path): + spec = r.ScenarioSpec.model_validate(valid_spec_dict) + catalog = r.load_catalog(fake_catalog) + assert r.validate_refs(spec, catalog) == [] + root = r.render_scenario(spec, catalog=catalog, dest=tmp_path / "out") + assert (root / "main.yml").is_file() + assert (root / "manifest" / "scenario_vms.json").is_file() + assert (root / "scenario.r42.yml").is_file() + + +def test_load_spec_then_render(fake_catalog, valid_spec_dict, tmp_path): + import yaml + spec_path = tmp_path / "scenario.r42.yml" + spec_path.write_text(yaml.safe_dump(valid_spec_dict), encoding="utf-8") + spec = r.load_spec(spec_path) + catalog = r.load_catalog(fake_catalog) + root = r.render_scenario(spec, catalog=catalog, dest=tmp_path / "out") + assert root.is_dir() + + +def test_unknown_catalog_ref_raises_core_error(fake_catalog, spec_factory, tmp_path): + bad = spec_factory(boxes=[{"template": "ghost-box", "subnet": "admin"}]) + spec = r.ScenarioSpec.model_validate(bad) + catalog = r.load_catalog(fake_catalog) + # validate_refs surfaces it as a message... + assert any("ghost-box" in p for p in r.validate_refs(spec, catalog)) + # ...and render raises the core error hierarchy (not a framework type) + try: + r.render_scenario(spec, catalog=catalog, dest=tmp_path / "out") + raised = None + except TopologyError as exc: + raised = exc + assert raised is not None diff --git a/tests/test_render.py b/tests/test_render.py new file mode 100644 index 00000000..d72ebd01 --- /dev/null +++ b/tests/test_render.py @@ -0,0 +1,345 @@ +"""S5b renderer — class-(B) verbatim-with-param boilerplate. + +Golden-shape assertions against the §4.3 demo_lab baseline: the renderer maps +that shape onto the *composed* scenario name + its actual boxes (it never copies +demo_lab's own VMs). Verifies: the vendored ``01_init_proxmox/`` subtree (H3), +per-box ``stage_00``/``stage_01`` + devkits, top-level scripts, class-B +templates, that ``stage_01`` lists role NAMES (not copied role code), that +``secrets/`` is NOT created, and that demo_lab's group-level richness is NOT +generated (plan §7.2 richness decision). +""" + +from pathlib import Path + +import pytest + +from r42playbooks.core import render_assets +from r42playbooks.core.allocate import allocate +from r42playbooks.core.catalog import load_catalog +from r42playbooks.core.render import render_scenario +from r42playbooks.core.spec import ScenarioSpec, load_spec + + +def test_fill_raises_on_unfilled_sentinel(): + """A misspelled/forgotten key must not silently ship a leftover @@X@@.""" + import pytest + with pytest.raises(RuntimeError): + render_assets.fill("hello @@MISSING@@", PRESENT="x") + + +@pytest.fixture +def rendered(fake_catalog, valid_spec_dict, tmp_path): + """Render the default 2-box composition (admin-wazuh + vuln-box×5) once.""" + spec = ScenarioSpec.model_validate(valid_spec_dict) + catalog = load_catalog(fake_catalog) + alloc = allocate(spec, catalog) + dest = tmp_path / "scenarios" + root = render_scenario(alloc, spec, catalog=catalog, dest=dest) + return spec, alloc, root + + +def _read(root: Path, rel: str) -> str: + return (root / rel).read_text(encoding="utf-8") + + +# --- top-level shape ------------------------------------------------------- + +def test_render_returns_scenario_root_dir(rendered): + spec, _alloc, root = rendered + assert root.is_dir() + assert root.name == spec.name # dest/ + + +def test_init_proxmox_subtree_present(rendered): + """H3: the 01_init_proxmox/ subtree is present; download play is rendered from catalog.""" + _spec, _alloc, root = rendered + init = root / "01_init_proxmox" + assert (init / "_main.yml").is_file() + dl = init / "stage_00-download_cloudinit_files" + assert (dl / "cloudinit_ubuntu_noble.yml").is_file() + download = (dl / "cloudinit_ubuntu_noble.yml").read_text(encoding="utf-8") + assert "noble-minimal-cloudimg-amd64.img" in download # URL comes from catalog + assert (init / "stage_01-create_templates" / "templates" + / "ubuntu_noble" / "_main_ubuntu_noble.yml").is_file() + + +def test_main_yml_imports_only_emitted_sections(rendered): + """main.yml wires 01_init_proxmox + the sections that actually have boxes.""" + _spec, _alloc, root = rendered + main = _read(root, "main.yml") + assert "./01_init_proxmox/_main.yml" in main + assert "./02_admin_infrastructure/_main.yml" in main + assert "./04_ctf_infrastructure/_main.yml" in main + # no student box was composed -> never import a section that wasn't emitted + assert "03_student_infrastructure" not in main + + +def test_main_yml_imports_init_proxmox(rendered): + """main.yml runs the staged 01_init_proxmox orchestrator then the sections.""" + _spec, _alloc, root = rendered + main = _read(root, "main.yml") + assert "- import_playbook: ./01_init_proxmox/_main.yml" in main + + +def test_init_proxmox_is_os_selective_ubuntu_only(rendered): + """A Ubuntu-only lab carries ONLY ubuntu_noble + downloads only ubuntu images.""" + _spec, _alloc, root = rendered + init = root / "01_init_proxmox" + templates = init / "stage_01-create_templates" / "templates" + assert (templates / "ubuntu_noble").is_dir() + assert not (templates / "debian_trixie").exists() # no inert debian template files + dl = init / "stage_00-download_cloudinit_files" + assert (dl / "cloudinit_ubuntu_noble.yml").is_file() + assert not (dl / "cloudinit_debian_trixie.yml").exists() # no unused debian image downloaded + # the stage orchestrators import only ubuntu + stage01_main = _read(root, "01_init_proxmox/stage_01-create_templates/_main.yml") + assert "ubuntu_noble/_main_ubuntu_noble.yml" in stage01_main + assert "debian" not in stage01_main + + +def test_top_level_scripts_present_and_named_for_scenario(rendered): + spec, _alloc, root = rendered + n = spec.name + for rel in ( + "_activate.sh", + f"{n}.setup.sh", + f"{n}.setup_vms_only.sh", + f"{n}.delete_all.sh", + f"{n}.delete_vms_only.sh", + f"{n}.reset.setup.sh", + f"{n}.reset.ssh_keys.sh", + "devkit_ansible.show_ansible_inventory.to.text.sh", + "README.md", + "scenario.r42.yml", + ): + assert (root / rel).is_file(), f"missing top-level file: {rel}" + + +def test_class_b_templates_present_and_parametrised(rendered): + spec, _alloc, root = rendered + assert (root / "templates" / "vault-example.yml").is_file() + ansible_vars = _read(root, "templates/ansible-vars.yml") + assert f'INFRASTRUCTURE_SCENARIO: "{spec.name}"' in ansible_vars + + +# --- per-section / per-box shape ------------------------------------------ + +def test_sections_match_composed_roles(rendered): + """admin box -> 02_admin, ctf box -> 04_ctf; no student section emitted (subnet-driven).""" + _spec, _alloc, root = rendered + assert (root / "02_admin_infrastructure").is_dir() + assert (root / "04_ctf_infrastructure").is_dir() + assert not (root / "03_student_infrastructure").exists() + for section in ("02_admin_infrastructure", "04_ctf_infrastructure"): + assert (root / section / "_main.reinstall.sh").is_file() + + +def test_each_box_has_stage00_and_stage01_playbooks(rendered): + _spec, alloc, root = rendered + for box in alloc.boxes: + section = box.section + assert (root / section / "stage_00" / f"{box.vm_name}.yml").is_file() + assert (root / section / "stage_01" / f"{box.vm_name}.yml").is_file() + + +def test_stage00_clone_is_parametrised_boilerplate(rendered): + """stage_00 clones via the proxmox controller and references the secrets symlink.""" + _spec, _alloc, root = rendered + stage00 = _read(root, "04_ctf_infrastructure/stage_00/ctf-vuln-box-00.yml") + assert "- hosts: proxmox" in stage00 + assert "{{ global_template_vm_id }}" in stage00 + assert "{{ global_vm_name }}" in stage00 + # C1: references the deploy-time secrets symlink, never creates it + assert "../../secrets/default_vault.yml" in stage00 + + +def test_stage01_lists_role_names_not_copied_code(rendered): + """The contract (§2): stage_01 references catalog roles BY NAME only.""" + _spec, _alloc, root = rendered + stage01 = _read(root, "04_ctf_infrastructure/stage_01/ctf-vuln-box-00.yml") + assert "roles:" in stage01 + assert "software.install.wazuh-agent" in stage01 # default_attachment + assert "software.install.extra" in stage01 # spec attachments_add + assert "hosts: r42.ctf-vuln-box-00" in stage01 # M5 naming contract + # no role *code* was vendored (a real role would carry tasks/handlers) + assert "include_role:" not in stage01 + assert "ansible.builtin." not in stage01 + + +def test_stage01_emits_attachment_params_as_vars(fake_catalog, spec_factory, tmp_path): + """An attachment's params (e.g. firewall_rules) land in its stage_01 play's vars.""" + spec = ScenarioSpec.model_validate(spec_factory(boxes=[{ + "template": "vuln-box", + "subnet": "ctf", + "attachments_add": [{ + "kind": "role", "catalog_ref": "software.configure.firewalls", + "params": {"firewall_rules": [{"ip": "all", "port": 8080, "protocol": "tcp"}]}, + }], + }])) + root = render_scenario(allocate(spec, load_catalog(fake_catalog)), spec, catalog=load_catalog(fake_catalog), dest=tmp_path / "s") + stage01 = _read(root, "04_ctf_infrastructure/stage_01/ctf-vuln-box-00.yml") + assert "- software.configure.firewalls" in stage01 + assert "firewall_rules:" in stage01 + assert "port: 8080" in stage01 + + +def test_stage01_container_attachment_emits_docker_compose_play(fake_catalog, spec_factory, tmp_path): + """A container attachment renders a docker-compose play wiring the stack.""" + spec = ScenarioSpec.model_validate(spec_factory(boxes=[{ + "template": "vuln-box", + "subnet": "ctf", + "attachments_add": [{"kind": "container", "catalog_ref": "cve/web/dvwa", "params": {}}], + }])) + root = render_scenario(allocate(spec, load_catalog(fake_catalog)), spec, catalog=load_catalog(fake_catalog), dest=tmp_path / "s") + stage01 = _read(root, "04_ctf_infrastructure/stage_01/ctf-vuln-box-00.yml") + assert "- software.configure.docker-compose" in stage01 + assert "cve/web/dvwa" in stage01 # stack path wired + assert "RANGE42_INVENTORY__DOCKER__CTF" in stage01 # env-based project dir + assert "LABEL_PROJECT_NAME: dvwa" in stage01 + + +def test_stage01_without_roles_is_valid_noop_play(rendered): + """A box with no role attachments gets a deployable no-op play, NOT a bare []. + + `_main.yml` imports stage_01 via import_playbook, which rejects `[]` with + "a play definition must contain exactly one of hosts/import_playbook/roles/tasks". + """ + import yaml + _spec, _alloc, root = rendered + text = _read(root, "02_admin_infrastructure/stage_01/admin-admin-wazuh-00.yml") + plays = yaml.safe_load(text) # a playbook is one doc: a list of plays + assert isinstance(plays, list) and len(plays) == 1 + play = plays[0] + assert play["hosts"] == "r42.admin-admin-wazuh-00" + assert play.get("tasks") == [] # valid no-op + assert "roles" not in play # no roles attached + + +def test_stage01_renders_box_vars(rendered): + """Box `vars` from the spec are emitted into the stage_01 play (not dropped).""" + _spec, _alloc, root = rendered + # valid_spec_dict sets vars={"difficulty": "hard"} on vuln-box + stage01 = _read(root, "04_ctf_infrastructure/stage_01/ctf-vuln-box-00.yml") + assert "vars:" in stage01 + assert "difficulty: hard" in stage01 + + +def test_scenario_name_with_slash_keeps_files_in_leaf(fake_catalog, valid_spec_dict, tmp_path): + """A '/'-nested scenario name nests the dir but file prefixes use the leaf.""" + from r42playbooks.core.spec import ScenarioSpec + from r42playbooks.core.catalog import load_catalog + from r42playbooks.core.allocate import allocate + from r42playbooks.core.render import render_scenario + spec = ScenarioSpec.model_validate({**valid_spec_dict, "name": "ctf/web1"}) + catalog = load_catalog(fake_catalog) + root = render_scenario(allocate(spec, catalog), spec, catalog=catalog, dest=tmp_path / "scenarios") + assert root == tmp_path / "scenarios" / "ctf" / "web1" + assert (root / "web1.setup.sh").is_file() # leaf prefix, not ctf/web1.setup.sh + assert not (root / "ctf").exists() # no extra nested dir inside root + + +def test_each_box_has_devkit_scripts(rendered): + spec, _alloc, root = rendered + devkit = root / "04_ctf_infrastructure" / "stage_01" / "ctf-vuln-box-00.devkit" + assert (devkit / f"{spec.name}.ctf-vuln-box-00.install.sh").is_file() + assert (devkit / f"{spec.name}.ctf-vuln-box-00.snapshot.sh").is_file() + assert (devkit / f"{spec.name}.ctf-vuln-box-00.revert.sh").is_file() + + +# --- negative assertions (decision boundaries) ---------------------------- + +def test_secrets_dir_is_not_created(rendered): + """§4.2 / C1: secrets/ is a deploy-time symlink, never generated.""" + _spec, _alloc, root = rendered + assert not (root / "secrets").exists() + + +def test_no_group_level_richness_generated(rendered): + """Plan §7.2: group playbooks, group devkits, _testing/, builder_* are NOT generated.""" + _spec, _alloc, root = rendered + files = [str(p.relative_to(root)) for p in root.rglob("*")] + assert not any("_testing" in f for f in files) + assert not any("builder_" in f for f in files) + # no group playbook like _r42_admin_group.yml / _r42_ctf_group.yml + assert not any(Path(f).name.startswith("_r42_") and f.endswith(".yml") for f in files) + + +# --- reproducibility ------------------------------------------------------- + +def test_scenario_spec_roundtrips_into_tree(rendered): + spec, _alloc, root = rendered + reloaded = load_spec(root / "scenario.r42.yml") + assert reloaded.name == spec.name + assert reloaded.subnet_layout == spec.subnet_layout + assert [b.template for b in reloaded.boxes] == [b.template for b in spec.boxes] + + +def test_init_proxmox_is_os_selective_debian(fake_catalog, tmp_path): + """A Debian lab carries ONLY debian_trixie/ + downloads only the trixie image.""" + # add a debian_trixie box to the catalog + layer = fake_catalog / "05_topology_layer" / "box_templates" / "deb-box" / "v1.0.0" + layer.mkdir(parents=True) + (layer / "template.yml").write_text( + "id: deb-box\napi_version: 1\n" + "template_vm: \"template-vm-debian-trixie-small\"\n", encoding="utf-8", + ) + spec = ScenarioSpec.model_validate({ + "name": "deb_lab", "subnet_layout": "default-3zone", "boxes": [{"template": "deb-box", "subnet": "student"}], + }) + root = render_scenario(allocate(spec, load_catalog(fake_catalog)), spec, catalog=load_catalog(fake_catalog), dest=tmp_path / "s") + init = root / "01_init_proxmox" + templates = init / "stage_01-create_templates" / "templates" + assert (templates / "debian_trixie").is_dir() + assert not (templates / "ubuntu_noble").exists() # no inert ubuntu template files + stage01_main = _read(root, "01_init_proxmox/stage_01-create_templates/_main.yml") + assert "debian_trixie/_main_debian_trixie.yml" in stage01_main + assert "ubuntu_noble" not in stage01_main + dl = init / "stage_00-download_cloudinit_files" + assert (dl / "cloudinit_debian_trixie.yml").is_file() + assert not (dl / "cloudinit_ubuntu_noble.yml").exists() # no unused ubuntu image + download = _read(root, "01_init_proxmox/stage_00-download_cloudinit_files/cloudinit_debian_trixie.yml") + assert "debian-13-genericcloud" in download + assert "noble" not in download + + +def test_render_refuses_existing_dir_then_overwrites(fake_catalog, valid_spec_dict, tmp_path): + """A second render into the same dir must not silently clobber (overwrite gate).""" + import pytest + from r42playbooks.core.errors import ScenarioExistsError + spec = ScenarioSpec.model_validate(valid_spec_dict) + catalog = load_catalog(fake_catalog) + alloc = allocate(spec, catalog) + dest = tmp_path / "scenarios" + render_scenario(alloc, spec, catalog=catalog, dest=dest) + with pytest.raises(ScenarioExistsError): + render_scenario(alloc, spec, catalog=catalog, dest=dest) # default: refuse + root = render_scenario(alloc, spec, catalog=catalog, dest=dest, overwrite=True) # explicit: ok + assert (root / "main.yml").is_file() + + +def test_render_overwrite_removes_stale_files(fake_catalog, valid_spec_dict, tmp_path): + """--force must clean the old tree; stale files from a prior run must not survive.""" + spec = ScenarioSpec.model_validate(valid_spec_dict) + catalog = load_catalog(fake_catalog) + alloc = allocate(spec, catalog) + dest = tmp_path / "scenarios" + root = render_scenario(alloc, spec, catalog=catalog, dest=dest) + stale = root / "01_init_proxmox" / "00-old-template-vm-ubuntu.yml" + stale.write_text("# stale artifact from a previous generator run\n") + render_scenario(alloc, spec, catalog=catalog, dest=dest, overwrite=True) + assert not stale.exists(), "stale file from prior run survived --force overwrite" + assert (root / "main.yml").is_file() + + +def test_render_is_deterministic(fake_catalog, valid_spec_dict, tmp_path): + """Two renders of the same composition produce byte-identical class-B files.""" + spec = ScenarioSpec.model_validate(valid_spec_dict) + catalog = load_catalog(fake_catalog) + alloc = allocate(spec, catalog) + a = render_scenario(alloc, spec, catalog=catalog, dest=tmp_path / "a") + b = render_scenario(alloc, spec, catalog=catalog, dest=tmp_path / "b") + files_a = sorted(p.relative_to(a).as_posix() for p in a.rglob("*") if p.is_file()) + files_b = sorted(p.relative_to(b).as_posix() for p in b.rglob("*") if p.is_file()) + assert files_a == files_b + for rel in files_a: + assert (a / rel).read_bytes() == (b / rel).read_bytes(), f"non-deterministic: {rel}" diff --git a/tests/test_render_classa.py b/tests/test_render_classa.py new file mode 100644 index 00000000..c02f33d2 --- /dev/null +++ b/tests/test_render_classa.py @@ -0,0 +1,140 @@ +"""S5a renderer — class-(A) manifest-derived artifacts + frozen public API. + +These files must reflect *this* composition (never copied from demo_lab): +``manifest/scenario_vms.json``, ``templates/ansible-inventory.j2`` (groups + +member hosts), ``templates/ssh-config.j2`` (a Host block per VM), and each +section's ``_main.yml`` (stage imports + per-VM ``global_*`` overrides). Also +asserts the frozen ``r42playbooks.api`` surface that gates S6 ∥ S7, and the M5 +consistency invariant: every concrete stage ``hosts:`` resolves in the inventory. +""" + +import json +import re +from pathlib import Path + +import pytest +import yaml + +import r42playbooks.api as api +from r42playbooks.core.allocate import allocate +from r42playbooks.core.catalog import load_catalog +from r42playbooks.core.render import render_scenario +from r42playbooks.core.spec import ScenarioSpec + + +@pytest.fixture +def rendered(fake_catalog, valid_spec_dict, tmp_path): + spec = ScenarioSpec.model_validate(valid_spec_dict) + catalog = load_catalog(fake_catalog) + alloc = allocate(spec, catalog) + root = render_scenario(alloc, spec, catalog=catalog, dest=tmp_path / "scenarios") + return spec, alloc, root + + +def _read(root: Path, rel: str) -> str: + return (root / rel).read_text(encoding="utf-8") + + +# --- manifest -------------------------------------------------------------- + +def test_manifest_written_with_vms_and_templates(rendered): + _spec, alloc, root = rendered + manifest = json.loads(_read(root, "manifest/scenario_vms.json")) + vm_ids = {v["vm_id"] for v in manifest["vms"]} + assert vm_ids == {b.vm_id for b in alloc.boxes} + # H1: templates[] is populated (stage_00 global_template_vm_id depends on it) + assert manifest["templates"], "templates[] must not be empty" + assert manifest["scenario"] == alloc.scenario + + +# --- inventory (.j2) ------------------------------------------------------- + +def test_inventory_lists_composed_hosts_under_their_groups(rendered): + _spec, _alloc, root = rendered + inv = _read(root, "templates/ansible-inventory.j2") + assert "r42_admin_group:" in inv + assert "r42_ctf_group:" in inv + assert "r42.admin-admin-wazuh-00:" in inv + assert "r42.ctf-vuln-box-00:" in inv + assert "r42.ctf-vuln-box-04:" in inv + assert "proxmox:" in inv and "proxmox-cli:" in inv + + +def test_inventory_excludes_demo_lab_specific_hosts(rendered): + """Class-A files reflect THIS composition, not demo_lab's 10 VMs.""" + _spec, _alloc, root = rendered + inv = _read(root, "templates/ansible-inventory.j2") + for ghost in ("student-box-01", "init-vm", "deployer", "builder"): + assert ghost not in inv + + +def test_inventory_is_valid_yaml_when_jinja_neutralised(rendered): + """The .j2 is YAML-shaped (Jinja only in proxmox host values).""" + _spec, _alloc, root = rendered + inv = _read(root, "templates/ansible-inventory.j2") + neutral = re.sub(r"\{\{.*?\}\}", "PLACEHOLDER", inv) + data = yaml.safe_load(neutral) + groups = data["all"]["children"]["range42_infrastructure"]["children"] + assert "r42_admin_group" in groups and "r42_ctf_group" in groups + + +# --- ssh-config (.j2) ------------------------------------------------------ + +def test_ssh_config_has_one_block_per_vm(rendered): + _spec, alloc, root = rendered + ssh = _read(root, "templates/ssh-config.j2") + for box in alloc.boxes: + assert f"Host r42.{box.vm_name}\n Hostname {box.ip}" in ssh + assert "192.168.143.160" not in ssh # demo_lab student box IP + + +# --- section _main.yml ----------------------------------------------------- + +def test_section_main_imports_stages_with_global_vars(rendered): + _spec, alloc, root = rendered + main = _read(root, "04_ctf_infrastructure/_main.yml") + assert "- import_playbook: ./stage_00/ctf-vuln-box-00.yml" in main + assert "- import_playbook: ./stage_01/ctf-vuln-box-00.yml" in main + vb0 = next(b for b in alloc.boxes if b.vm_name == "ctf-vuln-box-00") + assert f"global_vm_id: {vb0.vm_id}" in main + assert f'global_vm_ci_ip: "{vb0.ip}"' in main + assert f"global_template_vm_id: {vb0.template_vm_id}" in main + assert 'global_vm_ssh_name: "r42.ctf-vuln-box-00"' in main + + +def test_section_main_is_valid_yaml(rendered): + _spec, _alloc, root = rendered + for section in ("02_admin_infrastructure", "04_ctf_infrastructure"): + docs = list(yaml.safe_load_all(_read(root, f"{section}/_main.yml"))) + plays = [d for d in docs if d] + assert plays, f"{section}/_main.yml parsed empty" + + +# --- M5 consistency invariant --------------------------------------------- + +def test_every_stage01_host_resolves_in_inventory(rendered): + """Golden-assert: each concrete `hosts: r42.` exists in the inventory.""" + _spec, _alloc, root = rendered + inv = _read(root, "templates/ansible-inventory.j2") + host_lines = set(re.findall(r"^\s*(r42\.[a-z0-9-]+):\s*$", inv, re.MULTILINE)) + for stage01 in root.rglob("stage_01/*.yml"): + for host in re.findall(r"hosts:\s*(r42\.[a-z0-9-]+)", stage01.read_text()): + assert host in host_lines, f"{stage01.name}: {host} missing from inventory" + + +# --- frozen public API ----------------------------------------------------- + +def test_api_render_scenario_signature_and_output(fake_catalog, valid_spec_dict, tmp_path): + spec = ScenarioSpec.model_validate(valid_spec_dict) + catalog = load_catalog(fake_catalog) + root = api.render_scenario(spec, catalog=catalog, dest=tmp_path / "out") + assert isinstance(root, Path) + assert root.is_dir() + assert (root / "manifest" / "scenario_vms.json").is_file() + assert (root / "templates" / "ansible-inventory.j2").is_file() + + +def test_api_exposes_frozen_surface(): + for name in ("render_scenario", "allocate", "load_spec", "load_catalog", + "list_roles", "list_containers", "validate_refs", "ScenarioSpec"): + assert hasattr(api, name), f"api.{name} missing (frozen surface)" diff --git a/tests/test_render_network_isolation.py b/tests/test_render_network_isolation.py new file mode 100644 index 00000000..fa23cb56 --- /dev/null +++ b/tests/test_render_network_isolation.py @@ -0,0 +1,357 @@ +"""Tests for the 05_network_isolation render stage. + +Covers: + - render_scenario emits / skips 05_network_isolation/ based on spec.network_policy + - main.yml / main_vms_only.yml include / exclude the isolation import + - isolation playbook targets proxmox-cli, creates + flushes R42-FORWARD + - rules appear in weight order (ESTABLISHED before DROP) + - compile_network_policy_from_alloc produces correct rules for air-gap-ctf +""" + +import pytest + +from r42playbooks.core.allocate import allocate, Allocation +from r42playbooks.core.catalog import load_catalog +from r42playbooks.core.compiler.network_policy import ( + compile_network_policy_from_alloc, + W_ESTABLISHED, + W_SERVICE_ACCEPT, + W_INTRA, + W_AIRGAP, + W_DEFAULT, +) +from r42playbooks.core.models import Subnet +from r42playbooks.core.render import render_scenario +from r42playbooks.core.spec import ScenarioSpec + + +# --------------------------------------------------------------------------- +# fixtures +# --------------------------------------------------------------------------- + +@pytest.fixture +def cat(fake_catalog): + return load_catalog(fake_catalog) + + +@pytest.fixture +def rendered_with_policy(fake_catalog, valid_spec_dict, tmp_path): + """Render with network_policy: air-gap-ctf (already set in valid_spec_dict).""" + spec = ScenarioSpec.model_validate(valid_spec_dict) + catalog = load_catalog(fake_catalog) + alloc = allocate(spec, catalog) + root = render_scenario(alloc, spec, catalog=catalog, dest=tmp_path / "s") + return spec, alloc, root + + +@pytest.fixture +def rendered_no_policy(fake_catalog, valid_spec_dict, tmp_path): + """Render with network_policy explicitly None.""" + spec = ScenarioSpec.model_validate({**valid_spec_dict, "network_policy": None}) + catalog = load_catalog(fake_catalog) + alloc = allocate(spec, catalog) + root = render_scenario(alloc, spec, catalog=catalog, dest=tmp_path / "s") + return spec, alloc, root + + +# --------------------------------------------------------------------------- +# no-policy cases +# --------------------------------------------------------------------------- + +def test_no_policy_no_isolation_dir(rendered_no_policy): + _, _, root = rendered_no_policy + assert not (root / "05_network_isolation").exists() + + +def test_no_policy_main_yml_excludes_isolation(rendered_no_policy): + _, _, root = rendered_no_policy + assert "05_network_isolation" not in (root / "main.yml").read_text() + + +def test_no_policy_main_vms_only_excludes_isolation(rendered_no_policy): + _, _, root = rendered_no_policy + assert "05_network_isolation" not in (root / "main_vms_only.yml").read_text() + + +# --------------------------------------------------------------------------- +# with-policy: directory + main.yml wiring +# --------------------------------------------------------------------------- + +def test_policy_creates_isolation_main_yml(rendered_with_policy): + _, _, root = rendered_with_policy + assert (root / "05_network_isolation" / "_main.yml").is_file() + + +def test_policy_main_yml_includes_isolation(rendered_with_policy): + _, _, root = rendered_with_policy + assert "- import_playbook: ./05_network_isolation/_main.yml" in (root / "main.yml").read_text() + + +def test_policy_main_vms_only_includes_isolation(rendered_with_policy): + _, _, root = rendered_with_policy + assert "- import_playbook: ./05_network_isolation/_main.yml" in (root / "main_vms_only.yml").read_text() + + +def test_isolation_import_after_vm_sections(rendered_with_policy): + """Isolation import must follow all VM infrastructure section imports in main.yml.""" + _, _, root = rendered_with_policy + main = (root / "main.yml").read_text() + last_infra = max( + main.rfind("_infrastructure/_main.yml"), + main.rfind("./01_init_proxmox/_main.yml"), + ) + isolation_pos = main.find("05_network_isolation/_main.yml") + assert isolation_pos > last_infra + + +# --------------------------------------------------------------------------- +# isolation playbook content +# --------------------------------------------------------------------------- + +def test_isolation_targets_proxmox_cli(rendered_with_policy): + _, _, root = rendered_with_policy + text = (root / "05_network_isolation" / "_main.yml").read_text() + assert "hosts: proxmox-cli" in text + + +def test_isolation_references_vault(rendered_with_policy): + _, _, root = rendered_with_policy + text = (root / "05_network_isolation" / "_main.yml").read_text() + assert "../../secrets/default_vault.yml" in text + + +def test_isolation_creates_r42_forward_chain(rendered_with_policy): + _, _, root = rendered_with_policy + text = (root / "05_network_isolation" / "_main.yml").read_text() + assert "chain: R42-FORWARD" in text + assert "chain_management: true" in text + + +def test_isolation_flushes_r42_forward(rendered_with_policy): + _, _, root = rendered_with_policy + text = (root / "05_network_isolation" / "_main.yml").read_text() + assert "flush: true" in text + + +def test_isolation_hooks_into_forward(rendered_with_policy): + _, _, root = rendered_with_policy + text = (root / "05_network_isolation" / "_main.yml").read_text() + assert "chain: FORWARD" in text + assert "jump: R42-FORWARD" in text + assert "action: insert" in text + + +def test_isolation_established_rule_present(rendered_with_policy): + _, _, root = rendered_with_policy + assert "ESTABLISHED" in (root / "05_network_isolation" / "_main.yml").read_text() + + +def test_isolation_drop_rule_ctf_to_admin(rendered_with_policy): + """air-gap-ctf: ctf (192.168.144.0/24) -> admin (192.168.142.0/24) must DROP.""" + _, _, root = rendered_with_policy + text = (root / "05_network_isolation" / "_main.yml").read_text() + assert "192.168.144.0/24" in text + assert "jump: DROP" in text + + +def test_isolation_terminal_default_drop(rendered_with_policy): + """air-gap-ctf defaults to drop — terminal comment must reflect that.""" + _, _, root = rendered_with_policy + text = (root / "05_network_isolation" / "_main.yml").read_text() + assert "r42: default drop" in text + + +def test_isolation_established_before_drops(rendered_with_policy): + """ESTABLISHED rule (w=0) must appear earlier in the file than the first DROP task.""" + _, _, root = rendered_with_policy + text = (root / "05_network_isolation" / "_main.yml").read_text() + established_pos = text.find("ESTABLISHED") + drop_pos = text.find("jump: DROP") + assert 0 < established_pos < drop_pos + + +def test_isolation_policy_id_and_version_in_header(rendered_with_policy): + """The generated playbook header names the policy id and the highest version.""" + _, _, root = rendered_with_policy + text = (root / "05_network_isolation" / "_main.yml").read_text() + assert "air-gap-ctf" in text + assert "1.1.0" in text # fake_catalog has v1.0.0 + v1.1.0; loader picks highest + + +def test_isolation_is_deterministic(fake_catalog, valid_spec_dict, tmp_path): + """Two renders of the same spec produce byte-identical isolation playbooks.""" + spec = ScenarioSpec.model_validate(valid_spec_dict) + catalog = load_catalog(fake_catalog) + alloc = allocate(spec, catalog) + a = render_scenario(alloc, spec, catalog=catalog, dest=tmp_path / "a") + b = render_scenario(alloc, spec, catalog=catalog, dest=tmp_path / "b") + iso_a = (a / "05_network_isolation" / "_main.yml").read_bytes() + iso_b = (b / "05_network_isolation" / "_main.yml").read_bytes() + assert iso_a == iso_b + + +# --------------------------------------------------------------------------- +# teardown.yml +# --------------------------------------------------------------------------- + +def test_policy_creates_teardown_yml(rendered_with_policy): + _, _, root = rendered_with_policy + assert (root / "05_network_isolation" / "teardown.yml").is_file() + + +def test_no_policy_no_teardown_yml(rendered_no_policy): + _, _, root = rendered_no_policy + assert not (root / "05_network_isolation" / "teardown.yml").exists() + + +def test_teardown_targets_proxmox_cli(rendered_with_policy): + _, _, root = rendered_with_policy + text = (root / "05_network_isolation" / "teardown.yml").read_text() + assert "hosts: proxmox-cli" in text + + +def test_teardown_references_vault(rendered_with_policy): + _, _, root = rendered_with_policy + text = (root / "05_network_isolation" / "teardown.yml").read_text() + assert "../../secrets/default_vault.yml" in text + + +def test_teardown_unhooks_and_deletes_chain(rendered_with_policy): + _, _, root = rendered_with_policy + text = (root / "05_network_isolation" / "teardown.yml").read_text() + assert "chain: FORWARD" in text + assert "state: absent" in text + assert "chain_management: true" in text + assert "flush: true" in text + assert "ignore_errors: true" in text + + +def test_teardown_contains_scenario_name(rendered_with_policy): + spec, _, root = rendered_with_policy + text = (root / "05_network_isolation" / "teardown.yml").read_text() + assert spec.name in text + + +# --------------------------------------------------------------------------- +# compile_network_policy_from_alloc unit tests +# --------------------------------------------------------------------------- + +def _bare_alloc(subnets: list[Subnet]) -> Allocation: + """Minimal Allocation with only subnets populated.""" + return Allocation( + scenario="test", description="", boxes=(), templates=(), subnets=tuple(subnets) + ) + + +_SUBNETS = [ + Subnet(name="admin", cidr="192.168.142.0/24", bridge="vmbr142", gateway="192.168.142.1"), + Subnet(name="ctf", cidr="192.168.144.0/24", bridge="vmbr144"), +] + + +def test_compile_from_alloc_rules_sorted_by_weight(cat): + policy = cat.network_policies["air-gap-ctf"] + version = cat.resolved_version("network_policies", "air-gap-ctf") + compiled = compile_network_policy_from_alloc(_bare_alloc(_SUBNETS), policy, version=version) + weights = [r.weight for r in compiled.rules] + assert weights == sorted(weights) + + +def test_compile_from_alloc_established_first(cat): + policy = cat.network_policies["air-gap-ctf"] + version = cat.resolved_version("network_policies", "air-gap-ctf") + compiled = compile_network_policy_from_alloc(_bare_alloc(_SUBNETS), policy, version=version) + first = compiled.rules[0] + assert first.ctstate == "ESTABLISHED,RELATED" + assert first.jump == "ACCEPT" + assert first.weight == W_ESTABLISHED + + +def test_compile_from_alloc_admin_to_ctf_accept(cat): + policy = cat.network_policies["air-gap-ctf"] + version = cat.resolved_version("network_policies", "air-gap-ctf") + compiled = compile_network_policy_from_alloc(_bare_alloc(_SUBNETS), policy, version=version) + assert any( + r.source == "192.168.142.0/24" and r.destination == "192.168.144.0/24" and r.jump == "ACCEPT" + for r in compiled.rules + ) + + +def test_compile_from_alloc_ctf_to_admin_drop(cat): + policy = cat.network_policies["air-gap-ctf"] + version = cat.resolved_version("network_policies", "air-gap-ctf") + compiled = compile_network_policy_from_alloc(_bare_alloc(_SUBNETS), policy, version=version) + assert any( + r.source == "192.168.144.0/24" and r.destination == "192.168.142.0/24" and r.jump == "DROP" + for r in compiled.rules + ) + + +def test_compile_from_alloc_siem_service_rules(cat): + """ctf -> svc:siem must produce ACCEPT rules for ports 1514 + 1515.""" + policy = cat.network_policies["air-gap-ctf"] + version = cat.resolved_version("network_policies", "air-gap-ctf") + compiled = compile_network_policy_from_alloc(_bare_alloc(_SUBNETS), policy, version=version) + siem_rules = [ + r for r in compiled.rules + if r.destination == "192.168.142.100" and r.jump == "ACCEPT" + ] + assert {r.destination_port for r in siem_rules} == {"1514", "1515"} + assert all(r.weight == W_SERVICE_ACCEPT for r in siem_rules) + + +def test_compile_from_alloc_airgap_ctf_wan_drop(cat): + """air-gap: ctf bridge (vmbr144) -> wan (vmbr0) must DROP.""" + policy = cat.network_policies["air-gap-ctf"] + version = cat.resolved_version("network_policies", "air-gap-ctf") + compiled = compile_network_policy_from_alloc( + _bare_alloc(_SUBNETS), policy, version=version, wan_interface="vmbr0" + ) + assert any( + r.in_interface == "vmbr144" and r.out_interface == "vmbr0" + and r.jump == "DROP" and r.weight == W_AIRGAP + for r in compiled.rules + ) + + +def test_compile_from_alloc_terminal_default_drop(cat): + policy = cat.network_policies["air-gap-ctf"] + version = cat.resolved_version("network_policies", "air-gap-ctf") + compiled = compile_network_policy_from_alloc(_bare_alloc(_SUBNETS), policy, version=version) + terminal = compiled.rules[-1] + assert terminal.weight == W_DEFAULT + assert terminal.jump == "DROP" + + +def test_compile_from_alloc_intra_zone_rules(cat): + """allow_intra_zone: each resolved zone gets a same-CIDR ACCEPT rule.""" + policy = cat.network_policies["air-gap-ctf"] + version = cat.resolved_version("network_policies", "air-gap-ctf") + compiled = compile_network_policy_from_alloc(_bare_alloc(_SUBNETS), policy, version=version) + for cidr in ("192.168.142.0/24", "192.168.144.0/24"): + assert any( + r.source == cidr and r.destination == cidr + and r.jump == "ACCEPT" and r.weight == W_INTRA + for r in compiled.rules + ) + + +def test_compile_from_alloc_skips_unmapped_zones(cat): + """A policy zone with no matching subnet in the layout is silently skipped.""" + policy = cat.network_policies["air-gap-ctf"] + version = cat.resolved_version("network_policies", "air-gap-ctf") + # Only ctf subnet; admin zone has no match + subnets = [Subnet(name="ctf", cidr="192.168.144.0/24", bridge="vmbr144")] + compiled = compile_network_policy_from_alloc(_bare_alloc(subnets), policy, version=version) + assert not any(r.source == "192.168.142.0/24" for r in compiled.rules) + assert compiled.rules[-1].weight == W_DEFAULT # terminal still present + + +def test_compile_from_alloc_deterministic(cat): + """Same inputs must produce byte-identical JSON output.""" + policy = cat.network_policies["air-gap-ctf"] + version = cat.resolved_version("network_policies", "air-gap-ctf") + alloc = _bare_alloc(_SUBNETS) + a = compile_network_policy_from_alloc(alloc, policy, version=version).model_dump_json() + b = compile_network_policy_from_alloc(alloc, policy, version=version).model_dump_json() + assert a == b diff --git a/tests/test_scaffold.py b/tests/test_scaffold.py new file mode 100644 index 00000000..3b224f05 --- /dev/null +++ b/tests/test_scaffold.py @@ -0,0 +1,33 @@ +"""P4 scaffold tests — pure starter-topology generation (shared by CLI + TUI).""" + +from r42playbooks.core import constants as C +from r42playbooks.core.catalog import load_catalog +from r42playbooks.core.scaffold import scaffold_topology +from r42playbooks.core.validate import semantic_problems + + +def test_scaffold_produces_valid_topology(fake_catalog): + cat = load_catalog(fake_catalog) + t = scaffold_topology(cat, scenario="lab1", layout_id="default-3zone", + policy_id="air-gap-ctf") + assert t.scenario == "lab1" + assert semantic_problems(t, cat) == [] + assert t.network_policy.template == "air-gap-ctf" + + +def test_scaffold_picks_box_template_by_role(fake_catalog): + cat = load_catalog(fake_catalog) + t = scaffold_topology(cat, scenario="lab1", layout_id="default-3zone", + policy_id="air-gap-ctf") + templates = {b.box_template for b in t.boxes} + assert "admin-wazuh" in templates # admin zone gets the admin-prefixed template + # Each zone gets a distinct template (one per zone, no duplicates) + assert len(templates) == len(t.boxes) + + +def test_scaffold_respects_octet_rule(fake_catalog): + cat = load_catalog(fake_catalog) + t = scaffold_topology(cat, scenario="lab1", layout_id="default-3zone", + policy_id="air-gap-ctf") + for box in t.boxes: + assert C.octet_matches_vm_id(box.vm_id, box.ip), box diff --git a/tests/test_security_fixes.py b/tests/test_security_fixes.py new file mode 100644 index 00000000..4db5b390 --- /dev/null +++ b/tests/test_security_fixes.py @@ -0,0 +1,98 @@ +"""Regression tests for the ECC review findings (security + correctness).""" + +import json + +import pytest +from pydantic import ValidationError as PydErr + +from r42playbooks.core.catalog import load_catalog +from r42playbooks.core.catalog_models import MatrixRule, NetworkPolicyTemplate, ZoneDecl +from r42playbooks.core.compiler.network_policy import compile_network_policy, lint_segmentation +from r42playbooks.core.errors import CompileError, TopologyError +from r42playbooks.core.extravars import resolve_universal_extravars +from r42playbooks.core.models import Topology + + +# --- C3: wildcard-source DROP must not be shadowed undetected --- + +def test_linter_catches_wildcard_drop_bypass(topology_factory, fake_catalog): + """accept ctf->admin (w200) + drop *->admin (w500) must be flagged, not pass.""" + t = Topology.model_validate(topology_factory()) + pol = load_catalog(fake_catalog).network_policies["air-gap-ctf"].model_copy(deep=True) + pol.matrix = [ + MatrixRule(src="ctf", dst="admin", action="accept", comment="opens ctf->admin"), + MatrixRule(src="*", dst="admin", action="drop", comment="drop all to admin"), + ] + compiled = compile_network_policy(t, pol, version="1.1.0") + assert lint_segmentation(compiled, pol, t) != [] # bypass is now caught + + +def test_air_gap_reference_policy_still_clean(topology_factory, fake_catalog): + """The legitimate air-gap-ctf policy must NOT trip the stricter linter.""" + t = Topology.model_validate(topology_factory()) + pol = load_catalog(fake_catalog).network_policies["air-gap-ctf"] + compiled = compile_network_policy(t, pol, version="1.1.0") + assert lint_segmentation(compiled, pol, t) == [] + + +# --- C1: params / overrides injection --- + +def test_attachment_params_reject_injection(topology_factory): + spec = topology_factory() + spec["boxes"][0]["attachments"] = [ + {"kind": "role", "catalog_ref": "software.install.x", + "params": {"cmd": "{{ lookup('pipe','id') }}"}}, + ] + with pytest.raises(PydErr): + Topology.model_validate(spec) + + +def test_network_policy_overrides_reject_injection(topology_factory): + spec = topology_factory() + spec["network_policy"]["overrides"] = {"wan_interface": "eth0; iptables -F"} + with pytest.raises(PydErr): + Topology.model_validate(spec) + + +# --- C2: missing/invalid service IP must fail closed, not broaden the rule --- + +def test_missing_service_ip_raises(topology_factory, fake_catalog): + t = Topology.model_validate(topology_factory()) + pol = load_catalog(fake_catalog).network_policies["air-gap-ctf"].model_copy(deep=True) + pol.params = {} # remove siem_ip default + with pytest.raises(CompileError): + compile_network_policy(t, pol, version="1.1.0") + + +# --- H1/H2/H3: field guards --- + +def test_description_rejects_injection(topology_factory): + with pytest.raises(PydErr): + Topology.model_validate(topology_factory(description="{{ evil }}")) + + +def test_proxmox_node_rejects_empty(topology_factory): + with pytest.raises(PydErr): + Topology.model_validate(topology_factory(proxmox_node="")) + + +def test_scenario_rejects_leading_dash(topology_factory): + with pytest.raises(PydErr): + Topology.model_validate(topology_factory(scenario="-evil")) + + +def test_schema_version_pinned(topology_factory): + with pytest.raises(PydErr): + Topology.model_validate(topology_factory(schema_version=2)) + + +# --- CRITICAL(py): load_topology wraps pydantic errors as TopologyError --- + +def test_load_topology_wraps_schema_error(tmp_path, valid_topology_dict): + from r42playbooks.core.io import load_topology + bad = dict(valid_topology_dict) + bad["proxmox_node"] = "" # schema-invalid but valid JSON + p = tmp_path / "bad.json" + p.write_text(json.dumps(bad), encoding="utf-8") + with pytest.raises(TopologyError): + load_topology(p) diff --git a/tests/test_services.py b/tests/test_services.py new file mode 100644 index 00000000..70bdeedd --- /dev/null +++ b/tests/test_services.py @@ -0,0 +1,406 @@ +"""Tests for compiler/services.py — post-allocation apt services wiring pass.""" + +import dataclasses +from types import MappingProxyType + +import pytest + +from r42playbooks.core.allocate import Allocation, AllocatedBox +from r42playbooks.core.catalog import Catalog +from r42playbooks.core.catalog_models import ImageDef +from r42playbooks.core.compiler.services import resolve_services +from r42playbooks.core.errors import CompileError +from r42playbooks.core.models import Attachment, Subnet +from r42playbooks.core.spec import ScenarioSpec + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +def _box( + *, + vm_id: int, + vm_name: str, + ip: str, + box_template: str, + image: str, + attachments: tuple[Attachment, ...] = (), +) -> AllocatedBox: + return AllocatedBox( + vm_id=vm_id, + vm_name=vm_name, + ip=ip, + bridge="vmbr142", + subnet_name="admin", + section="02_admin_infrastructure", + label="ADMIN INFRASTRUCTURE INIT", + gateway="192.168.142.1", + inventory_group="r42_admin_group", + box_template=box_template, + image=image, + template_vm_id=9221, + template_name="template-vm-debian-trixie-small", + attachments=attachments, + box_vars=MappingProxyType({}), + ) + + +def _alloc(*boxes: AllocatedBox) -> Allocation: + return Allocation( + scenario="test_lab", + description="", + boxes=tuple(boxes), + templates=(), + subnets=( + Subnet( + name="admin", + cidr="192.168.142.0/24", + bridge="vmbr142", + gateway="192.168.142.1", + ), + ), + ) + + +def _spec_dict(*box_templates: str, services: dict | None = None) -> dict: + base: dict = { + "schema_version": 1, + "name": "test_lab", + "subnet_layout": "default-3zone", + "boxes": [{"template": t, "subnet": "admin"} for t in box_templates], + } + if services is not None: + base["services"] = services + return base + + +def _catalog_with_images(**images: tuple[str, str]) -> Catalog: + """Build a minimal Catalog with ImageDef entries. + + images: keyword mapping of image_id → (distro, codename). + """ + cat = Catalog() + for image_id, (distro, codename) in images.items(): + cat.images[image_id] = ImageDef( + id=image_id, + api_version=1, + distro=distro, + codename=codename, + description=f"{distro} {codename}", + proxmox_templates=[], + ) + return cat + + +# --------------------------------------------------------------------------- +# Passthrough when no services declared +# --------------------------------------------------------------------------- + +def test_no_services_returns_alloc_unchanged(): + server = _box(vm_id=1010, vm_name="admin-apt-cache-00", ip="192.168.142.10", + box_template="apt-cache", image="debian_trixie") + alloc = _alloc(server) + spec = ScenarioSpec.model_validate(_spec_dict("apt-cache")) + + result = resolve_services(alloc, spec, Catalog()) + + assert result is alloc # same object — no copy made + + +def test_services_apt_none_returns_alloc_unchanged(): + server = _box(vm_id=1010, vm_name="admin-apt-cache-00", ip="192.168.142.10", + box_template="apt-cache", image="debian_trixie") + alloc = _alloc(server) + spec = ScenarioSpec.model_validate( + _spec_dict("apt-cache", services={"apt": None}) + ) + + result = resolve_services(alloc, spec, Catalog()) + + assert result is alloc + + +# --------------------------------------------------------------------------- +# Proxy mode — wire_to: all +# --------------------------------------------------------------------------- + +def test_proxy_mode_injects_client_attachment(): + server = _box(vm_id=1010, vm_name="admin-apt-cache-00", ip="192.168.142.10", + box_template="apt-cache", image="debian_trixie") + client = _box(vm_id=1020, vm_name="admin-debian-jump-00", ip="192.168.142.20", + box_template="debian-jump", image="debian_trixie") + alloc = _alloc(server, client) + spec = ScenarioSpec.model_validate( + _spec_dict("apt-cache", "debian-jump", + services={"apt": {"box": "apt-cache", "mode": "proxy"}}) + ) + + result = resolve_services(alloc, spec, Catalog()) + + client_result = result.boxes[1] + assert len(client_result.attachments) == 1 + att = client_result.attachments[0] + assert att.catalog_ref == "software.configure.apt_mirror_client" + assert att.params["apt_mirror_enabled"] is True + assert att.params["apt_proxy_url"] == "http://192.168.142.10:3142" + + +def test_proxy_mode_server_box_unchanged(): + server = _box(vm_id=1010, vm_name="admin-apt-cache-00", ip="192.168.142.10", + box_template="apt-cache", image="debian_trixie") + client = _box(vm_id=1020, vm_name="admin-debian-jump-00", ip="192.168.142.20", + box_template="debian-jump", image="debian_trixie") + alloc = _alloc(server, client) + spec = ScenarioSpec.model_validate( + _spec_dict("apt-cache", "debian-jump", + services={"apt": {"box": "apt-cache", "mode": "proxy"}}) + ) + + result = resolve_services(alloc, spec, Catalog()) + + assert result.boxes[0].attachments == server.attachments + + +def test_proxy_mode_wire_to_subset(): + server = _box(vm_id=1010, vm_name="admin-apt-cache-00", ip="192.168.142.10", + box_template="apt-cache", image="debian_trixie") + client_a = _box(vm_id=1020, vm_name="admin-debian-jump-00", ip="192.168.142.20", + box_template="debian-jump", image="debian_trixie") + client_b = _box(vm_id=1030, vm_name="admin-ubuntu-jump-00", ip="192.168.142.30", + box_template="ubuntu-jump", image="ubuntu_resolute") + alloc = _alloc(server, client_a, client_b) + spec = ScenarioSpec.model_validate( + _spec_dict("apt-cache", "debian-jump", "ubuntu-jump", + services={"apt": {"box": "apt-cache", "mode": "proxy", + "wire_to": ["debian-jump"]}}) + ) + + result = resolve_services(alloc, spec, Catalog()) + + assert any(a.catalog_ref == "software.configure.apt_mirror_client" + for a in result.boxes[1].attachments) + assert not any(a.catalog_ref == "software.configure.apt_mirror_client" + for a in result.boxes[2].attachments) + + +def test_proxy_mode_wire_to_all_skips_server(): + server = _box(vm_id=1010, vm_name="admin-apt-cache-00", ip="192.168.142.10", + box_template="apt-cache", image="debian_trixie") + client = _box(vm_id=1020, vm_name="admin-debian-jump-00", ip="192.168.142.20", + box_template="debian-jump", image="debian_trixie") + alloc = _alloc(server, client) + spec = ScenarioSpec.model_validate( + _spec_dict("apt-cache", "debian-jump", + services={"apt": {"box": "apt-cache", "wire_to": "all"}}) + ) + + result = resolve_services(alloc, spec, Catalog()) + + # Server must never receive the client attachment. + assert not any(a.catalog_ref == "software.configure.apt_mirror_client" + for a in result.boxes[0].attachments) + + +# --------------------------------------------------------------------------- +# Mirror mode — suite auto-detection +# --------------------------------------------------------------------------- + +def test_mirror_mode_patches_server_suite_flags(): + mirror_att = Attachment( + kind="role", + catalog_ref="software.install.apt_mirror", + params={"apt_mirror_http_port": 80}, + ) + server = _box(vm_id=1010, vm_name="admin-apt-mirror-00", ip="192.168.142.10", + box_template="apt-mirror", image="debian_trixie", + attachments=(mirror_att,)) + client = _box(vm_id=1020, vm_name="admin-debian-jump-00", ip="192.168.142.20", + box_template="debian-jump", image="debian_trixie") + alloc = _alloc(server, client) + spec = ScenarioSpec.model_validate( + _spec_dict("apt-mirror", "debian-jump", + services={"apt": {"box": "apt-mirror", "mode": "mirror"}}) + ) + catalog = _catalog_with_images(debian_trixie=("debian", "trixie")) + + result = resolve_services(alloc, spec, catalog) + + server_atts = {a.catalog_ref: a for a in result.boxes[0].attachments} + params = server_atts["software.install.apt_mirror"].params + assert params.get("apt_mirror_debian_trixie") is True + assert params.get("apt_mirror_http_port") == 80 # original param preserved + + +def test_mirror_mode_multiple_distros(): + mirror_att = Attachment(kind="role", catalog_ref="software.install.apt_mirror", params={}) + server = _box(vm_id=1010, vm_name="admin-apt-mirror-00", ip="192.168.142.10", + box_template="apt-mirror", image="debian_trixie", + attachments=(mirror_att,)) + deb_client = _box(vm_id=1020, vm_name="admin-debian-jump-00", ip="192.168.142.20", + box_template="debian-jump", image="debian_trixie") + ubuntu_client = _box(vm_id=1030, vm_name="admin-ubuntu-jump-00", ip="192.168.142.30", + box_template="ubuntu-jump", image="ubuntu_noble") + alloc = _alloc(server, deb_client, ubuntu_client) + spec = ScenarioSpec.model_validate( + _spec_dict("apt-mirror", "debian-jump", "ubuntu-jump", + services={"apt": {"box": "apt-mirror", "mode": "mirror"}}) + ) + catalog = _catalog_with_images( + debian_trixie=("debian", "trixie"), + ubuntu_noble=("ubuntu", "noble"), + ) + + result = resolve_services(alloc, spec, catalog) + + server_atts = {a.catalog_ref: a for a in result.boxes[0].attachments} + params = server_atts["software.install.apt_mirror"].params + assert params.get("apt_mirror_debian_trixie") is True + assert params.get("apt_mirror_ubuntu_noble") is True + assert "apt_mirror_ubuntu_jammy" not in params # jammy not in scenario + + +def test_mirror_mode_authored_params_override_auto_flags(): + mirror_att = Attachment( + kind="role", + catalog_ref="software.install.apt_mirror", + # Author explicitly disables the flag even though a debian client is wired. + params={"apt_mirror_debian_trixie": False}, + ) + server = _box(vm_id=1010, vm_name="admin-apt-mirror-00", ip="192.168.142.10", + box_template="apt-mirror", image="debian_trixie", + attachments=(mirror_att,)) + client = _box(vm_id=1020, vm_name="admin-debian-jump-00", ip="192.168.142.20", + box_template="debian-jump", image="debian_trixie") + alloc = _alloc(server, client) + spec = ScenarioSpec.model_validate( + _spec_dict("apt-mirror", "debian-jump", + services={"apt": {"box": "apt-mirror", "mode": "mirror"}}) + ) + catalog = _catalog_with_images(debian_trixie=("debian", "trixie")) + + result = resolve_services(alloc, spec, catalog) + + server_atts = {a.catalog_ref: a for a in result.boxes[0].attachments} + assert server_atts["software.install.apt_mirror"].params["apt_mirror_debian_trixie"] is False + + +def test_mirror_mode_client_attachment_airgapped(): + mirror_att = Attachment(kind="role", catalog_ref="software.install.apt_mirror", params={}) + server = _box(vm_id=1010, vm_name="admin-apt-mirror-00", ip="192.168.142.10", + box_template="apt-mirror", image="debian_trixie", + attachments=(mirror_att,)) + client = _box(vm_id=1020, vm_name="admin-debian-jump-00", ip="192.168.142.20", + box_template="debian-jump", image="debian_trixie") + alloc = _alloc(server, client) + spec = ScenarioSpec.model_validate( + _spec_dict("apt-mirror", "debian-jump", + services={"apt": {"box": "apt-mirror", "mode": "mirror"}}) + ) + catalog = _catalog_with_images(debian_trixie=("debian", "trixie")) + + result = resolve_services(alloc, spec, catalog) + + client_atts = {a.catalog_ref: a for a in result.boxes[1].attachments} + params = client_atts["software.configure.apt_mirror_client"].params + assert params["apt_mirror_enabled"] is True + assert params["apt_mirror_airgapped"] is True + assert params["apt_mirror_vm_ip"] == "192.168.142.10" + assert params["apt_mirror_http_port"] == 80 + + +def test_mirror_mode_no_suite_flags_when_image_unknown(): + mirror_att = Attachment(kind="role", catalog_ref="software.install.apt_mirror", params={}) + server = _box(vm_id=1010, vm_name="admin-apt-mirror-00", ip="192.168.142.10", + box_template="apt-mirror", image="debian_trixie", + attachments=(mirror_att,)) + client = _box(vm_id=1020, vm_name="admin-debian-jump-00", ip="192.168.142.20", + box_template="debian-jump", image="debian_trixie") + alloc = _alloc(server, client) + spec = ScenarioSpec.model_validate( + _spec_dict("apt-mirror", "debian-jump", + services={"apt": {"box": "apt-mirror", "mode": "mirror"}}) + ) + # Empty catalog — image lookup will return None, so no suite flags set. + result = resolve_services(alloc, spec, Catalog()) + + server_atts = {a.catalog_ref: a for a in result.boxes[0].attachments} + params = server_atts["software.install.apt_mirror"].params + assert "apt_mirror_debian_trixie" not in params + + +# --------------------------------------------------------------------------- +# Error handling +# --------------------------------------------------------------------------- + +def test_missing_server_box_raises_compile_error(): + client = _box(vm_id=1020, vm_name="admin-debian-jump-00", ip="192.168.142.20", + box_template="debian-jump", image="debian_trixie") + # Allocation has only debian-jump but spec declares services.apt.box = apt-cache. + alloc = _alloc(client) + spec = ScenarioSpec.model_validate( + _spec_dict("debian-jump", "apt-cache", + services={"apt": {"box": "apt-cache", "mode": "proxy"}}) + ) + + with pytest.raises(CompileError, match="apt-cache"): + resolve_services(alloc, spec, Catalog()) + + +# --------------------------------------------------------------------------- +# Spec schema validation +# --------------------------------------------------------------------------- + +def test_spec_services_round_trips_yaml(): + import yaml + from r42playbooks.core.spec import dumps_spec + + spec = ScenarioSpec.model_validate( + _spec_dict("apt-cache", "debian-jump", + services={"apt": {"box": "apt-cache", "mode": "proxy", "wire_to": "all"}}) + ) + yaml_out = dumps_spec(spec) + reloaded = ScenarioSpec.model_validate(yaml.safe_load(yaml_out)) + assert reloaded.services == spec.services + + +def test_spec_services_mode_default_is_proxy(): + spec = ScenarioSpec.model_validate( + _spec_dict("apt-cache", services={"apt": {"box": "apt-cache"}}) + ) + assert spec.services.apt.mode == "proxy" + + +def test_spec_services_wire_to_default_is_all(): + spec = ScenarioSpec.model_validate( + _spec_dict("apt-cache", services={"apt": {"box": "apt-cache"}}) + ) + assert spec.services.apt.wire_to == "all" + + +def test_spec_services_unknown_field_rejected(): + from pydantic import ValidationError as PydanticValidationError + + with pytest.raises(PydanticValidationError): + ScenarioSpec.model_validate( + _spec_dict("apt-cache", + services={"apt": {"box": "apt-cache", "unknown_field": "boom"}}) + ) + + +def test_spec_services_invalid_mode_rejected(): + from pydantic import ValidationError as PydanticValidationError + + with pytest.raises(PydanticValidationError): + ScenarioSpec.model_validate( + _spec_dict("apt-cache", + services={"apt": {"box": "apt-cache", "mode": "ftp"}}) + ) + + +def test_spec_services_none_is_valid(): + # Omitting services is valid; auto-inject populates it when an apt provider is present. + spec = ScenarioSpec.model_validate(_spec_dict("apt-cache")) + assert spec.services is not None + assert spec.services.apt is not None + assert spec.services.apt.box == "apt-cache" diff --git a/tests/test_spec.py b/tests/test_spec.py new file mode 100644 index 00000000..12e81c90 --- /dev/null +++ b/tests/test_spec.py @@ -0,0 +1,203 @@ +"""Step 2 — scenario.r42.yml composition spec tests (RED before GREEN). + +Covers: valid spec round-trips through YAML; unknown/bad fields are rejected +(extra="forbid"); deny-listed free-text (injection) is rejected; IO error paths. +""" + +import pytest +from pydantic import ValidationError as PydanticValidationError + +from r42playbooks.core.errors import TopologyError +from r42playbooks.core.spec import ScenarioSpec, dump_spec_atomic, load_spec + + +def _minimal(boxes: list[dict] | None = None) -> dict: + return { + "schema_version": 1, + "name": "test_lab", + "subnet_layout": "dual-lan", + "boxes": boxes or [{"template": "debian-jump", "subnet": "lan1"}], + } + + +def test_attachment_catalog_ref_rejects_path_traversal(spec_factory): + """catalog_ref is written verbatim as an Ansible role name -> deny-list '..'.""" + bad = spec_factory(boxes=[{ + "template": "vuln-box", + "attachments_add": [{"kind": "role", "catalog_ref": "software/../etc", "params": {}}], + }]) + with pytest.raises(PydanticValidationError): + ScenarioSpec.model_validate(bad) + + +def test_valid_spec_round_trips(tmp_path, valid_spec_dict): + # Arrange + spec = ScenarioSpec.model_validate(valid_spec_dict) + path = tmp_path / "scenario.r42.yml" + + # Act + dump_spec_atomic(spec, path) + loaded = load_spec(path) + + # Assert + assert loaded == spec + + +def test_dump_is_deterministic(tmp_path, valid_spec_dict): + spec = ScenarioSpec.model_validate(valid_spec_dict) + p1, p2 = tmp_path / "a.yml", tmp_path / "b.yml" + dump_spec_atomic(spec, p1) + dump_spec_atomic(spec, p2) + assert p1.read_text() == p2.read_text() # byte-identical + + +def test_count_defaults_to_one(valid_spec_dict): + spec = ScenarioSpec.model_validate(valid_spec_dict) + assert spec.boxes[0].count == 1 # admin-wazuh has no explicit count + + +def test_optional_fields_may_be_omitted(spec_factory): + data = spec_factory() + del data["proxmox_node"] + del data["notes"] + spec = ScenarioSpec.model_validate(data) + assert spec.proxmox_node is None + assert spec.notes == "" + + +def test_unknown_top_level_field_rejected(spec_factory): + data = spec_factory(unexpected="boom") + with pytest.raises(PydanticValidationError): + ScenarioSpec.model_validate(data) + + +def test_unknown_box_field_rejected(valid_spec_dict): + valid_spec_dict["boxes"][0]["junk"] = 1 + with pytest.raises(PydanticValidationError): + ScenarioSpec.model_validate(valid_spec_dict) + + +def test_bad_name_rejected(spec_factory): + data = spec_factory(name="has spaces") + with pytest.raises(PydanticValidationError): + ScenarioSpec.model_validate(data) + + +def test_empty_boxes_rejected(spec_factory): + data = spec_factory(boxes=[]) + with pytest.raises(PydanticValidationError): + ScenarioSpec.model_validate(data) + + +def test_injection_in_notes_rejected(spec_factory): + data = spec_factory(notes="{{ malicious }}") + with pytest.raises(PydanticValidationError): + ScenarioSpec.model_validate(data) + + +def test_injection_in_box_vars_rejected(valid_spec_dict): + valid_spec_dict["boxes"][0]["vars"] = {"x": "${SECRET}"} + with pytest.raises(PydanticValidationError): + ScenarioSpec.model_validate(valid_spec_dict) + + +def test_count_must_be_positive(valid_spec_dict): + valid_spec_dict["boxes"][0]["count"] = 0 + with pytest.raises(PydanticValidationError): + ScenarioSpec.model_validate(valid_spec_dict) + + +def test_template_vm_id_override_accepted(valid_spec_dict): + # §7.1 resolved decision: a box may pin its template via template_vm_id. + valid_spec_dict["boxes"][0]["template_vm_id"] = 9234 + spec = ScenarioSpec.model_validate(valid_spec_dict) + assert spec.boxes[0].template_vm_id == 9234 + + +def test_load_spec_rejects_missing_file(tmp_path): + with pytest.raises(TopologyError): + load_spec(tmp_path / "nope.r42.yml") + + +def test_load_spec_rejects_invalid_yaml(tmp_path): + bad = tmp_path / "bad.r42.yml" + bad.write_text("name: [unclosed\n", encoding="utf-8") + with pytest.raises(TopologyError): + load_spec(bad) + + +def test_load_spec_rejects_schema_violation(tmp_path): + bad = tmp_path / "bad.r42.yml" + bad.write_text("name: my_lab\n", encoding="utf-8") # missing required fields + with pytest.raises(TopologyError): + load_spec(bad) + + +# --------------------------------------------------------------------------- +# Auto-inject services.apt +# --------------------------------------------------------------------------- + +def test_auto_inject_apt_cache_sets_proxy_mode(): + spec = ScenarioSpec.model_validate(_minimal([ + {"template": "apt-cache", "subnet": "lan1"}, + {"template": "debian-jump", "subnet": "lan2"}, + ])) + assert spec.services is not None + assert spec.services.apt is not None + assert spec.services.apt.box == "apt-cache" + assert spec.services.apt.wire_to == "all" + assert spec.services.apt.mode == "proxy" + + +def test_auto_inject_apt_mirror_sets_mirror_mode(): + spec = ScenarioSpec.model_validate(_minimal([ + {"template": "apt-mirror", "subnet": "lan1"}, + {"template": "ubuntu-jump", "subnet": "lan2"}, + ])) + assert spec.services.apt.box == "apt-mirror" + assert spec.services.apt.mode == "mirror" + + +def test_auto_inject_apt_mirror_airgapped_sets_mirror_mode(): + spec = ScenarioSpec.model_validate(_minimal([ + {"template": "apt-mirror-airgapped", "subnet": "lan1"}, + {"template": "debian-jump", "subnet": "lan2"}, + ])) + assert spec.services.apt.box == "apt-mirror-airgapped" + assert spec.services.apt.mode == "mirror" + + +def test_no_auto_inject_without_apt_provider(): + spec = ScenarioSpec.model_validate(_minimal([ + {"template": "debian-jump", "subnet": "lan1"}, + ])) + assert spec.services is None + + +def test_no_auto_inject_when_services_explicitly_disabled(): + data = _minimal([ + {"template": "apt-cache", "subnet": "lan1"}, + {"template": "debian-jump", "subnet": "lan2"}, + ]) + data["services"] = {"apt": None} + spec = ScenarioSpec.model_validate(data) + assert spec.services.apt is None + + +def test_no_auto_inject_when_services_explicitly_set(): + data = _minimal([ + {"template": "apt-cache", "subnet": "lan1"}, + {"template": "debian-jump", "subnet": "lan2"}, + ]) + data["services"] = {"apt": {"box": "apt-cache", "mode": "proxy", "wire_to": ["debian-jump"]}} + spec = ScenarioSpec.model_validate(data) + assert spec.services.apt.wire_to == ["debian-jump"] + + +def test_no_auto_inject_with_two_apt_providers(): + spec = ScenarioSpec.model_validate(_minimal([ + {"template": "apt-cache", "subnet": "lan1"}, + {"template": "apt-mirror", "subnet": "lan1"}, + {"template": "debian-jump", "subnet": "lan2"}, + ])) + assert spec.services is None diff --git a/tests/test_tui.py b/tests/test_tui.py new file mode 100644 index 00000000..4ca33833 --- /dev/null +++ b/tests/test_tui.py @@ -0,0 +1,304 @@ +"""S7 TUI tests — the pure ScenarioComposerController (compose + generate). + +The view (Textual) stays a thin shell; all logic lives in the controller, so it +is unit-tested headless. A single mount smoke test confirms the app composes. +""" + +import asyncio + +import pytest + +from r42playbooks.core.errors import TopologyError +from r42playbooks.tui.controller import ScenarioComposerController + + +def _composed(fake_catalog) -> ScenarioComposerController: + ctl = ScenarioComposerController(fake_catalog) + ctl.set_name("tui_lab") + ctl.set_subnet("default-3zone") + ctl.set_policy("air-gap-ctf") + ctl.add_box("admin-wazuh", subnet="admin") + ctl.add_box("vuln-box", count=3, subnet="ctf") + return ctl + + +def test_controller_lists_catalog_choices(fake_catalog): + ctl = ScenarioComposerController(fake_catalog) + assert "default-3zone" in ctl.layouts() + assert "air-gap-ctf" in ctl.policies() + assert "admin-wazuh" in ctl.box_templates() + assert "vuln-box" in ctl.box_templates() + + +def test_controller_compose_builds_spec(fake_catalog): + ctl = _composed(fake_catalog) + assert ctl.validate() == [] + spec = ctl.build_spec() + assert spec.name == "tui_lab" + assert [b.template for b in spec.boxes] == ["admin-wazuh", "vuln-box"] + assert spec.boxes[1].count == 3 + + +def test_controller_remove_and_clear_boxes(fake_catalog): + ctl = _composed(fake_catalog) + ctl.remove_box(0) + assert [t for t, _c, _s, _o in ctl.boxes] == ["vuln-box"] + ctl.clear_boxes() + assert ctl.boxes == [] + + +def test_controller_validate_reports_incomplete_then_bad_ref(fake_catalog): + ctl = ScenarioComposerController(fake_catalog) + assert ctl.validate() # nothing composed -> non-empty problem list + ctl.set_name("x") + ctl.set_subnet("default-3zone") + ctl.set_policy("air-gap-ctf") + ctl.add_box("no-such-box", subnet="admin") + problems = ctl.validate() + assert any("no-such-box" in p for p in problems) + + +def test_controller_preview_shows_allocation(fake_catalog): + ctl = _composed(fake_catalog) + preview = ctl.preview() + assert "admin-wazuh" in preview + assert "vuln-box-00" in preview + assert "vuln-box-02" in preview # count=3 expanded + + +def test_controller_preview_lists_added_boxes_when_incomplete(fake_catalog): + """Every add_box gives visible feedback even before name/subnet/policy are set.""" + ctl = ScenarioComposerController(fake_catalog) + ctl.add_box("vuln-box", count=2) # nothing else picked yet + preview = ctl.preview() + assert "vuln-box ×2" in preview # the pick is shown + assert "not ready" in preview # and the readiness gap + + +def test_controller_generate_writes_deployable_tree(fake_catalog, tmp_path): + ctl = _composed(fake_catalog) + root = ctl.generate(tmp_path / "scenarios") + assert root.name == "tui_lab" + assert (root / "main.yml").is_file() + assert (root / "manifest" / "scenario_vms.json").is_file() + assert (root / "scenario.r42.yml").is_file() + + +def test_controller_generate_without_compose_raises(fake_catalog, tmp_path): + ctl = ScenarioComposerController(fake_catalog) + with pytest.raises(TopologyError): + ctl.generate(tmp_path / "scenarios") + + +def test_controller_generate_refuses_existing_then_overwrites(fake_catalog, tmp_path): + from r42playbooks.core.errors import ScenarioExistsError + ctl = _composed(fake_catalog) + out = tmp_path / "scenarios" + ctl.generate(out) + with pytest.raises(ScenarioExistsError): + ctl.generate(out) # default refuses + root = ctl.generate(out, overwrite=True) # explicit overwrite + assert (root / "main.yml").is_file() + + +def test_controller_generate_autodetects_reserved_json(fake_catalog, tmp_path): + import json + out = tmp_path / "scenarios" + out.mkdir() + entry = {"vm_id": 1010, "ip": "192.168.144.10", "vm_name": "vuln-box-00", + "subnet": "ctf", "bridge": "vmbr144", "scenario": "other_lab"} + (out / "_reserved.json").write_text(json.dumps(entry) + "\n", encoding="utf-8") + ctl = ScenarioComposerController(fake_catalog) + ctl.set_name("tui_lab2") + ctl.set_subnet("default-3zone") + ctl.add_box("vuln-box", subnet="ctf") + root = ctl.generate(out) + manifest = json.loads((root / "manifest" / "scenario_vms.json").read_text()) + vm_ids = {v["vm_id"] for v in manifest["vms"]} + assert 1010 not in vm_ids + assert 1011 in vm_ids + + +def test_app_generate_warns_on_existing_then_overwrites(fake_catalog, tmp_path): + """Two-press overwrite: first Generate warns, second overwrites (no crash).""" + import asyncio + from textual.widgets import Input, Select + from r42playbooks.tui.app import ScenarioComposerApp + + out = tmp_path / "scenarios" + (out / "dup").mkdir(parents=True) # pre-existing scenario dir + + async def _go(): + app = ScenarioComposerApp(ScenarioComposerController(fake_catalog), out_dir=out) + shown: list[str] = [] + app._set_output = lambda t: shown.append(t) + async with app.run_test() as pilot: + await pilot.pause() + app.query_one("#scenario", Input).value = "dup" + app.query_one("#layout", Select).value = "default-3zone" + app.query_one("#box", Select).value = "admin-wazuh" + app.query_one("#subnet", Select).value = "admin" + await pilot.pause() + app._do_add() + app._do_generate() # first: warns, arms overwrite + assert app._pending_overwrite is True + app._do_generate() # second: overwrites + assert app._pending_overwrite is False + assert any("again to overwrite" in s for s in shown) + assert any(s.startswith("✓ generated") for s in shown) + + asyncio.run(_go()) + + +def test_app_button_fires_on_mouse_click(fake_catalog): + """A mouse click on the Add button invokes the handler.""" + import asyncio + from r42playbooks.tui.app import ScenarioComposerApp + + async def _go(): + app = ScenarioComposerApp(ScenarioComposerController(fake_catalog)) + async with app.run_test() as pilot: + await pilot.pause() + await pilot.click("#add") + await pilot.pause() + assert len(app.controller.boxes) == 1 + + asyncio.run(_go()) + + +def test_app_button_fires_on_enter_when_focused(fake_catalog): + """Enter on a focused button invokes the handler (keyboard, no mouse).""" + import asyncio + from textual.widgets import Button + from r42playbooks.tui.app import ScenarioComposerApp + + async def _go(): + app = ScenarioComposerApp(ScenarioComposerController(fake_catalog)) + async with app.run_test() as pilot: + await pilot.pause() + app.query_one("#add", Button).focus() + await pilot.pause() + await pilot.press("enter") + await pilot.pause() + assert len(app.controller.boxes) == 1 + + asyncio.run(_go()) + + +def test_app_full_flow_via_buttons(fake_catalog, tmp_path): + """Compose + generate using only button presses (the standard interaction).""" + import asyncio + from textual.widgets import Input + from r42playbooks.tui.app import ScenarioComposerApp + + out = tmp_path / "scenarios" + + async def _go(): + app = ScenarioComposerApp(ScenarioComposerController(fake_catalog), out_dir=out) + shown: list[str] = [] + app._set_output = lambda t: shown.append(t) + async with app.run_test() as pilot: + await pilot.pause() + app.query_one("#scenario", Input).value = "btn_lab" + await pilot.pause() + await pilot.click("#add") + await pilot.click("#generate") + await pilot.pause() + assert any(s.startswith("✓ generated") for s in shown) + assert (out / "btn_lab" / "main.yml").is_file() + + asyncio.run(_go()) + + +def test_app_layout_buttons_and_output_visible(fake_catalog): + """Regression: every button is on-screen and the output pane has real height. + + The tall single-column layout used to squeeze #output to height 0 and push + the buttons below the fold, so only Quit (which closes the app) had a visible + effect. Guard the side-by-side layout at 80x28 (form has grown by one row). + """ + import asyncio + from textual.widgets import Button, Static + from r42playbooks.tui.app import ScenarioComposerApp + + async def _go(): + app = ScenarioComposerApp(ScenarioComposerController(fake_catalog)) + async with app.run_test(size=(80, 28)) as pilot: + await pilot.pause() + out = app.query_one("#output", Static) + assert out.size.height > 0 and out.size.width > 0 # output is visible + for bid in ("add", "preview", "generate", "clear", "quit"): + b = app.query_one(f"#{bid}", Button) + assert b.region.width > 0 + assert b.region.y + b.region.height <= app.size.height # on-screen + await pilot.click("#add") # must not raise OutOfBounds + await pilot.pause() + assert len(app.controller.boxes) == 1 + + asyncio.run(_go()) + + +def test_app_quit_button_exits(fake_catalog): + """The Quit button exits the app (mouse users need a visible way out).""" + import asyncio + from r42playbooks.tui.app import ScenarioComposerApp + + async def _go(): + app = ScenarioComposerApp(ScenarioComposerController(fake_catalog)) + async with app.run_test() as pilot: + await pilot.pause() + await pilot.click("#quit") + await pilot.pause() + # app.run_test() context exits cleanly once the app has stopped + assert app.is_running is False + + asyncio.run(_go()) + + +def test_app_handler_catch_all_shows_error_not_crash(fake_catalog, monkeypatch): + """Any handler exception is shown in-pane, never silently killing the app.""" + import asyncio + from r42playbooks.tui.app import ScenarioComposerApp + from textual.widgets import Button + + async def _go(): + app = ScenarioComposerApp(ScenarioComposerController(fake_catalog)) + shown: list[str] = [] + app._set_output = lambda t: shown.append(t) + async with app.run_test(): + def _boom(): + raise RuntimeError("kaboom") + monkeypatch.setattr(app, "_do_preview", _boom) + app.on_button_pressed(Button.Pressed(app.query_one("#preview", Button))) + assert any("unexpected error" in s and "kaboom" in s for s in shown) + + asyncio.run(_go()) + + +def test_controller_preview_surfaces_allocation_error_without_raising(fake_catalog, monkeypatch): + """preview() must never crash the TUI on an allocation error — return text.""" + import r42playbooks.tui.controller as ctlmod + from r42playbooks.core.errors import CompileError + + def _boom(*_a, **_k): + raise CompileError("subnet exhausted") + + ctl = _composed(fake_catalog) + monkeypatch.setattr(ctlmod, "allocate", _boom) + out = ctl.preview() + assert "✗ cannot allocate" in out + assert "subnet exhausted" in out + + +def test_app_mounts(fake_catalog): + """The Textual app composes and mounts headlessly without error.""" + from r42playbooks.tui.app import ScenarioComposerApp + + async def _go(): + app = ScenarioComposerApp(ScenarioComposerController(fake_catalog)) + async with app.run_test() as pilot: + await pilot.pause() + assert app.query_one("#scenario") is not None + assert app.query_one("#output") is not None + + asyncio.run(_go()) diff --git a/tests/test_validate.py b/tests/test_validate.py new file mode 100644 index 00000000..c36d4ea5 --- /dev/null +++ b/tests/test_validate.py @@ -0,0 +1,55 @@ +"""P3 semantic validation tests — RED before GREEN.""" + +from r42playbooks.core.catalog import load_catalog +from r42playbooks.core.models import Topology +from r42playbooks.core.validate import semantic_problems + + +def _valid(topology_factory, fake_catalog): + return Topology.model_validate(topology_factory()), load_catalog(fake_catalog) + + +def test_valid_topology_has_no_semantic_problems(topology_factory, fake_catalog): + t, cat = _valid(topology_factory, fake_catalog) + assert semantic_problems(t, cat) == [] + + +def test_box_zone_must_exist(topology_factory, fake_catalog): + spec = topology_factory() + spec["boxes"][0]["zone"] = "ghost" + t = Topology.model_validate(spec) + probs = semantic_problems(t, load_catalog(fake_catalog)) + assert any("ghost" in p for p in probs) + + +def test_zone_subnet_must_exist(topology_factory, fake_catalog): + spec = topology_factory() + spec["zones"][0]["subnet"] = "nope" + t = Topology.model_validate(spec) + probs = semantic_problems(t, load_catalog(fake_catalog)) + assert any("nope" in p for p in probs) + + +def test_box_ip_must_be_in_zone_subnet(topology_factory, fake_catalog): + spec = topology_factory() + spec["boxes"][0]["ip"] = "10.0.0.100" # not in 192.168.142.0/24 + spec["boxes"][0]["vm_id"] = 1100 # keep octet rule happy (.100) + t = Topology.model_validate(spec) + probs = semantic_problems(t, load_catalog(fake_catalog)) + assert any("10.0.0.100" in p for p in probs) + + +def test_dangling_box_template_is_problem(topology_factory, fake_catalog): + spec = topology_factory() + spec["boxes"][0]["box_template"] = "missing-template" + t = Topology.model_validate(spec) + probs = semantic_problems(t, load_catalog(fake_catalog)) + assert any("missing-template" in p for p in probs) + + +def test_dangling_network_policy_is_problem(topology_factory, fake_catalog): + spec = topology_factory() + spec["network_policy"]["template"] = "no-such-policy" + t = Topology.model_validate(spec) + probs = semantic_problems(t, load_catalog(fake_catalog)) + assert any("no-such-policy" in p for p in probs)