From 6f6d887abd23741a227506ee763be3c65eeaeb75 Mon Sep 17 00:00:00 2001 From: Rain Date: Fri, 29 Aug 2025 22:56:39 +0000 Subject: [PATCH 1/3] [spr] changes to main this commit is based on Created using spr 1.3.6-beta.1 [skip ci] --- dpd/src/api_server.rs | 6130 +++++++++++++++++++++-------------------- 1 file changed, 3095 insertions(+), 3035 deletions(-) diff --git a/dpd/src/api_server.rs b/dpd/src/api_server.rs index 46ec95b..7a81ebd 100644 --- a/dpd/src/api_server.rs +++ b/dpd/src/api_server.rs @@ -67,912 +67,945 @@ use oxnet::{IpNet, Ipv4Net, Ipv6Net}; type ApiServer = dropshot::HttpServer>; -/// 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, -} +// 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 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 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), -} + /// Represents a specific egress port and nexthop target. + #[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)] + pub enum RouteTarget { + V4(Ipv4Route), + V6(Ipv6Route), + } -impl TryFrom for Ipv4Route { - type Error = HttpError; + impl TryFrom for Ipv4Route { + type Error = HttpError; - fn try_from(target: RouteTarget) -> Result { - match target { - RouteTarget::V4(route) => Ok(route), - _ => Err(DpdError::InvalidRoute( - "expected an IPv4 route target".to_string(), - ) - .into()), + fn try_from(target: RouteTarget) -> Result { + match target { + RouteTarget::V4(route) => Ok(route), + _ => Err(DpdError::InvalidRoute( + "expected an IPv4 route target".to_string(), + ) + .into()), + } } } -} -impl TryFrom for Ipv6Route { - type Error = HttpError; + impl TryFrom for Ipv6Route { + type Error = HttpError; - fn try_from(target: RouteTarget) -> Result { - match target { - RouteTarget::V6(route) => Ok(route), - _ => Err(DpdError::InvalidRoute( - "expected an IPv6 route target".to_string(), - ) - .into()), + fn try_from(target: RouteTarget) -> Result { + match target { + RouteTarget::V6(route) => Ok(route), + _ => Err(DpdError::InvalidRoute( + "expected an IPv6 route target".to_string(), + ) + .into()), + } } } -} -/// Represents a new or replacement mapping of a subnet to a single RouteTarget -/// nexthop target. -#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)] -pub struct RouteSet { - /// Traffic destined for any address within the CIDR block is routed using - /// this information. - pub cidr: IpNet, - /// A single RouteTarget associated with this CIDR - pub target: RouteTarget, - /// 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 RouteTarget + /// nexthop target. + #[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)] + pub struct RouteSet { + /// Traffic destined for any address within the CIDR block is routed using + /// this information. + pub cidr: IpNet, + /// A single RouteTarget associated with this CIDR + pub target: RouteTarget, + /// 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 single mapping of a subnet to a single RouteTarget -/// nexthop target. -#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)] -pub struct RouteAdd { - /// Traffic destined for any address within the CIDR block is routed using - /// this information. - pub cidr: IpNet, - /// A single RouteTarget associated with this CIDR - pub target: RouteTarget, -} + /// Represents a single mapping of a subnet to a single RouteTarget + /// nexthop target. + #[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)] + pub struct RouteAdd { + /// Traffic destined for any address within the CIDR block is routed using + /// this information. + pub cidr: IpNet, + /// A single RouteTarget associated with this CIDR + pub target: RouteTarget, + } -/// Represents all mappings of a subnet to a its nexthop target(s). -#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)] -pub struct Route { - /// Traffic destined for any address within the CIDR block is routed using - /// this information. - pub cidr: IpNet, - /// All RouteTargets associated with this CIDR - pub targets: Vec, -} + /// Represents all mappings of a subnet to a its nexthop target(s). + #[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)] + pub struct Route { + /// Traffic destined for any address within the CIDR block is routed using + /// this information. + pub cidr: IpNet, + /// 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(), - ) -} + // 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, -} + #[derive(Deserialize, Serialize, JsonSchema)] + struct Ipv6ArpParam { + ip: Ipv6Addr, + } -/** - * Represents a cursor into a paginated request for the contents of an ARP table - */ -#[derive(Deserialize, Serialize, JsonSchema)] -struct ArpToken { - ip: IpAddr, -} + /** + * Represents a cursor into a paginated request for the contents of an ARP table + */ + #[derive(Deserialize, Serialize, JsonSchema)] + struct ArpToken { + ip: IpAddr, + } -/** - * Represents a potential fault condtion on a link - */ -#[derive(Deserialize, Serialize, JsonSchema)] -struct FaultCondition { - fault: Option, -} + /** + * Represents a potential fault condtion on a link + */ + #[derive(Deserialize, Serialize, JsonSchema)] + struct FaultCondition { + fault: Option, + } -/** - * 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> { - let switch: &Switch = rqctx.context(); - let pag_params = query.into_inner(); - let max = rqctx.page_limit(&pag_params)?.get(); - - let previous = match &pag_params.page { - WhichPage::First(..) => None, - WhichPage::Next(ArpToken { ip }) => match ip { - IpAddr::V6(ip) => Some(ip), - IpAddr::V4(_) => { - return Err(DpdError::Invalid("bad token".into()).into()) - } - }, - }; + /** + * 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( + rqctx: RequestContext>, + query: Query>, + ) -> Result>, HttpError> { + let switch: &Switch = rqctx.context(); + let pag_params = query.into_inner(); + let max = rqctx.page_limit(&pag_params)?.get(); + + let previous = match &pag_params.page { + WhichPage::First(..) => None, + WhichPage::Next(ArpToken { ip }) => match ip { + IpAddr::V6(ip) => Some(ip), + IpAddr::V4(_) => { + return Err(DpdError::Invalid("bad token".into()).into()) + } + }, + }; - let entries = match arp::get_range_ipv6(switch, previous, max) { - Err(e) => return Err(e.into()), - Ok(v) => v, - }; + let entries = match arp::get_range_ipv6(switch, previous, max) { + Err(e) => return Err(e.into()), + Ok(v) => v, + }; - Ok(HttpResponseOk(ResultsPage::new( - entries, - &EmptyScanParams {}, - |e: &ArpEntry, _| ArpToken { ip: e.ip }, - )?)) -} + Ok(HttpResponseOk(ResultsPage::new( + entries, + &EmptyScanParams {}, + |e: &ArpEntry, _| ArpToken { ip: e.ip }, + )?)) + } -/** - * Remove all entries in the the IPv6 NDP tables. - */ -#[endpoint { - method = DELETE, - path = "/ndp" -}] -async fn ndp_reset( - rqctx: RequestContext>, -) -> Result { - let switch: &Switch = rqctx.context(); - - match arp::reset_ipv6(switch) { - Err(e) => Err(e.into()), - _ => Ok(HttpResponseUpdatedNoContent()), + /** + * Remove all entries in the the IPv6 NDP tables. + */ + #[endpoint { + method = DELETE, + path = "/ndp" + }] + pub(super) async fn ndp_reset( + rqctx: RequestContext>, + ) -> Result { + let switch: &Switch = rqctx.context(); + + match arp::reset_ipv6(switch) { + Err(e) => Err(e.into()), + _ => Ok(HttpResponseUpdatedNoContent()), + } } -} -/** - * 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> { - let switch: &Switch = rqctx.context(); - let ip = path.into_inner().ip; - - match arp::get_entry_ipv6(switch, ip) { - Err(e) => Err(e.into()), - Ok(entry) => Ok(HttpResponseOk(ArpEntry { - tag: String::new(), - ip: IpAddr::V6(ip), - mac: entry.mac, - update: entry.update.to_rfc3339(), - })), + /** + * Get a single IPv6 NDP table entry, by its IPv6 address. + */ + #[endpoint { + method = GET, + path = "/ndp/{ip}", + }] + pub(super) async fn ndp_get( + rqctx: RequestContext>, + path: Path, + ) -> Result, HttpError> { + let switch: &Switch = rqctx.context(); + let ip = path.into_inner().ip; + + match arp::get_entry_ipv6(switch, ip) { + Err(e) => Err(e.into()), + Ok(entry) => Ok(HttpResponseOk(ArpEntry { + tag: String::new(), + ip: IpAddr::V6(ip), + mac: entry.mac, + update: entry.update.to_rfc3339(), + })), + } } -} -/** - * 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 { - let switch: &Switch = rqctx.context(); - let entry = update.into_inner(); - let IpAddr::V6(ip) = entry.ip else { - return Err(client_error("NDP entry must have an IPv6 address")); - }; - match arp::add_entry_ipv6(switch, entry.tag, ip, entry.mac) { - Err(e) => Err(e.into()), - Ok(_) => Ok(HttpResponseUpdatedNoContent()), + /** + * Add an IPv6 NDP entry, mapping an IPv6 address to a MAC address. + */ + #[endpoint { + method = POST, + path = "/ndp", + }] + pub(super) async fn ndp_create( + rqctx: RequestContext>, + update: TypedBody, + ) -> Result { + let switch: &Switch = rqctx.context(); + let entry = update.into_inner(); + let IpAddr::V6(ip) = entry.ip else { + return Err(client_error("NDP entry must have an IPv6 address")); + }; + match arp::add_entry_ipv6(switch, entry.tag, ip, entry.mac) { + Err(e) => Err(e.into()), + Ok(_) => Ok(HttpResponseUpdatedNoContent()), + } } -} -/** - * Remove an IPv6 NDP entry, by its IPv6 address. - */ -#[endpoint { - method = DELETE, - path = "/ndp/{ip}", -}] -async fn ndp_delete( - rqctx: RequestContext>, - path: Path, -) -> Result { - let switch: &Switch = rqctx.context(); - let ip = path.into_inner().ip; - arp::delete_entry_ipv6(switch, ip) - .map(|_| HttpResponseDeleted()) - .map_err(HttpError::from) -} + /** + * Remove an IPv6 NDP entry, by its IPv6 address. + */ + #[endpoint { + method = DELETE, + path = "/ndp/{ip}", + }] + pub(super) async fn ndp_delete( + rqctx: RequestContext>, + path: Path, + ) -> Result { + let switch: &Switch = rqctx.context(); + let ip = path.into_inner().ip; + arp::delete_entry_ipv6(switch, ip) + .map(|_| HttpResponseDeleted()) + .map_err(HttpError::from) + } -#[derive(Deserialize, Serialize, JsonSchema)] -struct Ipv4ArpParam { - ip: Ipv4Addr, -} + #[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 + * 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, -} + /** + * 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", -}] -async fn arp_list( - rqctx: RequestContext>, - query: Query>, -) -> Result>, HttpError> { - let switch: &Switch = rqctx.context(); - let pag_params = query.into_inner(); - let max = rqctx.page_limit(&pag_params)?.get(); - - let previous = match &pag_params.page { - WhichPage::First(..) => None, - WhichPage::Next(ArpToken { ip }) => match ip { - IpAddr::V6(_) => { - return Err(DpdError::Invalid("bad token".into()).into()) - } - IpAddr::V4(ip) => Some(ip), - }, - }; + /** + * Fetch the configured IPv4 ARP table entries. + */ + #[endpoint { + method = GET, + path = "/arp", + }] + pub(super) async fn arp_list( + rqctx: RequestContext>, + query: Query>, + ) -> Result>, HttpError> { + let switch: &Switch = rqctx.context(); + let pag_params = query.into_inner(); + let max = rqctx.page_limit(&pag_params)?.get(); + + let previous = match &pag_params.page { + WhichPage::First(..) => None, + WhichPage::Next(ArpToken { ip }) => match ip { + IpAddr::V6(_) => { + return Err(DpdError::Invalid("bad token".into()).into()) + } + IpAddr::V4(ip) => Some(ip), + }, + }; - let entries = match arp::get_range_ipv4(switch, previous, max) { - Err(e) => return Err(e.into()), - Ok(v) => v, - }; + let entries = match arp::get_range_ipv4(switch, previous, max) { + Err(e) => return Err(e.into()), + Ok(v) => v, + }; - Ok(HttpResponseOk(ResultsPage::new( - entries, - &EmptyScanParams {}, - |e: &ArpEntry, _| ArpToken { ip: e.ip }, - )?)) -} + Ok(HttpResponseOk(ResultsPage::new( + entries, + &EmptyScanParams {}, + |e: &ArpEntry, _| ArpToken { ip: e.ip }, + )?)) + } -/** - * Remove all entries in the IPv4 ARP tables. - */ -#[endpoint { - method = DELETE, - path = "/arp", -}] -async fn arp_reset( - rqctx: RequestContext>, -) -> Result { - let switch: &Switch = rqctx.context(); - - match arp::reset_ipv4(switch) { - Err(e) => Err(e.into()), - _ => Ok(HttpResponseUpdatedNoContent()), + /** + * Remove all entries in the IPv4 ARP tables. + */ + #[endpoint { + method = DELETE, + path = "/arp", + }] + pub(super) async fn arp_reset( + rqctx: RequestContext>, + ) -> Result { + let switch: &Switch = rqctx.context(); + + match arp::reset_ipv4(switch) { + Err(e) => Err(e.into()), + _ => Ok(HttpResponseUpdatedNoContent()), + } } -} -/** - * 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> { - let switch: &Switch = rqctx.context(); - let ip = path.into_inner().ip; - - match arp::get_entry_ipv4(switch, ip) { - Err(e) => Err(e.into()), - Ok(entry) => Ok(HttpResponseOk(ArpEntry { - tag: String::new(), - ip: IpAddr::V4(ip), - mac: entry.mac, - update: entry.update.to_rfc3339(), - })), + /** + * Get a single IPv4 ARP table entry, by its IPv4 address. + */ + #[endpoint { + method = GET, + path = "/arp/{ip}", + }] + pub(super) async fn arp_get( + rqctx: RequestContext>, + path: Path, + ) -> Result, HttpError> { + let switch: &Switch = rqctx.context(); + let ip = path.into_inner().ip; + + match arp::get_entry_ipv4(switch, ip) { + Err(e) => Err(e.into()), + Ok(entry) => Ok(HttpResponseOk(ArpEntry { + tag: String::new(), + ip: IpAddr::V4(ip), + mac: entry.mac, + update: entry.update.to_rfc3339(), + })), + } } -} -/** - * 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 { - let switch: &Switch = rqctx.context(); - let entry = update.into_inner(); - let IpAddr::V4(ip) = entry.ip else { - return Err(client_error("ARP entry must have an IPv4 address")); - }; - match arp::add_entry_ipv4(switch, entry.tag, ip, entry.mac) { - Err(e) => Err(e.into()), - Ok(_) => Ok(HttpResponseUpdatedNoContent()), + /** + * 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( + rqctx: RequestContext>, + update: TypedBody, + ) -> Result { + let switch: &Switch = rqctx.context(); + let entry = update.into_inner(); + let IpAddr::V4(ip) = entry.ip else { + return Err(client_error("ARP entry must have an IPv4 address")); + }; + match arp::add_entry_ipv4(switch, entry.tag, ip, entry.mac) { + Err(e) => Err(e.into()), + Ok(_) => Ok(HttpResponseUpdatedNoContent()), + } } -} -/** - * 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 { - let switch: &Switch = rqctx.context(); - let ip = path.into_inner().ip; - arp::delete_entry_ipv4(switch, ip) - .map(|_| HttpResponseDeleted()) - .map_err(HttpError::from) -} + /** + * Remove a single IPv4 ARP entry, by its IPv4 address. + */ + #[endpoint { + method = DELETE, + path = "/arp/{ip}", + }] + pub(super) async fn arp_delete( + rqctx: RequestContext>, + path: Path, + ) -> Result { + let switch: &Switch = rqctx.context(); + let ip = path.into_inner().ip; + arp::delete_entry_ipv4(switch, ip) + .map(|_| HttpResponseDeleted()) + .map_err(HttpError::from) + } -#[derive(Deserialize, Serialize, JsonSchema)] -struct RoutePathV4 { - /// The IPv4 subnet in CIDR notation whose route entry is returned. - cidr: Ipv4Net, -} + #[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, -} + /// 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, -} + #[derive(Deserialize, Serialize, JsonSchema)] + struct RoutePathV6 { + /// The IPv6 subnet in CIDR notation whose route entry is returned. + cidr: Ipv6Net, + } -/** - * 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 RouteToken { - cidr: IpNet, -} + /** + * 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 RouteToken { + cidr: IpNet, + } -/** - * 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> { - let switch: &Switch = rqctx.context(); - let pag_params = query.into_inner(); - let max = rqctx.page_limit(&pag_params)?.get(); - - let previous = match &pag_params.page { - WhichPage::First(..) => None, - WhichPage::Next(RouteToken { cidr }) => match cidr { - IpNet::V6(cidr) => Some(*cidr), - IpNet::V4(_) => { - return Err(DpdError::Invalid("bad token".into()).into()) - } - }, - }; + /** + * 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( + rqctx: RequestContext>, + query: Query>, + ) -> Result>, HttpError> { + let switch: &Switch = rqctx.context(); + let pag_params = query.into_inner(); + let max = rqctx.page_limit(&pag_params)?.get(); + + let previous = match &pag_params.page { + WhichPage::First(..) => None, + WhichPage::Next(RouteToken { cidr }) => match cidr { + IpNet::V6(cidr) => Some(*cidr), + IpNet::V4(_) => { + return Err(DpdError::Invalid("bad token".into()).into()) + } + }, + }; - route::get_range_ipv6(switch, previous, max) - .await - .map_err(HttpError::from) - .and_then(|entries| { - ResultsPage::new(entries, &EmptyScanParams {}, |e: &Route, _| { - RouteToken { cidr: e.cidr } + route::get_range_ipv6(switch, previous, max) + .await + .map_err(HttpError::from) + .and_then(|entries| { + ResultsPage::new( + entries, + &EmptyScanParams {}, + |e: &Route, _| RouteToken { cidr: e.cidr }, + ) }) - }) - .map(HttpResponseOk) -} + .map(HttpResponseOk) + } -/** - * 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> { - let switch: &Switch = rqctx.context(); - let cidr = path.into_inner().cidr; - route::get_route_ipv6(switch, cidr) - .await - .map(HttpResponseOk) - .map_err(HttpError::from) -} + /** + * Get a single IPv6 route, by its IPv6 CIDR block. + */ + #[endpoint { + method = GET, + path = "/route/ipv6/{cidr}", + }] + pub(super) async fn route_ipv6_get( + rqctx: RequestContext>, + path: Path, + ) -> Result>, HttpError> { + let switch: &Switch = rqctx.context(); + let cidr = path.into_inner().cidr; + route::get_route_ipv6(switch, cidr) + .await + .map(HttpResponseOk) + .map_err(HttpError::from) + } -fn net_to_v6(net: IpNet) -> Result { - let IpNet::V6(subnet) = net else { - return Err(client_error(format!("{} is IPv4", net))); - }; - Ok(subnet) -} -fn net_to_v4(net: IpNet) -> Result { - let IpNet::V4(subnet) = net else { - return Err(client_error(format!("{} is IPv6", net))); - }; - Ok(subnet) -} + fn net_to_v6(net: IpNet) -> Result { + let IpNet::V6(subnet) = net else { + return Err(client_error(format!("{} is IPv4", net))); + }; + Ok(subnet) + } + fn net_to_v4(net: IpNet) -> Result { + let IpNet::V4(subnet) = net else { + return Err(client_error(format!("{} is IPv6", net))); + }; + Ok(subnet) + } -/** - * 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 { - let switch: &Switch = rqctx.context(); - let route = update.into_inner(); - let subnet = net_to_v6(route.cidr)?; - let target = Ipv6Route::try_from(route.target)?; - route::add_route_ipv6(switch, subnet, target) - .await - .map(|_| HttpResponseUpdatedNoContent()) - .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( + rqctx: RequestContext>, + update: TypedBody, + ) -> Result { + let switch: &Switch = rqctx.context(); + let route = update.into_inner(); + let subnet = net_to_v6(route.cidr)?; + let target = Ipv6Route::try_from(route.target)?; + route::add_route_ipv6(switch, subnet, target) + .await + .map(|_| HttpResponseUpdatedNoContent()) + .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", -}] -async fn route_ipv6_set( - rqctx: RequestContext>, - update: TypedBody, -) -> Result { - let switch: &Switch = rqctx.context(); - let route = update.into_inner(); - let subnet = net_to_v6(route.cidr)?; - let target = Ipv6Route::try_from(route.target)?; - route::set_route_ipv6(switch, subnet, target, route.replace) - .await - .map(|_| HttpResponseUpdatedNoContent()) - .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( + rqctx: RequestContext>, + update: TypedBody, + ) -> Result { + let switch: &Switch = rqctx.context(); + let route = update.into_inner(); + let subnet = net_to_v6(route.cidr)?; + let target = Ipv6Route::try_from(route.target)?; + route::set_route_ipv6(switch, subnet, target, route.replace) + .await + .map(|_| HttpResponseUpdatedNoContent()) + .map_err(HttpError::from) + } -/** - * 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 { - let switch: &Switch = rqctx.context(); - let cidr = path.into_inner().cidr; - route::delete_route_ipv6(switch, cidr) - .await - .map(|_| HttpResponseDeleted()) - .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( + rqctx: RequestContext>, + path: Path, + ) -> Result { + let switch: &Switch = rqctx.context(); + let cidr = path.into_inner().cidr; + route::delete_route_ipv6(switch, cidr) + .await + .map(|_| HttpResponseDeleted()) + .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", -}] -async fn route_ipv4_list( - rqctx: RequestContext>, - query: Query>, -) -> Result>, HttpError> { - let switch: &Switch = rqctx.context(); - let pag_params = query.into_inner(); - let max = rqctx.page_limit(&pag_params)?.get(); - - let previous = match &pag_params.page { - WhichPage::First(..) => None, - WhichPage::Next(RouteToken { cidr }) => match cidr { - IpNet::V6(_) => { - return Err(DpdError::Invalid("bad token".into()).into()) - } - IpNet::V4(cidr) => Some(*cidr), - }, - }; + /** + * 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( + rqctx: RequestContext>, + query: Query>, + ) -> Result>, HttpError> { + let switch: &Switch = rqctx.context(); + let pag_params = query.into_inner(); + let max = rqctx.page_limit(&pag_params)?.get(); + + let previous = match &pag_params.page { + WhichPage::First(..) => None, + WhichPage::Next(RouteToken { cidr }) => match cidr { + IpNet::V6(_) => { + return Err(DpdError::Invalid("bad token".into()).into()) + } + IpNet::V4(cidr) => Some(*cidr), + }, + }; - route::get_range_ipv4(switch, previous, max) - .await - .map_err(HttpError::from) - .and_then(|entries| { - ResultsPage::new(entries, &EmptyScanParams {}, |e: &Route, _| { - RouteToken { cidr: e.cidr } + route::get_range_ipv4(switch, previous, max) + .await + .map_err(HttpError::from) + .and_then(|entries| { + ResultsPage::new( + entries, + &EmptyScanParams {}, + |e: &Route, _| RouteToken { cidr: e.cidr }, + ) }) - }) - .map(HttpResponseOk) -} + .map(HttpResponseOk) + } -/** - * 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> { - let switch: &Switch = rqctx.context(); - let cidr = path.into_inner().cidr; - route::get_route_ipv4(switch, cidr) - .await - .map(HttpResponseOk) - .map_err(HttpError::from) -} + /** + * Get the configured route for the given IPv4 subnet. + */ + #[endpoint { + method = GET, + path = "/route/ipv4/{cidr}", + }] + pub(super) async fn route_ipv4_get( + rqctx: RequestContext>, + path: Path, + ) -> Result>, HttpError> { + let switch: &Switch = rqctx.context(); + let cidr = path.into_inner().cidr; + route::get_route_ipv4(switch, cidr) + .await + .map(HttpResponseOk) + .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", -}] -async fn route_ipv4_add( - rqctx: RequestContext>, - update: TypedBody, -) -> Result { - let switch: &Switch = rqctx.context(); - let route = update.into_inner(); - let subnet = net_to_v4(route.cidr)?; - let target = Ipv4Route::try_from(route.target)?; - - route::add_route_ipv4(switch, subnet, target) - .await - .map(|_| HttpResponseUpdatedNoContent()) - .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( + rqctx: RequestContext>, + update: TypedBody, + ) -> Result { + let switch: &Switch = rqctx.context(); + let route = update.into_inner(); + let subnet = net_to_v4(route.cidr)?; + let target = Ipv4Route::try_from(route.target)?; + + route::add_route_ipv4(switch, subnet, target) + .await + .map(|_| HttpResponseUpdatedNoContent()) + .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", -}] -async fn route_ipv4_set( - rqctx: RequestContext>, - update: TypedBody, -) -> Result { - let switch: &Switch = rqctx.context(); - let route = update.into_inner(); - let subnet = net_to_v4(route.cidr)?; - let target = Ipv4Route::try_from(route.target)?; - route::set_route_ipv4(switch, subnet, target, route.replace) - .await - .map(|_| HttpResponseUpdatedNoContent()) - .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( + rqctx: RequestContext>, + update: TypedBody, + ) -> Result { + let switch: &Switch = rqctx.context(); + let route = update.into_inner(); + let subnet = net_to_v4(route.cidr)?; + let target = Ipv4Route::try_from(route.target)?; + route::set_route_ipv4(switch, subnet, target, route.replace) + .await + .map(|_| HttpResponseUpdatedNoContent()) + .map_err(HttpError::from) + } -/** - * Remove all targets for the given subnet - */ -#[endpoint { - method = DELETE, - path = "/route/ipv4/{cidr}", -}] -async fn route_ipv4_delete( - rqctx: RequestContext>, - path: Path, -) -> Result { - let switch: &Switch = rqctx.context(); - let cidr = path.into_inner().cidr; - route::delete_route_ipv4(switch, cidr) - .await - .map(|_| HttpResponseDeleted()) - .map_err(HttpError::from) -} -/** - * Remove a single target for the given 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 { - let switch: &Switch = rqctx.context(); - let path = path.into_inner(); - let subnet = path.cidr; - let port_id = path.port_id; - let link_id = path.link_id; - let tgt_ip = path.tgt_ip; - route::delete_route_target_ipv4(switch, subnet, port_id, link_id, tgt_ip) + /** + * Remove all targets for the given subnet + */ + #[endpoint { + method = DELETE, + path = "/route/ipv4/{cidr}", + }] + pub(super) async fn route_ipv4_delete( + rqctx: RequestContext>, + path: Path, + ) -> Result { + let switch: &Switch = rqctx.context(); + let cidr = path.into_inner().cidr; + route::delete_route_ipv4(switch, cidr) + .await + .map(|_| HttpResponseDeleted()) + .map_err(HttpError::from) + } + /** + * Remove a single target for the given subnet + */ + #[endpoint { + method = DELETE, + path = "/route/ipv4/{cidr}/{port_id}/{link_id}/{tgt_ip}", + }] + pub(super) async fn route_ipv4_delete_target( + rqctx: RequestContext>, + path: Path, + ) -> Result { + let switch: &Switch = rqctx.context(); + let path = path.into_inner(); + let subnet = path.cidr; + let port_id = path.port_id; + let link_id = path.link_id; + let tgt_ip = path.tgt_ip; + route::delete_route_target_ipv4( + switch, subnet, port_id, link_id, tgt_ip, + ) .await .map(|_| HttpResponseDeleted()) .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 PortIpv4Path { + port: String, + ipv4: Ipv4Addr, + } -#[derive(Deserialize, Serialize, JsonSchema)] -struct LoopbackIpv4Path { - ipv4: Ipv4Addr, -} + #[derive(Deserialize, Serialize, JsonSchema)] + struct PortIpv6Path { + port: String, + ipv6: Ipv6Addr, + } -#[derive(Deserialize, Serialize, JsonSchema)] -struct LoopbackIpv6Path { - ipv6: Ipv6Addr, -} + #[derive(Deserialize, Serialize, JsonSchema)] + struct LoopbackIpv4Path { + ipv4: Ipv4Addr, + } -#[derive(Deserialize, Serialize, JsonSchema)] -struct NatIpv6Path { - ipv6: Ipv6Addr, -} + #[derive(Deserialize, Serialize, JsonSchema)] + struct LoopbackIpv6Path { + ipv6: Ipv6Addr, + } -#[derive(Deserialize, Serialize, JsonSchema)] -struct NatIpv6PortPath { - ipv6: Ipv6Addr, - low: u16, -} + #[derive(Deserialize, Serialize, JsonSchema)] + struct NatIpv6Path { + ipv6: Ipv6Addr, + } -#[derive(Deserialize, Serialize, JsonSchema)] -struct NatIpv6RangePath { - ipv6: Ipv6Addr, - low: u16, - high: u16, -} + #[derive(Deserialize, Serialize, JsonSchema)] + struct NatIpv6PortPath { + ipv6: Ipv6Addr, + low: u16, + } -#[derive(Deserialize, Serialize, JsonSchema)] -struct NatIpv4Path { - ipv4: Ipv4Addr, -} + #[derive(Deserialize, Serialize, JsonSchema)] + struct NatIpv6RangePath { + ipv6: Ipv6Addr, + low: u16, + high: u16, + } -#[derive(Deserialize, Serialize, JsonSchema)] -struct NatIpv4PortPath { - ipv4: Ipv4Addr, - low: u16, -} + #[derive(Deserialize, Serialize, JsonSchema)] + struct NatIpv4Path { + ipv4: Ipv4Addr, + } -#[derive(Deserialize, Serialize, JsonSchema)] -struct NatIpv4RangePath { - ipv4: Ipv4Addr, - low: u16, - high: u16, -} + #[derive(Deserialize, Serialize, JsonSchema)] + struct NatIpv4PortPath { + ipv4: Ipv4Addr, + low: u16, + } -/** - * Represents a cursor into a paginated request for all NAT data. - */ -#[derive(Deserialize, Serialize, JsonSchema)] -struct NatToken { - port: u16, -} + #[derive(Deserialize, Serialize, JsonSchema)] + struct NatIpv4RangePath { + ipv4: Ipv4Addr, + low: u16, + high: 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, -} + /** + * Represents a cursor into a paginated request for all NAT data. + */ + #[derive(Deserialize, Serialize, JsonSchema)] + struct NatToken { + port: u16, + } -#[derive(Deserialize, Serialize, JsonSchema)] -struct PortIdPathParams { - /// The switch port on which to operate. - port_id: PortId, -} + /** + * 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 PortSettingsTag { - /// Restrict operations on this port to the provided tag. - tag: Option, -} + #[derive(Deserialize, Serialize, JsonSchema)] + struct PortIdPathParams { + /// The switch port on which to operate. + port_id: PortId, + } -/// 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 PortSettingsTag { + /// Restrict operations on this port to the provided tag. + tag: Option, + } -#[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, -} + /// 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 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, -} + #[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, + } -/// List all switch ports on the system. -#[endpoint { - method = GET, - path = "/ports", -}] -async fn port_list( - rqctx: RequestContext>, -) -> Result>, HttpError> { - Ok(HttpResponseOk( - rqctx - .context() - .switch_ports - .port_map - .port_ids() - .copied() - .collect(), - )) -} + #[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, + } -/// 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> { - let switch: &Switch = rqctx.context(); - let avail = ports::get_avail(switch)?; - let rval = avail - .into_iter() - .map(|(connector, channels)| { - let port_id = switch + /// List all switch ports on the system. + #[endpoint { + method = GET, + path = "/ports", + }] + pub(super) async fn port_list( + rqctx: RequestContext>, + ) -> Result>, HttpError> { + Ok(HttpResponseOk( + rqctx + .context() .switch_ports .port_map - .connector_to_id(&connector) - .unwrap(); - FreeChannels { - port_id, - connector: match connector { - aal::Connector::CPU => String::from("CPU"), - aal::Connector::QSFP(x) => format!("{x}"), - }, - channels, - } - }) - .collect(); + .port_ids() + .copied() + .collect(), + )) + } - Ok(HttpResponseOk(rval)) -} + /// 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( + rqctx: RequestContext>, + ) -> Result>, HttpError> { + let switch: &Switch = rqctx.context(); + let avail = ports::get_avail(switch)?; + let rval = avail + .into_iter() + .map(|(connector, channels)| { + let port_id = switch + .switch_ports + .port_map + .connector_to_id(&connector) + .unwrap(); + FreeChannels { + port_id, + connector: match connector { + aal::Connector::CPU => String::from("CPU"), + aal::Connector::QSFP(x) => format!("{x}"), + }, + channels, + } + }) + .collect(); + + Ok(HttpResponseOk(rval)) + } -/// Return information about a single switch port. -#[endpoint { - method = GET, - path = "/ports/{port_id}", -}] -async fn port_get( - rqctx: RequestContext>, - path: Path, -) -> Result, HttpError> { - let switch = rqctx.context(); - let port_id = path.into_inner().port_id; - Ok(HttpResponseOk(views::SwitchPort::from( - &*switch + /// Return information about a single switch port. + #[endpoint { + method = GET, + path = "/ports/{port_id}", + }] + pub(super) async fn port_get( + rqctx: RequestContext>, + path: Path, + ) -> Result, HttpError> { + let switch = rqctx.context(); + let port_id = path.into_inner().port_id; + Ok(HttpResponseOk(views::SwitchPort::from( + &*switch + .switch_ports + .ports + .get(&port_id) + .ok_or_else(|| { + HttpError::from(DpdError::NoSuchSwitchPort { port_id }) + })? + .lock() + .await, + ))) + } + + /// 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( + rqctx: RequestContext>, + path: Path, + ) -> Result, HttpError> { + let switch = rqctx.context(); + let port_id = path.into_inner().port_id; + switch .switch_ports .ports .get(&port_id) @@ -980,2382 +1013,2409 @@ async fn port_get( HttpError::from(DpdError::NoSuchSwitchPort { port_id }) })? .lock() - .await, - ))) -} - -/// 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> { - let switch = rqctx.context(); - let port_id = path.into_inner().port_id; - switch - .switch_ports - .ports - .get(&port_id) - .ok_or_else(|| HttpError::from(DpdError::NoSuchSwitchPort { port_id }))? - .lock() - .await - .management_mode() - .map(HttpResponseOk) - .map_err(HttpError::from) -} - -/// 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 { - let switch = rqctx.context(); - let port_id = path.into_inner().port_id; - let mode = body.into_inner(); - - let mut port = switch - .switch_ports - .ports - .get(&port_id) - .ok_or_else(|| HttpError::from(DpdError::NoSuchSwitchPort { port_id }))? - .lock() - .await; - - // Cannot set the management mode while there are links. - let links = switch.links.lock().unwrap(); - if !links.port_links(port_id).is_empty() { - return Err(HttpError::for_bad_request( - None, - String::from( - "Cannot change port management mode while links exist", - ), - )); + .await + .management_mode() + .map(HttpResponseOk) + .map_err(HttpError::from) } - port.set_management_mode(mode) - .map(|_| HttpResponseUpdatedNoContent()) - .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( + rqctx: RequestContext>, + path: Path, + body: TypedBody, + ) -> Result { + let switch = rqctx.context(); + let port_id = path.into_inner().port_id; + let mode = body.into_inner(); + + let mut port = switch + .switch_ports + .ports + .get(&port_id) + .ok_or_else(|| { + HttpError::from(DpdError::NoSuchSwitchPort { port_id }) + })? + .lock() + .await; -/// 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> { - let switch = rqctx.context(); - let port_id = path.into_inner().port_id; - switch - .get_led(port_id) - .await - .map(HttpResponseOk) - .map_err(HttpError::from) -} + // Cannot set the management mode while there are links. + let links = switch.links.lock().unwrap(); + if !links.port_links(port_id).is_empty() { + return Err(HttpError::for_bad_request( + None, + String::from( + "Cannot change port management mode while links exist", + ), + )); + } -/// 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 { - let switch = rqctx.context(); - let port_id = path.into_inner().port_id; - let state = body.into_inner(); - switch - .set_led(port_id, state) - .await - .map(|_| HttpResponseUpdatedNoContent()) - .map_err(HttpError::from) -} + port.set_management_mode(mode) + .map(|_| HttpResponseUpdatedNoContent()) + .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", -}] -async fn backplane_map( - rqctx: RequestContext>, -) -> Result>, HttpError> { - let switch = rqctx.context(); - let port_map = &switch.switch_ports.port_map; - Ok(HttpResponseOk( - port_map - .port_ids() - .filter_map(|p| { - crate::switch_port::port_id_as_backplane_link(*p) - .map(|link| (*p, link)) - }) - .collect(), - )) -} + /// 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( + rqctx: RequestContext>, + path: Path, + ) -> Result, HttpError> { + let switch = rqctx.context(); + let port_id = path.into_inner().port_id; + switch + .get_led(port_id) + .await + .map(HttpResponseOk) + .map_err(HttpError::from) + } -/// 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> { - let switch = rqctx.context(); - let port_map = &switch.switch_ports.port_map; - let port_id = path.into_inner().port_id; - if port_map.id_to_connector(&port_id).is_some() { + /// 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( + rqctx: RequestContext>, + path: Path, + body: TypedBody, + ) -> Result { + let switch = rqctx.context(); + let port_id = path.into_inner().port_id; + let state = body.into_inner(); + switch + .set_led(port_id, state) + .await + .map(|_| HttpResponseUpdatedNoContent()) + .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( + rqctx: RequestContext>, + ) -> Result>, HttpError> + { + let switch = rqctx.context(); + let port_map = &switch.switch_ports.port_map; Ok(HttpResponseOk( - crate::switch_port::port_id_as_backplane_link(port_id).unwrap(), + port_map + .port_ids() + .filter_map(|p| { + crate::switch_port::port_id_as_backplane_link(*p) + .map(|link| (*p, link)) + }) + .collect(), )) - } else { - Err(HttpError::from(DpdError::NoSuchSwitchPort { port_id })) } -} -/// 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> { - let switch = rqctx.context(); - switch - .all_leds() - .await - .map(HttpResponseOk) - .map_err(HttpError::from) -} + /// Return the backplane mapping for a single switch port. + #[endpoint { + method = GET, + path = "/backplane-map/{port_id}", + }] + pub(super) async fn port_backplane_link( + rqctx: RequestContext>, + path: Path, + ) -> Result, HttpError> { + let switch = rqctx.context(); + let port_map = &switch.switch_ports.port_map; + let port_id = path.into_inner().port_id; + if port_map.id_to_connector(&port_id).is_some() { + Ok(HttpResponseOk( + crate::switch_port::port_id_as_backplane_link(port_id).unwrap(), + )) + } else { + Err(HttpError::from(DpdError::NoSuchSwitchPort { port_id })) + } + } -/// 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 { - let switch = rqctx.context(); - let port_id = path.into_inner().port_id; - switch - .set_led_auto(port_id) - .await - .map(|_| HttpResponseUpdatedNoContent()) - .map_err(HttpError::from) -} + /// Return the state of all attention LEDs on the Sidecar QSFP ports. + #[endpoint { + method = GET, + path = "/leds", + }] + pub(super) async fn leds_list( + rqctx: RequestContext>, + ) -> Result>, HttpError> { + let switch = rqctx.context(); + switch + .all_leds() + .await + .map(HttpResponseOk) + .map_err(HttpError::from) + } -/// Return information about all QSFP transceivers. -#[endpoint { - method = GET, - path = "/transceivers", -}] -async fn transceivers_list( - rqctx: RequestContext>, -) -> Result>, HttpError> { - let switch = rqctx.context(); - let mut out = BTreeMap::new(); - for (port_id, port) in switch.switch_ports.ports.iter() { - let port = port.lock().await; - if let Some(transceiver) = - port.as_qsfp().and_then(|q| q.transceiver.as_ref()).cloned() - { - out.insert(*port_id, transceiver); + /// 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( + rqctx: RequestContext>, + path: Path, + ) -> Result { + let switch = rqctx.context(); + let port_id = path.into_inner().port_id; + switch + .set_led_auto(port_id) + .await + .map(|_| HttpResponseUpdatedNoContent()) + .map_err(HttpError::from) + } + + /// Return information about all QSFP transceivers. + #[endpoint { + method = GET, + path = "/transceivers", + }] + pub(super) async fn transceivers_list( + rqctx: RequestContext>, + ) -> Result>, HttpError> { + let switch = rqctx.context(); + let mut out = BTreeMap::new(); + for (port_id, port) in switch.switch_ports.ports.iter() { + let port = port.lock().await; + if let Some(transceiver) = + port.as_qsfp().and_then(|q| q.transceiver.as_ref()).cloned() + { + out.insert(*port_id, transceiver); + } } + Ok(HttpResponseOk(out)) } - 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", -}] -async fn transceiver_get( - rqctx: RequestContext>, - path: Path, -) -> Result, HttpError> { - let switch = rqctx.context(); - let port_id = path.into_inner().port_id; - match switch.switch_ports.ports.get(&port_id).as_ref() { - None => Err(DpdError::NoSuchSwitchPort { port_id }.into()), - Some(sp) => { - let switch_port = sp.lock().await; - match &switch_port.fixed_side { - FixedSideDevice::Qsfp { device, .. } => { - match device.transceiver.as_ref().cloned() { - Some(tr) => Ok(HttpResponseOk(tr)), - None => { - let PortId::Qsfp(qsfp_port) = port_id else { - let msg = format!( - "Expected port {port_id} to be a QSFP port!" - ); - return Err(HttpError::for_internal_error(msg)); - }; - Err(DpdError::MissingTransceiver { qsfp_port } - .into()) + /// 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( + rqctx: RequestContext>, + path: Path, + ) -> Result, HttpError> { + let switch = rqctx.context(); + let port_id = path.into_inner().port_id; + match switch.switch_ports.ports.get(&port_id).as_ref() { + None => Err(DpdError::NoSuchSwitchPort { port_id }.into()), + Some(sp) => { + let switch_port = sp.lock().await; + match &switch_port.fixed_side { + FixedSideDevice::Qsfp { device, .. } => { + match device.transceiver.as_ref().cloned() { + Some(tr) => Ok(HttpResponseOk(tr)), + None => { + let PortId::Qsfp(qsfp_port) = port_id else { + let msg = format!( + "Expected port {port_id} to be a QSFP port!" + ); + return Err(HttpError::for_internal_error( + msg, + )); + }; + Err(DpdError::MissingTransceiver { qsfp_port } + .into()) + } } } + _ => Err(DpdError::NotAQsfpPort { port_id }.into()), } - _ => Err(DpdError::NotAQsfpPort { port_id }.into()), } } } -} - -/// 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 { - let switch = rqctx.context(); - let qsfp_port = path_to_qsfp(path)?; - switch - .reset_transceiver(qsfp_port) - .await - .map(|_| HttpResponseUpdatedNoContent()) - .map_err(HttpError::from) -} -/// 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 { - let switch = rqctx.context(); - let qsfp_port = path_to_qsfp(path)?; - let state = state.into_inner(); - switch - .set_transceiver_power(qsfp_port, state) - .await - .map(|_| HttpResponseUpdatedNoContent()) - .map_err(HttpError::from) -} - -/// 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> { - let switch = rqctx.context(); - let qsfp_port = path_to_qsfp(path)?; - switch - .transceiver_power(qsfp_port) - .await - .map(HttpResponseOk) - .map_err(HttpError::from) -} - -/// 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> { - let switch = rqctx.context(); - let qsfp_port = path_to_qsfp(path)?; - switch - .transceiver_monitors(qsfp_port) - .await - .map(HttpResponseOk) - .map_err(HttpError::from) -} - -/// 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> { - let switch = rqctx.context(); - let qsfp_port = path_to_qsfp(path)?; - switch - .transceiver_datapath(qsfp_port) - .await - .map(HttpResponseOk) - .map_err(HttpError::from) -} + /// 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( + rqctx: RequestContext>, + path: Path, + ) -> Result { + let switch = rqctx.context(); + let qsfp_port = path_to_qsfp(path)?; + switch + .reset_transceiver(qsfp_port) + .await + .map(|_| HttpResponseUpdatedNoContent()) + .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 })) + /// Control the power state of a transceiver. + #[endpoint { + method = PUT, + path = "/ports/{port_id}/transceiver/power", + }] + pub(super) async fn transceiver_power_set( + rqctx: RequestContext>, + path: Path, + state: TypedBody, + ) -> Result { + let switch = rqctx.context(); + let qsfp_port = path_to_qsfp(path)?; + let state = state.into_inner(); + switch + .set_transceiver_power(qsfp_port, state) + .await + .map(|_| HttpResponseUpdatedNoContent()) + .map_err(HttpError::from) } -} -/// 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, -} + /// Return the power state of a transceiver. + #[endpoint { + method = GET, + path = "/ports/{port_id}/transceiver/power", + }] + pub(super) async fn transceiver_power_get( + rqctx: RequestContext>, + path: Path, + ) -> Result, HttpError> { + let switch = rqctx.context(); + let qsfp_port = path_to_qsfp(path)?; + switch + .transceiver_power(qsfp_port) + .await + .map(HttpResponseOk) + .map_err(HttpError::from) + } -/// 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> { - let switch: &Switch = rqctx.context(); - let port_id = path.into_inner().port_id; - let params = params.into_inner(); - switch - .create_link(port_id, ¶ms) - .map(HttpResponseCreated) - .map_err(|e| e.into()) -} + /// 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( + rqctx: RequestContext>, + path: Path, + ) -> Result, HttpError> { + let switch = rqctx.context(); + let qsfp_port = path_to_qsfp(path)?; + switch + .transceiver_monitors(qsfp_port) + .await + .map(HttpResponseOk) + .map_err(HttpError::from) + } -/// 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> { - let switch: &Switch = rqctx.context(); - let path = path.into_inner(); - switch - .get_link(path.port_id, path.link_id) - .map(HttpResponseOk) - .map_err(|e| e.into()) -} + /// 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( + rqctx: RequestContext>, + path: Path, + ) -> Result, HttpError> { + let switch = rqctx.context(); + let qsfp_port = path_to_qsfp(path)?; + switch + .transceiver_datapath(qsfp_port) + .await + .map(HttpResponseOk) + .map_err(HttpError::from) + } -/// 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 { - let switch: &Switch = rqctx.context(); - let path = path.into_inner(); - switch - .delete_link(path.port_id, path.link_id) - .map(|_| HttpResponseDeleted()) - .map_err(|e| e.into()) -} + // 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 })) + } + } -/// 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> { - let switch = &rqctx.context(); - let port_id = path.into_inner().port_id; - switch - .list_links(port_id) - .map(HttpResponseOk) - .map_err(HttpError::from) -} + /// 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. + /// Create a link on a switch port. /// - /// If not provided, then all links are returned. - filter: Option, -} - -/// List all links, on all switch ports. -#[endpoint { - method = GET, - path = "/links", -}] -async fn link_list_all( - rqctx: RequestContext>, - query: Query, -) -> Result>, HttpError> { - let switch = &rqctx.context(); - let filter = query.into_inner().filter; - 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", -}] -async fn link_enabled_get( - rqctx: RequestContext>, - path: Path, -) -> Result, HttpError> { - let switch: &Switch = rqctx.context(); - let path = path.into_inner(); - let port_id = path.port_id; - let link_id = path.link_id; - switch - .link_enabled(port_id, link_id) - .map(HttpResponseOk) - .map_err(|e| e.into()) -} - -/// 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 { - let switch: &Switch = rqctx.context(); - let path = path.into_inner(); - let port_id = path.port_id; - let link_id = path.link_id; - let enabled = body.into_inner(); - switch - .set_link_enabled(port_id, link_id, enabled) - .map(|_| HttpResponseUpdatedNoContent()) - .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", -}] -async fn link_ipv6_enabled_get( - rqctx: RequestContext>, - path: Path, -) -> Result, HttpError> { - let switch: &Switch = rqctx.context(); - let path = path.into_inner(); - let port_id = path.port_id; - let link_id = path.link_id; - switch - .link_ipv6_enabled(port_id, link_id) - .map(HttpResponseOk) - .map_err(|e| e.into()) -} + /// 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( + rqctx: RequestContext>, + path: Path, + params: TypedBody, + ) -> Result, HttpError> { + let switch: &Switch = rqctx.context(); + let port_id = path.into_inner().port_id; + let params = params.into_inner(); + switch + .create_link(port_id, ¶ms) + .map(HttpResponseCreated) + .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", -}] -async fn link_ipv6_enabled_set( - rqctx: RequestContext>, - path: Path, - body: TypedBody, -) -> Result { - let switch: &Switch = rqctx.context(); - let path = path.into_inner(); - let port_id = path.port_id; - let link_id = path.link_id; - let enabled = body.into_inner(); - switch - .set_link_ipv6_enabled(port_id, link_id, enabled) - .map(|_| HttpResponseUpdatedNoContent()) - .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( + rqctx: RequestContext>, + path: Path, + ) -> Result, HttpError> { + let switch: &Switch = rqctx.context(); + let path = path.into_inner(); + switch + .get_link(path.port_id, path.link_id) + .map(HttpResponseOk) + .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", -}] -async fn link_kr_get( - rqctx: RequestContext>, - path: Path, -) -> Result, HttpError> { - let switch: &Switch = rqctx.context(); - let path = path.into_inner(); - let port_id = path.port_id; - let link_id = path.link_id; - switch - .link_kr(port_id, link_id) - .map(HttpResponseOk) - .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( + rqctx: RequestContext>, + path: Path, + ) -> Result { + let switch: &Switch = rqctx.context(); + let path = path.into_inner(); + switch + .delete_link(path.port_id, path.link_id) + .map(|_| HttpResponseDeleted()) + .map_err(|e| e.into()) + } -/// 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 { - let switch: &Switch = rqctx.context(); - let path = path.into_inner(); - let port_id = path.port_id; - let link_id = path.link_id; - let kr = body.into_inner(); - switch - .set_link_kr(port_id, link_id, kr) - .map(|_| HttpResponseUpdatedNoContent()) - .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( + rqctx: RequestContext>, + path: Path, + ) -> Result>, HttpError> { + let switch = &rqctx.context(); + let port_id = path.into_inner().port_id; + switch + .list_links(port_id) + .map(HttpResponseOk) + .map_err(HttpError::from) + } -/// 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> { - let switch: &Switch = rqctx.context(); - let path = path.into_inner(); - let port_id = path.port_id; - let link_id = path.link_id; - switch - .link_autoneg(port_id, link_id) - .map(HttpResponseOk) - .map_err(|e| e.into()) -} + #[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, + } -/// 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 { - let switch: &Switch = rqctx.context(); - let path = path.into_inner(); - let port_id = path.port_id; - let link_id = path.link_id; - let autoneg = body.into_inner(); - switch - .set_link_autoneg(port_id, link_id, autoneg) - .map(|_| HttpResponseUpdatedNoContent()) - .map_err(|e| e.into()) -} + /// List all links, on all switch ports. + #[endpoint { + method = GET, + path = "/links", + }] + pub(super) async fn link_list_all( + rqctx: RequestContext>, + query: Query, + ) -> Result>, HttpError> { + let switch = &rqctx.context(); + let filter = query.into_inner().filter; + Ok(HttpResponseOk(switch.list_all_links(filter.as_deref()))) + } -/// 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 { - let switch: &Switch = rqctx.context(); - let path = path.into_inner(); - let port_id = path.port_id; - let link_id = path.link_id; - let prbs = body.into_inner(); - switch - .set_link_prbs(port_id, link_id, prbs) - .map(|_| HttpResponseUpdatedNoContent()) - .map_err(|e| e.into()) -} + /// Return whether the link is enabled. + #[endpoint { + method = GET, + path = "/ports/{port_id}/links/{link_id}/enabled", + }] + pub(super) async fn link_enabled_get( + rqctx: RequestContext>, + path: Path, + ) -> Result, HttpError> { + let switch: &Switch = rqctx.context(); + let path = path.into_inner(); + let port_id = path.port_id; + let link_id = path.link_id; + switch + .link_enabled(port_id, link_id) + .map(HttpResponseOk) + .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", -}] -async fn link_prbs_get( - rqctx: RequestContext>, - path: Path, -) -> Result, HttpError> { - let switch: &Switch = rqctx.context(); - let path = path.into_inner(); - let port_id = path.port_id; - let link_id = path.link_id; - switch - .link_prbs(port_id, link_id) - .map(HttpResponseOk) - .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( + rqctx: RequestContext>, + path: Path, + body: TypedBody, + ) -> Result { + let switch: &Switch = rqctx.context(); + let path = path.into_inner(); + let port_id = path.port_id; + let link_id = path.link_id; + let enabled = body.into_inner(); + switch + .set_link_enabled(port_id, link_id, enabled) + .map(|_| HttpResponseUpdatedNoContent()) + .map_err(|e| e.into()) + } -/// 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> { - let switch: &Switch = rqctx.context(); - let path = path.into_inner(); - let port_id = path.port_id; - let link_id = path.link_id; - switch - .link_up(port_id, link_id) - .map(HttpResponseOk) - .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( + rqctx: RequestContext>, + path: Path, + ) -> Result, HttpError> { + let switch: &Switch = rqctx.context(); + let path = path.into_inner(); + let port_id = path.port_id; + let link_id = path.link_id; + switch + .link_ipv6_enabled(port_id, link_id) + .map(HttpResponseOk) + .map_err(|e| e.into()) + } -/// 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> { - let switch: &Switch = rqctx.context(); - let path = path.into_inner(); - let port_id = path.port_id; - let link_id = path.link_id; - switch - .link_get_fault(port_id, link_id) - .map(|fault| HttpResponseOk(FaultCondition { fault })) - .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( + rqctx: RequestContext>, + path: Path, + body: TypedBody, + ) -> Result { + let switch: &Switch = rqctx.context(); + let path = path.into_inner(); + let port_id = path.port_id; + let link_id = path.link_id; + let enabled = body.into_inner(); + switch + .set_link_ipv6_enabled(port_id, link_id, enabled) + .map(|_| HttpResponseUpdatedNoContent()) + .map_err(|e| e.into()) + } -/// 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 { - let switch: &Switch = rqctx.context(); - let path = path.into_inner(); - let port_id = path.port_id; - let link_id = path.link_id; - switch - .link_clear_fault(port_id, link_id) - .map(|_| HttpResponseDeleted()) - .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( + rqctx: RequestContext>, + path: Path, + ) -> Result, HttpError> { + let switch: &Switch = rqctx.context(); + let path = path.into_inner(); + let port_id = path.port_id; + let link_id = path.link_id; + switch + .link_kr(port_id, link_id) + .map(HttpResponseOk) + .map_err(|e| e.into()) + } -/// 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 { - let switch: &Switch = rqctx.context(); - let path = path.into_inner(); - let port_id = path.port_id; - let link_id = path.link_id; - let entry = entry.into_inner(); - switch - .link_set_fault(port_id, link_id, Fault::Injected(entry.to_string())) - .map(|_| HttpResponseUpdatedNoContent()) - .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( + rqctx: RequestContext>, + path: Path, + body: TypedBody, + ) -> Result { + let switch: &Switch = rqctx.context(); + let path = path.into_inner(); + let port_id = path.port_id; + let link_id = path.link_id; + let kr = body.into_inner(); + switch + .set_link_kr(port_id, link_id, kr) + .map(|_| HttpResponseUpdatedNoContent()) + .map_err(|e| e.into()) + } -/// 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> { - let switch: &Switch = rqctx.context(); - let path = path.into_inner(); - let port_id = path.port_id; - let link_id = path.link_id; - let pagination = query.into_inner(); - let Ok(limit) = usize::try_from(rqctx.page_limit(&pagination)?.get()) - else { - return Err(DpdError::Invalid("Invalid page limit".to_string()).into()); - }; - let addr = match &pagination.page { - WhichPage::First(..) => None, - WhichPage::Next(Ipv4Token { ip }) => Some(*ip), - }; - let entries = switch.list_ipv4_addresses(port_id, link_id, addr, limit)?; - ResultsPage::new(entries, &EmptyScanParams {}, |entry: &Ipv4Entry, _| { - Ipv4Token { ip: entry.addr } - }) - .map(HttpResponseOk) -} + /// 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( + rqctx: RequestContext>, + path: Path, + ) -> Result, HttpError> { + let switch: &Switch = rqctx.context(); + let path = path.into_inner(); + let port_id = path.port_id; + let link_id = path.link_id; + switch + .link_autoneg(port_id, link_id) + .map(HttpResponseOk) + .map_err(|e| e.into()) + } -/// 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 { - let switch: &Switch = rqctx.context(); - let path = path.into_inner(); - let port_id = path.port_id; - let link_id = path.link_id; - let entry = entry.into_inner(); - switch - .create_ipv4_address(port_id, link_id, entry) - .map(|_| HttpResponseUpdatedNoContent()) - .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( + rqctx: RequestContext>, + path: Path, + body: TypedBody, + ) -> Result { + let switch: &Switch = rqctx.context(); + let path = path.into_inner(); + let port_id = path.port_id; + let link_id = path.link_id; + let autoneg = body.into_inner(); + switch + .set_link_autoneg(port_id, link_id, autoneg) + .map(|_| HttpResponseUpdatedNoContent()) + .map_err(|e| e.into()) + } -/// 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 { - let switch: &Switch = rqctx.context(); - let path = path.into_inner(); - let port_id = path.port_id; - let link_id = path.link_id; - switch - .reset_ipv4_addresses(port_id, link_id) - .map(|_| HttpResponseUpdatedNoContent()) - .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( + rqctx: RequestContext>, + path: Path, + body: TypedBody, + ) -> Result { + let switch: &Switch = rqctx.context(); + let path = path.into_inner(); + let port_id = path.port_id; + let link_id = path.link_id; + let prbs = body.into_inner(); + switch + .set_link_prbs(port_id, link_id, prbs) + .map(|_| HttpResponseUpdatedNoContent()) + .map_err(|e| e.into()) + } -/// 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 { - let switch: &Switch = rqctx.context(); - let path = path.into_inner(); - let port_id = path.port_id; - let link_id = path.link_id; - let address = path.address; - switch - .delete_ipv4_address(port_id, link_id, address) - .map(|_| HttpResponseDeleted()) - .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( + rqctx: RequestContext>, + path: Path, + ) -> Result, HttpError> { + let switch: &Switch = rqctx.context(); + let path = path.into_inner(); + let port_id = path.port_id; + let link_id = path.link_id; + switch + .link_prbs(port_id, link_id) + .map(HttpResponseOk) + .map_err(|e| e.into()) + } -/// 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> { - let switch: &Switch = rqctx.context(); - let path = path.into_inner(); - let port_id = path.port_id; - let link_id = path.link_id; - let pagination = query.into_inner(); - let Ok(limit) = usize::try_from(rqctx.page_limit(&pagination)?.get()) - else { - return Err(DpdError::Invalid("Invalid page limit".to_string()).into()); - }; - let addr = match &pagination.page { - WhichPage::First(..) => None, - WhichPage::Next(Ipv6Token { ip }) => Some(*ip), - }; - let entries = switch.list_ipv6_addresses(port_id, link_id, addr, limit)?; - ResultsPage::new(entries, &EmptyScanParams {}, |entry: &Ipv6Entry, _| { - Ipv6Token { ip: entry.addr } - }) - .map(HttpResponseOk) -} + /// Return whether a link is up. + #[endpoint { + method = GET, + path = "/ports/{port_id}/links/{link_id}/linkup", + }] + pub(super) async fn link_linkup_get( + rqctx: RequestContext>, + path: Path, + ) -> Result, HttpError> { + let switch: &Switch = rqctx.context(); + let path = path.into_inner(); + let port_id = path.port_id; + let link_id = path.link_id; + switch + .link_up(port_id, link_id) + .map(HttpResponseOk) + .map_err(|e| e.into()) + } -/// 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 { - let switch: &Switch = rqctx.context(); - let path = path.into_inner(); - let port_id = path.port_id; - let link_id = path.link_id; - let entry = entry.into_inner(); - switch - .create_ipv6_address(port_id, link_id, entry) - .map(|_| HttpResponseUpdatedNoContent()) - .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( + rqctx: RequestContext>, + path: Path, + ) -> Result, HttpError> { + let switch: &Switch = rqctx.context(); + let path = path.into_inner(); + let port_id = path.port_id; + let link_id = path.link_id; + switch + .link_get_fault(port_id, link_id) + .map(|fault| HttpResponseOk(FaultCondition { fault })) + .map_err(|e| e.into()) + } -/// 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 { - let switch: &Switch = rqctx.context(); - let path = path.into_inner(); - let port_id = path.port_id; - let link_id = path.link_id; - switch - .reset_ipv6_addresses(port_id, link_id) - .map(|_| HttpResponseUpdatedNoContent()) - .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( + rqctx: RequestContext>, + path: Path, + ) -> Result { + let switch: &Switch = rqctx.context(); + let path = path.into_inner(); + let port_id = path.port_id; + let link_id = path.link_id; + switch + .link_clear_fault(port_id, link_id) + .map(|_| HttpResponseDeleted()) + .map_err(|e| e.into()) + } -/// 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 { - let switch: &Switch = rqctx.context(); - let path = path.into_inner(); - let port_id = path.port_id; - let link_id = path.link_id; - let address = path.address; - switch - .delete_ipv6_address(port_id, link_id, address) - .map(|_| HttpResponseDeleted()) - .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( + rqctx: RequestContext>, + path: Path, + entry: TypedBody, + ) -> Result { + let switch: &Switch = rqctx.context(); + let path = path.into_inner(); + let port_id = path.port_id; + let link_id = path.link_id; + let entry = entry.into_inner(); + switch + .link_set_fault( + port_id, + link_id, + Fault::Injected(entry.to_string()), + ) + .map(|_| HttpResponseUpdatedNoContent()) + .map_err(|e| e.into()) + } -/// 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> { - let switch: &Switch = rqctx.context(); - let path = path.into_inner(); - let port_id = path.port_id; - let link_id = path.link_id; - switch - .link_mac_address(port_id, link_id) + /// 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( + rqctx: RequestContext>, + path: Path, + query: Query>, + ) -> Result>, HttpError> { + let switch: &Switch = rqctx.context(); + let path = path.into_inner(); + let port_id = path.port_id; + let link_id = path.link_id; + let pagination = query.into_inner(); + let Ok(limit) = usize::try_from(rqctx.page_limit(&pagination)?.get()) + else { + return Err( + DpdError::Invalid("Invalid page limit".to_string()).into() + ); + }; + let addr = match &pagination.page { + WhichPage::First(..) => None, + WhichPage::Next(Ipv4Token { ip }) => Some(*ip), + }; + let entries = + switch.list_ipv4_addresses(port_id, link_id, addr, limit)?; + ResultsPage::new( + entries, + &EmptyScanParams {}, + |entry: &Ipv4Entry, _| Ipv4Token { ip: entry.addr }, + ) .map(HttpResponseOk) - .map_err(|e| e.into()) -} + } -/// Set a link's MAC address. -#[endpoint { - method = PUT, - path = "/ports/{port_id}/links/{link_id}/mac", -}] -async fn link_mac_set( - rqctx: RequestContext>, - path: Path, - body: TypedBody, -) -> Result { - let switch: &Switch = rqctx.context(); - let path = path.into_inner(); - let port_id = path.port_id; - let link_id = path.link_id; - let mac = body.into_inner(); - switch - .set_link_mac_address(port_id, link_id, mac) - .map(|_| HttpResponseUpdatedNoContent()) - .map_err(|e| e.into()) -} + /// 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( + rqctx: RequestContext>, + path: Path, + entry: TypedBody, + ) -> Result { + let switch: &Switch = rqctx.context(); + let path = path.into_inner(); + let port_id = path.port_id; + let link_id = path.link_id; + let entry = entry.into_inner(); + switch + .create_ipv4_address(port_id, link_id, entry) + .map(|_| HttpResponseUpdatedNoContent()) + .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", -}] -async fn link_nat_only_get( - rqctx: RequestContext>, - path: Path, -) -> Result, HttpError> { - let switch: &Switch = rqctx.context(); - let path = path.into_inner(); - let port_id = path.port_id; - let link_id = path.link_id; - switch - .link_nat_only(port_id, link_id) - .map(HttpResponseOk) - .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( + rqctx: RequestContext>, + path: Path, + ) -> Result { + let switch: &Switch = rqctx.context(); + let path = path.into_inner(); + let port_id = path.port_id; + let link_id = path.link_id; + switch + .reset_ipv4_addresses(port_id, link_id) + .map(|_| HttpResponseUpdatedNoContent()) + .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", -}] -async fn link_nat_only_set( - rqctx: RequestContext>, - path: Path, - body: TypedBody, -) -> Result { - let switch: &Switch = rqctx.context(); - let path = path.into_inner(); - let port_id = path.port_id; - let link_id = path.link_id; - let nat_only = body.into_inner(); - switch - .set_link_nat_only(port_id, link_id, nat_only) - .map(|_| HttpResponseUpdatedNoContent()) - .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( + rqctx: RequestContext>, + path: Path, + ) -> Result { + let switch: &Switch = rqctx.context(); + let path = path.into_inner(); + let port_id = path.port_id; + let link_id = path.link_id; + let address = path.address; + switch + .delete_ipv4_address(port_id, link_id, address) + .map(|_| HttpResponseDeleted()) + .map_err(|e| e.into()) + } -/// 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> { - let switch: &Switch = rqctx.context(); - let path = path.into_inner(); - let port_id = path.port_id; - let link_id = path.link_id; - switch - .link_history_get(port_id, link_id) + /// 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( + rqctx: RequestContext>, + path: Path, + query: Query>, + ) -> Result>, HttpError> { + let switch: &Switch = rqctx.context(); + let path = path.into_inner(); + let port_id = path.port_id; + let link_id = path.link_id; + let pagination = query.into_inner(); + let Ok(limit) = usize::try_from(rqctx.page_limit(&pagination)?.get()) + else { + return Err( + DpdError::Invalid("Invalid page limit".to_string()).into() + ); + }; + let addr = match &pagination.page { + WhichPage::First(..) => None, + WhichPage::Next(Ipv6Token { ip }) => Some(*ip), + }; + let entries = + switch.list_ipv6_addresses(port_id, link_id, addr, limit)?; + ResultsPage::new( + entries, + &EmptyScanParams {}, + |entry: &Ipv6Entry, _| Ipv6Token { ip: entry.addr }, + ) .map(HttpResponseOk) - .map_err(|e| e.into()) -} - -/** - * Get loopback IPv4 addresses. - */ -#[endpoint { - method = GET, - path = "/loopback/ipv4", -}] -async fn loopback_ipv4_list( - rqctx: RequestContext>, -) -> Result>, HttpError> { - let switch: &Switch = rqctx.context(); - let addrs = match switch.loopback.lock() { - Ok(loopback_data) => loopback_data.v4_addrs.iter().cloned().collect(), - Err(e) => return Err(HttpError::for_internal_error(e.to_string())), - }; - Ok(HttpResponseOk(addrs)) -} + } -/** - * Add a loopback IPv4. - */ -#[endpoint { - method = POST, - path = "/loopback/ipv4", -}] -async fn loopback_ipv4_create( - rqctx: RequestContext>, - val: TypedBody, -) -> Result { - let switch: &Switch = rqctx.context(); - let addr = val.into_inner(); - - loopback::add_loopback_ipv4(switch, &addr)?; - - Ok(HttpResponseUpdatedNoContent {}) -} + /// 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( + rqctx: RequestContext>, + path: Path, + entry: TypedBody, + ) -> Result { + let switch: &Switch = rqctx.context(); + let path = path.into_inner(); + let port_id = path.port_id; + let link_id = path.link_id; + let entry = entry.into_inner(); + switch + .create_ipv6_address(port_id, link_id, entry) + .map(|_| HttpResponseUpdatedNoContent()) + .map_err(|e| e.into()) + } -/** - * Remove one loopback IPv4 address. - */ -#[endpoint { - method = DELETE, - path = "/loopback/ipv4/{ipv4}", -}] -async fn loopback_ipv4_delete( - rqctx: RequestContext>, - path: Path, -) -> Result { - let switch: &Switch = rqctx.context(); - let addr = path.into_inner(); - loopback::delete_loopback_ipv4(switch, &addr.ipv4) - .map(|_| HttpResponseDeleted()) - .map_err(HttpError::from) -} + /// 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( + rqctx: RequestContext>, + path: Path, + ) -> Result { + let switch: &Switch = rqctx.context(); + let path = path.into_inner(); + let port_id = path.port_id; + let link_id = path.link_id; + switch + .reset_ipv6_addresses(port_id, link_id) + .map(|_| HttpResponseUpdatedNoContent()) + .map_err(|e| e.into()) + } -/** - * Get loopback IPv6 addresses. - */ -#[endpoint { - method = GET, - path = "/loopback/ipv6", -}] -async fn loopback_ipv6_list( - rqctx: RequestContext>, -) -> Result>, HttpError> { - let switch: &Switch = rqctx.context(); - let addrs = match switch.loopback.lock() { - Ok(loopback_data) => loopback_data.v6_addrs.iter().cloned().collect(), - Err(e) => return Err(HttpError::for_internal_error(e.to_string())), - }; - Ok(HttpResponseOk(addrs)) -} + /// 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( + rqctx: RequestContext>, + path: Path, + ) -> Result { + let switch: &Switch = rqctx.context(); + let path = path.into_inner(); + let port_id = path.port_id; + let link_id = path.link_id; + let address = path.address; + switch + .delete_ipv6_address(port_id, link_id, address) + .map(|_| HttpResponseDeleted()) + .map_err(|e| e.into()) + } -/** - * Add a loopback IPv6. - */ -#[endpoint { - method = POST, - path = "/loopback/ipv6", -}] -async fn loopback_ipv6_create( - rqctx: RequestContext>, - val: TypedBody, -) -> Result { - let switch: &Switch = rqctx.context(); - let addr = val.into_inner(); - - loopback::add_loopback_ipv6(switch, &addr)?; - - Ok(HttpResponseUpdatedNoContent {}) -} + /// Get a link's MAC address. + #[endpoint { + method = GET, + path = "/ports/{port_id}/links/{link_id}/mac", + }] + pub(super) async fn link_mac_get( + rqctx: RequestContext>, + path: Path, + ) -> Result, HttpError> { + let switch: &Switch = rqctx.context(); + let path = path.into_inner(); + let port_id = path.port_id; + let link_id = path.link_id; + switch + .link_mac_address(port_id, link_id) + .map(HttpResponseOk) + .map_err(|e| e.into()) + } -/** - * Remove one loopback IPv6 address. - */ -#[endpoint { - method = DELETE, - path = "/loopback/ipv6/{ipv6}", -}] -async fn loopback_ipv6_delete( - rqctx: RequestContext>, - path: Path, -) -> Result { - let switch: &Switch = rqctx.context(); - let addr = path.into_inner(); - loopback::delete_loopback_ipv6(switch, &addr.ipv6) - .map(|_| HttpResponseDeleted()) - .map_err(HttpError::from) -} + /// Set a link's MAC address. + #[endpoint { + method = PUT, + path = "/ports/{port_id}/links/{link_id}/mac", + }] + pub(super) async fn link_mac_set( + rqctx: RequestContext>, + path: Path, + body: TypedBody, + ) -> Result { + let switch: &Switch = rqctx.context(); + let path = path.into_inner(); + let port_id = path.port_id; + let link_id = path.link_id; + let mac = body.into_inner(); + switch + .set_link_mac_address(port_id, link_id, mac) + .map(|_| HttpResponseUpdatedNoContent()) + .map_err(|e| e.into()) + } -/** - * 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> { - let switch: &Switch = rqctx.context(); - let pag_params = query.into_inner(); - let max = rqctx.page_limit(&pag_params)?.get(); - - let last_addr = match &pag_params.page { - WhichPage::First(..) => None, - WhichPage::Next(Ipv6Token { ip }) => Some(*ip), - }; + /// 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( + rqctx: RequestContext>, + path: Path, + ) -> Result, HttpError> { + let switch: &Switch = rqctx.context(); + let path = path.into_inner(); + let port_id = path.port_id; + let link_id = path.link_id; + switch + .link_nat_only(port_id, link_id) + .map(HttpResponseOk) + .map_err(|e| e.into()) + } - let entries = nat::get_ipv6_addrs_range( - switch, - last_addr, - usize::try_from(max).expect("invalid usize"), - ); - - Ok(HttpResponseOk(ResultsPage::new( - entries, - &EmptyScanParams {}, - |ip: &Ipv6Addr, _| Ipv6Token { ip: *ip }, - )?)) -} + /// 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( + rqctx: RequestContext>, + path: Path, + body: TypedBody, + ) -> Result { + let switch: &Switch = rqctx.context(); + let path = path.into_inner(); + let port_id = path.port_id; + let link_id = path.link_id; + let nat_only = body.into_inner(); + switch + .set_link_nat_only(port_id, link_id, nat_only) + .map(|_| HttpResponseUpdatedNoContent()) + .map_err(|e| e.into()) + } -/** - * 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> { - let switch: &Switch = rqctx.context(); - let params = path.into_inner(); - let pag_params = query.into_inner(); - let max = rqctx.page_limit(&pag_params)?.get(); - let port = match &pag_params.page { - WhichPage::First(..) => None, - WhichPage::Next(NatToken { port }) => Some(*port), - }; + /// 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( + rqctx: RequestContext>, + path: Path, + ) -> Result, HttpError> { + let switch: &Switch = rqctx.context(); + let path = path.into_inner(); + let port_id = path.port_id; + let link_id = path.link_id; + switch + .link_history_get(port_id, link_id) + .map(HttpResponseOk) + .map_err(|e| e.into()) + } - let entries = nat::get_ipv6_mappings_range( - switch, - params.ipv6, - port, - usize::try_from(max).expect("invalid usize"), - ); - - Ok(HttpResponseOk(ResultsPage::new( - entries, - &EmptyScanParams {}, - |e: &Ipv6Nat, _| NatToken { port: e.low }, - )?)) -} + /** + * Get loopback IPv4 addresses. + */ + #[endpoint { + method = GET, + path = "/loopback/ipv4", + }] + pub(super) async fn loopback_ipv4_list( + rqctx: RequestContext>, + ) -> Result>, HttpError> { + let switch: &Switch = rqctx.context(); + let addrs = match switch.loopback.lock() { + Ok(loopback_data) => { + loopback_data.v4_addrs.iter().cloned().collect() + } + Err(e) => return Err(HttpError::for_internal_error(e.to_string())), + }; + Ok(HttpResponseOk(addrs)) + } -/** - * 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> { - let switch: &Switch = rqctx.context(); - let params = path.into_inner(); - match nat::get_ipv6_mapping(switch, params.ipv6, params.low, params.low) { - Ok(tgt) => Ok(HttpResponseOk(tgt)), - Err(e) => Err(e.into()), + /** + * Add a loopback IPv4. + */ + #[endpoint { + method = POST, + path = "/loopback/ipv4", + }] + pub(super) async fn loopback_ipv4_create( + rqctx: RequestContext>, + val: TypedBody, + ) -> Result { + let switch: &Switch = rqctx.context(); + let addr = val.into_inner(); + + loopback::add_loopback_ipv4(switch, &addr)?; + + Ok(HttpResponseUpdatedNoContent {}) } -} -/** - * 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 { - let switch: &Switch = rqctx.context(); - let params = path.into_inner(); - match nat::set_ipv6_mapping( - switch, - params.ipv6, - params.low, - params.high, - target.into_inner(), - ) { - Ok(_) => Ok(HttpResponseUpdatedNoContent()), - Err(e) => Err(e.into()), + /** + * Remove one loopback IPv4 address. + */ + #[endpoint { + method = DELETE, + path = "/loopback/ipv4/{ipv4}", + }] + pub(super) async fn loopback_ipv4_delete( + rqctx: RequestContext>, + path: Path, + ) -> Result { + let switch: &Switch = rqctx.context(); + let addr = path.into_inner(); + loopback::delete_loopback_ipv4(switch, &addr.ipv4) + .map(|_| HttpResponseDeleted()) + .map_err(HttpError::from) } -} -/** - * 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 { - let switch: &Switch = rqctx.context(); - let params = path.into_inner(); - nat::clear_ipv6_mapping(switch, params.ipv6, params.low, params.low) - .map(|_| HttpResponseDeleted()) - .map_err(HttpError::from) -} + /** + * Get loopback IPv6 addresses. + */ + #[endpoint { + method = GET, + path = "/loopback/ipv6", + }] + pub(super) async fn loopback_ipv6_list( + rqctx: RequestContext>, + ) -> Result>, HttpError> { + let switch: &Switch = rqctx.context(); + let addrs = match switch.loopback.lock() { + Ok(loopback_data) => { + loopback_data.v6_addrs.iter().cloned().collect() + } + Err(e) => return Err(HttpError::for_internal_error(e.to_string())), + }; + Ok(HttpResponseOk(addrs)) + } -/** - * Clear all IPv6 NAT mappings. - */ -#[endpoint { - method = DELETE, - path = "/nat/ipv6" -}] -async fn nat_ipv6_reset( - rqctx: RequestContext>, -) -> Result { - let switch: &Switch = rqctx.context(); - - match nat::reset_ipv6(switch) { - Ok(_) => Ok(HttpResponseUpdatedNoContent()), - Err(e) => Err(e.into()), + /** + * Add a loopback IPv6. + */ + #[endpoint { + method = POST, + path = "/loopback/ipv6", + }] + pub(super) async fn loopback_ipv6_create( + rqctx: RequestContext>, + val: TypedBody, + ) -> Result { + let switch: &Switch = rqctx.context(); + let addr = val.into_inner(); + + loopback::add_loopback_ipv6(switch, &addr)?; + + Ok(HttpResponseUpdatedNoContent {}) } -} -/** - * 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> { - let switch: &Switch = rqctx.context(); - let pag_params = query.into_inner(); - let max = rqctx.page_limit(&pag_params)?.get(); - - let last_addr = match &pag_params.page { - WhichPage::First(..) => None, - WhichPage::Next(Ipv4Token { ip }) => Some(*ip), - }; + /** + * Remove one loopback IPv6 address. + */ + #[endpoint { + method = DELETE, + path = "/loopback/ipv6/{ipv6}", + }] + pub(super) async fn loopback_ipv6_delete( + rqctx: RequestContext>, + path: Path, + ) -> Result { + let switch: &Switch = rqctx.context(); + let addr = path.into_inner(); + loopback::delete_loopback_ipv6(switch, &addr.ipv6) + .map(|_| HttpResponseDeleted()) + .map_err(HttpError::from) + } - let entries = nat::get_ipv4_addrs_range( - switch, - last_addr, - usize::try_from(max).expect("invalid usize"), - ); - - Ok(HttpResponseOk(ResultsPage::new( - entries, - &EmptyScanParams {}, - |ip: &Ipv4Addr, _| Ipv4Token { ip: *ip }, - )?)) -} + /** + * 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( + rqctx: RequestContext>, + query: Query>, + ) -> Result>, HttpError> { + let switch: &Switch = rqctx.context(); + let pag_params = query.into_inner(); + let max = rqctx.page_limit(&pag_params)?.get(); + + let last_addr = match &pag_params.page { + WhichPage::First(..) => None, + WhichPage::Next(Ipv6Token { ip }) => Some(*ip), + }; -/** - * 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> { - let switch: &Switch = rqctx.context(); - let params = path.into_inner(); - let pag_params = query.into_inner(); - let max = rqctx.page_limit(&pag_params)?.get(); - - let port = match &pag_params.page { - WhichPage::First(..) => None, - WhichPage::Next(NatToken { port }) => Some(*port), - }; + let entries = nat::get_ipv6_addrs_range( + switch, + last_addr, + usize::try_from(max).expect("invalid usize"), + ); + + Ok(HttpResponseOk(ResultsPage::new( + entries, + &EmptyScanParams {}, + |ip: &Ipv6Addr, _| Ipv6Token { ip: *ip }, + )?)) + } - let entries = nat::get_ipv4_mappings_range( - switch, - params.ipv4, - port, - usize::try_from(max).expect("invalid usize"), - ); - - Ok(HttpResponseOk(ResultsPage::new( - entries, - &EmptyScanParams {}, - |e: &Ipv4Nat, _| NatToken { port: e.low }, - )?)) -} + /** + * 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( + rqctx: RequestContext>, + path: Path, + query: Query>, + ) -> Result>, HttpError> { + let switch: &Switch = rqctx.context(); + let params = path.into_inner(); + let pag_params = query.into_inner(); + let max = rqctx.page_limit(&pag_params)?.get(); + let port = match &pag_params.page { + WhichPage::First(..) => None, + WhichPage::Next(NatToken { port }) => Some(*port), + }; -/** - * 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> { - let switch: &Switch = rqctx.context(); - let params = path.into_inner(); - match nat::get_ipv4_mapping(switch, params.ipv4, params.low, params.low) { - Ok(tgt) => Ok(HttpResponseOk(tgt)), - Err(e) => Err(e.into()), + let entries = nat::get_ipv6_mappings_range( + switch, + params.ipv6, + port, + usize::try_from(max).expect("invalid usize"), + ); + + Ok(HttpResponseOk(ResultsPage::new( + entries, + &EmptyScanParams {}, + |e: &Ipv6Nat, _| NatToken { port: e.low }, + )?)) } -} -/** - * 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 { - let switch: &Switch = rqctx.context(); - let params = path.into_inner(); - match nat::set_ipv4_mapping( - switch, - params.ipv4, - params.low, - params.high, - target.into_inner(), - ) { - Ok(_) => Ok(HttpResponseUpdatedNoContent()), - Err(e) => Err(e.into()), + /** + * 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( + rqctx: RequestContext>, + path: Path, + ) -> Result, HttpError> { + let switch: &Switch = rqctx.context(); + let params = path.into_inner(); + match nat::get_ipv6_mapping(switch, params.ipv6, params.low, params.low) + { + Ok(tgt) => Ok(HttpResponseOk(tgt)), + Err(e) => Err(e.into()), + } } -} -/** - * 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 { - let switch: &Switch = rqctx.context(); - let params = path.into_inner(); - nat::clear_ipv4_mapping(switch, params.ipv4, params.low, params.low) - .map(|_| HttpResponseDeleted()) - .map_err(HttpError::from) -} + /** + * 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( + rqctx: RequestContext>, + path: Path, + target: TypedBody, + ) -> Result { + let switch: &Switch = rqctx.context(); + let params = path.into_inner(); + match nat::set_ipv6_mapping( + switch, + params.ipv6, + params.low, + params.high, + target.into_inner(), + ) { + Ok(_) => Ok(HttpResponseUpdatedNoContent()), + Err(e) => Err(e.into()), + } + } -/** - * Clear all IPv4 NAT mappings. - */ -#[endpoint { - method = DELETE, - path = "/nat/ipv4" -}] -async fn nat_ipv4_reset( - rqctx: RequestContext>, -) -> Result { - let switch: &Switch = rqctx.context(); - - match nat::reset_ipv4(switch) { - Ok(_) => Ok(HttpResponseUpdatedNoContent()), - Err(e) => Err(e.into()), + /** + * 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( + rqctx: RequestContext>, + path: Path, + ) -> Result { + let switch: &Switch = rqctx.context(); + let params = path.into_inner(); + nat::clear_ipv6_mapping(switch, params.ipv6, params.low, params.low) + .map(|_| HttpResponseDeleted()) + .map_err(HttpError::from) } -} -#[derive(Deserialize, Serialize, JsonSchema)] -struct TagPath { - tag: String, -} + /** + * Clear all IPv6 NAT mappings. + */ + #[endpoint { + method = DELETE, + path = "/nat/ipv6" + }] + pub(super) async fn nat_ipv6_reset( + rqctx: RequestContext>, + ) -> Result { + let switch: &Switch = rqctx.context(); + + match nat::reset_ipv6(switch) { + Ok(_) => Ok(HttpResponseUpdatedNoContent()), + Err(e) => Err(e.into()), + } + } -/** - * 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 { - let switch: &Switch = rqctx.context(); - let tag = path.into_inner().tag; - - debug!(switch.log, "resetting settings tagged with {}", tag); - - arp::reset_ipv4_tag(switch, &tag); - arp::reset_ipv6_tag(switch, &tag); - route::reset_ipv4_tag(switch, &tag).await; - route::reset_ipv6_tag(switch, &tag).await; - switch - .clear_link_addresses(Some(&tag)) - .map(|_| HttpResponseUpdatedNoContent()) - .map_err(|e| e.into()) -} + /** + * 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( + rqctx: RequestContext>, + query: Query>, + ) -> Result>, HttpError> { + let switch: &Switch = rqctx.context(); + let pag_params = query.into_inner(); + let max = rqctx.page_limit(&pag_params)?.get(); + + let last_addr = match &pag_params.page { + WhichPage::First(..) => None, + WhichPage::Next(Ipv4Token { ip }) => Some(*ip), + }; -/** - * 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 { - let switch: &Switch = rqctx.context(); + let entries = nat::get_ipv4_addrs_range( + switch, + last_addr, + usize::try_from(max).expect("invalid usize"), + ); + + Ok(HttpResponseOk(ResultsPage::new( + entries, + &EmptyScanParams {}, + |ip: &Ipv4Addr, _| Ipv4Token { ip: *ip }, + )?)) + } - let mut err = None; + /** + * 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( + rqctx: RequestContext>, + path: Path, + query: Query>, + ) -> Result>, HttpError> { + let switch: &Switch = rqctx.context(); + let params = path.into_inner(); + let pag_params = query.into_inner(); + let max = rqctx.page_limit(&pag_params)?.get(); + + let port = match &pag_params.page { + WhichPage::First(..) => None, + WhichPage::Next(NatToken { port }) => Some(*port), + }; - if let Err(e) = arp::reset_ipv4(switch) { - error!(switch.log, "failed to reset ipv4 arp table: {:?}", e); - err = Some(e); + let entries = nat::get_ipv4_mappings_range( + switch, + params.ipv4, + port, + usize::try_from(max).expect("invalid usize"), + ); + + Ok(HttpResponseOk(ResultsPage::new( + entries, + &EmptyScanParams {}, + |e: &Ipv4Nat, _| NatToken { port: e.low }, + )?)) } - if let Err(e) = arp::reset_ipv6(switch) { - error!(switch.log, "failed to reset ipv6 arp table: {:?}", e); - err = Some(e); + + /** + * 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( + rqctx: RequestContext>, + path: Path, + ) -> Result, HttpError> { + let switch: &Switch = rqctx.context(); + let params = path.into_inner(); + match nat::get_ipv4_mapping(switch, params.ipv4, params.low, params.low) + { + Ok(tgt) => Ok(HttpResponseOk(tgt)), + Err(e) => Err(e.into()), + } } - if let Err(e) = route::reset(switch).await { - error!(switch.log, "failed to reset route data: {:?}", e); - err = Some(e); + + /** + * 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( + rqctx: RequestContext>, + path: Path, + target: TypedBody, + ) -> Result { + let switch: &Switch = rqctx.context(); + let params = path.into_inner(); + match nat::set_ipv4_mapping( + switch, + params.ipv4, + params.low, + params.high, + target.into_inner(), + ) { + Ok(_) => Ok(HttpResponseUpdatedNoContent()), + Err(e) => Err(e.into()), + } } - if let Err(e) = switch.clear_link_state() { - error!(switch.log, "failed to clear all link state: {:?}", e); - err = Some(e); + + /** + * 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( + rqctx: RequestContext>, + path: Path, + ) -> Result { + let switch: &Switch = rqctx.context(); + let params = path.into_inner(); + nat::clear_ipv4_mapping(switch, params.ipv4, params.low, params.low) + .map(|_| HttpResponseDeleted()) + .map_err(HttpError::from) } - if let Err(e) = nat::reset_ipv4(switch) { - error!(switch.log, "failed to reset ipv4 nat table: {:?}", e); - err = Some(e); + + /** + * Clear all IPv4 NAT mappings. + */ + #[endpoint { + method = DELETE, + path = "/nat/ipv4" + }] + pub(super) async fn nat_ipv4_reset( + rqctx: RequestContext>, + ) -> Result { + let switch: &Switch = rqctx.context(); + + match nat::reset_ipv4(switch) { + Ok(_) => Ok(HttpResponseUpdatedNoContent()), + Err(e) => Err(e.into()), + } } - if let Err(e) = nat::reset_ipv6(switch) { - error!(switch.log, "failed to reset ipv6 nat table: {:?}", e); - err = Some(e); + + #[derive(Deserialize, Serialize, JsonSchema)] + struct TagPath { + tag: String, } - if let Err(e) = mcast::reset(switch) { - error!(switch.log, "failed to reset multicast state: {:?}", e); - err = Some(e); + + /** + * 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( + rqctx: RequestContext>, + path: Path, + ) -> Result { + let switch: &Switch = rqctx.context(); + let tag = path.into_inner().tag; + + debug!(switch.log, "resetting settings tagged with {}", tag); + + arp::reset_ipv4_tag(switch, &tag); + arp::reset_ipv6_tag(switch, &tag); + route::reset_ipv4_tag(switch, &tag).await; + route::reset_ipv6_tag(switch, &tag).await; + switch + .clear_link_addresses(Some(&tag)) + .map(|_| HttpResponseUpdatedNoContent()) + .map_err(|e| e.into()) } - match err { - Some(e) => Err(e.into()), - None => Ok(HttpResponseUpdatedNoContent()), + /** + * 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( + rqctx: RequestContext>, + ) -> Result { + let switch: &Switch = rqctx.context(); + + let mut err = None; + + if let Err(e) = arp::reset_ipv4(switch) { + error!(switch.log, "failed to reset ipv4 arp table: {:?}", e); + err = Some(e); + } + if let Err(e) = arp::reset_ipv6(switch) { + error!(switch.log, "failed to reset ipv6 arp table: {:?}", e); + err = Some(e); + } + if let Err(e) = route::reset(switch).await { + error!(switch.log, "failed to reset route data: {:?}", e); + err = Some(e); + } + if let Err(e) = switch.clear_link_state() { + error!(switch.log, "failed to clear all link state: {:?}", e); + err = Some(e); + } + if let Err(e) = nat::reset_ipv4(switch) { + error!(switch.log, "failed to reset ipv4 nat table: {:?}", e); + err = Some(e); + } + if let Err(e) = nat::reset_ipv6(switch) { + error!(switch.log, "failed to reset ipv6 nat table: {:?}", e); + err = Some(e); + } + if let Err(e) = mcast::reset(switch) { + error!(switch.log, "failed to reset multicast state: {:?}", e); + err = Some(e); + } + + match err { + Some(e) => Err(e.into()), + None => Ok(HttpResponseUpdatedNoContent()), + } } -} -/// Get the LinkUp counters for all links. -#[endpoint { - method = GET, - path = "/counters/linkup", -}] -async fn link_up_counters_list( - rqctx: RequestContext>, -) -> Result>, HttpError> { - let switch: &Switch = rqctx.context(); - Ok(HttpResponseOk( - switch.get_linkup_counters_all().into_iter().collect(), - )) -} + /// Get the LinkUp counters for all links. + #[endpoint { + method = GET, + path = "/counters/linkup", + }] + pub(super) async fn link_up_counters_list( + rqctx: RequestContext>, + ) -> Result>, HttpError> { + let switch: &Switch = rqctx.context(); + Ok(HttpResponseOk( + switch.get_linkup_counters_all().into_iter().collect(), + )) + } -/// 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> { - let switch: &Switch = rqctx.context(); - let path = path.into_inner(); - let port_id = path.port_id; - let link_id = path.link_id; - switch - .get_linkup_counters(port_id, link_id) - .map(HttpResponseOk) - .map_err(|e| e.into()) -} + /// 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( + rqctx: RequestContext>, + path: Path, + ) -> Result, HttpError> { + let switch: &Switch = rqctx.context(); + let path = path.into_inner(); + let port_id = path.port_id; + let link_id = path.link_id; + switch + .get_linkup_counters(port_id, link_id) + .map(HttpResponseOk) + .map_err(|e| e.into()) + } -/// 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> { - let switch: &Switch = rqctx.context(); - let path = path.into_inner(); - let port_id = path.port_id; - let link_id = path.link_id; - switch - .get_fsm_counters(port_id, link_id) - .map(HttpResponseOk) - .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( + rqctx: RequestContext>, + path: Path, + ) -> Result, HttpError> { + let switch: &Switch = rqctx.context(); + let path = path.into_inner(); + let port_id = path.port_id; + let link_id = path.link_id; + switch + .get_fsm_counters(port_id, link_id) + .map(HttpResponseOk) + .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, -} + /// 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(), + 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", -}] -async fn build_info( - _rqctx: RequestContext>, -) -> Result, HttpError> { - Ok(HttpResponseOk(BuildInfo::default())) -} + /// Return detailed build information about the `dpd` server itself. + #[endpoint { + method = GET, + path = "/build-info", + }] + pub(super) async fn build_info( + _rqctx: RequestContext>, + ) -> Result, HttpError> { + Ok(HttpResponseOk(BuildInfo::default())) + } -/** - * Return the version of the `dpd` server itself. - */ -#[endpoint { - method = GET, - path = "/dpd-version", -}] -async fn dpd_version( - _rqctx: RequestContext>, -) -> Result, HttpError> { - Ok(HttpResponseOk(crate::version::version())) -} + /** + * Return the version of the `dpd` server itself. + */ + #[endpoint { + method = GET, + path = "/dpd-version", + }] + pub(super) async fn dpd_version( + _rqctx: RequestContext>, + ) -> Result, HttpError> { + Ok(HttpResponseOk(crate::version::version())) + } -/** - * Return the server uptime. - */ -#[endpoint { - method = GET, - path = "/dpd-uptime", -}] -async fn dpd_uptime( - rqctx: RequestContext>, -) -> Result, HttpError> { - let switch: &Switch = rqctx.context(); - - let uptime = chrono::Utc::now().timestamp() - switch.start_time.timestamp(); - Ok(HttpResponseOk(uptime)) -} + /** + * Return the server uptime. + */ + #[endpoint { + method = GET, + path = "/dpd-uptime", + }] + pub(super) async fn dpd_uptime( + rqctx: RequestContext>, + ) -> Result, HttpError> { + let switch: &Switch = rqctx.context(); + + let uptime = + chrono::Utc::now().timestamp() - switch.start_time.timestamp(); + 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, -}] -async fn oximeter_collect_meta_endpoint( - rqctx: RequestContext>, -) -> Result>, HttpError> { - let switch: &Switch = rqctx.context(); - Ok(HttpResponseOk(oxstats::oximeter_meta(switch))) -} + /// 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( + rqctx: RequestContext>, + ) -> 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, -} + /// 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 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, + 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, -} + /// 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, + 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, -} + /// 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, + 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" -}] -async fn port_settings_apply( - rqctx: RequestContext>, - path: Path, - query: Query, - body: TypedBody, -) -> Result, HttpError> { - let switch = rqctx.context(); - let path = path.into_inner(); - let query = query.into_inner(); - let port_id = path.port_id; - let settings = body.into_inner(); - - switch - .apply_port_settings(port_id, settings, query.tag) - .await - .map(HttpResponseOk) - .map_err(HttpError::from) -} + /** + * 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( + rqctx: RequestContext>, + path: Path, + query: Query, + body: TypedBody, + ) -> Result, HttpError> { + let switch = rqctx.context(); + let path = path.into_inner(); + let query = query.into_inner(); + let port_id = path.port_id; + let settings = body.into_inner(); + + switch + .apply_port_settings(port_id, settings, query.tag) + .await + .map(HttpResponseOk) + .map_err(HttpError::from) + } -/** - * 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> { - let switch = rqctx.context(); - let path = path.into_inner(); - let query = query.into_inner(); - let port_id = path.port_id; - - switch - .clear_port_settings(port_id, query.tag) - .await - .map(HttpResponseOk) - .map_err(HttpError::from) -} + /** + * Clear port settings atomically. + */ + #[endpoint { + method = DELETE, + path = "/port/{port_id}/settings" + }] + pub(super) async fn port_settings_clear( + rqctx: RequestContext>, + path: Path, + query: Query, + ) -> Result, HttpError> { + let switch = rqctx.context(); + let path = path.into_inner(); + let query = query.into_inner(); + let port_id = path.port_id; + + switch + .clear_port_settings(port_id, query.tag) + .await + .map(HttpResponseOk) + .map_err(HttpError::from) + } -/** - * 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> { - let switch = rqctx.context(); - let path = path.into_inner(); - let query = query.into_inner(); - let port_id = path.port_id; - - switch - .get_port_settings(port_id, query.tag) - .await - .map(HttpResponseOk) - .map_err(HttpError::from) -} + /** + * Get port settings atomically. + */ + #[endpoint { + method = GET, + path = "/port/{port_id}/settings" + }] + pub(super) async fn port_settings_get( + rqctx: RequestContext>, + path: Path, + query: Query, + ) -> Result, HttpError> { + let switch = rqctx.context(); + let path = path.into_inner(); + let query = query.into_inner(); + let port_id = path.port_id; + + switch + .get_port_settings(port_id, query.tag) + .await + .map(HttpResponseOk) + .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", -}] -async fn switch_identifiers( - rqctx: RequestContext>, -) -> Result, HttpError> { - let switch = &rqctx.context(); - let idents = switch.identifiers.lock().unwrap(); - idents - .clone() - .ok_or(HttpError::from(DpdError::NoSwitchIdentifiers)) - .map(HttpResponseOk) -} + /// 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( + rqctx: RequestContext>, + ) -> Result, HttpError> { + let switch = &rqctx.context(); + let idents = switch.identifiers.lock().unwrap(); + idents + .clone() + .ok_or(HttpError::from(DpdError::NoSwitchIdentifiers)) + .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", -}] -async fn tfport_data( - rqctx: RequestContext>, -) -> Result>, HttpError> { - let switch = &rqctx.context(); - Ok(HttpResponseOk(switch.all_tfport_data())) -} + /// 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( + 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" -}] -async fn ipv4_nat_generation( - rqctx: RequestContext>, -) -> Result, HttpError> { - let switch = rqctx.context(); - - Ok(HttpResponseOk(nat::get_ipv4_nat_generation(switch))) -} + /** + * Get NATv4 generation number + */ + #[endpoint { + method = GET, + path = "/rpw/nat/ipv4/gen" + }] + pub(super) async fn ipv4_nat_generation( + rqctx: RequestContext>, + ) -> Result, HttpError> { + let switch = rqctx.context(); + + Ok(HttpResponseOk(nat::get_ipv4_nat_generation(switch))) + } -/** - * Trigger NATv4 Reconciliation - */ -#[endpoint { - method = POST, - path = "/rpw/nat/ipv4/trigger" -}] -async fn ipv4_nat_trigger_update( - rqctx: RequestContext>, -) -> Result, HttpError> { - let switch = rqctx.context(); - - match switch.workflow_server.trigger(Task::Ipv4Nat) { - Ok(_) => Ok(HttpResponseOk(())), - Err(e) => { - error!(rqctx.log, "unable to trigger rpw"; "error" => ?e); - Err(DpdError::Other("RPW Trigger Failure".to_string()).into()) + /** + * Trigger NATv4 Reconciliation + */ + #[endpoint { + method = POST, + path = "/rpw/nat/ipv4/trigger" + }] + pub(super) async fn ipv4_nat_trigger_update( + rqctx: RequestContext>, + ) -> Result, HttpError> { + let switch = rqctx.context(); + + match switch.workflow_server.trigger(Task::Ipv4Nat) { + Ok(_) => Ok(HttpResponseOk(())), + Err(e) => { + error!(rqctx.log, "unable to trigger rpw"; "error" => ?e); + Err(DpdError::Other("RPW Trigger Failure".to_string()).into()) + } } } -} -/** - * Get the list of P4 tables - */ -#[endpoint { - method = GET, - path = "/table" -}] -async fn table_list( - rqctx: RequestContext>, -) -> Result>, HttpError> { - let switch: &Switch = rqctx.context(); - Ok(HttpResponseOk(crate::table::list(switch))) -} + /** + * Get the list of P4 tables + */ + #[endpoint { + method = GET, + path = "/table" + }] + pub(super) 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, -} + #[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" -}] -async fn table_dump( - rqctx: RequestContext>, - path: Path, -) -> Result, HttpError> { - let switch: &Switch = rqctx.context(); - let table = path.into_inner().table; - crate::table::get_entries(switch, table) - .map(HttpResponseOk) - .map_err(HttpError::from) -} + /** + * 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( + rqctx: RequestContext>, + path: Path, + ) -> Result, HttpError> { + let switch: &Switch = rqctx.context(); + let table = path.into_inner().table; + crate::table::get_entries(switch, table) + .map(HttpResponseOk) + .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" -}] -async fn table_counters( - rqctx: RequestContext>, - query: Query, - path: Path, -) -> Result>, HttpError> { - let switch: &Switch = rqctx.context(); - let force_sync = query.into_inner().force_sync; - let table = path.into_inner().table; - crate::table::get_counters(switch, force_sync, table) - .map(HttpResponseOk) - .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( + rqctx: RequestContext>, + query: Query, + path: Path, + ) -> Result>, HttpError> { + let switch: &Switch = rqctx.context(); + let force_sync = query.into_inner().force_sync; + let table = path.into_inner().table; + crate::table::get_counters(switch, force_sync, table) + .map(HttpResponseOk) + .map_err(HttpError::from) + } -/** - * Get a list of all the available p4-defined counters. - */ -#[endpoint { - method = GET, - path = "/counters/p4", -}] -async fn counter_list( - _rqctx: RequestContext>, -) -> Result>, HttpError> { - match counters::get_counter_names() { - Err(e) => Err(e.into()), - Ok(counters) => Ok(HttpResponseOk(counters)), + /** + * Get a list of all the available p4-defined counters. + */ + #[endpoint { + method = GET, + path = "/counters/p4", + }] + pub(super) async fn counter_list( + _rqctx: RequestContext>, + ) -> Result>, HttpError> { + match counters::get_counter_names() { + Err(e) => Err(e.into()), + Ok(counters) => Ok(HttpResponseOk(counters)), + } } -} -#[derive(Deserialize, Serialize, JsonSchema)] -struct CounterPath { - counter: String, -} + #[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", -}] -async fn counter_reset( - rqctx: RequestContext>, - path: Path, -) -> Result { - let switch: &Switch = rqctx.context(); - let counter = path.into_inner().counter; - - match counters::reset(switch, counter) { - Ok(_) => Ok(HttpResponseUpdatedNoContent()), - Err(e) => Err(e.into()), + /** + * 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( + rqctx: RequestContext>, + path: Path, + ) -> Result { + let switch: &Switch = rqctx.context(); + let counter = path.into_inner().counter; + + match counters::reset(switch, counter) { + Ok(_) => Ok(HttpResponseUpdatedNoContent()), + Err(e) => Err(e.into()), + } } -} -/** - * 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> { - let switch: &Arc = rqctx.context(); - let counter = path.into_inner().counter; - let force_sync = query.into_inner().force_sync; - - counters::get_values(switch, force_sync, counter) - .await - .map(HttpResponseOk) - .map_err(HttpError::from) -} + /** + * 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( + rqctx: RequestContext>, + query: Query, + path: Path, + ) -> Result>, HttpError> { + let switch: &Arc = rqctx.context(); + let counter = path.into_inner().counter; + let force_sync = query.into_inner().force_sync; + + counters::get_values(switch, force_sync, counter) + .await + .map(HttpResponseOk) + .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 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, -} + /// 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", -}] -async fn multicast_group_create_external( - rqctx: RequestContext>, - group: TypedBody, -) -> Result, HttpError> { - let switch: &Switch = rqctx.context(); - let entry = group.into_inner(); - - mcast::add_group_external(switch, entry) - .map(HttpResponseCreated) - .map_err(HttpError::from) -} + /** + * 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> + { + let switch: &Switch = rqctx.context(); + let entry = group.into_inner(); + + mcast::add_group_external(switch, entry) + .map(HttpResponseCreated) + .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", -}] -async fn multicast_group_create( - rqctx: RequestContext>, - group: TypedBody, -) -> Result, HttpError> { - let switch: &Switch = rqctx.context(); - let entry = group.into_inner(); - - mcast::add_group_internal(switch, entry) - .map(HttpResponseCreated) - .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> + { + let switch: &Switch = rqctx.context(); + let entry = group.into_inner(); + + mcast::add_group_internal(switch, entry) + .map(HttpResponseCreated) + .map_err(HttpError::from) + } -/** - * 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 { - let switch: &Switch = rqctx.context(); - let ip = path.into_inner().group_ip; - - mcast::del_group(switch, ip) - .map(|_| HttpResponseDeleted()) - .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( + rqctx: RequestContext>, + path: Path, + ) -> Result { + let switch: &Switch = rqctx.context(); + let ip = path.into_inner().group_ip; + + mcast::del_group(switch, ip) + .map(|_| HttpResponseDeleted()) + .map_err(HttpError::from) + } -/** - * Reset all multicast group configurations. - */ -#[endpoint { - method = DELETE, - path = "/multicast/groups", -}] -async fn multicast_reset( - rqctx: RequestContext>, -) -> Result { - let switch: &Switch = rqctx.context(); - - mcast::reset(switch) - .map(|_| HttpResponseDeleted()) - .map_err(HttpError::from) -} + /** + * Reset all multicast group configurations. + */ + #[endpoint { + method = DELETE, + path = "/multicast/groups", + }] + pub(super) async fn multicast_reset( + rqctx: RequestContext>, + ) -> Result { + let switch: &Switch = rqctx.context(); + + mcast::reset(switch) + .map(|_| HttpResponseDeleted()) + .map_err(HttpError::from) + } -/** - * 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> { - let switch: &Switch = rqctx.context(); - let ip = path.into_inner().group_ip; - - // Get the multicast group - mcast::get_group(switch, ip) - .map(HttpResponseOk) - .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( + rqctx: RequestContext>, + path: Path, + ) -> Result, HttpError> { + let switch: &Switch = rqctx.context(); + let ip = path.into_inner().group_ip; + + // Get the multicast group + mcast::get_group(switch, ip) + .map(HttpResponseOk) + .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}", -}] -async fn multicast_group_update( - rqctx: RequestContext>, - path: Path, - group: TypedBody, -) -> Result, HttpError> { - let switch: &Switch = rqctx.context(); - let ip = path.into_inner().group_ip; - - let ipv6 = match ip { - IpAddr::V6(ipv6) => ipv6, - IpAddr::V4(_) => { - return Err(HttpError::for_bad_request( - None, - "Internal multicast groups must use IPv6 addresses".to_string(), - )); - } - }; + /** + * 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( + rqctx: RequestContext>, + path: Path, + group: TypedBody, + ) -> Result, HttpError> { + let switch: &Switch = rqctx.context(); + let ip = path.into_inner().group_ip; + + let ipv6 = match ip { + IpAddr::V6(ipv6) => ipv6, + IpAddr::V4(_) => { + return Err(HttpError::for_bad_request( + None, + "Internal multicast groups must use IPv6 addresses" + .to_string(), + )); + } + }; - mcast::modify_group_internal(switch, ipv6, group.into_inner()) - .map(HttpResponseOk) - .map_err(HttpError::from) -} + mcast::modify_group_internal(switch, ipv6, group.into_inner()) + .map(HttpResponseOk) + .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}", -}] -async fn multicast_group_update_external( - rqctx: RequestContext>, - path: Path, - group: TypedBody, -) -> Result, HttpError> { - let switch: &Switch = rqctx.context(); - let entry = group.into_inner(); - let ip = path.into_inner().group_ip; - - mcast::modify_group_external(switch, ip, entry) - .map(HttpResponseCreated) - .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( + rqctx: RequestContext>, + path: Path, + group: TypedBody, + ) -> Result, HttpError> + { + let switch: &Switch = rqctx.context(); + let entry = group.into_inner(); + let ip = path.into_inner().group_ip; + + mcast::modify_group_external(switch, ip, entry) + .map(HttpResponseCreated) + .map_err(HttpError::from) + } -/** - * List all multicast groups. - */ -#[endpoint { - method = GET, - path = "/multicast/groups", -}] -async fn multicast_groups_list( - rqctx: RequestContext>, - query_params: Query< - PaginationParams, - >, -) -> Result>, HttpError> -{ - let switch: &Switch = rqctx.context(); - - // If a group ID is provided, get the group by ID - - // If no group ID is provided, paginate through the groups - let pag_params = query_params.into_inner(); - let Ok(limit) = usize::try_from(rqctx.page_limit(&pag_params)?.get()) - else { - return Err(DpdError::Invalid("Invalid page limit".to_string()).into()); - }; + /** + * List all multicast groups. + */ + #[endpoint { + method = GET, + path = "/multicast/groups", + }] + pub(super) async fn multicast_groups_list( + rqctx: RequestContext>, + query_params: Query< + PaginationParams, + >, + ) -> Result< + HttpResponseOk>, + HttpError, + > { + let switch: &Switch = rqctx.context(); + + // If a group ID is provided, get the group by ID + + // If no group ID is provided, paginate through the groups + let pag_params = query_params.into_inner(); + let Ok(limit) = usize::try_from(rqctx.page_limit(&pag_params)?.get()) + else { + return Err( + DpdError::Invalid("Invalid page limit".to_string()).into() + ); + }; - let last_addr = match &pag_params.page { - WhichPage::First(..) => None, - WhichPage::Next(MulticastGroupIpParam { group_ip }) => Some(*group_ip), - }; + let last_addr = match &pag_params.page { + WhichPage::First(..) => None, + WhichPage::Next(MulticastGroupIpParam { group_ip }) => { + Some(*group_ip) + } + }; - let entries = mcast::get_range(switch, last_addr, limit, None); + let entries = mcast::get_range(switch, last_addr, limit, None); - Ok(HttpResponseOk(ResultsPage::new( - entries, - &EmptyScanParams {}, - |e: &mcast::MulticastGroupResponse, _| MulticastGroupIpParam { - group_ip: e.ip(), - }, - )?)) -} + Ok(HttpResponseOk(ResultsPage::new( + entries, + &EmptyScanParams {}, + |e: &mcast::MulticastGroupResponse, _| MulticastGroupIpParam { + group_ip: e.ip(), + }, + )?)) + } -/** - * 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>, HttpError> -{ - let switch: &Switch = rqctx.context(); - let tag = path.into_inner().tag; - - let pag_params = query_params.into_inner(); - let Ok(limit) = usize::try_from(rqctx.page_limit(&pag_params)?.get()) - else { - return Err(DpdError::Invalid("Invalid page limit".to_string()).into()); - }; + /** + * List all multicast groups with a given tag. + */ + #[endpoint { + method = GET, + path = "/multicast/tags/{tag}", + }] + pub(super) async fn multicast_groups_list_by_tag( + rqctx: RequestContext>, + path: Path, + query_params: Query< + PaginationParams, + >, + ) -> Result< + HttpResponseOk>, + HttpError, + > { + let switch: &Switch = rqctx.context(); + let tag = path.into_inner().tag; + + let pag_params = query_params.into_inner(); + let Ok(limit) = usize::try_from(rqctx.page_limit(&pag_params)?.get()) + else { + return Err( + DpdError::Invalid("Invalid page limit".to_string()).into() + ); + }; - let last_addr = match &pag_params.page { - WhichPage::First(..) => None, - WhichPage::Next(MulticastGroupIpParam { group_ip }) => Some(*group_ip), - }; + let last_addr = match &pag_params.page { + WhichPage::First(..) => None, + WhichPage::Next(MulticastGroupIpParam { group_ip }) => { + Some(*group_ip) + } + }; - let entries = mcast::get_range(switch, last_addr, limit, Some(&tag)); - Ok(HttpResponseOk(ResultsPage::new( - entries, - &EmptyScanParams {}, - |e: &mcast::MulticastGroupResponse, _| MulticastGroupIpParam { - group_ip: e.ip(), - }, - )?)) -} + let entries = mcast::get_range(switch, last_addr, limit, Some(&tag)); + Ok(HttpResponseOk(ResultsPage::new( + entries, + &EmptyScanParams {}, + |e: &mcast::MulticastGroupResponse, _| MulticastGroupIpParam { + group_ip: e.ip(), + }, + )?)) + } -/** - * 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 { - let switch: &Switch = rqctx.context(); - let tag = path.into_inner().tag; - - mcast::reset_tag(switch, &tag) - .map(|_| HttpResponseDeleted()) - .map_err(HttpError::from) -} + /** + * 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( + rqctx: RequestContext>, + path: Path, + ) -> Result { + let switch: &Switch = rqctx.context(); + let tag = path.into_inner().tag; + + mcast::reset_tag(switch, &tag) + .map(|_| HttpResponseDeleted()) + .map_err(HttpError::from) + } -/** - * Delete all multicast groups (and associated routes) without a tag. - */ -#[endpoint { - method = DELETE, - path = "/multicast/untagged", -}] -async fn multicast_reset_untagged( - rqctx: RequestContext>, -) -> Result { - let switch: &Switch = rqctx.context(); - - mcast::reset_untagged(switch) - .map(|_| HttpResponseDeleted()) - .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( + rqctx: RequestContext>, + ) -> Result { + let switch: &Switch = rqctx.context(); + + mcast::reset_untagged(switch) + .map(|_| HttpResponseDeleted()) + .map_err(HttpError::from) + } } +pub use imp::*; + pub fn http_api() -> dropshot::ApiDescription> { let mut api = dropshot::ApiDescription::new(); api.register(build_info).unwrap(); From 78b496cfd66f6ac038bcdbc6624fa96cbf6d0a58 Mon Sep 17 00:00:00 2001 From: Rain Date: Sat, 30 Aug 2025 04:41:09 +0000 Subject: [PATCH 2/3] fix up tofino_asic compile issues Created using spr 1.3.6-beta.1 --- dpd/src/macaddrs.rs | 2 +- dpd/src/switch_port.rs | 1 - dpd/src/tofino_api_server.rs | 4 ++-- dpd/src/transceivers/mod.rs | 18 ++++++++++-------- dpd/src/transceivers/tofino_impl.rs | 10 +++++----- 5 files changed, 18 insertions(+), 17 deletions(-) diff --git a/dpd/src/macaddrs.rs b/dpd/src/macaddrs.rs index f0fc225..7074faf 100644 --- a/dpd/src/macaddrs.rs +++ b/dpd/src/macaddrs.rs @@ -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; } } diff --git a/dpd/src/switch_port.rs b/dpd/src/switch_port.rs index f7b7a7a..a18f298 100644 --- a/dpd/src/switch_port.rs +++ b/dpd/src/switch_port.rs @@ -8,7 +8,6 @@ use anyhow::Context; use dpd_types::port_map::BackplaneLink; -#[cfg(not(feature = "tofino_asic"))] use dpd_types::switch_port::Led; use dpd_types::switch_port::LedPolicy; use dpd_types::switch_port::ManagementMode; 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 7421fd3..31fd849 100644 --- a/dpd/src/transceivers/mod.rs +++ b/dpd/src/transceivers/mod.rs @@ -36,6 +36,8 @@ pub fn qsfp_xcvr_mpn( #[cfg(feature = "tofino_asic")] { + use dpd_types::transceivers::Transceiver; + match &qsfp.transceiver { Some(Transceiver::Supported(xcvr_info)) => { if let Some(vendor_info) = &xcvr_info.vendor_info { @@ -169,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 @@ -213,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] @@ -234,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, @@ -247,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 312ef17..0d27868 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; From 56968863d7fae76a0ae18959d350fd1bd0fdc9e9 Mon Sep 17 00:00:00 2001 From: Rain Date: Sat, 30 Aug 2025 04:58:06 +0000 Subject: [PATCH 3/3] more fixes Created using spr 1.3.6-beta.1 --- dpd/src/main.rs | 2 +- dpd/src/softnpu_api_server.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dpd/src/main.rs b/dpd/src/main.rs index cb77776..30b0192 100644 --- a/dpd/src/main.rs +++ b/dpd/src/main.rs @@ -757,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)) 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 {