Skip to content

feat(manifest): scenario_vms.json schema v3 — additive nics[] for multi-homed VMs #110

Description

@pparage

Context

app/core/scenario_renderer/manifest.py emits manifest/scenario_vms.json, the
scenario's identity declaration consumed by the deployer-cli, by the scenario lifecycle
scripts, and by the _reserved.json collision ledger. It is schema v2, and it gives
each VM exactly one ip and one bridge:

{ "vm_id": 1100, "vm_name": "admin-wazuh", "ip": "192.168.142.100",
  "role": "admin", "bridge": "vmbr142" }

That single-address assumption makes it impossible to describe a router, firewall,
dual-homed jump host or pivot box
— a VM with a leg in several segments. Which means it
is impossible to describe any realistic segmented topology (simulated internet / DMZ /
office LANs), where the routing and filtering device is part of the exercise rather than
the Proxmox host. A DMZ with no firewall in front of it is just another subnet.

Why a naive v3 is dangerous

Moving ip and bridge down inside a nics[] array makes both fields null at every
existing reader, and almost all of those readers fail silently:

Consumer Sites Failure mode
range42-playbooks scenarios/_check_reserved.sh checks 4, 5, 7, 8 (:79 :86 :126 :145) 4 "\(.bridge) \(.ip)" collapses to the constant "null null" for every VM — the IP-collision safety net dies without printing anything
range42 range42-context.sh known_hosts flush (:221, :1124) 2 jq -r '.vms[].ip' → null → ssh-keygen -R null no-ops → stale host keys on every redeploy
range42-playbooks scenario reset/delete scripts 57 × jq -r '.vms[].ip' across 16 scenarios all null, all silent
range42-ansible_roles-debug-devkit manifest lookups, incl. find_ips_by_bridge / find_vms_by_bridge 31 select(.bridge == $b) matches zero rows

Proposal: additive v3

Add nics[], and keep top-level ip / bridge as a mirror of nics[0], the
management NIC:

{ "vm_id": 3000, "vm_name": "fw01",
  "ip": "192.168.149.10", "bridge": "vmbr149",
  "role": "admin",
  "nics": [
    { "index": 0, "bridge": "vmbr149", "ip": "192.168.149.10", "gateway": "192.168.149.1" },
    { "index": 1, "bridge": "vmbr150", "ip": "192.168.150.1" },
    { "index": 2, "bridge": "vmbr151", "ip": "192.168.151.1" }
  ] }

Every existing consumer keeps reading the scalar and keeps working. New consumers read
nics[]. Cost is one redundant field plus a rule that the two must agree — cheap to
assert as an additional check in _check_reserved.sh, alongside a per-NIC cross-product
collision check.

The alternative (a clean v3 that drops the scalars) requires migrating five repos and
~90 call sites in one go, and strands the only collision-checking safety net the project
has in the meantime.

Scope of this issue (backend side only)

  • manifest.py — emit nics[] + mirrored scalars; bump "version" to 3.
  • manifest.py:_validate_duplicates([(vm.bridge, vm.ip) ...]) is structurally
    single-NIC; make the collision check a per-NIC cross-product.
  • types.pyVmSpec.ip / .bridge / .gateway become a NIC list; nics[0] is the
    management NIC by definition.
  • workspace_templates.py:175f" Hostname {vm.ip}" in ssh-config.j2 must
    resolve to the management NIC explicitly (ssh-config.j2 allows exactly one Hostname
    per Host, so this needs a stated rule, not an accident).
  • bootstrap.py:28-30 and stage01.py:71 — currently emit the scalar
    global_vm_ci_ip / global_vm_net_virtio_bridge / global_vm_ci_ip_gw into stage
    files. These follow whatever the vm-bootstrap bundle contract becomes.
  • tests/core/test_scenario_renderer_manifest.py:42 and :134 pin version == 2 and an
    exact key-set; they will fail loudly, which is correct.

Blocked on

This is a cross-repo contract change and must not land unilaterally:

  1. Agreement from the playbooks owner on additive-v3 vs clean-v3.
  2. The vm-bootstrap bundle contract (scalar → NIC list) in range42-playbooks — the
    manifest change is downstream of it.
  3. Multi-NIC support in range42-ansible_roles-proxmox_controller: vm_create.yaml:38
    hardcodes a single net0, vm_clone.yaml:18-24 passes no network params at all, and
    cloudinit_set_variables.yaml:33 emits ipconfig0 only — so today a second NIC can
    be attached but can never be given an IP. Emitting a schema for VMs nothing can build
    would be premature.

Until those land, the renderer should keep emitting v2 and reject a >1-NIC scenario with
a clear error naming the blocker.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions