diff --git a/Cargo.lock b/Cargo.lock index 4b7360ae8b..7db604dcb5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10298,6 +10298,20 @@ dependencies = [ "zone_cfg_derive", ] +[[package]] +name = "zone-network-setup" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap 4.4.3", + "dropshot", + "illumos-utils", + "omicron-common", + "omicron-workspace-hack", + "slog", + "tokio", +] + [[package]] name = "zone_cfg_derive" version = "0.3.0" diff --git a/Cargo.toml b/Cargo.toml index 238b9e36bf..515e767bfb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -69,6 +69,7 @@ members = [ "wicket", "wicketd", "workspace-hack", + "zone-network-setup", ] default-members = [ @@ -137,6 +138,7 @@ default-members = [ "wicket-dbg", "wicket", "wicketd", + "zone-network-setup", ] resolver = "2" @@ -167,7 +169,7 @@ chacha20poly1305 = "0.10.1" ciborium = "0.2.1" cfg-if = "1.0" chrono = { version = "0.4", features = [ "serde" ] } -clap = { version = "4.4", features = ["derive", "env", "wrap_help"] } +clap = { version = "4.4", features = ["cargo", "derive", "env", "wrap_help"] } cookie = "0.18" criterion = { version = "0.5.1", features = [ "async_tokio" ] } crossbeam = "0.8" diff --git a/illumos-utils/src/ipadm.rs b/illumos-utils/src/ipadm.rs new file mode 100644 index 0000000000..f4d884d452 --- /dev/null +++ b/illumos-utils/src/ipadm.rs @@ -0,0 +1,110 @@ +// 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/. + +//! Utilities for managing IP interfaces. + +use crate::zone::IPADM; +use crate::{execute, ExecutionError, PFEXEC}; +use std::net::Ipv6Addr; + +/// Wraps commands for interacting with interfaces. +pub struct Ipadm {} + +#[cfg_attr(any(test, feature = "testing"), mockall::automock)] +impl Ipadm { + // Remove current IP interface and create a new temporary one. + pub fn set_temp_interface_for_datalink( + datalink: &str, + ) -> Result<(), ExecutionError> { + let mut cmd = std::process::Command::new(PFEXEC); + let cmd = cmd.args(&[IPADM, "delete-if", datalink]); + // First we remove IP interface if it already exists. If it doesn't + // exist and the command returns an error we continue anyway as + // the next step is to create it. + let _ = execute(cmd); + + let mut cmd = std::process::Command::new(PFEXEC); + let cmd = cmd.args(&[IPADM, "create-if", "-t", datalink]); + execute(cmd)?; + Ok(()) + } + + // Set MTU to 9000 on both IPv4 and IPv6 + pub fn set_interface_mtu(datalink: &str) -> Result<(), ExecutionError> { + let mut cmd = std::process::Command::new(PFEXEC); + let cmd = cmd.args(&[ + IPADM, + "set-ifprop", + "-t", + "-p", + "mtu=9000", + "-m", + "ipv4", + datalink, + ]); + execute(cmd)?; + + let mut cmd = std::process::Command::new(PFEXEC); + let cmd = cmd.args(&[ + IPADM, + "set-ifprop", + "-t", + "-p", + "mtu=9000", + "-m", + "ipv6", + datalink, + ]); + execute(cmd)?; + Ok(()) + } + + pub fn create_static_and_autoconfigured_addrs( + datalink: &str, + listen_addr: &Ipv6Addr, + ) -> Result<(), ExecutionError> { + // Create auto-configured address on the IP interface if it doesn't already exist + let addrobj = format!("{}/ll", datalink); + let mut cmd = std::process::Command::new(PFEXEC); + let cmd = cmd.args(&[IPADM, "show-addr", &addrobj]); + match execute(cmd) { + Err(_) => { + let mut cmd = std::process::Command::new(PFEXEC); + let cmd = cmd.args(&[ + IPADM, + "create-addr", + "-t", + "-T", + "addrconf", + &addrobj, + ]); + execute(cmd)?; + } + Ok(_) => (), + }; + + // Create static address on the IP interface if it doesn't already exist + let addrobj = format!("{}/omicron6", datalink); + let mut cmd = std::process::Command::new(PFEXEC); + let cmd = cmd.args(&[IPADM, "show-addr", &addrobj]); + match execute(cmd) { + Err(_) => { + let mut cmd = std::process::Command::new(PFEXEC); + let cmd = cmd.args(&[ + IPADM, + "create-addr", + "-t", + "-T", + "static", + "-a", + &listen_addr.to_string(), + &addrobj, + ]); + execute(cmd)?; + } + Ok(_) => (), + }; + Ok(()) + } +} diff --git a/illumos-utils/src/lib.rs b/illumos-utils/src/lib.rs index 1faa4c5c37..550170b0f2 100644 --- a/illumos-utils/src/lib.rs +++ b/illumos-utils/src/lib.rs @@ -16,9 +16,11 @@ pub mod dkio; pub mod dladm; pub mod dumpadm; pub mod fstyp; +pub mod ipadm; pub mod libc; pub mod link; pub mod opte; +pub mod route; pub mod running_zone; pub mod scf; pub mod svc; @@ -70,7 +72,7 @@ pub enum ExecutionError { mod inner { use super::*; - fn to_string(command: &mut std::process::Command) -> String { + pub fn to_string(command: &mut std::process::Command) -> String { command .get_args() .map(|s| s.to_string_lossy().into()) diff --git a/illumos-utils/src/route.rs b/illumos-utils/src/route.rs new file mode 100644 index 0000000000..2b6af9a9fd --- /dev/null +++ b/illumos-utils/src/route.rs @@ -0,0 +1,59 @@ +// 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/. + +//! Utilities for manipulating the routing tables. + +use crate::zone::ROUTE; +use crate::{execute, inner, output_to_exec_error, ExecutionError, PFEXEC}; +use libc::ESRCH; +use std::net::Ipv6Addr; + +/// Wraps commands for interacting with routing tables. +pub struct Route {} + +#[cfg_attr(any(test, feature = "testing"), mockall::automock)] +impl Route { + pub fn ensure_default_route_with_gateway( + gateway: &Ipv6Addr, + ) -> Result<(), ExecutionError> { + // Add the desired route if it doesn't already exist + let destination = "default"; + let mut cmd = std::process::Command::new(PFEXEC); + let cmd = cmd.args(&[ + ROUTE, + "-n", + "get", + "-inet6", + destination, + "-inet6", + &gateway.to_string(), + ]); + + let out = + cmd.output().map_err(|err| ExecutionError::ExecutionStart { + command: inner::to_string(cmd), + err, + })?; + match out.status.code() { + Some(0) => (), + // If the entry is not found in the table, + // the exit status of the command will be 3 (ESRCH). + // When that is the case, we'll add the route. + Some(ESRCH) => { + let mut cmd = std::process::Command::new(PFEXEC); + let cmd = cmd.args(&[ + ROUTE, + "add", + "-inet6", + destination, + "-inet6", + &gateway.to_string(), + ]); + execute(cmd)?; + } + Some(_) | None => return Err(output_to_exec_error(cmd, &out)), + }; + Ok(()) + } +} diff --git a/illumos-utils/src/zone.rs b/illumos-utils/src/zone.rs index a3f73b3954..3f749fc352 100644 --- a/illumos-utils/src/zone.rs +++ b/illumos-utils/src/zone.rs @@ -22,6 +22,7 @@ pub const IPADM: &str = "/usr/sbin/ipadm"; pub const SVCADM: &str = "/usr/sbin/svcadm"; pub const SVCCFG: &str = "/usr/sbin/svccfg"; pub const ZLOGIN: &str = "/usr/sbin/zlogin"; +pub const ROUTE: &str = "/usr/sbin/route"; // TODO: These could become enums pub const ZONE_PREFIX: &str = "oxz_"; diff --git a/package-manifest.toml b/package-manifest.toml index 406b53c97e..16f8f70c73 100644 --- a/package-manifest.toml +++ b/package-manifest.toml @@ -132,7 +132,7 @@ output.type = "zone" service_name = "clickhouse" only_for_targets.image = "standard" source.type = "composite" -source.packages = [ "clickhouse_svc.tar.gz", "internal-dns-cli.tar.gz" ] +source.packages = [ "clickhouse_svc.tar.gz", "internal-dns-cli.tar.gz", "zone-network-setup.tar.gz" ] output.type = "zone" [package.clickhouse_svc] @@ -153,7 +153,7 @@ setup_hint = "Run `./tools/ci_download_clickhouse` to download the necessary bin service_name = "clickhouse_keeper" only_for_targets.image = "standard" source.type = "composite" -source.packages = [ "clickhouse_keeper_svc.tar.gz", "internal-dns-cli.tar.gz" ] +source.packages = [ "clickhouse_keeper_svc.tar.gz", "internal-dns-cli.tar.gz", "zone-network-setup.tar.gz" ] output.type = "zone" [package.clickhouse_keeper_svc] @@ -174,7 +174,7 @@ setup_hint = "Run `./tools/ci_download_clickhouse` to download the necessary bin service_name = "cockroachdb" only_for_targets.image = "standard" source.type = "composite" -source.packages = [ "cockroachdb-service.tar.gz", "internal-dns-cli.tar.gz" ] +source.packages = [ "cockroachdb-service.tar.gz", "internal-dns-cli.tar.gz", "zone-network-setup.tar.gz" ] output.type = "zone" [package.cockroachdb-service] @@ -613,3 +613,15 @@ source.packages = [ "sp-sim-softnpu.tar.gz" ] output.type = "zone" + +[package.zone-network-setup] +service_name = "zone-network-setup" +only_for_targets.image = "standard" +source.type = "local" +source.rust.binary_names = ["zone-networking"] +source.rust.release = true +source.paths = [ + { from = "smf/zone-network-setup/manifest.xml", to = "/var/svc/manifest/site/zone-network-setup/manifest.xml" }, +] +output.type = "zone" +output.intermediate_only = true diff --git a/sled-agent/src/services.rs b/sled-agent/src/services.rs index e240fb4d03..adabe80807 100644 --- a/sled-agent/src/services.rs +++ b/sled-agent/src/services.rs @@ -1419,6 +1419,25 @@ impl ServiceManager { .add_instance(ServiceInstanceBuilder::new("default"))) } + fn zone_network_setup_install( + info: &SledAgentInfo, + zone: &InstalledZone, + static_addr: &String, + ) -> Result { + let datalink = zone.get_control_vnic_name(); + let gateway = &info.underlay_address.to_string(); + + let mut config_builder = PropertyGroupBuilder::new("config"); + config_builder = config_builder + .add_property("datalink", "astring", datalink) + .add_property("gateway", "astring", gateway) + .add_property("static_addr", "astring", static_addr); + + Ok(ServiceBuilder::new("oxide/zone-network-setup") + .add_property_group(config_builder) + .add_instance(ServiceInstanceBuilder::new("default"))) + } + async fn initialize_zone( &self, request: ZoneArgs<'_>, @@ -1532,16 +1551,18 @@ impl ServiceManager { return Err(Error::SledAgentNotReady); }; - let dns_service = Self::dns_install(info).await?; - - let datalink = installed_zone.get_control_vnic_name(); - let gateway = &info.underlay_address.to_string(); let listen_addr = &underlay_address.to_string(); let listen_port = &CLICKHOUSE_PORT.to_string(); + let nw_setup_service = Self::zone_network_setup_install( + info, + &installed_zone, + listen_addr, + )?; + + let dns_service = Self::dns_install(info).await?; + let config = PropertyGroupBuilder::new("config") - .add_property("datalink", "astring", datalink) - .add_property("gateway", "astring", gateway) .add_property("listen_addr", "astring", listen_addr) .add_property("listen_port", "astring", listen_port) .add_property("store", "astring", "/data"); @@ -1552,6 +1573,7 @@ impl ServiceManager { ); let profile = ProfileBuilder::new("omicron") + .add_service(nw_setup_service) .add_service(disabled_ssh_service) .add_service(clickhouse_service) .add_service(dns_service); @@ -1577,16 +1599,18 @@ impl ServiceManager { return Err(Error::SledAgentNotReady); }; - let dns_service = Self::dns_install(info).await?; - - let datalink = installed_zone.get_control_vnic_name(); - let gateway = &info.underlay_address.to_string(); let listen_addr = &underlay_address.to_string(); let listen_port = &CLICKHOUSE_KEEPER_PORT.to_string(); + let nw_setup_service = Self::zone_network_setup_install( + info, + &installed_zone, + listen_addr, + )?; + + let dns_service = Self::dns_install(info).await?; + let config = PropertyGroupBuilder::new("config") - .add_property("datalink", "astring", datalink) - .add_property("gateway", "astring", gateway) .add_property("listen_addr", "astring", listen_addr) .add_property("listen_port", "astring", listen_port) .add_property("store", "astring", "/data"); @@ -1597,6 +1621,7 @@ impl ServiceManager { .add_property_group(config), ); let profile = ProfileBuilder::new("omicron") + .add_service(nw_setup_service) .add_service(disabled_ssh_service) .add_service(clickhouse_keeper_service) .add_service(dns_service); @@ -1625,11 +1650,6 @@ impl ServiceManager { return Err(Error::SledAgentNotReady); }; - let dns_service = Self::dns_install(info).await?; - - // Configure the CockroachDB service. - let datalink = installed_zone.get_control_vnic_name(); - let gateway = &info.underlay_address.to_string(); let address = SocketAddr::new( IpAddr::V6(*underlay_address), COCKROACH_PORT, @@ -1637,9 +1657,16 @@ impl ServiceManager { let listen_addr = &address.ip().to_string(); let listen_port = &address.port().to_string(); + let nw_setup_service = Self::zone_network_setup_install( + info, + &installed_zone, + listen_addr, + )?; + + let dns_service = Self::dns_install(info).await?; + + // Configure the CockroachDB service. let cockroachdb_config = PropertyGroupBuilder::new("config") - .add_property("datalink", "astring", datalink) - .add_property("gateway", "astring", gateway) .add_property("listen_addr", "astring", listen_addr) .add_property("listen_port", "astring", listen_port) .add_property("store", "astring", "/data"); @@ -1650,6 +1677,7 @@ impl ServiceManager { ); let profile = ProfileBuilder::new("omicron") + .add_service(nw_setup_service) .add_service(disabled_ssh_service) .add_service(cockroachdb_service) .add_service(dns_service); diff --git a/smf/clickhouse/manifest.xml b/smf/clickhouse/manifest.xml index bf8d0d7e8a..5d227f1b28 100644 --- a/smf/clickhouse/manifest.xml +++ b/smf/clickhouse/manifest.xml @@ -11,14 +11,17 @@ + + + + - - diff --git a/smf/clickhouse/method_script.sh b/smf/clickhouse/method_script.sh index 3cc8c585ad..224d759cf3 100755 --- a/smf/clickhouse/method_script.sh +++ b/smf/clickhouse/method_script.sh @@ -9,24 +9,6 @@ set -o pipefail LISTEN_ADDR="$(svcprop -c -p config/listen_addr "${SMF_FMRI}")" LISTEN_PORT="$(svcprop -c -p config/listen_port "${SMF_FMRI}")" DATASTORE="$(svcprop -c -p config/store "${SMF_FMRI}")" -DATALINK="$(svcprop -c -p config/datalink "${SMF_FMRI}")" -GATEWAY="$(svcprop -c -p config/gateway "${SMF_FMRI}")" - -if [[ $DATALINK == unknown ]] || [[ $GATEWAY == unknown ]]; then - printf 'ERROR: missing datalink or gateway\n' >&2 - exit "$SMF_EXIT_ERR_CONFIG" -fi - -# TODO remove when https://github.com/oxidecomputer/stlouis/issues/435 is addressed -ipadm delete-if "$DATALINK" || true -ipadm create-if -t "$DATALINK" - -ipadm set-ifprop -t -p mtu=9000 -m ipv4 "$DATALINK" -ipadm set-ifprop -t -p mtu=9000 -m ipv6 "$DATALINK" - -ipadm show-addr "$DATALINK/ll" || ipadm create-addr -t -T addrconf "$DATALINK/ll" -ipadm show-addr "$DATALINK/omicron6" || ipadm create-addr -t -T static -a "$LISTEN_ADDR" "$DATALINK/omicron6" -route get -inet6 default -inet6 "$GATEWAY" || route add -inet6 default -inet6 "$GATEWAY" # TEMPORARY: Racks will be set up with single node ClickHouse until # Nexus provisions services so there is no divergence between racks diff --git a/smf/clickhouse_keeper/manifest.xml b/smf/clickhouse_keeper/manifest.xml index 9e79cc131c..fc11e3dfd5 100644 --- a/smf/clickhouse_keeper/manifest.xml +++ b/smf/clickhouse_keeper/manifest.xml @@ -11,14 +11,17 @@ + + + + - - diff --git a/smf/clickhouse_keeper/method_script.sh b/smf/clickhouse_keeper/method_script.sh index 0e785f2aec..8499e0001f 100755 --- a/smf/clickhouse_keeper/method_script.sh +++ b/smf/clickhouse_keeper/method_script.sh @@ -9,24 +9,6 @@ set -o pipefail LISTEN_ADDR="$(svcprop -c -p config/listen_addr "${SMF_FMRI}")" LISTEN_PORT="$(svcprop -c -p config/listen_port "${SMF_FMRI}")" DATASTORE="$(svcprop -c -p config/store "${SMF_FMRI}")" -DATALINK="$(svcprop -c -p config/datalink "${SMF_FMRI}")" -GATEWAY="$(svcprop -c -p config/gateway "${SMF_FMRI}")" - -if [[ $DATALINK == unknown ]] || [[ $GATEWAY == unknown ]]; then - printf 'ERROR: missing datalink or gateway\n' >&2 - exit "$SMF_EXIT_ERR_CONFIG" -fi - -# TODO remove when https://github.com/oxidecomputer/stlouis/issues/435 is addressed -ipadm delete-if "$DATALINK" || true -ipadm create-if -t "$DATALINK" - -ipadm set-ifprop -t -p mtu=9000 -m ipv4 "$DATALINK" -ipadm set-ifprop -t -p mtu=9000 -m ipv6 "$DATALINK" - -ipadm show-addr "$DATALINK/ll" || ipadm create-addr -t -T addrconf "$DATALINK/ll" -ipadm show-addr "$DATALINK/omicron6" || ipadm create-addr -t -T static -a "$LISTEN_ADDR" "$DATALINK/omicron6" -route get -inet6 default -inet6 "$GATEWAY" || route add -inet6 default -inet6 "$GATEWAY" # Retrieve hostnames (SRV records in internal DNS) of all keeper nodes. K_ADDRS="$(/opt/oxide/internal-dns-cli/bin/dnswait clickhouse-keeper -H)" diff --git a/smf/cockroachdb/manifest.xml b/smf/cockroachdb/manifest.xml index b4e69f6376..3a9b1a7cb8 100644 --- a/smf/cockroachdb/manifest.xml +++ b/smf/cockroachdb/manifest.xml @@ -11,6 +11,11 @@ + + + + @@ -23,8 +28,6 @@ - - diff --git a/smf/cockroachdb/method_script.sh b/smf/cockroachdb/method_script.sh index e5ab4e8eaa..e8b02eb1eb 100755 --- a/smf/cockroachdb/method_script.sh +++ b/smf/cockroachdb/method_script.sh @@ -9,24 +9,6 @@ set -o pipefail LISTEN_ADDR="$(svcprop -c -p config/listen_addr "${SMF_FMRI}")" LISTEN_PORT="$(svcprop -c -p config/listen_port "${SMF_FMRI}")" DATASTORE="$(svcprop -c -p config/store "${SMF_FMRI}")" -DATALINK="$(svcprop -c -p config/datalink "${SMF_FMRI}")" -GATEWAY="$(svcprop -c -p config/gateway "${SMF_FMRI}")" - -if [[ $DATALINK == unknown ]] || [[ $GATEWAY == unknown ]]; then - printf 'ERROR: missing datalink or gateway\n' >&2 - exit "$SMF_EXIT_ERR_CONFIG" -fi - -# TODO remove when https://github.com/oxidecomputer/stlouis/issues/435 is addressed -ipadm delete-if "$DATALINK" || true -ipadm create-if -t "$DATALINK" - -ipadm set-ifprop -t -p mtu=9000 -m ipv4 "$DATALINK" -ipadm set-ifprop -t -p mtu=9000 -m ipv6 "$DATALINK" - -ipadm show-addr "$DATALINK/ll" || ipadm create-addr -t -T addrconf "$DATALINK/ll" -ipadm show-addr "$DATALINK/omicron6" || ipadm create-addr -t -T static -a "$LISTEN_ADDR" "$DATALINK/omicron6" -route get -inet6 default -inet6 "$GATEWAY" || route add -inet6 default -inet6 "$GATEWAY" # We need to tell CockroachDB the DNS names or IP addresses of the other nodes # in the cluster. Look these up in internal DNS. Per the recommendations in diff --git a/smf/zone-network-setup/manifest.xml b/smf/zone-network-setup/manifest.xml new file mode 100644 index 0000000000..0776329749 --- /dev/null +++ b/smf/zone-network-setup/manifest.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/workspace-hack/Cargo.toml b/workspace-hack/Cargo.toml index 4eda5c1af4..0240b45f90 100644 --- a/workspace-hack/Cargo.toml +++ b/workspace-hack/Cargo.toml @@ -27,8 +27,8 @@ byteorder = { version = "1.5.0" } bytes = { version = "1.5.0", features = ["serde"] } chrono = { version = "0.4.31", features = ["alloc", "serde"] } cipher = { version = "0.4.4", default-features = false, features = ["block-padding", "zeroize"] } -clap = { version = "4.4.3", features = ["derive", "env", "wrap_help"] } -clap_builder = { version = "4.4.2", default-features = false, features = ["color", "env", "std", "suggestions", "usage", "wrap_help"] } +clap = { version = "4.4.3", features = ["cargo", "derive", "env", "wrap_help"] } +clap_builder = { version = "4.4.2", default-features = false, features = ["cargo", "color", "env", "std", "suggestions", "usage", "wrap_help"] } console = { version = "0.15.8" } const-oid = { version = "0.9.5", default-features = false, features = ["db", "std"] } crossbeam-epoch = { version = "0.9.15" } @@ -130,8 +130,8 @@ byteorder = { version = "1.5.0" } bytes = { version = "1.5.0", features = ["serde"] } chrono = { version = "0.4.31", features = ["alloc", "serde"] } cipher = { version = "0.4.4", default-features = false, features = ["block-padding", "zeroize"] } -clap = { version = "4.4.3", features = ["derive", "env", "wrap_help"] } -clap_builder = { version = "4.4.2", default-features = false, features = ["color", "env", "std", "suggestions", "usage", "wrap_help"] } +clap = { version = "4.4.3", features = ["cargo", "derive", "env", "wrap_help"] } +clap_builder = { version = "4.4.2", default-features = false, features = ["cargo", "color", "env", "std", "suggestions", "usage", "wrap_help"] } console = { version = "0.15.8" } const-oid = { version = "0.9.5", default-features = false, features = ["db", "std"] } crossbeam-epoch = { version = "0.9.15" } diff --git a/zone-network-setup/Cargo.toml b/zone-network-setup/Cargo.toml new file mode 100644 index 0000000000..10eec5c554 --- /dev/null +++ b/zone-network-setup/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "zone-network-setup" +version = "0.1.0" +edition = "2021" +license = "MPL-2.0" + +[dependencies] +anyhow.workspace = true +clap.workspace = true +illumos-utils.workspace = true +omicron-common.workspace = true +slog.workspace = true +dropshot.workspace = true +tokio.workspace = true +omicron-workspace-hack.workspace = true diff --git a/zone-network-setup/src/bin/zone-networking.rs b/zone-network-setup/src/bin/zone-networking.rs new file mode 100644 index 0000000000..b955ca856a --- /dev/null +++ b/zone-network-setup/src/bin/zone-networking.rs @@ -0,0 +1,92 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! CLI to set up zone networking + +use anyhow::anyhow; +use clap::{arg, command}; +use illumos_utils::ipadm::Ipadm; +use illumos_utils::route::Route; +use omicron_common::cmd::fatal; +use omicron_common::cmd::CmdError; +use slog::info; +use std::net::Ipv6Addr; + +fn parse_ipv6(s: &str) -> anyhow::Result { + if s == "unknown" { + return Err(anyhow!("ERROR: Missing input value")); + }; + s.parse().map_err(|_| anyhow!("ERROR: Invalid IPv6 address")) +} + +fn parse_datalink(s: &str) -> anyhow::Result { + if s == "unknown" { + return Err(anyhow!("ERROR: Missing data link")); + }; + s.parse().map_err(|_| anyhow!("ERROR: Invalid data link")) +} + +#[tokio::main] +async fn main() { + if let Err(message) = do_run().await { + fatal(message); + } +} + +async fn do_run() -> Result<(), CmdError> { + let log = dropshot::ConfigLogging::File { + path: "/dev/stderr".into(), + level: dropshot::ConfigLoggingLevel::Info, + if_exists: dropshot::ConfigLoggingIfExists::Append, + } + .to_logger("zone-networking") + .map_err(|err| CmdError::Failure(anyhow!(err)))?; + + let matches = command!() + .arg( + arg!( + -d --datalink "datalink" + ) + .required(true) + .value_parser(parse_datalink), + ) + .arg( + arg!( + -g --gateway "gateway" + ) + .required(true) + .value_parser(parse_ipv6), + ) + .arg( + arg!( + -s --static_addr "static_addr" + ) + .required(true) + .value_parser(parse_ipv6), + ) + .get_matches(); + + let datalink: &String = matches.get_one("datalink").unwrap(); + let static_addr: &Ipv6Addr = matches.get_one("static_addr").unwrap(); + let gateway: &Ipv6Addr = matches.get_one("gateway").unwrap(); + + // TODO: remove when https://github.com/oxidecomputer/stlouis/issues/435 is addressed + info!(&log, "Ensuring a temporary IP interface is created"; "data link" => ?datalink); + Ipadm::set_temp_interface_for_datalink(&datalink) + .map_err(|err| CmdError::Failure(anyhow!(err)))?; + + info!(&log, "Setting MTU to 9000 for IPv6 and IPv4"; "data link" => ?datalink); + Ipadm::set_interface_mtu(&datalink) + .map_err(|err| CmdError::Failure(anyhow!(err)))?; + + info!(&log, "Ensuring static and auto-configured addresses are set on the IP interface"; "data link" => ?datalink, "static address" => ?static_addr); + Ipadm::create_static_and_autoconfigured_addrs(&datalink, static_addr) + .map_err(|err| CmdError::Failure(anyhow!(err)))?; + + info!(&log, "Ensuring there is a default route"; "gateway" => ?gateway); + Route::ensure_default_route_with_gateway(gateway) + .map_err(|err| CmdError::Failure(anyhow!(err)))?; + + Ok(()) +}