Skip to content

fix(admin-cli): delete machine interfaces by MAC address (#3046) - #4466

Open
hatamzad-nv wants to merge 1 commit into
NVIDIA:mainfrom
hatamzad-nv:fix/3046-delete-interface-by-mac
Open

fix(admin-cli): delete machine interfaces by MAC address (#3046)#4466
hatamzad-nv wants to merge 1 commit into
NVIDIA:mainfrom
hatamzad-nv:fix/3046-delete-interface-by-mac

Conversation

@hatamzad-nv

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

Copy link
Copy Markdown

Extends DeleteInterface to accept a MAC address, so an operator can clear a leftover interface record when they have the BMC MAC but not the interface ID, the case that blocks re-ingestion of a replacement host.

A MAC is only unique per network segment, so it can match several interfaces. All matches are validated before anything is deleted, and the request is refused if any of them still belong to a live machine, DPU, switch, or power shelf.

Also clears machine_boot_override in the shared machine_interface::delete. That FK has no ON DELETE CASCADE, so a leftover override used to fail the delete with a foreign-key violation, fixing it in the shared path also fixes the same latent bug for machine, switch, and power-shelf force-delete.

Exploration reports are intentionally left to Site Explorer, which prunes them on its next run.

Supersedes #4013, reimplemented per @Matthias247's review to extend delete_interface rather than add a parallel deletion flow, which also addresses @kensimon's point about the regression cost of a second order-sensitive DB path.

Part of #3046: this covers the machine-interface blocker. Expected-machines records remain out of scope per @ajf's comment on the issue.

Note: integration tests weren't run locally, relying on CI.

Extend DeleteInterface to accept a MAC address, so an operator can clear a
leftover interface record when they have the BMC MAC but not the interface id --
the case that blocks re-ingestion of a replacement host.

A MAC is unique only per network segment, so it can match several interfaces.
All matches are validated before any is deleted, and the request is refused if
any of them still belongs to a live machine, DPU, switch or power shelf.

Also clear machine_boot_override in the shared machine_interface::delete. That
foreign key has no ON DELETE CASCADE, so a leftover override previously failed
the delete with a foreign-key violation; fixing it in the shared path also fixes
the same latent bug for machine, switch and power-shelf force-delete.

Exploration reports are intentionally left to site explorer, which prunes them
on its next run.

Part of NVIDIA#3046

Signed-off-by: Amir Hatamzad <ahatamzad@nvidia.com>
@hatamzad-nv
hatamzad-nv requested a review from a team as a code owner July 31, 2026 22:25
@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Summary by CodeRabbit

  • New Features

    • Delete machine interfaces using either an interface ID or BMC MAC address.
    • MAC-based deletion can remove all matching interfaces across network segments.
    • Added validation to require exactly one selector and reject malformed or unknown values.
  • Bug Fixes

    • Cleans up boot overrides before interface deletion, preventing related deletion failures.
    • Deletions are refused safely when any matching interface belongs to a protected managed component.

Walkthrough

The deletion flow now accepts an interface ID or MAC address. MAC selection can resolve multiple interfaces. The API validates all matches before deletion, clears boot overrides, and returns not-found errors for unknown selectors.

Changes

Interface deletion selection and execution

Layer / File(s) Summary
Selector contracts and CLI wiring
crates/rpc/proto/forge.proto, rest-api/proto/core/src/v1/nico_nico.proto, crates/admin-cli/src/machine_interfaces/delete/*
The RPC contracts and CLI accept exactly one interface ID or MAC address. The CLI validates selectors and builds InterfaceDeleteQuery requests.
Interface resolution and validation
crates/api-core/src/handlers/machine_interface.rs, crates/api-db/src/machine_interface.rs
The handler resolves ID or MAC matches, sorts MAC results, validates device associations, and deletes all approved matches. Database deletion clears related boot overrides first.
Deletion behavior coverage
crates/api-core/src/tests/machine_interfaces.rs
Tests cover MAC deletion, not-found responses, multi-segment matches, boot-override cleanup, and all-or-nothing rejection for switch-owned interfaces.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant AdminCLI
  participant APIClient
  participant APIHandler
  participant Database
  AdminCLI->>APIClient: Submit InterfaceDeleteQuery with ID or MAC
  APIClient->>APIHandler: Forward deletion request
  APIHandler->>Database: Resolve matching interfaces
  APIHandler->>Database: Validate associations
  APIHandler->>Database: Clear boot overrides and delete interfaces
  Database-->>APIHandler: Return deletion result
  APIHandler-->>APIClient: Return success or validation error
  APIClient-->>AdminCLI: Display result
Loading

Possibly related PRs

Suggested labels: rest-api

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the primary change: enabling machine-interface deletion by MAC address.
Description check ✅ Passed The description accurately explains MAC-based deletion, validation behavior, boot-override cleanup, scope, and testing status.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@hatamzad-nv

Copy link
Copy Markdown
Author

@Matthias247 @kensimon this is the smaller version we discussed—it extends delete_interface with a MAC selector instead of adding a separate flow, and leaves explored_endpoints to Site Explorer.

@github-actions

Copy link
Copy Markdown

🔐 TruffleHog Secret Scan

No secrets or credentials found!

Your code has been scanned for 700+ types of secrets and credentials. All clear! 🎉

🔗 View scan details

🕐 Last updated: 2026-07-31 22:29:03 UTC | Commit: fc9285a

@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.

🧹 Nitpick comments (5)
crates/admin-cli/src/machine_interfaces/tests.rs (1)

96-113: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a scenario for no selector.

The table covers ID only, MAC only, both selectors, and a malformed MAC. It does not cover the case with no selector. That case is the sole reason for .required(true) on the ArgGroup, so a regression there would pass unnoticed. The admin-cli crate is excluded from the CI test job, which raises the value of explicit coverage.

♻️ Proposed additional scenario
         "a malformed MAC is rejected at parse time" {
             &["machine-interface", "delete", "--mac-address", "not-a-mac"][..] => Fails,
         }
+
+        "no selector is rejected" {
+            &["machine-interface", "delete"][..] => Fails,
+        }
🤖 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/admin-cli/src/machine_interfaces/tests.rs` around lines 96 - 113, Add
a table-driven scenario in the machine-interface delete argument tests for
invoking the command with no selector, and assert that parsing fails. Place it
alongside the existing selector cases to cover the required ArgGroup behavior.

Sources: Coding guidelines, Learnings

crates/api-core/src/tests/machine_interfaces.rs (2)

1631-1654: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the reason for the refusal, not only its category.

The test asserts Code::InvalidArgument. The handler returns that same code for six distinct conditions, including both selectors set and every association check. An unrelated validation change could therefore keep this test green while the switch-ownership check stops working. Assert a fragment of the switch message to pin the actual cause.

♻️ Proposed assertion
         .expect_err("a switch-owned match must refuse the whole request");
     assert_eq!(status.code(), Code::InvalidArgument);
+    assert!(
+        status.message().contains("belongs to switch"),
+        "the refusal must name switch ownership as the cause, got: {}",
+        status.message()
+    );
🤖 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/tests/machine_interfaces.rs` around lines 1631 - 1654,
Strengthen the refusal assertion in the delete_interface test by also checking
that the returned status message contains the switch-ownership-specific text,
not just Code::InvalidArgument. Update the assertion around the status from
delete_interface and preserve the existing all-or-nothing database checks.

1660-1681: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add tests for both invalid selector combinations.

delete_interface must reject requests with both selectors and requests with neither selector. Assert Code::InvalidArgument for both cases.

🤖 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/tests/machine_interfaces.rs` around lines 1660 - 1681,
Extend test_delete_interface_by_unknown_id_is_not_found with coverage for
delete_interface requests containing both id and mac_address, and requests
containing neither selector. Assert that each call fails with
tonic::Code::InvalidArgument while preserving the existing unknown-id NotFound
assertion.

Source: Path instructions

crates/api-db/src/machine_interface.rs (1)

3544-3548: 🗄️ Data Integrity & Integration | 🔵 Trivial

The placement and the reasoning are correct.

clear runs before the row DELETE, which the FK requires. The operation is idempotent, so callers with no override are unaffected. No caller can have depended on the override outliving its interface, because the FK made that delete fail.

One forward-looking note, outside the scope of this PR: an ON DELETE CASCADE on the machine_boot_override FK would enforce the same invariant at the schema level. That would protect any future code path that deletes a machine_interfaces row without going through this function. Consider it for a separate migration.

🤖 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-db/src/machine_interface.rs` around lines 3544 - 3548, The current
placement and behavior of machine_boot_override::clear before deleting the
interface are correct; no code changes are required for this review. Leave the
existing cleanup in place and defer any ON DELETE CASCADE schema migration to a
separate change.
crates/api-core/src/handlers/machine_interface.rs (1)

137-190: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Consider reporting every blocking association at once.

The loop returns on the first interface that fails validation. When a MAC matches several owned interfaces, the operator clears one blocker, retries, and then discovers the next one. The all-or-nothing contract already refuses the whole request, so the handler holds enough information to report every blocker in a single response. Collect the reasons and return one InvalidArgument that lists them. This is an ergonomics improvement, not a correctness defect; defer it if the multi-match case is rare in practice.

♻️ Sketch of an aggregated refusal
let mut blockers = Vec::new();
for interface in &interfaces {
    if let Some(machine_id) = interface.machine_id {
        blockers.push(if interface.interface_type == InterfaceType::Bmc {
            format!("{}: BMC interface attached to machine {machine_id}", interface.id)
        } else {
            format!("{}: machine {machine_id} is attached", interface.id)
        });
        continue;
    }
    if let Some(dpu_machine_id) = interface.attached_dpu_machine_id {
        blockers.push(format!("{}: attached to DPU machine {dpu_machine_id}", interface.id));
        continue;
    }
    // switch_id, power_shelf_id, BMC-IP checks follow the same shape.
}

if !blockers.is_empty() {
    return Err(CarbideError::InvalidArgument(format!(
        "these interfaces are still in use, delete their owners first: {}",
        blockers.join("; ")
    ))
    .into());
}
🤖 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/handlers/machine_interface.rs` around lines 137 - 190,
Update the interface validation loop to collect every blocking association in a
blockers collection instead of returning immediately from each check. Apply this
to machine, DPU, switch, power-shelf, and BMC-IP ownership checks, then return
one InvalidArgument after the loop containing all interface-specific reasons
joined together; preserve the existing all-or-nothing refusal when any blocker
exists.
🤖 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.

Nitpick comments:
In `@crates/admin-cli/src/machine_interfaces/tests.rs`:
- Around line 96-113: Add a table-driven scenario in the machine-interface
delete argument tests for invoking the command with no selector, and assert that
parsing fails. Place it alongside the existing selector cases to cover the
required ArgGroup behavior.

In `@crates/api-core/src/handlers/machine_interface.rs`:
- Around line 137-190: Update the interface validation loop to collect every
blocking association in a blockers collection instead of returning immediately
from each check. Apply this to machine, DPU, switch, power-shelf, and BMC-IP
ownership checks, then return one InvalidArgument after the loop containing all
interface-specific reasons joined together; preserve the existing all-or-nothing
refusal when any blocker exists.

In `@crates/api-core/src/tests/machine_interfaces.rs`:
- Around line 1631-1654: Strengthen the refusal assertion in the
delete_interface test by also checking that the returned status message contains
the switch-ownership-specific text, not just Code::InvalidArgument. Update the
assertion around the status from delete_interface and preserve the existing
all-or-nothing database checks.
- Around line 1660-1681: Extend test_delete_interface_by_unknown_id_is_not_found
with coverage for delete_interface requests containing both id and mac_address,
and requests containing neither selector. Assert that each call fails with
tonic::Code::InvalidArgument while preserving the existing unknown-id NotFound
assertion.

In `@crates/api-db/src/machine_interface.rs`:
- Around line 3544-3548: The current placement and behavior of
machine_boot_override::clear before deleting the interface are correct; no code
changes are required for this review. Leave the existing cleanup in place and
defer any ON DELETE CASCADE schema migration to a separate change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 493ca68f-269a-4841-aeee-9a5e02675734

📥 Commits

Reviewing files that changed from the base of the PR and between 00e989f and fc9285a.

⛔ Files ignored due to path filters (1)
  • rest-api/proto/core/gen/v1/nico_nico.pb.go is excluded by !**/*.pb.go, !**/gen/**, !rest-api/**/*.pb.go
📒 Files selected for processing (8)
  • crates/admin-cli/src/machine_interfaces/delete/args.rs
  • crates/admin-cli/src/machine_interfaces/delete/cmd.rs
  • crates/admin-cli/src/machine_interfaces/tests.rs
  • crates/api-core/src/handlers/machine_interface.rs
  • crates/api-core/src/tests/machine_interfaces.rs
  • crates/api-db/src/machine_interface.rs
  • crates/rpc/proto/forge.proto
  • rest-api/proto/core/src/v1/nico_nico.proto

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.

1 participant