From 4e683c45e186f8c90f29cbddf0daa14f943cc0c8 Mon Sep 17 00:00:00 2001 From: karencfv Date: Fri, 7 Aug 2026 11:58:44 +1200 Subject: [PATCH 1/4] [update-status] Handle svcs in-transition and unrecognized states --- illumos-utils/src/svcs.rs | 15 +- nexus/db-model/src/inventory.rs | 7 + nexus/db-model/src/schema_versions.rs | 3 +- .../sled-agent-45.0.0-264d85.json.gitstub | 1 + ...d85.json => sled-agent-46.0.0-c87e34.json} | 9 +- openapi/sled-agent/sled-agent-latest.json | 2 +- .../up.sql | 7 + schema/crdb/dbinit.sql | 5 +- sled-agent/api/src/lib.rs | 20 +- .../types/versions/src/impls/inventory.rs | 4 + sled-agent/types/versions/src/latest.rs | 16 +- sled-agent/types/versions/src/lib.rs | 2 + .../src/modify_svc_state_enum/inventory.rs | 306 ++++++++++++++++++ .../versions/src/modify_svc_state_enum/mod.rs | 12 + 14 files changed, 392 insertions(+), 17 deletions(-) create mode 100644 openapi/sled-agent/sled-agent-45.0.0-264d85.json.gitstub rename openapi/sled-agent/{sled-agent-45.0.0-264d85.json => sled-agent-46.0.0-c87e34.json} (99%) create mode 100644 schema/crdb/add-unrecognized-svc-enabled-not-online-state/up.sql create mode 100644 sled-agent/types/versions/src/modify_svc_state_enum/inventory.rs create mode 100644 sled-agent/types/versions/src/modify_svc_state_enum/mod.rs diff --git a/illumos-utils/src/svcs.rs b/illumos-utils/src/svcs.rs index e989a0d1650..0aef00275fa 100644 --- a/illumos-utils/src/svcs.rs +++ b/illumos-utils/src/svcs.rs @@ -101,7 +101,9 @@ impl SvcsResult { | SvcState::Disabled | SvcState::Offline | SvcState::Online - | SvcState::Uninitialized => { + | SvcState::Uninitialized + | SvcState::InTransition + | SvcState::Unrecognized => { let fmri = if let Some(fmri) = svc.next() { fmri.to_string() } else { @@ -166,6 +168,9 @@ impl SvcsResult { SvcState::Maintenance => { SvcEnabledNotOnlineState::Maintenance } + SvcState::Unrecognized => { + SvcEnabledNotOnlineState::Unrecognized + } // `legacy_run` is excluded here because this state doesn't // really say anything about whether a service is running or // not. It just states that this is a service that isn't @@ -176,10 +181,16 @@ impl SvcsResult { // returns, so we exclude it as well. // More detail in // https://github.com/oxidecomputer/omicron/issues/10316 + // + // `InTransition` (or state with '*' appended as represented + // in svcs) is excluded because it is a momentary state + // while a service moves between two states, not a stable + // "enabled not online" condition worth reporting. SvcState::Online | SvcState::Uninitialized | SvcState::Disabled - | SvcState::LegacyRun => return None, + | SvcState::LegacyRun + | SvcState::InTransition => return None, }; Some(SvcEnabledNotOnline { fmri: svc.fmri, diff --git a/nexus/db-model/src/inventory.rs b/nexus/db-model/src/inventory.rs index b9aae70e92e..cd41452f742 100644 --- a/nexus/db-model/src/inventory.rs +++ b/nexus/db-model/src/inventory.rs @@ -2303,6 +2303,7 @@ impl_enum_type!( Offline => b"offline" Degraded => b"degraded" Maintenance => b"maintenance" + Unrecognized => b"unrecognized" ); impl From for InvSvcEnabledNotOnlineState { @@ -2317,6 +2318,9 @@ impl From for InvSvcEnabledNotOnlineState { SvcEnabledNotOnlineState::Maintenance => { InvSvcEnabledNotOnlineState::Maintenance } + SvcEnabledNotOnlineState::Unrecognized => { + InvSvcEnabledNotOnlineState::Unrecognized + } } } } @@ -2333,6 +2337,9 @@ impl From for SvcEnabledNotOnlineState { InvSvcEnabledNotOnlineState::Maintenance => { SvcEnabledNotOnlineState::Maintenance } + InvSvcEnabledNotOnlineState::Unrecognized => { + SvcEnabledNotOnlineState::Unrecognized + } } } } diff --git a/nexus/db-model/src/schema_versions.rs b/nexus/db-model/src/schema_versions.rs index 58483b9d0af..f63fc8df8e4 100644 --- a/nexus/db-model/src/schema_versions.rs +++ b/nexus/db-model/src/schema_versions.rs @@ -16,7 +16,7 @@ use std::{collections::BTreeMap, sync::LazyLock}; /// /// This must be updated when you change the database schema. Refer to /// schema/crdb/README.adoc in the root of this repository for details. -pub const SCHEMA_VERSION: Version = Version::new(285, 0, 0); +pub const SCHEMA_VERSION: Version = Version::new(286, 0, 0); /// List of all past database schema versions, in *reverse* order /// @@ -28,6 +28,7 @@ pub static KNOWN_VERSIONS: LazyLock> = LazyLock::new(|| { // | leaving the first copy as an example for the next person. // v // KnownVersion::new(next_int, "unique-dirname-with-the-sql-files"), + KnownVersion::new(286, "add-unrecognized-svc-enabled-not-online-state"), KnownVersion::new(285, "fm-config"), KnownVersion::new(284, "prune-service-nat-entries"), KnownVersion::new(283, "inventory-zone-nic-dual-stack"), diff --git a/openapi/sled-agent/sled-agent-45.0.0-264d85.json.gitstub b/openapi/sled-agent/sled-agent-45.0.0-264d85.json.gitstub new file mode 100644 index 00000000000..421f47dd06d --- /dev/null +++ b/openapi/sled-agent/sled-agent-45.0.0-264d85.json.gitstub @@ -0,0 +1 @@ +1a20a12eb13a20c58f91dfae51bb3c4836b4e069:openapi/sled-agent/sled-agent-45.0.0-264d85.json diff --git a/openapi/sled-agent/sled-agent-45.0.0-264d85.json b/openapi/sled-agent/sled-agent-46.0.0-c87e34.json similarity index 99% rename from openapi/sled-agent/sled-agent-45.0.0-264d85.json rename to openapi/sled-agent/sled-agent-46.0.0-c87e34.json index 13d4a74d7a6..8331ccc2768 100644 --- a/openapi/sled-agent/sled-agent-45.0.0-264d85.json +++ b/openapi/sled-agent/sled-agent-46.0.0-c87e34.json @@ -7,7 +7,7 @@ "url": "https://oxide.computer", "email": "api@oxide.computer" }, - "version": "45.0.0" + "version": "46.0.0" }, "paths": { "/artifacts": { @@ -9907,6 +9907,13 @@ "enum": [ "maintenance" ] + }, + { + "description": "An instance whose state is absent or unrecognized. Like `InTransition`, this state does not explicitly exist in `svcs`. Per `man svcs`: Absent or unrecognized states are denoted by a question mark (?) character.", + "type": "string", + "enum": [ + "unrecognized" + ] } ] }, diff --git a/openapi/sled-agent/sled-agent-latest.json b/openapi/sled-agent/sled-agent-latest.json index 583c5d79804..9bf3bc731e1 120000 --- a/openapi/sled-agent/sled-agent-latest.json +++ b/openapi/sled-agent/sled-agent-latest.json @@ -1 +1 @@ -sled-agent-45.0.0-264d85.json \ No newline at end of file +sled-agent-46.0.0-c87e34.json \ No newline at end of file diff --git a/schema/crdb/add-unrecognized-svc-enabled-not-online-state/up.sql b/schema/crdb/add-unrecognized-svc-enabled-not-online-state/up.sql new file mode 100644 index 00000000000..acfb53390e7 --- /dev/null +++ b/schema/crdb/add-unrecognized-svc-enabled-not-online-state/up.sql @@ -0,0 +1,7 @@ +ALTER TYPE + omicron.public.inv_svc_enabled_not_online_state +ADD VALUE IF NOT EXISTS + 'unrecognized' +AFTER + 'maintenance' +; diff --git a/schema/crdb/dbinit.sql b/schema/crdb/dbinit.sql index c5ff38a704b..878a33009d4 100644 --- a/schema/crdb/dbinit.sql +++ b/schema/crdb/dbinit.sql @@ -5318,7 +5318,8 @@ CREATE TABLE IF NOT EXISTS omicron.public.inv_internal_dns ( CREATE TYPE IF NOT EXISTS omicron.public.inv_svc_enabled_not_online_state AS ENUM ( 'offline', 'degraded', - 'maintenance' + 'maintenance', + 'unrecognized' ); CREATE TABLE IF NOT EXISTS omicron.public.inv_svc_enabled_not_online ( @@ -9181,7 +9182,7 @@ INSERT INTO omicron.public.db_metadata ( version, target_version ) VALUES - (TRUE, NOW(), NOW(), '285.0.0', NULL) + (TRUE, NOW(), NOW(), '286.0.0', NULL) ON CONFLICT DO NOTHING; COMMIT; diff --git a/sled-agent/api/src/lib.rs b/sled-agent/api/src/lib.rs index 4c5af9cd980..c2a690e7e7d 100644 --- a/sled-agent/api/src/lib.rs +++ b/sled-agent/api/src/lib.rs @@ -22,6 +22,7 @@ use omicron_common::api::internal::{ use sled_agent_types_versions::{ latest, v1, v4, v6, v7, v9, v10, v11, v12, v14, v16, v17, v18, v20, v22, v24, v25, v26, v28, v29, v30, v31, v32, v33, v34, v37, v39, v40, v41, v42, + v43, }; use sled_diagnostics::SledDiagnosticsQueryOutput; use slog_error_chain::InlineErrorChain; @@ -38,6 +39,7 @@ api_versions!([ // | example for the next person. // v // (next_int, IDENT), + (46, MODIFY_SVC_STATE_ENUM), (45, REMOVE_UPLINK_ENSURE), (44, PROPOLIS_NVME_VWC), (43, INVENTORY_BASEBOARD_ID), @@ -1130,12 +1132,26 @@ pub trait SledAgentApi { #[endpoint { method = GET, path = "/inventory", - versions = VERSION_INVENTORY_BASEBOARD_ID.., + versions = VERSION_MODIFY_SVC_STATE_ENUM.., }] async fn inventory( rqctx: RequestContext, ) -> Result, HttpError>; + /// Fetch basic information about this sled + #[endpoint { + operation_id = "inventory", + method = GET, + path = "/inventory", + versions = VERSION_INVENTORY_BASEBOARD_ID..VERSION_MODIFY_SVC_STATE_ENUM, + }] + async fn inventory_v43( + rqctx: RequestContext, + ) -> Result, HttpError> { + let HttpResponseOk(inventory) = Self::inventory(rqctx).await?; + inventory.try_into().map_err(HttpError::from).map(HttpResponseOk) + } + /// Fetch basic information about this sled #[endpoint { operation_id = "inventory", @@ -1146,7 +1162,7 @@ pub trait SledAgentApi { async fn inventory_v40( rqctx: RequestContext, ) -> Result, HttpError> { - Self::inventory(rqctx).await.map(|HttpResponseOk(inv)| { + Self::inventory_v43(rqctx).await.map(|HttpResponseOk(inv)| { HttpResponseOk(v40::inventory::Inventory::from(inv)) }) } diff --git a/sled-agent/types/versions/src/impls/inventory.rs b/sled-agent/types/versions/src/impls/inventory.rs index dcfb1ba543b..1dd5091ce9b 100644 --- a/sled-agent/types/versions/src/impls/inventory.rs +++ b/sled-agent/types/versions/src/impls/inventory.rs @@ -1055,6 +1055,7 @@ impl From for SvcState { SvcEnabledNotOnlineState::Degraded => Self::Degraded, SvcEnabledNotOnlineState::Maintenance => Self::Maintenance, SvcEnabledNotOnlineState::Offline => Self::Offline, + SvcEnabledNotOnlineState::Unrecognized => Self::Unrecognized, } } } @@ -1069,6 +1070,8 @@ impl fmt::Display for SvcState { SvcState::Maintenance => "maintenance", SvcState::Disabled => "disabled", SvcState::LegacyRun => "legacy_run", + SvcState::InTransition => "in_transition", + SvcState::Unrecognized => "unrecognized", }; write!(f, "{state}") @@ -1081,6 +1084,7 @@ impl fmt::Display for SvcEnabledNotOnlineState { SvcEnabledNotOnlineState::Offline => "offline", SvcEnabledNotOnlineState::Degraded => "degraded", SvcEnabledNotOnlineState::Maintenance => "maintenance", + SvcEnabledNotOnlineState::Unrecognized => "unrecognized", }; write!(f, "{state}") diff --git a/sled-agent/types/versions/src/latest.rs b/sled-agent/types/versions/src/latest.rs index 5d6c4db3714..dc30509d6e6 100644 --- a/sled-agent/types/versions/src/latest.rs +++ b/sled-agent/types/versions/src/latest.rs @@ -176,15 +176,8 @@ pub mod inventory { pub use crate::v24::inventory::InventoryZpool; pub use crate::v24::inventory::ZpoolHealth; - pub use crate::v34::inventory::Svc; - pub use crate::v34::inventory::SvcState; pub use crate::v34::inventory::SvcsError; - pub use crate::v37::inventory::SvcEnabledNotOnline; - pub use crate::v37::inventory::SvcEnabledNotOnlineState; - pub use crate::v37::inventory::SvcsEnabledNotOnline; - pub use crate::v37::inventory::SvcsEnabledNotOnlineResult; - pub use crate::v40::inventory::FMD_MAX_CASES; pub use crate::v40::inventory::FMD_MAX_RESOURCES; pub use crate::v40::inventory::FmdHostCase; @@ -192,7 +185,14 @@ pub mod inventory { pub use crate::v40::inventory::FmdInventoryError; pub use crate::v40::inventory::FmdInventoryErrorKind; pub use crate::v40::inventory::FmdResource; - pub use crate::v43::inventory::Inventory; + + pub use crate::v46::inventory::Inventory; + pub use crate::v46::inventory::Svc; + pub use crate::v46::inventory::SvcEnabledNotOnline; + pub use crate::v46::inventory::SvcEnabledNotOnlineState; + pub use crate::v46::inventory::SvcState; + pub use crate::v46::inventory::SvcsEnabledNotOnline; + pub use crate::v46::inventory::SvcsEnabledNotOnlineResult; pub use crate::impls::inventory::FmdHostCaseDisplay; pub use crate::impls::inventory::FmdInventoryDisplay; diff --git a/sled-agent/types/versions/src/lib.rs b/sled-agent/types/versions/src/lib.rs index f9376806bee..d5df19ee63c 100644 --- a/sled-agent/types/versions/src/lib.rs +++ b/sled-agent/types/versions/src/lib.rs @@ -95,6 +95,8 @@ pub mod v42; pub mod v43; #[path = "propolis_nvme_vwc/mod.rs"] pub mod v44; +#[path = "modify_svc_state_enum/mod.rs"] +pub mod v46; #[path = "add_probe_put_endpoint/mod.rs"] pub mod v6; #[path = "multicast_support/mod.rs"] diff --git a/sled-agent/types/versions/src/modify_svc_state_enum/inventory.rs b/sled-agent/types/versions/src/modify_svc_state_enum/inventory.rs new file mode 100644 index 00000000000..6ed0f989f10 --- /dev/null +++ b/sled-agent/types/versions/src/modify_svc_state_enum/inventory.rs @@ -0,0 +1,306 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use chrono::DateTime; +use chrono::Utc; +use iddqd::IdOrdMap; +use omicron_common::api::external; +use omicron_common::api::external::ByteCount; +use omicron_common::snake_case_result; +use omicron_common::snake_case_result::SnakeCaseResult; +use omicron_uuid_kinds::SledUuid; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use sled_hardware_types::{BaseboardId, SledCpuFamily}; +use std::net::SocketAddrV6; +use strum::EnumIter; + +use crate::v1::inventory::InventoryDataset; +use crate::v1::inventory::InventoryDisk; +use crate::v1::inventory::SledRole; +use crate::v14::inventory::ConfigReconcilerInventoryStatus; +use crate::v14::inventory::OmicronFileSourceResolverInventory; +use crate::v14::inventory::OmicronSledConfig; +use crate::v16::inventory::ConfigReconcilerInventory; +use crate::v16::inventory::SingleMeasurementInventory; +use crate::v24::inventory::InventoryZpool; +use crate::v34; +use crate::v34::inventory::SvcsError; +use crate::v37; +use crate::v40::inventory::{FmdInventory, FmdInventoryError}; +use crate::v43; + +/// Each service instance is always in a well-defined state based on its +/// dependencies, the results of the execution of its methods, and its potential +/// contracts events. See for more information. +#[derive( + Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize, JsonSchema, +)] +#[serde(rename_all = "snake_case")] +pub enum SvcState { + /// Initial state for all service instances. + Uninitialized, + /// The instance is enabled, but not yet running or available to run. + Offline, + /// The instance is enabled and running or is available to run. + Online, + /// The instance is enabled and running or available to run. It is, however, + /// functioning at a limited capacity in comparison to normal operation. + Degraded, + /// The instance is enabled, but not able to run. + Maintenance, + /// The instance is disabled. + Disabled, + /// Represents a legacy instance that is not managed by the service + /// management facility. + LegacyRun, + /// An instance whose state is in transition from one to another. Note: as + /// per `man svcs`, An asterisk (*) is appended for instances in transition. + /// So there is not an "in-transition" state per se. + InTransition, + /// An instance whose state is absent or unrecognized. Like `InTransition`, + /// this state does not explicitly exist in `svcs`. Per `man svcs`: Absent + /// or unrecognized states are denoted by a question mark (?) character. + Unrecognized, +} + +impl TryFrom for v34::inventory::SvcState { + type Error = external::Error; + + fn try_from(value: SvcState) -> Result { + match value { + SvcState::Degraded => Ok(Self::Degraded), + SvcState::Maintenance => Ok(Self::Maintenance), + SvcState::Offline => Ok(Self::Offline), + SvcState::Uninitialized => Ok(Self::Uninitialized), + SvcState::Disabled => Ok(Self::Disabled), + SvcState::LegacyRun => Ok(Self::LegacyRun), + SvcState::Online => Ok(Self::Online), + SvcState::InTransition | SvcState::Unrecognized => { + Err(external::Error::InternalError { + internal_message: format!("unknown state {:?}", value), + }) + } + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +/// Information about an SMF service that is enabled but not running +pub struct Svc { + pub fmri: String, + pub zone: String, + pub state: SvcState, +} + +impl TryFrom for v34::inventory::Svc { + type Error = external::Error; + + fn try_from(value: Svc) -> Result { + let Svc { fmri, zone, state } = value; + let state = Self { fmri, zone, state: state.try_into()? }; + Ok(state) + } +} + +/// Each service instance is always in a well-defined state based on its +/// dependencies, the results of the execution of its methods, and its potential +/// contracts events. +/// +/// This enum contains all possible states except `online`, `disabled`, +/// `uninitialized` and `legacy_run`. We only want to represent states that +/// represent some sort of "unhealthy" or "unexpected" state. +/// See for more information. +#[derive( + Clone, + Copy, + Debug, + PartialEq, + Eq, + Deserialize, + Serialize, + JsonSchema, + EnumIter, +)] +#[serde(rename_all = "snake_case")] +pub enum SvcEnabledNotOnlineState { + /// The instance is enabled, but not yet running or available to run. + Offline, + /// The instance is enabled and running or available to run. It is, however, + /// functioning at a limited capacity in comparison to normal operation. + Degraded, + /// The instance is enabled, but not able to run. + Maintenance, + /// An instance whose state is absent or unrecognized. Like `InTransition`, + /// this state does not explicitly exist in `svcs`. Per `man svcs`: Absent + /// or unrecognized states are denoted by a question mark (?) character. + Unrecognized, +} + +impl TryFrom + for v37::inventory::SvcEnabledNotOnlineState +{ + type Error = external::Error; + + fn try_from(value: SvcEnabledNotOnlineState) -> Result { + match value { + SvcEnabledNotOnlineState::Offline => Ok(Self::Offline), + SvcEnabledNotOnlineState::Degraded => Ok(Self::Degraded), + SvcEnabledNotOnlineState::Maintenance => Ok(Self::Maintenance), + SvcEnabledNotOnlineState::Unrecognized => { + Err(external::Error::InternalError { + internal_message: format!("unknown state {:?}", value), + }) + } + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +/// Information about an SMF service that is enabled but not running +pub struct SvcEnabledNotOnline { + pub fmri: String, + pub zone: String, + pub state: SvcEnabledNotOnlineState, +} + +impl TryFrom for v37::inventory::SvcEnabledNotOnline { + type Error = external::Error; + + fn try_from(value: SvcEnabledNotOnline) -> Result { + let SvcEnabledNotOnline { fmri, zone, state } = value; + Ok(Self { fmri, zone, state: state.try_into()? }) + } +} + +/// Lists services that are enabled but not in an online state if any, the time +/// the sample was collected, and any errors that may have ocurred during the +/// collection +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct SvcsEnabledNotOnline { + pub services: Vec, + pub errors: Vec, + pub time_of_status: DateTime, +} + +impl TryFrom for v37::inventory::SvcsEnabledNotOnline { + type Error = external::Error; + + fn try_from(value: SvcsEnabledNotOnline) -> Result { + let SvcsEnabledNotOnline { services, errors, time_of_status } = value; + let services = services + .into_iter() + .map(TryInto::try_into) + .collect::, _>>()?; + Ok(Self { services, errors, time_of_status }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, JsonSchema)] +#[serde(tag = "type", content = "value", rename_all = "snake_case")] +pub enum SvcsEnabledNotOnlineResult { + SvcsEnabledNotOnline(SvcsEnabledNotOnline), + SvcsCmdError(SvcsError), + DataUnavailable, +} + +impl TryFrom + for v37::inventory::SvcsEnabledNotOnlineResult +{ + type Error = external::Error; + + fn try_from( + value: SvcsEnabledNotOnlineResult, + ) -> Result { + Ok(match value { + SvcsEnabledNotOnlineResult::DataUnavailable => { + Self::DataUnavailable + } + SvcsEnabledNotOnlineResult::SvcsCmdError(e) => { + Self::SvcsCmdError(e) + } + SvcsEnabledNotOnlineResult::SvcsEnabledNotOnline(svcs) => { + Self::SvcsEnabledNotOnline(svcs.try_into()?) + } + }) + } +} + +/// Identity and basic status information about this sled agent +#[derive(Clone, Debug, Deserialize, JsonSchema, Serialize)] +pub struct Inventory { + pub sled_id: SledUuid, + pub sled_agent_address: SocketAddrV6, + pub sled_role: SledRole, + pub baseboard_id: BaseboardId, + pub usable_hardware_threads: u32, + pub usable_physical_ram: ByteCount, + pub cpu_family: SledCpuFamily, + pub reservoir_size: ByteCount, + pub disks: Vec, + pub zpools: Vec, + pub datasets: Vec, + pub ledgered_sled_config: Option, + pub reconciler_status: ConfigReconcilerInventoryStatus, + pub last_reconciliation: Option, + pub file_source_resolver: OmicronFileSourceResolverInventory, + pub smf_services_enabled_not_online: SvcsEnabledNotOnlineResult, + pub reference_measurements: IdOrdMap, + #[serde(with = "snake_case_result")] + #[schemars( + schema_with = "SnakeCaseResult::::json_schema" + )] + pub fmd: Result, +} + +impl TryFrom for v43::inventory::Inventory { + type Error = external::Error; + + fn try_from(value: Inventory) -> Result { + let Inventory { + sled_id, + sled_agent_address, + sled_role, + baseboard_id, + usable_hardware_threads, + usable_physical_ram, + cpu_family, + reservoir_size, + disks, + zpools, + datasets, + ledgered_sled_config, + reconciler_status, + last_reconciliation, + file_source_resolver, + smf_services_enabled_not_online, + reference_measurements, + fmd, + } = value; + Ok(Self { + sled_id, + sled_agent_address, + sled_role, + baseboard_id, + usable_hardware_threads, + usable_physical_ram, + cpu_family, + reservoir_size, + disks, + zpools, + datasets, + ledgered_sled_config, + reconciler_status, + last_reconciliation, + file_source_resolver, + smf_services_enabled_not_online: smf_services_enabled_not_online + .try_into()?, + reference_measurements, + fmd, + }) + } +} diff --git a/sled-agent/types/versions/src/modify_svc_state_enum/mod.rs b/sled-agent/types/versions/src/modify_svc_state_enum/mod.rs new file mode 100644 index 00000000000..a545f314107 --- /dev/null +++ b/sled-agent/types/versions/src/modify_svc_state_enum/mod.rs @@ -0,0 +1,12 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! Version `MODIFY_SVC_STATE_ENUM` of the Sled Agent API. +//! +//! This version adds `InTransition` and `Unrecognized` variants to `SvcState`, +//! and an `Unrecognized` variant to `SvcEnabledNotOnlineState`, so that SMF +//! service states that are in transition or that `svcs` reports as absent or +//! unrecognized can be represented. + +pub mod inventory; From 35839edf6dccc6db1f6d35ae9c69fe36d0e757ba Mon Sep 17 00:00:00 2001 From: karencfv Date: Fri, 7 Aug 2026 12:18:32 +1200 Subject: [PATCH 2/4] fix parsing --- illumos-utils/src/svcs.rs | 49 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/illumos-utils/src/svcs.rs b/illumos-utils/src/svcs.rs index 0aef00275fa..a7fdf2f55b3 100644 --- a/illumos-utils/src/svcs.rs +++ b/illumos-utils/src/svcs.rs @@ -209,6 +209,11 @@ impl SvcsResult { } fn parse_svc_state(state: &str) -> Option { + // Per `man svcs`, an asterisk (*) is appended to the state of instances + // that are in transition from one state to another. + if state.ends_with('*') { + return Some(SvcState::InTransition); + } match state { "uninitialized" => Some(SvcState::Uninitialized), "offline" => Some(SvcState::Offline), @@ -217,6 +222,9 @@ fn parse_svc_state(state: &str) -> Option { "maintenance" => Some(SvcState::Maintenance), "disabled" => Some(SvcState::Disabled), "legacy_run" => Some(SvcState::LegacyRun), + // Per `man svcs`, absent or unrecognized states are denoted by a + // question mark (?) character. + "?" => Some(SvcState::Unrecognized), _ => None, } } @@ -451,6 +459,44 @@ disabled svc:/network/tcpkey:default global ); } + #[test] + fn test_svc_parse_in_transition_and_unrecognized() { + let output = r#"online* svc:/milestone/sysconfig:default global +? svc:/site/fake-service:default global +disabled svc:/network/tcpkey:default global +"#; + + let log = log(); + let result = SvcsResult::parse(&log, output.as_bytes()); + + assert_eq!(result.services.len(), 3); + assert_eq!(result.errors.len(), 0); + assert_eq!( + result.services[0], + Svc { + fmri: "svc:/milestone/sysconfig:default".to_string(), + zone: "global".to_string(), + state: SvcState::InTransition, + } + ); + assert_eq!( + result.services[1], + Svc { + fmri: "svc:/site/fake-service:default".to_string(), + zone: "global".to_string(), + state: SvcState::Unrecognized, + } + ); + assert_eq!( + result.services[2], + Svc { + fmri: "svc:/network/tcpkey:default".to_string(), + zone: "global".to_string(), + state: SvcState::Disabled, + } + ); + } + #[test] fn test_to_enabled_not_online() { let mk_svc = |i: usize, state: SvcState| Svc { @@ -477,6 +523,8 @@ disabled svc:/network/tcpkey:default global mk_svc(7, SvcState::Maintenance), mk_svc(8, SvcState::Maintenance), mk_svc(9, SvcState::Uninitialized), + mk_svc(10, SvcState::Unrecognized), + mk_svc(11, SvcState::InTransition), ]; let result = SvcsResult { services, @@ -493,6 +541,7 @@ disabled svc:/network/tcpkey:default global mk_e_not_o_svc(3, SvcEnabledNotOnlineState::Degraded), mk_e_not_o_svc(7, SvcEnabledNotOnlineState::Maintenance), mk_e_not_o_svc(8, SvcEnabledNotOnlineState::Maintenance), + mk_e_not_o_svc(10, SvcEnabledNotOnlineState::Unrecognized), ] ); } From c3021cb931463ef503f5234c6b4868f0ceee086a Mon Sep 17 00:00:00 2001 From: karencfv Date: Fri, 7 Aug 2026 12:22:04 +1200 Subject: [PATCH 3/4] revert 10998 --- nexus/src/app/update.rs | 6 ------ nexus/types/src/inventory.rs | 6 +----- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/nexus/src/app/update.rs b/nexus/src/app/update.rs index e2877c911d9..71a630340f6 100644 --- a/nexus/src/app/update.rs +++ b/nexus/src/app/update.rs @@ -1433,9 +1433,6 @@ mod test { } #[nexus_test(server = crate::Server)] - // TODO-K: Enable once https://github.com/oxidecomputer/omicron/issues/10997 - // is worked on - #[ignore] async fn test_contact_support_services_errors_only( cptestctx: &ControlPlaneTestContext, ) { @@ -2237,9 +2234,6 @@ mod test { } #[test] - // TODO-K: Enable once https://github.com/oxidecomputer/omicron/issues/10997 - // is worked on - #[ignore] fn test_problems_unhealthy_services_errors_only() { let logctx = test_setup_log("test_problems_unhealthy_services_errors_only"); diff --git a/nexus/types/src/inventory.rs b/nexus/types/src/inventory.rs index 89591fbb213..71e837e1d16 100644 --- a/nexus/types/src/inventory.rs +++ b/nexus/types/src/inventory.rs @@ -324,11 +324,7 @@ impl Collection { .filter_map(|sled_agent| { match &sled_agent.smf_services_enabled_not_online { SvcsEnabledNotOnlineResult::SvcsEnabledNotOnline(svcs) - // This should check if svcs.is_empty() which includes - // parsing errors. We have a bug with this at the moment - // https://github.com/oxidecomputer/omicron/issues/10997 - // This check should change once that issue is resolved - if svcs.services.is_empty() => + if svcs.is_empty() => { None } From f899ae540839a2ea3b1f42b043c78edd553f13f9 Mon Sep 17 00:00:00 2001 From: karencfv Date: Fri, 7 Aug 2026 12:49:17 +1200 Subject: [PATCH 4/4] fix comments --- ....0.0-c87e34.json => sled-agent-46.0.0-1baf31.json} | 2 +- openapi/sled-agent/sled-agent-latest.json | 2 +- .../versions/src/modify_svc_state_enum/inventory.rs | 11 ++++++----- 3 files changed, 8 insertions(+), 7 deletions(-) rename openapi/sled-agent/{sled-agent-46.0.0-c87e34.json => sled-agent-46.0.0-1baf31.json} (99%) diff --git a/openapi/sled-agent/sled-agent-46.0.0-c87e34.json b/openapi/sled-agent/sled-agent-46.0.0-1baf31.json similarity index 99% rename from openapi/sled-agent/sled-agent-46.0.0-c87e34.json rename to openapi/sled-agent/sled-agent-46.0.0-1baf31.json index 8331ccc2768..0d9a52cc5bb 100644 --- a/openapi/sled-agent/sled-agent-46.0.0-c87e34.json +++ b/openapi/sled-agent/sled-agent-46.0.0-1baf31.json @@ -9909,7 +9909,7 @@ ] }, { - "description": "An instance whose state is absent or unrecognized. Like `InTransition`, this state does not explicitly exist in `svcs`. Per `man svcs`: Absent or unrecognized states are denoted by a question mark (?) character.", + "description": "An instance whose state is absent or unrecognized. Note: as per `man svcs`, \"Absent or unrecognized states are denoted by a question mark (?) character\". So there is not an \"unrecognized\" state per se in svcs.", "type": "string", "enum": [ "unrecognized" diff --git a/openapi/sled-agent/sled-agent-latest.json b/openapi/sled-agent/sled-agent-latest.json index 9bf3bc731e1..2303a35b29d 120000 --- a/openapi/sled-agent/sled-agent-latest.json +++ b/openapi/sled-agent/sled-agent-latest.json @@ -1 +1 @@ -sled-agent-46.0.0-c87e34.json \ No newline at end of file +sled-agent-46.0.0-1baf31.json \ No newline at end of file diff --git a/sled-agent/types/versions/src/modify_svc_state_enum/inventory.rs b/sled-agent/types/versions/src/modify_svc_state_enum/inventory.rs index 6ed0f989f10..0db232bf388 100644 --- a/sled-agent/types/versions/src/modify_svc_state_enum/inventory.rs +++ b/sled-agent/types/versions/src/modify_svc_state_enum/inventory.rs @@ -56,8 +56,8 @@ pub enum SvcState { /// management facility. LegacyRun, /// An instance whose state is in transition from one to another. Note: as - /// per `man svcs`, An asterisk (*) is appended for instances in transition. - /// So there is not an "in-transition" state per se. + /// per `man svcs`, "An asterisk (*) is appended for instances in + /// transition". So there is not an "in-transition" state per se in svcs. InTransition, /// An instance whose state is absent or unrecognized. Like `InTransition`, /// this state does not explicitly exist in `svcs`. Per `man svcs`: Absent @@ -133,9 +133,10 @@ pub enum SvcEnabledNotOnlineState { Degraded, /// The instance is enabled, but not able to run. Maintenance, - /// An instance whose state is absent or unrecognized. Like `InTransition`, - /// this state does not explicitly exist in `svcs`. Per `man svcs`: Absent - /// or unrecognized states are denoted by a question mark (?) character. + /// An instance whose state is absent or unrecognized. Note: as per + /// `man svcs`, "Absent or unrecognized states are denoted by a question + /// mark (?) character". So there is not an "unrecognized" state per se in + /// svcs. Unrecognized, }