diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 0000000..c4dc638 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,2 @@ +# Whitespace-only changes +7cb947fb3820d02ac4aef3ce1244443a3a46ca8a diff --git a/Cargo.lock b/Cargo.lock index cc6cce7..e8bc606 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1471,7 +1471,9 @@ dependencies = [ "console-subscriber", "csv", "display-error-chain", + "dpd-api", "dpd-client 0.1.0", + "dpd-types", "dropshot", "expectorate", "futures", @@ -1512,6 +1514,19 @@ dependencies = [ "vergen", ] +[[package]] +name = "dpd-api" +version = "0.1.0" +dependencies = [ + "common 0.1.0", + "dpd-types", + "dropshot", + "oxnet", + "schemars", + "serde", + "transceiver-controller", +] + [[package]] name = "dpd-client" version = "0.1.0" @@ -1570,6 +1585,22 @@ dependencies = [ "uuid", ] +[[package]] +name = "dpd-types" +version = "0.1.0" +dependencies = [ + "aal", + "chrono", + "common 0.1.0", + "omicron-common", + "oxnet", + "schemars", + "serde", + "thiserror 1.0.69", + "transceiver-controller", + "uuid", +] + [[package]] name = "dropshot" version = "0.16.3" diff --git a/Cargo.toml b/Cargo.toml index 9cb5e2f..bd10670 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,9 @@ members = [ "aal_macros", "common", "dpd", + "dpd-api", "dpd-client", + "dpd-types", "packet", "pcap", "swadm", @@ -29,7 +31,9 @@ panic = "abort" aal = { path = "aal" } aal_macros = { path = "aal_macros" } asic = { path = "asic" } +dpd-api = { path = "dpd-api" } dpd-client = { path = "dpd-client" } +dpd-types = { path = "dpd-types" } common = { path = "common" } packet = { path = "packet" } pcap = { path = "pcap" } diff --git a/dpd-api/Cargo.toml b/dpd-api/Cargo.toml new file mode 100644 index 0000000..35f806d --- /dev/null +++ b/dpd-api/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "dpd-api" +version = "0.1.0" +edition = "2024" + +[dependencies] +common.workspace = true +dpd-types.workspace = true +dropshot.workspace = true +oxnet.workspace = true +schemars.workspace = true +serde.workspace = true +transceiver-controller.workspace = true diff --git a/dpd-api/src/lib.rs b/dpd-api/src/lib.rs new file mode 100644 index 0000000..37df934 --- /dev/null +++ b/dpd-api/src/lib.rs @@ -0,0 +1,2042 @@ +// 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/ +// +// Copyright 2025 Oxide Computer Company + +use std::{ + collections::{BTreeMap, HashMap, HashSet}, + net::{IpAddr, Ipv4Addr, Ipv6Addr}, +}; + +use common::{ + nat::{Ipv4Nat, Ipv6Nat, NatTarget}, + network::MacAddr, + ports::{ + Ipv4Entry, Ipv6Entry, PortFec, PortId, PortPrbsMode, PortSpeed, TxEq, + }, +}; +use dpd_types::{ + fault::Fault, + link::{LinkFsmCounters, LinkId, LinkUpCounter}, + mcast, oxstats, + port_map::BackplaneLink, + route::{Ipv4Route, Ipv6Route}, + switch_identifiers::SwitchIdentifiers, + switch_port::{Led, ManagementMode}, + transceivers::Transceiver, + views, +}; +use dropshot::{ + EmptyScanParams, HttpError, HttpResponseCreated, HttpResponseDeleted, + HttpResponseOk, HttpResponseUpdatedNoContent, PaginationParams, Path, + Query, RequestContext, ResultsPage, TypedBody, +}; +use oxnet::{Ipv4Net, Ipv6Net}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use transceiver_controller::{ + Datapath, Monitors, PowerState, message::LedState, +}; + +#[dropshot::api_description] +pub trait DpdApi { + type Context; + + /** + * Fetch the IPv6 NDP table entries. + * + * This returns a paginated list of all IPv6 neighbors directly connected to the + * switch. + */ + #[endpoint { + method = GET, + path = "/ndp", + }] + async fn ndp_list( + rqctx: RequestContext, + query: Query>, + ) -> Result>, HttpError>; + + /** + * Remove all entries in the the IPv6 NDP tables. + */ + #[endpoint { + method = DELETE, + path = "/ndp" + }] + async fn ndp_reset( + rqctx: RequestContext, + ) -> Result; + + /** + * Get a single IPv6 NDP table entry, by its IPv6 address. + */ + #[endpoint { + method = GET, + path = "/ndp/{ip}", + }] + async fn ndp_get( + rqctx: RequestContext, + path: Path, + ) -> Result, HttpError>; + + /** + * Add an IPv6 NDP entry, mapping an IPv6 address to a MAC address. + */ + #[endpoint { + method = POST, + path = "/ndp", + }] + async fn ndp_create( + rqctx: RequestContext, + update: TypedBody, + ) -> Result; + + /** + * Remove an IPv6 NDP entry, by its IPv6 address. + */ + #[endpoint { + method = DELETE, + path = "/ndp/{ip}", + }] + async fn ndp_delete( + rqctx: RequestContext, + path: Path, + ) -> Result; + + /** + * Fetch the configured IPv4 ARP table entries. + */ + #[endpoint { + method = GET, + path = "/arp", + }] + async fn arp_list( + rqctx: RequestContext, + query: Query>, + ) -> Result>, HttpError>; + + /** + * Remove all entries in the IPv4 ARP tables. + */ + #[endpoint { + method = DELETE, + path = "/arp", + }] + async fn arp_reset( + rqctx: RequestContext, + ) -> Result; + + /** + * Get a single IPv4 ARP table entry, by its IPv4 address. + */ + #[endpoint { + method = GET, + path = "/arp/{ip}", + }] + async fn arp_get( + rqctx: RequestContext, + path: Path, + ) -> Result, HttpError>; + + /** + * Add an IPv4 ARP table entry, mapping an IPv4 address to a MAC address. + */ + #[endpoint { + method = POST, + path = "/arp", + }] + async fn arp_create( + rqctx: RequestContext, + update: TypedBody, + ) -> Result; + + /** + * Remove a single IPv4 ARP entry, by its IPv4 address. + */ + #[endpoint { + method = DELETE, + path = "/arp/{ip}", + }] + async fn arp_delete( + rqctx: RequestContext, + path: Path, + ) -> Result; + + /** + * Fetch the configured IPv6 routes, mapping IPv6 CIDR blocks to the switch port + * used for sending out that traffic, and optionally a gateway. + */ + #[endpoint { + method = GET, + path = "/route/ipv6", + }] + async fn route_ipv6_list( + rqctx: RequestContext, + query: Query>, + ) -> Result>, HttpError>; + + /** + * Get a single IPv6 route, by its IPv6 CIDR block. + */ + #[endpoint { + method = GET, + path = "/route/ipv6/{cidr}", + }] + async fn route_ipv6_get( + rqctx: RequestContext, + path: Path, + ) -> Result>, HttpError>; + + /** + * Route an IPv6 subnet to a link and a nexthop gateway. + * + * This call can be used to create a new single-path route or to add new targets + * to a multipath route. + */ + #[endpoint { + method = POST, + path = "/route/ipv6", + }] + async fn route_ipv6_add( + rqctx: RequestContext, + update: TypedBody, + ) -> Result; + + /** + * Route an IPv6 subnet to a link and a nexthop gateway. + * + * This call can be used to create a new single-path route or to replace any + * existing routes with a new single-path route. + */ + #[endpoint { + method = PUT, + path = "/route/ipv6", + }] + async fn route_ipv6_set( + rqctx: RequestContext, + update: TypedBody, + ) -> Result; + + /** + * Remove an IPv6 route, by its IPv6 CIDR block. + */ + #[endpoint { + method = DELETE, + path = "/route/ipv6/{cidr}", + }] + async fn route_ipv6_delete( + rqctx: RequestContext, + path: Path, + ) -> Result; + + /** + * Remove a single target for the given IPv6 subnet + */ + #[endpoint { + method = DELETE, + path = "/route/ipv6/{cidr}/{port_id}/{link_id}/{tgt_ip}", + }] + async fn route_ipv6_delete_target( + rqctx: RequestContext, + path: Path, + ) -> Result; + + /** + * Fetch the configured IPv4 routes, mapping IPv4 CIDR blocks to the switch port + * used for sending out that traffic, and optionally a gateway. + */ + #[endpoint { + method = GET, + path = "/route/ipv4", + }] + async fn route_ipv4_list( + rqctx: RequestContext, + query: Query>, + ) -> Result>, HttpError>; + + /** + * Get the configured route for the given IPv4 subnet. + */ + #[endpoint { + method = GET, + path = "/route/ipv4/{cidr}", + }] + async fn route_ipv4_get( + rqctx: RequestContext, + path: Path, + ) -> Result>, HttpError>; + + /** + * Route an IPv4 subnet to a link and a nexthop gateway. + * + * This call can be used to create a new single-path route or to add new targets + * to a multipath route. + */ + #[endpoint { + method = POST, + path = "/route/ipv4", + }] + async fn route_ipv4_add( + rqctx: RequestContext, + update: TypedBody, + ) -> Result; + + /** + * Route an IPv4 subnet to a link and a nexthop gateway. + * + * This call can be used to create a new single-path route or to replace any + * existing routes with a new single-path route. + */ + #[endpoint { + method = PUT, + path = "/route/ipv4", + }] + async fn route_ipv4_set( + rqctx: RequestContext, + update: TypedBody, + ) -> Result; + + /** + * Remove all targets for the given subnet + */ + #[endpoint { + method = DELETE, + path = "/route/ipv4/{cidr}", + }] + async fn route_ipv4_delete( + rqctx: RequestContext, + path: Path, + ) -> Result; + + /** + * Remove a single target for the given IPv4 subnet + */ + #[endpoint { + method = DELETE, + path = "/route/ipv4/{cidr}/{port_id}/{link_id}/{tgt_ip}", + }] + async fn route_ipv4_delete_target( + rqctx: RequestContext, + path: Path, + ) -> Result; + + /// List all switch ports on the system. + #[endpoint { + method = GET, + path = "/ports", + }] + async fn port_list( + rqctx: RequestContext, + ) -> Result>, HttpError>; + + /// Get the set of available channels for all ports. + /// + /// This returns the unused MAC channels for each physical switch port. This can + /// be used to determine how many additional links can be crated on a physical + /// switch port. + #[endpoint { + method = GET, + path = "/channels", + }] + async fn channels_list( + rqctx: RequestContext, + ) -> Result>, HttpError>; + + /// Return information about a single switch port. + #[endpoint { + method = GET, + path = "/ports/{port_id}", + }] + async fn port_get( + rqctx: RequestContext, + path: Path, + ) -> Result, HttpError>; + + /// Return the current management mode of a QSFP switch port. + #[endpoint { + method = GET, + path = "/ports/{port_id}/management-mode", + }] + async fn management_mode_get( + rqctx: RequestContext, + path: Path, + ) -> Result, HttpError>; + + /// Set the current management mode of a QSFP switch port. + #[endpoint { + method = PUT, + path = "/ports/{port_id}/management-mode", + }] + async fn management_mode_set( + rqctx: RequestContext, + path: Path, + body: TypedBody, + ) -> Result; + + /// Return the current state of the attention LED on a front-facing QSFP port. + #[endpoint { + method = GET, + path = "/ports/{port_id}/led", + }] + async fn led_get( + rqctx: RequestContext, + path: Path, + ) -> Result, HttpError>; + + /// Override the current state of the attention LED on a front-facing QSFP port. + /// + /// The attention LED normally follows the state of the port itself. For + /// example, if a transceiver is powered and operating normally, then the LED is + /// solid on. An unexpected power fault would then be reflected by powering off + /// the LED. + /// + /// The client may override this behavior, explicitly setting the LED to a + /// specified state. This can be undone, sending the LED back to its default + /// policy, with the endpoint `/ports/{port_id}/led/auto`. + #[endpoint { + method = PUT, + path = "/ports/{port_id}/led", + }] + async fn led_set( + rqctx: RequestContext, + path: Path, + body: TypedBody, + ) -> Result; + + /// Return the full backplane map. + /// + /// This returns the entire mapping of all cubbies in a rack, through the cabled + /// backplane, and into the Sidecar main board. It also includes the Tofino + /// "connector", which is included in some contexts such as reporting counters. + #[endpoint { + method = GET, + path = "/backplane-map", + }] + async fn backplane_map( + rqctx: RequestContext, + ) -> Result>, HttpError>; + + /// Return the backplane mapping for a single switch port. + #[endpoint { + method = GET, + path = "/backplane-map/{port_id}", + }] + async fn port_backplane_link( + rqctx: RequestContext, + path: Path, + ) -> Result, HttpError>; + + /// Return the state of all attention LEDs on the Sidecar QSFP ports. + #[endpoint { + method = GET, + path = "/leds", + }] + async fn leds_list( + rqctx: RequestContext, + ) -> Result>, HttpError>; + + /// Set the LED policy to automatic. + /// + /// The automatic LED policy ensures that the state of the LED follows the state + /// of the switch port itself. + #[endpoint { + method = PUT, + path = "/ports/{port_id}/led/auto", + }] + async fn led_set_auto( + rqctx: RequestContext, + path: Path, + ) -> Result; + + /// Return information about all QSFP transceivers. + #[endpoint { + method = GET, + path = "/transceivers", + }] + async fn transceivers_list( + rqctx: RequestContext, + ) -> Result>, HttpError>; + + /// Return the information about a port's transceiver. + /// + /// This returns the status (presence, power state, etc) of the transceiver + /// along with its identifying information. If the port is an optical switch + /// port, but has no transceiver, then the identifying information is empty. + /// + /// If the switch port is not a QSFP port, and thus could never have a + /// transceiver, then "Not Found" is returned. + #[endpoint { + method = GET, + path = "/ports/{port_id}/transceiver", + }] + async fn transceiver_get( + rqctx: RequestContext, + path: Path, + ) -> Result, HttpError>; + + /// Effect a module-level reset of a QSFP transceiver. + /// + /// If the QSFP port has no transceiver or is not a QSFP port, then a client + /// error is returned. + #[endpoint { + method = POST, + path = "/ports/{port_id}/transceiver/reset", + }] + async fn transceiver_reset( + rqctx: RequestContext, + path: Path, + ) -> Result; + + /// Control the power state of a transceiver. + #[endpoint { + method = PUT, + path = "/ports/{port_id}/transceiver/power", + }] + async fn transceiver_power_set( + rqctx: RequestContext, + path: Path, + state: TypedBody, + ) -> Result; + + /// Return the power state of a transceiver. + #[endpoint { + method = GET, + path = "/ports/{port_id}/transceiver/power", + }] + async fn transceiver_power_get( + rqctx: RequestContext, + path: Path, + ) -> Result, HttpError>; + + /// Fetch the monitored environmental information for the provided transceiver. + #[endpoint { + method = GET, + path = "/ports/{port_id}/transceiver/monitors", + }] + async fn transceiver_monitors_get( + rqctx: RequestContext, + path: Path, + ) -> Result, HttpError>; + + /// Fetch the state of the datapath for the provided transceiver. + #[endpoint { + method = GET, + path = "/ports/{port_id}/transceiver/datapath" + }] + async fn transceiver_datapath_get( + rqctx: RequestContext, + path: Path, + ) -> Result, HttpError>; + + /// Create a link on a switch port. + /// + /// Create an interface that can be used for sending Ethernet frames on the + /// provided switch port. This will use the first available lanes in the + /// physical port to create an interface of the desired speed, if possible. + #[endpoint { + method = POST, + path = "/ports/{port_id}/links" + }] + async fn link_create( + rqctx: RequestContext, + path: Path, + params: TypedBody, + ) -> Result, HttpError>; + + /// Get an existing link by ID. + #[endpoint { + method = GET, + path = "/ports/{port_id}/links/{link_id}" + }] + async fn link_get( + rqctx: RequestContext, + path: Path, + ) -> Result, HttpError>; + + /// Delete a link from a switch port. + #[endpoint { + method = DELETE, + path = "/ports/{port_id}/links/{link_id}", + }] + async fn link_delete( + rqctx: RequestContext, + path: Path, + ) -> Result; + + /// List the links within a single switch port. + #[endpoint { + method = GET, + path = "/ports/{port_id}/links", + }] + async fn link_list( + rqctx: RequestContext, + path: Path, + ) -> Result>, HttpError>; + + /// List all links, on all switch ports. + #[endpoint { + method = GET, + path = "/links", + }] + async fn link_list_all( + rqctx: RequestContext, + query: Query, + ) -> Result>, HttpError>; + + /// Return whether the link is enabled. + #[endpoint { + method = GET, + path = "/ports/{port_id}/links/{link_id}/enabled", + }] + async fn link_enabled_get( + rqctx: RequestContext, + path: Path, + ) -> Result, HttpError>; + + /// Enable or disable a link. + #[endpoint { + method = PUT, + path = "/ports/{port_id}/links/{link_id}/enabled", + }] + async fn link_enabled_set( + rqctx: RequestContext, + path: Path, + body: TypedBody, + ) -> Result; + + /// Return whether the link is configured to act as an IPv6 endpoint + #[endpoint { + method = GET, + path = "/ports/{port_id}/links/{link_id}/ipv6_enabled", + }] + async fn link_ipv6_enabled_get( + rqctx: RequestContext, + path: Path, + ) -> Result, HttpError>; + + /// Set whether a port is configured to act as an IPv6 endpoint + #[endpoint { + method = PUT, + path = "/ports/{port_id}/links/{link_id}/ipv6_enabled", + }] + async fn link_ipv6_enabled_set( + rqctx: RequestContext, + path: Path, + body: TypedBody, + ) -> Result; + + /// Return whether the link is in KR mode. + /// + /// "KR" refers to the Ethernet standard for the link, which are defined in + /// various clauses of the IEEE 802.3 specification. "K" is used to denote a + /// link over an electrical cabled backplane, and "R" refers to "scrambled + /// encoding", a 64B/66B bit-encoding scheme. + /// + /// Thus this should be true iff a link is on the cabled backplane. + #[endpoint { + method = GET, + path = "/ports/{port_id}/links/{link_id}/kr", + }] + async fn link_kr_get( + rqctx: RequestContext, + path: Path, + ) -> Result, HttpError>; + + /// Enable or disable a link. + #[endpoint { + method = PUT, + path = "/ports/{port_id}/links/{link_id}/kr", + }] + async fn link_kr_set( + rqctx: RequestContext, + path: Path, + body: TypedBody, + ) -> Result; + + /// Return whether the link is configured to use autonegotiation with its peer + /// link. + #[endpoint { + method = GET, + path = "/ports/{port_id}/links/{link_id}/autoneg", + }] + async fn link_autoneg_get( + rqctx: RequestContext, + path: Path, + ) -> Result, HttpError>; + + /// Set whether a port is configured to use autonegotation with its peer link. + #[endpoint { + method = PUT, + path = "/ports/{port_id}/links/{link_id}/autoneg", + }] + async fn link_autoneg_set( + rqctx: RequestContext, + path: Path, + body: TypedBody, + ) -> Result; + + /// Set a link's PRBS speed and mode. + #[endpoint { + method = PUT, + path = "/ports/{port_id}/links/{link_id}/prbs", + }] + async fn link_prbs_set( + rqctx: RequestContext, + path: Path, + body: TypedBody, + ) -> Result; + + /// Return the link's PRBS speed and mode. + /// + /// During link training, a pseudorandom bit sequence (PRBS) is used to allow + /// each side to synchronize their clocks and set various parameters on the + /// underlying circuitry (such as filter gains). + #[endpoint { + method = GET, + path = "/ports/{port_id}/links/{link_id}/prbs", + }] + async fn link_prbs_get( + rqctx: RequestContext, + path: Path, + ) -> Result, HttpError>; + + /// Return whether a link is up. + #[endpoint { + method = GET, + path = "/ports/{port_id}/links/{link_id}/linkup", + }] + async fn link_linkup_get( + rqctx: RequestContext, + path: Path, + ) -> Result, HttpError>; + + /// Return any fault currently set on this link + #[endpoint { + method = GET, + path = "/ports/{port_id}/links/{link_id}/fault", + }] + async fn link_fault_get( + rqctx: RequestContext, + path: Path, + ) -> Result, HttpError>; + + /// Clear any fault currently set on this link + #[endpoint { + method = DELETE, + path = "/ports/{port_id}/links/{link_id}/fault", + }] + async fn link_fault_clear( + rqctx: RequestContext, + path: Path, + ) -> Result; + + /// Inject a fault on this link + #[endpoint { + method = POST, + path = "/ports/{port_id}/links/{link_id}/fault", + }] + async fn link_fault_inject( + rqctx: RequestContext, + path: Path, + entry: TypedBody, + ) -> Result; + + /// List the IPv4 addresses associated with a link. + #[endpoint { + method = GET, + path = "/ports/{port_id}/links/{link_id}/ipv4", + }] + async fn link_ipv4_list( + rqctx: RequestContext, + path: Path, + query: Query>, + ) -> Result>, HttpError>; + + /// Add an IPv4 address to a link. + #[endpoint { + method = POST, + path = "/ports/{port_id}/links/{link_id}/ipv4", + }] + async fn link_ipv4_create( + rqctx: RequestContext, + path: Path, + entry: TypedBody, + ) -> Result; + + /// Clear all IPv4 addresses from a link. + #[endpoint { + method = DELETE, + path = "/ports/{port_id}/links/{link_id}/ipv4", + }] + async fn link_ipv4_reset( + rqctx: RequestContext, + path: Path, + ) -> Result; + + /// Remove an IPv4 address from a link. + #[endpoint { + method = DELETE, + path = "/ports/{port_id}/links/{link_id}/ipv4/{address}", + }] + async fn link_ipv4_delete( + rqctx: RequestContext, + path: Path, + ) -> Result; + + /// List the IPv6 addresses associated with a link. + #[endpoint { + method = GET, + path = "/ports/{port_id}/links/{link_id}/ipv6", + }] + async fn link_ipv6_list( + rqctx: RequestContext, + path: Path, + query: Query>, + ) -> Result>, HttpError>; + + /// Add an IPv6 address to a link. + #[endpoint { + method = POST, + path = "/ports/{port_id}/links/{link_id}/ipv6", + }] + async fn link_ipv6_create( + rqctx: RequestContext, + path: Path, + entry: TypedBody, + ) -> Result; + + /// Clear all IPv6 addresses from a link. + #[endpoint { + method = DELETE, + path = "/ports/{port_id}/links/{link_id}/ipv6", + }] + async fn link_ipv6_reset( + rqctx: RequestContext, + path: Path, + ) -> Result; + + /// Remove an IPv6 address from a link. + #[endpoint { + method = DELETE, + path = "/ports/{port_id}/links/{link_id}/ipv6/{address}", + }] + async fn link_ipv6_delete( + rqctx: RequestContext, + path: Path, + ) -> Result; + + /// Get a link's MAC address. + #[endpoint { + method = GET, + path = "/ports/{port_id}/links/{link_id}/mac", + }] + async fn link_mac_get( + rqctx: RequestContext, + path: Path, + ) -> Result, HttpError>; + + /// Set a link's MAC address. + // TODO-correctness: A link's MAC address should be determined by the FRUID + // data, not under the control of the client. We really only need this for + // the integration tests in `dpd-client`. We should consider removing it for + // production. + #[endpoint { + method = PUT, + path = "/ports/{port_id}/links/{link_id}/mac", + }] + async fn link_mac_set( + rqctx: RequestContext, + path: Path, + body: TypedBody, + ) -> Result; + + /// Return whether the link is configured to drop non-nat traffic + #[endpoint { + method = GET, + path = "/ports/{port_id}/links/{link_id}/nat_only", + }] + async fn link_nat_only_get( + rqctx: RequestContext, + path: Path, + ) -> Result, HttpError>; + + /// Set whether a port is configured to use drop non-nat traffic + #[endpoint { + method = PUT, + path = "/ports/{port_id}/links/{link_id}/nat_only", + }] + async fn link_nat_only_set( + rqctx: RequestContext, + path: Path, + body: TypedBody, + ) -> Result; + + /// Get the event history for the given link. + #[endpoint { + method = GET, + path = "/ports/{port_id}/links/{link_id}/history", + }] + async fn link_history_get( + rqctx: RequestContext, + path: Path, + ) -> Result, HttpError>; + + /** + * Get loopback IPv4 addresses. + */ + #[endpoint { + method = GET, + path = "/loopback/ipv4", + }] + async fn loopback_ipv4_list( + rqctx: RequestContext, + ) -> Result>, HttpError>; + + /** + * Add a loopback IPv4. + */ + #[endpoint { + method = POST, + path = "/loopback/ipv4", + }] + async fn loopback_ipv4_create( + rqctx: RequestContext, + val: TypedBody, + ) -> Result; + + /** + * Remove one loopback IPv4 address. + */ + #[endpoint { + method = DELETE, + path = "/loopback/ipv4/{ipv4}", + }] + async fn loopback_ipv4_delete( + rqctx: RequestContext, + path: Path, + ) -> Result; + + /** + * Get loopback IPv6 addresses. + */ + #[endpoint { + method = GET, + path = "/loopback/ipv6", + }] + async fn loopback_ipv6_list( + rqctx: RequestContext, + ) -> Result>, HttpError>; + + /** + * Add a loopback IPv6. + */ + #[endpoint { + method = POST, + path = "/loopback/ipv6", + }] + async fn loopback_ipv6_create( + rqctx: RequestContext, + val: TypedBody, + ) -> Result; + + /** + * Remove one loopback IPv6 address. + */ + #[endpoint { + method = DELETE, + path = "/loopback/ipv6/{ipv6}", + }] + async fn loopback_ipv6_delete( + rqctx: RequestContext, + path: Path, + ) -> Result; + + /** + * Get all of the external addresses in use for NAT mappings. + */ + #[endpoint { + method = GET, + path = "/nat/ipv6", + }] + async fn nat_ipv6_addresses_list( + rqctx: RequestContext, + query: Query>, + ) -> Result>, HttpError>; + + /** + * Get all of the external->internal NAT mappings for a given address. + */ + #[endpoint { + method = GET, + path = "/nat/ipv6/{ipv6}", + }] + async fn nat_ipv6_list( + rqctx: RequestContext, + path: Path, + query: Query>, + ) -> Result>, HttpError>; + + /** + * Get the external->internal NAT mapping for the given address and starting L3 + * port. + */ + #[endpoint { + method = GET, + path = "/nat/ipv6/{ipv6}/{low}", + }] + async fn nat_ipv6_get( + rqctx: RequestContext, + path: Path, + ) -> Result, HttpError>; + + /** + * Add an external->internal NAT mapping for the given address and L3 port + * range. + * + * This maps an external IPv6 address and L3 port range to: + * - A gimlet's IPv6 address + * - A gimlet's MAC address + * - A Geneve VNI + * + * These identify the gimlet on which a guest is running, and gives OPTE the + * information it needs to identify the guest VM that uses the external IPv6 + * and port range when making connections outside of an Oxide rack. + */ + #[endpoint { + method = PUT, + path = "/nat/ipv6/{ipv6}/{low}/{high}" + }] + async fn nat_ipv6_create( + rqctx: RequestContext, + path: Path, + target: TypedBody, + ) -> Result; + + /** + * Delete the NAT mapping for an IPv6 address and starting L3 port. + */ + #[endpoint { + method = DELETE, + path = "/nat/ipv6/{ipv6}/{low}" + }] + async fn nat_ipv6_delete( + rqctx: RequestContext, + path: Path, + ) -> Result; + + /** + * Clear all IPv6 NAT mappings. + */ + #[endpoint { + method = DELETE, + path = "/nat/ipv6" + }] + async fn nat_ipv6_reset( + rqctx: RequestContext, + ) -> Result; + + /** + * Get all of the external addresses in use for IPv4 NAT mappings. + */ + #[endpoint { + method = GET, + path = "/nat/ipv4", + }] + async fn nat_ipv4_addresses_list( + rqctx: RequestContext, + query: Query>, + ) -> Result>, HttpError>; + + /** + * Get all of the external->internal NAT mappings for a given IPv4 address. + */ + #[endpoint { + method = GET, + path = "/nat/ipv4/{ipv4}", + }] + async fn nat_ipv4_list( + rqctx: RequestContext, + path: Path, + query: Query>, + ) -> Result>, HttpError>; + + /** + * Get the external->internal NAT mapping for the given address/port + */ + #[endpoint { + method = GET, + path = "/nat/ipv4/{ipv4}/{low}", + }] + async fn nat_ipv4_get( + rqctx: RequestContext, + path: Path, + ) -> Result, HttpError>; + + /** + * Add an external->internal NAT mapping for the given address/port range + * + * This maps an external IPv6 address and L3 port range to: + * - A gimlet's IPv6 address + * - A gimlet's MAC address + * - A Geneve VNI + * + * These identify the gimlet on which a guest is running, and gives OPTE the + * information it needs to identify the guest VM that uses the external IPv6 + * and port range when making connections outside of an Oxide rack. + */ + + #[endpoint { + method = PUT, + path = "/nat/ipv4/{ipv4}/{low}/{high}" + }] + async fn nat_ipv4_create( + rqctx: RequestContext, + path: Path, + target: TypedBody, + ) -> Result; + + /** + * Clear the NAT mappings for an IPv4 address and starting L3 port. + */ + #[endpoint { + method = DELETE, + path = "/nat/ipv4/{ipv4}/{low}" + }] + async fn nat_ipv4_delete( + rqctx: RequestContext, + path: Path, + ) -> Result; + + /** + * Clear all IPv4 NAT mappings. + */ + #[endpoint { + method = DELETE, + path = "/nat/ipv4" + }] + async fn nat_ipv4_reset( + rqctx: RequestContext, + ) -> Result; + + /** + * Clear all settings associated with a specific tag. + * + * This removes: + * + * - All ARP or NDP table entries. + * - All routes + * - All links on all switch ports + */ + // TODO-security: This endpoint should probably not exist. + #[endpoint { + method = DELETE, + path = "/all-settings/{tag}", + }] + async fn reset_all_tagged( + rqctx: RequestContext, + path: Path, + ) -> Result; + + /** + * Clear all settings. + * + * This removes all data entirely. + */ + // TODO-security: This endpoint should probably not exist. + #[endpoint { + method = DELETE, + path = "/all-settings" + }] + async fn reset_all( + rqctx: RequestContext, + ) -> Result; + + /// Get the LinkUp counters for all links. + #[endpoint { + method = GET, + path = "/counters/linkup", + }] + async fn link_up_counters_list( + rqctx: RequestContext, + ) -> Result>, HttpError>; + + /// Get the LinkUp counters for the given link. + #[endpoint { + method = GET, + path = "/counters/linkup/{port_id}/{link_id}", + }] + async fn link_up_counters_get( + rqctx: RequestContext, + path: Path, + ) -> Result, HttpError>; + + /// Get the autonegotiation FSM counters for the given link. + #[endpoint { + method = GET, + path = "/counters/fsm/{port_id}/{link_id}", + }] + async fn link_fsm_counters_get( + rqctx: RequestContext, + path: Path, + ) -> Result, HttpError>; + + /// Return detailed build information about the `dpd` server itself. + #[endpoint { + method = GET, + path = "/build-info", + }] + async fn build_info( + _rqctx: RequestContext, + ) -> Result, HttpError>; + + /** + * Return the version of the `dpd` server itself. + */ + #[endpoint { + method = GET, + path = "/dpd-version", + }] + async fn dpd_version( + _rqctx: RequestContext, + ) -> Result, HttpError>; + + /** + * Return the server uptime. + */ + #[endpoint { + method = GET, + path = "/dpd-uptime", + }] + async fn dpd_uptime( + rqctx: RequestContext, + ) -> Result, HttpError>; + + /// Used to request the metadata used to identify this dpd instance and its + /// data with oximeter. + #[endpoint { + method = GET, + path = "/oximeter-metadata", + unpublished = true, + }] + async fn oximeter_collect_meta_endpoint( + rqctx: RequestContext, + ) -> Result>, HttpError>; + + /** + * Apply port settings atomically. + * + * These settings will be applied holistically, and to the extent possible + * atomically to a given port. In the event of a failure a rollback is + * attempted. If the rollback fails there will be inconsistent state. This + * failure mode returns the error code "rollback failure". For more details see + * the docs on the [`PortSettings`] type. + */ + #[endpoint { + method = POST, + path = "/port/{port_id}/settings" + }] + async fn port_settings_apply( + rqctx: RequestContext, + path: Path, + query: Query, + body: TypedBody, + ) -> Result, HttpError>; + + /** + * Clear port settings atomically. + */ + #[endpoint { + method = DELETE, + path = "/port/{port_id}/settings" + }] + async fn port_settings_clear( + rqctx: RequestContext, + path: Path, + query: Query, + ) -> Result, HttpError>; + + /** + * Get port settings atomically. + */ + #[endpoint { + method = GET, + path = "/port/{port_id}/settings" + }] + async fn port_settings_get( + rqctx: RequestContext, + path: Path, + query: Query, + ) -> Result, HttpError>; + + /// Get switch identifiers. + /// + /// This endpoint returns the switch identifiers, which can be used for + /// consistent field definitions across oximeter time series schemas. + #[endpoint { + method = GET, + path = "/switch/identifiers", + }] + async fn switch_identifiers( + rqctx: RequestContext, + ) -> Result, HttpError>; + + /// Collect the link data consumed by `tfportd`. This app-specific convenience + /// routine is meant to reduce the time and traffic expended on this once-per- + /// second operation, by consolidating multiple per-link requests into a single + /// per-switch request. + #[endpoint { + method = GET, + path = "/links/tfport_data", + }] + async fn tfport_data( + rqctx: RequestContext, + ) -> Result>, HttpError>; + + /** + * Get NATv4 generation number + */ + #[endpoint { + method = GET, + path = "/rpw/nat/ipv4/gen" + }] + async fn ipv4_nat_generation( + rqctx: RequestContext, + ) -> Result, HttpError>; + + /** + * Trigger NATv4 Reconciliation + */ + #[endpoint { + method = POST, + path = "/rpw/nat/ipv4/trigger" + }] + async fn ipv4_nat_trigger_update( + rqctx: RequestContext, + ) -> Result, HttpError>; + + /** + * Get the list of P4 tables + */ + #[endpoint { + method = GET, + path = "/table" + }] + async fn table_list( + rqctx: RequestContext, + ) -> Result>, HttpError>; + + /** + * Get the contents of a single P4 table. + * The name of the table should match one of those returned by the + * `table_list()` call. + */ + #[endpoint { + method = GET, + path = "/table/{table}/dump" + }] + async fn table_dump( + rqctx: RequestContext, + path: Path, + ) -> Result, HttpError>; + + /** + * Get any counter data from a single P4 match-action table. + * The name of the table should match one of those returned by the + * `table_list()` call. + */ + #[endpoint { + method = GET, + path = "/table/{table}/counters" + }] + async fn table_counters( + rqctx: RequestContext, + query: Query, + path: Path, + ) -> Result>, HttpError>; + + /** + * Get a list of all the available p4-defined counters. + */ + #[endpoint { + method = GET, + path = "/counters/p4", + }] + async fn counter_list( + _rqctx: RequestContext, + ) -> Result>, HttpError>; + + /** + * Reset a single p4-defined counter. + * The name of the counter should match one of those returned by the + * `counter_list()` call. + */ + #[endpoint { + method = POST, + path = "/counters/p4/{counter}/reset", + }] + async fn counter_reset( + rqctx: RequestContext, + path: Path, + ) -> Result; + + /** + * Get the values for a given counter. + * The name of the counter should match one of those returned by the + * `counter_list()` call. + */ + #[endpoint { + method = GET, + path = "/counters/p4/{counter}", + }] + async fn counter_get( + rqctx: RequestContext, + query: Query, + path: Path, + ) -> Result>, HttpError>; + + /** + * Create an external-only multicast group configuration. + * + * External-only groups are used for IPv4 and non-admin-scoped IPv6 multicast + * traffic that doesn't require replication infrastructure. These groups use + * simple forwarding tables and require a NAT target. + */ + #[endpoint { + method = POST, + path = "/multicast/external-groups", + }] + async fn multicast_group_create_external( + rqctx: RequestContext, + group: TypedBody, + ) -> Result, HttpError>; + + /** + * Create an internal multicast group configuration. + * + * Internal groups are used for admin-scoped IPv6 multicast traffic that + * requires replication infrastructure. These groups support both external + * and underlay members with full replication capabilities. + */ + #[endpoint { + method = POST, + path = "/multicast/groups", + }] + async fn multicast_group_create( + rqctx: RequestContext, + group: TypedBody, + ) -> Result, HttpError>; + + /** + * Delete a multicast group configuration by IP address. + */ + #[endpoint { + method = DELETE, + path = "/multicast/groups/{group_ip}", + }] + async fn multicast_group_delete( + rqctx: RequestContext, + path: Path, + ) -> Result; + + /** + * Reset all multicast group configurations. + */ + #[endpoint { + method = DELETE, + path = "/multicast/groups", + }] + async fn multicast_reset( + rqctx: RequestContext, + ) -> Result; + + /** + * Get the multicast group configuration for a given group IP address. + */ + #[endpoint { + method = GET, + path = "/multicast/groups/{group_ip}", + }] + async fn multicast_group_get( + rqctx: RequestContext, + path: Path, + ) -> Result, HttpError>; + + /** + * Update an internal multicast group configuration for a given group IP address. + * + * Internal groups are used for admin-scoped IPv6 multicast traffic that + * requires replication infrastructure with external and underlay members. + */ + #[endpoint { + method = PUT, + path = "/multicast/groups/{group_ip}", + }] + async fn multicast_group_update( + rqctx: RequestContext, + path: Path, + group: TypedBody, + ) -> Result, HttpError>; + + /** + * Update an external-only multicast group configuration for a given group IP address. + * + * External-only groups are used for IPv4 and non-admin-scoped IPv6 multicast + * traffic that doesn't require replication infrastructure. + */ + #[endpoint { + method = PUT, + path = "/multicast/external-groups/{group_ip}", + }] + async fn multicast_group_update_external( + rqctx: RequestContext, + path: Path, + group: TypedBody, + ) -> Result, HttpError>; + + /** + * List all multicast groups. + */ + #[endpoint { + method = GET, + path = "/multicast/groups", + }] + async fn multicast_groups_list( + rqctx: RequestContext, + query_params: Query< + PaginationParams, + >, + ) -> Result< + HttpResponseOk>, + HttpError, + >; + + /** + * List all multicast groups with a given tag. + */ + #[endpoint { + method = GET, + path = "/multicast/tags/{tag}", + }] + async fn multicast_groups_list_by_tag( + rqctx: RequestContext, + path: Path, + query_params: Query< + PaginationParams, + >, + ) -> Result< + HttpResponseOk>, + HttpError, + >; + + /** + * Delete all multicast groups (and associated routes) with a given tag. + */ + #[endpoint { + method = DELETE, + path = "/multicast/tags/{tag}", + }] + async fn multicast_reset_by_tag( + rqctx: RequestContext, + path: Path, + ) -> Result; + + /** + * Delete all multicast groups (and associated routes) without a tag. + */ + #[endpoint { + method = DELETE, + path = "/multicast/untagged", + }] + async fn multicast_reset_untagged( + rqctx: RequestContext, + ) -> Result; +} + +/// Parameter used to create a port. +#[derive(Clone, Debug, Deserialize, JsonSchema, Serialize)] +pub struct PortCreateParams { + /// The name of the port. This should be a string like `"3:0"`. + pub name: String, + /// The speed at which to configure the port. + pub speed: PortSpeed, + /// The forward error-correction scheme for the port. + pub fec: PortFec, +} + +/// Represents the free MAC channels on a single physical port. +#[derive(Deserialize, Serialize, JsonSchema, Debug)] +pub struct FreeChannels { + /// The switch port. + pub port_id: PortId, + /// The Tofino connector for this port. + /// + /// This describes the set of electrical connections representing this port + /// object, which are defined by the pinout and board design of the Sidecar. + pub connector: String, + /// The set of available channels (lanes) on this connector. + pub channels: Vec, +} + +/// Represents the mapping of an IP address to a MAC address. +#[derive(Deserialize, Serialize, JsonSchema)] +pub struct ArpEntry { + /// A tag used to associate this entry with a client. + pub tag: String, + /// The IP address for the entry. + pub ip: IpAddr, + /// The MAC address to which `ip` maps. + pub mac: MacAddr, + /// The time the entry was updated + pub update: String, +} + +/// Represents a specific egress port and nexthop target. +#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)] +pub enum RouteTarget { + V4(Ipv4Route), + V6(Ipv6Route), +} + +impl From<&Ipv4Route> for RouteTarget { + fn from(route: &Ipv4Route) -> RouteTarget { + RouteTarget::V4(route.clone()) + } +} + +impl From for RouteTarget { + fn from(route: Ipv4Route) -> RouteTarget { + RouteTarget::V4(route) + } +} + +impl From<&Ipv6Route> for RouteTarget { + fn from(route: &Ipv6Route) -> RouteTarget { + RouteTarget::V6(route.clone()) + } +} + +impl From for RouteTarget { + fn from(route: Ipv6Route) -> RouteTarget { + RouteTarget::V6(route) + } +} + +impl TryFrom for Ipv4Route { + type Error = HttpError; + + fn try_from(target: RouteTarget) -> Result { + match target { + RouteTarget::V4(route) => Ok(route), + _ => Err(dropshot::HttpError::for_bad_request( + None, + "expected an IPv4 route target".to_string(), + )), + } + } +} + +impl TryFrom for Ipv6Route { + type Error = HttpError; + + fn try_from(target: RouteTarget) -> Result { + match target { + RouteTarget::V6(route) => Ok(route), + _ => Err(dropshot::HttpError::for_bad_request( + None, + "expected an IPv6 route target".to_string(), + )), + } + } +} + +/// Represents a new or replacement mapping of a subnet to a single IPv4 +/// RouteTarget nexthop target. +#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)] +pub struct Ipv4RouteUpdate { + /// Traffic destined for any address within the CIDR block is routed using + /// this information. + pub cidr: Ipv4Net, + /// A single Route associated with this CIDR + pub target: Ipv4Route, + /// Should this route replace any existing route? If a route exists and + /// this parameter is false, then the call will fail. + pub replace: bool, +} + +/// Represents a new or replacement mapping of a subnet to a single IPv6 +/// RouteTarget nexthop target. +#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)] +pub struct Ipv6RouteUpdate { + /// Traffic destined for any address within the CIDR block is routed using + /// this information. + pub cidr: Ipv6Net, + /// A single RouteTarget associated with this CIDR + pub target: Ipv6Route, + /// Should this route replace any existing route? If a route exists and + /// this parameter is false, then the call will fail. + pub replace: bool, +} + +/// Represents all mappings of an IPv4 subnet to a its nexthop target(s). +#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)] +pub struct Ipv4Routes { + /// Traffic destined for any address within the CIDR block is routed using + /// this information. + pub cidr: Ipv4Net, + /// All RouteTargets associated with this CIDR + pub targets: Vec, +} + +/// Represents all mappings of an IPv6 subnet to a its nexthop target(s). +#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)] +pub struct Ipv6Routes { + /// Traffic destined for any address within the CIDR block is routed using + /// this information. + pub cidr: Ipv6Net, + /// All RouteTargets associated with this CIDR + pub targets: Vec, +} + +#[derive(Deserialize, Serialize, JsonSchema)] +pub struct Ipv6ArpParam { + pub ip: Ipv6Addr, +} + +/** + * Represents a cursor into a paginated request for the contents of an ARP table + */ +#[derive(Deserialize, Serialize, JsonSchema)] +pub struct ArpToken { + pub ip: IpAddr, +} + +/** + * Represents a potential fault condtion on a link + */ +#[derive(Deserialize, Serialize, JsonSchema)] +pub struct FaultCondition { + pub fault: Option, +} + +#[derive(Deserialize, Serialize, JsonSchema)] +pub struct Ipv4ArpParam { + pub ip: Ipv4Addr, +} + +/** + * Represents a cursor into a paginated request for the contents of an + * Ipv4-indexed table. + */ +#[derive(Deserialize, Serialize, JsonSchema)] +pub struct Ipv4Token { + pub ip: Ipv4Addr, +} + +/** + * Represents a cursor into a paginated request for the contents of an + * IPv6-indexed table. + */ +#[derive(Deserialize, Serialize, JsonSchema)] +pub struct Ipv6Token { + pub ip: Ipv6Addr, +} + +#[derive(Deserialize, Serialize, JsonSchema)] +pub struct RoutePathV4 { + /// The IPv4 subnet in CIDR notation whose route entry is returned. + pub cidr: Ipv4Net, +} + +/// Represents a single subnet->target route entry +#[derive(Deserialize, Serialize, JsonSchema)] +pub struct RouteTargetIpv4Path { + /// The subnet being routed + pub cidr: Ipv4Net, + /// The switch port to which packets should be sent + pub port_id: PortId, + /// The link to which packets should be sent + pub link_id: LinkId, + /// The next hop in the IPv4 route + pub tgt_ip: Ipv4Addr, +} + +#[derive(Deserialize, Serialize, JsonSchema)] +pub struct RoutePathV6 { + /// The IPv6 subnet in CIDR notation whose route entry is returned. + pub cidr: Ipv6Net, +} + +/// Represents a single subnet->target route entry +#[derive(Deserialize, Serialize, JsonSchema)] +pub struct RouteTargetIpv6Path { + /// The subnet being routed + pub cidr: Ipv6Net, + /// The switch port to which packets should be sent + pub port_id: PortId, + /// The link to which packets should be sent + pub link_id: LinkId, + /// The next hop in the IPv4 route + pub tgt_ip: Ipv6Addr, +} + +/** + * Represents a cursor into a paginated request for the contents of the + * subnet routing table. Because we don't (yet) support filtering or arbitrary + * sorting, it is sufficient to track the last mac address reported. + */ +#[derive(Deserialize, Serialize, JsonSchema)] +pub struct Ipv4RouteToken { + pub cidr: Ipv4Net, +} + +#[derive(Deserialize, Serialize, JsonSchema)] +pub struct Ipv6RouteToken { + pub cidr: Ipv6Net, +} + +#[derive(Deserialize, Serialize, JsonSchema)] +pub struct PortIpv4Path { + pub port: String, + pub ipv4: Ipv4Addr, +} + +#[derive(Deserialize, Serialize, JsonSchema)] +pub struct PortIpv6Path { + pub port: String, + pub ipv6: Ipv6Addr, +} + +#[derive(Deserialize, Serialize, JsonSchema)] +pub struct LoopbackIpv4Path { + pub ipv4: Ipv4Addr, +} + +#[derive(Deserialize, Serialize, JsonSchema)] +pub struct LoopbackIpv6Path { + pub ipv6: Ipv6Addr, +} + +#[derive(Deserialize, Serialize, JsonSchema)] +pub struct NatIpv6Path { + pub ipv6: Ipv6Addr, +} + +#[derive(Deserialize, Serialize, JsonSchema)] +pub struct NatIpv6PortPath { + pub ipv6: Ipv6Addr, + pub low: u16, +} + +#[derive(Deserialize, Serialize, JsonSchema)] +pub struct NatIpv6RangePath { + pub ipv6: Ipv6Addr, + pub low: u16, + pub high: u16, +} + +#[derive(Deserialize, Serialize, JsonSchema)] +pub struct NatIpv4Path { + pub ipv4: Ipv4Addr, +} + +#[derive(Deserialize, Serialize, JsonSchema)] +pub struct NatIpv4PortPath { + pub ipv4: Ipv4Addr, + pub low: u16, +} + +#[derive(Deserialize, Serialize, JsonSchema)] +pub struct NatIpv4RangePath { + pub ipv4: Ipv4Addr, + pub low: u16, + pub high: u16, +} + +/** + * Represents a cursor into a paginated request for all NAT data. + */ +#[derive(Deserialize, Serialize, JsonSchema)] +pub struct NatToken { + pub port: u16, +} + +/** + * Represents a cursor into a paginated request for all port data. Because we + * don't (yet) support filtering or arbitrary sorting, it is sufficient to + * track the last port returned. + */ +#[derive(Deserialize, Serialize, JsonSchema)] +pub struct PortToken { + pub port: u16, +} + +#[derive(Deserialize, Serialize, JsonSchema)] +pub struct PortIdPathParams { + /// The switch port on which to operate. + pub port_id: PortId, +} + +#[derive(Deserialize, Serialize, JsonSchema)] +pub struct PortSettingsTag { + /// Restrict operations on this port to the provided tag. + pub tag: Option, +} + +/// Identifies a logical link on a physical port. +#[derive(Deserialize, Serialize, JsonSchema)] +pub struct LinkPath { + /// The switch port on which to operate. + pub port_id: PortId, + /// The link in the switch port on which to operate. + pub link_id: LinkId, +} + +#[derive(Deserialize, Serialize, JsonSchema)] +pub struct LinkIpv4Path { + /// The switch port on which to operate. + pub port_id: PortId, + /// The link in the switch port on which to operate. + pub link_id: LinkId, + /// The IPv4 address on which to operate. + pub address: Ipv4Addr, +} + +#[derive(Deserialize, Serialize, JsonSchema)] +pub struct LinkIpv6Path { + /// The switch port on which to operate. + pub port_id: PortId, + /// The link in the switch port on which to operate. + pub link_id: LinkId, + /// The IPv6 address on which to operate. + pub address: Ipv6Addr, +} + +/// Parameters used to create a link on a switch port. +#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] +pub struct LinkCreate { + /// The first lane of the port to use for the new link + pub lane: Option, + /// The requested speed of the link. + pub speed: PortSpeed, + /// The requested forward-error correction method. If this is None, the + /// standard FEC for the underlying media will be applied if it can be + /// determined. + pub fec: Option, + /// Whether the link is configured to autonegotiate with its peer during + /// link training. + /// + /// This is generally only true for backplane links, and defaults to + /// `false`. + #[serde(default)] + pub autoneg: bool, + /// Whether the link is configured in KR mode, an electrical specification + /// generally only true for backplane link. + /// + /// This defaults to `false`. + #[serde(default)] + pub kr: bool, + + /// Transceiver equalization adjustment parameters. + /// This defaults to `None`. + #[serde(default)] + pub tx_eq: Option, +} + +#[derive(Clone, Debug, Deserialize, JsonSchema)] +pub struct LinkFilter { + /// Filter links to those whose name contains the provided string. + /// + /// If not provided, then all links are returned. + pub filter: Option, +} + +#[derive(Deserialize, Serialize, JsonSchema)] +pub struct TagPath { + pub tag: String, +} + +/// Detailed build information about `dpd`. +#[derive(Clone, Debug, Deserialize, JsonSchema, Serialize)] +pub struct BuildInfo { + pub version: String, + pub git_sha: String, + pub git_commit_timestamp: String, + pub git_branch: String, + pub rustc_semver: String, + pub rustc_channel: String, + pub rustc_host_triple: String, + pub rustc_commit_sha: String, + pub cargo_triple: String, + pub debug: bool, + pub opt_level: u8, + pub sde_commit_sha: String, +} + +/// A port settings transaction object. When posted to the +/// `/port-settings/{port_id}` API endpoint, these settings will be applied +/// holistically, and to the extent possible atomically to a given port. +#[derive(Default, Clone, Debug, Deserialize, JsonSchema, Serialize)] +pub struct PortSettings { + /// The link settings to apply to the port on a per-link basis. Any links + /// not in this map that are resident on the switch port will be removed. + /// Any links that are in this map that are not resident on the switch port + /// will be added. Any links that are resident on the switch port and in + /// this map, and are different, will be modified. Links are indexed by + /// spatial index within the port. + pub links: HashMap, +} + +/// An object with link settings used in concert with [`PortSettings`]. +#[derive(Clone, Debug, Deserialize, JsonSchema, Serialize)] +pub struct LinkSettings { + pub params: LinkCreate, + pub addrs: HashSet, +} + +/// An object with IPv4 route settings used in concert with [`PortSettings`]. +#[derive(Clone, Debug, Deserialize, JsonSchema, Serialize)] +pub struct RouteSettingsV4 { + pub link_id: u8, + pub nexthop: Ipv4Addr, +} + +/// An object with IPV6 route settings used in concert with [`PortSettings`]. +#[derive(Clone, Debug, Deserialize, JsonSchema, Serialize)] +pub struct RouteSettingsV6 { + pub link_id: u8, + pub nexthop: Ipv6Addr, +} + +#[derive(Deserialize, Serialize, JsonSchema)] +pub struct CounterSync { + /// Force a sync of the counters from the ASIC to memory, even if the + /// default refresh timeout hasn't been reached. + pub force_sync: bool, +} + +#[derive(Deserialize, Serialize, JsonSchema)] +pub struct TableParam { + pub table: String, +} + +#[derive(Deserialize, Serialize, JsonSchema)] +pub struct CounterPath { + pub counter: String, +} + +/// Used to identify a multicast group by IP address, the main +/// identifier for a multicast group. +#[derive(Deserialize, Serialize, JsonSchema)] +pub struct MulticastGroupIpParam { + pub group_ip: IpAddr, +} + +/// Used to identify a multicast group by ID. +/// +/// If not provided, it will return all multicast groups. +#[derive(Deserialize, Serialize, JsonSchema)] +pub struct MulticastGroupIdParam { + pub group_id: Option, +} diff --git a/dpd-types/Cargo.toml b/dpd-types/Cargo.toml new file mode 100644 index 0000000..8dce945 --- /dev/null +++ b/dpd-types/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "dpd-types" +version = "0.1.0" +edition = "2024" + +[dependencies] +aal.workspace = true +chrono.workspace = true +common.workspace = true +omicron-common.workspace = true +oxnet.workspace = true +schemars.workspace = true +serde.workspace = true +thiserror.workspace = true +transceiver-controller.workspace = true +uuid.workspace = true diff --git a/dpd-types/src/fault.rs b/dpd-types/src/fault.rs new file mode 100644 index 0000000..4026715 --- /dev/null +++ b/dpd-types/src/fault.rs @@ -0,0 +1,18 @@ +// 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/ +// +// Copyright 2025 Oxide Computer Company + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +/// A Fault represents a specific kind of failure, and carries some additional +/// context. Currently Faults are only used to describe Link failures, but +/// there is no reason they couldn't be used elsewhere. +#[derive(Clone, Debug, PartialEq, Deserialize, JsonSchema, Serialize)] +pub enum Fault { + LinkFlap(String), + Autoneg(String), + Injected(String), +} diff --git a/dpd-types/src/lib.rs b/dpd-types/src/lib.rs new file mode 100644 index 0000000..79e6809 --- /dev/null +++ b/dpd-types/src/lib.rs @@ -0,0 +1,16 @@ +// 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/ +// +// Copyright 2025 Oxide Computer Company + +pub mod fault; +pub mod link; +pub mod mcast; +pub mod oxstats; +pub mod port_map; +pub mod route; +pub mod switch_identifiers; +pub mod switch_port; +pub mod transceivers; +pub mod views; diff --git a/dpd-types/src/link.rs b/dpd-types/src/link.rs new file mode 100644 index 0000000..f2201bb --- /dev/null +++ b/dpd-types/src/link.rs @@ -0,0 +1,147 @@ +// 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/ +// +// Copyright 2025 Oxide Computer Company + +use std::fmt; + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::fault::Fault; + +/// An identifier for a link within a switch port. +/// +/// A switch port identified by a [`PortId`] may have multiple links within it, +/// each identified by a `LinkId`. These are unique within a switch port only. +#[derive( + Clone, + Copy, + Debug, + Deserialize, + Eq, + Hash, + JsonSchema, + Ord, + PartialEq, + PartialOrd, + Serialize, +)] +pub struct LinkId(pub u8); + +impl From for u8 { + fn from(l: LinkId) -> Self { + l.0 + } +} + +impl From for u16 { + fn from(l: LinkId) -> Self { + l.0 as u16 + } +} + +impl From for LinkId { + fn from(x: u8) -> Self { + Self(x) + } +} + +impl fmt::Display for LinkId { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +/// The state of a data link with a peer. +#[derive(Clone, Deserialize, JsonSchema, PartialEq, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum LinkState { + /// An error was encountered while trying to configure the link in the + /// switch hardware. + ConfigError(String), + /// The link is up. + Up, + /// The link is down. + Down, + /// The Link is offline due to a fault + Faulted(Fault), + /// The link's state is not known. + Unknown, +} + +impl LinkState { + /// A shortcut to tell whether a LinkState is Faulted or not, allowing for + /// cleaner code in the callers. + pub fn is_fault(&self) -> bool { + matches!(self, LinkState::Faulted(_)) + } + + /// If the link is in a faulted state, return the Fault. If not, return + /// None. + pub fn get_fault(&self) -> Option { + match self { + LinkState::Faulted(f) => Some(f.clone()), + _ => None, + } + } +} + +impl std::fmt::Display for LinkState { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + LinkState::Up => write!(f, "Up"), + LinkState::Down => write!(f, "Down"), + LinkState::ConfigError(_) => write!(f, "ConfigError"), + LinkState::Faulted(_) => write!(f, "Faulted"), + LinkState::Unknown => write!(f, "Unknown"), + } + } +} + +impl std::fmt::Debug for LinkState { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + LinkState::Up => write!(f, "Up"), + LinkState::Down => write!(f, "Down"), + LinkState::ConfigError(detail) => { + write!(f, "ConfigError - {:?}", detail) + } + LinkState::Faulted(reason) => write!(f, "Faulted - {:?}", reason), + LinkState::Unknown => write!(f, "Unknown"), + } + } +} + +/// Reports how many times a link has transitioned from Down to Up. +#[derive(Clone, Deserialize, JsonSchema, PartialEq, Serialize)] +pub struct LinkUpCounter { + /// Link being reported + pub link_path: String, + /// LinkUp transitions since the link was last enabled + pub current: u32, + /// LinkUp transitions since the link was created + pub total: u32, +} + +/// Reports how many times a given autoneg/link-training state has been entered +#[derive(Clone, Deserialize, JsonSchema, PartialEq, Serialize)] +pub struct LinkFsmCounter { + /// FSM state being counted + pub state_name: String, + /// Times entered since the link was last enabled + pub current: u32, + /// Times entered since the link was created + pub total: u32, +} + +/// Reports all the autoneg/link-training states a link has transitioned into. +#[derive(Clone, Deserialize, JsonSchema, PartialEq, Serialize)] +pub struct LinkFsmCounters { + /// Link being reported + pub link_path: String, + /// All the states this link has entered, along with counts of how many + /// times each state was entered. + pub counters: Vec, +} diff --git a/dpd-types/src/mcast.rs b/dpd-types/src/mcast.rs new file mode 100644 index 0000000..585e587 --- /dev/null +++ b/dpd-types/src/mcast.rs @@ -0,0 +1,128 @@ +// 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/ +// +// Copyright 2025 Oxide Computer Company + +use std::{ + fmt, + net::{IpAddr, Ipv6Addr}, +}; + +use common::{nat::NatTarget, ports::PortId}; +use oxnet::Ipv4Net; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::link::LinkId; + +/// Type alias for multicast group IDs. +pub type MulticastGroupId = u16; + +/// A multicast group configuration for POST requests for external (to the rack) +/// groups. +#[derive(Debug, Deserialize, Serialize, JsonSchema)] +pub struct MulticastGroupCreateExternalEntry { + pub group_ip: IpAddr, + pub tag: Option, + pub nat_target: NatTarget, + pub vlan_id: Option, + pub sources: Option>, +} + +/// Source filter match key for multicast traffic. +#[derive( + Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize, JsonSchema, +)] +pub enum IpSrc { + /// Exact match for the source IP address. + Exact(IpAddr), + /// Subnet match for the source IP address. + Subnet(Ipv4Net), +} + +impl fmt::Display for IpSrc { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + IpSrc::Exact(ip) => write!(f, "{}", ip), + IpSrc::Subnet(subnet) => write!(f, "{}", subnet), + } + } +} + +/// A multicast group configuration for POST requests for internal (to the rack) +/// groups. +#[derive(Debug, Deserialize, Serialize, JsonSchema)] +pub struct MulticastGroupCreateEntry { + pub group_ip: Ipv6Addr, + pub tag: Option, + pub sources: Option>, + pub members: Vec, +} + +/// Represents a multicast replication entry for PUT requests for internal +/// (to the rack) groups. +#[derive(Debug, Deserialize, Serialize, JsonSchema)] +pub struct MulticastGroupUpdateEntry { + pub tag: Option, + pub sources: Option>, + pub members: Vec, +} + +/// A multicast group update entry for PUT requests for external (to the rack) +/// groups. +#[derive(Debug, Deserialize, Serialize, JsonSchema)] +pub struct MulticastGroupUpdateExternalEntry { + pub tag: Option, + pub nat_target: NatTarget, + pub vlan_id: Option, + pub sources: Option>, +} + +/// Response structure for multicast group operations. +#[derive(Debug, Deserialize, Serialize, JsonSchema)] +pub struct MulticastGroupResponse { + pub group_ip: IpAddr, + pub external_group_id: Option, + pub underlay_group_id: Option, + pub tag: Option, + pub int_fwding: InternalForwarding, + pub ext_fwding: ExternalForwarding, + pub sources: Option>, + pub members: Vec, +} + +/// Represents the NAT target for multicast traffic for internal/underlay +/// forwarding. +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, JsonSchema)] +pub struct InternalForwarding { + pub nat_target: Option, +} + +/// Represents the forwarding configuration for external multicast traffic. +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, JsonSchema)] +pub struct ExternalForwarding { + pub vlan_id: Option, +} + +/// Represents a member of a multicast group. +#[derive( + Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize, JsonSchema, +)] +pub struct MulticastGroupMember { + pub port_id: PortId, + pub link_id: LinkId, + pub direction: Direction, +} + +/// Direction a multicast group member is reached by. +/// +/// `External` group members must have any packet encapsulation removed +/// before packet delivery. +#[derive( + Copy, Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize, JsonSchema, +)] +pub enum Direction { + Underlay, + External, +} diff --git a/dpd-types/src/oxstats.rs b/dpd-types/src/oxstats.rs new file mode 100644 index 0000000..e898a99 --- /dev/null +++ b/dpd-types/src/oxstats.rs @@ -0,0 +1,39 @@ +// 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/ +// +// Copyright 2025 Oxide Computer Company + +use std::net::Ipv6Addr; + +use chrono::{DateTime, Utc}; +use omicron_common::api::internal::shared::SledIdentifiers; +use schemars::JsonSchema; +use serde::Serialize; + +use crate::switch_identifiers::SwitchIdentifiers; + +/// Data associated with this dpd instance as an oximeter producer +#[derive(Clone, Debug, JsonSchema, Serialize)] +pub struct OximeterMetadata { + /// Configuration of the server and our timeseries. + #[serde(flatten)] + pub config: OximeterConfig, + /// When we registered with nexus + // + // NOTE: This is really the time we created the producer server, not when we + // registered with Nexus. Registration happens in the background and + // continually renews. + pub registered_at: Option>, +} + +/// Configuration for the oximeter producer server and our timeseries. +#[derive(Clone, Debug, JsonSchema, Serialize)] +pub struct OximeterConfig { + /// IP address of the producer server. + pub listen_address: Ipv6Addr, + /// Identifiers for the Scrimlet we're running on. + pub sled_identifiers: SledIdentifiers, + /// Identifiers for the Sidecar we're managing. + pub switch_identifiers: SwitchIdentifiers, +} diff --git a/dpd-types/src/port_map.rs b/dpd-types/src/port_map.rs new file mode 100644 index 0000000..3d2dac3 --- /dev/null +++ b/dpd-types/src/port_map.rs @@ -0,0 +1,177 @@ +// 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/ +// +// Copyright 2025 Oxide Computer Company + +use common::ports::RearPort; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +/// The Sidecar chassis connector mating the backplane and internal cabling. +/// +/// This describes the "group" of backplane links that all terminate in one +/// connector on the Sidecar itself. This is the connection point between a +/// cable on the backplane itself and the Sidecar chassis. +#[derive(Clone, Copy, Debug, Deserialize, JsonSchema, PartialEq, Serialize)] +pub struct SidecarConnector(u8); + +impl From for u8 { + fn from(g: SidecarConnector) -> u8 { + g.as_u8() + } +} + +impl TryFrom for SidecarConnector { + type Error = Error; + + fn try_from(x: u8) -> Result { + if x > 7 { + return Err(Error::SidecarConnector(x)); + } + Ok(Self(x)) + } +} + +impl SidecarConnector { + /// Create a new backplane group. + pub fn new(x: u8) -> Result { + Self::try_from(x) + } + + /// Return the index of this group as an integer. + pub const fn as_u8(&self) -> u8 { + self.0 + } +} + +/// A single point-to-point connection on the cabled backplane. +/// +/// This describes a single link from the Sidecar switch to a cubby, via the +/// cabled backplane. It ultimately maps the Tofino ASIC pins to the cubby at +/// which that link terminates. This path follows the Sidecar internal cable; +/// the Sidecar chassis connector; and the backplane cable itself. This is used +/// to map the Tofino driver's "connector" number (an index in its possible +/// pinouts) through the backplane to our logical cubby numbering. +#[derive(Clone, Copy, Debug, Deserialize, JsonSchema, PartialEq, Serialize)] +pub struct BackplaneLink { + // The internal Tofino driver connector number. + pub tofino_connector: u8, + // The leg label on the Sidecar-internal cable. + pub sidecar_leg: SidecarCableLeg, + // The Sidecar chassis connector. + pub sidecar_connector: SidecarConnector, + // The leg label on the cabled backplane. + pub backplane_leg: BackplaneCableLeg, + // The cubby at which the cable terminates. + pub cubby: u8, +} + +impl From for BackplaneLink { + fn from(p: RearPort) -> Self { + Self::from_cubby(p.as_u8()).unwrap() + } +} + +/// The leg of the backplane cable. +/// +/// This describes the leg on the actual backplane cable that connects the +/// Sidecar chassis connector to a cubby endpoint. +// NOTE: This is the connector on the cubby chassis end of the part +// HDR-222627-xx-EBCM. The `xx` describes the length of the cable. +#[derive(Clone, Copy, Debug, Deserialize, JsonSchema, PartialEq, Serialize)] +pub enum BackplaneCableLeg { + A, + B, + C, + D, +} + +// Helper macro to make a backplane map entry. +macro_rules! bp_entry { + ( + $connector:literal, + $sidecar_leg:expr, + $sidecar_connector:literal, + $backplane_leg:expr, + $cubby:literal + ) => { + BackplaneLink { + tofino_connector: $connector, + sidecar_leg: $sidecar_leg, + sidecar_connector: SidecarConnector($sidecar_connector), + backplane_leg: $backplane_leg, + cubby: $cubby, + } + }; +} + +/// The leg of the Sidecar-internal cable. +/// +/// This describes the leg on the cabling that connects the pins on the Tofino +/// ASIC to the Sidecar chassis connector. +// NOTE: This is the connector on the Sidecar main board side of the part +// HDR-222623-01-EBCF. +#[derive(Clone, Copy, Debug, Deserialize, JsonSchema, PartialEq, Serialize)] +pub enum SidecarCableLeg { + A, + C, +} + +pub const SIDECAR_REV_AB_BACKPLANE_MAP: [BackplaneLink; 32] = [ + bp_entry!(1, SidecarCableLeg::C, 0, BackplaneCableLeg::C, 29), + bp_entry!(2, SidecarCableLeg::C, 0, BackplaneCableLeg::D, 31), + bp_entry!(3, SidecarCableLeg::A, 0, BackplaneCableLeg::A, 25), + bp_entry!(4, SidecarCableLeg::A, 0, BackplaneCableLeg::B, 27), + bp_entry!(5, SidecarCableLeg::C, 1, BackplaneCableLeg::C, 21), + bp_entry!(6, SidecarCableLeg::C, 1, BackplaneCableLeg::D, 23), + bp_entry!(7, SidecarCableLeg::A, 1, BackplaneCableLeg::A, 17), + bp_entry!(8, SidecarCableLeg::A, 1, BackplaneCableLeg::B, 19), + bp_entry!(9, SidecarCableLeg::A, 2, BackplaneCableLeg::B, 11), + bp_entry!(10, SidecarCableLeg::A, 2, BackplaneCableLeg::A, 9), + bp_entry!(11, SidecarCableLeg::C, 2, BackplaneCableLeg::D, 15), + bp_entry!(12, SidecarCableLeg::C, 2, BackplaneCableLeg::C, 13), + bp_entry!(13, SidecarCableLeg::A, 3, BackplaneCableLeg::B, 3), + bp_entry!(14, SidecarCableLeg::A, 3, BackplaneCableLeg::A, 1), + bp_entry!(15, SidecarCableLeg::C, 3, BackplaneCableLeg::D, 7), + bp_entry!(16, SidecarCableLeg::C, 3, BackplaneCableLeg::C, 5), + bp_entry!(17, SidecarCableLeg::C, 4, BackplaneCableLeg::C, 28), + bp_entry!(18, SidecarCableLeg::C, 4, BackplaneCableLeg::D, 30), + bp_entry!(19, SidecarCableLeg::A, 4, BackplaneCableLeg::A, 24), + bp_entry!(20, SidecarCableLeg::A, 4, BackplaneCableLeg::B, 26), + bp_entry!(21, SidecarCableLeg::C, 5, BackplaneCableLeg::C, 20), + bp_entry!(22, SidecarCableLeg::C, 5, BackplaneCableLeg::D, 22), + bp_entry!(23, SidecarCableLeg::A, 5, BackplaneCableLeg::A, 16), + bp_entry!(24, SidecarCableLeg::A, 5, BackplaneCableLeg::B, 18), + bp_entry!(25, SidecarCableLeg::A, 6, BackplaneCableLeg::B, 10), + bp_entry!(26, SidecarCableLeg::A, 6, BackplaneCableLeg::A, 8), + bp_entry!(27, SidecarCableLeg::C, 6, BackplaneCableLeg::D, 14), + bp_entry!(28, SidecarCableLeg::C, 6, BackplaneCableLeg::C, 12), + bp_entry!(29, SidecarCableLeg::A, 7, BackplaneCableLeg::B, 2), + bp_entry!(30, SidecarCableLeg::A, 7, BackplaneCableLeg::A, 0), + bp_entry!(31, SidecarCableLeg::C, 7, BackplaneCableLeg::D, 6), + bp_entry!(32, SidecarCableLeg::C, 7, BackplaneCableLeg::C, 4), +]; + +impl BackplaneLink { + /// Construct a link from the cubby number. + pub fn from_cubby(cubby: u8) -> Result { + SIDECAR_REV_AB_BACKPLANE_MAP + .iter() + .find(|entry| entry.cubby == cubby) + .copied() + .ok_or(Error::Cubby(cubby)) + } +} + +#[derive(Clone, Debug, thiserror::Error)] +pub enum Error { + #[error("Invalid backplane group {0}, must be in [0, 7]")] + SidecarConnector(u8), + + #[error("Invalid cubby {0}, must be in [0, 31]")] + Cubby(u8), + + #[error("Invalid SoftNPU revision '{found}', expected '{expected}'")] + SoftNpuRevision { expected: String, found: String }, +} diff --git a/dpd-types/src/route.rs b/dpd-types/src/route.rs new file mode 100644 index 0000000..003daae --- /dev/null +++ b/dpd-types/src/route.rs @@ -0,0 +1,115 @@ +// 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/ +// +// Copyright 2025 Oxide Computer Company + +use std::{ + fmt, + net::{Ipv4Addr, Ipv6Addr}, +}; + +use common::ports::PortId; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::link::LinkId; + +/// A route for an IPv4 subnet. +#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)] +pub struct Ipv4Route { + // The client-specific tag for this route. + pub tag: String, + // The switch port out which routed traffic is sent. + pub port_id: PortId, + // The link out which routed traffic is sent. + pub link_id: LinkId, + // Route traffic matching the subnet via this IP. + pub tgt_ip: Ipv4Addr, + // Tag traffic on this route with this vlan ID. + pub vlan_id: Option, +} + +// We implement PartialEq for Ipv4Route because we want to exclude the tag and +// vlan_id from any comparisons. We do this because the tag is a comment +// identifying the originator rather than a semantically meaningful part of the +// route. The vlan_id is used to modify the traffic on a specific route, rather +// then being part of the route itself. +impl PartialEq for Ipv4Route { + fn eq(&self, other: &Self) -> bool { + self.port_id == other.port_id + && self.link_id == other.link_id + && self.tgt_ip == other.tgt_ip + } +} + +// See the comment above PartialEq to understand why we implement Hash rather +// then Deriving it. +impl std::hash::Hash for Ipv4Route { + fn hash(&self, state: &mut H) + where + H: std::hash::Hasher, + { + self.port_id.hash(state); + self.link_id.hash(state); + self.tgt_ip.hash(state); + } +} + +impl fmt::Display for Ipv4Route { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "port: {} link: {} gw: {} vlan: {:?}", + self.port_id, self.link_id, self.tgt_ip, self.vlan_id + )?; + Ok(()) + } +} + +/// A route for an IPv6 subnet. +#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)] +pub struct Ipv6Route { + // The client-specific tag for this route. + pub tag: String, + // The switch port out which routed traffic is sent. + pub port_id: PortId, + // The link out which routed traffic is sent. + pub link_id: LinkId, + // Route traffic matching the subnet to this IP. + pub tgt_ip: Ipv6Addr, + // Tag traffic on this route with this vlan ID. + pub vlan_id: Option, +} + +// See the comment above the PartialEq for IPv4Route +impl PartialEq for Ipv6Route { + fn eq(&self, other: &Self) -> bool { + self.port_id == other.port_id + && self.link_id == other.link_id + && self.tgt_ip == other.tgt_ip + } +} + +// See the comment above PartialEq for IPv4Route +impl std::hash::Hash for Ipv6Route { + fn hash(&self, state: &mut H) + where + H: std::hash::Hasher, + { + self.port_id.hash(state); + self.link_id.hash(state); + self.tgt_ip.hash(state); + } +} + +impl fmt::Display for Ipv6Route { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "port: {} link: {} gw: {} vlan: {:?}", + self.port_id, self.link_id, self.tgt_ip, self.vlan_id + )?; + Ok(()) + } +} diff --git a/dpd-types/src/switch_identifiers.rs b/dpd-types/src/switch_identifiers.rs new file mode 100644 index 0000000..c4fa7f9 --- /dev/null +++ b/dpd-types/src/switch_identifiers.rs @@ -0,0 +1,37 @@ +// 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/ +// +// Copyright 2025 Oxide Computer Company + +use schemars::JsonSchema; +use serde::Serialize; +use uuid::Uuid; + +/// Identifiers for a switch. +#[derive(Clone, Debug, JsonSchema, Serialize)] +pub struct SwitchIdentifiers { + /// Unique identifier for the chip. + pub sidecar_id: Uuid, + /// Asic backend (compiler target) responsible for these identifiers. + pub asic_backend: String, + /// Fabrication plant identifier. + pub fab: Option, + /// Lot identifier. + pub lot: Option, + /// Wafer number within the lot. + pub wafer: Option, + /// The wafer location as (x, y) coordinates on the wafer, represented as + /// an array due to the lack of tuple support in OpenAPI. + pub wafer_loc: Option<[i16; 2]>, + /// The model number of the switch being managed. + pub model: String, + /// The revision number of the switch being managed. + pub revision: u32, + /// The serial number of the switch being managed. + pub serial: String, + /// The slot number of the switch being managed. + /// + /// MGS uses u16 for this internally. + pub slot: u16, +} diff --git a/dpd-types/src/switch_port.rs b/dpd-types/src/switch_port.rs new file mode 100644 index 0000000..09c7ab4 --- /dev/null +++ b/dpd-types/src/switch_port.rs @@ -0,0 +1,92 @@ +// 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/ +// +// Copyright 2025 Oxide Computer Company + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use transceiver_controller::message::LedState; + +/// How a switch port is managed. +/// +/// The free-side devices in QSFP ports are complex devices, whose operation +/// usually involves coordinated steps through one or more state machines. For +/// example, when bringing up an optical link, a signal from the peer link must +/// be detected; then a signal recovered; equalizer gains set; etc. In +/// `Automatic` mode, all these kinds of steps are managed autonomously by +/// switch driver software. In `Manual` mode, none of these will occur -- a +/// switch port will only change in response to explicit requests from the +/// operator or Oxide control plane. +// +// NOTE: This is the parameter which marks a switch port _visible_ to the BF +// SDE. `Manual` means under our control, `Automatic` means visible to the SDE +// and under its control. +#[derive( + Clone, + Copy, + Debug, + Deserialize, + Eq, + Hash, + JsonSchema, + Ord, + PartialEq, + PartialOrd, + Serialize, +)] +#[serde(rename_all = "snake_case")] +pub enum ManagementMode { + /// A port is managed manually, by either the Oxide control plane or an + /// operator. + Manual, + /// A port is managed automatically by the switch software. + Automatic, +} + +/// The policy by which a port's LED is controlled. +#[derive( + Clone, + Copy, + Debug, + Deserialize, + Eq, + Hash, + JsonSchema, + Ord, + PartialEq, + PartialOrd, + Serialize, +)] +#[serde(rename_all = "snake_case")] +pub enum LedPolicy { + /// The default policy is for the LED to reflect the port's state itself. + /// + /// If the port is operating normally, the LED will be solid on. Without a + /// transceiver, the LED will be solid off. A blinking LED is used to + /// indicate an unsupported module or other failure on that port. + Automatic, + /// The LED is explicitly overridden by client requests. + Override, +} + +/// Information about a QSFP port's LED. +#[derive( + Clone, + Copy, + Debug, + Deserialize, + Eq, + Hash, + JsonSchema, + Ord, + PartialEq, + PartialOrd, + Serialize, +)] +pub struct Led { + /// The policy by which the LED is controlled. + pub policy: LedPolicy, + /// The state of the LED. + pub state: LedState, +} diff --git a/dpd-types/src/transceivers.rs b/dpd-types/src/transceivers.rs new file mode 100644 index 0000000..0906829 --- /dev/null +++ b/dpd-types/src/transceivers.rs @@ -0,0 +1,130 @@ +// 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/ +// +// Copyright 2025 Oxide Computer Company + +use std::time::Instant; + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use transceiver_controller::{PowerMode, VendorInfo}; + +use crate::switch_port::ManagementMode; + +/// A QSFP switch port. +/// +/// This includes the hardware controls and information relevant to QSFP ports +/// specifically. For example, these ports are on the front IO panel of the +/// switch, and have LEDs used for status and attention. This includes the state +/// and controls for those LEDs. It also includes information about the +/// free-side QSFP module, should one be plugged in. +#[derive(Clone, Debug, JsonSchema, Serialize)] +pub struct QsfpDevice { + /// Details about a transceiver module inserted into the switch port. + /// + /// If there is no transceiver at all, this will be `None`. + pub transceiver: Option, + /// How the QSFP device is managed. + /// + /// See `ManagementMode` for details. + pub management_mode: ManagementMode, +} + +impl Default for QsfpDevice { + fn default() -> Self { + Self { + transceiver: None, + management_mode: ManagementMode::Automatic, + } + } +} + +/// The cause of a fault on a transceiver. +#[derive(Clone, Copy, Debug, Deserialize, JsonSchema, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum FaultReason { + /// An error occurred accessing the transceiver. + Failed, + /// Power was enabled, but did not come up in the requisite time. + PowerTimeout, + /// Power was enabled and later lost. + PowerLost, + /// The service processor disabled the transceiver. + /// + /// The SP is responsible for monitoring the thermal data from the + /// transceivers, and controlling the fans to compensate. If a module's + /// thermal data cannot be read, the SP may completely disable the + /// transceiver to ensure it cannot overheat the Sidecar. + DisabledBySp, +} + +/// The state of a transceiver in a QSFP switch port. +#[derive(Clone, Debug, JsonSchema, Serialize)] +#[serde(rename_all = "snake_case", tag = "state", content = "info")] +pub enum Transceiver { + /// The transceiver could not be managed due to a power fault. + Faulted(FaultReason), + /// A transceiver was present, but unsupported and automatically disabled. + Unsupported, + /// A transceiver is present and supported. + Supported(TransceiverInfo), +} + +/// Information about a QSFP transceiver. +/// +/// This stores the most relevant information about a transceiver module, such +/// as vendor info or power. Each field may be missing, indicating it could not +/// be determined. +#[derive(Clone, Debug, JsonSchema, Serialize)] +pub struct TransceiverInfo { + /// Vendor and part identifying information. + /// + /// The information will not be populated if it could not be read. + pub vendor_info: Option, + /// True if the module is currently in reset. + pub in_reset: Option, + /// True if there is a pending interrupt on the module. + pub interrupt_pending: Option, + /// The power mode of the transceiver. + pub power_mode: Option, + /// The electrical mode of the transceiver. + /// + /// See [`ElectricalMode`] for details. + pub electrical_mode: ElectricalMode, + // The instant at which we first saw this transceiver. + // + // This is only used to support initially blinking the transceiver to + // acknowledge insertion. + #[serde(skip)] + pub first_seen: Instant, +} + +impl Default for TransceiverInfo { + fn default() -> Self { + Self { + vendor_info: None, + in_reset: None, + interrupt_pending: None, + power_mode: None, + electrical_mode: ElectricalMode::Single, + first_seen: Instant::now(), + } + } +} + +/// The electrical mode of a QSFP-capable port. +/// +/// QSFP ports can be broken out into one of several different electrical +/// configurations or modes. This describes how the transmit/receive lanes are +/// grouped into a single, logical link. +/// +/// Note that the electrical mode may only be changed if there are no links +/// within the port, _and_ if the inserted QSFP module actually supports this +/// mode. +#[derive(Clone, Copy, Debug, Default, Deserialize, JsonSchema, Serialize)] +pub enum ElectricalMode { + /// All transmit/receive lanes are used for a single link. + #[default] + Single, +} diff --git a/dpd/src/views.rs b/dpd-types/src/views.rs similarity index 74% rename from dpd/src/views.rs rename to dpd-types/src/views.rs index 142372b..fdd00cb 100644 --- a/dpd/src/views.rs +++ b/dpd-types/src/views.rs @@ -6,25 +6,20 @@ //! Public API view types, exposing the internal Dendrite data in a manner //! suitable for API clients. -//! -use std::collections::BTreeMap; -use std::net::Ipv6Addr; +use std::{collections::BTreeMap, net::Ipv6Addr}; + +use common::{ + network::MacAddr, + ports::{PortFec, PortId, PortMedia, PortPrbsMode, PortSpeed}, +}; use schemars::JsonSchema; -use serde::Deserialize; -use serde::Serialize; +use serde::{Deserialize, Serialize}; -use crate::link::LinkId; -use crate::link::LinkState; -use crate::switch_port; -use crate::switch_port::FixedSideDevice; -use crate::transceivers::QsfpDevice; -use common::network::MacAddr; -use common::ports::PortFec; -use common::ports::PortId; -use common::ports::PortMedia; -use common::ports::PortPrbsMode; -use common::ports::PortSpeed; +use crate::{ + link::{LinkId, LinkState}, + transceivers::QsfpDevice, +}; /// A physical port on the Sidecar switch. // @@ -39,19 +34,6 @@ pub struct SwitchPort { pub qsfp_device: Option, } -impl From<&switch_port::SwitchPort> for SwitchPort { - fn from(p: &switch_port::SwitchPort) -> Self { - let qsfp_device = match &p.fixed_side { - FixedSideDevice::Qsfp { device, .. } => Some(device.clone()), - _ => None, - }; - Self { - port_id: p.port_id(), - qsfp_device, - } - } -} - /// An Ethernet-capable link within a switch port. // // NOTE: This is a view onto `crate::link::Link`. @@ -98,72 +80,6 @@ impl std::fmt::Display for Link { } } -impl From<&crate::link::Link> for Link { - fn from(m: &crate::link::Link) -> Self { - Self { - port_id: m.port_id, - link_id: m.link_id, - tofino_connector: m.port_hdl.connector.as_u16(), - asic_id: m.asic_port_id, - presence: m.presence, - fsm_state: m.fsm_state.to_string(), - media: m.media, - link_state: m.link_state.clone(), - ipv6_enabled: m.ipv6_enabled, - enabled: m.config.enabled, - prbs: m.config.prbs, - speed: m.config.speed, - fec: m.get_fec(), - kr: m.config.kr, - autoneg: m.config.autoneg, - address: m.config.mac, - } - } -} - -impl From for Link { - fn from(m: crate::link::Link) -> Self { - Self::from(&m) - } -} - -/// The per-link data consumed by tfportd -#[derive(Clone, Debug, Deserialize, JsonSchema, Serialize)] -pub struct TfportData { - /// The switch port ID for this link. - pub port_id: PortId, - /// The link ID for this link. - pub link_id: LinkId, - /// The lower-level ASIC ID used to refer to this object in the switch - /// driver software. - pub asic_id: u16, - /// The MAC address for the link. - pub mac: MacAddr, - /// Is ipv6 enabled for this link - pub ipv6_enabled: bool, - /// The IPv6 link-local address of the link, if it exists. - pub link_local: Option, -} - -impl From<&crate::link::Link> for TfportData { - fn from(m: &crate::link::Link) -> Self { - Self { - port_id: m.port_id, - link_id: m.link_id, - asic_id: m.asic_port_id, - mac: m.config.mac, - ipv6_enabled: m.ipv6_enabled, - link_local: m.link_local(), - } - } -} - -impl From for TfportData { - fn from(m: crate::link::Link) -> Self { - Self::from(&m) - } -} - #[derive(Clone, Debug, Deserialize, JsonSchema, Serialize)] pub struct LinkEvent { /// Time the event occurred. The time is represented in milliseconds, @@ -189,6 +105,24 @@ pub struct LinkHistory { pub events: Vec, } +/// The per-link data consumed by tfportd +#[derive(Clone, Debug, Deserialize, JsonSchema, Serialize)] +pub struct TfportData { + /// The switch port ID for this link. + pub port_id: PortId, + /// The link ID for this link. + pub link_id: LinkId, + /// The lower-level ASIC ID used to refer to this object in the switch + /// driver software. + pub asic_id: u16, + /// The MAC address for the link. + pub mac: MacAddr, + /// Is ipv6 enabled for this link + pub ipv6_enabled: bool, + /// The IPv6 link-local address of the link, if it exists. + pub link_local: Option, +} + /// Each entry in a P4 table is addressed by matching against a set of key /// values. If an entry is found, an action is taken with an action-specific /// set of arguments. diff --git a/dpd/Cargo.toml b/dpd/Cargo.toml index d2a90d8..c8460a8 100644 --- a/dpd/Cargo.toml +++ b/dpd/Cargo.toml @@ -24,6 +24,8 @@ aal.workspace = true aal_macros.workspace = true asic.workspace = true common.workspace = true +dpd-api.workspace = true +dpd-types.workspace = true anyhow.workspace = true cfg-if.workspace = true diff --git a/dpd/src/api_server.rs b/dpd/src/api_server.rs index 6879325..18e6910 100644 --- a/dpd/src/api_server.rs +++ b/dpd/src/api_server.rs @@ -13,7 +13,23 @@ use std::convert::TryFrom; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; use std::sync::Arc; -use dropshot::endpoint; +use dpd_types::fault::Fault; +use dpd_types::link::LinkFsmCounters; +use dpd_types::link::LinkId; +use dpd_types::link::LinkUpCounter; +use dpd_types::mcast::MulticastGroupCreateEntry; +use dpd_types::mcast::MulticastGroupCreateExternalEntry; +use dpd_types::mcast::MulticastGroupResponse; +use dpd_types::mcast::MulticastGroupUpdateEntry; +use dpd_types::mcast::MulticastGroupUpdateExternalEntry; +use dpd_types::oxstats::OximeterMetadata; +use dpd_types::port_map::BackplaneLink; +use dpd_types::route::Ipv4Route; +use dpd_types::route::Ipv6Route; +use dpd_types::switch_identifiers::SwitchIdentifiers; +use dpd_types::switch_port::Led; +use dpd_types::switch_port::ManagementMode; +use dpd_types::transceivers::Transceiver; use dropshot::ClientErrorStatusCode; use dropshot::EmptyScanParams; use dropshot::HttpError; @@ -28,176 +44,44 @@ use dropshot::RequestContext; use dropshot::ResultsPage; use dropshot::TypedBody; use dropshot::WhichPage; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; use slog::{debug, error, info, o}; use transceiver_controller::Datapath; use transceiver_controller::Monitors; use crate::counters; -use crate::fault::Fault; -use crate::link::LinkFsmCounters; -use crate::link::LinkId; -use crate::link::LinkUpCounter; use crate::mcast; use crate::oxstats; -use crate::port_map::BackplaneLink; -use crate::route::Ipv4Route; -use crate::route::Ipv6Route; use crate::rpw::Task; -use crate::switch_identifiers::SwitchIdentifiers; use crate::switch_port::FixedSideDevice; -use crate::switch_port::Led; use crate::switch_port::LedState; -use crate::switch_port::ManagementMode; use crate::transceivers::PowerState; -use crate::transceivers::Transceiver; use crate::types::DpdError; -use crate::views; use crate::{arp, loopback, nat, ports, route, Switch}; use common::nat::{Ipv4Nat, Ipv6Nat, NatTarget}; use common::network::MacAddr; -use common::ports::PortFec; use common::ports::PortId; -use common::ports::PortSpeed; use common::ports::QsfpPort; -use common::ports::TxEq; use common::ports::{Ipv4Entry, Ipv6Entry, PortPrbsMode}; -use oxnet::{Ipv4Net, Ipv6Net}; +use dpd_api::*; +use dpd_types::views; type ApiServer = dropshot::HttpServer>; -// Temporary module to provide an indent and avoid destroying blame. -mod imp { - use super::*; - - /// Parameter used to create a port. - #[derive(Clone, Debug, Deserialize, JsonSchema, Serialize)] - pub struct PortCreateParams { - /// The name of the port. This should be a string like `"3:0"`. - pub name: String, - /// The speed at which to configure the port. - pub speed: PortSpeed, - /// The forward error-correction scheme for the port. - pub fec: PortFec, - } - - /// Represents the free MAC channels on a single physical port. - #[derive(Deserialize, Serialize, JsonSchema, Debug)] - pub struct FreeChannels { - /// The switch port. - pub port_id: PortId, - /// The Tofino connector for this port. - /// - /// This describes the set of electrical connections representing this port - /// object, which are defined by the pinout and board design of the Sidecar. - pub connector: String, - /// The set of available channels (lanes) on this connector. - pub channels: Vec, - } - - /// Represents the mapping of an IP address to a MAC address. - #[derive(Deserialize, Serialize, JsonSchema)] - pub struct ArpEntry { - /// A tag used to associate this entry with a client. - pub tag: String, - /// The IP address for the entry. - pub ip: IpAddr, - /// The MAC address to which `ip` maps. - pub mac: MacAddr, - /// The time the entry was updated - pub update: String, - } - - /// Represents a new or replacement mapping of a subnet to a single IPv4 - /// RouteTarget nexthop target. - #[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)] - pub struct Ipv4RouteUpdate { - /// Traffic destined for any address within the CIDR block is routed using - /// this information. - pub cidr: Ipv4Net, - /// A single Route associated with this CIDR - pub target: Ipv4Route, - /// Should this route replace any existing route? If a route exists and - /// this parameter is false, then the call will fail. - pub replace: bool, - } - - /// Represents a new or replacement mapping of a subnet to a single IPv6 - /// RouteTarget nexthop target. - #[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)] - pub struct Ipv6RouteUpdate { - /// Traffic destined for any address within the CIDR block is routed using - /// this information. - pub cidr: Ipv6Net, - /// A single RouteTarget associated with this CIDR - pub target: Ipv6Route, - /// Should this route replace any existing route? If a route exists and - /// this parameter is false, then the call will fail. - pub replace: bool, - } - - /// Represents all mappings of an IPv4 subnet to a its nexthop target(s). - #[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)] - pub struct Ipv4Routes { - /// Traffic destined for any address within the CIDR block is routed using - /// this information. - pub cidr: Ipv4Net, - /// All RouteTargets associated with this CIDR - pub targets: Vec, - } - - /// Represents all mappings of an IPv6 subnet to a its nexthop target(s). - #[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)] - pub struct Ipv6Routes { - /// Traffic destined for any address within the CIDR block is routed using - /// this information. - pub cidr: Ipv6Net, - /// All RouteTargets associated with this CIDR - pub targets: Vec, - } - - // Generate a 400 client error with the provided message. - fn client_error(message: impl ToString) -> HttpError { - HttpError::for_client_error( - None, - ClientErrorStatusCode::BAD_REQUEST, - message.to_string(), - ) - } - - #[derive(Deserialize, Serialize, JsonSchema)] - struct Ipv6ArpParam { - ip: Ipv6Addr, - } +// Generate a 400 client error with the provided message. +fn client_error(message: impl ToString) -> HttpError { + HttpError::for_client_error( + None, + ClientErrorStatusCode::BAD_REQUEST, + message.to_string(), + ) +} - /** - * Represents a cursor into a paginated request for the contents of an ARP table - */ - #[derive(Deserialize, Serialize, JsonSchema)] - struct ArpToken { - ip: IpAddr, - } +pub enum DpdApiImpl {} - /** - * Represents a potential fault condtion on a link - */ - #[derive(Deserialize, Serialize, JsonSchema)] - struct FaultCondition { - fault: Option, - } +impl DpdApi for DpdApiImpl { + type Context = Arc; - /** - * Fetch the IPv6 NDP table entries. - * - * This returns a paginated list of all IPv6 neighbors directly connected to the - * switch. - */ - #[endpoint { - method = GET, - path = "/ndp", - }] - pub(super) async fn ndp_list( + async fn ndp_list( rqctx: RequestContext>, query: Query>, ) -> Result>, HttpError> { @@ -227,14 +111,7 @@ mod imp { )?)) } - /** - * Remove all entries in the the IPv6 NDP tables. - */ - #[endpoint { - method = DELETE, - path = "/ndp" - }] - pub(super) async fn ndp_reset( + async fn ndp_reset( rqctx: RequestContext>, ) -> Result { let switch: &Switch = rqctx.context(); @@ -245,14 +122,7 @@ mod imp { } } - /** - * Get a single IPv6 NDP table entry, by its IPv6 address. - */ - #[endpoint { - method = GET, - path = "/ndp/{ip}", - }] - pub(super) async fn ndp_get( + async fn ndp_get( rqctx: RequestContext>, path: Path, ) -> Result, HttpError> { @@ -270,14 +140,7 @@ mod imp { } } - /** - * Add an IPv6 NDP entry, mapping an IPv6 address to a MAC address. - */ - #[endpoint { - method = POST, - path = "/ndp", - }] - pub(super) async fn ndp_create( + async fn ndp_create( rqctx: RequestContext>, update: TypedBody, ) -> Result { @@ -292,14 +155,7 @@ mod imp { } } - /** - * Remove an IPv6 NDP entry, by its IPv6 address. - */ - #[endpoint { - method = DELETE, - path = "/ndp/{ip}", - }] - pub(super) async fn ndp_delete( + async fn ndp_delete( rqctx: RequestContext>, path: Path, ) -> Result { @@ -310,37 +166,7 @@ mod imp { .map_err(HttpError::from) } - #[derive(Deserialize, Serialize, JsonSchema)] - struct Ipv4ArpParam { - ip: Ipv4Addr, - } - - /** - * Represents a cursor into a paginated request for the contents of an - * Ipv4-indexed table. - */ - #[derive(Deserialize, Serialize, JsonSchema)] - struct Ipv4Token { - ip: Ipv4Addr, - } - - /** - * Represents a cursor into a paginated request for the contents of an - * IPv6-indexed table. - */ - #[derive(Deserialize, Serialize, JsonSchema)] - struct Ipv6Token { - ip: Ipv6Addr, - } - - /** - * Fetch the configured IPv4 ARP table entries. - */ - #[endpoint { - method = GET, - path = "/arp", - }] - pub(super) async fn arp_list( + async fn arp_list( rqctx: RequestContext>, query: Query>, ) -> Result>, HttpError> { @@ -370,14 +196,7 @@ mod imp { )?)) } - /** - * Remove all entries in the IPv4 ARP tables. - */ - #[endpoint { - method = DELETE, - path = "/arp", - }] - pub(super) async fn arp_reset( + async fn arp_reset( rqctx: RequestContext>, ) -> Result { let switch: &Switch = rqctx.context(); @@ -388,14 +207,7 @@ mod imp { } } - /** - * Get a single IPv4 ARP table entry, by its IPv4 address. - */ - #[endpoint { - method = GET, - path = "/arp/{ip}", - }] - pub(super) async fn arp_get( + async fn arp_get( rqctx: RequestContext>, path: Path, ) -> Result, HttpError> { @@ -413,14 +225,7 @@ mod imp { } } - /** - * Add an IPv4 ARP table entry, mapping an IPv4 address to a MAC address. - */ - #[endpoint { - method = POST, - path = "/arp", - }] - pub(super) async fn arp_create( + async fn arp_create( rqctx: RequestContext>, update: TypedBody, ) -> Result { @@ -435,14 +240,7 @@ mod imp { } } - /** - * Remove a single IPv4 ARP entry, by its IPv4 address. - */ - #[endpoint { - method = DELETE, - path = "/arp/{ip}", - }] - pub(super) async fn arp_delete( + async fn arp_delete( rqctx: RequestContext>, path: Path, ) -> Result { @@ -453,67 +251,7 @@ mod imp { .map_err(HttpError::from) } - #[derive(Deserialize, Serialize, JsonSchema)] - struct RoutePathV4 { - /// The IPv4 subnet in CIDR notation whose route entry is returned. - cidr: Ipv4Net, - } - - /// Represents a single subnet->target route entry - #[derive(Deserialize, Serialize, JsonSchema)] - struct RouteTargetIpv4Path { - /// The subnet being routed - cidr: Ipv4Net, - /// The switch port to which packets should be sent - port_id: PortId, - /// The link to which packets should be sent - link_id: LinkId, - /// The next hop in the IPv4 route - tgt_ip: Ipv4Addr, - } - - #[derive(Deserialize, Serialize, JsonSchema)] - struct RoutePathV6 { - /// The IPv6 subnet in CIDR notation whose route entry is returned. - cidr: Ipv6Net, - } - - /// Represents a single subnet->target route entry - #[derive(Deserialize, Serialize, JsonSchema)] - struct RouteTargetIpv6Path { - /// The subnet being routed - cidr: Ipv6Net, - /// The switch port to which packets should be sent - port_id: PortId, - /// The link to which packets should be sent - link_id: LinkId, - /// The next hop in the IPv4 route - tgt_ip: Ipv6Addr, - } - - /** - * Represents a cursor into a paginated request for the contents of the - * subnet routing table. Because we don't (yet) support filtering or arbitrary - * sorting, it is sufficient to track the last mac address reported. - */ - #[derive(Deserialize, Serialize, JsonSchema)] - struct Ipv4RouteToken { - cidr: Ipv4Net, - } - #[derive(Deserialize, Serialize, JsonSchema)] - struct Ipv6RouteToken { - cidr: Ipv6Net, - } - - /** - * Fetch the configured IPv6 routes, mapping IPv6 CIDR blocks to the switch port - * used for sending out that traffic, and optionally a gateway. - */ - #[endpoint { - method = GET, - path = "/route/ipv6", -}] - pub(super) async fn route_ipv6_list( + async fn route_ipv6_list( rqctx: RequestContext>, query: Query>, ) -> Result>, HttpError> { @@ -539,14 +277,7 @@ mod imp { .map(HttpResponseOk) } - /** - * Get a single IPv6 route, by its IPv6 CIDR block. - */ - #[endpoint { - method = GET, - path = "/route/ipv6/{cidr}", -}] - pub(super) async fn route_ipv6_get( + async fn route_ipv6_get( rqctx: RequestContext>, path: Path, ) -> Result>, HttpError> { @@ -558,17 +289,7 @@ mod imp { .map_err(HttpError::from) } - /** - * Route an IPv6 subnet to a link and a nexthop gateway. - * - * This call can be used to create a new single-path route or to add new targets - * to a multipath route. - */ - #[endpoint { - method = POST, - path = "/route/ipv6", -}] - pub(super) async fn route_ipv6_add( + async fn route_ipv6_add( rqctx: RequestContext>, update: TypedBody, ) -> Result { @@ -580,17 +301,7 @@ mod imp { .map_err(HttpError::from) } - /** - * Route an IPv6 subnet to a link and a nexthop gateway. - * - * This call can be used to create a new single-path route or to replace any - * existing routes with a new single-path route. - */ - #[endpoint { - method = PUT, - path = "/route/ipv6", -}] - pub(super) async fn route_ipv6_set( + async fn route_ipv6_set( rqctx: RequestContext>, update: TypedBody, ) -> Result { @@ -602,14 +313,7 @@ mod imp { .map_err(HttpError::from) } - /** - * Remove an IPv6 route, by its IPv6 CIDR block. - */ - #[endpoint { - method = DELETE, - path = "/route/ipv6/{cidr}", -}] - pub(super) async fn route_ipv6_delete( + async fn route_ipv6_delete( rqctx: RequestContext>, path: Path, ) -> Result { @@ -620,14 +324,8 @@ mod imp { .map(|_| HttpResponseDeleted()) .map_err(HttpError::from) } - /** - * Remove a single target for the given IPv6 subnet - */ - #[endpoint { - method = DELETE, - path = "/route/ipv6/{cidr}/{port_id}/{link_id}/{tgt_ip}", -}] - pub(super) async fn route_ipv6_delete_target( + + async fn route_ipv6_delete_target( rqctx: RequestContext>, path: Path, ) -> Result { @@ -645,15 +343,7 @@ mod imp { .map_err(HttpError::from) } - /** - * Fetch the configured IPv4 routes, mapping IPv4 CIDR blocks to the switch port - * used for sending out that traffic, and optionally a gateway. - */ - #[endpoint { - method = GET, - path = "/route/ipv4", -}] - pub(super) async fn route_ipv4_list( + async fn route_ipv4_list( rqctx: RequestContext>, query: Query>, ) -> Result>, HttpError> { @@ -679,14 +369,7 @@ mod imp { .map(HttpResponseOk) } - /** - * Get the configured route for the given IPv4 subnet. - */ - #[endpoint { - method = GET, - path = "/route/ipv4/{cidr}", -}] - pub(super) async fn route_ipv4_get( + async fn route_ipv4_get( rqctx: RequestContext>, path: Path, ) -> Result>, HttpError> { @@ -698,17 +381,7 @@ mod imp { .map_err(HttpError::from) } - /** - * Route an IPv4 subnet to a link and a nexthop gateway. - * - * This call can be used to create a new single-path route or to add new targets - * to a multipath route. - */ - #[endpoint { - method = POST, - path = "/route/ipv4", -}] - pub(super) async fn route_ipv4_add( + async fn route_ipv4_add( rqctx: RequestContext>, update: TypedBody, ) -> Result { @@ -721,17 +394,7 @@ mod imp { .map_err(HttpError::from) } - /** - * Route an IPv4 subnet to a link and a nexthop gateway. - * - * This call can be used to create a new single-path route or to replace any - * existing routes with a new single-path route. - */ - #[endpoint { - method = PUT, - path = "/route/ipv4", -}] - pub(super) async fn route_ipv4_set( + async fn route_ipv4_set( rqctx: RequestContext>, update: TypedBody, ) -> Result { @@ -743,14 +406,7 @@ mod imp { .map_err(HttpError::from) } - /** - * Remove all targets for the given subnet - */ - #[endpoint { - method = DELETE, - path = "/route/ipv4/{cidr}", -}] - pub(super) async fn route_ipv4_delete( + async fn route_ipv4_delete( rqctx: RequestContext>, path: Path, ) -> Result { @@ -761,14 +417,8 @@ mod imp { .map(|_| HttpResponseDeleted()) .map_err(HttpError::from) } - /** - * Remove a single target for the given IPv4 subnet - */ - #[endpoint { - method = DELETE, - path = "/route/ipv4/{cidr}/{port_id}/{link_id}/{tgt_ip}", -}] - pub(super) async fn route_ipv4_delete_target( + + async fn route_ipv4_delete_target( rqctx: RequestContext>, path: Path, ) -> Result { @@ -786,129 +436,7 @@ mod imp { .map_err(HttpError::from) } - #[derive(Deserialize, Serialize, JsonSchema)] - struct PortIpv4Path { - port: String, - ipv4: Ipv4Addr, - } - - #[derive(Deserialize, Serialize, JsonSchema)] - struct PortIpv6Path { - port: String, - ipv6: Ipv6Addr, - } - - #[derive(Deserialize, Serialize, JsonSchema)] - struct LoopbackIpv4Path { - ipv4: Ipv4Addr, - } - - #[derive(Deserialize, Serialize, JsonSchema)] - struct LoopbackIpv6Path { - ipv6: Ipv6Addr, - } - - #[derive(Deserialize, Serialize, JsonSchema)] - struct NatIpv6Path { - ipv6: Ipv6Addr, - } - - #[derive(Deserialize, Serialize, JsonSchema)] - struct NatIpv6PortPath { - ipv6: Ipv6Addr, - low: u16, - } - - #[derive(Deserialize, Serialize, JsonSchema)] - struct NatIpv6RangePath { - ipv6: Ipv6Addr, - low: u16, - high: u16, - } - - #[derive(Deserialize, Serialize, JsonSchema)] - struct NatIpv4Path { - ipv4: Ipv4Addr, - } - - #[derive(Deserialize, Serialize, JsonSchema)] - struct NatIpv4PortPath { - ipv4: Ipv4Addr, - low: u16, - } - - #[derive(Deserialize, Serialize, JsonSchema)] - struct NatIpv4RangePath { - ipv4: Ipv4Addr, - low: u16, - high: u16, - } - - /** - * Represents a cursor into a paginated request for all NAT data. - */ - #[derive(Deserialize, Serialize, JsonSchema)] - struct NatToken { - port: u16, - } - - /** - * Represents a cursor into a paginated request for all port data. Because we - * don't (yet) support filtering or arbitrary sorting, it is sufficient to - * track the last port returned. - */ - #[derive(Deserialize, Serialize, JsonSchema)] - struct PortToken { - port: u16, - } - - #[derive(Deserialize, Serialize, JsonSchema)] - struct PortIdPathParams { - /// The switch port on which to operate. - port_id: PortId, - } - - #[derive(Deserialize, Serialize, JsonSchema)] - struct PortSettingsTag { - /// Restrict operations on this port to the provided tag. - tag: Option, - } - - /// Identifies a logical link on a physical port. - #[derive(Deserialize, Serialize, JsonSchema)] - pub(crate) struct LinkPath { - /// The switch port on which to operate. - pub port_id: PortId, - /// The link in the switch port on which to operate. - pub link_id: LinkId, - } - - #[derive(Deserialize, Serialize, JsonSchema)] - struct LinkIpv4Path { - /// The switch port on which to operate. - port_id: PortId, - /// The link in the switch port on which to operate. - link_id: LinkId, - /// The IPv4 address on which to operate. - address: Ipv4Addr, - } - - #[derive(Deserialize, Serialize, JsonSchema)] - struct LinkIpv6Path { - /// The switch port on which to operate. - port_id: PortId, - /// The link in the switch port on which to operate. - link_id: LinkId, - /// The IPv6 address on which to operate. - address: Ipv6Addr, - } - - /// List all switch ports on the system. - #[endpoint { - method = GET, - path = "/ports", - }] - pub(super) async fn port_list( + async fn port_list( rqctx: RequestContext>, ) -> Result>, HttpError> { Ok(HttpResponseOk( @@ -922,16 +450,7 @@ mod imp { )) } - /// Get the set of available channels for all ports. - /// - /// This returns the unused MAC channels for each physical switch port. This can - /// be used to determine how many additional links can be crated on a physical - /// switch port. - #[endpoint { - method = GET, - path = "/channels", - }] - pub(super) async fn channels_list( + async fn channels_list( rqctx: RequestContext>, ) -> Result>, HttpError> { let switch: &Switch = rqctx.context(); @@ -958,12 +477,7 @@ mod imp { Ok(HttpResponseOk(rval)) } - /// Return information about a single switch port. - #[endpoint { - method = GET, - path = "/ports/{port_id}", - }] - pub(super) async fn port_get( + async fn port_get( rqctx: RequestContext>, path: Path, ) -> Result, HttpError> { @@ -982,12 +496,7 @@ mod imp { ))) } - /// Return the current management mode of a QSFP switch port. - #[endpoint { - method = GET, - path = "/ports/{port_id}/management-mode", - }] - pub(super) async fn management_mode_get( + async fn management_mode_get( rqctx: RequestContext>, path: Path, ) -> Result, HttpError> { @@ -1007,12 +516,7 @@ mod imp { .map_err(HttpError::from) } - /// Set the current management mode of a QSFP switch port. - #[endpoint { - method = PUT, - path = "/ports/{port_id}/management-mode", - }] - pub(super) async fn management_mode_set( + async fn management_mode_set( rqctx: RequestContext>, path: Path, body: TypedBody, @@ -1047,12 +551,7 @@ mod imp { .map_err(HttpError::from) } - /// Return the current state of the attention LED on a front-facing QSFP port. - #[endpoint { - method = GET, - path = "/ports/{port_id}/led", - }] - pub(super) async fn led_get( + async fn led_get( rqctx: RequestContext>, path: Path, ) -> Result, HttpError> { @@ -1065,21 +564,7 @@ mod imp { .map_err(HttpError::from) } - /// Override the current state of the attention LED on a front-facing QSFP port. - /// - /// The attention LED normally follows the state of the port itself. For - /// example, if a transceiver is powered and operating normally, then the LED is - /// solid on. An unexpected power fault would then be reflected by powering off - /// the LED. - /// - /// The client may override this behavior, explicitly setting the LED to a - /// specified state. This can be undone, sending the LED back to its default - /// policy, with the endpoint `/ports/{port_id}/led/auto`. - #[endpoint { - method = PUT, - path = "/ports/{port_id}/led", - }] - pub(super) async fn led_set( + async fn led_set( rqctx: RequestContext>, path: Path, body: TypedBody, @@ -1094,16 +579,7 @@ mod imp { .map_err(HttpError::from) } - /// Return the full backplane map. - /// - /// This returns the entire mapping of all cubbies in a rack, through the cabled - /// backplane, and into the Sidecar main board. It also includes the Tofino - /// "connector", which is included in some contexts such as reporting counters. - #[endpoint { - method = GET, - path = "/backplane-map", - }] - pub(super) async fn backplane_map( + async fn backplane_map( rqctx: RequestContext>, ) -> Result>, HttpError> { @@ -1120,12 +596,7 @@ mod imp { )) } - /// Return the backplane mapping for a single switch port. - #[endpoint { - method = GET, - path = "/backplane-map/{port_id}", - }] - pub(super) async fn port_backplane_link( + async fn port_backplane_link( rqctx: RequestContext>, path: Path, ) -> Result, HttpError> { @@ -1141,12 +612,7 @@ mod imp { } } - /// Return the state of all attention LEDs on the Sidecar QSFP ports. - #[endpoint { - method = GET, - path = "/leds", - }] - pub(super) async fn leds_list( + async fn leds_list( rqctx: RequestContext>, ) -> Result>, HttpError> { let switch = rqctx.context(); @@ -1157,15 +623,7 @@ mod imp { .map_err(HttpError::from) } - /// Set the LED policy to automatic. - /// - /// The automatic LED policy ensures that the state of the LED follows the state - /// of the switch port itself. - #[endpoint { - method = PUT, - path = "/ports/{port_id}/led/auto", - }] - pub(super) async fn led_set_auto( + async fn led_set_auto( rqctx: RequestContext>, path: Path, ) -> Result { @@ -1178,12 +636,7 @@ mod imp { .map_err(HttpError::from) } - /// Return information about all QSFP transceivers. - #[endpoint { - method = GET, - path = "/transceivers", - }] - pub(super) async fn transceivers_list( + async fn transceivers_list( rqctx: RequestContext>, ) -> Result>, HttpError> { let switch = rqctx.context(); @@ -1199,19 +652,7 @@ mod imp { Ok(HttpResponseOk(out)) } - /// Return the information about a port's transceiver. - /// - /// This returns the status (presence, power state, etc) of the transceiver - /// along with its identifying information. If the port is an optical switch - /// port, but has no transceiver, then the identifying information is empty. - /// - /// If the switch port is not a QSFP port, and thus could never have a - /// transceiver, then "Not Found" is returned. - #[endpoint { - method = GET, - path = "/ports/{port_id}/transceiver", - }] - pub(super) async fn transceiver_get( + async fn transceiver_get( rqctx: RequestContext>, path: Path, ) -> Result, HttpError> { @@ -1245,15 +686,7 @@ mod imp { } } - /// Effect a module-level reset of a QSFP transceiver. - /// - /// If the QSFP port has no transceiver or is not a QSFP port, then a client - /// error is returned. - #[endpoint { - method = POST, - path = "/ports/{port_id}/transceiver/reset", - }] - pub(super) async fn transceiver_reset( + async fn transceiver_reset( rqctx: RequestContext>, path: Path, ) -> Result { @@ -1266,12 +699,7 @@ mod imp { .map_err(HttpError::from) } - /// Control the power state of a transceiver. - #[endpoint { - method = PUT, - path = "/ports/{port_id}/transceiver/power", - }] - pub(super) async fn transceiver_power_set( + async fn transceiver_power_set( rqctx: RequestContext>, path: Path, state: TypedBody, @@ -1286,12 +714,7 @@ mod imp { .map_err(HttpError::from) } - /// Return the power state of a transceiver. - #[endpoint { - method = GET, - path = "/ports/{port_id}/transceiver/power", - }] - pub(super) async fn transceiver_power_get( + async fn transceiver_power_get( rqctx: RequestContext>, path: Path, ) -> Result, HttpError> { @@ -1304,12 +727,7 @@ mod imp { .map_err(HttpError::from) } - /// Fetch the monitored environmental information for the provided transceiver. - #[endpoint { - method = GET, - path = "/ports/{port_id}/transceiver/monitors", - }] - pub(super) async fn transceiver_monitors_get( + async fn transceiver_monitors_get( rqctx: RequestContext>, path: Path, ) -> Result, HttpError> { @@ -1322,12 +740,7 @@ mod imp { .map_err(HttpError::from) } - /// Fetch the state of the datapath for the provided transceiver. - #[endpoint { - method = GET, - path = "/ports/{port_id}/transceiver/datapath" - }] - pub(super) async fn transceiver_datapath_get( + async fn transceiver_datapath_get( rqctx: RequestContext>, path: Path, ) -> Result, HttpError> { @@ -1340,61 +753,7 @@ mod imp { .map_err(HttpError::from) } - // Convert a port ID path into a `QsfpPort` if possible. This is generally used - // for endpoints which only apply to the QSFP ports, such as transceiver - // management. - fn path_to_qsfp( - path: Path, - ) -> Result { - let port_id = path.into_inner().port_id; - if let PortId::Qsfp(qsfp_port) = port_id { - Ok(qsfp_port) - } else { - Err(HttpError::from(DpdError::NotAQsfpPort { port_id })) - } - } - - /// Parameters used to create a link on a switch port. - #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] - pub struct LinkCreate { - /// The first lane of the port to use for the new link - pub lane: Option, - /// The requested speed of the link. - pub speed: PortSpeed, - /// The requested forward-error correction method. If this is None, the - /// standard FEC for the underlying media will be applied if it can be - /// determined. - pub fec: Option, - /// Whether the link is configured to autonegotiate with its peer during - /// link training. - /// - /// This is generally only true for backplane links, and defaults to - /// `false`. - #[serde(default)] - pub autoneg: bool, - /// Whether the link is configured in KR mode, an electrical specification - /// generally only true for backplane link. - /// - /// This defaults to `false`. - #[serde(default)] - pub kr: bool, - - /// Transceiver equalization adjustment parameters. - /// This defaults to `None`. - #[serde(default)] - pub tx_eq: Option, - } - - /// Create a link on a switch port. - /// - /// Create an interface that can be used for sending Ethernet frames on the - /// provided switch port. This will use the first available lanes in the - /// physical port to create an interface of the desired speed, if possible. - #[endpoint { - method = POST, - path = "/ports/{port_id}/links" - }] - pub(super) async fn link_create( + async fn link_create( rqctx: RequestContext>, path: Path, params: TypedBody, @@ -1408,12 +767,7 @@ mod imp { .map_err(|e| e.into()) } - /// Get an existing link by ID. - #[endpoint { - method = GET, - path = "/ports/{port_id}/links/{link_id}" - }] - pub(super) async fn link_get( + async fn link_get( rqctx: RequestContext>, path: Path, ) -> Result, HttpError> { @@ -1425,12 +779,7 @@ mod imp { .map_err(|e| e.into()) } - /// Delete a link from a switch port. - #[endpoint { - method = DELETE, - path = "/ports/{port_id}/links/{link_id}", - }] - pub(super) async fn link_delete( + async fn link_delete( rqctx: RequestContext>, path: Path, ) -> Result { @@ -1442,12 +791,7 @@ mod imp { .map_err(|e| e.into()) } - /// List the links within a single switch port. - #[endpoint { - method = GET, - path = "/ports/{port_id}/links", - }] - pub(super) async fn link_list( + async fn link_list( rqctx: RequestContext>, path: Path, ) -> Result>, HttpError> { @@ -1459,20 +803,7 @@ mod imp { .map_err(HttpError::from) } - #[derive(Clone, Debug, Deserialize, JsonSchema)] - pub struct LinkFilter { - /// Filter links to those whose name contains the provided string. - /// - /// If not provided, then all links are returned. - filter: Option, - } - - /// List all links, on all switch ports. - #[endpoint { - method = GET, - path = "/links", - }] - pub(super) async fn link_list_all( + async fn link_list_all( rqctx: RequestContext>, query: Query, ) -> Result>, HttpError> { @@ -1481,12 +812,7 @@ mod imp { Ok(HttpResponseOk(switch.list_all_links(filter.as_deref()))) } - /// Return whether the link is enabled. - #[endpoint { - method = GET, - path = "/ports/{port_id}/links/{link_id}/enabled", - }] - pub(super) async fn link_enabled_get( + async fn link_enabled_get( rqctx: RequestContext>, path: Path, ) -> Result, HttpError> { @@ -1500,12 +826,7 @@ mod imp { .map_err(|e| e.into()) } - /// Enable or disable a link. - #[endpoint { - method = PUT, - path = "/ports/{port_id}/links/{link_id}/enabled", - }] - pub(super) async fn link_enabled_set( + async fn link_enabled_set( rqctx: RequestContext>, path: Path, body: TypedBody, @@ -1521,12 +842,7 @@ mod imp { .map_err(|e| e.into()) } - /// Return whether the link is configured to act as an IPv6 endpoint - #[endpoint { - method = GET, - path = "/ports/{port_id}/links/{link_id}/ipv6_enabled", - }] - pub(super) async fn link_ipv6_enabled_get( + async fn link_ipv6_enabled_get( rqctx: RequestContext>, path: Path, ) -> Result, HttpError> { @@ -1540,12 +856,7 @@ mod imp { .map_err(|e| e.into()) } - /// Set whether a port is configured to act as an IPv6 endpoint - #[endpoint { - method = PUT, - path = "/ports/{port_id}/links/{link_id}/ipv6_enabled", - }] - pub(super) async fn link_ipv6_enabled_set( + async fn link_ipv6_enabled_set( rqctx: RequestContext>, path: Path, body: TypedBody, @@ -1561,19 +872,7 @@ mod imp { .map_err(|e| e.into()) } - /// Return whether the link is in KR mode. - /// - /// "KR" refers to the Ethernet standard for the link, which are defined in - /// various clauses of the IEEE 802.3 specification. "K" is used to denote a - /// link over an electrical cabled backplane, and "R" refers to "scrambled - /// encoding", a 64B/66B bit-encoding scheme. - /// - /// Thus this should be true iff a link is on the cabled backplane. - #[endpoint { - method = GET, - path = "/ports/{port_id}/links/{link_id}/kr", - }] - pub(super) async fn link_kr_get( + async fn link_kr_get( rqctx: RequestContext>, path: Path, ) -> Result, HttpError> { @@ -1587,12 +886,7 @@ mod imp { .map_err(|e| e.into()) } - /// Enable or disable a link. - #[endpoint { - method = PUT, - path = "/ports/{port_id}/links/{link_id}/kr", - }] - pub(super) async fn link_kr_set( + async fn link_kr_set( rqctx: RequestContext>, path: Path, body: TypedBody, @@ -1608,13 +902,7 @@ mod imp { .map_err(|e| e.into()) } - /// Return whether the link is configured to use autonegotiation with its peer - /// link. - #[endpoint { - method = GET, - path = "/ports/{port_id}/links/{link_id}/autoneg", - }] - pub(super) async fn link_autoneg_get( + async fn link_autoneg_get( rqctx: RequestContext>, path: Path, ) -> Result, HttpError> { @@ -1628,12 +916,7 @@ mod imp { .map_err(|e| e.into()) } - /// Set whether a port is configured to use autonegotation with its peer link. - #[endpoint { - method = PUT, - path = "/ports/{port_id}/links/{link_id}/autoneg", - }] - pub(super) async fn link_autoneg_set( + async fn link_autoneg_set( rqctx: RequestContext>, path: Path, body: TypedBody, @@ -1649,12 +932,7 @@ mod imp { .map_err(|e| e.into()) } - /// Set a link's PRBS speed and mode. - #[endpoint { - method = PUT, - path = "/ports/{port_id}/links/{link_id}/prbs", - }] - pub(super) async fn link_prbs_set( + async fn link_prbs_set( rqctx: RequestContext>, path: Path, body: TypedBody, @@ -1670,16 +948,7 @@ mod imp { .map_err(|e| e.into()) } - /// Return the link's PRBS speed and mode. - /// - /// During link training, a pseudorandom bit sequence (PRBS) is used to allow - /// each side to synchronize their clocks and set various parameters on the - /// underlying circuitry (such as filter gains). - #[endpoint { - method = GET, - path = "/ports/{port_id}/links/{link_id}/prbs", - }] - pub(super) async fn link_prbs_get( + async fn link_prbs_get( rqctx: RequestContext>, path: Path, ) -> Result, HttpError> { @@ -1693,12 +962,7 @@ mod imp { .map_err(|e| e.into()) } - /// Return whether a link is up. - #[endpoint { - method = GET, - path = "/ports/{port_id}/links/{link_id}/linkup", - }] - pub(super) async fn link_linkup_get( + async fn link_linkup_get( rqctx: RequestContext>, path: Path, ) -> Result, HttpError> { @@ -1712,12 +976,7 @@ mod imp { .map_err(|e| e.into()) } - /// Return any fault currently set on this link - #[endpoint { - method = GET, - path = "/ports/{port_id}/links/{link_id}/fault", - }] - pub(super) async fn link_fault_get( + async fn link_fault_get( rqctx: RequestContext>, path: Path, ) -> Result, HttpError> { @@ -1731,12 +990,7 @@ mod imp { .map_err(|e| e.into()) } - /// Clear any fault currently set on this link - #[endpoint { - method = DELETE, - path = "/ports/{port_id}/links/{link_id}/fault", - }] - pub(super) async fn link_fault_clear( + async fn link_fault_clear( rqctx: RequestContext>, path: Path, ) -> Result { @@ -1750,12 +1004,7 @@ mod imp { .map_err(|e| e.into()) } - /// Inject a fault on this link - #[endpoint { - method = POST, - path = "/ports/{port_id}/links/{link_id}/fault", - }] - pub(super) async fn link_fault_inject( + async fn link_fault_inject( rqctx: RequestContext>, path: Path, entry: TypedBody, @@ -1775,12 +1024,7 @@ mod imp { .map_err(|e| e.into()) } - /// List the IPv4 addresses associated with a link. - #[endpoint { - method = GET, - path = "/ports/{port_id}/links/{link_id}/ipv4", - }] - pub(super) async fn link_ipv4_list( + async fn link_ipv4_list( rqctx: RequestContext>, path: Path, query: Query>, @@ -1810,12 +1054,7 @@ mod imp { .map(HttpResponseOk) } - /// Add an IPv4 address to a link. - #[endpoint { - method = POST, - path = "/ports/{port_id}/links/{link_id}/ipv4", - }] - pub(super) async fn link_ipv4_create( + async fn link_ipv4_create( rqctx: RequestContext>, path: Path, entry: TypedBody, @@ -1831,12 +1070,7 @@ mod imp { .map_err(|e| e.into()) } - /// Clear all IPv4 addresses from a link. - #[endpoint { - method = DELETE, - path = "/ports/{port_id}/links/{link_id}/ipv4", - }] - pub(super) async fn link_ipv4_reset( + async fn link_ipv4_reset( rqctx: RequestContext>, path: Path, ) -> Result { @@ -1850,12 +1084,7 @@ mod imp { .map_err(|e| e.into()) } - /// Remove an IPv4 address from a link. - #[endpoint { - method = DELETE, - path = "/ports/{port_id}/links/{link_id}/ipv4/{address}", - }] - pub(super) async fn link_ipv4_delete( + async fn link_ipv4_delete( rqctx: RequestContext>, path: Path, ) -> Result { @@ -1870,12 +1099,7 @@ mod imp { .map_err(|e| e.into()) } - /// List the IPv6 addresses associated with a link. - #[endpoint { - method = GET, - path = "/ports/{port_id}/links/{link_id}/ipv6", - }] - pub(super) async fn link_ipv6_list( + async fn link_ipv6_list( rqctx: RequestContext>, path: Path, query: Query>, @@ -1905,12 +1129,7 @@ mod imp { .map(HttpResponseOk) } - /// Add an IPv6 address to a link. - #[endpoint { - method = POST, - path = "/ports/{port_id}/links/{link_id}/ipv6", - }] - pub(super) async fn link_ipv6_create( + async fn link_ipv6_create( rqctx: RequestContext>, path: Path, entry: TypedBody, @@ -1926,12 +1145,7 @@ mod imp { .map_err(|e| e.into()) } - /// Clear all IPv6 addresses from a link. - #[endpoint { - method = DELETE, - path = "/ports/{port_id}/links/{link_id}/ipv6", - }] - pub(super) async fn link_ipv6_reset( + async fn link_ipv6_reset( rqctx: RequestContext>, path: Path, ) -> Result { @@ -1945,12 +1159,7 @@ mod imp { .map_err(|e| e.into()) } - /// Remove an IPv6 address from a link. - #[endpoint { - method = DELETE, - path = "/ports/{port_id}/links/{link_id}/ipv6/{address}", - }] - pub(super) async fn link_ipv6_delete( + async fn link_ipv6_delete( rqctx: RequestContext>, path: Path, ) -> Result { @@ -1965,12 +1174,7 @@ mod imp { .map_err(|e| e.into()) } - /// Get a link's MAC address. - #[endpoint { - method = GET, - path = "/ports/{port_id}/links/{link_id}/mac", - }] - pub(super) async fn link_mac_get( + async fn link_mac_get( rqctx: RequestContext>, path: Path, ) -> Result, HttpError> { @@ -1984,12 +1188,7 @@ mod imp { .map_err(|e| e.into()) } - /// Set a link's MAC address. - #[endpoint { - method = PUT, - path = "/ports/{port_id}/links/{link_id}/mac", - }] - pub(super) async fn link_mac_set( + async fn link_mac_set( rqctx: RequestContext>, path: Path, body: TypedBody, @@ -2005,12 +1204,7 @@ mod imp { .map_err(|e| e.into()) } - /// Return whether the link is configured to drop non-nat traffic - #[endpoint { - method = GET, - path = "/ports/{port_id}/links/{link_id}/nat_only", - }] - pub(super) async fn link_nat_only_get( + async fn link_nat_only_get( rqctx: RequestContext>, path: Path, ) -> Result, HttpError> { @@ -2024,12 +1218,7 @@ mod imp { .map_err(|e| e.into()) } - /// Set whether a port is configured to use drop non-nat traffic - #[endpoint { - method = PUT, - path = "/ports/{port_id}/links/{link_id}/nat_only", - }] - pub(super) async fn link_nat_only_set( + async fn link_nat_only_set( rqctx: RequestContext>, path: Path, body: TypedBody, @@ -2045,12 +1234,7 @@ mod imp { .map_err(|e| e.into()) } - /// Get the event history for the given link. - #[endpoint { - method = GET, - path = "/ports/{port_id}/links/{link_id}/history", - }] - pub(super) async fn link_history_get( + async fn link_history_get( rqctx: RequestContext>, path: Path, ) -> Result, HttpError> { @@ -2064,14 +1248,7 @@ mod imp { .map_err(|e| e.into()) } - /** - * Get loopback IPv4 addresses. - */ - #[endpoint { - method = GET, - path = "/loopback/ipv4", - }] - pub(super) async fn loopback_ipv4_list( + async fn loopback_ipv4_list( rqctx: RequestContext>, ) -> Result>, HttpError> { let switch: &Switch = rqctx.context(); @@ -2084,14 +1261,7 @@ mod imp { Ok(HttpResponseOk(addrs)) } - /** - * Add a loopback IPv4. - */ - #[endpoint { - method = POST, - path = "/loopback/ipv4", - }] - pub(super) async fn loopback_ipv4_create( + async fn loopback_ipv4_create( rqctx: RequestContext>, val: TypedBody, ) -> Result { @@ -2103,14 +1273,7 @@ mod imp { Ok(HttpResponseUpdatedNoContent {}) } - /** - * Remove one loopback IPv4 address. - */ - #[endpoint { - method = DELETE, - path = "/loopback/ipv4/{ipv4}", - }] - pub(super) async fn loopback_ipv4_delete( + async fn loopback_ipv4_delete( rqctx: RequestContext>, path: Path, ) -> Result { @@ -2121,14 +1284,7 @@ mod imp { .map_err(HttpError::from) } - /** - * Get loopback IPv6 addresses. - */ - #[endpoint { - method = GET, - path = "/loopback/ipv6", - }] - pub(super) async fn loopback_ipv6_list( + async fn loopback_ipv6_list( rqctx: RequestContext>, ) -> Result>, HttpError> { let switch: &Switch = rqctx.context(); @@ -2141,14 +1297,7 @@ mod imp { Ok(HttpResponseOk(addrs)) } - /** - * Add a loopback IPv6. - */ - #[endpoint { - method = POST, - path = "/loopback/ipv6", - }] - pub(super) async fn loopback_ipv6_create( + async fn loopback_ipv6_create( rqctx: RequestContext>, val: TypedBody, ) -> Result { @@ -2160,14 +1309,7 @@ mod imp { Ok(HttpResponseUpdatedNoContent {}) } - /** - * Remove one loopback IPv6 address. - */ - #[endpoint { - method = DELETE, - path = "/loopback/ipv6/{ipv6}", - }] - pub(super) async fn loopback_ipv6_delete( + async fn loopback_ipv6_delete( rqctx: RequestContext>, path: Path, ) -> Result { @@ -2178,14 +1320,7 @@ mod imp { .map_err(HttpError::from) } - /** - * Get all of the external addresses in use for NAT mappings. - */ - #[endpoint { - method = GET, - path = "/nat/ipv6", - }] - pub(super) async fn nat_ipv6_addresses_list( + async fn nat_ipv6_addresses_list( rqctx: RequestContext>, query: Query>, ) -> Result>, HttpError> { @@ -2211,14 +1346,7 @@ mod imp { )?)) } - /** - * Get all of the external->internal NAT mappings for a given address. - */ - #[endpoint { - method = GET, - path = "/nat/ipv6/{ipv6}", - }] - pub(super) async fn nat_ipv6_list( + async fn nat_ipv6_list( rqctx: RequestContext>, path: Path, query: Query>, @@ -2246,15 +1374,7 @@ mod imp { )?)) } - /** - * Get the external->internal NAT mapping for the given address and starting L3 - * port. - */ - #[endpoint { - method = GET, - path = "/nat/ipv6/{ipv6}/{low}", - }] - pub(super) async fn nat_ipv6_get( + async fn nat_ipv6_get( rqctx: RequestContext>, path: Path, ) -> Result, HttpError> { @@ -2267,24 +1387,7 @@ mod imp { } } - /** - * Add an external->internal NAT mapping for the given address and L3 port - * range. - * - * This maps an external IPv6 address and L3 port range to: - * - A gimlet's IPv6 address - * - A gimlet's MAC address - * - A Geneve VNI - * - * These identify the gimlet on which a guest is running, and gives OPTE the - * information it needs to identify the guest VM that uses the external IPv6 - * and port range when making connections outside of an Oxide rack. - */ - #[endpoint { - method = PUT, - path = "/nat/ipv6/{ipv6}/{low}/{high}" - }] - pub(super) async fn nat_ipv6_create( + async fn nat_ipv6_create( rqctx: RequestContext>, path: Path, target: TypedBody, @@ -2303,14 +1406,7 @@ mod imp { } } - /** - * Delete the NAT mapping for an IPv6 address and starting L3 port. - */ - #[endpoint { - method = DELETE, - path = "/nat/ipv6/{ipv6}/{low}" - }] - pub(super) async fn nat_ipv6_delete( + async fn nat_ipv6_delete( rqctx: RequestContext>, path: Path, ) -> Result { @@ -2321,14 +1417,7 @@ mod imp { .map_err(HttpError::from) } - /** - * Clear all IPv6 NAT mappings. - */ - #[endpoint { - method = DELETE, - path = "/nat/ipv6" - }] - pub(super) async fn nat_ipv6_reset( + async fn nat_ipv6_reset( rqctx: RequestContext>, ) -> Result { let switch: &Switch = rqctx.context(); @@ -2339,14 +1428,7 @@ mod imp { } } - /** - * Get all of the external addresses in use for IPv4 NAT mappings. - */ - #[endpoint { - method = GET, - path = "/nat/ipv4", - }] - pub(super) async fn nat_ipv4_addresses_list( + async fn nat_ipv4_addresses_list( rqctx: RequestContext>, query: Query>, ) -> Result>, HttpError> { @@ -2372,14 +1454,7 @@ mod imp { )?)) } - /** - * Get all of the external->internal NAT mappings for a given IPv4 address. - */ - #[endpoint { - method = GET, - path = "/nat/ipv4/{ipv4}", - }] - pub(super) async fn nat_ipv4_list( + async fn nat_ipv4_list( rqctx: RequestContext>, path: Path, query: Query>, @@ -2408,14 +1483,7 @@ mod imp { )?)) } - /** - * Get the external->internal NAT mapping for the given address/port - */ - #[endpoint { - method = GET, - path = "/nat/ipv4/{ipv4}/{low}", - }] - pub(super) async fn nat_ipv4_get( + async fn nat_ipv4_get( rqctx: RequestContext>, path: Path, ) -> Result, HttpError> { @@ -2428,24 +1496,7 @@ mod imp { } } - /** - * Add an external->internal NAT mapping for the given address/port range - * - * This maps an external IPv6 address and L3 port range to: - * - A gimlet's IPv6 address - * - A gimlet's MAC address - * - A Geneve VNI - * - * These identify the gimlet on which a guest is running, and gives OPTE the - * information it needs to identify the guest VM that uses the external IPv6 - * and port range when making connections outside of an Oxide rack. - */ - - #[endpoint { - method = PUT, - path = "/nat/ipv4/{ipv4}/{low}/{high}" - }] - pub(super) async fn nat_ipv4_create( + async fn nat_ipv4_create( rqctx: RequestContext>, path: Path, target: TypedBody, @@ -2464,14 +1515,7 @@ mod imp { } } - /** - * Clear the NAT mappings for an IPv4 address and starting L3 port. - */ - #[endpoint { - method = DELETE, - path = "/nat/ipv4/{ipv4}/{low}" - }] - pub(super) async fn nat_ipv4_delete( + async fn nat_ipv4_delete( rqctx: RequestContext>, path: Path, ) -> Result { @@ -2482,14 +1526,7 @@ mod imp { .map_err(HttpError::from) } - /** - * Clear all IPv4 NAT mappings. - */ - #[endpoint { - method = DELETE, - path = "/nat/ipv4" - }] - pub(super) async fn nat_ipv4_reset( + async fn nat_ipv4_reset( rqctx: RequestContext>, ) -> Result { let switch: &Switch = rqctx.context(); @@ -2500,26 +1537,7 @@ mod imp { } } - #[derive(Deserialize, Serialize, JsonSchema)] - struct TagPath { - tag: String, - } - - /** - * Clear all settings associated with a specific tag. - * - * This removes: - * - * - All ARP or NDP table entries. - * - All routes - * - All links on all switch ports - */ - // TODO-security: This endpoint should probably not exist. - #[endpoint { - method = DELETE, - path = "/all-settings/{tag}", - }] - pub(super) async fn reset_all_tagged( + async fn reset_all_tagged( rqctx: RequestContext>, path: Path, ) -> Result { @@ -2538,17 +1556,7 @@ mod imp { .map_err(|e| e.into()) } - /** - * Clear all settings. - * - * This removes all data entirely. - */ - // TODO-security: This endpoint should probably not exist. - #[endpoint { - method = DELETE, - path = "/all-settings" - }] - pub(super) async fn reset_all( + async fn reset_all( rqctx: RequestContext>, ) -> Result { let switch: &Switch = rqctx.context(); @@ -2590,12 +1598,7 @@ mod imp { } } - /// Get the LinkUp counters for all links. - #[endpoint { - method = GET, - path = "/counters/linkup", - }] - pub(super) async fn link_up_counters_list( + async fn link_up_counters_list( rqctx: RequestContext>, ) -> Result>, HttpError> { let switch: &Switch = rqctx.context(); @@ -2604,12 +1607,7 @@ mod imp { )) } - /// Get the LinkUp counters for the given link. - #[endpoint { - method = GET, - path = "/counters/linkup/{port_id}/{link_id}", - }] - pub(super) async fn link_up_counters_get( + async fn link_up_counters_get( rqctx: RequestContext>, path: Path, ) -> Result, HttpError> { @@ -2623,12 +1621,7 @@ mod imp { .map_err(|e| e.into()) } - /// Get the autonegotiation FSM counters for the given link. - #[endpoint { - method = GET, - path = "/counters/fsm/{port_id}/{link_id}", - }] - pub(super) async fn link_fsm_counters_get( + async fn link_fsm_counters_get( rqctx: RequestContext>, path: Path, ) -> Result, HttpError> { @@ -2642,75 +1635,19 @@ mod imp { .map_err(|e| e.into()) } - /// Detailed build information about `dpd`. - #[derive(Clone, Debug, Deserialize, JsonSchema, Serialize)] - pub struct BuildInfo { - pub version: String, - pub git_sha: String, - pub git_commit_timestamp: String, - pub git_branch: String, - pub rustc_semver: String, - pub rustc_channel: String, - pub rustc_host_triple: String, - pub rustc_commit_sha: String, - pub cargo_triple: String, - pub debug: bool, - pub opt_level: u8, - pub sde_commit_sha: String, - } - - impl Default for BuildInfo { - fn default() -> Self { - Self { - version: env!("CARGO_PKG_VERSION").to_string(), - git_sha: env!("VERGEN_GIT_SHA").to_string(), - git_commit_timestamp: env!("VERGEN_GIT_COMMIT_TIMESTAMP") - .to_string(), - git_branch: env!("VERGEN_GIT_BRANCH").to_string(), - rustc_semver: env!("VERGEN_RUSTC_SEMVER").to_string(), - rustc_channel: env!("VERGEN_RUSTC_CHANNEL").to_string(), - rustc_host_triple: env!("VERGEN_RUSTC_HOST_TRIPLE").to_string(), - rustc_commit_sha: env!("VERGEN_RUSTC_COMMIT_HASH").to_string(), - cargo_triple: env!("VERGEN_CARGO_TARGET_TRIPLE").to_string(), - debug: env!("VERGEN_CARGO_DEBUG").parse().unwrap(), - opt_level: env!("VERGEN_CARGO_OPT_LEVEL").parse().unwrap(), - sde_commit_sha: env!("SDE_COMMIT_SHA").to_string(), - } - } - } - - /// Return detailed build information about the `dpd` server itself. - #[endpoint { - method = GET, - path = "/build-info", - }] - pub(super) async fn build_info( + async fn build_info( _rqctx: RequestContext>, ) -> Result, HttpError> { - Ok(HttpResponseOk(BuildInfo::default())) + Ok(HttpResponseOk(build_info())) } - /** - * Return the version of the `dpd` server itself. - */ - #[endpoint { - method = GET, - path = "/dpd-version", - }] - pub(super) async fn dpd_version( + async fn dpd_version( _rqctx: RequestContext>, ) -> Result, HttpError> { Ok(HttpResponseOk(crate::version::version())) } - /** - * Return the server uptime. - */ - #[endpoint { - method = GET, - path = "/dpd-uptime", - }] - pub(super) async fn dpd_uptime( + async fn dpd_uptime( rqctx: RequestContext>, ) -> Result, HttpError> { let switch: &Switch = rqctx.context(); @@ -2720,111 +1657,14 @@ mod imp { Ok(HttpResponseOk(uptime)) } - /// Used to request the metadata used to identify this dpd instance and its - /// data with oximeter. - #[endpoint { - method = GET, - path = "/oximeter-metadata", - unpublished = true, - }] - pub(super) async fn oximeter_collect_meta_endpoint( + async fn oximeter_collect_meta_endpoint( rqctx: RequestContext>, - ) -> Result>, HttpError> - { + ) -> Result>, HttpError> { let switch: &Switch = rqctx.context(); Ok(HttpResponseOk(oxstats::oximeter_meta(switch))) } - /// A port settings transaction object. When posted to the - /// `/port-settings/{port_id}` API endpoint, these settings will be applied - /// holistically, and to the extent possible atomically to a given port. - #[derive(Default, Clone, Debug, Deserialize, JsonSchema, Serialize)] - pub struct PortSettings { - /// The link settings to apply to the port on a per-link basis. Any links - /// not in this map that are resident on the switch port will be removed. - /// Any links that are in this map that are not resident on the switch port - /// will be added. Any links that are resident on the switch port and in - /// this map, and are different, will be modified. Links are indexed by - /// spatial index within the port. - pub links: HashMap, - } - - /// An object with link settings used in concert with [`PortSettings`]. - #[derive(Clone, Debug, Deserialize, JsonSchema, Serialize)] - pub struct LinkSettings { - pub params: LinkCreate, - pub addrs: HashSet, - } - - impl From<&crate::link::Link> for LinkSettings { - fn from(l: &crate::link::Link) -> Self { - let mut addrs: HashSet = HashSet::new(); - for a in &l.ipv4 { - addrs.insert(a.addr.into()); - } - for a in &l.ipv6 { - addrs.insert(a.addr.into()); - } - LinkSettings { - params: LinkCreate { - lane: Some(l.link_id), - speed: l.config.speed, - fec: l.config.fec, - autoneg: l.config.autoneg, - kr: l.config.kr, - tx_eq: l.tx_eq, - }, - addrs, - } - } - } - - /// An object with IPv4 route settings used in concert with [`PortSettings`]. - #[derive(Clone, Debug, Deserialize, JsonSchema, Serialize)] - pub struct RouteSettingsV4 { - pub link_id: u8, - pub nexthop: Ipv4Addr, - } - - impl From<&crate::route::Ipv4Route> for RouteSettingsV4 { - fn from(r: &crate::route::Ipv4Route) -> Self { - Self { - link_id: r.link_id.0, - nexthop: r.tgt_ip, - } - } - } - - /// An object with IPV6 route settings used in concert with [`PortSettings`]. - #[derive(Clone, Debug, Deserialize, JsonSchema, Serialize)] - pub struct RouteSettingsV6 { - pub link_id: u8, - pub nexthop: Ipv6Addr, - } - - impl From<&crate::route::Ipv6Route> for RouteSettingsV6 { - fn from(r: &crate::route::Ipv6Route) -> Self { - Self { - link_id: r.link_id.0, - nexthop: r.tgt_ip, - } - } - } - - /** - * Apply port settings atomically. - * - * These settings will be applied holistically, and to the extent possible - * atomically to a given port. In the event of a failure a rollback is - * attempted. If the rollback fails there will be inconsistent state. This - * failure mode returns the error code "rollback failure". For more details see - * the docs on the [`PortSettings`] type. - */ - #[endpoint { - method = POST, - path = "/port/{port_id}/settings" - }] - pub(super) async fn port_settings_apply( + async fn port_settings_apply( rqctx: RequestContext>, path: Path, query: Query, @@ -2843,14 +1683,7 @@ mod imp { .map_err(HttpError::from) } - /** - * Clear port settings atomically. - */ - #[endpoint { - method = DELETE, - path = "/port/{port_id}/settings" - }] - pub(super) async fn port_settings_clear( + async fn port_settings_clear( rqctx: RequestContext>, path: Path, query: Query, @@ -2867,14 +1700,7 @@ mod imp { .map_err(HttpError::from) } - /** - * Get port settings atomically. - */ - #[endpoint { - method = GET, - path = "/port/{port_id}/settings" - }] - pub(super) async fn port_settings_get( + async fn port_settings_get( rqctx: RequestContext>, path: Path, query: Query, @@ -2891,15 +1717,7 @@ mod imp { .map_err(HttpError::from) } - /// Get switch identifiers. - /// - /// This endpoint returns the switch identifiers, which can be used for - /// consistent field definitions across oximeter time series schemas. - #[endpoint { - method = GET, - path = "/switch/identifiers", - }] - pub(super) async fn switch_identifiers( + async fn switch_identifiers( rqctx: RequestContext>, ) -> Result, HttpError> { let switch = &rqctx.context(); @@ -2910,29 +1728,14 @@ mod imp { .map(HttpResponseOk) } - /// Collect the link data consumed by `tfportd`. This app-specific convenience - /// routine is meant to reduce the time and traffic expended on this once-per- - /// second operation, by consolidating multiple per-link requests into a single - /// per-switch request. - #[endpoint { - method = GET, - path = "/links/tfport_data", - }] - pub(super) async fn tfport_data( + async fn tfport_data( rqctx: RequestContext>, ) -> Result>, HttpError> { let switch = &rqctx.context(); Ok(HttpResponseOk(switch.all_tfport_data())) } - /** - * Get NATv4 generation number - */ - #[endpoint { - method = GET, - path = "/rpw/nat/ipv4/gen" - }] - pub(super) async fn ipv4_nat_generation( + async fn ipv4_nat_generation( rqctx: RequestContext>, ) -> Result, HttpError> { let switch = rqctx.context(); @@ -2940,14 +1743,7 @@ mod imp { Ok(HttpResponseOk(nat::get_ipv4_nat_generation(switch))) } - /** - * Trigger NATv4 Reconciliation - */ - #[endpoint { - method = POST, - path = "/rpw/nat/ipv4/trigger" - }] - pub(super) async fn ipv4_nat_trigger_update( + async fn ipv4_nat_trigger_update( rqctx: RequestContext>, ) -> Result, HttpError> { let switch = rqctx.context(); @@ -2961,35 +1757,14 @@ mod imp { } } - /** - * Get the list of P4 tables - */ - #[endpoint { - method = GET, - path = "/table" - }] - pub(super) async fn table_list( + async fn table_list( rqctx: RequestContext>, ) -> Result>, HttpError> { let switch: &Switch = rqctx.context(); Ok(HttpResponseOk(crate::table::list(switch))) } - #[derive(Deserialize, Serialize, JsonSchema)] - struct TableParam { - table: String, - } - - /** - * Get the contents of a single P4 table. - * The name of the table should match one of those returned by the - * `table_list()` call. - */ - #[endpoint { - method = GET, - path = "/table/{table}/dump" - }] - pub(super) async fn table_dump( + async fn table_dump( rqctx: RequestContext>, path: Path, ) -> Result, HttpError> { @@ -3000,22 +1775,7 @@ mod imp { .map_err(HttpError::from) } - #[derive(Deserialize, Serialize, JsonSchema)] - struct CounterSync { - /// Force a sync of the counters from the ASIC to memory, even if the - /// default refresh timeout hasn't been reached. - force_sync: bool, - } - /** - * Get any counter data from a single P4 match-action table. - * The name of the table should match one of those returned by the - * `table_list()` call. - */ - #[endpoint { - method = GET, - path = "/table/{table}/counters" - }] - pub(super) async fn table_counters( + async fn table_counters( rqctx: RequestContext>, query: Query, path: Path, @@ -3028,14 +1788,7 @@ mod imp { .map_err(HttpError::from) } - /** - * Get a list of all the available p4-defined counters. - */ - #[endpoint { - method = GET, - path = "/counters/p4", - }] - pub(super) async fn counter_list( + async fn counter_list( _rqctx: RequestContext>, ) -> Result>, HttpError> { match counters::get_counter_names() { @@ -3044,21 +1797,7 @@ mod imp { } } - #[derive(Deserialize, Serialize, JsonSchema)] - struct CounterPath { - counter: String, - } - - /** - * Reset a single p4-defined counter. - * The name of the counter should match one of those returned by the - * `counter_list()` call. - */ - #[endpoint { - method = POST, - path = "/counters/p4/{counter}/reset", - }] - pub(super) async fn counter_reset( + async fn counter_reset( rqctx: RequestContext>, path: Path, ) -> Result { @@ -3071,16 +1810,7 @@ mod imp { } } - /** - * Get the values for a given counter. - * The name of the counter should match one of those returned by the - * `counter_list()` call. - */ - #[endpoint { - method = GET, - path = "/counters/p4/{counter}", - }] - pub(super) async fn counter_get( + async fn counter_get( rqctx: RequestContext>, query: Query, path: Path, @@ -3095,37 +1825,10 @@ mod imp { .map_err(HttpError::from) } - /// Used to identify a multicast group by IP address, the main - /// identifier for a multicast group. - #[derive(Deserialize, Serialize, JsonSchema)] - pub struct MulticastGroupIpParam { - pub group_ip: IpAddr, - } - - /// Used to identify a multicast group by ID. - /// - /// If not provided, it will return all multicast groups. - #[derive(Deserialize, Serialize, JsonSchema)] - pub struct MulticastGroupIdParam { - pub group_id: Option, - } - - /** - * Create an external-only multicast group configuration. - * - * External-only groups are used for IPv4 and non-admin-scoped IPv6 multicast - * traffic that doesn't require replication infrastructure. These groups use - * simple forwarding tables and require a NAT target. - */ - #[endpoint { - method = POST, - path = "/multicast/external-groups", - }] - pub(super) async fn multicast_group_create_external( - rqctx: RequestContext>, - group: TypedBody, - ) -> Result, HttpError> - { + async fn multicast_group_create_external( + rqctx: RequestContext>, + group: TypedBody, + ) -> Result, HttpError> { let switch: &Switch = rqctx.context(); let entry = group.into_inner(); @@ -3134,22 +1837,10 @@ mod imp { .map_err(HttpError::from) } - /** - * Create an internal multicast group configuration. - * - * Internal groups are used for admin-scoped IPv6 multicast traffic that - * requires replication infrastructure. These groups support both external - * and underlay members with full replication capabilities. - */ - #[endpoint { - method = POST, - path = "/multicast/groups", - }] - pub(super) async fn multicast_group_create( - rqctx: RequestContext>, - group: TypedBody, - ) -> Result, HttpError> - { + async fn multicast_group_create( + rqctx: RequestContext>, + group: TypedBody, + ) -> Result, HttpError> { let switch: &Switch = rqctx.context(); let entry = group.into_inner(); @@ -3158,14 +1849,7 @@ mod imp { .map_err(HttpError::from) } - /** - * Delete a multicast group configuration by IP address. - */ - #[endpoint { - method = DELETE, - path = "/multicast/groups/{group_ip}", - }] - pub(super) async fn multicast_group_delete( + async fn multicast_group_delete( rqctx: RequestContext>, path: Path, ) -> Result { @@ -3177,14 +1861,7 @@ mod imp { .map_err(HttpError::from) } - /** - * Reset all multicast group configurations. - */ - #[endpoint { - method = DELETE, - path = "/multicast/groups", - }] - pub(super) async fn multicast_reset( + async fn multicast_reset( rqctx: RequestContext>, ) -> Result { let switch: &Switch = rqctx.context(); @@ -3194,17 +1871,10 @@ mod imp { .map_err(HttpError::from) } - /** - * Get the multicast group configuration for a given group IP address. - */ - #[endpoint { - method = GET, - path = "/multicast/groups/{group_ip}", - }] - pub(super) async fn multicast_group_get( + async fn multicast_group_get( rqctx: RequestContext>, path: Path, - ) -> Result, HttpError> { + ) -> Result, HttpError> { let switch: &Switch = rqctx.context(); let ip = path.into_inner().group_ip; @@ -3214,21 +1884,11 @@ mod imp { .map_err(HttpError::from) } - /** - * Update an internal multicast group configuration for a given group IP address. - * - * Internal groups are used for admin-scoped IPv6 multicast traffic that - * requires replication infrastructure with external and underlay members. - */ - #[endpoint { - method = PUT, - path = "/multicast/groups/{group_ip}", - }] - pub(super) async fn multicast_group_update( + async fn multicast_group_update( rqctx: RequestContext>, path: Path, - group: TypedBody, - ) -> Result, HttpError> { + group: TypedBody, + ) -> Result, HttpError> { let switch: &Switch = rqctx.context(); let ip = path.into_inner().group_ip; @@ -3248,22 +1908,11 @@ mod imp { .map_err(HttpError::from) } - /** - * Update an external-only multicast group configuration for a given group IP address. - * - * External-only groups are used for IPv4 and non-admin-scoped IPv6 multicast - * traffic that doesn't require replication infrastructure. - */ - #[endpoint { - method = PUT, - path = "/multicast/external-groups/{group_ip}", - }] - pub(super) async fn multicast_group_update_external( + async fn multicast_group_update_external( rqctx: RequestContext>, path: Path, - group: TypedBody, - ) -> Result, HttpError> - { + group: TypedBody, + ) -> Result, HttpError> { let switch: &Switch = rqctx.context(); let entry = group.into_inner(); let ip = path.into_inner().group_ip; @@ -3273,22 +1922,13 @@ mod imp { .map_err(HttpError::from) } - /** - * List all multicast groups. - */ - #[endpoint { - method = GET, - path = "/multicast/groups", - }] - pub(super) async fn multicast_groups_list( + async fn multicast_groups_list( rqctx: RequestContext>, query_params: Query< PaginationParams, >, - ) -> Result< - HttpResponseOk>, - HttpError, - > { + ) -> Result>, HttpError> + { let switch: &Switch = rqctx.context(); // If a group ID is provided, get the group by ID @@ -3314,29 +1954,20 @@ mod imp { Ok(HttpResponseOk(ResultsPage::new( entries, &EmptyScanParams {}, - |e: &mcast::MulticastGroupResponse, _| MulticastGroupIpParam { - group_ip: e.ip(), + |e: &MulticastGroupResponse, _| MulticastGroupIpParam { + group_ip: e.group_ip, }, )?)) } - /** - * List all multicast groups with a given tag. - */ - #[endpoint { - method = GET, - path = "/multicast/tags/{tag}", - }] - pub(super) async fn multicast_groups_list_by_tag( + async fn multicast_groups_list_by_tag( rqctx: RequestContext>, path: Path, query_params: Query< PaginationParams, >, - ) -> Result< - HttpResponseOk>, - HttpError, - > { + ) -> Result>, HttpError> + { let switch: &Switch = rqctx.context(); let tag = path.into_inner().tag; @@ -3359,20 +1990,13 @@ mod imp { Ok(HttpResponseOk(ResultsPage::new( entries, &EmptyScanParams {}, - |e: &mcast::MulticastGroupResponse, _| MulticastGroupIpParam { - group_ip: e.ip(), + |e: &MulticastGroupResponse, _| MulticastGroupIpParam { + group_ip: e.group_ip, }, )?)) } - /** - * Delete all multicast groups (and associated routes) with a given tag. - */ - #[endpoint { - method = DELETE, - path = "/multicast/tags/{tag}", - }] - pub(super) async fn multicast_reset_by_tag( + async fn multicast_reset_by_tag( rqctx: RequestContext>, path: Path, ) -> Result { @@ -3384,14 +2008,7 @@ mod imp { .map_err(HttpError::from) } - /** - * Delete all multicast groups (and associated routes) without a tag. - */ - #[endpoint { - method = DELETE, - path = "/multicast/untagged", - }] - pub(super) async fn multicast_reset_untagged( + async fn multicast_reset_untagged( rqctx: RequestContext>, ) -> Result { let switch: &Switch = rqctx.context(); @@ -3402,149 +2019,63 @@ mod imp { } } -pub use imp::*; +// Convert a port ID path into a `QsfpPort` if possible. This is generally used +// for endpoints which only apply to the QSFP ports, such as transceiver +// management. +fn path_to_qsfp(path: Path) -> Result { + let port_id = path.into_inner().port_id; + if let PortId::Qsfp(qsfp_port) = port_id { + Ok(qsfp_port) + } else { + Err(HttpError::from(DpdError::NotAQsfpPort { port_id })) + } +} + +fn build_info() -> BuildInfo { + BuildInfo { + version: env!("CARGO_PKG_VERSION").to_string(), + git_sha: env!("VERGEN_GIT_SHA").to_string(), + git_commit_timestamp: env!("VERGEN_GIT_COMMIT_TIMESTAMP").to_string(), + git_branch: env!("VERGEN_GIT_BRANCH").to_string(), + rustc_semver: env!("VERGEN_RUSTC_SEMVER").to_string(), + rustc_channel: env!("VERGEN_RUSTC_CHANNEL").to_string(), + rustc_host_triple: env!("VERGEN_RUSTC_HOST_TRIPLE").to_string(), + rustc_commit_sha: env!("VERGEN_RUSTC_COMMIT_HASH").to_string(), + cargo_triple: env!("VERGEN_CARGO_TARGET_TRIPLE").to_string(), + debug: env!("VERGEN_CARGO_DEBUG").parse().unwrap(), + opt_level: env!("VERGEN_CARGO_OPT_LEVEL").parse().unwrap(), + sde_commit_sha: env!("SDE_COMMIT_SHA").to_string(), + } +} + +impl From<&crate::link::Link> for LinkSettings { + fn from(l: &crate::link::Link) -> Self { + let mut addrs: HashSet = HashSet::new(); + for a in &l.ipv4 { + addrs.insert(a.addr.into()); + } + for a in &l.ipv6 { + addrs.insert(a.addr.into()); + } + LinkSettings { + params: LinkCreate { + lane: Some(l.link_id), + speed: l.config.speed, + fec: l.config.fec, + autoneg: l.config.autoneg, + kr: l.config.kr, + tx_eq: l.tx_eq, + }, + addrs, + } + } +} pub fn http_api() -> dropshot::ApiDescription> { - let mut api = dropshot::ApiDescription::new(); - api.register(build_info).unwrap(); - api.register(dpd_version).unwrap(); - api.register(dpd_uptime).unwrap(); - api.register(reset_all).unwrap(); - api.register(reset_all_tagged).unwrap(); - api.register(table_list).unwrap(); - api.register(table_dump).unwrap(); - api.register(table_counters).unwrap(); - api.register(counter_list).unwrap(); - api.register(counter_get).unwrap(); - api.register(counter_reset).unwrap(); - api.register(arp_list).unwrap(); - api.register(arp_reset).unwrap(); - api.register(arp_get).unwrap(); - api.register(arp_create).unwrap(); - api.register(arp_delete).unwrap(); - api.register(ndp_list).unwrap(); - api.register(ndp_reset).unwrap(); - api.register(ndp_get).unwrap(); - api.register(ndp_create).unwrap(); - api.register(ndp_delete).unwrap(); - api.register(route_ipv4_list).unwrap(); - api.register(route_ipv4_get).unwrap(); - api.register(route_ipv4_add).unwrap(); - api.register(route_ipv4_set).unwrap(); - api.register(route_ipv4_delete).unwrap(); - api.register(route_ipv4_delete_target).unwrap(); - api.register(route_ipv6_list).unwrap(); - api.register(route_ipv6_get).unwrap(); - api.register(route_ipv6_add).unwrap(); - api.register(route_ipv6_set).unwrap(); - api.register(route_ipv6_delete).unwrap(); - api.register(route_ipv6_delete_target).unwrap(); - - api.register(backplane_map).unwrap(); - api.register(port_backplane_link).unwrap(); - - api.register(channels_list).unwrap(); - api.register(port_list).unwrap(); - api.register(port_get).unwrap(); - - api.register(management_mode_get).unwrap(); - api.register(management_mode_set).unwrap(); - api.register(led_get).unwrap(); - api.register(led_set).unwrap(); - api.register(led_set_auto).unwrap(); - api.register(leds_list).unwrap(); - - api.register(transceivers_list).unwrap(); - api.register(transceiver_get).unwrap(); - api.register(transceiver_reset).unwrap(); - api.register(transceiver_power_set).unwrap(); - api.register(transceiver_power_get).unwrap(); - api.register(transceiver_monitors_get).unwrap(); - api.register(transceiver_datapath_get).unwrap(); - - api.register(link_create).unwrap(); - api.register(link_get).unwrap(); - api.register(link_delete).unwrap(); - api.register(link_list).unwrap(); - api.register(link_list_all).unwrap(); - - api.register(link_enabled_get).unwrap(); - api.register(link_enabled_set).unwrap(); - api.register(link_ipv6_enabled_get).unwrap(); - api.register(link_ipv6_enabled_set).unwrap(); - api.register(link_kr_get).unwrap(); - api.register(link_kr_set).unwrap(); - api.register(link_autoneg_set).unwrap(); - api.register(link_autoneg_get).unwrap(); - api.register(link_prbs_set).unwrap(); - api.register(link_prbs_get).unwrap(); - api.register(link_linkup_get).unwrap(); - api.register(link_up_counters_list).unwrap(); - api.register(link_up_counters_get).unwrap(); - api.register(link_fsm_counters_get).unwrap(); - api.register(link_fault_get).unwrap(); - api.register(link_fault_clear).unwrap(); - api.register(link_fault_inject).unwrap(); - api.register(link_ipv4_list).unwrap(); - api.register(link_ipv4_create).unwrap(); - api.register(link_ipv4_reset).unwrap(); - api.register(link_ipv4_delete).unwrap(); - api.register(link_ipv6_list).unwrap(); - api.register(link_ipv6_create).unwrap(); - api.register(link_ipv6_delete).unwrap(); - api.register(link_ipv6_reset).unwrap(); - api.register(link_nat_only_set).unwrap(); - api.register(link_nat_only_get).unwrap(); - api.register(link_mac_get).unwrap(); - // TODO-correctness: A link's MAC address should be determined by the FRUID - // data, not under the control of the client. We really only need this for - // the integration tests in `dpd-client`. We should consider removing it for - // production. - api.register(link_mac_set).unwrap(); - api.register(link_history_get).unwrap(); - - api.register(loopback_ipv4_list).unwrap(); - api.register(loopback_ipv4_create).unwrap(); - api.register(loopback_ipv4_delete).unwrap(); - api.register(loopback_ipv6_list).unwrap(); - api.register(loopback_ipv6_create).unwrap(); - api.register(loopback_ipv6_delete).unwrap(); - - api.register(nat_ipv6_addresses_list).unwrap(); - api.register(nat_ipv6_list).unwrap(); - api.register(nat_ipv6_get).unwrap(); - api.register(nat_ipv6_create).unwrap(); - api.register(nat_ipv6_delete).unwrap(); - api.register(nat_ipv6_reset).unwrap(); - api.register(nat_ipv4_addresses_list).unwrap(); - api.register(nat_ipv4_list).unwrap(); - api.register(nat_ipv4_get).unwrap(); - api.register(nat_ipv4_create).unwrap(); - api.register(nat_ipv4_delete).unwrap(); - api.register(nat_ipv4_reset).unwrap(); - api.register(oximeter_collect_meta_endpoint).unwrap(); - - api.register(port_settings_apply).unwrap(); - api.register(port_settings_clear).unwrap(); - api.register(port_settings_get).unwrap(); - api.register(switch_identifiers).unwrap(); - api.register(tfport_data).unwrap(); - - api.register(ipv4_nat_generation).unwrap(); - api.register(ipv4_nat_trigger_update).unwrap(); - - api.register(multicast_group_create).unwrap(); - api.register(multicast_group_create_external).unwrap(); - api.register(multicast_reset).unwrap(); - api.register(multicast_group_delete).unwrap(); - api.register(multicast_group_update).unwrap(); - api.register(multicast_group_update_external).unwrap(); - api.register(multicast_group_get).unwrap(); - api.register(multicast_groups_list).unwrap(); - api.register(multicast_groups_list_by_tag).unwrap(); - api.register(multicast_reset_by_tag).unwrap(); - api.register(multicast_reset_untagged).unwrap(); + #[allow(unused_mut)] + let mut api = dpd_api_mod::api_description::().unwrap(); + // TODO: need to move these into dpd-api #[cfg(feature = "tofino_asic")] crate::tofino_api_server::init(&mut api); #[cfg(feature = "softnpu")] @@ -3649,12 +2180,12 @@ pub async fn api_server_manager( #[cfg(test)] mod tests { - use super::BuildInfo; + use super::build_info; use std::process::Command; #[test] fn test_build_info() { - let info = BuildInfo::default(); + let info = build_info(); println!("{info:#?}"); let out = Command::new("git") .arg("rev-parse") diff --git a/dpd/src/arp.rs b/dpd/src/arp.rs index 7e033ee..96dc5e6 100644 --- a/dpd/src/arp.rs +++ b/dpd/src/arp.rs @@ -12,7 +12,6 @@ use std::ops::Bound; use chrono::prelude::*; use slog::debug; -use crate::api_server; use crate::types::{DpdError, DpdResult}; use crate::{table, Switch}; use common::network::MacAddr; @@ -153,7 +152,7 @@ pub fn get_range_ipv4( switch: &Switch, last: Option<&Ipv4Addr>, mut max: u32, -) -> DpdResult> { +) -> DpdResult> { if max > 32 { max = 32 }; @@ -168,7 +167,7 @@ pub fn get_range_ipv4( .v4 .range((lower_bound, Bound::Unbounded)) .take(usize::try_from(max).expect("invalid usize")) - .map(|(ip, entry)| api_server::ArpEntry { + .map(|(ip, entry)| dpd_api::ArpEntry { tag: entry.tag.clone(), ip: IpAddr::V4((*ip).into()), mac: entry.mac, @@ -183,7 +182,7 @@ pub fn get_range_ipv6( switch: &Switch, last: Option<&Ipv6Addr>, mut max: u32, -) -> DpdResult> { +) -> DpdResult> { max = std::cmp::max(max, 32); let lower_bound = match last { @@ -196,7 +195,7 @@ pub fn get_range_ipv6( .v6 .range((lower_bound, Bound::Unbounded)) .take(usize::try_from(max).expect("invalid usize")) - .map(|(ip, entry)| api_server::ArpEntry { + .map(|(ip, entry)| dpd_api::ArpEntry { tag: entry.tag.clone(), ip: IpAddr::V6((*ip).into()), mac: entry.mac, diff --git a/dpd/src/counters.rs b/dpd/src/counters.rs index 6b6d01b..530775f 100644 --- a/dpd/src/counters.rs +++ b/dpd/src/counters.rs @@ -19,13 +19,13 @@ use std::sync::Mutex; use crate::table; use crate::types::{DpdError, DpdResult}; -use crate::views; use crate::Switch; use aal::MatchParse; use aal_macros::*; use asic::Handle; use anyhow::Context; +use dpd_types::views; // Counters in an indirect table are accessed by their index number rather than // a key. Still, we define a key anyway to allow us to use the direct counter diff --git a/dpd/src/fault.rs b/dpd/src/fault.rs index 6ab2d1a..5eb94fd 100644 --- a/dpd/src/fault.rs +++ b/dpd/src/fault.rs @@ -8,11 +8,9 @@ use std::fmt; use std::sync::Mutex; use std::time::{Duration, Instant}; -use schemars::JsonSchema; -use serde::Deserialize; -use serde::Serialize; +use dpd_types::fault::Fault; +use dpd_types::link::LinkFsmCounter; -use crate::link::LinkFsmCounter; use asic::FsmStats; use asic::PortFsmState; @@ -88,16 +86,6 @@ impl Limiter { } } -/// A Fault represents a specific kind of failure, and carries some additional -/// context. Currently Faults are only used to describe Link failures, but -/// there is no reason they couldn't be used elsewhere. -#[derive(Clone, Debug, PartialEq, Deserialize, JsonSchema, Serialize)] -pub enum Fault { - LinkFlap(String), - Autoneg(String), - Injected(String), -} - /// The Faultable trait is implemented by metadata structures whose values could /// indicate that the link/device/construct has entered a failed state that /// requires admin intervention to correct. diff --git a/dpd/src/link.rs b/dpd/src/link.rs index 35805a5..0bff698 100644 --- a/dpd/src/link.rs +++ b/dpd/src/link.rs @@ -6,9 +6,9 @@ //! Manage logical Ethernet links on the Sidecar switch. -use crate::api_server::LinkCreate; -use crate::fault; +use crate::fault::AutonegTracker; use crate::fault::Faultable; +use crate::fault::LinkUpTracker; use crate::ports::AdminEvent; use crate::ports::Event; use crate::table::mcast; @@ -16,9 +16,9 @@ use crate::table::port_ip; use crate::table::port_mac; use crate::table::port_nat; use crate::table::MacOps; +use crate::transceivers::qsfp_xcvr_mpn; use crate::types::DpdError; use crate::types::DpdResult; -use crate::views; use crate::MacAddr; use crate::Switch; use aal::AsicId; @@ -34,6 +34,12 @@ use common::ports::PortMedia; use common::ports::PortPrbsMode; use common::ports::PortSpeed; use common::ports::TxEq; +use dpd_api::LinkCreate; +use dpd_types::link::LinkFsmCounters; +use dpd_types::link::LinkId; +use dpd_types::link::LinkState; +use dpd_types::link::LinkUpCounter; +use dpd_types::views; use schemars::JsonSchema; use serde::Deserialize; use serde::Serialize; @@ -44,7 +50,6 @@ use slog::o; use std::collections::btree_map::Entry; use std::collections::BTreeMap; use std::collections::BTreeSet; -use std::fmt; use std::net::Ipv4Addr; use std::net::Ipv6Addr; use std::sync::Arc; @@ -236,10 +241,10 @@ pub struct Link { pub ipv6: BTreeSet, /// Tracks the history of linkup/linkdown transitions, allowing us to /// detect flapping links. - pub linkup_tracker: fault::LinkUpTracker, + pub linkup_tracker: LinkUpTracker, /// Tracks the link's progress through the autonegotiation/link-training /// finite state machine, so we can detect and diagnose linkup failures. - pub autoneg_tracker: fault::AutonegTracker, + pub autoneg_tracker: AutonegTracker, /// The configuration of the link as requested by the user / sled-agent pub config: LinkConfig, @@ -248,6 +253,54 @@ pub struct Link { plumbed: LinkPlumbed, } +impl From<&Link> for views::Link { + fn from(m: &Link) -> Self { + Self { + port_id: m.port_id, + link_id: m.link_id, + tofino_connector: m.port_hdl.connector.as_u16(), + asic_id: m.asic_port_id, + presence: m.presence, + fsm_state: m.fsm_state.to_string(), + media: m.media, + link_state: m.link_state.clone(), + ipv6_enabled: m.ipv6_enabled, + enabled: m.config.enabled, + prbs: m.config.prbs, + speed: m.config.speed, + fec: m.get_fec(), + kr: m.config.kr, + autoneg: m.config.autoneg, + address: m.config.mac, + } + } +} + +impl From for views::Link { + fn from(m: crate::link::Link) -> Self { + Self::from(&m) + } +} + +impl From<&Link> for views::TfportData { + fn from(m: &Link) -> Self { + Self { + port_id: m.port_id, + link_id: m.link_id, + asic_id: m.asic_port_id, + mac: m.config.mac, + ipv6_enabled: m.ipv6_enabled, + link_local: m.link_local(), + } + } +} + +impl From for views::TfportData { + fn from(m: Link) -> Self { + Self::from(&m) + } +} + // This struct represents the configuration of the link requested by the // user/sled-agent #[derive(Debug)] @@ -407,8 +460,8 @@ impl Link { media: PortMedia::None, ipv4: BTreeSet::new(), ipv6: BTreeSet::new(), - linkup_tracker: fault::LinkUpTracker::default(), - autoneg_tracker: fault::AutonegTracker::default(), + linkup_tracker: LinkUpTracker::default(), + autoneg_tracker: AutonegTracker::default(), config, plumbed, @@ -438,141 +491,6 @@ impl Link { } } -/// An identifier for a link within a switch port. -/// -/// A switch port identified by a [`PortId`] may have multiple links within it, -/// each identified by a `LinkId`. These are unique within a switch port only. -#[derive( - Clone, - Copy, - Debug, - Deserialize, - Eq, - Hash, - JsonSchema, - Ord, - PartialEq, - PartialOrd, - Serialize, -)] -pub struct LinkId(pub u8); - -impl From for u8 { - fn from(l: LinkId) -> Self { - l.0 - } -} - -impl From for u16 { - fn from(l: LinkId) -> Self { - l.0 as u16 - } -} - -impl From for LinkId { - fn from(x: u8) -> Self { - Self(x) - } -} - -impl fmt::Display for LinkId { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", self.0) - } -} - -/// The state of a data link with a peer. -#[derive(Clone, Deserialize, JsonSchema, PartialEq, Serialize)] -#[serde(rename_all = "snake_case")] -pub enum LinkState { - /// An error was encountered while trying to configure the link in the - /// switch hardware. - ConfigError(String), - /// The link is up. - Up, - /// The link is down. - Down, - /// The Link is offline due to a fault - Faulted(fault::Fault), - /// The link's state is not known. - Unknown, -} - -impl LinkState { - /// A shortcut to tell whether a LinkState is Faulted or not, allowing for - /// cleaner code in the callers. - pub fn is_fault(&self) -> bool { - matches!(self, LinkState::Faulted(_)) - } - - /// If the link is in a faulted state, return the Fault. If not, return - /// None. - pub fn get_fault(&self) -> Option { - match self { - LinkState::Faulted(f) => Some(f.clone()), - _ => None, - } - } -} - -impl std::fmt::Display for LinkState { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - match self { - LinkState::Up => write!(f, "Up"), - LinkState::Down => write!(f, "Down"), - LinkState::ConfigError(_) => write!(f, "ConfigError"), - LinkState::Faulted(_) => write!(f, "Faulted"), - LinkState::Unknown => write!(f, "Unknown"), - } - } -} - -impl std::fmt::Debug for LinkState { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - match self { - LinkState::Up => write!(f, "Up"), - LinkState::Down => write!(f, "Down"), - LinkState::ConfigError(detail) => { - write!(f, "ConfigError - {:?}", detail) - } - LinkState::Faulted(reason) => write!(f, "Faulted - {:?}", reason), - LinkState::Unknown => write!(f, "Unknown"), - } - } -} - -/// Reports how many times a link has transitioned from Down to Up. -#[derive(Clone, Deserialize, JsonSchema, PartialEq, Serialize)] -pub struct LinkUpCounter { - /// Link being reported - pub link_path: String, - /// LinkUp transitions since the link was last enabled - pub current: u32, - /// LinkUp transitions since the link was created - pub total: u32, -} - -/// Reports how many times a given autoneg/link-training state has been entered -#[derive(Clone, Deserialize, JsonSchema, PartialEq, Serialize)] -pub struct LinkFsmCounter { - /// FSM state being counted - pub state_name: String, - /// Times entered since the link was last enabled - pub current: u32, - /// Times entered since the link was created - pub total: u32, -} - -/// Reports all the autoneg/link-training states a link has transitioned into. -#[derive(Clone, Deserialize, JsonSchema, PartialEq, Serialize)] -pub struct LinkFsmCounters { - /// Link being reported - pub link_path: String, - /// All the states this link has entered, along with counts of how many - /// times each state was entered. - pub counters: Vec, -} - /// Reports the bit-error rate (BER) for a link. #[derive(Clone, Deserialize, JsonSchema, PartialEq, Serialize)] pub struct Ber { @@ -1594,7 +1512,7 @@ impl Switch { &self, port_id: PortId, link_id: LinkId, - ) -> DpdResult> { + ) -> DpdResult> { self.link_fetch(port_id, link_id, |link| link.link_state.get_fault()) } @@ -1602,7 +1520,7 @@ impl Switch { fn link_set_fault_locked( &self, link: &mut Link, - fault: fault::Fault, + fault: dpd_types::fault::Fault, ) -> DpdResult<()> { if !link.link_state.is_fault() { link.link_state = LinkState::Faulted(fault.clone()); @@ -1618,7 +1536,7 @@ impl Switch { &self, port_id: PortId, link_id: LinkId, - fault: fault::Fault, + fault: dpd_types::fault::Fault, ) -> DpdResult<()> { self.link_update(port_id, link_id, |link| { self.link_set_fault_locked(link, fault) @@ -1825,7 +1743,7 @@ async fn reconcile_link( .as_qsfp() .cloned(); if let Some(qsfp) = qsfp { - qsfp.xcvr_mpn().unwrap_or(None) + qsfp_xcvr_mpn(&qsfp).unwrap_or(None) } else { None } diff --git a/dpd/src/macaddrs.rs b/dpd/src/macaddrs.rs index 71535b4..7074faf 100644 --- a/dpd/src/macaddrs.rs +++ b/dpd/src/macaddrs.rs @@ -6,10 +6,10 @@ use std::collections::BTreeSet; +use dpd_types::link::LinkId; use slog::debug; use slog::o; -use crate::link::LinkId; use crate::types::DpdError; use crate::types::DpdResult; use crate::Switch; @@ -22,13 +22,13 @@ use common::ports::PORT_COUNT_REAR; cfg_if::cfg_if! { if #[cfg(feature = "tofino_asic")] { use std::convert::TryFrom; - use crate::api_server::LinkCreate; use crate::table::mcast; use crate::table::port_mac; use crate::table::MacOps; use common::ports::PortFec; use common::ports::PortSpeed; use common::ports::InternalPort; + use dpd_api::LinkCreate; use transceiver_controller::Error as TransceiverError; } } @@ -529,7 +529,6 @@ fn mac_offset(port_id: PortId, link_id: LinkId) -> Option { #[cfg(test)] mod tests { use super::mac_offset; - use crate::link::LinkId; use crate::macaddrs::BaseMac; use crate::macaddrs::MacManagement; use common::network::MacAddr; @@ -537,6 +536,7 @@ mod tests { use common::ports::PortId; use common::ports::QsfpPort; use common::ports::RearPort; + use dpd_types::link::LinkId; use slog::Drain; use std::convert::TryFrom; diff --git a/dpd/src/main.rs b/dpd/src/main.rs index 38786e6..30b0192 100644 --- a/dpd/src/main.rs +++ b/dpd/src/main.rs @@ -16,6 +16,9 @@ use std::sync::Mutex; use std::sync::MutexGuard; use anyhow::Context; +use dpd_api::LinkCreate; +use dpd_types::link::LinkId; +use dpd_types::oxstats::OximeterMetadata; use futures::stream::StreamExt; use libc::c_int; use signal_hook::consts::SIGHUP; @@ -32,15 +35,14 @@ use tokio::sync::Mutex as TokioMutex; use tokio::time::sleep; use tokio::time::Duration; -use crate::api_server::LinkCreate; use crate::macaddrs::BaseMac; use crate::port_map::SidecarRevision; use crate::rpw::WorkflowServer; -use crate::switch_identifiers::SwitchIdentifiers; use crate::switch_port::SwitchPorts; use aal::{ActionParse, AsicError, MatchParse}; use common::network::MacAddr; use common::ports::PortId; +use dpd_types::switch_identifiers::SwitchIdentifiers; use table::Table; use types::*; @@ -77,7 +79,6 @@ mod tofino_api_server; mod transceivers; mod types; mod version; -mod views; #[derive(Debug, StructOpt)] #[structopt(name = "dpd", about = "dataplane controller for oxide switch")] @@ -202,7 +203,7 @@ pub struct Switch { pub loopback: Mutex, pub identifiers: Mutex>, pub oximeter_producer: Mutex>, - pub oximeter_meta: Mutex>, + pub oximeter_meta: Mutex>, pub reconciler: link::LinkReconciler, pub mcast: Mutex, @@ -404,10 +405,10 @@ impl Switch { pub fn table_dump( &self, t: table::TableType, - ) -> DpdResult { + ) -> DpdResult { let t = self.table_get(t)?; - Ok(views::Table { + Ok(dpd_types::views::Table { name: t.name.to_string(), size: t.usage.size as usize, entries: t @@ -421,7 +422,7 @@ impl Switch { .map(|vec| { vec.into_iter() .map(|(key, action): (M, A)| { - views::TableEntry::new(key, action) + dpd_types::views::TableEntry::new(key, action) }) .collect() })?, @@ -433,7 +434,7 @@ impl Switch { &self, force_sync: bool, t: table::TableType, - ) -> DpdResult> { + ) -> DpdResult> { let t = self.table_get(t)?; t.get_counters::(&self.asic_hdl, force_sync) @@ -446,7 +447,7 @@ impl Switch { .map(|vec| { vec.into_iter() .map(|(key, data): (M, aal::CounterData)| { - views::TableCounterEntry::new(key, data) + dpd_types::views::TableCounterEntry::new(key, data) }) .collect() }) @@ -461,7 +462,7 @@ impl Switch { pub fn allocate_mac_address( &self, port_id: PortId, - link_id: link::LinkId, + link_id: LinkId, ) -> DpdResult { let mut mgr = self.mac_mgmt.lock().unwrap(); mgr.allocate_mac_address(port_id, link_id) @@ -756,7 +757,7 @@ async fn sidecar_main(mut switch: Switch) -> anyhow::Result<()> { fec: Some(common::ports::PortFec::RS), autoneg: true, kr: true, - lane: Some(crate::link::LinkId(0)), + lane: Some(dpd_types::link::LinkId(0)), tx_eq: None, }; Some((*port_id, create)) @@ -807,6 +808,11 @@ fn main() -> anyhow::Result<()> { } fn print_openapi() -> anyhow::Result<()> { + // TODO: Once migrated to the OpenAPI manager, this should use the stub API + // description. But there are currently additional backend-specific methods + // added by the tofino-asic and softnpu features -- those would need to be + // migrated to the API trait (possibly via a uniform API across all + // backends). crate::api_server::http_api() .openapi( "Oxide Switch Dataplane Controller", diff --git a/dpd/src/mcast/mod.rs b/dpd/src/mcast/mod.rs index 8d2e5bd..a2ffa00 100644 --- a/dpd/src/mcast/mod.rs +++ b/dpd/src/mcast/mod.rs @@ -11,23 +11,29 @@ use std::{ collections::{BTreeMap, HashSet}, - fmt, net::{IpAddr, Ipv4Addr, Ipv6Addr}, ops::Bound, sync::{Arc, Mutex, Weak}, }; use crate::{ - link::LinkId, table, types::{DpdError, DpdResult}, Switch, }; use aal::{AsicError, AsicOps}; use common::{nat::NatTarget, ports::PortId}; +use dpd_types::{ + link::LinkId, + mcast::{ + Direction, ExternalForwarding, InternalForwarding, IpSrc, + MulticastGroupCreateEntry, MulticastGroupCreateExternalEntry, + MulticastGroupId, MulticastGroupMember, MulticastGroupResponse, + MulticastGroupUpdateEntry, MulticastGroupUpdateExternalEntry, + }, +}; use oxnet::{Ipv4Net, Ipv6Net}; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; + use slog::{debug, error}; mod validate; @@ -36,9 +42,6 @@ use validate::{ validate_not_admin_scoped_ipv6, }; -/// Type alias for multicast group IDs. -pub(crate) type MulticastGroupId = u16; - #[derive(Debug)] struct ScopedIdInner(MulticastGroupId, Weak>>); @@ -75,49 +78,6 @@ impl From for ScopedGroupId { } } -/// Source filter match key for multicast traffic. -#[derive( - Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize, JsonSchema, -)] -pub(crate) enum IpSrc { - /// Exact match for the source IP address. - Exact(IpAddr), - /// Subnet match for the source IP address. - Subnet(Ipv4Net), -} - -impl fmt::Display for IpSrc { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - IpSrc::Exact(ip) => write!(f, "{}", ip), - IpSrc::Subnet(subnet) => write!(f, "{}", subnet), - } - } -} - -/// Represents a member of a multicast group. -#[derive( - Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize, JsonSchema, -)] -pub(crate) struct MulticastGroupMember { - pub port_id: PortId, - pub link_id: LinkId, - pub direction: Direction, -} - -/// Represents the NAT target for multicast traffic for internal/underlay -/// forwarding. -#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, JsonSchema)] -pub(crate) struct InternalForwarding { - pub nat_target: Option, -} - -/// Represents the forwarding configuration for external multicast traffic. -#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, JsonSchema)] -pub(crate) struct ExternalForwarding { - pub vlan_id: Option, -} - /// Multicast replication configuration (internal only). #[derive(Clone, Debug, Default, PartialEq, Eq)] struct MulticastReplicationInfo { @@ -150,95 +110,23 @@ impl MulticastGroup { fn underlay_group_id(&self) -> Option { self.underlay_group_id.as_ref().map(ScopedGroupId::id) } -} - -/// A multicast group configuration for POST requests for internal (to the rack) -/// groups. -#[derive(Debug, Deserialize, Serialize, JsonSchema)] -pub(crate) struct MulticastGroupCreateEntry { - group_ip: Ipv6Addr, - tag: Option, - sources: Option>, - members: Vec, -} -/// A multicast group configuration for POST requests for external (to the rack) -/// groups. -#[derive(Debug, Deserialize, Serialize, JsonSchema)] -pub(crate) struct MulticastGroupCreateExternalEntry { - group_ip: IpAddr, - tag: Option, - nat_target: NatTarget, - vlan_id: Option, - sources: Option>, -} - -/// Represents a multicast replication entry for PUT requests for internal -/// (to the rack) groups. -#[derive(Debug, Deserialize, Serialize, JsonSchema)] -pub(crate) struct MulticastGroupUpdateEntry { - tag: Option, - sources: Option>, - members: Vec, -} - -/// A multicast group update entry for PUT requests for external (to the rack) -/// groups. -#[derive(Debug, Deserialize, Serialize, JsonSchema)] -pub(crate) struct MulticastGroupUpdateExternalEntry { - tag: Option, - nat_target: NatTarget, - vlan_id: Option, - sources: Option>, -} - -/// Response structure for multicast group operations. -#[derive(Debug, Deserialize, Serialize, JsonSchema)] -pub struct MulticastGroupResponse { - group_ip: IpAddr, - external_group_id: Option, - underlay_group_id: Option, - tag: Option, - int_fwding: InternalForwarding, - ext_fwding: ExternalForwarding, - sources: Option>, - members: Vec, -} - -impl MulticastGroupResponse { - fn new(group_ip: IpAddr, group: &MulticastGroup) -> Self { - Self { + fn to_response(&self, group_ip: IpAddr) -> MulticastGroupResponse { + MulticastGroupResponse { group_ip, - external_group_id: group.external_group_id(), - underlay_group_id: group.underlay_group_id(), - tag: group.tag.clone(), + external_group_id: self.external_group_id(), + underlay_group_id: self.underlay_group_id(), + tag: self.tag.clone(), int_fwding: InternalForwarding { - nat_target: group.int_fwding.nat_target, + nat_target: self.int_fwding.nat_target, }, ext_fwding: ExternalForwarding { - vlan_id: group.ext_fwding.vlan_id, + vlan_id: self.ext_fwding.vlan_id, }, - sources: group.sources.clone(), - members: group.members.to_vec(), + sources: self.sources.clone(), + members: self.members.to_vec(), } } - - /// Get the multicast group IP address. - pub(crate) fn ip(&self) -> IpAddr { - self.group_ip - } -} - -/// Direction a multicast group member is reached by. -/// -/// `External` group members must have any packet encapsulation removed -/// before packet delivery. -#[derive( - Copy, Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize, JsonSchema, -)] -pub(crate) enum Direction { - Underlay, - External, } /// Stores multicast group configurations. @@ -497,7 +385,7 @@ pub(crate) fn add_group_external( } } - Ok(MulticastGroupResponse::new(group_ip, &group)) + Ok(group.to_response(group_ip)) } /// Add an internal multicast group to the switch, which creates the group on @@ -573,7 +461,7 @@ fn add_group_internal_only( mcast.groups.insert(group_ip.into(), group.clone()); - Ok(MulticastGroupResponse::new(group_ip.into(), &group)) + Ok(group.to_response(group_ip.into())) } /// Delete a multicast group from the switch, including all associated tables @@ -628,7 +516,7 @@ pub(crate) fn get_group( })? .clone(); - Ok(MulticastGroupResponse::new(group_ip, &group)) + Ok(group.to_response(group_ip)) } pub(crate) fn modify_group_external( @@ -679,8 +567,7 @@ pub(crate) fn modify_group_external( updated_group.sources = new_group_info.sources.or(updated_group.sources); - let response = - MulticastGroupResponse::new(group_ip, &updated_group); + let response = updated_group.to_response(group_ip); mcast.groups.insert(group_ip, updated_group); Ok(response) } @@ -809,8 +696,7 @@ fn modify_group_internal_only( group_entry.replication_info = replication_info; group_entry.members = new_group_info.members; - let response = - MulticastGroupResponse::new(group_ip.into(), &group_entry); + let response = group_entry.to_response(group_ip.into()); mcast.groups.insert(group_ip.into(), group_entry); Ok(response) } @@ -855,7 +741,7 @@ pub(crate) fn get_range( } } - Some(MulticastGroupResponse::new(*ip, group)) + Some(group.to_response(*ip)) }) .take(limit) .collect() diff --git a/dpd/src/oxstats.rs b/dpd/src/oxstats.rs index 521aeca..e87e840 100644 --- a/dpd/src/oxstats.rs +++ b/dpd/src/oxstats.rs @@ -9,10 +9,12 @@ use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; use std::sync::Arc; use std::time::{Duration, Instant}; -use chrono::{DateTime, Utc}; +use chrono::Utc; use common::ports::PortId; -use schemars::JsonSchema; -use serde::Serialize; +use dpd_types::link::{LinkId, LinkState}; +use dpd_types::oxstats::{OximeterConfig, OximeterMetadata}; +use dpd_types::switch_identifiers::SwitchIdentifiers; + use slog::{debug, error, info, o, warn}; use tokio::sync::Mutex; use uuid::Uuid; @@ -25,8 +27,6 @@ use omicron_common::backoff::{ use oximeter::types::{ProducerRegistry, Sample}; use oximeter::{MetricsError, Producer}; -use crate::link::{LinkId, LinkState}; -use crate::switch_identifiers::SwitchIdentifiers; use crate::table; use crate::DpdResult; use crate::Switch; @@ -56,20 +56,6 @@ const LINK_NETWORK_TYPE: &str = "primary-data"; /// Model type for the data link. const LINK_MODEL_TYPE: &str = "TF2"; -/// Data associated with this dpd instance as an oximeter producer -#[derive(Clone, Debug, JsonSchema, Serialize)] -pub struct OximeterMetadata { - /// Configuration of the server and our timeseries. - #[serde(flatten)] - config: OximeterConfig, - /// When we registered with nexus - // - // NOTE: This is really the time we created the producer server, not when we - // registered with Nexus. Registration happens in the background and - // continually renews. - registered_at: Option>, -} - /// Statistics collected for a single link #[derive(Clone, Debug)] struct LinkStats { @@ -489,17 +475,6 @@ pub fn oximeter_meta(switch: &Switch) -> Option { switch.oximeter_meta.lock().unwrap().clone() } -/// Configuration for the oximeter producer server and our timeseries. -#[derive(Clone, Debug, JsonSchema, Serialize)] -struct OximeterConfig { - /// IP address of the producer server. - listen_address: Ipv6Addr, - /// Identifiers for the Scrimlet we're running on. - sled_identifiers: SledIdentifiers, - /// Identifiers for the Sidecar we're managing. - switch_identifiers: SwitchIdentifiers, -} - pub fn is_localhost(addr: &SocketAddr) -> bool { match addr.ip() { IpAddr::V4(ipv4) => ipv4 == Ipv4Addr::LOCALHOST, diff --git a/dpd/src/port_map.rs b/dpd/src/port_map.rs index 946b32f..0e45ad0 100644 --- a/dpd/src/port_map.rs +++ b/dpd/src/port_map.rs @@ -34,9 +34,10 @@ use common::ports::InternalPort; use common::ports::PortId; use common::ports::QsfpPort; use common::ports::RearPort; -use schemars::JsonSchema; -use serde::Deserialize; -use serde::Serialize; + +use dpd_types::port_map::Error; +use dpd_types::port_map::SIDECAR_REV_AB_BACKPLANE_MAP; + use std::collections::BTreeMap; use std::convert::TryFrom; use std::fmt; @@ -220,184 +221,17 @@ fn rev_chaos_map() -> BTreeMap { inner } -/// The Sidecar chassis connector mating the backplane and internal cabling. -/// -/// This describes the "group" of backplane links that all terminate in one -/// connector on the Sidecar itself. This is the connection point between a -/// cable on the backplane itself and the Sidecar chassis. -#[derive(Clone, Copy, Debug, Deserialize, JsonSchema, PartialEq, Serialize)] -pub struct SidecarConnector(u8); - -impl From for u8 { - fn from(g: SidecarConnector) -> u8 { - g.as_u8() - } -} - -impl TryFrom for SidecarConnector { - type Error = Error; - - fn try_from(x: u8) -> Result { - if x > 7 { - return Err(Error::SidecarConnector(x)); - } - Ok(Self(x)) - } -} - -impl SidecarConnector { - /// Create a new backplane group. - pub fn new(x: u8) -> Result { - Self::try_from(x) - } - - /// Return the index of this group as an integer. - pub const fn as_u8(&self) -> u8 { - self.0 - } -} - -// Helper macro to make a backplane map entry. -macro_rules! bp_entry { - ( - $connector:literal, - $sidecar_leg:expr, - $sidecar_connector:literal, - $backplane_leg:expr, - $cubby:literal - ) => { - BackplaneLink { - tofino_connector: $connector, - sidecar_leg: $sidecar_leg, - sidecar_connector: SidecarConnector($sidecar_connector), - backplane_leg: $backplane_leg, - cubby: $cubby, - } - }; -} - -/// The leg of the Sidecar-internal cable. -/// -/// This describes the leg on the cabling that connects the pins on the Tofino -/// ASIC to the Sidecar chassis connector. -// NOTE: This is the connector on the Sidecar main board side of the part -// HDR-222623-01-EBCF. -#[derive(Clone, Copy, Debug, Deserialize, JsonSchema, PartialEq, Serialize)] -pub enum SidecarCableLeg { - A, - C, -} - -/// The leg of the backplane cable. -/// -/// This describes the leg on the actual backplane cable that connects the -/// Sidecar chassis connector to a cubby endpoint. -// NOTE: This is the connector on the cubby chassis end of the part -// HDR-222627-xx-EBCM. The `xx` describes the length of the cable. -#[derive(Clone, Copy, Debug, Deserialize, JsonSchema, PartialEq, Serialize)] -pub enum BackplaneCableLeg { - A, - B, - C, - D, -} - -/// A single point-to-point connection on the cabled backplane. -/// -/// This describes a single link from the Sidecar switch to a cubby, via the -/// cabled backplane. It ultimately maps the Tofino ASIC pins to the cubby at -/// which that link terminates. This path follows the Sidecar internal cable; -/// the Sidecar chassis connector; and the backplane cable itself. This is used -/// to map the Tofino driver's "connector" number (an index in its possible -/// pinouts) through the backplane to our logical cubby numbering. -#[derive(Clone, Copy, Debug, Deserialize, JsonSchema, PartialEq, Serialize)] -pub struct BackplaneLink { - // The internal Tofino driver connector number. - tofino_connector: u8, - // The leg label on the Sidecar-internal cable. - sidecar_leg: SidecarCableLeg, - // The Sidecar chassis connector. - sidecar_connector: SidecarConnector, - // The leg label on the cabled backplane. - backplane_leg: BackplaneCableLeg, - // The cubby at which the cable terminates. - cubby: u8, -} - -const SIDECAR_REV_AB_BACKPLANE_MAP: [BackplaneLink; 32] = [ - bp_entry!(1, SidecarCableLeg::C, 0, BackplaneCableLeg::C, 29), - bp_entry!(2, SidecarCableLeg::C, 0, BackplaneCableLeg::D, 31), - bp_entry!(3, SidecarCableLeg::A, 0, BackplaneCableLeg::A, 25), - bp_entry!(4, SidecarCableLeg::A, 0, BackplaneCableLeg::B, 27), - bp_entry!(5, SidecarCableLeg::C, 1, BackplaneCableLeg::C, 21), - bp_entry!(6, SidecarCableLeg::C, 1, BackplaneCableLeg::D, 23), - bp_entry!(7, SidecarCableLeg::A, 1, BackplaneCableLeg::A, 17), - bp_entry!(8, SidecarCableLeg::A, 1, BackplaneCableLeg::B, 19), - bp_entry!(9, SidecarCableLeg::A, 2, BackplaneCableLeg::B, 11), - bp_entry!(10, SidecarCableLeg::A, 2, BackplaneCableLeg::A, 9), - bp_entry!(11, SidecarCableLeg::C, 2, BackplaneCableLeg::D, 15), - bp_entry!(12, SidecarCableLeg::C, 2, BackplaneCableLeg::C, 13), - bp_entry!(13, SidecarCableLeg::A, 3, BackplaneCableLeg::B, 3), - bp_entry!(14, SidecarCableLeg::A, 3, BackplaneCableLeg::A, 1), - bp_entry!(15, SidecarCableLeg::C, 3, BackplaneCableLeg::D, 7), - bp_entry!(16, SidecarCableLeg::C, 3, BackplaneCableLeg::C, 5), - bp_entry!(17, SidecarCableLeg::C, 4, BackplaneCableLeg::C, 28), - bp_entry!(18, SidecarCableLeg::C, 4, BackplaneCableLeg::D, 30), - bp_entry!(19, SidecarCableLeg::A, 4, BackplaneCableLeg::A, 24), - bp_entry!(20, SidecarCableLeg::A, 4, BackplaneCableLeg::B, 26), - bp_entry!(21, SidecarCableLeg::C, 5, BackplaneCableLeg::C, 20), - bp_entry!(22, SidecarCableLeg::C, 5, BackplaneCableLeg::D, 22), - bp_entry!(23, SidecarCableLeg::A, 5, BackplaneCableLeg::A, 16), - bp_entry!(24, SidecarCableLeg::A, 5, BackplaneCableLeg::B, 18), - bp_entry!(25, SidecarCableLeg::A, 6, BackplaneCableLeg::B, 10), - bp_entry!(26, SidecarCableLeg::A, 6, BackplaneCableLeg::A, 8), - bp_entry!(27, SidecarCableLeg::C, 6, BackplaneCableLeg::D, 14), - bp_entry!(28, SidecarCableLeg::C, 6, BackplaneCableLeg::C, 12), - bp_entry!(29, SidecarCableLeg::A, 7, BackplaneCableLeg::B, 2), - bp_entry!(30, SidecarCableLeg::A, 7, BackplaneCableLeg::A, 0), - bp_entry!(31, SidecarCableLeg::C, 7, BackplaneCableLeg::D, 6), - bp_entry!(32, SidecarCableLeg::C, 7, BackplaneCableLeg::C, 4), -]; - -impl BackplaneLink { - /// Construct a link from the cubby number. - pub fn from_cubby(cubby: u8) -> Result { - SIDECAR_REV_AB_BACKPLANE_MAP - .iter() - .find(|entry| entry.cubby == cubby) - .copied() - .ok_or(Error::Cubby(cubby)) - } -} - -impl From for BackplaneLink { - fn from(p: RearPort) -> Self { - Self::from_cubby(p.as_u8()).unwrap() - } -} - -#[derive(Clone, Debug, thiserror::Error)] -pub enum Error { - #[error("Invalid backplane group {0}, must be in [0, 7]")] - SidecarConnector(u8), - - #[error("Invalid cubby {0}, must be in [0, 31]")] - Cubby(u8), - - #[error("Invalid SoftNPU revision '{found}', expected '{expected}'")] - SoftNpuRevision { expected: String, found: String }, -} - #[cfg(test)] mod tests { - use super::BackplaneLink; + use dpd_types::port_map::BackplaneLink; + use dpd_types::port_map::SidecarConnector; + use super::Connector; use super::InternalPort; use super::PortId; use super::PortMap; use super::QsfpPort; use super::RearPort; - use super::SidecarConnector; use super::SidecarRevision; use std::convert::TryFrom; diff --git a/dpd/src/port_settings.rs b/dpd/src/port_settings.rs index cab00c1..3c38cf3 100644 --- a/dpd/src/port_settings.rs +++ b/dpd/src/port_settings.rs @@ -4,10 +4,7 @@ // // Copyright 2025 Oxide Computer Company -use crate::api_server::LinkSettings; -use crate::api_server::PortSettings; use crate::link::Link; -use crate::link::LinkId; use crate::link::LinkParams; use crate::DpdError; use crate::DpdResult; @@ -19,6 +16,9 @@ use common::ports::PortFec; use common::ports::PortId; use common::ports::PortSpeed; use common::ports::TxEq; +use dpd_api::LinkSettings; +use dpd_api::PortSettings; +use dpd_types::link::LinkId; use slog::debug; use slog::error; use slog::trace; diff --git a/dpd/src/ports.rs b/dpd/src/ports.rs index f77813d..dae46cd 100644 --- a/dpd/src/ports.rs +++ b/dpd/src/ports.rs @@ -7,6 +7,9 @@ use std::collections::HashMap; use std::collections::VecDeque; +use dpd_api::LinkCreate; +use dpd_types::link::LinkId; +use dpd_types::views::LinkEvent; use slog::debug; use slog::error; use slog::info; @@ -16,10 +19,7 @@ use aal::AsicOps; use aal::Connector; use aal::PortHdl; -use crate::api_server::LinkCreate; -use crate::link::LinkId; use crate::types::DpdResult; -use crate::views::LinkEvent; use crate::Switch; use common::ports::PortId; diff --git a/dpd/src/route.rs b/dpd/src/route.rs index 4f9db26..8665100 100644 --- a/dpd/src/route.rs +++ b/dpd/src/route.rs @@ -108,18 +108,16 @@ use std::collections::BTreeMap; use std::convert::TryFrom; use std::convert::TryInto; -use std::fmt; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; use std::ops::Bound; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; +use dpd_types::link::LinkId; +use dpd_types::route::Ipv4Route; +use dpd_types::route::Ipv6Route; use slog::debug; use slog::info; -use crate::api_server; use crate::freemap; -use crate::link::LinkId; use crate::types::{DpdError, DpdResult}; use crate::{table, Switch}; use common::ports::PortId; @@ -331,105 +329,6 @@ impl RouteData { } } -/// A route for an IPv4 subnet. -#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)] -pub struct Ipv4Route { - // The client-specific tag for this route. - pub(crate) tag: String, - // The switch port out which routed traffic is sent. - pub(crate) port_id: PortId, - // The link out which routed traffic is sent. - pub(crate) link_id: LinkId, - // Route traffic matching the subnet via this IP. - pub(crate) tgt_ip: Ipv4Addr, - // Tag traffic on this route with this vlan ID. - pub(crate) vlan_id: Option, -} - -// We implement PartialEq for Ipv4Route because we want to exclude the tag and -// vlan_id from any comparisons. We do this because the tag is a comment -// identifying the originator rather than a semantically meaningful part of the -// route. The vlan_id is used to modify the traffic on a specific route, rather -// then being part of the route itself. -impl PartialEq for Ipv4Route { - fn eq(&self, other: &Self) -> bool { - self.port_id == other.port_id - && self.link_id == other.link_id - && self.tgt_ip == other.tgt_ip - } -} - -// See the comment above PartialEq to understand why we implement Hash rather -// then Deriving it. -impl std::hash::Hash for Ipv4Route { - fn hash(&self, state: &mut H) - where - H: std::hash::Hasher, - { - self.port_id.hash(state); - self.link_id.hash(state); - self.tgt_ip.hash(state); - } -} - -impl fmt::Display for Ipv4Route { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!( - f, - "port: {} link: {} gw: {} vlan: {:?}", - self.port_id, self.link_id, self.tgt_ip, self.vlan_id - )?; - Ok(()) - } -} - -/// A route for an IPv6 subnet. -#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)] -pub struct Ipv6Route { - // The client-specific tag for this route. - pub(crate) tag: String, - // The switch port out which routed traffic is sent. - pub(crate) port_id: PortId, - // The link out which routed traffic is sent. - pub(crate) link_id: LinkId, - // Route traffic matching the subnet to this IP. - pub(crate) tgt_ip: Ipv6Addr, - // Tag traffic on this route with this vlan ID. - pub(crate) vlan_id: Option, -} - -// See the comment above the PartialEq for IPv4Route -impl PartialEq for Ipv6Route { - fn eq(&self, other: &Self) -> bool { - self.port_id == other.port_id - && self.link_id == other.link_id - && self.tgt_ip == other.tgt_ip - } -} - -// See the comment above PartialEq for IPv4Route -impl std::hash::Hash for Ipv6Route { - fn hash(&self, state: &mut H) - where - H: std::hash::Hasher, - { - self.port_id.hash(state); - self.link_id.hash(state); - self.tgt_ip.hash(state); - } -} - -impl fmt::Display for Ipv6Route { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!( - f, - "port: {} link: {} gw: {} vlan: {:?}", - self.port_id, self.link_id, self.tgt_ip, self.vlan_id - )?; - Ok(()) - } -} - // Remove all the data for a given route from both the route_data and // route_index tables. // @@ -912,7 +811,7 @@ pub async fn get_range_ipv4( switch: &Switch, last: Option, max: u32, -) -> DpdResult> { +) -> DpdResult> { let route_data = switch.routes.lock().await; let lower = match last { None => Bound::Unbounded, @@ -925,7 +824,7 @@ pub async fn get_range_ipv4( .range((lower, Bound::Unbounded)) .take(usize::try_from(max).expect("invalid usize")) { - routes.push(api_server::Ipv4Routes { + routes.push(dpd_api::Ipv4Routes { cidr: match subnet { IpNet::V4(n) => *n, IpNet::V6(_) => { @@ -945,7 +844,7 @@ pub async fn get_range_ipv6( switch: &Switch, last: Option, max: u32, -) -> DpdResult> { +) -> DpdResult> { let route_data = switch.routes.lock().await; let lower = match last { None => Bound::Unbounded, @@ -958,7 +857,7 @@ pub async fn get_range_ipv6( .range((lower, Bound::Unbounded)) .take(usize::try_from(max).expect("invalid usize")) { - routes.push(api_server::Ipv6Routes { + routes.push(dpd_api::Ipv6Routes { cidr: match subnet { IpNet::V6(n) => *n, IpNet::V4(_) => { diff --git a/dpd/src/softnpu_api_server.rs b/dpd/src/softnpu_api_server.rs index 5a87017..3820d1b 100644 --- a/dpd/src/softnpu_api_server.rs +++ b/dpd/src/softnpu_api_server.rs @@ -14,12 +14,12 @@ use dropshot::Path; use dropshot::RequestContext; use dropshot::TypedBody; -use crate::api_server::LinkPath; use crate::types::DpdError; use crate::Switch; use aal::AsicOps; use common::ports::TxEq; use common::ports::TxEqSwHw; +use dpd_api::LinkPath; /// Get the per-lane tx eq settings for each lane on this link #[endpoint { diff --git a/dpd/src/switch_identifiers.rs b/dpd/src/switch_identifiers.rs index 38a7a29..1f7daa9 100644 --- a/dpd/src/switch_identifiers.rs +++ b/dpd/src/switch_identifiers.rs @@ -9,10 +9,8 @@ use std::sync::Arc; use display_error_chain::DisplayErrorChain; -use schemars::JsonSchema; -use serde::Serialize; +use dpd_types::switch_identifiers::SwitchIdentifiers; use slog::{debug, error, info, o}; -use uuid::Uuid; use crate::DpdResult; use crate::Switch; @@ -24,34 +22,6 @@ use omicron_common::{ }, }; -/// Identifiers for a switch. -#[derive(Clone, Debug, JsonSchema, Serialize)] -pub struct SwitchIdentifiers { - /// Unique identifier for the chip. - pub sidecar_id: Uuid, - /// Asic backend (compiler target) responsible for these identifiers. - pub asic_backend: String, - /// Fabrication plant identifier. - pub fab: Option, - /// Lot identifier. - pub lot: Option, - /// Wafer number within the lot. - pub wafer: Option, - /// The wafer location as (x, y) coordinates on the wafer, represented as - /// an array due to the lack of tuple support in OpenAPI. - pub wafer_loc: Option<[i16; 2]>, - /// The model number of the switch being managed. - pub model: String, - /// The revision number of the switch being managed. - pub revision: u32, - /// The serial number of the switch being managed. - pub serial: String, - /// The slot number of the switch being managed. - /// - /// MGS uses u16 for this internally. - pub slot: u16, -} - /// Fetch unique switch identifying information from the local MGS and /// get current sidecar ID, which is embedded with the fab, lot, wafer id, /// and location on the wafer. diff --git a/dpd/src/switch_port.rs b/dpd/src/switch_port.rs index a4a7516..a18f298 100644 --- a/dpd/src/switch_port.rs +++ b/dpd/src/switch_port.rs @@ -7,18 +7,20 @@ //! Types for describing and managing physical ports on the Sidecar switch. use anyhow::Context; -use schemars::JsonSchema; +use dpd_types::port_map::BackplaneLink; +use dpd_types::switch_port::Led; +use dpd_types::switch_port::LedPolicy; +use dpd_types::switch_port::ManagementMode; +use dpd_types::transceivers::QsfpDevice; +use dpd_types::views; use serde::Deserialize; -use serde::Serialize; use tokio::sync::Mutex; pub use transceiver_controller::message::LedState; use crate::link::Link; -use crate::port_map::BackplaneLink; use crate::port_map::PortMap; use crate::port_map::SidecarRevision; use crate::transceivers::FakeQsfpModule; -use crate::transceivers::QsfpDevice; use crate::types::DpdError; use crate::types::DpdResult; use aal::AsicOps; @@ -151,89 +153,6 @@ impl SwitchPorts { } } -/// How a switch port is managed. -/// -/// The free-side devices in QSFP ports are complex devices, whose operation -/// usually involves coordinated steps through one or more state machines. For -/// example, when bringing up an optical link, a signal from the peer link must -/// be detected; then a signal recovered; equalizer gains set; etc. In -/// `Automatic` mode, all these kinds of steps are managed autonomously by -/// switch driver software. In `Manual` mode, none of these will occur -- a -/// switch port will only change in response to explicit requests from the -/// operator or Oxide control plane. -// -// NOTE: This is the parameter which marks a switch port _visible_ to the BF -// SDE. `Manual` means under our control, `Automatic` means visible to the SDE -// and under its control. -#[derive( - Clone, - Copy, - Debug, - Deserialize, - Eq, - Hash, - JsonSchema, - Ord, - PartialEq, - PartialOrd, - Serialize, -)] -#[serde(rename_all = "snake_case")] -pub enum ManagementMode { - /// A port is managed manually, by either the Oxide control plane or an - /// operator. - Manual, - /// A port is managed automatically by the switch software. - Automatic, -} - -/// The policy by which a port's LED is controlled. -#[derive( - Clone, - Copy, - Debug, - Deserialize, - Eq, - Hash, - JsonSchema, - Ord, - PartialEq, - PartialOrd, - Serialize, -)] -#[serde(rename_all = "snake_case")] -pub enum LedPolicy { - /// The default policy is for the LED to reflect the port's state itself. - /// - /// If the port is operating normally, the LED will be solid on. Without a - /// transceiver, the LED will be solid off. A blinking LED is used to - /// indicate an unsupported module or other failure on that port. - Automatic, - /// The LED is explicitly overridden by client requests. - Override, -} - -/// Information about a QSFP port's LED. -#[derive( - Clone, - Copy, - Debug, - Deserialize, - Eq, - Hash, - JsonSchema, - Ord, - PartialEq, - PartialOrd, - Serialize, -)] -pub struct Led { - /// The policy by which the LED is controlled. - pub policy: LedPolicy, - /// The state of the LED. - pub state: LedState, -} - /// A physical port on the Sidecar switch. #[derive(Debug)] pub struct SwitchPort { @@ -362,6 +281,19 @@ impl SwitchPort { } } +impl From<&SwitchPort> for views::SwitchPort { + fn from(p: &SwitchPort) -> Self { + let qsfp_device = match &p.fixed_side { + FixedSideDevice::Qsfp { device, .. } => Some(device.clone()), + _ => None, + }; + Self { + port_id: p.port_id(), + qsfp_device, + } + } +} + /// Data specific to each kind of fixed-side switch port. #[derive(Clone, Debug)] pub enum FixedSideDevice { diff --git a/dpd/src/table/mcast/mcast_egress.rs b/dpd/src/table/mcast/mcast_egress.rs index 107ac3d..ff7086f 100644 --- a/dpd/src/table/mcast/mcast_egress.rs +++ b/dpd/src/table/mcast/mcast_egress.rs @@ -8,10 +8,11 @@ use std::fmt; -use crate::{mcast::MulticastGroupId, table::*, Switch}; +use crate::{table::*, Switch}; use aal::{ActionParse, MatchParse}; use aal_macros::*; +use dpd_types::mcast::MulticastGroupId; use slog::debug; /// Table for multicast egress entries matching the multicast group ID diff --git a/dpd/src/table/mcast/mcast_replication.rs b/dpd/src/table/mcast/mcast_replication.rs index 917dd1b..64dd79f 100644 --- a/dpd/src/table/mcast/mcast_replication.rs +++ b/dpd/src/table/mcast/mcast_replication.rs @@ -8,12 +8,13 @@ use std::net::Ipv6Addr; -use crate::{mcast::MulticastGroupId, table::*, Switch}; +use crate::{table::*, Switch}; use super::Ipv6MatchKey; use aal::ActionParse; use aal_macros::*; +use dpd_types::mcast::MulticastGroupId; use slog::debug; /// IPv6 Table for multicast replication entries and group membership. diff --git a/dpd/src/table/mod.rs b/dpd/src/table/mod.rs index 50448da..295373b 100644 --- a/dpd/src/table/mod.rs +++ b/dpd/src/table/mod.rs @@ -10,12 +10,12 @@ use std::hash::Hash; use slog::{debug, error, info}; use crate::types::*; -use crate::views; use crate::Switch; use aal::ActionParse; use aal::MatchParse; use aal::TableOps; use common::network::MacAddr; +use dpd_types::views; pub mod arp_ipv4; pub mod mcast; diff --git a/dpd/src/tofino_api_server.rs b/dpd/src/tofino_api_server.rs index b0d0f30..216afc5 100644 --- a/dpd/src/tofino_api_server.rs +++ b/dpd/src/tofino_api_server.rs @@ -6,6 +6,8 @@ use std::sync::Arc; +use dpd_api::LinkPath; +use dpd_types::link::LinkId; use dropshot::endpoint; use dropshot::HttpError; use dropshot::HttpResponseOk; @@ -16,8 +18,6 @@ use dropshot::TypedBody; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use crate::api_server::LinkPath; -use crate::link::LinkId; use crate::types::DpdError; use crate::PortId; use crate::Switch; diff --git a/dpd/src/transceivers/mod.rs b/dpd/src/transceivers/mod.rs index 82a2cc6..31fd849 100644 --- a/dpd/src/transceivers/mod.rs +++ b/dpd/src/transceivers/mod.rs @@ -8,17 +8,10 @@ // Copyright 2025 Oxide Computer -use std::time::Instant; - -use crate::switch_port::ManagementMode; use crate::types::DpdResult; -use schemars::JsonSchema; -use serde::Deserialize; -use serde::Serialize; +use dpd_types::transceivers::QsfpDevice; use transceiver_controller::Identifier; -pub use transceiver_controller::PowerMode; pub use transceiver_controller::PowerState; -pub use transceiver_controller::VendorInfo; cfg_if::cfg_if! { if #[cfg(feature = "tofino_asic")] { @@ -29,161 +22,44 @@ cfg_if::cfg_if! { } } -/// A QSFP switch port. -/// -/// This includes the hardware controls and information relevant to QSFP ports -/// specifically. For example, these ports are on the front IO panel of the -/// switch, and have LEDs used for status and attention. This includes the state -/// and controls for those LEDs. It also includes information about the -/// free-side QSFP module, should one be plugged in. -#[derive(Clone, Debug, JsonSchema, Serialize)] -pub struct QsfpDevice { - /// Details about a transceiver module inserted into the switch port. - /// - /// If there is no transceiver at all, this will be `None`. - pub transceiver: Option, - /// How the QSFP device is managed. - /// - /// See `ManagementMode` for details. - pub management_mode: ManagementMode, -} - -impl Default for QsfpDevice { - fn default() -> Self { - Self { - transceiver: None, - management_mode: ManagementMode::Automatic, - } +/// If this qsfp port has a supported transceiver that provides an MPN, +/// return it to the caller. If we have a supported transceiver that hasn't +/// returned an MPN yet, return Ok(None). If there is no transceiver +/// detected at all, return DpdError::Missing. +pub fn qsfp_xcvr_mpn( + #[allow(unused_variables)] qsfp: &QsfpDevice, +) -> DpdResult> { + #[cfg(feature = "softnpu")] + { + Ok(Some("OXIDESOFTNPU".to_string())) } -} -impl QsfpDevice { - /// If this qsfp port has a supported transceiver that provides an MPN, - /// return it to the caller. If we have a supported transceiver that hasn't - /// returned an MPN yet, return Ok(None). If there is no transceiver - /// detected at all, return DpdError::Missing. - pub fn xcvr_mpn(&self) -> DpdResult> { - #[cfg(feature = "softnpu")] - { - Ok(Some("OXIDESOFTNPU".to_string())) - } + #[cfg(feature = "tofino_asic")] + { + use dpd_types::transceivers::Transceiver; - #[cfg(feature = "tofino_asic")] - { - match &self.transceiver { - Some(Transceiver::Supported(xcvr_info)) => { - if let Some(vendor_info) = &xcvr_info.vendor_info { - Ok(Some(vendor_info.vendor.part.clone())) - } else { - Ok(None) - } + match &qsfp.transceiver { + Some(Transceiver::Supported(xcvr_info)) => { + if let Some(vendor_info) = &xcvr_info.vendor_info { + Ok(Some(vendor_info.vendor.part.clone())) + } else { + Ok(None) } - // XXX: Is it worth returning different errors for faulted - // and/or unsupported transceiver? - _ => Err(crate::DpdError::Missing( - "no qsfp xcvr found".to_string(), - )), } - } - - #[cfg(not(any(feature = "tofino_asic", feature = "softnpu",)))] - { - Ok(None) + // XXX: Is it worth returning different errors for faulted + // and/or unsupported transceiver? + _ => { + Err(crate::DpdError::Missing("no qsfp xcvr found".to_string())) + } } } -} - -/// The cause of a fault on a transceiver. -#[derive(Clone, Copy, Debug, Deserialize, JsonSchema, Serialize)] -#[serde(rename_all = "snake_case")] -pub enum FaultReason { - /// An error occurred accessing the transceiver. - Failed, - /// Power was enabled, but did not come up in the requisite time. - PowerTimeout, - /// Power was enabled and later lost. - PowerLost, - /// The service processor disabled the transceiver. - /// - /// The SP is responsible for monitoring the thermal data from the - /// transceivers, and controlling the fans to compensate. If a module's - /// thermal data cannot be read, the SP may completely disable the - /// transceiver to ensure it cannot overheat the Sidecar. - DisabledBySp, -} - -/// The state of a transceiver in a QSFP switch port. -#[derive(Clone, Debug, JsonSchema, Serialize)] -#[serde(rename_all = "snake_case", tag = "state", content = "info")] -#[cfg_attr(not(feature = "tofino_asic"), allow(dead_code))] -pub enum Transceiver { - /// The transceiver could not be managed due to a power fault. - Faulted(FaultReason), - /// A transceiver was present, but unsupported and automatically disabled. - Unsupported, - /// A transceiver is present and supported. - Supported(TransceiverInfo), -} - -/// Information about a QSFP transceiver. -/// -/// This stores the most relevant information about a transceiver module, such -/// as vendor info or power. Each field may be missing, indicating it could not -/// be determined. -#[derive(Clone, Debug, JsonSchema, Serialize)] -pub struct TransceiverInfo { - /// Vendor and part identifying information. - /// - /// The information will not be populated if it could not be read. - pub vendor_info: Option, - /// True if the module is currently in reset. - pub in_reset: Option, - /// True if there is a pending interrupt on the module. - pub interrupt_pending: Option, - /// The power mode of the transceiver. - pub power_mode: Option, - /// The electrical mode of the transceiver. - /// - /// See [`ElectricalMode`] for details. - pub electrical_mode: ElectricalMode, - // The instant at which we first saw this transceiver. - // - // This is only used to support initially blinking the transceiver to - // acknowledge insertion. - #[cfg_attr(not(feature = "tofino_asic"), allow(dead_code))] - #[serde(skip)] - first_seen: Instant, -} -impl Default for TransceiverInfo { - fn default() -> Self { - Self { - vendor_info: None, - in_reset: None, - interrupt_pending: None, - power_mode: None, - electrical_mode: ElectricalMode::Single, - first_seen: Instant::now(), - } + #[cfg(not(any(feature = "tofino_asic", feature = "softnpu",)))] + { + Ok(None) } } -/// The electrical mode of a QSFP-capable port. -/// -/// QSFP ports can be broken out into one of several different electrical -/// configurations or modes. This describes how the transmit/receive lanes are -/// grouped into a single, logical link. -/// -/// Note that the electrical mode may only be changed if there are no links -/// within the port, _and_ if the inserted QSFP module actually supports this -/// mode. -#[derive(Clone, Copy, Debug, Default, Deserialize, JsonSchema, Serialize)] -pub enum ElectricalMode { - /// All transmit/receive lanes are used for a single link. - #[default] - Single, -} - /// The BF SDE considers all ports except the CPU port as "QSFPs". This is not /// accurate for the backplane / rear ports, which are completely different /// electromechanical devices. Nonetheless we present a "QSFP-like" device to @@ -295,14 +171,14 @@ impl FakeQsfpModule { mod mpn_test { use std::time::Instant; - use crate::switch_port::ManagementMode; - use crate::types::DpdError; + use crate::{transceivers::qsfp_xcvr_mpn, types::DpdError}; + use dpd_types::{ + switch_port::ManagementMode, + transceivers::{ElectricalMode, Transceiver, TransceiverInfo}, + }; use transceiver_controller::Identifier; - use super::ElectricalMode; use super::QsfpDevice; - use super::Transceiver; - use super::TransceiverInfo; #[test] // If a QsfpDevice is found with a transceiver present, and if the VendorInfo @@ -339,7 +215,7 @@ mod mpn_test { transceiver: Some(transceiver), management_mode: ManagementMode::Manual, }; - assert_eq!(qsfp.xcvr_mpn().unwrap(), Some("part".to_string())); + assert_eq!(qsfp_xcvr_mpn(&qsfp).unwrap(), Some("part".to_string())); } #[test] @@ -360,7 +236,7 @@ mod mpn_test { transceiver: Some(transceiver), management_mode: ManagementMode::Manual, }; - assert_eq!(qsfp.xcvr_mpn().unwrap(), None); + assert_eq!(qsfp_xcvr_mpn(&qsfp).unwrap(), None); } // If a Qsfp port is found without any transceiver detected, @@ -373,6 +249,6 @@ mod mpn_test { }; // It would be preferable to use assert_matches! here, but that's still // unstable. - assert!(matches!(qsfp.xcvr_mpn(), Err(DpdError::Missing(_)))); + assert!(matches!(qsfp_xcvr_mpn(&qsfp), Err(DpdError::Missing(_)))); } } diff --git a/dpd/src/transceivers/tofino_impl.rs b/dpd/src/transceivers/tofino_impl.rs index eb8df94..9005070 100644 --- a/dpd/src/transceivers/tofino_impl.rs +++ b/dpd/src/transceivers/tofino_impl.rs @@ -41,15 +41,10 @@ // both in some conditions. Regardless, the controller must always be acquired // first to avoid deadlocks. -use crate::link::LinkState; use crate::port_map::PortMap; -use crate::switch_port::LedPolicy; use crate::switch_port::LedState; -use crate::switch_port::ManagementMode; use crate::switch_port::SwitchPort; use crate::switch_port::SwitchPorts; -use crate::transceivers::FaultReason; -use crate::transceivers::Transceiver; use crate::types::DpdError; use crate::types::DpdResult; use crate::Switch; @@ -61,6 +56,11 @@ use asic::tofino_asic::qsfp::SdeTransceiverResponse; use asic::tofino_asic::qsfp::WriteRequest; use common::ports::PortId; use common::ports::QsfpPort; +use dpd_types::link::LinkState; +use dpd_types::switch_port::LedPolicy; +use dpd_types::switch_port::ManagementMode; +use dpd_types::transceivers::FaultReason; +use dpd_types::transceivers::Transceiver; use slog::debug; use slog::error; use slog::info; diff --git a/dpd/src/types.rs b/dpd/src/types.rs index 96c9768..c764fd4 100644 --- a/dpd/src/types.rs +++ b/dpd/src/types.rs @@ -6,13 +6,12 @@ //! General types used throughout Dendrite. -use crate::link::LinkId; - use aal::AsicError; use common::ports::PortId; use common::ports::QsfpPort; use common::SmfError; use common::ROLLBACK_FAILURE_ERROR_CODE; +use dpd_types::link::LinkId; use slog::error; use std::{convert, net::IpAddr}; use transceiver_controller::Error as TransceiverError;