Skip to content

feat(scenarios): add test_mattermost scenario (vm_ids 1003/3003, port 8065) - #92

Closed
t0kubetsu wants to merge 133 commits into
devfrom
feat/test-mattermost-scenario
Closed

feat(scenarios): add test_mattermost scenario (vm_ids 1003/3003, port 8065)#92
t0kubetsu wants to merge 133 commits into
devfrom
feat/test-mattermost-scenario

Conversation

@t0kubetsu

Copy link
Copy Markdown
Contributor

Summary

  • Adds scenarios/test_mattermost/ — dual-LAN scenario using the admin-mattermost BoxTemplate
    • lan1-admin-mattermost-00: vm_id 1003, IP 192.168.150.3, vmbr150, port 8065
    • lan2-debian-jump-00: vm_id 3003, IP 192.168.151.3, vmbr151 (jump box)
  • Registers vm_ids 1003 and 3003 in scenarios/_reserved.json
  • Mirrors the test_rocketchat / test_nextcloud pattern exactly

Test plan

  • Deploy via test_mattermost.setup.sh on Proxmox
  • SSH into lan1-admin-mattermost-00 and confirm Mattermost stack is up (docker compose ps)
  • Access Mattermost UI on port 8065

t0kubetsu and others added 30 commits May 15, 2026 10:44
…es isolation

Implements the network isolation design from issue #20.

Adds `scenarios/demo_lab_network/` — a self-contained scenario based on
demo_lab that enforces zone separation between the admin (vmbr142) and
CTF (vmbr144) bridges via iptables FORWARD chain rules on the Proxmox host.

New: 05_network_isolation/stage_00/proxmox_forward_rules.yml
  - Installs iptables-persistent on pve01
  - ACCEPT: admin → ctf (management access)
  - ACCEPT: ctf → wazuh :1514/:1515 (agent reporting)
  - DROP:   ctf → admin (zone isolation)
  - DROP:   ctf → vmbr0/WAN (air-gap)
  - Rules are idempotent and persist across reboots via netfilter-persistent

All rules implemented as Ansible tasks (not Python API calls), per
hyde-repo's recommendation in the issue discussion.

Two-zone design (admin/ctf) chosen over three-zone; the dedicated
management bridge alternative is documented in README.md as a future option.

Closes #20
ansible_connection: local on the proxmox host targets the deployer machine,
which doesn't have iptables. Switch to proxmox-cli (SSH to pve01 as root)
where iptables is available.
…d packages can reach internet

Before this fix, the CTF→WAN FORWARD DROP rule was already in place when the
CTF VMs were deployed, blocking DNS and NTP during cloud-init and package
installation. This caused the deployment to hang (30-60s DNS timeouts) and
ultimately fail at the NTP sync step.

New lift/restore pattern:
  1. lift_ctf_airgap.yml removes the DROP rule before CTF infrastructure runs
  2. 04_ctf_infrastructure/_main.yml runs with internet access
  3. 05_network_isolation/_main.yml re-installs the DROP rule after setup

Both main.yml and main_vms_only.yml updated. lift_ctf_airgap.yml handles
both DROP and REJECT variants (idempotent, state=absent).
…, tcpdump)

INSTALL_PACKAGES_UTILS_NETWORK was hardcoded to NO for the vuln-box stage,
blocking installation of nmap, tcpdump, net-tools and iputils-ping even though
iputils-ping was added to the role. Set to YES so all network diagnostic tools
are available on vuln-boxes.
…ir-gap rules

netfilter-persistent can save rules with a newline embedded in the comment
("r42: ctf\n  air-gap"), which ansible.builtin.iptables state=absent cannot
match. Add a second comment-based pass for duplicates, then a shell fallback
that loops iptables -D until no more DROP rules on vmbr144→vmbr0 remain.
Introduces the r42topo package skeleton in the playbooks repo: a pure,
framework-agnostic pydantic v2 core consumed by the backend API, r42topo's
own CLI/TUI, and the (rewrite-in-progress) range42 deployment CLI/TUI.

- core/models.py: Topology schema tree (Subnet/Zone/Box/Attachment/
  NetworkPolicyRef), extra=forbid, security deny-list guards
- core/constants.py: scenario-name regex (matches backend resolver, no dots),
  catalog-ref/bridge/IP patterns, vm_id bounds, octet rule, injection deny-list
- core/io.py: deterministic atomic JSON load/dump (sorted keys, round-trip)
- core/errors.py: framework-free exception hierarchy
- pyproject.toml: pydantic+pyyaml core deps; typer/textual/dev as extras
- tests: 14 tests, RED->GREEN, 94% coverage on core
- docs/r42topo-plan.md: full architecture + phased build order

Deploy path is _universal-only (rewrite-aligned): the compiler emits
topology.json + artifacts; deployment runs via _universal Plan B (external
prerequisite), not a materialized scenario dir.
Adds the catalog-template layer and global vm_id/IP allocation validation.

- core/catalog_models.py: BoxTemplate, NetworkPolicyTemplate (symbolic zones,
  services, allow/deny matrix, defaults), SubnetLayout — all extra=forbid
- core/catalog.py: load_catalog() resolves <category>/<id>/vX.Y.Z/template.yml,
  picks highest version, validates, records version+sha256; path posture mirrors
  backend checks_playbooks.py (regex + strict resolve + is_relative_to + symlink guard)
- core/idalloc.py: ReservedIndex parses JSONL _reserved.json; validate_allocation()
  enforces octet rule, intra-topology dup vm_id/IP, and cross-scenario collisions
  (own-scenario re-deploy allowed). Read-only/pure.
- core/constants.py: TEMPLATE_ID_RE, VERSION_DIR_RE, topology-layer dir/category names
- tests: +16 (catalog loader incl. shipped-template validation; idalloc rules)
- suite: 30 passing, 93% core coverage
First end-to-end vertical slice: author -> compile -> _universal.

- core/validate.py: semantic checks (zone/subnet refs, IP-in-subnet,
  dangling catalog refs)
- core/compiler/network_policy.py: compile symbolic policy + topology bindings
  into deterministically ordered FORWARD rules via weight bands (ESTABLISHED ->
  service -> zone-accept -> intra -> zone-drop -> air-gap -> default); plus
  lint_segmentation() fail-closed checks (FORWARD-only/host-SSH-safe, established
  precedes drops, no ACCEPT shadows a deny, air-gap present, terminal default-deny)
- core/compiler/{inventory,scenario_vms,stages}.py: hosts.yml (range42 nested
  groups), scenario_vms.json (demo_lab shape), per-zone attachment dispatch
  (template defaults + box attachments, deduped)
- core/compiler/__init__.py: compile_topology() orchestrator -> CompileResult
- core/extravars.py: resolve_universal_extravars() — typed r42_* allow-list
- api.py: load_catalog/author_topology/validate_topology/compile_topology/
  resolve_universal_extravars — pure adapter for backend + CLI/TUI consumers
- tests: +26 (56 total), 93% coverage

Verified: compiling against the REAL range42-catalog 05_topology_layer and
running the actual _universal stub (origin/feature/gamenet-authoring-v1) passes
green; generated FORWARD table reproduces demo_lab_network's isolation rules.
Thin frontends over the pure core, plus a shared scaffolding helper.

- core/scaffold.py: scaffold_topology() builds a minimal valid topology from a
  subnet_layout + policy (one box per role-matched zone, octet-rule vm_id/IP) —
  pure, reused by both CLI and TUI
- cli.py: Typer app — author / validate / compile / show (--rules); maps core
  errors to exit codes; console_script `r42topo`
- tui/controller.py: pure, framework-free façade (choices, scaffold, validate,
  save, summary, rules) — unit-tested without the event loop
- tui/app.py: Textual view over the controller; console_script `r42topo-tui`
- pyproject: add r42topo-tui entry point
- tests: +12 (68 total, 90% coverage) incl. CliRunner + headless Textual mount

Verified: `r42topo author --layout default-3zone --policy air-gap-ctf` against
the real catalog scaffolds a 3-box topology; `show --rules` renders the
compiled FORWARD table.
Security (from security-reviewer):
- C3 (CRITICAL): close the segmentation-linter bypass — wildcard-source DROP
  rules were skipped, so `accept ctf->admin` + `drop *->admin` passed clean.
  Rewrote lint_segmentation to check every compiled DROP (interface-aware, so
  air-gap rules don't false-positive) against earlier ACCEPTs; excludes the
  terminal catch-all. Added regression test (bypass caught, air-gap still clean).
- C1: deny-list now applied recursively to Attachment.params and
  NetworkPolicyRef.overrides (Jinja/SSTI surface) via reject_injection_nested
- C2: validate wan_interface (IFACE_RE) and service *_ip (ipaddress) overrides;
  missing/invalid service IP now fails closed instead of broadening the rule to
  "any destination". CompiledRule interface/port fields gain patterns.
- H1/H2/H3: injection guards on Topology.scenario/description/proxmox_node;
  PROXMOX_NODE_RE requires leading alnum (no empty/leading-dash)
- H4: extravars allow-list uses explicit raise, not assert (-O strips asserts)
- H5: air-gap zone absent from topology is now flagged (was silently skipped)
- M1/M2: MatrixRule src/dst patterns + comment guard; linter terminal-rule check
  covers reject as well as drop
- M4: CLI warns when --reserved is omitted (collision checks disabled)

Python quality (from python-reviewer):
- load_topology + api.author_topology wrap pydantic.ValidationError as
  TopologyError (no raw traceback leaks to the CLI)
- _fail typed NoReturn; schema_version pinned Literal[1]
- scenario_vms.json vms sorted by vm_id (full determinism)
- scaffold avoids dup vm_id/IP for repeated roles; drops redundant model_dump
- TUI handlers catch TopologyError/ValueError, not bare Exception
- idalloc docstring documents the TOCTOU window

Suite: 78 tests, 90% coverage.
ECC blueprint skill output — a cold-start plan for r42playbooks, the
msfvenom-style scenario generator (list catalog modules -> compose -> generate
a demo_lab-shaped scenarios/<name>/). 10 steps (S0 housekeeping, S1 rename
r42topo->r42playbooks, S2 spec model, S3 catalog pick/validate, S4 alloc+manifest,
S5b verbatim tree, S5a generated-from-manifest + API freeze, S6 CLI, S7 TUI,
S8 importable API, S9 ECC review). Adversarially reviewed (architect/opus);
CRITICAL+HIGH folded in: secrets/ symlink contract, generated-vs-copied
templates/*.j2, populate manifest templates[], resolved template-selection +
role->section decisions, name-reference (not copy) catalog contract.
Mechanical rename of the scenario-authoring package to its real identity
(what r42deploy/r42runtime will import). Behaviour unchanged.

- git mv r42topo -> r42playbooks (all modules + tui + tests imports)
- pyproject: name, readme, [project.scripts] (r42playbooks / r42playbooks-tui),
  packages.find include
- update docstrings, README, and the plan's REUSE table paths
- .gitignore module comment

Suite green (78 passed); import r42playbooks + cli/tui/api entrypoints OK.
Step 1 of docs/r42playbooks-plan.md.
Step 2 of docs/r42playbooks-plan.md. The reproducible msfvenom-style 'options'
artifact a user composes, written verbatim into every generated scenario.

- core/spec.py: ScenarioSpec + BoxSpec (extra='forbid', deny-list guards on
  free-text name/notes/vars), load_spec / dump_spec_atomic (YAML, deterministic).
- core/io.py: extract atomic_write_text helper (reused by spec IO); dump_topology
  now delegates to it (byte-identical output preserved).
- constants.py: BOX_COUNT_MAX bound on count expansion.
- BoxSpec.template_vm_id optional override (per plan §7.1, unblocks S4).
- tests/test_spec.py (15 cases) + conftest valid_spec_dict/spec_factory fixtures.

Full suite green.
Step 3 of docs/r42playbooks-plan.md. The list/validate source of truth.

- catalog.py: list_roles() scans 02_ansible_layer/**/roles/ names; list_containers()
  scans 03_container_layer/docker/_ctf/ for compose-bearing dirs (ref = path under
  _ctf/). Both read-only with symlink-escape guards; empty when a layer is absent.
- Catalog gains roles/containers sets, populated by load_catalog.
- validate_refs(spec, catalog): typo guard reporting unknown subnet_layout /
  network_policy / box template / role / container refs (gamification skipped).
- constants.py: 02/03 layer + compose-filename constants.
- tests/test_catalog_pick.py (10 cases) + fake_catalog fixture extended with 02/03
  layers (real range42-catalog is a separate gitignored repo, never in checkout).

Full suite green (103).
Step 4 of docs/r42playbooks-plan.md (Renderer A). Turn a ScenarioSpec into a
concrete VM list honouring the project invariants.

- core/templates_table.py: the fixed 12-row 9xxx Proxmox template table (verbatim
  from demo_lab manifest templates[]); select_template() picks lowest matching
  vm_id for a box spec, with explicit template_vm_id override (§7.1 / H2).
- core/allocate.py: place each box in its role's subnet, fill octets from a
  per-role base, assign vm_id=band*1000+octet (bands 1..8; 9xxx reserved for
  templates) so the octet rule holds by construction; expand count>1 to
  <template>-00..; merge template default_attachments + spec additions (names
  only, §2); skip ids/IPs owned by other scenarios in _reserved.json.
  manifest_dict/manifest_json emit the demo_lab shape with a populated
  templates[] (H1: old compiler hard-coded []).
- constants.py: ROLE_SUBNET_NAME / ROLE_BASE_OCTET / vm_id band bounds (team
  defaults to the student subnet per §7.1).
- tests/test_allocate.py (19 cases); fake_catalog gains a student-box template.

Full suite green (120).
Step 0 housekeeping: re-add /topology.json, /hosts.yml, /expanded.json scratch
ignores (present on feat/r42topo-canonical-schema, absent on this branch).
Plan §4.3 prerequisite: the explicit `find scenarios/demo_lab` baseline (110
files) the S5b/S5a golden tests assert the generated tree against. Captured now
so the renderer step has a frozen reference shape to mirror.
Render a composed Allocation + ScenarioSpec into scenarios/<name>/ mirroring
the demo_lab shape, emitting only the class-(B) files (plan §4.1): per-box
stage_00 clone playbooks, stage_01 plays that list catalog roles BY NAME
(the catalog↔playbooks name-reference contract, §2), per-box devkits,
per-section reinstall scripts, top-level setup/delete/reset scripts, the
class-B templates/ files, the vendored 01_init_proxmox/ template-creation
subtree (H3), and the originating scenario.r42.yml.

Resolved decisions (plan §8 mutation protocol, MUTATED 2026-06-03):
- H3: copy a single canonical 01_init_proxmox/ vendored as a package asset
  (sourced from blank_scenario_2_subnets, matching TEMPLATE_TABLE's vmbr140
  management subnet) into every scenario; main.yml imports its two playbooks.
- Richness: generate a uniform per-box structure only — NOT demo_lab's
  group playbooks / group devkits / _testing/ / builder_* (KISS/YAGNI).

Does NOT emit (left to S5a class-A): each section's _main.yml, templates/*.j2,
manifest/scenario_vms.json. Never creates secrets/ (deploy-time symlink, §4.2).

Pure + deterministic: all writes via core.io.atomic_write_text; output depends
only on the Allocation + spec. New: core/render.py, core/render_assets.py,
assets/scenario/01_init_proxmox/. tests/test_render.py (15 tests). 135 green.
…ss A)

Layer the class-(A) files that must reflect THIS composition (never copied from
demo_lab) on top of the S5b boilerplate:
- manifest/scenario_vms.json (vms[] + populated templates[], H1)
- templates/ansible-inventory.j2: per-composition groups (box.inventory_group)
  + member hosts r42.<vm_name>; proxmox/-cli groups kept verbatim
- templates/ssh-config.j2: one Host/Hostname block per VM (M5 naming) + a
  shared r42.* user/key/ProxyJump block
- each section's _main.yml: stage_00 imports with per-VM global_* overrides,
  then stage_01 imports

FREEZE the public API surface (gates S6 ∥ S7): api.render_scenario(spec, *,
catalog, dest, reserved=None) -> Path (allocates then renders), plus exported
allocate / load_spec / list_roles / list_containers / validate_refs /
ScenarioSpec. M5 invariant covered by a golden test: every concrete stage_01
`hosts: r42.<x>` resolves in the generated inventory.

New tests/test_render_classa.py (10 tests). 145 green.
Replace the legacy topology-compiler CLI (validate/compile/author/show on a
topology.json) with the scenario-generator surface over the frozen api.py:

- list <boxes|subnets|policies|roles|containers|scenarios> — enumerate pickable
  catalog modules, or existing generated scenarios under -o
- show <module> — auto-detect + describe one box/subnet/policy/role/container
- new <name> --subnet … --policy … --box admin-wazuh --box vuln-box:count=5
  [--spec scenario.r42.yml] [-o scenarios/] — compose a ScenarioSpec (flags or
  --spec, positional name wins) and render a deployable tree; prints the path

Thin shell only: validate_refs typo-guards before rendering, TopologyError ->
exit 1, no business logic in the CLI. The parked topology compiler stays
reachable via api.py for back-compat; it's just no longer a CLI subcommand
(pivot: r42playbooks is the generator, not the canonical engine).

tests/test_cli.py rewritten for the generator commands (11 tests). 156 green.
Replace the legacy topology-scaffold TUI with the msfvenom-style composer, all
logic in the pure (Textual-free) controller so it unit-tests headless:

- ScenarioComposerController: layouts/policies/box_templates/roles/containers
  choices; set_name/set_subnet/set_policy; add_box(template, count)/remove_box/
  clear_boxes; build_spec (raises TopologyError if incomplete/invalid);
  validate() (structural gaps + validate_refs); preview() (allocation preview);
  generate(dest) -> Path via api.render_scenario.
- ScenarioComposerApp: thin view — name + layout/policy selects, add-box row
  (template + count), Preview/Generate/Clear, output pane. Keeps #scenario /
  #output ids; errors surfaced in-pane.

tests/test_tui.py rewritten (8 tests: controller compose+validate+preview+
generate + mount smoke). core/ stays pure (no textual/typer imports). 155 green.
Make `import r42playbooks` drive generation by import alone (for r42deploy/
r42runtime and the range42 deployment CLI/TUI):

- __init__.py re-exports the frozen generator surface from api.py:
  load_catalog, list_roles, list_containers, validate_refs, load_spec,
  dump_spec_atomic, allocate, render_scenario, ScenarioSpec, Allocation,
  Catalog, ReservedIndex (+ legacy Topology, __version__).
- README rewritten generator-first: install extras, list/show/new CLI, TUI,
  and an import-only library example; documents the generated tree + the
  name-reference contract (secrets/ not generated).
- pyproject description updated (generator, not "topology compiler").

tests/test_package_api.py: pins the top-level export contract + import-only
roundtrip + core-error propagation (4 tests). 159 green.
ECC python-reviewer + code-reviewer + security-reviewer pass over S1–S8.
Fixes (CRITICAL: none; HIGH + security MEDIUMs resolved):

- render: STAGE01_PLACEHOLDER emitted a bare `[]` that `import_playbook`
  rejects ("a play must contain hosts/import_playbook/roles/tasks") — a no-role
  box would break the deploy. Now a valid no-op play (hosts + tasks: []).
- render: box `vars` from the spec were validated + carried to AllocatedBox but
  silently dropped — now emitted into the stage_01 play `vars:` block.
- render: SCENARIO_NAME_RE permits '/', which spawned malformed nested file
  paths (ctf/web1.setup.sh). File prefixes now use the leaf; dir tree keeps the
  full name. No write ever escaped dest ('..' was already deny-listed.)
- render_assets.fill(): raise on any leftover @@sentinel@@ (catches a misspelled
  key before it ships into a generated file).
- security: Attachment.catalog_ref had no deny-list (CATALOG_REF_RE admits '..');
  it is written verbatim as an Ansible role name. Added reject_injection guard.
- catalog: validate_refs now also checks each box template's default_attachments
  (the renderer emits them) — closes a silent typo-guard gap.
- catalog: resolved_version/hash raise CatalogNotFoundError not raw KeyError;
  _load_category narrows `except Exception` to pydantic ValidationError;
  _resolved is field(init=False).
- allocate: AllocatedBox.box_vars is now MappingProxyType (frozen-safe);
  manifest_dict -> dict[str, Any].
- cli: default --catalog ../range42-catalog (matches workspace layout, per
  user request); _build_spec uses try/else so `data` is always bound.
- spec: dump_spec_atomic/load_spec typed (Path); module-level Path import.
- tui: remove_box rebuilds the list immutably.
- api.render_scenario: docstring notes callers should validate_refs first.

Tests: +9 (placeholder no-op play, box_vars render, slash-name leaf prefix,
fill sentinel guard, catalog_ref traversal reject, validate_refs default-attach
gap, list scenarios CLI, tightened allocator raises to typed errors). 165 green.

Noted for a later step (pre-existing, out of S9 scope): TEMPLATE_TABLE does not
cover every real-catalog box spec (e.g. admin-wazuh 2cpu/4gb/32gb) — surfaces as
a clean ValidationError, not a crash.
t0kubetsu added 28 commits June 9, 2026 17:17
Role lives in range42-catalog which is not checked out in CI;
stub it so ansible-lint syntax-check passes.
- yaml[indentation]: fix 6-space vars_files indent in stage_00/mon_wazuh.yml
- schema[playbook]: remove null name key in stage_01/deployer_ui.yml
- yaml[truthy]: replace become: yes with become: true in stage_01/mon_wazuh.yml
- key-order[task]: move when: before block: in _r42_admin, _r42_student_box_group, _r42_vuln_box_group
- vars null: revert vars: {} -> vars: in vuln_box_02/03/04 stage_00 (had real vars below)
- vars null: fix vars: -> vars: {} in stage_00 files with comments-only after vars:
- add .ansible/roles/ stubs to work around ansible-lint 26.x mock_roles bug for 3-part dotted role names
….1.1

Every scenario (and the render_assets.py generator templates) hardcoded
vm_ci_dns_ips: "1.1.1.1". On lab networks that firewall 1.1.1.1, template
first-boot warm-up can't resolve apt mirrors and 'range42-context deploy'
hangs at the cloud-init auto-poweroff wait.

Use vm_ci_dns_ips: "{{ default_vm_ci_dns_ips | default('1.1.1.1') }}" (same
pattern as the adjacent vm_ci_user/vm_ci_password lines). The default is set
by range42-init.py from the Proxmox node resolver; 1.1.1.1 remains the
fallback for environments where it isn't set.
Scenarios consume vm_ci_dns_ips via {{ default_vm_ci_dns_ips | default('1.1.1.1') }}.
Define that default in the scenario templates so it lands in the right scope:
- ansible-vars.yml  -> group_vars/<scenario>/vars.yml (site.yml deploy path)
- vault-example.yml -> the operator-built vault (workspace deploy path, which is
  what range42-context deploy actually loads)

Ships 1.1.1.1 as the portable fallback; range42-init.py substitutes the detected
Proxmox node resolver at workspace creation.
Dual-lan test scenario exercising the new admin-rocketchat box template
(range42-catalog): a lan1 admin-rocketchat box + a lan2 debian-jump box,
generated by the scenario generator. For deploy-testing the Rocket.Chat
bootstrap (software.install.rocketchat) on the deployer.
Rebuild the aggregated reservation file from all per-scenario manifests so it
includes test_rocketchat (and back-fills demo_lab_network, test_apt_cacher,
test_apt_mirror, which were missing — the committed file was stale).

test_rocketchat VMs (1002/.150.2, 3002/.151.2) are collision-free. Pre-existing
cross-scenario collisions surfaced by the now-in-sync file (test_apt_cacher vs
test_apt_mirror, demo_lab vs demo_lab_network, blank_scenario_4 vs
demo_lab_network) are NOT addressed here — flagged for separate cleanup.
…lates

The template-create plays read {{ default_vm_ci_dns_ips | default('1.1.1.1') }},
but the generator's ANSIBLE_VARS_YML / VAULT_EXAMPLE_YML templates never defined
the var — so freshly generated scenarios fell back to 1.1.1.1. On networks where
1.1.1.1 is firewalled, cloud-init can't resolve, the template never auto-powers
off, and deploy hangs at "WAIT for cloud-init to auto-poweroff".

Define default_vm_ci_dns_ips in both generator templates (range42-init.py
substitutes the node resolver at workspace creation), matching the hand-fixed
scenarios. Also backfill the already-generated test_rocketchat scenario.
…rrored)

Template build IPs were derived as {template_subnet_prefix}.{box_octet}. Two
boxes in different lab subnets can legitimately share a host octet (e.g. dual-lan
.150.2 + .151.2), which mapped their two DISTINCT templates onto the same
template-subnet IP (both .140.2). The templates boot concurrently during
01_init_proxmox, so the shared IP collided on the bridge: the loser's cloud-init
couldn't reach the network, its apt-get update stalled, the VM never
auto-powered-off, and the deploy hung at "WAIT for cloud-init to auto-poweroff"
(intermittently small vs medium, an ARP race).

Allocate each distinct template a unique octet on the template subnet via
_next_free_octet (skipping the gateway) instead of mirroring the box octet.

- allocate.py: sequential collision-free template IPs (+ _TEMPLATE_IP_BASE)
- tests/test_allocate.py: regression — two boxes sharing an octet across subnets
  must yield distinct template IPs
- regenerate test_rocketchat (small template 9321 .140.2 -> .140.3) + _reserved.json
…nct template IPs)

Apply the allocate.py template-IP fix to the two apt scenarios: their templates
previously shared a build IP (box-octet mirroring), colliding on concurrent boot.
Regenerated so each scenario's templates get distinct template-subnet IPs
(cacher .140.2/.3; mirror .140.2/.3/.4) + refreshed _reserved.json.

Cross-scenario template-subnet overlap (distinct scenarios reusing low .140.x)
remains by design — scenarios deploy one at a time (delete-everything between),
so it does not cause the concurrent-boot collision that hung deploys.
…ir-gapped range)

Regenerate against the updated admin-rocketchat box template, which no longer
sets INSTALL_PACKAGES_NTP_AND_UPDATE_TIME. In an egress-filtered range udp/123
is blocked (even the Proxmox node shows clock unsynchronized), so the
basic_packages role's hard "wait for NTPSynchronized == yes" can never succeed
and failed stage_01. VMs take host (kvm-clock) time instead.
…/3003

Adds the test_mattermost scenario (dual-LAN, admin-mattermost BoxTemplate,
port 8065) mirroring the test_nextcloud/test_rocketchat pattern.
Registers vm_ids 1003 (lan1-admin-mattermost-00, 192.168.150.3) and
3003 (lan2-debian-jump-00, 192.168.151.3) in _reserved.json.
@t0kubetsu

Copy link
Copy Markdown
Contributor Author

Closing: branch was based on dev_ada, carries unrelated dev_ada commits. Replaced by a clean cherry-pick PR.

@t0kubetsu t0kubetsu closed this Jun 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants