Skip to content

Commit

Permalink
first pass at list IP pools for silo endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
david-crespo committed Jan 18, 2024
1 parent 6b20cd2 commit 270133e
Show file tree
Hide file tree
Showing 6 changed files with 244 additions and 10 deletions.
38 changes: 33 additions & 5 deletions nexus/db-queries/src/db/datastore/ip_pool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<'_>,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
17 changes: 15 additions & 2 deletions nexus/src/app/ip_pool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<db::model::IpPool> {
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
Expand All @@ -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,
Expand All @@ -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,
Expand Down
46 changes: 44 additions & 2 deletions nexus/src/external_api/http_entrypoints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)?;
Expand Down Expand Up @@ -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}",
Expand All @@ -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<Arc<ServerContext>>,
path_params: Path<params::SiloPath>,
query_params: Query<PaginatedById>,
) -> Result<HttpResponseOk<ResultsPage<views::SiloIpPool>>, 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.
Expand Down Expand Up @@ -1302,6 +1343,7 @@ async fn project_policy_update(
async fn project_ip_pool_list(
rqctx: RequestContext<Arc<ServerContext>>,
query_params: Query<PaginatedByNameOrId>,
// TODO: have this return SiloIpPool so it has is_default on it
) -> Result<HttpResponseOk<ResultsPage<IpPool>>, HttpError> {
let apictx = rqctx.context();
let handler = async {
Expand All @@ -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)
Expand Down
1 change: 1 addition & 0 deletions nexus/tests/output/nexus_tags.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 16 additions & 0 deletions nexus/types/src/external_api/views.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
136 changes: 135 additions & 1 deletion openapi/nexus.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
{
Expand Down Expand Up @@ -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": [
Expand Down Expand Up @@ -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",
Expand Down

0 comments on commit 270133e

Please sign in to comment.