Skip to content
Merged
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
128 changes: 72 additions & 56 deletions nexus/src/app/external_subnet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,28 +7,21 @@
//! External subnets are similar to floating IPs but allocate entire subnets
//! rather than individual IP addresses. They can be attached to instances
//! to provide external connectivity.
//!
//! TODO(#9453): This module contains stub implementations that return
//! "not implemented" errors. Full implementation requires:
//! - Database schema and models (see nexus/db-model/)
//! - Datastore methods (see nexus/db-queries/src/db/datastore/)
//! - Authorization resources (see nexus/auth/src/authz/)
//! - Sagas for attach/detach operations
//! - Replacing these stubs with real implementations

use crate::app::Unimpl;
use crate::app::sagas::subnet_attach;
use crate::app::sagas::subnet_detach;
use nexus_auth::authn;
use nexus_db_lookup::LookupPath;
use nexus_db_lookup::lookup;
use nexus_db_queries::authz;
use nexus_db_queries::context::OpContext;
use nexus_types::external_api::{params, views};
use omicron_common::api::external::DeleteResult;
use omicron_common::api::external::Error;
use omicron_common::api::external::InternalContext as _;
use omicron_common::api::external::ListResultVec;
use omicron_common::api::external::LookupResult;
use omicron_common::api::external::LookupType;
use omicron_common::api::external::NameOrId;
use omicron_common::api::external::ResourceType;
use omicron_common::api::external::UpdateResult;
use omicron_common::api::external::http_pagination::PaginatedBy;
use omicron_uuid_kinds::ExternalSubnetUuid;
Expand Down Expand Up @@ -140,63 +133,86 @@ impl super::Nexus {
self.datastore().delete_external_subnet(opctx, &authz_subnet).await
}

// TODO-remove: This is a temporary method to ensure we continue to fail
// reliably for the methods below that remain unimplemented.
fn external_subnet_lookup_not_found(
&self,
selector: params::ExternalSubnetSelector,
) -> LookupResult<()> {
let lookup_type = match selector {
params::ExternalSubnetSelector {
external_subnet: NameOrId::Id(id),
project: None,
} => LookupType::ById(id),
params::ExternalSubnetSelector {
external_subnet: NameOrId::Name(name),
project: Some(_),
} => LookupType::ByName(name.to_string()),
params::ExternalSubnetSelector {
external_subnet: NameOrId::Id(_),
..
} => {
return Err(Error::invalid_request(
"when providing external subnet as an ID \
project should not be specified",
));
}
_ => {
return Err(Error::invalid_request(
"external subnet should either be a UUID or \
project should be specified",
));
}
};
Err(lookup_type.into_not_found(ResourceType::ExternalSubnet))
}

pub(crate) async fn external_subnet_attach(
&self,
opctx: &OpContext,
selector: params::ExternalSubnetSelector,
_attach: params::ExternalSubnetAttach,
attach: params::ExternalSubnetAttach,
) -> UpdateResult<views::ExternalSubnet> {
let not_found =
self.external_subnet_lookup_not_found(selector).unwrap_err();
Err(self
.unimplemented_todo(opctx, Unimpl::ProtectedLookup(not_found))
.await)
let (.., authz_project, authz_subnet, db_subnet) = self
.external_subnet_lookup(opctx, selector)?
.fetch_for(authz::Action::Modify)
.await?;
let instance_selector = match &attach.instance {
NameOrId::Id(id) => params::InstanceSelector {
project: None,
instance: NameOrId::Id(*id),
},
NameOrId::Name(name) => params::InstanceSelector {
project: Some(NameOrId::Id(authz_project.id())),
instance: NameOrId::Name(name.clone()),
},
};
let (.., authz_instance, db_instance) = self
.instance_lookup(opctx, instance_selector)?
.fetch_for(authz::Action::Modify)
.await?;
if db_instance.project_id != db_subnet.project_id {
return Err(Error::invalid_request(
"External subnet and instance must be in the same project",
));
}
let params = subnet_attach::Params {
authz_subnet,
ip_version: db_subnet.subnet.ip_version(),
authz_instance,
serialized_authn: authn::saga::Serialized::for_opctx(opctx),
};
let output = self
.sagas
.saga_execute::<subnet_attach::SagaSubnetAttach>(params)
.await?;
output
.lookup_node_output::<views::ExternalSubnet>("output")
.map_err(|e| Error::internal_error(&format!("{e:#}")))
.internal_context("looking up output from subnet attach saga")
}

pub(crate) async fn external_subnet_detach(
&self,
opctx: &OpContext,
selector: params::ExternalSubnetSelector,
) -> UpdateResult<views::ExternalSubnet> {
let not_found =
self.external_subnet_lookup_not_found(selector).unwrap_err();
Err(self
.unimplemented_todo(opctx, Unimpl::ProtectedLookup(not_found))
.await)
let (.., authz_subnet, db_subnet) = self
.external_subnet_lookup(opctx, selector)?
.fetch_for(authz::Action::Modify)
.await?;
let Some(instance_id) = &db_subnet.instance_id else {
return Err(Error::invalid_request(
"External subnet is not attached to an instance",
));
};
let instance_selector = params::InstanceSelector {
project: None,
instance: NameOrId::Id(instance_id.into_untyped_uuid()),
};
let (.., authz_instance, _db_instance) = self
.instance_lookup(opctx, instance_selector)?
.fetch_for(authz::Action::Modify)
.await?;
let params = subnet_detach::Params {
authz_instance,
authz_subnet,
serialized_authn: authn::saga::Serialized::for_opctx(opctx),
};
let output = self
.sagas
.saga_execute::<subnet_detach::SagaSubnetDetach>(params)
.await?;
output
.lookup_node_output::<views::ExternalSubnet>("output")
.map_err(|e| Error::internal_error(&format!("{e:#}")))
.internal_context("looking up output from subnet detach saga")
}

pub(crate) async fn instance_list_external_subnets(
Expand Down
Loading
Loading