Skip to content

feat: support IP allocation policies for expected interfaces#3991

Open
chet wants to merge 1 commit into
NVIDIA:mainfrom
chet:gh-issue-3990
Open

feat: support IP allocation policies for expected interfaces#3991
chet wants to merge 1 commit into
NVIDIA:mainfrom
chet:gh-issue-3990

Conversation

@chet

@chet chet commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

As it stood, ExpectedMachine.host_nics could reserve a host NIC with fixed_ip, but every entry was treated as a host data interface. There was no way to say "this is the DPU OS/BMC" or choose Dynamic, Fixed, or Retained for that interface.

So, add Host, DpuOs, and DpuBmc roles plus an allocation policy for every entry. Fixed addresses find their segment by prefix containment, while DHCP uses the relay/link segment; network_segment_type is an optional check for either path. Retained DHCP allocations use the existing Static representation once an address has been selected.

The protobuf fields are optional, and old host_nics payloads still infer Fixed from fixed_ip and Dynamic otherwise. ExpectedMachine updates only reconcile never-associated reservations, so managed, preserved, and operator-owned interfaces are left alone. host_nics also gets a jsonb_path_ops GIN index now that DHCP and Site Explorer hit that lookup regularly.

Tests cover the old payloads, every role/policy combination, reservation transitions, DHCPv4/v6 retention, and the managed-state boundaries. HostBmc, DPU loopbacks, and the ExpectedInterface/interfaces rename stay in separate follow-ups.

Configuration examples

Expected Machine manifests remain JSON today, so these examples use the current expected_machines.json input rather than TOML. A single machine can mix every supported role and allocation policy:

{
  "expected_machines": [
    {
      "bmc_mac_address": "02:00:00:00:00:01",
      "bmc_username": "root",
      "bmc_password": "default-password",
      "chassis_serial_number": "SERIAL-1",
      "host_nics": [
        {
          "mac_address": "02:00:00:00:00:10",
          "role": "host",
          "ip_allocation": "dynamic",
          "primary": true
        },
        {
          "mac_address": "02:00:00:00:00:20",
          "role": "dpu_os",
          "ip_allocation": "fixed",
          "fixed_ip": "192.0.2.20"
        },
        {
          "mac_address": "02:00:00:00:00:21",
          "role": "dpu_bmc",
          "ip_allocation": "retained"
        }
      ]
    }
  ]
}

Dynamic uses the DHCP relay or link segment and keeps a normal lease. Fixed requires fixed_ip, which selects its segment by prefix containment. Retained must omit fixed_ip; NICo retains the address selected through DHCP. network_segment_type remains optional and only narrows or verifies the selected segment.

Existing host reservations remain valid without either new field:

{
  "host_nics": [
    {
      "mac_address": "02:00:00:00:00:30",
      "fixed_ip": "192.0.2.30"
    }
  ]
}

Omitting role means host; omitting ip_allocation infers fixed when fixed_ip is present and dynamic otherwise.

Related issues

This supports #3990

Type of Change

  • Add - New feature or capability
  • Change - Changes in existing functionality
  • Fix - Bug fixes
  • Remove - Removed features or deprecated functionality
  • Internal - Internal changes (refactoring, tests, docs, etc.)

Breaking Changes

  • This PR contains breaking changes

Testing

  • Unit tests added/updated

  • Integration tests added/updated

  • Manual testing performed

  • No testing required (docs, internal refactor, etc.)

  • cargo make format-nightly

  • cargo make check-format-nightly

  • cargo make clippy

  • cargo make carbide-lints

  • cargo make check-workspace-deps

  • cargo check --tests -p carbide-api-db -p carbide-site-explorer -p carbide-api-core

  • cargo test -p carbide-api-db --lib -- --test-threads=1

  • cargo test -p carbide-api-core --lib

  • cargo test -p carbide-site-explorer --lib

  • cargo test -p carbide-rpc --features model --lib expected_interface

  • cargo test -p nico-admin-cli expected_machine_json_preserves_host_nics_field_presence

  • cargo test -p carbide-api-db expected_interface -- --test-threads=1

  • cargo test -p carbide-api-db force_delet --lib -- --test-threads=1

  • cargo test -p carbide-api-db host_mac_lookup_rejects_duplicate --lib -- --test-threads=1

  • cargo test -p carbide-api-db admin_segment_snapshot_is_loaded_after_exclusive_lock --lib -- --nocapture

  • cargo test -p carbide-api-db bmc_metadata::tests --lib

  • cargo test -p carbide-api-db expected_switch::tests --lib

  • cargo test -p carbide-api-db machine_interface::tests --lib

  • cargo test -p carbide-api-core expected_interface -- --test-threads=1

  • cargo test -p carbide-api-core tests::expected_machine --lib

  • cargo test -p carbide-api-core test_replace_all_expected_switches --lib -- --nocapture

  • cargo test -p carbide-api-core machine_admin_force_delete:: --lib -- --test-threads=1

  • cargo test -p carbide-api-core machine_discovery:: --lib -- --test-threads=1

  • cargo test -p carbide-api-core expected_switch:: --lib -- --test-threads=1

  • cargo test -p carbide-api-core older_client --lib -- --test-threads=1

  • cargo test -p carbide-api-core legacy_expected_machine --lib -- --test-threads=1

  • cargo test -p carbide-api-core --test integration test_force_delete_power_shelf_deletes_interfaces -- --test-threads=1

  • cargo test -p carbide-site-explorer machine_creator::tests --lib -- --test-threads=1

  • cargo test -p carbide-site-explorer --test integration test_machine_creator_clears_removed_dpu_bmc_policy_before_association -- --test-threads=1

  • cargo test -p carbide-site-explorer --test integration test_site_explorer_reconcile_ -- --test-threads=1

  • Regenerated the REST Core protobufs and verified that a second generation pass left the source and generated diff byte-for-byte unchanged.

Additional Notes

  • The migration adds expected-interface snapshots, capture markers, a nullable ExpectedMachine reservation marker, and the host_nics GIN index.
  • expected_machine_preallocation = NULL intentionally identifies writes from before ownership tracking or from older binaries during a rolling upgrade. Current writers store true or false.
  • Reservation, discovery, and force-deletion paths use the shared allocator and machine-lifecycle lock order, then revalidate ownership after waiting. Force deletion preserves interfaces merely attached to the deleted machine and removes only interfaces it owns.
  • BMC association locks the current IP owner before trusting a cached interface ID, then rejects stale ID or MAC state before writing.
  • Repeated DHCP observations skip the capture update after the stored snapshot is complete; a later non-null declaration can still fill a captured null snapshot.
  • ExpectedSwitch bulk replacement locks the complete old/new MAC set before replacing configuration, then leaves interface materialization to DHCP or Site Explorer, matching add behavior.
  • Retained uses the existing Static database representation; it does not add another allocation type.
  • Configuration changes do not rewrite associated or preserved interfaces. The current policy takes effect after the relevant interface is removed and the machine is re-ingested.
  • Cross-segment uniqueness for the same fixed address is tracked separately in Serialize Fixed-Address Reservations Across Network Segments #4150.
  • Host BMC canonicalization is Add HostBmc to Expected Interfaces #4103, the ExpectedHostNic/host_nics rename is Rename ExpectedHostNic to ExpectedInterface #4104, and the docs-only CLI update is Document Expected Interface IP Allocation Policies #4151.

@chet
chet requested a review from a team as a code owner July 23, 2026 17:21
@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Expected-machine interfaces now carry explicit roles, allocation policies, captured state, and reservation provenance through CLI, RPC, database, DHCP, discovery, Site Explorer, and cleanup workflows. Lock ordering, ambiguity validation, atomic reconciliation, and prepared deletion flows are also expanded.

Changes

Expected interface lifecycle

Layer / File(s) Summary
Interface contracts and compatibility
crates/api-model/..., crates/rpc/..., crates/admin-cli/..., */nico_nico.proto
Adds role and IP-allocation fields with legacy-compatible JSON, protobuf, and CLI handling.
Reservation state and reconciliation
crates/api-db/..., crates/api-core/src/handlers/expected_machine.rs
Adds captured interface state, allocation provenance, identity locking, typed segment resolution, fixed-reservation reconciliation, and atomic batch updates.
Runtime integration
crates/api-core/src/dhcp/..., crates/api-core/src/handlers/machine_discovery.rs, crates/site-explorer/...
Applies expected-interface policy during DHCP, discovery, managed-host ingestion, BMC association, and Site Explorer reconciliation.
Prepared cleanup
crates/api-core/src/handlers/{machine,machine_interface,power_shelf,switch}.rs, crates/api-db/src/machine_interface.rs
Uses prepared deletion records, ordered locks, identity revalidation, and retry preconditions for interface cleanup.
Validation and concurrency tests
crates/api-core/src/tests/..., crates/api-db/src/.../tests.rs, crates/site-explorer/tests/...
Adds coverage for policy validation, allocation transitions, advisory locks, ambiguity rejection, discovery races, reconciliation, migration state, and deletion behavior.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant AdminCLI
  participant ExpectedMachineHandler
  participant ApiDB
  participant DhcpDiscovery
  participant SiteExplorer

  AdminCLI->>ExpectedMachineHandler: Submit expected interface policy
  ExpectedMachineHandler->>ApiDB: Validate identity and reconcile reservations
  DhcpDiscovery->>ApiDB: Resolve captured policy and allocation state
  SiteExplorer->>ApiDB: Reload current configuration before preallocation
  ApiDB-->>DhcpDiscovery: Return interface and address state
Loading

Possibly related issues

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main feature added to expected interfaces.
Description check ✅ Passed The description is clearly related to the changeset and summarizes the added roles, allocation policies, and compatibility work.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@chet

chet commented Jul 23, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai full_review, thanks!

@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

@chet Understood — I’ll perform a complete review of PR #3991, including the full diff and its integration points.

🐇🔍

✅ Action performed

Full review finished.

@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-23 17:25:23 UTC | Commit: c10be0b

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/site-explorer/src/lib.rs (1)

2051-2058: 🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

Per-NIC DB round trip now happens even for dynamic-allocation interfaces.

try_preallocate_one previously required a concrete ip: IpAddr, so this loop must have filtered on nic.fixed_ip.is_some() before calling it. That guard has been removed: try_preallocate_expected_interface now unconditionally opens a transaction, runs the locked find_by_host_mac_address query, and only then checks fixed_ip. Every host NIC using Dynamic/Retained allocation (the common case) now incurs a full txn+query+lock on every SiteExplorer iteration, across the whole fleet, for no reason — since the caller already has expected_machine.data.host_nics in memory with fixed_ip populated.

Restore the cheap in-memory filter before paying for the DB round trip; re-validation under lock still happens inside try_preallocate_expected_interface for NICs that do declare a fixed IP, so TOCTOU protection is unaffected.

🐎 Proposed fix to skip needless transactions for dynamic NICs
 for nic in &expected_machine.data.host_nics {
+    if nic.fixed_ip.is_none() {
+        continue;
+    }
     try_preallocate_expected_interface(
         &self.database_connection,
         nic.mac_address,
         self.config.retained_boot_interface_window,
     )
     .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/site-explorer/src/lib.rs` around lines 2051 - 2058, In the loop over
expected_machine.data.host_nics, skip NICs whose fixed_ip is absent before
calling try_preallocate_expected_interface. Preserve the existing
database-backed revalidation inside try_preallocate_expected_interface for NICs
with a concrete fixed IP.
🧹 Nitpick comments (1)
crates/site-explorer/src/lib.rs (1)

4250-4317: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Prefer the file's established table-driven test pattern here.

expected_data_interfaces_are_not_redfish_scan_candidates covers six input combinations with hand-rolled assert!/assert!(!...) calls, but this same file already uses check_cases/check_values/value_scenarios! for exactly this "pure input → bool" shape (e.g. bmc_reset_results_update_legacy_gauge_event_and_log, test_find_host_pf_mac_address). Table-driving this test would make the six scenarios self-documenting and easier to extend.

check_values(
    [
        Check { scenario: "unowned data on underlay, unknown mac", input: (unknown_mac, underlay_segment, InterfaceType::Data, false), expect: true },
        Check { scenario: "unowned data on underlay, expected data mac", input: (expected_data_mac, underlay_segment, InterfaceType::Data, false), expect: false },
        Check { scenario: "owned data on underlay", input: (unknown_mac, underlay_segment, InterfaceType::Data, true), expect: false },
        Check { scenario: "bmc on underlay, expected data mac, owned", input: (expected_data_mac, underlay_segment, InterfaceType::Bmc, true), expect: true },
        Check { scenario: "bmc on host-inband", input: (unknown_mac, host_inband_segment, InterfaceType::Bmc, false), expect: true },
        Check { scenario: "data on host-inband", input: (unknown_mac, host_inband_segment, InterfaceType::Data, false), expect: false },
    ],
    |(mac, segment, itype, owned)| scannable(&interface(mac, segment, itype, owned)),
);

As per coding guidelines, "For tests, prefer table-driven scenarios or explicit grouped cases (such as scenarios!, value_scenarios!, check_cases, and check_values) to cover input variants, especially for parsers, validators, and conversions."

🤖 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/site-explorer/src/lib.rs` around lines 4250 - 4317, Refactor
expected_data_interfaces_are_not_redfish_scan_candidates to use the file’s
established check_values table-driven pattern instead of six standalone
assertions. Define named Check scenarios for each existing MAC, segment,
interface-type, and ownership combination, then evaluate them through scannable
and interface while preserving all expected boolean results.

Source: Coding guidelines

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

Outside diff comments:
In `@crates/site-explorer/src/lib.rs`:
- Around line 2051-2058: In the loop over expected_machine.data.host_nics, skip
NICs whose fixed_ip is absent before calling try_preallocate_expected_interface.
Preserve the existing database-backed revalidation inside
try_preallocate_expected_interface for NICs with a concrete fixed IP.

---

Nitpick comments:
In `@crates/site-explorer/src/lib.rs`:
- Around line 4250-4317: Refactor
expected_data_interfaces_are_not_redfish_scan_candidates to use the file’s
established check_values table-driven pattern instead of six standalone
assertions. Define named Check scenarios for each existing MAC, segment,
interface-type, and ownership combination, then evaluate them through scannable
and interface while preserving all expected boolean results.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 26a16d52-706c-4325-acd8-0c88fbf7ff44

📥 Commits

Reviewing files that changed from the base of the PR and between d851387 and c10be0b.

⛔ 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 (29)
  • crates/admin-cli/src/expected_machines/common.rs
  • crates/admin-cli/src/expected_machines/tests.rs
  • crates/admin-cli/src/expected_machines/update/mod.rs
  • crates/admin-cli/src/rpc.rs
  • crates/api-core/src/dhcp/discover.rs
  • crates/api-core/src/handlers/expected_machine.rs
  • crates/api-core/src/handlers/expected_power_shelf.rs
  • crates/api-core/src/handlers/expected_switch.rs
  • crates/api-core/src/handlers/machine_interface_address.rs
  • crates/api-core/src/tests/expected_machine.rs
  • crates/api-core/src/tests/expected_switch.rs
  • crates/api-core/src/tests/machine_dhcp.rs
  • crates/api-core/tests/integration/expected_power_shelf_static_address.rs
  • crates/api-db/migrations/20260722120000_expected_interface_allocation_state.sql
  • crates/api-db/src/expected_machine.rs
  • crates/api-db/src/machine_interface.rs
  • crates/api-db/src/machine_interface/tests.rs
  • crates/api-db/src/machine_interface_address.rs
  • crates/api-db/src/network_segment.rs
  • crates/api-model/src/expected_machine.rs
  • crates/machine-a-tron/src/machine_a_tron.rs
  • crates/rpc/build.rs
  • crates/rpc/proto/forge.proto
  • crates/rpc/src/lib.rs
  • crates/rpc/src/model/expected_machine.rs
  • crates/site-explorer/src/lib.rs
  • crates/site-explorer/src/machine_creator.rs
  • crates/site-explorer/tests/integration/reconcile.rs
  • rest-api/proto/core/src/v1/nico_nico.proto

@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 (2)
crates/site-explorer/src/lib.rs (2)

4250-4317: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Prefer table-driven assertions for this predicate.

expected_data_interfaces_are_not_redfish_scan_candidates exercises six combinations of is_bmc/segment/owned/expected via a flat series of assert!/assert!(!...) calls. This module already imports check_cases/value_scenarios! for exactly this kind of validator coverage elsewhere in the file (e.g. test_find_host_pf_mac_address).

♻️ Sketch using check_values
+    use carbide_test_support::{Check, check_values};
+
     #[test]
     fn expected_data_interfaces_are_not_redfish_scan_candidates() {
         ...
-        assert!(scannable(&interface(unknown_mac, underlay_segment, InterfaceType::Data, false)));
-        assert!(!scannable(&interface(expected_data_mac, underlay_segment, InterfaceType::Data, false)));
-        ...
+        check_values(
+            [
+                Check { scenario: "unowned unknown data on underlay", input: (unknown_mac, underlay_segment, InterfaceType::Data, false), expect: true },
+                Check { scenario: "unowned expected data on underlay", input: (expected_data_mac, underlay_segment, InterfaceType::Data, false), expect: false },
+                // ... remaining cases
+            ],
+            |(mac, seg, ty, owned)| scannable(&interface(mac, seg, ty, owned)),
+        );
     }

As per coding guidelines, "For tests, prefer table-driven scenarios or explicit grouped cases (such as scenarios!, value_scenarios!, check_cases, and check_values) to cover input variants, especially for parsers, validators, and conversions."

🤖 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/site-explorer/src/lib.rs` around lines 4250 - 4317, Refactor
expected_data_interfaces_are_not_redfish_scan_candidates into a table-driven
test using the existing check_cases, check_values, or value_scenarios! helpers.
Represent the six combinations of interface type, segment, ownership, MAC
expectation, and expected scannability as grouped scenarios, while preserving
each case’s current outcome.

Source: Coding guidelines


2051-2058: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Skip the per-NIC round trip for non-fixed interfaces.

try_preallocate_expected_interface is invoked unconditionally for every host_nic on every reconciliation pass, but the loop already holds nic: &ExpectedHostNic in memory — a Dynamic/Retained NIC's fixed_ip is None and known here without a DB call. Each invocation still pays for a full txn.begin(), the find_by_host_mac_address jsonb lookup, and an implicit rollback before discovering there's nothing to do. At scale (many machines × NICs, run every iteration) this is a steady stream of wasted transactions.

⚡ Proposed fix: filter by `fixed_ip` before the expensive call
             for nic in &expected_machine.data.host_nics {
-                try_preallocate_expected_interface(
-                    &self.database_connection,
-                    nic.mac_address,
-                    self.config.retained_boot_interface_window,
-                )
-                .await;
+                if nic.fixed_ip.is_some() {
+                    try_preallocate_expected_interface(
+                        &self.database_connection,
+                        nic.mac_address,
+                        self.config.retained_boot_interface_window,
+                    )
+                    .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/site-explorer/src/lib.rs` around lines 2051 - 2058, In the
reconciliation loop over expected_machine.data.host_nics, invoke
try_preallocate_expected_interface only when nic.fixed_ip is present; skip
Dynamic/Retained NICs with fixed_ip set to None before entering the
database-backed call, while preserving the existing arguments and await behavior
for fixed interfaces.
🤖 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/site-explorer/src/lib.rs`:
- Around line 4250-4317: Refactor
expected_data_interfaces_are_not_redfish_scan_candidates into a table-driven
test using the existing check_cases, check_values, or value_scenarios! helpers.
Represent the six combinations of interface type, segment, ownership, MAC
expectation, and expected scannability as grouped scenarios, while preserving
each case’s current outcome.
- Around line 2051-2058: In the reconciliation loop over
expected_machine.data.host_nics, invoke try_preallocate_expected_interface only
when nic.fixed_ip is present; skip Dynamic/Retained NICs with fixed_ip set to
None before entering the database-backed call, while preserving the existing
arguments and await behavior for fixed interfaces.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: b3ce8801-09e2-4a0e-a7aa-d363a774b688

📥 Commits

Reviewing files that changed from the base of the PR and between d851387 and c10be0b.

⛔ 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 (29)
  • crates/admin-cli/src/expected_machines/common.rs
  • crates/admin-cli/src/expected_machines/tests.rs
  • crates/admin-cli/src/expected_machines/update/mod.rs
  • crates/admin-cli/src/rpc.rs
  • crates/api-core/src/dhcp/discover.rs
  • crates/api-core/src/handlers/expected_machine.rs
  • crates/api-core/src/handlers/expected_power_shelf.rs
  • crates/api-core/src/handlers/expected_switch.rs
  • crates/api-core/src/handlers/machine_interface_address.rs
  • crates/api-core/src/tests/expected_machine.rs
  • crates/api-core/src/tests/expected_switch.rs
  • crates/api-core/src/tests/machine_dhcp.rs
  • crates/api-core/tests/integration/expected_power_shelf_static_address.rs
  • crates/api-db/migrations/20260722120000_expected_interface_allocation_state.sql
  • crates/api-db/src/expected_machine.rs
  • crates/api-db/src/machine_interface.rs
  • crates/api-db/src/machine_interface/tests.rs
  • crates/api-db/src/machine_interface_address.rs
  • crates/api-db/src/network_segment.rs
  • crates/api-model/src/expected_machine.rs
  • crates/machine-a-tron/src/machine_a_tron.rs
  • crates/rpc/build.rs
  • crates/rpc/proto/forge.proto
  • crates/rpc/src/lib.rs
  • crates/rpc/src/model/expected_machine.rs
  • crates/site-explorer/src/lib.rs
  • crates/site-explorer/src/machine_creator.rs
  • crates/site-explorer/tests/integration/reconcile.rs
  • rest-api/proto/core/src/v1/nico_nico.proto

@chet

chet commented Jul 23, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai full_review, thanks!

@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

@chet Understood — I’ll perform a complete review of PR #3991, including the full diff and its integration points.

🐇🔍

✅ Action performed

Full review finished.


Your included review limit is currently reached under our Fair Usage Limits Policy. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 48 seconds.

@github-actions

Copy link
Copy Markdown

@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: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/api-core/src/handlers/expected_machine.rs (1)

238-259: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Add chassis-serial validation to the single update path

The update handler only checks duplicate DPU serials before converting the request, while add and batch writes enforce CHASSIS_SERIAL_REGEX. A malformed chassis_serial_number can still pass through this RPC. Reuse the shared parser here or apply the same regex before try_into() so all write paths enforce the same constraint.

🤖 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/expected_machine.rs` around lines 238 - 259, The
single-machine update flow around request conversion lacks chassis serial
validation. In the update handler, validate request.chassis_serial_number using
the shared chassis-serial parser or the same CHASSIS_SERIAL_REGEX enforcement
used by add and batch writes before request.try_into(), while preserving the
existing invalid-argument error behavior.
🤖 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/expected_machine.rs`:
- Around line 579-582: Update the InvalidArgument message in the
expected-machine BMC MAC validation path to begin with a lowercase phrase, while
preserving the existing interpolation and no-trailing-period format.

In
`@crates/api-db/migrations/20260722120000_expected_interface_allocation_state.sql`:
- Around line 4-8: Update the migration adding expected_machine_preallocation to
backfill existing NULL values to false before relying on the default, then
enforce the non-NULL invariant with a NOT NULL constraint. Keep the false
default for future inserts.

---

Outside diff comments:
In `@crates/api-core/src/handlers/expected_machine.rs`:
- Around line 238-259: The single-machine update flow around request conversion
lacks chassis serial validation. In the update handler, validate
request.chassis_serial_number using the shared chassis-serial parser or the same
CHASSIS_SERIAL_REGEX enforcement used by add and batch writes before
request.try_into(), while preserving the existing invalid-argument error
behavior.
🪄 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: 3568b2f2-58fc-416a-9b4b-d575e6d07418

📥 Commits

Reviewing files that changed from the base of the PR and between c10be0b and a2a477f.

⛔ 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 (29)
  • crates/admin-cli/src/expected_machines/common.rs
  • crates/admin-cli/src/expected_machines/tests.rs
  • crates/admin-cli/src/expected_machines/update/mod.rs
  • crates/admin-cli/src/rpc.rs
  • crates/api-core/src/dhcp/discover.rs
  • crates/api-core/src/handlers/expected_machine.rs
  • crates/api-core/src/handlers/expected_power_shelf.rs
  • crates/api-core/src/handlers/expected_switch.rs
  • crates/api-core/src/handlers/machine_interface_address.rs
  • crates/api-core/src/tests/expected_machine.rs
  • crates/api-core/src/tests/expected_switch.rs
  • crates/api-core/src/tests/machine_dhcp.rs
  • crates/api-core/tests/integration/expected_power_shelf_static_address.rs
  • crates/api-db/migrations/20260722120000_expected_interface_allocation_state.sql
  • crates/api-db/src/expected_machine.rs
  • crates/api-db/src/machine_interface.rs
  • crates/api-db/src/machine_interface/tests.rs
  • crates/api-db/src/machine_interface_address.rs
  • crates/api-db/src/network_segment.rs
  • crates/api-model/src/expected_machine.rs
  • crates/machine-a-tron/src/machine_a_tron.rs
  • crates/rpc/build.rs
  • crates/rpc/proto/forge.proto
  • crates/rpc/src/lib.rs
  • crates/rpc/src/model/expected_machine.rs
  • crates/site-explorer/src/lib.rs
  • crates/site-explorer/src/machine_creator.rs
  • crates/site-explorer/tests/integration/reconcile.rs
  • rest-api/proto/core/src/v1/nico_nico.proto
🚧 Files skipped from review as they are similar to previous changes (26)
  • crates/admin-cli/src/expected_machines/common.rs
  • crates/admin-cli/src/expected_machines/update/mod.rs
  • crates/api-core/src/handlers/expected_power_shelf.rs
  • crates/rpc/build.rs
  • crates/api-core/src/tests/expected_switch.rs
  • crates/admin-cli/src/rpc.rs
  • crates/api-core/src/handlers/expected_switch.rs
  • crates/site-explorer/src/machine_creator.rs
  • crates/machine-a-tron/src/machine_a_tron.rs
  • crates/api-db/src/network_segment.rs
  • crates/rpc/src/lib.rs
  • crates/admin-cli/src/expected_machines/tests.rs
  • crates/rpc/proto/forge.proto
  • crates/api-core/src/handlers/machine_interface_address.rs
  • crates/api-db/src/machine_interface_address.rs
  • rest-api/proto/core/src/v1/nico_nico.proto
  • crates/api-db/src/machine_interface/tests.rs
  • crates/rpc/src/model/expected_machine.rs
  • crates/api-core/tests/integration/expected_power_shelf_static_address.rs
  • crates/api-db/src/expected_machine.rs
  • crates/api-model/src/expected_machine.rs
  • crates/site-explorer/src/lib.rs
  • crates/api-core/src/tests/expected_machine.rs
  • crates/api-core/src/dhcp/discover.rs
  • crates/api-core/src/tests/machine_dhcp.rs
  • crates/api-db/src/machine_interface.rs

Comment thread crates/api-core/src/handlers/expected_machine.rs
Comment thread crates/api-db/migrations/20260722120000_expected_interface_allocation_state.sql Outdated

@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-db/src/expected_machine.rs`:
- Around line 205-207: Update the DatabaseError::InvalidArgument message in the
ExpectedMachine identity-conflict branch to begin with lowercase text while
preserving the existing interpolation and wording. Keep the message free of a
trailing period, consistent with the neighboring error messages in the same
helper.
🪄 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: 5c93563e-64ea-476f-a6d7-5f11d52ccc53

📥 Commits

Reviewing files that changed from the base of the PR and between 29808a2 and a2a477f.

⛔ 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 (29)
  • crates/admin-cli/src/expected_machines/common.rs
  • crates/admin-cli/src/expected_machines/tests.rs
  • crates/admin-cli/src/expected_machines/update/mod.rs
  • crates/admin-cli/src/rpc.rs
  • crates/api-core/src/dhcp/discover.rs
  • crates/api-core/src/handlers/expected_machine.rs
  • crates/api-core/src/handlers/expected_power_shelf.rs
  • crates/api-core/src/handlers/expected_switch.rs
  • crates/api-core/src/handlers/machine_interface_address.rs
  • crates/api-core/src/tests/expected_machine.rs
  • crates/api-core/src/tests/expected_switch.rs
  • crates/api-core/src/tests/machine_dhcp.rs
  • crates/api-core/tests/integration/expected_power_shelf_static_address.rs
  • crates/api-db/migrations/20260722120000_expected_interface_allocation_state.sql
  • crates/api-db/src/expected_machine.rs
  • crates/api-db/src/machine_interface.rs
  • crates/api-db/src/machine_interface/tests.rs
  • crates/api-db/src/machine_interface_address.rs
  • crates/api-db/src/network_segment.rs
  • crates/api-model/src/expected_machine.rs
  • crates/machine-a-tron/src/machine_a_tron.rs
  • crates/rpc/build.rs
  • crates/rpc/proto/forge.proto
  • crates/rpc/src/lib.rs
  • crates/rpc/src/model/expected_machine.rs
  • crates/site-explorer/src/lib.rs
  • crates/site-explorer/src/machine_creator.rs
  • crates/site-explorer/tests/integration/reconcile.rs
  • rest-api/proto/core/src/v1/nico_nico.proto

Comment thread crates/api-db/src/expected_machine.rs
@chet

chet commented Jul 23, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai full_review, thanks!

@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

@chet: Initiating a complete re-review of #3991, including the full diff and integration points.

🐇🔍

✅ Action performed

Full review finished.

@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 (2)
crates/api-db/migrations/20260722120000_expected_interface_allocation_state.sql (1)

12-13: 🗄️ Data Integrity & Integration | 🔵 Trivial

The Squawk require-concurrent-index-creation warning is a false positive in this repository: sqlx::migrate!() runs migrations in a transaction, and CREATE INDEX CONCURRENTLY is incompatible with transactional DDL. Plain CREATE INDEX is the established convention here, so no change is warranted.

That said, given expected_machines is written on the operator/reconciliation path, do confirm the table is small enough that a brief write lock during index build is acceptable in production; if not, this index may warrant an out-of-band concurrent build outside the transactional 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/migrations/20260722120000_expected_interface_allocation_state.sql`
around lines 12 - 13, No code change is warranted: retain the plain CREATE INDEX
statement because sqlx::migrate!() runs migrations transactionally and CREATE
INDEX CONCURRENTLY is incompatible with transactional DDL. Confirm that
expected_machines is small enough for the brief write lock during index
creation; only pursue an out-of-band concurrent build if production sizing makes
that lock unacceptable.

Sources: Learnings, Linters/SAST tools

crates/api-core/src/handlers/expected_machine.rs (1)

433-478: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider rejecting duplicate fixed_ip values across host_nics here.

Interface validation catches duplicate MACs but not two distinct NICs declaring the same fixed_ip. That collision currently escapes validation and only surfaces downstream in reconcile_configured_interfaces as a generic IP address … is already allocated DatabaseError, which is opaque to the operator. A small HashSet<IpAddr> guard alongside the existing seen_macs check would fail fast with an actionable 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/handlers/expected_machine.rs` around lines 433 - 478, The
validate_expected_interfaces function should reject duplicate fixed_ip values
across distinct host_nics before reconciliation. Add a HashSet<IpAddr> alongside
seen_macs, insert each present fixed_ip during interface validation, and return
CarbideError::InvalidArgument with an actionable host_nics-specific message when
insertion detects a duplicate.
🤖 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/api-core/src/handlers/expected_machine.rs`:
- Around line 433-478: The validate_expected_interfaces function should reject
duplicate fixed_ip values across distinct host_nics before reconciliation. Add a
HashSet<IpAddr> alongside seen_macs, insert each present fixed_ip during
interface validation, and return CarbideError::InvalidArgument with an
actionable host_nics-specific message when insertion detects a duplicate.

In
`@crates/api-db/migrations/20260722120000_expected_interface_allocation_state.sql`:
- Around line 12-13: No code change is warranted: retain the plain CREATE INDEX
statement because sqlx::migrate!() runs migrations transactionally and CREATE
INDEX CONCURRENTLY is incompatible with transactional DDL. Confirm that
expected_machines is small enough for the brief write lock during index
creation; only pursue an out-of-band concurrent build if production sizing makes
that lock unacceptable.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 66ab5277-f8e4-4d01-9909-6f0b9a337ad8

📥 Commits

Reviewing files that changed from the base of the PR and between cc92c42 and 74bc231.

⛔ 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 (29)
  • crates/admin-cli/src/expected_machines/common.rs
  • crates/admin-cli/src/expected_machines/tests.rs
  • crates/admin-cli/src/expected_machines/update/mod.rs
  • crates/admin-cli/src/rpc.rs
  • crates/api-core/src/dhcp/discover.rs
  • crates/api-core/src/handlers/expected_machine.rs
  • crates/api-core/src/handlers/expected_power_shelf.rs
  • crates/api-core/src/handlers/expected_switch.rs
  • crates/api-core/src/handlers/machine_interface_address.rs
  • crates/api-core/src/tests/expected_machine.rs
  • crates/api-core/src/tests/expected_switch.rs
  • crates/api-core/src/tests/machine_dhcp.rs
  • crates/api-core/tests/integration/expected_power_shelf_static_address.rs
  • crates/api-db/migrations/20260722120000_expected_interface_allocation_state.sql
  • crates/api-db/src/expected_machine.rs
  • crates/api-db/src/machine_interface.rs
  • crates/api-db/src/machine_interface/tests.rs
  • crates/api-db/src/machine_interface_address.rs
  • crates/api-db/src/network_segment.rs
  • crates/api-model/src/expected_machine.rs
  • crates/machine-a-tron/src/machine_a_tron.rs
  • crates/rpc/build.rs
  • crates/rpc/proto/forge.proto
  • crates/rpc/src/lib.rs
  • crates/rpc/src/model/expected_machine.rs
  • crates/site-explorer/src/lib.rs
  • crates/site-explorer/src/machine_creator.rs
  • crates/site-explorer/tests/integration/reconcile.rs
  • rest-api/proto/core/src/v1/nico_nico.proto

@chet

chet commented Jul 23, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai full_review, thanks!

@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

@chet: Initiating a complete re-review of #3991, including the full diff and relevant integration paths.

🐇🔍

✅ Action performed

Full review finished.

@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 (1)
crates/api-db/src/expected_machine.rs (1)

93-145: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Host-MAC disambiguation reads cleanly.

The cross-result top_level/nested accounting mirrors the write-time invariant enforced by validate_identity_macs_available, and the FOR SHARE lock is held for the caller's preallocation window as intended. One observation: a BMC-only match deliberately yields Ok(None) here and is routed to the legacy BMC path downstream — worth a one-line inline note so a future reader does not mistake it for a missed case.

🤖 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/expected_machine.rs` around lines 93 - 145, Add a concise
inline comment in find_by_host_mac_address explaining that a BMC-only match
intentionally returns Ok(None) so the caller can use the legacy BMC path. Keep
the existing matching logic and return behavior unchanged.
🤖 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/api-db/src/expected_machine.rs`:
- Around line 93-145: Add a concise inline comment in find_by_host_mac_address
explaining that a BMC-only match intentionally returns Ok(None) so the caller
can use the legacy BMC path. Keep the existing matching logic and return
behavior unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 16a0ffeb-7ec3-406f-882e-702703f22859

📥 Commits

Reviewing files that changed from the base of the PR and between a2ae020 and a982cfd.

⛔ 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 (29)
  • crates/admin-cli/src/expected_machines/common.rs
  • crates/admin-cli/src/expected_machines/tests.rs
  • crates/admin-cli/src/expected_machines/update/mod.rs
  • crates/admin-cli/src/rpc.rs
  • crates/api-core/src/dhcp/discover.rs
  • crates/api-core/src/handlers/expected_machine.rs
  • crates/api-core/src/handlers/expected_power_shelf.rs
  • crates/api-core/src/handlers/expected_switch.rs
  • crates/api-core/src/handlers/machine_interface_address.rs
  • crates/api-core/src/tests/expected_machine.rs
  • crates/api-core/src/tests/expected_switch.rs
  • crates/api-core/src/tests/machine_dhcp.rs
  • crates/api-core/tests/integration/expected_power_shelf_static_address.rs
  • crates/api-db/migrations/20260722120000_expected_interface_allocation_state.sql
  • crates/api-db/src/expected_machine.rs
  • crates/api-db/src/machine_interface.rs
  • crates/api-db/src/machine_interface/tests.rs
  • crates/api-db/src/machine_interface_address.rs
  • crates/api-db/src/network_segment.rs
  • crates/api-model/src/expected_machine.rs
  • crates/machine-a-tron/src/machine_a_tron.rs
  • crates/rpc/build.rs
  • crates/rpc/proto/forge.proto
  • crates/rpc/src/lib.rs
  • crates/rpc/src/model/expected_machine.rs
  • crates/site-explorer/src/lib.rs
  • crates/site-explorer/src/machine_creator.rs
  • crates/site-explorer/tests/integration/reconcile.rs
  • rest-api/proto/core/src/v1/nico_nico.proto

@chet

chet commented Jul 23, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai full_review, thanks!

@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

@chet: Initiating a complete re-review of #3991, including the full diff and relevant integration paths.

🐇🔍

✅ Action performed

Full review finished.

@chet

chet commented Jul 23, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai full_review, thanks!

@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

@chet: Initiating a complete re-review of #3991, including the full diff and relevant integration paths.

🐇🔍

✅ Action performed

Full review finished.

@chet

chet commented Jul 25, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai full_review, thanks!

@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

@chet: Initiating a complete re-review of #3991, including the full diff and relevant integration paths.

🐇🔍

✅ Action performed

Full review finished.

@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: 4

🧹 Nitpick comments (11)
crates/api-core/src/tests/expected_switch.rs (2)

499-514: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the lock-timeout condition rather than any error.

result.is_err() is satisfied by any failure — a pool error or an unrelated DatabaseError would keep this test green while proving nothing about lock retention. Bind the assertion to Postgres lock_not_available (SQLSTATE 55P03) so the test fails loudly if the contention mechanism changes.

♻️ Suggested assertion tightening
-        assert!(
-            result.is_err(),
-            "ExpectedSwitch replacement did not hold its {name} MAC lock"
-        );
+        let error = result.expect_err(&format!(
+            "ExpectedSwitch replacement did not hold its {name} MAC lock"
+        ));
+        assert!(
+            format!("{error:?}").contains("55P03"),
+            "expected a lock timeout for the {name} MAC, got: {error:?}"
+        );

Prefer matching the concrete DatabaseError variant and its sqlx::Error::Database(db).code() if that is reachable from this module, rather than string matching.

🤖 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/expected_switch.rs` around lines 499 - 514,
Strengthen the assertion in the loop testing lock retention by requiring the
`lock_expected_machine_interface_macs` result to be a `sqlx::Error::Database`
whose database error code is PostgreSQL `55P03` (`lock_not_available`). Reject
pool errors and unrelated database errors while preserving the existing rollback
flow.

29-83: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consolidate the two lock-wait helpers and add a poll interval.

The two helpers differ only in EXISTS versus count(*) >= n, i.e. the first is the second with expected_waiters = 1. Additionally, yield_now() provides no backoff, so each helper issues pg_stat_activity round-trips as fast as the pool permits for up to five seconds, occupying a pooled connection and contending with the very tasks under observation. A short sleep is the idiomatic cadence for this pattern.

♻️ Proposed consolidation
-async fn wait_for_expected_switch_mac_lock(pool: &sqlx::PgPool, blocker_pid: i32) {
-    tokio::time::timeout(std::time::Duration::from_secs(5), async {
-        loop {
-            let waiting = sqlx::query_scalar::<_, bool>(
-                "SELECT EXISTS (
-                    SELECT 1
-                    FROM pg_stat_activity
-                    WHERE datname = current_database()
-                      AND wait_event_type = 'Lock'
-                      AND wait_event = 'advisory'
-                      AND $1::integer = ANY(pg_blocking_pids(pid))
-                )",
-            )
-            .bind(blocker_pid)
-            .fetch_one(pool)
-            .await
-            .expect("inspect ExpectedSwitch MAC lock wait");
-            if waiting {
-                return;
-            }
-            tokio::task::yield_now().await;
-        }
-    })
-    .await
-    .expect("ExpectedSwitch replacement did not reach the MAC lock wait");
-}
-
-async fn wait_for_expected_switch_write_lock_waiters(
-    pool: &sqlx::PgPool,
-    blocker_pid: i32,
-    expected_waiters: i64,
-) {
-    tokio::time::timeout(std::time::Duration::from_secs(5), async {
+async fn wait_for_advisory_lock_waiters(
+    pool: &sqlx::PgPool,
+    blocker_pid: i32,
+    expected_waiters: i64,
+    context: &str,
+) {
+    tokio::time::timeout(std::time::Duration::from_secs(5), async {
         loop {
             let waiters = sqlx::query_scalar::<_, i64>(
                 "SELECT count(*)
                  FROM pg_stat_activity
                  WHERE datname = current_database()
                    AND wait_event_type = 'Lock'
                    AND wait_event = 'advisory'
                    AND $1::integer = ANY(pg_blocking_pids(pid))",
             )
             .bind(blocker_pid)
             .fetch_one(pool)
             .await
-            .expect("inspect ExpectedSwitch write lock waiters");
+            .expect("inspect advisory lock waiters");
             if waiters >= expected_waiters {
                 return;
             }
-            tokio::task::yield_now().await;
+            tokio::time::sleep(std::time::Duration::from_millis(10)).await;
         }
     })
     .await
-    .expect("ExpectedSwitch creates did not reach the write lock wait");
+    .unwrap_or_else(|_| panic!("did not reach the advisory lock wait: {context}"));
 }
🤖 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/expected_switch.rs` around lines 29 - 83,
Consolidate wait_for_expected_switch_mac_lock and
wait_for_expected_switch_write_lock_waiters into one helper parameterized by
expected_waiters, using a count query with the existing blocker and
advisory-lock predicates; update callers so the MAC-lock case passes 1. Replace
tokio::task::yield_now() in the polling loop with a short tokio::time::sleep
interval before retrying, while preserving the five-second timeout and existing
error messages or equivalent context.
crates/api-core/src/tests/machine_dhcp.rs (1)

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

Two divergent advisory-lock wait helpers, one of them name-colliding. This PR introduces advisory-lock synchronization scaffolding independently in two test modules, with different detection strategies and a shared function name that does not share a signature. The machine_dhcp.rs variant matches waiters by SQL text (query LIKE '%pg_advisory_xact_lock%') and a global count, so it cannot attribute a waiter to the intended blocker and quietly stops matching if a lock is ever taken through a helper whose SQL text differs — for example a shared-mode or wrapped variant. The expected_machine.rs variant keys on a pid and pg_blocking_pids, which is the precise formulation. Consolidating on the pid-based approach would remove the duplication and make the weaker call sites attribute-correct; both currently pass only because #[crate::sqlx_test] isolates each test in its own database.

  • crates/api-core/src/tests/machine_dhcp.rs#L96-L123: replace the SQL-text/global-count predicate with the pid-based pg_blocking_pids form, and either rename wait_for_advisory_lock_wait or align its signature with the identically named helper in expected_machine.rs so the two cannot be confused; the multi-waiter call sites at lines 2002 and 2012 then need the blocker or waiter pid threaded through instead of a bare count.
  • crates/api-core/src/tests/expected_machine.rs#L110-L162: promote these two helpers into the shared test-support module (alongside the other common test fixtures) so machine_dhcp.rs consumes them rather than carrying a second implementation.
♻️ Sketch of the shared, pid-attributed helper
/// Wait until `waiting_pid`'s backend is parked on an advisory lock held by
/// `blocker_pid`. Attribution by pid keeps the predicate independent of the
/// SQL text used to take the lock.
pub async fn wait_for_advisory_lock_wait(pool: &sqlx::PgPool, blocker_pid: i32) {
    tokio::time::timeout(std::time::Duration::from_secs(5), async {
        loop {
            let waiting = sqlx::query_scalar::<_, bool>(
                "SELECT EXISTS (
                    SELECT 1
                    FROM pg_stat_activity
                    WHERE datname = current_database()
                      AND wait_event_type = 'Lock'
                      AND wait_event = 'advisory'
                      AND $1::integer = ANY(pg_blocking_pids(pid))
                )",
            )
            .bind(blocker_pid)
            .fetch_one(pool)
            .await
            .expect("inspect advisory lock wait");
            if waiting {
                return;
            }
            tokio::task::yield_now().await;
        }
    })
    .await
    .expect("request did not reach the advisory lock wait");
}
🤖 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_dhcp.rs` around lines 96 - 123, Consolidate
the advisory-lock wait helpers into shared test support: in
crates/api-core/src/tests/expected_machine.rs lines 110-162, promote the
pid-attributed helpers for reuse; in crates/api-core/src/tests/machine_dhcp.rs
lines 96-123, remove the SQL-text/global-count implementation and consume the
shared helpers, aligning or renaming wait_for_advisory_lock_wait to avoid the
signature collision. Update machine_dhcp.rs multi-waiter call sites at lines
2002 and 2012 to pass blocker or waiter PIDs rather than a bare count; no
separate implementation should remain in machine_dhcp.rs.
crates/api-db/src/predicted_machine_interface.rs (1)

124-137: 🗄️ Data Integrity & Integration | 🔵 Trivial | 💤 Low value

Consider asserting the conditional UPDATE actually won the race.

The statement is guarded by AND NOT expected_interface_captured, yet the result is discarded and the caller-supplied declaration is returned unconditionally. Today the MAC advisory lock taken in capture_expected_interface_for_discovery serializes writers, so a zero-row outcome is unreachable; if that lock discipline is ever relaxed, this silently reports a capture that did not happen. Inspecting rows_affected() (and returning the stored value on 0) would make the invariant self-enforcing rather than lock-dependent.

🤖 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/predicted_machine_interface.rs` around lines 124 - 137,
Update the conditional UPDATE in the expected-interface capture method to
inspect its rows_affected result. If no row was updated, return the persisted
interface value instead of unconditionally returning the caller-supplied
declaration; otherwise preserve the current captured-value return behavior.
crates/site-explorer/src/machine_creator.rs (1)

1283-1298: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Compare the id as a Uuid, not a String.

ExpectedMachineIngestionIdentity stringifies expected_machine.id solely to obtain equality, allocating on every ingestion attempt for a type that is already Copy + Eq.

♻️ Suggested simplification
-struct ExpectedMachineIngestionIdentity {
-    id: Option<String>,
+struct ExpectedMachineIngestionIdentity {
+    id: Option<uuid::Uuid>,
     bmc_mac_address: MacAddress,
     has_rack_id: bool,
 }
@@
-            id: expected_machine.id.map(|id| id.to_string()),
+            id: expected_machine.id,

The struct can then derive Copy as well.

🤖 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/site-explorer/src/machine_creator.rs` around lines 1283 - 1298, Update
ExpectedMachineIngestionIdentity to store expected_machine.id as its native
Uuid-compatible type instead of converting it to String in
From<&ExpectedMachine>. Preserve the existing optional-ID equality semantics and
derive Copy alongside the existing traits, since all fields are copyable.
crates/site-explorer/src/lib.rs (1)

2035-2061: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Gate the per-entity reconcile calls on the snapshot, as the host-NIC loop already does.

try_reconcile_expected_machine_bmc and try_reconcile_expected_switch_addresses are invoked unconditionally for every expected machine and every expected switch. Each call opens its own transaction, takes the shared configuration lock, and re-reads the row — even when the snapshot shows neither a fixed address nor a retention policy, i.e. when the helper is guaranteed to be a no-op. On a large site that is one extra transaction plus advisory lock per entity per pass.

The host-NIC loop three lines below already establishes the right pattern with if nic.fixed_ip.is_none() { continue; }; the helpers still re-read under lock, so a cheap snapshot guard can only skip work that would have been a no-op.

⚡ Suggested guard
-            try_reconcile_expected_machine_bmc(
-                &self.database_connection,
-                expected_machine.bmc_mac_address,
-                self.config.retained_boot_interface_window,
-            )
-            .await;
+            if expected_machine.data.bmc_ip_address.is_some()
+                || expected_machine
+                    .data
+                    .bmc_ip_allocation
+                    .retains_dynamic_ip(false)
+            {
+                try_reconcile_expected_machine_bmc(
+                    &self.database_connection,
+                    expected_machine.bmc_mac_address,
+                    self.config.retained_boot_interface_window,
+                )
+                .await;
+            }

An equivalent bmc_ip_address.is_some() || nvos_ip_address.is_some() guard applies to the expected-switch loop.

🤖 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/site-explorer/src/lib.rs` around lines 2035 - 2061, Gate the calls to
try_reconcile_expected_machine_bmc and try_reconcile_expected_switch_addresses
using the snapshot before invoking them: skip machines when neither
bmc_ip_address nor nvos_ip_address is present, and skip switches unless
bmc_ip_address or nvos_ip_address is present. Preserve the existing host-NIC
fixed_ip guard and reconciliation behavior for entities that pass these checks.
crates/api-db/src/machine.rs (1)

854-951: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Two near-identical lock helpers; consider extracting the shared plumbing.

Both functions normalize the id set, bind stringified ids, map 55P03 to a retryable FailedPrecondition, and verify the locked set matches the request. Only the lock strength (FOR KEY SHARE vs FOR UPDATE), the message, and the extra ForceDeletion check differ. A small private helper taking the lock clause plus the message would remove the duplicated error mapping and keep future callers honest about the NOWAIT contract.

Otherwise the semantics read correctly: NOWAIT avoids waiting behind a force-delete that will later need the caller's allocator/interface locks, and 55P03 is indeed lock_not_available.

🤖 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.rs` around lines 854 - 951, Extract the duplicated
machine-id normalization, string binding, NOWAIT query execution, 55P03 error
mapping, and locked-set validation from lock_interface_association_targets and
lock_force_deletion_targets into a small private helper. Parameterize the helper
with the lock clause and retry message, while keeping each public function’s
distinct query result handling and ForceDeletion validation unchanged.
crates/api-core/src/handlers/expected_machine.rs (1)

134-138: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Doc comments left behind by the validation/creation centralization. Both sites still describe the pre-refactor topology, where validation was insert-only and replace_all delegated per-entry work to add.

  • crates/api-core/src/handlers/expected_machine.rs#L134-L138: drop "prior to insert" and the add/import-only sharing claim; validate_expected_machine now serves the add, update, replace-all, and batch paths.
  • crates/api-core/src/handlers/expected_machine.rs#L376-L377: stop referencing [add]; describe the inline parse/validate/release/recreate flow and the fact that reconciled replacements materialize reservations here, with deferred materialization retained only for brand-new identities.
🤖 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/expected_machine.rs` around lines 134 - 138, The
doc comment for validate_expected_machine must describe its use across add,
update, replace-all, and batch paths rather than only insert and add/import
flows. Also update the replace-all documentation at
crates/api-core/src/handlers/expected_machine.rs#L376-L377 to describe its
inline parse, validate, release, and recreate process, including immediate
reservation materialization for reconciled replacements and deferred
materialization only for brand-new identities.
crates/api-db/src/machine_interface.rs (2)

1029-1044: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Test candidate emptiness directly rather than through the rendered type string.

Line 1038 uses !candidate_segment_types.is_empty() as a proxy for "there were candidate segments". It is correct today only because every NetworkSegmentType renders to a non-empty string; the predicate silently inverts if that ever stops holding. Capture the count before the into_iter() and assert on it.

♻️ Proposed clarification
     if let Some(network_segment_type) = expected_network_segment_type {
+        let had_candidates = !candidate_segments.is_empty();
         let candidate_segment_types = candidate_segments
             .iter()
             .map(|segment| segment.config.segment_type.to_string())
             .join(", ");
         let matching_segments = candidate_segments
             .into_iter()
             .filter(|segment| segment.config.segment_type == network_segment_type)
             .collect::<Vec<_>>();
-        if matching_segments.is_empty() && !candidate_segment_types.is_empty() {
+        if matching_segments.is_empty() && had_candidates {
🤖 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 1029 - 1044, In the
expected network segment type branch, capture candidate_segments’ count before
consuming it with into_iter(), then use that count to determine whether
candidates existed when matching_segments is empty. Keep candidate_segment_types
solely for constructing the error message and preserve the existing filtering
and return behavior.

1357-1366: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use the shared retains_dynamic_ip() predicate here too.

This branch compares against ExpectedInterfaceIpAllocation::Retained directly, while the sibling path in capture_expected_interface_before_association (line 2067) asks resolved_ip_allocation().retains_dynamic_ip(). Two encodings of one rule will drift the moment another retaining variant appears. Prefer the named predicate in both places.

♻️ Proposed alignment
-        if expected_interface.resolved_ip_allocation() == ExpectedInterfaceIpAllocation::Retained {
+        if expected_interface
+            .resolved_ip_allocation()
+            .retains_dynamic_ip()
+        {
🤖 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 1357 - 1366, Update the
conditional surrounding retain_expected_machine_dhcp_address_for_family in the
shown machine-interface flow to use
resolved_ip_allocation().retains_dynamic_ip() instead of directly comparing with
ExpectedInterfaceIpAllocation::Retained. Align this check with
capture_expected_interface_before_association and preserve the existing
IPv4/IPv6 retention loop.
crates/api-core/src/tests/machine_discovery.rs (1)

85-107: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Consider a short sleep instead of yield_now in the polling loop.

tokio::task::yield_now() reschedules immediately, so this helper issues pg_stat_activity queries as fast as the runtime allows for up to five seconds. A tokio::time::sleep(Duration::from_millis(10)) keeps the same detection latency at a fraction of the database churn, which matters when the suite runs many of these concurrency tests.

♻️ Proposed refactor
-            tokio::task::yield_now().await;
+            tokio::time::sleep(std::time::Duration::from_millis(10)).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/src/tests/machine_discovery.rs` around lines 85 - 107, Update
the polling loop in wait_for_advisory_lock_wait to replace
tokio::task::yield_now() with a 10-millisecond tokio time sleep, preserving the
existing timeout and advisory-lock detection behavior.
🤖 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/expected_switch.rs`:
- Around line 428-450: Update replace_all_expected_switches to reuse the
static-address allocation locking and preallocated-interface materialization
performed by update_expected_switch before inserting replacements through
db_expected_switch::create(). Ensure both bmc_ip_address and nvos_ip_address
allocations are covered, while preserving the existing MAC lock and replacement
flow.
- Around line 244-259: Update replace_all_expected_switches to perform the same
BMC/NVOS static-IP reconciliation as update_expected_switch: lock static
addresses before replacing rows and invoke update_preallocated_machine_interface
for affected interfaces after recreation. Preserve the existing MAC locking and
bulk replacement flow while ensuring fixed-address allocation and
machine-interface state remain synchronized.

In `@crates/api-core/src/tests/expected_switch.rs`:
- Around line 280-319: Update the expected_switch fixture closure to use move
capture, ensuring the referenced nvos_mac value is captured by value and the
closure remains reusable within both tokio::spawn tasks. Keep the existing
fixture fields and call sites unchanged.

In `@crates/api-db/src/machine_interface.rs`:
- Around line 2290-2313: The doc comment for the function returning
ExistingPreallocationResult still describes the removed boolean contract. Update
that comment to document Missing, Unchanged, and Assigned, including the
conditions that produce each outcome, and remove references to Ok(true) or
Ok(false).

---

Nitpick comments:
In `@crates/api-core/src/handlers/expected_machine.rs`:
- Around line 134-138: The doc comment for validate_expected_machine must
describe its use across add, update, replace-all, and batch paths rather than
only insert and add/import flows. Also update the replace-all documentation at
crates/api-core/src/handlers/expected_machine.rs#L376-L377 to describe its
inline parse, validate, release, and recreate process, including immediate
reservation materialization for reconciled replacements and deferred
materialization only for brand-new identities.

In `@crates/api-core/src/tests/expected_switch.rs`:
- Around line 499-514: Strengthen the assertion in the loop testing lock
retention by requiring the `lock_expected_machine_interface_macs` result to be a
`sqlx::Error::Database` whose database error code is PostgreSQL `55P03`
(`lock_not_available`). Reject pool errors and unrelated database errors while
preserving the existing rollback flow.
- Around line 29-83: Consolidate wait_for_expected_switch_mac_lock and
wait_for_expected_switch_write_lock_waiters into one helper parameterized by
expected_waiters, using a count query with the existing blocker and
advisory-lock predicates; update callers so the MAC-lock case passes 1. Replace
tokio::task::yield_now() in the polling loop with a short tokio::time::sleep
interval before retrying, while preserving the five-second timeout and existing
error messages or equivalent context.

In `@crates/api-core/src/tests/machine_dhcp.rs`:
- Around line 96-123: Consolidate the advisory-lock wait helpers into shared
test support: in crates/api-core/src/tests/expected_machine.rs lines 110-162,
promote the pid-attributed helpers for reuse; in
crates/api-core/src/tests/machine_dhcp.rs lines 96-123, remove the
SQL-text/global-count implementation and consume the shared helpers, aligning or
renaming wait_for_advisory_lock_wait to avoid the signature collision. Update
machine_dhcp.rs multi-waiter call sites at lines 2002 and 2012 to pass blocker
or waiter PIDs rather than a bare count; no separate implementation should
remain in machine_dhcp.rs.

In `@crates/api-core/src/tests/machine_discovery.rs`:
- Around line 85-107: Update the polling loop in wait_for_advisory_lock_wait to
replace tokio::task::yield_now() with a 10-millisecond tokio time sleep,
preserving the existing timeout and advisory-lock detection behavior.

In `@crates/api-db/src/machine_interface.rs`:
- Around line 1029-1044: In the expected network segment type branch, capture
candidate_segments’ count before consuming it with into_iter(), then use that
count to determine whether candidates existed when matching_segments is empty.
Keep candidate_segment_types solely for constructing the error message and
preserve the existing filtering and return behavior.
- Around line 1357-1366: Update the conditional surrounding
retain_expected_machine_dhcp_address_for_family in the shown machine-interface
flow to use resolved_ip_allocation().retains_dynamic_ip() instead of directly
comparing with ExpectedInterfaceIpAllocation::Retained. Align this check with
capture_expected_interface_before_association and preserve the existing
IPv4/IPv6 retention loop.

In `@crates/api-db/src/machine.rs`:
- Around line 854-951: Extract the duplicated machine-id normalization, string
binding, NOWAIT query execution, 55P03 error mapping, and locked-set validation
from lock_interface_association_targets and lock_force_deletion_targets into a
small private helper. Parameterize the helper with the lock clause and retry
message, while keeping each public function’s distinct query result handling and
ForceDeletion validation unchanged.

In `@crates/api-db/src/predicted_machine_interface.rs`:
- Around line 124-137: Update the conditional UPDATE in the expected-interface
capture method to inspect its rows_affected result. If no row was updated,
return the persisted interface value instead of unconditionally returning the
caller-supplied declaration; otherwise preserve the current captured-value
return behavior.

In `@crates/site-explorer/src/lib.rs`:
- Around line 2035-2061: Gate the calls to try_reconcile_expected_machine_bmc
and try_reconcile_expected_switch_addresses using the snapshot before invoking
them: skip machines when neither bmc_ip_address nor nvos_ip_address is present,
and skip switches unless bmc_ip_address or nvos_ip_address is present. Preserve
the existing host-NIC fixed_ip guard and reconciliation behavior for entities
that pass these checks.

In `@crates/site-explorer/src/machine_creator.rs`:
- Around line 1283-1298: Update ExpectedMachineIngestionIdentity to store
expected_machine.id as its native Uuid-compatible type instead of converting it
to String in From<&ExpectedMachine>. Preserve the existing optional-ID equality
semantics and derive Copy alongside the existing traits, since all fields are
copyable.
🪄 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: a5873b46-284c-4598-9f1b-194026164972

📥 Commits

Reviewing files that changed from the base of the PR and between 7f3f044 and 13d93cc.

⛔ 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 (56)
  • crates/admin-cli/src/expected_machines/add/args.rs
  • crates/admin-cli/src/expected_machines/common.rs
  • crates/admin-cli/src/expected_machines/patch/args.rs
  • crates/admin-cli/src/expected_machines/tests.rs
  • crates/admin-cli/src/expected_machines/update/mod.rs
  • crates/admin-cli/src/rpc.rs
  • crates/api-core/src/dhcp/discover.rs
  • crates/api-core/src/dhcp/v6.rs
  • crates/api-core/src/handlers/bmc_endpoint_explorer.rs
  • crates/api-core/src/handlers/expected_machine.rs
  • crates/api-core/src/handlers/expected_power_shelf.rs
  • crates/api-core/src/handlers/expected_switch.rs
  • crates/api-core/src/handlers/machine.rs
  • crates/api-core/src/handlers/machine_discovery.rs
  • crates/api-core/src/handlers/machine_interface.rs
  • crates/api-core/src/handlers/machine_interface_address.rs
  • crates/api-core/src/handlers/power_shelf.rs
  • crates/api-core/src/handlers/switch.rs
  • crates/api-core/src/tests/expected_machine.rs
  • crates/api-core/src/tests/expected_switch.rs
  • crates/api-core/src/tests/machine_admin_force_delete.rs
  • crates/api-core/src/tests/machine_dhcp.rs
  • crates/api-core/src/tests/machine_discovery.rs
  • crates/api-core/src/tests/switch.rs
  • crates/api-core/tests/integration/expected_power_shelf_static_address.rs
  • crates/api-core/tests/integration/machine_boot_interfaces.rs
  • crates/api-core/tests/integration/power_shelf_delete.rs
  • crates/api-core/tests/integration/static_address_management.rs
  • crates/api-db/migrations/20260722120000_expected_interface_allocation_state.sql
  • crates/api-db/src/bmc_metadata.rs
  • crates/api-db/src/bmc_metadata/tests.rs
  • crates/api-db/src/expected_machine.rs
  • crates/api-db/src/expected_machine/test_allocation_state_migration.rs
  • crates/api-db/src/expected_machine/tests.rs
  • crates/api-db/src/expected_switch.rs
  • crates/api-db/src/expected_switch/tests.rs
  • crates/api-db/src/machine.rs
  • crates/api-db/src/machine_interface.rs
  • crates/api-db/src/machine_interface/tests.rs
  • crates/api-db/src/machine_interface_address.rs
  • crates/api-db/src/network_segment.rs
  • crates/api-db/src/predicted_machine_interface.rs
  • crates/api-model/src/expected_machine.rs
  • crates/api-model/src/machine/mod.rs
  • crates/api-model/src/predicted_machine_interface.rs
  • crates/machine-a-tron/src/machine_a_tron.rs
  • crates/machine-controller/src/boot_interface.rs
  • crates/rpc/build.rs
  • crates/rpc/proto/forge.proto
  • crates/rpc/src/lib.rs
  • crates/rpc/src/model/expected_machine.rs
  • crates/site-explorer/src/lib.rs
  • crates/site-explorer/src/machine_creator.rs
  • crates/site-explorer/tests/integration/machine_creator.rs
  • crates/site-explorer/tests/integration/reconcile.rs
  • rest-api/proto/core/src/v1/nico_nico.proto

Comment thread crates/api-core/src/handlers/expected_switch.rs
Comment thread crates/api-core/src/handlers/expected_switch.rs Outdated
Comment thread crates/api-core/src/tests/expected_switch.rs
Comment thread crates/api-db/src/machine_interface.rs

chet commented Jul 25, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai full_review, thanks!

@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

@chet: Initiating a complete re-review of #3991, including the full diff and relevant integration paths.

🐇🔍

✅ Action performed

Full review finished.

@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: 5

🧹 Nitpick comments (13)
crates/api-db/src/bmc_metadata.rs (1)

227-232: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Prefer % over ? for a Display value.

MacAddress implements Display; ? emits Debug formatting, which is noisier in logfmt output.

♻️ Proposed tweak
-                mac_address = ?bmc_interface_owner.mac_address,
+                mac_address = %bmc_interface_owner.mac_address,

As per coding guidelines: "record dynamic values as structured fields and use native shorthand, % for Display, and ? for Debug."

🤖 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/bmc_metadata.rs` around lines 227 - 232, Update the
mac_address structured field in the tracing::info! call within the BMC
enrichment flow to use % formatting instead of ? because MacAddress implements
Display; leave the other fields and message unchanged.

Source: Coding guidelines

crates/api-core/src/tests/expected_machine.rs (1)

175-182: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Prefer the typed lock helper over hand-rolled advisory-lock key SQL.

Three tests reproduce the hashtextextended('expected_machine_interface.' || $1::text, 0) key derivation inline (here, Lines 234-241, and Lines 393-400), while test_update_and_add_serialize_identity_changes already uses db::machine_interface::lock_expected_machine_interface_macs at Line 333. Duplicating the formula couples these tests to a private DB implementation detail: if the key derivation ever changes, the blocker grabs an unrelated key and the tests degrade into timeouts rather than meaningful assertions. Using the helper uniformly keeps the intent and the production key in one place.

♻️ Suggested change
-    sqlx::query(
-        "SELECT pg_advisory_xact_lock(
-            hashtextextended('expected_machine_interface.' || $1::text, 0)
-        )",
-    )
-    .bind(interface_mac)
-    .execute(&mut *blocker)
-    .await?;
+    db::machine_interface::lock_expected_machine_interface_macs(&mut blocker, [interface_mac])
+        .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/src/tests/expected_machine.rs` around lines 175 - 182,
Replace the inline pg_advisory_xact_lock SQL in the affected expected-machine
tests with the existing
db::machine_interface::lock_expected_machine_interface_macs helper, matching the
usage in test_update_and_add_serialize_identity_changes. Pass the relevant
interface MAC and blocker transaction through the helper so all three tests use
the shared production lock-key derivation.
crates/rpc/src/lib.rs (1)

580-697: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Collapse the duplicate enum adapters. ExpectedInterfaceRole and ExpectedInterfaceIpAllocation each repeat the same untagged input enum plus optional serialize/deserialize helpers; a small declarative macro would centralize the shared flow and reduce drift. Also make the Error::custom calls explicit on both paths for consistency.

🤖 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/rpc/src/lib.rs` around lines 580 - 697, Consolidate the duplicated
ExpectedInterfaceRole and ExpectedInterfaceIpAllocation adapters into a small
declarative macro that generates their untagged input type and optional
serialize_optional/deserialize_optional helpers, while preserving each enum’s
accepted names, aliases, and output strings. Ensure the generated numeric and
serialization error conversions use explicit serde::de::Error::custom and
serde::ser::Error::custom calls consistently.
crates/api-db/src/machine_interface.rs (1)

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

Remove the unused newly_created flag. Every caller here drops it, including the DPU path, so the widened tuple return is dead API surface. Keep the creation-state logic local if it is only needed for future logging or metrics.

🤖 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` at line 703, Remove the unused
newly_created value from the tuple produced by the relevant mapping flow around
the interface extraction, changing the return type to expose only interface.
Update all callers, including the DPU path, to consume the narrowed result while
keeping any creation-state tracking local where needed for future logging or
metrics.
crates/site-explorer/src/machine_creator.rs (2)

1568-1590: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Replace the yield_now() busy-spin with a short sleep.

The loop re-issues a pg_stat_activity query as fast as the runtime will schedule it, burning CPU and a pool connection for up to five seconds. A small sleep gives the waiting transaction time to register without the spin.

♻️ Proposed refactor
                 if waiting > 0 {
                     return;
                 }
-                tokio::task::yield_now().await;
+                tokio::time::sleep(std::time::Duration::from_millis(10)).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/site-explorer/src/machine_creator.rs` around lines 1568 - 1590, Update
wait_for_advisory_lock_wait so the polling loop uses a short Tokio sleep instead
of tokio::task::yield_now(). Preserve the existing query, timeout, and immediate
return once waiting is greater than zero.

1520-1522: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Sort MAC addresses directly. sort_by_key(ToString::to_string) allocates a String per entry and ties the ordering to the display format. MacAddress already orders by its raw bytes, so sort_unstable() (or sort_unstable_by_key(|mac| mac.bytes())) is clearer and cheaper.

🤖 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/site-explorer/src/machine_creator.rs` around lines 1520 - 1522, Update
the MAC address sorting in the visible collection-building flow to use
MacAddress’s native ordering via sort_unstable() (or its raw-byte key), instead
of sort_by_key(ToString::to_string). Keep the subsequent deduplication and
return behavior unchanged.
crates/api-core/src/tests/machine_discovery.rs (1)

566-572: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Prefer panic! over synthesising an io::Error for an unmet test expectation.

Wrapping the message in std::io::Error::other then boxing it obscures a plain assertion failure and produces a less legible report. The same let-else shape elsewhere in this PR (crates/api-db/src/machine_interface/tests.rs) uses panic! directly.

♻️ Suggested adjustment
     let [host_interface] = host_interfaces.as_slice() else {
-        return Err(std::io::Error::other(format!(
-            "expected one proactive host interface, got {}",
-            host_interfaces.len()
-        ))
-        .into());
+        panic!(
+            "expected one proactive host interface, got {}",
+            host_interfaces.len()
+        );
     };
🤖 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_discovery.rs` around lines 566 - 572,
Replace the io::Error construction in the host_interfaces let-else branch with a
direct panic! carrying the existing expectation message. Keep the successful
single-interface destructuring unchanged and remove the unnecessary error
conversion and boxing.
crates/api-core/src/tests/switch.rs (1)

712-721: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Two new tests leave their final transaction to the implicit drop. Every other verification block added in this PR ends with an explicit txn.rollback().await? (for example crates/api-core/tests/integration/power_shelf_delete.rs). Relying on drop is functionally equivalent under sqlx but breaks the local convention and hides the intent to discard.

  • crates/api-core/src/tests/switch.rs#L712-L721: add txn.rollback().await?; after the final address assertion, before Ok(()).
  • crates/api-db/src/machine_interface/tests.rs#L1708-L1717: add txn.rollback().await?; after the InvalidArgument assertion, matching the explicit commit/rollback calls in the earlier blocks of the same 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/tests/switch.rs` around lines 712 - 721, Add explicit
transaction rollback before returning from the verification block in switch.rs,
after the final matching-address assertion. Also add the same rollback after the
InvalidArgument assertion in machine_interface/tests.rs; both sites should call
the existing txn rollback method before Ok(()) to match the surrounding
transaction convention.
crates/api-db/src/machine_interface/tests.rs (1)

154-187: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Three overlapping segment builders now coexist in this module.

create_static_assignments_segment, create_test_segment, and the new create_managed_segment all wrap network_segment::persist with a NewNetworkSegment literal, differing only in name, prefix list, segment type, and reserved count. A single builder taking those four inputs — with thin named wrappers where call sites benefit from the intent — would keep the fixture surface from growing further as more segment shapes are needed.

🤖 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/tests.rs` around lines 154 - 187,
Consolidate create_static_assignments_segment, create_test_segment, and
create_managed_segment around one shared network-segment builder accepting name,
prefix list, segment type, and reserved count. Preserve each helper’s existing
fixture behavior through thin intent-specific wrappers where needed, and update
their call sites to use the shared implementation without duplicating the
NewNetworkSegment construction.
crates/api-db/src/machine_interface_address.rs (1)

184-234: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

retain_dhcp_address_for_family and its ExpectedMachine twin differ by one assignment.

Two functions carry the same predicate, the same four bindings, and the same idempotency contract; only the expected_machine_preallocation = true clause differs. Consider a single private implementation parameterised by provenance, with the two public names as thin wrappers, so the predicate cannot drift between them.

♻️ Suggested consolidation
+async fn retain_dhcp_address_for_family_inner(
+    txn: &mut PgConnection,
+    interface_id: MachineInterfaceId,
+    family: IpAddressFamily,
+    expected_machine_preallocation: bool,
+) -> Result<bool, DatabaseError> {
+    let query = "UPDATE machine_interface_addresses
+                 SET allocation_type = $3,
+                     expected_machine_preallocation = $5
+                 WHERE interface_id = $1
+                   AND family(address) = $2
+                   AND allocation_type = $4";
+    sqlx::query(query)
+        .bind(interface_id)
+        .bind(family.pg_family())
+        .bind(AllocationType::Static)
+        .bind(AllocationType::Dhcp)
+        .bind(expected_machine_preallocation)
+        .execute(txn)
+        .await
+        .map(|result| result.rows_affected() > 0)
+        .map_err(|error| DatabaseError::query(query, error))
+}

Note that this changes the non-ExpectedMachine path to write false explicitly rather than leaving the column untouched; confirm that is acceptable before adopting, otherwise keep the two statements and extract only the shared predicate as a constant.

🤖 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_address.rs` around lines 184 - 234,
Consolidate retain_dhcp_address_for_family and
retain_expected_machine_dhcp_address_for_family through one private helper
parameterized by provenance, keeping both public functions as thin wrappers.
Reuse the shared predicate, bindings, execution, and result handling; have the
helper explicitly set expected_machine_preallocation according to the parameter,
and verify that writing false for the non-ExpectedMachine path is acceptable
before applying it.
crates/api-core/src/handlers/machine.rs (2)

383-393: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

The dpu_machines.len() != 1 guard is dead in this branch.

This branch is only reached when host_machine is None, and in that case dpu_machines was constructed as vec![machine]. The meaningful check is the find_host_by_dpu_machine_id re-read. Retaining the length test is defensible as defence against future refactoring of the branch above, but a one-line comment stating that intent would spare the next reader the same trace.

🤖 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.rs` around lines 383 - 393, The
dpu_machines.len() != 1 check in the force-deletion validation is defensive
rather than meaningful for the current host_machine-none branch. Add a concise
comment immediately above that condition explaining it protects against future
refactoring, while preserving the existing find_host_by_dpu_machine_id re-read
and behavior.

341-357: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

The ok_or_else arm here is unreachable.

interface_ids is collected from prepared_interfaces immediately above and nothing mutates the map in between, so remove always returns Some. The FailedPrecondition message therefore documents a state that cannot occur, which is misleading next to the two genuinely reachable checks at Lines 809 and 884. Consider draining directly.

♻️ Suggested simplification
-    for interface_id in interface_ids {
-        let prepared = prepared_interfaces.remove(&interface_id).ok_or_else(|| {
-            CarbideError::FailedPrecondition(format!(
-                "machine interface {interface_id} changed while preparing force deletion; retry the request",
-            ))
-        })?;
-        db::machine_interface::delete_prepared(prepared, txn.as_pgconn()).await?;
-    }
+    for interface_id in interface_ids {
+        let Some(prepared) = prepared_interfaces.remove(&interface_id) else {
+            continue;
+        };
+        db::machine_interface::delete_prepared(prepared, txn.as_pgconn()).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/src/handlers/machine.rs` around lines 341 - 357, Update the
interface deletion loop around prepared_interfaces and interface_ids to drain or
otherwise remove the matching entries directly, eliminating the unreachable
ok_or_else FailedPrecondition branch. Preserve the existing filtering for
non-BMC interfaces belonging to machine_id and continue passing each prepared
interface to db::machine_interface::delete_prepared.
crates/api-core/src/tests/machine_dhcp.rs (1)

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

The advisory-lock wait helper is now copied into three modules and the copies have already diverged. All three poll pg_stat_activity for pg_advisory_xact_lock waiters under a five-second timeout, but they differ in wait strategy (yield_now versus a 10 ms sleep), in parameterisation (waiter count versus a fixed one), and in failure message. Extracting one shared fixture would keep the synchronisation semantics — the linchpin of every new concurrency test in this PR — defined in a single place.

  • crates/api-core/src/tests/machine_dhcp.rs#L96-L122: promote this parameterised version (waiter count plus the sleep-based backoff) into a shared test helper and re-export it; drop the local definition.
  • crates/api-core/src/tests/machine_discovery.rs#L85-L107: delete the local copy and call the shared helper.
  • crates/api-db/src/machine_interface/tests.rs#L235-L257: delete the local copy and call the shared helper. Note this crate is api-db, so the fixture likely belongs in a shared test-support crate rather than in api-core.
🤖 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_dhcp.rs` around lines 96 - 122, Centralize
the parameterized advisory-lock waiter helper using the implementation in
crates/api-core/src/tests/machine_dhcp.rs:96-122 as the basis, changing it to
use the 10 ms sleep backoff, then re-export it from shared test support. Remove
the local definition and call the shared helper in
crates/api-core/src/tests/machine_dhcp.rs:96-122,
crates/api-core/src/tests/machine_discovery.rs:85-107, and
crates/api-db/src/machine_interface/tests.rs:235-257; place the fixture in a
shared test-support crate accessible to both api-core and api-db, while
preserving the waiter-count parameter and five-second timeout.
🤖 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/migrations/20260722120000_expected_interface_allocation_state.sql`:
- Around line 13-40: Update the declarations CTE and its downstream filtering to
exclude MAC addresses matching expected_machines.bmc_mac_address, so overlapping
host NIC/BMC entries are not backfilled into predicted_machine_interfaces;
preserve the existing duplicate nested-declaration exclusion and update the
lookup logic using the existing expected_machines symbol.

In `@crates/api-db/src/bmc_metadata.rs`:
- Around line 155-199: In the BMC-IP resolution branch of the interface
selection logic, replace find_interface_by_bmc_ip with
find_interface_by_bmc_ip_for_update so the relevant machine interface rows are
locked before capture_expected_interface_before_association and
associate_bmc_interface execute. Preserve the existing not-found handling and
tuple construction.

In `@crates/api-db/src/expected_switch.rs`:
- Around line 49-64: In the ExpectedSwitch ambiguity handling, replace the
DatabaseError::InvalidArgument classification with DatabaseError::internal,
matching find_interface_by_bmc_ip_with_query for multiple owners while
preserving the existing error message and branch behavior.

In `@crates/api-db/src/machine_interface.rs`:
- Around line 5068-5079: Remove the unnecessary .bind(interface_id) call from
record_machine_interface_deletion, since its UPDATE query has no placeholders.
Keep the existing execution and error mapping unchanged.

In `@crates/site-explorer/tests/integration/machine_creator.rs`:
- Around line 1001-1012: Check the result returned by the UPDATE query in the
stale-policy fixture setup and assert that exactly one row was affected before
committing the transaction. Update the query flow around the machine_interfaces
update, preserving the existing bindings and commit behavior while making a
missing target row fail the test.

---

Nitpick comments:
In `@crates/api-core/src/handlers/machine.rs`:
- Around line 383-393: The dpu_machines.len() != 1 check in the force-deletion
validation is defensive rather than meaningful for the current host_machine-none
branch. Add a concise comment immediately above that condition explaining it
protects against future refactoring, while preserving the existing
find_host_by_dpu_machine_id re-read and behavior.
- Around line 341-357: Update the interface deletion loop around
prepared_interfaces and interface_ids to drain or otherwise remove the matching
entries directly, eliminating the unreachable ok_or_else FailedPrecondition
branch. Preserve the existing filtering for non-BMC interfaces belonging to
machine_id and continue passing each prepared interface to
db::machine_interface::delete_prepared.

In `@crates/api-core/src/tests/expected_machine.rs`:
- Around line 175-182: Replace the inline pg_advisory_xact_lock SQL in the
affected expected-machine tests with the existing
db::machine_interface::lock_expected_machine_interface_macs helper, matching the
usage in test_update_and_add_serialize_identity_changes. Pass the relevant
interface MAC and blocker transaction through the helper so all three tests use
the shared production lock-key derivation.

In `@crates/api-core/src/tests/machine_dhcp.rs`:
- Around line 96-122: Centralize the parameterized advisory-lock waiter helper
using the implementation in crates/api-core/src/tests/machine_dhcp.rs:96-122 as
the basis, changing it to use the 10 ms sleep backoff, then re-export it from
shared test support. Remove the local definition and call the shared helper in
crates/api-core/src/tests/machine_dhcp.rs:96-122,
crates/api-core/src/tests/machine_discovery.rs:85-107, and
crates/api-db/src/machine_interface/tests.rs:235-257; place the fixture in a
shared test-support crate accessible to both api-core and api-db, while
preserving the waiter-count parameter and five-second timeout.

In `@crates/api-core/src/tests/machine_discovery.rs`:
- Around line 566-572: Replace the io::Error construction in the host_interfaces
let-else branch with a direct panic! carrying the existing expectation message.
Keep the successful single-interface destructuring unchanged and remove the
unnecessary error conversion and boxing.

In `@crates/api-core/src/tests/switch.rs`:
- Around line 712-721: Add explicit transaction rollback before returning from
the verification block in switch.rs, after the final matching-address assertion.
Also add the same rollback after the InvalidArgument assertion in
machine_interface/tests.rs; both sites should call the existing txn rollback
method before Ok(()) to match the surrounding transaction convention.

In `@crates/api-db/src/bmc_metadata.rs`:
- Around line 227-232: Update the mac_address structured field in the
tracing::info! call within the BMC enrichment flow to use % formatting instead
of ? because MacAddress implements Display; leave the other fields and message
unchanged.

In `@crates/api-db/src/machine_interface_address.rs`:
- Around line 184-234: Consolidate retain_dhcp_address_for_family and
retain_expected_machine_dhcp_address_for_family through one private helper
parameterized by provenance, keeping both public functions as thin wrappers.
Reuse the shared predicate, bindings, execution, and result handling; have the
helper explicitly set expected_machine_preallocation according to the parameter,
and verify that writing false for the non-ExpectedMachine path is acceptable
before applying it.

In `@crates/api-db/src/machine_interface.rs`:
- Line 703: Remove the unused newly_created value from the tuple produced by the
relevant mapping flow around the interface extraction, changing the return type
to expose only interface. Update all callers, including the DPU path, to consume
the narrowed result while keeping any creation-state tracking local where needed
for future logging or metrics.

In `@crates/api-db/src/machine_interface/tests.rs`:
- Around line 154-187: Consolidate create_static_assignments_segment,
create_test_segment, and create_managed_segment around one shared
network-segment builder accepting name, prefix list, segment type, and reserved
count. Preserve each helper’s existing fixture behavior through thin
intent-specific wrappers where needed, and update their call sites to use the
shared implementation without duplicating the NewNetworkSegment construction.

In `@crates/rpc/src/lib.rs`:
- Around line 580-697: Consolidate the duplicated ExpectedInterfaceRole and
ExpectedInterfaceIpAllocation adapters into a small declarative macro that
generates their untagged input type and optional
serialize_optional/deserialize_optional helpers, while preserving each enum’s
accepted names, aliases, and output strings. Ensure the generated numeric and
serialization error conversions use explicit serde::de::Error::custom and
serde::ser::Error::custom calls consistently.

In `@crates/site-explorer/src/machine_creator.rs`:
- Around line 1568-1590: Update wait_for_advisory_lock_wait so the polling loop
uses a short Tokio sleep instead of tokio::task::yield_now(). Preserve the
existing query, timeout, and immediate return once waiting is greater than zero.
- Around line 1520-1522: Update the MAC address sorting in the visible
collection-building flow to use MacAddress’s native ordering via sort_unstable()
(or its raw-byte key), instead of sort_by_key(ToString::to_string). Keep the
subsequent deduplication and return behavior unchanged.
🪄 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: e905561f-929c-47df-b103-519afc7745c7

📥 Commits

Reviewing files that changed from the base of the PR and between 7f3f044 and c0906a0.

⛔ 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 (58)
  • crates/admin-cli/src/expected_machines/add/args.rs
  • crates/admin-cli/src/expected_machines/common.rs
  • crates/admin-cli/src/expected_machines/patch/args.rs
  • crates/admin-cli/src/expected_machines/tests.rs
  • crates/admin-cli/src/expected_machines/update/mod.rs
  • crates/admin-cli/src/rpc.rs
  • crates/api-core/src/dhcp/discover.rs
  • crates/api-core/src/dhcp/v6.rs
  • crates/api-core/src/handlers/bmc_endpoint_explorer.rs
  • crates/api-core/src/handlers/expected_machine.rs
  • crates/api-core/src/handlers/expected_power_shelf.rs
  • crates/api-core/src/handlers/expected_switch.rs
  • crates/api-core/src/handlers/machine.rs
  • crates/api-core/src/handlers/machine_discovery.rs
  • crates/api-core/src/handlers/machine_interface.rs
  • crates/api-core/src/handlers/machine_interface_address.rs
  • crates/api-core/src/handlers/power_shelf.rs
  • crates/api-core/src/handlers/switch.rs
  • crates/api-core/src/tests/expected_machine.rs
  • crates/api-core/src/tests/expected_switch.rs
  • crates/api-core/src/tests/machine_admin_force_delete.rs
  • crates/api-core/src/tests/machine_dhcp.rs
  • crates/api-core/src/tests/machine_discovery.rs
  • crates/api-core/src/tests/machine_states.rs
  • crates/api-core/src/tests/switch.rs
  • crates/api-core/tests/integration/expected_power_shelf_static_address.rs
  • crates/api-core/tests/integration/machine_boot_interfaces.rs
  • crates/api-core/tests/integration/power_shelf_delete.rs
  • crates/api-core/tests/integration/static_address_management.rs
  • crates/api-db/migrations/20260722120000_expected_interface_allocation_state.sql
  • crates/api-db/src/bmc_metadata.rs
  • crates/api-db/src/bmc_metadata/tests.rs
  • crates/api-db/src/expected_machine.rs
  • crates/api-db/src/expected_machine/test_allocation_state_migration.rs
  • crates/api-db/src/expected_machine/tests.rs
  • crates/api-db/src/expected_switch.rs
  • crates/api-db/src/expected_switch/tests.rs
  • crates/api-db/src/machine.rs
  • crates/api-db/src/machine_interface.rs
  • crates/api-db/src/machine_interface/tests.rs
  • crates/api-db/src/machine_interface_address.rs
  • crates/api-db/src/network_segment.rs
  • crates/api-db/src/predicted_machine_interface.rs
  • crates/api-model/src/expected_machine.rs
  • crates/api-model/src/machine/mod.rs
  • crates/api-model/src/predicted_machine_interface.rs
  • crates/machine-a-tron/src/machine_a_tron.rs
  • crates/machine-controller/src/boot_interface.rs
  • crates/rpc/build.rs
  • crates/rpc/proto/forge.proto
  • crates/rpc/src/lib.rs
  • crates/rpc/src/model/expected_machine.rs
  • crates/site-explorer/Cargo.toml
  • crates/site-explorer/src/lib.rs
  • crates/site-explorer/src/machine_creator.rs
  • crates/site-explorer/tests/integration/machine_creator.rs
  • crates/site-explorer/tests/integration/reconcile.rs
  • rest-api/proto/core/src/v1/nico_nico.proto

Comment thread crates/api-db/src/bmc_metadata.rs Outdated
Comment thread crates/api-db/src/expected_switch.rs
Comment thread crates/api-db/src/machine_interface.rs Outdated
Comment thread crates/site-explorer/tests/integration/machine_creator.rs

chet commented Jul 25, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai full_review, thanks!

@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

@chet: Initiating a complete re-review of #3991, including the full diff and relevant integration paths.

🐇🔍

✅ Action performed

Full review finished.

@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: 3

🧹 Nitpick comments (9)
crates/api-core/src/dhcp/discover.rs (1)

626-636: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Collapse the redundant rebinding into the original let.

Line 636 exists only to acquire mutability that the if/else at 626-630 could have declared directly. Threading the binding through a bare let mut x = x; obscures ownership for no benefit.

♻️ Proposed simplification
-    let predicted_interface = if prediction_needs_refresh {
+    let mut predicted_interface = if prediction_needs_refresh {
         db::predicted_machine_interface::find_by_mac_address(&mut txn, parsed_mac).await?
     } else {
         predicted_interface
     };
     let is_primary_nic = host_nic
         .as_ref()
         .filter(|interface| interface.role == ExpectedInterfaceRole::Host)
         .map(ExpectedHostNic::initial_primary_interface);
-
-    let mut predicted_interface = predicted_interface;
🤖 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/discover.rs` around lines 626 - 636, Make the
original predicted_interface binding mutable in the if/else assignment, then
remove the redundant `let mut predicted_interface = predicted_interface;`
rebinding. Preserve the existing refresh and reuse behavior while updating only
the binding declaration near the prediction logic.
crates/api-db/src/expected_machine/test_allocation_state_migration.rs (1)

166-210: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider folding the three prediction snapshot lookups into a table-driven check.

Lines 166-210 repeat the same query three times, differing only in the bound MAC and the expected (snapshot, captured) pair. A single check_values/check_cases table keyed by MAC would remove the duplication and make the classification matrix (claimed / unclaimed-duplicate / unclaimed-BMC-collision) readable at a glance.

As per coding guidelines, "Use table-driven tests for functions mapping inputs to outputs or errors."

🤖 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/expected_machine/test_allocation_state_migration.rs` around
lines 166 - 210, Replace the three repeated `sqlx::query_as` assertions in the
migration test with a table-driven `check_cases` collection containing each MAC
and its expected `(Option<Value>, bool)` result, then iterate through it to
execute the shared query and assertions. Preserve the existing expected host
snapshot, duplicate, and BMC-collision values while making the classification
matrix explicit.

Source: Coding guidelines

crates/admin-cli/src/expected_machines/common.rs (1)

139-140: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Document the tri-state contract of the new optional host_nics field at its definition. Making this field optional introduced three distinct meanings — omitted, explicitly empty, explicitly populated — that are recorded nowhere, forcing every reader of a consuming call site to trace the Option back through patch_expected_machine and replace_all_expected_machines to recover the intent.

  • crates/admin-cli/src/expected_machines/common.rs#L139-L140: add a doc comment on host_nics stating that None (omitted) preserves the stored list on update, Some(vec![]) clears it, and replace-all collapses None to an empty list.
  • crates/admin-cli/src/expected_machines/update/mod.rs#L41-L45: add a brief note that an omitted host_nics deliberately preserves the server value, so this command's "result matches the file" claim does not hold for that field.
🤖 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/expected_machines/common.rs` around lines 139 - 140,
Document the tri-state behavior of the optional host_nics field at its
definition: None means omitted and preserves the stored list on update,
Some(empty) clears it, and replace-all converts None to an empty list. Also add
a brief note in update/mod.rs near the command’s result-matches-file claim
explaining that omitted host_nics intentionally preserves the server value, so
that claim excludes this field; update both
crates/admin-cli/src/expected_machines/common.rs:139-140 and
crates/admin-cli/src/expected_machines/update/mod.rs:41-45.
crates/rpc/src/lib.rs (1)

580-697: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider collapsing the two adapters into one macro.

The role and IP-allocation adapters are structurally identical — untagged input enum, Option short-circuit, alias normalization, canonical string emission — differing only in variant names and aliases. A small declarative macro would keep future enums (and future variants) from drifting between the two copies. Entirely optional if the team prefers the explicit form.

🤖 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/rpc/src/lib.rs` around lines 580 - 697, Optionally replace the
duplicated ExpectedInterfaceRole and ExpectedInterfaceIpAllocation adapter
implementations with a small declarative macro that generates their input enum
and serialize_optional/deserialize_optional methods. Parameterize the macro with
the enum type, input name, variants, canonical names, and accepted aliases,
while preserving numeric inputs, null handling, unknown-value errors, and
canonical string serialization.
crates/api-core/src/handlers/machine.rs (1)

758-801: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Preparation covers interfaces the request may never delete.

affected_interface_ids is assembled unconditionally, so when delete_interfaces is false the batch still takes exclusive segment and row locks on every associated interface and then discards the prepared handles. Gating the association-derived ids on request.delete_interfaces (BMC ids already are gated) would narrow the lock footprint of force-delete without changing behavior.

🤖 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.rs` around lines 758 - 801, Only add
machine-associated interface IDs to affected_interface_ids when
request.delete_interfaces is enabled, while preserving the existing
BMC-interface gating and preparation flow. Update the association lookup near
prepare_deletes_with_exclusive_segments so no interface locks or prepared
handles are created when interface deletion is disabled.
crates/site-explorer/tests/integration/machine_creator.rs (1)

450-461: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Drop the redundant .clone() by capturing the persisted record.

db::expected_machine::create returns the persisted ExpectedMachine (confirmed by the sibling usage in reconcile.rs: let mut expected_machine = db::expected_machine::create(&mut txn, ...).await?;). Cloning here just to keep a copy for the fixture is unnecessary.

♻️ Proposed fix
-    let expected_machine = expected_machine(managed_host);
     let mut txn = env.pool.begin().await.unwrap();
-    db::expected_machine::create(&mut txn, expected_machine.clone())
-        .await
-        .unwrap();
+    let expected_machine = db::expected_machine::create(&mut txn, expected_machine(managed_host))
+        .await
+        .unwrap();
     txn.commit().await.unwrap();

As per coding guidelines, "Avoid needless .clone() calls; borrow values, consume iterators with into_iter(), reorder struct initialization when possible, or use Cow where values may be borrowed or owned."

🤖 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/site-explorer/tests/integration/machine_creator.rs` around lines 450 -
461, Update the fixture setup around db::expected_machine::create to capture its
returned persisted ExpectedMachine in expected_machine, rather than cloning the
input before the call. Preserve using that returned value in ExploredHostFixture
after the transaction commits.

Source: Coding guidelines

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

235-257: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

The lock-wait probe busy-polls the database.

yield_now() gives no back-off, so this loop issues pg_stat_activity counts as fast as the runtime will schedule it for up to five seconds — measurable CPU and connection churn in CI for every test that uses it. A short sleep keeps the intent identical at a fraction of the cost.

♻️ Proposed back-off
             if waiting > 0 {
                 return;
             }
-            tokio::task::yield_now().await;
+            tokio::time::sleep(std::time::Duration::from_millis(25)).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-db/src/machine_interface/tests.rs` around lines 235 - 257, Update
wait_for_advisory_lock_wait to replace the tight-loop tokio::task::yield_now()
polling with a short tokio sleep between pg_stat_activity checks, while
preserving the five-second timeout and immediate return once waiting is greater
than zero.
crates/api-db/src/machine_interface.rs (2)

1615-1666: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider collapsing the duplicated snapshot SQL.

The for_update and read-only variants differ only by the trailing FOR UPDATE; the sizeable projection (including the three-clause associated expression) is copied verbatim. A concat! of a shared body with the optional suffix keeps the two definitions from drifting apart on the next projection change.

🤖 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 1615 - 1666, Refactor
expected_interface_snapshot so the shared SELECT projection and WHERE clause are
defined once, with the FOR UPDATE suffix appended only when for_update is true.
Preserve the existing query behavior and DatabaseError handling while removing
the duplicated SQL definitions.

5163-5183: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

One query per machine where a single array bind would do.

find_ids_by_machine_associations issues N round trips for N machine ids. WHERE machine_id = ANY($1) OR attached_dpu_machine_id = ANY($1) collapses this into one statement and removes the manual sort/dedup of the concatenated results (Postgres ORDER BY id already yields them ordered). Worth doing if force-cleanup can ever be handed a sizeable machine set.

🤖 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 5163 - 5183, Update
find_ids_by_machine_associations to execute one SQL query for all machine_ids,
binding the slice as an array and filtering with ANY for both association
columns. Preserve ORDER BY id, remove the per-machine loop and manual
sort/dedup, and return the single query’s results directly.
🤖 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/expected_machine.rs`:
- Around line 1492-1548: Wrap the fallible work in the BatchOperation::Create
arm of the operation_result match in an async block and await it, so conversion,
validation, locking, and creation errors are returned through operation_result.
Preserve the existing Create behavior while ensuring failures reach the shared
rollback_or_log("expected-machine atomic batch failure") path, matching
BatchOperation::Update.

In `@crates/api-core/src/handlers/expected_switch.rs`:
- Around line 538-580: The expected-switch replacement flow must release static
reservations belonging to switches removed from the configuration before
db_expected_switch::clear(). Identify removed switches by comparing previous
with replacements, then mirror the release logic used by the expected-machine
removal path for their materialized bmc_ip_address and nvos_ip_address
reservations, preserving existing reconciliation for retained or newly added
switches.

In `@crates/api-db/src/machine_interface.rs`:
- Around line 4454-4458: Update load_and_lock_all_admin_segments so
lock_network_segments_exclusive acquires the segment locks before
load_prelocked_admin_segments reads the rows. Keep the same segment_ids,
transaction connection, error propagation, and return value, ensuring validation
and snapshot loading occur only after the exclusive locks are held.

---

Nitpick comments:
In `@crates/admin-cli/src/expected_machines/common.rs`:
- Around line 139-140: Document the tri-state behavior of the optional host_nics
field at its definition: None means omitted and preserves the stored list on
update, Some(empty) clears it, and replace-all converts None to an empty list.
Also add a brief note in update/mod.rs near the command’s result-matches-file
claim explaining that omitted host_nics intentionally preserves the server
value, so that claim excludes this field; update both
crates/admin-cli/src/expected_machines/common.rs:139-140 and
crates/admin-cli/src/expected_machines/update/mod.rs:41-45.

In `@crates/api-core/src/dhcp/discover.rs`:
- Around line 626-636: Make the original predicted_interface binding mutable in
the if/else assignment, then remove the redundant `let mut predicted_interface =
predicted_interface;` rebinding. Preserve the existing refresh and reuse
behavior while updating only the binding declaration near the prediction logic.

In `@crates/api-core/src/handlers/machine.rs`:
- Around line 758-801: Only add machine-associated interface IDs to
affected_interface_ids when request.delete_interfaces is enabled, while
preserving the existing BMC-interface gating and preparation flow. Update the
association lookup near prepare_deletes_with_exclusive_segments so no interface
locks or prepared handles are created when interface deletion is disabled.

In `@crates/api-db/src/expected_machine/test_allocation_state_migration.rs`:
- Around line 166-210: Replace the three repeated `sqlx::query_as` assertions in
the migration test with a table-driven `check_cases` collection containing each
MAC and its expected `(Option<Value>, bool)` result, then iterate through it to
execute the shared query and assertions. Preserve the existing expected host
snapshot, duplicate, and BMC-collision values while making the classification
matrix explicit.

In `@crates/api-db/src/machine_interface.rs`:
- Around line 1615-1666: Refactor expected_interface_snapshot so the shared
SELECT projection and WHERE clause are defined once, with the FOR UPDATE suffix
appended only when for_update is true. Preserve the existing query behavior and
DatabaseError handling while removing the duplicated SQL definitions.
- Around line 5163-5183: Update find_ids_by_machine_associations to execute one
SQL query for all machine_ids, binding the slice as an array and filtering with
ANY for both association columns. Preserve ORDER BY id, remove the per-machine
loop and manual sort/dedup, and return the single query’s results directly.

In `@crates/api-db/src/machine_interface/tests.rs`:
- Around line 235-257: Update wait_for_advisory_lock_wait to replace the
tight-loop tokio::task::yield_now() polling with a short tokio sleep between
pg_stat_activity checks, while preserving the five-second timeout and immediate
return once waiting is greater than zero.

In `@crates/rpc/src/lib.rs`:
- Around line 580-697: Optionally replace the duplicated ExpectedInterfaceRole
and ExpectedInterfaceIpAllocation adapter implementations with a small
declarative macro that generates their input enum and
serialize_optional/deserialize_optional methods. Parameterize the macro with the
enum type, input name, variants, canonical names, and accepted aliases, while
preserving numeric inputs, null handling, unknown-value errors, and canonical
string serialization.

In `@crates/site-explorer/tests/integration/machine_creator.rs`:
- Around line 450-461: Update the fixture setup around
db::expected_machine::create to capture its returned persisted ExpectedMachine
in expected_machine, rather than cloning the input before the call. Preserve
using that returned value in ExploredHostFixture after the transaction commits.
🪄 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: 97df67d8-4648-4c19-8858-5e2006842da9

📥 Commits

Reviewing files that changed from the base of the PR and between 7f3f044 and 93e4b83.

⛔ 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 (58)
  • crates/admin-cli/src/expected_machines/add/args.rs
  • crates/admin-cli/src/expected_machines/common.rs
  • crates/admin-cli/src/expected_machines/patch/args.rs
  • crates/admin-cli/src/expected_machines/tests.rs
  • crates/admin-cli/src/expected_machines/update/mod.rs
  • crates/admin-cli/src/rpc.rs
  • crates/api-core/src/dhcp/discover.rs
  • crates/api-core/src/dhcp/v6.rs
  • crates/api-core/src/handlers/bmc_endpoint_explorer.rs
  • crates/api-core/src/handlers/expected_machine.rs
  • crates/api-core/src/handlers/expected_power_shelf.rs
  • crates/api-core/src/handlers/expected_switch.rs
  • crates/api-core/src/handlers/machine.rs
  • crates/api-core/src/handlers/machine_discovery.rs
  • crates/api-core/src/handlers/machine_interface.rs
  • crates/api-core/src/handlers/machine_interface_address.rs
  • crates/api-core/src/handlers/power_shelf.rs
  • crates/api-core/src/handlers/switch.rs
  • crates/api-core/src/tests/expected_machine.rs
  • crates/api-core/src/tests/expected_switch.rs
  • crates/api-core/src/tests/machine_admin_force_delete.rs
  • crates/api-core/src/tests/machine_dhcp.rs
  • crates/api-core/src/tests/machine_discovery.rs
  • crates/api-core/src/tests/machine_states.rs
  • crates/api-core/src/tests/switch.rs
  • crates/api-core/tests/integration/expected_power_shelf_static_address.rs
  • crates/api-core/tests/integration/machine_boot_interfaces.rs
  • crates/api-core/tests/integration/power_shelf_delete.rs
  • crates/api-core/tests/integration/static_address_management.rs
  • crates/api-db/migrations/20260722120000_expected_interface_allocation_state.sql
  • crates/api-db/src/bmc_metadata.rs
  • crates/api-db/src/bmc_metadata/tests.rs
  • crates/api-db/src/expected_machine.rs
  • crates/api-db/src/expected_machine/test_allocation_state_migration.rs
  • crates/api-db/src/expected_machine/tests.rs
  • crates/api-db/src/expected_switch.rs
  • crates/api-db/src/expected_switch/tests.rs
  • crates/api-db/src/machine.rs
  • crates/api-db/src/machine_interface.rs
  • crates/api-db/src/machine_interface/tests.rs
  • crates/api-db/src/machine_interface_address.rs
  • crates/api-db/src/network_segment.rs
  • crates/api-db/src/predicted_machine_interface.rs
  • crates/api-model/src/expected_machine.rs
  • crates/api-model/src/machine/mod.rs
  • crates/api-model/src/predicted_machine_interface.rs
  • crates/machine-a-tron/src/machine_a_tron.rs
  • crates/machine-controller/src/boot_interface.rs
  • crates/rpc/build.rs
  • crates/rpc/proto/forge.proto
  • crates/rpc/src/lib.rs
  • crates/rpc/src/model/expected_machine.rs
  • crates/site-explorer/Cargo.toml
  • crates/site-explorer/src/lib.rs
  • crates/site-explorer/src/machine_creator.rs
  • crates/site-explorer/tests/integration/machine_creator.rs
  • crates/site-explorer/tests/integration/reconcile.rs
  • rest-api/proto/core/src/v1/nico_nico.proto

Comment thread crates/api-core/src/handlers/expected_machine.rs Outdated
Comment thread crates/api-core/src/handlers/expected_switch.rs Outdated
Comment thread crates/api-db/src/machine_interface.rs Outdated

chet commented Jul 25, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai full_review, thanks!

@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

@chet: Initiating a complete re-review of #3991, including the full diff and relevant integration paths.

🐇🔍

✅ Action performed

Full review finished.

@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: 11

🧹 Nitpick comments (15)
crates/site-explorer/tests/integration/machine_creator.rs (2)

741-748: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Decode the nullable expected_interface column as Option<Json<...>>.

expected_interface is nullable, so decoding straight into Json<ExpectedHostNic> turns a regression that leaves the column NULL into an opaque decode error rather than a readable assertion failure. The sibling tests at lines 954-961 and 1027-1034 already use the Option form.

♻️ Proposed refactor
-    let saved_interface = sqlx::query_scalar::<
-        _,
-        sqlx::types::Json<model::expected_machine::ExpectedHostNic>,
-    >("SELECT expected_interface FROM machine_interfaces WHERE id = $1")
-    .bind(interface.id)
-    .fetch_one(&mut *txn)
-    .await?;
+    let saved_interface: Option<sqlx::types::Json<ExpectedHostNic>> =
+        sqlx::query_scalar("SELECT expected_interface FROM machine_interfaces WHERE id = $1")
+            .bind(interface.id)
+            .fetch_one(&mut *txn)
+            .await?;
+    let saved_interface =
+        saved_interface.expect("the fixed policy should be captured before association");
🤖 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/site-explorer/tests/integration/machine_creator.rs` around lines 741 -
748, Update the query in the test around saved_interface to decode
expected_interface as Option<Json<model::expected_machine::ExpectedHostNic>>,
matching the sibling tests. Preserve the existing fixed_ip assertion while
ensuring a NULL column produces an assertion failure rather than a decode error.

450-462: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Store the row returned by db::expected_machine::create, not the pre-insert value.

The fixture discards create's return value and keeps the locally constructed expected_machine. Since create_managed_host re-fetches the row FOR UPDATE and compares identity, any field the database normalizes or assigns would make the fixture diverge from the persisted row and surface as an opaque FailedPrecondition. crates/api-core/src/tests/machine_states.rs (lines 526-528) already rebinds to the returned row; aligning here keeps the two fixtures on one contract.

♻️ Proposed refactor
-    let expected_machine = expected_machine(managed_host);
     let mut txn = env.pool.begin().await.unwrap();
-    db::expected_machine::create(&mut txn, expected_machine.clone())
+    let expected_machine = db::expected_machine::create(&mut txn, expected_machine(managed_host))
         .await
         .unwrap();
     txn.commit().await.unwrap();
🤖 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/site-explorer/tests/integration/machine_creator.rs` around lines 450 -
462, In the fixture setup around db::expected_machine::create, rebind
expected_machine to the row returned by create instead of retaining the
pre-insert value. Continue cloning or passing this persisted result into
ExploredHostFixture so it reflects database-normalized or assigned fields.
crates/api-db/src/machine_interface/tests.rs (1)

235-257: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Add a small delay to the advisory-lock poll loop.

yield_now() provides no backpressure, so this helper issues COUNT(*) against pg_stat_activity as fast as the runtime permits while contending for pool connections with the very transactions under test. A short sleep detects the wait just as promptly at a fraction of the cost, and this helper is now on the hot path of six concurrency tests.

♻️ Proposed refactor
             if waiting > 0 {
                 return;
             }
-            tokio::task::yield_now().await;
+            tokio::time::sleep(std::time::Duration::from_millis(10)).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-db/src/machine_interface/tests.rs` around lines 235 - 257, Update
wait_for_advisory_lock_wait to replace the unbounded tokio::task::yield_now
polling with a short tokio sleep between pg_stat_activity queries, preserving
the existing timeout and successful return behavior.
crates/api-db/src/machine_interface.rs (3)

1838-1853: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

The guarded pre-read does not protect the final UPDATE in replace_expected_interface.

The SELECT ... FOR UPDATE filters associated rows out, so for an associated row it returns None and takes no row lock at all — after which the unconditional UPDATE at line 1910 still overwrites expected_interface. Today every caller pre-checks association (the deliberate exception being the legacy-finalization path at line 1786), so this is a hardening gap rather than a live defect, but the asymmetry with clear_expected_interface_if_anonymous, whose final statement does carry the anonymity predicate, invites a future regression.

Either mirror the predicate here or state in the doc comment that associated rewrites are the caller's responsibility.

Also applies to: 1910-1921

🤖 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 1838 - 1853, Update
replace_expected_interface, including its final UPDATE, so the
anonymity/association predicate is enforced by the write itself rather than only
by the guarded SELECT; alternatively, explicitly document in the function’s doc
comment that callers must prevent associated rewrites. Prefer mirroring the
existing SELECT predicate in the UPDATE while preserving the transaction and
expected-interface behavior.

1620-1643: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Two hand-copied SQL bodies differing only by FOR UPDATE.

The associated expression is now repeated verbatim in four places in this file (here twice, plus lines 1841-1845 and 1930-1934, 1975-1979). Consider a single const ASSOCIATED_PREDICATE: &str and a conditional " FOR UPDATE" suffix so the definition of "associated" cannot drift between the locking and non-locking readers.

♻️ Sketch
-    let query = if for_update {
-        "SELECT
-            id,
-            ...
-        WHERE mac_address = $1
-        FOR UPDATE"
-    } else {
-        "SELECT
-            id,
-            ...
-        WHERE mac_address = $1"
-    };
+    let query = format!(
+        "SELECT id, segment_id, expected_interface, expected_interface_captured,
+                {ASSOCIATED_PREDICATE} AS associated
+         FROM machine_interfaces
+         WHERE mac_address = $1{}",
+        if for_update { " FOR UPDATE" } else { "" }
+    );

(DatabaseError::query takes the statement text, so a String works with a borrow at the call sites.)

[scratchpad_end_marker_none]
🤖 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 1620 - 1643, Consolidate
the duplicated associated SQL expression used by the query-building code around
the visible `for_update` branch into one shared `ASSOCIATED_PREDICATE` constant,
and construct a single base query with a conditional `FOR UPDATE` suffix. Reuse
the same predicate for the related query definitions in this file so locking and
non-locking readers cannot diverge.

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

Drop the unused newly_created return value crates/api-db/src/machine_interface.rs:728-763 — both find_or_create_machine_interface_inner and validate_existing_mac_and_create_inner thread this flag through only to discard it at the call sites. Returning just MachineInterfaceSnapshot would simplify the internal API.

🤖 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` at line 735, Remove the unused
newly_created boolean from the return type and implementations of
find_or_create_machine_interface_inner and
validate_existing_mac_and_create_inner, returning only MachineInterfaceSnapshot.
Update all callers and destructuring to consume the snapshot directly while
preserving the existing machine-interface creation and validation behavior.
crates/site-explorer/tests/integration/reconcile.rs (1)

426-438: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider a positive counterpart for the retention path.

try_reconcile_expected_machine_bmc swallows every failure into a warn!, so this assertion also holds if the helper aborted early for an unrelated reason. A sibling case that leaves the policy at Retained and asserts the address flips to Static would prove the mechanism is actually exercised, not merely inert.

🤖 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/site-explorer/tests/integration/reconcile.rs` around lines 426 - 438,
Add a sibling integration test covering the retained-policy path through
try_reconcile_expected_machine_bmc: keep the expected machine’s BMC IP
allocation as Retained, run reconciliation, then query the interface addresses
and assert the allocation type becomes Static. Follow the existing transaction
and setup pattern so the assertion verifies reconciliation occurred rather than
passing after an early failure.
crates/site-explorer/src/lib.rs (2)

3570-3577: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Three distinct failure causes emit the identical log line.

"Site-explorer expected-machine BMC reconcile skipped" is used for lock acquisition failure, lookup failure, and preallocation/retain failure. The message literal should stay stable, so add a discriminating structured field rather than varying the text — the guidelines already reserve reason for exactly this.

As per coding guidelines, use "stable semantic logging field names: error, grpc_status_code, http_status, reason, …".

♻️ Suggested shape
     if let Err(error) = db::expected_machine::lock_config_mutations_shared(txn.as_pgconn()).await {
         tracing::warn!(
             %error,
             %bmc_mac_address,
+            reason = "lock_config_mutations",
             "Site-explorer expected-machine BMC reconcile skipped"
         );
         return;
     }

The same applies to try_reconcile_expected_switch_addresses, which repeats one message across four sites.

Also applies to: 3589-3596, 3622-3629

🤖 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/site-explorer/src/lib.rs` around lines 3570 - 3577, Add a structured
`reason` field to every warning using the stable “Site-explorer expected-machine
BMC reconcile skipped” message, assigning distinct values for lock acquisition,
lookup, and preallocation/retain failures. Apply the same treatment to all
warning sites in `try_reconcile_expected_switch_addresses`, covering each of its
four failure paths, without changing the message literal.

Source: Coding guidelines


2035-2061: 🚀 Performance & Scalability | 🔵 Trivial

Serial per-entry transactions will dominate this phase as the site grows.

Each expected machine now costs one transaction for the BMC reconcile plus one per fixed-IP host NIC, all sequential. The update_explored_endpoints_preallocate phase latency already instruments this, so it is observable rather than invisible — worth watching the metric at scale and, if it grows, either batching the reconcile per machine or bounding concurrency with a semaphore the way the exploration probes are bounded.

🤖 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/site-explorer/src/lib.rs` around lines 2035 - 2061, Update the
update_explored_endpoints_preallocate phase so expected-machine BMC
reconciliation and fixed-IP NIC preallocation do not execute as unbounded serial
transactions. Reuse the existing bounded-concurrency semaphore pattern from
exploration probes, or batch each machine’s reconciliation work, while
preserving switch reconciliation behavior and the phase’s existing
instrumentation.
crates/api-db/src/bmc_metadata.rs (1)

68-83: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Read-only lookup should accept impl DbReader.

find_interface_by_bmc_ip performs no writes and takes no locks, yet it requires &mut PgConnection, which forces read-only callers into a transaction handle. Generifying the shared helper over DbReader keeps the _for_update variant on &mut PgConnection while making the plain variant usable from read paths.

As per coding guidelines: "Keep database calls in api-db as bare functions accepting model values, while keeping model definitions in api-model. Use impl DbReader for read-only database functions."

🤖 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/bmc_metadata.rs` around lines 68 - 83, Update
find_interface_by_bmc_ip and the shared find_interface_by_bmc_ip_with_query
helper to accept impl DbReader for read-only access, while preserving the
_for_update variant’s &mut PgConnection signature for locking behavior. Keep the
existing query and return contract unchanged.

Source: Coding guidelines

crates/api-db/migrations/20260722120000_expected_interface_allocation_state.sql (1)

49-50: 🧹 Nitpick | 🔵 Trivial

Plan the rollout window for the unqualified rewrites and the index build.

Two observations for deployment, not code defects:

  • Line 49-50 rewrites every predicted_machine_interfaces row (the second full-table pass in this migration), and Lines 56-60 rewrite a large share of machine_interfaces. Both run inside the migration transaction, so row locks and bloat persist until commit.
  • Lines 71-72 take ACCESS EXCLUSIVE on expected_machines for the duration of the build. That table is small, so this should be brief; the note is only so the combined migration duration is budgeted against the API-server startup gate.

The jsonb_path_ops opclass choice is correct for the host_nics @> … containment lookups added in api-db, and plain CREATE INDEX is the right call for a transactional migration.

The Squawk require-concurrent-index-creation hint is not applicable: CONCURRENTLY is incompatible with sqlx::migrate!()'s transactional DDL, which is this repository's established convention.

Also applies to: 71-72

🤖 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/migrations/20260722120000_expected_interface_allocation_state.sql`
around lines 49 - 50, The review identifies deployment planning considerations
only; no code changes are required. Keep the full-table updates, transactional
index creation, and existing jsonb_path_ops choice unchanged, and ensure the
rollout window accounts for migration locks, bloat, index-build duration, and
the API-server startup gate.

Source: Linters/SAST tools

crates/api-db/src/expected_machine.rs (2)

133-139: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Document that this containment predicate is representation-coupled.

host_nics @> [{"mac_address": "<Display>"}] matches on the textual JSON value, so it silently returns no rows if a writer ever persists a MAC in a different case or separator style. Note the contrast with validate_identity_macs_available (Line 250) and the migration, which both cast to macaddr and are representation-independent.

The coupling is currently sound because reads and writes share one ExpectedHostNic serde, and the textual form is what lets the new jsonb_path_ops GIN index serve this lookup. A short comment stating that invariant would prevent a future serde change from turning a lookup miss into a silent ingestion failure.

🤖 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/expected_machine.rs` around lines 133 - 139, Add a concise
comment immediately above the mac_address JSON construction or query in the
expected-machine lookup explaining that the JSON containment predicate compares
textual MAC representations, relies on shared ExpectedHostNic serialization for
reads and writes, and must remain aligned with the jsonb_path_ops index;
contrast this with macaddr-cast validation only if needed to clarify the
invariant.

242-254: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Identity validation is a per-record full scan with lateral expansion.

The jsonb_array_elements(host_nics) + (… ->> 'mac_address')::macaddr predicate cannot use the new jsonb_path_ops GIN index, so each invocation is a sequential scan over expected_machines with per-row array expansion. Callers run it in a loop — one call per model in create_expected_machine_records, create_missing_from, and per changed machine in apply_atomic_batch_updates — making a large import O(batch × rows × nics).

Consider a single set-based query per batch, or an index-friendly containment predicate over the candidate MACs, so bulk imports do not scale quadratically.

🤖 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/expected_machine.rs` around lines 242 - 254, The identity
validation query should avoid per-record JSON expansion and repeated full-table
scans. Update the expected-machine validation flow around this query and its
callers in create_expected_machine_records, create_missing_from, and
apply_atomic_batch_updates to perform batch-level validation, or replace the
jsonb_array_elements predicate with an index-friendly containment check over
candidate MAC addresses while preserving the existing identity-matching
behavior.
crates/rpc/src/lib.rs (1)

580-696: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Collapse the two identical adapters into one macro.

The role and allocation implementations differ only in their alias tables; the input enums, Option handling, numeric conversion, and error shaping are byte-for-byte duplicates. A small declarative macro taking the type plus its alias/label pairs would keep future enums from copying another sixty lines.

🤖 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/rpc/src/lib.rs` around lines 580 - 696, Replace the duplicated
ExpectedInterfaceRole and ExpectedInterfaceIpAllocation adapters with one
declarative macro parameterized by the enum type and alias/serialized-label
mappings. Have the macro generate the shared input enum, deserialize_optional,
and serialize_optional logic while preserving numeric conversion, Option
handling, and existing unknown-value errors; invoke it separately with each
enum’s current aliases and labels.
crates/api-db/src/machine_interface_address.rs (1)

184-234: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider collapsing the two retain helpers onto one implementation.

retain_expected_machine_dhcp_address_for_family differs from retain_dhcp_address_for_family only by also setting the provenance marker. Expressing the promotion once, parameterised by ownership, keeps the two SQL predicates from drifting apart later.

♻️ Suggested consolidation
async fn retain_dhcp_address_for_family_owned(
    txn: &mut PgConnection,
    interface_id: MachineInterfaceId,
    family: IpAddressFamily,
    expected_machine_owned: bool,
) -> Result<bool, DatabaseError> {
    let query = "UPDATE machine_interface_addresses
                 SET allocation_type = $3,
                     expected_machine_preallocation =
                         expected_machine_preallocation OR $5
                 WHERE interface_id = $1
                   AND family(address) = $2
                   AND allocation_type = $4";
    sqlx::query(query)
        .bind(interface_id)
        .bind(family.pg_family())
        .bind(AllocationType::Static)
        .bind(AllocationType::Dhcp)
        .bind(expected_machine_owned)
        .execute(txn)
        .await
        .map(|result| result.rows_affected() > 0)
        .map_err(|error| DatabaseError::query(query, error))
}
🤖 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_address.rs` around lines 184 - 234,
Consolidate retain_dhcp_address_for_family and
retain_expected_machine_dhcp_address_for_family through a shared helper, such as
retain_dhcp_address_for_family_owned, parameterized by whether the address is
expected-machine-owned. Use one UPDATE statement that promotes DHCP to Static
and preserves or sets expected_machine_preallocation via the ownership
parameter, while keeping both public helpers’ return behavior unchanged.
🤖 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/discover.rs`:
- Around line 825-830: Update persist_expected_interface_if_missing to avoid
issuing a write when expected_interface_captured is already true by adding that
flag to the statement predicate; preserve the existing behavior of marking the
interface captured, including when host_nic is None.

In `@crates/api-core/src/handlers/expected_machine.rs`:
- Around line 825-837: Update the conflict error construction in the second pass
over machines to report the changed machine identified by
changed_owners[&mac_address], rather than the unchanged machine’s
machine.bmc_mac_address. Preserve the existing conflict detection and error
behavior while using the recorded changed-machine counterpart in the message.
- Around line 1045-1056: Update the interface resolution in the expected-machine
handler around expected_interface_for_mac to propagate a lookup failure as an
InvalidArgument response instead of panicking via expect. Preserve the existing
MAC-based resolution and sorting behavior, and use the handler’s established
error-return path.

In `@crates/api-core/src/handlers/expected_switch.rs`:
- Around line 363-377: Update update_expected_switch to release prior
expected_machine_preallocation entries when the new switch payload clears
bmc_ip_address or nvos_ip_address, before reconciling the remaining static
interfaces. Ensure retained addresses continue through
resolve_and_lock_expected_switch_static_interfaces and
reconcile_expected_switch_static_interfaces unchanged; if updates are
intentionally one-way instead, add an inline note documenting that contract.

In `@crates/api-core/src/handlers/machine.rs`:
- Around line 341-349: Update the interface filtering in
delete_prepared_machine_interfaces() to reuse the same association predicate as
find_ids_by_machine_associations(), including attached_dpu_machine_id alongside
machine_id while still excluding BMC interfaces. Ensure association-only
interfaces are included in both deletion and the reported results.

In `@crates/api-core/src/tests/expected_machine.rs`:
- Around line 110-162: The advisory-lock wait helpers busy-poll the database and
can contend with the task being observed. In
crates/api-core/src/tests/expected_machine.rs#L110-L162, update both
wait_for_expected_machine_advisory_lock and wait_for_advisory_lock_wait to sleep
for 10 ms between probes instead of calling yield_now; make the same replacement
in wait_for_advisory_lock_waiters in
crates/api-core/src/tests/machine_dhcp.rs#L96-L118.

In `@crates/api-db/src/bmc_metadata.rs`:
- Around line 155-159: The enrich-then-associate flow resolves its write target
without locking, allowing ownership changes before
capture_expected_interface_before_association or associate_bmc_interface runs.
In crates/api-db/src/bmc_metadata.rs:155-159, replace
machine_interface::find_one with a locking single-interface lookup or fall back
to find_interface_by_bmc_ip_for_update when bmc_info.ip is available. In
crates/api-db/src/bmc_metadata.rs:222-234, use
find_interface_by_bmc_ip_for_update whenever the resolved machine_interface_id
will be reused for a write, including the persist path, so cached IDs cannot
bypass the locked association flow.

In `@crates/api-db/src/expected_machine.rs`:
- Around line 149-166: Update the ambiguity errors in the shown ExpectedMachine
MAC-resolution logic to use DatabaseError::internal instead of
DatabaseError::InvalidArgument for both the BMC-and-nested conflict and multiple
nested interface declarations. Preserve the existing messages and successful
resolution behavior, matching find_interface_by_bmc_ip_with_query and
find_by_nvos_mac_address.

In `@crates/api-db/src/expected_switch.rs`:
- Around line 374-384: The row-resolution logic in delete_by_id/delete_by_mac
and the site-explorer hardware-truth write must preserve their prior tolerant
behavior: when find returns no expected-switch row, return Ok(()) without
applying MAC locks or propagating NotFoundError. Update both affected sites in
crates/api-db/src/expected_switch.rs (anchor lines 374-384 and sibling lines
450-456), while retaining normal processing when a row is found.

In `@crates/api-db/src/machine_interface.rs`:
- Around line 4110-4114: Update the FailedPrecondition messages in the visible
reconciliation error and the additional occurrences around the related Loaded
and Previously messages to begin with lowercase words, while preserving the
remaining text and existing no-trailing-period format.
- Around line 3540-3547: Update the shared
unique_instance_discovery_interface_id helper to sort and deduplicate the
supplied MachineInterfaceId values before performing its length-based ambiguity
check. Preserve the existing unambiguous, missing, and ambiguous outcomes, and
apply this in the helper so both regular and locking query variants handle
duplicate joined rows consistently.

---

Nitpick comments:
In
`@crates/api-db/migrations/20260722120000_expected_interface_allocation_state.sql`:
- Around line 49-50: The review identifies deployment planning considerations
only; no code changes are required. Keep the full-table updates, transactional
index creation, and existing jsonb_path_ops choice unchanged, and ensure the
rollout window accounts for migration locks, bloat, index-build duration, and
the API-server startup gate.

In `@crates/api-db/src/bmc_metadata.rs`:
- Around line 68-83: Update find_interface_by_bmc_ip and the shared
find_interface_by_bmc_ip_with_query helper to accept impl DbReader for read-only
access, while preserving the _for_update variant’s &mut PgConnection signature
for locking behavior. Keep the existing query and return contract unchanged.

In `@crates/api-db/src/expected_machine.rs`:
- Around line 133-139: Add a concise comment immediately above the mac_address
JSON construction or query in the expected-machine lookup explaining that the
JSON containment predicate compares textual MAC representations, relies on
shared ExpectedHostNic serialization for reads and writes, and must remain
aligned with the jsonb_path_ops index; contrast this with macaddr-cast
validation only if needed to clarify the invariant.
- Around line 242-254: The identity validation query should avoid per-record
JSON expansion and repeated full-table scans. Update the expected-machine
validation flow around this query and its callers in
create_expected_machine_records, create_missing_from, and
apply_atomic_batch_updates to perform batch-level validation, or replace the
jsonb_array_elements predicate with an index-friendly containment check over
candidate MAC addresses while preserving the existing identity-matching
behavior.

In `@crates/api-db/src/machine_interface_address.rs`:
- Around line 184-234: Consolidate retain_dhcp_address_for_family and
retain_expected_machine_dhcp_address_for_family through a shared helper, such as
retain_dhcp_address_for_family_owned, parameterized by whether the address is
expected-machine-owned. Use one UPDATE statement that promotes DHCP to Static
and preserves or sets expected_machine_preallocation via the ownership
parameter, while keeping both public helpers’ return behavior unchanged.

In `@crates/api-db/src/machine_interface.rs`:
- Around line 1838-1853: Update replace_expected_interface, including its final
UPDATE, so the anonymity/association predicate is enforced by the write itself
rather than only by the guarded SELECT; alternatively, explicitly document in
the function’s doc comment that callers must prevent associated rewrites. Prefer
mirroring the existing SELECT predicate in the UPDATE while preserving the
transaction and expected-interface behavior.
- Around line 1620-1643: Consolidate the duplicated associated SQL expression
used by the query-building code around the visible `for_update` branch into one
shared `ASSOCIATED_PREDICATE` constant, and construct a single base query with a
conditional `FOR UPDATE` suffix. Reuse the same predicate for the related query
definitions in this file so locking and non-locking readers cannot diverge.
- Line 735: Remove the unused newly_created boolean from the return type and
implementations of find_or_create_machine_interface_inner and
validate_existing_mac_and_create_inner, returning only MachineInterfaceSnapshot.
Update all callers and destructuring to consume the snapshot directly while
preserving the existing machine-interface creation and validation behavior.

In `@crates/api-db/src/machine_interface/tests.rs`:
- Around line 235-257: Update wait_for_advisory_lock_wait to replace the
unbounded tokio::task::yield_now polling with a short tokio sleep between
pg_stat_activity queries, preserving the existing timeout and successful return
behavior.

In `@crates/rpc/src/lib.rs`:
- Around line 580-696: Replace the duplicated ExpectedInterfaceRole and
ExpectedInterfaceIpAllocation adapters with one declarative macro parameterized
by the enum type and alias/serialized-label mappings. Have the macro generate
the shared input enum, deserialize_optional, and serialize_optional logic while
preserving numeric conversion, Option handling, and existing unknown-value
errors; invoke it separately with each enum’s current aliases and labels.

In `@crates/site-explorer/src/lib.rs`:
- Around line 3570-3577: Add a structured `reason` field to every warning using
the stable “Site-explorer expected-machine BMC reconcile skipped” message,
assigning distinct values for lock acquisition, lookup, and preallocation/retain
failures. Apply the same treatment to all warning sites in
`try_reconcile_expected_switch_addresses`, covering each of its four failure
paths, without changing the message literal.
- Around line 2035-2061: Update the update_explored_endpoints_preallocate phase
so expected-machine BMC reconciliation and fixed-IP NIC preallocation do not
execute as unbounded serial transactions. Reuse the existing bounded-concurrency
semaphore pattern from exploration probes, or batch each machine’s
reconciliation work, while preserving switch reconciliation behavior and the
phase’s existing instrumentation.

In `@crates/site-explorer/tests/integration/machine_creator.rs`:
- Around line 741-748: Update the query in the test around saved_interface to
decode expected_interface as
Option<Json<model::expected_machine::ExpectedHostNic>>, matching the sibling
tests. Preserve the existing fixed_ip assertion while ensuring a NULL column
produces an assertion failure rather than a decode error.
- Around line 450-462: In the fixture setup around db::expected_machine::create,
rebind expected_machine to the row returned by create instead of retaining the
pre-insert value. Continue cloning or passing this persisted result into
ExploredHostFixture so it reflects database-normalized or assigned fields.

In `@crates/site-explorer/tests/integration/reconcile.rs`:
- Around line 426-438: Add a sibling integration test covering the
retained-policy path through try_reconcile_expected_machine_bmc: keep the
expected machine’s BMC IP allocation as Retained, run reconciliation, then query
the interface addresses and assert the allocation type becomes Static. Follow
the existing transaction and setup pattern so the assertion verifies
reconciliation occurred rather than passing after an early failure.
🪄 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: 34868109-f906-4f26-a448-eaa052498a26

📥 Commits

Reviewing files that changed from the base of the PR and between 9e7cadc and 3e751a5.

⛔ 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 (58)
  • crates/admin-cli/src/expected_machines/add/args.rs
  • crates/admin-cli/src/expected_machines/common.rs
  • crates/admin-cli/src/expected_machines/patch/args.rs
  • crates/admin-cli/src/expected_machines/tests.rs
  • crates/admin-cli/src/expected_machines/update/mod.rs
  • crates/admin-cli/src/rpc.rs
  • crates/api-core/src/dhcp/discover.rs
  • crates/api-core/src/dhcp/v6.rs
  • crates/api-core/src/handlers/bmc_endpoint_explorer.rs
  • crates/api-core/src/handlers/expected_machine.rs
  • crates/api-core/src/handlers/expected_power_shelf.rs
  • crates/api-core/src/handlers/expected_switch.rs
  • crates/api-core/src/handlers/machine.rs
  • crates/api-core/src/handlers/machine_discovery.rs
  • crates/api-core/src/handlers/machine_interface.rs
  • crates/api-core/src/handlers/machine_interface_address.rs
  • crates/api-core/src/handlers/power_shelf.rs
  • crates/api-core/src/handlers/switch.rs
  • crates/api-core/src/tests/expected_machine.rs
  • crates/api-core/src/tests/expected_switch.rs
  • crates/api-core/src/tests/machine_admin_force_delete.rs
  • crates/api-core/src/tests/machine_dhcp.rs
  • crates/api-core/src/tests/machine_discovery.rs
  • crates/api-core/src/tests/machine_states.rs
  • crates/api-core/src/tests/switch.rs
  • crates/api-core/tests/integration/expected_power_shelf_static_address.rs
  • crates/api-core/tests/integration/machine_boot_interfaces.rs
  • crates/api-core/tests/integration/power_shelf_delete.rs
  • crates/api-core/tests/integration/static_address_management.rs
  • crates/api-db/migrations/20260722120000_expected_interface_allocation_state.sql
  • crates/api-db/src/bmc_metadata.rs
  • crates/api-db/src/bmc_metadata/tests.rs
  • crates/api-db/src/expected_machine.rs
  • crates/api-db/src/expected_machine/test_allocation_state_migration.rs
  • crates/api-db/src/expected_machine/tests.rs
  • crates/api-db/src/expected_switch.rs
  • crates/api-db/src/expected_switch/tests.rs
  • crates/api-db/src/machine.rs
  • crates/api-db/src/machine_interface.rs
  • crates/api-db/src/machine_interface/tests.rs
  • crates/api-db/src/machine_interface_address.rs
  • crates/api-db/src/network_segment.rs
  • crates/api-db/src/predicted_machine_interface.rs
  • crates/api-model/src/expected_machine.rs
  • crates/api-model/src/machine/mod.rs
  • crates/api-model/src/predicted_machine_interface.rs
  • crates/machine-a-tron/src/machine_a_tron.rs
  • crates/machine-controller/src/boot_interface.rs
  • crates/rpc/build.rs
  • crates/rpc/proto/forge.proto
  • crates/rpc/src/lib.rs
  • crates/rpc/src/model/expected_machine.rs
  • crates/site-explorer/Cargo.toml
  • crates/site-explorer/src/lib.rs
  • crates/site-explorer/src/machine_creator.rs
  • crates/site-explorer/tests/integration/machine_creator.rs
  • crates/site-explorer/tests/integration/reconcile.rs
  • rest-api/proto/core/src/v1/nico_nico.proto

Comment thread crates/api-core/src/dhcp/discover.rs
Comment thread crates/api-core/src/handlers/expected_machine.rs
Comment thread crates/api-core/src/handlers/expected_machine.rs
Comment thread crates/api-core/src/handlers/expected_switch.rs
Comment thread crates/api-core/src/handlers/machine.rs
Comment thread crates/api-db/src/bmc_metadata.rs Outdated
Comment thread crates/api-db/src/expected_machine.rs
Comment thread crates/api-db/src/expected_switch.rs
Comment thread crates/api-db/src/machine_interface.rs
Comment thread crates/api-db/src/machine_interface.rs

chet commented Jul 25, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai full_review, thanks!

@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

@chet: Initiating a complete re-review of #3991, including the full diff and relevant integration paths.

🐇🔍

✅ Action performed

Full review finished.

@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: 2

🧹 Nitpick comments (10)
crates/rpc/src/lib.rs (1)

580-636: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Two structurally identical adapters — consider collapsing the boilerplate.

ExpectedInterfaceRole and ExpectedInterfaceIpAllocation differ only in their alias tables and canonical names; the untagged input enum, the Option unwrap, the numeric try_from, and the serializer shape are byte-for-byte equivalent. A single declarative macro taking the enum path plus (canonical_name, aliases…) => Variant rows would keep the alias tables visible while eliminating ~50 duplicated lines, and would guarantee the next enum added to ExpectedHostNic inherits identical semantics rather than a hand-copied approximation.

Deferrable — the current form is correct and readable. Flagging it because a third copy is the point at which the drift becomes expensive.

Also applies to: 638-696

🤖 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/rpc/src/lib.rs` around lines 580 - 636, Consolidate the duplicated
ExpectedInterfaceRole and ExpectedInterfaceIpAllocation serde adapters into one
declarative macro that accepts the enum type and canonical-name/alias mappings.
Reuse the macro for both implementations, preserving their numeric handling,
optional values, validation, canonical serialization names, and distinct alias
tables.
crates/api-core/src/handlers/expected_switch.rs (1)

338-345: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

The NotFound identifier can render empty.

When neither expected_switch_id nor bmc_mac_address survives conversion, unwrap_or_default() yields an empty id, producing a NotFound message with no subject. If ExpectedSwitchRequest permits both selectors to be absent, rejecting that shape as InvalidArgument before the lookup would give operators an actionable message instead.

🤖 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/expected_switch.rs` around lines 338 - 345,
Update the expected-switch lookup flow around the NotFoundError construction to
validate that ExpectedSwitchRequest provides at least one usable selector before
querying. If both expected_switch_id and bmc_mac_address are absent or unusable,
return InvalidArgument with an actionable message; otherwise preserve the
existing lookup and NotFound behavior.
crates/api-core/src/handlers/expected_machine.rs (1)

883-951: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Rename the transitioned_macs parameter where it is consumed as a retained set.

release_removed_fixed_reservations forwards transitioned_macs into clear_removed_expected_interface_snapshots(txn, previous, retained_macs), where the value is negated (!retained_macs.contains(...)). The two names describe opposite intents at the same call boundary, which makes the delete/delete_all/replace_all call sites (empty set vs. full declaration set) harder to reason about. A single name such as still_declared_macs would remove the ambiguity without changing behavior.

🤖 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/expected_machine.rs` around lines 883 - 951,
Rename the consumed `transitioned_macs` parameter and its corresponding argument
in `release_removed_fixed_reservations` to a retained-set name such as
`still_declared_macs`, including the call to
`clear_removed_expected_interface_snapshots`. Preserve the existing set contents
and all reservation-release behavior; only clarify the parameter naming at this
boundary.
crates/api-core/src/handlers/machine.rs (1)

341-357: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the unreachable remove() error path. interface_ids is derived from prepared_interfaces, so remove() cannot return None here. Drain the matching entries directly with extract_if to make the invariant explicit.

🤖 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.rs` around lines 341 - 357, Replace the
interface_ids collection and subsequent remove loop with direct draining via
extract_if on prepared_interfaces, selecting non-Bmc interfaces belonging to
machine_id and passing each extracted prepared entry to
db::machine_interface::delete_prepared. Remove the unreachable
FailedPrecondition error handling while preserving transaction usage and
deletion behavior.
crates/api-core/src/tests/machine_dhcp.rs (1)

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

Three byte-identical advisory-lock wait helpers, each coupled to the SQL text of the lock helper. All three poll pg_stat_activity with query LIKE '%pg_advisory_xact_lock%'. If a DB lock helper ever switches to pg_advisory_xact_lock_shared or wraps the call, every one of these silently stops matching and the affected tests degrade into 5-second timeouts rather than reporting the real problem. Extracting one shared helper (and preferring the pg_blocking_pids-based predicate already used in crates/api-core/src/tests/expected_machine.rs, which does not depend on query text) removes both the duplication and the coupling.

  • crates/api-core/src/tests/machine_dhcp.rs#L96-L118: move this parameterised waiter into the shared test-support module and drop the query LIKE predicate in favour of blocker-pid scoping.
  • crates/api-core/src/tests/machine_discovery.rs#L85-L107: delete the local copy and call the shared helper.
  • crates/site-explorer/src/machine_creator.rs#L1568-L1590: delete the local copy and call the shared helper.
🤖 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_dhcp.rs` around lines 96 - 118, Extract the
parameterized wait_for_advisory_lock_waiters helper from
crates/api-core/src/tests/machine_dhcp.rs#L96-L118 into the shared test-support
module, replacing the query-text predicate with the existing
pg_blocking_pids-based scoping used in
crates/api-core/src/tests/expected_machine.rs. Delete the duplicate local helper
and call the shared helper from
crates/api-core/src/tests/machine_discovery.rs#L85-L107 and
crates/site-explorer/src/machine_creator.rs#L1568-L1590.
crates/site-explorer/src/machine_creator.rs (1)

705-725: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Prediction paths hand-roll the expected-interface lookup instead of reusing expected_interface_for_mac. Both sites search machine_data.host_nics by MAC and then patch primary themselves, diverging from the association paths (Lines 631-638, 763-770) that call ExpectedMachineData::expected_interface_for_mac — and diverging from each other, since only one of the two applies the role.is_host() guard. A single helper that returns the normalised declaration and then applies the settled boot choice keeps the stored snapshot consistent across every ingestion path.

  • crates/site-explorer/src/machine_creator.rs#L705-L725: replace the manual host_nics search with expected_interface_for_mac(mac_address) and apply the is_primary override on the result.
  • crates/site-explorer/src/machine_creator.rs#L828-L846: do the same for declared_mac, so the role.is_host() handling matches the sibling site rather than unconditionally forcing primary = Some(true).
🤖 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/site-explorer/src/machine_creator.rs` around lines 705 - 725, Update
crates/site-explorer/src/machine_creator.rs:705-725 and 828-846 to replace each
manual host_nics MAC lookup with
ExpectedMachineData::expected_interface_for_mac, then apply the settled boot
choice only through the helper’s normalized result and existing role.is_host()
behavior; use mac_address at 705-725 and declared_mac at 828-846, ensuring both
prediction paths produce the same expected-interface snapshot.
crates/api-model/src/expected_machine.rs (1)

255-265: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Keep primary normalized in one place. ExpectedHostNic::primary is public, and expected_interface_for_mac() can return DPU interfaces with a raw primary value that disagrees with initial_primary_interface(). Hide the field behind the accessor, or normalize primary for every role when building the snapshot so callers cannot observe two different answers.

🤖 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-model/src/expected_machine.rs` around lines 255 - 265, Ensure
ExpectedHostNic::primary has a single normalized source of truth: make the field
private and route reads through initial_primary_interface(), or normalize it for
every role when constructing snapshots returned by expected_interface_for_mac().
Prevent callers from observing a raw primary value that conflicts with
initial_primary_interface(), while preserving the existing Host, DpuOs, and
DpuBmc semantics.
crates/api-db/src/bmc_metadata.rs (1)

232-244: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use % shorthand for the MAC field and drop the duplicate read.

MacAddress implements Display, so ? emits a Debug rendering that is inconsistent with the surrounding logfmt fields; the already-bound bmc_mac_address local also makes the second field access redundant.

As per coding guidelines: "Use logfmt-compatible structured logging … use native shorthand, % for Display, and ? for Debug."

♻️ Proposed logging fix
             tracing::info!(
                 caller = %caller,
                 machine_id = %machine_id,
-                mac_address = ?bmc_interface_owner.mac_address,
+                mac_address = %bmc_mac_address,
                 "Enriching BMC information",
             );
🤖 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/bmc_metadata.rs` around lines 232 - 244, Update the
tracing::info! call in the bmc_interface_owner branch to log the existing
bmc_mac_address local with `%` instead of rereading
bmc_interface_owner.mac_address with `?`; leave the remaining enrichment
assignments unchanged.

Source: Coding guidelines

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

205-224: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Sort identity_macs with sort_unstable(). MacAddress already implements Ord, so ordering directly makes the lock-order invariant explicit and avoids allocating a String per key.

🤖 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/expected_machine.rs` around lines 205 - 224, Update
lock_identity_macs to sort identity_macs directly with sort_unstable(), relying
on MacAddress’s Ord implementation; remove the sort_by_key(ToString::to_string)
call while preserving deduplication and lock acquisition order.

Source: Coding guidelines

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

523-568: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Document the lock preconditions on these two public deletion helpers.

Unlike the neighbouring additions (ensure_available_for_interface, find_for_interface_for_update, release_expected_machine_retained_addresses), these two bypass delete_address_allocations_with_lock_order and issue an unguarded DELETE. They are safe today only because their callers in machine_interface.rs (replace_expected_interface, clear_expected_interface_if_anonymous) already hold the MAC, fixed-address, and interface-row locks. Stating that contract in the doc comments keeps a future caller from reintroducing the lock-order inversion this PR removes elsewhere.

♻️ Proposed documentation
 /// Delete a static address only when ExpectedMachine reconciliation created it.
+///
+/// Callers must already hold the interface MAC, fixed-address, and interface
+/// row locks; this helper deliberately performs no locking of its own.
 pub async fn delete_expected_machine_preallocation_by_interface_and_address(
🤖 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_address.rs` around lines 523 - 568,
Update the doc comments for
delete_expected_machine_preallocation_by_interface_and_address and
delete_expected_machine_preallocation_or_legacy_by_interface_and_address to
state that callers must already hold the MAC, fixed-address, and interface-row
locks before invoking these helpers. Preserve the existing deletion semantics
and clarify that the helpers intentionally perform an unguarded DELETE under
those lock preconditions.
🤖 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_discovery.rs`:
- Around line 315-340: Sort and deduplicate the accumulated segment_ids and
fixed_allocations before invoking lock_network_segments_exclusive and
lock_static_address_keys_after_segment_locks. Preserve the existing per-MAC
collection logic, but establish one deterministic ordering for both lock sets so
concurrent discoveries acquire overlapping locks consistently.

In `@crates/site-explorer/src/lib.rs`:
- Around line 2054-2060: Update update_explored_endpoints and the corresponding
reconciliation path around try_reconcile_expected_switch_addresses so
ExpectedSwitch records with no declared static address are identified before
acquiring the write lock or opening a per-switch write transaction. Reload and
filter the relevant rows first, then invoke reconciliation only for switches
requiring changes, while preserving existing address reconciliation behavior for
applicable records.

---

Nitpick comments:
In `@crates/api-core/src/handlers/expected_machine.rs`:
- Around line 883-951: Rename the consumed `transitioned_macs` parameter and its
corresponding argument in `release_removed_fixed_reservations` to a retained-set
name such as `still_declared_macs`, including the call to
`clear_removed_expected_interface_snapshots`. Preserve the existing set contents
and all reservation-release behavior; only clarify the parameter naming at this
boundary.

In `@crates/api-core/src/handlers/expected_switch.rs`:
- Around line 338-345: Update the expected-switch lookup flow around the
NotFoundError construction to validate that ExpectedSwitchRequest provides at
least one usable selector before querying. If both expected_switch_id and
bmc_mac_address are absent or unusable, return InvalidArgument with an
actionable message; otherwise preserve the existing lookup and NotFound
behavior.

In `@crates/api-core/src/handlers/machine.rs`:
- Around line 341-357: Replace the interface_ids collection and subsequent
remove loop with direct draining via extract_if on prepared_interfaces,
selecting non-Bmc interfaces belonging to machine_id and passing each extracted
prepared entry to db::machine_interface::delete_prepared. Remove the unreachable
FailedPrecondition error handling while preserving transaction usage and
deletion behavior.

In `@crates/api-core/src/tests/machine_dhcp.rs`:
- Around line 96-118: Extract the parameterized wait_for_advisory_lock_waiters
helper from crates/api-core/src/tests/machine_dhcp.rs#L96-L118 into the shared
test-support module, replacing the query-text predicate with the existing
pg_blocking_pids-based scoping used in
crates/api-core/src/tests/expected_machine.rs. Delete the duplicate local helper
and call the shared helper from
crates/api-core/src/tests/machine_discovery.rs#L85-L107 and
crates/site-explorer/src/machine_creator.rs#L1568-L1590.

In `@crates/api-db/src/bmc_metadata.rs`:
- Around line 232-244: Update the tracing::info! call in the bmc_interface_owner
branch to log the existing bmc_mac_address local with `%` instead of rereading
bmc_interface_owner.mac_address with `?`; leave the remaining enrichment
assignments unchanged.

In `@crates/api-db/src/expected_machine.rs`:
- Around line 205-224: Update lock_identity_macs to sort identity_macs directly
with sort_unstable(), relying on MacAddress’s Ord implementation; remove the
sort_by_key(ToString::to_string) call while preserving deduplication and lock
acquisition order.

In `@crates/api-db/src/machine_interface_address.rs`:
- Around line 523-568: Update the doc comments for
delete_expected_machine_preallocation_by_interface_and_address and
delete_expected_machine_preallocation_or_legacy_by_interface_and_address to
state that callers must already hold the MAC, fixed-address, and interface-row
locks before invoking these helpers. Preserve the existing deletion semantics
and clarify that the helpers intentionally perform an unguarded DELETE under
those lock preconditions.

In `@crates/api-model/src/expected_machine.rs`:
- Around line 255-265: Ensure ExpectedHostNic::primary has a single normalized
source of truth: make the field private and route reads through
initial_primary_interface(), or normalize it for every role when constructing
snapshots returned by expected_interface_for_mac(). Prevent callers from
observing a raw primary value that conflicts with initial_primary_interface(),
while preserving the existing Host, DpuOs, and DpuBmc semantics.

In `@crates/rpc/src/lib.rs`:
- Around line 580-636: Consolidate the duplicated ExpectedInterfaceRole and
ExpectedInterfaceIpAllocation serde adapters into one declarative macro that
accepts the enum type and canonical-name/alias mappings. Reuse the macro for
both implementations, preserving their numeric handling, optional values,
validation, canonical serialization names, and distinct alias tables.

In `@crates/site-explorer/src/machine_creator.rs`:
- Around line 705-725: Update
crates/site-explorer/src/machine_creator.rs:705-725 and 828-846 to replace each
manual host_nics MAC lookup with
ExpectedMachineData::expected_interface_for_mac, then apply the settled boot
choice only through the helper’s normalized result and existing role.is_host()
behavior; use mac_address at 705-725 and declared_mac at 828-846, ensuring both
prediction paths produce the same expected-interface snapshot.
🪄 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: f4e26a71-1e80-4f47-9a2a-479743628ba6

📥 Commits

Reviewing files that changed from the base of the PR and between 9e7cadc and 35773d0.

⛔ 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 (58)
  • crates/admin-cli/src/expected_machines/add/args.rs
  • crates/admin-cli/src/expected_machines/common.rs
  • crates/admin-cli/src/expected_machines/patch/args.rs
  • crates/admin-cli/src/expected_machines/tests.rs
  • crates/admin-cli/src/expected_machines/update/mod.rs
  • crates/admin-cli/src/rpc.rs
  • crates/api-core/src/dhcp/discover.rs
  • crates/api-core/src/dhcp/v6.rs
  • crates/api-core/src/handlers/bmc_endpoint_explorer.rs
  • crates/api-core/src/handlers/expected_machine.rs
  • crates/api-core/src/handlers/expected_power_shelf.rs
  • crates/api-core/src/handlers/expected_switch.rs
  • crates/api-core/src/handlers/machine.rs
  • crates/api-core/src/handlers/machine_discovery.rs
  • crates/api-core/src/handlers/machine_interface.rs
  • crates/api-core/src/handlers/machine_interface_address.rs
  • crates/api-core/src/handlers/power_shelf.rs
  • crates/api-core/src/handlers/switch.rs
  • crates/api-core/src/tests/expected_machine.rs
  • crates/api-core/src/tests/expected_switch.rs
  • crates/api-core/src/tests/machine_admin_force_delete.rs
  • crates/api-core/src/tests/machine_dhcp.rs
  • crates/api-core/src/tests/machine_discovery.rs
  • crates/api-core/src/tests/machine_states.rs
  • crates/api-core/src/tests/switch.rs
  • crates/api-core/tests/integration/expected_power_shelf_static_address.rs
  • crates/api-core/tests/integration/machine_boot_interfaces.rs
  • crates/api-core/tests/integration/power_shelf_delete.rs
  • crates/api-core/tests/integration/static_address_management.rs
  • crates/api-db/migrations/20260722120000_expected_interface_allocation_state.sql
  • crates/api-db/src/bmc_metadata.rs
  • crates/api-db/src/bmc_metadata/tests.rs
  • crates/api-db/src/expected_machine.rs
  • crates/api-db/src/expected_machine/test_allocation_state_migration.rs
  • crates/api-db/src/expected_machine/tests.rs
  • crates/api-db/src/expected_switch.rs
  • crates/api-db/src/expected_switch/tests.rs
  • crates/api-db/src/machine.rs
  • crates/api-db/src/machine_interface.rs
  • crates/api-db/src/machine_interface/tests.rs
  • crates/api-db/src/machine_interface_address.rs
  • crates/api-db/src/network_segment.rs
  • crates/api-db/src/predicted_machine_interface.rs
  • crates/api-model/src/expected_machine.rs
  • crates/api-model/src/machine/mod.rs
  • crates/api-model/src/predicted_machine_interface.rs
  • crates/machine-a-tron/src/machine_a_tron.rs
  • crates/machine-controller/src/boot_interface.rs
  • crates/rpc/build.rs
  • crates/rpc/proto/forge.proto
  • crates/rpc/src/lib.rs
  • crates/rpc/src/model/expected_machine.rs
  • crates/site-explorer/Cargo.toml
  • crates/site-explorer/src/lib.rs
  • crates/site-explorer/src/machine_creator.rs
  • crates/site-explorer/tests/integration/machine_creator.rs
  • crates/site-explorer/tests/integration/reconcile.rs
  • rest-api/proto/core/src/v1/nico_nico.proto

Comment thread crates/api-core/src/handlers/machine_discovery.rs
Comment thread crates/site-explorer/src/lib.rs
As it stood, `ExpectedMachine.host_nics` could reserve a host NIC with `fixed_ip`, but every entry was treated as a host data interface. There was no way to say "this is the DPU OS/BMC" or choose `Dynamic`, `Fixed`, or `Retained` for that interface.

So, add `Host`, `DpuOs`, and `DpuBmc` roles plus an allocation policy for every entry. Fixed addresses find their segment by prefix containment, while DHCP uses the relay/link segment; `network_segment_type` is an optional check for either path. Retained DHCP allocations use the existing `Static` representation once an address has been selected.

The protobuf fields are optional, and old `host_nics` payloads still infer `Fixed` from `fixed_ip` and `Dynamic` otherwise. ExpectedMachine updates only reconcile never-associated reservations, so managed, preserved, and operator-owned interfaces are left alone. `host_nics` also gets a `jsonb_path_ops` GIN index now that DHCP and Site Explorer hit that lookup regularly.

Tests cover the old payloads, every role/policy combination, reservation transitions, DHCPv4/v6 retention, and the managed-state boundaries. `HostBmc`, DPU loopbacks, and the `ExpectedInterface`/`interfaces` rename stay in separate follow-ups.

This supports NVIDIA#3990

Signed-off-by: Chet Nichols III <chetn@nvidia.com>
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