# pfi-gx10 — RETIRED 2026-09-03. Kept for its reasoning, not for running. # # ⚠ THE STATIC-IP APPROACH IN THIS FILE WAS NOT USED. Operator ruling on the # day of the rack move: put the address on the SWITCH/FIREWALL side as a DHCP # RESERVATION and leave the host on DHCP, so moving the box later does not mean # unpicking a stale netplan static on a machine you may not be able to reach. # The host-static this playbook writes is exactly the "interesting adventure" # that ruling avoids. # # What was actually done instead (see docs/runbooks/gx10-rack-network.md): # 1. UniFi switch port 22 native network -> nh3-servers (VLAN 50) # 2. UniFi client reservation: 30:c5:99:3d:a7:45 -> 10.100.50.60 # 3. host DHCP renew; Wi-Fi left UP until the wired path was proven # 4. dns/internal.yaml + dns-sync # 5. nmcli radio wifi off, last, as its own step # # The safety ORDER below is still right and was followed: verify carrier, # verify MAC not interface name, keep Wi-Fi up as the escape hatch, prove the # new address from OUTSIDE, tear the hatch down separately and last. # # pfi-gx10 — move from desk (Wi-Fi + DHCP, VLAN 10) to rack (wired static, # VLAN 50 nh3-servers). Run AFTER the box is racked and the switch port has # been placed on nh3-servers. # # scripts/elway infra-ops@ --playbook playbooks/gx10-rack-network.yaml # # Find without asking anyone — the wired NIC has its own MAC: # curl -sk "https://10.100.0.1/proxy/network/api/s/default/stat/sta" \ # -H "X-API-KEY: $(secret get unifi/pfi-udmse-api-key)" \ # | python3 -c "import json,sys;[print(c['ip'],c.get('sw_port')) for c in json.load(sys.stdin)['data'] if c['mac']=='30:c5:99:3d:a7:45']" # # ───────────────────────────────────────────────────────────────────────────── # WHY THIS IS SAFE TO RUN AGAINST A BOX IN A RACK # # The lockout risk in remote network reconfiguration is applying a static # address that does not work and losing the only path back. This playbook never # has one path: Wi-Fi (wlP9s9) STAYS UP THROUGHOUT. The wired interface is # configured alongside it, verified from outside on the new address, and only # then is Wi-Fi torn down — by a SEPARATE playbook run, after a human has seen # the new address answer. # # So a botched netplan costs a retry over Wi-Fi, not a trip to the rack. # ───────────────────────────────────────────────────────────────────────────── vars: wired_if: enP7s7 wired_mac: "30:c5:99:3d:a7:45" static_ip: 10.100.50.60 prefix: "24" gateway: 10.100.50.1 dns_servers: "10.100.50.40" # nh3-docker / AdGuard, per dns/internal.yaml search_domain: nh3.internal steps: - name: Refuse to proceed if the cable is not actually in # carrier=0 means no link. Writing a static config for a dead interface and # calling it done is the kind of green that costs a rack visit to discover. shell: test "$(cat /sys/class/net/{{ wired_if }}/carrier 2>/dev/null)" = "1" - name: Confirm the interface is the NIC we think it is # Interface names can renumber across kernels. The MAC cannot. shell: test "$(cat /sys/class/net/{{ wired_if }}/address)" = "{{ wired_mac }}" - name: Back up the existing netplan shell: sudo cp -an /etc/netplan /etc/netplan.bak-preRack 2>/dev/null || true creates: /etc/netplan.bak-preRack - name: Write the wired static config # A SEPARATE file from whatever manages Wi-Fi. Wi-Fi keeps working while # this lands; that is the escape hatch and it is deliberate. shell: | sudo tee /etc/netplan/60-wired-static.yaml >/dev/null <<'EOF' network: version: 2 ethernets: {{ wired_if }}: dhcp4: false dhcp6: false addresses: [{{ static_ip }}/{{ prefix }}] routes: - to: default via: {{ gateway }} metric: 100 nameservers: addresses: [{{ dns_servers }}] search: [{{ search_domain }}] EOF sudo chmod 600 /etc/netplan/60-wired-static.yaml creates: /etc/netplan/60-wired-static.yaml - name: Apply # `netplan try` would be better but needs a TTY to accept or roll back; # under elway there is nobody to press a key. Wi-Fi staying up is what # substitutes for the auto-rollback. shell: sudo netplan apply - name: Wait for the address to come up locally shell: | for i in $(seq 1 15); do ip -4 addr show {{ wired_if }} | grep -q "{{ static_ip }}/{{ prefix }}" && exit 0 sleep 2 done exit 1 verify: - name: Wired interface holds the static address shell: ip -4 addr show {{ wired_if }} | grep -q "{{ static_ip }}/{{ prefix }}" changed_when: "false" - name: Default route is via the servers-VLAN gateway shell: ip route get 1.1.1.1 | grep -q "via {{ gateway }}" changed_when: "false" - name: Gateway answers shell: ping -c 2 -W 2 {{ gateway }} >/dev/null changed_when: "false" - name: DNS resolves through the fleet resolver shell: getent hosts nh3-docker.nh3.internal >/dev/null changed_when: "false" - name: Wi-Fi is STILL UP — the escape hatch must survive this run # If this fails, the box may be reachable only on an address that was just # proven or just broken. Loudly not optional. shell: ip link show wlP9s9 | grep -q "state UP" changed_when: "false" # ───────────────────────────────────────────────────────────────────────────── # AFTERWARDS, and only once the new address has been confirmed from OUTSIDE: # # 1. ssh infra-ops@10.100.50.60 # prove it from another host first # 2. update servers/pfi-gx10/ssh-target # 3. add to dns/internal.yaml, then scripts/dns-sync.py --dry-run && ...sync # 4. tear down Wi-Fi — a server with two default routes picks one at random # and you find out later, in a way that looks like packet loss: # sudo nmcli radio wifi off # or delete the wlP9s9 netplan stanza # Do that as its own change, not bundled in here. # ─────────────────────────────────────────────────────────────────────────────