diff --git a/docs/changelog-20260730-dc0-node-carve.md b/docs/changelog-20260730-dc0-node-carve.md index 2226c15..e873b04 100644 --- a/docs/changelog-20260730-dc0-node-carve.md +++ b/docs/changelog-20260730-dc0-node-carve.md @@ -92,6 +92,34 @@ **Revert.** `git rm scripts/dc-node-carve.sh tests/dc-node-carve/run-tests.sh` and `bash scripts/run-tests-all.sh --record-manifest`. Nothing else depends on it yet. +## Item 1b -- the LIVE DRY-RUN found a defect the whole fixture suite had missed + +**What.** The first live `apply` (plan only) against the canary refused `enp1s0`: + + [FAIL] enp1s0: already linked to 10.12.8.0/22 as 'auto:-' but the ruled address is + '10.12.8.153' -- refusing to silently accept it + +**Why it matters.** Every PXE leg comes out of enlistment holding an `auto` link on +metal-admin -- MEASURED on all ten dc0 machines. So the tool could not carve the +metal-admin leg on ANY machine. The "wrong address on the right subnet is a FAILURE" +rule was correct in intent and too broad in fact: a commissioning link is the EXPECTED +STARTING STATE, not a conflict. + +**Fix.** The conversion is narrowed BY MODE. An `auto`/`dhcp`/`link_up` link is MAAS's +own default and gets unlinked and re-created as STATIC (MAAS will not convert a link's +mode in place). A `static` link with the wrong address is still REFUSED -- something +deliberate put it there, and moving it silently could strand whatever depends on it. +Both directions are now fixtures (T26, T27) and both are mutation-proven: dropping the +unlink turns T26c red, widening the conversion to include `static:` turns T27 red. + +**THIS IS THE THIRD TIME IN TWO DAYS A LIVE RUN CAUGHT WHAT FIXTURES DID NOT** -- the +same reason this tool was deliberately not written blind last session. The `raw` fixture +DID model the auto link correctly; what the harness lacked was an assertion that +`apply` over the raw state SUCCEEDS. Tests T21-T24 only grepped for planned strings and +never checked the exit code, so a refusal on the very first NIC read as green. + +**Revert.** Same as item 1. + ## Item 2 -- `tests/HARNESS-MANIFEST` re-recorded (92 -> 93) **What.** Re-recorded to pin the new harness. Required: the gauntlet's R15(2) drift gate diff --git a/scripts/dc-node-carve.sh b/scripts/dc-node-carve.sh index 402fe02..0f65f19 100755 --- a/scripts/dc-node-carve.sh +++ b/scripts/dc-node-carve.sh @@ -213,6 +213,19 @@ print("%s:%s"%(l.get("mode",""),l.get("ip_address") or "-")); sys.exit() ' "$1" "$2" 2>/dev/null; } +# if_link_id -- the MAAS link row id for that interface's link on +# that subnet, else "". Needed to unlink a commissioning AUTO link. +if_link_id(){ printf '%s' "$IFJSON" | python3 -c ' +import json,sys +n,cidr=sys.argv[1],sys.argv[2] +for i in json.load(sys.stdin): + if i.get("name")==n: + for l in (i.get("links") or []): + s=l.get("subnet") or {} + if s.get("cidr")==cidr: + print(l.get("id","")); sys.exit() +' "$1" "$2" 2>/dev/null; } + # if_l3_count -- how many links carry a subnet at all (br-ex member must be 0). if_l3_count(){ printf '%s' "$IFJSON" | python3 -c ' import json,sys @@ -261,7 +274,7 @@ # --- one raw plane leg: VLAN move then STATIC ------------------------------ carve_raw(){ - local sid="$1" nic="$2" cidr="$3" ip="$4" ifid vlan sub cur link + local sid="$1" nic="$2" cidr="$3" ip="$4" ifid vlan sub cur link lid ifid="$(if_field "$nic" id)" [ -n "$ifid" ] || { bad "$nic not found on this machine"; return 1; } vlan="$(sub_field "$cidr" vlan)"; sub="$(sub_field "$cidr" id)" @@ -269,10 +282,31 @@ link="$(if_link "$nic" "$cidr")" if [ "$link" = "static:$ip" ]; then note "$nic already STATIC $ip on $cidr -- SKIP"; ok "$nic STATIC $ip on $cidr"; return 0; fi - # A link on the RIGHT subnet with the WRONG address is a FAILURE, never a skip. - if [ -n "$link" ] && [ "$link" != "static:$ip" ]; then - bad "$nic: already linked to $cidr as '$link' but the ruled address is '$ip' -- refusing to silently accept it"; return 1 - fi + + # A COMMISSIONING LINK IS THE EXPECTED STARTING STATE, NOT A CONFLICT. Every + # PXE leg comes out of enlistment holding `auto` on metal-admin (measured: all + # ten dc0 machines). MAAS will not convert a link's mode in place, so the auto + # link is UNLINKED and re-created as STATIC. This is the one case where an + # existing link on the target subnet is replaced rather than refused -- and it + # is narrow BY MODE: an `auto`/`dhcp`/`link_up` link is MAAS's own default, a + # `static` one was put there by a human or an earlier ruled carve. + case "$link" in + auto:*|dhcp:*|link_up:*) + lid="$(if_link_id "$nic" "$cidr")" + if [ -n "$lid" ]; then + note "$nic holds a commissioning '${link%%:*}' link on $cidr -- converting to STATIC $ip" + do_unlink "$sid" "$ifid" "$lid" "$nic" || return 1 + ifid="$(if_field "$nic" id)" + fi + ;; + "") ;; + *) + # A STATIC link on the RIGHT subnet with the WRONG address is a FAILURE, + # never a skip: something deliberate put it there and silently moving it + # could strand whatever depends on it. + bad "$nic: already linked to $cidr as '$link' but the ruled address is '$ip' -- refusing to silently accept it"; return 1 + ;; + esac cur="$(if_field "$nic" vlan)" if [ "$cur" != "$vlan" ]; then do_update_vlan "$sid" "$ifid" "$vlan" "$nic" || return 1 diff --git a/tests/dc-node-carve/run-tests.sh b/tests/dc-node-carve/run-tests.sh index ca5c66b..d8e04d0 100755 --- a/tests/dc-node-carve/run-tests.sh +++ b/tests/dc-node-carve/run-tests.sh @@ -270,6 +270,23 @@ has "T22 plans the VLAN move" "$out" "\[plan\] enp3s0" has "T23 plans the br-ex create" "$out" "\[plan\] create br-ex (OVS) parent=enp2s0" hasnt "T24 dry-run reports no mutations" "$out" "mutations applied" +# THE COMMISSIONING LINK IS THE EXPECTED STARTING STATE, NOT A CONFLICT. Every +# PXE leg comes out of enlistment holding `auto` on metal-admin. A live dry-run +# caught this: the tool refused enp1s0 outright, so it could not carve the +# metal-admin leg on ANY of the ten machines. +chk "T26 apply over the measured raw state plans cleanly rc=0" "$rc" "0" +has "T26b names the commissioning link it is converting" "$out" "holds a commissioning 'auto' link" +has "T26c plans the unlink before the static" "$out" "\[plan\] unlink enp1s0" +has "T26d plans the metal-admin static" "$out" "\[plan\] enp1s0(id=1) -> STATIC 10.12.8.153" +hasnt "T26e does NOT refuse the auto link" "$out" "refusing to silently accept it" + +# The auto-link conversion above must NOT have widened into "overwrite anything". +# A STATIC link on the right subnet with the wrong address was put there +# deliberately; moving it silently could strand whatever depends on it. +out="$(R wrongip apply vr1-dc0 "${GATE[@]}" --host vr1-dc0-storage-04)"; rc=$? +chk "T27 apply still REFUSES a static wrong address rc=1" "$rc" "1" +has "T27b names the refusal" "$out" "refusing to silently accept it" +has "T27c names the conflicting address" "$out" "10.12.32.199" out="$(STUB_S04_STATUS=Deployed R carved apply vr1-dc0 "${GATE[@]}" --host vr1-dc0-storage-04 --commit)"; rc=$? chk "T25 apply on a Deployed machine FAILS rc=1" "$rc" "1"