From d9ed9cc88ba1c91f28add3425bcc015a3a691222 Mon Sep 17 00:00:00 2001 From: Nils Nieuwejaar Date: Fri, 8 Nov 2024 16:05:29 +0000 Subject: [PATCH 1/2] 6750 Add support for post-install LLDP configuration 6751 Add support for extracting LLDP neighbor information --- Cargo.lock | 40 ++- Cargo.toml | 1 + common/src/api/external/http_pagination.rs | 7 +- common/src/api/external/mod.rs | 61 +++- dev-tools/ls-apis/api-manifest.toml | 9 +- 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 | 182 ++++++++++++ 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 | 154 ++++++++++ nexus/src/app/mod.rs | 34 +++ nexus/src/app/sagas/instance_ip_attach.rs | 2 +- nexus/src/app/sagas/instance_ip_detach.rs | 2 +- nexus/src/external_api/http_entrypoints.rs | 104 +++++++ nexus/tests/integration_tests/disks.rs | 2 +- nexus/tests/integration_tests/ip_pools.rs | 2 +- .../tests/integration_tests/router_routes.rs | 2 +- nexus/tests/integration_tests/vpc_routers.rs | 2 +- .../output/uncovered-authz-endpoints.txt | 3 + nexus/types/src/external_api/params.rs | 13 + nexus/types/src/external_api/views.rs | 4 +- openapi/nexus.json | 281 ++++++++++++++++++ package-manifest.toml | 4 +- 26 files changed, 936 insertions(+), 19 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 eb7ce9060b..03c0f20469 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -718,7 +718,7 @@ dependencies = [ "bitflags 2.6.0", "cexpr", "clang-sys", - "itertools 0.12.1", + "itertools 0.10.5", "lazy_static", "lazycell", "log", @@ -1561,6 +1561,22 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "common" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/lldp#ce952e61f444119a2a9fe0d5c5c3db96daf70d96" +dependencies = [ + "anyhow", + "schemars", + "serde", + "serde_json", + "slog", + "slog-async", + "slog-bunyan", + "slog-term", + "thiserror 1.0.69", +] + [[package]] name = "compact_str" version = "0.8.0" @@ -5118,7 +5134,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" dependencies = [ "cfg-if", - "windows-targets 0.52.6", + "windows-targets 0.48.5", ] [[package]] @@ -5293,6 +5309,23 @@ dependencies = [ "syn 2.0.96", ] +[[package]] +name = "lldpd-client" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/lldp#ce952e61f444119a2a9fe0d5c5c3db96daf70d96" +dependencies = [ + "chrono", + "common", + "futures", + "progenitor 0.8.0", + "reqwest", + "schemars", + "serde", + "serde_json", + "slog", + "uuid", +] + [[package]] name = "lock_api" version = "0.4.12" @@ -6991,6 +7024,7 @@ dependencies = [ "internal-dns-types", "ipnetwork", "itertools 0.13.0", + "lldpd-client", "macaddr", "mg-admin-client", "nexus-auth", @@ -13393,7 +13427,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.48.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 3106c357a0..d1d39a306c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -457,6 +457,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/http_pagination.rs b/common/src/api/external/http_pagination.rs index 65237f73c6..d4d237b234 100644 --- a/common/src/api/external/http_pagination.rs +++ b/common/src/api/external/http_pagination.rs @@ -59,6 +59,7 @@ use std::num::NonZeroU32; use uuid::Uuid; use super::SimpleIdentity; +use super::SimpleIdentityOrName; // General pagination infrastructure @@ -140,8 +141,8 @@ pub fn marker_for_name(_: &S, t: &T) -> Name { /// /// This is intended for use with [`ScanById::results_page`] with objects that /// impl [`ObjectIdentity`]. -pub fn marker_for_id(_: &S, t: &T) -> Uuid { - t.identity().id +pub fn marker_for_id(_: &S, t: &T) -> Uuid { + t.id() } /// Marker function that extracts the "name" or "id" from an object, depending @@ -149,7 +150,7 @@ pub fn marker_for_id(_: &S, t: &T) -> Uuid { /// /// This is intended for use with [`ScanByNameOrId::results_page`] with objects /// that impl [`ObjectIdentity`]. -pub fn marker_for_name_or_id( +pub fn marker_for_name_or_id( scan: &ScanByNameOrId, item: &T, ) -> NameOrId { diff --git a/common/src/api/external/mod.rs b/common/src/api/external/mod.rs index 723cf856e7..fd5cbe3804 100644 --- a/common/src/api/external/mod.rs +++ b/common/src/api/external/mod.rs @@ -77,16 +77,28 @@ pub trait ObjectIdentity { } /// Exists for types that don't properly implement `ObjectIdentity` but -/// still need to be paginated by name or id. +/// still need to be paginated by id. pub trait SimpleIdentity { fn id(&self) -> Uuid; - fn name(&self) -> &Name; } impl SimpleIdentity for T { fn id(&self) -> Uuid { self.identity().id } +} + +/// Exists for types that don't properly implement `ObjectIdentity` but +/// still need to be paginated by name or id. +pub trait SimpleIdentityOrName { + fn id(&self) -> Uuid; + fn name(&self) -> &Name; +} + +impl SimpleIdentityOrName for T { + fn id(&self) -> Uuid { + self.identity().id + } fn name(&self) -> &Name { &self.identity().name @@ -1032,6 +1044,7 @@ pub enum ResourceType { FloatingIp, Probe, ProbeNetworkInterface, + LldpLinkConfig, } // IDENTITY METADATA @@ -2595,6 +2608,50 @@ 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 { + // Unique ID assigned to this neighbor - only used for pagination + #[serde(skip)] + pub id: Uuid, + + /// 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, +} + +impl SimpleIdentity for LldpNeighbor { + fn id(&self) -> Uuid { + self.id + } +} + /// 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 75f4777a48..d865b6e115 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", @@ -270,6 +270,13 @@ 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" +versioned_how = "server" +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 d2b672e62c..45dc84a537 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 fbc86a42c2..8afc36bc03 100644 --- a/nexus/Cargo.toml +++ b/nexus/Cargo.toml @@ -48,6 +48,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..40f55b3f84 --- /dev/null +++ b/nexus/db-queries/src/db/datastore/lldp.rs @@ -0,0 +1,182 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! [`DataStore`] methods on [`LldpLinkConfig`]s. + +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. +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 d14eb58ea6..e98cded365 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 4fc92b18d8..82767e399c 100644 --- a/nexus/external-api/output/nexus_tags.txt +++ b/nexus/external-api/output/nexus_tags.txt @@ -206,6 +206,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/rack-switch-port/{rack_id}/{switch_location}/{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 bc453a97e8..a832bde319 100644 --- a/nexus/external-api/src/lib.rs +++ b/nexus/external-api/src/lib.rs @@ -1486,6 +1486,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/rack-switch-port/{rack_id}/{switch_location}/{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..fdf9bdb966 --- /dev/null +++ b/nexus/src/app/lldp.rs @@ -0,0 +1,154 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! LLDP + +use crate::app::authz; +use futures::stream::TryStreamExt; +use lldpd_client::types::ChassisId; +use lldpd_client::types::Neighbor; +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, + previous: &Option, + limit: u32, + 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 mut neighbors: Vec = lldpd + .get_neighbors_stream(&format!("{port}/0"), None) + .try_collect() + .await + .map_err(|e| { + Error::internal_error(&format!( + "failed to get neighbor list for {loc}/{port}: {e}" + )) + })?; + + // Strip out any neighbors seen on previous pages prior to sorting the + // remaining neighbors by their id. + if let Some(p) = previous { + neighbors.retain(|n| n.id > *p); + }; + neighbors.sort_by_key(|n| n.id); + + let limit = usize::try_from(limit) + .expect("u32 to usize should succeed on any machine running nexus"); + + // 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 + .into_iter() + .take(limit) + .map(|n| LldpNeighbor { + id: n.id, + 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) => mac.to_string(), + 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) => mac.to_string(), + 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 636a47f14a..c16e26c992 100644 --- a/nexus/src/app/mod.rs +++ b/nexus/src/app/mod.rs @@ -63,6 +63,7 @@ mod instance; mod instance_network; mod internet_gateway; mod ip_pool; +mod lldp; mod login; mod metrics; mod network_interface; @@ -996,6 +997,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> { @@ -1072,6 +1081,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/app/sagas/instance_ip_attach.rs b/nexus/src/app/sagas/instance_ip_attach.rs index c1dd77fe63..7fb5f61628 100644 --- a/nexus/src/app/sagas/instance_ip_attach.rs +++ b/nexus/src/app/sagas/instance_ip_attach.rs @@ -338,7 +338,7 @@ pub(crate) mod test { create_project, }; use nexus_test_utils_macros::nexus_test; - use omicron_common::api::external::SimpleIdentity; + use omicron_common::api::external::SimpleIdentityOrName; use sled_agent_types::instance::InstanceExternalIpBody; type ControlPlaneTestContext = diff --git a/nexus/src/app/sagas/instance_ip_detach.rs b/nexus/src/app/sagas/instance_ip_detach.rs index 0b7f1378f2..321ae65c2a 100644 --- a/nexus/src/app/sagas/instance_ip_detach.rs +++ b/nexus/src/app/sagas/instance_ip_detach.rs @@ -298,7 +298,7 @@ pub(crate) mod test { use nexus_db_queries::context::OpContext; use nexus_test_utils::resource_helpers::create_instance; use nexus_test_utils_macros::nexus_test; - use omicron_common::api::external::{Name, SimpleIdentity}; + use omicron_common::api::external::{Name, SimpleIdentityOrName}; use std::sync::Arc; type ControlPlaneTestContext = diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index 1d43dc3bdd..191c304216 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -54,6 +54,7 @@ use nexus_types::{ }, }; use omicron_common::api::external::http_pagination::data_page_params_for; +use omicron_common::api::external::http_pagination::marker_for_id; 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; @@ -81,6 +82,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; @@ -3017,6 +3020,107 @@ 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 query = query_params.into_inner(); + let path = path_params.into_inner(); + let pag_params = data_page_params_for(&rqctx, &query)?; + let limit = pag_params.limit.into(); + let prev = pag_params.marker.cloned(); + + let nexus = &apictx.context.nexus; + let opctx = + crate::context::op_context_for_external_api(&rqctx).await?; + let neighbors = nexus + .lldp_neighbors_get( + &opctx, + &prev, + limit, + path.rack_id, + &path.switch_location, + &path.port, + ) + .await?; + + Ok(HttpResponseOk(ScanById::results_page( + &query, + neighbors, + &marker_for_id, + )?)) + }; + 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/integration_tests/disks.rs b/nexus/tests/integration_tests/disks.rs index cf423a6253..a4cf2941b2 100644 --- a/nexus/tests/integration_tests/disks.rs +++ b/nexus/tests/integration_tests/disks.rs @@ -42,7 +42,7 @@ use omicron_common::api::external::Instance; use omicron_common::api::external::InstanceState; use omicron_common::api::external::Name; use omicron_common::api::external::NameOrId; -use omicron_common::api::external::{ByteCount, SimpleIdentity as _}; +use omicron_common::api::external::{ByteCount, SimpleIdentityOrName as _}; use omicron_nexus::app::{MAX_DISK_SIZE_BYTES, MIN_DISK_SIZE_BYTES}; use omicron_nexus::Nexus; use omicron_nexus::TestInterfaces as _; diff --git a/nexus/tests/integration_tests/ip_pools.rs b/nexus/tests/integration_tests/ip_pools.rs index bb2fba8be6..e148d5b239 100644 --- a/nexus/tests/integration_tests/ip_pools.rs +++ b/nexus/tests/integration_tests/ip_pools.rs @@ -58,7 +58,7 @@ use omicron_common::api::external::IdentityMetadataUpdateParams; use omicron_common::api::external::InstanceState; use omicron_common::api::external::LookupType; use omicron_common::api::external::NameOrId; -use omicron_common::api::external::SimpleIdentity; +use omicron_common::api::external::SimpleIdentityOrName; use omicron_common::api::external::{IdentityMetadataCreateParams, Name}; use omicron_nexus::TestInterfaces; use omicron_uuid_kinds::GenericUuid; diff --git a/nexus/tests/integration_tests/router_routes.rs b/nexus/tests/integration_tests/router_routes.rs index b7ef395636..dcc2b24589 100644 --- a/nexus/tests/integration_tests/router_routes.rs +++ b/nexus/tests/integration_tests/router_routes.rs @@ -19,7 +19,7 @@ use nexus_test_utils::resource_helpers::objects_list_page_authz; use nexus_test_utils_macros::nexus_test; use nexus_types::external_api::params; use nexus_types::external_api::params::RouterRouteUpdate; -use omicron_common::api::external::SimpleIdentity; +use omicron_common::api::external::SimpleIdentityOrName; use omicron_common::api::external::{ IdentityMetadataCreateParams, IdentityMetadataUpdateParams, RouteDestination, RouteTarget, RouterRoute, RouterRouteKind, diff --git a/nexus/tests/integration_tests/vpc_routers.rs b/nexus/tests/integration_tests/vpc_routers.rs index eee4d3eaae..71ef0e13e0 100644 --- a/nexus/tests/integration_tests/vpc_routers.rs +++ b/nexus/tests/integration_tests/vpc_routers.rs @@ -33,7 +33,7 @@ use nexus_types::external_api::views::VpcSubnet; use omicron_common::api::external::IdentityMetadataCreateParams; use omicron_common::api::external::IdentityMetadataUpdateParams; use omicron_common::api::external::NameOrId; -use omicron_common::api::external::SimpleIdentity; +use omicron_common::api::external::SimpleIdentityOrName; use omicron_common::api::internal::shared::ResolvedVpcRoute; use omicron_common::api::internal::shared::RouterTarget; use omicron_uuid_kinds::GenericUuid; diff --git a/nexus/tests/output/uncovered-authz-endpoints.txt b/nexus/tests/output/uncovered-authz-endpoints.txt index a53e94414a..8a639f1224 100644 --- a/nexus/tests/output/uncovered-authz-endpoints.txt +++ b/nexus/tests/output/uncovered-authz-endpoints.txt @@ -6,6 +6,8 @@ support_bundle_download (get "/experimental/v1/system/suppor support_bundle_download_file (get "/experimental/v1/system/support-bundles/{support_bundle}/download/{file}") support_bundle_index (get "/experimental/v1/system/support-bundles/{support_bundle}/index") ping (get "/v1/ping") +networking_switch_port_lldp_neighbors (get "/v1/system/hardware/rack-switch-port/{rack_id}/{switch_location}/{port}/lldp/neighbors") +networking_switch_port_lldp_config_view (get "/v1/system/hardware/switch-port/{port}/lldp/config") networking_switch_port_status (get "/v1/system/hardware/switch-port/{port}/status") support_bundle_head (head "/experimental/v1/system/support-bundles/{support_bundle}/download") support_bundle_head_file (head "/experimental/v1/system/support-bundles/{support_bundle}/download/{file}") @@ -16,3 +18,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/nexus/types/src/external_api/params.rs b/nexus/types/src/external_api/params.rs index d4d09ad46a..8294e23e2f 100644 --- a/nexus/types/src/external_api/params.rs +++ b/nexus/types/src/external_api/params.rs @@ -2020,6 +2020,19 @@ pub struct SwitchPortApplySettings { pub port_settings: NameOrId, } +/// Select an LLDP endpoint by rack/switch/port +#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, PartialEq)] +pub struct LldpPortPathSelector { + /// A rack id to use when selecting switch ports. + pub rack_id: Uuid, + + /// A switch location to use when selecting switch ports. + pub switch_location: Name, + + /// A name to use when selecting switch ports. + pub port: Name, +} + // IMAGES /// The source of the underlying image. diff --git a/nexus/types/src/external_api/views.rs b/nexus/types/src/external_api/views.rs index 94b2279906..3430d06f72 100644 --- a/nexus/types/src/external_api/views.rs +++ b/nexus/types/src/external_api/views.rs @@ -15,7 +15,7 @@ use diffus::Diffus; use omicron_common::api::external::{ AllowedSourceIps as ExternalAllowedSourceIps, ByteCount, Digest, Error, IdentityMetadata, InstanceState, Name, ObjectIdentity, RoleName, - SimpleIdentity, + SimpleIdentityOrName, }; use oxnet::{Ipv4Net, Ipv6Net}; use schemars::JsonSchema; @@ -103,7 +103,7 @@ pub struct SiloUtilization { // but we can't derive ObjectIdentity because this isn't a typical asset. // Instead we implement this new simple identity trait which is used under the // hood by the pagination code. -impl SimpleIdentity for SiloUtilization { +impl SimpleIdentityOrName for SiloUtilization { fn id(&self) -> Uuid { self.silo_id } diff --git a/openapi/nexus.json b/openapi/nexus.json index bc043059dd..45f2d4787c 100644 --- a/openapi/nexus.json +++ b/openapi/nexus.json @@ -5134,6 +5134,93 @@ } } }, + "/v1/system/hardware/rack-switch-port/{rack_id}/{switch_location}/{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": "path", + "name": "rack_id", + "description": "A rack id to use when selecting switch ports.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "in": "path", + "name": "switch_location", + "description": "A switch location to use when selecting switch ports.", + "required": true, + "schema": { + "$ref": "#/components/schemas/Name" + } + }, + { + "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/IdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LldpNeighborResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + } + }, "/v1/system/hardware/racks": { "get": { "tags": [ @@ -5673,6 +5760,121 @@ } } }, + "/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}/settings": { "post": { "tags": [ @@ -17796,6 +17998,85 @@ "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" + ] + }, + "LldpNeighborResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/LldpNeighbor" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, "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 f3773b372f..2786c2a49c 100644 --- a/package-manifest.toml +++ b/package-manifest.toml @@ -682,8 +682,8 @@ output.intermediate_only = true service_name = "lldp" source.type = "prebuilt" source.repo = "lldp" -source.commit = "188f0f6d4c066f1515bd707050407cedd790fcf1" -source.sha256 = "132d0760be5208f60b58bcaed98fa6384b09f41dd5febf51970f5cbf46138ecf" +source.commit = "ce952e61f444119a2a9fe0d5c5c3db96daf70d96" +source.sha256 = "0404deaa0c70a34890cb13766925508feb58deefff5eb27376c7f1ea7af45824" output.type = "zone" output.intermediate_only = true From 713d804b4e5321d0dac462010901ec546e904abb Mon Sep 17 00:00:00 2001 From: Nils Nieuwejaar Date: Fri, 24 Jan 2025 18:21:53 +0000 Subject: [PATCH 2/2] satisfy hakari --- Cargo.lock | 1 - workspace-hack/Cargo.toml | 6 ++---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 03c0f20469..20a2979ebd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7511,7 +7511,6 @@ dependencies = [ "indicatif", "inout", "itertools 0.10.5", - "itertools 0.12.1", "lalrpop-util", "lazy_static", "libc", diff --git a/workspace-hack/Cargo.toml b/workspace-hack/Cargo.toml index 3e71e76a87..5e55260694 100644 --- a/workspace-hack/Cargo.toml +++ b/workspace-hack/Cargo.toml @@ -70,8 +70,7 @@ hyper = { version = "1.5.2", features = ["full"] } idna = { version = "1.0.3" } indexmap = { version = "2.7.0", features = ["serde"] } inout = { version = "0.1.3", default-features = false, features = ["std"] } -itertools-5ef9efb8ec2df382 = { package = "itertools", version = "0.12.1" } -itertools-93f6ce9d446188ac = { package = "itertools", version = "0.10.5" } +itertools = { version = "0.10.5" } lalrpop-util = { version = "0.19.12" } lazy_static = { version = "1.5.0", default-features = false, features = ["spin_no_std"] } libc = { version = "0.2.169", features = ["extra_traits"] } @@ -190,8 +189,7 @@ hyper = { version = "1.5.2", features = ["full"] } idna = { version = "1.0.3" } indexmap = { version = "2.7.0", features = ["serde"] } inout = { version = "0.1.3", default-features = false, features = ["std"] } -itertools-5ef9efb8ec2df382 = { package = "itertools", version = "0.12.1" } -itertools-93f6ce9d446188ac = { package = "itertools", version = "0.10.5" } +itertools = { version = "0.10.5" } lalrpop-util = { version = "0.19.12" } lazy_static = { version = "1.5.0", default-features = false, features = ["spin_no_std"] } libc = { version = "0.2.169", features = ["extra_traits"] }