Context
The scenario renderer (app/core/scenario_renderer/) consumes a ScenarioSpec
(app/core/scenario_renderer/types.py) — a resolved, deployment-ready description of a
scenario: tiers, VMs, bundle attachments, templates, cross-tier groups.
The canvas emits something else: the topology/CatalogEntry model in
app/schemas/generated.py (Node with kind, role, networks[],
attachments[], config, children, cidr_template, bridge_template,
replication, …).
Nothing currently maps one to the other, so the renderer cannot be driven from a
canvas-authored project. This is the missing link between the UI and the renderer.
Scope
Write the adapter: canvas topology → ScenarioSpec.
The canvas model is a superset of what ScenarioSpec needs, so this is mostly a
projection, not an invention:
Node.kind == vm|lxc → VmSpec (with role, template_vmid, config)
Node.kind == network → the bridge / CIDR / gateway that VM NICs reference
(cidr_template, bridge_template)
Node.networks[] (NetworkAttachment: node_ref, ip, ip_template, dhcp) → the
VM's NIC(s) — note this is already a list, i.e. the canvas can already express a
multi-homed VM even though the manifest cannot yet represent one
Node.attachments[] → BundleRef on the VM (stage_01) or on the tier group (baseline),
resolved through the bundle registry by grammar name
role → tier membership (TierSpec: admin / student / ctf / team)
replication → author-time expansion into concrete per-team VMs
Depends on
- The VMID/IP allocator: the canvas supplies no vmids, and
VmSpec requires them.
The clean split is: the adapter produces an unresolved draft, the allocator resolves
it into a ScenarioSpec. Design the draft types with the allocator, not separately.
Notes
- Validate early and loudly.
ScenarioSpec.__post_init__ and VmSpec.__post_init__
already reject unsafe scenario/VM names (they become filenames, ssh host tokens, and the
workspace name the TUI parses) — the adapter should surface those failures with the
offending canvas node id, not a bare ValueError.
- Bundle references must resolve by grammar name
(<tier>/<subject>.<verb>[.<object>]) through registry.py, never by physical path.
Acceptance
- A canvas project round-trips to a
ScenarioSpec and renders to a scenario directory
that passes ansible-playbook --syntax-check against the real bundles and catalog roles.
- An invalid canvas node (bad name, unknown bundle, missing network ref) produces an error
naming the node.
Context
The scenario renderer (
app/core/scenario_renderer/) consumes aScenarioSpec(
app/core/scenario_renderer/types.py) — a resolved, deployment-ready description of ascenario: tiers, VMs, bundle attachments, templates, cross-tier groups.
The canvas emits something else: the topology/
CatalogEntrymodel inapp/schemas/generated.py(Nodewithkind,role,networks[],attachments[],config,children,cidr_template,bridge_template,replication, …).Nothing currently maps one to the other, so the renderer cannot be driven from a
canvas-authored project. This is the missing link between the UI and the renderer.
Scope
Write the adapter: canvas topology →
ScenarioSpec.The canvas model is a superset of what
ScenarioSpecneeds, so this is mostly aprojection, not an invention:
Node.kind == vm|lxc→VmSpec(withrole,template_vmid,config)Node.kind == network→ the bridge / CIDR / gateway that VM NICs reference(
cidr_template,bridge_template)Node.networks[](NetworkAttachment:node_ref,ip,ip_template,dhcp) → theVM's NIC(s) — note this is already a list, i.e. the canvas can already express a
multi-homed VM even though the manifest cannot yet represent one
Node.attachments[]→BundleRefon the VM (stage_01) or on the tier group (baseline),resolved through the bundle registry by grammar name
role→ tier membership (TierSpec: admin / student / ctf / team)replication→ author-time expansion into concrete per-team VMsDepends on
VmSpecrequires them.The clean split is: the adapter produces an unresolved draft, the allocator resolves
it into a
ScenarioSpec. Design the draft types with the allocator, not separately.Notes
ScenarioSpec.__post_init__andVmSpec.__post_init__already reject unsafe scenario/VM names (they become filenames, ssh host tokens, and the
workspace name the TUI parses) — the adapter should surface those failures with the
offending canvas node id, not a bare
ValueError.(
<tier>/<subject>.<verb>[.<object>]) throughregistry.py, never by physical path.Acceptance
ScenarioSpecand renders to a scenario directorythat passes
ansible-playbook --syntax-checkagainst the real bundles and catalog roles.naming the node.