From 270133ea95166b0edc8efd1585b061ba9121f55e Mon Sep 17 00:00:00 2001 From: David Crespo Date: Thu, 18 Jan 2024 11:44:24 -0600 Subject: [PATCH] first pass at list IP pools for silo endpoint --- nexus/db-queries/src/db/datastore/ip_pool.rs | 38 +++++- nexus/src/app/ip_pool.rs | 17 ++- nexus/src/external_api/http_entrypoints.rs | 46 ++++++- nexus/tests/output/nexus_tags.txt | 1 + nexus/types/src/external_api/views.rs | 16 +++ openapi/nexus.json | 136 ++++++++++++++++++- 6 files changed, 244 insertions(+), 10 deletions(-) diff --git a/nexus/db-queries/src/db/datastore/ip_pool.rs b/nexus/db-queries/src/db/datastore/ip_pool.rs index c9fdb5f0ee..a7f715771d 100644 --- a/nexus/db-queries/src/db/datastore/ip_pool.rs +++ b/nexus/db-queries/src/db/datastore/ip_pool.rs @@ -80,7 +80,7 @@ impl DataStore { } /// List IP pools linked to the current silo - pub async fn silo_ip_pools_list( + pub async fn current_silo_ip_pool_list( &self, opctx: &OpContext, pagparams: &PaginatedBy<'_>, @@ -400,6 +400,34 @@ impl DataStore { .map_err(|e| public_error_from_diesel(e, ErrorHandler::Server)) } + /// Returns (IpPool, IpPoolResource) so we can know in the calling code + /// whether the pool is default for the silo + pub async fn silo_ip_pool_list( + &self, + opctx: &OpContext, + authz_silo: &authz::Silo, + pagparams: &DataPageParams<'_, Uuid>, + ) -> ListResultVec<(IpPool, IpPoolResource)> { + use db::schema::ip_pool; + use db::schema::ip_pool_resource; + + paginated( + ip_pool_resource::table, + ip_pool_resource::ip_pool_id, + pagparams, + ) + .inner_join(ip_pool::table) + .filter(ip_pool_resource::resource_id.eq(authz_silo.id())) + .filter(ip_pool_resource::resource_type.eq(IpPoolResourceType::Silo)) + .filter(ip_pool::time_deleted.is_null()) + .select(<(IpPool, IpPoolResource)>::as_select()) + .load_async::<(IpPool, IpPoolResource)>( + &*self.pool_connection_authorized(opctx).await?, + ) + .await + .map_err(|e| public_error_from_diesel(e, ErrorHandler::Server)) + } + pub async fn ip_pool_link_silo( &self, opctx: &OpContext, @@ -868,7 +896,7 @@ mod test { .expect("Should list IP pools"); assert_eq!(all_pools.len(), 0); let silo_pools = datastore - .silo_ip_pools_list(&opctx, &pagbyid) + .current_silo_ip_pool_list(&opctx, &pagbyid) .await .expect("Should list silo IP pools"); assert_eq!(silo_pools.len(), 0); @@ -893,7 +921,7 @@ mod test { .expect("Should list IP pools"); assert_eq!(all_pools.len(), 1); let silo_pools = datastore - .silo_ip_pools_list(&opctx, &pagbyid) + .current_silo_ip_pool_list(&opctx, &pagbyid) .await .expect("Should list silo IP pools"); assert_eq!(silo_pools.len(), 0); @@ -929,7 +957,7 @@ mod test { // now it shows up in the silo list let silo_pools = datastore - .silo_ip_pools_list(&opctx, &pagbyid) + .current_silo_ip_pool_list(&opctx, &pagbyid) .await .expect("Should list silo IP pools"); assert_eq!(silo_pools.len(), 1); @@ -998,7 +1026,7 @@ mod test { // and silo pools list is empty again let silo_pools = datastore - .silo_ip_pools_list(&opctx, &pagbyid) + .current_silo_ip_pool_list(&opctx, &pagbyid) .await .expect("Should list silo IP pools"); assert_eq!(silo_pools.len(), 0); diff --git a/nexus/src/app/ip_pool.rs b/nexus/src/app/ip_pool.rs index 1d9b3e515e..876728fa4c 100644 --- a/nexus/src/app/ip_pool.rs +++ b/nexus/src/app/ip_pool.rs @@ -74,12 +74,12 @@ impl super::Nexus { } /// List IP pools in current silo - pub(crate) async fn silo_ip_pools_list( + pub(crate) async fn current_silo_ip_pool_list( &self, opctx: &OpContext, pagparams: &PaginatedBy<'_>, ) -> ListResultVec { - self.db_datastore.silo_ip_pools_list(opctx, pagparams).await + self.db_datastore.current_silo_ip_pool_list(opctx, pagparams).await } // Look up pool by name or ID, but only return it if it's linked to the @@ -101,6 +101,7 @@ impl super::Nexus { Ok(pool) } + /// List silos for a given pool pub(crate) async fn ip_pool_silo_list( &self, opctx: &OpContext, @@ -112,6 +113,18 @@ impl super::Nexus { self.db_datastore.ip_pool_silo_list(opctx, &authz_pool, pagparams).await } + // List pools for a given silo + pub(crate) async fn silo_ip_pool_list( + &self, + opctx: &OpContext, + silo_lookup: &lookup::Silo<'_>, + pagparams: &DataPageParams<'_, Uuid>, + ) -> ListResultVec<(db::model::IpPool, db::model::IpPoolResource)> { + let (.., authz_silo) = + silo_lookup.lookup_for(authz::Action::Read).await?; + self.db_datastore.silo_ip_pool_list(opctx, &authz_silo, pagparams).await + } + pub(crate) async fn ip_pool_link_silo( &self, opctx: &OpContext, diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index 21acb45ed3..ca569553fe 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -279,6 +279,7 @@ pub(crate) fn external_api() -> NexusApiDescription { api.register(silo_delete)?; api.register(silo_policy_view)?; api.register(silo_policy_update)?; + api.register(silo_ip_pool_list)?; api.register(silo_utilization_view)?; api.register(silo_utilization_list)?; @@ -741,7 +742,7 @@ async fn silo_create( /// Fetch a silo /// -/// Fetch a silo by name. +/// Fetch a silo by name or ID. #[endpoint { method = GET, path = "/v1/system/silos/{silo}", @@ -763,6 +764,46 @@ async fn silo_view( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } +/// List IP pools available within silo +#[endpoint { + method = GET, + path = "/v1/system/silos/{silo}/ip-pools", + tags = ["system/silos"], +}] +async fn silo_ip_pool_list( + rqctx: RequestContext>, + path_params: Path, + query_params: Query, +) -> Result>, HttpError> { + 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 pag_params = data_page_params_for(&rqctx, &query)?; + + let silo_lookup = nexus.silo_lookup(&opctx, path.silo)?; + let pools = nexus + .silo_ip_pool_list(&opctx, &silo_lookup, &pag_params) + .await? + .iter() + .map(|(pool, silo_link)| views::SiloIpPool { + identity: pool.identity(), + is_default: silo_link.is_default, + }) + .collect(); + + Ok(HttpResponseOk(ScanById::results_page( + &query, + pools, + &|_, pool: &views::SiloIpPool| pool.identity.id, + )?)) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + /// Delete a silo /// /// Delete a silo by name. @@ -1302,6 +1343,7 @@ async fn project_policy_update( async fn project_ip_pool_list( rqctx: RequestContext>, query_params: Query, + // TODO: have this return SiloIpPool so it has is_default on it ) -> Result>, HttpError> { let apictx = rqctx.context(); let handler = async { @@ -1312,7 +1354,7 @@ async fn project_ip_pool_list( let paginated_by = name_or_id_pagination(&pag_params, scan_params)?; let opctx = crate::context::op_context_for_external_api(&rqctx).await?; let pools = nexus - .silo_ip_pools_list(&opctx, &paginated_by) + .current_silo_ip_pool_list(&opctx, &paginated_by) .await? .into_iter() .map(IpPool::from) diff --git a/nexus/tests/output/nexus_tags.txt b/nexus/tests/output/nexus_tags.txt index 2d842dd930..bd79a9c3e9 100644 --- a/nexus/tests/output/nexus_tags.txt +++ b/nexus/tests/output/nexus_tags.txt @@ -185,6 +185,7 @@ saml_identity_provider_view GET /v1/system/identity-providers/ silo_create POST /v1/system/silos silo_delete DELETE /v1/system/silos/{silo} silo_identity_provider_list GET /v1/system/identity-providers +silo_ip_pool_list GET /v1/system/silos/{silo}/ip-pools silo_list GET /v1/system/silos silo_policy_update PUT /v1/system/silos/{silo}/policy silo_policy_view GET /v1/system/silos/{silo}/policy diff --git a/nexus/types/src/external_api/views.rs b/nexus/types/src/external_api/views.rs index cf312d3b82..cc0e8c62c1 100644 --- a/nexus/types/src/external_api/views.rs +++ b/nexus/types/src/external_api/views.rs @@ -303,6 +303,22 @@ pub struct IpPool { pub identity: IdentityMetadata, } +/// A collection of IP ranges. If a pool is linked to a silo, IP addresses from +/// the pool can be allocated within that silo +#[derive(ObjectIdentity, Clone, Debug, Deserialize, Serialize, JsonSchema)] +pub struct SiloIpPool { + #[serde(flatten)] + pub identity: IdentityMetadata, + + /// When a pool is the default for a silo, floating IPs and instance + /// ephemeral IPs will come from that pool when no other pool is specified. + /// There can be at most one default for a given silo. + pub is_default: bool, +} + +// TODO: rename IpPoolSilo or get rid of it somehow. we cannot have both +// IpPoolSilo and SiloIpPool. come on + /// A link between an IP pool and a silo that allows one to allocate IPs from /// the pool within the silo #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, PartialEq)] diff --git a/openapi/nexus.json b/openapi/nexus.json index a4ba6cbb86..21d9a98f51 100644 --- a/openapi/nexus.json +++ b/openapi/nexus.json @@ -6580,7 +6580,7 @@ "system/silos" ], "summary": "Fetch a silo", - "description": "Fetch a silo by name.", + "description": "Fetch a silo by name or ID.", "operationId": "silo_view", "parameters": [ { @@ -6643,6 +6643,74 @@ } } }, + "/v1/system/silos/{silo}/ip-pools": { + "get": { + "tags": [ + "system/silos" + ], + "summary": "List IP pools available within silo", + "operationId": "silo_ip_pool_list", + "parameters": [ + { + "in": "path", + "name": "silo", + "description": "Name or ID of the silo", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/IdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SiloIpPoolResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + } + }, "/v1/system/silos/{silo}/policy": { "get": { "tags": [ @@ -13802,6 +13870,72 @@ } ] }, + "SiloIpPool": { + "description": "A collection of IP ranges. If a pool is linked to a silo, IP addresses from the pool can be allocated within that silo", + "type": "object", + "properties": { + "description": { + "description": "human-readable free-form text about a resource", + "type": "string" + }, + "id": { + "description": "unique, immutable, system-controlled identifier for each resource", + "type": "string", + "format": "uuid" + }, + "is_default": { + "description": "When a pool is the default for a silo, floating IPs and instance ephemeral IPs will come from that pool when no other pool is specified. There can be at most one default for a given silo.", + "type": "boolean" + }, + "name": { + "description": "unique, mutable, user-controlled identifier for each resource", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "time_created": { + "description": "timestamp when this resource was created", + "type": "string", + "format": "date-time" + }, + "time_modified": { + "description": "timestamp when this resource was last modified", + "type": "string", + "format": "date-time" + } + }, + "required": [ + "description", + "id", + "is_default", + "name", + "time_created", + "time_modified" + ] + }, + "SiloIpPoolResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/SiloIpPool" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, "SiloQuotas": { "description": "A collection of resource counts used to set the virtual capacity of a silo", "type": "object",