Skip to content
19 changes: 19 additions & 0 deletions nexus/db-model/src/external_ip.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ use db_macros::Resource;
use diesel::Queryable;
use diesel::Selectable;
use ipnetwork::IpNetwork;
use nexus_types::external_api::params;
use nexus_types::external_api::shared;
use nexus_types::external_api::views;
use omicron_common::address::NUM_SOURCE_NAT_PORTS;
Expand Down Expand Up @@ -538,6 +539,24 @@ impl From<FloatingIp> for views::FloatingIp {
}
}

#[derive(AsChangeset)]
#[diesel(table_name = external_ip)]
pub struct FloatingIpUpdate {
pub name: Option<Name>,
pub description: Option<String>,
pub time_modified: DateTime<Utc>,
}

impl From<params::FloatingIpUpdate> for FloatingIpUpdate {
fn from(params: params::FloatingIpUpdate) -> Self {
Self {
name: params.identity.name.map(Name),
description: params.identity.description,
time_modified: Utc::now(),
}
}
}

impl TryFrom<ExternalIp> for InstanceExternalIpBody {
type Error = Error;

Expand Down
27 changes: 27 additions & 0 deletions nexus/db-queries/src/db/datastore/external_ip.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ use crate::db::update_and_check::UpdateStatus;
use async_bb8_diesel::AsyncRunQueryDsl;
use chrono::Utc;
use diesel::prelude::*;
use nexus_db_model::FloatingIpUpdate;
use nexus_db_model::Instance;
use nexus_db_model::IpAttachState;
use nexus_types::external_api::params;
Expand Down Expand Up @@ -823,6 +824,32 @@ impl DataStore {
.map_err(|e| public_error_from_diesel(e, ErrorHandler::Server))
}

/// Update a Floating IP
pub async fn floating_ip_update(
&self,
opctx: &OpContext,
authz_fip: &authz::FloatingIp,
update: FloatingIpUpdate,
) -> UpdateResult<ExternalIp> {
use db::schema::external_ip::dsl;

opctx.authorize(authz::Action::Modify, authz_fip).await?;

diesel::update(dsl::external_ip)
.filter(dsl::id.eq(authz_fip.id()))
.filter(dsl::time_deleted.is_null())
.set(update)
.returning(ExternalIp::as_returning())
.get_result_async(&*self.pool_connection_authorized(opctx).await?)
.await
.map_err(|e| {
public_error_from_diesel(
e,
ErrorHandler::NotFoundByResource(authz_fip),
)
})
}

/// Delete a Floating IP, verifying first that it is not in use.
pub async fn floating_ip_delete(
&self,
Expand Down
16 changes: 16 additions & 0 deletions nexus/src/app/external_ip.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,22 @@ impl super::Nexus {
.unwrap())
}

pub(crate) async fn floating_ip_update(
&self,
opctx: &OpContext,
ip_lookup: lookup::FloatingIp<'_>,
params: params::FloatingIpUpdate,
) -> UpdateResult<FloatingIp> {
let (.., authz_fip) =
ip_lookup.lookup_for(authz::Action::Modify).await?;
Ok(self
.db_datastore
.floating_ip_update(opctx, &authz_fip, params.clone().into())
.await?
.try_into()
.unwrap())
}

pub(crate) async fn floating_ip_delete(
&self,
opctx: &OpContext,
Expand Down
46 changes: 42 additions & 4 deletions nexus/src/external_api/http_entrypoints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
use super::{
console_api, device_auth, params,
views::{
self, Certificate, Group, IdentityProvider, Image, IpPool, IpPoolRange,
PhysicalDisk, Project, Rack, Role, Silo, SiloQuotas, SiloUtilization,
Sled, Snapshot, SshKey, User, UserBuiltin, Utilization, Vpc, VpcRouter,
VpcSubnet,
self, Certificate, FloatingIp, Group, IdentityProvider, Image, IpPool,
IpPoolRange, PhysicalDisk, Project, Rack, Role, Silo, SiloQuotas,
SiloUtilization, Sled, Snapshot, SshKey, User, UserBuiltin,
Utilization, Vpc, VpcRouter, VpcSubnet,
},
};
use crate::external_api::shared;
Expand Down Expand Up @@ -141,6 +141,7 @@ pub(crate) fn external_api() -> NexusApiDescription {
api.register(floating_ip_list)?;
api.register(floating_ip_create)?;
api.register(floating_ip_view)?;
api.register(floating_ip_update)?;
api.register(floating_ip_delete)?;
api.register(floating_ip_attach)?;
api.register(floating_ip_detach)?;
Expand Down Expand Up @@ -1921,6 +1922,43 @@ async fn floating_ip_create(
apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await
}

/// Update floating IP
#[endpoint {
method = PUT,
path = "/v1/floating-ips/{floating_ip}",
tags = ["floating-ips"],
}]
async fn floating_ip_update(
rqctx: RequestContext<Arc<ServerContext>>,
path_params: Path<params::FloatingIpPath>,
query_params: Query<params::OptionalProjectSelector>,
updated_floating_ip: TypedBody<params::FloatingIpUpdate>,
) -> Result<HttpResponseOk<FloatingIp>, HttpError> {
let apictx = rqctx.context();
let handler = async {
let nexus = &apictx.nexus;
let path = path_params.into_inner();
let query = query_params.into_inner();
let updated_floating_ip_params = updated_floating_ip.into_inner();
let opctx = crate::context::op_context_for_external_api(&rqctx).await?;
let floating_ip_selector = params::FloatingIpSelector {
project: query.project,
floating_ip: path.floating_ip,
};
let floating_ip_lookup =
nexus.floating_ip_lookup(&opctx, floating_ip_selector)?;
let floating_ip = nexus
.floating_ip_update(
&opctx,
floating_ip_lookup,
updated_floating_ip_params,
)
.await?;
Ok(HttpResponseOk(floating_ip))
};
apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await
}

/// Delete floating IP
#[endpoint {
method = DELETE,
Expand Down
11 changes: 11 additions & 0 deletions nexus/tests/integration_tests/endpoints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -772,6 +772,14 @@ pub static DEMO_FLOAT_IP_CREATE: Lazy<params::FloatingIpCreate> =
pool: None,
});

pub static DEMO_FLOAT_IP_UPDATE: Lazy<params::FloatingIpUpdate> =
Lazy::new(|| params::FloatingIpUpdate {
identity: IdentityMetadataUpdateParams {
name: None,
description: Some(String::from("an updated Floating IP")),
},
});

pub static DEMO_FLOAT_IP_ATTACH: Lazy<params::FloatingIpAttach> =
Lazy::new(|| params::FloatingIpAttach {
kind: params::FloatingIpParentKind::Instance,
Expand Down Expand Up @@ -2277,6 +2285,9 @@ pub static VERIFY_ENDPOINTS: Lazy<Vec<VerifyEndpoint>> = Lazy::new(|| {
unprivileged_access: UnprivilegedAccess::None,
allowed_methods: vec![
AllowedMethod::Get,
AllowedMethod::Put(
serde_json::to_value(&*DEMO_FLOAT_IP_UPDATE).unwrap()
),
AllowedMethod::Delete,
],
},
Expand Down
55 changes: 55 additions & 0 deletions nexus/tests/integration_tests/external_ips.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ use nexus_test_utils::resource_helpers::object_create;
use nexus_test_utils::resource_helpers::object_create_error;
use nexus_test_utils::resource_helpers::object_delete;
use nexus_test_utils::resource_helpers::object_delete_error;
use nexus_test_utils::resource_helpers::object_get;
use nexus_test_utils::resource_helpers::object_put;
use nexus_test_utils_macros::nexus_test;
use nexus_types::external_api::params;
use nexus_types::external_api::shared;
Expand All @@ -37,6 +39,7 @@ use nexus_types::identity::Resource;
use omicron_common::address::IpRange;
use omicron_common::address::Ipv4Range;
use omicron_common::api::external::IdentityMetadataCreateParams;
use omicron_common::api::external::IdentityMetadataUpdateParams;
use omicron_common::api::external::Instance;
use omicron_common::api::external::Name;
use omicron_common::api::external::NameOrId;
Expand Down Expand Up @@ -381,6 +384,58 @@ async fn test_floating_ip_create_name_in_use(
);
}

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

create_default_ip_pool(&client).await;
let project = create_project(client, PROJECT_NAME).await;

// Create the Floating IP
let fip = create_floating_ip(
client,
FIP_NAMES[0],
project.identity.name.as_str(),
None,
None,
)
.await;

let floating_ip_url = get_floating_ip_by_id_url(&fip.identity.id);

// Verify that the Floating IP was created correctly
let fetched_floating_ip: FloatingIp =
object_get(client, &floating_ip_url).await;

assert_eq!(fip.identity, fetched_floating_ip.identity);

// Set up the updated values
let new_fip_name: &str = "updated";
let new_fip_desc: &str = "updated description";
let updates: params::FloatingIpUpdate = params::FloatingIpUpdate {
identity: IdentityMetadataUpdateParams {
name: Some(String::from(new_fip_name).parse().unwrap()),
description: Some(String::from(new_fip_desc).parse().unwrap()),
},
};

// Update the Floating IP
let new_fip: FloatingIp =
object_put(client, &floating_ip_url, &updates).await;

assert_eq!(new_fip.identity.name.as_str(), new_fip_name);
assert_eq!(new_fip.identity.description, new_fip_desc);
assert_eq!(new_fip.project_id, project.identity.id);
assert_eq!(new_fip.identity.time_created, fip.identity.time_created);
assert_ne!(new_fip.identity.time_modified, fip.identity.time_modified);

// Verify that the Floating IP was updated correctly
let fetched_modified_floating_ip: FloatingIp =
object_get(client, &floating_ip_url).await;

assert_eq!(new_fip.identity, fetched_modified_floating_ip.identity);
}

#[nexus_test]
async fn test_floating_ip_delete(cptestctx: &ControlPlaneTestContext) {
let client = &cptestctx.external_client;
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 @@ -17,6 +17,7 @@ floating_ip_create POST /v1/floating-ips
floating_ip_delete DELETE /v1/floating-ips/{floating_ip}
floating_ip_detach POST /v1/floating-ips/{floating_ip}/detach
floating_ip_list GET /v1/floating-ips
floating_ip_update PUT /v1/floating-ips/{floating_ip}
floating_ip_view GET /v1/floating-ips/{floating_ip}

API operations found with tag "hidden"
Expand Down
6 changes: 6 additions & 0 deletions nexus/types/src/external_api/params.rs
Original file line number Diff line number Diff line change
Expand Up @@ -890,6 +890,12 @@ pub struct FloatingIpCreate {
pub pool: Option<NameOrId>,
}

#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)]
pub struct FloatingIpUpdate {
#[serde(flatten)]
pub identity: IdentityMetadataUpdateParams,
}

/// The type of resource that a floating IP is attached to
#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
Expand Down
72 changes: 72 additions & 0 deletions openapi/nexus.json
Original file line number Diff line number Diff line change
Expand Up @@ -964,6 +964,60 @@
}
}
},
"put": {
"tags": [
"floating-ips"
],
"summary": "Update floating IP",
"operationId": "floating_ip_update",
"parameters": [
{
"in": "path",
"name": "floating_ip",
"description": "Name or ID of the floating IP",
"required": true,
"schema": {
"$ref": "#/components/schemas/NameOrId"
}
},
{
"in": "query",
"name": "project",
"description": "Name or ID of the project",
"schema": {
"$ref": "#/components/schemas/NameOrId"
}
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/FloatingIpUpdate"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "successful operation",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/FloatingIp"
}
}
}
},
"4XX": {
"$ref": "#/components/responses/Error"
},
"5XX": {
"$ref": "#/components/responses/Error"
}
}
},
"delete": {
"tags": [
"floating-ips"
Expand Down Expand Up @@ -11919,6 +11973,24 @@
"items"
]
},
"FloatingIpUpdate": {
"description": "Updateable identity-related parameters",
"type": "object",
"properties": {
"description": {
"nullable": true,
"type": "string"
},
"name": {
"nullable": true,
"allOf": [
{
"$ref": "#/components/schemas/Name"
}
]
}
}
},
"Group": {
"description": "View of a Group",
"type": "object",
Expand Down