From 23d9e4c90f0af2b331901db03e6170090b34a7e8 Mon Sep 17 00:00:00 2001 From: Nils Nieuwejaar Date: Fri, 8 Nov 2024 16:05:29 +0000 Subject: [PATCH] 6750 Add support for post-install LLDP configuration 6751 Add support for extracting LLDP neighbor information --- Cargo.lock | 32 +++ Cargo.toml | 1 + common/src/api/external/mod.rs | 35 +++ dev-tools/ls-apis/api-manifest.toml | 8 +- dev-tools/ls-apis/src/workspaces.rs | 1 + dev-tools/ls-apis/tests/api_dependencies.out | 3 + nexus/Cargo.toml | 1 + nexus/db-queries/src/db/datastore/lldp.rs | 178 +++++++++++++ nexus/db-queries/src/db/datastore/mod.rs | 1 + nexus/external-api/output/nexus_tags.txt | 3 + nexus/external-api/src/lib.rs | 37 +++ nexus/src/app/lldp.rs | 148 +++++++++++ nexus/src/app/mod.rs | 34 +++ nexus/src/external_api/http_entrypoints.rs | 93 +++++++ .../output/uncovered-authz-endpoints.txt | 3 + openapi/nexus.json | 234 ++++++++++++++++++ package-manifest.toml | 4 +- 17 files changed, 813 insertions(+), 3 deletions(-) create mode 100644 nexus/db-queries/src/db/datastore/lldp.rs create mode 100644 nexus/src/app/lldp.rs diff --git a/Cargo.lock b/Cargo.lock index 802e91b633..b74f8d4dbc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1534,6 +1534,22 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "common" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/lldp#471a2dc103081e003e700c9825de608073a850cb" +dependencies = [ + "anyhow", + "schemars", + "serde", + "serde_json", + "slog", + "slog-async", + "slog-bunyan", + "slog-term", + "thiserror", +] + [[package]] name = "compact_str" version = "0.8.0" @@ -5272,6 +5288,21 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "lldpd-client" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/lldp#471a2dc103081e003e700c9825de608073a850cb" +dependencies = [ + "chrono", + "common", + "progenitor", + "reqwest", + "schemars", + "serde", + "serde_json", + "slog", +] + [[package]] name = "lock_api" version = "0.4.12" @@ -6930,6 +6961,7 @@ dependencies = [ "internal-dns-types", "ipnetwork", "itertools 0.13.0", + "lldpd-client", "macaddr", "mg-admin-client", "nexus-auth", diff --git a/Cargo.toml b/Cargo.toml index 3b9683a10f..926d488204 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -439,6 +439,7 @@ libfalcon = { git = "https://github.com/oxidecomputer/falcon", branch = "main" } libnvme = { git = "https://github.com/oxidecomputer/libnvme", rev = "dd5bb221d327a1bc9287961718c3c10d6bd37da0" } linear-map = "1.2.0" live-tests-macros = { path = "live-tests/macros" } +lldpd-client = { git = "https://github.com/oxidecomputer/lldp" } macaddr = { version = "1.0.1", features = ["serde_std"] } maplit = "1.0.2" mockall = "0.13" diff --git a/common/src/api/external/mod.rs b/common/src/api/external/mod.rs index 38a9de0564..9d543ffa8b 100644 --- a/common/src/api/external/mod.rs +++ b/common/src/api/external/mod.rs @@ -1010,6 +1010,7 @@ pub enum ResourceType { FloatingIp, Probe, ProbeNetworkInterface, + LldpLinkConfig, } // IDENTITY METADATA @@ -2555,6 +2556,40 @@ pub struct LldpLinkConfig { pub management_ip: Option, } +/// Information about LLDP advertisements from other network entities directly +/// connected to a switch port. This structure contains both metadata about +/// when and where the neighbor was seen, as well as the specific information +/// the neighbor was advertising. +#[derive(Clone, Debug, Deserialize, JsonSchema, Serialize, PartialEq)] +pub struct LldpNeighbor { + /// The port on which the neighbor was seen + pub local_port: String, + + /// Initial sighting of this LldpNeighbor + pub first_seen: DateTime, + + /// Most recent sighting of this LldpNeighbor + pub last_seen: DateTime, + + /// The LLDP link name advertised by the neighbor + pub link_name: String, + + /// The LLDP link description advertised by the neighbor + pub link_description: Option, + + /// The LLDP chassis identifier advertised by the neighbor + pub chassis_id: String, + + /// The LLDP system name advertised by the neighbor + pub system_name: Option, + + /// The LLDP system description advertised by the neighbor + pub system_description: Option, + + /// The LLDP management IP(s) advertised by the neighbor + pub management_ip: Vec, +} + /// Per-port tx-eq overrides. This can be used to fine-tune the transceiver /// equalization settings to improve signal integrity. #[derive(Clone, Debug, Deserialize, JsonSchema, Serialize, PartialEq)] diff --git a/dev-tools/ls-apis/api-manifest.toml b/dev-tools/ls-apis/api-manifest.toml index 766708f478..eb5a493b85 100644 --- a/dev-tools/ls-apis/api-manifest.toml +++ b/dev-tools/ls-apis/api-manifest.toml @@ -21,7 +21,6 @@ # Progenitor clients or APIs, so they're left out to avoid needing to create and # process clones of these repos: # -# - lldp # - pumpkind # - thundermuffin # @@ -74,6 +73,7 @@ packages = [ # switch zone "ddmd", "dpd", + "lldpd", "mgd", "omicron-gateway", "tfportd", @@ -231,6 +231,12 @@ and exists as a client library within omicron. This is because the Dendrite \ repo is not currently open source. """ +[[apis]] +client_package_name = "lldpd-client" +label = "LLDP daemon" +server_package_name = "lldpd" +notes = "The LLDP daemon runs in the switch zone and is deployed next to dpd." + [[apis]] client_package_name = "gateway-client" label = "Management Gateway Service" diff --git a/dev-tools/ls-apis/src/workspaces.rs b/dev-tools/ls-apis/src/workspaces.rs index 54df7a44e3..143f1e59e0 100644 --- a/dev-tools/ls-apis/src/workspaces.rs +++ b/dev-tools/ls-apis/src/workspaces.rs @@ -77,6 +77,7 @@ impl Workspaces { )])), ), ("maghemite", "mg-admin-client", None), + ("lldp", "lldpd-client", None), ] .into_iter() .map(|(repo, omicron_pkg, extra_features)| { diff --git a/dev-tools/ls-apis/tests/api_dependencies.out b/dev-tools/ls-apis/tests/api_dependencies.out index 7da9613014..d066a383bb 100644 --- a/dev-tools/ls-apis/tests/api_dependencies.out +++ b/dev-tools/ls-apis/tests/api_dependencies.out @@ -51,6 +51,9 @@ Management Gateway Service (client: gateway-client) Wicketd Installinator (client: installinator-client) consumed by: installinator (omicron/installinator) via 1 path +LLDP daemon (client: lldpd-client) + consumed by: omicron-nexus (omicron/nexus) via 1 path + Maghemite MG Admin (client: mg-admin-client) consumed by: omicron-nexus (omicron/nexus) via 1 path consumed by: omicron-sled-agent (omicron/sled-agent) via 1 path diff --git a/nexus/Cargo.toml b/nexus/Cargo.toml index 6ba66e058f..b2231cbeb9 100644 --- a/nexus/Cargo.toml +++ b/nexus/Cargo.toml @@ -47,6 +47,7 @@ internal-dns-resolver.workspace = true internal-dns-types.workspace = true ipnetwork.workspace = true itertools.workspace = true +lldpd-client.workspace = true macaddr.workspace = true # Not under "dev-dependencies"; these also need to be implemented for # integration tests. diff --git a/nexus/db-queries/src/db/datastore/lldp.rs b/nexus/db-queries/src/db/datastore/lldp.rs new file mode 100644 index 0000000000..0abe1c2c02 --- /dev/null +++ b/nexus/db-queries/src/db/datastore/lldp.rs @@ -0,0 +1,178 @@ +use super::DataStore; +use crate::context::OpContext; +use crate::db; +use crate::db::error::public_error_from_diesel; +use crate::db::error::ErrorHandler; +use crate::db::model::LldpLinkConfig; +use async_bb8_diesel::AsyncRunQueryDsl; +use chrono::Utc; +use diesel::ExpressionMethods; +use diesel::QueryDsl; +use diesel::SelectableHelper; +use ipnetwork::IpNetwork; +use omicron_common::api::external; +use omicron_common::api::external::Error; +use omicron_common::api::external::LookupResult; +use omicron_common::api::external::Name; +use omicron_common::api::external::ResourceType; +use omicron_common::api::external::UpdateResult; +use uuid::Uuid; + +// The LLDP configuration has been defined as a leaf of the switch-port-settings +// tree, and is identified in the database with a UUID stored in that tree. +// Using the uuid as the target argument for the config operations would be +// reasonable, and similar in spirit to the link configuration operations. +// +// On the other hand, the neighbors are discovered on a configured link, but the +// data is otherwise completely independent of the configuration. Furthermore, +// the questions answered by the neighbor information have to do with the +// physical connections between the Oxide rack and the upstream, datacenter +// switch. Accordingly, it seems more appropriate to use the physical +// rack/switch/port triple to identify the port of interest for the neighbors +// query. +// +// For consistency across the lldp operations, all use rack/switch/port rather +// than the uuid. +// XXX: Is this the right call? The other options being: uuid for all +// operations, or uuid for config and r/s/p for neighbors. +impl DataStore { + /// Look up the settings id for this port in the switch_port table by its + /// rack/switch/port triple, and then use that id to look up the lldp + /// config id in the switch_port_settings_link_config table. + async fn lldp_config_id_get( + &self, + opctx: &OpContext, + rack_id: Uuid, + switch_location: Name, + port_name: Name, + ) -> LookupResult { + use db::schema::switch_port; + use db::schema::switch_port::dsl as switch_port_dsl; + use db::schema::switch_port_settings_link_config; + use db::schema::switch_port_settings_link_config::dsl as config_dsl; + + let conn = self.pool_connection_authorized(opctx).await?; + + let port_settings_id: Uuid = switch_port_dsl::switch_port + .filter(switch_port::rack_id.eq(rack_id)) + .filter( + switch_port::switch_location.eq(switch_location.to_string()), + ) + .filter(switch_port::port_name.eq(port_name.to_string())) + .select(switch_port::port_settings_id) + .limit(1) + .first_async::>(&*conn) + .await + .map_err(|_| { + Error::not_found_by_name(ResourceType::SwitchPort, &port_name) + })? + .ok_or(Error::invalid_value( + "settings", + "switch port not yet configured".to_string(), + ))?; + + let lldp_id: Uuid = config_dsl::switch_port_settings_link_config + .filter( + switch_port_settings_link_config::port_settings_id + .eq(port_settings_id), + ) + .select(switch_port_settings_link_config::lldp_link_config_id) + .limit(1) + .first_async::>(&*conn) + .await + .map_err(|_| { + Error::not_found_by_id( + ResourceType::SwitchPortSettings, + &port_settings_id, + ) + })? + .ok_or(Error::invalid_value( + "settings", + "lldp not configured for this port".to_string(), + ))?; + Ok(lldp_id) + } + + /// Fetch the current LLDP configuration settings for the link identified + /// using the rack/switch/port triple. + pub async fn lldp_config_get( + &self, + opctx: &OpContext, + rack_id: Uuid, + switch_location: Name, + port_name: Name, + ) -> LookupResult { + use db::schema::lldp_link_config; + use db::schema::lldp_link_config::dsl; + + let id = self + .lldp_config_id_get(opctx, rack_id, switch_location, port_name) + .await?; + + let conn = self.pool_connection_authorized(opctx).await?; + dsl::lldp_link_config + .filter(lldp_link_config::id.eq(id)) + .select(LldpLinkConfig::as_select()) + .limit(1) + .first_async::(&*conn) + .await + .map(|config| config.into()) + .map_err(|e| { + let msg = "failed to lookup lldp config by id"; + error!(opctx.log, "{msg}"; "error" => ?e); + + match e { + diesel::result::Error::NotFound => Error::not_found_by_id( + ResourceType::LldpLinkConfig, + &id, + ), + _ => Error::internal_error(msg), + } + }) + } + + /// Update the current LLDP configuration settings for the link identified + /// using the rack/switch/port triple. n.b.: each link is given an empty + /// configuration structure at link creation time, so there are no + /// lldp config create/delete operations. + pub async fn lldp_config_update( + &self, + opctx: &OpContext, + rack_id: Uuid, + switch_location: Name, + port_name: Name, + config: external::LldpLinkConfig, + ) -> UpdateResult<()> { + use db::schema::lldp_link_config::dsl; + + let id = self + .lldp_config_id_get(opctx, rack_id, switch_location, port_name) + .await?; + if id != config.id { + return Err(external::Error::invalid_request(&format!( + "id ({}) doesn't match provided config ({})", + id, config.id + ))); + } + + diesel::update(dsl::lldp_link_config) + .filter(dsl::time_deleted.is_null()) + .filter(dsl::id.eq(id)) + .set(( + dsl::time_modified.eq(Utc::now()), + dsl::enabled.eq( config.enabled), + dsl::link_name.eq( config.link_name.clone()), + dsl::link_description.eq( config.link_description.clone()), + dsl::chassis_id.eq( config.chassis_id.clone()), + dsl::system_name.eq( config.system_name.clone()), + dsl::system_description.eq( config.system_description.clone()), + dsl::management_ip.eq( config.management_ip.map(|a| IpNetwork::from(a))))) + .execute_async(&*self.pool_connection_authorized(opctx).await?) + .await + .map_err(|err| { + error!(opctx.log, "lldp link config update failed"; "error" => ?err); + public_error_from_diesel(err, ErrorHandler::Server) + })?; + Ok(()) + } +} diff --git a/nexus/db-queries/src/db/datastore/mod.rs b/nexus/db-queries/src/db/datastore/mod.rs index 5bd35fbba9..571466308c 100644 --- a/nexus/db-queries/src/db/datastore/mod.rs +++ b/nexus/db-queries/src/db/datastore/mod.rs @@ -72,6 +72,7 @@ pub mod instance; mod inventory; mod ip_pool; mod ipv4_nat_entry; +mod lldp; mod migration; mod network_interface; mod oximeter; diff --git a/nexus/external-api/output/nexus_tags.txt b/nexus/external-api/output/nexus_tags.txt index 8102ebce08..2dd1125fa8 100644 --- a/nexus/external-api/output/nexus_tags.txt +++ b/nexus/external-api/output/nexus_tags.txt @@ -196,6 +196,9 @@ 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_lldp_config_update POST /v1/system/hardware/switch-port/{port}/lldp/config +networking_switch_port_lldp_config_view GET /v1/system/hardware/switch-port/{port}/lldp/config +networking_switch_port_lldp_neighbors GET /v1/system/hardware/switch-port/{port}/lldp/neighbors 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 diff --git a/nexus/external-api/src/lib.rs b/nexus/external-api/src/lib.rs index 1c5c7c1d2d..094dea1d7d 100644 --- a/nexus/external-api/src/lib.rs +++ b/nexus/external-api/src/lib.rs @@ -1478,6 +1478,43 @@ pub trait NexusExternalApi { query_params: Query, ) -> Result; + /// Fetch the LLDP configuration for a switch port + #[endpoint { + method = GET, + path = "/v1/system/hardware/switch-port/{port}/lldp/config", + tags = ["system/networking"], + }] + async fn networking_switch_port_lldp_config_view( + rqctx: RequestContext, + path_params: Path, + query_params: Query, + ) -> Result, HttpError>; + + /// Update the LLDP configuration for a switch port + #[endpoint { + method = POST, + path = "/v1/system/hardware/switch-port/{port}/lldp/config", + tags = ["system/networking"], + }] + async fn networking_switch_port_lldp_config_update( + rqctx: RequestContext, + path_params: Path, + query_params: Query, + config: TypedBody, + ) -> Result; + + /// Fetch the LLDP neighbors seen on a switch port + #[endpoint { + method = GET, + path = "/v1/system/hardware/switch-port/{port}/lldp/neighbors", + tags = ["system/networking"], + }] + async fn networking_switch_port_lldp_neighbors( + rqctx: RequestContext, + path_params: Path, + query_params: Query, + ) -> Result>, HttpError>; + /// Create new BGP configuration #[endpoint { method = POST, diff --git a/nexus/src/app/lldp.rs b/nexus/src/app/lldp.rs new file mode 100644 index 0000000000..e00e85ca72 --- /dev/null +++ b/nexus/src/app/lldp.rs @@ -0,0 +1,148 @@ +use crate::app::authz; +use lldpd_client::types::ChassisId; +use lldpd_client::types::PortId; +use nexus_db_queries::context::OpContext; +use omicron_common::api::external::Error; +use omicron_common::api::external::LldpLinkConfig; +use omicron_common::api::external::LldpNeighbor; +use omicron_common::api::external::LookupResult; +use omicron_common::api::external::Name; +use omicron_common::api::external::SwitchLocation; +use omicron_common::api::external::UpdateResult; +use uuid::Uuid; + +impl super::Nexus { + /// Lookup and return the LLDP config associated with the link identified + /// using a rack/switch/port triple. + pub(crate) async fn lldp_config_get( + &self, + opctx: &OpContext, + rack_id: Uuid, + switch_location: Name, + port: Name, + ) -> LookupResult { + opctx.authorize(authz::Action::Read, &authz::FLEET).await?; + self.db_datastore + .lldp_config_get(opctx, rack_id, switch_location, port) + .await + } + + /// Lookup the LLDP config associated with the link identified using a + /// rack/switch/port triple, and update all fields in the database to match + /// those in the provided struct. + pub async fn lldp_config_update( + &self, + opctx: &OpContext, + rack_id: Uuid, + switch_location: Name, + port: Name, + config: LldpLinkConfig, + ) -> UpdateResult<()> { + opctx.authorize(authz::Action::Modify, &authz::FLEET).await?; + self.db_datastore + .lldp_config_update(opctx, rack_id, switch_location, port, config) + .await?; + + // eagerly propagate changes via rpw + self.background_tasks + .activate(&self.background_tasks.task_switch_port_settings_manager); + Ok(()) + } + + /// Query the LLDP daemon running on this rack/switch about all neighbors + /// that have been identified on the specified port. + pub async fn lldp_neighbors_get( + &self, + opctx: &OpContext, + rack_id: Uuid, + switch_location: Name, + port: Name, + ) -> Result, Error> { + opctx.authorize(authz::Action::Read, &authz::FLEET).await?; + + let loc: SwitchLocation = + switch_location.as_str().parse().map_err(|e| { + Error::invalid_request(&format!( + "invalid switch name {switch_location}: {e}" + )) + })?; + + let lldpd_clients = self.lldpd_clients(rack_id).await.map_err(|e| { + Error::internal_error(&format!("lldpd clients get: {e}")) + })?; + + let lldpd = + lldpd_clients.get(&loc).ok_or(Error::internal_error(&format!( + "no lldpd client for rack: {rack_id} switch {switch_location}" + )))?; + + let neighbors = lldpd + .get_neighbors() + .await + .map_err(|e| { + Error::internal_error(&format!( + "failed to get neighbor list for {loc}/{port}: {e}" + )) + })? + .into_inner(); + + // The RFC defines several possible data classes for the port_id and + // chassis_id TLVs. There is no real semantic meaning associated with + // the different types, other than describing how the binary payload + // should be parsed. The lldp client interface passes those values to + // us in a type-specific enum. Since there seems to be little value in + // passing that complexity on to consumers of our API, we flatten these + // fields into strings. + Ok(neighbors + .iter() + .map(|n| LldpNeighbor { + local_port: n.port.to_string(), + first_seen: n.first_seen, + last_seen: n.last_seen, + link_name: match &n.system_info.port_id { + PortId::InterfaceAlias(s) => s.to_string(), + PortId::MacAddress(mac) => format!( + "{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}", + mac.a[0], + mac.a[1], + mac.a[2], + mac.a[3], + mac.a[4], + mac.a[5] + ), + PortId::NetworkAddress(ip) => ip.to_string(), + PortId::InterfaceName(s) => s.to_string(), + PortId::PortComponent(s) => s.to_string(), + PortId::AgentCircuitId(s) => s.to_string(), + PortId::LocallyAssigned(s) => s.to_string(), + }, + link_description: n.system_info.port_description.clone(), + chassis_id: match &n.system_info.chassis_id { + ChassisId::ChassisComponent(s) => s.to_string(), + ChassisId::InterfaceAlias(s) => s.to_string(), + ChassisId::PortComponent(s) => s.to_string(), + ChassisId::MacAddress(mac) => format!( + "{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}", + mac.a[0], + mac.a[1], + mac.a[2], + mac.a[3], + mac.a[4], + mac.a[5] + ), + ChassisId::NetworkAddress(ip) => ip.to_string(), + ChassisId::InterfaceName(s) => s.to_string(), + ChassisId::LocallyAssigned(s) => s.to_string(), + }, + system_name: n.system_info.system_name.clone(), + system_description: n.system_info.system_description.clone(), + management_ip: n + .system_info + .management_addresses + .iter() + .map(|a| oxnet::IpNet::host_net(a.addr)) + .collect(), + }) + .collect()) + } +} diff --git a/nexus/src/app/mod.rs b/nexus/src/app/mod.rs index e451119bfc..a938543b32 100644 --- a/nexus/src/app/mod.rs +++ b/nexus/src/app/mod.rs @@ -66,6 +66,7 @@ mod instance; mod instance_network; mod internet_gateway; mod ip_pool; +mod lldp; mod metrics; mod network_interface; pub(crate) mod oximeter; @@ -1022,6 +1023,14 @@ impl Nexus { dpd_clients(resolver, &self.log).await } + pub(crate) async fn lldpd_clients( + &self, + rack_id: Uuid, + ) -> Result, String> { + let resolver = self.resolver(); + lldpd_clients(resolver, rack_id, &self.log).await + } + pub(crate) async fn mg_clients( &self, ) -> Result, String> { @@ -1098,6 +1107,31 @@ pub(crate) async fn dpd_clients( Ok(clients) } +// We currently ignore the rack_id argument here, as the shared +// switch_zone_address_mappings function doesn't allow filtering on the rack ID. +// Since we only have a single rack, this is OK for now. +// TODO: https://github.com/oxidecomputer/omicron/issues/1276 +pub(crate) async fn lldpd_clients( + resolver: &internal_dns_resolver::Resolver, + _rack_id: Uuid, + log: &slog::Logger, +) -> Result, String> { + let mappings = switch_zone_address_mappings(resolver, log).await?; + let log = log.new(o!( "component" => "LldpdClient")); + let port = lldpd_client::default_port(); + let clients: HashMap = mappings + .iter() + .map(|(location, addr)| { + let lldpd_client = lldpd_client::Client::new( + &format!("http://[{addr}]:{port}"), + log.clone(), + ); + (*location, lldpd_client) + }) + .collect(); + Ok(clients) +} + async fn switch_zone_address_mappings( resolver: &internal_dns_resolver::Resolver, log: &slog::Logger, diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index a285542442..a629df3b29 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -76,6 +76,8 @@ use omicron_common::api::external::Error; use omicron_common::api::external::Instance; use omicron_common::api::external::InstanceNetworkInterface; use omicron_common::api::external::InternalContext; +use omicron_common::api::external::LldpLinkConfig; +use omicron_common::api::external::LldpNeighbor; use omicron_common::api::external::LoopbackAddress; use omicron_common::api::external::NameOrId; use omicron_common::api::external::Probe; @@ -3010,6 +3012,97 @@ impl NexusExternalApi for NexusExternalApiImpl { .await } + async fn networking_switch_port_lldp_config_view( + 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?; + let settings = nexus + .lldp_config_get( + &opctx, + query.rack_id, + query.switch_location, + path.port, + ) + .await?; + Ok(HttpResponseOk(settings)) + }; + apictx + .context + .external_latencies + .instrument_dropshot_handler(&rqctx, handler) + .await + } + + async fn networking_switch_port_lldp_config_update( + rqctx: RequestContext, + path_params: Path, + query_params: Query, + config: TypedBody, + ) -> Result { + let apictx = rqctx.context(); + let query = query_params.into_inner(); + let path = path_params.into_inner(); + let handler = async { + let nexus = &apictx.context.nexus; + let config = config.into_inner(); + let opctx = + crate::context::op_context_for_external_api(&rqctx).await?; + nexus + .lldp_config_update( + &opctx, + query.rack_id, + query.switch_location, + path.port, + config, + ) + .await?; + Ok(HttpResponseUpdatedNoContent {}) + }; + apictx + .context + .external_latencies + .instrument_dropshot_handler(&rqctx, handler) + .await + } + + async fn networking_switch_port_lldp_neighbors( + 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 + .lldp_neighbors_get( + &opctx, + query.rack_id, + query.switch_location, + path.port, + ) + .await?, + )) + }; + apictx + .context + .external_latencies + .instrument_dropshot_handler(&rqctx, handler) + .await + } + async fn networking_bgp_config_create( rqctx: RequestContext, config: TypedBody, diff --git a/nexus/tests/output/uncovered-authz-endpoints.txt b/nexus/tests/output/uncovered-authz-endpoints.txt index c5091c5a3b..14f2c364aa 100644 --- a/nexus/tests/output/uncovered-authz-endpoints.txt +++ b/nexus/tests/output/uncovered-authz-endpoints.txt @@ -3,6 +3,8 @@ probe_delete (delete "/experimental/v1/probes/{probe probe_list (get "/experimental/v1/probes") probe_view (get "/experimental/v1/probes/{probe}") ping (get "/v1/ping") +networking_switch_port_lldp_config_view (get "/v1/system/hardware/switch-port/{port}/lldp/config") +networking_switch_port_lldp_neighbors (get "/v1/system/hardware/switch-port/{port}/lldp/neighbors") networking_switch_port_status (get "/v1/system/hardware/switch-port/{port}/status") device_auth_request (post "/device/auth") device_auth_confirm (post "/device/confirm") @@ -11,3 +13,4 @@ probe_create (post "/experimental/v1/probes") login_saml (post "/login/{silo_name}/saml/{provider_name}") login_local (post "/v1/login/{silo_name}/local") logout (post "/v1/logout") +networking_switch_port_lldp_config_update (post "/v1/system/hardware/switch-port/{port}/lldp/config") diff --git a/openapi/nexus.json b/openapi/nexus.json index b4abc02a9f..8d52c025a4 100644 --- a/openapi/nexus.json +++ b/openapi/nexus.json @@ -5350,6 +5350,182 @@ } } }, + "/v1/system/hardware/switch-port/{port}/lldp/config": { + "get": { + "tags": [ + "system/networking" + ], + "summary": "Fetch the LLDP configuration for a switch port", + "operationId": "networking_switch_port_lldp_config_view", + "parameters": [ + { + "in": "path", + "name": "port", + "description": "A name to use when selecting switch ports.", + "required": true, + "schema": { + "$ref": "#/components/schemas/Name" + } + }, + { + "in": "query", + "name": "rack_id", + "description": "A rack id to use when selecting switch ports.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "in": "query", + "name": "switch_location", + "description": "A switch location to use when selecting switch ports.", + "required": true, + "schema": { + "$ref": "#/components/schemas/Name" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LldpLinkConfig" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "post": { + "tags": [ + "system/networking" + ], + "summary": "Update the LLDP configuration for a switch port", + "operationId": "networking_switch_port_lldp_config_update", + "parameters": [ + { + "in": "path", + "name": "port", + "description": "A name to use when selecting switch ports.", + "required": true, + "schema": { + "$ref": "#/components/schemas/Name" + } + }, + { + "in": "query", + "name": "rack_id", + "description": "A rack id to use when selecting switch ports.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "in": "query", + "name": "switch_location", + "description": "A switch location to use when selecting switch ports.", + "required": true, + "schema": { + "$ref": "#/components/schemas/Name" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LldpLinkConfig" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/system/hardware/switch-port/{port}/lldp/neighbors": { + "get": { + "tags": [ + "system/networking" + ], + "summary": "Fetch the LLDP neighbors seen on a switch port", + "operationId": "networking_switch_port_lldp_neighbors", + "parameters": [ + { + "in": "path", + "name": "port", + "description": "A name to use when selecting switch ports.", + "required": true, + "schema": { + "$ref": "#/components/schemas/Name" + } + }, + { + "in": "query", + "name": "rack_id", + "description": "A rack id to use when selecting switch ports.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "in": "query", + "name": "switch_location", + "description": "A switch location to use when selecting switch ports.", + "required": true, + "schema": { + "$ref": "#/components/schemas/Name" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_LldpNeighbor", + "type": "array", + "items": { + "$ref": "#/components/schemas/LldpNeighbor" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, "/v1/system/hardware/switch-port/{port}/settings": { "post": { "tags": [ @@ -17424,6 +17600,64 @@ "enabled" ] }, + "LldpNeighbor": { + "description": "Information about LLDP advertisements from other network entities directly connected to a switch port. This structure contains both metadata about when and where the neighbor was seen, as well as the specific information the neighbor was advertising.", + "type": "object", + "properties": { + "chassis_id": { + "description": "The LLDP chassis identifier advertised by the neighbor", + "type": "string" + }, + "first_seen": { + "description": "Initial sighting of this LldpNeighbor", + "type": "string", + "format": "date-time" + }, + "last_seen": { + "description": "Most recent sighting of this LldpNeighbor", + "type": "string", + "format": "date-time" + }, + "link_description": { + "nullable": true, + "description": "The LLDP link description advertised by the neighbor", + "type": "string" + }, + "link_name": { + "description": "The LLDP link name advertised by the neighbor", + "type": "string" + }, + "local_port": { + "description": "The port on which the neighbor was seen", + "type": "string" + }, + "management_ip": { + "description": "The LLDP management IP(s) advertised by the neighbor", + "type": "array", + "items": { + "$ref": "#/components/schemas/IpNet" + } + }, + "system_description": { + "nullable": true, + "description": "The LLDP system description advertised by the neighbor", + "type": "string" + }, + "system_name": { + "nullable": true, + "description": "The LLDP system name advertised by the neighbor", + "type": "string" + } + }, + "required": [ + "chassis_id", + "first_seen", + "last_seen", + "link_name", + "local_port", + "management_ip" + ] + }, "LoopbackAddress": { "description": "A loopback address is an address that is assigned to a rack switch but is not associated with any particular port.", "type": "object", diff --git a/package-manifest.toml b/package-manifest.toml index 4481f153f2..50600f236d 100644 --- a/package-manifest.toml +++ b/package-manifest.toml @@ -679,8 +679,8 @@ output.intermediate_only = true service_name = "lldp" source.type = "prebuilt" source.repo = "lldp" -source.commit = "188f0f6d4c066f1515bd707050407cedd790fcf1" -source.sha256 = "132d0760be5208f60b58bcaed98fa6384b09f41dd5febf51970f5cbf46138ecf" +source.commit = "471a2dc103081e003e700c9825de608073a850cb" +source.sha256 = "09a030be2b1cd3d2068f2aa38b5fd83ac202bc18dde7d1a41b5eb856779d1e5e" output.type = "zone" output.intermediate_only = true