diff --git a/dev-tools/omicron-dev/tests/test_omicron_dev.rs b/dev-tools/omicron-dev/tests/test_omicron_dev.rs index f1e8177243..88d92a7f69 100644 --- a/dev-tools/omicron-dev/tests/test_omicron_dev.rs +++ b/dev-tools/omicron-dev/tests/test_omicron_dev.rs @@ -27,7 +27,7 @@ use subprocess::Redirection; const CMD_OMICRON_DEV: &str = env!("CARGO_BIN_EXE_omicron-dev"); /// timeout used for various things that should be pretty quick -const TIMEOUT: Duration = Duration::from_secs(15); +const TIMEOUT: Duration = Duration::from_secs(60); fn path_to_omicron_dev() -> PathBuf { path_to_executable(CMD_OMICRON_DEV) diff --git a/nexus/src/app/instance.rs b/nexus/src/app/instance.rs index 21461b8780..76e9a0b4ff 100644 --- a/nexus/src/app/instance.rs +++ b/nexus/src/app/instance.rs @@ -5,6 +5,7 @@ //! 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; @@ -52,6 +53,7 @@ use sled_agent_client::types::InstanceProperties; use sled_agent_client::types::InstancePutMigrationIdsBody; use sled_agent_client::types::InstancePutStateBody; use sled_agent_client::types::SourceNatConfig; +use std::matches; use std::net::SocketAddr; use std::sync::Arc; use tokio::io::{AsyncRead, AsyncWrite}; @@ -168,6 +170,18 @@ impl super::Nexus { MAX_EXTERNAL_IPS_PER_INSTANCE, ))); } + if params + .external_ips + .iter() + .filter(|v| matches!(v, params::ExternalIpCreate::Ephemeral { .. })) + .count() + > MAX_EPHEMERAL_IPS_PER_INSTANCE + { + return Err(Error::invalid_request(&format!( + "An instance may not have more than {} ephemeral IP address", + MAX_EPHEMERAL_IPS_PER_INSTANCE, + ))); + } if let params::InstanceNetworkInterfaceAttachment::Create(ref ifaces) = params.network_interfaces { diff --git a/nexus/src/app/mod.rs b/nexus/src/app/mod.rs index b04344b2a4..a3b2e96360 100644 --- a/nexus/src/app/mod.rs +++ b/nexus/src/app/mod.rs @@ -82,6 +82,7 @@ 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). 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; diff --git a/nexus/tests/integration_tests/endpoints.rs b/nexus/tests/integration_tests/endpoints.rs index 64790c49c2..a421f7a701 100644 --- a/nexus/tests/integration_tests/endpoints.rs +++ b/nexus/tests/integration_tests/endpoints.rs @@ -115,6 +115,7 @@ lazy_static! { pub static ref DEMO_PROJECT_URL_INSTANCES: String = format!("/v1/instances?project={}", *DEMO_PROJECT_NAME); pub static ref DEMO_PROJECT_URL_SNAPSHOTS: String = format!("/v1/snapshots?project={}", *DEMO_PROJECT_NAME); pub static ref DEMO_PROJECT_URL_VPCS: String = format!("/v1/vpcs?project={}", *DEMO_PROJECT_NAME); + pub static ref DEMO_PROJECT_URL_FIPS: String = format!("/v1/floating-ips?project={}", *DEMO_PROJECT_NAME); pub static ref DEMO_PROJECT_CREATE: params::ProjectCreate = params::ProjectCreate { identity: IdentityMetadataCreateParams { @@ -554,6 +555,22 @@ lazy_static! { }; } +lazy_static! { + // Project Floating IPs + pub static ref DEMO_FLOAT_IP_NAME: Name = "float-ip".parse().unwrap(); + pub static ref DEMO_FLOAT_IP_URL: String = + format!("/v1/floating-ips/{}?project={}", *DEMO_FLOAT_IP_NAME, *DEMO_PROJECT_NAME); + pub static ref DEMO_FLOAT_IP_CREATE: params::FloatingIpCreate = + params::FloatingIpCreate { + identity: IdentityMetadataCreateParams { + name: DEMO_FLOAT_IP_NAME.clone(), + description: String::from("a new IP pool"), + }, + address: Some(std::net::Ipv4Addr::new(10, 0, 0, 141).into()), + pool: None, + }; +} + lazy_static! { // Identity providers pub static ref IDENTITY_PROVIDERS_URL: String = format!("/v1/system/identity-providers?silo=demo-silo"); @@ -1961,6 +1978,29 @@ lazy_static! { allowed_methods: vec![ AllowedMethod::GetNonexistent, ], + }, + + // Floating IPs + VerifyEndpoint { + url: &DEMO_PROJECT_URL_FIPS, + visibility: Visibility::Protected, + unprivileged_access: UnprivilegedAccess::None, + allowed_methods: vec![ + AllowedMethod::Post( + serde_json::to_value(&*DEMO_FLOAT_IP_CREATE).unwrap(), + ), + AllowedMethod::Get, + ], + }, + + VerifyEndpoint { + url: &DEMO_FLOAT_IP_URL, + visibility: Visibility::Protected, + unprivileged_access: UnprivilegedAccess::None, + allowed_methods: vec![ + AllowedMethod::Get, + AllowedMethod::Delete, + ], } ]; } diff --git a/nexus/tests/integration_tests/unauthorized.rs b/nexus/tests/integration_tests/unauthorized.rs index 9936af20bf..1cb2eaca3a 100644 --- a/nexus/tests/integration_tests/unauthorized.rs +++ b/nexus/tests/integration_tests/unauthorized.rs @@ -278,6 +278,12 @@ lazy_static! { body: serde_json::to_value(&*DEMO_IMAGE_CREATE).unwrap(), id_routes: vec!["/v1/images/{id}"], }, + // Create a Floating IP in the project + SetupReq::Post { + url: &DEMO_PROJECT_URL_FIPS, + body: serde_json::to_value(&*DEMO_FLOAT_IP_CREATE).unwrap(), + id_routes: vec!["/v1/floating-ips/{id}"], + }, // Create a SAML identity provider SetupReq::Post { url: &SAML_IDENTITY_PROVIDERS_URL, diff --git a/nexus/types/src/external_api/params.rs b/nexus/types/src/external_api/params.rs index 12dd01299b..13433d0001 100644 --- a/nexus/types/src/external_api/params.rs +++ b/nexus/types/src/external_api/params.rs @@ -770,13 +770,11 @@ pub struct FloatingIpCreate { /// An IP address to reserve for use as a floating IP. This field is /// optional if a pool is provided, in which case an address will /// be automatically chosen from there. - // TODO: draw from pool if needed. pub address: Option, /// The parent IP pool that a floating IP is pulled from. If combined /// with an explicit address, then that address must be available in /// the pool. - // TODO: support tie-in to pools. pub pool: Option, }