Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 38 additions & 10 deletions nexus/db-queries/src/db/datastore/ip_pool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,19 +97,26 @@ const INTERNAL_SILO_DEFAULT_CONSTRAINT: &'static str =
const INTERNAL_SILO_DEFAULT_ERROR: &'static str =
"The internal Silo cannot have a default IP Pool";

#[derive(Clone, Copy, Debug, Default)]
pub struct IpPoolListFilters {
pub ip_version: Option<IpVersion>,
pub delegated_for_internal_use: Option<bool>,
}

impl DataStore {
/// List IP Pools
pub async fn ip_pools_list(
&self,
opctx: &OpContext,
pagparams: &PaginatedBy<'_>,
filters: &IpPoolListFilters,
) -> ListResultVec<IpPool> {
use nexus_db_schema::schema::ip_pool;

opctx
.authorize(authz::Action::ListChildren, &authz::IP_POOL_LIST)
.await?;
match pagparams {
let mut query = match pagparams {
PaginatedBy::Id(pagparams) => {
paginated(ip_pool::table, ip_pool::id, pagparams)
}
Expand All @@ -118,14 +125,34 @@ impl DataStore {
ip_pool::name,
&pagparams.map_name(|n| Name::ref_cast(n)),
),
};

query = query.filter(ip_pool::time_deleted.is_null());

if let Some(ip_version) = filters.ip_version {
query = query.filter(ip_pool::ip_version.eq(ip_version));
}
.filter(ip_pool::name.ne(SERVICE_IPV4_POOL_NAME))
.filter(ip_pool::name.ne(SERVICE_IPV6_POOL_NAME))
.filter(ip_pool::time_deleted.is_null())
.select(IpPool::as_select())
.get_results_async(&*self.pool_connection_authorized(opctx).await?)
.await
.map_err(|e| public_error_from_diesel(e, ErrorHandler::Server))

match filters.delegated_for_internal_use {
Some(true) => {
query = query.filter(
ip_pool::name
.eq(SERVICE_IPV4_POOL_NAME)
.or(ip_pool::name.eq(SERVICE_IPV6_POOL_NAME)),
);
}
_ => {
query = query
.filter(ip_pool::name.ne(SERVICE_IPV4_POOL_NAME))
.filter(ip_pool::name.ne(SERVICE_IPV6_POOL_NAME));
}
}

query
.select(IpPool::as_select())
.get_results_async(&*self.pool_connection_authorized(opctx).await?)
.await
.map_err(|e| public_error_from_diesel(e, ErrorHandler::Server))
}

/// Look up whether the given pool is available to users in the current
Expand Down Expand Up @@ -1317,6 +1344,7 @@ impl DataStore {
mod test {
use std::num::NonZeroU32;

use super::IpPoolListFilters;
use crate::authz;
use crate::db::datastore::ip_pool::INTERNAL_SILO_DEFAULT_ERROR;
use crate::db::model::{
Expand Down Expand Up @@ -1353,7 +1381,7 @@ mod test {
let pagbyid = PaginatedBy::Id(pagparams_id);

let all_pools = datastore
.ip_pools_list(&opctx, &pagbyid)
.ip_pools_list(&opctx, &pagbyid, &IpPoolListFilters::default())
.await
.expect("Should list IP pools");
assert_eq!(all_pools.len(), 0);
Expand Down Expand Up @@ -1381,7 +1409,7 @@ mod test {

// shows up in full list but not silo list
let all_pools = datastore
.ip_pools_list(&opctx, &pagbyid)
.ip_pools_list(&opctx, &pagbyid, &IpPoolListFilters::default())
.await
.expect("Should list IP pools");
assert_eq!(all_pools.len(), 1);
Expand Down
1 change: 1 addition & 0 deletions nexus/db-queries/src/db/datastore/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ pub use instance::{
InstanceAndActiveVmm, InstanceGestalt, InstanceStateComputer,
};
pub use inventory::DataStoreInventoryTest;
pub use ip_pool::IpPoolListFilters;
use nexus_db_model::AllSchemaVersions;
use nexus_types::internal_api::views::HeldDbClaimInfo;
pub use oximeter::CollectorReassignment;
Expand Down
2 changes: 1 addition & 1 deletion nexus/external-api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -708,7 +708,7 @@ pub trait NexusExternalApi {
}]
async fn ip_pool_list(
rqctx: RequestContext<Self::Context>,
query_params: Query<PaginatedByNameOrId>,
query_params: Query<PaginatedByNameOrId<params::IpPoolListSelector>>,
) -> Result<HttpResponseOk<ResultsPage<views::IpPool>>, HttpError>;

/// Create IP pool
Expand Down
8 changes: 7 additions & 1 deletion nexus/src/app/ip_pool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use nexus_db_queries::authz;
use nexus_db_queries::authz::ApiResource;
use nexus_db_queries::context::OpContext;
use nexus_db_queries::db;
use nexus_db_queries::db::datastore::IpPoolListFilters;
use nexus_db_queries::db::model::Name;
use nexus_types::identity::Resource;
use omicron_common::api::external::CreateResult;
Expand Down Expand Up @@ -249,8 +250,13 @@ impl super::Nexus {
&self,
opctx: &OpContext,
pagparams: &PaginatedBy<'_>,
selector: &params::IpPoolListSelector,
) -> ListResultVec<db::model::IpPool> {
self.db_datastore.ip_pools_list(opctx, pagparams).await
let filters = IpPoolListFilters {
ip_version: selector.ip_version.map(Into::into),
delegated_for_internal_use: selector.delegated_for_internal_use,
};
self.db_datastore.ip_pools_list(opctx, pagparams, &filters).await
}

pub(crate) async fn ip_pool_delete(
Expand Down
5 changes: 3 additions & 2 deletions nexus/src/external_api/http_entrypoints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1152,19 +1152,20 @@ impl NexusExternalApi for NexusExternalApiImpl {

async fn ip_pool_list(
rqctx: RequestContext<ApiContext>,
query_params: Query<PaginatedByNameOrId>,
query_params: Query<PaginatedByNameOrId<params::IpPoolListSelector>>,
) -> Result<HttpResponseOk<ResultsPage<IpPool>>, HttpError> {
let apictx = rqctx.context();
let handler = async {
let nexus = &apictx.context.nexus;
let query = query_params.into_inner();
let pag_params = data_page_params_for(&rqctx, &query)?;
let scan_params = ScanByNameOrId::from_query(&query)?;
let filters = scan_params.selector.clone();
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
.ip_pools_list(&opctx, &paginated_by)
.ip_pools_list(&opctx, &paginated_by, &filters)
.await?
.into_iter()
.map(IpPool::from)
Expand Down
94 changes: 91 additions & 3 deletions nexus/tests/integration_tests/ip_pools.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ use http::StatusCode;
use http::method::Method;
use nexus_db_queries::authz;
use nexus_db_queries::context::OpContext;
use nexus_db_queries::db::datastore::SERVICE_IPV4_POOL_NAME;
use nexus_db_queries::db::datastore::{
SERVICE_IPV4_POOL_NAME, SERVICE_IPV6_POOL_NAME,
};
use nexus_db_queries::db::fixed_data::silo::DEFAULT_SILO;
use nexus_test_utils::http_testing::AuthnMode;
use nexus_test_utils::http_testing::NexusRequest;
Expand Down Expand Up @@ -222,18 +224,25 @@ async fn test_ip_pool_basic_crud(cptestctx: &ControlPlaneTestContext) {
.expect("Expected to be able to delete an empty IP Pool");
}

async fn get_ip_pools(client: &ClientTestContext) -> Vec<IpPool> {
async fn get_ip_pools_with_params(
client: &ClientTestContext,
params: &str,
) -> Vec<IpPool> {
NexusRequest::iter_collection_authn::<IpPool>(
client,
"/v1/system/ip-pools",
"",
params,
None,
)
.await
.expect("Failed to list IP Pools")
.all_items
}

async fn get_ip_pools(client: &ClientTestContext) -> Vec<IpPool> {
get_ip_pools_with_params(client, "").await
}

// this test exists primarily because of a bug in the initial implementation
// where we included a duplicate of each pool in the list response for every
// associated silo
Expand Down Expand Up @@ -706,6 +715,85 @@ async fn test_ip_pool_update_default(cptestctx: &ControlPlaneTestContext) {
assert_eq!(silos_p1.items[0].is_default, false);
}

#[nexus_test]
async fn test_ip_pool_list_filter_delegated(
cptestctx: &ControlPlaneTestContext,
) {
let client = &cptestctx.external_client;

let (user_pool, ..) = create_ip_pool(client, "pool-filter", None).await;

let default_pools = get_ip_pools(client).await;
assert!(
default_pools
.iter()
.any(|pool| pool.identity.id == user_pool.identity.id)
);

let delegated_only =
get_ip_pools_with_params(client, "delegated_for_internal_use=true")
.await;
assert!(
delegated_only
.iter()
.any(|pool| pool.identity.name == SERVICE_IPV4_POOL_NAME)
);
assert!(
delegated_only
.iter()
.any(|pool| pool.identity.name == SERVICE_IPV6_POOL_NAME)
);
assert!(
delegated_only
.iter()
.all(|pool| pool.identity.id != user_pool.identity.id)
);

let non_delegated =
get_ip_pools_with_params(client, "delegated_for_internal_use=false")
.await;
assert!(
non_delegated
.iter()
.any(|pool| pool.identity.id == user_pool.identity.id)
);
assert!(non_delegated.iter().all(|pool| {
pool.identity.name != SERVICE_IPV4_POOL_NAME
&& pool.identity.name != SERVICE_IPV6_POOL_NAME
}));
}

#[nexus_test]
async fn test_ip_pool_list_filter_ip_version(
cptestctx: &ControlPlaneTestContext,
) {
let client = &cptestctx.external_client;

let (user_pool, ..) = create_ip_pool(client, "pool-filter-v4", None).await;

let v4_pools = get_ip_pools_with_params(
client,
"ip_version=v4&delegated_for_internal_use=false",
)
.await;
assert!(v4_pools.iter().all(|pool| pool.ip_version == IpVersion::V4));
assert!(
v4_pools.iter().any(|pool| pool.identity.id == user_pool.identity.id)
);

let delegated_v6 = get_ip_pools_with_params(
client,
"delegated_for_internal_use=true&ip_version=v6",
)
.await;
assert!(delegated_v6.iter().all(|pool| pool.ip_version == IpVersion::V6));
assert!(
delegated_v6
.iter()
.any(|pool| pool.identity.name == SERVICE_IPV6_POOL_NAME)
);
}

// IP pool list fetch logic includes a join to ip_pool_resource, which is
// unusual, so we want to make sure pagination logic still works
#[nexus_test]
Expand Down
16 changes: 16 additions & 0 deletions nexus/types/src/external_api/params.rs
Original file line number Diff line number Diff line change
Expand Up @@ -997,6 +997,22 @@ impl std::fmt::Debug for CertificateCreate {

// IP POOLS

/// Filters for listing IP pools.
#[derive(
Clone, Debug, Default, Deserialize, Serialize, JsonSchema, PartialEq,
)]
pub struct IpPoolListSelector {
/// Restrict pools to a specific IP version.
#[serde(default)]
pub ip_version: Option<IpVersion>,

/// Filter on pools delegated for internal Oxide use.
///
/// Defaults to excluding internal pools when unset.
#[serde(default)]
pub delegated_for_internal_use: Option<bool>,
}

/// Create-time parameters for an `IpPool`
#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)]
pub struct IpPoolCreate {
Expand Down
17 changes: 17 additions & 0 deletions openapi/nexus.json
Original file line number Diff line number Diff line change
Expand Up @@ -8249,6 +8249,23 @@
"summary": "List IP pools",
"operationId": "ip_pool_list",
"parameters": [
{
"in": "query",
"name": "delegated_for_internal_use",
"description": "Filter on pools delegated for internal Oxide use.\n\nDefaults to excluding internal pools when unset.",
"schema": {
"nullable": true,
"type": "boolean"
}
},
{
"in": "query",
"name": "ip_version",
"description": "Restrict pools to a specific IP version.",
"schema": {
"$ref": "#/components/schemas/IpVersion"
}
},
{
"in": "query",
"name": "limit",
Expand Down