Skip to content

fix(dhcp): resync hostname on static assign/remove-address (#3383)#3515

Open
hatamzad-nv wants to merge 4 commits into
NVIDIA:mainfrom
hatamzad-nv:fix/3383-resync-hostname-static-address
Open

fix(dhcp): resync hostname on static assign/remove-address (#3383)#3515
hatamzad-nv wants to merge 4 commits into
NVIDIA:mainfrom
hatamzad-nv:fix/3383-resync-hostname-static-address

Conversation

@hatamzad-nv

@hatamzad-nv hatamzad-nv commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Problem

carbide-admin-cli machine-interfaces assign-address and remove-address
mutate an interface's address but skip the hostname-resync step that every
other allocation path runs. With the default IP-derived naming strategy, this
leaves the interface's hostname pinned to an IP it no longer holds — e.g. the
interface is moved to 7.243.174.25 but stays named 7-243-174-20.

That single stale row then wedges all new DHCP allocation on the segment:

  1. The fast-path allocator deterministically reuses the lowest free IP (the one
    just freed).
  2. It re-derives the same IP-based hostname and the insert collides with the
    fqdn_must_be_unique (domain_id, hostname) constraint.
  3. The collision is treated as a transient race and retried — but candidate
    selection is deterministic, so all 128 retries pick the same IP and fail.
  4. Every DHCP DISCOVER on the segment queues behind that IP and is dropped.

Observed on ytl-dev1: after a repave, no host BMC or DPU BMC could get an
address and site-explorer could not discover attached DPUs, until the stale
interface was deleted.

Fixes #3383.

Fix

Both handlers now run the same resync the other paths already use, before
committing:

  • assign_static_addresssync_hostname_after_address_assignment,
    mirroring the static-preallocation path.
  • remove_static_addresssync_hostname_after_address_change for the
    interface that actually owned the removed address.

remove_static_address deletes by IP, so the owning interface can differ from
the caller-supplied interface_id. delete_by_address now returns the affected
interface_id (via RETURNING interface_id) and the handler resyncs that
owner, so the real owner can't be left with a stale hostname. The DHCP
lease-expiry path is unchanged (it already resyncs the interface it looked up).

Testing

  • test_assign_and_remove_resync_hostname — hostname follows the new static IP
    on assign, and resets to the dormant noip-… placeholder on remove.
  • test_remove_resyncs_the_address_owner_not_the_request_id — removing an
    address while passing a different interface_id still resyncs the true
    owner.
  • Full static_address_management suite (18 tests) — passes, no regressions.

Run on Linux (the crate pulls in tss-esapi-sys, which does not build on macOS):

DATABASE_URL="postgresql://…@…:5432/postgres"
cargo test -p carbide-api-core --test integration static_address_management

Notes / follow-ups (out of scope)

  • The allocator's deterministic retry-on-fqdn_must_be_unique means one
    inconsistent row can still block allocation of unrelated IPs. This PR removes
    the cause; hardening the allocator against any stale row is a separate change.
  • The misleading num_free_ips: 0 diagnostic on a mostly-empty prefix is filed
    separately.

assign-address and remove-address mutated the interface address but
skipped the hostname resync every other allocation path runs, leaving a
stale IP-derived hostname that collided with fqdn_must_be_unique and
wedged DHCP allocation on the whole segment.
@hatamzad-nv hatamzad-nv requested a review from a team as a code owner July 14, 2026 22:18
@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Summary by CodeRabbit

  • Bug Fixes
    • Hostnames now automatically resynchronize after static IP assignment and removal, including resetting to the dormant placeholder when no addresses remain.
    • Removing a static IP now updates the hostname of the correct owning interface, even when the request references a different interface.
    • DHCP lease expiration now more accurately determines when an allocation was deleted and triggers hostname resynchronization for all affected interfaces.
  • Tests
    • Added regression tests covering hostname resynchronization for static address assignment/removal and correct owner targeting.
    • Updated DHCP-related test assertion for the revised deletion result semantics.

Walkthrough

Static address assignment and removal now resynchronize IP-derived interface hostnames. Address deletion returns all owning interfaces, DHCP expiry adapts to the new result, and regression tests cover normal and mismatched-owner removal paths.

Changes

Address hostname synchronization

Layer / File(s) Summary
Return deleted address owners
crates/api-db/src/machine_interface_address.rs, crates/api-db/src/machine_interface/tests.rs, crates/api-core/src/dhcp/expire.rs
delete_by_address now returns all owning interface IDs; related assertions and DHCP expiry interpret the collection result.
Synchronize hostnames during address changes
crates/api-core/src/handlers/machine_interface_address.rs
Static assignment and successful removal invoke hostname synchronization, with removal targeting each actual deleted address owner.
Validate hostname re-encoding
crates/api-core/tests/integration/static_address_management.rs
Integration tests verify hostname updates during assignment and reset after removal, including removal requested with an unrelated interface ID.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant AddressHandler
  participant AddressDatabase
  participant InterfaceDatabase
  AddressHandler->>AddressDatabase: Assign or remove address
  AddressDatabase-->>AddressHandler: Deleted owner interface IDs
  AddressHandler->>InterfaceDatabase: Synchronize IP-derived hostname
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main fix: hostname resync for static assign/remove-address tied to #3383.
Description check ✅ Passed The description matches the implemented fix and explains the hostname-resync and owner-tracking changes.
Linked Issues check ✅ Passed The changes satisfy #3383 by resyncing hostnames on assign/remove and updating delete_by_address to target the real owner.
Out of Scope Changes check ✅ Passed The DHCP expiry and test updates are supporting changes for the same hostname-resync fix, not unrelated scope.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
crates/api-core/tests/integration/static_address_management.rs (1)

455-529: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Solid regression coverage for the hostname resync fix.

The three-stage flow (DHCP → static assign → static remove) correctly exercises hostname re-derivation and the dormant-placeholder reset, matching the sync_hostname_after_address_assignment/sync_hostname_after_address_change contracts.

One optional enhancement: since domain_id clearing is also part of the same sync contract (cleared to None once addresses are empty), consider asserting iface_after_remove.domain_id.is_none() alongside the hostname check to more fully pin down the fix's behavior.

🧪 Optional: assert domain clearing too
     assert!(
         iface_after_remove.hostname.to_lowercase().starts_with("noip"),
         "hostname should reset to dormant format after removal, got: {}",
         iface_after_remove.hostname,
     );
+    assert!(
+        iface_after_remove.domain_id.is_none(),
+        "domain should be cleared once the interface has no addresses"
+    );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/api-core/tests/integration/static_address_management.rs` around lines
455 - 529, Extend test_assign_and_remove_resync_hostname to also assert that
iface_after_remove.domain_id is None after removing the final static address,
covering the domain-clearing behavior of the hostname/address synchronization
contract while preserving the existing hostname assertion.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/api-core/src/handlers/machine_interface_address.rs`:
- Around line 194-201: The deleted address may belong to a different interface
than the supplied interface_id, causing hostname resynchronization for the wrong
owner. Update remove_static_address to either constrain deletion by interface_id
or return the deleted row’s owning interface_id, then pass that confirmed owner
to sync_hostname_after_address_change only when a row was deleted.

---

Nitpick comments:
In `@crates/api-core/tests/integration/static_address_management.rs`:
- Around line 455-529: Extend test_assign_and_remove_resync_hostname to also
assert that iface_after_remove.domain_id is None after removing the final static
address, covering the domain-clearing behavior of the hostname/address
synchronization contract while preserving the existing hostname assertion.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: ac5f066d-24c9-4d7a-ae33-643662831f38

📥 Commits

Reviewing files that changed from the base of the PR and between 2c2ecff and a3ea39f.

📒 Files selected for processing (2)
  • crates/api-core/src/handlers/machine_interface_address.rs
  • crates/api-core/tests/integration/static_address_management.rs

Comment thread crates/api-core/src/handlers/machine_interface_address.rs
@github-actions

github-actions Bot commented Jul 14, 2026

Copy link
Copy Markdown

🔍 Container Scan Summary

Service Total Critical High Medium Low Other
boot-artifacts-aarch64 3 0 0 3 0 0
boot-artifacts-x86_64 3 0 0 3 0 0
forge-admin-cli-x86_64 259 13 30 78 7 131
machine-validation-runner 807 40 234 288 36 209
machine_validation 807 40 234 288 36 209
machine_validation-aarch64 807 40 234 288 36 209
nvmetal-carbide 807 40 234 288 36 209
TOTAL 3493 173 966 1236 151 967

Per-CVE detail lives in the per-service grype-* artifacts (JSON + SARIF). Severity counts only — no CVE IDs published here.

Address review feedback: remove-address deletes by IP, so resync the
interface returned by delete_by_address instead of the request's
interface_id, which may not own the row. Also wrap an assert to satisfy
nightly rustfmt.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
crates/api-core/tests/integration/static_address_management.rs (1)

576-596: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Strengthen the regression by asserting other_id's hostname is untouched.

The test proves the owner gets resynced, but doesn't assert that other_id (the mismatched interface passed in the request) is left alone. Adding that check would positively confirm the fix targets the real owner rather than merely happening to not throw.

🧪 Suggested addition
     // The owner (which actually lost the address) must be resynced to dormant.
     let mut txn = env.db_txn().await;
     let owner_after = db::machine_interface::find_one(&mut *txn, owner_id).await?;
     assert!(
         owner_after.hostname.to_lowercase().starts_with("noip"),
         "the address owner should be resynced to dormant, got: {}",
         owner_after.hostname,
     );
+    // The mismatched request interface must be left untouched.
+    let other_after = db::machine_interface::find_one(&mut *txn, other_id).await?;
+    assert_eq!(
+        other_after.hostname, other.hostname,
+        "the interface named in the request but not owning the address must not be resynced"
+    );
     txn.commit().await?;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/api-core/tests/integration/static_address_management.rs` around lines
576 - 596, Extend the regression test around the existing owner_after assertion
to fetch the interface identified by other_id and assert its hostname remains
unchanged from before removal. Keep the existing owner resync assertion, and use
the test’s established pre-removal hostname or expected value for the
untouched-interface comparison.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/api-db/src/machine_interface_address.rs`:
- Around line 209-223: Update delete_by_address to preserve every interface_id
returned by the DELETE ... RETURNING query instead of taking only the first
owner; change its return type and callers as needed to return and resync all
affected interfaces, or enforce a database-level uniqueness guarantee if the
operation must remain singular.

---

Nitpick comments:
In `@crates/api-core/tests/integration/static_address_management.rs`:
- Around line 576-596: Extend the regression test around the existing
owner_after assertion to fetch the interface identified by other_id and assert
its hostname remains unchanged from before removal. Keep the existing owner
resync assertion, and use the test’s established pre-removal hostname or
expected value for the untouched-interface comparison.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 43100277-158b-49d0-a77a-f82649d19e0d

📥 Commits

Reviewing files that changed from the base of the PR and between a3ea39f and cb1842e.

📒 Files selected for processing (5)
  • crates/api-core/src/dhcp/expire.rs
  • crates/api-core/src/handlers/machine_interface_address.rs
  • crates/api-core/tests/integration/static_address_management.rs
  • crates/api-db/src/machine_interface/tests.rs
  • crates/api-db/src/machine_interface_address.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/api-core/src/handlers/machine_interface_address.rs

Comment thread crates/api-db/src/machine_interface_address.rs
delete_by_address can match multiple rows since (address, allocation_type)
is not unique on its own; return every affected interface and resync each
instead of dropping all but the first.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/api-core/src/dhcp/expire.rs`:
- Around line 80-86: Update the deletion flow around
db::machine_interface_address::delete_by_address to retain its returned owner
IDs instead of converting the Vec to a boolean. Resynchronize every returned
owner, while preserving the existing synchronization behavior for the interface
found by find_by_ip and the branch’s result semantics.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: cbd2d2cb-6762-42f3-ab1c-bb8be013e714

📥 Commits

Reviewing files that changed from the base of the PR and between cb1842e and c904fed.

📒 Files selected for processing (4)
  • crates/api-core/src/dhcp/expire.rs
  • crates/api-core/src/handlers/machine_interface_address.rs
  • crates/api-db/src/machine_interface/tests.rs
  • crates/api-db/src/machine_interface_address.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • crates/api-db/src/machine_interface/tests.rs
  • crates/api-core/src/handlers/machine_interface_address.rs

Comment thread crates/api-core/src/dhcp/expire.rs Outdated

@chet chet left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, this is great. Thank you! Fun how it ultimately all wires through HostNamingStrategy too so we can do stuff like this. Appreciate the fix!

@hatamzad-nv

Copy link
Copy Markdown
Contributor Author

@chet Thanks! Yeah, the HostNamingStrategy wiring made this a lot cleaner than I expected, once I traced how the other allocation paths resync through it, the fix was really just making assign/remove do the same thing. Appreciate the review!

…NVIDIA#3383)

Address review feedback: the DHCP lease-expiry path now resyncs every
interface returned by delete_by_address instead of only the find_by_ip
owner. Also apply nightly rustfmt to the query literal and test setup.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/api-core/src/dhcp/expire.rs`:
- Around line 73-86: The DHCP expiration path around delete_by_address_and_mac
must derive the resync target from the row actually deleted, not the IP-only
interface lookup. Update delete_by_address_and_mac to return the affected
interface_id using RETURNING, use that returned ID when building resync_targets
in the mac_address branch, and add a regression test covering a mismatched owner
between lookup and deletion.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 110f2ad4-c296-4af1-963d-5af28b8f5b6b

📥 Commits

Reviewing files that changed from the base of the PR and between c904fed and 987bec4.

📒 Files selected for processing (3)
  • crates/api-core/src/dhcp/expire.rs
  • crates/api-core/tests/integration/static_address_management.rs
  • crates/api-db/src/machine_interface_address.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • crates/api-core/tests/integration/static_address_management.rs
  • crates/api-db/src/machine_interface_address.rs

Comment on lines +73 to +86
let (deleted, resync_targets) = match mac_address {
Some(mac) => {
db::machine_interface_address::delete_by_address_and_mac(
let deleted = db::machine_interface_address::delete_by_address_and_mac(
&mut txn,
ip_address,
mac,
model::allocation_type::AllocationType::Dhcp,
)
.await?
.await?;
let targets = match (deleted, &interface) {
(true, Some(iface)) => vec![iface.id],
_ => Vec::new(),
};
(deleted, targets)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Derive the MAC-scoped resync target from the deleted row.

interface was looked up by IP alone, while deletion is scoped by (IP, MAC, allocation type). If multiple rows share the address, or ownership changes between the lookup and delete, this can resynchronize the wrong interface—or none—leaving the actual owner’s hostname stale. Make delete_by_address_and_mac return the affected interface_id via RETURNING, then use that authoritative ID here and add a mismatched-owner regression test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/api-core/src/dhcp/expire.rs` around lines 73 - 86, The DHCP expiration
path around delete_by_address_and_mac must derive the resync target from the row
actually deleted, not the IP-only interface lookup. Update
delete_by_address_and_mac to return the affected interface_id using RETURNING,
use that returned ID when building resync_targets in the mac_address branch, and
add a regression test covering a mismatched owner between lookup and deletion.

Source: Path instructions

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: carbide-admin-cli machine-interfaces assign-address does not recompute hostname

2 participants