CHG-0044: Fixed Addresses for Tenants’ Devices #
| Date | 2026-10-06 |
| Change type | Deployment · Configuration · Migration |
| Classification | Structural |
| Status | Complete |
| Window | 2026-10-06 to 2026-10-07. Before CHG-0034, which it doesn’t block |
| Site | mobile |
| Systems | dv02cor002p01 (the core router’s DHCP server), dv02prv001v01 (the Deevnet API), the tenant DNS server; the provider; the mabell tenant and its gateway |
| Automation | deevnet-provisioning-api, terraform-provider-deevnet, deevnet.net dhcp.yml and dns.yml, deevnet.mgmt site.yml --limit deevnet_api, the mobile inventory |
| Risk | Medium. Most likely to go wrong: moving the IoT network’s dynamic pool gives every device on it a new address at its next renewal |
| Related changes | CHG-0014 (the registry this builds on), CHG-0038 (narrows the router credential this widens the use of), CHG-0034 |
| Related incidents | None |
| Related runbooks | Devices and MQTT, Connect a Device, Network Reference |
Summary #
A tenant’s device leases whatever the IoT network’s pool gives it, and the one device with a fixed address, the Ma Bell gateway, has it as a substrate host in inventory. ADR-0035 makes a fixed address a tenant service: a tenant asks the Deevnet API for one for a device it registered with a MAC, the API reserves it on the router’s DHCP server and publishes the device’s name in the tenant’s zone.
This change builds that, rearranges the IoT network to make room for it, and moves the Ma Bell gateway out of inventory to its tenant as the first device to use it.
Goal #
- The IoT network is in three parts: substrate hosts below
.25, tenants’ device addresses from.25to.200, the dynamic pool from.201to.254. - A tenant’s Terraform reserves an address for a registered device and gets back the address and the device’s name.
- The Ma Bell gateway is not in inventory. It holds an address its tenant reserved, and resolves as a name in its tenant’s zone.
- The router’s DHCP role and the API each leave the other’s reservations alone.
- A second apply of the tenant plans nothing, and a re-apply after the API’s record is deleted asks for the same address.
- ADR-0035 is Accepted.
Scope #
In scope: the API route and the provider resource; the DHCP role’s pool update and ownership guard; the inventory’s IoT ranges; the Ma Bell gateway’s move. Out of scope: any firewall rule (none changes); reverse DNS for a device’s address, which is ADR-0035’s open question 1; a tenant range on the vendor-device network; narrowing the API’s router credential, which is CHG-0038.
Risk and impact #
| Risk | Where | Guard |
|---|---|---|
| Devices on the old pool change address | the IoT network | Expected, once, at each device’s next renewal. Devices dial the broker by name and reconnect. Done in a window; the Pi lab’s reservations don’t move |
| The pool update resets other settings of the subnet | the router | The role sends the pool only, reads the subnet back and fails the run if its routers option or subnet changed. Proven against a stand-in that resets them |
| The role takes over a tenant’s reservation | the router | The role stops when an inventory MAC is reserved under another description. Proven by removing the guard and watching it happen |
| The API reserves a substrate host’s MAC or address | the router | The API refuses a MAC or address any other reservation on the subnet holds; the router’s own model rejects a duplicate as well |
| The gateway is unreachable between leaving inventory and its tenant’s apply | the gateway | It leases from the pool in between, and its broker connection doesn’t depend on its address. Steps 4 and 5 run back to back |
Something still dials bellgw or mabell in the substrate zone | operators | Checked before step 4; the new name is handed over with the change |
| An address is handed out while an old lease still holds it | the IoT network | Allocation starts at .25 and old leases were at .100 and above; they expire long before allocation gets there |
Prerequisites #
- The pull requests merged: API, provider,
deevnet.net,deevnet.mgmt, and the inventory’s range change. The inventory’s removal of the gateway merged with them, ahead of step 4; nothing on the router changes until the pruning runs in that step - API v0.10.0 and provider v0.6.0 tagged and staged
- Vault decrypted, collections built
- The
mabelltenant’s agreement, and its repository’s pull request for the address, merged - The items under To discover answered
Procedure #
Step 1: Read the router #
Read-only. Confirms what the next steps assume about this build.
Run:
cd deevnet-provisioning-api
DEEVNET_TEST_OPNSENSE_URL=https://<router>/api DEEVNET_TEST_OPNSENSE_SUBNET=10.20.30.0/24 \
go test ./internal/backend/opnsense/ -run RealRouter -v # key and secret from the vault, in the environment
cd ../ansible-collection-deevnet.net
ansible-playbook playbooks/dhcp.yml --check
Verify:
- The test lists every reservation with a MAC, an address and a description, and resolves the IoT subnet.
- The check run reports one pool that differs, IoT’s, and no other change it didn’t report before.
Result, 2026-10-06: passed.
- The API’s client read all 15 reservations, each with a MAC, an address and a description, and resolved the IoT subnet. Every row is inventory’s.
- The check run reported one change: IoT’s pool is
10.20.30.100 - 10.20.30.200and inventory says10.20.30.201 - 10.20.30.254. It wrote nothing. - The role’s new ownership check passed: no inventory MAC is reserved under another description.
- The run named the gateway’s reservation as no longer declared and left it in place, as expected until step 4.
Step 2: Move the pool #
Disruptive to devices on the IoT pool: each takes a new address at its next renewal.
Run:
ansible-playbook playbooks/dhcp.yml
Verify:
- The run passes its own read-back of the IoT subnet.
- The router shows the IoT pool as
.201to.254, and the Pi lab’s reservations unchanged. - A second run reports no pool change.
- A device that rejoins gets an address in the new pool.
Result, 2026-10-06: passed, except item 4, which was not observed.
- The pool went from
10.20.30.100 - 10.20.30.200to10.20.30.201 - 10.20.30.254. - The role’s read-back passed: the subnet and its routers option were unchanged. This build’s subnet update leaves alone what it isn’t sent.
- The four Pi lab reservations and the gateway’s were unchanged, and a second run reported no pool change.
- No device was on the pool to rejoin, so item 4 waits for the first one that does.
Undo: set the old range in inventory and run the playbook again.
Step 3: Deploy the API and stage the provider #
Not disruptive: nothing a running device depends on.
Run:
cd ../ansible-collection-deevnet.mgmt
ansible-playbook playbooks/site.yml --limit deevnet_api
Verify:
/versionanswers v0.10.0.- As the operator, against a throwaway device in
tdemo:- a device with a MAC is given
10.20.30.25, and the router shows the reservation describedDeevnet API - tdemo/<device>; - its name resolves through the router;
- an address outside the range is refused with
400, and a Pi’s MAC with409; - the same MAC from a second tenant is refused with
409, naming nobody.
- a device with a MAC is given
- The address is removed again, and the router no longer shows it.
ansible-playbook playbooks/dhcp.ymlindeevnet.net, run while the test reservation exists, leaves it alone.
Result, 2026-10-06: passed.
/versionanswers v0.10.0, and the role confirmed the running version is the pinned one.- A throwaway device in
tdemowas given10.20.30.25; the router showed it asDeevnet API - tdemo/chg44, hostnametdemo-chg44. Asking again returned the same address. chg44.tdemo.mobile.deevnet.netresolved through the router to10.20.30.25, with no PTR.- Refused as designed: an address in the dynamic pool (
400); a Pi lab host’s MAC (409, and no record left behind); the same MAC from thecdeevertenant (409, naming nobody). - A second name for the address was accepted from
tdemoand refused fromcdeever. The address could not be given back while that name pointed at it. - A real run of the DHCP role, with the test reservation present, left it untouched.
- Everything was removed afterwards: the router has no reservation at
.25, the tenant DNS server answers NXDOMAIN for both names, andtdemohas no devices. The router’s resolver kept the answer cached for a few minutes more. - Provider v0.6.0 is staged on the Builder and installed in the control host’s local mirror. The tenant downloads site has not been refreshed with it.
Undo: redeploy v0.9.1. A reservation left behind is removed in the router’s UI by its description.
Step 4: The gateway leaves inventory #
The gateway’s reservation and its names in the substrate’s zone are removed.
Run: merge the inventory pull request that removes dv02bgw001e01, then:
cd ../ansible-collection-deevnet.net
ansible-playbook playbooks/dhcp.yml -e dhcp_delete_unmanaged=true
ansible-playbook playbooks/dns.yml -e dns_delete_unmanaged=true
Verify:
- The router has no reservation for the gateway’s MAC.
dv02bgw001e01,bellgwandmabellno longer resolve inmobile.deevnet.net.
Result, 2026-10-06: passed. A check run first showed the prune would remove exactly one
reservation, one host override and two aliases, all the gateway’s. After the real run the router
holds no reservation for the gateway’s MAC, none of the three names resolves, and the mabell
tenant’s own zone still does.
Undo: revert the inventory commit and run both playbooks.
Step 5: The tenant reserves the gateway’s address #
Run by the tenant, from its own repository: the provider pinned to ~> 0.6, the gateway’s MAC on its
device, and one address resource.
Verify:
- The apply reports an address in
.25to.200and the namema-bell-gw-01.mabell.mobile.deevnet.net. - The router shows the reservation, described
Deevnet API - mabell/ma-bell-gw-01. - The gateway, after its next renewal or a restart, holds that address and answers at that name from an operator network.
- A second apply plans nothing.
Result, 2026-10-07: passed, except item 3, which was not observed.
- The apply added the address and recorded the gateway’s MAC:
10.20.30.25, namedma-bell-gw-01.mabell.mobile.deevnet.net. - The router shows the reservation for the gateway’s MAC, described
Deevnet API - mabell/ma-bell-gw-01, and the name resolves through the router. - A second plan showed no changes.
- The gateway was not on the network, so nothing answered at the address. Item 3 waits for it to join.
Undo: destroy the address resource; the gateway returns to the pool.
Step 6: Restore drill #
Run: as the operator, delete the gateway’s address through the API, then have the tenant plan and apply.
Verify:
- The plan shows the address resource changing, with the same address.
- After the apply the gateway’s address is the one it had.
Result, 2026-10-07: passed. With the address deleted through the API, the router’s reservation
was gone and the tenant’s plan showed the address resource updating in place with its address
unchanged. The apply put 10.20.30.25 back on the router, and the next plan showed no changes.
Verification #
Steps 3, 5 and 6 pass; the firewall plan shows no drift; the documentation site builds without warnings.
Result, 2026-10-07: all three hold. The firewall plan found the router’s 69 rules matching inventory.
Two things were not observed, because no device was on the IoT network during the change, and are recorded as untested, not as passed:
- a device on the old pool taking an address in the new one at renewal (step 2);
- the gateway leasing its reserved address (step 5).
Undo #
Each step’s own undo, in reverse. Nothing here is destructive of anything a tenant holds: the worst case is every device back on the pool.
To discover #
Whether this OPNsense build’s subnet update leaves alone the settings it isn’t sent.Found 2026-10-06: it does. The role’s read-back in step 2 passed.The lease lifetime on the IoT subnet.Found 2026-10-06: the IoT subnet sets none, so the server’s general setting applies: 4000 seconds. A device on the old pool keeps its address for a little over an hour at most after step 2.- That a device holding a pool lease moves to its reserved address at renewal, without a restart. Not seen: no device was on the network.
- Whether anything still dials
bellgwormabellin the substrate’s zone. Nothing in the repositories does; the names are gone, and the gateway isma-bell-gw-01.mabell.mobile.deevnet.net. - Whether the router’s reservation search returns every row in one answer once tenants’ rows are added to inventory’s. With 16 rows it does; not yet seen with many.