Skip to content

feat: Managed power shelf decommissioning - #4680

Open
ericpretzel wants to merge 8 commits into
NVIDIA:mainfrom
ericpretzel:managed-power-shelf-decommissioning
Open

feat: Managed power shelf decommissioning#4680
ericpretzel wants to merge 8 commits into
NVIDIA:mainfrom
ericpretzel:managed-power-shelf-decommissioning

Conversation

@ericpretzel

Copy link
Copy Markdown
Contributor

Summary

Adds managed power-shelf decommissioning, including Core API support, state-controller orchestration, Flow integration, and admin CLI commands.

Decommissioning workflow

Preparing
  → VerifyingDhcpRelease
      → FactoryResetBmc
      → WaitingForBmcDhcpAcknowledgement
  → Decommissioned
  • Suppresses Site Explorer before starting destructive operations.
  • Resets the power shelf PMC through Redfish.
  • Suppresses PMC DHCP only after the Redfish reset succeeds.
  • Waits for DHCP suppression acknowledgement before marking the power shelf decommissioned.

API and CLI

  • Adds the managed power-shelf decommission request path.
  • Adds the DeleteDecommissionedPowerShelf RPC and authorization rules.
  • Allows permanent deletion only after the power shelf reaches Decommissioned.
  • Permanent deletion removes credentials, interfaces, retained boot records, and suppression records while preserving expected inventory.
  • Adds admin CLI commands:
    • nico-admin-cli power-shelf decommission
    • nico-admin-cli power-shelf delete-decommissioned
  • Adds generated CLI reference pages for both commands.
  • Adds the database flag used to request decommissioning.

Flow integration

  • Adds power-shelf decommission control support to the NICo component-manager implementation.
  • Invokes the Core decommissioning RPC and preserves polling behavior while decommissioning is in progress.
  • Reports completion after the power shelf reaches Decommissioned.

Validation

I have no managed power shelf in my dev environment so this code path is untested. Will be able to test in other sites that do have racks once this is merged. It has very similar logic to that of managed host decommissioning which does successfully work in my dev environment, so hopefully it will need minimal changes to become ready.

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

Will need manual testing.

  • Unit tests added/updated
  • Integration tests added/updated
  • Manual testing performed
  • No testing required (docs, internal refactor, etc.)

@ericpretzel
ericpretzel requested review from a team and polarweasel as code owners August 6, 2026 18:07
@coderabbitai

coderabbitai Bot commented Aug 6, 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

Summary by CodeRabbit

  • New Features

    • Added commands to decommission ready power shelves and permanently delete decommissioned shelves.
    • Added API support for both lifecycle operations.
    • Decommissioning now performs BMC reset, network cleanup, and resource removal.
  • Safety and Reliability

    • Added state and stale-request validation.
  • Documentation

    • Added command reference pages and usage examples.
  • Tests

    • Added coverage for parsing, lifecycle transitions, validation, and API behavior.

Walkthrough

The change adds managed power-shelf decommissioning and permanent deletion. It adds lifecycle states, database tracking, controller cleanup, Forge RPCs, Flow integration, authorization, Admin CLI commands, documentation, and integration tests.

Changes

Power shelf lifecycle

Layer / File(s) Summary
Lifecycle contracts and persistence
crates/api-db/..., crates/api-model/..., crates/rpc/proto/..., rest-api/proto/...
Power shelves track decommission_requested. Database operations validate shelf state and controller-state versions. Model states cover preparation, DHCP verification, and Decommissioned. RPC contracts and machine-interface queries support the lifecycle.
Controller decommissioning workflow
crates/power-shelf-controller/src/*.rs
The controller processes BMC suppression, BMC factory reset, DHCP suppression, interface deletion recording, acknowledgement polling, and transition to Decommissioned.
API lifecycle operations
crates/api-core/src/api.rs, crates/api-core/src/auth/..., crates/api-core/src/handlers/..., crates/api-core/src/tests/...
Forge handlers authorize and validate both operations. Deletion removes credentials, interfaces, suppression records, and the power shelf. Integration tests cover invalid states and the complete workflow.
Flow gRPC integration
rest-api/flow/internal/nicoapi/...
The Flow client calls the Core decommission RPC with a timeout and wraps errors. Tests verify request forwarding, success, and error propagation.
Admin CLI lifecycle commands
crates/admin-cli/src/power_shelf/..., docs/manuals/nico-admin-cli/commands/power-shelf/...
The CLI adds decommission and delete-decommissioned commands with typed shelf IDs, execution handlers, parsing tests, and command documentation.

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

Sequence Diagram(s)

sequenceDiagram
  participant AdminCLI
  participant FlowGrpcClient
  participant ForgeApi
  participant PowerShelfHandler
  participant PowerShelfController
  participant Database
  participant BMCAndDHCPSystems

  AdminCLI->>FlowGrpcClient: DecommissionPowerShelf(PowerShelfId)
  FlowGrpcClient->>ForgeApi: DecommissionPowerShelf(PowerShelfId)
  ForgeApi->>PowerShelfHandler: record decommission request
  PowerShelfHandler->>Database: set decommission_requested
  PowerShelfController->>Database: transition to Preparing
  PowerShelfController->>BMCAndDHCPSystems: suppress BMC and verify DHCP release
  PowerShelfController->>Database: transition to Decommissioned
  AdminCLI->>ForgeApi: DeleteDecommissionedPowerShelf(PowerShelfId)
  ForgeApi->>PowerShelfHandler: delete decommissioned shelf
  PowerShelfHandler->>Database: remove interfaces, suppressions, and shelf
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the primary change: managed power-shelf decommissioning.
Description check ✅ Passed The description directly covers the decommissioning workflow, API, CLI, database, Flow integration, testing, and deletion behavior.
Docstring Coverage ✅ Passed Docstring coverage is 95.45% 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.

@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 (3)
crates/api-core/src/tests/power_shelf_state_controller/decommissioning.rs (2)

238-263: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Exercise the retained boot record cleanup.

These assertions cover the shelf row, the interfaces, and both suppression subsystems. They do not cover retained boot records.

db::machine_interface::delete writes a retained_boot_interfaces row only for an interface that carries a boot_interface_id. seed_pmc_endpoint at Lines 74-85 does not set that column, so the db::retained_boot_interface::take_by_mac call at Line 191 of crates/api-core/src/handlers/power_shelf.rs is a no-op in this test. Removing that call would not fail the suite, even though the PR objectives state that permanent deletion removes retained boot records.

Set boot_interface_id in the fixture and assert the retained record is absent after deletion.

💚 Proposed fixture and assertion change
     let interface_id: MachineInterfaceId = sqlx::query_scalar(
         "INSERT INTO machine_interfaces
              (power_shelf_id, association_type, segment_id, mac_address,
-              primary_interface, hostname, interface_type)
-         VALUES ($1, 'PowerShelf', $2, $3, false, 'pmc', 'Bmc')
+              primary_interface, hostname, interface_type, boot_interface_id)
+         VALUES ($1, 'PowerShelf', $2, $3, false, 'pmc', 'Bmc', 'NIC.Slot.1-1')
          RETURNING id",
     )

Then add the assertion after Line 261:

     assert!(
         db::bmc_suppression::find(&pool, pmc_mac, BmcSuppressionSubsystem::Dhcp)
             .await?
             .is_none()
     );
+    assert!(
+        db::retained_boot_interface::find_by_mac(conn.as_mut(), pmc_mac)
+            .await?
+            .is_none()
+    );
     Ok(())

Confirm the exact retained_boot_interface read helper name before applying.

🤖 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/power_shelf_state_controller/decommissioning.rs`
around lines 238 - 263, Update the decommissioning test fixture created by
seed_pmc_endpoint to assign a boot_interface_id, ensuring interface deletion
creates a retained boot record. After delete_decommissioned_power_shelf
completes, use the existing db::retained_boot_interface read helper to assert
that the retained record for the fixture’s boot interface or MAC is absent,
alongside the existing cleanup assertions.

104-130: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add coverage for the NotFound path.

This test pins the FailedPrecondition contract for both RPCs. Neither new handler has coverage for an unknown PowerShelfId, which returns CarbideError::NotFoundError at Lines 110-113 and Lines 163-166 of crates/api-core/src/handlers/power_shelf.rs.

The NotFound code is part of the operator-facing contract for the CLI and the Flow client. A test prevents a silent regression to Internal or FailedPrecondition.

If you add these cases, use scenarios! with Outcome over the shelf-identifier input, because the same two operations would then run with different inputs.

As per coding guidelines: "Use a table whenever two or more tests invoke the same operation with different inputs, but keep genuinely distinct tests standalone."

🤖 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/power_shelf_state_controller/decommissioning.rs`
around lines 104 - 130, Extend decommission_requires_ready_power_shelf coverage
to include unknown PowerShelfId inputs for both decommission_power_shelf and
delete_decommissioned_power_shelf, asserting Code::NotFound. Refactor the shared
operation cases into scenarios! with Outcome over the shelf identifier, while
retaining the existing initializing-shelf FailedPrecondition assertions.

Source: Coding guidelines

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

145-151: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Move the BMC MAC resolution rule onto PowerShelf.

power_shelf_bmc_mac duplicates bmc_mac in crates/power-shelf-controller/src/decommissioning.rs (Lines 33-43). Both apply the same rule: prefer bmc_info.mac, then fall back to bmc_mac_address. The two copies differ only in the failure type.

The rule is a property of PowerShelf. Define it once as a method on the model in crates/api-model, then let each caller adapt the Option to its own error type. This prevents the two copies from drifting when BMC identity handling changes.

As per coding guidelines: "When behavior primarily operates on one type, define it as a method on that type; use free functions only for multi-type logic or utilities with no natural owner."

♻️ Proposed consolidation

Add the method to PowerShelf in crates/api-model/src/power_shelf/mod.rs:

impl PowerShelf {
    /// Resolves the PMC MAC, preferring the explored BMC info over the
    /// directly recorded address.
    pub fn bmc_mac(&self) -> Option<mac_address::MacAddress> {
        self.bmc_info
            .as_ref()
            .and_then(|info| info.mac)
            .or(self.bmc_mac_address)
    }
}

Then remove the local helper:

-fn power_shelf_bmc_mac(power_shelf: &PowerShelf) -> Option<mac_address::MacAddress> {
-    power_shelf
-        .bmc_info
-        .as_ref()
-        .and_then(|info| info.mac)
-        .or(power_shelf.bmc_mac_address)
-}

Update the two call sites at Lines 168 and 194 to power_shelf.bmc_mac(), and have crates/power-shelf-controller/src/decommissioning.rs map the Option to StateHandlerError::MissingData.

🤖 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/power_shelf.rs` around lines 145 - 151, Move the
BMC MAC resolution rule into a `PowerShelf::bmc_mac` method in the model,
preferring `bmc_info.mac` and falling back to `bmc_mac_address`. Remove the
local `power_shelf_bmc_mac` helper and update its call sites to use the method;
likewise update `decommissioning.rs` to call `PowerShelf::bmc_mac()` and adapt
the returned `Option` to `StateHandlerError::MissingData`.

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.

Inline comments:
In `@crates/api-core/src/auth/internal_rbac_rules.rs`:
- Around line 728-731: Update the DeleteDecommissionedPowerShelf permission
grant in the RBAC rules to remove Flow, leaving only ForgeAdminCLI and
Machineatron as authorized principals.

In `@crates/api-db/src/power_shelf.rs`:
- Around line 343-356: Update set_decommission_requested to make the Ready-state
validation atomic with the update: lock the selected row or add the expected
controller_state_version to the UPDATE predicate and bind that expected version.
Ensure the update succeeds only when the row still matches the validated state,
while preserving the existing error handling and return behavior.

In `@crates/api-model/src/power_shelf/mod.rs`:
- Around line 326-328: The SLA mapping currently disables overdue detection for
active decommissioning states. Update the match handling in the power-shelf
state SLA definition so Preparing and VerifyingDhcpRelease use bounded SLA
policies, while Decommissioned continues using StateSla::no_sla(); add
table-driven rows covering both active states and their expected policy.

In
`@docs/manuals/nico-admin-cli/commands/power-shelf/power-shelf-decommission.md`:
- Around line 15-17: The power-shelf lifecycle contract is missing from the
decommission and delete documentation. Update the canonical generated sources
under crates/admin-cli/src/power_shelf/ to document the Ready precondition,
asynchronous decommission sequence, waits, errors, retries, and
Decommissioned-only deletion cleanup order; regenerate both
docs/manuals/nico-admin-cli/commands/power-shelf/power-shelf-decommission.md:15-17
and
docs/manuals/nico-admin-cli/commands/power-shelf/power-shelf-delete-decommissioned.md:15-17,
then validate the rendered output.

---

Nitpick comments:
In `@crates/api-core/src/handlers/power_shelf.rs`:
- Around line 145-151: Move the BMC MAC resolution rule into a
`PowerShelf::bmc_mac` method in the model, preferring `bmc_info.mac` and falling
back to `bmc_mac_address`. Remove the local `power_shelf_bmc_mac` helper and
update its call sites to use the method; likewise update `decommissioning.rs` to
call `PowerShelf::bmc_mac()` and adapt the returned `Option` to
`StateHandlerError::MissingData`.

In `@crates/api-core/src/tests/power_shelf_state_controller/decommissioning.rs`:
- Around line 238-263: Update the decommissioning test fixture created by
seed_pmc_endpoint to assign a boot_interface_id, ensuring interface deletion
creates a retained boot record. After delete_decommissioned_power_shelf
completes, use the existing db::retained_boot_interface read helper to assert
that the retained record for the fixture’s boot interface or MAC is absent,
alongside the existing cleanup assertions.
- Around line 104-130: Extend decommission_requires_ready_power_shelf coverage
to include unknown PowerShelfId inputs for both decommission_power_shelf and
delete_decommissioned_power_shelf, asserting Code::NotFound. Refactor the shared
operation cases into scenarios! with Outcome over the shelf identifier, while
retaining the existing initializing-shelf FailedPrecondition assertions.
🪄 Autofix

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: 5638f7c0-c109-4604-ab42-73c9c7f6fbd6

📥 Commits

Reviewing files that changed from the base of the PR and between 6ca668d and 99824b7.

⛔ Files ignored due to path filters (2)
  • rest-api/proto/core/gen/v1/nico_nico.pb.go is excluded by !**/*.pb.go, !**/gen/**, !rest-api/**/*.pb.go
  • rest-api/proto/core/gen/v1/nico_nico_grpc.pb.go is excluded by !**/*.pb.go, !**/gen/**, !rest-api/**/*.pb.go, !rest-api/**/*_grpc.pb.go
📒 Files selected for processing (30)
  • crates/admin-cli/src/power_shelf/decommission/args.rs
  • crates/admin-cli/src/power_shelf/decommission/cmd.rs
  • crates/admin-cli/src/power_shelf/decommission/mod.rs
  • crates/admin-cli/src/power_shelf/delete_decommissioned/args.rs
  • crates/admin-cli/src/power_shelf/delete_decommissioned/cmd.rs
  • crates/admin-cli/src/power_shelf/delete_decommissioned/mod.rs
  • crates/admin-cli/src/power_shelf/mod.rs
  • crates/admin-cli/src/power_shelf/tests.rs
  • crates/api-core/src/api.rs
  • crates/api-core/src/auth/internal_rbac_rules.rs
  • crates/api-core/src/handlers/power_shelf.rs
  • crates/api-core/src/tests/power_shelf_state_controller/decommissioning.rs
  • crates/api-core/src/tests/power_shelf_state_controller/mod.rs
  • crates/api-db/migrations/20260806120000_power_shelf_decommission_requested.sql
  • crates/api-db/src/machine_interface.rs
  • crates/api-db/src/power_shelf.rs
  • crates/api-model/src/power_shelf/mod.rs
  • crates/power-shelf-controller/src/decommissioning.rs
  • crates/power-shelf-controller/src/handler.rs
  • crates/power-shelf-controller/src/io.rs
  • crates/power-shelf-controller/src/lib.rs
  • crates/power-shelf-controller/src/ready.rs
  • crates/rpc/proto/forge.proto
  • docs/manuals/nico-admin-cli/commands/power-shelf/power-shelf-decommission.md
  • docs/manuals/nico-admin-cli/commands/power-shelf/power-shelf-delete-decommissioned.md
  • docs/manuals/nico-admin-cli/commands/power-shelf/power-shelf.md
  • rest-api/flow/internal/nicoapi/grpc.go
  • rest-api/flow/internal/nicoapi/grpc_test.go
  • rest-api/flow/internal/nicoapi/mod.go
  • rest-api/proto/core/src/v1/nico_nico.proto
💤 Files with no reviewable changes (1)
  • rest-api/flow/internal/nicoapi/mod.go

Comment on lines +728 to +731
x.perm(
"DeleteDecommissionedPowerShelf",
vec![ForgeAdminCLI, Machineatron, Flow],
);

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.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Find Flow callers of the two new power-shelf lifecycle RPCs.
set -uo pipefail

echo "=== Go references in rest-api/flow ==="
rg -n -C 3 'DeleteDecommissionedPowerShelf|DecommissionPowerShelf' rest-api/flow

echo "=== Go references repository-wide ==="
rg -n --type=go -C 2 'DeleteDecommissionedPowerShelf'

echo "=== Flow interface declarations ==="
fd -t f 'mod.go' rest-api/flow --exec rg -n -C 4 'PowerShelf'

Repository: NVIDIA/infra-controller

Length of output: 7703


🏁 Script executed:

#!/bin/bash
set -uo pipefail

echo "=== RBAC rule context ==="
sed -n '700,745p' crates/api-core/src/auth/internal_rbac_rules.rs
sed -n '285,315p' crates/api-core/src/auth/internal_rbac_rules.rs

echo "=== All repository references to DeleteDecommissionedPowerShelf ==="
rg -n -C 3 'DeleteDecommissionedPowerShelf' . || true

echo "=== All repository references to power-shelf delete operations ==="
rg -n -C 2 'DeletePowerShelf|AdminForceDeletePowerShelf|DecommissionPowerShelf' . || true

echo "=== RPC declarations and implementations ==="
rg -n -C 4 'rpc (DeleteDecommissionedPowerShelf|DeletePowerShelf|AdminForceDeletePowerShelf|DecommissionPowerShelf)|DeleteDecommissionedPowerShelf\(' . || true

Repository: NVIDIA/infra-controller

Length of output: 50379


🏁 Script executed:

#!/bin/bash
set -uo pipefail

echo "=== Non-generated application references ==="
rg -n --glob '!**/gen/**' --glob '!**/*.proto' \
  'DeleteDecommissionedPowerShelf|delete_decommissioned_power_shelf|deleteDecommissionedPowerShelf|decommissioned power shelf' \
  crates rest-api || true

echo "=== Flow references, excluding mocks and generated code ==="
rg -n --glob '!**/gen/**' \
  'DeleteDecommissionedPowerShelf|delete_decommissioned_power_shelf|DeletePowerShelf|AdminForceDeletePowerShelf' \
  rest-api/flow || true

echo "=== Power-shelf deletion implementations and authorization checks ==="
rg -n -C 4 --glob '*.rs' \
  'delete_decommissioned|DeleteDecommissioned|admin_force_delete_power_shelf|delete_power_shelf|PowerShelf.*delete|delete.*PowerShelf' \
  crates || true

Repository: NVIDIA/infra-controller

Length of output: 27312


🏁 Script executed:

#!/bin/bash
set -uo pipefail

echo "=== Permanent deletion handler ==="
sed -n '135,225p' crates/api-core/src/handlers/power_shelf.rs

echo "=== RBAC enforcement references ==="
rg -n -C 4 \
  'internal_rbac_rules|check_permission|permission|authorize|authorization' \
  crates/api-core/src/auth crates/api-core/src | head -n 220

echo "=== Direct application callers of the permanent-deletion API ==="
rg -n --glob '*.rs' --glob '!**/gen/**' \
  'delete_decommissioned_power_shelf\(' crates rest-api | \
  grep -v 'fn delete_decommissioned_power_shelf' || true

Repository: NVIDIA/infra-controller

Length of output: 20935


Restrict DeleteDecommissionedPowerShelf to admin principals.

Only the admin CLI and tests call this permanent-deletion RPC. Remove Flow from the grant list. The RPC deletes credentials, interfaces, retained boot records, suppression records, and the shelf.

🤖 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/auth/internal_rbac_rules.rs` around lines 728 - 731,
Update the DeleteDecommissionedPowerShelf permission grant in the RBAC rules to
remove Flow, leaving only ForgeAdminCLI and Machineatron as authorized
principals.

Comment thread crates/api-db/src/power_shelf.rs Outdated
Comment thread crates/api-model/src/power_shelf/mod.rs Outdated
Comment on lines +15 to +17
## DESCRIPTION

Start decommissioning a managed power shelf

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.

📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- target files ---'
for f in \
  docs/manuals/nico-admin-cli/commands/power-shelf/power-shelf-decommission.md \
  docs/manuals/nico-admin-cli/commands/power-shelf/power-shelf-delete-decommissioned.md
do
  if [ -f "$f" ]; then
    wc -l "$f"
    cat -n "$f"
  else
    printf 'MISSING %s\n' "$f"
  fi
done

printf '%s\n' '--- related power-shelf documentation and source references ---'
rg -n -i \
  'decommission(ed|ing)?|power shelf|power_shelf|PMC reset|DHCP|boot record|suppression|credential|interface' \
  docs crates rest-api helm helm-prereqs deploy README.md \
  --glob '*.md' --glob '*.rs' --glob '*.go' --glob '*.yaml' --glob '*.yml' --glob '*.toml' \
  | head -n 400

Repository: NVIDIA/infra-controller

Length of output: 50381


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- candidate generated-doc sources ---'
rg -n -i \
  'power-shelf-decommission|power-shelf-delete-decommissioned|delete-decommissioned|Start decommissioning a managed power shelf|Permanently delete a decommissioned managed power shelf' \
  . --glob '!docs/manuals/nico-admin-cli/commands/power-shelf/power-shelf-decommission.md' \
  --glob '!docs/manuals/nico-admin-cli/commands/power-shelf/power-shelf-delete-decommissioned.md' \
  | head -n 300

printf '%s\n' '--- power-shelf file inventory ---'
git ls-files | rg -i 'power.?shelf|power_shelf|nico-admin-cli' | head -n 300

Repository: NVIDIA/infra-controller

Length of output: 22610


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- CLI command definitions ---'
for f in \
  crates/admin-cli/src/power_shelf/mod.rs \
  crates/admin-cli/src/power_shelf/decommission/mod.rs \
  crates/admin-cli/src/power_shelf/decommission/args.rs \
  crates/admin-cli/src/power_shelf/decommission/cmd.rs \
  crates/admin-cli/src/power_shelf/delete_decommissioned/mod.rs \
  crates/admin-cli/src/power_shelf/delete_decommissioned/args.rs \
  crates/admin-cli/src/power_shelf/delete_decommissioned/cmd.rs
do
  printf '\n### %s\n' "$f"
  cat -n "$f"
done

printf '%s\n' '--- state-machine documentation ---'
wc -l docs/architecture/state_machines/power_shelf.md
cat -n docs/architecture/state_machines/power_shelf.md

printf '%s\n' '--- controller outlines ---'
ast-grep outline crates/power-shelf-controller/src/decommissioning.rs
ast-grep outline crates/power-shelf-controller/src/deleting.rs
ast-grep outline crates/api-core/src/handlers/power_shelf.rs

Repository: NVIDIA/infra-controller

Length of output: 23436


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- decommission/delete implementation and tests ---'
for f in \
  crates/power-shelf-controller/src/decommissioning.rs \
  crates/power-shelf-controller/src/deleting.rs \
  crates/api-core/src/tests/power_shelf_state_controller/decommissioning.rs \
  crates/api-core/tests/integration/power_shelf_delete.rs \
  crates/api-db/migrations/20260806120000_power_shelf_decommission_requested.sql
do
  if [ -f "$f" ]; then
    printf '\n### %s\n' "$f"
    wc -l "$f"
    cat -n "$f"
  fi
done

Repository: NVIDIA/infra-controller

Length of output: 28547


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- lifecycle symbols and side effects ---'
rg -n -i \
  'Ready|Decommissioned|Decommissioning|Deleting|PMC|reset|DHCP|acknowledge|boot|suppression|credential|interface|retry|failed|error|transition|decommission_requested' \
  crates/power-shelf-controller/src \
  crates/api-core/src/handlers/power_shelf.rs \
  crates/api-core/src/tests/power_shelf_state_controller \
  crates/api-core/tests/integration/power_shelf_delete.rs \
  docs/architecture/state_machines/power_shelf.md \
  | head -n 700

printf '%s\n' '--- all power-shelf state declarations/usages ---'
rg -n \
  'enum PowerShelfState|PowerShelfState::|power_shelf_state|decommission' \
  crates/api-model crates/api-db crates/api-core/src/handlers/power_shelf.rs \
  crates/power-shelf-controller/src \
  | head -n 700

Repository: NVIDIA/infra-controller

Length of output: 50379


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- API lifecycle handlers ---'
cat -n crates/api-core/src/handlers/power_shelf.rs | sed -n '95,215p'

printf '%s\n' '--- database deletion implementation ---'
rg -n 'fn final_delete|final_delete|take_by_mac|delete_many|delete_bmc_root_credentials_by_mac' \
  crates/api-db crates/api-core/src/handlers crates --glob '*.rs' \
  | head -n 120

printf '%s\n' '--- relevant deletion helpers ---'
for f in \
  crates/api-db/src/power_shelf.rs \
  crates/api-db/src/machine_interface.rs \
  crates/api-db/src/retained_boot_interface.rs \
  crates/api-db/src/bmc_suppression.rs \
  crates/api-core/src/handlers/credential.rs
do
  if [ -f "$f" ]; then
    printf '\n### %s\n' "$f"
    rg -n -A 45 -B 8 'final_delete|take_by_mac|delete_many|delete_bmc_root_credentials_by_mac' "$f" || true
  fi
done

printf '%s\n' '--- controller state model and ready transition ---'
rg -n -A 35 -B 10 \
  'enum PowerShelfControllerState|Decommissioned|decommission_requested|handle_ready' \
  crates/api-model/src/power_shelf crates/power-shelf-controller/src/ready.rs \
  crates/power-shelf-controller/src/handler.rs crates/api-db/src/power_shelf.rs \
  | head -n 500

Repository: NVIDIA/infra-controller

Length of output: 50379


🏁 Script executed:

#!/bin/bash
set -eu

python3 - <<'PY'
from pathlib import Path

handler = Path("crates/api-core/src/handlers/power_shelf.rs").read_text()
decommissioning = Path("crates/power-shelf-controller/src/decommissioning.rs").read_text()

checks = {
    "decommission requires Ready": "must be Ready to be decommissioned" in handler,
    "decommission request is persisted": "set_decommission_requested" in handler,
    "waits for Site Explorer acknowledgement": "waiting for Site Explorer suppression acknowledgement" in decommissioning,
    "performs BMC factory reset": "bmc_reset_to_defaults" in decommissioning,
    "suppresses DHCP and records interface deletion": (
        "BmcSuppressionSubsystem::Dhcp" in decommissioning
        and "machine_interface::record_deletion" in decommissioning
    ),
    "waits for DHCP acknowledgement": "WaitingForBmcDhcpAcknowledgement" in decommissioning,
    "reaches Decommissioned after DHCP acknowledgement": (
        "PowerShelfControllerState::Decommissioned" in decommissioning
    ),
    "deletion requires Decommissioned": "must be Decommissioned before deletion" in handler,
    "credentials are deleted before the transaction": (
        "delete_bmc_root_credentials_by_mac" in handler
        and handler.index("delete_bmc_root_credentials_by_mac")
        < handler.index("db::machine_interface::find_by_power_shelf_id")
    ),
    "deletion removes interfaces and retained boot records": (
        "machine_interface::delete" in handler
        and "retained_boot_interface::take_by_mac" in handler
    ),
    "deletion removes both suppression records": (
        handler.count("db::bmc_suppression::delete_many") >= 2
    ),
}

for name, passed in checks.items():
    print(f"{'PASS' if passed else 'FAIL'}: {name}")

# Show the source ordering that determines the retry-safe boundary.
start = handler.index("pub(crate) async fn delete_decommissioned_power_shelf")
end = handler.find("\npub(crate)", start + 1)
if end == -1:
    end = min(len(handler), start + 5000)
print("\n--- delete handler operation order ---")
for i, line in enumerate(handler[start:end].splitlines(), 1):
    if any(term in line for term in (
        "find_by_id", "require_decommissioned", "delete_bmc_root",
        "begin", "machine_interface::delete", "retained_boot_interface",
        "bmc_suppression::delete_many", "final_delete", "commit",
    )):
        print(f"{i:03}: {line.strip()}")
PY

Repository: NVIDIA/infra-controller

Length of output: 1530


Document the managed power-shelf lifecycle contract.

  • Document the Ready precondition and asynchronous sequence: ReadyPreparing → Site Explorer suppression acknowledgement → BMC factory reset → BMC DHCP suppression acknowledgement → Decommissioned.
  • Document waits, errors, and retry behavior.
  • Document that deletion requires Decommissioned and removes BMC credentials, interfaces, retained boot-interface records, suppression records, and the shelf row. State that credential deletion occurs before database cleanup and that later failures leave the shelf available for retry.
  • Update the canonical generated sources under crates/admin-cli/src/power_shelf/, regenerate both pages, and validate the rendered output.
📍 Affects 2 files
  • docs/manuals/nico-admin-cli/commands/power-shelf/power-shelf-decommission.md#L15-L17 (this comment)
  • docs/manuals/nico-admin-cli/commands/power-shelf/power-shelf-delete-decommissioned.md#L15-L17
🤖 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 `@docs/manuals/nico-admin-cli/commands/power-shelf/power-shelf-decommission.md`
around lines 15 - 17, The power-shelf lifecycle contract is missing from the
decommission and delete documentation. Update the canonical generated sources
under crates/admin-cli/src/power_shelf/ to document the Ready precondition,
asynchronous decommission sequence, waits, errors, retries, and
Decommissioned-only deletion cleanup order; regenerate both
docs/manuals/nico-admin-cli/commands/power-shelf/power-shelf-decommission.md:15-17
and
docs/manuals/nico-admin-cli/commands/power-shelf/power-shelf-delete-decommissioned.md:15-17,
then validate the rendered output.

Source: Coding guidelines

@thossain-nv thossain-nv added the rest-api Add this label when an issue or PR concerns NICo REST API label Aug 6, 2026 — with ChatGPT Codex Connector
@zhaozhongn
zhaozhongn requested a review from spydaNVIDIA August 6, 2026 19:45
@github-actions

github-actions Bot commented Aug 6, 2026

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-08-07 02:27:03 UTC | Commit: e2b56ec

@github-actions

github-actions Bot commented Aug 6, 2026

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.

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

356-359: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use query_scalar for the single returned column.

PowerShelfId supports both SQLx scalar decoding and FromRow, so query_as is valid. query_scalar maps RETURNING id directly and matches existing usage for scalar PowerShelfId results.

🤖 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/power_shelf.rs` around lines 356 - 359, Update the query
construction in the PowerShelf update flow to use SQLx’s query_scalar API for
the single returned id column instead of query_as::<_, PowerShelfId>. Preserve
the existing bindings and fetch_optional behavior, and continue returning the
scalar PowerShelfId result.

Source: Learnings

🤖 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/power_shelf.rs`:
- Around line 356-359: Update the query construction in the PowerShelf update
flow to use SQLx’s query_scalar API for the single returned id column instead of
query_as::<_, PowerShelfId>. Preserve the existing bindings and fetch_optional
behavior, and continue returning the scalar PowerShelfId result.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 6f1f9538-5a65-44d0-bcc8-30848f120e74

📥 Commits

Reviewing files that changed from the base of the PR and between 99824b7 and e2b56ec.

📒 Files selected for processing (3)
  • crates/api-core/src/handlers/power_shelf.rs
  • crates/api-db/migrations/20260806120001_power_shelf_decommission_requested.sql
  • crates/api-db/src/power_shelf.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/api-core/src/handlers/power_shelf.rs

Ok(Response::new(()))
}

fn require_decommissioned(power_shelf: &PowerShelf) -> Result<(), CarbideError> {

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.

This helper func seems overdone. Dont think it adds much and it only has one caller.

)))
}

fn power_shelf_bmc_mac(power_shelf: &PowerShelf) -> Option<mac_address::MacAddress> {

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.

ditto here: this helper function is not adding much

Comment on lines +183 to +190
let mut txn = api.txn_begin().await?;
let power_shelf = db_power_shelf::find_by_id(&mut txn, &power_shelf_id)
.await?
.ok_or_else(|| CarbideError::NotFoundError {
kind: "power_shelf",
id: power_shelf_id.to_string(),
})?;
require_decommissioned(&power_shelf)?;

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.

Isnt this a duplicate of lines 165-172 from above?

// Secret-store operations cannot participate in the database transaction.
// Delete the credential first so a failure leaves the terminal shelf
// available for a safe retry.
if let Some(bmc_mac) = bmc_mac {

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.

Shouldnt we return an error if the bmc mac of a powershelf cant be found?

db::retained_boot_interface::take_by_mac(&mut txn, interface.mac_address, None).await?;
}

if let Some(bmc_mac) = power_shelf_bmc_mac(&power_shelf) {

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.

we are extracting the bmc mac from the powershelf twice in this func --we have already done this above in line 179

}

/// Find all machine interfaces associated with a power shelf.
pub async fn find_by_power_shelf_id(

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.

Is this helper function overkill? Its a very thin wrapper.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'm gonna leave it in but change it to follow the shape of the function above

Comment thread crates/api-model/src/power_shelf/mod.rs Outdated
reprovisioning_state: ReProvisioningState,
},
/// Site Explorer is being suppressed before the destructive reset.
Preparing,

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.

I think this state's name needs to give an operator better context on the semantic nature of the operations being done. How about something like SuppressSiteExplorer.

Comment on lines +271 to +278
/// Site Explorer is being suppressed before the destructive reset.
Preparing,
/// The controller is resetting the PMC and waiting for DHCP release.
VerifyingDhcpRelease {
verifying_state: PowerShelfVerifyingDhcpReleaseState,
},
/// Terminal state: the power shelf has been removed from managed service.
Decommissioned,

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.

Doesnt it make more sense to have a top level Decommission state and then have these three as sub-states


if state.decommission_requested {
let mut txn = ctx.services.db_pool.begin().await?;
db_power_shelf::clear_decommission_requested(txn.as_mut(), *power_shelf_id).await?;

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.

Do you need to care about clearing this flag if the powershelf object is going to be deleted from the table?

@ericpretzel ericpretzel Aug 8, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Doesn't hurt, just following the design pattern for these flags.


use crate::context::PowerShelfStateHandlerContextObjects;

fn bmc_mac(power_shelf: &PowerShelf) -> Result<mac_address::MacAddress, StateHandlerError> {

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.

nit: this seems like a very thin wrapper. keep it if you think if its helpful, but it might be overkill

verifying_state,
PowerShelfVerifyingDhcpReleaseState::FactoryResetBmc
) {
return handle_factory_reset_bmc(&power_shelf.id, power_shelf, ctx).await;

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.

IMO this is where we should delete the credentials for the PMC from vault--after doing a factory reset

},
)
.await?;
db::machine_interface::record_deletion(&mut txn).await?;

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.

why do we want to delete the interface here? Doesnt it make sense to do that as part of the delete decomissioned powershelf logic?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This was a trick that I used to have the kea dhcp server restart. It doesn't actually delete the interface yet. I'll comment to make that more clear.

})?;

let mut txn = ctx.services.db_pool.begin().await?;
db::bmc_suppression::upsert(

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.

Should this be in a separate step? If for some reason, we fail to update the db, and re-execute this state, we will keep trying to factory reset the BMC in a loop.

reason: format!("power shelf {power_shelf_id} is being decommissioned"),
},
)
.await?;

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.

Im having a hard time following this logic. Arent we doing the same thing in in lines 106-115 in the handler for FactoryResetBMC?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is for suppressing site explorer (BmcSuppressionSubsystem::SiteExplorer), lines 106-115 is for DHCP. These need to be done at different times.

@spydaNVIDIA

Copy link
Copy Markdown
Contributor

Why do we want to do this in two steps: 1) request decomission and 2) delete decomissioned powershelf?

Wouldnt it make more sense to add the logic in (2) as the final/teminal step in the state machine once decomission is requested (1).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

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

86-89: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Accept a generic database reader.

This helper only reads data. Accept impl DbReader<'_> so callers can use a pool or an existing transaction without requiring a mutable PgConnection.

Proposed change
 pub async fn has_instance_assigned_host_in_rack(
-    txn: &mut PgConnection,
+    txn: impl DbReader<'_>,
     rack_id: &RackId,
 ) -> Result<bool, DatabaseError> {

As per coding guidelines, “For read-only database functions, accept impl DbReader so callers can use a pool without unnecessary transactions.”

🤖 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/managed_host.rs` around lines 86 - 89, Update
has_instance_assigned_host_in_rack to accept an impl DbReader<'_> instead of
&mut PgConnection, and adjust its query usage as needed to work with the generic
reader while preserving the existing read-only result behavior.

Sources: Coding guidelines, Path instructions

🤖 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/managed_host.rs`:
- Around line 90-102: Serialize decommissioning and instance allocation by
acquiring the same rack-scoped or advisory lock in decommission_power_shelf and
batch_allocate_instances before checking or modifying related rows. Preserve the
existing DPU-machine query while ensuring both transactions hold the lock
through commit, and add a concurrent integration test proving allocation cannot
commit between the decommission check and update.

---

Nitpick comments:
In `@crates/api-db/src/managed_host.rs`:
- Around line 86-89: Update has_instance_assigned_host_in_rack to accept an impl
DbReader<'_> instead of &mut PgConnection, and adjust its query usage as needed
to work with the generic reader while preserving the existing read-only result
behavior.
🪄 Autofix

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: 5ff5e426-0110-44f9-8032-a8334233335b

📥 Commits

Reviewing files that changed from the base of the PR and between 5907061 and 8e2fce2.

📒 Files selected for processing (3)
  • crates/api-core/src/handlers/power_shelf.rs
  • crates/api-core/src/tests/power_shelf_state_controller/decommissioning.rs
  • crates/api-db/src/managed_host.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/api-core/src/handlers/power_shelf.rs

Comment on lines +90 to +102
let query = r#"
SELECT EXISTS (
SELECT 1
FROM machines m
INNER JOIN instances i ON i.machine_id = m.id
WHERE m.rack_id = $1
AND NOT starts_with(m.id, $2)
)
"#;
sqlx::query_scalar(query)
.bind(rack_id)
.bind(MachineType::Dpu.id_prefix())
.fetch_one(txn)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the transaction isolation used by the API lifecycle operation.
rg -n -C 5 'async fn txn_begin|fn txn_begin|txn_begin\(' crates/api-core/src crates/api-db/src || true

# Trace instance allocation and writes to the instances relation.
rg -n -C 8 -P 'INSERT\s+INTO\s+instances|allocate.*instance|create.*instance|instances.*machine_id|machine_id.*instances' \
  crates/api-core/src crates/api-db/src || true

# Confirm the ordering of the decommission precondition and state update.
rg -n -C 8 'has_instance_assigned_host_in_rack|set_decommission_requested' \
  crates/api-core/src/handlers crates/api-db/src || true

Repository: NVIDIA/infra-controller

Length of output: 50380


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- decommission call sites ---'
rg -n -C 20 'has_instance_assigned_host_in_rack|set_decommission_requested|decommission_power_shelf' \
  crates/api-core/src crates/api-db/src \
  | head -n 1200

printf '%s\n' '--- transaction creation and isolation ---'
rg -n -C 12 'begin\(\)|Transaction|IsolationLevel|REPEATABLE|SERIALIZABLE|FOR UPDATE|FOR NO KEY UPDATE|advisory_xact_lock' \
  crates/api-core/src crates/api-db/src \
  | head -n 1600

printf '%s\n' '--- instance allocation implementation ---'
rg -n -C 18 'pub async fn allocate_instance|async fn allocate_instance|INSERT INTO instances|insert_instance|create_instance' \
  crates/api-core/src crates/api-db/src \
  | head -n 2000

Repository: NVIDIA/infra-controller

Length of output: 50380


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- exact files ---'
rg -l 'has_instance_assigned_host_in_rack|set_decommission_requested|decommission_power_shelf' \
  crates/api-core/src crates/api-db/src

printf '%s\n' '--- exact decommission references ---'
rg -n -C 35 'has_instance_assigned_host_in_rack|set_decommission_requested' \
  crates/api-core/src/handlers crates/api-core/src/power_shelf* crates/api-db/src/managed_host.rs \
  2>/dev/null | head -n 700

printf '%s\n' '--- allocation batch definition and transaction boundaries ---'
rg -n -C 35 'batch_allocate_instances|pub\(crate\) async fn batch_allocate_instances|async fn batch_allocate_instances' \
  crates/api-core/src/instance/mod.rs crates/api-core/src/handlers/instance.rs \
  | head -n 1000

printf '%s\n' '--- migration constraints and triggers for instances/machines/power shelves ---'
rg -n -i -C 8 'CREATE TABLE (instances|machines|power_shelves)|CREATE UNIQUE|FOREIGN KEY.*machine|REFERENCES machines|decommission_requested|instance.*machine' \
  migrations crates 2>/dev/null | head -n 1600

Repository: NVIDIA/infra-controller

Length of output: 12178


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- transaction helper ---'
rg -n -C 20 'txn_begin|IsolationLevel|SET TRANSACTION' crates/api-core/src || true

printf '%s\n' '--- batch allocation implementation ---'
rg -n -C 25 'batch_allocate_instances' crates/api-core/src/instance/mod.rs crates/api-core/src/handlers/instance.rs || true

printf '%s\n' '--- instance writes in allocation modules ---'
rg -n -C 18 'INSERT INTO instances|instances::create|instance::create|insert.*instance' \
  crates/api-core/src/instance crates/api-core/src/handlers/instance.rs crates/api-db/src \
  | head -n 1800 || true

printf '%s\n' '--- power shelf row locking and update ---'
rg -n -C 25 'pub async fn find_by_id|async fn find_by_id|set_decommission_requested|FOR UPDATE|FOR NO KEY UPDATE' \
  crates/api-db/src/power_shelf.rs crates/api-core/src/handlers/power_shelf.rs || true

printf '%s\n' '--- relevant schema objects ---'
rg -n -i -C 12 'CREATE TABLE .*instances|CREATE TABLE .*machines|CREATE TABLE .*power_shelves|REFERENCES machines|REFERENCES power_shelves|decommission_requested' \
  migrations crates/api-db 2>/dev/null | head -n 1800 || true

Repository: NVIDIA/infra-controller

Length of output: 50380


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- allocation transaction and host locks ---'
sed -n '1350,1660p' crates/api-core/src/instance/mod.rs
rg -n -C 12 'FOR UPDATE|lock.*machine|machine.*lock|batch.*lock' \
  crates/api-core/src/instance/mod.rs crates/api-db/src/machine.rs crates/api-db/src/instance.rs

printf '%s\n' '--- transaction helper definition ---'
rg -n 'txn_begin' crates/api-core/src --glob '*.rs' | head -n 80
while IFS=: read -r file line rest; do
  [ -n "${file:-}" ] || continue
  start=$((line - 20)); [ "$start" -lt 1 ] && start=1
  end=$((line + 45))
  sed -n "${start},${end}p" "$file"
done < <(rg -n 'fn txn_begin' crates/api-core/src --glob '*.rs' | head -n 10)

printf '%s\n' '--- power shelf query/update definitions ---'
rg -n -C 30 'find_by_id|set_decommission_requested|FOR UPDATE|FOR NO KEY UPDATE' \
  crates/api-db/src/power_shelf.rs

printf '%s\n' '--- schema file names and relevant constraints ---'
git ls-files | rg '(^|/)(migrations?|schema|.*\\.sql$)' | head -n 300

Repository: NVIDIA/infra-controller

Length of output: 35595


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- transaction helper definition ---'
sed -n '3728,3765p' crates/api-core/src/api.rs

printf '%s\n' '--- power shelf database operations ---'
rg -n 'pub async fn find_by_id|pub async fn set_decommission_requested|async fn find_by_id|async fn set_decommission_requested' \
  crates/api-db/src/power_shelf.rs
sed -n '1,220p' crates/api-db/src/power_shelf.rs | grep -n -A45 -B8 \
  -E 'find_by_id|set_decommission_requested' || true

printf '%s\n' '--- decommission concurrency tests ---'
rg -n -i -C 8 'concurr|race|parallel|spawn|decommission_power_shelf|allocate_instance' \
  crates/api-core/src/tests/power_shelf_state_controller/decommissioning.rs \
  crates/api-core/src/tests/instance* \
  | head -n 1200 || true

Repository: NVIDIA/infra-controller

Length of output: 50380


Serialize power-shelf decommission with instance allocation.

decommission_power_shelf checks instances before updating power_shelves, while batch_allocate_instances locks only machines. An allocation can commit after the check and before decommission commits. Use a shared rack or advisory lock in both transactions, and add a concurrent integration 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-db/src/managed_host.rs` around lines 90 - 102, Serialize
decommissioning and instance allocation by acquiring the same rack-scoped or
advisory lock in decommission_power_shelf and batch_allocate_instances before
checking or modifying related rows. Preserve the existing DPU-machine query
while ensuring both transactions hold the lock through commit, and add a
concurrent integration test proving allocation cannot commit between the
decommission check and update.

@coderabbitai coderabbitai Bot mentioned this pull request Aug 7, 2026
10 tasks
@ericpretzel

Copy link
Copy Markdown
Contributor Author

Why do we want to do this in two steps: 1) request decomission and 2) delete decomissioned powershelf?

Wouldnt it make more sense to add the logic in (2) as the final/teminal step in the state machine once decomission is requested (1).

For the use case where we want to decommission a machine before it is physically removed from the site, we can't automatically delete the records from NICo because NICo would just start automatically ingesting them again*. We want to pause it just before (2) so the operator can confirm with the datacenter that the machines have been removed. Once that's happened they can then run (2).

*we could get around this by having an option to not delete the suppression entries but that still leaves a manual second step for later to remove the suppressions, which is easy to forget about and significantly reduces visibility.

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

Labels

rest-api Add this label when an issue or PR concerns NICo REST API

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants