Skip to content
Open
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
261 changes: 260 additions & 1 deletion nexus/db-queries/src/db/datastore/bgp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ use omicron_common::api::external;
use omicron_common::api::external::http_pagination::PaginatedBy;
use omicron_common::api::external::{
CreateResult, DeleteResult, Error, ListResultVec, LookupResult, NameOrId,
ResourceType,
ResourceType, UpdateResult,
};
use ref_cast::RefCast;
use sled_agent_types::early_networking::RouterPeerType;
Expand Down Expand Up @@ -225,6 +225,143 @@ impl DataStore {
})
}

pub async fn bgp_config_update(
&self,
opctx: &OpContext,
sel: &networking::BgpConfigSelector,
update: &networking::BgpConfigUpdate,
) -> UpdateResult<BgpConfig> {
use nexus_db_schema::schema::bgp_config;
use nexus_db_schema::schema::bgp_config::dsl as bgp_config_dsl;
use nexus_db_schema::schema::{
bgp_announce_set, bgp_announce_set::dsl as announce_set_dsl,
};

let err = OptionalError::new();
let transaction = async |conn| {
// Look up the existing config
let (query, not_found_err, msg) = match &sel.name_or_id {
NameOrId::Id(id) => (
bgp_config_dsl::bgp_config
.filter(bgp_config::id.eq(*id))
.into_boxed(),
Error::not_found_by_id(ResourceType::BgpConfig, id),
"failed to lookup bgp config by id",
),
NameOrId::Name(name) => (
bgp_config_dsl::bgp_config
.filter(bgp_config::name.eq(name.to_string()))
.into_boxed(),
Error::not_found_by_name(ResourceType::BgpConfig, name),
"failed to lookup bgp config by name",
),
};

let lookup_err = |e, not_found_err, msg| {
error!(opctx.log, "{msg}"; "error" => ?e);
match e {
diesel::result::Error::NotFound => err.bail(not_found_err),
_ => err.bail(Error::internal_error(msg)),
}
};

let existing: BgpConfig = query
.filter(bgp_config::time_deleted.is_null())
.select(BgpConfig::as_select())
.limit(1)
.first_async::<BgpConfig>(&conn)
.await
.map_err(|e| lookup_err(e, not_found_err, msg))?;

// Resolve bgp_announce_set_id if an update was requested
let new_bgp_announce_set_id = match &update.bgp_announce_set_id {
None => existing.bgp_announce_set_id,
Some(name_or_id) => {
let (query, not_found_err, msg) = match name_or_id {
NameOrId::Id(id) => (
announce_set_dsl::bgp_announce_set
.filter(bgp_announce_set::id.eq(*id))
.into_boxed(),
Error::not_found_by_id(
ResourceType::BgpAnnounceSet,
id,
),
"failed to lookup announce set by id",
),
NameOrId::Name(name) => (
announce_set_dsl::bgp_announce_set
.filter(
bgp_announce_set::name.eq(name.to_string()),
)
.into_boxed(),
Error::not_found_by_name(
ResourceType::BgpAnnounceSet,
name,
),
"failed to lookup announce set by name",
),
};
query
.filter(bgp_announce_set::time_deleted.is_null())
.select(bgp_announce_set::id)
.limit(1)
.first_async::<Uuid>(&conn)
.await
.map_err(|e| lookup_err(e, not_found_err, msg))?
}
};

let new_name = update
.identity
.name
.as_ref()
.unwrap_or(existing.name())
.to_string();
let new_description = update
.identity
.description
.as_deref()
.unwrap_or(existing.description())
.to_string();
let new_max_paths =
update.max_paths.map_or(*existing.max_paths, |m| m.as_u8());

diesel::update(bgp_config_dsl::bgp_config)
.filter(bgp_config_dsl::id.eq(existing.id()))
.set((
bgp_config_dsl::time_modified.eq(Utc::now()),
bgp_config_dsl::name.eq(new_name),
bgp_config_dsl::description.eq(new_description),
bgp_config_dsl::bgp_announce_set_id
.eq(new_bgp_announce_set_id),
bgp_config_dsl::max_paths.eq(i16::from(new_max_paths)),
))
.returning(BgpConfig::as_returning())
.get_result_async(&conn)
.await
.map_err(|e| {
let msg = "bgp_config_update failed";
error!(opctx.log, "{msg}"; "error" => ?e);
err.bail(public_error_from_diesel(e, ErrorHandler::Server))
})
};

let conn = self.pool_connection_authorized(opctx).await?;
self.transaction_retry_wrapper("bgp_config_update")
.transaction(&conn, transaction)
.await
.map_err(|e| {
let msg = "bgp_config_update failed";
if let Some(err) = err.take() {
error!(opctx.log, "{msg}"; "error" => ?err);
err
} else {
error!(opctx.log, "{msg}"; "error" => ?e);
public_error_from_diesel(e, ErrorHandler::Server)
}
})
}

pub async fn bgp_config_delete(
&self,
opctx: &OpContext,
Expand Down Expand Up @@ -1049,13 +1186,135 @@ mod tests {
use nexus_db_model::SwitchPortBgpPeerConfig;
use nexus_types::external_api::networking::BgpPeer;
use omicron_common::api::external::IdentityMetadataCreateParams;
use omicron_common::api::external::IdentityMetadataUpdateParams;
use omicron_common::api::external::Name;
use omicron_test_utils::dev;
use oxnet::IpNet;
use sled_agent_types::early_networking::ImportExportPolicy;
use sled_agent_types::early_networking::MaxPathConfig;
use sled_agent_types::early_networking::RouterLifetimeConfig;
use std::net::IpAddr;

#[tokio::test]
async fn test_update_bgp_config() {
let logctx = dev::test_setup_log("test_update_bgp_config");
let db = TestDatabase::new_with_datastore(&logctx.log).await;
let (opctx, datastore) = (db.opctx(), db.datastore());

let config_name: Name = "config-name".parse().unwrap();
let announce_name: Name = "announce-name".parse().unwrap();

let config = networking::BgpConfigCreate {
identity: IdentityMetadataCreateParams {
name: config_name.clone(),
description: String::from("a test config"),
},
asn: 47,
bgp_announce_set_id: NameOrId::Name(announce_name.clone()),
vrf: None,
shaper: None,
checker: None,
max_paths: MaxPathConfig::new(1).unwrap(),
};

let new_announce_name: Name = "new-announce-name".parse().unwrap();
let new_max_paths = MaxPathConfig::new(2).unwrap();
let new_config_name: Name = "new-config-name".parse().unwrap();
let new_description = String::from("updated description");

let update = networking::BgpConfigUpdate {
identity: IdentityMetadataUpdateParams {
name: Some(new_config_name.clone()),
description: Some(new_description.clone()),
},
bgp_announce_set_id: Some(NameOrId::Name(
new_announce_name.clone(),
)),
max_paths: Some(new_max_paths),
};

// Make sure the announces exist
datastore
.bgp_create_announce_set(
&opctx,
&networking::BgpAnnounceSetCreate {
identity: IdentityMetadataCreateParams {
name: announce_name.clone(),
description: String::from("the first announce set"),
},
announcement: Vec::default(),
},
)
.await
.expect("create bgp announce set");

let new_announce_id = datastore
.bgp_create_announce_set(
&opctx,
&networking::BgpAnnounceSetCreate {
identity: IdentityMetadataCreateParams {
name: new_announce_name.clone(),
description: String::from("the second announce set"),
},
announcement: Vec::default(),
},
)
.await
.expect("create bgp announce set")
.0
.identity
.id;

// Try to update an inexistent BGP config
let res = datastore
.bgp_config_update(
&opctx,
&networking::BgpConfigSelector {
name_or_id: NameOrId::Name(config_name.clone()),
},
&update,
)
.await;
assert!(res.is_err());

// Create the BGP config
let config_id = datastore
.bgp_config_create(&opctx, &config)
.await
.expect("create bgp config")
.identity
.id;

// Update the BGP config
datastore
.bgp_config_update(
&opctx,
&networking::BgpConfigSelector {
name_or_id: NameOrId::Name(config_name.clone()),
},
&update,
)
.await
.expect("update bgp config");

// Verify the BGP config was updated
let bgp_config = datastore
.bgp_config_get(&opctx, &NameOrId::Id(config_id))
.await
.expect("get bgp config");

assert_eq!(
bgp_config.identity.name.to_string(),
new_config_name.to_string()
);
assert_eq!(bgp_config.identity.description, new_description);
assert_eq!(bgp_config.bgp_announce_set_id, new_announce_id);
assert_eq!(bgp_config.max_paths.0, new_max_paths.as_u8());

db.terminate().await;
logctx.cleanup_successful();
}

#[tokio::test]
async fn test_delete_bgp_config_and_announce_set_by_name() {
let logctx = dev::test_setup_log(
Expand Down
1 change: 1 addition & 0 deletions nexus/external-api/output/nexus_tags.txt
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,7 @@ networking_bgp_announcement_list GET /v1/system/networking/bgp-anno
networking_bgp_config_create POST /v1/system/networking/bgp
networking_bgp_config_delete DELETE /v1/system/networking/bgp
networking_bgp_config_list GET /v1/system/networking/bgp
networking_bgp_config_update PUT /v1/system/networking/bgp
networking_bgp_exported GET /v1/system/networking/bgp-exported
networking_bgp_imported GET /v1/system/networking/bgp-imported
networking_bgp_message_history GET /v1/system/networking/bgp-message-history
Expand Down
19 changes: 19 additions & 0 deletions nexus/external-api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ api_versions!([
// | date-based version should be at the top of the list.
// v
// (next_yyyy_mm_dd_nn, IDENT),
(2026_06_10_00, BGP_CONFIGURATION_UPDATE),
(2026_06_08_00, INSTANCE_CPU_TYPE_TURIN_V2),
(2026_06_05_00, EXTERNAL_JUMBO_FRAMES),
(2026_06_04_00, IMAGE_BLOCK_SIZE_TYPE),
Expand Down Expand Up @@ -5814,6 +5815,24 @@ pub trait NexusExternalApi {
sel: Query<latest::networking::BgpConfigSelector>,
) -> Result<HttpResponseUpdatedNoContent, HttpError>;

/// Update BGP configuration
///
/// Update the mutable fields of an existing BGP configuration. The `asn`
/// field is intentionally not updatable; changing the autonomous system
/// number requires creating a new BGP configuration object, since many
/// things are keyed off the ASN.
#[endpoint {
method = PUT,
path = "/v1/system/networking/bgp",
tags = ["system/networking"],
versions = VERSION_BGP_CONFIGURATION_UPDATE..,
}]
async fn networking_bgp_config_update(
rqctx: RequestContext<Self::Context>,
sel: Query<latest::networking::BgpConfigSelector>,
update: TypedBody<latest::networking::BgpConfigUpdate>,
) -> Result<HttpResponseOk<latest::networking::BgpConfig>, HttpError>;

/// Update BGP announce set
///
/// If the announce set exists, this endpoint replaces the existing announce
Expand Down
16 changes: 16 additions & 0 deletions nexus/src/app/bgp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use nexus_types::external_api::networking;
use omicron_common::api::external::http_pagination::PaginatedBy;
use omicron_common::api::external::{
self, CreateResult, DeleteResult, ListResultVec, LookupResult, NameOrId,
UpdateResult,
};
use slog_error_chain::InlineErrorChain;

Expand Down Expand Up @@ -42,6 +43,21 @@ impl super::Nexus {
self.db_datastore.bgp_config_list(opctx, pagparams).await
}

pub async fn bgp_config_update(
&self,
opctx: &OpContext,
sel: &networking::BgpConfigSelector,
update: &networking::BgpConfigUpdate,
) -> UpdateResult<BgpConfig> {
opctx.authorize(authz::Action::Modify, &authz::FLEET).await?;
let result =
self.db_datastore.bgp_config_update(opctx, sel, update).await?;
// Eagerly propagate changes via background task
self.background_tasks
.activate(&self.background_tasks.task_switch_port_settings_manager);
Ok(result)
}

pub async fn bgp_config_delete(
&self,
opctx: &OpContext,
Expand Down
14 changes: 14 additions & 0 deletions nexus/src/external_api/http_entrypoints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4532,6 +4532,20 @@ impl NexusExternalApi for NexusExternalApiImpl {
.await
}

async fn networking_bgp_config_update(
rqctx: RequestContext<ApiContext>,
sel: Query<networking::BgpConfigSelector>,
update: TypedBody<networking::BgpConfigUpdate>,
) -> Result<HttpResponseOk<networking::BgpConfig>, HttpError> {
audit_and_time(&rqctx, |opctx, nexus| async move {
let sel = sel.into_inner();
let update = update.into_inner();
let result = nexus.bgp_config_update(&opctx, &sel, &update).await?;
Ok(HttpResponseOk::<networking::BgpConfig>(result.try_into()?))
})
.await
}

async fn networking_bgp_announce_set_update(
rqctx: RequestContext<ApiContext>,
config: TypedBody<networking::BgpAnnounceSetCreate>,
Expand Down
Loading
Loading