Newer
Older
openstack-caracal-dc-dc / runbooks / dc-dc-phase2-tofu-dc-substrate.md

DC-DC Phase 2 -- OpenTofu builds each DC substrate (Stage 3)

From Office1, script each DC's "physical" layer into existence: the node-VM libvirt domains MAAS will discover and deploy, the DC's plane networks, and its OPNsense edge -- then register the DC's libvirt host to that DC's own MAAS rack controller as a virsh VM-host. This runbook is written DC1-first. DC2 is explicitly gated (see the callout below) and is NOT part of this session's run until that gate clears -- do not duplicate these steps for DC2 with invented values in the meantime.

Governing docs: docs/dc-dc-buildout-design.md Section 4 "Phase 2" (the goal/build/gate this runbook satisfies), Section 9 (shim register -- the node-VM creation module has NO Roosevelt analog, see the callout below), and Section 6 (WAN simulation / netem, same-metro lean). docs/dc-dc-deployment-workflow.md Stage 3 row and the Tooling gap register (items 1, 4, 11 govern this stage directly). opentofu/README.md and opentofu/templates/README.md for exact module/token status -- read both before touching any .tf file or the OPNsense template.

Decisions this runbook owns: D-103 (the OpenTofu/MAAS/Juju lifecycle seam -- OpenTofu creates, MAAS discovers/commissions, never composes), D-100 (the dark-fiber mesh + per-site OPNsense edge, and the netem WAN-simulation mechanism riding on it).


!!! Depends on Stage 2 (Office1 headend standup). This stage's MAAS registration step needs a REACHABLE MAAS region controller (Stage 2's gate) and Stage 1's OpenTofu-reaches-vcloud-libvirt gate already closed. If Stage 2 has not completed when this session runs, STOP at Step 9 (MAAS registration) -- everything through Step 8 (planes/pool/edge/node-VM creation at the libvirt layer) does not itself require MAAS and may proceed, but do not fabricate a maas_api_url/maas_api_key pair to push past that dependency. Confirm Stage 2's actual state in docs/dc-dc-deployment-workflow.md before starting.

!!! DC2 is explicitly gated, not merely "later." D-101 has not assigned DC2's real, non-overlapping v4 supernet yet (open sub-item; Tooling gap register item 3's DATA half -- the import pipeline mechanism (netbox/dc-dc-prefixes-import.py) is built, but the literals themselves are not assigned). opentofu/main.tf's dc2_planes/dc2_storage blocks are commented out for exactly this reason. Do NOT uncomment them, do NOT invent a placeholder DC2 CIDR, and do NOT run this runbook's steps against DC2 until NetBox has assigned the real supernet and main.tf reflects it for real. This is a hard gate, re-stated in the GATE section.

!!! Node-VM creation has NO Roosevelt analog (Section 9 shim register). modules/node-vm (the blank-disk, PXE-boot libvirt domain this stage calls) is a virtual-only stand-in for what, in Roosevelt, is physical racking plus BMC enlistment -- MAAS enlists hardware that already exists there. Do not treat this module, or the act of Terraform-creating "node hardware," as reusable production IaC. Everything from MAAS enlistment onward (commission/deploy, Juju bundle) IS Roosevelt-transferable; this one step is not.

!!! Node sizing is not decided -- do not invent vCPU/RAM/disk. modules/node-vm's memory_mib, vcpu, and disk_size_bytes variables have no defaults by design (buildout-design Section 3's Phase-0 host/ disk-budget decision has not been made). If that decision is still open when you reach Step 6, STOP there and flag it -- do not pick numbers that "seem reasonable" to get tofu plan to run.

!!! netem parameters are still an unruled D-100 sub-item (gap #11). This stage's own gate text says "netem parameters applied and measured," but the exact latency/jitter/loss/rate values do not exist as a ruled decision -- only the qualitative buildout-design Section 6 lean exists: "same-metro dark fiber... low single-digit ms, jitter-capable, jumbo- capable." Step 11 below applies ONLY that qualitative lean as an explicitly-labeled PLACEHOLDER profile (recorded as such, with a citation to Section 6), pending a firmer numeric ruling. Do not write a specific number (e.g. "delay 3ms") into netem_args and represent it as measured or ruled -- if you pick a number to instantiate the mechanism for real, log it as exactly what it is: a placeholder pending D-100's still-open sub-item, not a final value.

!!! OPNsense prep tools not confirmed present. scripts/opnsense-prep- image.sh requires bunzip2 + qemu-img on PATH; scripts/opnsense- build-config-iso.sh requires genisoimage OR xorriso. Neither has been confirmed present on the vcloud host as of this writing (opentofu/README.md). Step 2 below checks for all four before anything else in this runbook proceeds -- treat a missing tool as a blocking finding (install it, or escalate), not something to route around.


Sequence (DC1; DC2 remains gated -- see callout above)

1.  Pre-flight: confirm Stage 1 + Stage 2 gates closed       (read-only)
2.  Confirm OPNsense prep-tool prereqs on the vcloud host    (read-only)
3.  Confirm/record still-open decisions this stage depends
    on (node sizing, config.xml tokens, MAAS zone/pool,
    netem parameters) -- STOP on any that are unresolved      (read-only)
4.  Prepare DC1's OPNsense base image + render config.xml
    + build the config ISO                                   [MUTATION: host-local files, gated]
5.  Wire modules/opnsense-edge for DC1 into main.tf           [repo change, gated]
6.  Wire modules/node-vm calls for DC1's node VMs into
    main.tf (ONLY once sizing is real)                        [repo change, gated]
7.  tofu init / validate / plan -- review before apply         (read-only)
8.  tofu apply -- DC1 OPNsense edge + DC1 node-VM domains      [MUTATION: creates libvirt objects, gated]
9.  Wire + apply modules/maas-vm-host -- register DC1's
    libvirt host with DC1's MAAS rack controller                [MUTATION: MAAS + repo change, gated]
10. Verify MAAS discovers the node VMs                          (read-only)
11. Wire + apply modules/netem-link on DC1's mesh legs,
    using the Section-6 placeholder lean, explicitly flagged     [MUTATION: gated]
12. Verify edge routing + simulated ISP uplink + netem applied   (read-only)
    -> EXIT GATE -> Stage 4 (per-DC, once DC2 clears its own gate)

Step 1 -- Pre-flight: confirm Stage 1 + Stage 2 gates closed (READ-ONLY)

CHECK -- Stage 1 (vcloud host prep) closed?

virsh net-list --all
virsh pool-list --all

Expect: DC1's six plane networks, the three mesh-link networks (dc1-dc2, dc1-office1, dc2-office1), and the dc1/office1 storage pools, all active -- Stage 1's Step 11 verification. If any are missing, Stage 1 is not actually done regardless of what docs/dc-dc-deployment-workflow.md says -- stop and reconcile there first.

CHECK -- Stage 2 (Office1 headend) closed? Confirm against docs/dc-dc-deployment-workflow.md's Stage 2 row and, if a Stage 2 runbook exists by the time you run this, its own exit gate. At minimum, confirm:

# from Office1 (or wherever this session's tofu apply will run from)
tofu -chdir=opentofu version
curl -sI "$MAAS_API_URL" 2>/dev/null | head -1   # MAAS_API_URL: this session's real value, not invented

Expect a real MAAS region controller reachable at a real URL. If Stage 2 is not done, everything through Step 8 below (libvirt-layer objects only) may still proceed since it needs no MAAS reach; Step 9 onward (MAAS registration) is a hard stop until Stage 2 closes -- do not substitute a placeholder MAAS endpoint to push past it.

GATE: Stage 1 objects present and active; Stage 2's MAAS reachability either confirmed, or explicitly recorded as NOT YET (in which case this session's scope for today is Steps 1-8 only).


Step 2 -- Confirm OPNsense prep-tool prerequisites on the vcloud host (READ-ONLY)

CHECK

for bin in bunzip2 qemu-img genisoimage xorriso curl wget; do
  command -v "$bin" >/dev/null 2>&1 && echo "present: $bin" || echo "absent:  $bin"
done

Required: bunzip2 + qemu-img (for scripts/opnsense-prep-image.sh), and EITHER genisoimage OR xorriso (for scripts/opnsense-build-config-iso.sh), plus curl or wget for the image fetch. If any required tool is absent, install it on the vcloud host (an OS-specific package install, out of scope to prescribe here) before Step 4 -- do not skip the tool and hand-build the ISO/qcow2 by some improvised method not covered by this repo's tested scripts.

GATE: all required tools present (or installed and re-checked).


Step 3 -- Confirm/record still-open decisions this stage depends on (READ-ONLY)

Before wiring anything into main.tf, walk this list. Any "NOT YET" item is a STOP for the corresponding later step -- record it plainly rather than inventing a value to keep moving:

  • Node sizing (memory_mib/vcpu/disk_size_bytes for modules/node-vm, and how many node VMs DC1 needs) -- buildout-design Section 3's Phase-0 host/disk-budget decision. Blocks Step 6.
  • MAAS zone/pool for DC1's VM-host registration (modules/maas-vm-host's zone/pool variables) -- both are optional (MAAS computes them if left null, confirmed safe per opentofu/README.md's audit pass), so this is NOT necessarily a blocker; record whichever real zone/pool name the operator wants, or confirm the null no-op is intentional. Affects Step 9.
  • DC1's real power_address (the virsh URI MAAS uses to reach the vcloud host) -- must be measured this session (or carried from Stage 1's libvirt_uri tfvar, if it is the SAME endpoint -- confirm, do not assume identical per modules/maas-vm-host's own variable note). Blocks Step 9.
  • OPNsense config.xml tokens -- per opentofu/templates/README.md's token legend, several tokens are pending Stage-0-adjacent decisions or a real boot measurement: {{WAN_IF}}/{{LAN_IF}} (only measurable by booting the domain and running ifconfig -- genuinely cannot be filled in before Step 8 creates the domain), {{WAN_IPADDR}}/{{WAN_SUBNET_BITS}}/ {{WAN_GATEWAY}} (pending NetBox), {{LAN_IPADDR}}/{{LAN_SUBNET_BITS}} (pending which plane(s) OPNsense's LAN interface actually serves -- flagged in the template README as not fully specified in the buildout design), {{MIRROR_SYNC_PROTOCOL}}/{{MIRROR_UPSTREAM_NET}}/ {{MIRROR_SYNC_PORT}} (pending mirror-software choice, gap #3-adjacent), and {{ROOT_PASSWORD_HASH}} (must be freshly generated per site, never reused from the shipped sample). Blocks Step 4's render sub-step for everything except {{OPNSENSE_HOSTNAME}}/{{DOMAIN}}/NTP defaults.
  • netem parameters -- still an unruled D-100 sub-item (gap #11); Step 11 uses the Section-6 qualitative placeholder only, explicitly labeled as such. Not a hard blocker for Step 11 (the mechanism can be exercised with a labeled placeholder), but IS a blocker for calling this stage's netem gate "final" -- see the GATE section.

GATE: every item above is either resolved with a real, measured/ruled value, or explicitly recorded as NOT YET with the corresponding downstream step named as blocked. Do not proceed past a step whose input is still "NOT YET" by inventing one.


Step 4 -- Prepare DC1's OPNsense base image + config.xml + config ISO [MUTATION: host-local files, gated]

This step produces plain files on the vcloud host filesystem -- no libvirt or MAAS object is created yet. Run from the vcloud host (or wherever these scripts execute with reach to write there).

MUTATION -- prep the base image (needs Step 2's tools)

bash scripts/opnsense-prep-image.sh   # see the script's own header for its exact args/output path

Expect: a decompressed, qcow2-converted, resized OPNsense nano image at a real output path -- record that path, it feeds Step 5's base_volume_path.

MUTATION -- render config.xml (only if every non-boot-measured token from Step 3 is real)

export OPNSENSE_HOSTNAME=... DOMAIN=... ROOT_PASSWORD_HASH=...
export WAN_IPADDR=... WAN_SUBNET_BITS=... WAN_GATEWAY=...
export LAN_IPADDR=... LAN_SUBNET_BITS=...
export MIRROR_SYNC_PROTOCOL=... MIRROR_UPSTREAM_NET=... MIRROR_SYNC_PORT=...
# WAN_IF / LAN_IF: cannot be set for real yet -- see the note below.
bash scripts/opnsense-render-config.sh dc1-opnsense-config.xml

WAN_IF/LAN_IF are the one pair the script's own REQUIRED_VARS list demands that genuinely cannot be measured before the domain exists (Step 8 creates it). If Step 3 flagged these as not yet measurable, this render sub-step cannot complete for real this session -- STOP here for DC1's config render, record it as blocked pending a real boot, and do not substitute a guessed vtnetN name. (One documented path forward once the domain exists: boot the domain with a MINIMAL/placeholder config good enough to reach a console, measure ifconfig's real vtnetN mapping, then re-render and re-seed -- that re-seed mechanic is not designed in this delivery; treat it as a real follow-up need if it comes to that, not an invented shortcut here.)

MUTATION -- build the config ISO (only once config.xml render succeeded)

bash scripts/opnsense-build-config-iso.sh dc1-opnsense-config.xml dc1-opnsense-config.iso

Expect: a plain ISO9660 image containing /conf/config.xml at a real output path -- record it, it feeds Step 5's config_iso_path.

GATE: base image path and (if unblocked) config ISO path recorded as real host-local file paths. If the config.xml render step is blocked on WAN_IF/LAN_IF, record that explicitly and treat Step 5's config_iso_path input as still pending -- do not wire Step 5 with a placeholder ISO.


Step 5 -- Wire modules/opnsense-edge for DC1 into main.tf [repo change, gated]

Only proceed once Step 4 produced real base_volume_path and config_iso_path values (or an explicit decision to proceed with a genuinely-final, if minimal, config.xml -- not a placeholder).

MUTATION -- edit opentofu/main.tf, adding a block shaped like:

module "dc1_opnsense" {
  source           = "./modules/opnsense-edge"
  vm_name          = "dc1-opnsense"
  memory_mib       = <REAL VALUE -- OPNsense's own sizing guidance, not invented>
  vcpu             = <REAL VALUE>
  pool_name        = module.dc1_storage.pool_name   # confirm real output name in modules/dc-storage-pool
  disk_size_bytes  = <REAL VALUE>
  base_volume_path = "<Step 4's real base image path>"
  config_iso_path  = "<Step 4's real config ISO path>"
  lan_network_name = <the dc1_planes output this edge's LAN side attaches to -- confirm which plane per Step 3's open LAN-role item>
  wan_network_name = module.mesh_dc1_office1... # or a dedicated per-site ISP-uplink segment -- confirm which, do not assume
}

Every placeholder above (<...>) must be a real, measured/decided value before this is committed -- this template line exists to show shape, not to be copy-pasted with invented numbers. memory_mib/vcpu/disk_size_bytes here have no ratified default in the module either (same "no invented specs" discipline as modules/node-vm).

GATE: opentofu/main.tf diff shows exactly one new module "dc1_opnsense" block, every argument a real value, nothing else changed.


Step 6 -- Wire modules/node-vm calls for DC1's node VMs into main.tf [repo change, gated]

ONLY if Step 3 confirmed node sizing is real. If sizing is still "NOT YET," STOP here -- this step does not proceed, and this session's DC1 pass ends at whatever the last unblocked step was (Step 5 or wherever Step 4 left off). Re-run from here once sizing exists; do not invent numbers to close this out early.

MUTATION -- edit opentofu/main.tf, one module "dc1_node_XX" block per node VM, shaped like:

module "dc1_node_01" {
  source          = "./modules/node-vm"
  vm_name         = "dc1-node-01"
  memory_mib      = <REAL VALUE from the Phase-0 sizing decision>
  vcpu            = <REAL VALUE>
  pool_name       = module.dc1_storage.pool_name
  disk_size_bytes = <REAL VALUE>
  network_names   = [ <ordered list of module.dc1_planes outputs -- confirm which plane goes first for PXE priority, per D-103> ]
}

Repeat per node, per the real node count from the sizing decision.

GATE: every module "dc1_node_*" block uses only real, decided values; node count matches the recorded sizing decision exactly.


Step 7 -- tofu init / validate / plan (READ-ONLY against providers)

cd opentofu
tofu fmt -check -recursive -diff .
tofu init -backend=false -input=false
tofu validate
tofu plan -out=phase2-dc1.tfplan

Review the plan line by line: expect creates for module.dc1_opnsense (the edge VM's boot volume, config-seed volume, and domain) and each module.dc1_node_* (a blank boot volume + domain per node). Confirm nothing else is planned -- in particular, confirm no DC2 resource appears (its module blocks should still be commented out per the DC2 gate above).

GATE: plan matches this expectation exactly; no unexpected creates, updates, or destroys. Do not apply a plan you have not read.


Step 8 -- tofu apply -- DC1 OPNsense edge + node-VM domains [MUTATION: gated]

cd opentofu
tofu apply phase2-dc1.tfplan

Confirm this is the exact reviewed plan file from Step 7 (not re-planned) before running. This is the first live mutation THIS stage performs.

VERIFY

virsh list --all
virsh domblklist dc1-opnsense
virsh domblklist dc1-node-01   # per real node name

Expect the new domains present (likely shut off until first boot/PXE), the OPNsense domain showing both the base overlay disk and the config ISO attached as a secondary cdrom, each node VM showing a single blank boot disk.

GATE: all new domains defined as planned; disk attachments match Step 5/6's wiring.


Step 9 -- Wire + apply modules/maas-vm-host -- register DC1's libvirt host with DC1's MAAS rack controller [MUTATION: gated]

Hard dependency on Stage 2. This step requires DC1's MAAS rack controller (under the Stage-2-stood-up MAAS region) to actually exist and be reachable. If Stage 2 (or the DC1 rack-controller enrollment specifically, however that ends up sequenced relative to Stage 2's own runbook) is not done, STOP here -- this stage's remaining steps (9-12) do not proceed, and DC1's Phase 2 work is INCOMPLETE, not silently skippable.

MUTATION -- edit opentofu/main.tf

module "dc1_maas_vm_host" {
  source         = "./modules/maas-vm-host"
  vm_host_name   = "dc1-vcloud"
  power_address  = "<Step 3's real, measured virsh URI -- confirm same as var.libvirt_uri or a distinct value>"
  zone           = <real zone name, or leave unset for MAAS's computed default -- per Step 3>
  pool           = <real pool name, or leave unset -- per Step 3>
}
cd opentofu
tofu plan -out=phase2-dc1-maashost.tfplan   # review: exactly one new maas_vm_host resource
tofu apply phase2-dc1-maashost.tfplan

GATE: maas_vm_host resource created; no other resource touched by this plan.


Step 10 -- Verify MAAS discovers the node VMs (READ-ONLY)

CHECK

maas admin vm-hosts read | jq -r '.[] | select(.name=="dc1-vcloud") | {id, name, resources}'
maas admin machines read | jq -r '.[] | select(.hostname|test("^dc1-node")) | "\(.hostname)\t\(.status_name)"'

Expect DC1's node VMs listed as MAAS machines (status likely New -- enlistment discovered them; commissioning is Stage 4's job, not this stage's). If the node VMs do not appear, do not proceed to declare this stage's MAAS-discovery gate met -- troubleshoot the VM-host registration (power_address correctness, virsh reachability from the MAAS rack controller) before moving on.

GATE: DC1's OpenTofu-created node VMs visible in MAAS, associated with the dc1-vcloud VM host.


Step 11 -- Wire + apply modules/netem-link on DC1's mesh legs, placeholder profile [MUTATION: gated]

Per the netem callout above: this step applies ONLY the buildout design's qualitative Section 6 lean ("same-metro dark fiber... low single-digit ms, jitter-capable, jumbo-capable"), explicitly recorded as a PLACEHOLDER, not a ruled numeric value. If the operator has since ruled a specific numeric profile, use that instead and cite the ruling; do not use this placeholder language once a real ruling exists.

MUTATION -- edit opentofu/main.tf, one block per relevant mesh leg (at minimum DC1<->DC2 and DC1<->Office1; DC2<->Office1 is out of scope while DC2 remains gated):

module "netem_dc1_office1" {
  source                  = "./modules/netem-link"
  link_name               = "dc1-office1"
  vcloud_host_ssh_target  = "<real SSH target for the vcloud host -- measured, not invented>"
  bridge_name             = "<real OS bridge name for this mesh-link network -- confirm via `virsh net-info dc1-office1` or `tofu show`, not assumed>"
  netem_args              = "<PLACEHOLDER pending D-100 sub-item ruling -- record explicitly as provisional, e.g. a low single-digit-ms delay with modest jitter and negligible loss, cited to buildout-design Section 6, NOT presented as a measured or ruled value>"
}

(A DC1<->DC2 block follows the same shape once DC2 itself is ungated; until then, applying netem on that specific leg has no DC2-side substrate to matter for, so it is naturally deferred alongside the rest of DC2's work.)

cd opentofu
tofu plan -out=phase2-dc1-netem.tfplan
tofu apply phase2-dc1-netem.tfplan

GATE: the terraform_data/local-exec resource applies without error; record the exact netem_args string used and its PLACEHOLDER status in the as-executed log, alongside a note that D-100's exact parameters remain open (gap #11).


CHECK -- from the vcloud host

sudo tc qdisc show dev <bridge_name>   # per Step 11's real bridge name

Expect a netem qdisc present with the applied (placeholder) parameters.

CHECK -- OPNsense edge reachability (only meaningful once WAN_IF/LAN_IF have been measured on a real boot per Step 4's note, and the domain has actually been booted/configured)

virsh domstate dc1-opnsense

If the config.xml render was blocked in Step 4 (pending WAN_IF/LAN_IF), this verification is necessarily partial this session -- record that honestly rather than declaring edge routing verified when it is not.

GATE (this step): netem qdisc present and matching Step 11's recorded placeholder profile; OPNsense domain running IF its config.xml was successfully completed, otherwise recorded as blocked pending a real-boot vtnetN measurement and re-seed.


GATE -- Stage 3 exit condition (honest state, not aspirational)

Buildout-design Section 4 / deployment-workflow Stage 3 states the gate as: "MAAS rack controller per DC up; MAAS discovers the OpenTofu-created node VMs; edge routing + simulated ISP uplink up; netem parameters applied and measured." As of what this runbook can actually close this session:

  • MAAS rack controller per DC up: achievable ONLY once Stage 2 (or whatever runbook stands up DC1's specific rack controller) is done -- Step 1/9 depend on it. Record actual state, do not assume it.
  • MAAS discovers the OpenTofu-created node VMs: achievable through Step 10, CONDITIONAL on node sizing being real (Step 3/6) and Stage 2's MAAS region being reachable (Step 9's dependency).
  • Edge routing + simulated ISP uplink up: PARTIALLY achievable. The OPNsense domain and its network attachments can be created (Step 8), but full edge routing verification depends on WAN_IF/LAN_IF being measured on a real boot (Step 4's documented blocker) and on the WAN/LAN addressing tokens (pending NetBox) -- do not mark this sub-gate fully closed unless those are genuinely resolved.
  • netem parameters applied and measured: applied, YES (Step 11), using an explicitly-labeled PLACEHOLDER same-metro-lean profile per buildout-design Section 6 -- MEASURED in the sense of "the qdisc is present and its effect can be observed" (e.g. via ping/iperf once the domains are reachable), but NOT measured against a ruled, final numeric target, because that target does not exist yet (D-100 gap #11). Do not represent this sub-gate as "done" in the same sense as a stage with no open decisions -- it is done AS FAR AS A PLACEHOLDER ALLOWS, with the remaining gap named.

This stage's exit gate is therefore CONDITIONALLY MET at best on any run before (a) Stage 2 is complete, (b) node sizing is decided, (c) DC1's OPNsense WAN/LAN addressing and vtnetN mapping are measured, and (d) D-100's exact netem parameters are ruled. Update docs/dc-dc-deployment-workflow.md's Stage 3 row to reflect the REAL state after running this (e.g. "PARTIAL -- libvirt objects created, MAAS registration blocked on Stage 2" or similar), never to a blanket DONE unless every sub-condition above is genuinely true.

DC2: none of the above applies to DC2 until NetBox assigns its real supernet (D-101 open item) and main.tf's DC2 blocks are uncommented with real values -- re-run this entire runbook for DC2 at that point, do not attempt a partial DC2 pass now.

-> Proceed to Stage 4 (MAAS enlist/commission/deploy) for DC1 once this gate's applicable sub-conditions are genuinely met; DC2 remains blocked at Stage 3 until its own gate (NetBox supernet) clears.


Delivery checklist (this repo's standard discipline)

  • bash scripts/repo-lint.sh clean (0 fail) before committing any repo
    changes made while executing this runbook (`main.tf` edits, any new
    tfvars, minus secrets).
  • bash scripts/opentofu-validate.sh green (the harness for opentofu/,
    per its own README) -- re-run after every `main.tf` change in Steps
    5/6/9/11, not just once at the end.
  • Changelog entry for this runbook's first real execution (next DOCFIX
    number via `bash scripts/ledger-scan.sh`), noting the ACTUAL measured/
    decided values used (node sizing, MAAS zone/pool, power_address, the
    netem placeholder profile and its provisional status) -- redact
    nothing that isn't a secret.
  • docs/session-ledger.md updated with the outcome, including which
    sub-steps were blocked and why (Stage 2 dependency, node sizing,
    WAN/LAN token gaps, D-100 netem sub-item).
  • docs/dc-dc-deployment-workflow.md Stage 3 row updated to the REAL,
    honest state (PARTIAL/DONE/NOT STARTED per the GATE section above),
    never rounded up to DONE with open sub-conditions.
  • opentofu/README.md updated to reflect DC1's OPNsense edge / node-VM /
    MAAS-vm-host / netem-link modules moving from "not instantiated" to
    instantiated (or partially so) once this runbook actually applies
    them, including any real bug found while doing so (matching this
    repo's practice of logging schema surprises as they're found).
  • DC2 gate re-confirmed as still closed (NetBox supernet not yet
    assigned) if this session did not clear it -- do not let this
    runbook's DC1 completion be misread as covering DC2.