diff --git a/clients/sled-agent-client/src/lib.rs b/clients/sled-agent-client/src/lib.rs index 39de64ec62..fb468d9672 100644 --- a/clients/sled-agent-client/src/lib.rs +++ b/clients/sled-agent-client/src/lib.rs @@ -134,7 +134,7 @@ impl From for types::InstanceCpuCount { fn from(s: omicron_common::api::external::InstanceCpuCount) -> Self { - Self(s.0) + Self(s.0.try_into().unwrap()) } } @@ -196,7 +196,7 @@ impl From for omicron_common::api::external::InstanceCpuCount { fn from(s: types::InstanceCpuCount) -> Self { - Self(s.0) + Self(s.0.try_into().unwrap()) } } diff --git a/common/src/api/external/mod.rs b/common/src/api/external/mod.rs index 68fcb0f9fa..8ecf1798ab 100644 --- a/common/src/api/external/mod.rs +++ b/common/src/api/external/mod.rs @@ -41,6 +41,8 @@ use std::num::{NonZeroU16, NonZeroU32}; use std::str::FromStr; use uuid::Uuid; +use crate::limits::MAX_VCPU_PER_INSTANCE; + // The type aliases below exist primarily to ensure consistency among return // types for functions in the `nexus::Nexus` and `nexus::DataStore`. The // type argument `T` generally implements `Object`. @@ -902,9 +904,39 @@ impl InstanceState { } /// The number of CPUs in an Instance -#[derive(Copy, Clone, Debug, Deserialize, Serialize, JsonSchema)] +#[derive(Copy, Clone, Debug, Deserialize, Serialize)] pub struct InstanceCpuCount(pub u16); +impl JsonSchema for InstanceCpuCount { + fn schema_name() -> String { + "InstanceCpuCount".to_string() + } + + fn json_schema( + _: &mut schemars::gen::SchemaGenerator, + ) -> schemars::schema::Schema { + schemars::schema::SchemaObject { + metadata: Some(Box::new(schemars::schema::Metadata { + description: Some( + "The number of CPUs in an Instance".to_string(), + ), + ..Default::default() + })), + format: Some("uint16".to_string()), + instance_type: Some(schemars::schema::InstanceType::Integer.into()), + number: Some(Box::new(schemars::schema::NumberValidation { + multiple_of: None, + maximum: Some(MAX_VCPU_PER_INSTANCE.into()), + exclusive_maximum: None, + minimum: Some(1.0), + exclusive_minimum: None, + })), + ..Default::default() + } + .into() + } +} + impl TryFrom for InstanceCpuCount { type Error = anyhow::Error; diff --git a/common/src/lib.rs b/common/src/lib.rs index 0d63de90fb..b494eba568 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -26,6 +26,7 @@ pub mod backoff; pub mod cmd; pub mod disk; pub mod ledger; +pub mod limits; pub mod nexus_config; pub mod postgres_config; pub mod update; diff --git a/common/src/limits.rs b/common/src/limits.rs new file mode 100644 index 0000000000..9698cedd21 --- /dev/null +++ b/common/src/limits.rs @@ -0,0 +1,24 @@ +// 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/. + +// Contains constants that define the hard limits of Nexus + +pub const MAX_VCPU_PER_INSTANCE: u16 = 64; + +pub const MIN_MEMORY_BYTES_PER_INSTANCE: u32 = 1 << 30; // 1 GiB +pub const MAX_MEMORY_BYTES_PER_INSTANCE: u64 = 256 * (1 << 30); // 256 GiB + +pub const MAX_DISKS_PER_INSTANCE: u32 = 8; +pub const MIN_DISK_SIZE_BYTES: u32 = 1 << 30; // 1 GiB +pub const MAX_DISK_SIZE_BYTES: u64 = 1023 * (1 << 30); // 1023 GiB + +pub const MAX_NICS_PER_INSTANCE: usize = 8; + +// XXX: Might want to recast as max *floating* IPs, we have at most one +// ephemeral (so bounded in saga by design). +// The value here is arbitrary, but we need *a* limit for the instance +// create saga to have a bounded DAG. We might want to only enforce +// this during instance create (rather than live attach) in future. +pub const MAX_EXTERNAL_IPS_PER_INSTANCE: usize = 32; +pub const MAX_EPHEMERAL_IPS_PER_INSTANCE: usize = 1; diff --git a/end-to-end-tests/src/bin/bootstrap.rs b/end-to-end-tests/src/bin/bootstrap.rs index b02bed4265..9549210401 100644 --- a/end-to-end-tests/src/bin/bootstrap.rs +++ b/end-to-end-tests/src/bin/bootstrap.rs @@ -92,7 +92,7 @@ async fn main() -> Result<()> { disk_source: DiskSource::Blank { block_size: 512.try_into().unwrap(), }, - size: ByteCount(1024 * 1024 * 1024), + size: ByteCount::from(1024 * 1024 * 1024), }) .send() .await diff --git a/nexus/db-model/src/instance_cpu_count.rs b/nexus/db-model/src/instance_cpu_count.rs index 6c7debc6a5..8d676e0cac 100644 --- a/nexus/db-model/src/instance_cpu_count.rs +++ b/nexus/db-model/src/instance_cpu_count.rs @@ -47,6 +47,6 @@ where impl From for sled_agent_client::types::InstanceCpuCount { fn from(i: InstanceCpuCount) -> Self { - Self(i.0 .0) + Self((&i.0).try_into().unwrap()) } } diff --git a/nexus/db-queries/src/db/queries/disk.rs b/nexus/db-queries/src/db/queries/disk.rs index 9fd56c3ce8..64af650634 100644 --- a/nexus/db-queries/src/db/queries/disk.rs +++ b/nexus/db-queries/src/db/queries/disk.rs @@ -11,15 +11,9 @@ use diesel::{ query_builder::{AstPass, QueryFragment, QueryId}, sql_types, Column, QueryResult, }; +use omicron_common::limits::MAX_DISKS_PER_INSTANCE; use uuid::Uuid; -/// The maximum number of disks that can be attached to an instance. -// -// This is defined here for layering reasons: the main Nexus crate depends on -// the db-queries crate, so the disk-per-instance limit lives here and Nexus -// proper re-exports it. -pub const MAX_DISKS_PER_INSTANCE: u32 = 8; - /// A wrapper for the query that selects a PCI slot for a newly-attached disk. /// /// The general idea is to produce a query that left joins a single-column table diff --git a/nexus/src/app/disk.rs b/nexus/src/app/disk.rs index 5dd49a2efb..9a4454c52a 100644 --- a/nexus/src/app/disk.rs +++ b/nexus/src/app/disk.rs @@ -24,13 +24,12 @@ use omicron_common::api::external::LookupResult; use omicron_common::api::external::NameOrId; use omicron_common::api::external::UpdateResult; use omicron_common::api::internal::nexus::DiskRuntimeState; +use omicron_common::limits::MAX_DISK_SIZE_BYTES; +use omicron_common::limits::MIN_DISK_SIZE_BYTES; use sled_agent_client::Client as SledAgentClient; use std::sync::Arc; use uuid::Uuid; -use super::MAX_DISK_SIZE_BYTES; -use super::MIN_DISK_SIZE_BYTES; - impl super::Nexus { // Disks pub fn disk_lookup<'a>( diff --git a/nexus/src/app/instance.rs b/nexus/src/app/instance.rs index 778c5e2fe1..97ad3e64c4 100644 --- a/nexus/src/app/instance.rs +++ b/nexus/src/app/instance.rs @@ -4,13 +4,6 @@ //! Virtual Machine Instances -use super::MAX_DISKS_PER_INSTANCE; -use super::MAX_EPHEMERAL_IPS_PER_INSTANCE; -use super::MAX_EXTERNAL_IPS_PER_INSTANCE; -use super::MAX_MEMORY_BYTES_PER_INSTANCE; -use super::MAX_NICS_PER_INSTANCE; -use super::MAX_VCPU_PER_INSTANCE; -use super::MIN_MEMORY_BYTES_PER_INSTANCE; use crate::app::sagas; use crate::cidata::InstanceCiData; use crate::external_api::params; @@ -41,6 +34,14 @@ use omicron_common::api::external::NameOrId; use omicron_common::api::external::UpdateResult; use omicron_common::api::internal::nexus; use omicron_common::api::internal::shared::SourceNatConfig; +use omicron_common::limits::MAX_EXTERNAL_IPS_PER_INSTANCE; +use omicron_common::limits::MAX_MEMORY_BYTES_PER_INSTANCE; +use omicron_common::limits::MAX_NICS_PER_INSTANCE; +use omicron_common::limits::MAX_VCPU_PER_INSTANCE; +use omicron_common::limits::MIN_MEMORY_BYTES_PER_INSTANCE; +use omicron_common::limits::{ + MAX_DISKS_PER_INSTANCE, MAX_EPHEMERAL_IPS_PER_INSTANCE, +}; use propolis_client::support::tungstenite::protocol::frame::coding::CloseCode; use propolis_client::support::tungstenite::protocol::CloseFrame; use propolis_client::support::tungstenite::Message as WebSocketMessage; diff --git a/nexus/src/app/mod.rs b/nexus/src/app/mod.rs index 80bfd5ef22..0a9ce19dc3 100644 --- a/nexus/src/app/mod.rs +++ b/nexus/src/app/mod.rs @@ -75,29 +75,6 @@ mod vpc_subnet; // application logic. pub(crate) mod sagas; -// TODO: When referring to API types, we should try to include -// the prefix unless it is unambiguous. - -pub(crate) use nexus_db_queries::db::queries::disk::MAX_DISKS_PER_INSTANCE; - -pub(crate) const MAX_NICS_PER_INSTANCE: usize = 8; - -// XXX: Might want to recast as max *floating* IPs, we have at most one -// ephemeral (so bounded in saga by design). -// The value here is arbitrary, but we need *a* limit for the instance -// create saga to have a bounded DAG. We might want to only enforce -// this during instance create (rather than live attach) in future. -pub(crate) const MAX_EXTERNAL_IPS_PER_INSTANCE: usize = 32; -pub(crate) const MAX_EPHEMERAL_IPS_PER_INSTANCE: usize = 1; - -pub const MAX_VCPU_PER_INSTANCE: u16 = 64; - -pub const MIN_MEMORY_BYTES_PER_INSTANCE: u32 = 1 << 30; // 1 GiB -pub const MAX_MEMORY_BYTES_PER_INSTANCE: u64 = 256 * (1 << 30); // 256 GiB - -pub const MIN_DISK_SIZE_BYTES: u32 = 1 << 30; // 1 GiB -pub const MAX_DISK_SIZE_BYTES: u64 = 1023 * (1 << 30); // 1023 GiB - /// Manages an Oxide fleet -- the heart of the control plane pub struct Nexus { /// uuid for this nexus instance. diff --git a/nexus/src/app/sagas/instance_create.rs b/nexus/src/app/sagas/instance_create.rs index c4c9c4e083..5604c1be39 100644 --- a/nexus/src/app/sagas/instance_create.rs +++ b/nexus/src/app/sagas/instance_create.rs @@ -5,10 +5,6 @@ use super::{NexusActionContext, NexusSaga, SagaInitError, ACTION_GENERATE_ID}; use crate::app::sagas::declare_saga_actions; use crate::app::sagas::disk_create::{self, SagaDiskCreate}; -use crate::app::{ - MAX_DISKS_PER_INSTANCE, MAX_EXTERNAL_IPS_PER_INSTANCE, - MAX_NICS_PER_INSTANCE, -}; use crate::external_api::params; use nexus_db_model::NetworkInterfaceKind; use nexus_db_queries::db::identity::Resource; @@ -22,6 +18,10 @@ use omicron_common::api::external::IdentityMetadataCreateParams; use omicron_common::api::external::InstanceState; use omicron_common::api::external::Name; use omicron_common::api::internal::shared::SwitchLocation; +use omicron_common::limits::{ + MAX_DISKS_PER_INSTANCE, MAX_EXTERNAL_IPS_PER_INSTANCE, + MAX_NICS_PER_INSTANCE, +}; use serde::Deserialize; use serde::Serialize; use slog::warn; diff --git a/nexus/tests/integration_tests/disks.rs b/nexus/tests/integration_tests/disks.rs index b9023a8212..68730ec853 100644 --- a/nexus/tests/integration_tests/disks.rs +++ b/nexus/tests/integration_tests/disks.rs @@ -34,7 +34,8 @@ use omicron_common::api::external::IdentityMetadataCreateParams; use omicron_common::api::external::Instance; use omicron_common::api::external::Name; use omicron_common::api::external::NameOrId; -use omicron_nexus::app::{MAX_DISK_SIZE_BYTES, MIN_DISK_SIZE_BYTES}; +use omicron_common::limits::MAX_DISK_SIZE_BYTES; +use omicron_common::limits::MIN_DISK_SIZE_BYTES; use omicron_nexus::Nexus; use omicron_nexus::TestInterfaces as _; use oximeter::types::Datum; diff --git a/nexus/tests/integration_tests/instances.rs b/nexus/tests/integration_tests/instances.rs index 2f4e913185..0995a98156 100644 --- a/nexus/tests/integration_tests/instances.rs +++ b/nexus/tests/integration_tests/instances.rs @@ -55,9 +55,9 @@ use omicron_common::api::external::Ipv4Net; use omicron_common::api::external::Name; use omicron_common::api::external::NameOrId; use omicron_common::api::external::Vni; -use omicron_nexus::app::MAX_MEMORY_BYTES_PER_INSTANCE; -use omicron_nexus::app::MAX_VCPU_PER_INSTANCE; -use omicron_nexus::app::MIN_MEMORY_BYTES_PER_INSTANCE; +use omicron_common::limits::MAX_MEMORY_BYTES_PER_INSTANCE; +use omicron_common::limits::MAX_VCPU_PER_INSTANCE; +use omicron_common::limits::MIN_MEMORY_BYTES_PER_INSTANCE; use omicron_nexus::Nexus; use omicron_nexus::TestInterfaces as _; use omicron_sled_agent::sim::SledAgent; diff --git a/nexus/tests/integration_tests/snapshots.rs b/nexus/tests/integration_tests/snapshots.rs index 87ec2b3163..56d4240c0d 100644 --- a/nexus/tests/integration_tests/snapshots.rs +++ b/nexus/tests/integration_tests/snapshots.rs @@ -32,7 +32,7 @@ use omicron_common::api::external::IdentityMetadataCreateParams; use omicron_common::api::external::Instance; use omicron_common::api::external::InstanceCpuCount; use omicron_common::api::external::Name; -use omicron_nexus::app::MIN_DISK_SIZE_BYTES; +use omicron_common::limits::MIN_DISK_SIZE_BYTES; use uuid::Uuid; type ControlPlaneTestContext = diff --git a/nexus/types/src/external_api/params.rs b/nexus/types/src/external_api/params.rs index 750e83c2a2..4c2b4bb9b0 100644 --- a/nexus/types/src/external_api/params.rs +++ b/nexus/types/src/external_api/params.rs @@ -8,11 +8,17 @@ use crate::external_api::shared; use base64::Engine; use chrono::{DateTime, Utc}; -use omicron_common::api::external::{ - AddressLotKind, ByteCount, IdentityMetadataCreateParams, - IdentityMetadataUpdateParams, InstanceCpuCount, IpNet, Ipv4Net, Ipv6Net, - Name, NameOrId, PaginationOrder, RouteDestination, RouteTarget, - SemverVersion, +use omicron_common::{ + api::external::{ + AddressLotKind, ByteCount, IdentityMetadataCreateParams, + IdentityMetadataUpdateParams, InstanceCpuCount, IpNet, Ipv4Net, + Ipv6Net, Name, NameOrId, PaginationOrder, RouteDestination, + RouteTarget, SemverVersion, + }, + limits::{ + MAX_DISK_SIZE_BYTES, MAX_MEMORY_BYTES_PER_INSTANCE, + MIN_DISK_SIZE_BYTES, MIN_MEMORY_BYTES_PER_INSTANCE, + }, }; use schemars::JsonSchema; use serde::{ @@ -970,6 +976,7 @@ pub struct InstanceCreate { #[serde(flatten)] pub identity: IdentityMetadataCreateParams, pub ncpus: InstanceCpuCount, + #[schemars(schema_with = "memory_limits")] pub memory: ByteCount, pub hostname: String, // TODO-cleanup different type? @@ -1013,6 +1020,21 @@ fn bool_true() -> bool { true } +fn memory_limits( + gen: &mut schemars::gen::SchemaGenerator, +) -> schemars::schema::Schema { + let mut schema: schemars::schema::SchemaObject = + ::json_schema(gen).into(); + let min_mem = MIN_MEMORY_BYTES_PER_INSTANCE / (1 << 30); + let max_mem = MAX_MEMORY_BYTES_PER_INSTANCE / (1 << 30); + schema.metadata().description = Some( + format!("the amount of memory to allocate to the instance, in bytes.\n\nMust be between {min_mem} and {max_mem} GiB.") + ); + schema.number().minimum = Some(MIN_MEMORY_BYTES_PER_INSTANCE as f64); + schema.number().maximum = Some(MAX_MEMORY_BYTES_PER_INSTANCE as f64); + schema.into() +} + // If you change this, also update the error message in // `UserData::deserialize()` below. pub const MAX_USER_DATA_BYTES: usize = 32 * 1024; // 32 KiB @@ -1305,9 +1327,46 @@ pub struct DiskCreate { /// initial source for this disk pub disk_source: DiskSource, /// total size of the Disk in bytes + #[schemars(schema_with = "disk_size_limits")] pub size: ByteCount, } +fn disk_size_limits( + gen: &mut schemars::gen::SchemaGenerator, +) -> schemars::schema::Schema { + let mut schema: schemars::schema::SchemaObject = + ByteCount::json_schema(gen).into(); + schema.instance_type = None; + let min_disk = MIN_DISK_SIZE_BYTES / (1 << 30); + let max_disk = MAX_DISK_SIZE_BYTES / (1 << 30); + + schema.subschemas = Some(Box::new(schemars::schema::SubschemaValidation { + all_of: Some(vec![ + schemars::schema::Schema::new_ref( + "#/components/schemas/ByteCount".to_string(), + ), + schemars::schema::SchemaObject { + number: Some(Box::new(schemars::schema::NumberValidation { + minimum: Some(MIN_DISK_SIZE_BYTES as f64), + maximum: Some(MAX_DISK_SIZE_BYTES as f64), + ..Default::default() + })), + ..ByteCount::json_schema(gen).into() + } + .into(), + ]), + ..Default::default() + })); + + // schema.reference = Some("#/components/schemas/ByteCount".to_string()); + schema.metadata().description = Some( + format!("total size of the disk in bytes.\n\nMust be between {min_disk} and {max_disk} GiB.") + ); + // schema.number().minimum = Some(MIN_DISK_SIZE_BYTES as f64); + // schema.number().maximum = Some(MAX_DISK_SIZE_BYTES as f64); + schema.into() +} + // equivalent to crucible_pantry_client::types::ExpectedDigest #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] #[serde(rename_all = "snake_case")] diff --git a/openapi/nexus.json b/openapi/nexus.json index 2dd4037430..dbbc9603af 100644 --- a/openapi/nexus.json +++ b/openapi/nexus.json @@ -10666,11 +10666,10 @@ }, "size": { "description": "total size of the Disk in bytes", - "allOf": [ - { - "$ref": "#/components/schemas/ByteCount" - } - ] + "type": "integer", + "format": "uint64", + "minimum": 1073741824, + "maximum": 1098437885952 } }, "required": [ @@ -11990,7 +11989,8 @@ "description": "The number of CPUs in an Instance", "type": "integer", "format": "uint16", - "minimum": 0 + "minimum": 1, + "maximum": 64 }, "InstanceCreate": { "description": "Create-time parameters for an `Instance`", @@ -12019,7 +12019,11 @@ "type": "string" }, "memory": { - "$ref": "#/components/schemas/ByteCount" + "description": "the amount of memory to allocate to the instance, in bytes.\n\nMust be between 1 and 256 GiB.", + "type": "integer", + "format": "uint64", + "minimum": 1073741824, + "maximum": 274877906944 }, "name": { "$ref": "#/components/schemas/Name" @@ -12081,11 +12085,10 @@ }, "size": { "description": "total size of the Disk in bytes", - "allOf": [ - { - "$ref": "#/components/schemas/ByteCount" - } - ] + "type": "integer", + "format": "uint64", + "minimum": 1073741824, + "maximum": 1098437885952 }, "type": { "type": "string", diff --git a/openapi/sled-agent.json b/openapi/sled-agent.json index b5b9d3fd5b..cce7586fa0 100644 --- a/openapi/sled-agent.json +++ b/openapi/sled-agent.json @@ -4493,7 +4493,8 @@ "description": "The number of CPUs in an Instance", "type": "integer", "format": "uint16", - "minimum": 0 + "minimum": 1, + "maximum": 64 }, "InstanceEnsureBody": { "description": "The body of a request to ensure that a instance and VMM are known to a sled agent.",