Skip to content

Gate sled-agent startup on trust quorum unlock - #11684

Open
andrewjstone wants to merge 1 commit into
mainfrom
gate-sled-agent-start-on-rack-unlock
Open

andrewjstone wants to merge 1 commit into
mainfrom
gate-sled-agent-start-on-rack-unlock

Conversation

@andrewjstone

Copy link
Copy Markdown
Contributor

We discovered in #11671 that sled-agent will start even if the sled has been expunged from the trust quorum. A sled learns that it is expunged from the trust quorum when it tries to load the rack secret. Therefore before starting the sled-agent we wait until we can load the rack secret.

Since the sled-agent has not been started at this point, we also need a way to inspect the state of the rack. A lockstep bootstrap server endpoint was added for this. We could potentially add this to the commissioning API as well, or instead of this bootstrap endpoint.

Also note that this assumes that LRTQ is no longer in use. I think that is true across all customers, and if so we should implement #9311 to remove it.

We discovered in #11671 that sled-agent will start even if the sled has
been expunged from the trust quorum. A sled learns that it is expunged
from the trust quorum when it tries to load the rack secret. Therefore
before starting the sled-agent we wait until we can load the rack secret.

Since the sled-agent has not been started at this point, we also need
a way to inspect the state of the rack. A lockstep bootstrap server
endpoint was added for this. We could potentially add this to the
commissioning API as well, or instead of this bootstrap endpoint.

Also note that this assumes that LRTQ is no longer in use. I think that
is true across all customers, and if so we should implement #9311 to
remove it.
@andrewjstone

Copy link
Copy Markdown
Contributor Author

I plan to test the scenario in #11671 on voxel. Alternatively, if someone wants to test on hardware while I do something else that would be appreciated.

@andrewjstone

Copy link
Copy Markdown
Contributor Author

I tested this on voxel. I started with a 4 node cluster, then removed g2 from the trust quorum. I waited for it to commit.

Then I logged into g2, which I had not yet rebooted, and checked the status of sled-agent with the new bootstrap API endpoint and the ipadm state, and rebooted:

root@g2:~# ipadm
ADDROBJ           TYPE     STATE        ADDR
lo0/v4            static   ok           127.0.0.1/8
vioif2/v4         dhcp     ok           192.168.1.70/24
lo0/v6            static   ok           ::1/128
vioif0/ll         addrconf ok           fe80::aa40:25ff:fe00:5%vioif0/10
vioif1/ll         addrconf ok           fe80::aa40:25ff:fe00:6%vioif1/10
bootstrap0/ll     addrconf ok           fe80::8:20ff:fe1b:c7a3%bootstrap0/10
bootstrap0/bootstrap6 static ok         fdb0:a840:2500:5::1/64
underlay0/ll      addrconf ok           fe80::8:20ff:fe61:1698%underlay0/10
underlay0/sled6   static   ok           fd47:65d2:3fea:103::1/64
underlay0/internaldns2 static ok        fd47:65d2:3fea:3::2/64

root@g2:~# curl http://[fdb0:a840:2500:5::1]:8080/sled-agent-start-status
{"state":"started","sled_id":"e2f15c01-cd2f-42de-b2fa-3da5aaec8425"}root@g2:~# reboot

reboot: Halting 10 zones.
reboot: Completing system halt.
updating /platform/i86pc/amd64/boot_archive (CPIO)
Connection to 192.168.1.70 closed by remote host.

I then logged back in and did the same thing. The gate held and eventually the state transitioned to expunged. I added some spaces for clarity.

➜  voxel git:(main) target/release/voxel host login g2
[voxel] ssh root@192.168.1.70  (g2 global zone)
The illumos Project     helios-3.0.23976        May 2026
root@g2:~# ipadm show-addr -p -o addr 2>/dev/null
127.0.0.1/8
192.168.1.70/24
::1/128
fe80::aa40:25ff:fe00:5%vioif0/10
fe80::aa40:25ff:fe00:6%vioif1/10
fe80::8:20ff:fe3a:ce32%bootstrap0/10
fdb0:a840:2500:5::1/64

root@g2:~# curl http://[fdb0:a840:2500:5::1]:8080/sled-agent-start-status
{"state":"waiting_for_rack_unlock","connected_peers":[],"latest_committed_epoch":1}
root@g2:~# curl http://[fdb0:a840:2500:5::1]:8080/sled-agent-start-status
{"state":"waiting_for_rack_unlock","connected_peers":[],"latest_committed_epoch":1}

root@g2:~# curl http://[fdb0:a840:2500:5::1]:8080/sled-agent-start-status
{"state":"expunged","epoch":2,"from":{"part_number":"913-0000019","serial_number":"2FAKE003"}}
root@g2:~# curl http://[fdb0:a840:2500:5::1]:8080/sled-agent-start-status
{"state":"expunged","epoch":2,"from":{"part_number":"913-0000019","serial_number":"2FAKE003"}}

root@g2:~# ipadm
ADDROBJ           TYPE     STATE        ADDR
lo0/v4            static   ok           127.0.0.1/8
vioif2/v4         dhcp     ok           192.168.1.70/24
lo0/v6            static   ok           ::1/128
vioif0/ll         addrconf ok           fe80::aa40:25ff:fe00:5%vioif0/10
vioif1/ll         addrconf ok           fe80::aa40:25ff:fe00:6%vioif1/10
bootstrap0/ll     addrconf ok           fe80::8:20ff:fe3a:ce32%bootstrap0/10
bootstrap0/bootstrap6 static ok         fdb0:a840:2500:5::1/64

@andrewjstone

Copy link
Copy Markdown
Contributor Author

I'm going to run another test in voxel to ensure that we can still add sleds.

@andrewjstone

Copy link
Copy Markdown
Contributor Author

Adding a sled works fine

➜  oxide.rs git:(main) ✗ target/debug/oxide --insecure --profile recovery system hardware rack list
[
  {
    "id": "05139caa-3b02-4f03-9fc0-0844d4a4d798",
    "time_created": "2026-11-04T19:16:27.772482Z",
    "time_modified": "2026-11-04T19:16:34.508873Z"
  }
]
➜  oxide.rs git:(main) ✗ target/debug/oxide --insecure --profile recovery system hardware sled list
[
  {
    "baseboard": {
      "part": "913-0000019",
      "revision": 0,
      "serial": "2FAKE001"
    },
    "id": "4851a0b8-1ecd-4787-b6e1-c67f5169b3fd",
    "policy": {
      "kind": "in_service",
      "provision_policy": "provisionable"
    },
    "rack_id": "05139caa-3b02-4f03-9fc0-0844d4a4d798",
    "slot": 1,
    "state": "active",
    "time_created": "2026-11-04T19:16:27.305844Z",
    "time_modified": "2026-11-04T19:16:27.305844Z",
    "usable_hardware_threads": 8,
    "usable_physical_ram": 7508520960
  }, {
    "baseboard": {
      "part": "913-0000019",
      "revision": 0,
      "serial": "2FAKE002"
    },
    "id": "b6373ffa-af25-404f-8218-694fc2c72824",
    "policy": {
      "kind": "in_service",
      "provision_policy": "provisionable"
    },
    "rack_id": "05139caa-3b02-4f03-9fc0-0844d4a4d798",
    "slot": 2,
    "state": "active",
    "time_created": "2026-11-04T19:16:28.365273Z",
    "time_modified": "2026-11-04T19:16:28.365273Z",
    "usable_hardware_threads": 8,
    "usable_physical_ram": 7508520960
  }, {
    "baseboard": {
      "part": "913-0000019",
      "revision": 0,
      "serial": "2FAKE000"
    },
    "id": "bc5d15a5-4937-47cf-83c9-81afef41de4b",
    "policy": {
      "kind": "in_service",
      "provision_policy": "provisionable"
    },
    "rack_id": "05139caa-3b02-4f03-9fc0-0844d4a4d798",
    "slot": 0,
    "state": "active",
    "time_created": "2026-11-04T19:16:26.493787Z",
    "time_modified": "2026-11-04T19:16:26.493787Z",
    "usable_hardware_threads": 8,
    "usable_physical_ram": 7508520960
  }
]

➜  oxide.rs git:(main) ✗ target/debug/oxide --insecure --profile recovery system hardware sled list-uninitialized
[
  {
    "baseboard": {
      "part": "913-0000019",
      "revision": 0,
      "serial": "2FAKE003"
    },
    "cubby": 3,
    "rack_id": "05139caa-3b02-4f03-9fc0-0844d4a4d798"
  }
]

➜  oxide.rs git:(main) ✗ target/debug/oxide --insecure --profile recovery system hardware rack membership add --rack-id 05139caa-3b02-4f03-9fc0-0844d4a4d798 --json-body add-g3.json
{
  "members": [
    {
      "part_number": "913-0000019",
      "serial_number": "2FAKE000"
    },
    {
      "part_number": "913-0000019",
      "serial_number": "2FAKE001"
    },
    {
      "part_number": "913-0000019",
      "serial_number": "2FAKE002"
    },
    {
      "part_number": "913-0000019",
      "serial_number": "2FAKE003"
    }
  ],
  "rack_id": "05139caa-3b02-4f03-9fc0-0844d4a4d798",
  "state": "in_progress",
  "time_created": "2026-11-04T20:07:22.237641Z",
  "unacknowledged_members": [
    {
      "part_number": "913-0000019",
      "serial_number": "2FAKE000"
    },
    {
      "part_number": "913-0000019",
      "serial_number": "2FAKE001"
    },
    {
      "part_number": "913-0000019",
      "serial_number": "2FAKE002"
    },
    {
      "part_number": "913-0000019",
      "serial_number": "2FAKE003"
    }
  ],
  "version": 2
}

➜  oxide.rs git:(main) ✗ target/debug/oxide --insecure --profile recovery system hardware rack membership  status --rack-id 05139caa-3b02-4f03-9fc0-0844d4a4d798
{
  "members": [
    {
      "part_number": "913-0000019",
      "serial_number": "2FAKE000"
    },
    {
      "part_number": "913-0000019",
      "serial_number": "2FAKE001"
    },
    {
      "part_number": "913-0000019",
      "serial_number": "2FAKE002"
    },
    {
      "part_number": "913-0000019",
      "serial_number": "2FAKE003"
    }
  ],
  "rack_id": "05139caa-3b02-4f03-9fc0-0844d4a4d798",
  "state": "committed",
  "time_committed": "2026-11-04T20:07:29.291034Z",
  "time_created": "2026-11-04T20:07:22.237641Z",
  "unacknowledged_members": [],
  "version": 2
}
➜  oxide.rs git:(main) ✗ target/debug/oxide --insecure --profile recovery system hardware sled list
[
  {
    "baseboard": {
      "part": "913-0000019",
      "revision": 0,
      "serial": "2FAKE001"
    },
    "id": "4851a0b8-1ecd-4787-b6e1-c67f5169b3fd",
    "policy": {
      "kind": "in_service",
      "provision_policy": "provisionable"
    },
    "rack_id": "05139caa-3b02-4f03-9fc0-0844d4a4d798",
    "slot": 1,
    "state": "active",
    "time_created": "2026-11-04T19:16:27.305844Z",
    "time_modified": "2026-11-04T19:16:27.305844Z",
    "usable_hardware_threads": 8,
    "usable_physical_ram": 7508520960
  }, {
    "baseboard": {
      "part": "913-0000019",
      "revision": 0,
      "serial": "2FAKE002"
    },
    "id": "b6373ffa-af25-404f-8218-694fc2c72824",
    "policy": {
      "kind": "in_service",
      "provision_policy": "provisionable"
    },
    "rack_id": "05139caa-3b02-4f03-9fc0-0844d4a4d798",
    "slot": 2,
    "state": "active",
    "time_created": "2026-11-04T19:16:28.365273Z",
    "time_modified": "2026-11-04T19:16:28.365273Z",
    "usable_hardware_threads": 8,
    "usable_physical_ram": 7508520960
  }, {
    "baseboard": {
      "part": "913-0000019",
      "revision": 0,
      "serial": "2FAKE003"
    },
    "id": "b7bb061c-4ef9-4d72-bf1e-2c7baae8eaa8",
    "policy": {
      "kind": "in_service",
      "provision_policy": "provisionable"
    },
    "rack_id": "05139caa-3b02-4f03-9fc0-0844d4a4d798",
    "slot": 3,
    "state": "active",
    "time_created": "2026-11-04T20:07:33.429199Z",
    "time_modified": "2026-11-04T20:07:33.429199Z",
    "usable_hardware_threads": 8,
    "usable_physical_ram": 7508520960
  }, {
    "baseboard": {
      "part": "913-0000019",
      "revision": 0,
      "serial": "2FAKE000"
    },
    "id": "bc5d15a5-4937-47cf-83c9-81afef41de4b",
    "policy": {
      "kind": "in_service",
      "provision_policy": "provisionable"
    },
    "rack_id": "05139caa-3b02-4f03-9fc0-0844d4a4d798",
    "slot": 0,
    "state": "active",
    "time_created": "2026-11-04T19:16:26.493787Z",
    "time_modified": "2026-11-04T19:16:26.493787Z",
    "usable_hardware_threads": 8,
    "usable_physical_ram": 7508520960
  }
]

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

claude found a previously-unreachable panic that's now reachable: the HardwareMonitor has a select! loop over the sled_agent_started_rx oneshot channel. This was fine because we always held the sending side until sled-agent started, but now if we're expunged we'll drop the sending side. If the select! loop fires multiple times (e.g., because disks are changing), it'll try to poll the oneshot multiple times (which will panic). I assume you didn't see this in voxel because there wouldn't be any hardware changes.

This should fix it, I think, but please double-check:

--- a/sled-agent/src/hardware_monitor.rs
+++ b/sled-agent/src/hardware_monitor.rs
@@ -133,14 +133,34 @@ impl HardwareMonitor {
         // its monitor.
         self.check_latest_hardware_snapshot().await;

+        // If `sled_agent_started_rx` is dropped without ever sending us the
+        // "sled agent started" message, the bootstrap server has decided it
+        // will never happen. This should only happen if we've been expunged.
+        let mut is_sled_agent_started_rx_alive = true;
+
         loop {
             tokio::select! {
-                Ok(sled_agent) = &mut self.sled_agent_started_rx,
-                    if self.sled_agent.is_none() =>
+                result = &mut self.sled_agent_started_rx,
+                    if is_sled_agent_started_rx_alive =>
                 {
-                    info!(self.log, "Sled Agent Started");
-                    self.sled_agent = Some(sled_agent);
-                    self.check_latest_hardware_snapshot().await;
+                    match result {
+                        Ok(sled_agent) => {
+                            info!(self.log, "Sled Agent Started");
+                            self.sled_agent = Some(sled_agent);
+                            self.check_latest_hardware_snapshot().await;
+                        }
+                        Err(_) => {
+                            error!(
+                                self.log,
+                                "Sled Agent will never start - start channel \
+                                 dropped. This should only happen if the sled \
+                                 has been expunged.",
+                            );
+                        }
+                    }
+                    // This is a oneshot channel: whatever result we got, it's
+                    // done, and we must not poll it again.
+                    is_sled_agent_started_rx_alive = false;
                 }
                 Ok(service_manager) = &mut self.service_manager_ready_rx,
                     if self.service_manager.is_none() =>

info!(log, "rack unlocked");
return RackUnlockOutcome::Unlocked;
}
Ok(None) => (),

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.

Just making sure I understand the happy path - POLL_INTERVAL is pretty aggressive, but this is the branch we expect to loop in (which doesn't log anything, which seems correct) if we're in a normal "waiting for rack unlock" case, right? All the other logs in this function are either "we're done" or "something unexpected has happened"?

"error" => InlineErrorChain::new(&err),
);
sleep(POLL_INTERVAL).await;
continue;

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 the continue here because we can't / shouldn't call load_latest_rack_secret() if calling status() failed?

"epoch" => %expunged.epoch,
"learned_from" => %expunged.from,
);
return RackUnlockOutcome::Expunged(expunged);

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 there any TOCTOU issue with calling status() separately from load_latest_rack_secret()? E.g., if this sled were expunged in between the two calls, could it still conceivably load the (old) rack secret and report Unlocked below even if it would have simultaneously reported "expunged" if we'd checked status at the same time?

This might not matter much - expunging a sled as its sled-agent is starting up is inherently racy, I guess.

This branch has not been deployed

No deployments
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.

2 participants