diff --git a/Cargo.lock b/Cargo.lock index c9f6ff10..dbf43b56 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -630,6 +630,16 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" +[[package]] +name = "colored" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbf2150cce219b664a8a70df7a1f933836724b503f8a413af9365b4dcc4d90b8" +dependencies = [ + "lazy_static", + "windows-sys 0.48.0", +] + [[package]] name = "compact_str" version = "0.7.1" @@ -1596,6 +1606,15 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" +[[package]] +name = "ipnetwork" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf466541e9d546596ee94f9f69590f89473455f88372423e0008fc1a7daf100e" +dependencies = [ + "serde", +] + [[package]] name = "is-docker" version = "0.2.0" @@ -2124,6 +2143,7 @@ dependencies = [ "chrono", "clap", "clap_complete", + "colored", "crossterm", "dialoguer", "dirs", @@ -2132,12 +2152,14 @@ dependencies = [ "expectorate", "futures", "httpmock", + "humantime", "indicatif", "log", "oauth2", "open", "oxide", "oxide-httpmock", + "oxnet", "predicates", "pretty_assertions", "rand", @@ -2168,6 +2190,17 @@ dependencies = [ "uuid", ] +[[package]] +name = "oxnet" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/oxnet#f37a7aaf5ca96d87af5f8194c19b20e276b5d5e1" +dependencies = [ + "ipnetwork", + "schemars", + "serde", + "serde_json", +] + [[package]] name = "parking" version = "2.2.0" diff --git a/Cargo.toml b/Cargo.toml index 573b78ce..5895d386 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ built = { version = "0.7.3", features = ["git2"] } chrono = { version = "0.4.38", features = ["serde"] } clap = { version = "4.5.7", features = ["derive", "string", "env"] } clap_complete = "4.5.7" +colored = "2.1.0" crossterm = { version = "0.27.0", features = [ "event-stream" ] } dialoguer = "0.10.4" dirs = "4.0.0" @@ -25,6 +26,7 @@ env_logger = "0.10.2" expectorate = { version = "1.1.0", features = ["predicates"] } futures = "0.3.30" httpmock = "0.6.8" +humantime = "2" indicatif = "0.17" log = "0.4.22" newline-converter = "0.3.0" @@ -32,6 +34,7 @@ oauth2 = "4.4.2" open = "4.2.0" oxide = { path = "sdk", version = "0.5.0" } oxide-httpmock = { path = "sdk-httpmock", version = "0.5.0" } +oxnet = { git = "https://github.com/oxidecomputer/oxnet" } predicates = "3.1.0" pretty_assertions = "1.4.0" progenitor = { git = "https://github.com/oxidecomputer/progenitor" } diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 0f7fb1b7..491b5eba 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -24,16 +24,19 @@ base64 = { workspace = true } chrono = { workspace = true } clap = { workspace = true } clap_complete = { workspace = true } +colored = { workspace = true } crossterm = { workspace = true } dialoguer = { workspace = true } dirs = { workspace = true } env_logger = { workspace = true } futures = { workspace = true } +humantime = { workspace = true } indicatif = { workspace = true } log = { workspace = true } oauth2 = { workspace = true } open = { workspace = true } oxide = { workspace = true } +oxnet = { workspace = true } predicates = { workspace = true } ratatui = { workspace = true } reqwest = { workspace = true, features = ["native-tls-vendored"] } diff --git a/cli/docs/cli.json b/cli/docs/cli.json index 5dd7fab7..8a94e354 100644 --- a/cli/docs/cli.json +++ b/cli/docs/cli.json @@ -2979,6 +2979,10 @@ } ] }, + { + "name": "show-status", + "about": "Get the status of switch ports." + }, { "name": "status", "about": "Get switch port status", @@ -3004,6 +3008,140 @@ { "name": "networking", "subcommands": [ + { + "name": "addr", + "about": "Manage switch port addresses.", + "subcommands": [ + { + "name": "add", + "about": "Add an address to a port configuration", + "args": [ + { + "long": "addr", + "help": "Address to add" + }, + { + "long": "lot", + "help": "Address lot to allocate from" + }, + { + "long": "port", + "values": [ + "qsfp0", + "qsfp1", + "qsfp2", + "qsfp3", + "qsfp4", + "qsfp5", + "qsfp6", + "qsfp7", + "qsfp8", + "qsfp9", + "qsfp10", + "qsfp11", + "qsfp12", + "qsfp13", + "qsfp14", + "qsfp15", + "qsfp16", + "qsfp17", + "qsfp18", + "qsfp19", + "qsfp20", + "qsfp21", + "qsfp22", + "qsfp23", + "qsfp24", + "qsfp25", + "qsfp26", + "qsfp27", + "qsfp28", + "qsfp29", + "qsfp30", + "qsfp31" + ], + "help": "Port to add the port to" + }, + { + "long": "rack", + "help": "Id of the rack to add the address to" + }, + { + "long": "switch", + "values": [ + "switch0", + "switch1" + ], + "help": "Switch to add the address to" + }, + { + "long": "vlan", + "help": "Optional VLAN to assign to the address" + } + ] + }, + { + "name": "del", + "about": "Remove an address from a port configuration", + "args": [ + { + "long": "addr", + "help": "Address to remove" + }, + { + "long": "port", + "values": [ + "qsfp0", + "qsfp1", + "qsfp2", + "qsfp3", + "qsfp4", + "qsfp5", + "qsfp6", + "qsfp7", + "qsfp8", + "qsfp9", + "qsfp10", + "qsfp11", + "qsfp12", + "qsfp13", + "qsfp14", + "qsfp15", + "qsfp16", + "qsfp17", + "qsfp18", + "qsfp19", + "qsfp20", + "qsfp21", + "qsfp22", + "qsfp23", + "qsfp24", + "qsfp25", + "qsfp26", + "qsfp27", + "qsfp28", + "qsfp29", + "qsfp30", + "qsfp31" + ], + "help": "Port to remove the address from" + }, + { + "long": "rack", + "help": "Id of the rack to remove the address from" + }, + { + "long": "switch", + "values": [ + "switch0", + "switch1" + ], + "help": "Switch to remove the address from" + } + ] + } + ] + }, { "name": "address-lot", "subcommands": [ @@ -3196,28 +3334,30 @@ "name": "bgp", "subcommands": [ { - "name": "announce-set", - "subcommands": [ + "name": "announce", + "about": "Announce a prefix over BGP.", + "long_about": "Announce a prefix over BGP.\n\nThis command adds the provided prefix to the specified announce set. It is\nrequired that the prefix be available in the given address lot. The add is\nperformed as a read-modify-write on the specified address lot.", + "args": [ { - "name": "create", - "about": "Create new BGP announce set", - "args": [ - { - "long": "description" - }, - { - "long": "json-body", - "help": "Path to a file that contains the full json body." - }, - { - "long": "json-body-template", - "help": "XXX" - }, - { - "long": "name" - } - ] + "long": "address-lot", + "help": "The address lot to draw from" + }, + { + "long": "announce-set", + "help": "The announce set to announce from" }, + { + "long": "description" + }, + { + "long": "prefix", + "help": "The prefix to announce" + } + ] + }, + { + "name": "announce-set", + "subcommands": [ { "name": "delete", "about": "Delete BGP announce set", @@ -3237,6 +3377,27 @@ "help": "A name or id to use when selecting BGP port settings" } ] + }, + { + "name": "update", + "about": "Update BGP announce set", + "long_about": "If the announce set exists, this endpoint replaces the existing announce set with the one specified.", + "args": [ + { + "long": "description" + }, + { + "long": "json-body", + "help": "Path to a file that contains the full json body." + }, + { + "long": "json-body-template", + "help": "XXX" + }, + { + "long": "name" + } + ] } ] }, @@ -3308,6 +3469,83 @@ } ] }, + { + "name": "filter", + "about": "Add a filtering requirement to a BGP session.", + "long_about": "Add a filtering requirement to a BGP session.\n\nThe Oxide BGP implementation can filter prefixes received from peers\non import and filter prefixes sent to peers on export. This command\nprovides a way to specify import/export filtering. Filtering is a\nproperty of the BGP peering settings found in port settings configuration.\nThis command works by performing a read-modify-write on the port settings\nconfiguration identified by the specified rack/switch/port.", + "args": [ + { + "long": "allowed", + "help": "Prefixes to allow for the peer" + }, + { + "long": "direction", + "values": [ + "import", + "export" + ], + "help": "Whether to apply the filter to imported or exported prefixes" + }, + { + "long": "no-filtering", + "help": "Do not filter" + }, + { + "long": "peer", + "help": "Peer to apply allow list to" + }, + { + "long": "port", + "values": [ + "qsfp0", + "qsfp1", + "qsfp2", + "qsfp3", + "qsfp4", + "qsfp5", + "qsfp6", + "qsfp7", + "qsfp8", + "qsfp9", + "qsfp10", + "qsfp11", + "qsfp12", + "qsfp13", + "qsfp14", + "qsfp15", + "qsfp16", + "qsfp17", + "qsfp18", + "qsfp19", + "qsfp20", + "qsfp21", + "qsfp22", + "qsfp23", + "qsfp24", + "qsfp25", + "qsfp26", + "qsfp27", + "qsfp28", + "qsfp29", + "qsfp30", + "qsfp31" + ], + "help": "Port to add the port to" + }, + { + "long": "rack", + "help": "Id of the rack to add the address to" + }, + { + "long": "switch", + "values": [ + "switch0", + "switch1" + ], + "help": "Switch to add the address to" + } + ] + }, { "name": "history", "about": "Get BGP router message history", @@ -3333,9 +3571,355 @@ } ] }, + { + "name": "peer", + "about": "Manage BGP peers.", + "long_about": "Manage BGP peers.\n\nThis command provides add and delete subcommands for managing BGP peers.\nBGP peer configuration is a part of a switch port settings configuration.\nThe peer add and remove subcommands perform read-modify-write operations\non switch port settings objects to manage BGP peer configurations.", + "subcommands": [ + { + "name": "add", + "about": "Add a BGP peer to a port configuration", + "args": [ + { + "long": "addr", + "help": "Address of the peer to add" + }, + { + "long": "allowed-exports", + "help": "Prefixes that may be exported to the peer. Empty list means all prefixes allowed" + }, + { + "long": "allowed-imports", + "help": "Prefixes that may be imported form the peer. Empty list means all prefixes allowed" + }, + { + "long": "bgp-config", + "help": "BGP configuration this peer is associated with" + }, + { + "long": "communities", + "help": "Include the provided communities in updates sent to the peer" + }, + { + "long": "connect-retry", + "help": "How long to to wait between TCP connection retries (seconds)" + }, + { + "long": "delay-open", + "help": "How long to delay sending an open request after establishing a TCP session (seconds)" + }, + { + "long": "enforce-first-as", + "help": "Enforce that the first AS in paths received from this peer is the peer's AS" + }, + { + "long": "hold-time", + "help": "How long to hold peer connections between keepalives (seconds)" + }, + { + "long": "idle-hold-time", + "help": "How long to hold a peer in idle before attempting a new session (seconds)" + }, + { + "long": "keepalive", + "help": "How often to send keepalive requests (seconds)" + }, + { + "long": "local-pref", + "help": "Apply a local preference to routes received from this peer" + }, + { + "long": "md5-auth-key", + "help": "Use the given key for TCP-MD5 authentication with the peer" + }, + { + "long": "min-ttl", + "help": "Require messages from a peer have a minimum IP time to live field" + }, + { + "long": "multi-exit-discriminator", + "help": "Apply the provided multi-exit discriminator (MED) updates sent to the peer" + }, + { + "long": "port", + "values": [ + "qsfp0", + "qsfp1", + "qsfp2", + "qsfp3", + "qsfp4", + "qsfp5", + "qsfp6", + "qsfp7", + "qsfp8", + "qsfp9", + "qsfp10", + "qsfp11", + "qsfp12", + "qsfp13", + "qsfp14", + "qsfp15", + "qsfp16", + "qsfp17", + "qsfp18", + "qsfp19", + "qsfp20", + "qsfp21", + "qsfp22", + "qsfp23", + "qsfp24", + "qsfp25", + "qsfp26", + "qsfp27", + "qsfp28", + "qsfp29", + "qsfp30", + "qsfp31" + ], + "help": "Port to add the peer to" + }, + { + "long": "rack", + "help": "Id of the rack to add the peer to" + }, + { + "long": "remote-asn", + "help": "Require that a peer has a specified ASN" + }, + { + "long": "switch", + "values": [ + "switch0", + "switch1" + ], + "help": "Switch to add the peer to" + }, + { + "long": "vlan-id", + "help": "Associate a VLAN ID with a peer" + } + ] + }, + { + "name": "del", + "about": "Remove a BGP from a port configuration", + "args": [ + { + "long": "addr", + "help": "Address of the peer to remove" + }, + { + "long": "port", + "values": [ + "qsfp0", + "qsfp1", + "qsfp2", + "qsfp3", + "qsfp4", + "qsfp5", + "qsfp6", + "qsfp7", + "qsfp8", + "qsfp9", + "qsfp10", + "qsfp11", + "qsfp12", + "qsfp13", + "qsfp14", + "qsfp15", + "qsfp16", + "qsfp17", + "qsfp18", + "qsfp19", + "qsfp20", + "qsfp21", + "qsfp22", + "qsfp23", + "qsfp24", + "qsfp25", + "qsfp26", + "qsfp27", + "qsfp28", + "qsfp29", + "qsfp30", + "qsfp31" + ], + "help": "Port to remove the peer from" + }, + { + "long": "rack", + "help": "Id of the rack to remove the peer from" + }, + { + "long": "switch", + "values": [ + "switch0", + "switch1" + ], + "help": "Switch to remove the peer from" + } + ] + } + ] + }, + { + "name": "show-status", + "about": "Get the status of BGP on the rack.", + "long_about": "Get the status of BGP on the rack.\n\nThis will show the peering status for all peers on all switches." + }, { "name": "status", "about": "Get BGP peer status" + }, + { + "name": "withdraw", + "about": "Withdraw a prefix over BGP.", + "long_about": "Withdraw a prefix over BGP.\n\nThis command removes the provided prefix to the specified announce set.\nThe remove is performed as a read-modify-write on the specified address lot.", + "args": [ + { + "long": "announce-set", + "help": "The announce set to withdraw from" + }, + { + "long": "prefix", + "help": "The prefix to withdraw" + } + ] + } + ] + }, + { + "name": "link", + "about": "Manage switch port links.", + "long_about": "Manage switch port links.\n\nLinks carry layer-2 Ethernet properties for a lane or set of lanes on a\nswitch port. Lane geometry is defined in physical port settings. At the\npresent time only single lane configurations are supported, and thus only\na single link per physical port is supported.", + "subcommands": [ + { + "name": "add", + "about": "Add a link to a port", + "args": [ + { + "long": "autoneg", + "help": "Whether or not to set auto-negotiation" + }, + { + "long": "fec", + "help": "The forward error correction mode of the link" + }, + { + "long": "mtu", + "help": "Maximum transmission unit for the link" + }, + { + "long": "port", + "values": [ + "qsfp0", + "qsfp1", + "qsfp2", + "qsfp3", + "qsfp4", + "qsfp5", + "qsfp6", + "qsfp7", + "qsfp8", + "qsfp9", + "qsfp10", + "qsfp11", + "qsfp12", + "qsfp13", + "qsfp14", + "qsfp15", + "qsfp16", + "qsfp17", + "qsfp18", + "qsfp19", + "qsfp20", + "qsfp21", + "qsfp22", + "qsfp23", + "qsfp24", + "qsfp25", + "qsfp26", + "qsfp27", + "qsfp28", + "qsfp29", + "qsfp30", + "qsfp31" + ], + "help": "Port to add the link to" + }, + { + "long": "rack", + "help": "Id of the rack to add the link to" + }, + { + "long": "speed", + "help": "The speed of the link" + }, + { + "long": "switch", + "values": [ + "switch0", + "switch1" + ], + "help": "Switch to add the link to" + } + ] + }, + { + "name": "del", + "about": "Remove a link from a port", + "args": [ + { + "long": "port", + "values": [ + "qsfp0", + "qsfp1", + "qsfp2", + "qsfp3", + "qsfp4", + "qsfp5", + "qsfp6", + "qsfp7", + "qsfp8", + "qsfp9", + "qsfp10", + "qsfp11", + "qsfp12", + "qsfp13", + "qsfp14", + "qsfp15", + "qsfp16", + "qsfp17", + "qsfp18", + "qsfp19", + "qsfp20", + "qsfp21", + "qsfp22", + "qsfp23", + "qsfp24", + "qsfp25", + "qsfp26", + "qsfp27", + "qsfp28", + "qsfp29", + "qsfp30", + "qsfp31" + ], + "help": "Port to remove the link from" + }, + { + "long": "rack", + "help": "Id of the rack to remove the link from" + }, + { + "long": "switch", + "values": [ + "switch0", + "switch1" + ], + "help": "Switch to remove the link from" + } + ] } ] }, @@ -3479,6 +4063,10 @@ } ] }, + { + "name": "show", + "about": "Get the configuration of switch ports." + }, { "name": "view", "about": "Get information about switch port", diff --git a/cli/src/cli_builder.rs b/cli/src/cli_builder.rs index 9935b4a3..11bdae72 100644 --- a/cli/src/cli_builder.rs +++ b/cli/src/cli_builder.rs @@ -439,8 +439,8 @@ fn xxx<'a>(command: CliCommand) -> Option<&'a str> { CliCommand::NetworkingBgpConfigCreate => Some("system networking bgp config create"), CliCommand::NetworkingBgpConfigDelete => Some("system networking bgp config delete"), CliCommand::NetworkingBgpConfigList => Some("system networking bgp config list"), - CliCommand::NetworkingBgpAnnounceSetCreate => { - Some("system networking bgp announce-set create") + CliCommand::NetworkingBgpAnnounceSetUpdate => { + Some("system networking bgp announce-set update") } CliCommand::NetworkingBgpAnnounceSetDelete => { Some("system networking bgp announce-set delete") diff --git a/cli/src/cmd_disk.rs b/cli/src/cmd_disk.rs index 92b643b5..f8fd801c 100644 --- a/cli/src/cmd_disk.rs +++ b/cli/src/cmd_disk.rs @@ -323,7 +323,7 @@ impl RunnableCmd for CmdDiskImport { disk_source: DiskSource::ImportingBlocks { block_size: disk_block_size.clone(), }, - size: disk_size.try_into()?, + size: disk_size.into(), }) .send() .await?; diff --git a/cli/src/cmd_net.rs b/cli/src/cmd_net.rs new file mode 100644 index 00000000..7fff375b --- /dev/null +++ b/cli/src/cmd_net.rs @@ -0,0 +1,1514 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +// Copyright 2024 Oxide Computer Company + +use std::{collections::HashMap, net::IpAddr}; + +use crate::RunnableCmd; +use anyhow::Result; +use async_trait::async_trait; +use clap::Parser; +use colored::*; +use futures::TryStreamExt; +use oxide::{ + context::Context, + types::{ + Address, AddressConfig, BgpAnnounceSetCreate, BgpAnnouncementCreate, BgpPeer, + BgpPeerConfig, BgpPeerStatus, ImportExportPolicy, IpNet, LinkConfigCreate, LinkFec, + LinkSpeed, LldpServiceConfigCreate, Name, NameOrId, Route, RouteConfig, + SwitchInterfaceConfigCreate, SwitchInterfaceKind, SwitchInterfaceKind2, SwitchLocation, + SwitchPort, SwitchPortConfigCreate, SwitchPortGeometry, SwitchPortGeometry2, + SwitchPortSettingsCreate, + }, + Client, ClientSystemHardwareExt, ClientSystemNetworkingExt, +}; +use serde::{Deserialize, Serialize}; +use std::io::Write; +use tabwriter::TabWriter; +use uuid::Uuid; + +// We do not yet support port breakouts, but the API is phrased in terms of +// ports that can be broken out. The constant phy0 represents the first port +// in a breakout. +const PHY0: &str = "phy0"; + +/// Manage switch port links. +/// +/// Links carry layer-2 Ethernet properties for a lane or set of lanes on a +/// switch port. Lane geometry is defined in physical port settings. At the +/// present time only single lane configurations are supported, and thus only +/// a single link per physical port is supported. +#[derive(Parser, Debug, Clone)] +#[command(verbatim_doc_comment)] +#[command(name = "link")] +pub struct CmdLink { + #[clap(subcommand)] + subcmd: LinkSubCommand, +} + +#[async_trait] +impl RunnableCmd for CmdLink { + async fn run(&self, ctx: &Context) -> Result<()> { + match &self.subcmd { + LinkSubCommand::Add(cmd) => cmd.run(ctx).await, + LinkSubCommand::Del(cmd) => cmd.run(ctx).await, + } + } +} + +#[derive(Parser, Debug, Clone)] +enum LinkSubCommand { + /// Add a link to a port. + Add(CmdLinkAdd), + + /// Remove a link from a port. + Del(CmdLinkDel), +} + +/// Add a link to a switch port. +/// +/// This operation performs a read-modify write on the port settings object +/// identified by the rack/switch/port parameters, adding a link to the +/// corresponding port settings. +#[derive(Parser, Debug, Clone)] +#[command(verbatim_doc_comment)] +#[command(name = "add")] +pub struct CmdLinkAdd { + /// Id of the rack to add the link to. + #[arg(long)] + rack: Uuid, + + /// Switch to add the link to. + #[arg(long, value_enum)] + switch: Switch, + + /// Port to add the link to. + #[arg(long, value_enum)] + port: Port, + + /// Whether or not to set auto-negotiation + #[arg(long)] + pub autoneg: bool, + + /// The forward error correction mode of the link. + #[arg(long, value_enum)] + pub fec: LinkFec, + + /// Maximum transmission unit for the link. + #[arg(long, default_value_t = 1500u16)] + pub mtu: u16, + + /// The speed of the link. + #[arg(long, value_enum)] + pub speed: LinkSpeed, +} + +#[async_trait] +impl RunnableCmd for CmdLinkAdd { + async fn run(&self, ctx: &Context) -> Result<()> { + let mut settings = current_port_settings(ctx, &self.rack, &self.switch, &self.port).await?; + let link = LinkConfigCreate { + autoneg: self.autoneg, + fec: self.fec, + mtu: self.mtu, + speed: self.speed, + //TODO not fully plumbed on the back end yet. + lldp: LldpServiceConfigCreate { + enabled: false, + lldp_config: None, + }, + }; + match settings.links.get(PHY0) { + Some(_) => { + return Err(anyhow::anyhow!("only one link per port supported")); + } + None => { + settings.links.insert(String::from(PHY0), link); + } + } + ctx.client()? + .networking_switch_port_settings_create() + .body(settings) + .send() + .await?; + Ok(()) + } +} + +/// Remove a link from a switch port. +/// +/// This operation performs a read-modify write on the port settings object +/// identified by the rack/switch/port parameters, removing a link from the +/// corresponding port settings. +#[derive(Parser, Debug, Clone)] +#[command(verbatim_doc_comment)] +#[command(name = "del")] +pub struct CmdLinkDel { + /// Id of the rack to remove the link from. + #[arg(long)] + rack: Uuid, + + /// Switch to remove the link from. + #[arg(long, value_enum)] + switch: Switch, + + /// Port to remove the link from. + #[arg(long, value_enum)] + port: Port, +} + +#[async_trait] +impl RunnableCmd for CmdLinkDel { + async fn run(&self, ctx: &Context) -> Result<()> { + let mut settings = current_port_settings(ctx, &self.rack, &self.switch, &self.port).await?; + settings.links.clear(); + ctx.client()? + .networking_switch_port_settings_create() + .body(settings) + .send() + .await?; + Ok(()) + } +} + +/// Manage and query rack Border Gateway Protocol (BGP) configuration and +/// status. +#[derive(Parser, Debug, Clone)] +#[command(verbatim_doc_comment)] +#[command(name = "bgp")] +pub struct CmdBgp { + #[clap(subcommand)] + subcmd: BgpSubCommand, +} + +#[async_trait] +impl RunnableCmd for CmdBgp { + async fn run(&self, ctx: &Context) -> Result<()> { + match &self.subcmd { + BgpSubCommand::Status(cmd) => cmd.run(ctx).await, + BgpSubCommand::Config(cmd) => cmd.run(ctx).await, + BgpSubCommand::Announce(cmd) => cmd.run(ctx).await, + BgpSubCommand::Withdraw(cmd) => cmd.run(ctx).await, + BgpSubCommand::Filter(cmd) => cmd.run(ctx).await, + } + } +} + +#[allow(clippy::large_enum_variant)] +#[derive(Parser, Debug, Clone)] +enum BgpSubCommand { + /// Observe BGP status. + Status(CmdBgpStatus), + + /// Manage BGP configuration. + Config(CmdBgpConfig), + + /// Make a BGP announcement. + Announce(CmdBgpAnnounce), + + /// Make a BGP announcement. + Withdraw(CmdBgpWithdraw), + + /// Set a filtering specification for a peer. + Filter(CmdBgpFilter), +} + +/// Announce a prefix over BGP. +/// +/// This command adds the provided prefix to the specified announce set. It is +/// required that the prefix be available in the given address lot. The add is +/// performed as a read-modify-write on the specified address lot. +#[derive(Parser, Debug, Clone)] +#[command(verbatim_doc_comment)] +#[command(name = "announce")] +pub struct CmdBgpAnnounce { + /// The announce set to announce from. + #[arg(long)] + announce_set: Name, + + /// The address lot to draw from. + #[arg(long)] + address_lot: Name, + + /// The prefix to announce. + #[arg(long)] + prefix: oxnet::IpNet, + + #[arg(long, default_value = "")] + description: String, +} + +#[async_trait] +impl RunnableCmd for CmdBgpAnnounce { + async fn run(&self, ctx: &Context) -> Result<()> { + let mut current: Vec = ctx + .client()? + .networking_bgp_announce_set_list() + .name_or_id(NameOrId::Name(self.announce_set.clone())) + .send() + .await? + .into_inner() + .into_iter() + .map(|x| BgpAnnouncementCreate { + address_lot_block: NameOrId::Id(x.address_lot_block_id), + network: x.network, + }) + .collect(); + + current.push(BgpAnnouncementCreate { + address_lot_block: NameOrId::Name(self.address_lot.clone()), + network: self.prefix.to_string().parse().unwrap(), + }); + + ctx.client()? + .networking_bgp_announce_set_update() + .body(BgpAnnounceSetCreate { + announcement: current, + name: self.announce_set.clone(), + description: self.description.clone(), + }) + .send() + .await?; + + Ok(()) + } +} + +/// Withdraw a prefix over BGP. +/// +/// This command removes the provided prefix to the specified announce set. +/// The remove is performed as a read-modify-write on the specified address lot. +#[derive(Parser, Debug, Clone)] +#[command(verbatim_doc_comment)] +#[command(name = "withdraw")] +pub struct CmdBgpWithdraw { + /// The announce set to withdraw from. + #[arg(long)] + announce_set: Name, + + /// The prefix to withdraw. + #[arg(long)] + prefix: oxnet::IpNet, +} + +#[async_trait] +impl RunnableCmd for CmdBgpWithdraw { + async fn run(&self, ctx: &Context) -> Result<()> { + let mut current: Vec = ctx + .client()? + .networking_bgp_announce_set_list() + .name_or_id(NameOrId::Name(self.announce_set.clone())) + .send() + .await? + .into_inner() + .into_iter() + .map(|x| BgpAnnouncementCreate { + address_lot_block: NameOrId::Id(x.address_lot_block_id), + network: x.network, + }) + .collect(); + + current.retain(|x| x.network.to_string() != self.prefix.to_string()); + + ctx.client()? + .networking_bgp_announce_set_update() + .body(BgpAnnounceSetCreate { + announcement: current, + name: self.announce_set.clone(), + description: self.announce_set.to_string(), //TODO? + }) + .send() + .await?; + + Ok(()) + } +} + +#[derive(clap::ValueEnum, Clone, Debug)] +enum FilterDirection { + Import, + Export, +} + +/// Add a filtering requirement to a BGP session. +/// +/// The Oxide BGP implementation can filter prefixes received from peers +/// on import and filter prefixes sent to peers on export. This command +/// provides a way to specify import/export filtering. Filtering is a +/// property of the BGP peering settings found in port settings configuration. +/// This command works by performing a read-modify-write on the port settings +/// configuration identified by the specified rack/switch/port. +#[derive(Parser, Debug, Clone)] +#[command(verbatim_doc_comment)] +#[command(name = "filter")] +pub struct CmdBgpFilter { + /// Id of the rack to add the address to. + #[arg(long)] + rack: Uuid, + + /// Switch to add the address to. + #[arg(long, value_enum)] + switch: Switch, + + /// Port to add the port to. + #[arg(long, value_enum)] + port: Port, + + /// Peer to apply allow list to. + #[arg(long)] + peer: IpAddr, + + /// Whether to apply the filter to imported or exported prefixes. + #[arg(long, value_enum)] + direction: FilterDirection, + + /// Prefixes to allow for the peer. + #[clap(long, conflicts_with = "no_filtering")] + allowed: Vec, + + /// Do not filter. + #[clap(long, conflicts_with = "allowed")] + no_filtering: bool, +} + +#[async_trait] +impl RunnableCmd for CmdBgpFilter { + async fn run(&self, ctx: &Context) -> Result<()> { + let mut settings = current_port_settings(ctx, &self.rack, &self.switch, &self.port).await?; + match settings.bgp_peers.get_mut(PHY0) { + None => return Err(anyhow::anyhow!("no BGP peers configured")), + Some(config) => { + let peer = config + .peers + .iter_mut() + .find(|x| x.addr == self.peer) + .ok_or(anyhow::anyhow!("specified peer does not exist"))?; + + let list: Vec = self + .allowed + .iter() + .map(|x| x.to_string().parse().unwrap()) + .collect(); + match self.direction { + FilterDirection::Import => { + if self.no_filtering { + peer.allowed_import = ImportExportPolicy::NoFiltering; + } else { + peer.allowed_import = ImportExportPolicy::Allow(list); + } + } + FilterDirection::Export => { + if self.no_filtering { + peer.allowed_export = ImportExportPolicy::NoFiltering; + } else { + peer.allowed_export = ImportExportPolicy::Allow(list); + } + } + } + } + } + ctx.client()? + .networking_switch_port_settings_create() + .body(settings) + .send() + .await?; + Ok(()) + } +} + +/// Manage switch port addresses. +#[derive(Parser, Debug, Clone)] +#[command(verbatim_doc_comment)] +#[command(name = "addr")] +pub struct CmdAddr { + #[clap(subcommand)] + subcmd: AddrSubCommand, +} + +#[async_trait] +impl RunnableCmd for CmdAddr { + async fn run(&self, ctx: &Context) -> Result<()> { + match &self.subcmd { + AddrSubCommand::Add(cmd) => cmd.run(ctx).await, + AddrSubCommand::Del(cmd) => cmd.run(ctx).await, + } + } +} + +#[derive(Parser, Debug, Clone)] +enum AddrSubCommand { + /// Add an address to a port configuration. + Add(CmdAddrAdd), + + /// Remove an address from a port configuration. + Del(CmdAddrDel), +} + +/// Add an address to a switch port. +/// +/// Addresses are a part of switch port settings configuration. This command +/// works by performing a read-modify-write on the switch port settings +/// configuration identified by the specified rack/switch/port. +#[derive(Parser, Debug, Clone)] +#[command(verbatim_doc_comment)] +#[command(name = "add")] +pub struct CmdAddrAdd { + /// Id of the rack to add the address to. + #[arg(long)] + rack: Uuid, + + /// Switch to add the address to. + #[arg(long, value_enum)] + switch: Switch, + + /// Port to add the port to. + #[arg(long, value_enum)] + port: Port, + + /// Address to add. + #[arg(long)] + addr: oxnet::Ipv4Net, + + /// Address lot to allocate from. + #[arg(long)] + lot: NameOrId, + + /// Optional VLAN to assign to the address. + #[arg(long)] + vlan: Option, +} + +#[async_trait] +impl RunnableCmd for CmdAddrAdd { + async fn run(&self, ctx: &Context) -> Result<()> { + let mut settings = current_port_settings(ctx, &self.rack, &self.switch, &self.port).await?; + let addr = Address { + address: IpNet::V4(self.addr.to_string().parse().unwrap()), + address_lot: self.lot.clone(), + vlan_id: self.vlan, + }; + match settings.addresses.get_mut(PHY0) { + Some(ac) => { + ac.addresses.push(addr); + } + None => { + settings.addresses.insert( + String::from(PHY0), + AddressConfig { + addresses: vec![addr], + }, + ); + } + } + ctx.client()? + .networking_switch_port_settings_create() + .body(settings) + .send() + .await?; + Ok(()) + } +} + +/// Remove an address from a switch port. +/// +/// Addresses are a part of switch port settings configuration. This command +/// works by performing a read-modify-write on the switch port settings +/// configuration identified by the specified rack/switch/port. +#[derive(Parser, Debug, Clone)] +#[command(verbatim_doc_comment)] +#[command(name = "del")] +pub struct CmdAddrDel { + /// Id of the rack to remove the address from. + #[arg(long)] + rack: Uuid, + + /// Switch to remove the address from. + #[arg(long, value_enum)] + switch: Switch, + + /// Port to remove the address from. + #[arg(long, value_enum)] + port: Port, + + /// Address to remove. + #[arg(long)] + addr: oxnet::Ipv4Net, +} + +#[async_trait] +impl RunnableCmd for CmdAddrDel { + async fn run(&self, ctx: &Context) -> Result<()> { + let mut settings = current_port_settings(ctx, &self.rack, &self.switch, &self.port).await?; + if let Some(addrs) = settings.addresses.get_mut(PHY0) { + addrs + .addresses + .retain(|x| x.address.to_string() != self.addr.to_string()); + } + ctx.client()? + .networking_switch_port_settings_create() + .body(settings) + .send() + .await?; + Ok(()) + } +} + +/// Manage a rack's BGP configuration. +/// +/// Rack BGP configuration is centered around peers. The subcommands available +/// within this command allow for managing BGP peer sessions. +#[derive(Parser, Debug, Clone)] +#[command(verbatim_doc_comment)] +#[command(name = "config")] +pub struct CmdBgpConfig { + #[clap(subcommand)] + subcmd: BgpConfigSubCommand, +} + +#[async_trait] +impl RunnableCmd for CmdBgpConfig { + async fn run(&self, ctx: &Context) -> Result<()> { + match &self.subcmd { + BgpConfigSubCommand::Peer(cmd) => cmd.run(ctx).await, + } + } +} + +#[derive(Parser, Debug, Clone)] +enum BgpConfigSubCommand { + /// Manage BGP peer configuration. + Peer(CmdBgpPeer), +} + +/// Manage BGP peers. +/// +/// This command provides add and delete subcommands for managing BGP peers. +/// BGP peer configuration is a part of a switch port settings configuration. +/// The peer add and remove subcommands perform read-modify-write operations +/// on switch port settings objects to manage BGP peer configurations. +#[derive(Parser, Debug, Clone)] +#[command(verbatim_doc_comment)] +#[command(name = "peer")] +pub struct CmdBgpPeer { + #[clap(subcommand)] + subcmd: BgpConfigPeerSubCommand, +} + +#[async_trait] +impl RunnableCmd for CmdBgpPeer { + async fn run(&self, ctx: &Context) -> Result<()> { + match &self.subcmd { + BgpConfigPeerSubCommand::Add(cmd) => cmd.run(ctx).await, + BgpConfigPeerSubCommand::Del(cmd) => cmd.run(ctx).await, + } + } +} + +#[derive(Parser, Debug, Clone)] +enum BgpConfigPeerSubCommand { + /// Add a BGP peer to a port configuration. + Add(CmdBgpPeerAdd), + + /// Remove a BGP from a port configuration. + Del(CmdBgpPeerDel), +} + +/// Add a BGP peer to a given switch port configuration. +/// +/// This performs a read-modify-write on the specified switch port +/// configuration. +#[derive(Parser, Debug, Clone)] +#[command(verbatim_doc_comment)] +#[command(name = "add")] +pub struct CmdBgpPeerAdd { + /// Id of the rack to add the peer to. + #[arg(long)] + rack: Uuid, + + /// Switch to add the peer to. + #[arg(long, value_enum)] + switch: Switch, + + /// Port to add the peer to. + #[arg(long, value_enum)] + port: Port, + + /// Address of the peer to add. + #[arg(long)] + addr: IpAddr, + + /// BGP configuration this peer is associated with. + #[arg(long)] + bgp_config: NameOrId, + + /// Prefixes that may be imported form the peer. Empty list means all prefixes + /// allowed. + #[arg(long)] + allowed_imports: Vec, + + /// Prefixes that may be exported to the peer. Empty list means all prefixes + /// allowed. + #[arg(long)] + allowed_exports: Vec, + + /// Include the provided communities in updates sent to the peer. + #[arg(long)] + communities: Vec, + + /// How long to to wait between TCP connection retries (seconds). + #[clap(long, default_value_t = 0u32)] + pub connect_retry: u32, + + /// How long to delay sending an open request after establishing a TCP + /// session (seconds). + #[clap(long, default_value_t = 0u32)] + pub delay_open: u32, + + /// Enforce that the first AS in paths received from this peer is the + /// peer's AS. + #[clap(long, default_value_t = false)] + pub enforce_first_as: bool, + + /// How long to hold peer connections between keepalives (seconds). + #[clap(long, default_value_t = 6u32)] + pub hold_time: u32, + + /// How often to send keepalive requests (seconds). + #[clap(long, default_value_t = 2u32)] + pub keepalive: u32, + + /// How long to hold a peer in idle before attempting a new session + /// (seconds). + #[clap(long, default_value_t = 0u32)] + pub idle_hold_time: u32, + + /// Apply a local preference to routes received from this peer. + #[arg(long)] + pub local_pref: Option, + + /// Use the given key for TCP-MD5 authentication with the peer. + #[arg(long)] + pub md5_auth_key: Option, + + /// Require messages from a peer have a minimum IP time to live field. + #[arg(long)] + pub min_ttl: Option, + + /// Apply the provided multi-exit discriminator (MED) updates sent to + /// the peer. + #[arg(long)] + pub multi_exit_discriminator: Option, + + /// Require that a peer has a specified ASN. + #[arg(long)] + pub remote_asn: Option, + + /// Associate a VLAN ID with a peer. + #[arg(long)] + pub vlan_id: Option, +} + +#[async_trait] +impl RunnableCmd for CmdBgpPeerAdd { + async fn run(&self, ctx: &Context) -> Result<()> { + let mut settings = current_port_settings(ctx, &self.rack, &self.switch, &self.port).await?; + let peer = BgpPeer { + addr: self.addr, + allowed_import: if self.allowed_imports.is_empty() { + ImportExportPolicy::NoFiltering + } else { + ImportExportPolicy::Allow( + self.allowed_imports + .clone() + .into_iter() + .map(|x| x.to_string().parse().unwrap()) + .collect(), + ) + }, + allowed_export: if self.allowed_exports.is_empty() { + ImportExportPolicy::NoFiltering + } else { + ImportExportPolicy::Allow( + self.allowed_exports + .clone() + .into_iter() + .map(|x| x.to_string().parse().unwrap()) + .collect(), + ) + }, + bgp_config: self.bgp_config.clone(), + communities: self.communities.clone(), + connect_retry: self.connect_retry, + delay_open: self.delay_open, + enforce_first_as: self.enforce_first_as, + hold_time: self.hold_time, + idle_hold_time: self.idle_hold_time, + interface_name: PHY0.to_owned(), + keepalive: self.keepalive, + local_pref: self.local_pref, + md5_auth_key: self.md5_auth_key.clone(), + min_ttl: self.min_ttl, + multi_exit_discriminator: self.multi_exit_discriminator, + remote_asn: self.remote_asn, + vlan_id: self.vlan_id, + }; + match settings.bgp_peers.get_mut(PHY0) { + Some(conf) => { + conf.peers.push(peer); + } + None => { + settings + .bgp_peers + .insert(String::from(PHY0), BgpPeerConfig { peers: vec![peer] }); + } + } + ctx.client()? + .networking_switch_port_settings_create() + .body(settings) + .send() + .await?; + Ok(()) + } +} + +/// Remove a BGP peer from a given switch port configuration. +/// +/// This performs a read-modify-write on the specified switch port +/// configuration. +#[derive(Parser, Debug, Clone)] +#[command(verbatim_doc_comment)] +#[command(name = "del")] +pub struct CmdBgpPeerDel { + /// Id of the rack to remove the peer from. + #[arg(long)] + rack: Uuid, + + /// Switch to remove the peer from. + #[arg(long, value_enum)] + switch: Switch, + + /// Port to remove the peer from. + #[arg(long, value_enum)] + port: Port, + + /// Address of the peer to remove. + #[arg(long)] + addr: IpAddr, +} + +#[async_trait] +impl RunnableCmd for CmdBgpPeerDel { + async fn run(&self, ctx: &Context) -> Result<()> { + let mut settings = current_port_settings(ctx, &self.rack, &self.switch, &self.port).await?; + if let Some(config) = settings.bgp_peers.get_mut(PHY0) { + config.peers.retain(|x| x.addr != self.addr); + } + ctx.client()? + .networking_switch_port_settings_create() + .body(settings) + .send() + .await?; + Ok(()) + } +} + +/// Get the configuration of switch ports. +#[derive(Parser, Debug, Clone)] +#[command(verbatim_doc_comment)] +#[command(name = "net port config")] +pub struct CmdPortConfig {} + +#[async_trait] +impl RunnableCmd for CmdPortConfig { + async fn run(&self, ctx: &Context) -> Result<()> { + let c = ctx.client()?; + + let ports = c + .networking_switch_port_list() + .stream() + .try_collect::>() + .await?; + + let mut tw = TabWriter::new(std::io::stdout()).ansi(true); + + // TODO bad API, having to pull all address lots to work backwards from + // address reference to address lot block is terribad + let addr_lots = c + .networking_address_lot_list() + .stream() + .try_collect::>() + .await?; + + let mut addr_lot_blocks = Vec::new(); + for a in addr_lots.iter() { + let blocks = c + .networking_address_lot_block_list() + .address_lot(a.id) + .stream() + .try_collect::>() + .await?; + + for b in blocks.iter() { + addr_lot_blocks.push((a.clone(), b.clone())); + } + } + + for p in &ports { + if let Some(id) = p.port_settings_id { + let config = c + .networking_switch_port_settings_view() + .port(id) + .send() + .await? + .into_inner(); + + let bgp_configs: HashMap = c + .networking_bgp_config_list() + .stream() + .try_collect::>() + .await? + .into_iter() + .map(|x| (x.id, x.name)) + .collect(); + + println!( + "{}{}{}", + p.switch_location.to_string().blue(), + "/".dimmed(), + p.port_name.blue(), + ); + + println!( + "{}", + "=".repeat(p.port_name.len() + p.switch_location.to_string().len() + 1) + .dimmed() + ); + + writeln!( + &mut tw, + "{}\t{}\t{}", + "Autoneg".dimmed(), + "Fec".dimmed(), + "Speed".dimmed(), + )?; + + for l in &config.links { + writeln!(&mut tw, "{:?}\t{:?}\t{:?}", l.autoneg, l.fec, l.speed,)?; + } + tw.flush()?; + println!(); + + writeln!( + &mut tw, + "{}\t{}\t{}", + "Address".dimmed(), + "Lot".dimmed(), + "VLAN".dimmed() + )?; + for a in &config.addresses { + let addr = match &a.address { + oxide::types::IpNet::V4(a) => a.to_string(), + oxide::types::IpNet::V6(a) => a.to_string(), + }; + + let alb = addr_lot_blocks + .iter() + .find(|x| x.1.id == a.address_lot_block_id) + .unwrap(); + + writeln!(&mut tw, "{}\t{}\t{:?}", addr, *alb.0.name, a.vlan_id)?; + } + tw.flush()?; + println!(); + + writeln!( + &mut tw, + "{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}", + "BGP Peer".dimmed(), + "Config".dimmed(), + "Export".dimmed(), + "Import".dimmed(), + "Communities".dimmed(), + "Connect Retry".dimmed(), + "Delay Open".dimmed(), + "Enforce First AS".dimmed(), + "Hold Time".dimmed(), + "Idle Hold Time".dimmed(), + "Keepalive".dimmed(), + "Local Pref".dimmed(), + "Md5 Auth".dimmed(), + "Min TTL".dimmed(), + "MED".dimmed(), + "Remote ASN".dimmed(), + "VLAN".dimmed(), + )?; + for p in &config.bgp_peers { + writeln!( + &mut tw, + "{}\t{}\t[{}]\t[{}]\t{:?}\t{}\t{}\t{}\t{}\t{}\t{}\t{:?}\t{:?}\t{:?}\t{:?}\t{:?}\t{:?}", + p.addr, + match &p.bgp_config { + NameOrId::Id(id) => bgp_configs[id].to_string(), + NameOrId::Name(name) => name.to_string(), + }, + match &p.allowed_export { + ImportExportPolicy::NoFiltering => String::from("no filtering"), + ImportExportPolicy::Allow(list) => list + .iter() + .map(|x| x.to_string()) + .collect::>() + .join(" "), + }, + match &p.allowed_import { + ImportExportPolicy::NoFiltering => String::from("no filtering"), + ImportExportPolicy::Allow(list) => list + .iter() + .map(|x| x.to_string()) + .collect::>() + .join(" "), + }, + p.communities, + p.connect_retry, + p.delay_open, + p.enforce_first_as, + p.hold_time, + p.idle_hold_time, + p.keepalive, + p.local_pref, + p.md5_auth_key, + p.min_ttl, + p.multi_exit_discriminator, + p.remote_asn, + p.vlan_id, + )?; + } + tw.flush()?; + println!(); + + // Uncomment to see full payload + //println!(""); + //println!("{:#?}", config); + //println!(""); + } + } + + Ok(()) + } +} + +/// Get the status of BGP on the rack. +/// +/// This will show the peering status for all peers on all switches. +#[derive(Parser, Debug, Clone)] +#[command(verbatim_doc_comment)] +#[command(name = "net bgp status")] +pub struct CmdBgpStatus {} + +#[async_trait] +impl RunnableCmd for CmdBgpStatus { + async fn run(&self, ctx: &Context) -> Result<()> { + let c = ctx.client()?; + + let status = c.networking_bgp_status().send().await?.into_inner(); + + let (sw0, sw1) = status + .iter() + .partition(|x| x.switch == SwitchLocation::Switch0); + + println!("{}", "switch0".dimmed()); + println!("{}", "=======".dimmed()); + show_status(&sw0)?; + println!(); + + println!("{}", "switch1".dimmed()); + println!("{}", "=======".dimmed()); + show_status(&sw1)?; + + Ok(()) + } +} + +fn show_status(st: &Vec<&BgpPeerStatus>) -> Result<()> { + let mut tw = TabWriter::new(std::io::stdout()).ansi(true); + writeln!( + &mut tw, + "{}\t{}\t{}\t{}\t{}", + "Peer Address".dimmed(), + "Local ASN".dimmed(), + "Remote ASN".dimmed(), + "Session State".dimmed(), + "State Duration".dimmed(), + )?; + for s in st { + writeln!( + tw, + "{}\t{}\t{}\t{:?}\t{}", + s.addr, + s.local_asn, + s.remote_asn, + s.state, + humantime::Duration::from(std::time::Duration::from_millis(s.state_duration_millis)), + )?; + } + tw.flush()?; + Ok(()) +} + +/// Get the status of switch ports. +#[derive(Parser, Debug, Clone)] +#[command(verbatim_doc_comment)] +#[command(name = "net port status")] +pub struct CmdPortStatus {} + +#[async_trait] +impl RunnableCmd for CmdPortStatus { + async fn run(&self, ctx: &Context) -> Result<()> { + let c = ctx.client()?; + + let ports = c + .networking_switch_port_list() + .stream() + .try_collect::>() + .await?; + + let (mut sw0, mut sw1): (Vec<&SwitchPort>, Vec<&SwitchPort>) = ports + .iter() + .partition(|x| x.switch_location.as_str() == "switch0"); + + sw0.sort_by_key(|x| x.port_name.as_str()); + sw1.sort_by_key(|x| x.port_name.as_str()); + + println!("{}", "switch0".dimmed()); + println!("{}", "=======".dimmed()); + self.show_switch(c, "switch0", &sw0).await?; + + println!("{}", "switch1".dimmed()); + println!("{}", "=======".dimmed()); + self.show_switch(c, "switch1", &sw1).await?; + + Ok(()) + } +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct MacAddr { + a: [u8; 6], +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +struct LinkStatus { + address: MacAddr, + enabled: bool, + autoneg: bool, + fec: String, + link_state: String, + fsm_state: String, + media: String, + speed: String, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +#[serde(rename_all = "snake_case")] +pub enum ReceiverPower { + /// The measurement is represents average optical power, in mW. + Average(f32), + + /// The measurement represents a peak-to-peak, in mW. + PeakToPeak(f32), +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +struct Monitors { + receiver_power: Vec, + transmitter_bias_current: Vec, + transmitter_power: Vec, +} + +impl CmdPortStatus { + async fn show_switch(&self, c: &Client, sw: &str, ports: &Vec<&SwitchPort>) -> Result<()> { + let mut ltw = TabWriter::new(std::io::stdout()).ansi(true); + let mut mtw = TabWriter::new(std::io::stdout()).ansi(true); + + writeln!( + &mut ltw, + "{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}", + "Port".dimmed(), + "Configured".dimmed(), + "Enabled".dimmed(), + "MAC".dimmed(), + "Autoneg".dimmed(), + "FEC".dimmed(), + "Link/FSM State".dimmed(), + "Media".dimmed(), + "Speed".dimmed(), + )?; + + writeln!( + &mut mtw, + "{}\t{}\t{}", + "Receiver Power".dimmed(), + "Transmitter Bias Current".dimmed(), + "Transmitter Power".dimmed(), + )?; + + for p in ports { + let status = c + .networking_switch_port_status() + .port(&p.port_name) + .rack_id(p.rack_id) + .switch_location(sw) + .send() + .await + .ok() + .map(|x| x.into_inner().0); + + let link = status.as_ref().map(|x| { + let ls: LinkStatus = + serde_json::from_value(x.get("link").unwrap().clone()).unwrap(); + ls + }); + + writeln!( + &mut ltw, + "{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}", + p.port_name, + p.port_settings_id.is_some(), + link.as_ref() + .map(|x| x.enabled.to_string()) + .unwrap_or("-".to_string()), + link.as_ref() + .map(|x| format!( + "{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}", + x.address.a[0], + x.address.a[1], + x.address.a[2], + x.address.a[3], + x.address.a[4], + x.address.a[5] + )) + .unwrap_or("-".to_string()), + link.as_ref() + .map(|x| x.autoneg.to_string()) + .unwrap_or("-".to_string()), + link.as_ref() + .map(|x| x.fec.clone()) + .unwrap_or("-".to_string()), + link.as_ref() + .map(|x| format!("{}/{}", x.link_state, x.fsm_state)) + .unwrap_or("-".to_string()), + link.as_ref() + .map(|x| x.media.clone()) + .unwrap_or("-".to_string()), + link.as_ref() + .map(|x| x.speed.clone()) + .unwrap_or("-".to_string()), + )?; + + let monitors = status + .as_ref() + .and_then(|value| value.get("monitors")) + .and_then(|value| Monitors::deserialize(value).ok()); + + writeln!( + &mut mtw, + "{}\t{}\t{}", + monitors + .as_ref() + .map(|x| format!("{:?}", x.receiver_power)) + .unwrap_or("-".to_string()), + monitors + .as_ref() + .map(|x| format!("{:?}", x.transmitter_bias_current)) + .unwrap_or("-".to_string()), + monitors + .as_ref() + .map(|x| format!("{:?}", x.transmitter_power)) + .unwrap_or("-".to_string()), + )?; + } + + ltw.flush()?; + println!(); + mtw.flush()?; + println!(); + + Ok(()) + } +} + +// NOTE: This bonanza of befuckerry is needed to translate the current +// switch port settings view into a corresponding switch port +// settings create request. It's the preliminary step for a read- +// modify-write operation. +async fn create_current(settings_id: Uuid, ctx: &Context) -> Result { + let list = ctx + .client()? + .networking_switch_port_settings_list() + .stream() + .try_collect::>() + .await?; + + let name = list + .iter() + .find(|x| x.id == settings_id) + .ok_or(anyhow::anyhow!("settings not found for {}", settings_id))? + .name + .clone(); + + let current = ctx + .client()? + .networking_switch_port_settings_view() + .port(settings_id) + .send() + .await + .unwrap() + .into_inner(); + + let mut block_to_lot = HashMap::new(); + let lots = ctx + .client()? + .networking_address_lot_list() + .stream() + .try_collect::>() + .await?; + + for lot in lots.iter() { + let lot_blocks = ctx + .client()? + .networking_address_lot_block_list() + .address_lot(lot.id) + .stream() + .try_collect::>() + .await?; + for block in lot_blocks.iter() { + block_to_lot.insert(block.id, lot.id); + } + } + + let addrs: Vec
= current + .addresses + .clone() + .into_iter() + .map(|x| Address { + address: x.address, + address_lot: NameOrId::Id(block_to_lot[&x.address_lot_block_id]), + vlan_id: x.vlan_id, + }) + .collect(); + + let mut addresses = HashMap::new(); + addresses.insert(String::from(PHY0), AddressConfig { addresses: addrs }); + + let mut bgp_peers = HashMap::new(); + bgp_peers.insert( + String::from(PHY0), + BgpPeerConfig { + peers: current.bgp_peers, + }, + ); + + let groups: Vec = current + .groups + .iter() + .map(|x| NameOrId::Id(x.port_settings_group_id)) + .collect(); + + let mut interfaces: HashMap = current + .interfaces + .iter() + .map(|x| { + ( + x.interface_name.clone(), + SwitchInterfaceConfigCreate { + kind: match x.kind { + SwitchInterfaceKind2::Primary => SwitchInterfaceKind::Primary, + SwitchInterfaceKind2::Loopback => SwitchInterfaceKind::Loopback, + SwitchInterfaceKind2::Vlan => { + todo!("vlan interface outside vlan interfaces?") + } + }, + v6_enabled: x.v6_enabled, + }, + ) + }) + .collect(); + + for v in current.vlan_interfaces.iter() { + interfaces.insert( + format!("vlan-{}", v.vlan_id), + SwitchInterfaceConfigCreate { + kind: SwitchInterfaceKind::Vlan(v.vlan_id), + v6_enabled: false, + }, + ); + } + + let links: HashMap = current + .links + .iter() + .enumerate() + .map(|(i, x)| { + ( + format!("phy{}", i), + LinkConfigCreate { + autoneg: x.autoneg, + fec: x.fec, + lldp: LldpServiceConfigCreate { + //TODO + enabled: false, + lldp_config: None, + }, + mtu: x.mtu, + speed: x.speed, + }, + ) + }) + .collect(); + + let port_config = SwitchPortConfigCreate { + geometry: match current.port.geometry { + SwitchPortGeometry2::Qsfp28x1 => SwitchPortGeometry::Qsfp28x1, + SwitchPortGeometry2::Qsfp28x2 => SwitchPortGeometry::Qsfp28x2, + SwitchPortGeometry2::Sfp28x4 => SwitchPortGeometry::Sfp28x4, + }, + }; + + let route_config = RouteConfig { + routes: current + .routes + .iter() + .map(|x| Route { + dst: x.dst.clone(), + gw: x.gw.to_string().parse().unwrap(), + vid: x.vlan_id, + }) + .collect(), + }; + + let mut routes = HashMap::new(); + routes.insert(String::from(PHY0), route_config); + + let create = SwitchPortSettingsCreate { + addresses, + bgp_peers, + description: String::from("switch port settings"), + groups, + interfaces, + links, + name: name.parse().unwrap(), + port_config, + routes, + }; + + Ok(create) +} + +#[derive(clap::ValueEnum, Clone, Debug)] +enum Switch { + Switch0, + Switch1, +} + +impl std::fmt::Display for Switch { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", format!("{self:?}").to_lowercase()) + } +} + +#[derive(clap::ValueEnum, Clone, Debug)] +enum Port { + Qsfp0, + Qsfp1, + Qsfp2, + Qsfp3, + Qsfp4, + Qsfp5, + Qsfp6, + Qsfp7, + Qsfp8, + Qsfp9, + Qsfp10, + Qsfp11, + Qsfp12, + Qsfp13, + Qsfp14, + Qsfp15, + Qsfp16, + Qsfp17, + Qsfp18, + Qsfp19, + Qsfp20, + Qsfp21, + Qsfp22, + Qsfp23, + Qsfp24, + Qsfp25, + Qsfp26, + Qsfp27, + Qsfp28, + Qsfp29, + Qsfp30, + Qsfp31, +} + +impl std::fmt::Display for Port { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", format!("{self:?}").to_lowercase()) + } +} + +async fn get_port( + ctx: &Context, + rack_id: &Uuid, + switch: &Switch, + port: &Port, +) -> Result { + let ports = ctx + .client()? + .networking_switch_port_list() + .stream() + .try_collect::>() + .await?; + + let port = ports + .into_iter() + .find(|x| { + x.rack_id == *rack_id + && x.switch_location == switch.to_string() + && x.port_name == port.to_string() + }) + .ok_or(anyhow::anyhow!( + "port {} not found for rack {} switch {}", + port, + switch, + rack_id + ))?; + + Ok(port) +} + +async fn get_port_settings_id( + ctx: &Context, + rack_id: &Uuid, + switch: &Switch, + port: &Port, +) -> Result { + let port = get_port(ctx, rack_id, switch, port).await?; + let id = port.port_settings_id.ok_or(anyhow::anyhow!( + "Port settings uninitialized. Initialize by creating a link." + ))?; + Ok(id) +} + +async fn current_port_settings( + ctx: &Context, + rack_id: &Uuid, + switch: &Switch, + port: &Port, +) -> Result { + let id = get_port_settings_id(ctx, rack_id, switch, port).await?; + let settings = create_current(id, ctx).await?; + Ok(settings) +} diff --git a/cli/src/generated_cli.rs b/cli/src/generated_cli.rs index 020fdcfa..b40f2691 100644 --- a/cli/src/generated_cli.rs +++ b/cli/src/generated_cli.rs @@ -164,8 +164,8 @@ impl Cli { CliCommand::NetworkingBgpAnnounceSetList => { Self::cli_networking_bgp_announce_set_list() } - CliCommand::NetworkingBgpAnnounceSetCreate => { - Self::cli_networking_bgp_announce_set_create() + CliCommand::NetworkingBgpAnnounceSetUpdate => { + Self::cli_networking_bgp_announce_set_update() } CliCommand::NetworkingBgpAnnounceSetDelete => { Self::cli_networking_bgp_announce_set_delete() @@ -4212,7 +4212,7 @@ impl Cli { .about("Get originated routes for a BGP configuration") } - pub fn cli_networking_bgp_announce_set_create() -> clap::Command { + pub fn cli_networking_bgp_announce_set_update() -> clap::Command { clap::Command::new("") .arg( clap::Arg::new("description") @@ -4240,7 +4240,11 @@ impl Cli { .action(clap::ArgAction::SetTrue) .help("XXX"), ) - .about("Create new BGP announce set") + .about("Update BGP announce set") + .long_about( + "If the announce set exists, this endpoint replaces the existing announce set \ + with the one specified.", + ) } pub fn cli_networking_bgp_announce_set_delete() -> clap::Command { @@ -6209,8 +6213,8 @@ impl Cli { CliCommand::NetworkingBgpAnnounceSetList => { self.execute_networking_bgp_announce_set_list(matches).await } - CliCommand::NetworkingBgpAnnounceSetCreate => { - self.execute_networking_bgp_announce_set_create(matches) + CliCommand::NetworkingBgpAnnounceSetUpdate => { + self.execute_networking_bgp_announce_set_update(matches) .await } CliCommand::NetworkingBgpAnnounceSetDelete => { @@ -10824,11 +10828,11 @@ impl Cli { } } - pub async fn execute_networking_bgp_announce_set_create( + pub async fn execute_networking_bgp_announce_set_update( &self, matches: &clap::ArgMatches, ) -> anyhow::Result<()> { - let mut request = self.client.networking_bgp_announce_set_create(); + let mut request = self.client.networking_bgp_announce_set_update(); if let Some(value) = matches.get_one::("description") { request = request.body_map(|body| body.description(value.clone())) } @@ -10845,7 +10849,7 @@ impl Cli { } self.config - .execute_networking_bgp_announce_set_create(matches, &mut request)?; + .execute_networking_bgp_announce_set_update(matches, &mut request)?; let result = request.send().await; match result { Ok(r) => { @@ -13955,10 +13959,10 @@ pub trait CliConfig { Ok(()) } - fn execute_networking_bgp_announce_set_create( + fn execute_networking_bgp_announce_set_update( &self, matches: &clap::ArgMatches, - request: &mut builder::NetworkingBgpAnnounceSetCreate, + request: &mut builder::NetworkingBgpAnnounceSetUpdate, ) -> anyhow::Result<()> { Ok(()) } @@ -14567,7 +14571,7 @@ pub enum CliCommand { NetworkingBgpConfigCreate, NetworkingBgpConfigDelete, NetworkingBgpAnnounceSetList, - NetworkingBgpAnnounceSetCreate, + NetworkingBgpAnnounceSetUpdate, NetworkingBgpAnnounceSetDelete, NetworkingBgpMessageHistory, NetworkingBgpImportedRoutesIpv4, @@ -14768,7 +14772,7 @@ impl CliCommand { CliCommand::NetworkingBgpConfigCreate, CliCommand::NetworkingBgpConfigDelete, CliCommand::NetworkingBgpAnnounceSetList, - CliCommand::NetworkingBgpAnnounceSetCreate, + CliCommand::NetworkingBgpAnnounceSetUpdate, CliCommand::NetworkingBgpAnnounceSetDelete, CliCommand::NetworkingBgpMessageHistory, CliCommand::NetworkingBgpImportedRoutesIpv4, diff --git a/cli/src/main.rs b/cli/src/main.rs index 38694e52..70e7d618 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -23,6 +23,7 @@ mod cmd_completion; mod cmd_disk; mod cmd_docs; mod cmd_instance; +mod cmd_net; mod cmd_timeseries; mod cmd_version; @@ -51,6 +52,15 @@ pub fn make_cli() -> NewCli<'static> { .add_custom::("instance from-image") .add_custom::("completion") .add_custom::("experimental timeseries dashboard") + .add_custom::("system networking addr") + .add_custom::("system networking link") + .add_custom::("system networking switch-port-settings show") + .add_custom::("system hardware switch-port show-status") + .add_custom::("system networking bgp show-status") + .add_custom::("system networking bgp peer") + .add_custom::("system networking bgp announce") + .add_custom::("system networking bgp withdraw") + .add_custom::("system networking bgp filter") } #[tokio::main] diff --git a/cli/tests/data/json-body-required.txt b/cli/tests/data/json-body-required.txt index 407459fe..8b8950cc 100644 --- a/cli/tests/data/json-body-required.txt +++ b/cli/tests/data/json-body-required.txt @@ -1,10 +1,6 @@ oxide image create oxide policy update oxide project policy update -oxide system networking address-lot create -oxide system networking bgp announce-set create -oxide system networking switch-port-settings create -oxide system policy update oxide silo idp local user create oxide silo idp local user set-password oxide silo create @@ -12,4 +8,8 @@ oxide silo policy update oxide vpc firewall-rules update oxide vpc router route create oxide vpc router route update -oxide disk create \ No newline at end of file +oxide disk create +oxide system policy update +oxide system networking address-lot create +oxide system networking switch-port-settings create +oxide system networking bgp announce-set update \ No newline at end of file diff --git a/cli/tests/data/test_switch_port_settings_show.stdout b/cli/tests/data/test_switch_port_settings_show.stdout new file mode 100644 index 00000000..a8aa20b4 --- /dev/null +++ b/cli/tests/data/test_switch_port_settings_show.stdout @@ -0,0 +1,26 @@ +switch0/qsfp0 +============= +Autoneg Fec Speed +false None Speed100G + +Address Lot VLAN +169.254.10.2/30 initial-infra None +169.254.30.2/30 initial-infra Some(300) + +BGP Peer Config Export Import Communities Connect Retry Delay Open Enforce First AS Hold Time Idle Hold Time Keepalive Local Pref Md5 Auth Min TTL MED Remote ASN VLAN +169.254.10.1 as65547 [198.51.100.0/24] [no filtering] [] 3 3 false 6 3 2 None None None None None None +169.254.30.1 as65547 [203.0.113.0/24] [no filtering] [] 0 0 false 6 0 2 None None None None None Some(300) + +switch1/qsfp0 +============= +Autoneg Fec Speed +false None Speed100G + +Address Lot VLAN +169.254.20.2/30 initial-infra None +169.254.40.2/30 initial-infra Some(400) + +BGP Peer Config Export Import Communities Connect Retry Delay Open Enforce First AS Hold Time Idle Hold Time Keepalive Local Pref Md5 Auth Min TTL MED Remote ASN VLAN +169.254.20.1 as65547 [198.51.100.0/24] [no filtering] [] 3 3 false 6 3 2 None None None None None None +169.254.40.1 as65547 [203.0.113.0/24] [no filtering] [] 0 0 false 6 0 2 None None None None None Some(400) + diff --git a/cli/tests/test_net.rs b/cli/tests/test_net.rs new file mode 100644 index 00000000..ee725cb2 --- /dev/null +++ b/cli/tests/test_net.rs @@ -0,0 +1,309 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +// Copyright 2024 Oxide Computer Company + +use assert_cmd::Command; +use chrono::prelude::*; +use httpmock::MockServer; +use oxide::types::{ + AddressLot, AddressLotBlock, AddressLotBlockResultsPage, AddressLotKind, AddressLotResultsPage, + BgpConfig, BgpConfigResultsPage, BgpPeer, ImportExportPolicy, LinkFec, LinkSpeed, NameOrId, + SwitchPort, SwitchPortAddressConfig, SwitchPortConfig, SwitchPortGeometry2, + SwitchPortLinkConfig, SwitchPortResultsPage, SwitchPortSettings, SwitchPortSettingsView, +}; +use oxide_httpmock::MockServerExt; +use uuid::Uuid; + +#[test] +fn test_port_config() { + let server = MockServer::start(); + + let rack_id = Uuid::new_v4(); + let switch0_qsfp0_settings_id = Uuid::new_v4(); + let switch1_qsfp0_settings_id = Uuid::new_v4(); + let lot_id = Uuid::new_v4(); + let lot_block_id = Uuid::new_v4(); + + let ports = SwitchPortResultsPage { + items: vec![ + SwitchPort { + id: Uuid::new_v4(), + port_name: String::from("qsfp0"), + port_settings_id: Some(switch0_qsfp0_settings_id), + rack_id, + switch_location: String::from("switch0"), + }, + SwitchPort { + id: Uuid::new_v4(), + port_name: String::from("qsfp0"), + port_settings_id: Some(switch1_qsfp0_settings_id), + rack_id, + switch_location: String::from("switch1"), + }, + ], + next_page: None, + }; + + let mock_ports = server.networking_switch_port_list(|when, then| { + when.into_inner().any_request(); + then.ok(&ports); + }); + + let lots = AddressLotResultsPage { + items: vec![AddressLot { + description: String::from("Initial infrastructure address lot"), + id: lot_id, + kind: AddressLotKind::Infra, + name: "initial-infra".parse().unwrap(), + time_created: Utc.with_ymd_and_hms(2024, 7, 8, 9, 10, 11).unwrap(), + time_modified: Utc.with_ymd_and_hms(2024, 7, 8, 9, 10, 11).unwrap(), + }], + next_page: None, + }; + + let mock_lots = server.networking_address_lot_list(|when, then| { + when.into_inner().any_request(); + then.ok(&lots); + }); + + let lot_blocks = AddressLotBlockResultsPage { + items: vec![AddressLotBlock { + first_address: "198.51.100.0".parse().unwrap(), + last_address: "198.51.100.254".parse().unwrap(), + id: lot_block_id, + }], + next_page: None, + }; + + let mock_lot_blocks = server.networking_address_lot_block_list(|when, then| { + when.into_inner().any_request(); + then.ok(&lot_blocks); + }); + + let bgp_configs = BgpConfigResultsPage { + items: vec![BgpConfig { + asn: 65547, + description: String::from("as65547"), + id: Uuid::new_v4(), + name: "as65547".parse().unwrap(), + vrf: None, + time_created: Utc.with_ymd_and_hms(2024, 7, 8, 9, 10, 11).unwrap(), + time_modified: Utc.with_ymd_and_hms(2024, 7, 8, 9, 10, 11).unwrap(), + }], + next_page: None, + }; + + let mock_bgp_configs = server.networking_bgp_config_list(|when, then| { + when.into_inner().any_request(); + then.ok(&bgp_configs); + }); + + let switch0_qsfp0_view = SwitchPortSettingsView { + addresses: vec![ + SwitchPortAddressConfig { + address: "169.254.10.2/30".parse().unwrap(), + address_lot_block_id: lot_block_id, + interface_name: String::from("phy0"), + port_settings_id: switch0_qsfp0_settings_id, + vlan_id: None, + }, + SwitchPortAddressConfig { + address: "169.254.30.2/30".parse().unwrap(), + address_lot_block_id: lot_block_id, + interface_name: String::from("phy0"), + port_settings_id: switch0_qsfp0_settings_id, + vlan_id: Some(300), + }, + ], + bgp_peers: vec![ + BgpPeer { + interface_name: String::from("phy0"), + addr: "169.254.10.1".parse().unwrap(), + bgp_config: NameOrId::Id(bgp_configs.items[0].id), + allowed_export: ImportExportPolicy::Allow(vec!["198.51.100.0/24".parse().unwrap()]), + allowed_import: ImportExportPolicy::NoFiltering, + communities: Vec::new(), + connect_retry: 3, + delay_open: 3, + enforce_first_as: false, + hold_time: 6, + idle_hold_time: 3, + keepalive: 2, + local_pref: None, + md5_auth_key: None, + min_ttl: None, + multi_exit_discriminator: None, + remote_asn: None, + vlan_id: None, + }, + BgpPeer { + interface_name: String::from("phy0"), + addr: "169.254.30.1".parse().unwrap(), + bgp_config: NameOrId::Id(bgp_configs.items[0].id), + allowed_export: ImportExportPolicy::Allow(vec!["203.0.113.0/24".parse().unwrap()]), + allowed_import: ImportExportPolicy::NoFiltering, + communities: Vec::new(), + connect_retry: 0, + delay_open: 0, + enforce_first_as: false, + hold_time: 6, + idle_hold_time: 0, + keepalive: 2, + local_pref: None, + md5_auth_key: None, + min_ttl: None, + multi_exit_discriminator: None, + remote_asn: None, + vlan_id: Some(300), + }, + ], + groups: Vec::new(), + interfaces: Vec::new(), + link_lldp: Vec::new(), + links: vec![SwitchPortLinkConfig { + autoneg: false, + fec: LinkFec::None, + link_name: String::from("phy0"), + lldp_service_config_id: Uuid::new_v4(), + mtu: 1500, + port_settings_id: switch1_qsfp0_settings_id, + speed: LinkSpeed::Speed100G, + }], + port: SwitchPortConfig { + geometry: SwitchPortGeometry2::Qsfp28x1, + port_settings_id: switch1_qsfp0_settings_id, + }, + routes: Vec::new(), + settings: SwitchPortSettings { + description: String::from("default uplink 0 switch port settings"), + id: switch1_qsfp0_settings_id, + name: "default-uplink0".parse().unwrap(), + time_created: Utc.with_ymd_and_hms(2024, 7, 8, 9, 10, 11).unwrap(), + time_modified: Utc.with_ymd_and_hms(2024, 7, 8, 9, 10, 11).unwrap(), + }, + vlan_interfaces: Vec::new(), + }; + let switch1_qsfp0_view = SwitchPortSettingsView { + addresses: vec![ + SwitchPortAddressConfig { + address: "169.254.20.2/30".parse().unwrap(), + address_lot_block_id: lot_block_id, + interface_name: String::from("phy0"), + port_settings_id: switch0_qsfp0_settings_id, + vlan_id: None, + }, + SwitchPortAddressConfig { + address: "169.254.40.2/30".parse().unwrap(), + address_lot_block_id: lot_block_id, + interface_name: String::from("phy0"), + port_settings_id: switch0_qsfp0_settings_id, + vlan_id: Some(400), + }, + ], + bgp_peers: vec![ + BgpPeer { + interface_name: String::from("phy0"), + addr: "169.254.20.1".parse().unwrap(), + bgp_config: NameOrId::Id(bgp_configs.items[0].id), + allowed_export: ImportExportPolicy::Allow(vec!["198.51.100.0/24".parse().unwrap()]), + allowed_import: ImportExportPolicy::NoFiltering, + communities: Vec::new(), + connect_retry: 3, + delay_open: 3, + enforce_first_as: false, + hold_time: 6, + idle_hold_time: 3, + keepalive: 2, + local_pref: None, + md5_auth_key: None, + min_ttl: None, + multi_exit_discriminator: None, + remote_asn: None, + vlan_id: None, + }, + BgpPeer { + interface_name: String::from("phy0"), + addr: "169.254.40.1".parse().unwrap(), + bgp_config: NameOrId::Id(bgp_configs.items[0].id), + allowed_export: ImportExportPolicy::Allow(vec!["203.0.113.0/24".parse().unwrap()]), + allowed_import: ImportExportPolicy::NoFiltering, + communities: Vec::new(), + connect_retry: 0, + delay_open: 0, + enforce_first_as: false, + hold_time: 6, + idle_hold_time: 0, + keepalive: 2, + local_pref: None, + md5_auth_key: None, + min_ttl: None, + multi_exit_discriminator: None, + remote_asn: None, + vlan_id: Some(400), + }, + ], + groups: Vec::new(), + interfaces: Vec::new(), + link_lldp: Vec::new(), + links: vec![SwitchPortLinkConfig { + autoneg: false, + fec: LinkFec::None, + link_name: String::from("phy0"), + lldp_service_config_id: Uuid::new_v4(), + mtu: 1500, + port_settings_id: switch1_qsfp0_settings_id, + speed: LinkSpeed::Speed100G, + }], + port: SwitchPortConfig { + geometry: SwitchPortGeometry2::Qsfp28x1, + port_settings_id: switch1_qsfp0_settings_id, + }, + routes: Vec::new(), + settings: SwitchPortSettings { + description: String::from("default uplink 1 switch port settings"), + id: switch1_qsfp0_settings_id, + name: "default-uplink1".parse().unwrap(), + time_created: Utc.with_ymd_and_hms(2024, 7, 8, 9, 10, 11).unwrap(), + time_modified: Utc.with_ymd_and_hms(2024, 7, 8, 9, 10, 11).unwrap(), + }, + vlan_interfaces: Vec::new(), + }; + + let mock_switch0_qsfp0_settings_view = + server.networking_switch_port_settings_view(|when, then| { + when.port(&NameOrId::Id(ports.items[0].port_settings_id.unwrap())); + then.ok(&switch0_qsfp0_view); + }); + + let mock_switch1_qsfp0_settings_view = + server.networking_switch_port_settings_view(|when, then| { + when.port(&NameOrId::Id(ports.items[1].port_settings_id.unwrap())); + then.ok(&switch1_qsfp0_view); + }); + + env_logger::init(); + + Command::cargo_bin("oxide") + .unwrap() + .env("RUST_BACKTRACE", "1") + .env("OXIDE_HOST", server.url("")) + .env("OXIDE_TOKEN", "fake-token") + .arg("system") + .arg("networking") + .arg("switch-port-settings") + .arg("show") + .assert() + .success() + .stdout(expectorate::eq_file_or_panic( + "tests/data/test_switch_port_settings_show.stdout", + )); + + mock_ports.assert(); + mock_lots.assert(); + mock_lot_blocks.assert(); + mock_bgp_configs.assert_hits(2); + mock_switch0_qsfp0_settings_view.assert(); + mock_switch1_qsfp0_settings_view.assert(); +} diff --git a/oxide.json b/oxide.json index 589c3a22..51278f3f 100644 --- a/oxide.json +++ b/oxide.json @@ -6630,12 +6630,13 @@ } } }, - "post": { + "put": { "tags": [ "system/networking" ], - "summary": "Create new BGP announce set", - "operationId": "networking_bgp_announce_set_create", + "summary": "Update BGP announce set", + "description": "If the announce set exists, this endpoint replaces the existing announce set with the one specified.", + "operationId": "networking_bgp_announce_set_update", "requestBody": { "content": { "application/json": { @@ -19798,7 +19799,9 @@ "type": "string", "enum": [ "count", - "bytes" + "bytes", + "seconds", + "nanoseconds" ] }, "User": { diff --git a/sdk-httpmock/src/generated_httpmock.rs b/sdk-httpmock/src/generated_httpmock.rs index 55e8d69d..f674a2ee 100644 --- a/sdk-httpmock/src/generated_httpmock.rs +++ b/sdk-httpmock/src/generated_httpmock.rs @@ -10747,11 +10747,11 @@ pub mod operations { } } - pub struct NetworkingBgpAnnounceSetCreateWhen(httpmock::When); - impl NetworkingBgpAnnounceSetCreateWhen { + pub struct NetworkingBgpAnnounceSetUpdateWhen(httpmock::When); + impl NetworkingBgpAnnounceSetUpdateWhen { pub fn new(inner: httpmock::When) -> Self { Self( - inner.method(httpmock::Method::POST).path_matches( + inner.method(httpmock::Method::PUT).path_matches( regex::Regex::new("^/v1/system/networking/bgp-announce$").unwrap(), ), ) @@ -10766,8 +10766,8 @@ pub mod operations { } } - pub struct NetworkingBgpAnnounceSetCreateThen(httpmock::Then); - impl NetworkingBgpAnnounceSetCreateThen { + pub struct NetworkingBgpAnnounceSetUpdateThen(httpmock::Then); + impl NetworkingBgpAnnounceSetUpdateThen { pub fn new(inner: httpmock::Then) -> Self { Self(inner) } @@ -16163,11 +16163,11 @@ pub trait MockServerExt { operations::NetworkingBgpAnnounceSetListWhen, operations::NetworkingBgpAnnounceSetListThen, ); - fn networking_bgp_announce_set_create(&self, config_fn: F) -> httpmock::Mock + fn networking_bgp_announce_set_update(&self, config_fn: F) -> httpmock::Mock where F: FnOnce( - operations::NetworkingBgpAnnounceSetCreateWhen, - operations::NetworkingBgpAnnounceSetCreateThen, + operations::NetworkingBgpAnnounceSetUpdateWhen, + operations::NetworkingBgpAnnounceSetUpdateThen, ); fn networking_bgp_announce_set_delete(&self, config_fn: F) -> httpmock::Mock where @@ -18101,17 +18101,17 @@ impl MockServerExt for httpmock::MockServer { }) } - fn networking_bgp_announce_set_create(&self, config_fn: F) -> httpmock::Mock + fn networking_bgp_announce_set_update(&self, config_fn: F) -> httpmock::Mock where F: FnOnce( - operations::NetworkingBgpAnnounceSetCreateWhen, - operations::NetworkingBgpAnnounceSetCreateThen, + operations::NetworkingBgpAnnounceSetUpdateWhen, + operations::NetworkingBgpAnnounceSetUpdateThen, ), { self.mock(|when, then| { config_fn( - operations::NetworkingBgpAnnounceSetCreateWhen::new(when), - operations::NetworkingBgpAnnounceSetCreateThen::new(then), + operations::NetworkingBgpAnnounceSetUpdateWhen::new(when), + operations::NetworkingBgpAnnounceSetUpdateThen::new(then), ) }) } diff --git a/sdk/src/generated_sdk.rs b/sdk/src/generated_sdk.rs index b4ef1663..74e63699 100644 --- a/sdk/src/generated_sdk.rs +++ b/sdk/src/generated_sdk.rs @@ -23260,7 +23260,9 @@ pub mod types { /// "type": "string", /// "enum": [ /// "count", - /// "bytes" + /// "bytes", + /// "seconds", + /// "nanoseconds" /// ] /// } /// ``` @@ -23283,6 +23285,10 @@ pub mod types { Count, #[serde(rename = "bytes")] Bytes, + #[serde(rename = "seconds")] + Seconds, + #[serde(rename = "nanoseconds")] + Nanoseconds, } impl From<&Units> for Units { @@ -23296,6 +23302,8 @@ pub mod types { match *self { Self::Count => "count".to_string(), Self::Bytes => "bytes".to_string(), + Self::Seconds => "seconds".to_string(), + Self::Nanoseconds => "nanoseconds".to_string(), } } } @@ -23306,6 +23314,8 @@ pub mod types { match value { "count" => Ok(Self::Count), "bytes" => Ok(Self::Bytes), + "seconds" => Ok(Self::Seconds), + "nanoseconds" => Ok(Self::Nanoseconds), _ => Err("invalid value".into()), } } @@ -48499,17 +48509,20 @@ pub trait ClientSystemNetworkingExt { /// .await; /// ``` fn networking_bgp_announce_set_list(&self) -> builder::NetworkingBgpAnnounceSetList; - /// Create new BGP announce set + /// Update BGP announce set + /// + /// If the announce set exists, this endpoint replaces the existing announce + /// set with the one specified. /// - /// Sends a `POST` request to `/v1/system/networking/bgp-announce` + /// Sends a `PUT` request to `/v1/system/networking/bgp-announce` /// /// ```ignore - /// let response = client.networking_bgp_announce_set_create() + /// let response = client.networking_bgp_announce_set_update() /// .body(body) /// .send() /// .await; /// ``` - fn networking_bgp_announce_set_create(&self) -> builder::NetworkingBgpAnnounceSetCreate; + fn networking_bgp_announce_set_update(&self) -> builder::NetworkingBgpAnnounceSetUpdate; /// Delete BGP announce set /// /// Sends a `DELETE` request to `/v1/system/networking/bgp-announce` @@ -48798,8 +48811,8 @@ impl ClientSystemNetworkingExt for Client { builder::NetworkingBgpAnnounceSetList::new(self) } - fn networking_bgp_announce_set_create(&self) -> builder::NetworkingBgpAnnounceSetCreate { - builder::NetworkingBgpAnnounceSetCreate::new(self) + fn networking_bgp_announce_set_update(&self) -> builder::NetworkingBgpAnnounceSetUpdate { + builder::NetworkingBgpAnnounceSetUpdate::new(self) } fn networking_bgp_announce_set_delete(&self) -> builder::NetworkingBgpAnnounceSetDelete { @@ -63714,16 +63727,16 @@ pub mod builder { } /// Builder for - /// [`ClientSystemNetworkingExt::networking_bgp_announce_set_create`] + /// [`ClientSystemNetworkingExt::networking_bgp_announce_set_update`] /// - /// [`ClientSystemNetworkingExt::networking_bgp_announce_set_create`]: super::ClientSystemNetworkingExt::networking_bgp_announce_set_create + /// [`ClientSystemNetworkingExt::networking_bgp_announce_set_update`]: super::ClientSystemNetworkingExt::networking_bgp_announce_set_update #[derive(Debug, Clone)] - pub struct NetworkingBgpAnnounceSetCreate<'a> { + pub struct NetworkingBgpAnnounceSetUpdate<'a> { client: &'a super::Client, body: Result, } - impl<'a> NetworkingBgpAnnounceSetCreate<'a> { + impl<'a> NetworkingBgpAnnounceSetUpdate<'a> { pub fn new(client: &'a super::Client) -> Self { Self { client: client, @@ -63755,7 +63768,7 @@ pub mod builder { self } - /// Sends a `POST` request to `/v1/system/networking/bgp-announce` + /// Sends a `PUT` request to `/v1/system/networking/bgp-announce` pub async fn send( self, ) -> Result, Error> { @@ -63767,7 +63780,7 @@ pub mod builder { #[allow(unused_mut)] let mut request = client .client - .post(url) + .put(url) .header( reqwest::header::ACCEPT, reqwest::header::HeaderValue::from_static("application/json"),