diff --git a/common/src/api/external/mod.rs b/common/src/api/external/mod.rs index 07e4fd0b83..feafbe7d7b 100644 --- a/common/src/api/external/mod.rs +++ b/common/src/api/external/mod.rs @@ -2113,9 +2113,6 @@ impl std::fmt::Display for Digest { pub struct AddressLotCreateResponse { /// The address lot that was created. pub lot: AddressLot, - - /// The address lot blocks that were created. - pub blocks: Vec, } /// Represents an address lot object, containing the id of the lot that can be @@ -2241,7 +2238,7 @@ pub struct SwitchPortSettingsView { pub routes: Vec, /// BGP peer settings. - pub bgp_peers: Vec, + pub bgp_peers: Vec, /// Layer 3 IP address settings. pub addresses: Vec, @@ -2520,12 +2517,12 @@ pub struct SwitchPortBgpPeerConfig { /// parameter is a reference to global BGP parameters. The `interface_name` /// indicates what interface the peer should be contacted on. #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, PartialEq)] -pub struct BgpPeer { +pub struct BgpPeerCombined { /// The global BGP configuration used for establishing a session with this /// peer. pub bgp_config: NameOrId, - /// The name of interface to peer on. This is relative to the port + /// The name of the interface to peer on. This is relative to the port /// configuration this BGP peer configuration is a part of. For example this /// value could be phy0 to refer to a primary physical interface. Or it /// could be vlan47 to refer to a VLAN interface. @@ -2551,19 +2548,19 @@ pub struct BgpPeer { /// How often to send keepalive requests (seconds). pub keepalive: u32, - /// Require that a peer has a specified ASN. + /// Require that this peer has a specified ASN. pub remote_asn: Option, - /// Require messages from a peer have a minimum IP time to live field. + /// Require messages from this peer have a minimum IP time to live field. pub min_ttl: Option, - /// Use the given key for TCP-MD5 authentication with the peer. + /// Use the given key for TCP-MD5 authentication with this peer. pub md5_auth_key: Option, - /// Apply the provided multi-exit discriminator (MED) updates sent to the peer. + /// Apply a multi-exit discriminator (MED) in updates sent to this peer. pub multi_exit_discriminator: Option, - /// Include the provided communities in updates sent to the peer. + /// Include the provided communities in updates sent to this peer. pub communities: Vec, /// Apply a local preference to routes received from this peer. @@ -2572,16 +2569,120 @@ pub struct BgpPeer { /// Enforce that the first AS in paths received from this peer is the peer's AS. pub enforce_first_as: bool, - /// Define import policy for a peer. + /// Define import policy for this peer. pub allowed_import: ImportExportPolicy, - /// Define export policy for a peer. + /// Define export policy for this peer. pub allowed_export: ImportExportPolicy, - /// Associate a VLAN ID with a peer. + /// Associate a VLAN ID with this peer. + pub vlan_id: Option, +} + +/// The information required to configure a BGP peer. +#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, PartialEq)] +pub struct BgpPeer { + /// The global BGP configuration used for establishing a session with this + /// peer. + pub bgp_config: NameOrId, + + /// The name of the interface to peer on. This is relative to the port + /// configuration this BGP peer configuration is a part of. For example this + /// value could be phy0 to refer to a primary physical interface. Or it + /// could be vlan47 to refer to a VLAN interface. + pub interface_name: String, + + /// The address of the host to peer with. + pub addr: oxnet::IpNet, + + /// How long to hold peer connections between keepalives (seconds). + pub hold_time: u32, + + /// How long to hold this peer in idle before attempting a new session + /// (seconds). + pub idle_hold_time: u32, + + /// How long to delay sending an open request after establishing a TCP + /// session (seconds). + pub delay_open: u32, + + /// How long to to wait between TCP connection retries (seconds). + pub connect_retry: u32, + + /// How often to send keepalive requests (seconds). + pub keepalive: u32, + + /// Require that this peer have a specified ASN. + pub remote_asn: Option, + + /// Require messages from this peer to have a minimum IP time to live field. + pub min_ttl: Option, + + /// Use the given key for TCP-MD5 authentication with this peer. + pub md5_auth_key: Option, + + /// Apply a multi-exit discriminator (MED) in updates sent to this peer. + pub multi_exit_discriminator: Option, + + /// Apply a local preference to routes received from this peer. + pub local_pref: Option, + + /// Enforce that the first AS in paths received from this peer is the peer's AS. + pub enforce_first_as: bool, + + /// Enable import policies + pub allow_import_list_active: bool, + + /// Enable export policies + pub allow_export_list_active: bool, + + /// Associate a VLAN ID with this peer. pub vlan_id: Option, } +/// A BGP peer configuration to remove from an interface +#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, PartialEq)] +pub struct BgpPeerRemove { + /// The global BGP configuration used for establishing a session with this + /// peer. + pub bgp_config: NameOrId, + + /// The name of the interface to peer on. This is relative to the port + /// configuration this BGP peer configuration is a part of. For example this + /// value could be phy0 to refer to a primary physical interface. Or it + /// could be vlan47 to refer to a VLAN interface. + pub interface_name: String, + + /// The address of the host to peer with. + pub addr: IpAddr, +} + +/// A BGP allowed prefix entry +#[derive(Clone, Debug, Deserialize, JsonSchema, Serialize, PartialEq)] +pub struct BgpAllowedPrefix { + /// Parent switch port configuration + pub port_settings_id: Uuid, + /// Interface peer is reachable on + pub interface_name: String, + /// Peer Address + pub addr: oxnet::IpNet, + /// Allowed Prefix + pub prefix: oxnet::IpNet, +} + +/// A BGP community +#[derive(Clone, Debug, Deserialize, JsonSchema, Serialize, PartialEq)] +pub struct BgpCommunity { + /// Parent switch port configuration + pub port_settings_id: Uuid, + /// Interface peer is reachable on + pub interface_name: String, + /// Peer Address + pub addr: oxnet::IpNet, + /// Community + pub community: u32, +} + /// A base BGP configuration. #[derive( ObjectIdentity, Clone, Debug, Deserialize, JsonSchema, Serialize, PartialEq, diff --git a/nexus/db-model/src/schema.rs b/nexus/db-model/src/schema.rs index f630bbbeac..c50609399c 100644 --- a/nexus/db-model/src/schema.rs +++ b/nexus/db-model/src/schema.rs @@ -1904,3 +1904,15 @@ joinable!(instance_ssh_key -> instance (instance_id)); allow_tables_to_appear_in_same_query!(sled, sled_instance); joinable!(network_interface -> probe (parent_id)); + +allow_tables_to_appear_in_same_query!( + switch_port_settings, + switch_port_settings_port_config, + switch_port_settings_link_config, + switch_port_settings_address_config, + switch_port_settings_route_config, + switch_port_settings_bgp_peer_config, + switch_port_settings_bgp_peer_config_allow_export, + switch_port_settings_bgp_peer_config_allow_import, + switch_port_settings_bgp_peer_config_communities, +); diff --git a/nexus/db-model/src/schema_versions.rs b/nexus/db-model/src/schema_versions.rs index d0542874fb..5fcd0c6b3f 100644 --- a/nexus/db-model/src/schema_versions.rs +++ b/nexus/db-model/src/schema_versions.rs @@ -17,7 +17,7 @@ use std::collections::BTreeMap; /// /// This must be updated when you change the database schema. Refer to /// schema/crdb/README.adoc in the root of this repository for details. -pub const SCHEMA_VERSION: SemverVersion = SemverVersion::new(90, 0, 0); +pub const SCHEMA_VERSION: SemverVersion = SemverVersion::new(91, 0, 0); /// List of all past database schema versions, in *reverse* order /// @@ -29,6 +29,7 @@ static KNOWN_VERSIONS: Lazy> = Lazy::new(|| { // | leaving the first copy as an example for the next person. // v // KnownVersion::new(next_int, "unique-dirname-with-the-sql-files"), + KnownVersion::new(91, "refactor-network-apis"), KnownVersion::new(90, "lookup-bgp-config-by-asn"), KnownVersion::new(89, "collapse_lldp_settings"), KnownVersion::new(88, "route-local-pref"), diff --git a/nexus/db-model/src/switch_port.rs b/nexus/db-model/src/switch_port.rs index 09f1327be2..ee57a3066f 100644 --- a/nexus/db-model/src/switch_port.rs +++ b/nexus/db-model/src/switch_port.rs @@ -20,8 +20,10 @@ use diesel::AsChangeset; use ipnetwork::IpNetwork; use nexus_types::external_api::params; use nexus_types::identity::Resource; -use omicron_common::api::external; -use omicron_common::api::external::{BgpPeer, ImportExportPolicy}; +use omicron_common::api::external::{ + self, BgpAllowedPrefix, BgpCommunity, NameOrId, +}; +use omicron_common::api::external::{BgpPeerCombined, ImportExportPolicy}; use omicron_common::api::internal::shared::{PortFec, PortSpeed}; use serde::{Deserialize, Serialize}; use uuid::Uuid; @@ -620,6 +622,32 @@ pub struct SwitchPortBgpPeerConfig { pub vlan_id: Option, } +impl Into for SwitchPortBgpPeerConfig { + fn into(self) -> external::BgpPeer { + external::BgpPeer { + bgp_config: NameOrId::Id(self.bgp_config_id), + interface_name: self.interface_name, + addr: self.addr.into(), + hold_time: self.hold_time.into(), + idle_hold_time: self.idle_hold_time.into(), + delay_open: self.delay_open.into(), + connect_retry: self.connect_retry.into(), + keepalive: self.keepalive.into(), + remote_asn: self.remote_asn.map(Into::into), + min_ttl: self.min_ttl.map(Into::into), + md5_auth_key: self.md5_auth_key, + multi_exit_discriminator: self + .multi_exit_discriminator + .map(Into::into), + local_pref: self.local_pref.map(Into::into), + enforce_first_as: self.enforce_first_as, + allow_import_list_active: self.allow_import_list_active, + allow_export_list_active: self.allow_export_list_active, + vlan_id: self.vlan_id.map(Into::into), + } + } +} + #[derive( Queryable, Insertable, @@ -638,6 +666,17 @@ pub struct SwitchPortBgpPeerConfigCommunity { pub community: SqlU32, } +impl Into for SwitchPortBgpPeerConfigCommunity { + fn into(self) -> BgpCommunity { + BgpCommunity { + port_settings_id: self.port_settings_id, + interface_name: self.interface_name, + addr: self.addr.into(), + community: self.community.into(), + } + } +} + #[derive( Queryable, Insertable, @@ -660,6 +699,17 @@ pub struct SwitchPortBgpPeerConfigAllowExport { pub prefix: IpNetwork, } +impl Into for SwitchPortBgpPeerConfigAllowExport { + fn into(self) -> BgpAllowedPrefix { + BgpAllowedPrefix { + port_settings_id: self.port_settings_id, + interface_name: self.interface_name, + addr: self.addr.into(), + prefix: self.prefix.into(), + } + } +} + #[derive( Queryable, Insertable, @@ -688,7 +738,7 @@ impl SwitchPortBgpPeerConfig { port_settings_id: Uuid, bgp_config_id: Uuid, interface_name: String, - p: &BgpPeer, + p: &BgpPeerCombined, ) -> Self { Self { port_settings_id, @@ -721,6 +771,17 @@ impl SwitchPortBgpPeerConfig { } } +impl Into for SwitchPortBgpPeerConfigAllowImport { + fn into(self) -> BgpAllowedPrefix { + BgpAllowedPrefix { + port_settings_id: self.port_settings_id, + interface_name: self.interface_name, + addr: self.addr.into(), + prefix: self.prefix.into(), + } + } +} + #[derive( Queryable, Insertable, diff --git a/nexus/db-queries/src/db/datastore/address_lot.rs b/nexus/db-queries/src/db/datastore/address_lot.rs index 459c2a4c36..8ed805e8e5 100644 --- a/nexus/db-queries/src/db/datastore/address_lot.rs +++ b/nexus/db-queries/src/db/datastore/address_lot.rs @@ -20,30 +20,21 @@ use diesel::{ExpressionMethods, QueryDsl, SelectableHelper}; use diesel_dtrace::DTraceConnection; use ipnetwork::IpNetwork; use nexus_types::external_api::params; -use nexus_types::identity::Resource; use omicron_common::api::external::http_pagination::PaginatedBy; use omicron_common::api::external::{ CreateResult, DataPageParams, DeleteResult, Error, ListResultVec, LookupResult, ResourceType, }; use ref_cast::RefCast; -use serde::{Deserialize, Serialize}; use uuid::Uuid; -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct AddressLotCreateResult { - pub lot: AddressLot, - pub blocks: Vec, -} - impl DataStore { pub async fn address_lot_create( &self, opctx: &OpContext, params: ¶ms::AddressLotCreate, - ) -> CreateResult { + ) -> CreateResult { use db::schema::address_lot::dsl as lot_dsl; - use db::schema::address_lot_block::dsl as block_dsl; let conn = self.pool_connection_authorized(opctx).await?; @@ -81,63 +72,7 @@ impl DataStore { } }; - let desired_blocks: Vec = params - .blocks - .iter() - .map(|b| { - AddressLotBlock::new( - db_lot.id(), - b.first_address.into(), - b.last_address.into(), - ) - }) - .collect(); - - let found_blocks: Vec = - block_dsl::address_lot_block - .filter(block_dsl::address_lot_id.eq(db_lot.id())) - .filter( - block_dsl::first_address.eq_any( - desired_blocks - .iter() - .map(|b| b.first_address) - .collect::>(), - ), - ) - .filter( - block_dsl::last_address.eq_any( - desired_blocks - .iter() - .map(|b| b.last_address) - .collect::>(), - ), - ) - .get_results_async(&conn) - .await?; - - let mut blocks = vec![]; - - // If the block is found in the database, use the found block. - // If the block is not found in the database, insert it. - for desired_block in desired_blocks { - let block = match found_blocks.iter().find(|db_b| { - db_b.first_address == desired_block.first_address - && db_b.last_address == desired_block.last_address - }) { - Some(block) => block.clone(), - None => { - diesel::insert_into(block_dsl::address_lot_block) - .values(desired_block) - .returning(AddressLotBlock::as_returning()) - .get_results_async(&conn) - .await?[0] - .clone() - } - }; - blocks.push(block); - } - - Ok(AddressLotCreateResult { lot: db_lot, blocks }) + Ok(db_lot) }) .await .map_err(|e| { @@ -263,6 +198,177 @@ impl DataStore { .map_err(|e| public_error_from_diesel(e, ErrorHandler::Server)) } + pub async fn address_lot_block_create( + &self, + opctx: &OpContext, + address_lot_id: Uuid, + params: params::AddressLotBlockAddRemove, + ) -> CreateResult { + use db::schema::address_lot; + use db::schema::address_lot_block::dsl; + + let conn = self.pool_connection_authorized(opctx).await?; + let err = OptionalError::new(); + + self.transaction_retry_wrapper("address_lot_create") + .transaction(&conn, |conn| { + let err = err.clone(); + + async move { + + // Verify parent address lot exists + address_lot::table + .filter(address_lot::id.eq(address_lot_id)) + .select(AddressLot::as_select()) + .get_result_async(&conn) + .await + .map_err(|e: diesel::result::Error| { + match e { + diesel::result::Error::NotFound => { + err.bail( + Error::non_resourcetype_not_found( + format!("unable to find address lot with identifier {address_lot_id}") + ) + ) + }, + _ => { + err.bail(Error::internal_error( + "error while looking up address lot for address lot block" + )) + }, + } + })?; + + let found_block: Option = + dsl::address_lot_block + .filter(dsl::address_lot_id.eq(address_lot_id)) + .filter( + dsl::first_address + .eq(IpNetwork::from(params.first_address)), + ) + .filter( + dsl::last_address + .eq(IpNetwork::from(params.last_address)), + ) + .select(AddressLotBlock::as_select()) + .limit(1) + .first_async(&conn) + .await + .ok(); + + let new_block = AddressLotBlock::new( + address_lot_id, + IpNetwork::from(params.first_address), + IpNetwork::from(params.last_address), + ); + + let db_block = match found_block { + Some(v) => v, + None => { + diesel::insert_into(dsl::address_lot_block) + .values(new_block) + .returning(AddressLotBlock::as_returning()) + .get_result_async(&conn) + .await? + } + }; + + Ok(db_block) + } + }) + .await + .map_err(|e| { + let message = "address_lot_block_create failed"; + match err.take() { + Some(external_error) => { + error!(opctx.log, "{message}"; "error" => ?external_error); + external_error + }, + None => { + error!(opctx.log, "{message}"; "error" => ?e); + Error::internal_error("error while adding address block to address lot") + }, + } + }) + } + + pub async fn address_lot_block_delete( + &self, + opctx: &OpContext, + address_lot_id: Uuid, + params: params::AddressLotBlockAddRemove, + ) -> DeleteResult { + use db::schema::address_lot_block::dsl; + use db::schema::address_lot_rsvd_block::dsl as rsvd_block_dsl; + + #[derive(Debug)] + enum AddressLotBlockDeleteError { + BlockInUse, + } + + let conn = self.pool_connection_authorized(opctx).await?; + + let err = OptionalError::new(); + + self.transaction_retry_wrapper("address_lot_delete") + .transaction(&conn, |conn| { + let err = err.clone(); + async move { + let rsvd: Vec = + rsvd_block_dsl::address_lot_rsvd_block + .filter( + rsvd_block_dsl::address_lot_id + .eq(address_lot_id), + ) + .filter( + rsvd_block_dsl::first_address + .eq(IpNetwork::from(params.first_address)), + ) + .filter( + rsvd_block_dsl::last_address + .eq(IpNetwork::from(params.last_address)), + ) + .select(AddressLotReservedBlock::as_select()) + .limit(1) + .load_async(&conn) + .await?; + + if !rsvd.is_empty() { + return Err( + err.bail(AddressLotBlockDeleteError::BlockInUse) + ); + } + + diesel::delete(dsl::address_lot_block) + .filter(dsl::address_lot_id.eq(address_lot_id)) + .filter( + dsl::first_address + .eq(IpNetwork::from(params.first_address)), + ) + .filter( + dsl::last_address + .eq(IpNetwork::from(params.last_address)), + ) + .execute_async(&conn) + .await?; + + Ok(()) + } + }) + .await + .map_err(|e| { + if let Some(err) = err.take() { + match err { + AddressLotBlockDeleteError::BlockInUse => { + Error::invalid_request("block is in use") + } + } + } else { + public_error_from_diesel(e, ErrorHandler::Server) + } + }) + } + pub async fn address_lot_id_for_block_id( &self, opctx: &OpContext, diff --git a/nexus/db-queries/src/db/datastore/bgp.rs b/nexus/db-queries/src/db/datastore/bgp.rs index fdb9629543..c0c320424f 100644 --- a/nexus/db-queries/src/db/datastore/bgp.rs +++ b/nexus/db-queries/src/db/datastore/bgp.rs @@ -223,7 +223,7 @@ impl DataStore { pub async fn bgp_config_delete( &self, opctx: &OpContext, - sel: ¶ms::BgpConfigSelector, + sel: &NameOrId, ) -> DeleteResult { use db::schema::bgp_config; use db::schema::bgp_config::dsl as bgp_config_dsl; @@ -237,7 +237,7 @@ impl DataStore { .transaction(&conn, |conn| { let err = err.clone(); async move { - let name_or_id = sel.name_or_id.clone(); + let name_or_id = sel.clone(); let id: Uuid = match name_or_id { NameOrId::Id(id) => bgp_config_dsl::bgp_config diff --git a/nexus/db-queries/src/db/datastore/mod.rs b/nexus/db-queries/src/db/datastore/mod.rs index 2cd21754f8..a163d2b0e1 100644 --- a/nexus/db-queries/src/db/datastore/mod.rs +++ b/nexus/db-queries/src/db/datastore/mod.rs @@ -108,7 +108,6 @@ mod volume_repair; mod vpc; mod zpool; -pub use address_lot::AddressLotCreateResult; pub use dns::DataStoreDnsTest; pub use dns::DnsVersionUpdateBuilder; pub use instance::{InstanceAndActiveVmm, InstanceGestalt}; @@ -121,6 +120,7 @@ pub use region::RegionAllocationParameters; pub use silo::Discoverability; pub use sled::SledTransition; pub use sled::TransitionError; +pub use switch_port::BgpPeerConfig; pub use switch_port::SwitchPortSettingsCombinedResult; pub use virtual_provisioning_collection::StorageType; pub use vmm::VmmStateUpdateResult; diff --git a/nexus/db-queries/src/db/datastore/switch_port.rs b/nexus/db-queries/src/db/datastore/switch_port.rs index 2e09c1ac13..578b363b42 100644 --- a/nexus/db-queries/src/db/datastore/switch_port.rs +++ b/nexus/db-queries/src/db/datastore/switch_port.rs @@ -24,8 +24,9 @@ use crate::db::model::{ use crate::db::pagination::paginated; use crate::transaction_retry::OptionalError; use async_bb8_diesel::{AsyncRunQueryDsl, Connection}; +use diesel::CombineDsl; use diesel::{ - CombineDsl, ExpressionMethods, JoinOnDsl, NullableExpressionMethods, + ExpressionMethods, JoinOnDsl, NullableExpressionMethods, OptionalExtension, PgConnection, QueryDsl, SelectableHelper, }; use diesel_dtrace::DTraceConnection; @@ -33,13 +34,16 @@ use ipnetwork::IpNetwork; use nexus_db_model::{ BgpConfig, SqlU16, SqlU32, SqlU8, SwitchPortBgpPeerConfigAllowExport, SwitchPortBgpPeerConfigAllowImport, SwitchPortBgpPeerConfigCommunity, + SwitchPortGeometry, +}; +use nexus_types::external_api::params::{ + self, AllowedPrefixAddRemove, BgpCommunityAddRemove, }; -use nexus_types::external_api::params; use omicron_common::api::external::http_pagination::PaginatedBy; use omicron_common::api::external::{ - self, CreateResult, DataPageParams, DeleteResult, Error, - ImportExportPolicy, ListResultVec, LookupResult, NameOrId, ResourceType, - UpdateResult, + self, BgpPeer, BgpPeerRemove, CreateResult, DataPageParams, DeleteResult, + Error, ImportExportPolicy, ListResultVec, LookupResult, NameOrId, + ResourceType, UpdateResult, }; use ref_cast::RefCast; use serde::{Deserialize, Serialize}; @@ -68,9 +72,9 @@ pub struct BgpPeerConfig { pub vlan_id: Option, } -impl Into for BgpPeerConfig { - fn into(self) -> external::BgpPeer { - external::BgpPeer { +impl Into for BgpPeerConfig { + fn into(self) -> external::BgpPeerCombined { + external::BgpPeerCombined { bgp_config: self.bgp_config_id.into(), interface_name: self.interface_name.clone(), addr: self.addr.ip(), @@ -169,12 +173,25 @@ impl DataStore { port_settings_dsl::switch_port_settings .filter(switch_port_settings::time_deleted.is_null()) - .filter(switch_port_settings::name.eq(name)) + .filter(switch_port_settings::name.eq(name.clone())) .select(switch_port_settings::id) .limit(1) .first_async::(&*pool) .await - .map_err(|e| public_error_from_diesel(e, ErrorHandler::Server)) + .map_err(|e| { + let msg = "failed to lookup switch port settings by name"; + error!(opctx.log, "{msg}"; "error" => ?e); + + match e { + diesel::result::Error::NotFound => { + Error::not_found_by_name( + ResourceType::SwitchPortSettings, + &name, + ) + } + _ => Error::internal_error(msg), + } + }) } pub async fn switch_ports_using_settings( @@ -241,390 +258,2128 @@ impl DataStore { }) } - pub async fn switch_port_settings_delete( + pub async fn switch_port_settings_delete( + &self, + opctx: &OpContext, + params: &NameOrId, + ) -> DeleteResult { + let conn = self.pool_connection_authorized(opctx).await?; + + let err = OptionalError::new(); + + // TODO https://github.com/oxidecomputer/omicron/issues/2811 + // Audit external networking database transaction usage + self.transaction_retry_wrapper("switch_port_settings_delete") + .transaction(&conn, |conn| { + let err = err.clone(); + async move { + do_switch_port_settings_delete(&conn, ¶ms, err).await + } + }) + .await + .map_err(|e| { + if let Some(err) = err.take() { + match err { + SwitchPortSettingsDeleteError::SwitchPortSettingsNotFound => { + Error::invalid_request("port settings not found") + } + } + } else { + let name = match params { + NameOrId::Id(id) => id.to_string(), + NameOrId::Name(name) => name.to_string(), + }; + public_error_from_diesel( + e, + ErrorHandler::Conflict( + ResourceType::SwitchPortSettings, + &name, + ), + ) + } + }) + } + + pub async fn switch_port_settings_update( + &self, + opctx: &OpContext, + params: ¶ms::SwitchPortSettingsCreate, + id: Uuid, + ) -> UpdateResult { + let delete_err = OptionalError::new(); + let create_err = OptionalError::new(); + let conn = self.pool_connection_authorized(opctx).await?; + + // TODO https://github.com/oxidecomputer/omicron/issues/2811 + // Audit external networking database transaction usage + self.transaction_retry_wrapper("switch_port_settings_update") + .transaction(&conn, |conn| { + let delete_err = delete_err.clone(); + let create_err = create_err.clone(); + let selector = NameOrId::Id(id); + async move { + do_switch_port_settings_delete(&conn, &selector, delete_err).await?; + do_switch_port_settings_create(&conn, Some(id), params, create_err).await + } + }) + .await + .map_err(|e| { + if let Some(err) = delete_err.take() { + match err { + SwitchPortSettingsDeleteError::SwitchPortSettingsNotFound => { + Error::invalid_request("port settings not found") + } + } + } + else if let Some(err) = create_err.take() { + match err { + SpsCreateError::AddressLotNotFound => { + Error::invalid_request("AddressLot not found") + } + SpsCreateError::BgpConfigNotFound => { + Error::invalid_request("BGP config not found") + } + SwitchPortSettingsCreateError::ReserveBlock( + ReserveBlockError::AddressUnavailable, + ) => Error::invalid_request("address unavailable"), + SwitchPortSettingsCreateError::ReserveBlock( + ReserveBlockError::AddressNotInLot, + ) => Error::invalid_request("address not in lot"), + + } + } + else { + public_error_from_diesel(e, ErrorHandler::Server) + } + }) + } + + pub async fn switch_port_settings_list( + &self, + opctx: &OpContext, + pagparams: &PaginatedBy<'_>, + ) -> ListResultVec { + use db::schema::switch_port_settings::dsl; + + match pagparams { + PaginatedBy::Id(pagparams) => { + paginated(dsl::switch_port_settings, dsl::id, &pagparams) + } + PaginatedBy::Name(pagparams) => paginated( + dsl::switch_port_settings, + dsl::name, + &pagparams.map_name(|n| Name::ref_cast(n)), + ), + } + .filter(dsl::time_deleted.is_null()) + .select(SwitchPortSettings::as_select()) + .load_async(&*self.pool_connection_authorized(opctx).await?) + .await + .map_err(|e| public_error_from_diesel(e, ErrorHandler::Server)) + } + + pub async fn switch_port_settings_get( + &self, + opctx: &OpContext, + name_or_id: &NameOrId, + ) -> LookupResult { + #[derive(Debug)] + enum SwitchPortSettingsGetError { + NotFound(external::Name), + } + + let err = OptionalError::new(); + let conn = self.pool_connection_authorized(opctx).await?; + + // TODO https://github.com/oxidecomputer/omicron/issues/2811 + // Audit external networking database transaction usage + self.transaction_retry_wrapper("switch_port_settings_get") + .transaction(&conn, |conn| { + let err = err.clone(); + async move { + // get the top level port settings object + use db::schema::switch_port_settings::{ + self, dsl as port_settings_dsl, + }; + use db::schema::{ + switch_port_settings_bgp_peer_config_allow_import::dsl as allow_import_dsl, + switch_port_settings_bgp_peer_config_allow_export::dsl as allow_export_dsl, + switch_port_settings_bgp_peer_config_communities::dsl as bgp_communities_dsl, + }; + + let id = match name_or_id { + NameOrId::Id(id) => *id, + NameOrId::Name(name) => { + let name_str = name.to_string(); + port_settings_dsl::switch_port_settings + .filter(switch_port_settings::time_deleted.is_null()) + .filter(switch_port_settings::name.eq(name_str)) + .select(switch_port_settings::id) + .limit(1) + .first_async::(&conn) + .await + .map_err(|diesel_error| { + err.bail_retryable_or_else(diesel_error, |_| { + SwitchPortSettingsGetError::NotFound( + name.clone(), + ) + }) + })? + } + }; + + let settings: SwitchPortSettings = + port_settings_dsl::switch_port_settings + .filter(switch_port_settings::time_deleted.is_null()) + .filter(switch_port_settings::id.eq(id)) + .select(SwitchPortSettings::as_select()) + .limit(1) + .first_async::(&conn) + .await?; + + // get the port config + use db::schema::switch_port_settings_port_config::{ + self as port_config, dsl as port_config_dsl, + }; + let port: SwitchPortConfig = + port_config_dsl::switch_port_settings_port_config + .filter(port_config::port_settings_id.eq(id)) + .select(SwitchPortConfig::as_select()) + .limit(1) + .first_async::(&conn) + .await?; + + // initialize result + let mut result = + SwitchPortSettingsCombinedResult::new(settings, port); + + // get the link configs + use db::schema::switch_port_settings_link_config::{ + self as link_config, dsl as link_config_dsl, + }; + + result.links = link_config_dsl::switch_port_settings_link_config + .filter(link_config::port_settings_id.eq(id)) + .select(SwitchPortLinkConfig::as_select()) + .load_async::(&conn) + .await?; + + let lldp_link_ids: Vec = result + .links + .iter() + .map(|link| link.lldp_link_config_id) + .collect(); + + use db::schema::lldp_link_config; + result.link_lldp = lldp_link_config::dsl::lldp_link_config + .filter(lldp_link_config::id.eq_any(lldp_link_ids)) + .select(LldpLinkConfig::as_select()) + .limit(1) + .load_async::(&conn) + .await?; + + // get the interface configs + use db::schema::switch_port_settings_interface_config::{ + self as interface_config, dsl as interface_config_dsl, + }; + + result.interfaces = + interface_config_dsl::switch_port_settings_interface_config + .filter(interface_config::port_settings_id.eq(id)) + .select(SwitchInterfaceConfig::as_select()) + .load_async::(&conn) + .await?; + + use db::schema::switch_vlan_interface_config as vlan_config; + use db::schema::switch_vlan_interface_config::dsl as vlan_dsl; + let interface_ids: Vec = result + .interfaces + .iter() + .map(|interface| interface.id) + .collect(); + + result.vlan_interfaces = vlan_dsl::switch_vlan_interface_config + .filter(vlan_config::interface_config_id.eq_any(interface_ids)) + .select(SwitchVlanInterfaceConfig::as_select()) + .load_async::(&conn) + .await?; + + // get the route configs + use db::schema::switch_port_settings_route_config::{ + self as route_config, dsl as route_config_dsl, + }; + + result.routes = route_config_dsl::switch_port_settings_route_config + .filter(route_config::port_settings_id.eq(id)) + .select(SwitchPortRouteConfig::as_select()) + .load_async::(&conn) + .await?; + + // get the bgp peer configs + use db::schema::switch_port_settings_bgp_peer_config::{ + self as bgp_peer, dsl as bgp_peer_dsl, + }; + + let peers: Vec = + bgp_peer_dsl::switch_port_settings_bgp_peer_config + .filter(bgp_peer::port_settings_id.eq(id)) + .select(SwitchPortBgpPeerConfig::as_select()) + .load_async::(&conn) + .await?; + + for p in peers.iter() { + let allowed_import: ImportExportPolicy = if p.allow_import_list_active { + let db_list: Vec = + allow_import_dsl::switch_port_settings_bgp_peer_config_allow_import + .filter(allow_import_dsl::port_settings_id.eq(id)) + .filter(allow_import_dsl::interface_name.eq(p.interface_name.clone())) + .filter(allow_import_dsl::addr.eq(p.addr)) + .select(SwitchPortBgpPeerConfigAllowImport::as_select()) + .load_async::(&conn) + .await?; + + ImportExportPolicy::Allow(db_list + .into_iter() + .map(|x| x.prefix.into()) + .collect() + ) + } else { + ImportExportPolicy::NoFiltering + }; + + let allowed_export: ImportExportPolicy = if p.allow_export_list_active { + let db_list: Vec = + allow_export_dsl::switch_port_settings_bgp_peer_config_allow_export + .filter(allow_export_dsl::port_settings_id.eq(id)) + .filter(allow_export_dsl::interface_name.eq(p.interface_name.clone())) + .filter(allow_export_dsl::addr.eq(p.addr)) + .select(SwitchPortBgpPeerConfigAllowExport::as_select()) + .load_async::(&conn) + .await?; + + ImportExportPolicy::Allow(db_list + .into_iter() + .map(|x| x.prefix.into()) + .collect() + ) + } else { + ImportExportPolicy::NoFiltering + }; + + let communities: Vec = + bgp_communities_dsl::switch_port_settings_bgp_peer_config_communities + .filter(bgp_communities_dsl::port_settings_id.eq(id)) + .filter(bgp_communities_dsl::interface_name.eq(p.interface_name.clone())) + .filter(bgp_communities_dsl::addr.eq(p.addr)) + .select(SwitchPortBgpPeerConfigCommunity::as_select()) + .load_async::(&conn) + .await?; + + let view = BgpPeerConfig { + port_settings_id: p.port_settings_id, + bgp_config_id: p.bgp_config_id, + interface_name: p.interface_name.clone(), + addr: p.addr, + hold_time: p.hold_time, + idle_hold_time: p.idle_hold_time, + delay_open: p.delay_open, + connect_retry: p.connect_retry, + keepalive: p.keepalive, + remote_asn: p.remote_asn, + min_ttl: p.min_ttl, + md5_auth_key: p.md5_auth_key.clone(), + multi_exit_discriminator: p.multi_exit_discriminator, + local_pref: p.local_pref, + enforce_first_as: p.enforce_first_as, + vlan_id: p.vlan_id, + communities: communities.into_iter().map(|c| c.community.0).collect(), + allowed_import, + allowed_export, + }; + + result.bgp_peers.push(view); + } + + // get the address configs + use db::schema::switch_port_settings_address_config::{ + self as address_config, dsl as address_config_dsl, + }; + + result.addresses = + address_config_dsl::switch_port_settings_address_config + .filter(address_config::port_settings_id.eq(id)) + .select(SwitchPortAddressConfig::as_select()) + .load_async::(&conn) + .await?; + + Ok(result) + } + }) + .await + .map_err(|e| { + if let Some(err) = err.take() { + match err { + SwitchPortSettingsGetError::NotFound(name) => { + Error::not_found_by_name( + ResourceType::SwitchPortSettings, + &name, + ) + } + } + } else { + let name = name_or_id.to_string(); + public_error_from_diesel( + e, + ErrorHandler::Conflict( + ResourceType::SwitchPortSettings, + &name, + ), + ) + } + }) + } + + pub async fn switch_port_configuration_geometry_get( + &self, + opctx: &OpContext, + name_or_id: NameOrId, + ) -> LookupResult { + use db::schema::switch_port_settings as port_settings; + use db::schema::switch_port_settings::dsl as port_settings_dsl; + use db::schema::switch_port_settings_port_config::dsl; + + let dataset = port_settings_dsl::switch_port_settings.inner_join( + dsl::switch_port_settings_port_config + .on(dsl::port_settings_id.eq(port_settings_dsl::id)), + ); + + let query = match name_or_id { + NameOrId::Id(id) => { + // find port config using port settings id + dataset.filter(port_settings::id.eq(id)).into_boxed() + } + NameOrId::Name(name) => { + // find port config using port settings name + dataset + .filter(port_settings::name.eq(name.to_string())) + .into_boxed() + } + }; + + let geometry: SwitchPortConfig = query + .select(SwitchPortConfig::as_select()) + .limit(1) + .first_async(&*self.pool_connection_authorized(opctx).await?) + .await + .map_err(|e| public_error_from_diesel(e, ErrorHandler::Server))?; + + Ok(geometry) + } + + pub async fn switch_port_configuration_geometry_set( + &self, + opctx: &OpContext, + name_or_id: NameOrId, + new_geometry: SwitchPortGeometry, + ) -> CreateResult { + use db::schema::switch_port_settings_port_config::dsl; + + let conn = self.pool_connection_authorized(opctx).await?; + + self.transaction_retry_wrapper("switch_port_configuration_geometry_set") + .transaction(&conn, |conn| { + let identity = name_or_id.clone(); + async move { + // we query for the parent record instead of trusting that the + // uuid is valid, since we don't have true referential integrity between + // the tables + let port_settings_id = + switch_port_configuration_id(&conn, identity).await?; + + let port_config = SwitchPortConfig { + port_settings_id, + geometry: new_geometry, + }; + + // create or update geometry + diesel::insert_into(dsl::switch_port_settings_port_config) + .values(port_config.clone()) + .on_conflict(dsl::port_settings_id) + .do_update() + .set(dsl::geometry.eq(new_geometry)) + .execute_async(&conn) + .await?; + + Ok(port_config) + } + }) + .await + .map_err(|e| public_error_from_diesel(e, ErrorHandler::Server)) + } + + pub async fn switch_port_configuration_link_list( + &self, + opctx: &OpContext, + name_or_id: NameOrId, + ) -> ListResultVec { + use db::schema::switch_port_settings as port_settings; + use db::schema::switch_port_settings::dsl as port_settings_dsl; + use db::schema::switch_port_settings_link_config::dsl as link_dsl; + + let dataset = port_settings_dsl::switch_port_settings.inner_join( + link_dsl::switch_port_settings_link_config + .on(link_dsl::port_settings_id.eq(port_settings_dsl::id)), + ); + + let query = match name_or_id { + NameOrId::Id(id) => { + // find port config using port settings id + dataset.filter(port_settings::id.eq(id)).into_boxed() + } + NameOrId::Name(name) => { + // find port config using port settings name + dataset + .filter(port_settings::name.eq(name.to_string())) + .into_boxed() + } + }; + + let configs: Vec = query + .select(SwitchPortLinkConfig::as_select()) + .load_async(&*self.pool_connection_authorized(opctx).await?) + .await + .map_err(|e| public_error_from_diesel(e, ErrorHandler::Server))?; + + Ok(configs) + } + + pub async fn switch_port_configuration_link_create( + &self, + opctx: &OpContext, + name_or_id: NameOrId, + new_settings: params::NamedLinkConfigCreate, + ) -> CreateResult { + use db::schema::lldp_link_config::dsl as lldp_link_dsl; + use db::schema::switch_port_settings_link_config::dsl as link_dsl; + + let conn = self.pool_connection_authorized(opctx).await?; + + let config = self + .transaction_retry_wrapper("switch_port_configuration_link_create") + .transaction(&conn, |conn| { + let identity = name_or_id.clone(); + let new_settings = new_settings.clone(); + + async move { + // fetch id of parent record + let parent_id = + switch_port_configuration_id(&conn, identity).await?; + + let lldp_link_config_id = match new_settings.lldp_config { + Some(name_or_id) => { + let config_id = + lldp_configuration_id(&conn, name_or_id) + .await?; + Ok::(config_id) + } + None => { + let values = LldpLinkConfig::new( + false, None, None, None, None, None, None, + ); + let lldp_config = diesel::insert_into( + lldp_link_dsl::lldp_link_config, + ) + .values(values) + .returning(LldpLinkConfig::as_returning()) + .get_result_async(&conn) + .await?; + Ok(lldp_config.id) + } + }?; + + let link_config = SwitchPortLinkConfig { + port_settings_id: parent_id, + lldp_link_config_id, + link_name: new_settings.name.to_string(), + mtu: new_settings.mtu.into(), + fec: new_settings.fec.into(), + speed: new_settings.speed.into(), + autoneg: new_settings.autoneg, + }; + + let link = diesel::insert_into( + link_dsl::switch_port_settings_link_config, + ) + .values(link_config.clone()) + .returning(SwitchPortLinkConfig::as_returning()) + .get_result_async(&conn) + .await?; + + Ok(link) + } + }) + .await + .map_err(|e| public_error_from_diesel(e, ErrorHandler::Server))?; + Ok(config) + } + + pub async fn switch_port_configuration_link_view( + &self, + opctx: &OpContext, + name_or_id: NameOrId, + link: Name, + ) -> LookupResult { + use db::schema::switch_port_settings as port_settings; + use db::schema::switch_port_settings::dsl as port_settings_dsl; + use db::schema::switch_port_settings_link_config::dsl as link_dsl; + + let dataset = port_settings_dsl::switch_port_settings.inner_join( + link_dsl::switch_port_settings_link_config + .on(link_dsl::port_settings_id.eq(port_settings_dsl::id)), + ); + + let query = match name_or_id { + NameOrId::Id(id) => { + // find port config using port settings id + dataset.filter(port_settings::id.eq(id)).into_boxed() + } + NameOrId::Name(name) => { + // find port config using port settings name + dataset + .filter(port_settings::name.eq(name.to_string())) + .into_boxed() + } + }; + + let config: SwitchPortLinkConfig = query + .filter(link_dsl::link_name.eq(link.to_string())) + .select(SwitchPortLinkConfig::as_select()) + .limit(1) + .first_async(&*self.pool_connection_authorized(opctx).await?) + .await + .map_err(|e| public_error_from_diesel(e, ErrorHandler::Server))?; + + Ok(config) + } + + pub async fn switch_port_configuration_link_delete( + &self, + opctx: &OpContext, + name_or_id: NameOrId, + link: Name, + ) -> DeleteResult { + use db::schema::lldp_link_config::dsl as lldp_link_dsl; + use db::schema::switch_port_settings_link_config::dsl as link_dsl; + + let conn = self.pool_connection_authorized(opctx).await?; + + self.transaction_retry_wrapper("switch_port_configuration_link_delete") + .transaction(&conn, |conn| { + let identity = name_or_id.clone(); + let link_name = link.clone(); + + async move { + // fetch id of parent record + let parent_id = + switch_port_configuration_id(&conn, identity).await?; + + // delete child record + let config = diesel::delete( + link_dsl::switch_port_settings_link_config, + ) + .filter(link_dsl::port_settings_id.eq(parent_id)) + .filter(link_dsl::link_name.eq(link_name.to_string())) + .returning(SwitchPortLinkConfig::as_returning()) + .get_result_async(&conn) + .await?; + + // delete lldp service configuration + diesel::delete(lldp_link_dsl::lldp_link_config) + .filter( + lldp_link_dsl::id.eq(config.lldp_link_config_id), + ) + .execute_async(&conn) + .await?; + + Ok(()) + } + }) + .await + .map_err(|e| public_error_from_diesel(e, ErrorHandler::Server))?; + Ok(()) + } + + pub async fn switch_port_configuration_address_list( + &self, + opctx: &OpContext, + configuration: NameOrId, + ) -> ListResultVec { + use db::schema::switch_port_settings as port_settings; + use db::schema::switch_port_settings_address_config as address_config; + + let dataset = + port_settings::table + .inner_join(address_config::table.on( + address_config::port_settings_id.eq(port_settings::id), + )); + + let query = match configuration { + NameOrId::Id(id) => { + // find port config using port settings id + dataset.filter(port_settings::id.eq(id)).into_boxed() + } + NameOrId::Name(name) => { + // find port config using port settings name + dataset + .filter(port_settings::name.eq(name.to_string())) + .into_boxed() + } + }; + + let configs: Vec = query + .select(SwitchPortAddressConfig::as_select()) + .load_async(&*self.pool_connection_authorized(opctx).await?) + .await + .map_err(|e: diesel::result::Error| { + let msg = + "error while looking up interface address configuration"; + match e { + diesel::result::Error::NotFound => { + Error::non_resourcetype_not_found( + "could not find address configuration for switch port configuration: {configuration}, interface: {interface}" + )}, + _ => Error::internal_error(msg), + } + })?; + + Ok(configs) + } + + pub async fn switch_port_configuration_address_add( + &self, + opctx: &OpContext, + configuration: NameOrId, + address: params::AddressAddRemove, + ) -> CreateResult { + use db::schema::address_lot; + use db::schema::switch_port_settings_address_config as address_config; + + let conn = self.pool_connection_authorized(opctx).await?; + let err = OptionalError::new(); + + self.transaction_retry_wrapper( + "switch_port_configuration_interface_address_add", + ) + .transaction(&conn, |conn| { + let parent_configuration = configuration.clone(); + let new_settings = address.clone(); + let err = err.clone(); + + async move { + + // resolve id of port_settings record + let port_settings_id = switch_port_configuration_id(&conn, parent_configuration.clone()) + .await + .map_err(|e: diesel::result::Error| { + match e { + diesel::result::Error::NotFound => { + err.bail( + Error::non_resourcetype_not_found( + format!("unable to lookup configuration with identifier {parent_configuration}") + ) + ) + }, + _ => { + err.bail(Error::internal_error( + "error while looking up configuration for interface address" + )) + }, + } + })?; + + // resolve id of referenced address lot + let address_lot_id = match new_settings.address_lot { + + NameOrId::Id(id) => { + // verify id is valid + address_lot::table + .filter(address_lot::time_deleted.is_null()) + .filter(address_lot::id.eq(id)) + .select(address_lot::id) + .limit(1) + .first_async::(&conn) + .await + .map_err(|e: diesel::result::Error| { + match e { + diesel::result::Error::NotFound => { + err.bail(Error::not_found_by_id(ResourceType::AddressLot, &id)) + }, + _ => { + let message = "error while looking up address lot for interface address"; + error!(opctx.log, "{message}"; "error" => ?e); + err.bail(Error::internal_error(message)) + }, + } + }) + }, + + NameOrId::Name(name) => { + address_lot::table + .filter(address_lot::time_deleted.is_null()) + .filter(address_lot::name.eq(name.to_string())) + .select(address_lot::id) + .limit(1) + .first_async::(&conn) + .await + .map_err(|e: diesel::result::Error| { + match e { + diesel::result::Error::NotFound => { + err.bail(Error::not_found_by_name(ResourceType::AddressLot, &name)) + }, + _ => { + let message = "error while looking up address lot for interface address"; + error!(opctx.log, "{message}"; "error" => ?e); + err.bail(Error::internal_error(message)) + }, + } + }) + } + }?; + + // create a reservation + let (block, rsvd_block) = + crate::db::datastore::address_lot::try_reserve_block( + address_lot_id, + new_settings.address.addr().into(), + // TODO: Should we allow anycast addresses for switch_ports? + // anycast + false, + &conn, + ) + .await + .map_err(|e| match e { + ReserveBlockTxnError::CustomError(e) => { + let message = match e { + ReserveBlockError::AddressUnavailable => "address unavailable", + ReserveBlockError::AddressNotInLot => "address not in lot", + }; + err.bail(Error::conflict(message)) + } + ReserveBlockTxnError::Database(e) => { + let message = "error while reserving address"; + error!(opctx.log, "{message}"; "error" => ?e); + err.bail(Error::internal_error(message)) + }, + })?; + + let address_config = SwitchPortAddressConfig { + port_settings_id, + address_lot_block_id: block.id, + rsvd_address_lot_block_id: rsvd_block.id, + address: new_settings.address.into(), + interface_name: new_settings.interface.to_string(), + vlan_id: new_settings.vlan_id.map(|i| i.into()), + }; + + let address = diesel::insert_into( + address_config::table, + ) + .values(address_config) + .returning(SwitchPortAddressConfig::as_returning()) + .get_result_async(&conn) + .await?; + + Ok(address) + } + }) + .await + .map_err(|e| { + let message = "switch_port_configuration_interface_address_add failed"; + match err.take() { + Some(external_error) => { + error!(opctx.log, "{message}"; "error" => ?external_error); + external_error + }, + None => { + error!(opctx.log, "{message}"; "error" => ?e); + Error::internal_error("error while adding address to interface") + }, + } + }) + } + + pub async fn switch_port_configuration_address_remove( + &self, + opctx: &OpContext, + configuration: NameOrId, + address: params::AddressAddRemove, + ) -> DeleteResult { + use db::schema::switch_port_settings_address_config as address_config; + + let conn = self.pool_connection_authorized(opctx).await?; + let err = OptionalError::new(); + + self.transaction_retry_wrapper( + "switch_port_configuration_interface_address_remove", + ) + .transaction(&conn, |conn| { + let parent_configuration = configuration.clone(); + let settings_to_remove = address.clone(); + let err = err.clone(); + + async move { + + // resolve id of port_settings record + let port_settings_id = switch_port_configuration_id(&conn, parent_configuration.clone()) + .await + .map_err(|e: diesel::result::Error| { + match e { + diesel::result::Error::NotFound => { + err.bail( + Error::non_resourcetype_not_found( + format!("unable to lookup configuration with identifier {parent_configuration}") + ) + ) + }, + _ => { + err.bail(Error::internal_error( + "error while looking up configuration for interface address" + )) + }, + } + })?; + + // find address config + let found_address_config = address_config::table + .filter(address_config::address.eq(IpNetwork::from(settings_to_remove.address))) + .filter(address_config::port_settings_id.eq(port_settings_id)) + .filter(address_config::interface_name.eq(settings_to_remove.interface.clone().to_string())) + .select(SwitchPortAddressConfig::as_select()) + .limit(1) + .first_async::(&conn) + .await + .map_err(|e: diesel::result::Error| { + match e { + diesel::result::Error::NotFound => { + err.bail(Error::non_resourcetype_not_found("unable to find requested address config")) + }, + _ => { + let message = "error while looking up address lot for interface address"; + error!(opctx.log, "{message}"; "error" => ?e); + err.bail(Error::internal_error(message)) + }, + } + })?; + + // delete reservation + use db::schema::address_lot_rsvd_block as rsvd_block; + diesel::delete(rsvd_block::table) + .filter(rsvd_block::id.eq(found_address_config.rsvd_address_lot_block_id)) + .execute_async(&conn) + .await?; + + // delete address config + diesel::delete(address_config::table) + .filter(address_config::address.eq(IpNetwork::from(settings_to_remove.address))) + .filter(address_config::port_settings_id.eq(port_settings_id)) + .filter(address_config::interface_name.eq(settings_to_remove.interface.to_string())) + .execute_async(&conn) + .await?; + + Ok(()) + } + }) + .await + .map_err(|e| { + let message = "switch_port_configuration_interface_address_remove failed"; + match err.take() { + Some(external_error) => { + error!(opctx.log, "{message}"; "error" => ?external_error); + external_error + }, + None => { + error!(opctx.log, "{message}"; "error" => ?e); + Error::internal_error("error while removing address from interface") + }, + } + }) + } + + pub async fn switch_port_configuration_route_list( + &self, + opctx: &OpContext, + configuration: NameOrId, + ) -> ListResultVec { + use db::schema::switch_port_settings as port_settings; + use db::schema::switch_port_settings_route_config as route_config; + + let dataset = port_settings::table.inner_join( + route_config::table + .on(route_config::port_settings_id.eq(port_settings::id)), + ); + + let query = match configuration { + NameOrId::Id(id) => { + // find port config using port settings id + dataset.filter(port_settings::id.eq(id)).into_boxed() + } + NameOrId::Name(name) => { + // find port config using port settings name + dataset + .filter(port_settings::name.eq(name.to_string())) + .into_boxed() + } + }; + + let configs: Vec = query + .select(SwitchPortRouteConfig::as_select()) + .load_async(&*self.pool_connection_authorized(opctx).await?) + .await + .map_err(|e: diesel::result::Error| { + let msg = + "error while looking up interface route configuration"; + match e { + diesel::result::Error::NotFound => { + Error::non_resourcetype_not_found( + "could not find route configuration for switch port configuration: {configuration}, interface: {interface}" + )}, + _ => Error::internal_error(msg), + } + })?; + + Ok(configs) + } + + pub async fn switch_port_configuration_route_add( + &self, + opctx: &OpContext, + configuration: NameOrId, + route: params::RouteAddRemove, + ) -> CreateResult { + use db::schema::switch_port_settings_route_config as route_config; + + let conn = self.pool_connection_authorized(opctx).await?; + let err = OptionalError::new(); + + self.transaction_retry_wrapper( + "switch_port_configuration_interface_route_add", + ) + .transaction(&conn, |conn| { + let parent_configuration = configuration.clone(); + let new_settings = route.clone(); + let err = err.clone(); + + async move { + // resolve id of port_settings record + let port_settings_id = switch_port_configuration_id(&conn, parent_configuration.clone()) + .await + .map_err(|e: diesel::result::Error| { + match e { + diesel::result::Error::NotFound => { + err.bail( + Error::non_resourcetype_not_found( + format!("unable to lookup configuration with identifier {parent_configuration}") + ) + ) + }, + _ => { + err.bail(Error::internal_error( + "error while looking up configuration for interface route" + )) + }, + } + })?; + + let route_config = SwitchPortRouteConfig{ + port_settings_id, + interface_name: new_settings.interface.to_string(), + dst: new_settings.dst.into(), + gw: new_settings.gw.into(), + vid: new_settings.vid.map(Into::into), + local_pref: new_settings.local_pref.map(Into::into), + }; + + let config = diesel::insert_into(route_config::table) + .values(route_config) + .returning(SwitchPortRouteConfig::as_returning()) + .get_result_async(&conn) + .await + .map_err(|e: diesel::result::Error| { + let message = "error while adding route to interface"; + match e { + diesel::result::Error::DatabaseError(kind, _) => { + match kind { + diesel::result::DatabaseErrorKind::UniqueViolation => { + err.bail(Error::conflict("route configuration conflicts with an existing route")) + }, + diesel::result::DatabaseErrorKind::NotNullViolation => { + err.bail(Error::invalid_request("a required field is not populated")) + }, + _ => err.bail(Error::internal_error(message)), + } + }, + _ => err.bail(Error::internal_error(message)), + } + })?; + + Ok(config) + } + }) + .await + .map_err(|e| { + let message = + "switch_port_configuration_interface_route_add failed"; + match err.take() { + Some(external_error) => { + error!(opctx.log, "{message}"; "error" => ?external_error); + external_error + } + None => { + error!(opctx.log, "{message}"; "error" => ?e); + Error::internal_error( + "error while adding route to interface", + ) + } + } + }) + } + + pub async fn switch_port_configuration_route_remove( + &self, + opctx: &OpContext, + configuration: NameOrId, + route: params::RouteAddRemove, + ) -> DeleteResult { + use db::schema::switch_port_settings_route_config as route_config; + + let conn = self.pool_connection_authorized(opctx).await?; + let err = OptionalError::new(); + + self.transaction_retry_wrapper( + "switch_port_configuration_interface_route_remove", + ) + .transaction(&conn, |conn| { + let parent_configuration = configuration.clone(); + let settings_to_remove = route.clone(); + let err = err.clone(); + + async move { + // resolve id of port_settings record + let port_settings_id = switch_port_configuration_id(&conn, parent_configuration.clone()) + .await + .map_err(|e: diesel::result::Error| { + match e { + diesel::result::Error::NotFound => { + err.bail( + Error::non_resourcetype_not_found( + format!("unable to lookup configuration with identifier {parent_configuration}") + ) + ) + }, + _ => { + err.bail(Error::internal_error( + "error while looking up configuration for interface route" + )) + }, + } + })?; + + // delete route config + // PRIMARY KEY (port_settings_id, interface_name, dst, gw) + diesel::delete(route_config::table) + .filter(route_config::dst.eq(IpNetwork::from(settings_to_remove.dst))) + .filter(route_config::gw.eq(IpNetwork::from(settings_to_remove.gw))) + .filter(route_config::port_settings_id.eq(port_settings_id)) + .filter(route_config::interface_name.eq(settings_to_remove.interface.to_string())) + .execute_async(&conn) + .await?; + + Ok(()) + + } + }) + .await + .map_err(|e| { + let message = + "switch_port_configuration_interface_route_remove failed"; + match err.take() { + Some(external_error) => { + error!(opctx.log, "{message}"; "error" => ?external_error); + external_error + } + None => { + error!(opctx.log, "{message}"; "error" => ?e); + Error::internal_error( + "error while removing route from interface", + ) + } + } + }) + } + + pub async fn switch_port_configuration_bgp_peer_list( + &self, + opctx: &OpContext, + configuration: NameOrId, + ) -> ListResultVec { + use db::schema::switch_port_settings as port_settings; + use db::schema::switch_port_settings_bgp_peer_config as bgp_peer_config; + + let conn = self.pool_connection_authorized(opctx).await?; + let err = OptionalError::new(); + + self.transaction_retry_wrapper( + "switch_port_configuration_bgp_peer_list", + ) + .transaction(&conn, |conn| { + let parent_configuration = configuration.clone(); + let err = err.clone(); + + async move { + + let dataset = port_settings::table.inner_join( + bgp_peer_config::table + .on(bgp_peer_config::port_settings_id.eq(port_settings::id)), + ); + + let query = match parent_configuration { + NameOrId::Id(id) => { + // find port config using port settings id + dataset.filter(port_settings::id.eq(id)).into_boxed() + } + NameOrId::Name(name) => { + // find port config using port settings name + dataset + .filter(port_settings::name.eq(name.to_string())) + .into_boxed() + } + }; + + let peers: Vec = query + .select(SwitchPortBgpPeerConfig::as_select()) + .load_async(&conn) + .await + .map_err(|e: diesel::result::Error| { + let message = "error while looking up interface bgp peer configuration"; + error!(opctx.log, "{message}"; "error" => ?e); + match e { + diesel::result::Error::NotFound => { + err.bail(Error::non_resourcetype_not_found( + "could not find bgp peer configuration for switch port configuration: {configuration}, interface: {interface}" + ))}, + _ => err.bail(Error::internal_error(message)), + } + })?; + + + Ok(peers) + } + }) + .await + .map_err(|e| { + let message = + "switch_port_configuration_bgp_peer_list failed"; + match err.take() { + Some(external_error) => { + error!(opctx.log, "{message}"; "error" => ?external_error); + external_error + } + None => { + error!(opctx.log, "{message}"; "error" => ?e); + Error::internal_error( + "error while listing peers for interface configuration", + ) + } + } + }) + } + + pub async fn switch_port_configuration_bgp_peer_add( + &self, + opctx: &OpContext, + configuration: NameOrId, + bgp_peer: BgpPeer, + ) -> CreateResult { + use db::schema::bgp_config; + use db::schema::switch_port_settings_bgp_peer_config as bgp_peer_config; + + let conn = self.pool_connection_authorized(opctx).await?; + let err = OptionalError::new(); + + self.transaction_retry_wrapper( + "switch_port_configuration_interface_bgp_peer_add", + ) + .transaction(&conn, |conn| { + let parent_configuration = configuration.clone(); + let new_settings = bgp_peer.clone(); + let err = err.clone(); + + async move { + // resolve id of port_settings record + let port_settings_id = switch_port_configuration_id(&conn, parent_configuration.clone()) + .await + .map_err(|e: diesel::result::Error| { + match e { + diesel::result::Error::NotFound => { + err.bail( + Error::non_resourcetype_not_found( + format!("unable to lookup configuration with identifier {parent_configuration}") + ) + ) + }, + _ => { + err.bail(Error::internal_error( + "error while looking up configuration for interface bgp peer" + )) + }, + } + })?; + + // resolve id of referenced bgp configuration + let bgp_config_id = match new_settings.bgp_config { + + NameOrId::Id(id) => { + // verify id is valid + bgp_config::table + .filter(bgp_config::time_deleted.is_null()) + .filter(bgp_config::id.eq(id)) + .select(bgp_config::id) + .limit(1) + .first_async::(&conn) + .await + .map_err(|e: diesel::result::Error| { + match e { + diesel::result::Error::NotFound => { + err.bail(Error::not_found_by_id(ResourceType::BgpConfig, &id)) + }, + _ => { + let message = "error while looking up bgp config for bgp peer"; + error!(opctx.log, "{message}"; "error" => ?e); + err.bail(Error::internal_error(message)) + }, + } + }) + }, + + NameOrId::Name(name) => { + bgp_config::table + .filter(bgp_config::time_deleted.is_null()) + .filter(bgp_config::name.eq(name.to_string())) + .select(bgp_config::id) + .limit(1) + .first_async::(&conn) + .await + .map_err(|e: diesel::result::Error| { + match e { + diesel::result::Error::NotFound => { + err.bail(Error::not_found_by_name(ResourceType::BgpConfig, &name)) + }, + _ => { + let message = "error while looking up bgp config for bgp peer"; + error!(opctx.log, "{message}"; "error" => ?e); + err.bail(Error::internal_error(message)) + }, + } + }) + } + }?; + + let bgp_peer_config = SwitchPortBgpPeerConfig { + port_settings_id, + bgp_config_id, + interface_name: new_settings.interface_name, + addr: new_settings.addr.into(), + hold_time: new_settings.hold_time.into(), + idle_hold_time: new_settings.idle_hold_time.into(), + delay_open: new_settings.delay_open.into(), + connect_retry: new_settings.connect_retry.into(), + keepalive: new_settings.keepalive.into(), + remote_asn: new_settings.remote_asn.map(Into::into), + min_ttl: new_settings.min_ttl.map(Into::into), + md5_auth_key: new_settings.md5_auth_key, + multi_exit_discriminator: new_settings.multi_exit_discriminator.map(Into::into), + local_pref: new_settings.local_pref.map(Into::into), + enforce_first_as: new_settings.enforce_first_as, + allow_import_list_active: new_settings.allow_import_list_active, + allow_export_list_active: new_settings.allow_import_list_active, + vlan_id: new_settings.vlan_id.map(Into::into), + }; + + let peer = diesel::insert_into(bgp_peer_config::table) + .values(bgp_peer_config) + .returning(SwitchPortBgpPeerConfig::as_returning()) + .get_result_async(&conn) + .await + .map_err(|e: diesel::result::Error| { + let message = "error while adding bgp peer to interface"; + match e { + diesel::result::Error::DatabaseError(kind, _) => { + match kind { + diesel::result::DatabaseErrorKind::UniqueViolation => { + err.bail(Error::conflict("bgp peer configuration conflicts with an existing configuration")) + }, + diesel::result::DatabaseErrorKind::NotNullViolation => { + err.bail(Error::invalid_request("a required field is not populated")) + }, + _ => err.bail(Error::internal_error(message)), + } + }, + _ => err.bail(Error::internal_error(message)), + } + })?; + + Ok(peer) + } + }) + .await + .map_err(|e| { + let message = + "switch_port_configuration_interface_bgp_peer_add failed"; + match err.take() { + Some(external_error) => { + error!(opctx.log, "{message}"; "error" => ?external_error); + external_error + } + None => { + error!(opctx.log, "{message}"; "error" => ?e); + Error::internal_error( + "error while adding bgp peer to interface", + ) + } + } + }) + } + + pub async fn switch_port_configuration_bgp_peer_remove( + &self, + opctx: &OpContext, + configuration: NameOrId, + bgp_peer: BgpPeerRemove, + ) -> DeleteResult { + use db::schema::switch_port_settings_bgp_peer_config as bgp_peer_config; + + let conn = self.pool_connection_authorized(opctx).await?; + let err = OptionalError::new(); + + self.transaction_retry_wrapper( + "switch_port_configuration_interface_bgp_peer_remove", + ) + .transaction(&conn, |conn| { + let parent_configuration = configuration.clone(); + let settings_to_remove = bgp_peer.clone(); + let err = err.clone(); + + async move { + // resolve id of port_settings record + let port_settings_id = switch_port_configuration_id(&conn, parent_configuration.clone()) + .await + .map_err(|e: diesel::result::Error| { + match e { + diesel::result::Error::NotFound => { + err.bail( + Error::non_resourcetype_not_found( + format!("unable to lookup configuration with identifier {parent_configuration}") + ) + ) + }, + _ => { + err.bail(Error::internal_error( + "error while looking up configuration for interface bgp peer" + )) + }, + } + })?; + + // delete allowed import / export + // PRIMARY KEY (port_settings_id, interface_name, addr, prefix) + use db::schema::switch_port_settings_bgp_peer_config_allow_import as allow_import; + diesel::delete(allow_import::table) + .filter(allow_import::port_settings_id.eq(port_settings_id)) + .filter(allow_import::interface_name.eq(settings_to_remove.interface_name.clone())) + .filter(allow_import::addr.eq(IpNetwork::from(settings_to_remove.addr))) + .execute_async(&conn) + .await + .map_err(|e| { + let message = "error while deleting import list for bgp peer"; + error!(opctx.log, "{message}"; "error" => ?e); + err.bail(Error::internal_error(message)) + })?; + + use db::schema::switch_port_settings_bgp_peer_config_allow_export as allow_export; + diesel::delete(allow_export::table) + .filter(allow_export::port_settings_id.eq(port_settings_id)) + .filter(allow_export::interface_name.eq(settings_to_remove.interface_name.clone())) + .filter(allow_export::addr.eq(IpNetwork::from(settings_to_remove.addr))) + .execute_async(&conn) + .await + .map_err(|e| { + let message = "error while deleting export list for bgp peer"; + error!(opctx.log, "{message}"; "error" => ?e); + err.bail(Error::internal_error(message)) + })?; + + // delete communities + // PRIMARY KEY (port_settings_id, interface_name, addr, community) + use db::schema::switch_port_settings_bgp_peer_config_communities as peer_communities; + diesel::delete(peer_communities::table) + .filter(peer_communities::port_settings_id.eq(port_settings_id)) + .filter(peer_communities::interface_name.eq(settings_to_remove.interface_name.clone())) + .filter(peer_communities::addr.eq(IpNetwork::from(settings_to_remove.addr))) + .execute_async(&conn) + .await + .map_err(|e| { + let message = "error while deleting communities for bgp peer"; + error!(opctx.log, "{message}"; "error" => ?e); + err.bail(Error::internal_error(message)) + })?; + + // delete bgp peer config + // PRIMARY KEY (port_settings_id, interface_name, addr) + diesel::delete(bgp_peer_config::table) + .filter(bgp_peer_config::addr.eq(IpNetwork::from(settings_to_remove.addr))) + .filter(bgp_peer_config::port_settings_id.eq(port_settings_id)) + .filter(bgp_peer_config::interface_name.eq(settings_to_remove.interface_name)) + .execute_async(&conn) + .await + .map_err(|e| { + let message = "error while deleting bgp peer"; + error!(opctx.log, "{message}"; "error" => ?e); + err.bail(Error::internal_error(message)) + })?; + + Ok(()) + + } + }) + .await + .map_err(|e| { + let message = + "switch_port_configuration_interface_bgp_peer_remove failed"; + match err.take() { + Some(external_error) => { + error!(opctx.log, "{message}"; "error" => ?external_error); + external_error + } + None => { + error!(opctx.log, "{message}"; "error" => ?e); + Error::internal_error( + "error while removing bgp peer from interface", + ) + } + } + }) + } + + pub async fn switch_port_configuration_bgp_peer_allow_import_list( + &self, + opctx: &OpContext, + configuration: NameOrId, + bgp_peer: IpAddr, + ) -> ListResultVec { + use db::schema::switch_port_settings as port_settings; + use db::schema::switch_port_settings_bgp_peer_config_allow_import as allow_import; + + let dataset = port_settings::table.inner_join( + allow_import::table + .on(allow_import::port_settings_id.eq(port_settings::id)), + ); + + let query = match configuration { + NameOrId::Id(id) => { + // find port config using port settings id + dataset.filter(port_settings::id.eq(id)).into_boxed() + } + NameOrId::Name(name) => { + // find port config using port settings name + dataset + .filter(port_settings::name.eq(name.to_string())) + .into_boxed() + } + }; + + query + .filter(allow_import::addr.eq(IpNetwork::from(bgp_peer))) + .select(SwitchPortBgpPeerConfigAllowImport::as_select()) + .load_async(&*self.pool_connection_authorized(opctx).await?) + .await + .map_err(|e: diesel::result::Error| { + let msg = "error while looking up bgp peer allowed import list"; + error!(opctx.log, "{msg}"; "error" => ?e); + Error::internal_error(msg) + }) + } + + pub async fn switch_port_configuration_bgp_peer_allow_import_add( + &self, + opctx: &OpContext, + configuration: NameOrId, + bgp_peer: IpAddr, + prefix: AllowedPrefixAddRemove, + ) -> CreateResult { + use db::schema::switch_port_settings_bgp_peer_config_allow_import as allow_import; + + let conn = self.pool_connection_authorized(opctx).await?; + let err = OptionalError::new(); + + self.transaction_retry_wrapper( + "switch_port_configuration_bgp_peer_allow_import_add", + ).transaction(&conn, |conn| { + let parent_configuration = configuration.clone(); + let new_settings = prefix.clone(); + let err = err.clone(); + + async move { + + // resolve id of port_settings record + let port_settings_id = switch_port_configuration_id(&conn, parent_configuration.clone()) + .await + .map_err(|e: diesel::result::Error| { + match e { + diesel::result::Error::NotFound => { + err.bail( + Error::non_resourcetype_not_found( + format!("unable to lookup configuration with identifier {parent_configuration}") + ) + ) + }, + _ => { + err.bail(Error::internal_error( + "error while looking up switch port configuration for bgp peer" + )) + }, + } + })?; + + let allow_import_config = SwitchPortBgpPeerConfigAllowImport { + port_settings_id, + interface_name: new_settings.interface.to_string(), + addr: bgp_peer.into(), + prefix: new_settings.prefix.into(), + }; + + let found_config = allow_import::table + .filter(allow_import::port_settings_id.eq(allow_import_config.port_settings_id)) + .filter(allow_import::interface_name.eq(allow_import_config.interface_name.clone())) + .filter(allow_import::addr.eq(allow_import_config.addr)) + .filter(allow_import::prefix.eq(allow_import_config.prefix)) + .select(SwitchPortBgpPeerConfigAllowImport::as_select()) + .get_result_async(&conn) + .await + .optional()?; + + if let Some(config) = found_config { + return Ok(config) + } + + let config = diesel::insert_into(allow_import::table) + .values(allow_import_config) + .returning(SwitchPortBgpPeerConfigAllowImport::as_returning()) + .get_result_async(&conn) + .await?; + + Ok(config) + } + }) + .await + .map_err(|e| { + let message = "switch_port_configuration_bgp_peer_allow_import_add failed"; + match err.take() { + Some(external_error) => { + error!(opctx.log, "{message}"; "error" => ?external_error); + external_error + }, + None => { + error!(opctx.log, "{message}"; "error" => ?e); + Error::internal_error("error while adding entry to allowed import list") + }, + } + }) + } + + pub async fn switch_port_configuration_bgp_peer_allow_import_remove( + &self, + opctx: &OpContext, + configuration: NameOrId, + bgp_peer: IpAddr, + prefix: AllowedPrefixAddRemove, + ) -> DeleteResult { + let conn = self.pool_connection_authorized(opctx).await?; + let err = OptionalError::new(); + + self.transaction_retry_wrapper( + "switch_port_configuration_bgp_peer_allow_import_remove", + ) + .transaction(&conn, |conn| { + let parent_configuration = configuration.clone(); + let settings_to_remove = prefix.clone(); + let err = err.clone(); + + async move { + // resolve id of port_settings record + let port_settings_id = switch_port_configuration_id(&conn, parent_configuration.clone()) + .await + .map_err(|e: diesel::result::Error| { + match e { + diesel::result::Error::NotFound => { + err.bail( + Error::non_resourcetype_not_found( + format!("unable to lookup configuration with identifier {parent_configuration}") + ) + ) + }, + _ => { + err.bail(Error::internal_error( + "error while looking up configuration for interface bgp peer" + )) + }, + } + })?; + + // delete allowed import + // PRIMARY KEY (port_settings_id, interface_name, addr, prefix) + use db::schema::switch_port_settings_bgp_peer_config_allow_import as allow_import; + diesel::delete(allow_import::table) + .filter(allow_import::port_settings_id.eq(port_settings_id)) + .filter(allow_import::interface_name.eq(settings_to_remove.interface.to_string())) + .filter(allow_import::addr.eq(IpNetwork::from(bgp_peer))) + .filter(allow_import::prefix.eq(IpNetwork::from(settings_to_remove.prefix))) + .execute_async(&conn) + .await + .map_err(|e| { + let message = "error while deleting import list entry for bgp peer"; + error!(opctx.log, "{message}"; "error" => ?e); + err.bail(Error::internal_error(message)) + })?; + Ok(()) + + } + }) + .await + .map_err(|e| { + let message = + "switch_port_configuration_bgp_peer_allow_import_remove failed"; + match err.take() { + Some(external_error) => { + error!(opctx.log, "{message}"; "error" => ?external_error); + external_error + } + None => { + error!(opctx.log, "{message}"; "error" => ?e); + Error::internal_error( + "error while removing entry from allow import list", + ) + } + } + }) + } + + pub async fn switch_port_configuration_bgp_peer_allow_export_list( + &self, + opctx: &OpContext, + configuration: NameOrId, + bgp_peer: IpAddr, + ) -> ListResultVec { + use db::schema::switch_port_settings as port_settings; + use db::schema::switch_port_settings_bgp_peer_config_allow_export as allow_export; + + let dataset = port_settings::table.inner_join( + allow_export::table + .on(allow_export::port_settings_id.eq(port_settings::id)), + ); + + let query = match configuration { + NameOrId::Id(id) => { + // find port config using port settings id + dataset.filter(port_settings::id.eq(id)).into_boxed() + } + NameOrId::Name(name) => { + // find port config using port settings name + dataset + .filter(port_settings::name.eq(name.to_string())) + .into_boxed() + } + }; + + query + .filter(allow_export::addr.eq(IpNetwork::from(bgp_peer))) + .select(SwitchPortBgpPeerConfigAllowExport::as_select()) + .load_async(&*self.pool_connection_authorized(opctx).await?) + .await + .map_err(|e: diesel::result::Error| { + let msg = "error while looking up bgp peer allowed export list"; + error!(opctx.log, "{msg}"; "error" => ?e); + Error::internal_error(msg) + }) + } + + pub async fn switch_port_configuration_bgp_peer_allow_export_add( + &self, + opctx: &OpContext, + configuration: NameOrId, + bgp_peer: IpAddr, + prefix: AllowedPrefixAddRemove, + ) -> CreateResult { + use db::schema::switch_port_settings_bgp_peer_config_allow_export as allow_export; + + let conn = self.pool_connection_authorized(opctx).await?; + let err = OptionalError::new(); + + self.transaction_retry_wrapper( + "switch_port_configuration_bgp_peer_allow_export_add", + ) + .transaction(&conn, |conn| { + let parent_configuration = configuration.clone(); + let new_settings = prefix.clone(); + let err = err.clone(); + + async move { + + // resolve id of port_settings record + let port_settings_id = switch_port_configuration_id(&conn, parent_configuration.clone()) + .await + .map_err(|e: diesel::result::Error| { + match e { + diesel::result::Error::NotFound => { + err.bail( + Error::non_resourcetype_not_found( + format!("unable to lookup configuration with identifier {parent_configuration}") + ) + ) + }, + _ => { + err.bail(Error::internal_error( + "error while looking up switch port configuration for bgp peer" + )) + }, + } + })?; + + let allow_export_config = SwitchPortBgpPeerConfigAllowExport { + port_settings_id, + interface_name: new_settings.interface.to_string(), + addr: bgp_peer.into(), + prefix: new_settings.prefix.into(), + }; + + let found_config = allow_export::table + .filter(allow_export::port_settings_id.eq(allow_export_config.port_settings_id)) + .filter(allow_export::interface_name.eq(allow_export_config.interface_name.clone())) + .filter(allow_export::addr.eq(allow_export_config.addr)) + .filter(allow_export::prefix.eq(allow_export_config.prefix)) + .select(SwitchPortBgpPeerConfigAllowExport::as_select()) + .get_result_async(&conn) + .await + .optional()?; + + if let Some(config) = found_config { + return Ok(config) + } + + let config = diesel::insert_into(allow_export::table) + .values(allow_export_config) + .returning(SwitchPortBgpPeerConfigAllowExport::as_returning()) + .get_result_async(&conn) + .await?; + + Ok(config) + } + }) + .await + .map_err(|e| { + let message = "switch_port_configuration_bgp_peer_allow_export_add failed"; + match err.take() { + Some(external_error) => { + error!(opctx.log, "{message}"; "error" => ?external_error); + external_error + }, + None => { + error!(opctx.log, "{message}"; "error" => ?e); + Error::internal_error("error while adding entry to allowed export list") + }, + } + }) + } + + pub async fn switch_port_configuration_bgp_peer_allow_export_remove( &self, opctx: &OpContext, - params: ¶ms::SwitchPortSettingsSelector, + configuration: NameOrId, + bgp_peer: IpAddr, + prefix: AllowedPrefixAddRemove, ) -> DeleteResult { let conn = self.pool_connection_authorized(opctx).await?; - - let selector = match ¶ms.port_settings { - None => return Err(Error::invalid_request("name or id required")), - Some(name_or_id) => name_or_id, - }; - let err = OptionalError::new(); - // TODO https://github.com/oxidecomputer/omicron/issues/2811 - // Audit external networking database transaction usage - self.transaction_retry_wrapper("switch_port_settings_delete") + self.transaction_retry_wrapper( + "switch_port_configuration_bgp_peer_allow_export_remove", + ) .transaction(&conn, |conn| { + let parent_configuration = configuration.clone(); + let settings_to_remove = prefix.clone(); let err = err.clone(); + async move { - do_switch_port_settings_delete(&conn, &selector, err).await - } - }) - .await - .map_err(|e| { - if let Some(err) = err.take() { - match err { - SwitchPortSettingsDeleteError::SwitchPortSettingsNotFound => { - Error::invalid_request("port settings not found") - } - } - } else { - let name = match ¶ms.port_settings { - Some(name_or_id) => name_or_id.to_string(), - None => String::new(), - }; - public_error_from_diesel( - e, - ErrorHandler::Conflict( - ResourceType::SwitchPortSettings, - &name, - ), - ) - } - }) - } + // resolve id of port_settings record + let port_settings_id = switch_port_configuration_id(&conn, parent_configuration.clone()) + .await + .map_err(|e: diesel::result::Error| { + match e { + diesel::result::Error::NotFound => { + err.bail( + Error::non_resourcetype_not_found( + format!("unable to lookup configuration with identifier {parent_configuration}") + ) + ) + }, + _ => { + err.bail(Error::internal_error( + "error while looking up configuration for interface bgp peer" + )) + }, + } + })?; - pub async fn switch_port_settings_update( - &self, - opctx: &OpContext, - params: ¶ms::SwitchPortSettingsCreate, - id: Uuid, - ) -> UpdateResult { - let delete_err = OptionalError::new(); - let create_err = OptionalError::new(); - let conn = self.pool_connection_authorized(opctx).await?; + // delete allowed export + // PRIMARY KEY (port_settings_id, interface_name, addr, prefix) + use db::schema::switch_port_settings_bgp_peer_config_allow_export as allow_export; + diesel::delete(allow_export::table) + .filter(allow_export::port_settings_id.eq(port_settings_id)) + .filter(allow_export::interface_name.eq(settings_to_remove.interface.to_string())) + .filter(allow_export::addr.eq(IpNetwork::from(bgp_peer))) + .filter(allow_export::prefix.eq(IpNetwork::from(settings_to_remove.prefix))) + .execute_async(&conn) + .await + .map_err(|e| { + let message = "error while deleting export list entry for bgp peer"; + error!(opctx.log, "{message}"; "error" => ?e); + err.bail(Error::internal_error(message)) + })?; + Ok(()) - // TODO https://github.com/oxidecomputer/omicron/issues/2811 - // Audit external networking database transaction usage - self.transaction_retry_wrapper("switch_port_settings_update") - .transaction(&conn, |conn| { - let delete_err = delete_err.clone(); - let create_err = create_err.clone(); - let selector = NameOrId::Id(id); - async move { - do_switch_port_settings_delete(&conn, &selector, delete_err).await?; - do_switch_port_settings_create(&conn, Some(id), params, create_err).await } }) .await .map_err(|e| { - if let Some(err) = delete_err.take() { - match err { - SwitchPortSettingsDeleteError::SwitchPortSettingsNotFound => { - Error::invalid_request("port settings not found") - } + let message = + "switch_port_configuration_bgp_peer_allow_export_remove failed"; + match err.take() { + Some(external_error) => { + error!(opctx.log, "{message}"; "error" => ?external_error); + external_error } - } - else if let Some(err) = create_err.take() { - match err { - SpsCreateError::AddressLotNotFound => { - Error::invalid_request("AddressLot not found") - } - SpsCreateError::BgpConfigNotFound => { - Error::invalid_request("BGP config not found") - } - SwitchPortSettingsCreateError::ReserveBlock( - ReserveBlockError::AddressUnavailable, - ) => Error::invalid_request("address unavailable"), - SwitchPortSettingsCreateError::ReserveBlock( - ReserveBlockError::AddressNotInLot, - ) => Error::invalid_request("address not in lot"), - + None => { + error!(opctx.log, "{message}"; "error" => ?e); + Error::internal_error( + "error while removing entry from allow export list", + ) } } - else { - public_error_from_diesel(e, ErrorHandler::Server) - } }) } - pub async fn switch_port_settings_list( + pub async fn switch_port_configuration_bgp_peer_community_list( &self, opctx: &OpContext, - pagparams: &PaginatedBy<'_>, - ) -> ListResultVec { - use db::schema::switch_port_settings::dsl; + configuration: NameOrId, + bgp_peer: IpAddr, + ) -> ListResultVec { + use db::schema::switch_port_settings as port_settings; + use db::schema::switch_port_settings_bgp_peer_config_communities as communities; + + let dataset = port_settings::table.inner_join( + communities::table + .on(communities::port_settings_id.eq(port_settings::id)), + ); - match pagparams { - PaginatedBy::Id(pagparams) => { - paginated(dsl::switch_port_settings, dsl::id, &pagparams) + let query = match configuration { + NameOrId::Id(id) => { + // find port config using port settings id + dataset.filter(port_settings::id.eq(id)).into_boxed() } - PaginatedBy::Name(pagparams) => paginated( - dsl::switch_port_settings, - dsl::name, - &pagparams.map_name(|n| Name::ref_cast(n)), - ), - } - .filter(dsl::time_deleted.is_null()) - .select(SwitchPortSettings::as_select()) - .load_async(&*self.pool_connection_authorized(opctx).await?) - .await - .map_err(|e| public_error_from_diesel(e, ErrorHandler::Server)) + NameOrId::Name(name) => { + // find port config using port settings name + dataset + .filter(port_settings::name.eq(name.to_string())) + .into_boxed() + } + }; + + query + .filter(communities::addr.eq(IpNetwork::from(bgp_peer))) + .select(SwitchPortBgpPeerConfigCommunity::as_select()) + .load_async(&*self.pool_connection_authorized(opctx).await?) + .await + .map_err(|e: diesel::result::Error| { + let msg = "error while looking up bgp peer community list"; + error!(opctx.log, "{msg}"; "error" => ?e); + Error::internal_error(msg) + }) } - pub async fn switch_port_settings_get( + pub async fn switch_port_configuration_bgp_peer_community_add( &self, opctx: &OpContext, - name_or_id: &NameOrId, - ) -> LookupResult { - #[derive(Debug)] - enum SwitchPortSettingsGetError { - NotFound(external::Name), - } + configuration: NameOrId, + bgp_peer: IpAddr, + community: BgpCommunityAddRemove, + ) -> CreateResult { + use db::schema::switch_port_settings_bgp_peer_config_communities as communities; - let err = OptionalError::new(); let conn = self.pool_connection_authorized(opctx).await?; + let err = OptionalError::new(); - // TODO https://github.com/oxidecomputer/omicron/issues/2811 - // Audit external networking database transaction usage - self.transaction_retry_wrapper("switch_port_settings_get") + self.transaction_retry_wrapper( + "switch_port_configuration_bgp_peer_community_add", + ) .transaction(&conn, |conn| { + let parent_configuration = configuration.clone(); + let new_settings = community.clone(); let err = err.clone(); - async move { - // get the top level port settings object - use db::schema::switch_port_settings::{ - self, dsl as port_settings_dsl, - }; - use db::schema::{ - switch_port_settings_bgp_peer_config_allow_import::dsl as allow_import_dsl, - switch_port_settings_bgp_peer_config_allow_export::dsl as allow_export_dsl, - switch_port_settings_bgp_peer_config_communities::dsl as bgp_communities_dsl, - }; - - let id = match name_or_id { - NameOrId::Id(id) => *id, - NameOrId::Name(name) => { - let name_str = name.to_string(); - port_settings_dsl::switch_port_settings - .filter(switch_port_settings::time_deleted.is_null()) - .filter(switch_port_settings::name.eq(name_str)) - .select(switch_port_settings::id) - .limit(1) - .first_async::(&conn) - .await - .map_err(|diesel_error| { - err.bail_retryable_or_else(diesel_error, |_| { - SwitchPortSettingsGetError::NotFound( - name.clone(), - ) - }) - })? - } - }; - - let settings: SwitchPortSettings = - port_settings_dsl::switch_port_settings - .filter(switch_port_settings::time_deleted.is_null()) - .filter(switch_port_settings::id.eq(id)) - .select(SwitchPortSettings::as_select()) - .limit(1) - .first_async::(&conn) - .await?; - - // get the port config - use db::schema::switch_port_settings_port_config::{ - self as port_config, dsl as port_config_dsl, - }; - let port: SwitchPortConfig = - port_config_dsl::switch_port_settings_port_config - .filter(port_config::port_settings_id.eq(id)) - .select(SwitchPortConfig::as_select()) - .limit(1) - .first_async::(&conn) - .await?; - - // initialize result - let mut result = - SwitchPortSettingsCombinedResult::new(settings, port); - - // get the link configs - use db::schema::switch_port_settings_link_config::{ - self as link_config, dsl as link_config_dsl, - }; - - result.links = link_config_dsl::switch_port_settings_link_config - .filter(link_config::port_settings_id.eq(id)) - .select(SwitchPortLinkConfig::as_select()) - .load_async::(&conn) - .await?; - - let lldp_link_ids: Vec = result - .links - .iter() - .map(|link| link.lldp_link_config_id) - .collect(); - - use db::schema::lldp_link_config; - result.link_lldp = lldp_link_config::dsl::lldp_link_config - .filter(lldp_link_config::id.eq_any(lldp_link_ids)) - .select(LldpLinkConfig::as_select()) - .limit(1) - .load_async::(&conn) - .await?; - - // get the interface configs - use db::schema::switch_port_settings_interface_config::{ - self as interface_config, dsl as interface_config_dsl, - }; - - result.interfaces = - interface_config_dsl::switch_port_settings_interface_config - .filter(interface_config::port_settings_id.eq(id)) - .select(SwitchInterfaceConfig::as_select()) - .load_async::(&conn) - .await?; - use db::schema::switch_vlan_interface_config as vlan_config; - use db::schema::switch_vlan_interface_config::dsl as vlan_dsl; - let interface_ids: Vec = result - .interfaces - .iter() - .map(|interface| interface.id) - .collect(); + async move { - result.vlan_interfaces = vlan_dsl::switch_vlan_interface_config - .filter(vlan_config::interface_config_id.eq_any(interface_ids)) - .select(SwitchVlanInterfaceConfig::as_select()) - .load_async::(&conn) - .await?; + // resolve id of port_settings record + let port_settings_id = switch_port_configuration_id(&conn, parent_configuration.clone()) + .await + .map_err(|e: diesel::result::Error| { + match e { + diesel::result::Error::NotFound => { + err.bail( + Error::non_resourcetype_not_found( + format!("unable to lookup configuration with identifier {parent_configuration}") + ) + ) + }, + _ => { + err.bail(Error::internal_error( + "error while looking up switch port configuration for bgp peer" + )) + }, + } + })?; - // get the route configs - use db::schema::switch_port_settings_route_config::{ - self as route_config, dsl as route_config_dsl, - }; + let community_config = SwitchPortBgpPeerConfigCommunity { + port_settings_id, + interface_name: new_settings.interface.to_string(), + addr: bgp_peer.into(), + community: new_settings.community.into(), + }; - result.routes = route_config_dsl::switch_port_settings_route_config - .filter(route_config::port_settings_id.eq(id)) - .select(SwitchPortRouteConfig::as_select()) - .load_async::(&conn) - .await?; + let found_config = communities::table + .filter(communities::port_settings_id.eq(community_config.port_settings_id)) + .filter(communities::interface_name.eq(community_config.interface_name.clone())) + .filter(communities::addr.eq(community_config.addr)) + .filter(communities::community.eq(community_config.community)) + .select(SwitchPortBgpPeerConfigCommunity::as_select()) + .get_result_async(&conn) + .await + .optional()?; - // get the bgp peer configs - use db::schema::switch_port_settings_bgp_peer_config::{ - self as bgp_peer, dsl as bgp_peer_dsl, - }; + if let Some(config) = found_config { + return Ok(config) + } - let peers: Vec = - bgp_peer_dsl::switch_port_settings_bgp_peer_config - .filter(bgp_peer::port_settings_id.eq(id)) - .select(SwitchPortBgpPeerConfig::as_select()) - .load_async::(&conn) + let config = diesel::insert_into(communities::table) + .values(community_config) + .returning(SwitchPortBgpPeerConfigCommunity::as_returning()) + .get_result_async(&conn) .await?; - for p in peers.iter() { - let allowed_import: ImportExportPolicy = if p.allow_import_list_active { - let db_list: Vec = - allow_import_dsl::switch_port_settings_bgp_peer_config_allow_import - .filter(allow_import_dsl::port_settings_id.eq(id)) - .filter(allow_import_dsl::interface_name.eq(p.interface_name.clone())) - .filter(allow_import_dsl::addr.eq(p.addr)) - .select(SwitchPortBgpPeerConfigAllowImport::as_select()) - .load_async::(&conn) - .await?; + Ok(config) + } + }) + .await + .map_err(|e| { + let message = "switch_port_configuration_bgp_peer_community_add failed"; + match err.take() { + Some(external_error) => { + error!(opctx.log, "{message}"; "error" => ?external_error); + external_error + }, + None => { + error!(opctx.log, "{message}"; "error" => ?e); + Error::internal_error("error while adding community to bgp peer") + }, + } + }) + } - ImportExportPolicy::Allow(db_list - .into_iter() - .map(|x| x.prefix.into()) - .collect() - ) - } else { - ImportExportPolicy::NoFiltering - }; + pub async fn switch_port_configuration_bgp_peer_community_remove( + &self, + opctx: &OpContext, + configuration: NameOrId, + bgp_peer: IpAddr, + community: BgpCommunityAddRemove, + ) -> DeleteResult { + let conn = self.pool_connection_authorized(opctx).await?; + let err = OptionalError::new(); - let allowed_export: ImportExportPolicy = if p.allow_export_list_active { - let db_list: Vec = - allow_export_dsl::switch_port_settings_bgp_peer_config_allow_export - .filter(allow_export_dsl::port_settings_id.eq(id)) - .filter(allow_export_dsl::interface_name.eq(p.interface_name.clone())) - .filter(allow_export_dsl::addr.eq(p.addr)) - .select(SwitchPortBgpPeerConfigAllowExport::as_select()) - .load_async::(&conn) - .await?; + self.transaction_retry_wrapper( + "switch_port_configuration_bgp_peer_community_remove", + ) + .transaction(&conn, |conn| { + let parent_configuration = configuration.clone(); + let settings_to_remove = community.clone(); + let err = err.clone(); - ImportExportPolicy::Allow(db_list - .into_iter() - .map(|x| x.prefix.into()) - .collect() - ) - } else { - ImportExportPolicy::NoFiltering - }; + async move { + // resolve id of port_settings record + let port_settings_id = switch_port_configuration_id(&conn, parent_configuration.clone()) + .await + .map_err(|e: diesel::result::Error| { + match e { + diesel::result::Error::NotFound => { + err.bail( + Error::non_resourcetype_not_found( + format!("unable to lookup configuration with identifier {parent_configuration}") + ) + ) + }, + _ => { + err.bail(Error::internal_error( + "error while looking up configuration for interface bgp peer" + )) + }, + } + })?; - let communities: Vec = - bgp_communities_dsl::switch_port_settings_bgp_peer_config_communities - .filter(bgp_communities_dsl::port_settings_id.eq(id)) - .filter(bgp_communities_dsl::interface_name.eq(p.interface_name.clone())) - .filter(bgp_communities_dsl::addr.eq(p.addr)) - .select(SwitchPortBgpPeerConfigCommunity::as_select()) - .load_async::(&conn) - .await?; + // delete communities + // PRIMARY KEY (port_settings_id, interface_name, addr, community) + use db::schema::switch_port_settings_bgp_peer_config_communities as peer_communities; + diesel::delete(peer_communities::table) + .filter(peer_communities::port_settings_id.eq(port_settings_id)) + .filter(peer_communities::interface_name.eq(settings_to_remove.interface.to_string())) + .filter(peer_communities::addr.eq(IpNetwork::from(bgp_peer))) + .filter(peer_communities::community.eq(SqlU32::from(settings_to_remove.community))) + .execute_async(&conn) + .await + .map_err(|e| { + let message = "error while deleting community entry for bgp peer"; + error!(opctx.log, "{message}"; "error" => ?e); + err.bail(Error::internal_error(message)) + })?; - let view = BgpPeerConfig { - port_settings_id: p.port_settings_id, - bgp_config_id: p.bgp_config_id, - interface_name: p.interface_name.clone(), - addr: p.addr, - hold_time: p.hold_time, - idle_hold_time: p.idle_hold_time, - delay_open: p.delay_open, - connect_retry: p.connect_retry, - keepalive: p.keepalive, - remote_asn: p.remote_asn, - min_ttl: p.min_ttl, - md5_auth_key: p.md5_auth_key.clone(), - multi_exit_discriminator: p.multi_exit_discriminator, - local_pref: p.local_pref, - enforce_first_as: p.enforce_first_as, - vlan_id: p.vlan_id, - communities: communities.into_iter().map(|c| c.community.0).collect(), - allowed_import, - allowed_export, - }; + Ok(()) - result.bgp_peers.push(view); } - - // get the address configs - use db::schema::switch_port_settings_address_config::{ - self as address_config, dsl as address_config_dsl, - }; - - result.addresses = - address_config_dsl::switch_port_settings_address_config - .filter(address_config::port_settings_id.eq(id)) - .select(SwitchPortAddressConfig::as_select()) - .load_async::(&conn) - .await?; - - Ok(result) - } - }) - .await - .map_err(|e| { - if let Some(err) = err.take() { - match err { - SwitchPortSettingsGetError::NotFound(name) => { - Error::not_found_by_name( - ResourceType::SwitchPortSettings, - &name, + }) + .await + .map_err(|e| { + let message = + "switch_port_configuration_bgp_peer_community_remove failed"; + match err.take() { + Some(external_error) => { + error!(opctx.log, "{message}"; "error" => ?external_error); + external_error + } + None => { + error!(opctx.log, "{message}"; "error" => ?e); + Error::internal_error( + "error while removing entry from community list", ) } } - } else { - let name = name_or_id.to_string(); - public_error_from_diesel( - e, - ErrorHandler::Conflict( - ResourceType::SwitchPortSettings, - &name, - ), - ) - } - }) + }) } // switch ports @@ -1064,7 +2819,7 @@ impl DataStore { // pagination in the future, or maybe a way to constrain the query to // a rack? .limit(64) - .union( + .union_all( switch_port_dsl::switch_port .filter(switch_port_dsl::port_settings_id.is_not_null()) .inner_join( @@ -1249,7 +3004,7 @@ async fn do_switch_port_settings_create( .get_results_async(conn) .await?; - let mut peer_by_addr: BTreeMap = + let mut peer_by_addr: BTreeMap = BTreeMap::new(); let mut bgp_peer_config = Vec::new(); @@ -1617,6 +3372,59 @@ async fn do_switch_port_settings_delete( Ok(()) } +// TODO: refactor to emit more detailed errors +async fn switch_port_configuration_id( + conn: &async_bb8_diesel::Connection>, + name_or_id: NameOrId, +) -> diesel::result::QueryResult { + use db::schema::switch_port_settings as port_settings; + use db::schema::switch_port_settings::dsl as port_settings_dsl; + + let dataset = port_settings_dsl::switch_port_settings; + + let query = match name_or_id { + NameOrId::Id(id) => { + // find port config using port settings id + dataset.filter(port_settings::id.eq(id)).into_boxed() + } + NameOrId::Name(name) => { + // find port config using port settings name + dataset + .filter(port_settings::name.eq(name.to_string())) + .into_boxed() + } + }; + + // get settings id + query.select(port_settings_dsl::id).limit(1).first_async(conn).await +} + +async fn lldp_configuration_id( + conn: &async_bb8_diesel::Connection>, + name_or_id: NameOrId, +) -> diesel::result::QueryResult { + use db::schema::lldp_link_config; + use db::schema::lldp_link_config::dsl as lldp_link_config_dsl; + + let dataset = lldp_link_config_dsl::lldp_link_config; + + let query = match name_or_id { + NameOrId::Id(id) => { + // find port config using port settings id + dataset.filter(lldp_link_config::id.eq(id)).into_boxed() + } + NameOrId::Name(name) => { + // find port config using port settings name + dataset + .filter(lldp_link_config::link_name.eq(name.to_string())) + .into_boxed() + } + }; + + // get settings id + query.select(lldp_link_config_dsl::id).limit(1).first_async(conn).await +} + #[cfg(test)] mod test { use crate::db::datastore::test_utils::datastore_test; @@ -1627,8 +3435,8 @@ mod test { SwitchPortConfigCreate, SwitchPortGeometry, SwitchPortSettingsCreate, }; use omicron_common::api::external::{ - BgpPeer, IdentityMetadataCreateParams, ImportExportPolicy, Name, - NameOrId, + BgpPeerCombined, IdentityMetadataCreateParams, ImportExportPolicy, + Name, NameOrId, }; use omicron_test_utils::dev; use std::collections::HashMap; @@ -1691,7 +3499,7 @@ mod test { bgp_peers: HashMap::from([( "phy0".into(), BgpPeerConfig { - peers: vec![BgpPeer { + peers: vec![BgpPeerCombined { bgp_config: NameOrId::Name( "test-bgp-config".parse().unwrap(), ), diff --git a/nexus/src/app/address_lot.rs b/nexus/src/app/address_lot.rs index 847021bdd4..8757f6c92d 100644 --- a/nexus/src/app/address_lot.rs +++ b/nexus/src/app/address_lot.rs @@ -7,7 +7,6 @@ use db::model::{AddressLot, AddressLotBlock}; use nexus_db_queries::authz; use nexus_db_queries::context::OpContext; use nexus_db_queries::db; -use nexus_db_queries::db::datastore::AddressLotCreateResult; use nexus_db_queries::db::lookup; use nexus_db_queries::db::lookup::LookupPath; use omicron_common::api::external::http_pagination::PaginatedBy; @@ -45,9 +44,8 @@ impl super::Nexus { self: &Arc, opctx: &OpContext, params: params::AddressLotCreate, - ) -> CreateResult { + ) -> CreateResult { opctx.authorize(authz::Action::CreateChild, &authz::FLEET).await?; - validate_blocks(¶ms)?; self.db_datastore.address_lot_create(opctx, ¶ms).await } @@ -70,6 +68,32 @@ impl super::Nexus { self.db_datastore.address_lot_list(opctx, pagparams).await } + pub(crate) async fn address_lot_block_create( + self: &Arc, + opctx: &OpContext, + address_lot_id: Uuid, + block: params::AddressLotBlockAddRemove, + ) -> CreateResult { + opctx.authorize(authz::Action::CreateChild, &authz::FLEET).await?; + validate_block(&block)?; + self.db_datastore + .address_lot_block_create(opctx, address_lot_id, block) + .await + } + + pub(crate) async fn address_lot_block_delete( + self: &Arc, + opctx: &OpContext, + address_lot_id: Uuid, + block: params::AddressLotBlockAddRemove, + ) -> DeleteResult { + opctx.authorize(authz::Action::Delete, &authz::FLEET).await?; + validate_block(&block)?; + self.db_datastore + .address_lot_block_delete(opctx, address_lot_id, block) + .await + } + pub(crate) async fn address_lot_block_list( self: &Arc, opctx: &OpContext, @@ -84,20 +108,20 @@ impl super::Nexus { } } -fn validate_blocks(lot: ¶ms::AddressLotCreate) -> Result<(), Error> { - for b in &lot.blocks { - match (&b.first_address, &b.last_address) { - (IpAddr::V4(first), IpAddr::V4(last)) => { - validate_v4_block(first, last)? - } - (IpAddr::V6(first), IpAddr::V6(last)) => { - validate_v6_block(first, last)? - } - _ => { - return Err(Error::invalid_request( - "Block bounds must be in same address family", - )); - } +fn validate_block( + block: ¶ms::AddressLotBlockAddRemove, +) -> Result<(), Error> { + match (&block.first_address, &block.last_address) { + (IpAddr::V4(first), IpAddr::V4(last)) => { + validate_v4_block(first, last)? + } + (IpAddr::V6(first), IpAddr::V6(last)) => { + validate_v6_block(first, last)? + } + _ => { + return Err(Error::invalid_request( + "Block bounds must be in same address family", + )); } } Ok(()) diff --git a/nexus/src/app/bgp.rs b/nexus/src/app/bgp.rs index 31a0faa663..1b027b94f5 100644 --- a/nexus/src/app/bgp.rs +++ b/nexus/src/app/bgp.rs @@ -47,7 +47,7 @@ impl super::Nexus { pub async fn bgp_config_delete( &self, opctx: &OpContext, - sel: ¶ms::BgpConfigSelector, + sel: &NameOrId, ) -> DeleteResult { opctx.authorize(authz::Action::Modify, &authz::FLEET).await?; let result = self.db_datastore.bgp_config_delete(opctx, sel).await?; diff --git a/nexus/src/app/rack.rs b/nexus/src/app/rack.rs index f3c0031327..52c2351641 100644 --- a/nexus/src/app/rack.rs +++ b/nexus/src/app/rack.rs @@ -28,7 +28,7 @@ use nexus_types::deployment::CockroachDbClusterVersion; use nexus_types::deployment::SledFilter; use nexus_types::external_api::params::Address; use nexus_types::external_api::params::AddressConfig; -use nexus_types::external_api::params::AddressLotBlockCreate; +use nexus_types::external_api::params::AddressLotBlockAddRemove; use nexus_types::external_api::params::BgpAnnounceSetCreate; use nexus_types::external_api::params::BgpAnnouncementCreate; use nexus_types::external_api::params::BgpConfigCreate; @@ -47,10 +47,11 @@ use nexus_types::external_api::shared::SiloIdentityMode; use nexus_types::external_api::shared::SiloRole; use nexus_types::external_api::shared::UninitializedSled; use nexus_types::external_api::views; +use nexus_types::identity::Resource; use nexus_types::internal_api::params::DnsRecord; use omicron_common::address::{get_64_subnet, Ipv6Subnet, RACK_PREFIX}; use omicron_common::api::external::AddressLotKind; -use omicron_common::api::external::BgpPeer; +use omicron_common::api::external::BgpPeerCombined; use omicron_common::api::external::DataPageParams; use omicron_common::api::external::Error; use omicron_common::api::external::IdentityMetadataCreateParams; @@ -401,26 +402,47 @@ impl super::Nexus { let first_address = IpAddr::V4(rack_network_config.infra_ip_first); let last_address = IpAddr::V4(rack_network_config.infra_ip_last); - let ipv4_block = AddressLotBlockCreate { first_address, last_address }; + let ipv4_block = + AddressLotBlockAddRemove { first_address, last_address }; - let blocks = vec![ipv4_block]; + let address_lot_params = AddressLotCreate { identity, kind }; - let address_lot_params = AddressLotCreate { identity, kind, blocks }; - - match self + let address_lot_id = match self .db_datastore .address_lot_create(opctx, &address_lot_params) .await { - Ok(_) => Ok(()), + Ok(v) => Ok(v.id()), Err(e) => match e { Error::ObjectAlreadyExists { type_name: _, object_name: _ } => { - Ok(()) + let address_lot_lookup = self.address_lot_lookup( + &opctx, + NameOrId::Name(address_lot_name.clone()), + )?; + + let (.., authz_address_lot) = address_lot_lookup + .lookup_for(authz::Action::CreateChild) + .await?; + Ok(authz_address_lot.id()) } _ => Err(e), }, }?; + match self + .db_datastore + .address_lot_block_create(opctx, address_lot_id, ipv4_block.clone()) + .await + { + Ok(_) => Ok(()), + Err(e) => match e { + Error::ObjectAlreadyExists { .. } => Ok(()), + _ => Err(Error::internal_error(&format!( + "unable to create block for address lot {address_lot_id}: {e}", + ))), + }, + }?; + let mut bgp_configs = HashMap::new(); for bgp_config in &rack_network_config.bgp { @@ -435,34 +457,36 @@ impl super::Nexus { let address_lot_name: Name = format!("as{}-lot", bgp_config.asn).parse().unwrap(); - match self + let address_lot_id = match self .db_datastore .address_lot_create( &opctx, &AddressLotCreate { identity: IdentityMetadataCreateParams { - name: address_lot_name, + name: address_lot_name.clone(), description: format!( "Address lot for announce set in as {}", bgp_config.asn ), }, kind: AddressLotKind::Infra, - blocks: bgp_config - .originate - .iter() - .map(|o| AddressLotBlockCreate { - first_address: o.first_addr().into(), - last_address: o.last_addr().into(), - }) - .collect(), }, ) .await { - Ok(_) => Ok(()), + Ok(v) => Ok(v.id()), Err(e) => match e { - Error::ObjectAlreadyExists { .. } => Ok(()), + Error::ObjectAlreadyExists { .. } => { + let address_lot_lookup = self.address_lot_lookup( + &opctx, + NameOrId::Name(address_lot_name), + )?; + + let (.., authz_address_lot) = address_lot_lookup + .lookup_for(authz::Action::CreateChild) + .await?; + Ok(authz_address_lot.id()) + } _ => Err(Error::internal_error(&format!( "unable to create address lot for BGP as {}: {e}", bgp_config.asn @@ -470,6 +494,30 @@ impl super::Nexus { }, }?; + for net in &bgp_config.originate { + match self + .db_datastore + .address_lot_block_create( + &opctx, + address_lot_id, + AddressLotBlockAddRemove { + first_address: net.first_addr().into(), + last_address: net.last_addr().into(), + }, + ) + .await + { + Ok(_) => Ok(()), + Err(e) => match e { + Error::ObjectAlreadyExists { .. } => Ok(()), + _ => Err(Error::internal_error(&format!( + "unable to create address lot block for BGP as {}: {e}", + bgp_config.asn + ))), + }, + }?; + } + match self .db_datastore .bgp_create_announce_set( @@ -601,10 +649,10 @@ impl super::Nexus { .routes .insert("phy0".to_string(), RouteConfig { routes }); - let peers: Vec = uplink_config + let peers: Vec = uplink_config .bgp_peers .iter() - .map(|r| BgpPeer { + .map(|r| BgpPeerCombined { bgp_config: NameOrId::Name( format!("as{}", r.asn).parse().unwrap(), ), diff --git a/nexus/src/app/switch_port.rs b/nexus/src/app/switch_port.rs index 9726a59d33..7fe3e091e6 100644 --- a/nexus/src/app/switch_port.rs +++ b/nexus/src/app/switch_port.rs @@ -8,18 +8,32 @@ use db::datastore::SwitchPortSettingsCombinedResult; use dpd_client::types::LinkId; use dpd_client::types::PortId; use http::StatusCode; +use nexus_db_model::SwitchPortAddressConfig; +use nexus_db_model::SwitchPortBgpPeerConfig; +use nexus_db_model::SwitchPortBgpPeerConfigAllowExport; +use nexus_db_model::SwitchPortBgpPeerConfigAllowImport; +use nexus_db_model::SwitchPortBgpPeerConfigCommunity; +use nexus_db_model::SwitchPortConfig; +use nexus_db_model::SwitchPortGeometry; +use nexus_db_model::SwitchPortLinkConfig; +use nexus_db_model::SwitchPortRouteConfig; use nexus_db_queries::authz; use nexus_db_queries::context::OpContext; use nexus_db_queries::db; use nexus_db_queries::db::datastore::UpdatePrecondition; use nexus_db_queries::db::model::{SwitchPort, SwitchPortSettings}; use nexus_db_queries::db::DataStore; +use nexus_types::external_api::params::AllowedPrefixAddRemove; +use nexus_types::external_api::params::BgpCommunityAddRemove; use omicron_common::api::external::http_pagination::PaginatedBy; +use omicron_common::api::external::BgpPeer; +use omicron_common::api::external::BgpPeerRemove; use omicron_common::api::external::SwitchLocation; use omicron_common::api::external::{ self, CreateResult, DataPageParams, DeleteResult, Error, ListResultVec, LookupResult, Name, NameOrId, UpdateResult, }; +use std::net::IpAddr; use std::sync::Arc; use uuid::Uuid; @@ -103,7 +117,7 @@ impl super::Nexus { pub(crate) async fn switch_port_settings_delete( &self, opctx: &OpContext, - params: ¶ms::SwitchPortSettingsSelector, + params: &NameOrId, ) -> DeleteResult { opctx.authorize(authz::Action::Modify, &authz::FLEET).await?; self.db_datastore.switch_port_settings_delete(opctx, params).await @@ -127,6 +141,361 @@ impl super::Nexus { self.db_datastore.switch_port_settings_get(opctx, name_or_id).await } + pub(crate) async fn switch_port_configuration_geometry_get( + &self, + opctx: &OpContext, + name_or_id: NameOrId, + ) -> LookupResult { + opctx.authorize(authz::Action::Read, &authz::FLEET).await?; + self.db_datastore + .switch_port_configuration_geometry_get(opctx, name_or_id) + .await + } + + pub(crate) async fn switch_port_configuration_geometry_set( + &self, + opctx: &OpContext, + name_or_id: NameOrId, + geometry: SwitchPortGeometry, + ) -> CreateResult { + opctx.authorize(authz::Action::Modify, &authz::FLEET).await?; + self.db_datastore + .switch_port_configuration_geometry_set(opctx, name_or_id, geometry) + .await + } + + pub(crate) async fn switch_port_configuration_link_list( + &self, + opctx: &OpContext, + name_or_id: NameOrId, + ) -> ListResultVec { + opctx.authorize(authz::Action::Read, &authz::FLEET).await?; + self.db_datastore + .switch_port_configuration_link_list(opctx, name_or_id) + .await + } + + pub(crate) async fn switch_port_configuration_link_create( + &self, + opctx: &OpContext, + name_or_id: NameOrId, + new_settings: params::NamedLinkConfigCreate, + ) -> CreateResult { + opctx.authorize(authz::Action::CreateChild, &authz::FLEET).await?; + self.db_datastore + .switch_port_configuration_link_create( + opctx, + name_or_id, + new_settings, + ) + .await + } + + pub(crate) async fn switch_port_configuration_link_view( + &self, + opctx: &OpContext, + name_or_id: NameOrId, + link: Name, + ) -> LookupResult { + opctx.authorize(authz::Action::Read, &authz::FLEET).await?; + self.db_datastore + .switch_port_configuration_link_view(opctx, name_or_id, link.into()) + .await + } + + pub(crate) async fn switch_port_configuration_link_delete( + &self, + opctx: &OpContext, + name_or_id: NameOrId, + link: Name, + ) -> DeleteResult { + opctx.authorize(authz::Action::Delete, &authz::FLEET).await?; + self.db_datastore + .switch_port_configuration_link_delete( + opctx, + name_or_id, + link.into(), + ) + .await + } + + pub(crate) async fn switch_port_configuration_address_list( + &self, + opctx: &OpContext, + configuration: NameOrId, + ) -> ListResultVec { + opctx.authorize(authz::Action::Read, &authz::FLEET).await?; + self.db_datastore + .switch_port_configuration_address_list(opctx, configuration) + .await + } + + pub(crate) async fn switch_port_configuration_address_add( + &self, + opctx: &OpContext, + configuration: NameOrId, + address: params::AddressAddRemove, + ) -> CreateResult { + opctx.authorize(authz::Action::CreateChild, &authz::FLEET).await?; + self.db_datastore + .switch_port_configuration_address_add( + opctx, + configuration, + address, + ) + .await + } + + pub(crate) async fn switch_port_configuration_address_remove( + &self, + opctx: &OpContext, + configuration: NameOrId, + address: params::AddressAddRemove, + ) -> DeleteResult { + opctx.authorize(authz::Action::Delete, &authz::FLEET).await?; + self.db_datastore + .switch_port_configuration_address_remove( + opctx, + configuration, + address, + ) + .await + } + + pub(crate) async fn switch_port_configuration_route_list( + &self, + opctx: &OpContext, + configuration: NameOrId, + ) -> ListResultVec { + opctx.authorize(authz::Action::Read, &authz::FLEET).await?; + self.db_datastore + .switch_port_configuration_route_list(opctx, configuration) + .await + } + + pub(crate) async fn switch_port_configuration_route_add( + &self, + opctx: &OpContext, + configuration: NameOrId, + route: params::RouteAddRemove, + ) -> CreateResult { + opctx.authorize(authz::Action::CreateChild, &authz::FLEET).await?; + self.db_datastore + .switch_port_configuration_route_add(opctx, configuration, route) + .await + } + + pub(crate) async fn switch_port_configuration_route_remove( + &self, + opctx: &OpContext, + configuration: NameOrId, + route: params::RouteAddRemove, + ) -> DeleteResult { + opctx.authorize(authz::Action::Delete, &authz::FLEET).await?; + self.db_datastore + .switch_port_configuration_route_remove(opctx, configuration, route) + .await + } + + pub(crate) async fn switch_port_configuration_bgp_peer_list( + &self, + opctx: &OpContext, + configuration: NameOrId, + ) -> ListResultVec { + opctx.authorize(authz::Action::Read, &authz::FLEET).await?; + self.db_datastore + .switch_port_configuration_bgp_peer_list(opctx, configuration) + .await + } + + pub(crate) async fn switch_port_configuration_bgp_peer_add( + &self, + opctx: &OpContext, + configuration: NameOrId, + bgp_peer: BgpPeer, + ) -> CreateResult { + opctx.authorize(authz::Action::CreateChild, &authz::FLEET).await?; + self.db_datastore + .switch_port_configuration_bgp_peer_add( + opctx, + configuration, + bgp_peer, + ) + .await + } + + pub(crate) async fn switch_port_configuration_bgp_peer_remove( + &self, + opctx: &OpContext, + configuration: NameOrId, + bgp_peer: BgpPeerRemove, + ) -> DeleteResult { + opctx.authorize(authz::Action::Delete, &authz::FLEET).await?; + self.db_datastore + .switch_port_configuration_bgp_peer_remove( + opctx, + configuration, + bgp_peer, + ) + .await + } + + pub(crate) async fn switch_port_configuration_bgp_peer_allow_import_list( + &self, + opctx: &OpContext, + configuration: NameOrId, + bgp_peer: IpAddr, + ) -> ListResultVec { + opctx.authorize(authz::Action::Read, &authz::FLEET).await?; + self.db_datastore + .switch_port_configuration_bgp_peer_allow_import_list( + opctx, + configuration, + bgp_peer, + ) + .await + } + + pub(crate) async fn switch_port_configuration_bgp_peer_allow_import_add( + &self, + opctx: &OpContext, + configuration: NameOrId, + bgp_peer: IpAddr, + prefix: AllowedPrefixAddRemove, + ) -> CreateResult { + opctx.authorize(authz::Action::CreateChild, &authz::FLEET).await?; + self.db_datastore + .switch_port_configuration_bgp_peer_allow_import_add( + opctx, + configuration, + bgp_peer, + prefix, + ) + .await + } + + pub(crate) async fn switch_port_configuration_bgp_peer_allow_import_remove( + &self, + opctx: &OpContext, + configuration: NameOrId, + bgp_peer: IpAddr, + prefix: AllowedPrefixAddRemove, + ) -> DeleteResult { + opctx.authorize(authz::Action::Delete, &authz::FLEET).await?; + self.db_datastore + .switch_port_configuration_bgp_peer_allow_import_remove( + opctx, + configuration, + bgp_peer, + prefix, + ) + .await + } + + pub(crate) async fn switch_port_configuration_bgp_peer_allow_export_list( + &self, + opctx: &OpContext, + configuration: NameOrId, + bgp_peer: IpAddr, + ) -> ListResultVec { + opctx.authorize(authz::Action::Read, &authz::FLEET).await?; + self.db_datastore + .switch_port_configuration_bgp_peer_allow_export_list( + opctx, + configuration, + bgp_peer, + ) + .await + } + + pub(crate) async fn switch_port_configuration_bgp_peer_allow_export_add( + &self, + opctx: &OpContext, + configuration: NameOrId, + bgp_peer: IpAddr, + prefix: AllowedPrefixAddRemove, + ) -> CreateResult { + opctx.authorize(authz::Action::CreateChild, &authz::FLEET).await?; + self.db_datastore + .switch_port_configuration_bgp_peer_allow_export_add( + opctx, + configuration, + bgp_peer, + prefix, + ) + .await + } + + pub(crate) async fn switch_port_configuration_bgp_peer_allow_export_remove( + &self, + opctx: &OpContext, + configuration: NameOrId, + bgp_peer: IpAddr, + prefix: AllowedPrefixAddRemove, + ) -> DeleteResult { + opctx.authorize(authz::Action::Delete, &authz::FLEET).await?; + self.db_datastore + .switch_port_configuration_bgp_peer_allow_export_remove( + opctx, + configuration, + bgp_peer, + prefix, + ) + .await + } + + pub(crate) async fn switch_port_configuration_bgp_peer_community_list( + &self, + opctx: &OpContext, + configuration: NameOrId, + bgp_peer: IpAddr, + ) -> ListResultVec { + opctx.authorize(authz::Action::Read, &authz::FLEET).await?; + self.db_datastore + .switch_port_configuration_bgp_peer_community_list( + opctx, + configuration, + bgp_peer, + ) + .await + } + + pub(crate) async fn switch_port_configuration_bgp_peer_community_add( + &self, + opctx: &OpContext, + configuration: NameOrId, + bgp_peer: IpAddr, + community: BgpCommunityAddRemove, + ) -> CreateResult { + opctx.authorize(authz::Action::CreateChild, &authz::FLEET).await?; + self.db_datastore + .switch_port_configuration_bgp_peer_community_add( + opctx, + configuration, + bgp_peer, + community, + ) + .await + } + + pub(crate) async fn switch_port_configuration_bgp_peer_community_remove( + &self, + opctx: &OpContext, + configuration: NameOrId, + bgp_peer: IpAddr, + community: BgpCommunityAddRemove, + ) -> DeleteResult { + opctx.authorize(authz::Action::Delete, &authz::FLEET).await?; + self.db_datastore + .switch_port_configuration_bgp_peer_community_remove( + opctx, + configuration, + bgp_peer, + community, + ) + .await + } + async fn switch_port_create( &self, opctx: &OpContext, diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index e11256f06e..fbf7b59e94 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -41,23 +41,6 @@ use nexus_db_queries::db::lookup::ImageLookup; use nexus_db_queries::db::lookup::ImageParentLookup; use nexus_db_queries::db::model::Name; use nexus_types::external_api::shared::{BfdStatus, ProbeInfo}; -use omicron_common::api::external::http_pagination::data_page_params_for; -use omicron_common::api::external::http_pagination::marker_for_name; -use omicron_common::api::external::http_pagination::marker_for_name_or_id; -use omicron_common::api::external::http_pagination::name_or_id_pagination; -use omicron_common::api::external::http_pagination::PaginatedBy; -use omicron_common::api::external::http_pagination::PaginatedById; -use omicron_common::api::external::http_pagination::PaginatedByName; -use omicron_common::api::external::http_pagination::PaginatedByNameOrId; -use omicron_common::api::external::http_pagination::ScanById; -use omicron_common::api::external::http_pagination::ScanByName; -use omicron_common::api::external::http_pagination::ScanByNameOrId; -use omicron_common::api::external::http_pagination::ScanParams; -use omicron_common::api::external::AddressLot; -use omicron_common::api::external::AddressLotBlock; -use omicron_common::api::external::AddressLotCreateResponse; -use omicron_common::api::external::AggregateBgpMessageHistory; -use omicron_common::api::external::BgpAnnounceSet; use omicron_common::api::external::BgpAnnouncement; use omicron_common::api::external::BgpConfig; use omicron_common::api::external::BgpExported; @@ -75,12 +58,29 @@ use omicron_common::api::external::Probe; use omicron_common::api::external::RouterRoute; use omicron_common::api::external::RouterRouteKind; use omicron_common::api::external::SwitchPort; +use omicron_common::api::external::SwitchPortConfig; +use omicron_common::api::external::SwitchPortLinkConfig; use omicron_common::api::external::SwitchPortSettings; use omicron_common::api::external::SwitchPortSettingsView; use omicron_common::api::external::TufRepoGetResponse; use omicron_common::api::external::TufRepoInsertResponse; use omicron_common::api::external::VpcFirewallRuleUpdateParams; use omicron_common::api::external::VpcFirewallRules; +use omicron_common::api::external::{ + http_pagination::{ + data_page_params_for, marker_for_name, marker_for_name_or_id, + name_or_id_pagination, PaginatedBy, PaginatedById, PaginatedByName, + PaginatedByNameOrId, ScanById, ScanByName, ScanByNameOrId, ScanParams, + }, + SwitchPortAddressConfig, +}; +use omicron_common::api::external::{AddressLot, BgpPeerRemove}; +use omicron_common::api::external::{AddressLotBlock, SwitchPortRouteConfig}; +use omicron_common::api::external::{ + AddressLotCreateResponse, BgpAllowedPrefix, +}; +use omicron_common::api::external::{AggregateBgpMessageHistory, BgpCommunity}; +use omicron_common::api::external::{BgpAnnounceSet, BgpPeer}; use omicron_common::bail_unless; use omicron_uuid_kinds::GenericUuid; use parse_display::Display; @@ -258,16 +258,95 @@ pub(crate) fn external_api() -> NexusApiDescription { api.register(networking_address_lot_list)?; api.register(networking_address_lot_create)?; api.register(networking_address_lot_delete)?; + api.register(networking_address_lot_block_list)?; + api.register(networking_address_lot_block_add)?; + api.register(networking_address_lot_block_remove)?; api.register(networking_loopback_address_create)?; api.register(networking_loopback_address_delete)?; api.register(networking_loopback_address_list)?; - api.register(networking_switch_port_settings_list)?; - api.register(networking_switch_port_settings_view)?; - api.register(networking_switch_port_settings_create)?; - api.register(networking_switch_port_settings_delete)?; + // TODO: Levon - Group composition (omicron#4405)? + // /v1/system/networking/switch-port-configuration-group/{name_or_id}/.. + + // /v1/system/networking/switch-port-configuration + api.register(networking_switch_port_configuration_list)?; + api.register(networking_switch_port_configuration_view)?; + api.register(networking_switch_port_configuration_create)?; + api.register(networking_switch_port_configuration_delete)?; + + // TODO: Levon - add tests + api.register(networking_switch_port_configuration_geometry_view)?; + // TODO: Levon - add tests + api.register(networking_switch_port_configuration_geometry_set)?; + + // TODO: Levon - add tests + api.register(networking_switch_port_configuration_link_create)?; + // TODO: Levon - add tests + api.register(networking_switch_port_configuration_link_delete)?; + // TODO: Levon - add tests + api.register(networking_switch_port_configuration_link_view)?; + // TODO: Levon - add tests + api.register(networking_switch_port_configuration_link_list)?; + + // TODO: Levon - add tests + api.register(networking_switch_port_configuration_address_add)?; + // TODO: Levon - add tests + api.register(networking_switch_port_configuration_address_remove)?; + // TODO: Levon - add tests + api.register(networking_switch_port_configuration_address_list)?; + + // TODO: Levon - add tests + api.register(networking_switch_port_configuration_route_add)?; + // TODO: Levon - add tests + api.register(networking_switch_port_configuration_route_remove)?; + // TODO: Levon - add tests + api.register(networking_switch_port_configuration_route_list)?; + + // TODO: Levon - add tests + api.register(networking_switch_port_configuration_bgp_peer_add)?; + // TODO: Levon - add tests + api.register(networking_switch_port_configuration_bgp_peer_remove)?; + // TODO: Levon - add tests + api.register(networking_switch_port_configuration_bgp_peer_list)?; + + // TODO: Levon - add tests + api.register( + networking_switch_port_configuration_bgp_peer_allow_import_add, + )?; + // TODO: Levon - add tests + api.register( + networking_switch_port_configuration_bgp_peer_allow_import_remove, + )?; + // TODO: Levon - add tests + api.register( + networking_switch_port_configuration_bgp_peer_allow_import_list, + )?; + + api.register( + networking_switch_port_configuration_bgp_peer_allow_export_add, + )?; + // TODO: Levon - add tests + api.register( + networking_switch_port_configuration_bgp_peer_allow_export_remove, + )?; + // TODO: Levon - add tests + api.register( + networking_switch_port_configuration_bgp_peer_allow_export_list, + )?; + + api.register( + networking_switch_port_configuration_bgp_peer_community_add, + )?; + // TODO: Levon - add tests + api.register( + networking_switch_port_configuration_bgp_peer_community_remove, + )?; + // TODO: Levon - add tests + api.register( + networking_switch_port_configuration_bgp_peer_community_list, + )?; api.register(networking_switch_port_list)?; api.register(networking_switch_port_status)?; @@ -3374,11 +3453,9 @@ async fn networking_address_lot_create( let opctx = crate::context::op_context_for_external_api(&rqctx).await?; let result = nexus.address_lot_create(&opctx, params).await?; - let lot: AddressLot = result.lot.into(); - let blocks: Vec = - result.blocks.iter().map(|b| b.clone().into()).collect(); + let lot: AddressLot = result.into(); - Ok(HttpResponseCreated(AddressLotCreateResponse { lot, blocks })) + Ok(HttpResponseCreated(AddressLotCreateResponse { lot })) }; apictx .context @@ -3452,6 +3529,84 @@ async fn networking_address_lot_list( .await } +/// Add block to address lot +#[endpoint { + method = POST, + path = "/v1/system/networking/address-lot/{address_lot}/blocks/add", + tags = ["system/networking"], +}] +async fn networking_address_lot_block_add( + rqctx: RequestContext, + path_params: Path, + block: TypedBody, +) -> Result, HttpError> { + let apictx = rqctx.context(); + let handler = async { + let nexus = &apictx.context.nexus; + let path = path_params.into_inner(); + let opctx = crate::context::op_context_for_external_api(&rqctx).await?; + let address_lot_lookup = + nexus.address_lot_lookup(&opctx, path.address_lot)?; + + let (.., authz_address_lot) = + address_lot_lookup.lookup_for(authz::Action::CreateChild).await?; + + let result = nexus + .address_lot_block_create( + &opctx, + authz_address_lot.id(), + block.into_inner(), + ) + .await?; + + Ok(HttpResponseCreated(result.into())) + }; + apictx + .context + .external_latencies + .instrument_dropshot_handler(&rqctx, handler) + .await +} + +/// Remove block from address lot +#[endpoint { + method = POST, + path = "/v1/system/networking/address-lot/{address_lot}/blocks/remove", + tags = ["system/networking"], +}] +async fn networking_address_lot_block_remove( + rqctx: RequestContext, + path_params: Path, + block: TypedBody, +) -> Result { + let apictx = rqctx.context(); + let handler = async { + let nexus = &apictx.context.nexus; + let path = path_params.into_inner(); + let opctx = crate::context::op_context_for_external_api(&rqctx).await?; + let address_lot_lookup = + nexus.address_lot_lookup(&opctx, path.address_lot)?; + + let (.., authz_address_lot) = + address_lot_lookup.lookup_for(authz::Action::Delete).await?; + + nexus + .address_lot_block_delete( + &opctx, + authz_address_lot.id(), + block.into_inner(), + ) + .await?; + + Ok(HttpResponseUpdatedNoContent()) + }; + apictx + .context + .external_latencies + .instrument_dropshot_handler(&rqctx, handler) + .await +} + /// List blocks in address lot #[endpoint { method = GET, @@ -3615,10 +3770,10 @@ async fn networking_loopback_address_list( /// Create switch port settings #[endpoint { method = POST, - path = "/v1/system/networking/switch-port-settings", + path = "/v1/system/networking/switch-port-configuration", tags = ["system/networking"], }] -async fn networking_switch_port_settings_create( +async fn networking_switch_port_configuration_create( rqctx: RequestContext, new_settings: TypedBody, ) -> Result, HttpError> { @@ -3642,17 +3797,17 @@ async fn networking_switch_port_settings_create( /// Delete switch port settings #[endpoint { method = DELETE, - path = "/v1/system/networking/switch-port-settings", + path = "/v1/system/networking/switch-port-configuration/{configuration}", tags = ["system/networking"], }] -async fn networking_switch_port_settings_delete( +async fn networking_switch_port_configuration_delete( rqctx: RequestContext, - query_params: Query, + path_params: Path, ) -> Result { let apictx = rqctx.context(); let handler = async { let nexus = &apictx.context.nexus; - let selector = query_params.into_inner(); + let selector = path_params.into_inner().configuration; let opctx = crate::context::op_context_for_external_api(&rqctx).await?; nexus.switch_port_settings_delete(&opctx, &selector).await?; Ok(HttpResponseDeleted()) @@ -3667,10 +3822,10 @@ async fn networking_switch_port_settings_delete( /// List switch port settings #[endpoint { method = GET, - path = "/v1/system/networking/switch-port-settings", + path = "/v1/system/networking/switch-port-configuration", tags = ["system/networking"], }] -async fn networking_switch_port_settings_list( +async fn networking_switch_port_configuration_list( rqctx: RequestContext, query_params: Query< PaginatedByNameOrId, @@ -3704,20 +3859,20 @@ async fn networking_switch_port_settings_list( .await } -/// Get information about switch port +/// Get information about a named set of switch-port-settings #[endpoint { method = GET, - path = "/v1/system/networking/switch-port-settings/{port}", + path = "/v1/system/networking/switch-port-configuration/{configuration}", tags = ["system/networking"], }] -async fn networking_switch_port_settings_view( +async fn networking_switch_port_configuration_view( rqctx: RequestContext, path_params: Path, ) -> Result, HttpError> { let apictx = rqctx.context(); let handler = async { let nexus = &apictx.context.nexus; - let query = path_params.into_inner().port; + let query = path_params.into_inner().configuration; let opctx = crate::context::op_context_for_external_api(&rqctx).await?; let settings = nexus.switch_port_settings_get(&opctx, &query).await?; Ok(HttpResponseOk(settings.into())) @@ -3729,34 +3884,26 @@ async fn networking_switch_port_settings_view( .await } -/// List switch ports +/// Get switch port geometry for a provided switch port configuration #[endpoint { method = GET, - path = "/v1/system/hardware/switch-port", - tags = ["system/hardware"], + path = "/v1/system/networking/switch-port-configuration/{configuration}/geometry", + tags = ["system/networking"], }] -async fn networking_switch_port_list( +async fn networking_switch_port_configuration_geometry_view( rqctx: RequestContext, - query_params: Query>, -) -> Result>, HttpError> { + path_params: Path, +) -> Result, HttpError> { let apictx = rqctx.context(); let handler = async { let nexus = &apictx.context.nexus; - let query = query_params.into_inner(); - let pagparams = data_page_params_for(&rqctx, &query)?; + let config = path_params.into_inner().configuration; let opctx = crate::context::op_context_for_external_api(&rqctx).await?; - let addrs = nexus - .switch_port_list(&opctx, &pagparams) - .await? - .into_iter() - .map(|p| p.into()) - .collect(); - Ok(HttpResponseOk(ScanById::results_page( - &query, - addrs, - &|_, x: &SwitchPort| x.id, - )?)) + let geometry = nexus + .switch_port_configuration_geometry_get(&opctx, config) + .await?; + Ok(HttpResponseOk(geometry.into())) }; apictx .context @@ -3765,28 +3912,31 @@ async fn networking_switch_port_list( .await } -/// Get switch port status +/// Set switch port geometry for a provided switch port configuration #[endpoint { - method = GET, - path = "/v1/system/hardware/switch-port/{port}/status", - tags = ["system/hardware"], + method = POST, + path = "/v1/system/networking/switch-port-configuration/{configuration}/geometry", + tags = ["system/networking"], }] -async fn networking_switch_port_status( +async fn networking_switch_port_configuration_geometry_set( rqctx: RequestContext, - path_params: Path, - query_params: Query, -) -> Result, HttpError> { + path_params: Path, + new_settings: TypedBody, +) -> Result, HttpError> { let apictx = rqctx.context(); let handler = async { let nexus = &apictx.context.nexus; - let query = query_params.into_inner(); - let path = path_params.into_inner(); + let config = path_params.into_inner().configuration; let opctx = crate::context::op_context_for_external_api(&rqctx).await?; - Ok(HttpResponseOk( - nexus - .switch_port_status(&opctx, query.switch_location, path.port) - .await?, - )) + + let geometry = nexus + .switch_port_configuration_geometry_set( + &opctx, + config, + new_settings.into_inner().geometry.into(), + ) + .await?; + Ok(HttpResponseCreated(geometry.into())) }; apictx .context @@ -3795,29 +3945,26 @@ async fn networking_switch_port_status( .await } -/// Apply switch port settings +/// List links for a provided switch port configuration #[endpoint { - method = POST, - path = "/v1/system/hardware/switch-port/{port}/settings", - tags = ["system/hardware"], + method = GET, + path = "/v1/system/networking/switch-port-configuration/{configuration}/link", + tags = ["system/networking"], }] -async fn networking_switch_port_apply_settings( +async fn networking_switch_port_configuration_link_list( rqctx: RequestContext, - path_params: Path, - query_params: Query, - settings_body: TypedBody, -) -> Result { + path_params: Path, + // omitting pagination should be ok since there are a small number of possible links +) -> Result>, HttpError> { let apictx = rqctx.context(); let handler = async { let nexus = &apictx.context.nexus; - let port = path_params.into_inner().port; - let query = query_params.into_inner(); - let settings = settings_body.into_inner(); + let config = path_params.into_inner().configuration; let opctx = crate::context::op_context_for_external_api(&rqctx).await?; - nexus - .switch_port_apply_settings(&opctx, &port, &query, &settings) - .await?; - Ok(HttpResponseUpdatedNoContent {}) + + let settings = + nexus.switch_port_configuration_link_list(&opctx, config).await?; + Ok(HttpResponseOk(settings.into_iter().map(Into::into).collect())) }; apictx .context @@ -3826,25 +3973,31 @@ async fn networking_switch_port_apply_settings( .await } -/// Clear switch port settings +/// Create a link for a provided switch port configuration #[endpoint { - method = DELETE, - path = "/v1/system/hardware/switch-port/{port}/settings", - tags = ["system/hardware"], + method = POST, + path = "/v1/system/networking/switch-port-configuration/{configuration}/link", + tags = ["system/networking"], }] -async fn networking_switch_port_clear_settings( +async fn networking_switch_port_configuration_link_create( rqctx: RequestContext, - path_params: Path, - query_params: Query, -) -> Result { + path_params: Path, + new_settings: TypedBody, +) -> Result, HttpError> { let apictx = rqctx.context(); let handler = async { let nexus = &apictx.context.nexus; - let port = path_params.into_inner().port; - let query = query_params.into_inner(); + let config = path_params.into_inner().configuration; let opctx = crate::context::op_context_for_external_api(&rqctx).await?; - nexus.switch_port_clear_settings(&opctx, &port, &query).await?; - Ok(HttpResponseUpdatedNoContent {}) + + let settings = nexus + .switch_port_configuration_link_create( + &opctx, + config, + new_settings.into_inner(), + ) + .await?; + Ok(HttpResponseCreated(settings.into())) }; apictx .context @@ -3853,23 +4006,27 @@ async fn networking_switch_port_clear_settings( .await } -/// Create new BGP configuration +/// View a link for a provided switch port configuration #[endpoint { - method = POST, - path = "/v1/system/networking/bgp", + method = GET, + path = "/v1/system/networking/switch-port-configuration/{configuration}/link/{link}", tags = ["system/networking"], }] -async fn networking_bgp_config_create( +async fn networking_switch_port_configuration_link_view( rqctx: RequestContext, - config: TypedBody, -) -> Result, HttpError> { + path_params: Path, +) -> Result, HttpError> { let apictx = rqctx.context(); let handler = async { let nexus = &apictx.context.nexus; - let config = config.into_inner(); + let params::SwitchPortSettingsLinkInfoSelector { configuration, link } = + path_params.into_inner(); let opctx = crate::context::op_context_for_external_api(&rqctx).await?; - let result = nexus.bgp_config_create(&opctx, &config).await?; - Ok(HttpResponseCreated::(result.into())) + + let settings = nexus + .switch_port_configuration_link_view(&opctx, configuration, link) + .await?; + Ok(HttpResponseOk(settings.into())) }; apictx .context @@ -3878,36 +4035,27 @@ async fn networking_bgp_config_create( .await } -/// List BGP configurations +/// Delete a link for a provided switch port configuration #[endpoint { - method = GET, - path = "/v1/system/networking/bgp", + method = DELETE, + path = "/v1/system/networking/switch-port-configuration/{configuration}/link/{link}", tags = ["system/networking"], }] -async fn networking_bgp_config_list( +async fn networking_switch_port_configuration_link_delete( rqctx: RequestContext, - query_params: Query>, -) -> Result>, HttpError> { + path_params: Path, +) -> Result { let apictx = rqctx.context(); let handler = async { let nexus = &apictx.context.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 params::SwitchPortSettingsLinkInfoSelector { configuration, link } = + path_params.into_inner(); let opctx = crate::context::op_context_for_external_api(&rqctx).await?; - let configs = nexus - .bgp_config_list(&opctx, &paginated_by) - .await? - .into_iter() - .map(|p| p.into()) - .collect(); - Ok(HttpResponseOk(ScanByNameOrId::results_page( - &query, - configs, - &marker_for_name_or_id, - )?)) + nexus + .switch_port_configuration_link_delete(&opctx, configuration, link) + .await?; + Ok(HttpResponseDeleted {}) }; apictx .context @@ -3916,22 +4064,26 @@ async fn networking_bgp_config_list( .await } -//TODO pagination? the normal by-name/by-id stuff does not work here -/// Get BGP peer status +/// List addresses assigned to a provided interface configuration #[endpoint { method = GET, - path = "/v1/system/networking/bgp-status", + path = "/v1/system/networking/switch-port-configuration/{configuration}/address", tags = ["system/networking"], }] -async fn networking_bgp_status( +async fn networking_switch_port_configuration_address_list( rqctx: RequestContext, -) -> Result>, HttpError> { + path_params: Path, +) -> Result>, HttpError> { let apictx = rqctx.context(); - let opctx = crate::context::op_context_for_external_api(&rqctx).await?; let handler = async { let nexus = &apictx.context.nexus; - let result = nexus.bgp_peer_status(&opctx).await?; - Ok(HttpResponseOk(result)) + let configuration = path_params.into_inner().configuration; + let opctx = crate::context::op_context_for_external_api(&rqctx).await?; + + let settings = nexus + .switch_port_configuration_address_list(&opctx, configuration) + .await?; + Ok(HttpResponseOk(settings.into_iter().map(Into::into).collect())) }; apictx .context @@ -3940,22 +4092,32 @@ async fn networking_bgp_status( .await } -//TODO pagination? the normal by-name/by-id stuff does not work here -/// Get BGP exported routes +/// Add address to an interface configuration #[endpoint { - method = GET, - path = "/v1/system/networking/bgp-exported", + method = POST, + path = "/v1/system/networking/switch-port-configuration/{configuration}/address/add", tags = ["system/networking"], }] -async fn networking_bgp_exported( +async fn networking_switch_port_configuration_address_add( rqctx: RequestContext, -) -> Result, HttpError> { + path_params: Path, + address: TypedBody, +) -> Result, HttpError> { let apictx = rqctx.context(); - let opctx = crate::context::op_context_for_external_api(&rqctx).await?; let handler = async { let nexus = &apictx.context.nexus; - let result = nexus.bgp_exported(&opctx).await?; - Ok(HttpResponseOk(result)) + let configuration = path_params.into_inner().configuration; + let address = address.into_inner(); + let opctx = crate::context::op_context_for_external_api(&rqctx).await?; + + let settings = nexus + .switch_port_configuration_address_add( + &opctx, + configuration, + address, + ) + .await?; + Ok(HttpResponseCreated(settings.into())) }; apictx .context @@ -3964,23 +4126,32 @@ async fn networking_bgp_exported( .await } -/// Get BGP router message history +/// Remove address from an interface configuration #[endpoint { - method = GET, - path = "/v1/system/networking/bgp-message-history", + method = POST, + path = "/v1/system/networking/switch-port-configuration/{configuration}/address/remove", tags = ["system/networking"], }] -async fn networking_bgp_message_history( +async fn networking_switch_port_configuration_address_remove( rqctx: RequestContext, - query_params: Query, -) -> Result, HttpError> { + path_params: Path, + address: TypedBody, +) -> Result { let apictx = rqctx.context(); - let opctx = crate::context::op_context_for_external_api(&rqctx).await?; let handler = async { let nexus = &apictx.context.nexus; - let sel = query_params.into_inner(); - let result = nexus.bgp_message_history(&opctx, &sel).await?; - Ok(HttpResponseOk(AggregateBgpMessageHistory::new(result))) + let configuration = path_params.into_inner().configuration; + let address = address.into_inner(); + let opctx = crate::context::op_context_for_external_api(&rqctx).await?; + + nexus + .switch_port_configuration_address_remove( + &opctx, + configuration, + address, + ) + .await?; + Ok(HttpResponseDeleted()) }; apictx .context @@ -3989,24 +4160,26 @@ async fn networking_bgp_message_history( .await } -//TODO pagination? the normal by-name/by-id stuff does not work here -/// Get imported IPv4 BGP routes +/// List routes assigned to a provided interface configuration #[endpoint { method = GET, - path = "/v1/system/networking/bgp-routes-ipv4", + path = "/v1/system/networking/switch-port-configuration/{configuration}/route", tags = ["system/networking"], }] -async fn networking_bgp_imported_routes_ipv4( +async fn networking_switch_port_configuration_route_list( rqctx: RequestContext, - query_params: Query, -) -> Result>, HttpError> { + path_params: Path, +) -> Result>, HttpError> { let apictx = rqctx.context(); - let opctx = crate::context::op_context_for_external_api(&rqctx).await?; let handler = async { let nexus = &apictx.context.nexus; - let sel = query_params.into_inner(); - let result = nexus.bgp_imported_routes_ipv4(&opctx, &sel).await?; - Ok(HttpResponseOk(result)) + let configuration = path_params.into_inner().configuration; + let opctx = crate::context::op_context_for_external_api(&rqctx).await?; + + let settings = nexus + .switch_port_configuration_route_list(&opctx, configuration) + .await?; + Ok(HttpResponseOk(settings.into_iter().map(Into::into).collect())) }; apictx .context @@ -4015,20 +4188,769 @@ async fn networking_bgp_imported_routes_ipv4( .await } -/// Delete BGP configuration +/// Add route to an interface configuration #[endpoint { - method = DELETE, - path = "/v1/system/networking/bgp", + method = POST, + path = "/v1/system/networking/switch-port-configuration/{configuration}/route/add", + tags = ["system/networking"], +}] +async fn networking_switch_port_configuration_route_add( + rqctx: RequestContext, + path_params: Path, + route: TypedBody, +) -> Result, HttpError> { + let apictx = rqctx.context(); + let handler = async { + let nexus = &apictx.context.nexus; + let configuration = path_params.into_inner().configuration; + let route = route.into_inner(); + let opctx = crate::context::op_context_for_external_api(&rqctx).await?; + + let settings = nexus + .switch_port_configuration_route_add(&opctx, configuration, route) + .await?; + Ok(HttpResponseCreated(settings.into())) + }; + apictx + .context + .external_latencies + .instrument_dropshot_handler(&rqctx, handler) + .await +} + +/// Remove route from an interface configuration +#[endpoint { + method = POST, + path = "/v1/system/networking/switch-port-configuration/{configuration}/route/remove", + tags = ["system/networking"], +}] +async fn networking_switch_port_configuration_route_remove( + rqctx: RequestContext, + path_params: Path, + route: TypedBody, +) -> Result { + let apictx = rqctx.context(); + let handler = async { + let nexus = &apictx.context.nexus; + let configuration = path_params.into_inner().configuration; + let route = route.into_inner(); + let opctx = crate::context::op_context_for_external_api(&rqctx).await?; + + nexus + .switch_port_configuration_route_remove( + &opctx, + configuration, + route, + ) + .await?; + Ok(HttpResponseDeleted()) + }; + apictx + .context + .external_latencies + .instrument_dropshot_handler(&rqctx, handler) + .await +} + +/// List bgp peers assigned to a provided interface configuration +#[endpoint { + method = GET, + path = "/v1/system/networking/switch-port-configuration/{configuration}/bgp-peer", + tags = ["system/networking"], +}] +async fn networking_switch_port_configuration_bgp_peer_list( + rqctx: RequestContext, + path_params: Path, +) -> Result>, HttpError> { + let apictx = rqctx.context(); + let handler = async { + let nexus = &apictx.context.nexus; + let configuration = path_params.into_inner().configuration; + let opctx = crate::context::op_context_for_external_api(&rqctx).await?; + + let settings = nexus + .switch_port_configuration_bgp_peer_list(&opctx, configuration) + .await?; + + Ok(HttpResponseOk(settings.into_iter().map(Into::into).collect())) + }; + apictx + .context + .external_latencies + .instrument_dropshot_handler(&rqctx, handler) + .await +} + +/// Add bgp peer to an interface configuration +#[endpoint { + method = POST, + path = "/v1/system/networking/switch-port-configuration/{configuration}/bgp-peer/add", + tags = ["system/networking"], +}] +async fn networking_switch_port_configuration_bgp_peer_add( + rqctx: RequestContext, + path_params: Path, + bgp_peer: TypedBody, +) -> Result, HttpError> { + let apictx = rqctx.context(); + let handler = async { + let nexus = &apictx.context.nexus; + let configuration = path_params.into_inner().configuration; + let bgp_peer = bgp_peer.into_inner(); + let opctx = crate::context::op_context_for_external_api(&rqctx).await?; + + let settings = nexus + .switch_port_configuration_bgp_peer_add( + &opctx, + configuration, + bgp_peer, + ) + .await?; + Ok(HttpResponseCreated(settings.into())) + }; + apictx + .context + .external_latencies + .instrument_dropshot_handler(&rqctx, handler) + .await +} + +/// Remove bgp peer from an interface configuration +#[endpoint { + method = POST, + path = "/v1/system/networking/switch-port-configuration/{configuration}/bgp-peer/remove", + tags = ["system/networking"], +}] +async fn networking_switch_port_configuration_bgp_peer_remove( + rqctx: RequestContext, + path_params: Path, + bgp_peer: TypedBody, +) -> Result { + let apictx = rqctx.context(); + let handler = async { + let nexus = &apictx.context.nexus; + let configuration = path_params.into_inner().configuration; + let bgp_peer = bgp_peer.into_inner(); + let opctx = crate::context::op_context_for_external_api(&rqctx).await?; + + nexus + .switch_port_configuration_bgp_peer_remove( + &opctx, + configuration, + bgp_peer, + ) + .await?; + Ok(HttpResponseDeleted()) + }; + apictx + .context + .external_latencies + .instrument_dropshot_handler(&rqctx, handler) + .await +} + +/// List prefixes allowed to be imported by a given bgp peer +#[endpoint { + method = GET, + path = "/v1/system/networking/switch-port-configuration/{configuration}/bgp-peer/allow-import", + tags = ["system/networking"], +}] +async fn networking_switch_port_configuration_bgp_peer_allow_import_list( + rqctx: RequestContext, + path_params: Path, + query: Query, +) -> Result>, HttpError> { + let apictx = rqctx.context(); + let handler = async { + let nexus = &apictx.context.nexus; + let opctx = crate::context::op_context_for_external_api(&rqctx).await?; + + let settings = nexus + .switch_port_configuration_bgp_peer_allow_import_list( + &opctx, + path_params.into_inner().configuration, + query.into_inner().peer_address, + ) + .await?; + + Ok(HttpResponseOk(settings.into_iter().map(Into::into).collect())) + }; + apictx + .context + .external_latencies + .instrument_dropshot_handler(&rqctx, handler) + .await +} + +/// Add prefix to bgp peer allowed import list +#[endpoint { + method = POST, + path = "/v1/system/networking/switch-port-configuration/{configuration}/bgp-peer/allow-import/add", + tags = ["system/networking"], +}] +async fn networking_switch_port_configuration_bgp_peer_allow_import_add( + rqctx: RequestContext, + path_params: Path, + prefix: TypedBody, +) -> Result, HttpError> { + let apictx = rqctx.context(); + let handler = async { + let nexus = &apictx.context.nexus; + let opctx = crate::context::op_context_for_external_api(&rqctx).await?; + let prefix = prefix.into_inner(); + + let settings = nexus + .switch_port_configuration_bgp_peer_allow_import_add( + &opctx, + path_params.into_inner().configuration, + prefix.peer_address, + prefix, + ) + .await?; + Ok(HttpResponseCreated(settings.into())) + }; + apictx + .context + .external_latencies + .instrument_dropshot_handler(&rqctx, handler) + .await +} + +/// Remove prefix from bgp peer allowed import list +#[endpoint { + method = POST, + path = "/v1/system/networking/switch-port-configuration/{configuration}/bgp-peer/allow-import/remove", + tags = ["system/networking"], +}] +async fn networking_switch_port_configuration_bgp_peer_allow_import_remove( + rqctx: RequestContext, + path_params: Path, + prefix: TypedBody, +) -> Result { + let apictx = rqctx.context(); + let handler = async { + let nexus = &apictx.context.nexus; + let opctx = crate::context::op_context_for_external_api(&rqctx).await?; + let prefix = prefix.into_inner(); + + nexus + .switch_port_configuration_bgp_peer_allow_import_remove( + &opctx, + path_params.into_inner().configuration, + prefix.peer_address, + prefix, + ) + .await?; + Ok(HttpResponseDeleted()) + }; + apictx + .context + .external_latencies + .instrument_dropshot_handler(&rqctx, handler) + .await +} + +/// List prefixes allowed to be exported by a given bgp peer +#[endpoint { + method = GET, + path = "/v1/system/networking/switch-port-configuration/{configuration}/bgp-peer/allow-export", + tags = ["system/networking"], +}] +async fn networking_switch_port_configuration_bgp_peer_allow_export_list( + rqctx: RequestContext, + path_params: Path, + query: Query, +) -> Result>, HttpError> { + let apictx = rqctx.context(); + let handler = async { + let nexus = &apictx.context.nexus; + let opctx = crate::context::op_context_for_external_api(&rqctx).await?; + + let settings = nexus + .switch_port_configuration_bgp_peer_allow_export_list( + &opctx, + path_params.into_inner().configuration, + query.into_inner().peer_address, + ) + .await?; + + Ok(HttpResponseOk(settings.into_iter().map(Into::into).collect())) + }; + apictx + .context + .external_latencies + .instrument_dropshot_handler(&rqctx, handler) + .await +} + +/// Add prefix to bgp peer allowed export list +#[endpoint { + method = POST, + path = "/v1/system/networking/switch-port-configuration/{configuration}/bgp-peer/allow-export/add", + tags = ["system/networking"], +}] +async fn networking_switch_port_configuration_bgp_peer_allow_export_add( + rqctx: RequestContext, + path_params: Path, + prefix: TypedBody, +) -> Result, HttpError> { + let apictx = rqctx.context(); + let handler = async { + let nexus = &apictx.context.nexus; + let opctx = crate::context::op_context_for_external_api(&rqctx).await?; + let prefix = prefix.into_inner(); + + let settings = nexus + .switch_port_configuration_bgp_peer_allow_export_add( + &opctx, + path_params.into_inner().configuration, + prefix.peer_address, + prefix, + ) + .await?; + Ok(HttpResponseCreated(settings.into())) + }; + apictx + .context + .external_latencies + .instrument_dropshot_handler(&rqctx, handler) + .await +} + +/// Remove prefix from bgp peer allowed export list +#[endpoint { + method = POST, + path = "/v1/system/networking/switch-port-configuration/{configuration}/bgp-peer/allow-export/remove", + tags = ["system/networking"], +}] +async fn networking_switch_port_configuration_bgp_peer_allow_export_remove( + rqctx: RequestContext, + path_params: Path, + prefix: TypedBody, +) -> Result { + let apictx = rqctx.context(); + let handler = async { + let nexus = &apictx.context.nexus; + let opctx = crate::context::op_context_for_external_api(&rqctx).await?; + let prefix = prefix.into_inner(); + + nexus + .switch_port_configuration_bgp_peer_allow_export_remove( + &opctx, + path_params.into_inner().configuration, + prefix.peer_address, + prefix, + ) + .await?; + Ok(HttpResponseDeleted()) + }; + apictx + .context + .external_latencies + .instrument_dropshot_handler(&rqctx, handler) + .await +} + +/// List communities assigned to a bgp peer +#[endpoint { + method = GET, + path = "/v1/system/networking/switch-port-configuration/{configuration}/bgp-peer/community", + tags = ["system/networking"], +}] +async fn networking_switch_port_configuration_bgp_peer_community_list( + rqctx: RequestContext, + path_params: Path, + query: Query, +) -> Result>, HttpError> { + let apictx = rqctx.context(); + let handler = async { + let nexus = &apictx.context.nexus; + let opctx = crate::context::op_context_for_external_api(&rqctx).await?; + + let settings = nexus + .switch_port_configuration_bgp_peer_community_list( + &opctx, + path_params.into_inner().configuration, + query.into_inner().peer_address, + ) + .await?; + Ok(HttpResponseOk(settings.into_iter().map(Into::into).collect())) + }; + apictx + .context + .external_latencies + .instrument_dropshot_handler(&rqctx, handler) + .await +} + +/// Add community to bgp peer +#[endpoint { + method = POST, + path = "/v1/system/networking/switch-port-configuration/{configuration}/bgp-peer/community/add", + tags = ["system/networking"], +}] +async fn networking_switch_port_configuration_bgp_peer_community_add( + rqctx: RequestContext, + path_params: Path, + community: TypedBody, +) -> Result, HttpError> { + let apictx = rqctx.context(); + let handler = async { + let nexus = &apictx.context.nexus; + let opctx = crate::context::op_context_for_external_api(&rqctx).await?; + let community = community.into_inner(); + + let settings = nexus + .switch_port_configuration_bgp_peer_community_add( + &opctx, + path_params.into_inner().configuration, + community.peer_address, + community, + ) + .await?; + Ok(HttpResponseCreated(settings.into())) + }; + apictx + .context + .external_latencies + .instrument_dropshot_handler(&rqctx, handler) + .await +} + +/// Remove community from bgp peer +#[endpoint { + method = POST, + path = "/v1/system/networking/switch-port-configuration/{configuration}/bgp-peer/community/remove", + tags = ["system/networking"], +}] +async fn networking_switch_port_configuration_bgp_peer_community_remove( + rqctx: RequestContext, + path_params: Path, + community: TypedBody, +) -> Result { + let apictx = rqctx.context(); + let handler = async { + let nexus = &apictx.context.nexus; + let opctx = crate::context::op_context_for_external_api(&rqctx).await?; + let community = community.into_inner(); + + nexus + .switch_port_configuration_bgp_peer_community_remove( + &opctx, + path_params.into_inner().configuration, + community.peer_address, + community, + ) + .await?; + Ok(HttpResponseDeleted()) + }; + apictx + .context + .external_latencies + .instrument_dropshot_handler(&rqctx, handler) + .await +} + +/// List switch ports +#[endpoint { + method = GET, + path = "/v1/system/hardware/switch-port", + tags = ["system/hardware"], +}] +async fn networking_switch_port_list( + rqctx: RequestContext, + query_params: Query>, +) -> Result>, HttpError> { + let apictx = rqctx.context(); + let handler = async { + let nexus = &apictx.context.nexus; + let query = query_params.into_inner(); + let pagparams = data_page_params_for(&rqctx, &query)?; + let opctx = crate::context::op_context_for_external_api(&rqctx).await?; + let addrs = nexus + .switch_port_list(&opctx, &pagparams) + .await? + .into_iter() + .map(|p| p.into()) + .collect(); + + Ok(HttpResponseOk(ScanById::results_page( + &query, + addrs, + &|_, x: &SwitchPort| x.id, + )?)) + }; + apictx + .context + .external_latencies + .instrument_dropshot_handler(&rqctx, handler) + .await +} + +/// Get switch port status +#[endpoint { + method = GET, + path = "/v1/system/hardware/switch-port/{port}/status", + tags = ["system/hardware"], +}] +async fn networking_switch_port_status( + rqctx: RequestContext, + path_params: Path, + query_params: Query, +) -> Result, HttpError> { + let apictx = rqctx.context(); + let handler = async { + let nexus = &apictx.context.nexus; + let query = query_params.into_inner(); + let path = path_params.into_inner(); + let opctx = crate::context::op_context_for_external_api(&rqctx).await?; + Ok(HttpResponseOk( + nexus + .switch_port_status(&opctx, query.switch_location, path.port) + .await?, + )) + }; + apictx + .context + .external_latencies + .instrument_dropshot_handler(&rqctx, handler) + .await +} + +/// Apply switch port settings +#[endpoint { + method = POST, + path = "/v1/system/hardware/switch-port/{port}/settings", + tags = ["system/hardware"], +}] +async fn networking_switch_port_apply_settings( + rqctx: RequestContext, + path_params: Path, + query_params: Query, + settings_body: TypedBody, +) -> Result { + let apictx = rqctx.context(); + let handler = async { + let nexus = &apictx.context.nexus; + let port = path_params.into_inner().port; + let query = query_params.into_inner(); + let settings = settings_body.into_inner(); + let opctx = crate::context::op_context_for_external_api(&rqctx).await?; + nexus + .switch_port_apply_settings(&opctx, &port, &query, &settings) + .await?; + Ok(HttpResponseUpdatedNoContent {}) + }; + apictx + .context + .external_latencies + .instrument_dropshot_handler(&rqctx, handler) + .await +} + +/// Clear switch port settings +#[endpoint { + method = DELETE, + path = "/v1/system/hardware/switch-port/{port}/settings", + tags = ["system/hardware"], +}] +async fn networking_switch_port_clear_settings( + rqctx: RequestContext, + path_params: Path, + query_params: Query, +) -> Result { + let apictx = rqctx.context(); + let handler = async { + let nexus = &apictx.context.nexus; + let port = path_params.into_inner().port; + let query = query_params.into_inner(); + let opctx = crate::context::op_context_for_external_api(&rqctx).await?; + nexus.switch_port_clear_settings(&opctx, &port, &query).await?; + Ok(HttpResponseUpdatedNoContent {}) + }; + apictx + .context + .external_latencies + .instrument_dropshot_handler(&rqctx, handler) + .await +} + +/// Create new BGP configuration +#[endpoint { + method = POST, + path = "/v1/system/networking/bgp", + tags = ["system/networking"], +}] +async fn networking_bgp_config_create( + rqctx: RequestContext, + config: TypedBody, +) -> Result, HttpError> { + let apictx = rqctx.context(); + let handler = async { + let nexus = &apictx.context.nexus; + let config = config.into_inner(); + let opctx = crate::context::op_context_for_external_api(&rqctx).await?; + let result = nexus.bgp_config_create(&opctx, &config).await?; + Ok(HttpResponseCreated::(result.into())) + }; + apictx + .context + .external_latencies + .instrument_dropshot_handler(&rqctx, handler) + .await +} + +/// List BGP configurations +#[endpoint { + method = GET, + path = "/v1/system/networking/bgp", + tags = ["system/networking"], +}] +async fn networking_bgp_config_list( + rqctx: RequestContext, + query_params: Query>, +) -> Result>, HttpError> { + let apictx = rqctx.context(); + let handler = async { + let nexus = &apictx.context.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 configs = nexus + .bgp_config_list(&opctx, &paginated_by) + .await? + .into_iter() + .map(|p| p.into()) + .collect(); + + Ok(HttpResponseOk(ScanByNameOrId::results_page( + &query, + configs, + &marker_for_name_or_id, + )?)) + }; + apictx + .context + .external_latencies + .instrument_dropshot_handler(&rqctx, handler) + .await +} + +//TODO pagination? the normal by-name/by-id stuff does not work here +/// Get BGP peer status +#[endpoint { + method = GET, + path = "/v1/system/networking/bgp-status", + tags = ["system/networking"], +}] +async fn networking_bgp_status( + rqctx: RequestContext, +) -> Result>, HttpError> { + let apictx = rqctx.context(); + let opctx = crate::context::op_context_for_external_api(&rqctx).await?; + let handler = async { + let nexus = &apictx.context.nexus; + let result = nexus.bgp_peer_status(&opctx).await?; + Ok(HttpResponseOk(result)) + }; + apictx + .context + .external_latencies + .instrument_dropshot_handler(&rqctx, handler) + .await +} + +//TODO pagination? the normal by-name/by-id stuff does not work here +/// Get BGP exported routes +#[endpoint { + method = GET, + path = "/v1/system/networking/bgp-exported", + tags = ["system/networking"], +}] +async fn networking_bgp_exported( + rqctx: RequestContext, +) -> Result, HttpError> { + let apictx = rqctx.context(); + let opctx = crate::context::op_context_for_external_api(&rqctx).await?; + let handler = async { + let nexus = &apictx.context.nexus; + let result = nexus.bgp_exported(&opctx).await?; + Ok(HttpResponseOk(result)) + }; + apictx + .context + .external_latencies + .instrument_dropshot_handler(&rqctx, handler) + .await +} + +/// Get BGP router message history +#[endpoint { + method = GET, + path = "/v1/system/networking/bgp-message-history", + tags = ["system/networking"], +}] +async fn networking_bgp_message_history( + rqctx: RequestContext, + query_params: Query, +) -> Result, HttpError> { + let apictx = rqctx.context(); + let opctx = crate::context::op_context_for_external_api(&rqctx).await?; + let handler = async { + let nexus = &apictx.context.nexus; + let sel = query_params.into_inner(); + let result = nexus.bgp_message_history(&opctx, &sel).await?; + Ok(HttpResponseOk(AggregateBgpMessageHistory::new(result))) + }; + apictx + .context + .external_latencies + .instrument_dropshot_handler(&rqctx, handler) + .await +} + +//TODO pagination? the normal by-name/by-id stuff does not work here +/// Get imported IPv4 BGP routes +#[endpoint { + method = GET, + path = "/v1/system/networking/bgp-routes-ipv4", + tags = ["system/networking"], +}] +async fn networking_bgp_imported_routes_ipv4( + rqctx: RequestContext, + query_params: Query, +) -> Result>, HttpError> { + let apictx = rqctx.context(); + let opctx = crate::context::op_context_for_external_api(&rqctx).await?; + let handler = async { + let nexus = &apictx.context.nexus; + let sel = query_params.into_inner(); + let result = nexus.bgp_imported_routes_ipv4(&opctx, &sel).await?; + Ok(HttpResponseOk(result)) + }; + apictx + .context + .external_latencies + .instrument_dropshot_handler(&rqctx, handler) + .await +} + +/// Delete BGP configuration +#[endpoint { + method = DELETE, + path = "/v1/system/networking/bgp/{bgp_config}", tags = ["system/networking"], }] async fn networking_bgp_config_delete( rqctx: RequestContext, - sel: Query, + sel: Path, ) -> Result { let apictx = rqctx.context(); let handler = async { let nexus = &apictx.context.nexus; - let sel = sel.into_inner(); + let sel = sel.into_inner().bgp_config; let opctx = crate::context::op_context_for_external_api(&rqctx).await?; nexus.bgp_config_delete(&opctx, &sel).await?; Ok(HttpResponseUpdatedNoContent {}) @@ -4052,14 +4974,14 @@ async fn networking_bgp_config_delete( async fn networking_bgp_announce_set_update( rqctx: RequestContext, config: TypedBody, -) -> Result, HttpError> { +) -> Result, HttpError> { let apictx = rqctx.context(); let handler = async { let nexus = &apictx.context.nexus; let config = config.into_inner(); let opctx = crate::context::op_context_for_external_api(&rqctx).await?; let result = nexus.bgp_update_announce_set(&opctx, &config).await?; - Ok(HttpResponseCreated::(result.0.into())) + Ok(HttpResponseOk::(result.0.into())) }; apictx .context diff --git a/nexus/tests/integration_tests/address_lots.rs b/nexus/tests/integration_tests/address_lots.rs index 7860dd463c..69ac2f0693 100644 --- a/nexus/tests/integration_tests/address_lots.rs +++ b/nexus/tests/integration_tests/address_lots.rs @@ -10,9 +10,7 @@ use nexus_test_utils::http_testing::AuthnMode; use nexus_test_utils::http_testing::NexusRequest; use nexus_test_utils::http_testing::RequestBuilder; use nexus_test_utils_macros::nexus_test; -use nexus_types::external_api::params::{ - AddressLotBlockCreate, AddressLotCreate, -}; +use nexus_types::external_api::params; use omicron_common::api::external::{ AddressLot, AddressLotBlock, AddressLotCreateResponse, AddressLotKind, IdentityMetadataCreateParams, @@ -39,22 +37,38 @@ async fn test_address_lot_basic_crud(ctx: &ControlPlaneTestContext) { assert_eq!(lots.len(), 1, "Expected one lot"); // Create a lot - let params = AddressLotCreate { + let lot_params = params::AddressLotCreate { identity: IdentityMetadataCreateParams { name: "parkinglot".parse().unwrap(), description: "an address parking lot".into(), }, kind: AddressLotKind::Infra, - blocks: vec![AddressLotBlockCreate { - first_address: "203.0.113.10".parse().unwrap(), - last_address: "203.0.113.20".parse().unwrap(), - }], }; - let response: AddressLotCreateResponse = NexusRequest::objects_post( + let block_params = params::AddressLotBlockAddRemove { + first_address: "203.0.113.10".parse().unwrap(), + last_address: "203.0.113.20".parse().unwrap(), + }; + + let lot_response: AddressLotCreateResponse = NexusRequest::objects_post( client, "/v1/system/networking/address-lot", - ¶ms, + &lot_params, + ) + .authn_as(AuthnMode::PrivilegedUser) + .execute() + .await + .unwrap() + .parsed_body() + .unwrap(); + + let block_response: AddressLotBlock = NexusRequest::objects_post( + client, + &format!( + "/v1/system/networking/address-lot/{}/blocks/add", + lot_params.identity.name + ), + &block_params, ) .authn_as(AuthnMode::PrivilegedUser) .execute() @@ -63,18 +77,20 @@ async fn test_address_lot_basic_crud(ctx: &ControlPlaneTestContext) { .parsed_body() .unwrap(); - let address_lot = response.lot; - let blocks = response.blocks; + let address_lot = lot_response.lot; + + assert_eq!(address_lot.identity.name, lot_params.identity.name); + assert_eq!( + address_lot.identity.description, + lot_params.identity.description + ); - assert_eq!(address_lot.identity.name, params.identity.name); - assert_eq!(address_lot.identity.description, params.identity.description); - assert_eq!(blocks.len(), params.blocks.len()); assert_eq!( - blocks[0].first_address, + block_response.first_address, "203.0.113.10".parse::().unwrap() ); assert_eq!( - blocks[0].last_address, + block_response.last_address, "203.0.113.20".parse::().unwrap() ); @@ -95,7 +111,10 @@ async fn test_address_lot_basic_crud(ctx: &ControlPlaneTestContext) { // Verify there are lot blocks let blist = NexusRequest::iter_collection_authn::( client, - "/v1/system/networking/address-lot/parkinglot/blocks", + &format!( + "/v1/system/networking/address-lot/{}/blocks", + lot_params.identity.name + ), "", None, ) @@ -104,7 +123,6 @@ async fn test_address_lot_basic_crud(ctx: &ControlPlaneTestContext) { .all_items; assert_eq!(blist.len(), 1, "Expected 1 address lot block"); - assert_eq!(blist[0], blocks[0]); } #[nexus_test] @@ -114,52 +132,75 @@ async fn test_address_lot_invalid_range(ctx: &ControlPlaneTestContext) { let mut params = Vec::new(); // Try to create a lot with different address families - params.push(AddressLotCreate { - identity: IdentityMetadataCreateParams { - name: "family".parse().unwrap(), - description: "an address parking lot".into(), + params.push(( + params::AddressLotCreate { + identity: IdentityMetadataCreateParams { + name: "family".parse().unwrap(), + description: "an address parking lot".into(), + }, + kind: AddressLotKind::Infra, }, - kind: AddressLotKind::Infra, - blocks: vec![AddressLotBlockCreate { + params::AddressLotBlockAddRemove { first_address: "203.0.113.10".parse().unwrap(), last_address: "fd00:1701::d".parse().unwrap(), - }], - }); + }, + )); // Try to create an IPv4 lot where the first address comes after the second. - params.push(AddressLotCreate { - identity: IdentityMetadataCreateParams { - name: "v4".parse().unwrap(), - description: "an address parking lot".into(), + params.push(( + params::AddressLotCreate { + identity: IdentityMetadataCreateParams { + name: "v4".parse().unwrap(), + description: "an address parking lot".into(), + }, + kind: AddressLotKind::Infra, }, - kind: AddressLotKind::Infra, - blocks: vec![AddressLotBlockCreate { + params::AddressLotBlockAddRemove { first_address: "203.0.113.20".parse().unwrap(), last_address: "203.0.113.10".parse().unwrap(), - }], - }); + }, + )); // Try to create an IPv6 lot where the first address comes after the second. - params.push(AddressLotCreate { - identity: IdentityMetadataCreateParams { - name: "v6".parse().unwrap(), - description: "an address parking lot".into(), + params.push(( + params::AddressLotCreate { + identity: IdentityMetadataCreateParams { + name: "v6".parse().unwrap(), + description: "an address parking lot".into(), + }, + kind: AddressLotKind::Infra, }, - kind: AddressLotKind::Infra, - blocks: vec![AddressLotBlockCreate { + params::AddressLotBlockAddRemove { first_address: "fd00:1701::d".parse().unwrap(), last_address: "fd00:1701::a".parse().unwrap(), - }], - }); + }, + )); - for params in ¶ms { + for (address_lot_params, address_lot_block_params) in ¶ms { NexusRequest::new( RequestBuilder::new( client, Method::POST, "/v1/system/networking/address-lot", ) - .body(Some(¶ms)) + .body(Some(&address_lot_params)) + .expect_status(Some(StatusCode::CREATED)), + ) + .authn_as(AuthnMode::PrivilegedUser) + .execute() + .await + .unwrap(); + + NexusRequest::new( + RequestBuilder::new( + client, + Method::POST, + &format!( + "/v1/system/networking/address-lot/{}/blocks/add", + address_lot_params.identity.name + ), + ) + .body(Some(&address_lot_block_params)) .expect_status(Some(StatusCode::BAD_REQUEST)), ) .authn_as(AuthnMode::PrivilegedUser) diff --git a/nexus/tests/integration_tests/endpoints.rs b/nexus/tests/integration_tests/endpoints.rs index 9703004c73..85d9850e2e 100644 --- a/nexus/tests/integration_tests/endpoints.rs +++ b/nexus/tests/integration_tests/endpoints.rs @@ -23,12 +23,15 @@ use nexus_types::external_api::shared; use nexus_types::external_api::shared::IpRange; use nexus_types::external_api::shared::Ipv4Range; use nexus_types::external_api::views::SledProvisionPolicy; +use omicron_common::api::external; use omicron_common::api::external::AddressLotKind; use omicron_common::api::external::AllowedSourceIps; use omicron_common::api::external::ByteCount; use omicron_common::api::external::IdentityMetadataCreateParams; use omicron_common::api::external::IdentityMetadataUpdateParams; use omicron_common::api::external::InstanceCpuCount; +use omicron_common::api::external::LinkFec; +use omicron_common::api::external::LinkSpeed; use omicron_common::api::external::Name; use omicron_common::api::external::NameOrId; use omicron_common::api::external::RouteDestination; @@ -527,9 +530,9 @@ pub static DEMO_LOOPBACK_CREATE: Lazy = }); pub const DEMO_SWITCH_PORT_SETTINGS_URL: &'static str = - "/v1/system/networking/switch-port-settings?port_settings=portofino"; + "/v1/system/networking/switch-port-configuration"; pub const DEMO_SWITCH_PORT_SETTINGS_INFO_URL: &'static str = - "/v1/system/networking/switch-port-settings/protofino"; + "/v1/system/networking/switch-port-configuration/portofino"; pub static DEMO_SWITCH_PORT_SETTINGS_CREATE: Lazy< params::SwitchPortSettingsCreate, > = Lazy::new(|| { @@ -539,12 +542,156 @@ pub static DEMO_SWITCH_PORT_SETTINGS_CREATE: Lazy< }) }); +pub const DEMO_SWITCH_PORT_GEOMETRY_URL: &'static str = + "/v1/system/networking/switch-port-configuration/portofino/geometry"; + +pub static DEMO_SWITCH_PORT_GEOMETRY_CREATE: Lazy< + params::SwitchPortConfigCreate, +> = Lazy::new(|| params::SwitchPortConfigCreate { + geometry: params::SwitchPortGeometry::Qsfp28x1, +}); + +pub const DEMO_SWITCH_PORT_LINK_URL: &'static str = + "/v1/system/networking/switch-port-configuration/portofino/link"; + +pub static DEMO_SWITCH_PORT_LINK_CREATE: Lazy = + Lazy::new(|| params::NamedLinkConfigCreate { + name: "my-link".parse().unwrap(), + mtu: 1500, + lldp_config: None, + fec: LinkFec::None, + speed: LinkSpeed::Speed100G, + autoneg: true, + }); + +pub const DEMO_SWITCH_PORT_LINK_INFO_URL: &'static str = + "/v1/system/networking/switch-port-configuration/portofino/link/my-link"; + +pub const DEMO_SWITCH_PORT_ADDRESS_URL: &'static str = + "/v1/system/networking/switch-port-configuration/portofino/address"; + +pub static DEMO_SWITCH_PORT_ADDRESS_ADD_REMOVE: Lazy = + Lazy::new(|| params::AddressAddRemove { + interface: "qsfp0".parse().unwrap(), + address_lot: NameOrId::Name("parkinglot".parse().unwrap()), + address: "203.0.113.11/24".parse().unwrap(), + vlan_id: None, + }); + +pub const DEMO_SWITCH_PORT_ADDRESS_ADD_URL: &'static str = + "/v1/system/networking/switch-port-configuration/portofino/address/add"; + +pub const DEMO_SWITCH_PORT_ADDRESS_REMOVE_URL: &'static str = + "/v1/system/networking/switch-port-configuration/portofino/address/remove"; + +pub const DEMO_SWITCH_PORT_ROUTE_URL: &'static str = + "/v1/system/networking/switch-port-configuration/portofino/route"; + +pub static DEMO_SWITCH_PORT_ROUTE_ADD_REMOVE: Lazy = + Lazy::new(|| params::RouteAddRemove { + interface: "qsfp0".parse().unwrap(), + dst: "0.0.0.0/0".parse().unwrap(), + gw: "203.0.113.1".parse().unwrap(), + vid: None, + local_pref: None, + }); + +pub const DEMO_SWITCH_PORT_ROUTE_ADD_URL: &'static str = + "/v1/system/networking/switch-port-configuration/portofino/route/add"; + +pub const DEMO_SWITCH_PORT_ROUTE_REMOVE_URL: &'static str = + "/v1/system/networking/switch-port-configuration/portofino/route/remove"; + +pub const DEMO_SWITCH_PORT_BGP_PEER_URL: &'static str = + "/v1/system/networking/switch-port-configuration/portofino/bgp-peer"; + +pub static DEMO_SWITCH_PORT_BGP_PEER_ADD: Lazy = + Lazy::new(|| external::BgpPeer { + bgp_config: NameOrId::Name("as47".parse().unwrap()), + interface_name: "qsfp0".into(), + addr: "203.0.113.12/24".parse().unwrap(), + hold_time: Default::default(), + idle_hold_time: Default::default(), + delay_open: Default::default(), + connect_retry: Default::default(), + keepalive: Default::default(), + remote_asn: None, + min_ttl: None, + md5_auth_key: None, + multi_exit_discriminator: None, + local_pref: None, + enforce_first_as: false, + allow_import_list_active: false, + allow_export_list_active: false, + vlan_id: None, + }); + +pub const DEMO_SWITCH_PORT_BGP_PEER_ADD_URL: &'static str = + "/v1/system/networking/switch-port-configuration/portofino/bgp-peer/add"; + +pub static DEMO_SWITCH_PORT_BGP_PEER_REMOVE: Lazy = + Lazy::new(|| external::BgpPeerRemove { + bgp_config: NameOrId::Name("as47".parse().unwrap()), + interface_name: "qsfp0".into(), + addr: "203.0.113.12".parse().unwrap(), + }); + +pub const DEMO_SWITCH_PORT_BGP_PEER_REMOVE_URL: &'static str = + "/v1/system/networking/switch-port-configuration/portofino/bgp-peer/remove"; + +pub static DEMO_SWITCH_PORT_BGP_PEER_ALLOWED_PREFIX: Lazy< + params::AllowedPrefixAddRemove, +> = Lazy::new(|| params::AllowedPrefixAddRemove { + peer_address: "203.0.113.12".parse().unwrap(), + interface: "qsfp0".parse().unwrap(), + prefix: "10.0.0.0/24".parse().unwrap(), +}); + +pub const DEMO_SWITCH_PORT_BGP_PEER_ALLOW_IMPORT_URL: &'static str = + "/v1/system/networking/switch-port-configuration/portofino/bgp-peer/allow-import?peer_address=203.0.113.12"; + +pub const DEMO_SWITCH_PORT_BGP_PEER_ALLOW_IMPORT_ADD_URL: &'static str = + "/v1/system/networking/switch-port-configuration/portofino/bgp-peer/allow-import/add"; + +pub const DEMO_SWITCH_PORT_BGP_PEER_ALLOW_IMPORT_REMOVE_URL: &'static str = + "/v1/system/networking/switch-port-configuration/portofino/bgp-peer/allow-import/remove"; + +pub const DEMO_SWITCH_PORT_BGP_PEER_ALLOW_EXPORT_URL: &'static str = + "/v1/system/networking/switch-port-configuration/portofino/bgp-peer/allow-export?peer_address=203.0.113.12"; + +pub const DEMO_SWITCH_PORT_BGP_PEER_ALLOW_EXPORT_ADD_URL: &'static str = + "/v1/system/networking/switch-port-configuration/portofino/bgp-peer/allow-export/add"; + +pub const DEMO_SWITCH_PORT_BGP_PEER_ALLOW_EXPORT_REMOVE_URL: &'static str = + "/v1/system/networking/switch-port-configuration/portofino/bgp-peer/allow-export/remove"; + +pub const DEMO_SWITCH_PORT_BGP_PEER_COMMUNITY_URL: &'static str = + "/v1/system/networking/switch-port-configuration/portofino/bgp-peer/community?peer_address=203.0.113.12"; + +pub static DEMO_SWITCH_PORT_BGP_PEER_COMMUNITY_ADD_REMOVE: Lazy< + params::BgpCommunityAddRemove, +> = Lazy::new(|| params::BgpCommunityAddRemove { + peer_address: "203.0.113.12".parse().unwrap(), + interface: "qsfp0".parse().unwrap(), + community: 100, +}); + +pub const DEMO_SWITCH_PORT_BGP_PEER_COMMUNITY_ADD_URL: &'static str = + "/v1/system/networking/switch-port-configuration/portofino/bgp-peer/community/add"; + +pub const DEMO_SWITCH_PORT_BGP_PEER_COMMUNITY_REMOVE_URL: &'static str = + "/v1/system/networking/switch-port-configuration/portofino/bgp-peer/community/remove"; + pub const DEMO_ADDRESS_LOTS_URL: &'static str = "/v1/system/networking/address-lot"; pub const DEMO_ADDRESS_LOT_URL: &'static str = "/v1/system/networking/address-lot/parkinglot"; pub const DEMO_ADDRESS_LOT_BLOCKS_URL: &'static str = "/v1/system/networking/address-lot/parkinglot/blocks"; +pub const DEMO_ADDRESS_LOT_BLOCK_ADD_URL: &'static str = + "/v1/system/networking/address-lot/parkinglot/blocks/add"; +pub const DEMO_ADDRESS_LOT_BLOCK_REMOVE_URL: &'static str = + "/v1/system/networking/address-lot/parkinglot/blocks/remove"; pub static DEMO_ADDRESS_LOT_CREATE: Lazy = Lazy::new(|| params::AddressLotCreate { identity: IdentityMetadataCreateParams { @@ -552,28 +699,36 @@ pub static DEMO_ADDRESS_LOT_CREATE: Lazy = description: "an address parking lot".into(), }, kind: AddressLotKind::Infra, - blocks: vec![params::AddressLotBlockCreate { - first_address: "203.0.113.10".parse().unwrap(), - last_address: "203.0.113.20".parse().unwrap(), - }], }); -pub const DEMO_BGP_CONFIG_CREATE_URL: &'static str = - "/v1/system/networking/bgp?name_or_id=as47"; +pub static DEMO_ADDRESS_LOT_BLOCK_CREATE: Lazy< + params::AddressLotBlockAddRemove, +> = Lazy::new(|| params::AddressLotBlockAddRemove { + first_address: "203.0.113.10".parse().unwrap(), + last_address: "203.0.113.20".parse().unwrap(), +}); + +pub const DEMO_BGP_CONFIG_URL: &'static str = "/v1/system/networking/bgp"; + pub static DEMO_BGP_CONFIG: Lazy = Lazy::new(|| params::BgpConfigCreate { identity: IdentityMetadataCreateParams { name: "as47".parse().unwrap(), description: "BGP config for AS47".into(), }, - bgp_announce_set_id: NameOrId::Name("instances".parse().unwrap()), + bgp_announce_set_id: NameOrId::Name("a-bag-of-addrs".parse().unwrap()), asn: 47, vrf: None, checker: None, shaper: None, }); + +pub const DEMO_BGP_CONFIG_INFO_URL: &'static str = + "/v1/system/networking/bgp/as47"; + pub const DEMO_BGP_ANNOUNCE_SET_URL: &'static str = "/v1/system/networking/bgp-announce-set"; + pub static DEMO_BGP_ANNOUNCE: Lazy = Lazy::new(|| params::BgpAnnounceSetCreate { identity: IdentityMetadataCreateParams { @@ -585,10 +740,13 @@ pub static DEMO_BGP_ANNOUNCE: Lazy = network: "10.0.0.0/16".parse().unwrap(), }], }); + pub const DEMO_BGP_ANNOUNCE_SET_DELETE_URL: &'static str = "/v1/system/networking/bgp-announce-set/a-bag-of-addrs"; + pub const DEMO_BGP_ANNOUNCEMENT_URL: &'static str = "/v1/system/networking/bgp-announce-set/a-bag-of-addrs/announcement"; + pub const DEMO_BGP_STATUS_URL: &'static str = "/v1/system/networking/bgp-status"; pub const DEMO_BGP_EXPORTED_URL: &'static str = @@ -2218,12 +2376,14 @@ pub static VERIFY_ENDPOINTS: Lazy> = Lazy::new(|| { }, VerifyEndpoint { - url: &DEMO_ADDRESS_LOT_URL, + url: &DEMO_ADDRESS_LOT_BLOCK_ADD_URL, visibility: Visibility::Protected, unprivileged_access: UnprivilegedAccess::None, allowed_methods: vec![ - AllowedMethod::Delete, - ] + AllowedMethod::Post( + serde_json::to_value(&*DEMO_ADDRESS_LOT_BLOCK_CREATE).unwrap(), + ), + ], }, VerifyEndpoint { @@ -2231,10 +2391,39 @@ pub static VERIFY_ENDPOINTS: Lazy> = Lazy::new(|| { visibility: Visibility::Protected, unprivileged_access: UnprivilegedAccess::None, allowed_methods: vec![ - AllowedMethod::GetNonexistent + AllowedMethod::Get, ], }, + VerifyEndpoint { + url: &DEMO_ADDRESS_LOT_BLOCK_REMOVE_URL, + visibility: Visibility::Protected, + unprivileged_access: UnprivilegedAccess::None, + allowed_methods: vec![ + AllowedMethod::Post( + serde_json::to_value(&*DEMO_ADDRESS_LOT_BLOCK_CREATE).unwrap(), + ), + ], + }, + + VerifyEndpoint { + url: &DEMO_ADDRESS_LOT_URL, + visibility: Visibility::Protected, + unprivileged_access: UnprivilegedAccess::None, + allowed_methods: vec![ + AllowedMethod::Delete, + ], + }, + + VerifyEndpoint { + url: &DEMO_ADDRESS_LOT_URL, + visibility: Visibility::Protected, + unprivileged_access: UnprivilegedAccess::None, + allowed_methods: vec![ + AllowedMethod::Delete, + ] + }, + VerifyEndpoint { url: &DEMO_LOOPBACK_CREATE_URL, visibility: Visibility::Public, @@ -2266,7 +2455,6 @@ pub static VERIFY_ENDPOINTS: Lazy> = Lazy::new(|| { &*DEMO_SWITCH_PORT_SETTINGS_CREATE).unwrap(), ), AllowedMethod::Get, - AllowedMethod::Delete ], }, @@ -2275,12 +2463,259 @@ pub static VERIFY_ENDPOINTS: Lazy> = Lazy::new(|| { visibility: Visibility::Public, unprivileged_access: UnprivilegedAccess::None, allowed_methods: vec![ - AllowedMethod::GetNonexistent + AllowedMethod::Get, + AllowedMethod::Delete, + ], + }, + + VerifyEndpoint { + url: &DEMO_SWITCH_PORT_GEOMETRY_URL, + visibility: Visibility::Public, + unprivileged_access: UnprivilegedAccess::None, + allowed_methods: vec![ + AllowedMethod::Post( + serde_json::to_value( + &*DEMO_SWITCH_PORT_GEOMETRY_CREATE).unwrap(), + ), + AllowedMethod::Get, + ], + }, + + VerifyEndpoint { + url: &DEMO_SWITCH_PORT_LINK_URL, + visibility: Visibility::Public, + unprivileged_access: UnprivilegedAccess::None, + allowed_methods: vec![ + AllowedMethod::Post( + serde_json::to_value( + &*DEMO_SWITCH_PORT_LINK_CREATE).unwrap(), + ), + AllowedMethod::Get, + ], + }, + + VerifyEndpoint { + url: &DEMO_SWITCH_PORT_LINK_INFO_URL, + visibility: Visibility::Public, + unprivileged_access: UnprivilegedAccess::None, + allowed_methods: vec![ + AllowedMethod::Get, + AllowedMethod::Delete, + ], + }, + + VerifyEndpoint { + url: &DEMO_SWITCH_PORT_ADDRESS_URL, + visibility: Visibility::Public, + unprivileged_access: UnprivilegedAccess::None, + allowed_methods: vec![ + AllowedMethod::Get, + ], + }, + + VerifyEndpoint { + url: &DEMO_SWITCH_PORT_ADDRESS_ADD_URL, + visibility: Visibility::Public, + unprivileged_access: UnprivilegedAccess::None, + allowed_methods: vec![ + AllowedMethod::Post( + serde_json::to_value( + &*DEMO_SWITCH_PORT_ADDRESS_ADD_REMOVE + ).unwrap() + ), + ], + }, + + VerifyEndpoint { + url: &DEMO_SWITCH_PORT_ADDRESS_REMOVE_URL, + visibility: Visibility::Public, + unprivileged_access: UnprivilegedAccess::None, + allowed_methods: vec![ + AllowedMethod::Post( + serde_json::to_value( + &*DEMO_SWITCH_PORT_ADDRESS_ADD_REMOVE + ).unwrap() + ), + ], + }, + + VerifyEndpoint { + url: &DEMO_SWITCH_PORT_ROUTE_URL, + visibility: Visibility::Public, + unprivileged_access: UnprivilegedAccess::None, + allowed_methods: vec![ + AllowedMethod::Get, ], }, VerifyEndpoint { - url: &DEMO_BGP_CONFIG_CREATE_URL, + url: &DEMO_SWITCH_PORT_ROUTE_ADD_URL, + visibility: Visibility::Public, + unprivileged_access: UnprivilegedAccess::None, + allowed_methods: vec![ + AllowedMethod::Post( + serde_json::to_value( + &*DEMO_SWITCH_PORT_ROUTE_ADD_REMOVE + ).unwrap() + ), + ], + }, + + VerifyEndpoint { + url: &DEMO_SWITCH_PORT_ROUTE_REMOVE_URL, + visibility: Visibility::Public, + unprivileged_access: UnprivilegedAccess::None, + allowed_methods: vec![ + AllowedMethod::Post( + serde_json::to_value( + &*DEMO_SWITCH_PORT_ROUTE_ADD_REMOVE + ).unwrap() + ), + ], + }, + + VerifyEndpoint { + url: &DEMO_SWITCH_PORT_BGP_PEER_URL, + visibility: Visibility::Public, + unprivileged_access: UnprivilegedAccess::None, + allowed_methods: vec![ + AllowedMethod::Get, + ], + }, + + VerifyEndpoint { + url: &DEMO_SWITCH_PORT_BGP_PEER_ADD_URL, + visibility: Visibility::Public, + unprivileged_access: UnprivilegedAccess::None, + allowed_methods: vec![ + AllowedMethod::Post( + serde_json::to_value( + &*DEMO_SWITCH_PORT_BGP_PEER_ADD + ).unwrap() + ), + ], + }, + + VerifyEndpoint { + url: &DEMO_SWITCH_PORT_BGP_PEER_REMOVE_URL, + visibility: Visibility::Public, + unprivileged_access: UnprivilegedAccess::None, + allowed_methods: vec![ + AllowedMethod::Post( + serde_json::to_value( + &*DEMO_SWITCH_PORT_BGP_PEER_REMOVE + ).unwrap() + ), + ], + }, + + VerifyEndpoint { + url: &DEMO_SWITCH_PORT_BGP_PEER_ALLOW_IMPORT_URL, + visibility: Visibility::Public, + unprivileged_access: UnprivilegedAccess::None, + allowed_methods: vec![ + AllowedMethod::Get, + ], + }, + + VerifyEndpoint { + url: &DEMO_SWITCH_PORT_BGP_PEER_ALLOW_IMPORT_ADD_URL, + visibility: Visibility::Public, + unprivileged_access: UnprivilegedAccess::None, + allowed_methods: vec![ + AllowedMethod::Post( + serde_json::to_value( + &*DEMO_SWITCH_PORT_BGP_PEER_ALLOWED_PREFIX + ).unwrap() + ), + ], + }, + + VerifyEndpoint { + url: &DEMO_SWITCH_PORT_BGP_PEER_ALLOW_IMPORT_REMOVE_URL, + visibility: Visibility::Public, + unprivileged_access: UnprivilegedAccess::None, + allowed_methods: vec![ + AllowedMethod::Post( + serde_json::to_value( + &*DEMO_SWITCH_PORT_BGP_PEER_ALLOWED_PREFIX + ).unwrap() + ), + ], + }, + + VerifyEndpoint { + url: &DEMO_SWITCH_PORT_BGP_PEER_ALLOW_EXPORT_URL, + visibility: Visibility::Public, + unprivileged_access: UnprivilegedAccess::None, + allowed_methods: vec![ + AllowedMethod::Get, + ], + }, + + VerifyEndpoint { + url: &DEMO_SWITCH_PORT_BGP_PEER_ALLOW_EXPORT_ADD_URL, + visibility: Visibility::Public, + unprivileged_access: UnprivilegedAccess::None, + allowed_methods: vec![ + AllowedMethod::Post( + serde_json::to_value( + &*DEMO_SWITCH_PORT_BGP_PEER_ALLOWED_PREFIX + ).unwrap() + ), + ], + }, + + VerifyEndpoint { + url: &DEMO_SWITCH_PORT_BGP_PEER_ALLOW_EXPORT_REMOVE_URL, + visibility: Visibility::Public, + unprivileged_access: UnprivilegedAccess::None, + allowed_methods: vec![ + AllowedMethod::Post( + serde_json::to_value( + &*DEMO_SWITCH_PORT_BGP_PEER_ALLOWED_PREFIX + ).unwrap() + ), + ], + }, + + VerifyEndpoint { + url: &DEMO_SWITCH_PORT_BGP_PEER_COMMUNITY_URL, + visibility: Visibility::Public, + unprivileged_access: UnprivilegedAccess::None, + allowed_methods: vec![ + AllowedMethod::Get, + ], + }, + + VerifyEndpoint { + url: &DEMO_SWITCH_PORT_BGP_PEER_COMMUNITY_ADD_URL, + visibility: Visibility::Public, + unprivileged_access: UnprivilegedAccess::None, + allowed_methods: vec![ + AllowedMethod::Post( + serde_json::to_value( + &*DEMO_SWITCH_PORT_BGP_PEER_COMMUNITY_ADD_REMOVE + ).unwrap() + ), + ], + }, + + VerifyEndpoint { + url: &DEMO_SWITCH_PORT_BGP_PEER_COMMUNITY_REMOVE_URL, + visibility: Visibility::Public, + unprivileged_access: UnprivilegedAccess::None, + allowed_methods: vec![ + AllowedMethod::Post( + serde_json::to_value( + &*DEMO_SWITCH_PORT_BGP_PEER_COMMUNITY_ADD_REMOVE + ).unwrap() + ), + ], + }, + + VerifyEndpoint { + url: &DEMO_BGP_CONFIG_URL, visibility: Visibility::Public, unprivileged_access: UnprivilegedAccess::None, allowed_methods: vec![ @@ -2288,6 +2723,14 @@ pub static VERIFY_ENDPOINTS: Lazy> = Lazy::new(|| { serde_json::to_value(&*DEMO_BGP_CONFIG).unwrap(), ), AllowedMethod::Get, + ], + }, + + VerifyEndpoint { + url: &DEMO_BGP_CONFIG_INFO_URL, + visibility: Visibility::Public, + unprivileged_access: UnprivilegedAccess::None, + allowed_methods: vec![ AllowedMethod::Delete ], }, diff --git a/nexus/tests/integration_tests/switch_port.rs b/nexus/tests/integration_tests/switch_port.rs index 92c44eddad..7e94c71ec9 100644 --- a/nexus/tests/integration_tests/switch_port.rs +++ b/nexus/tests/integration_tests/switch_port.rs @@ -9,7 +9,7 @@ use http::StatusCode; use nexus_test_utils::http_testing::{AuthnMode, NexusRequest, RequestBuilder}; use nexus_test_utils_macros::nexus_test; use nexus_types::external_api::params::{ - Address, AddressConfig, AddressLotBlockCreate, AddressLotCreate, + Address, AddressConfig, AddressLotBlockAddRemove, AddressLotCreate, BgpAnnounceSetCreate, BgpAnnouncementCreate, BgpConfigCreate, BgpPeerConfig, LinkConfigCreate, LldpLinkConfigCreate, Route, RouteConfig, SwitchInterfaceConfigCreate, SwitchInterfaceKind, SwitchPortApplySettings, @@ -18,8 +18,8 @@ use nexus_types::external_api::params::{ use nexus_types::external_api::views::Rack; use omicron_common::api::external::ImportExportPolicy; use omicron_common::api::external::{ - self, AddressLotKind, BgpPeer, IdentityMetadataCreateParams, LinkFec, - LinkSpeed, NameOrId, SwitchPort, SwitchPortSettingsView, + self, AddressLotKind, BgpPeerCombined, IdentityMetadataCreateParams, + LinkFec, LinkSpeed, NameOrId, SwitchPort, SwitchPortSettingsView, }; type ControlPlaneTestContext = @@ -40,18 +40,19 @@ async fn test_port_settings_basic_crud(ctx: &ControlPlaneTestContext) { description: "an address parking lot".into(), }, kind: AddressLotKind::Infra, - blocks: vec![ - AddressLotBlockCreate { - first_address: "203.0.113.10".parse().unwrap(), - last_address: "203.0.113.20".parse().unwrap(), - }, - AddressLotBlockCreate { - first_address: "1.2.3.0".parse().unwrap(), - last_address: "1.2.3.255".parse().unwrap(), - }, - ], }; + let block_params = vec![ + AddressLotBlockAddRemove { + first_address: "203.0.113.10".parse().unwrap(), + last_address: "203.0.113.20".parse().unwrap(), + }, + AddressLotBlockAddRemove { + first_address: "1.2.3.0".parse().unwrap(), + last_address: "1.2.3.255".parse().unwrap(), + }, + ]; + NexusRequest::objects_post( client, "/v1/system/networking/address-lot", @@ -62,6 +63,21 @@ async fn test_port_settings_basic_crud(ctx: &ControlPlaneTestContext) { .await .unwrap(); + for params in block_params { + NexusRequest::objects_post( + client, + &format!( + "/v1/system/networking/address-lot/{}/blocks", + lot_params.identity.name + ), + ¶ms, + ) + .authn_as(AuthnMode::PrivilegedUser) + .execute() + .await + .unwrap(); + } + // Create BGP announce set let announce_set = BgpAnnounceSetCreate { identity: IdentityMetadataCreateParams { @@ -282,7 +298,7 @@ async fn test_port_settings_basic_crud(ctx: &ControlPlaneTestContext) { settings.bgp_peers.insert( "phy0".into(), BgpPeerConfig { - peers: vec![BgpPeer { + peers: vec![BgpPeerCombined { bgp_config: NameOrId::Name("as47".parse().unwrap()), interface_name: "phy0".to_string(), addr: "1.2.3.4".parse().unwrap(), diff --git a/nexus/tests/integration_tests/unauthorized.rs b/nexus/tests/integration_tests/unauthorized.rs index 93e40dbc2e..c8f8262b0b 100644 --- a/nexus/tests/integration_tests/unauthorized.rs +++ b/nexus/tests/integration_tests/unauthorized.rs @@ -96,6 +96,16 @@ async fn test_unauthorized(cptestctx: &ControlPlaneTestContext) { .unwrap(), id_routes, ), + SetupReq::Put { url, body, id_routes } => ( + url, + NexusRequest::object_put(client, url, Some(body)) + .authn_as(AuthnMode::PrivilegedUser) + .execute() + .await + .map_err(|e| panic!("Failed to PUT to URL: {url}, {e}")) + .unwrap(), + id_routes, + ), }; setup_results.insert(url, result.clone()); @@ -169,6 +179,11 @@ enum SetupReq { body: serde_json::Value, id_routes: Vec<&'static str>, }, + Put { + url: &'static str, + body: serde_json::Value, + id_routes: Vec<&'static str>, + }, } pub static HTTP_SERVER: Lazy = @@ -214,6 +229,19 @@ static SETUP_REQUESTS: Lazy> = Lazy::new(|| { &*DEMO_SILO_USER_ID_SET_PASSWORD_URL, ], }, + // Create the default Address Lot + SetupReq::Post { + url: &DEMO_ADDRESS_LOTS_URL, + body: serde_json::to_value(&*DEMO_ADDRESS_LOT_CREATE).unwrap(), + id_routes: vec!["/v1/system/networking/address-lot/{id}"], + }, + // Create the default Address Lot Block + SetupReq::Post { + url: &DEMO_ADDRESS_LOT_BLOCK_ADD_URL, + body: serde_json::to_value(&*DEMO_ADDRESS_LOT_BLOCK_CREATE) + .unwrap(), + id_routes: vec![], + }, // Create the default IP pool SetupReq::Post { url: &DEMO_IP_POOLS_URL, @@ -321,6 +349,86 @@ static SETUP_REQUESTS: Lazy> = Lazy::new(|| { body: serde_json::to_value(&*DEMO_CERTIFICATE_CREATE).unwrap(), id_routes: vec![], }, + // Create a switch port configuration + SetupReq::Post { + url: &DEMO_SWITCH_PORT_SETTINGS_URL, + body: serde_json::to_value(&*DEMO_SWITCH_PORT_SETTINGS_CREATE) + .unwrap(), + id_routes: vec![], + }, + // Create a switch port geometry + SetupReq::Post { + url: &DEMO_SWITCH_PORT_GEOMETRY_URL, + body: serde_json::to_value(&*DEMO_SWITCH_PORT_GEOMETRY_CREATE) + .unwrap(), + id_routes: vec![], + }, + // Create a switch port link + SetupReq::Post { + url: &DEMO_SWITCH_PORT_LINK_URL, + body: serde_json::to_value(&*DEMO_SWITCH_PORT_LINK_CREATE).unwrap(), + id_routes: vec![], + }, + // Create a switch port address + SetupReq::Post { + url: &DEMO_SWITCH_PORT_ADDRESS_ADD_URL, + body: serde_json::to_value(&*DEMO_SWITCH_PORT_ADDRESS_ADD_REMOVE) + .unwrap(), + id_routes: vec![], + }, + // Create a switch port route + SetupReq::Post { + url: &DEMO_SWITCH_PORT_ROUTE_ADD_URL, + body: serde_json::to_value(&*DEMO_SWITCH_PORT_ROUTE_ADD_REMOVE) + .unwrap(), + id_routes: vec![], + }, + // Create a bgp announce set + SetupReq::Put { + url: &DEMO_BGP_ANNOUNCE_SET_URL, + body: serde_json::to_value(&*DEMO_BGP_ANNOUNCE).unwrap(), + id_routes: vec![], + }, + // Create a bgp config + SetupReq::Post { + url: &DEMO_BGP_CONFIG_URL, + body: serde_json::to_value(&*DEMO_BGP_CONFIG).unwrap(), + id_routes: vec![], + }, + // Create a switch port bgp peer + SetupReq::Post { + url: &DEMO_SWITCH_PORT_BGP_PEER_ADD_URL, + body: serde_json::to_value(&*DEMO_SWITCH_PORT_BGP_PEER_ADD) + .unwrap(), + id_routes: vec![], + }, + // Allow a prefix to be exported by a peer + SetupReq::Post { + url: &DEMO_SWITCH_PORT_BGP_PEER_ALLOW_IMPORT_ADD_URL, + body: serde_json::to_value( + &*DEMO_SWITCH_PORT_BGP_PEER_ALLOWED_PREFIX, + ) + .unwrap(), + id_routes: vec![], + }, + // Allow a prefix to be imported by a peer + SetupReq::Post { + url: &DEMO_SWITCH_PORT_BGP_PEER_ALLOW_EXPORT_ADD_URL, + body: serde_json::to_value( + &*DEMO_SWITCH_PORT_BGP_PEER_ALLOWED_PREFIX, + ) + .unwrap(), + id_routes: vec![], + }, + // Add a community to a peer + SetupReq::Post { + url: &DEMO_SWITCH_PORT_BGP_PEER_COMMUNITY_ADD_URL, + body: serde_json::to_value( + &*DEMO_SWITCH_PORT_BGP_PEER_COMMUNITY_ADD_REMOVE, + ) + .unwrap(), + id_routes: vec![], + }, ] }); diff --git a/nexus/tests/output/nexus_tags.txt b/nexus/tests/output/nexus_tags.txt index bde11e2de3..b08057f6e2 100644 --- a/nexus/tests/output/nexus_tags.txt +++ b/nexus/tests/output/nexus_tags.txt @@ -169,7 +169,9 @@ ip_pool_silo_update PUT /v1/system/ip-pools/{pool}/sil ip_pool_update PUT /v1/system/ip-pools/{pool} ip_pool_utilization_view GET /v1/system/ip-pools/{pool}/utilization ip_pool_view GET /v1/system/ip-pools/{pool} +networking_address_lot_block_add POST /v1/system/networking/address-lot/{address_lot}/blocks/add networking_address_lot_block_list GET /v1/system/networking/address-lot/{address_lot}/blocks +networking_address_lot_block_remove POST /v1/system/networking/address-lot/{address_lot}/blocks/remove networking_address_lot_create POST /v1/system/networking/address-lot networking_address_lot_delete DELETE /v1/system/networking/address-lot/{address_lot} networking_address_lot_list GET /v1/system/networking/address-lot @@ -183,7 +185,7 @@ networking_bgp_announce_set_list GET /v1/system/networking/bgp-anno networking_bgp_announce_set_update PUT /v1/system/networking/bgp-announce-set networking_bgp_announcement_list GET /v1/system/networking/bgp-announce-set/{name_or_id}/announcement networking_bgp_config_create POST /v1/system/networking/bgp -networking_bgp_config_delete DELETE /v1/system/networking/bgp +networking_bgp_config_delete DELETE /v1/system/networking/bgp/{bgp_config} networking_bgp_config_list GET /v1/system/networking/bgp networking_bgp_exported GET /v1/system/networking/bgp-exported networking_bgp_imported_routes_ipv4 GET /v1/system/networking/bgp-routes-ipv4 @@ -192,10 +194,34 @@ networking_bgp_status GET /v1/system/networking/bgp-stat networking_loopback_address_create POST /v1/system/networking/loopback-address networking_loopback_address_delete DELETE /v1/system/networking/loopback-address/{rack_id}/{switch_location}/{address}/{subnet_mask} networking_loopback_address_list GET /v1/system/networking/loopback-address -networking_switch_port_settings_create POST /v1/system/networking/switch-port-settings -networking_switch_port_settings_delete DELETE /v1/system/networking/switch-port-settings -networking_switch_port_settings_list GET /v1/system/networking/switch-port-settings -networking_switch_port_settings_view GET /v1/system/networking/switch-port-settings/{port} +networking_switch_port_configuration_address_add POST /v1/system/networking/switch-port-configuration/{configuration}/address/add +networking_switch_port_configuration_address_list GET /v1/system/networking/switch-port-configuration/{configuration}/address +networking_switch_port_configuration_address_remove POST /v1/system/networking/switch-port-configuration/{configuration}/address/remove +networking_switch_port_configuration_bgp_peer_add POST /v1/system/networking/switch-port-configuration/{configuration}/bgp-peer/add +networking_switch_port_configuration_bgp_peer_allow_export_add POST /v1/system/networking/switch-port-configuration/{configuration}/bgp-peer/allow-export/add +networking_switch_port_configuration_bgp_peer_allow_export_list GET /v1/system/networking/switch-port-configuration/{configuration}/bgp-peer/allow-export +networking_switch_port_configuration_bgp_peer_allow_export_remove POST /v1/system/networking/switch-port-configuration/{configuration}/bgp-peer/allow-export/remove +networking_switch_port_configuration_bgp_peer_allow_import_add POST /v1/system/networking/switch-port-configuration/{configuration}/bgp-peer/allow-import/add +networking_switch_port_configuration_bgp_peer_allow_import_list GET /v1/system/networking/switch-port-configuration/{configuration}/bgp-peer/allow-import +networking_switch_port_configuration_bgp_peer_allow_import_remove POST /v1/system/networking/switch-port-configuration/{configuration}/bgp-peer/allow-import/remove +networking_switch_port_configuration_bgp_peer_community_add POST /v1/system/networking/switch-port-configuration/{configuration}/bgp-peer/community/add +networking_switch_port_configuration_bgp_peer_community_list GET /v1/system/networking/switch-port-configuration/{configuration}/bgp-peer/community +networking_switch_port_configuration_bgp_peer_community_remove POST /v1/system/networking/switch-port-configuration/{configuration}/bgp-peer/community/remove +networking_switch_port_configuration_bgp_peer_list GET /v1/system/networking/switch-port-configuration/{configuration}/bgp-peer +networking_switch_port_configuration_bgp_peer_remove POST /v1/system/networking/switch-port-configuration/{configuration}/bgp-peer/remove +networking_switch_port_configuration_create POST /v1/system/networking/switch-port-configuration +networking_switch_port_configuration_delete DELETE /v1/system/networking/switch-port-configuration/{configuration} +networking_switch_port_configuration_geometry_set POST /v1/system/networking/switch-port-configuration/{configuration}/geometry +networking_switch_port_configuration_geometry_view GET /v1/system/networking/switch-port-configuration/{configuration}/geometry +networking_switch_port_configuration_link_create POST /v1/system/networking/switch-port-configuration/{configuration}/link +networking_switch_port_configuration_link_delete DELETE /v1/system/networking/switch-port-configuration/{configuration}/link/{link} +networking_switch_port_configuration_link_list GET /v1/system/networking/switch-port-configuration/{configuration}/link +networking_switch_port_configuration_link_view GET /v1/system/networking/switch-port-configuration/{configuration}/link/{link} +networking_switch_port_configuration_list GET /v1/system/networking/switch-port-configuration +networking_switch_port_configuration_route_add POST /v1/system/networking/switch-port-configuration/{configuration}/route/add +networking_switch_port_configuration_route_list GET /v1/system/networking/switch-port-configuration/{configuration}/route +networking_switch_port_configuration_route_remove POST /v1/system/networking/switch-port-configuration/{configuration}/route/remove +networking_switch_port_configuration_view GET /v1/system/networking/switch-port-configuration/{configuration} API operations found with tag "system/silos" OPERATION ID METHOD URL PATH diff --git a/nexus/tests/output/unexpected-authz-endpoints.txt b/nexus/tests/output/unexpected-authz-endpoints.txt index cd05058762..23235ecf64 100644 --- a/nexus/tests/output/unexpected-authz-endpoints.txt +++ b/nexus/tests/output/unexpected-authz-endpoints.txt @@ -1,3 +1,4 @@ API endpoints tested by unauthorized.rs but not found in the OpenAPI spec: PUT "/v1/system/update/repository?file_name=demo-repo.zip" GET "/v1/system/update/repository/1.0.0" +DELETE "/v1/system/networking/address-lot/parkinglot" diff --git a/nexus/types/src/external_api/params.rs b/nexus/types/src/external_api/params.rs index 83897cbd1d..ca20792bb8 100644 --- a/nexus/types/src/external_api/params.rs +++ b/nexus/types/src/external_api/params.rs @@ -9,8 +9,8 @@ use crate::external_api::shared; use base64::Engine; use chrono::{DateTime, Utc}; use omicron_common::api::external::{ - AddressLotKind, AllowedSourceIps, BfdMode, BgpPeer, ByteCount, Hostname, - IdentityMetadataCreateParams, IdentityMetadataUpdateParams, + AddressLotKind, AllowedSourceIps, BfdMode, BgpPeerCombined, ByteCount, + Hostname, IdentityMetadataCreateParams, IdentityMetadataUpdateParams, InstanceCpuCount, LinkFec, LinkSpeed, Name, NameOrId, PaginationOrder, RouteDestination, RouteTarget, SemverVersion, UserId, }; @@ -1381,14 +1381,12 @@ pub struct AddressLotCreate { pub identity: IdentityMetadataCreateParams, /// The kind of address lot to create. pub kind: AddressLotKind, - /// The blocks to add along with the new address lot. - pub blocks: Vec, } -/// Parameters for creating an address lot block. Fist and last addresses are -/// inclusive. +/// Parameters for adding or removing an address lot block. +/// First and last addresses are inclusive. #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] -pub struct AddressLotBlockCreate { +pub struct AddressLotBlockAddRemove { /// The first address in the lot (inclusive). pub first_address: IpAddr, /// The last address in the lot (inclusive). @@ -1512,6 +1510,28 @@ pub struct LinkConfigCreate { pub autoneg: bool, } +/// Named switch link configuration. +#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] +pub struct NamedLinkConfigCreate { + /// Name of link + pub name: Name, + + /// Maximum transmission unit for the link. + pub mtu: u16, + + /// The optional link-layer discovery protocol (LLDP) configuration for the link. + pub lldp_config: Option, + + /// The forward error correction mode of the link. + pub fec: LinkFec, + + /// The speed of the link. + pub speed: LinkSpeed, + + /// Whether or not to set autonegotiation + pub autoneg: bool, +} + /// The LLDP configuration associated with a port. #[derive(Clone, Debug, Default, Deserialize, Serialize, JsonSchema)] pub struct LldpLinkConfigCreate { @@ -1595,16 +1615,62 @@ pub struct Route { /// VLAN id the gateway is reachable over. pub vid: Option, - /// Local preference for route. Higher preference indictes precedence + /// Local preference for route. Higher preference indicates precedence /// within and across protocols. pub local_pref: Option, } +/// A network route to to add to or remove from an interface. +#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] +pub struct RouteAddRemove { + /// The interface to configure the route on + pub interface: Name, + + /// The route destination. + pub dst: IpNet, + + /// The route gateway. + pub gw: IpAddr, + + /// VLAN id the gateway is reachable over. + pub vid: Option, + + /// Local preference for route. Higher preference indicates precedence + /// within and across protocols. + pub local_pref: Option, +} + +/// A prefix allowed to be imported or exported by a bgp peer +#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] +pub struct AllowedPrefixAddRemove { + /// An address identifying the target bgp peer + pub peer_address: IpAddr, + + /// The interface the peer is configured on + pub interface: Name, + + /// The allowed prefix to add or remove + pub prefix: IpNet, +} + +/// A community to be added to or removed from a bgp peer +#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] +pub struct BgpCommunityAddRemove { + /// An address identifying the target bgp peer + pub peer_address: IpAddr, + + /// The interface the peer is configured on + pub interface: Name, + + /// The community to add or remove + pub community: u32, +} + /// Select a BGP config by a name or id. #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, PartialEq)] pub struct BgpConfigSelector { /// A name or id to use when selecting BGP config. - pub name_or_id: NameOrId, + pub bgp_config: NameOrId, } /// List BGP configs with an optional name or id. @@ -1616,7 +1682,7 @@ pub struct BgpConfigListSelector { #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] pub struct BgpPeerConfig { - pub peers: Vec, + pub peers: Vec, } /// Parameters for creating a named set of BGP announcements. @@ -1636,6 +1702,13 @@ pub struct OptionalBgpAnnounceSetSelector { pub name_or_id: Option, } +/// Optionally select a BGP Peer by a name or id. +#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, PartialEq)] +pub struct OptionalBgpPeerSelector { + /// A name or id to use when filtering or paginating bgp peers + pub name_or_id: Option, +} + /// Select a BGP announce set by a name or id. #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, PartialEq)] pub struct BgpAnnounceSetSelector { @@ -1748,7 +1821,23 @@ pub struct Address { /// The address lot this address is drawn from. pub address_lot: NameOrId, - /// The address and prefix length of this address. + /// The address and subnet mask + pub address: IpNet, + + /// Optional VLAN ID for this address + pub vlan_id: Option, +} + +/// An address to be added to or removed from an interface +#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] +pub struct AddressAddRemove { + /// The name of the interface + pub interface: Name, + + /// The address lot this address is drawn from. + pub address_lot: NameOrId, + + /// The address and subnet mask pub address: IpNet, /// Optional VLAN ID for this address @@ -1758,15 +1847,32 @@ pub struct Address { /// Select a port settings object by an optional name or id. #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, PartialEq)] pub struct SwitchPortSettingsSelector { - /// An optional name or id to use when selecting port settings. - pub port_settings: Option, + /// An optional name or id to use when selecting a switch port configuration. + pub configuration: Option, } /// Select a port settings info object by name or id. #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, PartialEq)] pub struct SwitchPortSettingsInfoSelector { - /// A name or id to use when selecting switch port settings info objects. - pub port: NameOrId, + /// A name or id to use when selecting a switch port configuration. + pub configuration: NameOrId, +} + +/// Select a bgp peer by address. +#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, PartialEq)] +pub struct BgpPeerQuerySelector { + /// An address identifying a configured bgp peer. + pub peer_address: IpAddr, +} + +/// Select a link settings info object by port settings name and link name. +#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, PartialEq)] +pub struct SwitchPortSettingsLinkInfoSelector { + /// A name or id to use when selecting a switch port configuration. + pub configuration: NameOrId, + + /// Link name + pub link: Name, } /// Select a switch port by name. @@ -1793,6 +1899,13 @@ pub struct SwitchPortPageSelector { pub switch_port_id: Option, } +/// Select switch port interface config by id +#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, PartialEq)] +pub struct SwitchPortInterfaceConfigPageSelector { + /// An optional switch port id to use when listing switch ports. + pub switch_port_id: Option, +} + /// Parameters for applying settings to switch ports. #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, PartialEq)] pub struct SwitchPortApplySettings { diff --git a/openapi/nexus.json b/openapi/nexus.json index 285dcd82bb..f90f16b705 100644 --- a/openapi/nexus.json +++ b/openapi/nexus.json @@ -6249,6 +6249,95 @@ } } }, + "/v1/system/networking/address-lot/{address_lot}/blocks/add": { + "post": { + "tags": [ + "system/networking" + ], + "summary": "Add block to address lot", + "operationId": "networking_address_lot_block_add", + "parameters": [ + { + "in": "path", + "name": "address_lot", + "description": "Name or ID of the address lot", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AddressLotBlockAddRemove" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AddressLotBlock" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/networking/address-lot/{address_lot}/blocks/remove": { + "post": { + "tags": [ + "system/networking" + ], + "summary": "Remove block from address lot", + "operationId": "networking_address_lot_block_remove", + "parameters": [ + { + "in": "path", + "name": "address_lot", + "description": "Name or ID of the address lot", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AddressLotBlockAddRemove" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, "/v1/system/networking/allow-list": { "get": { "tags": [ @@ -6502,7 +6591,9 @@ "$ref": "#/components/responses/Error" } } - }, + } + }, + "/v1/system/networking/bgp/{bgp_config}": { "delete": { "tags": [ "system/networking" @@ -6511,8 +6602,8 @@ "operationId": "networking_bgp_config_delete", "parameters": [ { - "in": "query", - "name": "name_or_id", + "in": "path", + "name": "bgp_config", "description": "A name or id to use when selecting BGP config.", "required": true, "schema": { @@ -6621,8 +6712,8 @@ "required": true }, "responses": { - "201": { - "description": "successful creation", + "200": { + "description": "successful operation", "content": { "application/json": { "schema": { @@ -6979,19 +7070,1130 @@ }, { "in": "path", - "name": "subnet_mask", - "description": "The IP address and subnet mask to use when selecting the loopback address.", + "name": "subnet_mask", + "description": "The IP address and subnet mask to use when selecting the loopback address.", + "required": true, + "schema": { + "type": "integer", + "format": "uint8", + "minimum": 0 + } + }, + { + "in": "path", + "name": "switch_location", + "description": "The switch location to use when selecting the loopback address.", + "required": true, + "schema": { + "$ref": "#/components/schemas/Name" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/networking/switch-port-configuration": { + "get": { + "tags": [ + "system/networking" + ], + "summary": "List switch port settings", + "operationId": "networking_switch_port_configuration_list", + "parameters": [ + { + "in": "query", + "name": "configuration", + "description": "An optional name or id to use when selecting a switch port configuration.", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SwitchPortSettingsResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + }, + "post": { + "tags": [ + "system/networking" + ], + "summary": "Create switch port settings", + "operationId": "networking_switch_port_configuration_create", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SwitchPortSettingsCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SwitchPortSettingsView" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/networking/switch-port-configuration/{configuration}": { + "get": { + "tags": [ + "system/networking" + ], + "summary": "Get information about a named set of switch-port-settings", + "operationId": "networking_switch_port_configuration_view", + "parameters": [ + { + "in": "path", + "name": "configuration", + "description": "A name or id to use when selecting a switch port configuration.", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SwitchPortSettingsView" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "system/networking" + ], + "summary": "Delete switch port settings", + "operationId": "networking_switch_port_configuration_delete", + "parameters": [ + { + "in": "path", + "name": "configuration", + "description": "A name or id to use when selecting a switch port configuration.", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/networking/switch-port-configuration/{configuration}/address": { + "get": { + "tags": [ + "system/networking" + ], + "summary": "List addresses assigned to a provided interface configuration", + "operationId": "networking_switch_port_configuration_address_list", + "parameters": [ + { + "in": "path", + "name": "configuration", + "description": "A name or id to use when selecting a switch port configuration.", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_SwitchPortAddressConfig", + "type": "array", + "items": { + "$ref": "#/components/schemas/SwitchPortAddressConfig" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/networking/switch-port-configuration/{configuration}/address/add": { + "post": { + "tags": [ + "system/networking" + ], + "summary": "Add address to an interface configuration", + "operationId": "networking_switch_port_configuration_address_add", + "parameters": [ + { + "in": "path", + "name": "configuration", + "description": "A name or id to use when selecting a switch port configuration.", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AddressAddRemove" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SwitchPortAddressConfig" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/networking/switch-port-configuration/{configuration}/address/remove": { + "post": { + "tags": [ + "system/networking" + ], + "summary": "Remove address from an interface configuration", + "operationId": "networking_switch_port_configuration_address_remove", + "parameters": [ + { + "in": "path", + "name": "configuration", + "description": "A name or id to use when selecting a switch port configuration.", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AddressAddRemove" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/networking/switch-port-configuration/{configuration}/bgp-peer": { + "get": { + "tags": [ + "system/networking" + ], + "summary": "List bgp peers assigned to a provided interface configuration", + "operationId": "networking_switch_port_configuration_bgp_peer_list", + "parameters": [ + { + "in": "path", + "name": "configuration", + "description": "A name or id to use when selecting a switch port configuration.", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_BgpPeer", + "type": "array", + "items": { + "$ref": "#/components/schemas/BgpPeer" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/networking/switch-port-configuration/{configuration}/bgp-peer/add": { + "post": { + "tags": [ + "system/networking" + ], + "summary": "Add bgp peer to an interface configuration", + "operationId": "networking_switch_port_configuration_bgp_peer_add", + "parameters": [ + { + "in": "path", + "name": "configuration", + "description": "A name or id to use when selecting a switch port configuration.", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BgpPeer" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BgpPeer" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/networking/switch-port-configuration/{configuration}/bgp-peer/allow-export": { + "get": { + "tags": [ + "system/networking" + ], + "summary": "List prefixes allowed to be exported by a given bgp peer", + "operationId": "networking_switch_port_configuration_bgp_peer_allow_export_list", + "parameters": [ + { + "in": "path", + "name": "configuration", + "description": "A name or id to use when selecting a switch port configuration.", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "peer_address", + "description": "An address identifying a configured bgp peer.", + "required": true, + "schema": { + "type": "string", + "format": "ip" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_BgpAllowedPrefix", + "type": "array", + "items": { + "$ref": "#/components/schemas/BgpAllowedPrefix" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/networking/switch-port-configuration/{configuration}/bgp-peer/allow-export/add": { + "post": { + "tags": [ + "system/networking" + ], + "summary": "Add prefix to bgp peer allowed export list", + "operationId": "networking_switch_port_configuration_bgp_peer_allow_export_add", + "parameters": [ + { + "in": "path", + "name": "configuration", + "description": "A name or id to use when selecting a switch port configuration.", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AllowedPrefixAddRemove" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BgpAllowedPrefix" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/networking/switch-port-configuration/{configuration}/bgp-peer/allow-export/remove": { + "post": { + "tags": [ + "system/networking" + ], + "summary": "Remove prefix from bgp peer allowed export list", + "operationId": "networking_switch_port_configuration_bgp_peer_allow_export_remove", + "parameters": [ + { + "in": "path", + "name": "configuration", + "description": "A name or id to use when selecting a switch port configuration.", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AllowedPrefixAddRemove" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/networking/switch-port-configuration/{configuration}/bgp-peer/allow-import": { + "get": { + "tags": [ + "system/networking" + ], + "summary": "List prefixes allowed to be imported by a given bgp peer", + "operationId": "networking_switch_port_configuration_bgp_peer_allow_import_list", + "parameters": [ + { + "in": "path", + "name": "configuration", + "description": "A name or id to use when selecting a switch port configuration.", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "peer_address", + "description": "An address identifying a configured bgp peer.", + "required": true, + "schema": { + "type": "string", + "format": "ip" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_BgpAllowedPrefix", + "type": "array", + "items": { + "$ref": "#/components/schemas/BgpAllowedPrefix" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/networking/switch-port-configuration/{configuration}/bgp-peer/allow-import/add": { + "post": { + "tags": [ + "system/networking" + ], + "summary": "Add prefix to bgp peer allowed import list", + "operationId": "networking_switch_port_configuration_bgp_peer_allow_import_add", + "parameters": [ + { + "in": "path", + "name": "configuration", + "description": "A name or id to use when selecting a switch port configuration.", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AllowedPrefixAddRemove" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BgpAllowedPrefix" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/networking/switch-port-configuration/{configuration}/bgp-peer/allow-import/remove": { + "post": { + "tags": [ + "system/networking" + ], + "summary": "Remove prefix from bgp peer allowed import list", + "operationId": "networking_switch_port_configuration_bgp_peer_allow_import_remove", + "parameters": [ + { + "in": "path", + "name": "configuration", + "description": "A name or id to use when selecting a switch port configuration.", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AllowedPrefixAddRemove" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/networking/switch-port-configuration/{configuration}/bgp-peer/community": { + "get": { + "tags": [ + "system/networking" + ], + "summary": "List communities assigned to a bgp peer", + "operationId": "networking_switch_port_configuration_bgp_peer_community_list", + "parameters": [ + { + "in": "path", + "name": "configuration", + "description": "A name or id to use when selecting a switch port configuration.", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "peer_address", + "description": "An address identifying a configured bgp peer.", + "required": true, + "schema": { + "type": "string", + "format": "ip" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_BgpCommunity", + "type": "array", + "items": { + "$ref": "#/components/schemas/BgpCommunity" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/networking/switch-port-configuration/{configuration}/bgp-peer/community/add": { + "post": { + "tags": [ + "system/networking" + ], + "summary": "Add community to bgp peer", + "operationId": "networking_switch_port_configuration_bgp_peer_community_add", + "parameters": [ + { + "in": "path", + "name": "configuration", + "description": "A name or id to use when selecting a switch port configuration.", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BgpCommunityAddRemove" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BgpCommunity" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/networking/switch-port-configuration/{configuration}/bgp-peer/community/remove": { + "post": { + "tags": [ + "system/networking" + ], + "summary": "Remove community from bgp peer", + "operationId": "networking_switch_port_configuration_bgp_peer_community_remove", + "parameters": [ + { + "in": "path", + "name": "configuration", + "description": "A name or id to use when selecting a switch port configuration.", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BgpCommunityAddRemove" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/networking/switch-port-configuration/{configuration}/bgp-peer/remove": { + "post": { + "tags": [ + "system/networking" + ], + "summary": "Remove bgp peer from an interface configuration", + "operationId": "networking_switch_port_configuration_bgp_peer_remove", + "parameters": [ + { + "in": "path", + "name": "configuration", + "description": "A name or id to use when selecting a switch port configuration.", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BgpPeerRemove" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/networking/switch-port-configuration/{configuration}/geometry": { + "get": { + "tags": [ + "system/networking" + ], + "summary": "Get switch port geometry for a provided switch port configuration", + "operationId": "networking_switch_port_configuration_geometry_view", + "parameters": [ + { + "in": "path", + "name": "configuration", + "description": "A name or id to use when selecting a switch port configuration.", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SwitchPortConfig" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "post": { + "tags": [ + "system/networking" + ], + "summary": "Set switch port geometry for a provided switch port configuration", + "operationId": "networking_switch_port_configuration_geometry_set", + "parameters": [ + { + "in": "path", + "name": "configuration", + "description": "A name or id to use when selecting a switch port configuration.", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SwitchPortConfigCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SwitchPortConfig" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/networking/switch-port-configuration/{configuration}/link": { + "get": { + "tags": [ + "system/networking" + ], + "summary": "List links for a provided switch port configuration", + "operationId": "networking_switch_port_configuration_link_list", + "parameters": [ + { + "in": "path", + "name": "configuration", + "description": "A name or id to use when selecting a switch port configuration.", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_SwitchPortLinkConfig", + "type": "array", + "items": { + "$ref": "#/components/schemas/SwitchPortLinkConfig" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "post": { + "tags": [ + "system/networking" + ], + "summary": "Create a link for a provided switch port configuration", + "operationId": "networking_switch_port_configuration_link_create", + "parameters": [ + { + "in": "path", + "name": "configuration", + "description": "A name or id to use when selecting a switch port configuration.", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NamedLinkConfigCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SwitchPortLinkConfig" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/networking/switch-port-configuration/{configuration}/link/{link}": { + "get": { + "tags": [ + "system/networking" + ], + "summary": "View a link for a provided switch port configuration", + "operationId": "networking_switch_port_configuration_link_view", + "parameters": [ + { + "in": "path", + "name": "configuration", + "description": "A name or id to use when selecting a switch port configuration.", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "path", + "name": "link", + "description": "Link name", + "required": true, + "schema": { + "$ref": "#/components/schemas/Name" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SwitchPortLinkConfig" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "system/networking" + ], + "summary": "Delete a link for a provided switch port configuration", + "operationId": "networking_switch_port_configuration_link_delete", + "parameters": [ + { + "in": "path", + "name": "configuration", + "description": "A name or id to use when selecting a switch port configuration.", "required": true, "schema": { - "type": "integer", - "format": "uint8", - "minimum": 0 + "$ref": "#/components/schemas/NameOrId" } }, { "in": "path", - "name": "switch_location", - "description": "The switch location to use when selecting the loopback address.", + "name": "link", + "description": "Link name", "required": true, "schema": { "$ref": "#/components/schemas/Name" @@ -7011,48 +8213,22 @@ } } }, - "/v1/system/networking/switch-port-settings": { + "/v1/system/networking/switch-port-configuration/{configuration}/route": { "get": { "tags": [ "system/networking" ], - "summary": "List switch port settings", - "operationId": "networking_switch_port_settings_list", + "summary": "List routes assigned to a provided interface configuration", + "operationId": "networking_switch_port_configuration_route_list", "parameters": [ { - "in": "query", - "name": "limit", - "description": "Maximum number of items returned by a single call", - "schema": { - "nullable": true, - "type": "integer", - "format": "uint32", - "minimum": 1 - } - }, - { - "in": "query", - "name": "page_token", - "description": "Token returned by previous call to retrieve the subsequent page", - "schema": { - "nullable": true, - "type": "string" - } - }, - { - "in": "query", - "name": "port_settings", - "description": "An optional name or id to use when selecting port settings.", + "in": "path", + "name": "configuration", + "description": "A name or id to use when selecting a switch port configuration.", + "required": true, "schema": { "$ref": "#/components/schemas/NameOrId" } - }, - { - "in": "query", - "name": "sort_by", - "schema": { - "$ref": "#/components/schemas/NameOrIdSortMode" - } } ], "responses": { @@ -7061,7 +8237,11 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SwitchPortSettingsResultsPage" + "title": "Array_of_SwitchPortRouteConfig", + "type": "array", + "items": { + "$ref": "#/components/schemas/SwitchPortRouteConfig" + } } } } @@ -7072,22 +8252,32 @@ "5XX": { "$ref": "#/components/responses/Error" } - }, - "x-dropshot-pagination": { - "required": [] } - }, + } + }, + "/v1/system/networking/switch-port-configuration/{configuration}/route/add": { "post": { "tags": [ "system/networking" ], - "summary": "Create switch port settings", - "operationId": "networking_switch_port_settings_create", + "summary": "Add route to an interface configuration", + "operationId": "networking_switch_port_configuration_route_add", + "parameters": [ + { + "in": "path", + "name": "configuration", + "description": "A name or id to use when selecting a switch port configuration.", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SwitchPortSettingsCreate" + "$ref": "#/components/schemas/RouteAddRemove" } } }, @@ -7099,7 +8289,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SwitchPortSettingsView" + "$ref": "#/components/schemas/SwitchPortRouteConfig" } } } @@ -7111,65 +8301,40 @@ "$ref": "#/components/responses/Error" } } - }, - "delete": { - "tags": [ - "system/networking" - ], - "summary": "Delete switch port settings", - "operationId": "networking_switch_port_settings_delete", - "parameters": [ - { - "in": "query", - "name": "port_settings", - "description": "An optional name or id to use when selecting port settings.", - "schema": { - "$ref": "#/components/schemas/NameOrId" - } - } - ], - "responses": { - "204": { - "description": "successful deletion" - }, - "4XX": { - "$ref": "#/components/responses/Error" - }, - "5XX": { - "$ref": "#/components/responses/Error" - } - } } }, - "/v1/system/networking/switch-port-settings/{port}": { - "get": { + "/v1/system/networking/switch-port-configuration/{configuration}/route/remove": { + "post": { "tags": [ "system/networking" ], - "summary": "Get information about switch port", - "operationId": "networking_switch_port_settings_view", + "summary": "Remove route from an interface configuration", + "operationId": "networking_switch_port_configuration_route_remove", "parameters": [ { "in": "path", - "name": "port", - "description": "A name or id to use when selecting switch port settings info objects.", + "name": "configuration", + "description": "A name or id to use when selecting a switch port configuration.", "required": true, "schema": { "$ref": "#/components/schemas/NameOrId" } } ], - "responses": { - "200": { - "description": "successful operation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SwitchPortSettingsView" - } + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RouteAddRemove" } } }, + "required": true + }, + "responses": { + "204": { + "description": "successful deletion" + }, "4XX": { "$ref": "#/components/responses/Error" }, @@ -9650,7 +10815,7 @@ "type": "object", "properties": { "address": { - "description": "The address and prefix length of this address.", + "description": "The address and subnet mask", "allOf": [ { "$ref": "#/components/schemas/IpNet" @@ -9678,6 +10843,48 @@ "address_lot" ] }, + "AddressAddRemove": { + "description": "An address to be added to or removed from an interface", + "type": "object", + "properties": { + "address": { + "description": "The address and subnet mask", + "allOf": [ + { + "$ref": "#/components/schemas/IpNet" + } + ] + }, + "address_lot": { + "description": "The address lot this address is drawn from.", + "allOf": [ + { + "$ref": "#/components/schemas/NameOrId" + } + ] + }, + "interface": { + "description": "The name of the interface", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "vlan_id": { + "nullable": true, + "description": "Optional VLAN ID for this address", + "type": "integer", + "format": "uint16", + "minimum": 0 + } + }, + "required": [ + "address", + "address_lot", + "interface" + ] + }, "AddressConfig": { "description": "A set of addresses associated with a port configuration.", "type": "object", @@ -9769,8 +10976,8 @@ "last_address" ] }, - "AddressLotBlockCreate": { - "description": "Parameters for creating an address lot block. Fist and last addresses are inclusive.", + "AddressLotBlockAddRemove": { + "description": "Parameters for adding or removing an address lot block. First and last addresses are inclusive.", "type": "object", "properties": { "first_address": { @@ -9814,13 +11021,6 @@ "description": "Parameters for creating an address lot.", "type": "object", "properties": { - "blocks": { - "description": "The blocks to add along with the new address lot.", - "type": "array", - "items": { - "$ref": "#/components/schemas/AddressLotBlockCreate" - } - }, "description": { "type": "string" }, @@ -9837,7 +11037,6 @@ } }, "required": [ - "blocks", "description", "kind", "name" @@ -9847,13 +11046,6 @@ "description": "An address lot and associated blocks resulting from creating an address lot.", "type": "object", "properties": { - "blocks": { - "description": "The address lot blocks that were created.", - "type": "array", - "items": { - "$ref": "#/components/schemas/AddressLotBlock" - } - }, "lot": { "description": "The address lot that was created.", "allOf": [ @@ -9864,7 +11056,6 @@ } }, "required": [ - "blocks", "lot" ] }, @@ -9970,6 +11161,38 @@ "allowed_ips" ] }, + "AllowedPrefixAddRemove": { + "description": "A prefix allowed to be imported or exported by a bgp peer", + "type": "object", + "properties": { + "interface": { + "description": "The interface the peer is configured on", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "peer_address": { + "description": "An address identifying the target bgp peer", + "type": "string", + "format": "ip" + }, + "prefix": { + "description": "The allowed prefix to add or remove", + "allOf": [ + { + "$ref": "#/components/schemas/IpNet" + } + ] + } + }, + "required": [ + "interface", + "peer_address", + "prefix" + ] + }, "AllowedSourceIps": { "description": "Description of source IPs allowed to reach rack services.", "oneOf": [ @@ -10223,6 +11446,43 @@ "switch" ] }, + "BgpAllowedPrefix": { + "description": "A BGP allowed prefix entry", + "type": "object", + "properties": { + "addr": { + "description": "Peer Address", + "allOf": [ + { + "$ref": "#/components/schemas/IpNet" + } + ] + }, + "interface_name": { + "description": "Interface peer is reachable on", + "type": "string" + }, + "port_settings_id": { + "description": "Parent switch port configuration", + "type": "string", + "format": "uuid" + }, + "prefix": { + "description": "Allowed Prefix", + "allOf": [ + { + "$ref": "#/components/schemas/IpNet" + } + ] + } + }, + "required": [ + "addr", + "interface_name", + "port_settings_id", + "prefix" + ] + }, "BgpAnnounceSet": { "description": "Represents a BGP announce set by id. The id can be used with other API calls to view and manage the announce set.", "type": "object", @@ -10311,35 +11571,100 @@ } }, "required": [ - "address_lot_block_id", - "announce_set_id", - "network" + "address_lot_block_id", + "announce_set_id", + "network" + ] + }, + "BgpAnnouncementCreate": { + "description": "A BGP announcement tied to a particular address lot block.", + "type": "object", + "properties": { + "address_lot_block": { + "description": "Address lot this announcement is drawn from.", + "allOf": [ + { + "$ref": "#/components/schemas/NameOrId" + } + ] + }, + "network": { + "description": "The network being announced.", + "allOf": [ + { + "$ref": "#/components/schemas/IpNet" + } + ] + } + }, + "required": [ + "address_lot_block", + "network" + ] + }, + "BgpCommunity": { + "description": "A BGP community", + "type": "object", + "properties": { + "addr": { + "description": "Peer Address", + "allOf": [ + { + "$ref": "#/components/schemas/IpNet" + } + ] + }, + "community": { + "description": "Community", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "interface_name": { + "description": "Interface peer is reachable on", + "type": "string" + }, + "port_settings_id": { + "description": "Parent switch port configuration", + "type": "string", + "format": "uuid" + } + }, + "required": [ + "addr", + "community", + "interface_name", + "port_settings_id" ] }, - "BgpAnnouncementCreate": { - "description": "A BGP announcement tied to a particular address lot block.", + "BgpCommunityAddRemove": { + "description": "A community to be added to or removed from a bgp peer", "type": "object", "properties": { - "address_lot_block": { - "description": "Address lot this announcement is drawn from.", - "allOf": [ - { - "$ref": "#/components/schemas/NameOrId" - } - ] + "community": { + "description": "The community to add or remove", + "type": "integer", + "format": "uint32", + "minimum": 0 }, - "network": { - "description": "The network being announced.", + "interface": { + "description": "The interface the peer is configured on", "allOf": [ { - "$ref": "#/components/schemas/IpNet" + "$ref": "#/components/schemas/Name" } ] + }, + "peer_address": { + "description": "An address identifying the target bgp peer", + "type": "string", + "format": "ip" } }, "required": [ - "address_lot_block", - "network" + "community", + "interface", + "peer_address" ] }, "BgpConfig": { @@ -10511,6 +11836,127 @@ }, "BgpMessageHistory": {}, "BgpPeer": { + "description": "The information required to configure a BGP peer.", + "type": "object", + "properties": { + "addr": { + "description": "The address of the host to peer with.", + "allOf": [ + { + "$ref": "#/components/schemas/IpNet" + } + ] + }, + "allow_export_list_active": { + "description": "Enable export policies", + "type": "boolean" + }, + "allow_import_list_active": { + "description": "Enable import policies", + "type": "boolean" + }, + "bgp_config": { + "description": "The global BGP configuration used for establishing a session with this peer.", + "allOf": [ + { + "$ref": "#/components/schemas/NameOrId" + } + ] + }, + "connect_retry": { + "description": "How long to to wait between TCP connection retries (seconds).", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "delay_open": { + "description": "How long to delay sending an open request after establishing a TCP session (seconds).", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "enforce_first_as": { + "description": "Enforce that the first AS in paths received from this peer is the peer's AS.", + "type": "boolean" + }, + "hold_time": { + "description": "How long to hold peer connections between keepalives (seconds).", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "idle_hold_time": { + "description": "How long to hold this peer in idle before attempting a new session (seconds).", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "interface_name": { + "description": "The name of the interface to peer on. This is relative to the port configuration this BGP peer configuration is a part of. For example this value could be phy0 to refer to a primary physical interface. Or it could be vlan47 to refer to a VLAN interface.", + "type": "string" + }, + "keepalive": { + "description": "How often to send keepalive requests (seconds).", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "local_pref": { + "nullable": true, + "description": "Apply a local preference to routes received from this peer.", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "md5_auth_key": { + "nullable": true, + "description": "Use the given key for TCP-MD5 authentication with this peer.", + "type": "string" + }, + "min_ttl": { + "nullable": true, + "description": "Require messages from this peer to have a minimum IP time to live field.", + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "multi_exit_discriminator": { + "nullable": true, + "description": "Apply a multi-exit discriminator (MED) in updates sent to this peer.", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "remote_asn": { + "nullable": true, + "description": "Require that this peer have a specified ASN.", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "vlan_id": { + "nullable": true, + "description": "Associate a VLAN ID with this peer.", + "type": "integer", + "format": "uint16", + "minimum": 0 + } + }, + "required": [ + "addr", + "allow_export_list_active", + "allow_import_list_active", + "bgp_config", + "connect_retry", + "delay_open", + "enforce_first_as", + "hold_time", + "idle_hold_time", + "interface_name", + "keepalive" + ] + }, + "BgpPeerCombined": { "description": "A BGP peer configuration for an interface. Includes the set of announcements that will be advertised to the peer identified by `addr`. The `bgp_config` parameter is a reference to global BGP parameters. The `interface_name` indicates what interface the peer should be contacted on.", "type": "object", "properties": { @@ -10520,7 +11966,7 @@ "format": "ip" }, "allowed_export": { - "description": "Define export policy for a peer.", + "description": "Define export policy for this peer.", "allOf": [ { "$ref": "#/components/schemas/ImportExportPolicy" @@ -10528,7 +11974,7 @@ ] }, "allowed_import": { - "description": "Define import policy for a peer.", + "description": "Define import policy for this peer.", "allOf": [ { "$ref": "#/components/schemas/ImportExportPolicy" @@ -10544,7 +11990,7 @@ ] }, "communities": { - "description": "Include the provided communities in updates sent to the peer.", + "description": "Include the provided communities in updates sent to this peer.", "type": "array", "items": { "type": "integer", @@ -10581,7 +12027,7 @@ "minimum": 0 }, "interface_name": { - "description": "The name of interface to peer on. This is relative to the port configuration this BGP peer configuration is a part of. For example this value could be phy0 to refer to a primary physical interface. Or it could be vlan47 to refer to a VLAN interface.", + "description": "The name of the interface to peer on. This is relative to the port configuration this BGP peer configuration is a part of. For example this value could be phy0 to refer to a primary physical interface. Or it could be vlan47 to refer to a VLAN interface.", "type": "string" }, "keepalive": { @@ -10599,33 +12045,33 @@ }, "md5_auth_key": { "nullable": true, - "description": "Use the given key for TCP-MD5 authentication with the peer.", + "description": "Use the given key for TCP-MD5 authentication with this peer.", "type": "string" }, "min_ttl": { "nullable": true, - "description": "Require messages from a peer have a minimum IP time to live field.", + "description": "Require messages from this peer have a minimum IP time to live field.", "type": "integer", "format": "uint8", "minimum": 0 }, "multi_exit_discriminator": { "nullable": true, - "description": "Apply the provided multi-exit discriminator (MED) updates sent to the peer.", + "description": "Apply a multi-exit discriminator (MED) in updates sent to this peer.", "type": "integer", "format": "uint32", "minimum": 0 }, "remote_asn": { "nullable": true, - "description": "Require that a peer has a specified ASN.", + "description": "Require that this peer has a specified ASN.", "type": "integer", "format": "uint32", "minimum": 0 }, "vlan_id": { "nullable": true, - "description": "Associate a VLAN ID with a peer.", + "description": "Associate a VLAN ID with this peer.", "type": "integer", "format": "uint16", "minimum": 0 @@ -10652,7 +12098,7 @@ "peers": { "type": "array", "items": { - "$ref": "#/components/schemas/BgpPeer" + "$ref": "#/components/schemas/BgpPeerCombined" } } }, @@ -10660,6 +12106,34 @@ "peers" ] }, + "BgpPeerRemove": { + "description": "A BGP peer configuration to remove from an interface", + "type": "object", + "properties": { + "addr": { + "description": "The address of the host to peer with.", + "type": "string", + "format": "ip" + }, + "bgp_config": { + "description": "The global BGP configuration used for establishing a session with this peer.", + "allOf": [ + { + "$ref": "#/components/schemas/NameOrId" + } + ] + }, + "interface_name": { + "description": "The name of the interface to peer on. This is relative to the port configuration this BGP peer configuration is a part of. For example this value could be phy0 to refer to a primary physical interface. Or it could be vlan47 to refer to a VLAN interface.", + "type": "string" + } + }, + "required": [ + "addr", + "bgp_config", + "interface_name" + ] + }, "BgpPeerState": { "description": "The current state of a BGP peer.", "oneOf": [ @@ -16546,6 +18020,62 @@ } ] }, + "NamedLinkConfigCreate": { + "description": "Named switch link configuration.", + "type": "object", + "properties": { + "autoneg": { + "description": "Whether or not to set autonegotiation", + "type": "boolean" + }, + "fec": { + "description": "The forward error correction mode of the link.", + "allOf": [ + { + "$ref": "#/components/schemas/LinkFec" + } + ] + }, + "lldp_config": { + "nullable": true, + "description": "The optional link-layer discovery protocol (LLDP) configuration for the link.", + "allOf": [ + { + "$ref": "#/components/schemas/NameOrId" + } + ] + }, + "mtu": { + "description": "Maximum transmission unit for the link.", + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "name": { + "description": "Name of link", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "speed": { + "description": "The speed of the link.", + "allOf": [ + { + "$ref": "#/components/schemas/LinkSpeed" + } + ] + } + }, + "required": [ + "autoneg", + "fec", + "mtu", + "name", + "speed" + ] + }, "NetworkInterface": { "description": "Information required to construct a virtual network interface", "type": "object", @@ -17351,7 +18881,7 @@ }, "local_pref": { "nullable": true, - "description": "Local preference for route. Higher preference indictes precedence within and across protocols.", + "description": "Local preference for route. Higher preference indicates precedence within and across protocols.", "type": "integer", "format": "uint32", "minimum": 0 @@ -17369,6 +18899,52 @@ "gw" ] }, + "RouteAddRemove": { + "description": "A network route to to add to or remove from an interface.", + "type": "object", + "properties": { + "dst": { + "description": "The route destination.", + "allOf": [ + { + "$ref": "#/components/schemas/IpNet" + } + ] + }, + "gw": { + "description": "The route gateway.", + "type": "string", + "format": "ip" + }, + "interface": { + "description": "The interface to configure the route on", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "local_pref": { + "nullable": true, + "description": "Local preference for route. Higher preference indicates precedence within and across protocols.", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "vid": { + "nullable": true, + "description": "VLAN id the gateway is reachable over.", + "type": "integer", + "format": "uint16", + "minimum": 0 + } + }, + "required": [ + "dst", + "gw", + "interface" + ] + }, "RouteConfig": { "description": "Route configuration data associated with a switch port configuration.", "type": "object", @@ -19604,7 +21180,7 @@ "description": "BGP peer settings.", "type": "array", "items": { - "$ref": "#/components/schemas/BgpPeer" + "$ref": "#/components/schemas/BgpPeerCombined" } }, "groups": { diff --git a/schema/crdb/dbinit.sql b/schema/crdb/dbinit.sql index baef38e44f..31d5cefd40 100644 --- a/schema/crdb/dbinit.sql +++ b/schema/crdb/dbinit.sql @@ -4201,6 +4201,10 @@ CREATE INDEX IF NOT EXISTS lookup_region_snapshot_by_snapshot_id on omicron.publ snapshot_id ); + +/* Lookup switch port settings by name */ +CREATE INDEX IF NOT EXISTS switch_port_settings_name ON omicron.public.switch_port_settings (name); + /* * Keep this at the end of file so that the database does not contain a version * until it is fully populated. @@ -4212,7 +4216,7 @@ INSERT INTO omicron.public.db_metadata ( version, target_version ) VALUES - (TRUE, NOW(), NOW(), '90.0.0', NULL) + (TRUE, NOW(), NOW(), '91.0.0', NULL) ON CONFLICT DO NOTHING; COMMIT; diff --git a/schema/crdb/refactor-network-apis/up01.sql b/schema/crdb/refactor-network-apis/up01.sql new file mode 100644 index 0000000000..5e645ed8b0 --- /dev/null +++ b/schema/crdb/refactor-network-apis/up01.sql @@ -0,0 +1 @@ +CREATE INDEX IF NOT EXISTS switch_port_settings_name ON omicron.public.switch_port_settings (name);