Skip to content

Commit

Permalink
Add API endpoints for retrieving IP pools available to projects (oxid…
Browse files Browse the repository at this point in the history
  • Loading branch information
lif committed Jun 7, 2023
1 parent 36faef6 commit c47beeb
Show file tree
Hide file tree
Showing 11 changed files with 473 additions and 7 deletions.
29 changes: 29 additions & 0 deletions nexus/db-queries/src/db/datastore/project.rs
Original file line number Diff line number Diff line change
Expand Up @@ -329,4 +329,33 @@ impl DataStore {
)
})
}

/// List IP Pools accessible to a project
pub async fn project_ip_pools_list(
&self,
opctx: &OpContext,
authz_project: &authz::Project,
pagparams: &PaginatedBy<'_>,
) -> ListResultVec<db::model::IpPool> {
use db::schema::ip_pool::dsl;
opctx.authorize(authz::Action::ListChildren, authz_project).await?;
match pagparams {
PaginatedBy::Id(pagparams) => {
paginated(dsl::ip_pool, dsl::id, pagparams)
}
PaginatedBy::Name(pagparams) => paginated(
dsl::ip_pool,
dsl::name,
&pagparams.map_name(|n| Name::ref_cast(n)),
),
}
// TODO(2148, 2056): filter only pools accessible by the given
// project, once specific projects for pools are implemented
.filter(dsl::internal.eq(false))
.filter(dsl::time_deleted.is_null())
.select(db::model::IpPool::as_select())
.get_results_async(self.pool_authorized(opctx).await?)
.await
.map_err(|e| public_error_from_diesel_pool(e, ErrorHandler::Server))
}
}
38 changes: 38 additions & 0 deletions nexus/src/app/project.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use crate::db::lookup::LookupPath;
use crate::external_api::params;
use crate::external_api::shared;
use anyhow::Context;
use nexus_db_model::Name;
use nexus_db_queries::context::OpContext;
use omicron_common::api::external::http_pagination::PaginatedBy;
use omicron_common::api::external::CreateResult;
Expand All @@ -23,6 +24,7 @@ use omicron_common::api::external::ListResultVec;
use omicron_common::api::external::LookupResult;
use omicron_common::api::external::NameOrId;
use omicron_common::api::external::UpdateResult;
use ref_cast::RefCast;
use std::sync::Arc;

impl super::Nexus {
Expand Down Expand Up @@ -147,4 +149,40 @@ impl super::Nexus {
.collect::<Result<Vec<_>, _>>()?;
Ok(shared::Policy { role_assignments })
}

pub async fn project_ip_pools_list(
&self,
opctx: &OpContext,
project_lookup: &lookup::Project<'_>,
pagparams: &PaginatedBy<'_>,
) -> ListResultVec<db::model::IpPool> {
let (.., authz_project) =
project_lookup.lookup_for(authz::Action::ListChildren).await?;

self.db_datastore
.project_ip_pools_list(opctx, &authz_project, pagparams)
.await
}

pub fn project_ip_pool_lookup<'a>(
&'a self,
opctx: &'a OpContext,
pool: &'a NameOrId,
_project_lookup: &Option<lookup::Project<'_>>,
) -> LookupResult<lookup::IpPool<'a>> {
// TODO(2148, 2056): check that the given project has access (if one
// is provided to the call) once that relation is implemented
match pool {
NameOrId::Name(name) => {
let pool = LookupPath::new(opctx, &self.db_datastore)
.ip_pool_name(Name::ref_cast(name));
Ok(pool)
}
NameOrId::Id(id) => {
let pool =
LookupPath::new(opctx, &self.db_datastore).ip_pool_id(*id);
Ok(pool)
}
}
}
}
79 changes: 79 additions & 0 deletions nexus/src/external_api/http_entrypoints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,10 @@ use dropshot::{
channel, endpoint, WebsocketChannelResult, WebsocketConnection,
};
use ipnetwork::IpNetwork;
use nexus_db_queries::authz::ApiResource;
use nexus_db_queries::db::lookup::ImageLookup;
use nexus_db_queries::db::lookup::ImageParentLookup;
use nexus_types::external_api::params::ProjectSelector;
use nexus_types::{
external_api::views::{SledInstance, Switch},
identity::AssetIdentityMetadata,
Expand Down Expand Up @@ -107,6 +109,8 @@ pub fn external_api() -> NexusApiDescription {
api.register(project_update)?;
api.register(project_policy_view)?;
api.register(project_policy_update)?;
api.register(project_ip_pool_list)?;
api.register(project_ip_pool_view)?;

// Operator-Accessible IP Pools API
api.register(ip_pool_list)?;
Expand Down Expand Up @@ -1074,6 +1078,81 @@ async fn project_policy_update(

// IP Pools

/// List all IP Pools that can be used by a given project.
#[endpoint {
method = GET,
path = "/v1/ip-pools",
tags = ["projects"],
}]
async fn project_ip_pool_list(
rqctx: RequestContext<Arc<ServerContext>>,
query_params: Query<PaginatedByNameOrId<params::ProjectSelector>>,
) -> Result<HttpResponseOk<ResultsPage<IpPool>>, HttpError> {
// Per https://github.com/oxidecomputer/omicron/issues/2148
// This is currently the same list as /v1/system/ip-pools, that is to say,
// IP pools that are *available to* a given project, those being ones that
// are not the internal pools for Oxide service usage. This may change
// in the future as the scoping of pools is further developed, but for now,
// this is literally a near-duplicate of `ip_pool_list`:
let apictx = rqctx.context();
let handler = async {
let nexus = &apictx.nexus;
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 opctx = crate::context::op_context_for_external_api(&rqctx).await?;
let project_lookup =
nexus.project_lookup(&opctx, scan_params.selector.clone())?;
let pools = nexus
.project_ip_pools_list(&opctx, &project_lookup, &paginated_by)
.await?
.into_iter()
.map(IpPool::from)
.collect();
Ok(HttpResponseOk(ScanByNameOrId::results_page(
&query,
pools,
&marker_for_name_or_id,
)?))
};
apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await
}

/// Fetch an IP pool
#[endpoint {
method = GET,
path = "/v1/ip-pools/{pool}",
tags = ["projects"],
}]
async fn project_ip_pool_view(
rqctx: RequestContext<Arc<ServerContext>>,
path_params: Path<params::IpPoolPath>,
project: Query<params::OptionalProjectSelector>,
) -> Result<HttpResponseOk<views::IpPool>, HttpError> {
let apictx = rqctx.context();
let handler = async {
let opctx = crate::context::op_context_for_external_api(&rqctx).await?;
let nexus = &apictx.nexus;
let pool_selector = path_params.into_inner().pool;
let project_lookup = if let Some(project) = project.into_inner().project
{
Some(nexus.project_lookup(&opctx, ProjectSelector { project })?)
} else {
None
};
let (authz_pool, pool) = nexus
.project_ip_pool_lookup(&opctx, &pool_selector, &project_lookup)?
.fetch()
.await?;
if pool.internal {
return Err(authz_pool.not_found().into());
}
Ok(HttpResponseOk(IpPool::from(pool)))
};
apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await
}

/// List IP pools
#[endpoint {
method = GET,
Expand Down
7 changes: 5 additions & 2 deletions nexus/test-utils/src/resource_helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,9 @@ pub async fn create_instance(
instance_name,
&params::InstanceNetworkInterfaceAttachment::Default,
// Disks=
vec![],
Vec::<params::InstanceDiskAttachment>::new(),
// External IPs=
Vec::<params::ExternalIpCreate>::new(),
)
.await
}
Expand All @@ -360,6 +362,7 @@ pub async fn create_instance_with(
instance_name: &str,
nics: &params::InstanceNetworkInterfaceAttachment,
disks: Vec<params::InstanceDiskAttachment>,
external_ips: Vec<params::ExternalIpCreate>,
) -> Instance {
let url = format!("/v1/instances?project={}", project_name);
object_create(
Expand All @@ -377,7 +380,7 @@ pub async fn create_instance_with(
b"#cloud-config\nsystem_info:\n default_user:\n name: oxide"
.to_vec(),
network_interfaces: nics.clone(),
external_ips: vec![],
external_ips,
disks,
start: true,
},
Expand Down
1 change: 1 addition & 0 deletions nexus/tests/integration_tests/disks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1357,6 +1357,7 @@ async fn create_instance_with_disk(client: &ClientTestContext) {
vec![params::InstanceDiskAttachment::Attach(
params::InstanceDiskAttach { name: DISK_NAME.parse().unwrap() },
)],
Vec::<params::ExternalIpCreate>::new(),
)
.await;
}
Expand Down
20 changes: 20 additions & 0 deletions nexus/tests/integration_tests/endpoints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,8 @@ lazy_static! {
};

// IP Pools
pub static ref DEMO_IP_POOLS_PROJ_URL: String =
format!("/v1/ip-pools?project={}", *DEMO_PROJECT_NAME);
pub static ref DEMO_IP_POOLS_URL: &'static str = "/v1/system/ip-pools";
pub static ref DEMO_IP_POOL_NAME: Name = "default".parse().unwrap();
pub static ref DEMO_IP_POOL_CREATE: params::IpPoolCreate =
Expand All @@ -450,6 +452,8 @@ lazy_static! {
description: String::from("an IP pool"),
},
};
pub static ref DEMO_IP_POOL_PROJ_URL: String =
format!("/v1/ip-pools/{}?project={}", *DEMO_IP_POOL_NAME, *DEMO_PROJECT_NAME);
pub static ref DEMO_IP_POOL_URL: String = format!("/v1/system/ip-pools/{}", *DEMO_IP_POOL_NAME);
pub static ref DEMO_IP_POOL_UPDATE: params::IpPoolUpdate =
params::IpPoolUpdate {
Expand Down Expand Up @@ -721,6 +725,14 @@ lazy_static! {
),
],
},
VerifyEndpoint {
url: &DEMO_IP_POOLS_PROJ_URL,
visibility: Visibility::Public,
unprivileged_access: UnprivilegedAccess::ReadOnly,
allowed_methods: vec![
AllowedMethod::Get
],
},

// Single IP Pool endpoint
VerifyEndpoint {
Expand All @@ -735,6 +747,14 @@ lazy_static! {
AllowedMethod::Delete,
],
},
VerifyEndpoint {
url: &DEMO_IP_POOL_PROJ_URL,
visibility: Visibility::Protected,
unprivileged_access: UnprivilegedAccess::ReadOnly,
allowed_methods: vec![
AllowedMethod::Get
],
},

// IP Pool ranges endpoint
VerifyEndpoint {
Expand Down
6 changes: 4 additions & 2 deletions nexus/tests/integration_tests/instances.rs
Original file line number Diff line number Diff line change
Expand Up @@ -522,7 +522,8 @@ async fn test_instance_migrate(cptestctx: &ControlPlaneTestContext) {
PROJECT_NAME,
instance_name,
&params::InstanceNetworkInterfaceAttachment::Default,
vec![],
Vec::<params::InstanceDiskAttachment>::new(),
Vec::<params::ExternalIpCreate>::new(),
)
.await;
let instance_id = instance.identity.id;
Expand Down Expand Up @@ -609,7 +610,8 @@ async fn test_instance_migrate_v2p(cptestctx: &ControlPlaneTestContext) {
&params::InstanceNetworkInterfaceAttachment::Default,
// Omit disks: simulated sled agent assumes that disks are always co-
// located with their instances.
vec![],
Vec::<params::InstanceDiskAttachment>::new(),
Vec::<params::ExternalIpCreate>::new(),
)
.await;
let instance_id = instance.identity.id;
Expand Down
Loading

0 comments on commit c47beeb

Please sign in to comment.