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
1 change: 1 addition & 0 deletions common/src/api/external/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -953,6 +953,7 @@ pub enum ResourceType {
DeviceAccessToken,
DeviceAuthRequest,
Disk,
ExternalSubnet,
Fleet,
FloatingIp,
IdentityProvider,
Expand Down
10 changes: 10 additions & 0 deletions nexus/external-api/output/nexus_tags.txt
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,16 @@ support_bundle_update PUT /experimental/v1/system/suppor
support_bundle_view GET /experimental/v1/system/support-bundles/{bundle_id}
timeseries_query POST /v1/timeseries/query

API operations found with tag "external-subnets"
OPERATION ID METHOD URL PATH
external_subnet_attach POST /v1/external-subnets/{external_subnet}/attach
external_subnet_create POST /v1/external-subnets
external_subnet_delete DELETE /v1/external-subnets/{external_subnet}
external_subnet_detach POST /v1/external-subnets/{external_subnet}/detach
external_subnet_list GET /v1/external-subnets
external_subnet_update PUT /v1/external-subnets/{external_subnet}
external_subnet_view GET /v1/external-subnets/{external_subnet}

API operations found with tag "floating-ips"
OPERATION ID METHOD URL PATH
floating_ip_attach POST /v1/floating-ips/{floating_ip}/attach
Expand Down
107 changes: 107 additions & 0 deletions nexus/external-api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,12 @@ const PUT_UPDATE_REPOSITORY_MAX_BYTES: usize = 4 * GIB;
url = "http://docs.oxide.computer/api/floating-ips"
}
},
"external-subnets" = {
description = "External subnets that can be attached to instances.",
external_docs = {
url = "http://docs.oxide.computer/api/external-subnets"
}
},
"images" = {
description = "Images are read-only virtual disks that may be used to boot virtual machines.",
external_docs = {
Expand Down Expand Up @@ -1452,6 +1458,107 @@ pub trait NexusExternalApi {
path_params: Path<params::SubnetPoolPath>,
) -> Result<HttpResponseOk<views::SubnetPoolUtilization>, HttpError>;

// External Subnets

/// List external subnets in a project
#[endpoint {
method = GET,
path = "/v1/external-subnets",
tags = ["external-subnets"],
versions = VERSION_EXTERNAL_SUBNET_ATTACHMENT..,
}]
async fn external_subnet_list(
rqctx: RequestContext<Self::Context>,
query_params: Query<PaginatedByNameOrId<params::ProjectSelector>>,
) -> Result<HttpResponseOk<ResultsPage<views::ExternalSubnet>>, HttpError>;

/// Create an external subnet
#[endpoint {
method = POST,
path = "/v1/external-subnets",
tags = ["external-subnets"],
versions = VERSION_EXTERNAL_SUBNET_ATTACHMENT..,
}]
async fn external_subnet_create(
rqctx: RequestContext<Self::Context>,
query_params: Query<params::ProjectSelector>,
subnet_params: TypedBody<params::ExternalSubnetCreate>,
) -> Result<HttpResponseCreated<views::ExternalSubnet>, HttpError>;

/// Fetch an external subnet
#[endpoint {
method = GET,
path = "/v1/external-subnets/{external_subnet}",
tags = ["external-subnets"],
versions = VERSION_EXTERNAL_SUBNET_ATTACHMENT..,
}]
async fn external_subnet_view(
rqctx: RequestContext<Self::Context>,
path_params: Path<params::ExternalSubnetPath>,
query_params: Query<params::OptionalProjectSelector>,
) -> Result<HttpResponseOk<views::ExternalSubnet>, HttpError>;

/// Update an external subnet
#[endpoint {
method = PUT,
path = "/v1/external-subnets/{external_subnet}",
tags = ["external-subnets"],
versions = VERSION_EXTERNAL_SUBNET_ATTACHMENT..,
}]
async fn external_subnet_update(
rqctx: RequestContext<Self::Context>,
path_params: Path<params::ExternalSubnetPath>,
query_params: Query<params::OptionalProjectSelector>,
subnet_params: TypedBody<params::ExternalSubnetUpdate>,
) -> Result<HttpResponseOk<views::ExternalSubnet>, HttpError>;

/// Delete an external subnet
#[endpoint {
method = DELETE,
path = "/v1/external-subnets/{external_subnet}",
tags = ["external-subnets"],
versions = VERSION_EXTERNAL_SUBNET_ATTACHMENT..,
}]
async fn external_subnet_delete(
rqctx: RequestContext<Self::Context>,
path_params: Path<params::ExternalSubnetPath>,
query_params: Query<params::OptionalProjectSelector>,
) -> Result<HttpResponseDeleted, HttpError>;

/// Attach an external subnet to an instance
///
/// Begins an asynchronous attach operation. Returns the subnet with
/// `instance_id` set to the target instance. The attach completes
/// asynchronously; poll the subnet to check completion.
#[endpoint {
method = POST,
path = "/v1/external-subnets/{external_subnet}/attach",
tags = ["external-subnets"],
versions = VERSION_EXTERNAL_SUBNET_ATTACHMENT..,
}]
async fn external_subnet_attach(
rqctx: RequestContext<Self::Context>,
path_params: Path<params::ExternalSubnetPath>,
query_params: Query<params::OptionalProjectSelector>,
attach_params: TypedBody<params::ExternalSubnetAttach>,
) -> Result<HttpResponseAccepted<views::ExternalSubnet>, HttpError>;

/// Detach an external subnet from an instance
///
/// Begins an asynchronous detach operation. Returns the subnet with
/// `instance_id` cleared. The detach completes asynchronously.
#[endpoint {
method = POST,
path = "/v1/external-subnets/{external_subnet}/detach",
tags = ["external-subnets"],
versions = VERSION_EXTERNAL_SUBNET_ATTACHMENT..,
}]
async fn external_subnet_detach(
rqctx: RequestContext<Self::Context>,
path_params: Path<params::ExternalSubnetPath>,
query_params: Query<params::OptionalProjectSelector>,
) -> Result<HttpResponseAccepted<views::ExternalSubnet>, HttpError>;

// Floating IP Addresses

/// List floating IPs
Expand Down
153 changes: 153 additions & 0 deletions nexus/src/app/external_subnet.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

//! External Subnets, project-scoped resources allocated from subnet pools
//!
//! 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 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::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;

impl super::Nexus {
/// Look up an external subnet by selector.
///
/// TODO: This is a stub that always returns a not-found error. The real
/// implementation should match other selector lookup functions (see
/// `disk_lookup`, `vpc_lookup`, `floating_ip_lookup`, etc.):
///
/// - Add lifetime parameter `'a` to the function
/// - Add `opctx: &'a OpContext` parameter
/// - Return `LookupResult<lookup::ExternalSubnet<'a>>`
/// - Return `Ok(lookup_handle)` on success instead of `Err`
fn external_subnet_lookup(
&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_list(
&self,
opctx: &OpContext,
project_lookup: &lookup::Project<'_>,
_pagparams: &PaginatedBy<'_>,
) -> ListResultVec<views::ExternalSubnet> {
let _ = project_lookup.lookup_for(authz::Action::ListChildren).await?;
Err(self.unimplemented_todo(opctx, Unimpl::Public).await)
}

pub(crate) async fn external_subnet_create(
&self,
opctx: &OpContext,
project_lookup: &lookup::Project<'_>,
_params: params::ExternalSubnetCreate,
) -> Result<views::ExternalSubnet, Error> {
let _ = project_lookup.lookup_for(authz::Action::CreateChild).await?;
Err(self.unimplemented_todo(opctx, Unimpl::Public).await)
}

pub(crate) async fn external_subnet_view(
&self,
opctx: &OpContext,
selector: params::ExternalSubnetSelector,
) -> LookupResult<views::ExternalSubnet> {
let not_found = self.external_subnet_lookup(selector).unwrap_err();
Err(self
.unimplemented_todo(opctx, Unimpl::ProtectedLookup(not_found))
.await)
}

pub(crate) async fn external_subnet_update(
&self,
opctx: &OpContext,
selector: params::ExternalSubnetSelector,
_params: params::ExternalSubnetUpdate,
) -> UpdateResult<views::ExternalSubnet> {
let not_found = self.external_subnet_lookup(selector).unwrap_err();
Err(self
.unimplemented_todo(opctx, Unimpl::ProtectedLookup(not_found))
.await)
}

pub(crate) async fn external_subnet_delete(
&self,
opctx: &OpContext,
selector: params::ExternalSubnetSelector,
) -> DeleteResult {
let not_found = self.external_subnet_lookup(selector).unwrap_err();
Err(self
.unimplemented_todo(opctx, Unimpl::ProtectedLookup(not_found))
.await)
}

pub(crate) async fn external_subnet_attach(
&self,
opctx: &OpContext,
selector: params::ExternalSubnetSelector,
_attach: params::ExternalSubnetAttach,
) -> UpdateResult<views::ExternalSubnet> {
let not_found = self.external_subnet_lookup(selector).unwrap_err();
Err(self
.unimplemented_todo(opctx, Unimpl::ProtectedLookup(not_found))
.await)
}

pub(crate) async fn external_subnet_detach(
&self,
opctx: &OpContext,
selector: params::ExternalSubnetSelector,
) -> UpdateResult<views::ExternalSubnet> {
let not_found = self.external_subnet_lookup(selector).unwrap_err();
Err(self
.unimplemented_todo(opctx, Unimpl::ProtectedLookup(not_found))
.await)
}
}
1 change: 1 addition & 0 deletions nexus/src/app/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ mod disk;
mod external_dns;
pub(crate) mod external_endpoints;
mod external_ip;
mod external_subnet;
mod iam;
mod image;
mod instance;
Expand Down
Loading
Loading