From d04eeeef5decc96d0db92617cee057bce51968f3 Mon Sep 17 00:00:00 2001 From: John Gallagher Date: Wed, 4 Nov 2026 16:41:09 -0500 Subject: [PATCH] graceful shutdown: rework API types / compatibility This PR addresses items 1 and 2 from https://github.com/oxidecomputer/omicron/pull/11565#discussion_r4427624514. (Item 3 was addressed yesterday in https://github.com/oxidecomputer/omicron/pull/11565/commits/28daa746e695264f034e193718f9c738493e8d35, which changed the default for the API to `hard_off`.) The changes for those two are: 1. The `shutdown_policy` field is no longer an `Option` for either `Instance` or `InstanceCreate`. They do both have a `#[serde(default)]` on them, so they're not required at the API layer, but this way (a) we don't have to deal with `None` and (b) we don't have the oddity of always returning `Some(_)` to clients even if they had specified `None`. 2. The type conversion for old clients just throws away the `shutdown_policy` field instead of failing if it isn't `HardOff`. This is less important now that the default is `HardOff`, but I still think it's reasonable - this way old clients can still see / list instances that were created by new clients. They just won't be able to tell what those instance's shutdown policies are. (`shutdown_policy` is still `None` in `InstanceUpdate`, which means "don't change", so an instance update from an old client is fine: it doesn't know about `shutdown_policy`, which turns into `None`, which turns into "don't change it".) Most of the bulk off the diff is trivial fallout from 1 - changing a bunch of `None`s to `InstanceShutdownPolicy::default()`s in a lot of tests. The end-to-end test had to switch from specifying all the fields to using the progenitor-client's builder API (which allows you to _not_ specify defaultable fields). A few other cleanup things came along for the ride: 1. I moved the enforcement of the ACPI timeout second range into a newtype in the API. This means it's enforced at deserialization time, so we don't have to remember to validate it every place it comes in in Nexus. 2. The instance update handler had duplicated the logic of "convert the `InstanceShutdownPolicy` into the 2-column database representation". I moved this into an `InstanceUpdate::new()` function in the db-model; I don't love that it takes a bunch of args but I think it's still a net win? 3. I changed the propolis pointer in `package-manifest.toml` to match what's in `Cargo.toml` (all of which now point to the latest commit on https://github.com/oxidecomputer/propolis/pull/1217). I'm hoping this lets CI make more progress; I'd like to confirm the end-to-end test change mentioned above, at least, which I think requires CI. --- Cargo.lock | 1 - end-to-end-tests/src/instance_launch.rs | 53 ++-- nexus/db-model/src/instance.rs | 128 +++++---- nexus/db-queries/src/db/datastore/instance.rs | 5 +- .../db-queries/src/db/datastore/migration.rs | 3 +- nexus/db-queries/src/db/datastore/sled.rs | 3 +- .../virtual_provisioning_collection.rs | 3 +- nexus/db-queries/src/db/datastore/vmm.rs | 5 +- nexus/db-queries/src/db/datastore/vpc.rs | 3 +- .../src/db/pub_test_utils/helpers.rs | 2 +- .../db-queries/src/db/queries/external_ip.rs | 5 +- .../src/db/queries/network_interface.rs | 3 +- nexus/defaults/src/lib.rs | 5 - nexus/external-api/src/lib.rs | 18 +- .../tasks/instance_reincarnation.rs | 2 +- .../background/tasks/vmm_stop_for_update.rs | 4 +- nexus/src/app/instance.rs | 75 +----- nexus/src/app/sagas/instance_create.rs | 3 +- nexus/src/app/sagas/instance_delete.rs | 2 +- nexus/src/app/sagas/instance_migrate.rs | 3 +- nexus/src/app/sagas/instance_start.rs | 3 +- nexus/src/app/sagas/instance_update/mod.rs | 3 +- nexus/src/app/sagas/snapshot_create.rs | 3 +- nexus/src/app/sagas/test_helpers.rs | 6 +- nexus/test-utils/src/resource_helpers.rs | 4 +- nexus/tests/integration_tests/affinity.rs | 2 +- nexus/tests/integration_tests/audit_log.rs | 2 +- nexus/tests/integration_tests/disks.rs | 2 +- nexus/tests/integration_tests/endpoints.rs | 4 +- nexus/tests/integration_tests/external_ips.rs | 20 +- .../integration_tests/external_subnets.rs | 6 +- nexus/tests/integration_tests/instances.rs | 250 +++++++++--------- .../integration_tests/internet_gateway.rs | 5 +- nexus/tests/integration_tests/projects.rs | 6 +- nexus/tests/integration_tests/quotas.rs | 2 +- .../integration_tests/sled_bp_availability.rs | 2 +- nexus/tests/integration_tests/snapshots.rs | 4 +- .../integration_tests/subnet_allocation.rs | 4 +- nexus/tests/integration_tests/utilization.rs | 2 +- nexus/tests/integration_tests/vpc_routers.rs | 2 +- nexus/tests/integration_tests/vpcs.rs | 2 +- nexus/types/versions/Cargo.toml | 1 - .../src/instance_acpi_shutdown/instance.rs | 182 ++++++++++--- nexus/types/versions/src/latest.rs | 1 + openapi/nexus-lockstep.json | 14 +- ....json => nexus-2026102800.0.0-b45545.json} | 21 +- openapi/nexus/nexus-latest.json | 2 +- package-manifest.toml | 4 +- 48 files changed, 475 insertions(+), 410 deletions(-) rename openapi/nexus/{nexus-2026102800.0.0-831569.json => nexus-2026102800.0.0-b45545.json} (99%) diff --git a/Cargo.lock b/Cargo.lock index 08edd856a22..45e768ef744 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8413,7 +8413,6 @@ dependencies = [ "http", "mg-admin-client", "mg-api-types", - "nexus-defaults", "omicron-common", "omicron-passwords", "omicron-uuid-kinds", diff --git a/end-to-end-tests/src/instance_launch.rs b/end-to-end-tests/src/instance_launch.rs index 342099fd10c..600d5655f07 100644 --- a/end-to-end-tests/src/instance_launch.rs +++ b/end-to-end-tests/src/instance_launch.rs @@ -9,9 +9,8 @@ use anyhow::{Context as _, Result, ensure}; use omicron_test_utils::dev::poll::{CondCheckError, wait_for_condition}; use oxide_client::types::{ ByteCount, DiskBackend, DiskCreate, DiskSource, ExternalIp, - ExternalIpCreate, InstanceCpuCount, InstanceCreate, InstanceDiskAttachment, - InstanceNetworkInterfaceAttachment, InstanceState, IpVersion, PoolSelector, - SshKeyCreate, + ExternalIpCreate, InstanceDiskAttachment, InstanceState, IpVersion, + PoolSelector, SshKeyCreate, }; use oxide_client::{ClientCurrentUserExt, ClientDisksExt, ClientInstancesExt}; use russh::keys::{ @@ -64,38 +63,30 @@ async fn instance_launch() -> Result<()> { .clone(); eprintln!("create instance"); + let instance_name = generate_name("instance")?; let instance = ctx .client .instance_create() .project(ctx.project_name.clone()) - .body(InstanceCreate { - name: generate_name("instance")?, - description: String::new(), - hostname: "localshark".parse().unwrap(), // 🦈 - memory: ByteCount(1024 * 1024 * 1024), - ncpus: InstanceCpuCount(2), - boot_disk: Some(InstanceDiskAttachment::Attach { - name: disk_name.clone(), - }), - disks: Vec::new(), - network_interfaces: - InstanceNetworkInterfaceAttachment::DefaultDualStack, - external_ips: vec![ExternalIpCreate::Ephemeral { - pool_selector: PoolSelector::Auto { - ip_version: Some(IpVersion::V4), - }, - }], - user_data: String::new(), - ssh_public_keys: Some(vec![oxide_client::types::NameOrId::Name( - ssh_key_name.clone(), - )]), - start: true, - auto_restart_policy: Default::default(), - anti_affinity_groups: Vec::new(), - cpu_platform: None, - multicast_groups: Vec::new(), - enable_jumbo_frames: false, - shutdown_policy: None, + .body_map(|builder| { + builder + .name(instance_name) + .description("") + .hostname("localshark") + .memory(ByteCount(1024 * 1024 * 1024)) + .ncpus(2) + .boot_disk(Some(InstanceDiskAttachment::Attach { + name: disk_name.clone(), + })) + .external_ips(vec![ExternalIpCreate::Ephemeral { + pool_selector: PoolSelector::Auto { + ip_version: Some(IpVersion::V4), + }, + }]) + .ssh_public_keys(Some(vec![ + oxide_client::types::NameOrId::Name(ssh_key_name.clone()), + ])) + .start(true) }) .send() .await?; diff --git a/nexus/db-model/src/instance.rs b/nexus/db-model/src/instance.rs index 4db0e9320ff..94a35a77cf1 100644 --- a/nexus/db-model/src/instance.rs +++ b/nexus/db-model/src/instance.rs @@ -20,6 +20,7 @@ use diesel::prelude::*; use diesel::sql_types::{Bool, Nullable}; use nexus_db_schema::schema::{disk, external_ip, external_subnet, instance}; use nexus_types::external_api::instance as instance_types; +use omicron_common::api::external; use omicron_common::api::external::Error; use omicron_generation_kinds::{ InstanceStateGeneration, InstanceStateGenerationKind, @@ -30,6 +31,7 @@ use omicron_uuid_kinds::ProjectUuid; use omicron_uuid_kinds::{GenericUuid, InstanceUuid}; use serde::Deserialize; use serde::Serialize; +use slog_error_chain::InlineErrorChain; use uuid::Uuid; /// An Instance (VM). @@ -191,26 +193,8 @@ impl Instance { IntendedState::Stopped }; - use instance_types::InstanceShutdownPolicy; - let (shutdown_policy_action, shutdown_policy_timeout) = match params - .shutdown_policy - { - // RFD 622: "A user-configurable timeout for the instance to - // finish stopping itself should be present, bounded between - // 1 and 3600 seconds, inclusive." This is enforced at the API - // level and as a crdb constraint (expect `timeout_secs as i64` - // to be handled as an error elsewhere if somehow in the range - // where the cast would be problematic) - Some(InstanceShutdownPolicy::PowerButton { timeout_secs }) => ( - InstanceShutdownAction::PowerButton, - Some(TimeDelta::seconds(timeout_secs as i64)), - ), - // None: Default to hard-off, preserving the existing behavior of - // our system prior to ACPI shutdown support. - None | Some(InstanceShutdownPolicy::HardOff) => { - (InstanceShutdownAction::HardOff, None) - } - }; + let (shutdown_policy_action, shutdown_policy_timeout) = + shutdown_policy_to_db_columns(params.shutdown_policy); Self { identity, @@ -397,6 +381,7 @@ impl InstanceShutdownPolicyColumns { pub fn shutdown_policy( &self, ) -> Result { + use instance_types::AcpiTimeoutSecs; use instance_types::InstanceShutdownPolicy; match self.shutdown_policy_action { InstanceShutdownAction::HardOff => { @@ -423,6 +408,16 @@ impl InstanceShutdownPolicyColumns { self.id )) })?; + let timeout_secs = AcpiTimeoutSecs::try_from(timeout_secs) + .map_err(|err| { + Error::internal_error(format!( + "db constraint violated or out of sync with API: \ + db timeout of {timeout_secs} is invalid: {} \ + (instance {})", + InlineErrorChain::new(&*err), + self.id, + )) + })?; Ok(InstanceShutdownPolicy::PowerButton { timeout_secs }) } } @@ -735,15 +730,67 @@ pub struct InstanceUpdate { /// If this is `None`, the instance's shutdown policy will be unchanged by /// this update request. + /// + /// Unlike `Instance` above, these fields are `pub` because we need direct + /// access to them in the datastore crate. Consumers outside that crate + /// should *not* access these; construction should go through the stronger + /// types available via [`InstanceUpdate::new()`]. #[diesel(treat_none_as_null = false)] pub shutdown_policy_action: Option, pub shutdown_policy_timeout: Option, } +impl InstanceUpdate { + pub fn new( + boot_disk_id: Option, + auto_restart_policy: Option, + ncpus: instance_types::InstanceCpuCount, + memory: external::ByteCount, + cpu_platform: Option, + enable_jumbo_frames: bool, + shutdown_policy: Option, + ) -> Self { + let (shutdown_policy_action, shutdown_policy_timeout) = + if let Some(shutdown_policy) = shutdown_policy { + let (action, timeout) = + shutdown_policy_to_db_columns(shutdown_policy); + (Some(action), timeout) + } else { + (None, None) + }; + Self { + boot_disk_id, + auto_restart_policy: auto_restart_policy.map(From::from), + ncpus: ncpus.into(), + memory: memory.into(), + cpu_platform: cpu_platform.map(From::from), + enable_jumbo_frames, + shutdown_policy_action, + shutdown_policy_timeout, + } + } +} + +// convert the API `InstanceShutdownPolicy` into its raw db column +// representation +fn shutdown_policy_to_db_columns( + shutdown_policy: instance_types::InstanceShutdownPolicy, +) -> (InstanceShutdownAction, Option) { + use instance_types::InstanceShutdownPolicy; + match shutdown_policy { + InstanceShutdownPolicy::HardOff => { + (InstanceShutdownAction::HardOff, None) + } + InstanceShutdownPolicy::PowerButton { timeout_secs } => ( + InstanceShutdownAction::PowerButton, + Some(TimeDelta::seconds(i64::from(timeout_secs))), + ), + } +} + #[cfg(test)] mod tests { use super::*; - use omicron_common::api::external; #[test] fn test_instance_shutdown_policy() { @@ -769,12 +816,13 @@ mod tests { anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), enable_jumbo_frames: false, - shutdown_policy: None, + // explicitly set hard-off + shutdown_policy: instance_types::InstanceShutdownPolicy::HardOff, }; let instance_id = InstanceUuid::from_untyped_uuid(Uuid::new_v4()); let project_id = ProjectUuid::from_untyped_uuid(Uuid::new_v4()); - // when shutdown policy omitted at creation, default to HardOff + // confirm expected columns for hard-off let db_instance = Instance::new(instance_id, project_id, ¶ms); assert_eq!( db_instance.shutdown_policy_action, @@ -782,21 +830,11 @@ mod tests { ); assert_eq!(db_instance.shutdown_policy_timeout, None); - // explicitly set hard-off + // set power button with timeout params.shutdown_policy = - Some(instance_types::InstanceShutdownPolicy::HardOff); - let db_instance = Instance::new(instance_id, project_id, ¶ms); - assert_eq!( - db_instance.shutdown_policy_action, - InstanceShutdownAction::HardOff - ); - assert_eq!(db_instance.shutdown_policy_timeout, None); - - // explicitly set power button with timeout - params.shutdown_policy = - Some(instance_types::InstanceShutdownPolicy::PowerButton { - timeout_secs: 42, - }); + instance_types::InstanceShutdownPolicy::PowerButton { + timeout_secs: 42.try_into().expect("42 is a valid timeout"), + }; let db_instance = Instance::new(instance_id, project_id, ¶ms); assert_eq!( db_instance.shutdown_policy_action, @@ -806,21 +844,5 @@ mod tests { db_instance.shutdown_policy_timeout.unwrap().num_seconds(), 42 ); - - // Instance::new doesn't clamp an invalid timeout - // (enforced instead by Nexus and crdb constraint) - params.shutdown_policy = - Some(instance_types::InstanceShutdownPolicy::PowerButton { - timeout_secs: 999000, - }); - let db_instance = Instance::new(instance_id, project_id, ¶ms); - assert_eq!( - db_instance.shutdown_policy_action, - InstanceShutdownAction::PowerButton - ); - assert_eq!( - db_instance.shutdown_policy_timeout.unwrap().num_seconds(), - 999000 - ); } } diff --git a/nexus/db-queries/src/db/datastore/instance.rs b/nexus/db-queries/src/db/datastore/instance.rs index 1ecd6120048..daf4c13256d 100644 --- a/nexus/db-queries/src/db/datastore/instance.rs +++ b/nexus/db-queries/src/db/datastore/instance.rs @@ -186,7 +186,7 @@ impl TryFrom for instance_types::Instance { }, auto_restart_status, enable_jumbo_frames: value.instance.enable_jumbo_frames, - shutdown_policy: Some(shutdown_policy), + shutdown_policy, }) } } @@ -2384,7 +2384,8 @@ mod tests { anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), enable_jumbo_frames: false, - shutdown_policy: None, + shutdown_policy: + instance_types::InstanceShutdownPolicy::default(), }, ), ) diff --git a/nexus/db-queries/src/db/datastore/migration.rs b/nexus/db-queries/src/db/datastore/migration.rs index 5775fcd1090..184762f8f37 100644 --- a/nexus/db-queries/src/db/datastore/migration.rs +++ b/nexus/db-queries/src/db/datastore/migration.rs @@ -245,7 +245,8 @@ mod tests { anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), enable_jumbo_frames: false, - shutdown_policy: None, + shutdown_policy: + instance_types::InstanceShutdownPolicy::default(), }, ), ) diff --git a/nexus/db-queries/src/db/datastore/sled.rs b/nexus/db-queries/src/db/datastore/sled.rs index 54e5903a049..d6f2bb60820 100644 --- a/nexus/db-queries/src/db/datastore/sled.rs +++ b/nexus/db-queries/src/db/datastore/sled.rs @@ -5802,7 +5802,8 @@ pub(in crate::db::datastore) mod test { anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), enable_jumbo_frames: false, - shutdown_policy: None, + shutdown_policy: + instance::InstanceShutdownPolicy::default(), }, ), ) diff --git a/nexus/db-queries/src/db/datastore/virtual_provisioning_collection.rs b/nexus/db-queries/src/db/datastore/virtual_provisioning_collection.rs index 2d8943b49f5..b4d8cd45702 100644 --- a/nexus/db-queries/src/db/datastore/virtual_provisioning_collection.rs +++ b/nexus/db-queries/src/db/datastore/virtual_provisioning_collection.rs @@ -472,7 +472,8 @@ mod test { anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), enable_jumbo_frames: false, - shutdown_policy: None, + shutdown_policy: + instance_types::InstanceShutdownPolicy::default(), }, ), ) diff --git a/nexus/db-queries/src/db/datastore/vmm.rs b/nexus/db-queries/src/db/datastore/vmm.rs index 32aaafe66e4..0605f945c92 100644 --- a/nexus/db-queries/src/db/datastore/vmm.rs +++ b/nexus/db-queries/src/db/datastore/vmm.rs @@ -1440,7 +1440,8 @@ mod tests { let mut shutdown_policy_iter = iter::once( InstanceShutdownPolicy::HardOff, ) - .chain((1000..).step_by(100).map(|timeout_secs| { + .chain((1000_u64..).step_by(100).map(|n| { + let timeout_secs = n.try_into().expect("these are valid timeouts"); InstanceShutdownPolicy::PowerButton { timeout_secs } })); let mut expected_shutdown_policies = @@ -1559,7 +1560,7 @@ mod tests { anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), enable_jumbo_frames: false, - shutdown_policy: Some(shutdown_policy), + shutdown_policy, }, ), ) diff --git a/nexus/db-queries/src/db/datastore/vpc.rs b/nexus/db-queries/src/db/datastore/vpc.rs index 4713f06935f..a3dfc89761d 100644 --- a/nexus/db-queries/src/db/datastore/vpc.rs +++ b/nexus/db-queries/src/db/datastore/vpc.rs @@ -4044,7 +4044,8 @@ mod tests { anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), enable_jumbo_frames: false, - shutdown_policy: None, + shutdown_policy: + instance_types::InstanceShutdownPolicy::default(), }, ), ) diff --git a/nexus/db-queries/src/db/pub_test_utils/helpers.rs b/nexus/db-queries/src/db/pub_test_utils/helpers.rs index a507e84c6b7..14aec7364b6 100644 --- a/nexus/db-queries/src/db/pub_test_utils/helpers.rs +++ b/nexus/db-queries/src/db/pub_test_utils/helpers.rs @@ -260,7 +260,7 @@ pub async fn create_stopped_instance_record( anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), enable_jumbo_frames: false, - shutdown_policy: None, + shutdown_policy: Default::default(), }, ); diff --git a/nexus/db-queries/src/db/queries/external_ip.rs b/nexus/db-queries/src/db/queries/external_ip.rs index 9a0cab175c5..558135fc880 100644 --- a/nexus/db-queries/src/db/queries/external_ip.rs +++ b/nexus/db-queries/src/db/queries/external_ip.rs @@ -998,6 +998,7 @@ mod tests { use nexus_types::external_api::instance; use nexus_types::external_api::instance::InstanceCreate; use nexus_types::external_api::instance::InstanceNetworkInterfaceAttachment; + use nexus_types::external_api::instance::InstanceShutdownPolicy; use omicron_common::address::IpRange; use omicron_common::address::NUM_SOURCE_NAT_PORTS; use omicron_common::api::external::Error; @@ -1139,7 +1140,7 @@ mod tests { anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), enable_jumbo_frames: false, - shutdown_policy: None, + shutdown_policy: InstanceShutdownPolicy::default(), }); let conn = self @@ -2694,7 +2695,7 @@ mod tests { anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), enable_jumbo_frames: false, - shutdown_policy: None, + shutdown_policy: InstanceShutdownPolicy::default(), }); let conn = context diff --git a/nexus/db-queries/src/db/queries/network_interface.rs b/nexus/db-queries/src/db/queries/network_interface.rs index 9e3a47e2b01..136e3ac070c 100644 --- a/nexus/db-queries/src/db/queries/network_interface.rs +++ b/nexus/db-queries/src/db/queries/network_interface.rs @@ -1971,6 +1971,7 @@ mod tests { use nexus_types::external_api::instance::InstanceCpuCount; use nexus_types::external_api::instance::InstanceCreate; use nexus_types::external_api::instance::InstanceNetworkInterfaceAttachment; + use nexus_types::external_api::instance::InstanceShutdownPolicy; use nexus_types::external_api::instance::Ipv4Assignment; use nexus_types::external_api::instance::PrivateIpStackCreate; use nexus_types::external_api::instance::PrivateIpv4StackCreate; @@ -2024,7 +2025,7 @@ mod tests { anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), enable_jumbo_frames: false, - shutdown_policy: None, + shutdown_policy: InstanceShutdownPolicy::default(), }; let instance = Instance::new(instance_id, project_id, ¶ms); diff --git a/nexus/defaults/src/lib.rs b/nexus/defaults/src/lib.rs index 1ff3f1a23ff..7ac164e9665 100644 --- a/nexus/defaults/src/lib.rs +++ b/nexus/defaults/src/lib.rs @@ -92,11 +92,6 @@ pub static DEFAULT_FIREWALL_RULES: LazyLock = } }); -/// Upper bound of the timeout of the 'power button' instance shutdown policy, -/// which the time before the VMM force-kills an instance that was requested -/// to shut down gracefully. -pub const MAX_ACPI_SHUTDOWN_TIMEOUT_SECS: u64 = 3600; - /// Generate a random VPC IPv6 prefix, in the range `fd00::/48`. pub fn random_vpc_ipv6_prefix() -> Result { let mut bytes = [0u8; 16]; diff --git a/nexus/external-api/src/lib.rs b/nexus/external-api/src/lib.rs index 5eafc9062a8..0fbf53682a5 100644 --- a/nexus/external-api/src/lib.rs +++ b/nexus/external-api/src/lib.rs @@ -3875,11 +3875,7 @@ pub trait NexusExternalApi { let resp = Self::instance_list(rqctx, query_params).await?; let inner = resp.0; Ok(HttpResponseOk(ResultsPage { - items: inner - .items - .into_iter() - .map(TryInto::try_into) - .collect::, _>>()?, + items: inner.items.into_iter().map(From::from).collect(), next_page: inner.next_page, })) } @@ -3972,7 +3968,7 @@ pub trait NexusExternalApi { new_instance.map(Into::into), ) .await?; - Ok(HttpResponseCreated(resp.0.try_into()?)) + Ok(HttpResponseCreated(resp.0.into())) } #[endpoint { @@ -4213,7 +4209,7 @@ pub trait NexusExternalApi { { let resp = Self::instance_view(rqctx, query_params, path_params).await?; - Ok(HttpResponseOk(resp.0.try_into()?)) + Ok(HttpResponseOk(resp.0.into())) } #[endpoint { @@ -4308,7 +4304,7 @@ pub trait NexusExternalApi { instance_config.map(Into::into), ) .await?; - Ok(HttpResponseOk(resp.0.try_into()?)) + Ok(HttpResponseOk(resp.0.into())) } /// Update instance @@ -4423,7 +4419,7 @@ pub trait NexusExternalApi { path_params, ) .await?; - Ok(HttpResponseAccepted(resp.0.try_into()?)) + Ok(HttpResponseAccepted(resp.0.into())) } #[endpoint { @@ -4504,7 +4500,7 @@ pub trait NexusExternalApi { > { let resp = Self::instance_start(rqctx, query_params, path_params).await?; - Ok(HttpResponseAccepted(resp.0.try_into()?)) + Ok(HttpResponseAccepted(resp.0.into())) } #[endpoint { @@ -4593,7 +4589,7 @@ pub trait NexusExternalApi { path_params, ) .await?; - Ok(HttpResponseAccepted(resp.0.try_into()?)) + Ok(HttpResponseAccepted(resp.0.into())) } #[endpoint { diff --git a/nexus/src/app/background/tasks/instance_reincarnation.rs b/nexus/src/app/background/tasks/instance_reincarnation.rs index c9e548c006c..390a47af2f1 100644 --- a/nexus/src/app/background/tasks/instance_reincarnation.rs +++ b/nexus/src/app/background/tasks/instance_reincarnation.rs @@ -414,7 +414,7 @@ mod test { anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), enable_jumbo_frames: false, - shutdown_policy: None, + shutdown_policy: instance::InstanceShutdownPolicy::default(), }, ) .await; diff --git a/nexus/src/app/background/tasks/vmm_stop_for_update.rs b/nexus/src/app/background/tasks/vmm_stop_for_update.rs index 52ff75f9860..a248f386c93 100644 --- a/nexus/src/app/background/tasks/vmm_stop_for_update.rs +++ b/nexus/src/app/background/tasks/vmm_stop_for_update.rs @@ -298,7 +298,7 @@ impl VmmStopForUpdate { InstanceShutdownPolicy::HardOff => None, InstanceShutdownPolicy::PowerButton { timeout_secs, - } => Some(timeout_secs), + } => Some(u64::from(timeout_secs)), }, }, ) @@ -772,7 +772,7 @@ mod tests { )], multicast_groups: Vec::new(), enable_jumbo_frames: false, - shutdown_policy: Some(InstanceShutdownPolicy::HardOff), + shutdown_policy: InstanceShutdownPolicy::HardOff, }, ) .await; diff --git a/nexus/src/app/instance.rs b/nexus/src/app/instance.rs index d158fc7dbce..dfafb1de5e2 100644 --- a/nexus/src/app/instance.rs +++ b/nexus/src/app/instance.rs @@ -40,7 +40,6 @@ use nexus_db_queries::db::DataStore; use nexus_db_queries::db::datastore::InstanceAndActiveVmm; use nexus_db_queries::db::identity::Resource; use nexus_db_queries::db::model::InstanceStateComputer; -use nexus_defaults::MAX_ACPI_SHUTDOWN_TIMEOUT_SECS; use nexus_types::external_api::disk; use nexus_types::external_api::external_ip; use nexus_types::external_api::instance; @@ -584,7 +583,6 @@ impl super::Nexus { } = params; check_instance_cpu_memory_sizes(*ncpus, *memory)?; - check_instance_shutdown_policy_valid(shutdown_policy)?; // Opting in to jumbo frames requires the fleet-wide opt-in. Opting out // (`Some(false)`) is always allowed. @@ -623,37 +621,15 @@ impl super::Nexus { None => None, }; - let auto_restart_policy = auto_restart_policy.map(Into::into); - let ncpus = (*ncpus).into(); - let memory = (*memory).into(); - let cpu_platform = cpu_platform.map(Into::into); - - let (shutdown_policy_action, shutdown_policy_timeout) = - match shutdown_policy { - Some(instance::InstanceShutdownPolicy::HardOff) => ( - Some(nexus_db_model::InstanceShutdownAction::HardOff), - None, - ), - Some(instance::InstanceShutdownPolicy::PowerButton { - timeout_secs, - }) => ( - Some(nexus_db_model::InstanceShutdownAction::PowerButton), - Some(chrono::TimeDelta::seconds((*timeout_secs) as i64)), - ), - // if policy action omitted from the update request, leave as-is - None => (None, None), - }; - - let update = InstanceUpdate { + let update = InstanceUpdate::new( boot_disk_id, - auto_restart_policy, - ncpus, - memory, - cpu_platform, - enable_jumbo_frames: *enable_jumbo_frames, - shutdown_policy_action, - shutdown_policy_timeout, - }; + **auto_restart_policy, + *ncpus, + *memory, + **cpu_platform, + *enable_jumbo_frames, + *shutdown_policy, + ); // Update the instance configuration let result = self @@ -692,7 +668,6 @@ impl super::Nexus { project_lookup.lookup_for(authz::Action::CreateChild).await?; check_instance_cpu_memory_sizes(params.ncpus, params.memory)?; - check_instance_shutdown_policy_valid(¶ms.shutdown_policy)?; let all_disks: Vec<&instance::InstanceDiskAttachment> = params.boot_disk.iter().chain(params.disks.iter()).collect(); @@ -823,13 +798,6 @@ impl super::Nexus { ) .await?; - // Default to hard-poweroff when instance stop or reboot is requested, - // consistent with the behavior prior to ACPI shutdown support. - let shutdown_policy = - Some(params.shutdown_policy.to_owned().unwrap_or( - instance::InstanceShutdownPolicy::HardOff, - )); - // It is deceptively inconvenient to do an early check that the boot // disk is valid here! We accept boot disk by name or ID, but disk // creation and attachment requests as part of instance creation all @@ -844,7 +812,6 @@ impl super::Nexus { create_params: instance::InstanceCreate { ssh_public_keys: ssh_keys, anti_affinity_groups, - shutdown_policy, ..params.clone() }, boundary_switches: self @@ -1487,7 +1454,7 @@ impl super::Nexus { instance::InstanceShutdownPolicy::HardOff => None, instance::InstanceShutdownPolicy::PowerButton { timeout_secs, - } => Some(timeout_secs), + } => Some(u64::from(timeout_secs)), } } _ => None, @@ -3004,25 +2971,6 @@ fn check_instance_cpu_memory_sizes( Ok(()) } -fn check_instance_shutdown_policy_valid( - policy: &Option, -) -> Result<(), Error> { - const MAX_PLUS_1: u64 = MAX_ACPI_SHUTDOWN_TIMEOUT_SECS + 1; - if let Some(instance::InstanceShutdownPolicy::PowerButton { - timeout_secs: 0 | MAX_PLUS_1.., - }) = policy - { - return Err(Error::invalid_value( - "timeout_secs", - format!( - "shutdown timeout must be between \ - 1 and {MAX_ACPI_SHUTDOWN_TIMEOUT_SECS}" - ), - )); - } - Ok(()) -} - /// Effective MTU for an instance's primary OPTE port. Jumbo frames are only /// applied when both the fleet-wide opt-in is enabled and the per-instance bit /// is set; otherwise this returns `None` and OPTE applies the default MTU. @@ -3132,6 +3080,7 @@ mod tests { use core::time::Duration; use futures::{SinkExt, StreamExt}; use instance::InstanceNetworkInterfaceAttachment; + use instance::InstanceShutdownPolicy; use nexus_db_model::{ Instance as DbInstance, InstanceState as DbInstanceState, VmmCpuPlatform, VmmState as DbVmmState, @@ -3263,7 +3212,7 @@ mod tests { anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), enable_jumbo_frames: false, - shutdown_policy: None, + shutdown_policy: InstanceShutdownPolicy::default(), }; let instance_id = InstanceUuid::from_untyped_uuid(Uuid::new_v4()); @@ -3421,7 +3370,7 @@ mod tests { anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), enable_jumbo_frames, - shutdown_policy: None, + shutdown_policy: InstanceShutdownPolicy::default(), }; let instance_id = InstanceUuid::from_untyped_uuid(Uuid::new_v4()); let project_id = ProjectUuid::new_v4(); diff --git a/nexus/src/app/sagas/instance_create.rs b/nexus/src/app/sagas/instance_create.rs index 6b90be49ed0..71796b320b6 100644 --- a/nexus/src/app/sagas/instance_create.rs +++ b/nexus/src/app/sagas/instance_create.rs @@ -1565,7 +1565,8 @@ pub mod test { anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), enable_jumbo_frames: false, - shutdown_policy: None, + shutdown_policy: + instance_types::InstanceShutdownPolicy::default(), }, boundary_switches: HashSet::from([SwitchSlot::Switch0]), } diff --git a/nexus/src/app/sagas/instance_delete.rs b/nexus/src/app/sagas/instance_delete.rs index a1fd7f6ef97..d270bc1ad69 100644 --- a/nexus/src/app/sagas/instance_delete.rs +++ b/nexus/src/app/sagas/instance_delete.rs @@ -321,7 +321,7 @@ mod test { anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), enable_jumbo_frames: false, - shutdown_policy: None, + shutdown_policy: instance_types::InstanceShutdownPolicy::default(), } } diff --git a/nexus/src/app/sagas/instance_migrate.rs b/nexus/src/app/sagas/instance_migrate.rs index 11fe0623a4a..f6fc7bc143e 100644 --- a/nexus/src/app/sagas/instance_migrate.rs +++ b/nexus/src/app/sagas/instance_migrate.rs @@ -680,7 +680,8 @@ mod tests { anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), enable_jumbo_frames: false, - shutdown_policy: None, + shutdown_policy: + instance_types::InstanceShutdownPolicy::default(), }, ) .await diff --git a/nexus/src/app/sagas/instance_start.rs b/nexus/src/app/sagas/instance_start.rs index d976fe5a4eb..df4c5428f62 100644 --- a/nexus/src/app/sagas/instance_start.rs +++ b/nexus/src/app/sagas/instance_start.rs @@ -1234,7 +1234,8 @@ mod test { anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), enable_jumbo_frames: false, - shutdown_policy: None, + shutdown_policy: + instance_types::InstanceShutdownPolicy::default(), }, ) .await diff --git a/nexus/src/app/sagas/instance_update/mod.rs b/nexus/src/app/sagas/instance_update/mod.rs index 682085660cc..a430ae64c81 100644 --- a/nexus/src/app/sagas/instance_update/mod.rs +++ b/nexus/src/app/sagas/instance_update/mod.rs @@ -1891,7 +1891,8 @@ mod test { anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), enable_jumbo_frames: false, - shutdown_policy: None, + shutdown_policy: + instance_types::InstanceShutdownPolicy::default(), }, ) .await diff --git a/nexus/src/app/sagas/snapshot_create.rs b/nexus/src/app/sagas/snapshot_create.rs index 7675b01976a..1dd0a40d9cd 100644 --- a/nexus/src/app/sagas/snapshot_create.rs +++ b/nexus/src/app/sagas/snapshot_create.rs @@ -2202,7 +2202,8 @@ mod test { anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), enable_jumbo_frames: false, - shutdown_policy: None, + shutdown_policy: + instance_types::InstanceShutdownPolicy::default(), }, ) .await; diff --git a/nexus/src/app/sagas/test_helpers.rs b/nexus/src/app/sagas/test_helpers.rs index 509764cc37a..21e6e17b227 100644 --- a/nexus/src/app/sagas/test_helpers.rs +++ b/nexus/src/app/sagas/test_helpers.rs @@ -97,8 +97,7 @@ pub(crate) async fn instance_stop( .instance_stop( &opctx, &instance_lookup, - // XXX: vast majority of tests were written before new instances had - // the default config of an ACPI shutdown with 10 minute timeout + // always hard stop instances for tests (ignoring shutdown policy) true, ) .await @@ -124,8 +123,7 @@ pub(crate) async fn instance_stop_by_name( .instance_stop( &opctx, &instance_lookup, - // XXX: vast majority of tests were written before new instances had - // the default config of an ACPI shutdown with 10 minute timeout + // always hard stop instances for tests (ignoring shutdown policy) true, ) .await diff --git a/nexus/test-utils/src/resource_helpers.rs b/nexus/test-utils/src/resource_helpers.rs index 4214ff57a20..93714c408ea 100644 --- a/nexus/test-utils/src/resource_helpers.rs +++ b/nexus/test-utils/src/resource_helpers.rs @@ -905,7 +905,7 @@ pub async fn create_instance( // Multicast groups= Vec::::new(), // Shutdown policy= - Some(instance::InstanceShutdownPolicy::HardOff), + instance::InstanceShutdownPolicy::HardOff, ) .await } @@ -924,7 +924,7 @@ pub async fn create_instance_with( auto_restart_policy: Option, cpu_platform: Option, multicast_groups: Vec, - shutdown_policy: Option, + shutdown_policy: instance::InstanceShutdownPolicy, ) -> Instance { let url = format!("/v1/instances?project={}", project_name); diff --git a/nexus/tests/integration_tests/affinity.rs b/nexus/tests/integration_tests/affinity.rs index 0b7d7501ffa..b867aec0b94 100644 --- a/nexus/tests/integration_tests/affinity.rs +++ b/nexus/tests/integration_tests/affinity.rs @@ -78,7 +78,7 @@ impl ProjectScopedApiHelper<'_, T> { // Multicast groups= Vec::new(), // Shutdown policy= - None, + instance::InstanceShutdownPolicy::default(), ) .await } diff --git a/nexus/tests/integration_tests/audit_log.rs b/nexus/tests/integration_tests/audit_log.rs index 5bfafb8ae54..587ace1260c 100644 --- a/nexus/tests/integration_tests/audit_log.rs +++ b/nexus/tests/integration_tests/audit_log.rs @@ -411,7 +411,7 @@ async fn test_audit_log_create_delete_ops(ctx: &ControlPlaneTestContext) { None::, None::, Vec::new(), - None::, + InstanceShutdownPolicy::default(), ) .await; let _disk = create_disk(client, "test-project", "test-disk").await; diff --git a/nexus/tests/integration_tests/disks.rs b/nexus/tests/integration_tests/disks.rs index 792315f6ead..9f7fa488261 100644 --- a/nexus/tests/integration_tests/disks.rs +++ b/nexus/tests/integration_tests/disks.rs @@ -3235,7 +3235,7 @@ async fn test_delete_local_storage_disk_retries_on_transient_error( Default::default(), None, Vec::new(), - None, + instance::InstanceShutdownPolicy::default(), ) .await; let instance_id = InstanceUuid::from_untyped_uuid(instance.identity.id); diff --git a/nexus/tests/integration_tests/endpoints.rs b/nexus/tests/integration_tests/endpoints.rs index d41a2051f04..043b4704278 100644 --- a/nexus/tests/integration_tests/endpoints.rs +++ b/nexus/tests/integration_tests/endpoints.rs @@ -795,7 +795,7 @@ pub static DEMO_INSTANCE_CREATE: LazyLock = anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), enable_jumbo_frames: false, - shutdown_policy: None, + shutdown_policy: instance::InstanceShutdownPolicy::default(), }); pub static DEMO_STOPPED_INSTANCE_CREATE: LazyLock = LazyLock::new(|| instance::InstanceCreate { @@ -823,7 +823,7 @@ pub static DEMO_STOPPED_INSTANCE_CREATE: LazyLock = anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), enable_jumbo_frames: false, - shutdown_policy: None, + shutdown_policy: instance::InstanceShutdownPolicy::default(), }); pub static DEMO_INSTANCE_UPDATE: LazyLock = LazyLock::new(|| instance::InstanceUpdate { diff --git a/nexus/tests/integration_tests/external_ips.rs b/nexus/tests/integration_tests/external_ips.rs index dff70257f7f..273f0f05f86 100644 --- a/nexus/tests/integration_tests/external_ips.rs +++ b/nexus/tests/integration_tests/external_ips.rs @@ -1172,7 +1172,7 @@ async fn test_floating_ip_attach_fail_between_projects( anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), enable_jumbo_frames: false, - shutdown_policy: None, + shutdown_policy: instance::InstanceShutdownPolicy::default(), }, StatusCode::BAD_REQUEST, ) @@ -1711,7 +1711,7 @@ async fn cannot_attach_floating_ipv4_to_instance_missing_ipv4_stack( None, None, vec![], - None, + instance::InstanceShutdownPolicy::default(), ) .await; @@ -1780,7 +1780,7 @@ async fn cannot_attach_floating_ipv6_to_instance_missing_ipv6_stack( None, None, vec![], - None, + instance::InstanceShutdownPolicy::default(), ) .await; @@ -1847,7 +1847,7 @@ async fn cannot_attach_ephemeral_ipv4_to_instance_missing_ipv4_stack( None, None, vec![], - None, + instance::InstanceShutdownPolicy::default(), ) .await; @@ -1903,7 +1903,7 @@ async fn cannot_attach_ephemeral_ipv6_to_instance_missing_ipv6_stack( None, None, vec![], - None, + instance::InstanceShutdownPolicy::default(), ) .await; @@ -2098,7 +2098,8 @@ async fn can_create_instance_with_ephemeral_ipv6_address( /* auto_restart_policy = */ Default::default(), /* instance_cpu_platform = */ None, /* multicast_groups = */ vec![], - /* shutdown_policy = */ None, + /* shutdown_policy = */ + instance::InstanceShutdownPolicy::default(), ) .await; @@ -2202,7 +2203,8 @@ async fn can_create_instance_with_floating_ipv6_address( /* auto_restart_policy = */ Default::default(), /* instance_cpu_platform = */ None, /* multicast_groups = */ vec![], - /* shutdown_policy = */ None, + /* shutdown_policy = */ + instance::InstanceShutdownPolicy::default(), ) .await; @@ -2309,7 +2311,7 @@ async fn instance_for_external_ips( Default::default(), None, Vec::new(), - None, + instance::InstanceShutdownPolicy::default(), ) .await } @@ -2487,7 +2489,7 @@ async fn no_automatic_snat_for_ipv6(cptestctx: &ControlPlaneTestContext) { None, None, vec![], - None, + instance::InstanceShutdownPolicy::default(), ) .await; diff --git a/nexus/tests/integration_tests/external_subnets.rs b/nexus/tests/integration_tests/external_subnets.rs index d8eade2ea26..0af448c9e2a 100644 --- a/nexus/tests/integration_tests/external_subnets.rs +++ b/nexus/tests/integration_tests/external_subnets.rs @@ -371,7 +371,7 @@ async fn attach_test_impl( None, None, vec![], - None, + Default::default(), ) .await } @@ -794,7 +794,7 @@ async fn cannot_delete_attached_external_subnet( None, None, vec![], - None, + Default::default(), ) .await; let _ = attach_external_subnet(client, INSTANCE_NAME, EXTERNAL_SUBNET_NAME) @@ -1065,7 +1065,7 @@ async fn test_limited_collaborator_external_subnet_lifecycle( None, None, vec![], - None, + Default::default(), ) .await; diff --git a/nexus/tests/integration_tests/instances.rs b/nexus/tests/integration_tests/instances.rs index 92562cc478f..a1875a55b8e 100644 --- a/nexus/tests/integration_tests/instances.rs +++ b/nexus/tests/integration_tests/instances.rs @@ -295,7 +295,7 @@ async fn test_create_instance_with_bad_hostname_impl( anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), enable_jumbo_frames: false, - shutdown_policy: None, + shutdown_policy: instance::InstanceShutdownPolicy::default(), }; let mut body: serde_json::Value = serde_json::from_str(&serde_json::to_string(¶ms).unwrap()).unwrap(); @@ -406,7 +406,7 @@ async fn test_instances_create_reboot_halt( anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), enable_jumbo_frames: false, - shutdown_policy: None, + shutdown_policy: instance::InstanceShutdownPolicy::default(), })) .expect_status(Some(StatusCode::BAD_REQUEST)), ) @@ -845,7 +845,7 @@ async fn test_instance_migrate(cptestctx: &ControlPlaneTestContext) { Default::default(), None, Vec::new(), - None, + instance::InstanceShutdownPolicy::default(), ) .await; let instance_id = InstanceUuid::from_untyped_uuid(instance.identity.id); @@ -1138,7 +1138,7 @@ async fn test_instance_migrate_target_finishes_first( Default::default(), None, Vec::new(), - None, + instance::InstanceShutdownPolicy::default(), ) .await; let instance_id = InstanceUuid::from_untyped_uuid(instance.identity.id); @@ -1443,7 +1443,7 @@ async fn test_instance_migrate_v2p_and_routes( Default::default(), None, Vec::new(), - None, + instance::InstanceShutdownPolicy::default(), ) .await; let instance_id = InstanceUuid::from_untyped_uuid(instance.identity.id); @@ -1625,7 +1625,7 @@ async fn test_instance_migration_compatible_cpu_platforms( Default::default(), Some(InstanceCpuPlatform::AmdMilan), Vec::new(), - None, + instance::InstanceShutdownPolicy::default(), ) .await; let instance_id = InstanceUuid::from_untyped_uuid(instance.identity.id); @@ -1810,7 +1810,7 @@ async fn test_instance_migration_incompatible_cpu_platforms( Default::default(), Some(InstanceCpuPlatform::AmdTurin), Vec::new(), - None, + instance::InstanceShutdownPolicy::default(), ) .await; let instance_id = InstanceUuid::from_untyped_uuid(instance.identity.id); @@ -1900,7 +1900,7 @@ async fn test_instance_migration_unknown_sled_type( Default::default(), None, Vec::new(), - None, + instance::InstanceShutdownPolicy::default(), ) .await; let instance_id = InstanceUuid::from_untyped_uuid(instance.identity.id); @@ -1990,7 +1990,7 @@ async fn test_instance_migrate_autoselect_no_other_sled( Default::default(), None, Vec::new(), - Some(instance::InstanceShutdownPolicy::HardOff), + instance::InstanceShutdownPolicy::HardOff, ) .await; let instance_id = InstanceUuid::from_untyped_uuid(instance.identity.id); @@ -2067,7 +2067,7 @@ async fn test_instance_migrate_autoselect_skips_non_provisionable_sled( Default::default(), None, Vec::new(), - Some(instance::InstanceShutdownPolicy::HardOff), + instance::InstanceShutdownPolicy::HardOff, ) .await; let instance_id = InstanceUuid::from_untyped_uuid(instance.identity.id); @@ -2195,9 +2195,7 @@ async fn test_instance_migrate_autoselect_anti_affinity( )], multicast_groups: Vec::new(), enable_jumbo_frames: false, - shutdown_policy: Some( - instance::InstanceShutdownPolicy::HardOff, - ), + shutdown_policy: instance::InstanceShutdownPolicy::HardOff, }, ) .await; @@ -2460,7 +2458,7 @@ async fn test_instance_failed_when_on_expunged_sled( Some(auto_restart), None, Vec::new(), - None, + instance::InstanceShutdownPolicy::default(), ) .await; let instance_id = @@ -2812,7 +2810,7 @@ async fn make_forgotten_instance( Some(auto_restart), None, Vec::new(), - None, + instance::InstanceShutdownPolicy::default(), ) .await; let instance_id = InstanceUuid::from_untyped_uuid(instance.identity.id); @@ -3043,7 +3041,7 @@ async fn test_instance_metrics_with_migration( Default::default(), None, Vec::new(), - None, + instance::InstanceShutdownPolicy::default(), ) .await; let instance_id = InstanceUuid::from_untyped_uuid(instance.identity.id); @@ -3188,7 +3186,7 @@ async fn test_instances_create_stopped_start( auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), enable_jumbo_frames: false, - shutdown_policy: None, + shutdown_policy: instance::InstanceShutdownPolicy::default(), }, ) .await; @@ -3364,7 +3362,7 @@ async fn test_instance_using_image_from_other_project_fails( anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), enable_jumbo_frames: false, - shutdown_policy: None, + shutdown_policy: instance::InstanceShutdownPolicy::default(), })) .expect_status(Some(StatusCode::BAD_REQUEST)), ) @@ -3435,7 +3433,7 @@ async fn test_instance_create_saga_removes_instance_database_record( anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), enable_jumbo_frames: false, - shutdown_policy: None, + shutdown_policy: instance::InstanceShutdownPolicy::default(), }; let response = NexusRequest::objects_post( client, @@ -3470,7 +3468,7 @@ async fn test_instance_create_saga_removes_instance_database_record( anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), enable_jumbo_frames: false, - shutdown_policy: None, + shutdown_policy: instance::InstanceShutdownPolicy::default(), }; let _ = NexusRequest::objects_post( client, @@ -3628,7 +3626,7 @@ async fn test_instance_with_single_explicit_ip_address_impl( anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), enable_jumbo_frames: false, - shutdown_policy: None, + shutdown_policy: instance::InstanceShutdownPolicy::default(), }; let response = NexusRequest::objects_post( client, @@ -3810,7 +3808,7 @@ async fn test_instance_with_new_custom_network_interfaces( anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), enable_jumbo_frames: false, - shutdown_policy: None, + shutdown_policy: instance::InstanceShutdownPolicy::default(), }; let response = NexusRequest::objects_post( client, @@ -3944,7 +3942,7 @@ async fn test_instance_create_delete_network_interface( anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), enable_jumbo_frames: false, - shutdown_policy: None, + shutdown_policy: instance::InstanceShutdownPolicy::default(), }; let response = NexusRequest::objects_post( client, @@ -4224,7 +4222,7 @@ async fn test_instance_update_network_interfaces( anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), enable_jumbo_frames: false, - shutdown_policy: None, + shutdown_policy: instance::InstanceShutdownPolicy::default(), }; let response = NexusRequest::objects_post( client, @@ -4650,7 +4648,7 @@ async fn cannot_make_new_primary_nic_lacking_ip_stack_for_external_addresses( anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), enable_jumbo_frames: false, - shutdown_policy: None, + shutdown_policy: instance::InstanceShutdownPolicy::default(), }; let response = NexusRequest::objects_post( client, @@ -4750,7 +4748,7 @@ async fn test_instance_update_network_interface_transit_ips( Default::default(), None, Vec::new(), - None, + instance::InstanceShutdownPolicy::default(), ) .await; @@ -5034,7 +5032,7 @@ async fn test_instance_with_multiple_nics_unwinds_completely( anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), enable_jumbo_frames: false, - shutdown_policy: None, + shutdown_policy: instance::InstanceShutdownPolicy::default(), }; let builder = RequestBuilder::new(client, http::Method::POST, &get_instances_url()) @@ -5112,7 +5110,7 @@ async fn test_attach_one_disk_to_instance(cptestctx: &ControlPlaneTestContext) { anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), enable_jumbo_frames: false, - shutdown_policy: None, + shutdown_policy: instance::InstanceShutdownPolicy::default(), }; let builder = @@ -5212,7 +5210,7 @@ async fn test_instance_create_attach_disks( anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), enable_jumbo_frames: false, - shutdown_policy: None, + shutdown_policy: instance::InstanceShutdownPolicy::default(), }; let builder = @@ -5321,7 +5319,7 @@ async fn test_instance_create_attach_disks_undo( anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), enable_jumbo_frames: false, - shutdown_policy: None, + shutdown_policy: instance::InstanceShutdownPolicy::default(), }; let builder = @@ -5410,7 +5408,7 @@ async fn test_attach_eight_disks_to_instance( anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), enable_jumbo_frames: false, - shutdown_policy: None, + shutdown_policy: instance::InstanceShutdownPolicy::default(), }; let builder = @@ -5536,7 +5534,7 @@ async fn test_disk_attach_limit(cptestctx: &ControlPlaneTestContext) { anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), enable_jumbo_frames: false, - shutdown_policy: None, + shutdown_policy: instance::InstanceShutdownPolicy::default(), }; let url_instances = format!("/v1/instances?project={}", project_name); @@ -5643,7 +5641,7 @@ async fn test_cannot_attach_faulted_disks(cptestctx: &ControlPlaneTestContext) { anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), enable_jumbo_frames: false, - shutdown_policy: None, + shutdown_policy: instance::InstanceShutdownPolicy::default(), }; let builder = @@ -5739,7 +5737,7 @@ async fn test_disks_detached_when_instance_destroyed( anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), enable_jumbo_frames: false, - shutdown_policy: None, + shutdown_policy: instance::InstanceShutdownPolicy::default(), }; let builder = @@ -5839,7 +5837,7 @@ async fn test_disks_detached_when_instance_destroyed( anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), enable_jumbo_frames: false, - shutdown_policy: None, + shutdown_policy: instance::InstanceShutdownPolicy::default(), }; let builder = @@ -5928,7 +5926,7 @@ async fn test_duplicate_disk_attach_requests_ok( anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), enable_jumbo_frames: false, - shutdown_policy: None, + shutdown_policy: instance::InstanceShutdownPolicy::default(), }; let builder = @@ -5977,7 +5975,7 @@ async fn test_duplicate_disk_attach_requests_ok( anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), enable_jumbo_frames: false, - shutdown_policy: None, + shutdown_policy: instance::InstanceShutdownPolicy::default(), }; let builder = @@ -6039,7 +6037,7 @@ async fn test_cannot_detach_boot_disk(cptestctx: &ControlPlaneTestContext) { auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), enable_jumbo_frames: false, - shutdown_policy: None, + shutdown_policy: instance::InstanceShutdownPolicy::default(), }; let builder = @@ -6182,7 +6180,7 @@ async fn test_updating_running_instance_boot_disk_is_conflict( anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), enable_jumbo_frames: false, - shutdown_policy: None, + shutdown_policy: instance::InstanceShutdownPolicy::default(), }; let builder = @@ -6363,7 +6361,7 @@ async fn test_size_can_be_changed(cptestctx: &ControlPlaneTestContext) { anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), enable_jumbo_frames: false, - shutdown_policy: None, + shutdown_policy: instance::InstanceShutdownPolicy::default(), }; let builder = @@ -6586,7 +6584,7 @@ async fn test_auto_restart_policy_can_be_changed( anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), enable_jumbo_frames: false, - shutdown_policy: None, + shutdown_policy: instance::InstanceShutdownPolicy::default(), }; let builder = @@ -6666,7 +6664,7 @@ async fn test_cpu_platform_can_be_changed(cptestctx: &ControlPlaneTestContext) { anti_affinity_groups: Vec::new(), multicast_groups: vec![], enable_jumbo_frames: false, - shutdown_policy: None, + shutdown_policy: instance::InstanceShutdownPolicy::default(), }; let builder = @@ -6747,7 +6745,7 @@ async fn test_enable_jumbo_frames_can_be_changed( multicast_groups: vec![], // Start out opted out. enable_jumbo_frames: false, - shutdown_policy: None, + shutdown_policy: instance::InstanceShutdownPolicy::default(), }; let builder = @@ -6891,7 +6889,7 @@ async fn test_boot_disk_can_be_changed(cptestctx: &ControlPlaneTestContext) { anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), enable_jumbo_frames: false, - shutdown_policy: None, + shutdown_policy: instance::InstanceShutdownPolicy::default(), }; let builder = @@ -6970,7 +6968,7 @@ async fn test_boot_disk_must_be_attached(cptestctx: &ControlPlaneTestContext) { anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), enable_jumbo_frames: false, - shutdown_policy: None, + shutdown_policy: instance::InstanceShutdownPolicy::default(), }; let builder = @@ -7075,7 +7073,7 @@ async fn test_instances_memory_rejected_less_than_min_memory_size( anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), enable_jumbo_frames: false, - shutdown_policy: None, + shutdown_policy: instance::InstanceShutdownPolicy::default(), }; let error = NexusRequest::new( @@ -7133,7 +7131,7 @@ async fn test_instances_memory_not_divisible_by_min_memory_size( anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), enable_jumbo_frames: false, - shutdown_policy: None, + shutdown_policy: instance::InstanceShutdownPolicy::default(), }; let error = NexusRequest::new( @@ -7191,7 +7189,7 @@ async fn test_instances_memory_greater_than_max_size( anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), enable_jumbo_frames: false, - shutdown_policy: None, + shutdown_policy: instance::InstanceShutdownPolicy::default(), }; let error = NexusRequest::new( @@ -7302,7 +7300,7 @@ async fn test_instance_create_with_anti_affinity_groups( auto_restart_policy: Default::default(), anti_affinity_groups: anti_affinity_groups_param, enable_jumbo_frames: false, - shutdown_policy: None, + shutdown_policy: instance::InstanceShutdownPolicy::default(), }; let builder = @@ -7376,7 +7374,7 @@ async fn test_instance_create_with_duplicate_anti_affinity_groups( auto_restart_policy: Default::default(), anti_affinity_groups: anti_affinity_groups_param, enable_jumbo_frames: false, - shutdown_policy: None, + shutdown_policy: instance::InstanceShutdownPolicy::default(), }; let builder = @@ -7451,7 +7449,7 @@ async fn test_instance_create_with_anti_affinity_groups_that_do_not_exist( auto_restart_policy: Default::default(), anti_affinity_groups: anti_affinity_groups_param, enable_jumbo_frames: false, - shutdown_policy: None, + shutdown_policy: instance::InstanceShutdownPolicy::default(), }; let error = object_create_error( @@ -7539,7 +7537,7 @@ async fn test_instance_create_with_ssh_keys( auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), enable_jumbo_frames: false, - shutdown_policy: None, + shutdown_policy: instance::InstanceShutdownPolicy::default(), }; let builder = @@ -7593,7 +7591,7 @@ async fn test_instance_create_with_ssh_keys( auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), enable_jumbo_frames: false, - shutdown_policy: None, + shutdown_policy: instance::InstanceShutdownPolicy::default(), }; let builder = @@ -7646,7 +7644,7 @@ async fn test_instance_create_with_ssh_keys( auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), enable_jumbo_frames: false, - shutdown_policy: None, + shutdown_policy: instance::InstanceShutdownPolicy::default(), }; let builder = @@ -7793,7 +7791,7 @@ async fn test_cannot_provision_instance_beyond_cpu_capacity( auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), enable_jumbo_frames: false, - shutdown_policy: None, + shutdown_policy: instance::InstanceShutdownPolicy::default(), }; let url_instances = get_instances_url(); @@ -7857,7 +7855,7 @@ async fn test_cannot_provision_instance_beyond_cpu_limit( auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), enable_jumbo_frames: false, - shutdown_policy: None, + shutdown_policy: instance::InstanceShutdownPolicy::default(), }; let url_instances = get_instances_url(); @@ -7917,7 +7915,7 @@ async fn test_cannot_provision_instance_beyond_ram_capacity( auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), enable_jumbo_frames: false, - shutdown_policy: None, + shutdown_policy: instance::InstanceShutdownPolicy::default(), }; let url_instances = get_instances_url(); @@ -7977,7 +7975,7 @@ async fn test_can_start_instance_with_cpu_platform( anti_affinity_groups: Vec::new(), multicast_groups: vec![], enable_jumbo_frames: false, - shutdown_policy: None, + shutdown_policy: instance::InstanceShutdownPolicy::default(), }; let url_instances = get_instances_url(); @@ -8127,7 +8125,7 @@ async fn test_cannot_start_instance_with_unsatisfiable_cpu_platform( anti_affinity_groups: Vec::new(), multicast_groups: vec![], enable_jumbo_frames: false, - shutdown_policy: None, + shutdown_policy: instance::InstanceShutdownPolicy::default(), }; let url_instances = get_instances_url(); @@ -8442,7 +8440,7 @@ async fn test_instance_ephemeral_ip_from_correct_pool( anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), enable_jumbo_frames: false, - shutdown_policy: None, + shutdown_policy: instance::InstanceShutdownPolicy::default(), }; let error = object_create_error( client, @@ -8525,7 +8523,7 @@ async fn test_instance_ephemeral_ip_from_orphan_pool( anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), enable_jumbo_frames: false, - shutdown_policy: None, + shutdown_policy: instance::InstanceShutdownPolicy::default(), }; // instance create 404s @@ -8594,7 +8592,7 @@ async fn test_instance_ephemeral_ip_no_default_pool_error( anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), enable_jumbo_frames: false, - shutdown_policy: None, + shutdown_policy: instance::InstanceShutdownPolicy::default(), }; let url = format!("/v1/instances?project={}", PROJECT_NAME); @@ -8631,7 +8629,7 @@ async fn test_instance_ephemeral_ip_no_default_pool_error( anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), enable_jumbo_frames: false, - shutdown_policy: None, + shutdown_policy: instance::InstanceShutdownPolicy::default(), }; let error = object_create_error(client, &url, &body, StatusCode::NOT_FOUND).await; @@ -8703,7 +8701,7 @@ async fn test_instance_attach_several_external_ips( Default::default(), None, Vec::new(), - None, + instance::InstanceShutdownPolicy::default(), ) .await; @@ -8785,7 +8783,7 @@ async fn test_instance_rejects_three_ephemeral_ips( anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), enable_jumbo_frames: false, - shutdown_policy: None, + shutdown_policy: instance::InstanceShutdownPolicy::default(), }; let error = object_create_error( client, @@ -8838,7 +8836,7 @@ async fn test_instance_rejects_two_ephemeral_auto_without_version( anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), enable_jumbo_frames: false, - shutdown_policy: None, + shutdown_policy: instance::InstanceShutdownPolicy::default(), }; let error = object_create_error( client, @@ -8897,7 +8895,7 @@ async fn test_instance_rejects_two_ephemeral_auto_none_with_explicit( anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), enable_jumbo_frames: false, - shutdown_policy: None, + shutdown_policy: instance::InstanceShutdownPolicy::default(), }; let error = object_create_error( client, @@ -8956,7 +8954,7 @@ async fn test_instance_rejects_two_ephemeral_same_pool( anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), enable_jumbo_frames: false, - shutdown_policy: None, + shutdown_policy: instance::InstanceShutdownPolicy::default(), }; let error = object_create_error( client, @@ -8995,7 +8993,7 @@ async fn create_instance_with_pool( Default::default(), None, Vec::new(), - None, + instance::InstanceShutdownPolicy::default(), ) .await } @@ -9120,7 +9118,7 @@ async fn test_instance_create_in_silo(cptestctx: &ControlPlaneTestContext) { anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), enable_jumbo_frames: false, - shutdown_policy: None, + shutdown_policy: instance::InstanceShutdownPolicy::default(), }; let url_instances = format!("/v1/instances?project={}", PROJECT_NAME); NexusRequest::objects_post(client, &url_instances, &instance_params) @@ -9307,7 +9305,7 @@ async fn test_instance_create_with_cross_project_subnet( auto_restart_policy: None, anti_affinity_groups: Vec::new(), enable_jumbo_frames: false, - shutdown_policy: None, + shutdown_policy: instance::InstanceShutdownPolicy::default(), }; let instances_url_a = format!("/v1/instances?project={}", project_a_name); @@ -9438,7 +9436,7 @@ async fn test_silo_limited_collaborator_cross_project_subnet( auto_restart_policy: None, anti_affinity_groups: Vec::new(), enable_jumbo_frames: false, - shutdown_policy: None, + shutdown_policy: instance::InstanceShutdownPolicy::default(), }; let instances_url_a = format!("/v1/instances?project={}", project_a_name); @@ -9507,7 +9505,7 @@ async fn test_silo_limited_collaborator_cross_project_subnet( auto_restart_policy: None, anti_affinity_groups: Vec::new(), enable_jumbo_frames: false, - shutdown_policy: None, + shutdown_policy: instance::InstanceShutdownPolicy::default(), }; // Should get 404 Not Found because VPC/subnet lookups are scoped to the @@ -10347,7 +10345,7 @@ async fn test_instance_with_max_disks(cptestctx: &ControlPlaneTestContext) { anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), enable_jumbo_frames: false, - shutdown_policy: None, + shutdown_policy: instance::InstanceShutdownPolicy::default(), }; let instance: Instance = @@ -10442,7 +10440,7 @@ async fn can_create_instance_with_multiple_nics_and_ephemeral_ip( None, None, vec![], - None, + instance::InstanceShutdownPolicy::default(), ) .await; } @@ -10450,7 +10448,7 @@ async fn can_create_instance_with_multiple_nics_and_ephemeral_ip( async fn setup_shutdown_test_instance_and_mock_propolis( cptestctx: &ControlPlaneTestContext, instance_name: &str, - shutdown_policy: Option, + shutdown_policy: instance::InstanceShutdownPolicy, ) -> (Instance, std::net::SocketAddr) { let client = &cptestctx.external_client; create_project_and_pool(&client).await; @@ -10552,24 +10550,23 @@ async fn test_instances_shutdown_policy_force_hardstop_during_soft( let client = &cptestctx.external_client; let instance_name = "resonance-cascade"; + // Create an instance with soft shutdown policy. const TIMEOUT_SECS: u64 = 42; let (instance, propolis_addr) = setup_shutdown_test_instance_and_mock_propolis( &cptestctx, instance_name, - Some(instance::InstanceShutdownPolicy::PowerButton { - timeout_secs: TIMEOUT_SECS, - }), + instance::InstanceShutdownPolicy::PowerButton { + timeout_secs: TIMEOUT_SECS.try_into().unwrap(), + }, ) .await; - // instance create API endpoint sets default policy - // (default in DB level is HardOff for migration to preserve existing behaviors) assert_eq!( instance.shutdown_policy, - Some(instance::InstanceShutdownPolicy::PowerButton { - timeout_secs: TIMEOUT_SECS, - }) + instance::InstanceShutdownPolicy::PowerButton { + timeout_secs: TIMEOUT_SECS.try_into().unwrap(), + } ); // check the instance receives a shutdown request on stop @@ -10600,20 +10597,19 @@ async fn test_instances_shutdown_policy_force_hardstop( let client = &cptestctx.external_client; let instance_name = "resonance-cascade"; const TIMEOUT_SECS: u64 = 531; - const POLICY: Option = - Some(instance::InstanceShutdownPolicy::PowerButton { - timeout_secs: TIMEOUT_SECS, - }); + let policy = instance::InstanceShutdownPolicy::PowerButton { + timeout_secs: TIMEOUT_SECS.try_into().unwrap(), + }; let (instance, propolis_addr) = setup_shutdown_test_instance_and_mock_propolis( &cptestctx, instance_name, - POLICY, + policy, ) .await; - assert_eq!(instance.shutdown_policy, POLICY); + assert_eq!(instance.shutdown_policy, policy); // stop with skip_os_shutdown=true param should request normal Stop // to propolis, ignoring configured soft-off policy. @@ -10634,20 +10630,19 @@ async fn test_instances_shutdown_policy_change_policy_and_send_hardstop_during_s let client = &cptestctx.external_client; let instance_name = "resonance-cascade"; const TIMEOUT_SECS: u64 = 42; - const POLICY: Option = - Some(instance::InstanceShutdownPolicy::PowerButton { - timeout_secs: TIMEOUT_SECS, - }); + let policy = instance::InstanceShutdownPolicy::PowerButton { + timeout_secs: TIMEOUT_SECS.try_into().unwrap(), + }; let (instance_soft, propolis_addr) = setup_shutdown_test_instance_and_mock_propolis( &cptestctx, instance_name, - POLICY, + policy, ) .await; - assert_eq!(instance_soft.shutdown_policy, POLICY); + assert_eq!(instance_soft.shutdown_policy, policy); expect_instance_stop_ok(client, instance_name, false).await; assert_matches::assert_matches!( @@ -10676,7 +10671,7 @@ async fn test_instances_shutdown_policy_change_policy_and_send_hardstop_during_s .await; assert_eq!( instance_hard.shutdown_policy, - Some(instance::InstanceShutdownPolicy::HardOff) + instance::InstanceShutdownPolicy::HardOff ); // stop without forcing, still is hard-off due to new policy @@ -10698,18 +10693,17 @@ async fn test_instances_shutdown_policy_change_timeout_and_resend( let instance_name = "resonance-cascade"; const TIMEOUT_SECS: u64 = 321; - const POLICY: Option = - Some(instance::InstanceShutdownPolicy::PowerButton { - timeout_secs: TIMEOUT_SECS, - }); + let policy = instance::InstanceShutdownPolicy::PowerButton { + timeout_secs: TIMEOUT_SECS.try_into().unwrap(), + }; let (instance_default, propolis_addr) = setup_shutdown_test_instance_and_mock_propolis( &cptestctx, instance_name, - POLICY, + policy, ) .await; - assert_eq!(instance_default.shutdown_policy, POLICY); + assert_eq!(instance_default.shutdown_policy, policy); // configured timeout is sent with normal stop request expect_instance_stop_ok(client, instance_name, false).await; @@ -10722,10 +10716,9 @@ async fn test_instances_shutdown_policy_change_timeout_and_resend( ); const NEW_TIMEOUT_SECS: u64 = 76; - const NEW_POLICY: Option = - Some(instance::InstanceShutdownPolicy::PowerButton { - timeout_secs: NEW_TIMEOUT_SECS, - }); + let new_policy = instance::InstanceShutdownPolicy::PowerButton { + timeout_secs: NEW_TIMEOUT_SECS.try_into().unwrap(), + }; let instance_soft = expect_instance_reconfigure_ok( client, &instance_default.identity.id, @@ -10738,15 +10731,15 @@ async fn test_instances_shutdown_policy_change_timeout_and_resend( multicast_groups: None, enable_jumbo_frames: instance_default.enable_jumbo_frames, // change shutdown policy to soft-off and different timeout - shutdown_policy: NEW_POLICY, + shutdown_policy: Some(new_policy), }, ) .await; assert_eq!( instance_soft.shutdown_policy, - Some(instance::InstanceShutdownPolicy::PowerButton { - timeout_secs: NEW_TIMEOUT_SECS - }) + instance::InstanceShutdownPolicy::PowerButton { + timeout_secs: NEW_TIMEOUT_SECS.try_into().unwrap(), + } ); // we still *send* propolis the different timeout (but per rfd622 it will @@ -10769,16 +10762,15 @@ async fn test_instances_shutdown_policy_unchanged_when_omitted_in_update( let instance_name = "resonance-cascade"; const TIMEOUT_SECS: u64 = 321; - const POLICY: Option = - Some(instance::InstanceShutdownPolicy::PowerButton { - timeout_secs: TIMEOUT_SECS, - }); + let policy = instance::InstanceShutdownPolicy::PowerButton { + timeout_secs: TIMEOUT_SECS.try_into().unwrap(), + }; let (instance_default, propolis_addr) = setup_shutdown_test_instance_and_mock_propolis( &cptestctx, instance_name, // Create an instance with a shutdown policy. - POLICY, + policy, ) .await; @@ -10798,11 +10790,11 @@ async fn test_instances_shutdown_policy_unchanged_when_omitted_in_update( }, ) .await; - assert_eq!(instance_unchanged.shutdown_policy, POLICY); + assert_eq!(instance_unchanged.shutdown_policy, policy); // test that hard-off is not sent for an unforced stop; that is, the // 'None' shutdown policy in the update request didn't reset it to the - // default HardOff configuration that it would've in a create request. + // default HardOff configuration. expect_instance_stop_ok(client, instance_name, false).await; assert_matches::assert_matches!( mock_get_last_state_requested(propolis_addr).await, @@ -10820,20 +10812,19 @@ async fn test_instances_shutdown_policy_reboot( let client = &cptestctx.external_client; let instance_name = "resonance-cascade"; const CUSTOM_TIMEOUT_SECS: u64 = 531; - const CUSTOM_POLICY: Option = - Some(instance::InstanceShutdownPolicy::PowerButton { - timeout_secs: CUSTOM_TIMEOUT_SECS, - }); + let custom_policy = instance::InstanceShutdownPolicy::PowerButton { + timeout_secs: CUSTOM_TIMEOUT_SECS.try_into().unwrap(), + }; let (instance, propolis_addr) = setup_shutdown_test_instance_and_mock_propolis( &cptestctx, instance_name, - CUSTOM_POLICY, + custom_policy, ) .await; - assert_eq!(instance.shutdown_policy, CUSTOM_POLICY); + assert_eq!(instance.shutdown_policy, custom_policy); expect_instance_reboot_ok(client, instance_name).await; assert_matches::assert_matches!( @@ -10852,20 +10843,19 @@ async fn test_instances_shutdown_policy_reboot_after_stop( let client = &cptestctx.external_client; let instance_name = "resonance-cascade"; const CUSTOM_TIMEOUT_SECS: u64 = 531; - const CUSTOM_POLICY: Option = - Some(instance::InstanceShutdownPolicy::PowerButton { - timeout_secs: CUSTOM_TIMEOUT_SECS, - }); + let custom_policy = instance::InstanceShutdownPolicy::PowerButton { + timeout_secs: CUSTOM_TIMEOUT_SECS.try_into().unwrap(), + }; let (instance, propolis_addr) = setup_shutdown_test_instance_and_mock_propolis( &cptestctx, instance_name, - CUSTOM_POLICY, + custom_policy, ) .await; - assert_eq!(instance.shutdown_policy, CUSTOM_POLICY); + assert_eq!(instance.shutdown_policy, custom_policy); expect_instance_stop_ok(client, instance_name, false).await; assert_matches::assert_matches!( diff --git a/nexus/tests/integration_tests/internet_gateway.rs b/nexus/tests/integration_tests/internet_gateway.rs index 18016056a74..660d3c6edad 100644 --- a/nexus/tests/integration_tests/internet_gateway.rs +++ b/nexus/tests/integration_tests/internet_gateway.rs @@ -20,7 +20,8 @@ use nexus_test_utils_macros::nexus_test; use nexus_types::external_api::floating_ip; use nexus_types::external_api::instance::{ ExternalIpCreate, InstanceNetworkInterfaceAttachment, - InstanceNetworkInterfaceCreate, PrivateIpStackCreate, + InstanceNetworkInterfaceCreate, InstanceShutdownPolicy, + PrivateIpStackCreate, }; use nexus_types::external_api::internet_gateway::{ InternetGateway, InternetGatewayIpAddress, InternetGatewayIpPool, @@ -686,7 +687,7 @@ async fn test_setup(c: &ClientTestContext) { None, None, Vec::new(), - None, + InstanceShutdownPolicy::default(), ) .await; diff --git a/nexus/tests/integration_tests/projects.rs b/nexus/tests/integration_tests/projects.rs index 1de742ea5dd..46cdb2d5dab 100644 --- a/nexus/tests/integration_tests/projects.rs +++ b/nexus/tests/integration_tests/projects.rs @@ -149,9 +149,7 @@ async fn test_project_create_defaults(cptestctx: &ControlPlaneTestContext) { anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), enable_jumbo_frames: false, - shutdown_policy: Some( - instance::InstanceShutdownPolicy::HardOff, - ), + shutdown_policy: instance::InstanceShutdownPolicy::HardOff, }, StatusCode::NOT_FOUND, ) @@ -327,7 +325,7 @@ async fn test_project_deletion_with_instance( anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), enable_jumbo_frames: false, - shutdown_policy: None, + shutdown_policy: instance::InstanceShutdownPolicy::default(), }, ) .await; diff --git a/nexus/tests/integration_tests/quotas.rs b/nexus/tests/integration_tests/quotas.rs index 8aba194e25c..0376de1c872 100644 --- a/nexus/tests/integration_tests/quotas.rs +++ b/nexus/tests/integration_tests/quotas.rs @@ -124,7 +124,7 @@ impl ResourceAllocator { anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), enable_jumbo_frames: false, - shutdown_policy: None, + shutdown_policy: instance::InstanceShutdownPolicy::default(), }, ) .authn_as(self.auth.clone()) diff --git a/nexus/tests/integration_tests/sled_bp_availability.rs b/nexus/tests/integration_tests/sled_bp_availability.rs index ae4749f1b8b..58840500f1a 100644 --- a/nexus/tests/integration_tests/sled_bp_availability.rs +++ b/nexus/tests/integration_tests/sled_bp_availability.rs @@ -149,7 +149,7 @@ async fn test_instance_placement_honors_blueprint_availability( Default::default(), None, Vec::new(), - Some(instance::InstanceShutdownPolicy::HardOff), + instance::InstanceShutdownPolicy::HardOff, ) .await; let instance_id = InstanceUuid::from_untyped_uuid(instance.identity.id); diff --git a/nexus/tests/integration_tests/snapshots.rs b/nexus/tests/integration_tests/snapshots.rs index 3ad098d265d..40835ccae57 100644 --- a/nexus/tests/integration_tests/snapshots.rs +++ b/nexus/tests/integration_tests/snapshots.rs @@ -144,7 +144,7 @@ async fn test_snapshot_basic(cptestctx: &ControlPlaneTestContext) { anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), enable_jumbo_frames: false, - shutdown_policy: None, + shutdown_policy: instance::InstanceShutdownPolicy::default(), }, ) .await; @@ -328,7 +328,7 @@ async fn test_snapshot_stopped_instance(cptestctx: &ControlPlaneTestContext) { anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), enable_jumbo_frames: false, - shutdown_policy: None, + shutdown_policy: instance::InstanceShutdownPolicy::default(), }, ) .await; diff --git a/nexus/tests/integration_tests/subnet_allocation.rs b/nexus/tests/integration_tests/subnet_allocation.rs index 792516f1d88..57be41d850d 100644 --- a/nexus/tests/integration_tests/subnet_allocation.rs +++ b/nexus/tests/integration_tests/subnet_allocation.rs @@ -73,7 +73,7 @@ async fn create_instance_expect_failure( anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), enable_jumbo_frames: false, - shutdown_policy: None, + shutdown_policy: instance::InstanceShutdownPolicy::default(), }; NexusRequest::new( @@ -166,7 +166,7 @@ async fn test_subnet_allocation(cptestctx: &ControlPlaneTestContext) { Default::default(), None, Vec::new(), - None, + instance::InstanceShutdownPolicy::default(), ) .await; } diff --git a/nexus/tests/integration_tests/utilization.rs b/nexus/tests/integration_tests/utilization.rs index c8cca6422fa..b8db05e1853 100644 --- a/nexus/tests/integration_tests/utilization.rs +++ b/nexus/tests/integration_tests/utilization.rs @@ -248,7 +248,7 @@ async fn create_resources_in_test_suite_silo( anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), enable_jumbo_frames: false, - shutdown_policy: None, + shutdown_policy: instance::InstanceShutdownPolicy::default(), }; NexusRequest::objects_post( diff --git a/nexus/tests/integration_tests/vpc_routers.rs b/nexus/tests/integration_tests/vpc_routers.rs index 601a9fd8a21..c6e44c7d204 100644 --- a/nexus/tests/integration_tests/vpc_routers.rs +++ b/nexus/tests/integration_tests/vpc_routers.rs @@ -518,7 +518,7 @@ async fn test_vpc_routers_custom_delivered_to_instance( Default::default(), None, Vec::new(), - None, + instance::InstanceShutdownPolicy::default(), ) .await; instance_simulate( diff --git a/nexus/tests/integration_tests/vpcs.rs b/nexus/tests/integration_tests/vpcs.rs index b75ff7467a6..bbef5a2aebc 100644 --- a/nexus/tests/integration_tests/vpcs.rs +++ b/nexus/tests/integration_tests/vpcs.rs @@ -525,7 +525,7 @@ async fn test_limited_collaborator_can_create_instance( auto_restart_policy: None, anti_affinity_groups: vec![], enable_jumbo_frames: false, - shutdown_policy: None, + shutdown_policy: instance::InstanceShutdownPolicy::default(), }, ) .authn_as(AuthnMode::SiloUser(limited_user.id)) diff --git a/nexus/types/versions/Cargo.toml b/nexus/types/versions/Cargo.toml index 37edec5fadb..2b326d78bfa 100644 --- a/nexus/types/versions/Cargo.toml +++ b/nexus/types/versions/Cargo.toml @@ -18,7 +18,6 @@ http.workspace = true lldp_protocol.workspace = true mg-admin-client.workspace = true mg-api-types.workspace = true -nexus-defaults.workspace = true omicron-common.workspace = true omicron-passwords.workspace = true omicron-uuid-kinds.workspace = true diff --git a/nexus/types/versions/src/instance_acpi_shutdown/instance.rs b/nexus/types/versions/src/instance_acpi_shutdown/instance.rs index dc49fd91963..f7682bd3397 100644 --- a/nexus/types/versions/src/instance_acpi_shutdown/instance.rs +++ b/nexus/types/versions/src/instance_acpi_shutdown/instance.rs @@ -2,6 +2,7 @@ // 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 anyhow::bail; use api_identity::ObjectIdentity; use omicron_common::api::external::{ ByteCount, Hostname, IdentityMetadata, IdentityMetadataCreateParams, @@ -24,25 +25,82 @@ use crate::v2026_06_08_00::instance::InstanceCpuPlatform; /// Instance-stopping method choices #[derive( - Clone, Copy, Debug, Eq, PartialEq, Deserialize, Serialize, JsonSchema, + Clone, + Copy, + Default, + Debug, + Eq, + PartialEq, + Deserialize, + Serialize, + JsonSchema, )] #[serde(tag = "action", rename_all = "snake_case")] pub enum InstanceShutdownPolicy { /// Instance's VM is forcefully terminated immediately. + // default to hard-off, preserving the existing behavior of our system prior + // to ACPI shutdown support + #[default] HardOff, /// Instance is sent PWRBTN_STS as described in ACPI chapter 4. If the VM - /// guest has not stopped itself with a power event within the given timeout, - /// it is *then* forcefully terminated. - PowerButton { - #[schemars(range(min = 1, max = 3600))] - timeout_secs: u64, - }, + /// guest has not stopped itself with a power event within the given + /// timeout, it is *then* forcefully terminated. + PowerButton { timeout_secs: AcpiTimeoutSecs }, } +/// ACPI power button timeout in seconds. +// RFD 622: "A user-configurable timeout for the instance to finish stopping +// itself should be present, bounded between 1 and 3600 seconds, inclusive." +// This is enforced at the API level (by this type and its `Deserialize` impl) +// and as a crdb constraint. +#[derive( + Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, JsonSchema, +)] +#[serde(try_from = "u64", into = "u64")] +pub struct AcpiTimeoutSecs(#[schemars(range(min = 1, max = 3600))] u64); + // schemars(range(max)) above is required to be a *literal*. if the constant // changes, and you're here because of the assertion, change it accordingly. -const _: () = - const { assert!(nexus_defaults::MAX_ACPI_SHUTDOWN_TIMEOUT_SECS == 3600) }; +const _: () = const { assert!(AcpiTimeoutSecs::MIN == 1) }; +const _: () = const { assert!(AcpiTimeoutSecs::MAX == 3600) }; + +// All the impls on `AcpiTimeoutSecs` live here instead of `crate::impls` +// because they're required as a part of implementing `Deserialize`. + +impl AcpiTimeoutSecs { + const MIN: u64 = 1; + const MAX: u64 = 3600; +} + +impl From for u64 { + fn from(value: AcpiTimeoutSecs) -> Self { + value.0 + } +} + +// Some consumers of this need it to be an `i64` (e.g., to convert it to a +// `TimeDelta` for storage in the db). We can provide an infallible conversion +// for it since we know the range we enforce is valid for both u64 and i64. +impl From for i64 { + fn from(value: AcpiTimeoutSecs) -> Self { + value.0.try_into().expect("AcpiTimeoutSecs range is always a valid i64") + } +} + +impl TryFrom for AcpiTimeoutSecs { + type Error = anyhow::Error; + + fn try_from(value: u64) -> Result { + if value < Self::MIN || value > Self::MAX { + bail!( + "timeout_secs must be between {} and {} (got {value})", + Self::MIN, + Self::MAX + ); + } + Ok(Self(value)) + } +} /// View of an Instance #[derive(ObjectIdentity, Clone, Debug, Deserialize, Serialize, JsonSchema)] @@ -84,7 +142,7 @@ pub struct Instance { /// The method by which the instance will be powered down when transitioning /// to the stopped state, either forcefully or by giving the guest an ACPI /// power button signal first. - pub shutdown_policy: Option, + pub shutdown_policy: InstanceShutdownPolicy, } impl From for Instance { @@ -100,36 +158,46 @@ impl From for Instance { auto_restart_status: value.auto_restart_status, cpu_platform: value.cpu_platform, enable_jumbo_frames: value.enable_jumbo_frames, - shutdown_policy: None, + shutdown_policy: InstanceShutdownPolicy::default(), } } } -impl TryFrom for v2026_06_08_00::instance::Instance { - type Error = dropshot::HttpError; - - fn try_from(value: Instance) -> Result { - if let Some(policy) = value.shutdown_policy - && policy != InstanceShutdownPolicy::HardOff - { - return Err(dropshot::HttpError::for_internal_error( - "Instance with shutdown_policy cannot be represented in \ - API version prior to shutdown_policy support" - .to_string(), - )); +impl From for v2026_06_08_00::instance::Instance { + fn from(value: Instance) -> Self { + let Instance { + identity, + project_id, + ncpus, + memory, + hostname, + boot_disk_id, + runtime, + auto_restart_status, + cpu_platform, + enable_jumbo_frames, + // `shutdown_policy` was added in this API version, so can't be + // returned to older clients. We omit it - this means they can't + // tell the difference between instances that have the default + // policy (e.g., that they created) and instances with a different + // policy (e.g., created by a newer client). Our only other option + // here is to make this fallible and only show them instances whose + // policy matches the default, which doesn't seem very sporting. + shutdown_policy: _, + } = value; + + v2026_06_08_00::instance::Instance { + identity, + project_id, + ncpus, + memory, + hostname, + boot_disk_id, + runtime, + auto_restart_status, + cpu_platform, + enable_jumbo_frames, } - Ok(v2026_06_08_00::instance::Instance { - identity: value.identity, - project_id: value.project_id, - ncpus: value.ncpus, - memory: value.memory, - hostname: value.hostname, - boot_disk_id: value.boot_disk_id, - runtime: value.runtime, - auto_restart_status: value.auto_restart_status, - cpu_platform: value.cpu_platform, - enable_jumbo_frames: value.enable_jumbo_frames, - }) } } @@ -242,11 +310,10 @@ pub struct InstanceCreate { /// restarted in order to update system software on the sled that was /// hosting it. /// - /// If not provided, new instances will be created with a default of an - /// ACPI fixed power button press, with a 10-minute timeout for the guest OS - /// to shut itself down before the instance is forcibly terminated. + /// If not provided, the current default policy will be selected (hard power + /// off without an ACPI soft shutdown). #[serde(default)] - pub shutdown_policy: Option, + pub shutdown_policy: InstanceShutdownPolicy, } impl From for InstanceCreate { @@ -268,7 +335,7 @@ impl From for InstanceCreate { anti_affinity_groups: old.anti_affinity_groups, cpu_platform: old.cpu_platform, enable_jumbo_frames: old.enable_jumbo_frames, - shutdown_policy: None, + shutdown_policy: InstanceShutdownPolicy::default(), } } } @@ -371,3 +438,38 @@ pub struct InstanceStopOrRebootParams { // (easier to grep for 'skip_os_shutdown' than 'force' in codebase) pub skip_os_shutdown: Option, } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_acpi_timeout_secs_range() { + // Test some numbers outside `AcpiTimeoutSecs`'s range in both + // directions: deserialization and the `TryFrom` should only accept + // values in the valid range. + for i in -100..(AcpiTimeoutSecs::MAX as i64) + 10 { + let deserialize_result = + serde_json::from_str::(&i.to_string()); + + if matches!( + u64::try_from(i), + Ok(AcpiTimeoutSecs::MIN..=AcpiTimeoutSecs::MAX) + ) { + let expected = AcpiTimeoutSecs(i as u64); + assert_eq!(deserialize_result.unwrap(), expected); + assert_eq!( + AcpiTimeoutSecs::try_from(i as u64).unwrap(), + expected + ); + assert_eq!(u64::from(expected), i as u64); + assert_eq!(i64::from(expected), i); + } else { + assert!(deserialize_result.is_err()); + if let Ok(i) = u64::try_from(i) { + assert!(AcpiTimeoutSecs::try_from(i).is_err()); + } + } + } + } +} diff --git a/nexus/types/versions/src/latest.rs b/nexus/types/versions/src/latest.rs index ffbf7d8afcb..8f135aaaab0 100644 --- a/nexus/types/versions/src/latest.rs +++ b/nexus/types/versions/src/latest.rs @@ -194,6 +194,7 @@ pub mod instance { pub use crate::v2026_06_08_00::instance::InstanceCpuPlatform; + pub use crate::v2026_10_28_00::instance::AcpiTimeoutSecs; pub use crate::v2026_10_28_00::instance::Instance; pub use crate::v2026_10_28_00::instance::InstanceCreate; pub use crate::v2026_10_28_00::instance::InstanceShutdownPolicy; diff --git a/openapi/nexus-lockstep.json b/openapi/nexus-lockstep.json index 88c7f9173a0..67eda08c456 100644 --- a/openapi/nexus-lockstep.json +++ b/openapi/nexus-lockstep.json @@ -1615,6 +1615,13 @@ }, "components": { "schemas": { + "AcpiTimeoutSecs": { + "description": "ACPI power button timeout in seconds.", + "type": "integer", + "format": "uint64", + "minimum": 1, + "maximum": 3600 + }, "ActivationReason": { "description": "Describes why a background task was activated\n\nThis is only used for debugging. This is deliberately not made available to the background task itself. See \"Design notes\" in the module-level documentation for details.", "type": "string", @@ -5822,7 +5829,6 @@ "$ref": "#/components/schemas/InstanceState" }, "shutdown_policy": { - "nullable": true, "description": "The method by which the instance will be powered down when transitioning to the stopped state, either forcefully or by giving the guest an ACPI power button signal first.", "allOf": [ { @@ -5862,6 +5868,7 @@ "ncpus", "project_id", "run_state", + "shutdown_policy", "time_created", "time_modified", "time_run_state_updated" @@ -5998,10 +6005,7 @@ ] }, "timeout_secs": { - "type": "integer", - "format": "uint64", - "minimum": 1, - "maximum": 3600 + "$ref": "#/components/schemas/AcpiTimeoutSecs" } }, "required": [ diff --git a/openapi/nexus/nexus-2026102800.0.0-831569.json b/openapi/nexus/nexus-2026102800.0.0-b45545.json similarity index 99% rename from openapi/nexus/nexus-2026102800.0.0-831569.json rename to openapi/nexus/nexus-2026102800.0.0-b45545.json index 25266045d4c..38329c481d5 100644 --- a/openapi/nexus/nexus-2026102800.0.0-831569.json +++ b/openapi/nexus/nexus-2026102800.0.0-b45545.json @@ -15892,6 +15892,13 @@ }, "components": { "schemas": { + "AcpiTimeoutSecs": { + "description": "ACPI power button timeout in seconds.", + "type": "integer", + "format": "uint64", + "minimum": 1, + "maximum": 3600 + }, "Address": { "description": "An address tied to an address lot.", "type": "object", @@ -23186,7 +23193,6 @@ "$ref": "#/components/schemas/InstanceState" }, "shutdown_policy": { - "nullable": true, "description": "The method by which the instance will be powered down when transitioning to the stopped state, either forcefully or by giving the guest an ACPI power button signal first.", "allOf": [ { @@ -23226,6 +23232,7 @@ "ncpus", "project_id", "run_state", + "shutdown_policy", "time_created", "time_modified", "time_run_state_updated" @@ -23395,9 +23402,10 @@ ] }, "shutdown_policy": { - "nullable": true, - "description": "The method by which the instance should be stopped. If not provided, new instances will be created with a default of an ACPI fixed power button press, with a 10-minute timeout for the guest OS to shut itself down before the instance is forcibly terminated.", - "default": null, + "description": "The method by which the instance should be stopped.\n\nThis determines what action the control plane performs when stopping this instance, either due to a request to the `instance_stop` or `instance_reboot` API endpoints, or because the instance must be restarted in order to update system software on the sled that was hosting it.\n\nIf not provided, the current default policy will be selected (hard power off without an ACPI soft shutdown).", + "default": { + "action": "hard_off" + }, "allOf": [ { "$ref": "#/components/schemas/InstanceShutdownPolicy" @@ -23882,10 +23890,7 @@ ] }, "timeout_secs": { - "type": "integer", - "format": "uint64", - "minimum": 1, - "maximum": 3600 + "$ref": "#/components/schemas/AcpiTimeoutSecs" } }, "required": [ diff --git a/openapi/nexus/nexus-latest.json b/openapi/nexus/nexus-latest.json index c1f44d76aa8..2d7352afa80 120000 --- a/openapi/nexus/nexus-latest.json +++ b/openapi/nexus/nexus-latest.json @@ -1 +1 @@ -nexus-2026102800.0.0-831569.json \ No newline at end of file +nexus-2026102800.0.0-b45545.json \ No newline at end of file diff --git a/package-manifest.toml b/package-manifest.toml index 7ad761151af..5f5dc85870c 100644 --- a/package-manifest.toml +++ b/package-manifest.toml @@ -674,10 +674,10 @@ service_name = "propolis-server" only_for_targets.image = "standard" source.type = "prebuilt" source.repo = "propolis" -source.commit = "3b60af56ed99c4497f02ac9fe014c76adfcf978b" +source.commit = "048f470dc54b27a4958c8f8906fca3c27d8f8f76" # The SHA256 digest is automatically posted to: # https://buildomat.eng.oxide.computer/public/file/oxidecomputer/propolis/image//propolis-server.sha256.txt -source.sha256 = "d28e918af20d6bb93462ac71506ddf821bdfef8510db055b8e3be5d6371f6dbb" +source.sha256 = "1a6e98a31cf83171d31710ea6f8208d549bf4fafc28ede75b4c7411e33def8b8" output.type = "zone" [package.mg-ddm-gz]