diff --git a/common/src/api/external/mod.rs b/common/src/api/external/mod.rs index adf661516a..b83fb0ce2c 100644 --- a/common/src/api/external/mod.rs +++ b/common/src/api/external/mod.rs @@ -751,6 +751,7 @@ pub enum ResourceType { Zpool, Vmm, Ipv4NatEntry, + // ExternalIp, } // IDENTITY METADATA diff --git a/nexus/db-model/src/external_ip.rs b/nexus/db-model/src/external_ip.rs index 5fdecc5337..583bc5c2a2 100644 --- a/nexus/db-model/src/external_ip.rs +++ b/nexus/db-model/src/external_ip.rs @@ -57,8 +57,8 @@ pub struct ExternalIp { pub time_created: DateTime, pub time_modified: DateTime, pub time_deleted: Option>, - pub ip_pool_id: Option, - pub ip_pool_range_id: Option, + pub ip_pool_id: Uuid, + pub ip_pool_range_id: Uuid, pub is_service: bool, // This is Some(_) for: // - all instance/service SNAT IPs @@ -69,6 +69,7 @@ pub struct ExternalIp { pub ip: IpNetwork, pub first_port: SqlU16, pub last_port: SqlU16, + // Only Some(_) for instance Floating IPs pub project_id: Option, } @@ -93,7 +94,7 @@ pub struct IncompleteExternalIp { kind: IpKind, is_service: bool, parent_id: Option, - pool_id: Option, + pool_id: Uuid, project_id: Option, // Optional address requesting that a specific IP address be allocated. explicit_ip: Option, @@ -115,7 +116,7 @@ impl IncompleteExternalIp { kind: IpKind::SNat, is_service: false, parent_id: Some(instance_id), - pool_id: Some(pool_id), + pool_id, project_id: None, explicit_ip: None, explicit_port_range: None, @@ -131,7 +132,7 @@ impl IncompleteExternalIp { kind: IpKind::Ephemeral, is_service: false, parent_id: Some(instance_id), - pool_id: Some(pool_id), + pool_id, project_id: None, explicit_ip: None, explicit_port_range: None, @@ -153,7 +154,7 @@ impl IncompleteExternalIp { kind: IpKind::Floating, is_service: false, parent_id: None, - pool_id: Some(pool_id), + pool_id, project_id: Some(project_id), explicit_ip: None, explicit_port_range: None, @@ -166,6 +167,7 @@ impl IncompleteExternalIp { description: &str, project_id: Uuid, explicit_ip: IpAddr, + pool_id: Uuid, ) -> Self { Self { id, @@ -175,7 +177,7 @@ impl IncompleteExternalIp { kind: IpKind::Floating, is_service: false, parent_id: None, - pool_id: None, + pool_id, project_id: Some(project_id), explicit_ip: Some(explicit_ip.into()), explicit_port_range: None, @@ -198,7 +200,7 @@ impl IncompleteExternalIp { kind: IpKind::Floating, is_service: true, parent_id: Some(service_id), - pool_id: Some(pool_id), + pool_id, project_id: None, explicit_ip: Some(IpNetwork::from(address)), explicit_port_range: None, @@ -227,7 +229,7 @@ impl IncompleteExternalIp { kind: IpKind::SNat, is_service: true, parent_id: Some(service_id), - pool_id: Some(pool_id), + pool_id, project_id: None, explicit_ip: Some(IpNetwork::from(address)), explicit_port_range, @@ -249,7 +251,7 @@ impl IncompleteExternalIp { kind: IpKind::Floating, is_service: true, parent_id: Some(service_id), - pool_id: Some(pool_id), + pool_id, project_id: None, explicit_ip: None, explicit_port_range: None, @@ -265,7 +267,7 @@ impl IncompleteExternalIp { kind: IpKind::SNat, is_service: true, parent_id: Some(service_id), - pool_id: Some(pool_id), + pool_id, project_id: None, explicit_ip: None, explicit_port_range: None, @@ -300,7 +302,7 @@ impl IncompleteExternalIp { &self.parent_id } - pub fn pool_id(&self) -> &Option { + pub fn pool_id(&self) -> &Uuid { &self.pool_id } diff --git a/nexus/db-model/src/schema.rs b/nexus/db-model/src/schema.rs index 1820239fbd..1449c3d20e 100644 --- a/nexus/db-model/src/schema.rs +++ b/nexus/db-model/src/schema.rs @@ -524,14 +524,16 @@ table! { time_created -> Timestamptz, time_modified -> Timestamptz, time_deleted -> Nullable, - ip_pool_id -> Nullable, - ip_pool_range_id -> Nullable, + + ip_pool_id -> Uuid, + ip_pool_range_id -> Uuid, is_service -> Bool, parent_id -> Nullable, kind -> crate::IpKindEnum, ip -> Inet, first_port -> Int4, last_port -> Int4, + project_id -> Nullable, } } diff --git a/nexus/db-queries/src/db/datastore/external_ip.rs b/nexus/db-queries/src/db/datastore/external_ip.rs index fa7d936de6..4fe0c6033a 100644 --- a/nexus/db-queries/src/db/datastore/external_ip.rs +++ b/nexus/db-queries/src/db/datastore/external_ip.rs @@ -132,20 +132,52 @@ impl DataStore { pub async fn allocate_floating_ip( &self, opctx: &OpContext, + pool_id: Option, project_id: Uuid, ip_id: Uuid, name: &Name, description: &str, - ip: IpAddr, + ip: Option, ) -> CreateResult { - let data = IncompleteExternalIp::for_floating_explicit( - ip_id, - name, - description, - project_id, - ip, - ); + // XXX: mux here to scan *all* project pools in + // current silo for convenience? + let pool_id = if let Some(id) = pool_id { + id + } else { + self.ip_pools_fetch_default(opctx).await?.id() + }; + + // XXX: Verify that chosen pool comes from my silo. + + let data = if let Some(ip) = ip { + IncompleteExternalIp::for_floating_explicit( + ip_id, + name, + description, + project_id, + ip, + pool_id, + ) + } else { + IncompleteExternalIp::for_floating( + ip_id, + name, + description, + project_id, + pool_id, + ) + }; + + // TODO: need to disambiguate no IP and/or IP taken + // from resource name collision, and expose those in + // a nice way. self.allocate_external_ip(opctx, data).await + // .map_err(|e| { + // public_error_from_diesel( + // e, + // ErrorHandler::Conflict(todo!(), name.as_str()) + // ) + // }) } /// Allocates a floating IP address for instance usage. diff --git a/nexus/db-queries/src/db/datastore/ip_pool.rs b/nexus/db-queries/src/db/datastore/ip_pool.rs index fb300ef833..6be134fd65 100644 --- a/nexus/db-queries/src/db/datastore/ip_pool.rs +++ b/nexus/db-queries/src/db/datastore/ip_pool.rs @@ -110,6 +110,16 @@ impl DataStore { .map_err(|e| public_error_from_diesel(e, ErrorHandler::Server)) } + /// Lookup an IP pool within the current silo which contains a target IP + /// address. + pub async fn ip_pools_fetch_for_ip( + &self, + opctx: &OpContext, + ip_addr: std::net::IpAddr, + ) -> LookupResult { + todo!() + } + /// Looks up an IP pool intended for internal services. /// /// This method may require an index by Availability Zone in the future. diff --git a/nexus/db-queries/src/db/datastore/mod.rs b/nexus/db-queries/src/db/datastore/mod.rs index 22b23cbef3..2456c79843 100644 --- a/nexus/db-queries/src/db/datastore/mod.rs +++ b/nexus/db-queries/src/db/datastore/mod.rs @@ -1565,8 +1565,8 @@ mod test { time_created: now, time_modified: now, time_deleted: None, - ip_pool_id: Some(Uuid::new_v4()), - ip_pool_range_id: Some(Uuid::new_v4()), + ip_pool_id: Uuid::new_v4(), + ip_pool_range_id: Uuid::new_v4(), project_id: None, is_service: false, parent_id: Some(instance_id), @@ -1626,8 +1626,8 @@ mod test { time_created: now, time_modified: now, time_deleted: None, - ip_pool_id: Some(Uuid::new_v4()), - ip_pool_range_id: Some(Uuid::new_v4()), + ip_pool_id: Uuid::new_v4(), + ip_pool_range_id: Uuid::new_v4(), project_id: None, is_service: false, parent_id: Some(Uuid::new_v4()), @@ -1697,8 +1697,8 @@ mod test { time_created: now, time_modified: now, time_deleted: None, - ip_pool_id: Some(Uuid::new_v4()), - ip_pool_range_id: Some(Uuid::new_v4()), + ip_pool_id: Uuid::new_v4(), + ip_pool_range_id: Uuid::new_v4(), project_id: None, is_service: false, parent_id: Some(Uuid::new_v4()), diff --git a/nexus/db-queries/src/db/datastore/rack.rs b/nexus/db-queries/src/db/datastore/rack.rs index 1c8f2f1f04..2cc5880470 100644 --- a/nexus/db-queries/src/db/datastore/rack.rs +++ b/nexus/db-queries/src/db/datastore/rack.rs @@ -1148,31 +1148,31 @@ mod test { assert_eq!(observed_ip_pool_ranges[0].ip_pool_id, svc_pool.id()); // Verify the allocated external IPs - assert_eq!(dns_external_ip.ip_pool_id, Some(svc_pool.id())); + assert_eq!(dns_external_ip.ip_pool_id, svc_pool.id()); assert_eq!( dns_external_ip.ip_pool_range_id, - Some(observed_ip_pool_ranges[0].id) + observed_ip_pool_ranges[0].id ); assert_eq!(dns_external_ip.ip.ip(), external_dns_ip); - assert_eq!(nexus_external_ip.ip_pool_id, Some(svc_pool.id())); + assert_eq!(nexus_external_ip.ip_pool_id, svc_pool.id()); assert_eq!( nexus_external_ip.ip_pool_range_id, - Some(observed_ip_pool_ranges[0].id) + observed_ip_pool_ranges[0].id ); assert_eq!(nexus_external_ip.ip.ip(), nexus_ip); - assert_eq!(ntp1_external_ip.ip_pool_id, Some(svc_pool.id())); + assert_eq!(ntp1_external_ip.ip_pool_id, svc_pool.id()); assert_eq!( ntp1_external_ip.ip_pool_range_id, - Some(observed_ip_pool_ranges[0].id) + observed_ip_pool_ranges[0].id ); assert_eq!(ntp1_external_ip.ip.ip(), ntp1_ip); - assert_eq!(ntp2_external_ip.ip_pool_id, Some(svc_pool.id())); + assert_eq!(ntp2_external_ip.ip_pool_id, svc_pool.id()); assert_eq!( ntp2_external_ip.ip_pool_range_id, - Some(observed_ip_pool_ranges[0].id) + observed_ip_pool_ranges[0].id ); assert_eq!(ntp2_external_ip.ip.ip(), ntp2_ip); diff --git a/nexus/db-queries/src/db/lookup.rs b/nexus/db-queries/src/db/lookup.rs index 72a32f562c..ead2350a50 100644 --- a/nexus/db-queries/src/db/lookup.rs +++ b/nexus/db-queries/src/db/lookup.rs @@ -632,6 +632,7 @@ lookup_resource! { lookup_resource! { name = "Project", ancestors = [ "Silo" ], + // children = [ "Disk", "Instance", "Vpc", "Snapshot", "ProjectImage", "ExternalIp" ], children = [ "Disk", "Instance", "Vpc", "Snapshot", "ProjectImage" ], lookup_by_name = true, soft_deletes = true, @@ -728,6 +729,15 @@ lookup_resource! { primary_key_columns = [ { column_name = "id", rust_type = Uuid } ] } +// lookup_resource! { +// name = "ExternalIp", +// ancestors = [ "Silo", "Project" ], +// children = [], +// lookup_by_name = true, +// soft_deletes = true, +// primary_key_columns = [ { column_name = "id", rust_type = Uuid } ] +// } + // Miscellaneous resources nested directly below "Fleet" lookup_resource! { diff --git a/nexus/db-queries/src/db/queries/external_ip.rs b/nexus/db-queries/src/db/queries/external_ip.rs index f93b26b9ef..4e5f59e79c 100644 --- a/nexus/db-queries/src/db/queries/external_ip.rs +++ b/nexus/db-queries/src/db/queries/external_ip.rs @@ -98,7 +98,8 @@ const MAX_PORT: u16 = u16::MAX; /// AS kind, /// candidate_ip AS ip, /// CAST(candidate_first_port AS INT4) AS first_port, -/// CAST(candidate_last_port AS INT4) AS last_port +/// CAST(candidate_last_port AS INT4) AS last_port, +/// AS project_id /// FROM /// SELECT * FROM ( /// -- Select all IP addresses by pool and range. @@ -371,6 +372,13 @@ impl NextExternalIp { out.push_identifier(dsl::first_port::NAME)?; out.push_sql(", CAST(candidate_last_port AS INT4) AS "); out.push_identifier(dsl::last_port::NAME)?; + out.push_sql(", "); + + // Project ID, possibly null + out.push_bind_param::, Option>(self.ip.project_id())?; + out.push_sql(" AS "); + out.push_identifier(dsl::project_id::NAME)?; + out.push_sql(" FROM ("); self.push_address_sequence_subquery(out.reborrow())?; out.push_sql(") CROSS JOIN ("); @@ -622,7 +630,7 @@ impl NextExternalIp { out.push_sql(" WHERE "); out.push_identifier(dsl::ip_pool_id::NAME)?; out.push_sql(" = "); - out.push_bind_param::, Option>(self.ip.pool_id())?; + out.push_bind_param::(self.ip.pool_id())?; out.push_sql(" AND "); out.push_identifier(dsl::time_deleted::NAME)?; out.push_sql(" IS NULL"); diff --git a/nexus/src/app/external_ip.rs b/nexus/src/app/external_ip.rs index 9b1b8535a1..0c3a577fda 100644 --- a/nexus/src/app/external_ip.rs +++ b/nexus/src/app/external_ip.rs @@ -8,11 +8,15 @@ use crate::external_api::views::ExternalIp; use nexus_db_queries::authz; use nexus_db_queries::context::OpContext; use nexus_db_queries::db::lookup; +use nexus_db_queries::db::lookup::LookupPath; use nexus_db_queries::db::model::IpKind; use nexus_types::external_api::params; use omicron_common::api::external::CreateResult; +use omicron_common::api::external::DeleteResult; use omicron_common::api::external::Error; use omicron_common::api::external::ListResultVec; +use omicron_common::api::external::LookupResult; +use omicron_common::api::external::NameOrId; use uuid::Uuid; impl super::Nexus { @@ -38,10 +42,45 @@ impl super::Nexus { .collect::>()) } - pub(crate) async fn list_floating_ips( + pub fn floating_ip_lookup<'a>( + &'a self, + opctx: &'a OpContext, + fip_selector: params::FloatingIpSelector, + ) -> LookupResult> { + match fip_selector { + params::FloatingIpSelector { floating_ip: NameOrId::Id(id), project: None } => { + // let floating_ip = + // LookupPath::new(opctx, &self.db_datastore).floating_ip_id(id); + // Ok(floating_ip) + todo!() + } + params::FloatingIpSelector { + floating_ip: NameOrId::Name(name), + project: Some(project), + } => { + // let floating_ip = self + // .project_lookup(opctx, params::ProjectSelector { project })? + // .floating_ip_name_owned(name.into()); + // Ok(floating_ip) + todo!() + } + params::FloatingIpSelector { + floating_ip: NameOrId::Id(_), + .. + } => Err(Error::invalid_request( + "when providing Floating IP as an ID project should not be specified", + )), + _ => Err(Error::invalid_request( + "Floating IP should either be UUID or project should be specified", + )), + } + } + + pub(crate) async fn floating_ips_list( &self, opctx: &OpContext, project_lookup: &lookup::Project<'_>, + // pagparams: &PaginatedBy<'_>, ) -> ListResultVec { let (.., authz_project) = project_lookup.lookup_for(authz::Action::Read).await?; @@ -54,7 +93,7 @@ impl super::Nexus { .collect::>()) } - pub(crate) async fn create_floating_ip( + pub(crate) async fn floating_ip_create( &self, opctx: &OpContext, project_lookup: &lookup::Project<'_>, @@ -63,30 +102,41 @@ impl super::Nexus { let (.., authz_project) = project_lookup.lookup_for(authz::Action::CreateChild).await?; - let chosen_addr = match (¶ms.pool, params.address) { - (Some(_), _) => { - todo!("Drawing floating IP from pools not yet supported.") - } - (None, Some(ip)) => ip, - _ => { - return Err(Error::invalid_request( - "floating IP needs a pool or ", + // XXX: support pool by name here. + let pool_id = match ¶ms.pool { + Some(NameOrId::Id(ref id)) => Some(*id), + Some(NameOrId::Name(ref _name)) => { + return Err(Error::internal_error( + "pool ref by name not yet supported", )) } + None => None, }; Ok(self .db_datastore .allocate_floating_ip( opctx, + pool_id, authz_project.id(), Uuid::new_v4(), ¶ms.identity.name.clone().into(), ¶ms.identity.description, - chosen_addr, + params.address, ) .await? .try_into() .unwrap()) } + + pub(crate) async fn floating_ip_delete( + &self, + opctx: &OpContext, + // pool_lookup: &lookup::<'_>, + ) -> DeleteResult { + // let (.., authz_pool, db_pool) = + // pool_lookup.fetch_for(authz::Action::Delete).await?; + // self.db_datastore.ip_pool_delete(opctx, &authz_pool, &db_pool).await + todo!() + } } diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index 843ffa4d60..aca6278f12 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -141,6 +141,8 @@ pub(crate) fn external_api() -> NexusApiDescription { api.register(floating_ip_list)?; api.register(floating_ip_create)?; + api.register(floating_ip_view)?; + api.register(floating_ip_delete)?; api.register(disk_list)?; api.register(disk_create)?; @@ -1526,21 +1528,32 @@ async fn ip_pool_service_range_remove( /// List all Floating IPs #[endpoint { method = GET, - path = "/v1/system/floating-ips", - tags = ["system/networking"], + path = "/v1/floating-ips", + tags = ["floating-ips"], }] async fn floating_ip_list( rqctx: RequestContext>, - query_params: Query, + query_params: Query>, ) -> Result>, HttpError> { let apictx = rqctx.context(); let handler = async { let nexus = &apictx.nexus; let opctx = crate::context::op_context_for_external_api(&rqctx).await?; + let query = query_params.into_inner(); + let pag_params = data_page_params_for(&rqctx, &query)?; + let scan_params = ScanByNameOrId::from_query(&query)?; + let paginated_by = name_or_id_pagination(&pag_params, scan_params)?; let project_lookup = - nexus.project_lookup(&opctx, query_params.into_inner())?; - let ips = nexus.list_floating_ips(&opctx, &project_lookup).await?; + nexus.project_lookup(&opctx, scan_params.selector.clone())?; + // XXX: impl relevant traits to make results_page work. + // let ips = nexus.list_floating_ips(&opctx, &project_lookup, &paginated_by).await?; + let ips = nexus.floating_ips_list(&opctx, &project_lookup).await?; Ok(HttpResponseOk(ResultsPage { items: ips, next_page: None })) + // Ok(HttpResponseOk(ScanByNameOrId::results_page( + // &query, + // ips, + // &marker_for_name_or_id, + // )?)) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } @@ -1548,8 +1561,8 @@ async fn floating_ip_list( /// Create a Floating IP #[endpoint { method = POST, - path = "/v1/system/floating-ips", - tags = ["system/networking"], + path = "/v1/floating-ips", + tags = ["floating-ips"], }] async fn floating_ip_create( rqctx: RequestContext>, @@ -1564,13 +1577,72 @@ async fn floating_ip_create( let project_lookup = nexus.project_lookup(&opctx, query_params.into_inner())?; let ip = nexus - .create_floating_ip(&opctx, &project_lookup, &floating_params) + .floating_ip_create(&opctx, &project_lookup, &floating_params) .await?; Ok(HttpResponseCreated(views::ExternalIp::from(ip))) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } +/// Delete a Floating IP +#[endpoint { + method = DELETE, + path = "/v1/floating-ips/{floating_ip}", + tags = ["floating-ips"], +}] +async fn floating_ip_delete( + rqctx: RequestContext>, + path_params: Path, + query_params: Query, +) -> Result { + todo!(); + + // TODO: more work needed here to plug into authz, and pathlookup + // etc. + + // let apictx = rqctx.context(); + // let handler = async { + // let opctx = crate::context::op_context_for_external_api(&rqctx).await?; + // let nexus = &apictx.nexus; + // let path = path_params.into_inner(); + // let query = query_params.into_inner(); + // let disk_selector = + // params::FloatingIpSelector { floating_ip: path.floating_ip, project: query.project }; + // let disk_lookup = nexus.disk_lookup(&opctx, disk_selector)?; + // nexus.project_delete_disk(&opctx, &disk_lookup).await?; + // Ok(HttpResponseDeleted()) + // }; + // apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + +/// Fetch a floating IP +#[endpoint { + method = GET, + path = "/v1/floating-ips/{floating_ip}", + tags = ["floating-ips"] +}] +async fn floating_ip_view( + rqctx: RequestContext>, + path_params: Path, + query_params: Query, +) -> Result, HttpError> { + todo!(); + + // let apictx = rqctx.context(); + // let handler = async { + // let opctx = crate::context::op_context_for_external_api(&rqctx).await?; + // let nexus = &apictx.nexus; + // let path = path_params.into_inner(); + // let query = query_params.into_inner(); + // let disk_selector = + // params::FloatingIpSelector { floating_ip: path.floating_ip, project: query.project }; + // let (.., disk) = + // nexus.disk_lookup(&opctx, disk_selector)?.fetch().await?; + // Ok(HttpResponseOk(disk.into())) + // }; + // apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + // Disks /// List disks diff --git a/nexus/src/external_api/tag-config.json b/nexus/src/external_api/tag-config.json index 07eb198016..3bc8006cee 100644 --- a/nexus/src/external_api/tag-config.json +++ b/nexus/src/external_api/tag-config.json @@ -8,6 +8,12 @@ "url": "http://docs.oxide.computer/api/disks" } }, + "floating-ips": { + "description": "Floating IPs allow a project to allocate well-known IPs to instances.", + "external_docs": { + "url": "http://docs.oxide.computer/api/floating-ips" + } + }, "hidden": { "description": "TODO operations that will not ship to customers", "external_docs": { diff --git a/nexus/types/src/external_api/params.rs b/nexus/types/src/external_api/params.rs index 3a3dabc969..0c24570e57 100644 --- a/nexus/types/src/external_api/params.rs +++ b/nexus/types/src/external_api/params.rs @@ -54,6 +54,7 @@ path_param!(VpcPath, vpc, "VPC"); path_param!(SubnetPath, subnet, "subnet"); path_param!(RouterPath, router, "router"); path_param!(RoutePath, route, "route"); +path_param!(FloatingIpPath, floating_ip, "Floating IP"); path_param!(DiskPath, disk, "disk"); path_param!(SnapshotPath, snapshot, "snapshot"); path_param!(ImagePath, image, "image"); @@ -129,6 +130,14 @@ pub struct OptionalProjectSelector { pub project: Option, } +#[derive(Deserialize, JsonSchema)] +pub struct FloatingIpSelector { + /// Name or ID of the project, only required if `floating_ip` is provided as a `Name` + pub project: Option, + /// Name or ID of the Floating IP + pub floating_ip: NameOrId, +} + #[derive(Deserialize, JsonSchema)] pub struct DiskSelector { /// Name or ID of the project, only required if `disk` is provided as a `Name` diff --git a/schema/crdb/14.0.0/up02.sql b/schema/crdb/14.0.0/up02.sql index 9d07275113..733c46b0dc 100644 --- a/schema/crdb/14.0.0/up02.sql +++ b/schema/crdb/14.0.0/up02.sql @@ -1,3 +1,4 @@ ALTER TABLE omicron.public.external_ip ADD CONSTRAINT IF NOT EXISTS null_project_id CHECK ( - kind = 'floating' OR project_id IS NULL + (kind = 'floating' AND is_service = FALSE AND project_id IS NOT NULL) OR + ((kind != 'floating' OR is_service = TRUE) AND project_id IS NULL) ); diff --git a/schema/crdb/14.0.0/up03.sql b/schema/crdb/14.0.0/up03.sql index 692770425c..d3577edc12 100644 --- a/schema/crdb/14.0.0/up03.sql +++ b/schema/crdb/14.0.0/up03.sql @@ -1,3 +1,6 @@ -ALTER TABLE omicron.public.external_ip ADD CONSTRAINT IF NOT EXISTS null_non_fip_pool_id CHECK ( - kind = 'floating' OR (ip_pool_id IS NOT NULL AND ip_pool_range_id IS NOT NULL) -); +CREATE UNIQUE INDEX IF NOT EXISTS lookup_floating_ip_by_name on omicron.public.external_ip ( + name +) WHERE + kind = 'floating' AND + time_deleted is NULL AND + project_id is NULL; diff --git a/schema/crdb/14.0.0/up04.sql b/schema/crdb/14.0.0/up04.sql index 742b83a9e6..9a40dc99c5 100644 --- a/schema/crdb/14.0.0/up04.sql +++ b/schema/crdb/14.0.0/up04.sql @@ -1,4 +1,7 @@ -ALTER TABLE omicron.public.external_ip ADD CONSTRAINT IF NOT EXISTS null_pool_range_id CHECK ( - (ip_pool_id IS NULL AND ip_pool_range_id IS NULL) OR - (ip_pool_id IS NOT NULL AND ip_pool_range_id IS NOT NULL) -); +CREATE UNIQUE INDEX IF NOT EXISTS lookup_floating_ip_by_name_and_project on omicron.public.external_ip ( + project_id, + name +) WHERE + kind = 'floating' AND + time_deleted is NULL AND + project_id is NOT NULL; diff --git a/schema/crdb/14.0.0/up05.sql b/schema/crdb/14.0.0/up05.sql deleted file mode 100644 index 2447899934..0000000000 --- a/schema/crdb/14.0.0/up05.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE omicron.public.external_ip ALTER COLUMN ip_pool_id TYPE UUID; diff --git a/schema/crdb/14.0.0/up06.sql b/schema/crdb/14.0.0/up06.sql deleted file mode 100644 index 844907d1bd..0000000000 --- a/schema/crdb/14.0.0/up06.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE omicron.public.external_ip ALTER COLUMN ip_pool_range_id TYPE UUID; diff --git a/schema/crdb/dbinit.sql b/schema/crdb/dbinit.sql index 3eb07aeede..1c31bcc31b 100644 --- a/schema/crdb/dbinit.sql +++ b/schema/crdb/dbinit.sql @@ -1579,10 +1579,10 @@ CREATE TABLE IF NOT EXISTS omicron.public.external_ip ( time_deleted TIMESTAMPTZ, /* FK to the `ip_pool` table. */ - ip_pool_id UUID, + ip_pool_id UUID NOT NULL, /* FK to the `ip_pool_range` table. */ - ip_pool_range_id UUID, + ip_pool_range_id UUID NOT NULL, /* True if this IP is associated with a service rather than an instance. */ is_service BOOL NOT NULL, @@ -1602,7 +1602,7 @@ CREATE TABLE IF NOT EXISTS omicron.public.external_ip ( /* The last port in the allowed range, also inclusive. */ last_port INT4 NOT NULL, - /* FK to the `ip_pool` table. */ + /* FK to the `project` table. */ project_id UUID, /* The name must be non-NULL iff this is a floating IP. */ @@ -1617,11 +1617,12 @@ CREATE TABLE IF NOT EXISTS omicron.public.external_ip ( (kind = 'floating' AND description IS NOT NULL) ), - /* Only floating IPs can be attached to a project. - * Projects are nullable in such a case. + /* Only floating IPs can be attached to a project, and + * they must have a parent project if they are instance FIPs. */ CONSTRAINT null_project_id CHECK ( - kind = 'floating' OR project_id IS NULL + (kind = 'floating' AND is_service = FALSE AND project_id is NOT NULL) OR + ((kind != 'floating' OR is_service = TRUE) AND project_id IS NULL) ), /* Ephemeral/SNAT IPs must have a parent pool and range, while this @@ -1631,12 +1632,6 @@ CREATE TABLE IF NOT EXISTS omicron.public.external_ip ( kind = 'floating' OR (ip_pool_id IS NOT NULL AND ip_pool_range_id IS NOT NULL) ), - /* If the IP pool is defined, the IP pool range must also be. */ - CONSTRAINT null_pool_range_id CHECK ( - (ip_pool_id IS NULL AND ip_pool_range_id IS NULL) OR - (ip_pool_id IS NOT NULL AND ip_pool_range_id IS NOT NULL) - ), - /* * Only nullable if this is a floating IP, which may exist not * attached to any instance or service yet. @@ -1680,6 +1675,23 @@ CREATE UNIQUE INDEX IF NOT EXISTS lookup_external_ip_by_parent ON omicron.public ) WHERE parent_id IS NOT NULL AND time_deleted IS NULL; +/* Enforce name-uniqueness of floating (service) IPs at fleet level. */ +CREATE UNIQUE INDEX IF NOT EXISTS lookup_floating_ip_by_name on omicron.public.external_ip ( + name +) WHERE + kind = 'floating' AND + time_deleted is NULL AND + project_id is NULL; + +/* Enforce name-uniqueness of floating IPs at project level. */ +CREATE UNIQUE INDEX IF NOT EXISTS lookup_floating_ip_by_name_and_project on omicron.public.external_ip ( + project_id, + name +) WHERE + kind = 'floating' AND + time_deleted is NULL AND + project_id is NOT NULL; + /*******************************************************************/ /*