diff --git a/nexus/external-api/src/lib.rs b/nexus/external-api/src/lib.rs index fb1ff2ccf74..027a6f4a05e 100644 --- a/nexus/external-api/src/lib.rs +++ b/nexus/external-api/src/lib.rs @@ -41,7 +41,6 @@ mod v2025122300; mod v2026010100; mod v2026010300; mod v2026010500; -mod v2026011501; mod v2026011600; #[cfg(test)] @@ -1618,17 +1617,19 @@ pub trait NexusExternalApi { method = POST, path = "/v1/floating-ips", tags = ["floating-ips"], - versions = VERSION_POOL_SELECTION_ENUMS..VERSION_RENAME_ADDRESS_SELECTOR_TO_ADDRESS_ALLOCATOR, + versions = ..VERSION_IP_VERSION_AND_MULTIPLE_DEFAULT_POOLS, }] - async fn v2026011501_floating_ip_create( + async fn v2025121200_floating_ip_create( rqctx: RequestContext, query_params: Query, - floating_params: TypedBody, + floating_params: TypedBody, ) -> Result, HttpError> { - Self::v2026011600_floating_ip_create( + let floating_params = + floating_params.map(v2026010300::FloatingIpCreate::from); + Self::v2026010300_floating_ip_create( rqctx, query_params, - floating_params.map(Into::into), + floating_params, ) .await } @@ -1648,8 +1649,8 @@ pub trait NexusExternalApi { floating_params: TypedBody, ) -> Result, HttpError> { let floating_params = - floating_params.try_map(v2026011501::FloatingIpCreate::try_from)?; - Self::v2026011501_floating_ip_create( + floating_params.try_map(v2026010500::FloatingIpCreate::try_from)?; + Self::v2026010500_floating_ip_create( rqctx, query_params, floating_params, @@ -1663,19 +1664,17 @@ pub trait NexusExternalApi { method = POST, path = "/v1/floating-ips", tags = ["floating-ips"], - versions = ..VERSION_IP_VERSION_AND_MULTIPLE_DEFAULT_POOLS, + versions = VERSION_POOL_SELECTION_ENUMS..VERSION_RENAME_ADDRESS_SELECTOR_TO_ADDRESS_ALLOCATOR, }] - async fn v2025121200_floating_ip_create( + async fn v2026010500_floating_ip_create( rqctx: RequestContext, query_params: Query, - floating_params: TypedBody, + floating_params: TypedBody, ) -> Result, HttpError> { - let floating_params = - floating_params.map(v2026010300::FloatingIpCreate::from); - Self::v2026010300_floating_ip_create( + Self::v2026011600_floating_ip_create( rqctx, query_params, - floating_params, + floating_params.map(Into::into), ) .await } @@ -2188,24 +2187,16 @@ pub trait NexusExternalApi { /// Create instance #[endpoint { - operation_id = "disk_create", method = POST, path = "/v1/instances", tags = ["instances"], - versions = ..VERSION_LOCAL_STORAGE, + versions = VERSION_MULTICAST_IMPLICIT_LIFECYCLE_UPDATES.., }] - async fn v2025112000_instance_create( + async fn instance_create( rqctx: RequestContext, query_params: Query, - new_instance: TypedBody, - ) -> Result, HttpError> { - Self::v2025121200_instance_create( - rqctx, - query_params, - new_instance.map(Into::into), - ) - .await - } + new_instance: TypedBody, + ) -> Result, HttpError>; /// Create instance #[endpoint { @@ -2213,19 +2204,15 @@ pub trait NexusExternalApi { method = POST, path = "/v1/instances", tags = ["instances"], - versions = VERSION_LOCAL_STORAGE..VERSION_IP_VERSION_AND_MULTIPLE_DEFAULT_POOLS, + versions = VERSION_POOL_SELECTION_ENUMS..VERSION_MULTICAST_IMPLICIT_LIFECYCLE_UPDATES, }] - async fn v2025121200_instance_create( + async fn v2026010500_instance_create( rqctx: RequestContext, query_params: Query, - new_instance: TypedBody, + new_instance: TypedBody, ) -> Result, HttpError> { - Self::v2026010100_instance_create( - rqctx, - query_params, - new_instance.map(Into::into), - ) - .await + Self::instance_create(rqctx, query_params, new_instance.map(Into::into)) + .await } /// Create instance @@ -2234,16 +2221,19 @@ pub trait NexusExternalApi { method = POST, path = "/v1/instances", tags = ["instances"], - versions = - VERSION_IP_VERSION_AND_MULTIPLE_DEFAULT_POOLS..VERSION_DUAL_STACK_NICS, + versions = VERSION_DUAL_STACK_NICS..VERSION_POOL_SELECTION_ENUMS, }] - async fn v2026010100_instance_create( + async fn v2026010300_instance_create( rqctx: RequestContext, query_params: Query, - new_instance: TypedBody, + new_instance: TypedBody, ) -> Result, HttpError> { - let new_instance = new_instance.try_map(TryInto::try_into)?; - Self::instance_create(rqctx, query_params, new_instance).await + Self::v2026010500_instance_create( + rqctx, + query_params, + new_instance.try_map(TryInto::try_into)?, + ) + .await } /// Create instance @@ -2252,15 +2242,20 @@ pub trait NexusExternalApi { method = POST, path = "/v1/instances", tags = ["instances"], - versions = VERSION_DUAL_STACK_NICS..VERSION_POOL_SELECTION_ENUMS, + versions = + VERSION_IP_VERSION_AND_MULTIPLE_DEFAULT_POOLS..VERSION_DUAL_STACK_NICS, }] - async fn v2026010300_instance_create( + async fn v2025122300_instance_create( rqctx: RequestContext, query_params: Query, - new_instance: TypedBody, + new_instance: TypedBody, ) -> Result, HttpError> { - let new_instance = new_instance.try_map(TryInto::try_into)?; - Self::instance_create(rqctx, query_params, new_instance).await + Self::v2026010300_instance_create( + rqctx, + query_params, + new_instance.try_map(TryInto::try_into)?, + ) + .await } /// Create instance @@ -2269,29 +2264,41 @@ pub trait NexusExternalApi { method = POST, path = "/v1/instances", tags = ["instances"], - versions = VERSION_POOL_SELECTION_ENUMS..VERSION_MULTICAST_IMPLICIT_LIFECYCLE_UPDATES, + versions = VERSION_LOCAL_STORAGE..VERSION_IP_VERSION_AND_MULTIPLE_DEFAULT_POOLS, }] - async fn v2026010500_instance_create( + async fn v2025120300_instance_create( rqctx: RequestContext, query_params: Query, - new_instance: TypedBody, + new_instance: TypedBody, ) -> Result, HttpError> { - Self::instance_create(rqctx, query_params, new_instance.map(Into::into)) - .await + Self::v2025122300_instance_create( + rqctx, + query_params, + new_instance.map(Into::into), + ) + .await } /// Create instance #[endpoint { + operation_id = "instance_create", method = POST, path = "/v1/instances", tags = ["instances"], - versions = VERSION_MULTICAST_IMPLICIT_LIFECYCLE_UPDATES.., + versions = ..VERSION_LOCAL_STORAGE, }] - async fn instance_create( + async fn v2025112000_instance_create( rqctx: RequestContext, query_params: Query, - new_instance: TypedBody, - ) -> Result, HttpError>; + new_instance: TypedBody, + ) -> Result, HttpError> { + Self::v2025120300_instance_create( + rqctx, + query_params, + new_instance.map(Into::into), + ) + .await + } /// Fetch instance #[endpoint { diff --git a/nexus/external-api/src/v2025112000.rs b/nexus/external-api/src/v2025112000.rs index 09684f532cf..b10683f468b 100644 --- a/nexus/external-api/src/v2025112000.rs +++ b/nexus/external-api/src/v2025112000.rs @@ -2,9 +2,10 @@ // 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/. -//! Nexus external types that changed from 2025112000 to 2025120300 +//! Types from API version 2025112000 (INITIAL) that changed in version +//! 2025120300 (LOCAL_STORAGE). -use crate::v2025121200; +use crate::v2025120300; use crate::v2026010100; use nexus_types::external_api::params; use omicron_common::api::external; @@ -220,9 +221,9 @@ pub struct InstanceCreate { /// By default, all instances have outbound connectivity, but no inbound /// connectivity. These external addresses can be used to provide a fixed, /// known IP address for making inbound connections to the instance. - // Delegates through v2025121200 → params::ExternalIpCreate + // Delegates through v2025120300 → params::ExternalIpCreate #[serde(default)] - pub external_ips: Vec, + pub external_ips: Vec, /// The multicast groups this instance should join. /// @@ -302,9 +303,9 @@ pub struct InstanceCreate { pub cpu_platform: Option, } -impl From for v2025121200::InstanceCreate { - fn from(old: InstanceCreate) -> v2025121200::InstanceCreate { - v2025121200::InstanceCreate { +impl From for v2025120300::InstanceCreate { + fn from(old: InstanceCreate) -> v2025120300::InstanceCreate { + v2025120300::InstanceCreate { identity: old.identity, ncpus: old.ncpus, memory: old.memory, diff --git a/nexus/external-api/src/v2025120300.rs b/nexus/external-api/src/v2025120300.rs index b67ad4c984a..127d8a17463 100644 --- a/nexus/external-api/src/v2025120300.rs +++ b/nexus/external-api/src/v2025120300.rs @@ -2,14 +2,35 @@ // 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/. -//! Nexus external types that changed from 2025120300 to 2025121200 +//! Types from API version 2025120300 (LOCAL_STORAGE) that changed in +//! subsequent versions. +//! +//! This version introduced local storage support for instances. +//! +//! ## Instance Creation Types +//! +//! Valid until 2025122300 (IP_VERSION_AND_MULTIPLE_DEFAULT_POOLS). +//! +//! [`InstanceCreate`] and [`ExternalIpCreate`] are defined here for use +//! by the `instance_create` endpoint in this version. These types don't +//! have the `ip_version` field that was added in later versions. +//! +//! ## BGP Types +//! +//! Valid until 2025121200 (BGP_PEER_COLLISION_STATE). +//! +//! [`BgpPeerStatus`] and [`BgpPeerState`] are older versions without the +//! `collision_state` field added in `BGP_PEER_COLLISION_STATE`. use std::net::IpAddr; +use nexus_types::external_api::params; use omicron_common::api::external; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use crate::{v2025122300, v2026010100, v2026010300}; + /// The current status of a BGP peer. #[derive(Clone, Debug, Deserialize, JsonSchema, Serialize, PartialEq)] pub struct BgpPeerStatus { @@ -59,3 +80,172 @@ pub enum BgpPeerState { /// messages with peers. Established, } + +/// The type of IP address to attach to an instance during creation. +#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum ExternalIpCreate { + /// An IP address providing both inbound and outbound access. + /// The address is automatically assigned from the provided IP pool + /// or the default IP pool if not specified. + Ephemeral { + /// Name or ID of the IP pool to use. If unspecified, the + /// default IP pool will be used. + pool: Option, + }, + /// A floating IP address. + Floating { + /// The name or ID of the floating IP address to attach. + floating_ip: external::NameOrId, + }, +} + +impl From for v2026010300::ExternalIpCreate { + fn from(old: ExternalIpCreate) -> v2026010300::ExternalIpCreate { + match old { + ExternalIpCreate::Ephemeral { pool } => { + v2026010300::ExternalIpCreate::Ephemeral { + pool, + ip_version: None, + } + } + ExternalIpCreate::Floating { floating_ip } => { + v2026010300::ExternalIpCreate::Floating { floating_ip } + } + } + } +} + +/// Create-time parameters for an `Instance` +#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] +pub struct InstanceCreate { + #[serde(flatten)] + pub identity: external::IdentityMetadataCreateParams, + /// The number of vCPUs to be allocated to the instance + pub ncpus: external::InstanceCpuCount, + /// The amount of RAM (in bytes) to be allocated to the instance + pub memory: external::ByteCount, + /// The hostname to be assigned to the instance + pub hostname: external::Hostname, + + /// User data for instance initialization systems (such as cloud-init). + /// Must be a Base64-encoded string, as specified in RFC 4648 § 4 (+ and / + /// characters with padding). Maximum 32 KiB unencoded data. + #[serde(default, with = "params::UserData")] + pub user_data: Vec, + + /// The network interfaces to be created for this instance. + #[serde(default)] + pub network_interfaces: v2026010100::InstanceNetworkInterfaceAttachment, + + /// The external IP addresses provided to this instance. + /// + /// By default, all instances have outbound connectivity, but no inbound + /// connectivity. These external addresses can be used to provide a fixed, + /// known IP address for making inbound connections to the instance. + #[serde(default)] + pub external_ips: Vec, + + /// The multicast groups this instance should join. + /// + /// The instance will be automatically added as a member of the specified + /// multicast groups during creation, enabling it to send and receive + /// multicast traffic for those groups. + #[serde(default)] + pub multicast_groups: Vec, + + /// A list of disks to be attached to the instance. + /// + /// Disk attachments of type "create" will be created, while those of type + /// "attach" must already exist. + /// + /// The order of this list does not guarantee a boot order for the instance. + /// Use the boot_disk attribute to specify a boot disk. When boot_disk is + /// specified it will count against the disk attachment limit. + #[serde(default)] + pub disks: Vec, + + /// The disk the instance is configured to boot from. + /// + /// This disk can either be attached if it already exists or created along + /// with the instance. + /// + /// Specifying a boot disk is optional but recommended to ensure predictable + /// boot behavior. The boot disk can be set during instance creation or + /// later if the instance is stopped. The boot disk counts against the disk + /// attachment limit. + /// + /// An instance that does not have a boot disk set will use the boot + /// options specified in its UEFI settings, which are controlled by both the + /// instance's UEFI firmware and the guest operating system. Boot options + /// can change as disks are attached and detached, which may result in an + /// instance that only boots to the EFI shell until a boot disk is set. + #[serde(default)] + pub boot_disk: Option, + + /// An allowlist of SSH public keys to be transferred to the instance via + /// cloud-init during instance creation. + /// + /// If not provided, all SSH public keys from the user's profile will be sent. + /// If an empty list is provided, no public keys will be transmitted to the + /// instance. + pub ssh_public_keys: Option>, + + /// Should this instance be started upon creation; true by default. + #[serde(default = "params::bool_true")] + pub start: bool, + + /// The auto-restart policy for this instance. + /// + /// This policy determines whether the instance should be automatically + /// restarted by the control plane on failure. If this is `null`, no + /// auto-restart policy will be explicitly configured for this instance, and + /// the control plane will select the default policy when determining + /// whether the instance can be automatically restarted. + /// + /// Currently, the global default auto-restart policy is "best-effort", so + /// instances with `null` auto-restart policies will be automatically + /// restarted. However, in the future, the default policy may be + /// configurable through other mechanisms, such as on a per-project basis. + /// In that case, any configured default policy will be used if this is + /// `null`. + #[serde(default)] + pub auto_restart_policy: Option, + + /// Anti-Affinity groups which this instance should be added. + #[serde(default)] + pub anti_affinity_groups: Vec, + + /// The CPU platform to be used for this instance. If this is `null`, the + /// instance requires no particular CPU platform; when it is started the + /// instance will have the most general CPU platform supported by the sled + /// it is initially placed on. + #[serde(default)] + pub cpu_platform: Option, +} + +impl From for v2025122300::InstanceCreate { + fn from(old: InstanceCreate) -> v2025122300::InstanceCreate { + v2025122300::InstanceCreate { + identity: old.identity, + ncpus: old.ncpus, + memory: old.memory, + hostname: old.hostname, + user_data: old.user_data, + network_interfaces: old.network_interfaces, + external_ips: old + .external_ips + .into_iter() + .map(Into::into) + .collect(), + multicast_groups: old.multicast_groups, + disks: old.disks, + boot_disk: old.boot_disk, + ssh_public_keys: old.ssh_public_keys, + start: old.start, + auto_restart_policy: old.auto_restart_policy, + anti_affinity_groups: old.anti_affinity_groups, + cpu_platform: old.cpu_platform, + } + } +} diff --git a/nexus/external-api/src/v2025121200.rs b/nexus/external-api/src/v2025121200.rs index 3e14913e95b..277415f4c9f 100644 --- a/nexus/external-api/src/v2025121200.rs +++ b/nexus/external-api/src/v2025121200.rs @@ -2,35 +2,27 @@ // 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/. -//! Nexus external types that changed from 2025121200 to 2026010100. +//! Types from API version 2025121200 (BGP_PEER_COLLISION_STATE) that changed in +//! subsequent versions. //! -//! Version 2025121200 types (before `ip_version` preference was added for -//! default IP pool selection). +//! Types before `ip_version` preference was added for default IP pool selection. //! -//! ## IP Pool Selection Changes +//! ## IP Pool Selection Types //! -//! Key differences from newer API versions: -//! - [`FloatingIpCreate`], [`EphemeralIpCreate`], and [`ExternalIpCreate`] -//! don't have the `ip_version` field. Newer versions allow specifying -//! IPv4/IPv6 preference when allocating from default pools. -//! - When multiple default pools of different IP versions exist for a silo, -//! older clients cannot resolve the conflict. Newer API versions -//! require the `ip_version` field in this scenario. +//! Valid until 2025122300 (IP_VERSION_AND_MULTIPLE_DEFAULT_POOLS). //! -//! Affected endpoints: -//! - `POST /v1/floating-ips` (floating_ip_create) -//! - `POST /v1/instances/{instance}/external-ips/ephemeral` (instance_ephemeral_ip_attach) -//! - `POST /v1/instances` (instance_create) +//! [`FloatingIpCreate`] and [`EphemeralIpCreate`] don't have the `ip_version` +//! field. Newer versions allow specifying IPv4/IPv6 preference when allocating +//! from default pools. //! //! ## Multicast Types //! +//! Valid until 2026010800 (MULTICAST_IMPLICIT_LIFECYCLE_UPDATES). +//! //! Multicast types are re-exported from `v2025122300`. -//! Both versions use `NameOrId` for group references and have the same -//! explicit create/update semantics. //! //! [`FloatingIpCreate`]: self::FloatingIpCreate //! [`EphemeralIpCreate`]: self::EphemeralIpCreate -//! [`ExternalIpCreate`]: self::ExternalIpCreate use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -38,7 +30,7 @@ use serde::{Deserialize, Serialize}; use nexus_types::external_api::params; use omicron_common::api::external; -use crate::{v2026010100, v2026010300}; +use crate::v2026010300; // Re-export multicast types from v2025122300. // They're identical for both versions (both use NameOrId, explicit @@ -69,42 +61,6 @@ impl From for params::EphemeralIpCreate { } } -/// The type of IP address to attach to an instance during creation. -#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] -#[serde(tag = "type", rename_all = "snake_case")] -pub enum ExternalIpCreate { - /// An IP address providing both inbound and outbound access. - /// The address is automatically assigned from the provided IP pool - /// or the default IP pool if not specified. - Ephemeral { - /// Name or ID of the IP pool to use. If unspecified, the - /// default IP pool will be used. - pool: Option, - }, - /// A floating IP address. - Floating { - /// The name or ID of the floating IP address to attach. - floating_ip: external::NameOrId, - }, -} - -// Converts to v2026010300::ExternalIpCreate (adds ip_version: None) -impl From for v2026010300::ExternalIpCreate { - fn from(old: ExternalIpCreate) -> v2026010300::ExternalIpCreate { - match old { - ExternalIpCreate::Ephemeral { pool } => { - v2026010300::ExternalIpCreate::Ephemeral { - pool, - ip_version: None, - } - } - ExternalIpCreate::Floating { floating_ip } => { - v2026010300::ExternalIpCreate::Floating { floating_ip } - } - } - } -} - /// Parameters for creating a new floating IP address for instances. #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] pub struct FloatingIpCreate { @@ -132,77 +88,6 @@ impl From for v2026010300::FloatingIpCreate { } } -/// Create-time parameters for an `Instance` -#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] -pub struct InstanceCreate { - #[serde(flatten)] - pub identity: external::IdentityMetadataCreateParams, - /// The number of vCPUs to be allocated to the instance - pub ncpus: external::InstanceCpuCount, - /// The amount of RAM (in bytes) to be allocated to the instance - pub memory: external::ByteCount, - /// The hostname to be assigned to the instance - pub hostname: external::Hostname, - /// User data for instance initialization systems (such as cloud-init). - #[serde(default, with = "params::UserData")] - pub user_data: Vec, - /// The network interfaces to be created for this instance. - #[serde(default)] - pub network_interfaces: v2026010100::InstanceNetworkInterfaceAttachment, - /// The external IP addresses provided to this instance. - #[serde(default)] - pub external_ips: Vec, - /// The multicast groups this instance should join. - #[serde(default)] - pub multicast_groups: Vec, - /// A list of disks to be attached to the instance. - #[serde(default)] - pub disks: Vec, - /// The disk the instance is configured to boot from. - #[serde(default)] - pub boot_disk: Option, - /// An allowlist of SSH public keys to be transferred to the instance. - pub ssh_public_keys: Option>, - /// Should this instance be started upon creation; true by default. - #[serde(default = "params::bool_true")] - pub start: bool, - /// The auto-restart policy for this instance. - #[serde(default)] - pub auto_restart_policy: Option, - /// Anti-Affinity groups which this instance should be added. - #[serde(default)] - pub anti_affinity_groups: Vec, - /// The CPU platform to be used for this instance. - #[serde(default)] - pub cpu_platform: Option, -} - -impl From for v2026010100::InstanceCreate { - fn from(old: InstanceCreate) -> v2026010100::InstanceCreate { - v2026010100::InstanceCreate { - identity: old.identity, - ncpus: old.ncpus, - memory: old.memory, - hostname: old.hostname, - user_data: old.user_data, - network_interfaces: old.network_interfaces, - external_ips: old - .external_ips - .into_iter() - .map(Into::into) - .collect(), - multicast_groups: old.multicast_groups, - disks: old.disks, - boot_disk: old.boot_disk, - ssh_public_keys: old.ssh_public_keys, - start: old.start, - auto_restart_policy: old.auto_restart_policy, - anti_affinity_groups: old.anti_affinity_groups, - cpu_platform: old.cpu_platform, - } - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/nexus/external-api/src/v2025122300.rs b/nexus/external-api/src/v2025122300.rs index c48e642b2f6..8b40b8ce892 100644 --- a/nexus/external-api/src/v2025122300.rs +++ b/nexus/external-api/src/v2025122300.rs @@ -2,13 +2,16 @@ // 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/. -//! Nexus external types that changed from 2025122300 to 2026010100. +//! Types from API version 2025122300 (IP_VERSION_AND_MULTIPLE_DEFAULT_POOLS) +//! that changed in subsequent versions. //! //! This module contains both **views** (response bodies) and **params** //! (request bodies) that differ from newer API versions. //! //! ## SiloIpPool Changes //! +//! Valid until 2026010100 (SILO_PROJECT_IP_VERSION_AND_POOL_TYPE). +//! //! [`SiloIpPool`] doesn't have `ip_version` or `pool_type` fields. //! Newer versions include these fields to indicate the IP version //! and pool type (unicast or multicast) of the pool. @@ -18,8 +21,16 @@ //! - `GET /v1/ip-pools/{pool}` (project_ip_pool_view) //! - `GET /v1/system/silos/{silo}/ip-pools` (silo_ip_pool_list) //! +//! ## Instance Changes +//! +//! Valid until 2026010300 (DUAL_STACK_NICS). +//! +//! [`InstanceCreate`] uses `v2026010100::InstanceNetworkInterfaceAttachment`. +//! //! ## Multicast Changes //! +//! Valid until 2026010800 (MULTICAST_IMPLICIT_LIFECYCLE_UPDATES). +//! //! Version 2025122300 types (before [`MulticastGroupIdentifier`] was introduced //! and before implicit group lifecycle). //! @@ -39,6 +50,7 @@ //! - `PUT /v1/instances/{instance}` (instance_update) //! //! [`SiloIpPool`]: self::SiloIpPool +//! [`InstanceCreate`]: self::InstanceCreate //! [`MulticastGroupIdentifier`]: nexus_types::external_api::params::MulticastGroupIdentifier //! [`NameOrId`]: omicron_common::api::external::NameOrId //! [`MulticastGroupMemberAdd`]: self::MulticastGroupMemberAdd @@ -53,12 +65,14 @@ use uuid::Uuid; use nexus_types::external_api::{params, views}; use nexus_types::multicast::MulticastGroupCreate as InternalMulticastGroupCreate; use omicron_common::api::external::{ - ByteCount, IdentityMetadata, IdentityMetadataCreateParams, + self, ByteCount, Hostname, IdentityMetadata, IdentityMetadataCreateParams, InstanceAutoRestartPolicy, InstanceCpuCount, InstanceCpuPlatform, Name, NameOrId, Nullable, }; use omicron_common::vlan::VlanID; +use crate::{v2026010100, v2026010300}; + /// Path parameter for multicast group operations. /// /// Uses `NameOrId` instead of `MulticastGroupIdentifier`. @@ -352,3 +366,136 @@ impl From for SiloIpPool { SiloIpPool { identity: new.identity, is_default: new.is_default } } } + +/// Create-time parameters for an `Instance` +#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] +pub struct InstanceCreate { + #[serde(flatten)] + pub identity: external::IdentityMetadataCreateParams, + /// The number of vCPUs to be allocated to the instance + pub ncpus: InstanceCpuCount, + /// The amount of RAM (in bytes) to be allocated to the instance + pub memory: ByteCount, + /// The hostname to be assigned to the instance + pub hostname: Hostname, + + /// User data for instance initialization systems (such as cloud-init). + /// Must be a Base64-encoded string, as specified in RFC 4648 § 4 (+ and / + /// characters with padding). Maximum 32 KiB unencoded data. + #[serde(default, with = "params::UserData")] + pub user_data: Vec, + + /// The network interfaces to be created for this instance. + #[serde(default)] + pub network_interfaces: v2026010100::InstanceNetworkInterfaceAttachment, + + /// The external IP addresses provided to this instance. + /// + /// By default, all instances have outbound connectivity, but no inbound + /// connectivity. These external addresses can be used to provide a fixed, + /// known IP address for making inbound connections to the instance. + #[serde(default)] + pub external_ips: Vec, + + /// The multicast groups this instance should join. + /// + /// The instance will be automatically added as a member of the specified + /// multicast groups during creation, enabling it to send and receive + /// multicast traffic for those groups. + #[serde(default)] + pub multicast_groups: Vec, + + /// A list of disks to be attached to the instance. + /// + /// Disk attachments of type "create" will be created, while those of type + /// "attach" must already exist. + /// + /// The order of this list does not guarantee a boot order for the instance. + /// Use the boot_disk attribute to specify a boot disk. When boot_disk is + /// specified it will count against the disk attachment limit. + #[serde(default)] + pub disks: Vec, + + /// The disk the instance is configured to boot from. + /// + /// This disk can either be attached if it already exists or created along + /// with the instance. + /// + /// Specifying a boot disk is optional but recommended to ensure predictable + /// boot behavior. The boot disk can be set during instance creation or + /// later if the instance is stopped. The boot disk counts against the disk + /// attachment limit. + /// + /// An instance that does not have a boot disk set will use the boot + /// options specified in its UEFI settings, which are controlled by both the + /// instance's UEFI firmware and the guest operating system. Boot options + /// can change as disks are attached and detached, which may result in an + /// instance that only boots to the EFI shell until a boot disk is set. + #[serde(default)] + pub boot_disk: Option, + + /// An allowlist of SSH public keys to be transferred to the instance via + /// cloud-init during instance creation. + /// + /// If not provided, all SSH public keys from the user's profile will be sent. + /// If an empty list is provided, no public keys will be transmitted to the + /// instance. + pub ssh_public_keys: Option>, + + /// Should this instance be started upon creation; true by default. + #[serde(default = "params::bool_true")] + pub start: bool, + + /// The auto-restart policy for this instance. + /// + /// This policy determines whether the instance should be automatically + /// restarted by the control plane on failure. If this is `null`, no + /// auto-restart policy will be explicitly configured for this instance, and + /// the control plane will select the default policy when determining + /// whether the instance can be automatically restarted. + /// + /// Currently, the global default auto-restart policy is "best-effort", so + /// instances with `null` auto-restart policies will be automatically + /// restarted. However, in the future, the default policy may be + /// configurable through other mechanisms, such as on a per-project basis. + /// In that case, any configured default policy will be used if this is + /// `null`. + #[serde(default)] + pub auto_restart_policy: Option, + + /// Anti-Affinity groups which this instance should be added. + #[serde(default)] + pub anti_affinity_groups: Vec, + + /// The CPU platform to be used for this instance. If this is `null`, the + /// instance requires no particular CPU platform; when it is started the + /// instance will have the most general CPU platform supported by the sled + /// it is initially placed on. + #[serde(default)] + pub cpu_platform: Option, +} + +impl TryFrom for v2026010300::InstanceCreate { + type Error = external::Error; + + fn try_from(value: InstanceCreate) -> Result { + let network_interfaces = value.network_interfaces.try_into()?; + Ok(Self { + identity: value.identity, + ncpus: value.ncpus, + memory: value.memory, + hostname: value.hostname, + user_data: value.user_data, + network_interfaces, + external_ips: value.external_ips, + multicast_groups: value.multicast_groups, + disks: value.disks, + boot_disk: value.boot_disk, + ssh_public_keys: value.ssh_public_keys, + start: value.start, + auto_restart_policy: value.auto_restart_policy, + anti_affinity_groups: value.anti_affinity_groups, + cpu_platform: value.cpu_platform, + }) + } +} diff --git a/nexus/external-api/src/v2026010100.rs b/nexus/external-api/src/v2026010100.rs index dabef26017b..6519ba49978 100644 --- a/nexus/external-api/src/v2026010100.rs +++ b/nexus/external-api/src/v2026010100.rs @@ -2,21 +2,13 @@ // 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/. -//! Nexus external types that changed from 2026010100 to 2026010300. +//! Types from API version 2026010100 (SILO_PROJECT_IP_VERSION_AND_POOL_TYPE) +//! that changed in version 2026010300 (DUAL_STACK_NICS). //! //! ## Network Interface Changes //! -//! This version adds dual-stack NIC support with [`InstanceNetworkInterfaceAttachment`] -//! and [`InstanceNetworkInterfaceCreate`] changes. -//! -//! ## Multicast Changes -//! -//! `InstanceCreate.multicast_groups` uses `Vec` instead of -//! `Vec`. The conversion adds default values for -//! `source_ips` and `ip_version` fields. -//! -//! Affected endpoints: -//! - `POST /v1/instances` (instance_create) +//! This version has pre-dual-stack NIC support with [`InstanceNetworkInterfaceAttachment`] +//! and [`InstanceNetworkInterfaceCreate`]. These are used by `v2025122300::InstanceCreate`. //! //! [`InstanceNetworkInterfaceAttachment`]: self::InstanceNetworkInterfaceAttachment //! [`InstanceNetworkInterfaceCreate`]: self::InstanceNetworkInterfaceCreate @@ -49,8 +41,6 @@ use serde::Serialize; use std::net::IpAddr; use uuid::Uuid; -use crate::v2026010300; - /// Describes an attachment of an `InstanceNetworkInterface` to an `Instance`, /// at the time the instance is created. // NOTE: VPC's are an organizing concept for networking resources, not for @@ -310,159 +300,6 @@ impl TryFrom } } -/// Create-time parameters for an `Instance` -#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] -pub struct InstanceCreate { - #[serde(flatten)] - pub identity: external::IdentityMetadataCreateParams, - /// The number of vCPUs to be allocated to the instance - pub ncpus: external::InstanceCpuCount, - /// The amount of RAM (in bytes) to be allocated to the instance - pub memory: external::ByteCount, - /// The hostname to be assigned to the instance - pub hostname: external::Hostname, - - /// User data for instance initialization systems (such as cloud-init). - /// Must be a Base64-encoded string, as specified in RFC 4648 § 4 (+ and / - /// characters with padding). Maximum 32 KiB unencoded data. - // While serde happily accepts #[serde(with = "")] as a shorthand for - // specifying `serialize_with` and `deserialize_with`, schemars requires the - // argument to `with` to be a type rather than merely a path prefix (i.e. a - // mod or type). It's admittedly a bit tricky for schemars to address; - // unlike `serialize` or `deserialize`, `JsonSchema` requires several - // functions working together. It's unfortunate that schemars has this - // built-in incompatibility, exacerbated by its glacial rate of progress - // and immunity to offers of help. - #[serde(default, with = "params::UserData")] - pub user_data: Vec, - - /// The network interfaces to be created for this instance. - #[serde(default)] - pub network_interfaces: InstanceNetworkInterfaceAttachment, - - /// The external IP addresses provided to this instance. - /// - /// By default, all instances have outbound connectivity, but no inbound - /// connectivity. These external addresses can be used to provide a fixed, - /// known IP address for making inbound connections to the instance. - #[serde(default)] - pub external_ips: Vec, - - /// The multicast groups this instance should join. - /// - /// The instance will be automatically added as a member of the specified - /// multicast groups during creation, enabling it to send and receive - /// multicast traffic for those groups. - #[serde(default)] - pub multicast_groups: Vec, - - /// A list of disks to be attached to the instance. - /// - /// Disk attachments of type "create" will be created, while those of type - /// "attach" must already exist. - /// - /// The order of this list does not guarantee a boot order for the instance. - /// Use the boot_disk attribute to specify a boot disk. When boot_disk is - /// specified it will count against the disk attachment limit. - #[serde(default)] - pub disks: Vec, - - /// The disk the instance is configured to boot from. - /// - /// This disk can either be attached if it already exists or created along - /// with the instance. - /// - /// Specifying a boot disk is optional but recommended to ensure predictable - /// boot behavior. The boot disk can be set during instance creation or - /// later if the instance is stopped. The boot disk counts against the disk - /// attachment limit. - /// - /// An instance that does not have a boot disk set will use the boot - /// options specified in its UEFI settings, which are controlled by both the - /// instance's UEFI firmware and the guest operating system. Boot options - /// can change as disks are attached and detached, which may result in an - /// instance that only boots to the EFI shell until a boot disk is set. - #[serde(default)] - pub boot_disk: Option, - - /// An allowlist of SSH public keys to be transferred to the instance via - /// cloud-init during instance creation. - /// - /// If not provided, all SSH public keys from the user's profile will be sent. - /// If an empty list is provided, no public keys will be transmitted to the - /// instance. - pub ssh_public_keys: Option>, - - /// Should this instance be started upon creation; true by default. - #[serde(default = "params::bool_true")] - pub start: bool, - - /// The auto-restart policy for this instance. - /// - /// This policy determines whether the instance should be automatically - /// restarted by the control plane on failure. If this is `null`, no - /// auto-restart policy will be explicitly configured for this instance, and - /// the control plane will select the default policy when determining - /// whether the instance can be automatically restarted. - /// - /// Currently, the global default auto-restart policy is "best-effort", so - /// instances with `null` auto-restart policies will be automatically - /// restarted. However, in the future, the default policy may be - /// configurable through other mechanisms, such as on a per-project basis. - /// In that case, any configured default policy will be used if this is - /// `null`. - #[serde(default)] - pub auto_restart_policy: Option, - - /// Anti-Affinity groups which this instance should be added. - #[serde(default)] - pub anti_affinity_groups: Vec, - - /// The CPU platform to be used for this instance. If this is `null`, the - /// instance requires no particular CPU platform; when it is started the - /// instance will have the most general CPU platform supported by the sled - /// it is initially placed on. - #[serde(default)] - pub cpu_platform: Option, -} - -impl TryFrom for params::InstanceCreate { - type Error = external::Error; - - fn try_from(value: InstanceCreate) -> Result { - let network_interfaces = value.network_interfaces.try_into()?; - Ok(Self { - identity: value.identity, - ncpus: value.ncpus, - memory: value.memory, - hostname: value.hostname, - user_data: value.user_data, - network_interfaces, - external_ips: value - .external_ips - .into_iter() - .map(TryInto::try_into) - .collect::, _>>()?, - multicast_groups: value - .multicast_groups - .into_iter() - .map(|g| params::MulticastGroupJoinSpec { - group: g.into(), - source_ips: None, - ip_version: None, - }) - .collect(), - disks: value.disks, - boot_disk: value.boot_disk, - ssh_public_keys: value.ssh_public_keys, - start: value.start, - auto_restart_policy: value.auto_restart_policy, - anti_affinity_groups: value.anti_affinity_groups, - cpu_platform: value.cpu_platform, - }) - } -} - #[derive(Debug, Clone, JsonSchema, Serialize, Deserialize)] pub struct ProbeInfo { pub id: Uuid, diff --git a/nexus/external-api/src/v2026010300.rs b/nexus/external-api/src/v2026010300.rs index e0e7640e500..2a01472a783 100644 --- a/nexus/external-api/src/v2026010300.rs +++ b/nexus/external-api/src/v2026010300.rs @@ -2,7 +2,8 @@ // 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/. -//! Nexus external types that changed from 2026010300 to 2026010500. +//! Types from API version 2026010300 (DUAL_STACK_NICS) that changed in version +//! 2026010500 (POOL_SELECTION_ENUMS). //! //! ## Pool Selection Changes //! @@ -26,7 +27,7 @@ //! [`EphemeralIpCreate`]: self::EphemeralIpCreate //! [`ExternalIpCreate`]: self::ExternalIpCreate //! [`InstanceCreate`]: self::InstanceCreate -//! [`AddressSelector`]: crate::v2026011501::AddressSelector +//! [`AddressSelector`]: crate::v2026010500::AddressSelector //! [`PoolSelector`]: nexus_types::external_api::params::PoolSelector //! [`MulticastGroupJoinSpec`]: nexus_types::external_api::params::MulticastGroupJoinSpec @@ -36,7 +37,7 @@ use serde::{Deserialize, Serialize}; use nexus_types::external_api::params; use omicron_common::api::external; -use crate::v2026011501; +use crate::v2026010500; use omicron_common::api::external::{ ByteCount, Hostname, IdentityMetadataCreateParams, InstanceAutoRestartPolicy, InstanceCpuCount, InstanceCpuPlatform, @@ -157,16 +158,16 @@ pub struct FloatingIpCreate { pub ip_version: Option, } -impl TryFrom for v2026011501::FloatingIpCreate { +impl TryFrom for v2026010500::FloatingIpCreate { type Error = external::Error; fn try_from( old: FloatingIpCreate, - ) -> Result { + ) -> Result { let address_selector = match (old.ip, old.pool, old.ip_version) { // Explicit IP address provided -> ip_version must not be set (Some(ip), pool, None) => { - v2026011501::AddressSelector::Explicit { ip, pool } + v2026010500::AddressSelector::Explicit { ip, pool } } // Explicit IP and ip_version is an invalid combination (Some(_), _, Some(_)) => { @@ -176,7 +177,7 @@ impl TryFrom for v2026011501::FloatingIpCreate { )); } // No explicit IP, but named pool specified -> ip_version must not be set - (None, Some(pool), None) => v2026011501::AddressSelector::Auto { + (None, Some(pool), None) => v2026010500::AddressSelector::Auto { pool_selector: params::PoolSelector::Explicit { pool }, }, // Named pool and ip_version is an invalid combination @@ -187,11 +188,11 @@ impl TryFrom for v2026011501::FloatingIpCreate { )); } // Allocate from default pool with optional IP version preference - (None, None, ip_version) => v2026011501::AddressSelector::Auto { + (None, None, ip_version) => v2026010500::AddressSelector::Auto { pool_selector: params::PoolSelector::Auto { ip_version }, }, }; - Ok(v2026011501::FloatingIpCreate { + Ok(v2026010500::FloatingIpCreate { identity: old.identity, address_selector, }) @@ -255,19 +256,19 @@ pub struct InstanceCreate { pub cpu_platform: Option, } -impl TryFrom for params::InstanceCreate { +impl TryFrom for v2026010500::InstanceCreate { type Error = external::Error; fn try_from( old: InstanceCreate, - ) -> Result { + ) -> Result { let external_ips: Vec = old .external_ips .into_iter() .map(TryInto::try_into) .collect::, _>>()?; - Ok(params::InstanceCreate { + Ok(v2026010500::InstanceCreate { identity: old.identity, ncpus: old.ncpus, memory: old.memory, @@ -275,15 +276,7 @@ impl TryFrom for params::InstanceCreate { user_data: old.user_data, network_interfaces: old.network_interfaces, external_ips, - multicast_groups: old - .multicast_groups - .into_iter() - .map(|g| params::MulticastGroupJoinSpec { - group: g.into(), - source_ips: None, - ip_version: None, - }) - .collect(), + multicast_groups: old.multicast_groups, disks: old.disks, boot_disk: old.boot_disk, ssh_public_keys: old.ssh_public_keys, @@ -376,7 +369,7 @@ mod tests { }) } - /// Verifies that valid inputs convert to v2026011501::FloatingIpCreate + /// Verifies that valid inputs convert to v2026010500::FloatingIpCreate /// with the correct AddressSelector variant based on ip/pool/ip_version. #[proptest] fn floating_ip_create_valid_converts_correctly( @@ -385,7 +378,7 @@ mod tests { ) { use proptest::test_runner::TestCaseError; - let output: v2026011501::FloatingIpCreate = + let output: v2026010500::FloatingIpCreate = input.clone().try_into().map_err(|e| { TestCaseError::fail(format!("unexpected error: {e}")) })?; @@ -399,7 +392,7 @@ mod tests { match (input.ip, input.pool, input.ip_version) { // Explicit IP address provided -> AddressSelector::Explicit. (Some(ip), pool, None) => { - let v2026011501::AddressSelector::Explicit { + let v2026010500::AddressSelector::Explicit { ip: out_ip, pool: out_pool, } = output.address_selector @@ -414,7 +407,7 @@ mod tests { // Pool specified without IP -> AddressSelector::Auto with // PoolSelector::Explicit. (None, Some(pool), None) => { - let v2026011501::AddressSelector::Auto { + let v2026010500::AddressSelector::Auto { pool_selector: params::PoolSelector::Explicit { pool: out_pool }, } = output.address_selector @@ -428,7 +421,7 @@ mod tests { // Neither IP nor pool -> AddressSelector::Auto with // PoolSelector::Auto. (None, None, ip_version) => { - let v2026011501::AddressSelector::Auto { + let v2026010500::AddressSelector::Auto { pool_selector: params::PoolSelector::Auto { ip_version: out_ip_version }, } = output.address_selector @@ -450,7 +443,7 @@ mod tests { #[strategy(invalid_floating_ip_create_strategy())] input: FloatingIpCreate, ) { - let result: Result = input.try_into(); + let result: Result = input.try_into(); prop_assert!(result.is_err()); } } diff --git a/nexus/external-api/src/v2026010500.rs b/nexus/external-api/src/v2026010500.rs index e75471586c3..ac1e22c6055 100644 --- a/nexus/external-api/src/v2026010500.rs +++ b/nexus/external-api/src/v2026010500.rs @@ -2,20 +2,36 @@ // 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/. -//! Nexus external types that changed from 2026010500 to 2026010800. +//! Types from API version 2026010500 (POOL_SELECTION_ENUMS) that changed in +//! version 2026011600 (RENAME_ADDRESS_SELECTOR_TO_ADDRESS_ALLOCATOR). +//! +//! ## Pool Selection Changes +//! +//! Valid until 2026011600 (RENAME_ADDRESS_SELECTOR_TO_ADDRESS_ALLOCATOR). +//! +//! [`FloatingIpCreate`] uses [`AddressSelector`] enum (renamed to `AddressAllocator` +//! in newer versions). This provides explicit/auto allocation selection. //! //! ## Multicast Changes //! +//! Valid until 2026010800 (MULTICAST_IMPLICIT_LIFECYCLE_UPDATES). +//! //! [`InstanceCreate`] uses `Vec` for `multicast_groups`. Newer //! versions use `Vec` which supports implicit group //! lifecycle (groups are created automatically when referenced). //! //! Affected endpoints: +//! - `POST /v1/floating-ips` (floating_ip_create) //! - `POST /v1/instances` (instance_create) //! +//! [`FloatingIpCreate`]: self::FloatingIpCreate +//! [`AddressSelector`]: self::AddressSelector //! [`InstanceCreate`]: self::InstanceCreate //! [`MulticastGroupJoinSpec`]: nexus_types::external_api::params::MulticastGroupJoinSpec +use std::net::IpAddr; + +use crate::v2026011600; use nexus_types::external_api::params; use omicron_common::api::external::{ ByteCount, Hostname, IdentityMetadataCreateParams, @@ -30,10 +46,65 @@ use serde::{Deserialize, Serialize}; // Only the multicast_groups field differs (Vec vs Vec). pub use params::InstanceNetworkInterfaceAttachment; -/// Create-time parameters for an `Instance` +/// Specify how to allocate a floating IP address. /// -/// This version uses `Vec` for `multicast_groups` instead of -/// `Vec` in newer versions. +/// This is the old name for what is now called `AddressAllocator`. +#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum AddressSelector { + /// Reserve a specific IP address. + Explicit { + /// The IP address to reserve. Must be available in the pool. + ip: IpAddr, + /// The pool containing this address. If not specified, the default + /// pool for the address's IP version is used. + pool: Option, + }, + /// Automatically allocate an IP address from a specified pool. + Auto { + /// Pool selection. + /// + /// If omitted, this field uses the silo's default pool. If the + /// silo has default pools for both IPv4 and IPv6, the request will + /// fail unless `ip_version` is specified in the pool selector. + #[serde(default)] + pool_selector: params::PoolSelector, + }, +} + +impl Default for AddressSelector { + fn default() -> Self { + AddressSelector::Auto { pool_selector: params::PoolSelector::default() } + } +} + +impl From for params::AddressAllocator { + fn from(value: AddressSelector) -> Self { + v2026011600::AddressAllocator::from(value).into() + } +} + +/// Parameters for creating a new floating IP address for instances. +/// +/// This version uses `address_selector` field instead of `address_allocator` +/// in newer versions. +#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] +pub struct FloatingIpCreate { + #[serde(flatten)] + pub identity: IdentityMetadataCreateParams, + + /// IP address allocation method. + #[serde(default)] + pub address_selector: AddressSelector, +} + +impl From for params::FloatingIpCreate { + fn from(value: FloatingIpCreate) -> Self { + v2026011600::FloatingIpCreate::from(value).into() + } +} + +/// Create-time parameters for an `Instance` #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] pub struct InstanceCreate { #[serde(flatten)] @@ -56,27 +127,55 @@ pub struct InstanceCreate { pub network_interfaces: InstanceNetworkInterfaceAttachment, /// The external IP addresses provided to this instance. + /// + /// By default, all instances have outbound connectivity, but no inbound + /// connectivity. These external addresses can be used to provide a fixed, + /// known IP address for making inbound connections to the instance. #[serde(default)] pub external_ips: Vec, - /// The multicast groups this instance should join. + /// Multicast groups this instance should join at creation. /// - /// The instance will be automatically added as a member of the specified - /// multicast groups during creation, enabling it to send and receive - /// multicast traffic for those groups. + /// Groups can be specified by name, UUID, or IP address. Non-existent + /// groups are created automatically. #[serde(default)] pub multicast_groups: Vec, /// A list of disks to be attached to the instance. + /// + /// Disk attachments of type "create" will be created, while those of type + /// "attach" must already exist. + /// + /// The order of this list does not guarantee a boot order for the instance. + /// Use the boot_disk attribute to specify a boot disk. When boot_disk is + /// specified it will count against the disk attachment limit. #[serde(default)] pub disks: Vec, /// The disk the instance is configured to boot from. + /// + /// This disk can either be attached if it already exists or created along + /// with the instance. + /// + /// Specifying a boot disk is optional but recommended to ensure predictable + /// boot behavior. The boot disk can be set during instance creation or + /// later if the instance is stopped. The boot disk counts against the disk + /// attachment limit. + /// + /// An instance that does not have a boot disk set will use the boot + /// options specified in its UEFI settings, which are controlled by both the + /// instance's UEFI firmware and the guest operating system. Boot options + /// can change as disks are attached and detached, which may result in an + /// instance that only boots to the EFI shell until a boot disk is set. #[serde(default)] pub boot_disk: Option, /// An allowlist of SSH public keys to be transferred to the instance via /// cloud-init during instance creation. + /// + /// If not provided, all SSH public keys from the user's profile will be sent. + /// If an empty list is provided, no public keys will be transmitted to the + /// instance. pub ssh_public_keys: Option>, /// Should this instance be started upon creation; true by default. @@ -84,6 +183,19 @@ pub struct InstanceCreate { pub start: bool, /// The auto-restart policy for this instance. + /// + /// This policy determines whether the instance should be automatically + /// restarted by the control plane on failure. If this is `null`, no + /// auto-restart policy will be explicitly configured for this instance, and + /// the control plane will select the default policy when determining + /// whether the instance can be automatically restarted. + /// + /// Currently, the global default auto-restart policy is "best-effort", so + /// instances with `null` auto-restart policies will be automatically + /// restarted. However, in the future, the default policy may be + /// configurable through other mechanisms, such as on a per-project basis. + /// In that case, any configured default policy will be used if this is + /// `null`. #[serde(default)] pub auto_restart_policy: Option, @@ -91,7 +203,10 @@ pub struct InstanceCreate { #[serde(default)] pub anti_affinity_groups: Vec, - /// The CPU platform to be used for this instance. + /// The CPU platform to be used for this instance. If this is `null`, the + /// instance requires no particular CPU platform; when it is started the + /// instance will have the most general CPU platform supported by the sled + /// it is initially placed on. #[serde(default)] pub cpu_platform: Option, } @@ -125,3 +240,70 @@ impl From for params::InstanceCreate { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_utils::{ + identity_strategy, optional_name_or_id_strategy, pool_selector_strategy, + }; + use proptest::prelude::*; + use std::net::IpAddr; + use test_strategy::proptest; + + fn floating_ip_create_strategy() -> impl Strategy + { + let address_selector = prop_oneof![ + (any::(), optional_name_or_id_strategy()) + .prop_map(|(ip, pool)| AddressSelector::Explicit { ip, pool }), + pool_selector_strategy().prop_map(|pool_selector| { + AddressSelector::Auto { pool_selector } + }), + ]; + + (identity_strategy(), address_selector).prop_map( + |(identity, address_selector)| FloatingIpCreate { + identity, + address_selector, + }, + ) + } + + /// Verifies that conversion to params::FloatingIpCreate preserves identity + /// and correctly maps AddressSelector to AddressAllocator. + #[proptest] + fn floating_ip_create_converts_correctly( + #[strategy(floating_ip_create_strategy())] expected: FloatingIpCreate, + ) { + use proptest::test_runner::TestCaseError; + let actual: params::FloatingIpCreate = expected.clone().into(); + + prop_assert_eq!(expected.identity.name, actual.identity.name); + prop_assert_eq!( + expected.identity.description, + actual.identity.description + ); + + match expected.address_selector { + AddressSelector::Explicit { ip: expected_ip, .. } => { + let params::AddressAllocator::Explicit { ip: actual_ip } = + actual.address_allocator + else { + return Err(TestCaseError::fail( + "expected Explicit variant", + )); + }; + prop_assert_eq!(expected_ip, actual_ip); + } + AddressSelector::Auto { pool_selector: expected_pool_selector } => { + let params::AddressAllocator::Auto { + pool_selector: actual_pool_selector, + } = actual.address_allocator + else { + return Err(TestCaseError::fail("expected Auto variant")); + }; + prop_assert_eq!(expected_pool_selector, actual_pool_selector); + } + } + } +} diff --git a/nexus/external-api/src/v2026011501.rs b/nexus/external-api/src/v2026011501.rs deleted file mode 100644 index d9c08417eaf..00000000000 --- a/nexus/external-api/src/v2026011501.rs +++ /dev/null @@ -1,257 +0,0 @@ -// 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/. - -//! Types from API version 2026010500 (`POOL_SELECTION_ENUMS`) that changed in -//! version 2026011600 (`RENAME_ADDRESS_SELECTOR_TO_ADDRESS_ALLOCATOR`). -//! -//! ## AddressAllocator Rename -//! -//! [`FloatingIpCreate`] has an `address_selector` field with type -//! [`AddressSelector`]. The "selector" naming is a misnomer in our current -//! scheme, where "selector" implies filtering/fetching from existing resources. -//! `RENAME_ADDRESS_SELECTOR_TO_ADDRESS_ALLOCATOR` renames these to -//! `address_allocator` and [`AddressAllocator`], which better describes the -//! action of reserving/assigning a floating IP from a pool. -//! -//! Affected endpoints: -//! - `POST /v1/floating-ips` (floating_ip_create) -//! -//! [`FloatingIpCreate`]: self::FloatingIpCreate -//! [`AddressSelector`]: self::AddressSelector -//! [`AddressAllocator`]: crate::v2026011600::AddressAllocator - -use std::net::IpAddr; - -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; - -use crate::v2026011600; -use nexus_types::external_api::params; -use omicron_common::api::external::{IdentityMetadataCreateParams, NameOrId}; - -/// Specify how to allocate a floating IP address. -#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] -#[serde(tag = "type", rename_all = "snake_case")] -pub enum AddressSelector { - /// Reserve a specific IP address. - Explicit { - /// The IP address to reserve. Must be available in the pool. - ip: IpAddr, - /// The pool containing this address. If not specified, the default - /// pool for the address's IP version is used. - pool: Option, - }, - /// Automatically allocate an IP address from a specified pool. - Auto { - /// Pool selection. - /// - /// If omitted, this field uses the silo's default pool. If the - /// silo has default pools for both IPv4 and IPv6, the request will - /// fail unless `ip_version` is specified in the pool selector. - #[serde(default)] - pool_selector: params::PoolSelector, - }, -} - -impl Default for AddressSelector { - fn default() -> Self { - AddressSelector::Auto { pool_selector: params::PoolSelector::default() } - } -} - -impl From for v2026011600::AddressAllocator { - fn from(value: AddressSelector) -> Self { - match value { - AddressSelector::Explicit { ip, pool } => { - v2026011600::AddressAllocator::Explicit { ip, pool } - } - AddressSelector::Auto { pool_selector } => { - v2026011600::AddressAllocator::Auto { pool_selector } - } - } - } -} - -impl From for params::AddressAllocator { - fn from(value: AddressSelector) -> Self { - v2026011600::AddressAllocator::from(value).into() - } -} - -/// Parameters for creating a new floating IP address for instances. -#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] -pub struct FloatingIpCreate { - #[serde(flatten)] - pub identity: IdentityMetadataCreateParams, - - /// IP address allocation method. - #[serde(default)] - pub address_selector: AddressSelector, -} - -impl From for v2026011600::FloatingIpCreate { - fn from(value: FloatingIpCreate) -> Self { - Self { - identity: value.identity, - address_allocator: value.address_selector.into(), - } - } -} - -impl From for params::FloatingIpCreate { - fn from(value: FloatingIpCreate) -> Self { - v2026011600::FloatingIpCreate::from(value).into() - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::test_utils::{ - identity_strategy, optional_name_or_id_strategy, pool_selector_strategy, - }; - use omicron_common::api::external::IpVersion; - use proptest::prelude::*; - use std::net::IpAddr; - use test_strategy::proptest; - - fn address_selector_strategy() -> impl Strategy { - prop_oneof![ - (any::(), optional_name_or_id_strategy()) - .prop_map(|(ip, pool)| AddressSelector::Explicit { ip, pool }), - pool_selector_strategy().prop_map(|pool_selector| { - AddressSelector::Auto { pool_selector } - }), - ] - } - - fn floating_ip_create_strategy() -> impl Strategy - { - (identity_strategy(), address_selector_strategy()).prop_map( - |(identity, address_selector)| FloatingIpCreate { - identity, - address_selector, - }, - ) - } - - /// Verifies that conversion to params::FloatingIpCreate preserves identity - /// and correctly maps AddressSelector to AddressAllocator. - #[proptest] - fn floating_ip_create_converts_correctly( - #[strategy(floating_ip_create_strategy())] expected: FloatingIpCreate, - ) { - use proptest::test_runner::TestCaseError; - let actual: params::FloatingIpCreate = expected.clone().into(); - - prop_assert_eq!(expected.identity.name, actual.identity.name); - prop_assert_eq!( - expected.identity.description, - actual.identity.description - ); - - match expected.address_selector { - AddressSelector::Explicit { ip: expected_ip, .. } => { - match actual.address_allocator { - params::AddressAllocator::Explicit { ip } => { - prop_assert_eq!(expected_ip, ip); - } - _ => { - return Err(TestCaseError::fail( - "expected Explicit variant", - )); - } - } - } - AddressSelector::Auto { pool_selector: expected_pool_selector } => { - match actual.address_allocator { - params::AddressAllocator::Auto { - pool_selector: actual_pool_selector, - } => { - prop_assert_eq!( - expected_pool_selector, - actual_pool_selector - ); - } - _ => { - return Err(TestCaseError::fail( - "expected Auto variant", - )); - } - } - } - } - } - - /// Verifies explicit JSON wire format parses into versioned types. - /// Conversion to latest params is tested by floating_ip_create_converts_correctly. - #[test] - fn explicit_json_wire_format() { - let json = - r#"{"type": "explicit", "ip": "10.0.0.1", "pool": "my-pool"}"#; - - // Must parse into this version (AddressSelector) - let v2026011501: AddressSelector = serde_json::from_str(json).unwrap(); - assert!(matches!(v2026011501, AddressSelector::Explicit { .. })); - - // Must also parse into v2026011600 (AddressAllocator) - let v2026011600: v2026011600::AddressAllocator = - serde_json::from_str(json).unwrap(); - assert!(matches!( - v2026011600, - v2026011600::AddressAllocator::Explicit { .. } - )); - } - - /// Verifies auto JSON wire format with explicit pool selector. - #[test] - fn auto_explicit_pool_json_wire_format() { - let json = r#"{"type": "auto", "pool_selector": {"type": "explicit", "pool": "my-pool"}}"#; - - let parsed: AddressSelector = serde_json::from_str(json).unwrap(); - match parsed { - AddressSelector::Auto { pool_selector } => { - assert!(matches!( - pool_selector, - params::PoolSelector::Explicit { .. } - )); - } - _ => panic!("Expected Auto variant"), - } - } - - /// Verifies auto JSON wire format with auto pool selector. - #[test] - fn auto_auto_pool_json_wire_format() { - let json = r#"{"type": "auto", "pool_selector": {"type": "auto", "ip_version": "v4"}}"#; - - let parsed: AddressSelector = serde_json::from_str(json).unwrap(); - match parsed { - AddressSelector::Auto { pool_selector } => match pool_selector { - params::PoolSelector::Auto { ip_version } => { - assert_eq!(ip_version, Some(IpVersion::V4)); - } - _ => panic!("Expected Auto pool selector"), - }, - _ => panic!("Expected Auto variant"), - } - } - - /// Verifies auto JSON wire format with default pool selector. - #[test] - fn auto_default_json_wire_format() { - let json = r#"{"type": "auto"}"#; - - let parsed: AddressSelector = serde_json::from_str(json).unwrap(); - match parsed { - AddressSelector::Auto { pool_selector } => match pool_selector { - params::PoolSelector::Auto { ip_version } => { - assert_eq!(ip_version, None); - } - _ => panic!("Expected Auto pool selector"), - }, - _ => panic!("Expected Auto variant"), - } - } -} diff --git a/nexus/external-api/src/v2026011600.rs b/nexus/external-api/src/v2026011600.rs index 8291db9afa1..34557f89789 100644 --- a/nexus/external-api/src/v2026011600.rs +++ b/nexus/external-api/src/v2026011600.rs @@ -25,6 +25,7 @@ use std::net::IpAddr; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use crate::v2026010500; use nexus_types::external_api::params; use omicron_common::api::external::{IdentityMetadataCreateParams, NameOrId}; @@ -85,6 +86,28 @@ pub struct FloatingIpCreate { pub address_allocator: AddressAllocator, } +impl From for AddressAllocator { + fn from(value: v2026010500::AddressSelector) -> Self { + match value { + v2026010500::AddressSelector::Explicit { ip, pool } => { + AddressAllocator::Explicit { ip, pool } + } + v2026010500::AddressSelector::Auto { pool_selector } => { + AddressAllocator::Auto { pool_selector } + } + } + } +} + +impl From for FloatingIpCreate { + fn from(value: v2026010500::FloatingIpCreate) -> Self { + Self { + identity: value.identity, + address_allocator: value.address_selector.into(), + } + } +} + impl From for params::FloatingIpCreate { fn from(value: FloatingIpCreate) -> Self { Self {