From a796560cf630b9eef6197c7ab80f5743051714c2 Mon Sep 17 00:00:00 2001 From: Levon Tarver <11586085+internet-diglett@users.noreply.github.com> Date: Wed, 22 Mar 2023 15:05:40 -0500 Subject: [PATCH] Integrate SoftNPU as virtual hardware (#2089) * Automate creation of `softnpu` zone * Automate startup up `softnpu` * Automate configuration of `softnpu` * Enable communication between `switch` zone and `softnpu` zone * Package `dendrite` with `softnpu` feature * When `dendrite` asic type is `Softnpu`, have `dendrite` configure switch via `softnpu` socketfile * Configure nat entries via `dendrite` upon guest VM creation * Cleanup nat entries upon guest VM deletion * Disable OPTE hack Closes #1465 Closes https://github.com/oxidecomputer/opte/issues/236 --------- Co-authored-by: Ryan Goodfellow --- .../buildomat/jobs/build-and-test-linux.sh | 6 +- .github/buildomat/jobs/build-and-test.sh | 6 +- .../buildomat/jobs/build-end-to-end-tests.sh | 1 - .github/buildomat/jobs/clippy.sh | 1 - .github/buildomat/jobs/deploy.sh | 26 +- .github/buildomat/jobs/package.sh | 7 +- .gitignore | 2 + Cargo.lock | 20 ++ Cargo.toml | 3 + README.adoc | 12 + common/src/nexus_config.rs | 20 +- docs/boundary-services-a-to-z.adoc | 135 +++++++++ docs/how-to-run.adoc | 50 ++-- docs/plumbing.png | Bin 0 -> 126393 bytes dpd-client/Cargo.toml | 20 ++ dpd-client/build.rs | 103 +++++++ dpd-client/src/lib.rs | 25 ++ illumos-utils/src/opte/illumos/mod.rs | 45 --- .../src/opte/illumos/port_manager.rs | 141 +++------- .../src/opte/non_illumos/port_manager.rs | 11 - illumos-utils/src/running_zone.rs | 2 + illumos-utils/src/zone.rs | 4 + nexus/Cargo.toml | 1 + nexus/examples/config.toml | 4 + nexus/src/app/mod.rs | 18 ++ nexus/src/app/sagas/instance_create.rs | 262 ++++++++++++++++++ nexus/src/app/sagas/instance_delete.rs | 81 ++++++ nexus/tests/config.test.toml | 4 + package-manifest.toml | 43 ++- package/src/bin/omicron-package.rs | 10 +- package/src/target.rs | 2 + sled-agent/src/bootstrap/hardware.rs | 4 +- sled-agent/src/config.rs | 6 +- sled-agent/src/instance.rs | 6 +- sled-agent/src/instance_manager.rs | 6 - sled-agent/src/params.rs | 2 +- sled-agent/src/services.rs | 106 +++++-- sled-agent/src/sled_agent.rs | 12 +- sled-agent/src/storage_manager.rs | 1 + sled-hardware/Cargo.toml | 1 + sled-hardware/src/illumos/mod.rs | 32 ++- sled-hardware/src/lib.rs | 15 + sled-hardware/src/non_illumos/mod.rs | 5 +- smf/nexus/config-partial.toml | 6 +- smf/sled-agent/non-gimlet/config.toml | 9 +- tools/ci_download_dendrite_openapi | 86 ++++++ tools/create_virtual_hardware.sh | 82 +++++- tools/dendrite_openapi_version | 2 + tools/destroy_virtual_hardware.sh | 31 ++- tools/install_builder_prerequisites.sh | 4 + tools/install_softnpu_machinery.sh | 40 +++ tools/scrimlet/create-softnpu-zone.sh | 22 ++ tools/scrimlet/destroy-softnpu-zone.sh | 12 + tools/scrimlet/softnpu-init.sh | 37 +++ tools/scrimlet/softnpu-zone.txt | 23 ++ tools/scrimlet/softnpu.toml | 5 + tools/setup_path.sh | 4 + 57 files changed, 1347 insertions(+), 277 deletions(-) create mode 100644 docs/boundary-services-a-to-z.adoc create mode 100644 docs/plumbing.png create mode 100644 dpd-client/Cargo.toml create mode 100644 dpd-client/build.rs create mode 100644 dpd-client/src/lib.rs create mode 100755 tools/ci_download_dendrite_openapi create mode 100644 tools/dendrite_openapi_version create mode 100755 tools/install_softnpu_machinery.sh create mode 100755 tools/scrimlet/create-softnpu-zone.sh create mode 100755 tools/scrimlet/destroy-softnpu-zone.sh create mode 100755 tools/scrimlet/softnpu-init.sh create mode 100644 tools/scrimlet/softnpu-zone.txt create mode 100644 tools/scrimlet/softnpu.toml create mode 100644 tools/setup_path.sh diff --git a/.github/buildomat/jobs/build-and-test-linux.sh b/.github/buildomat/jobs/build-and-test-linux.sh index b0a49ce641..4719c087d6 100644 --- a/.github/buildomat/jobs/build-and-test-linux.sh +++ b/.github/buildomat/jobs/build-and-test-linux.sh @@ -9,7 +9,6 @@ #: "!/var/tmp/omicron_tmp/crdb-base*", #: "!/var/tmp/omicron_tmp/rustc*", #: ] -#: set -o errexit set -o pipefail @@ -62,6 +61,11 @@ ptime -m cargo build --locked --all-targets --verbose # We also don't use `--workspace` here because we're not prepared to run tests # from end-to-end-tests. # + +# TODO: we are bypassing calls to DPD during these tests for now. This env var +# can be removed once we have dpd-stub added to the test setup +export SKIP_ASIC_CONFIG=1 + banner test ptime -m cargo test --locked --verbose --no-fail-fast diff --git a/.github/buildomat/jobs/build-and-test.sh b/.github/buildomat/jobs/build-and-test.sh index 4189a535a3..00f21cd322 100644 --- a/.github/buildomat/jobs/build-and-test.sh +++ b/.github/buildomat/jobs/build-and-test.sh @@ -9,7 +9,6 @@ #: "!/var/tmp/omicron_tmp/crdb-base*", #: "!/var/tmp/omicron_tmp/rustc*", #: ] -#: set -o errexit set -o pipefail @@ -62,6 +61,11 @@ ptime -m cargo build --locked --all-targets --verbose # We also don't use `--workspace` here because we're not prepared to run tests # from end-to-end-tests. # + +# TODO: we are bypassing calls to DPD during these tests for now. This env var +# can be removed once we have dpd-stub added to the test setup +export SKIP_ASIC_CONFIG=1 + banner test ptime -m cargo test --locked --verbose --no-fail-fast diff --git a/.github/buildomat/jobs/build-end-to-end-tests.sh b/.github/buildomat/jobs/build-end-to-end-tests.sh index c1ed86919c..20357a9d1b 100644 --- a/.github/buildomat/jobs/build-end-to-end-tests.sh +++ b/.github/buildomat/jobs/build-end-to-end-tests.sh @@ -7,7 +7,6 @@ #: output_rules = [ #: "=/work/*.gz", #: ] -#: set -o errexit set -o pipefail diff --git a/.github/buildomat/jobs/clippy.sh b/.github/buildomat/jobs/clippy.sh index d9e135d1dc..90d64dac9d 100644 --- a/.github/buildomat/jobs/clippy.sh +++ b/.github/buildomat/jobs/clippy.sh @@ -5,7 +5,6 @@ #: target = "helios-latest" #: rust_toolchain = "1.66.1" #: output_rules = [] -#: # Run clippy on illumos (not just other systems) because a bunch of our code # (that we want to check) is conditionally-compiled on illumos only. diff --git a/.github/buildomat/jobs/deploy.sh b/.github/buildomat/jobs/deploy.sh index 3a39772239..7a7a601ffa 100644 --- a/.github/buildomat/jobs/deploy.sh +++ b/.github/buildomat/jobs/deploy.sh @@ -15,7 +15,6 @@ #: #: [dependencies.build-end-to-end-tests] #: job = "helios / build-end-to-end-tests" -#: set -o errexit set -o pipefail @@ -39,6 +38,12 @@ _exit_trap() { pfexec netstat -rncva pfexec netstat -anu pfexec arp -an + pfexec ./out/softnpu/scadm \ + --server /opt/oxide/softnpu/stuff/server \ + --client /opt/oxide/softnpu/stuff/client \ + standalone \ + dump-state + pfexec zfs list pfexec zpool list pfexec fmdump -eVp @@ -167,6 +172,25 @@ OMICRON_NO_UNINSTALL=1 \ ptime -m pfexec ./target/release/omicron-package -t test install ./tests/bootstrap + +# NOTE: this script configures softnpu's "rack network" settings using swadm +GATEWAY_IP=192.168.1.199 ./tools/scrimlet/softnpu-init.sh + +# NOTE: this command configures proxy arp for softnpu. This is needed if you want to be +# able to reach instances from the same L2 network segment. +# /out/softnpu/scadm standalone add-proxy-arp 192.168.1.50 192.168.1.90 a8:e1:de:01:70:1d +pfexec ./out/softnpu/scadm \ + --server /opt/oxide/softnpu/stuff/server \ + --client /opt/oxide/softnpu/stuff/client \ + standalone \ + add-proxy-arp 192.168.1.50 192.168.1.90 a8:e1:de:01:70:1d + +pfexec ./out/softnpu/scadm \ + --server /opt/oxide/softnpu/stuff/server \ + --client /opt/oxide/softnpu/stuff/client \ + standalone \ + dump-state + rm ./tests/bootstrap for test_bin in tests/*; do ./"$test_bin" diff --git a/.github/buildomat/jobs/package.sh b/.github/buildomat/jobs/package.sh index cff631e8a1..51eab8e0c2 100755 --- a/.github/buildomat/jobs/package.sh +++ b/.github/buildomat/jobs/package.sh @@ -29,10 +29,11 @@ cargo --version rustc --version ptime -m ./tools/install_builder_prerequisites.sh -yp +ptime -m ./tools/install_softnpu_machinery.sh # Build the test target ptime -m cargo run --locked --release --bin omicron-package -- \ - -t test target create -i standard -m non-gimlet -s stub + -t test target create -i standard -m non-gimlet -s softnpu ptime -m cargo run --locked --release --bin omicron-package -- \ -t test package @@ -44,10 +45,12 @@ tarball_src_dir="$(pwd)/out" files=( out/*.tar out/target/test + out/softnpu/* package-manifest.toml smf/sled-agent/non-gimlet/config.toml target/release/omicron-package tools/create_virtual_hardware.sh + tools/scrimlet/* ) pfexec mkdir -p /work && pfexec chown $USER /work @@ -105,7 +108,7 @@ zones=( out/oximeter-collector.tar.gz out/propolis-server.tar.gz out/switch-asic.tar.gz - out/switch-stub.tar.gz + out/switch-softnpu.tar.gz ) cp "${zones[@]}" /work/zones/ diff --git a/.gitignore b/.gitignore index 746b3100b5..3d47743f80 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,5 @@ tools/cockroach* /cockroachdb/ smf/nexus/root.json core +*.vdev +debug.out diff --git a/Cargo.lock b/Cargo.lock index eba3e1c963..d122ea9063 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1685,6 +1685,24 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" +[[package]] +name = "dpd-client" +version = "0.1.0" +dependencies = [ + "anyhow", + "futures", + "omicron-zone-package", + "progenitor", + "progenitor-client", + "quote", + "regress", + "reqwest", + "serde", + "serde_json", + "slog", + "toml 0.7.2", +] + [[package]] name = "dropshot" version = "0.9.1-dev" @@ -3975,6 +3993,7 @@ dependencies = [ "diesel", "diesel-dtrace", "dns-service-client", + "dpd-client", "dropshot", "expectorate", "fatfs", @@ -6355,6 +6374,7 @@ dependencies = [ "libefi-illumos", "nexus-client", "omicron-test-utils", + "serde", "serial_test", "slog", "thiserror", diff --git a/Cargo.toml b/Cargo.toml index d997ab633b..bcc3d176ec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ members = [ "bootstore", "common", "ddm-admin-client", + "dpd-client", "deploy", "dns-server", "dns-service-client", @@ -56,6 +57,7 @@ members = [ default-members = [ "common", "ddm-admin-client", + "dpd-client", "deploy", "dns-server", "dns-service-client", @@ -140,6 +142,7 @@ diesel = { version = "2.0.3" } diesel-dtrace = { git = "https://github.com/oxidecomputer/diesel-dtrace", rev = "309bd361d886a237fbdd5d74992bdbd783f98bff" } dns-server = { path = "dns-server" } dns-service-client = { path = "dns-service-client" } +dpd-client = { path = "dpd-client" } dropshot = { git = "https://github.com/oxidecomputer/dropshot", branch = "main", features = [ "usdt-probes" ] } expectorate = "1.0.6" fatfs = "0.3.6" diff --git a/README.adoc b/README.adoc index aca0c2f3fb..9f651a1750 100644 --- a/README.adoc +++ b/README.adoc @@ -43,6 +43,18 @@ This mode of operation will be used in production. To build and run the non-simulated version of Omicron, see: xref:docs/how-to-run.adoc[]. +=== cargo test +If you are running unit tests that involve ASIC configuration, such as testing +instance creation sagas, you will need to either have a local instance of `dpd` +running, or you will need to set `SKIP_ASIC_CONFIG=1` when running `cargo test`: + +---- +SKIP_ASIC_CONFIG=1 cargo test +---- + +This is a temporary workaround until we have a stub version of `dpd` integrated +into the test suite. + === rustfmt and clippy You can **format the code** using `cargo fmt`. Make sure to run this before pushing changes. The CI checks that the code is correctly formatted. diff --git a/common/src/nexus_config.rs b/common/src/nexus_config.rs index 75b949bce6..0ff49e89a4 100644 --- a/common/src/nexus_config.rs +++ b/common/src/nexus_config.rs @@ -173,6 +173,12 @@ pub struct TimeseriesDbConfig { pub address: Option, } +/// Configuration for the `Dendrite` dataplane daemon. +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub struct DpdConfig { + pub address: SocketAddr, +} + // A deserializable type that does no validation on the tunable parameters. #[derive(Clone, Debug, Deserialize, PartialEq)] struct UnvalidatedTunables { @@ -278,6 +284,8 @@ pub struct PackageConfig { /// Tunable configuration for testing and experimentation #[serde(default)] pub tunables: Tunables, + /// `Dendrite` dataplane daemon configuration + pub dendrite: DpdConfig, } #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] @@ -350,7 +358,9 @@ mod test { SchemeName, TimeseriesDbConfig, UpdatesConfig, }; use crate::address::{Ipv6Subnet, RACK_PREFIX}; - use crate::nexus_config::{Database, DeploymentConfig, LoadErrorKind}; + use crate::nexus_config::{ + Database, DeploymentConfig, DpdConfig, LoadErrorKind, + }; use dropshot::ConfigDropshot; use dropshot::ConfigLogging; use dropshot::ConfigLoggingIfExists; @@ -360,6 +370,7 @@ mod test { use std::net::{Ipv6Addr, SocketAddr}; use std::path::Path; use std::path::PathBuf; + use std::str::FromStr; /// Generates a temporary filesystem path unique for the given label. fn temp_path(label: &str) -> PathBuf { @@ -479,6 +490,8 @@ mod test { net = "::/56" [deployment.database] type = "from_dns" + [dendrite] + address = "[::1]:12224" "##, ) .unwrap(); @@ -528,6 +541,9 @@ mod test { default_base_url: "http://example.invalid/".into(), }), tunables: Tunables { max_vpc_ipv4_subnet_prefix: 27 }, + dendrite: DpdConfig { + address: SocketAddr::from_str("[::1]:12224").unwrap() + }, }, } ); @@ -562,6 +578,8 @@ mod test { net = "::/56" [deployment.database] type = "from_dns" + [dendrite] + address = "[::1]:12224" "##, ) .unwrap(); diff --git a/docs/boundary-services-a-to-z.adoc b/docs/boundary-services-a-to-z.adoc new file mode 100644 index 0000000000..36e7c8eacc --- /dev/null +++ b/docs/boundary-services-a-to-z.adoc @@ -0,0 +1,135 @@ += Boundary Services A-Z + +This document describes how to run an environment with boundary services. +It's a quick rundown and assumes knowledge of the basic setup described in the +Running Omicron (Non-Simulated) document. + +== 0. Install softnpu ASIC emulator machinery + +---- +./tools/install_softnpu_machinery.sh +---- + +== 1. Setup virtual hardware + +---- +PHYSICAL_LINK= pfexec ./tools/create_virtual_hardware.sh +---- +Note that the `PHYSICAL_LINK` environment variable is optional. If not supplied, +the first link in `dladm show-phys` will be used. + +The virtual hardware is a bit different than what has previously been used. What +we now have looks like this. + +image::plumbing.png[] + +The `softnpu` zone will be configured and launched during the `create_virtual_hardware.sh` +script. + +== 2. Build and install the control plane. + +---- +./tools/create_self_signed_cert.sh + +cargo build --release --bin omicron-package +./target/release/omicron-package -t target create -i standard -m non-gimlet -s softnpu +./target/release/omicron-package package +pfexec ./target/release/omicron-package install +---- + +The control plane is now starting, reference the Running Omicron (Non-Simulated) +doc for more details on determining when things are ready to go. + +Once the control plane is running, `softnpu` can be configured via `dendrite` +using `swadm`. An example script is provided in `tools/scrimlet/softnpu-init.sh`. +This script should work without modification for basic development setups, +but feel free to tweak it as needed. + +---- +$ ./tools/scrimlet/softnpu-init.sh +++ netstat -rn +++ grep default +++ awk -F ' ' '{print $2}' ++ GATEWAY_IP=10.85.0.1 ++ echo 'Using 10.85.0.1 as gateway ip' +Using 10.85.0.1 as gateway ip +++ arp 10.85.0.1 +++ awk -F ' ' '{print $4}' ++ gateway_mac=68:d7:9a:1f:77:a1 ++ ./out/softnpu/swadm -h '[fd00:1122:3344:101::2]' port add 1:0 100G RS ++ ./out/softnpu/swadm -h '[fd00:1122:3344:101::2]' port add 2:0 100G RS ++ ./out/softnpu/swadm -h '[fd00:1122:3344:101::2]' addr add 1:0 fe80::aae1:deff:fe01:701c ++ ./out/softnpu/swadm -h '[fd00:1122:3344:101::2]' addr add 2:0 fe80::aae1:deff:fe01:701d ++ ./out/softnpu/swadm -h '[fd00:1122:3344:101::2]' addr add 1:0 fd00:99::1 ++ ./out/softnpu/swadm -h '[fd00:1122:3344:101::2]' route add fd00:1122:3344:0101::/64 1:0 fe80::aae1:deff:fe00:1 ++ ./out/softnpu/swadm -h '[fd00:1122:3344:101::2]' arp add fe80::aae1:deff:fe00:1 a8:e1:de:00:00:01 ++ ./out/softnpu/swadm -h '[fd00:1122:3344:101::2]' route add 0.0.0.0/0 2:0 10.85.0.1 ++ ./out/softnpu/swadm -h '[fd00:1122:3344:101::2]' arp add 10.85.0.1 68:d7:9a:1f:77:a1 ++ ./out/softnpu/swadm -h '[fd00:1122:3344:101::2]' port list + NAME MEDIA SPEED FEC ENA LINK MAC + 1:0 Copper 100G RS Ena Up a8:40:25:71:e3:82 + 2:0 Copper 100G RS Ena Up a8:40:25:71:e3:83 ++ ./out/softnpu/swadm -h '[fd00:1122:3344:101::2]' addr list +Port IPv4 IPv6 +1:0 fd00:99::1 + fe80::aae1:deff:fe01:701c +2:0 fe80::aae1:deff:fe01:701d ++ ./out/softnpu/swadm -h '[fd00:1122:3344:101::2]' route list +Subnet Port Gateway +0.0.0.0/0 2:0 10.85.0.1 +fd00:1122:3344:101::/64 1:0 fe80::aae1:deff:fe00:1 ++ ./out/softnpu/swadm -h '[fd00:1122:3344:101::2]' arp list +host mac age +10.85.0.1 68:d7:9a:1f:77:a1 0s +fe80::aae1:deff:fe00:1 a8:e1:de:00:00:01 0s +---- + +== 4. Populating the system + +Follow the +https://github.com/oxidecomputer/omicron/blob/main/docs/how-to-run.adoc[how-to-run.adoc] +to set up IPs, images, disks, instances etc. Things to pay particular attention +to here are the following. + +- The address range in the IP pool should be on a subnet in your local network that + can NAT out to the Internet. +- Be sure to set up an external IP for the instance you create. + +You will need to set up `proxy-arp` if your VM external IP addresses are on the +same L2 network as the router or other non-oxide hosts: +---- +pfexec /opt/oxide/softnpu/stuff/scadm \ + --server /opt/oxide/softnpu/stuff/server \ + --client /opt/oxide/softnpu/stuff/client \ + standalone \ + add-proxy-arp \ + $ip_pool_start \ + $ip_pool_end \ + $softnpu_mac +---- + +== 5. Configuring scrimlet/sidecar + +A this point we have an instance up and running with external connectivity +configured via boundary services: +---- +ry@korgano:~/omicron$ ~/propolis/target/release/propolis-cli --server fd00:1122:3344:101::c serial + +debian login: root +Linux debian 5.10.0-9-amd64 #1 SMP Debian 5.10.70-1 (2021-09-30) x86_64 + +The programs included with the Debian GNU/Linux system are free software; +the exact distribution terms for each program are described in the +individual files in /usr/share/doc/*/copyright. + +Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent +permitted by applicable law. +root@debian:~# host oxide.computer +oxide.computer has address 76.76.21.61 +oxide.computer has address 76.76.21.22 +oxide.computer mail is handled by 5 alt2.aspmx.l.google.com. +oxide.computer mail is handled by 1 aspmx.l.google.com. +oxide.computer mail is handled by 10 aspmx3.googlemail.com. +oxide.computer mail is handled by 5 alt1.aspmx.l.google.com. +oxide.computer mail is handled by 10 aspmx2.googlemail.com. +---- diff --git a/docs/how-to-run.adoc b/docs/how-to-run.adoc index 137ee333e9..169330b92f 100644 --- a/docs/how-to-run.adoc +++ b/docs/how-to-run.adoc @@ -22,7 +22,7 @@ Any additional prerequisite software may be installed with the following script: [source,text] ---- -$ ./tools/install_prerequisites.sh +$ pfexec ./tools/install_prerequisites.sh ---- This script expects that you are both attempting to compile code and execute @@ -42,12 +42,32 @@ $ ./tools/install_runner_prerequisites.sh The sled agent expects to manage a real Gimlet. However, until those are built, developers generally make do with something else, usually a commodity machine. To make your machine "look" like a Gimlet, the -`./tools/create_virtual_hardware.sh` script can be used. This creates a few +`pfexec ./tools/create_virtual_hardware.sh` script can be used. This creates a few file-based ZFS vdevs and ZFS zpools on top of those, and a couple of VNICs. The vdevs model the actual U.2s that will be in a Gimlet, and the VNICs model the two Chelsio NIC ports. -You can clean up these resources with `./tools/destroy_virtual_hardware.sh`. +Set the `GATEWAY_IP` variable when running the `create_virtual_hardware` script +to override the default logic used to automatically determine the gateway ip. +This variable is used to configure `softnpu` with a default route for external / +internet connectivity. Set the `PHYSICAL_LINK` environment variable to override +the default logic used to automatically determine the physical network connection +used for external communication. For example: + +=== Set up simulated scrimlet/sidecar dependencies. + +To access the outside world or other gimlets, we'll need to set up a virtual sidecar +device for those network paths. + +---- +./tools/install_softnpu_machinery.sh +---- + +---- +$ GATEWAY_IP=10.85.0.1 PHYSICAL_LINK=ixgbe0 pfexec ./tools/create_virtual_hardware.sh +---- + +You can clean up these resources with `pfexec ./tools/destroy_virtual_hardware.sh`. This script requires Omicron be uninstalled, e.g., with `pfexec ./target/release/omicron-package uninstall`, and a warning will be printed if that is not the case. The script will then remove the file-based vdevs and the @@ -79,30 +99,6 @@ hard-coded. === Getting into your guests -Omicron currently implements a bit of a hack to allow external connectivity into -guest instances. This overrides the default behavior of the Oxide Packet -Transformation Engine (OPTE, the kernel module which provides the private -virtual networking to guests). To make this work, OPTE needs to know about the -local networking configuration, in this case the MAC address of the local -internet gateway. To be able to get into your instances, you _must_ specify this -in the `gateway.mac` field of the config file `smf/sled-agent/config-rss.toml`. - -The value there is correct for the lab environment, so if you're running there, -no changes are needed. If you're running elsewhere, you find the value with: - -[source,text] ----- -$ arp -an | grep $(netstat -rn | awk '{ if ($1 == "default") print $2 }') -igb0 192.168.1.1 255.255.255.255 74:ac:b9:a4:dc:02 ----- - -That MAC address in the far right column should be provided in place of the -current value. Note that it can be specified as a string or an array of hex -values. If you have multiple gateways, e.g., different gateways for different -data links, you must choose the value for the physical link over which -`create_virtual_hardware.sh` will create the underlay VNICs. That is, this must -be a value in the same L2 segment as OPTE. - [source,text] ---- # Create a new build target (try 'target create -h' to see all the options) diff --git a/docs/plumbing.png b/docs/plumbing.png new file mode 100644 index 0000000000000000000000000000000000000000..a6eb2dda0d3d47bd02dfe269229768812206f666 GIT binary patch literal 126393 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

XtkWjQUyUsmG)bc{F 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 literal 0 HcmV?d00001 diff --git a/dpd-client/Cargo.toml b/dpd-client/Cargo.toml new file mode 100644 index 0000000000..80dd7f6b9c --- /dev/null +++ b/dpd-client/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "dpd-client" +version = "0.1.0" +edition = "2021" + +[dependencies] +futures.workspace = true +progenitor-client.workspace = true +reqwest = { workspace = true, features = ["json", "stream", "rustls-tls"] } +serde.workspace = true +slog.workspace = true +regress.workspace = true + +[build-dependencies] +anyhow.workspace = true +omicron-zone-package.workspace = true +progenitor.workspace = true +quote.workspace = true +serde_json.workspace = true +toml.workspace = true diff --git a/dpd-client/build.rs b/dpd-client/build.rs new file mode 100644 index 0000000000..a1cbc57fd1 --- /dev/null +++ b/dpd-client/build.rs @@ -0,0 +1,103 @@ +// 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 2022 Oxide Computer Company +// +// TODO: remove +// This code is only required at the moment because the source repo +// for `Dendrite` is not yet public. Once `Dendrite` has been made +// public, we can remove this and point the services in `omicron` +// that require the `dpd-client` library to the `Dendrite` repo. + +use anyhow::bail; +use anyhow::Context; +use anyhow::Result; +use omicron_zone_package::config::Config; +use omicron_zone_package::package::PackageSource; +use quote::quote; +use std::env; +use std::fs; +use std::path::Path; + +fn main() -> Result<()> { + // Find the current dendrite repo commit from our package manifest. + let manifest = fs::read_to_string("../package-manifest.toml") + .context("failed to read ../package-manifest.toml")?; + println!("cargo:rerun-if-changed=../package-manifest.toml"); + + let config: Config = toml::from_str(&manifest) + .context("failed to parse ../package-manifest.toml")?; + + let dendrite = config + .packages + .get("dendrite-asic") + .context("missing dendrite package in ../package-manifest.toml")?; + + let local_path = match &dendrite.source { + PackageSource::Prebuilt { commit, .. } => { + // Report a relatively verbose error if we haven't downloaded the requisite + // openapi spec. + let local_path = format!("../out/downloads/dpd-{commit}.json"); + if !Path::new(&local_path).exists() { + bail!("{local_path} doesn't exist; rerun `tools/ci_download_dendrite_openapi` (after updating `tools/dendrite_openapi_version` if the dendrite commit in package-manifest.toml has changed)"); + } + println!("cargo:rerun-if-changed={local_path}"); + local_path + } + + PackageSource::Manual => { + let local_path = "../out/downloads/dpd-manual.json".to_string(); + if !Path::new(&local_path).exists() { + bail!("{local_path} doesn't exist, please copy manually built dpd.json there!"); + } + println!("cargo:rerun-if-changed={local_path}"); + local_path + } + + _ => { + bail!("dendrite external package must have type `prebuilt` or `manual`") + } + }; + + let spec = { + let bytes = fs::read(&local_path) + .with_context(|| format!("failed to read {local_path}"))?; + serde_json::from_slice(&bytes).with_context(|| { + format!("failed to parse {local_path} as openapi spec") + })? + }; + + let content = progenitor::Generator::new( + progenitor::GenerationSettings::new() + .with_inner_type(quote!(ClientState)) + .with_pre_hook(quote! { + |state: &crate::ClientState, request: &reqwest::Request| { + slog::debug!(state.log, "client request"; + "method" => %request.method(), + "uri" => %request.url(), + "body" => ?&request.body(), + ); + } + }) + .with_post_hook(quote! { + |state: &crate::ClientState, result: &Result<_, _>| { + slog::debug!(state.log, "client response"; "result" => ?result); + } + }), + ) + .generate_text(&spec) + .with_context(|| { + format!("failed to generate progenitor client from {local_path}") + })?; + + let out_file = + Path::new(&env::var("OUT_DIR").expect("OUT_DIR env var not set")) + .join("dpd-client.rs"); + + fs::write(&out_file, content).with_context(|| { + format!("failed to write client to {}", out_file.display()) + })?; + + Ok(()) +} diff --git a/dpd-client/src/lib.rs b/dpd-client/src/lib.rs new file mode 100644 index 0000000000..09358d2487 --- /dev/null +++ b/dpd-client/src/lib.rs @@ -0,0 +1,25 @@ +// 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 2023 Oxide Computer Company + +#![allow(clippy::redundant_closure_call)] +#![allow(clippy::needless_lifetimes)] +#![allow(clippy::match_single_binding)] +#![allow(clippy::clone_on_copy)] +#![allow(clippy::unnecessary_to_owned)] + +use slog::Logger; + +include!(concat!(env!("OUT_DIR"), "/dpd-client.rs")); + +/// State maintained by a [`Client`]. +#[derive(Clone, Debug)] +pub struct ClientState { + /// An arbitrary tag used to identify a client, for controlling things like + /// per-client settings. + pub tag: String, + /// Used for logging requests and responses. + pub log: Logger, +} diff --git a/illumos-utils/src/opte/illumos/mod.rs b/illumos-utils/src/opte/illumos/mod.rs index a282e70cef..3e27b2cf1c 100644 --- a/illumos-utils/src/opte/illumos/mod.rs +++ b/illumos-utils/src/opte/illumos/mod.rs @@ -9,7 +9,6 @@ use crate::dladm; use opte_ioctl::OpteHdl; use slog::info; use slog::Logger; -use std::fs; use std::path::Path; mod firewall_rules; @@ -80,17 +79,6 @@ pub fn initialize_xde_driver( return Err(Error::NoXdeConf); } - // TODO-remove - // - // See https://github.com/oxidecomputer/omicron/issues/1337 - // - // An additional part of the workaround to connect into instances. This is - // required to tell OPTE to actually act as a 1-1 NAT when an instance is - // provided with an external IP address, rather than do its normal job of - // encapsulating the traffic onto the underlay (such as for delivery to - // boundary services). - use_external_ip_workaround(&log, &xde_conf); - info!(log, "using '{:?}' as data links for xde driver", underlay_nics); if underlay_nics.len() < 2 { const MESSAGE: &str = concat!( @@ -132,36 +120,3 @@ pub fn initialize_xde_driver( Err(e) => Err(e.into()), } } - -fn use_external_ip_workaround(log: &Logger, xde_conf: &Path) { - const NEEDLE: &str = "ext_ip_hack = 0;"; - const NEW_NEEDLE: &str = "ext_ip_hack = 1;"; - - // NOTE: This only works in the real sled agent, which is run as root. The - // file is not world-readable. - let contents = fs::read_to_string(xde_conf) - .expect("Failed to read xde configuration file"); - let new = contents.replace(NEEDLE, NEW_NEEDLE); - if contents == new { - info!( - log, - "xde driver configuration file appears to already use external IP workaround"; - "conf_file" => ?xde_conf, - ); - } else { - info!( - log, - "updating xde driver configuration file for external IP workaround"; - "conf_file" => ?xde_conf, - ); - fs::write(xde_conf, &new) - .expect("Failed to modify xde configuration file"); - } - - // Ensure the driver picks up the updated configuration file, if it's been - // loaded previously without the workaround. - std::process::Command::new(crate::PFEXEC) - .args(&["update_drv", "xde"]) - .output() - .expect("Failed to reload xde driver configuration file"); -} diff --git a/illumos-utils/src/opte/illumos/port_manager.rs b/illumos-utils/src/opte/illumos/port_manager.rs index 77fc465679..22a10ef02c 100644 --- a/illumos-utils/src/opte/illumos/port_manager.rs +++ b/illumos-utils/src/opte/illumos/port_manager.rs @@ -6,7 +6,6 @@ use crate::dladm::Dladm; use crate::dladm::PhysicalLink; -use crate::dladm::VnicSource; use crate::opte::default_boundary_services; use crate::opte::opte_firewall_rules; use crate::opte::params::NetworkInterface; @@ -41,7 +40,6 @@ use std::sync::atomic::AtomicU64; use std::sync::atomic::Ordering; use std::sync::Arc; use std::sync::Mutex; -use std::sync::MutexGuard; use uuid::Uuid; // Prefix used to identify xde data links. @@ -54,14 +52,6 @@ struct PortManagerInner { // Sequential identifier for each port on the system. next_port_id: AtomicU64, - // TODO-remove: This is part of the external IP address workaround - // - // See https://github.com/oxidecomputer/omicron/issues/1335 - // - // We only need to know this while we're setting the secondary MACs of the - // link to support OPTE's proxy ARP for the guest's IP. - data_link: PhysicalLink, - // TODO-remove: This is part of the external IP address workaround. // // See https://github.com/oxidecomputer/omicron/issues/1335 @@ -85,56 +75,6 @@ impl PortManagerInner { self.next_port_id.fetch_add(1, Ordering::SeqCst) ) } - - // TODO-remove - // - // See https://github.com/oxidecomputer/omicron/issues/1335 - // - // This is part of the workaround to get external connectivity into - // instances, without setting up all of boundary services. Rather than - // encap/decap the guest traffic, OPTE just performs 1-1 NAT between the - // private IP address of the guest and the external address provided by - // the control plane. This call here allows the underlay nic, `net0` to - // advertise as having the guest's MAC address. - fn update_secondary_macs( - &self, - ports: &mut MutexGuard<'_, BTreeMap<(Uuid, String), Port>>, - ) -> Result<(), Error> { - let secondary_macs = ports - .values() - .filter_map(|port| { - // Only advertise Ports with a publicly-visible external IP - // address, on the primary interface for this instance. - if port.external_ips().is_some() { - Some(port.mac().to_string()) - } else { - None - } - }) - .collect::>() - .join(","); - if secondary_macs.is_empty() { - Dladm::reset_linkprop(self.data_link.name(), "secondary-macs")?; - debug!( - self.log, - "Reset data link secondary MACs link prop for OPTE proxy ARP"; - "link_name" => self.data_link.name(), - ); - } else { - Dladm::set_linkprop( - self.data_link.name(), - "secondary-macs", - &secondary_macs, - )?; - debug!( - self.log, - "Updated data link secondary MACs link prop for OPTE proxy ARP"; - "data_link" => &self.data_link.0, - "secondary_macs" => ?secondary_macs, - ); - } - Ok(()) - } } /// The port manager controls all OPTE ports on a single host. @@ -148,14 +88,12 @@ impl PortManager { /// interfaces pub fn new( log: Logger, - data_link: PhysicalLink, underlay_ip: Ipv6Addr, gateway_mac: MacAddr6, ) -> Self { let inner = Arc::new(PortManagerInner { log, next_port_id: AtomicU64::new(0), - data_link, gateway_mac, underlay_ip, ports: Mutex::new(BTreeMap::new()), @@ -368,41 +306,37 @@ impl PortManager { vnic_name }; - let ticket = - PortTicket::new(instance_id, port_name.clone(), self.inner.clone()); - let port = Port::new( - port_name.clone(), - nic.ip, - subnet, - mac, - nic.slot, - vni, - self.inner.underlay_ip, - source_nat, - external_ips, - gateway, - boundary_services, - vnic, - ); - - // Update the secondary MAC of the underlay. - // - // TODO-remove: This is part of the external IP hack. - // - // Acquire the lock _after_ the ticket exists. If the - // `update_secondary_mac` call below fails, we'll propagate the - // error with `?`. We need the lock guard to be dropped first, so that - // the lock acquired when `ticket` is dropped is guaranteed to be free. - let mut ports = self.inner.ports.lock().unwrap(); - let old = ports.insert((instance_id, port_name.clone()), port.clone()); - assert!( - old.is_none(), - "Duplicate OPTE port detected: instance_id = {}, port_name = {}", - instance_id, - &port_name, - ); - self.inner.update_secondary_macs(&mut ports)?; - drop(ports); + let (port, ticket) = { + let mut ports = self.inner.ports.lock().unwrap(); + let ticket = PortTicket::new( + instance_id, + port_name.clone(), + self.inner.clone(), + ); + let port = Port::new( + port_name.clone(), + nic.ip, + subnet, + mac, + nic.slot, + vni, + self.inner.underlay_ip, + source_nat, + external_ips, + gateway, + boundary_services, + vnic, + ); + let old = + ports.insert((instance_id, port_name.clone()), port.clone()); + assert!( + old.is_none(), + "Duplicate OPTE port detected: instance_id = {}, port_name = {}", + instance_id, + &port_name, + ); + (port, ticket) + }; // Add a router entry for this interface's subnet, directing traffic to the // VPC subnet. @@ -530,18 +464,7 @@ impl PortTicket { "instance_id" => ?self.id, "port_name" => &self.port_name, ); - if let Err(e) = manager.update_secondary_macs(&mut ports) { - warn!( - manager.log, - "Failed to update secondary-macs linkprop when \ - releasing OPTE ports for instance"; - "instance_id" => ?self.id, - "err" => ?e, - ); - return Err(e); - } else { - return Ok(()); - } + return Ok(()); } Ok(()) } diff --git a/illumos-utils/src/opte/non_illumos/port_manager.rs b/illumos-utils/src/opte/non_illumos/port_manager.rs index e3c347a118..781fe64819 100644 --- a/illumos-utils/src/opte/non_illumos/port_manager.rs +++ b/illumos-utils/src/opte/non_illumos/port_manager.rs @@ -4,7 +4,6 @@ //! Manager for all OPTE ports on a Helios system -use crate::dladm::PhysicalLink; use crate::opte::default_boundary_services; use crate::opte::params::NetworkInterface; use crate::opte::params::SourceNatConfig; @@ -38,14 +37,6 @@ struct PortManagerInner { // Sequential identifier for each port on the system. next_port_id: AtomicU64, - // TODO-remove: This is part of the external IP address workaround - // - // See https://github.com/oxidecomputer/omicron/issues/1335 - // - // We only need to know this while we're setting the secondary MACs of the - // link to support OPTE's proxy ARP for the guest's IP. - data_link: PhysicalLink, - // TODO-remove: This is part of the external IP address workaround. // // See https://github.com/oxidecomputer/omicron/issues/1335 @@ -82,14 +73,12 @@ impl PortManager { /// interfaces pub fn new( log: Logger, - data_link: PhysicalLink, underlay_ip: Ipv6Addr, gateway_mac: MacAddr6, ) -> Self { let inner = Arc::new(PortManagerInner { log, next_port_id: AtomicU64::new(0), - data_link, gateway_mac, underlay_ip, ports: Mutex::new(BTreeMap::new()), diff --git a/illumos-utils/src/running_zone.rs b/illumos-utils/src/running_zone.rs index 42b5c089ef..810f439048 100644 --- a/illumos-utils/src/running_zone.rs +++ b/illumos-utils/src/running_zone.rs @@ -467,6 +467,7 @@ impl InstalledZone { zone_name: &str, unique_name: Option<&str>, datasets: &[zone::Dataset], + filesystems: &[zone::Fs], devices: &[zone::Device], opte_ports: Vec, bootstrap_vnic: Option, @@ -498,6 +499,7 @@ impl InstalledZone { &full_zone_name, &zone_image_path, &datasets, + &filesystems, &devices, net_device_names, limit_priv, diff --git a/illumos-utils/src/zone.rs b/illumos-utils/src/zone.rs index 01e0be00fc..7a116d2e79 100644 --- a/illumos-utils/src/zone.rs +++ b/illumos-utils/src/zone.rs @@ -236,6 +236,7 @@ impl Zones { zone_name: &str, zone_image: &std::path::Path, datasets: &[zone::Dataset], + filesystems: &[zone::Fs], devices: &[zone::Device], vnics: Vec, limit_priv: Vec, @@ -282,6 +283,9 @@ impl Zones { for dataset in datasets { cfg.add_dataset(&dataset); } + for filesystem in filesystems { + cfg.add_fs(&filesystem); + } for device in devices { cfg.add_device(device); } diff --git a/nexus/Cargo.toml b/nexus/Cargo.toml index bd68bbb14f..33d5e24926 100644 --- a/nexus/Cargo.toml +++ b/nexus/Cargo.toml @@ -21,6 +21,7 @@ crucible-pantry-client.workspace = true diesel = { workspace = true, features = ["postgres", "r2d2", "chrono", "serde_json", "network-address", "uuid"] } diesel-dtrace.workspace = true dns-service-client.workspace = true +dpd-client.workspace = true dropshot.workspace = true fatfs.workspace = true futures.workspace = true diff --git a/nexus/examples/config.toml b/nexus/examples/config.toml index 663034a4a7..f14c16c2af 100644 --- a/nexus/examples/config.toml +++ b/nexus/examples/config.toml @@ -64,3 +64,7 @@ url = "postgresql://root@127.0.0.1:32221/omicron?sslmode=disable" # The maximum allowed prefix (thus smallest size) for a VPC Subnet's # IPv4 subnetwork. This size allows for ~60 hosts. max_vpc_ipv4_subnet_prefix = 26 + +# Configuration for interacting with the dataplane daemon +[dendrite] +address = "[::1]:12224" diff --git a/nexus/src/app/mod.rs b/nexus/src/app/mod.rs index 5ab4f0d97b..7bb229bfab 100644 --- a/nexus/src/app/mod.rs +++ b/nexus/src/app/mod.rs @@ -162,6 +162,9 @@ pub struct Nexus { samael_max_issue_delay: std::sync::Mutex>, resolver: Arc>, + + /// Client for dataplane daemon / switch management API + dpd_client: Arc, } // TODO Is it possible to make some of these operations more generic? A @@ -201,6 +204,20 @@ impl Nexus { sec_store, )); + let client_state = dpd_client::ClientState { + tag: String::from("nexus"), + log: log.new(o!( + "component" => "DpdClient" + )), + }; + let dpd_address = config.pkg.dendrite.address; + let dpd_host = dpd_address.ip().to_string(); + let dpd_port = dpd_address.port(); + let dpd_client = Arc::new(dpd_client::Client::new( + &format!("http://[{dpd_host}]:{dpd_port}"), + client_state, + )); + // Connect to clickhouse - but do so lazily. // Clickhouse may not be executing when Nexus starts. let timeseries_client = if let Some(address) = @@ -261,6 +278,7 @@ impl Nexus { ), samael_max_issue_delay: std::sync::Mutex::new(None), resolver, + dpd_client, }; // TODO-cleanup all the extra Arcs here seems wrong diff --git a/nexus/src/app/sagas/instance_create.rs b/nexus/src/app/sagas/instance_create.rs index 5fc6e69c30..816b60b048 100644 --- a/nexus/src/app/sagas/instance_create.rs +++ b/nexus/src/app/sagas/instance_create.rs @@ -33,6 +33,7 @@ use slog::warn; use std::convert::TryFrom; use std::fmt::Debug; use std::net::Ipv6Addr; +use std::str::FromStr; use steno::ActionError; use steno::Node; use steno::{DagBuilder, SagaName}; @@ -58,6 +59,13 @@ struct NetParams { new_id: Uuid, } +#[derive(Debug, Deserialize, Serialize)] +struct NetworkConfigParams { + saga_params: Params, + instance_id: Uuid, + which: usize, +} + #[derive(Debug, Deserialize, Serialize)] struct DiskAttachParams { serialized_authn: authn::saga::Serialized, @@ -103,6 +111,10 @@ declare_saga_actions! { + sic_attach_disk_to_instance - sic_attach_disk_to_instance_undo } + CONFIGURE_ASIC -> "configure_asic" { + + sic_add_network_config + - sic_remove_network_config + } INSTANCE_ENSURE -> "instance_ensure" { + sic_instance_ensure } @@ -299,11 +311,261 @@ impl NexusSaga for SagaInstanceCreate { )?; } + // If a primary NIC exists, create a NAT entry for the default external IP, + // as well as additional NAT entries for each requested ephemeral IP + for i in 0..(params.create_params.external_ips.len() + 1) { + let subsaga_name = + SagaName::new(&format!("instance-configure-nat-{i}")); + let mut subsaga_builder = DagBuilder::new(subsaga_name); + subsaga_builder.append(Node::action( + "configure_asic", + format!("ConfigureAsic-{i}").as_str(), + CONFIGURE_ASIC.as_ref(), + )); + let net_params = NetworkConfigParams { + saga_params: params.clone(), + instance_id, + which: i, + }; + subsaga_append( + "configure_asic", + subsaga_builder.build()?, + &mut builder, + net_params, + i, + )?; + } builder.append(instance_ensure_action()); Ok(builder.build()?) } } +async fn sic_add_network_config( + sagactx: NexusActionContext, +) -> Result<(), ActionError> { + let net_params = sagactx.saga_params::()?; + let which = net_params.which; + let instance_id = net_params.instance_id; + let params = net_params.saga_params; + let opctx = crate::context::op_context_for_saga_action( + &sagactx, + ¶ms.serialized_authn, + ); + let osagactx = sagactx.user_data(); + let dpd_client: &dpd_client::Client = &osagactx.nexus().dpd_client; + let datastore = &osagactx.datastore(); + let log = sagactx.user_data().log(); + + let (.., authz_instance, db_instance) = LookupPath::new(&opctx, &datastore) + .instance_id(instance_id) + .fetch() + .await + .map_err(ActionError::action_failed)?; + + let instance_id = db_instance.id(); + let sled_uuid = db_instance.runtime_state.sled_id; + + let (.., sled) = LookupPath::new(&osagactx.nexus().opctx_alloc, &datastore) + .sled_id(sled_uuid) + .fetch() + .await + .map_err(ActionError::action_failed)?; + + let sled_ip_address = sled.address(); + + debug!(log, "fetching network interfaces"); + + let network_interface = match datastore + .derive_guest_network_interface_info(&opctx, &authz_instance) + .await + .map_err(ActionError::action_failed)? + .into_iter() + .find(|interface| interface.primary) + { + Some(interface) => interface, + // Return early if instance does not have a primary network + // interface + None => return Ok(()), + }; + + let mac_address = + macaddr::MacAddr6::from_str(&network_interface.mac.to_string()) + .map_err(|e| { + ActionError::action_failed(Error::internal_error(&format!( + "failed to convert mac address: {e}" + ))) + })?; + + let vni: u32 = network_interface.vni.into(); + + debug!(log, "fetching external ip addresses"); + + let target_ip = &datastore + .instance_lookup_external_ips(&opctx, instance_id) + .await + .map_err(ActionError::action_failed)? + .get(which) + .ok_or_else(|| { + ActionError::action_failed(Error::internal_error(&format!( + "failed to find external ip address at index: {which}" + ))) + })? + .to_owned(); + + debug!(log, "checking for existing nat mapping for {target_ip:#?}"); + + // TODO: https://github.com/oxidecomputer/omicron/issues/2629 + // + // currently if we have this environment variable set, we want to + // bypass all calls to DPD. This is mainly to facilitate some tests where + // we don't have dpd running. In the future we should probably have these + // testing environments running dpd-stub so that the full path can be tested. + if let Ok(_) = std::env::var("SKIP_ASIC_CONFIG") { + debug!(log, "SKIP_ASIC_CONFIG is set, disabling calls to dendrite"); + return Ok(()); + }; + + let existing_nat = match target_ip.ip { + ipnetwork::IpNetwork::V4(network) => { + dpd_client.nat_ipv4_get(&network.ip(), *target_ip.first_port).await + } + ipnetwork::IpNetwork::V6(network) => { + dpd_client.nat_ipv6_get(&network.ip(), *target_ip.first_port).await + } + }; + + match existing_nat { + Ok(_) => { + // nat entry already exists, do nothing + return Ok(()); + } + Err(e) => { + if e.status() == Some(http::StatusCode::NOT_FOUND) { + debug!(log, "no nat entry found for: {target_ip:#?}"); + } else { + return Err(ActionError::action_failed(Error::internal_error( + &format!("failed to query dpd: {e}"), + ))); + } + } + } + + debug!(log, "creating nat entry for: {target_ip:#?}"); + + let nat_target = dpd_client::types::NatTarget { + inner_mac: dpd_client::types::MacAddr { + a: mac_address.into_array().to_vec(), + }, + internal_ip: *sled_ip_address.ip(), + vni: vni.into(), + }; + + match target_ip.ip { + ipnetwork::IpNetwork::V4(network) => { + dpd_client + .nat_ipv4_create( + &network.ip(), + *target_ip.first_port, + *target_ip.last_port, + &nat_target, + ) + .await + } + ipnetwork::IpNetwork::V6(network) => { + dpd_client + .nat_ipv6_create( + &network.ip(), + *target_ip.first_port, + *target_ip.last_port, + &nat_target, + ) + .await + } + } + .map_err(|e| { + ActionError::action_failed(Error::internal_error(&format!( + "failed to create nat entry via dpd: {e}" + ))) + })?; + + debug!(log, "creation of nat entry successful for: {target_ip:#?}"); + Ok(()) +} + +async fn sic_remove_network_config( + sagactx: NexusActionContext, +) -> Result<(), anyhow::Error> { + let net_params = sagactx.saga_params::()?; + let which = net_params.which; + let instance_id = net_params.instance_id; + let params = net_params.saga_params; + let opctx = crate::context::op_context_for_saga_action( + &sagactx, + ¶ms.serialized_authn, + ); + let osagactx = sagactx.user_data(); + let dpd_client = &osagactx.nexus().dpd_client; + let datastore = &osagactx.datastore(); + let log = sagactx.user_data().log(); + + debug!(log, "fetching external ip addresses"); + + let target_ip = &datastore + .instance_lookup_external_ips(&opctx, instance_id) + .await + .map_err(ActionError::action_failed)? + .get(which) + .ok_or_else(|| { + ActionError::action_failed(Error::internal_error(&format!( + "failed to find external ip address at index: {which}" + ))) + })? + .to_owned(); + + // TODO: https://github.com/oxidecomputer/omicron/issues/2629 + // + // currently if we have this environment variable set, we want to + // bypass all calls to DPD. This is mainly to facilitate some tests where + // we don't have dpd running. In the future we should probably have these + // testing environments running dpd-stub so that the full path can be tested. + if let Ok(_) = std::env::var("SKIP_ASIC_CONFIG") { + debug!(log, "SKIP_ASIC_CONFIG is set, disabling calls to dendrite"); + return Ok(()); + }; + + debug!(log, "deleting nat mapping for entry: {target_ip:#?}"); + + let result = match target_ip.ip { + ipnetwork::IpNetwork::V4(network) => { + dpd_client + .nat_ipv4_delete(&network.ip(), *target_ip.first_port) + .await + } + ipnetwork::IpNetwork::V6(network) => { + dpd_client + .nat_ipv6_delete(&network.ip(), *target_ip.first_port) + .await + } + }; + match result { + Ok(_) => { + debug!(log, "deletion of nat entry successful for: {target_ip:#?}"); + Ok(()) + } + Err(e) => { + if e.status() == Some(http::StatusCode::NOT_FOUND) { + debug!(log, "no nat entry found for: {target_ip:#?}"); + Ok(()) + } else { + Err(ActionError::action_failed(Error::internal_error( + &format!("failed to delete nat entry via dpd: {e}"), + ))) + } + } + }?; + Ok(()) +} + async fn sic_alloc_server( sagactx: NexusActionContext, ) -> Result { diff --git a/nexus/src/app/sagas/instance_delete.rs b/nexus/src/app/sagas/instance_delete.rs index db1ef99da8..8258ca6528 100644 --- a/nexus/src/app/sagas/instance_delete.rs +++ b/nexus/src/app/sagas/instance_delete.rs @@ -30,6 +30,9 @@ declare_saga_actions! { INSTANCE_DELETE_RECORD -> "no_result1" { + sid_delete_instance_record } + DELETE_ASIC_CONFIGURATION -> "delete_asic_configuration" { + + sid_delete_network_config + } DELETE_NETWORK_INTERFACES -> "no_result2" { + sid_delete_network_interfaces } @@ -57,6 +60,7 @@ impl NexusSaga for SagaInstanceDelete { _params: &Self::Params, mut builder: steno::DagBuilder, ) -> Result { + builder.append(delete_asic_configuration_action()); builder.append(instance_delete_record_action()); builder.append(delete_network_interfaces_action()); builder.append(deallocate_external_ip_action()); @@ -67,6 +71,83 @@ impl NexusSaga for SagaInstanceDelete { // instance delete saga: action implementations +async fn sid_delete_network_config( + sagactx: NexusActionContext, +) -> Result<(), ActionError> { + let params = sagactx.saga_params::()?; + let opctx = crate::context::op_context_for_saga_action( + &sagactx, + ¶ms.serialized_authn, + ); + let osagactx = sagactx.user_data(); + let dpd_client = &osagactx.nexus().dpd_client; + let datastore = &osagactx.datastore(); + let log = sagactx.user_data().log(); + + debug!(log, "fetching external ip addresses"); + + let external_ips = &datastore + .instance_lookup_external_ips(&opctx, params.authz_instance.id()) + .await + .map_err(ActionError::action_failed)?; + + // TODO: https://github.com/oxidecomputer/omicron/issues/2629 + // + // currently if we have this environment variable set, we want to + // bypass all calls to DPD. This is mainly to facilitate some tests where + // we don't have dpd running. In the future we should probably have these + // testing environments running dpd-stub so that the full path can be tested. + if let Ok(_) = std::env::var("SKIP_ASIC_CONFIG") { + debug!(log, "SKIP_ASIC_CONFIG is set, disabling calls to dendrite"); + return Ok(()); + }; + + let mut errors: Vec = vec![]; + + // Here we are attempting to delete every existing NAT entry while deferring + // any error handling. If we don't defer error handling, we might end up + // bailing out before we've attempted deletion of all entries. + for entry in external_ips { + debug!(log, "deleting nat mapping for entry: {entry:#?}"); + let result = match entry.ip { + ipnetwork::IpNetwork::V4(network) => { + dpd_client + .nat_ipv4_delete(&network.ip(), *entry.first_port) + .await + } + ipnetwork::IpNetwork::V6(network) => { + dpd_client + .nat_ipv6_delete(&network.ip(), *entry.first_port) + .await + } + }; + + match result { + Ok(_) => { + debug!(log, "deletion of nat entry successful for: {entry:#?}"); + } + Err(e) => { + if e.status() == Some(http::StatusCode::NOT_FOUND) { + debug!(log, "no nat entry found for: {entry:#?}"); + } else { + let new_error = + ActionError::action_failed(Error::internal_error( + &format!("failed to delete nat entry via dpd: {e}"), + )); + error!(log, "{new_error:#?}"); + errors.push(new_error); + } + } + } + } + + if let Some(error) = errors.first() { + return Err(error.clone()); + } + + Ok(()) +} + async fn sid_delete_instance_record( sagactx: NexusActionContext, ) -> Result<(), ActionError> { diff --git a/nexus/tests/config.test.toml b/nexus/tests/config.test.toml index 0baaaf8b4b..27dd741882 100644 --- a/nexus/tests/config.test.toml +++ b/nexus/tests/config.test.toml @@ -65,3 +65,7 @@ net = "fd00:1122:3344:0100::/56" [deployment.database] type = "from_url" url = "postgresql://root@127.0.0.1:0/omicron?sslmode=disable" + +# Dendrite (dataplane daemon) API configuration +[dendrite] +address = "[::1]:12224" diff --git a/package-manifest.toml b/package-manifest.toml index 76fc25fc3a..2d9a9decdd 100644 --- a/package-manifest.toml +++ b/package-manifest.toml @@ -263,8 +263,8 @@ only_for_targets.image = "standard" # 3. Use source.type = "manual" instead of "prebuilt" source.type = "prebuilt" source.repo = "dendrite" -source.commit = "9bb68574476afbf847ec9490c708c878a3883a3e" -source.sha256 = "70bf400cb87dce9d9b03f8f723561f8101942db18cb6316b47ee2f8b0e7cee44" +source.commit = "231ad14027c01df2ce239b455220a90a24d4afc7" +source.sha256 = "240e43417c4f637d2a19b5a343097a134a7d6ac344ff3b62a0824e9f9b7417ad" output.type = "zone" output.intermediate_only = true @@ -282,8 +282,27 @@ only_for_targets.image = "standard" # 3. Use source.type = "manual" instead of "prebuilt" source.type = "prebuilt" source.repo = "dendrite" -source.commit = "9bb68574476afbf847ec9490c708c878a3883a3e" -source.sha256 = "c84131ed636d6757e7a9a26185d78d8a6a1db4fd83fa0fbdfa81fc308ad71b31" +source.commit = "231ad14027c01df2ce239b455220a90a24d4afc7" +source.sha256 = "939e839deb9cbe399f36873907521ee0daf4b9ab5139336f89fa03f1262b97e3" +output.type = "zone" +output.intermediate_only = true + +[package.dendrite-softnpu] +service_name = "dendrite" +only_for_targets.switch = "softnpu" +only_for_targets.image = "standard" +# To manually override the package source: +# +# 1. Build the zone image manually +# 1a. cd +# 1b. cargo build --features=softnpu --release +# 1c. cargo xtask dist -o -r --features softnpu +# 2. Copy dendrite.tar.gz from dendrite/out to omicron/out/dendrite-softnpu.tar.gz +# 3. Use source.type = "manual" instead of "prebuilt" +source.type = "prebuilt" +source.repo = "dendrite" +source.commit = "231ad14027c01df2ce239b455220a90a24d4afc7" +source.sha256 = "b175f795fb966b2c58cc772620b5a3affeb5bc978fe72720df2093576d480e15" output.type = "zone" output.intermediate_only = true @@ -301,7 +320,7 @@ output.type = "zone" # To package and install the stub variant of the switch, do the following: # -# - Set the sled agent's configuration option "stub_scrimlet" to "true" +# - Set the sled agent's configuration option "scrimlet_override" to "stub" # - Run the following: # $ cargo run --release -p omicron-package -- -t switch=stub package # $ pfexec ./target/release/omicron-package -t switch=stub install @@ -312,3 +331,17 @@ only_for_targets.image = "standard" source.type = "composite" source.packages = [ "omicron-gateway.tar.gz", "dendrite-stub.tar.gz", "wicketd.tar.gz", "wicket.tar.gz" ] output.type = "zone" + +# To package and install the softnpu variant of the switch, do the following: +# +# - Set the sled agent's configuration option "scrimlet_override" to "softnpu" +# - Run the following: +# $ cargo run --release -p omicron-package -- -t switch_variant=softnpu package +# $ pfexec ./target/release/omicron-package -t switch_variant=softnpu install +[package.switch-softnpu] +service_name = "switch" +only_for_targets.switch = "softnpu" +only_for_targets.image = "standard" +source.type = "composite" +source.packages = [ "omicron-gateway.tar.gz", "dendrite-softnpu.tar.gz", "wicketd.tar.gz", "wicket.tar.gz" ] +output.type = "zone" diff --git a/package/src/bin/omicron-package.rs b/package/src/bin/omicron-package.rs index f6188178af..53b09712ea 100644 --- a/package/src/bin/omicron-package.rs +++ b/package/src/bin/omicron-package.rs @@ -700,8 +700,14 @@ async fn do_clean( "Removing artifacts from {}", artifact_dir.to_string_lossy() ); - const ARTIFACTS_TO_KEEP: &[&str] = - &["clickhouse", "cockroachdb", "xde", "console-assets", "downloads"]; + const ARTIFACTS_TO_KEEP: &[&str] = &[ + "clickhouse", + "cockroachdb", + "xde", + "console-assets", + "downloads", + "softnpu", + ]; remove_all_except(artifact_dir, ARTIFACTS_TO_KEEP, &config.log)?; info!( config.log, diff --git a/package/src/target.rs b/package/src/target.rs index e56a6ff8c4..0a929e5b77 100644 --- a/package/src/target.rs +++ b/package/src/target.rs @@ -44,6 +44,8 @@ pub enum Switch { Asic, /// Use a "stub" Dendrite that does not require any real hardware Stub, + /// Use a "softnpu" Dendrite that uses the SoftNPU asic emulator + Softnpu, } /// A strongly-typed variant of [Target]. diff --git a/sled-agent/src/bootstrap/hardware.rs b/sled-agent/src/bootstrap/hardware.rs index 4ee072ec72..87ac1f042b 100644 --- a/sled-agent/src/bootstrap/hardware.rs +++ b/sled-agent/src/bootstrap/hardware.rs @@ -130,7 +130,7 @@ impl HardwareMonitor { bootstrap_etherstub: Etherstub, switch_zone_bootstrap_address: Ipv6Addr, ) -> Result { - let hardware = HardwareManager::new(log, sled_config.stub_scrimlet) + let hardware = HardwareManager::new(log, sled_config.scrimlet_override) .map_err(|e| Error::Hardware(e))?; let service_manager = ServiceManager::new( @@ -138,7 +138,7 @@ impl HardwareMonitor { underlay_etherstub.clone(), underlay_etherstub_vnic.clone(), bootstrap_etherstub, - sled_config.stub_scrimlet, + sled_config.scrimlet_override, sled_config.sidecar_revision.clone(), switch_zone_bootstrap_address, ) diff --git a/sled-agent/src/config.rs b/sled-agent/src/config.rs index c8ab7835a6..0ee375333d 100644 --- a/sled-agent/src/config.rs +++ b/sled-agent/src/config.rs @@ -13,7 +13,7 @@ use illumos_utils::dladm::CHELSIO_LINK_PREFIX; use illumos_utils::zpool::ZpoolName; use omicron_common::vlan::VlanID; use serde::Deserialize; -use sled_hardware::is_gimlet; +use sled_hardware::{is_gimlet, ScrimletMode}; use std::path::{Path, PathBuf}; /// Configuration for a sled agent @@ -22,8 +22,8 @@ pub struct Config { /// Configuration for the sled agent debug log pub log: ConfigLogging, /// Optionally force the sled to self-identify as a scrimlet (or gimlet, - /// if set to false). - pub stub_scrimlet: Option, + /// if set to Disabled). + pub scrimlet_override: Option, // TODO: Remove once this can be auto-detected. pub sidecar_revision: String, /// Optional VLAN ID to be used for tagging guest VNICs. diff --git a/sled-agent/src/instance.rs b/sled-agent/src/instance.rs index 2585e8d619..826f651513 100644 --- a/sled-agent/src/instance.rs +++ b/sled-agent/src/instance.rs @@ -573,6 +573,8 @@ impl Instance { Some(&inner.propolis_id().to_string()), // dataset= &[], + // filesystems= + &[], &[ zone::Device { name: "/dev/vmm/*".to_string() }, zone::Device { name: "/dev/vmmctl".to_string() }, @@ -874,7 +876,6 @@ mod test { use crate::params::SourceNatConfig; use chrono::Utc; use illumos_utils::dladm::Etherstub; - use illumos_utils::dladm::PhysicalLink; use illumos_utils::opte::PortManager; use macaddr::MacAddr6; use omicron_common::api::external::{ @@ -950,9 +951,8 @@ mod test { 0xfd00, 0x1de, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, ); let mac = MacAddr6::from([0u8; 6]); - let data_link = PhysicalLink("myphylink".to_string()); let port_manager = - PortManager::new(log.new(slog::o!()), data_link, underlay_ip, mac); + PortManager::new(log.new(slog::o!()), underlay_ip, mac); let lazy_nexus_client = LazyNexusClient::new(log.clone(), std::net::Ipv6Addr::LOCALHOST) .unwrap(); diff --git a/sled-agent/src/instance_manager.rs b/sled-agent/src/instance_manager.rs index bd9592bfe1..d81ee51319 100644 --- a/sled-agent/src/instance_manager.rs +++ b/sled-agent/src/instance_manager.rs @@ -16,7 +16,6 @@ use illumos_utils::link::VnicAllocator; use illumos_utils::opte::PortManager; use macaddr::MacAddr6; use omicron_common::api::internal::nexus::InstanceRuntimeState; -use sled_hardware::underlay; use slog::Logger; use std::collections::BTreeMap; use std::net::Ipv6Addr; @@ -74,10 +73,6 @@ impl InstanceManager { underlay_ip: Ipv6Addr, gateway_mac: MacAddr6, ) -> Result { - let data_link = underlay::find_chelsio_links()? - .into_iter() - .next() - .ok_or_else(|| Error::NoDatalinks)?; Ok(InstanceManager { inner: Arc::new(InstanceManagerInternal { log: log.new(o!("component" => "InstanceManager")), @@ -86,7 +81,6 @@ impl InstanceManager { vnic_allocator: VnicAllocator::new("Instance", etherstub), port_manager: PortManager::new( log.new(o!("component" => "PortManager")), - data_link, underlay_ip, gateway_mac, ), diff --git a/sled-agent/src/params.rs b/sled-agent/src/params.rs index 6828f4d205..e7768778f4 100644 --- a/sled-agent/src/params.rs +++ b/sled-agent/src/params.rs @@ -216,7 +216,7 @@ pub struct Zpool { // The type of networking 'ASIC' the Dendrite service is expected to manage #[derive( - Clone, Debug, Deserialize, Serialize, JsonSchema, PartialEq, Eq, Copy, Hash, + Clone, Debug, Deserialize, Serialize, JsonSchema, PartialEq, Eq, Hash, )] #[serde(rename_all = "snake_case")] pub enum DendriteAsic { diff --git a/sled-agent/src/services.rs b/sled-agent/src/services.rs index 5698f2d83f..c5f63fd7f5 100644 --- a/sled-agent/src/services.rs +++ b/sled-agent/src/services.rs @@ -51,6 +51,7 @@ use omicron_common::nexus_config::{ }; use once_cell::sync::OnceCell; use sled_hardware::underlay; +use sled_hardware::ScrimletMode; use slog::Logger; use std::collections::HashSet; use std::iter::FromIterator; @@ -217,6 +218,10 @@ enum SwitchZone { request: ServiceZoneRequest, // A background task which keeps looping until the zone is initialized worker: Option, + // Filesystems for the switch zone to mount + // Since Softnpu is currently managed via a UNIX socket, we need to + // pass those files in to the SwitchZone so Dendrite can manage Softnpu + filesystems: Vec, }, // The Zone is currently running. Running { @@ -231,7 +236,7 @@ enum SwitchZone { pub struct ServiceManagerInner { log: Logger, switch_zone: Mutex, - stub_scrimlet: Option, + scrimlet_override: Option, sidecar_revision: String, zones: Mutex>, underlay_vnic_allocator: VnicAllocator, @@ -270,7 +275,7 @@ impl ServiceManager { underlay_etherstub: Etherstub, underlay_vnic: EtherstubVnic, bootstrap_etherstub: Etherstub, - stub_scrimlet: Option, + scrimlet_override: Option, sidecar_revision: String, switch_zone_bootstrap_address: Ipv6Addr, ) -> Result { @@ -282,7 +287,7 @@ impl ServiceManager { // TODO(https://github.com/oxidecomputer/omicron/issues/725): // Load the switch zone if it already exists? switch_zone: Mutex::new(SwitchZone::Disabled), - stub_scrimlet, + scrimlet_override, sidecar_revision, zones: Mutex::new(vec![]), underlay_vnic_allocator: VnicAllocator::new( @@ -495,6 +500,7 @@ impl ServiceManager { async fn initialize_zone( &self, request: &ServiceZoneRequest, + filesystems: &[zone::Fs], ) -> Result { let device_names = Self::devices_needed(request)?; let bootstrap_vnic = self.bootstrap_vnic_needed(request)?; @@ -515,6 +521,8 @@ impl ServiceManager { None, // dataset= &[], + // filesystems= + &filesystems, &devices, // opte_ports= vec![], @@ -806,7 +814,7 @@ impl ServiceManager { &format!("[{}]:{}", address, DENDRITE_PORT), )?; } - match *asic { + match asic { DendriteAsic::TofinoAsic => { // There should be exactly one device_name // associated with this zone: the /dev path for @@ -837,7 +845,13 @@ impl ServiceManager { "config/port_config", "/opt/oxide/dendrite/misc/model_config.toml", )?, - DendriteAsic::Softnpu => {} + DendriteAsic::Softnpu => { + smfh.setprop("config/mgmt", "uds")?; + smfh.setprop( + "config/uds_path", + "/opt/softnpu/stuff", + )?; + } }; smfh.refresh()?; } @@ -906,7 +920,13 @@ impl ServiceManager { ); } - let running_zone = self.initialize_zone(req).await?; + let running_zone = self + .initialize_zone( + req, + // filesystems= + &[], + ) + .await?; existing_zones.push(running_zone); } Ok(()) @@ -987,15 +1007,37 @@ impl ServiceManager { switch_zone_ip: Option, ) -> Result<(), Error> { info!(self.inner.log, "Ensuring scrimlet services (enabling services)"); - - let services = match self.inner.stub_scrimlet { - Some(_) => { - vec![ - ServiceType::Dendrite { asic: DendriteAsic::TofinoStub }, - ServiceType::ManagementGatewayService, - ServiceType::Wicketd, - ] - } + let mut filesystems: Vec = vec![]; + + let services = match self.inner.scrimlet_override { + Some(mode) => match mode { + ScrimletMode::Stub => { + vec![ + ServiceType::Dendrite { + asic: DendriteAsic::TofinoStub, + }, + ServiceType::ManagementGatewayService, + ServiceType::Wicketd, + ] + } + ScrimletMode::Softnpu => { + let softnpu_filesystem = zone::Fs { + ty: "lofs".to_string(), + dir: "/opt/softnpu/stuff".to_string(), + special: "/opt/oxide/softnpu/stuff".to_string(), + ..Default::default() + }; + filesystems.push(softnpu_filesystem); + vec![ + ServiceType::Dendrite { asic: DendriteAsic::Softnpu }, + ServiceType::ManagementGatewayService, + ServiceType::Wicketd, + ] + } + _ => { + vec![] + } + }, None => { vec![ ServiceType::ManagementGatewayService, @@ -1018,12 +1060,24 @@ impl ServiceManager { services, }; - self.ensure_switch_zone(Some(request)).await + self.ensure_switch_zone( + // request= + Some(request), + // filesystems= + filesystems, + ) + .await } /// Ensures that no switch zone is active. pub async fn deactivate_switch(&self) -> Result<(), Error> { - self.ensure_switch_zone(None).await + self.ensure_switch_zone( + // request= + None, + // filesystems= + vec![], + ) + .await } // Forcefully initialize a switch zone. @@ -1033,10 +1087,12 @@ impl ServiceManager { self, switch_zone: &mut SwitchZone, request: ServiceZoneRequest, + filesystems: Vec, ) { let (exit_tx, exit_rx) = oneshot::channel(); *switch_zone = SwitchZone::Initializing { request, + filesystems, worker: Some(Task { exit_tx, initializer: tokio::task::spawn(async move { @@ -1050,6 +1106,7 @@ impl ServiceManager { async fn ensure_switch_zone( &self, request: Option, + filesystems: Vec, ) -> Result<(), Error> { let log = &self.inner.log; let mut switch_zone = self.inner.switch_zone.lock().await; @@ -1057,7 +1114,11 @@ impl ServiceManager { match (&mut *switch_zone, request) { (SwitchZone::Disabled, Some(request)) => { info!(log, "Enabling switch zone (new)"); - self.clone().start_switch_zone(&mut switch_zone, request); + self.clone().start_switch_zone( + &mut switch_zone, + request, + filesystems, + ); } (SwitchZone::Initializing { request, .. }, Some(new_request)) => { info!(log, "Enabling switch zone (already underway)"); @@ -1146,8 +1207,11 @@ impl ServiceManager { { let mut switch_zone = self.inner.switch_zone.lock().await; match &*switch_zone { - SwitchZone::Initializing { request, .. } => { - match self.initialize_zone(&request).await { + SwitchZone::Initializing { + request, filesystems, .. + } => { + match self.initialize_zone(&request, filesystems).await + { Ok(zone) => { *switch_zone = SwitchZone::Running { request: request.clone(), @@ -1212,7 +1276,7 @@ mod test { ); // Install the Omicron Zone let install_ctx = MockZones::install_omicron_zone_context(); - install_ctx.expect().return_once(|_, name, _, _, _, _, _| { + install_ctx.expect().return_once(|_, name, _, _, _, _, _, _| { assert_eq!(name, EXPECTED_ZONE_NAME); Ok(()) }); diff --git a/sled-agent/src/sled_agent.rs b/sled-agent/src/sled_agent.rs index 65ec5122dd..6b92ccf3fb 100644 --- a/sled-agent/src/sled_agent.rs +++ b/sled-agent/src/sled_agent.rs @@ -16,7 +16,7 @@ use crate::params::{ }; use crate::services::{self, ServiceManager}; use crate::storage_manager::StorageManager; -use crate::updates::UpdateManager; +use crate::updates::{ConfigUpdates, UpdateManager}; use dropshot::HttpError; use illumos_utils::{execute, PFEXEC}; use omicron_common::address::{ @@ -33,6 +33,7 @@ use sled_hardware::underlay; use sled_hardware::HardwareManager; use slog::Logger; use std::net::{Ipv6Addr, SocketAddrV6}; +use std::path::PathBuf; use std::sync::Arc; use uuid::Uuid; @@ -269,10 +270,13 @@ impl SledAgent { request.gateway.address, ); - let hardware = HardwareManager::new(&parent_log, config.stub_scrimlet) - .map_err(|e| Error::Hardware(e))?; + let hardware = + HardwareManager::new(&parent_log, config.scrimlet_override) + .map_err(|e| Error::Hardware(e))?; - let updates = UpdateManager::new(config.updates.clone()); + let update_config = + ConfigUpdates { zone_artifact_path: PathBuf::from("/opt/oxide") }; + let updates = UpdateManager::new(update_config); services .sled_agent_started( diff --git a/sled-agent/src/storage_manager.rs b/sled-agent/src/storage_manager.rs index 472ac48b71..b6f01aa3a3 100644 --- a/sled-agent/src/storage_manager.rs +++ b/sled-agent/src/storage_manager.rs @@ -502,6 +502,7 @@ async fn ensure_running_zone( Some(&dataset_name.pool_name), &[zone::Dataset { name: dataset_name.full() }], &[], + &[], vec![], None, None, diff --git a/sled-hardware/Cargo.toml b/sled-hardware/Cargo.toml index 38f2630460..5f7e9ce4c5 100644 --- a/sled-hardware/Cargo.toml +++ b/sled-hardware/Cargo.toml @@ -12,6 +12,7 @@ futures.workspace = true illumos-utils.workspace = true libc.workspace = true nexus-client.workspace = true +serde.workspace = true slog.workspace = true thiserror.workspace = true tofino.workspace = true diff --git a/sled-hardware/src/illumos/mod.rs b/sled-hardware/src/illumos/mod.rs index 624abf3f7f..3a9da30a2e 100644 --- a/sled-hardware/src/illumos/mod.rs +++ b/sled-hardware/src/illumos/mod.rs @@ -3,7 +3,8 @@ // file, You can obtain one at https://mozilla.org/MPL/2.0/. use crate::{ - Baseboard, DiskIdentity, DiskVariant, HardwareUpdate, UnparsedDisk, + Baseboard, DiskIdentity, DiskVariant, HardwareUpdate, ScrimletMode, + UnparsedDisk, }; use illumos_devinfo::{DevInfo, DevLinkType, DevLinks, Node, Property}; use slog::debug; @@ -518,13 +519,13 @@ impl HardwareManager { /// a task which periodically updates that representation. /// /// Arguments: - /// - `stub_scrimlet`: Identifies if we should ignore the attached Tofino - /// device, and assume the device is a scrimlet (true) or gimlet (false). + /// - `scrimlet_override`: Identifies if we should ignore the attached Tofino + /// device (`Disabled`), use the `Softnpu` asic, or use the `Stub` asic. /// If this argument is not supplied, we assume the device is a gimlet until - /// device scanning informs us otherwise. + /// device scanning informs us otherwise (normal behavior). pub fn new( log: &Logger, - stub_scrimlet: Option, + scrimlet_override: Option, ) -> Result { let log = log.new(o!("component" => "HardwareManager")); info!(log, "Creating HardwareManager"); @@ -534,11 +535,22 @@ impl HardwareManager { // receiver will receive a tokio::sync::broadcast::error::RecvError::Lagged // error, indicating they should re-scan the hardware themselves. let (tx, _) = broadcast::channel(1024); - let hw = match stub_scrimlet { - None => HardwareView::new().map_err(|e| e.to_string())?, - Some(active) => HardwareView::new_stub_tofino(active) - .map_err(|e| e.to_string())?, - }; + let hw = match scrimlet_override { + None => HardwareView::new(), + Some(mode) => match mode { + ScrimletMode::Disabled => HardwareView::new_stub_tofino( + // active= + false, + ), + ScrimletMode::Stub => HardwareView::new_stub_tofino(true), + // TODO: correctness + // I'm not sure whether or not we should be treating softnpu + // as a stub or treating it as a different HardwareView variant, + // so this might change. + ScrimletMode::Softnpu => HardwareView::new_stub_tofino(true), + }, + } + .map_err(|e| e.to_string())?; let inner = Arc::new(Mutex::new(hw)); // Force the device tree to be polled at least once before returning. diff --git a/sled-hardware/src/lib.rs b/sled-hardware/src/lib.rs index 7a98a85907..daa4f60687 100644 --- a/sled-hardware/src/lib.rs +++ b/sled-hardware/src/lib.rs @@ -5,6 +5,7 @@ use illumos_utils::fstyp::Fstyp; use illumos_utils::zpool::Zpool; use illumos_utils::zpool::ZpoolName; +use serde::Deserialize; use slog::Logger; use slog::{info, warn}; use std::path::PathBuf; @@ -39,6 +40,20 @@ pub enum HardwareUpdate { DiskRemoved(UnparsedDisk), } +/// Configuration for forcing a sled to run as a Scrimlet +#[derive(Clone, Debug, Deserialize, Copy)] +#[serde(rename_all = "snake_case")] +pub enum ScrimletMode { + /// Force sled to run as a Gimlet + /// this is to preserve the old behavior of `stub_scrimlet = false`, + /// but I haven't found where that logic has actually been leveraged... + Disabled, + /// Force sled to run in Scrimlet mode with a stub switch + Stub, + /// Force sled to run in Scrimlet mode with a Softnpu switch + Softnpu, +} + /// Describes properties that should uniquely identify a Gimlet. #[derive(Clone, Debug)] pub struct Baseboard { diff --git a/sled-hardware/src/non_illumos/mod.rs b/sled-hardware/src/non_illumos/mod.rs index 4f7c4927fd..e2f4795654 100644 --- a/sled-hardware/src/non_illumos/mod.rs +++ b/sled-hardware/src/non_illumos/mod.rs @@ -3,7 +3,8 @@ // file, You can obtain one at https://mozilla.org/MPL/2.0/. use crate::{ - Baseboard, DiskError, DiskPaths, DiskVariant, Partition, UnparsedDisk, + Baseboard, DiskError, DiskPaths, DiskVariant, Partition, ScrimletMode, + UnparsedDisk, }; use slog::Logger; use std::collections::HashSet; @@ -22,7 +23,7 @@ pub struct HardwareManager {} impl HardwareManager { pub fn new( _log: &Logger, - _stub_scrimlet: Option, + _scrimlet_override: Option, ) -> Result { unimplemented!("Accessing hardware unsupported on non-illumos"); } diff --git a/smf/nexus/config-partial.toml b/smf/nexus/config-partial.toml index 2dedbc4162..0ee174770b 100644 --- a/smf/nexus/config-partial.toml +++ b/smf/nexus/config-partial.toml @@ -15,7 +15,7 @@ schemes_external = ["spoof", "session_cookie", "access_token"] [log] # Show log messages of this level and more severe -level = "info" +level = "debug" mode = "file" path = "/dev/stdout" if_exists = "append" @@ -23,3 +23,7 @@ if_exists = "append" # Configuration for interacting with the timeseries database [timeseries_db] address = "[fd00:1122:3344:0101::6]:8123" + +# Configuration for interacting with the dataplane daemon +[dendrite] +address = "[fd00:1122:3344:101::2]:12224" diff --git a/smf/sled-agent/non-gimlet/config.toml b/smf/sled-agent/non-gimlet/config.toml index a429b2df09..5ae3088bac 100644 --- a/smf/sled-agent/non-gimlet/config.toml +++ b/smf/sled-agent/non-gimlet/config.toml @@ -2,12 +2,15 @@ # Identifies if the sled should be unconditionally treated as a scrimlet. # -# If this is set to "true", the sled agent treats itself as a scrimlet. -# If this is set to "false", the sled agent treats itself as a gimlet. +# If this is set to "stub", the sled agent treats itself as a scrimlet +# with a stub switch. +# If this is set to "softnpu", the sled agent treats itself as a scrimlet +# with a softnpu switch. +# If this is set to "disabled", the sled agent treats itself as a gimlet. # If this is unset: # - On illumos, the sled automatically detects whether or not it is a scrimlet. # - On all other platforms, the sled assumes it is a gimlet. -# stub_scrimlet = true +scrimlet_override = "softnpu" # Identifies the revision of the sidecar that is attached, if one is attached. # TODO: This field should be removed once Gimlets have the ability to auto-detect diff --git a/tools/ci_download_dendrite_openapi b/tools/ci_download_dendrite_openapi new file mode 100755 index 0000000000..0e4610b90e --- /dev/null +++ b/tools/ci_download_dendrite_openapi @@ -0,0 +1,86 @@ +#!/bin/bash + +# +# ci_download_dendrite_openapi: fetches the appropriate dendrite openapi spec. +# + +set -o pipefail +set -o xtrace +set -o errexit + +SOURCE_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" +ARG0="$(basename ${BASH_SOURCE[0]})" + +TARGET_DIR="out" +# Location where intermediate artifacts are downloaded / unpacked. +DOWNLOAD_DIR="$TARGET_DIR/downloads" + +source "$SOURCE_DIR/dendrite_openapi_version" + +URL="https://buildomat.eng.oxide.computer/public/file/oxidecomputer/dendrite/openapi/$COMMIT/dpd.json" +LOCAL_FILE="$DOWNLOAD_DIR/dpd-$COMMIT.json" + +function main +{ + if [[ $# != 0 ]]; then + echo "unexpected arguments" >&2 + exit 2 + fi + + # Download the file. + echo "URL: $URL" + echo "Local file: $LOCAL_FILE" + + local DO_DOWNLOAD="true" + if [[ -f "$LOCAL_FILE" ]]; then + calculated_sha2="$(do_sha256sum "$LOCAL_FILE")" || \ + fail "failed to calculate sha256sum" + if [[ "$calculated_sha2" == "$SHA2" ]]; then + DO_DOWNLOAD="false" + fi + fi + + mkdir -p "$DOWNLOAD_DIR" + + if [ "$DO_DOWNLOAD" == "true" ]; then + echo "Downloading..." + do_download_curl "$URL" "$LOCAL_FILE" || \ + fail "failed to download file" + + # Verify the sha256sum. + calculated_sha2="$(do_sha256sum "$LOCAL_FILE")" || \ + fail "failed to calculate sha256sum" + if [[ "$calculated_sha2" != "$SHA2" ]]; then + fail "sha256sum mismatch \ + (expected $SHA2, found $calculated_sha2)" + fi + + fi +} + +function fail +{ + echo "$ARG0: $@" >&2 + exit 1 +} + +function do_download_curl +{ + curl --silent --show-error --fail --location --output "$2" "$1" +} + +function do_sha256sum +{ + case "$OSTYPE" in + darwin*) + SHA="shasum -a 256" + ;; + *) + SHA="sha256sum" + ;; + esac + + $SHA < "$1" | awk '{print $1}' +} + +main "$@" diff --git a/tools/create_virtual_hardware.sh b/tools/create_virtual_hardware.sh index 63827b166d..b44e4708c2 100755 --- a/tools/create_virtual_hardware.sh +++ b/tools/create_virtual_hardware.sh @@ -24,11 +24,7 @@ if [[ -f "$MARKER" ]]; then fi # Select the physical link over which to simulate the Chelsio links -if [[ $# -ge 1 ]]; then - PHYSICAL_LINK="$1" -else - PHYSICAL_LINK="$(dladm show-phys -p -o LINK | head -1)" -fi +PHYSICAL_LINK=${PHYSICAL_LINK:=$(dladm show-phys -p -o LINK | head -1)} echo "Using $PHYSICAL_LINK as physical link" function success { @@ -66,20 +62,39 @@ function get_vnic_name_if_exists { dladm show-vnic -p -o LINK "$1" 2> /dev/null || echo "" } -# Create VNICs to represent the Chelsio physical links +function get_simnet_name_if_exists { + dladm show-simnet -p -o LINK "$1" 2> /dev/null || echo "" +} + +# Create virtual links to represent the Chelsio physical links # # Arguments: # $1: Optional name of the physical link to use. If not provided, use the # first physical link available on the machine. function ensure_simulated_chelsios { local PHYSICAL_LINK="$1" - VNIC_NAMES=("net0" "net1") - for VNIC in "${VNIC_NAMES[@]}"; do - if [[ -z "$(get_vnic_name_if_exists "$VNIC")" ]]; then - dladm create-vnic -t -l "$PHYSICAL_LINK" "$VNIC" + INDICES=("0" "1") + for I in "${INDICES[@]}"; do + if [[ -z "$(get_simnet_name_if_exists "net$I")" ]]; then + # sidecar ports + dladm create-simnet -t "net$I" + dladm create-simnet -t "sc${I}_0" + dladm modify-simnet -t -p "net$I" "sc${I}_0" + dladm set-linkprop -p mtu=1600 "net$I" # encap headroom + dladm set-linkprop -p mtu=1600 "sc${I}_0" # encap headroom + + # corresponding scrimlet ports + dladm create-simnet -t "sr0_$I" + dladm create-simnet -t "scr0_$I" + dladm modify-simnet -t -p "sr0_$I" "scr0_$I" fi - success "VNIC $VNIC exists" + success "Simnet net$I/sc${I}_0/sr0_$I/scr0_$I exists" done + + if [[ -z "$(get_vnic_name_if_exists "sc0_1")" ]]; then + dladm create-vnic -t "sc0_1" -l $PHYSICAL_LINK -m a8:e1:de:01:70:1d + fi + success "Vnic sc0_1 exists" } function ensure_run_as_root { @@ -89,6 +104,51 @@ function ensure_run_as_root { fi } +function ensure_softnpu_zone { + zoneadm list | grep -q softnpu || { + mkdir -p /softnpu-zone + mkdir -p /opt/oxide/softnpu/stuff + cp tools/scrimlet/softnpu.toml /opt/oxide/softnpu/stuff/ + cp tools/scrimlet/softnpu-init.sh /opt/oxide/softnpu/stuff/ + cp out/softnpu/libsidecar_lite.so /opt/oxide/softnpu/stuff/ + cp out/softnpu/softnpu /opt/oxide/softnpu/stuff/ + cp out/softnpu/scadm /opt/oxide/softnpu/stuff/ + + zfs create -p -o mountpoint=/softnpu-zone rpool/softnpu-zone + + zonecfg -z softnpu -f tools/scrimlet/softnpu-zone.txt + zoneadm -z softnpu install + zoneadm -z softnpu boot + } + success "softnpu zone exists" +} + +function enable_softnpu { + zlogin softnpu uname -a || { + echo "softnpu zone not ready" + exit 1 + } + zlogin softnpu pgrep softnpu || { + zlogin softnpu /stuff/softnpu /stuff/softnpu.toml & + } + success "softnpu started" +} + +function ensure_ext_ip_hack_disabled { + grep "ext_ip_hack = 1;" /kernel/drv/xde.conf && { + sed -i 's/ext_ip_hack = 1;/ext_ip_hack = 0;/g' /kernel/drv/xde.conf + } + + grep "ext_ip_hack = 0;" /kernel/drv/xde.conf || { + echo "failed to disable ext_ip_hack" + exit 1 + } + success "ext_ip_hack disabled" +} + ensure_run_as_root +ensure_ext_ip_hack_disabled ensure_zpools ensure_simulated_chelsios "$PHYSICAL_LINK" +ensure_softnpu_zone +enable_softnpu diff --git a/tools/dendrite_openapi_version b/tools/dendrite_openapi_version new file mode 100644 index 0000000000..c47e524b33 --- /dev/null +++ b/tools/dendrite_openapi_version @@ -0,0 +1,2 @@ +COMMIT="231ad14027c01df2ce239b455220a90a24d4afc7" +SHA2="0e7019f0ab0331f7386da0c198da74e5ccbf7c96fed118f34bfda32762ae526a" diff --git a/tools/destroy_virtual_hardware.sh b/tools/destroy_virtual_hardware.sh index b6db5abe0d..5a48acc5a1 100755 --- a/tools/destroy_virtual_hardware.sh +++ b/tools/destroy_virtual_hardware.sh @@ -79,11 +79,25 @@ function try_remove_vnic { success "Verified VNIC link $LINK does not exist" } +function try_remove_simnet { + local LINK="$1" + if [[ "$(dladm show-simnet -p -o LINK "$LINK")" ]]; then + dladm delete-simnet -t "$LINK" || warn "Failed to delete simnet link $LINK" + fi + success "Verified simnet link $LINK does not exist" +} + function try_remove_vnics { try_remove_address "lo0/underlay" - VNIC_LINKS=("net0" "net1") - for LINK in "${VNIC_LINKS[@]}"; do - try_remove_interface "$LINK" && try_remove_vnic "$LINK" + try_remove_interface sc0_1 + try_remove_vnic sc0_1 + INDICES=("0" "1") + for I in "${INDICES[@]}"; do + try_remove_interface "net$I" + try_remove_simnet "net$I" + try_remove_simnet "sc${I}_0" + try_remove_simnet "sr0_$I" + try_remove_simnet "scr0_$I" done } @@ -101,7 +115,18 @@ function try_destroy_zpools { done } +function remove_softnpu_zone { + zoneadm -z softnpu halt + zoneadm -z softnpu uninstall -F + zonecfg -z softnpu delete -F + + rm -rf /opt/oxide/softnpu/stuff + zfs destroy rpool/softnpu-zone + rm -rf /softnpu-zone +} + verify_omicron_uninstalled unload_xde_driver +remove_softnpu_zone try_remove_vnics try_destroy_zpools diff --git a/tools/install_builder_prerequisites.sh b/tools/install_builder_prerequisites.sh index ae46b7224e..12efdffc3f 100755 --- a/tools/install_builder_prerequisites.sh +++ b/tools/install_builder_prerequisites.sh @@ -143,6 +143,10 @@ fi # Download the OpenAPI spec for maghemite. This is required to build the # ddm-admin-api crate. ./tools/ci_download_maghemite_openapi +# +# Download the OpenAPI spec for dendrite. This is required to build the +# dpd-client crate. +./tools/ci_download_dendrite_openapi # Validate the PATH: expected_in_path=( diff --git a/tools/install_softnpu_machinery.sh b/tools/install_softnpu_machinery.sh new file mode 100755 index 0000000000..6b1c86a283 --- /dev/null +++ b/tools/install_softnpu_machinery.sh @@ -0,0 +1,40 @@ +#!/bin/bash +# +# This script fetches the following from CI +# +# - the softnpu ASIC simulator (softnpu) +# - a softnpu admin program (scadm) +# - the sidecar-lite precompiled P4 program +# + +# This is the softnpu ASIC emulator +if [[ ! -f out/softnpu/softnpu ]]; then + echo "fetching softnpu" + curl -OL https://buildomat.eng.oxide.computer/wg/0/artefact/01GTD3CPEENJZ9K1VA0J3GYD5D/WZ2Rw4MGOeSr06SEfmyuMfsp7i5rgwzvENWnAUjShI8FGryp/01GTD3D91TCD903Z0BAYSA31JR/01GTD3T8VFSZTE59Y0SVAR4CWC/softnpu + chmod +x softnpu + mkdir -p out/softnpu + mv softnpu out/softnpu/ +fi + +# This is an ASIC administration program. +if [[ ! -f out/softnpu/scadm ]]; then + echo "fetching scadm" + curl -OL https://buildomat.eng.oxide.computer/wg/0/artefact/01GTD3Y38K0Q14F2989R20QCBA/Ny1N0clhEwz2nfbsdTY8ta7pd9IYy1ofxG6ViCDN6Uy4xW3F/01GTD3YZ0TC9SCB4TFNYWP0DEM/01GTD4CCW2F8Q0NQGD7WEY91D0/scadm + chmod +x scadm + mv scadm out/softnpu/ +fi + +# Fetch the pre-compiled sidecar_lite p4 program +if [[ ! -f out/softnpu/libsidecar_lite.so ]]; then + echo "fetching libsidecar_lite.so" + curl -OL https://buildomat.eng.oxide.computer/wg/0/artefact/01GTD3Y38K0Q14F2989R20QCBA/Ny1N0clhEwz2nfbsdTY8ta7pd9IYy1ofxG6ViCDN6Uy4xW3F/01GTD3YZ0TC9SCB4TFNYWP0DEM/01GTD4CA8KRGR8HK4S6B0DEEJC/libsidecar_lite.so + mv libsidecar_lite.so out/softnpu/ +fi + +# This is the CLI client for dendrite +if [[ ! -f out/softnpu/swadm ]]; then + echo "fetching swadm" + curl -OL https://buildomat.eng.oxide.computer/wg/0/artefact/01GVRJ8RKWX9R26DX39KMGYZ3Z/IXahhCNnTV5VY8QPRWC9acX9ZNaDFLnY7TyTOZ0ch3rnHFqs/01GVRJ9QZ278DAD31FVSD5NH2A/01GVRKVC0F833P1XR0W9W8X6N7/swadm + chmod +x swadm + mv swadm out/softnpu/ +fi diff --git a/tools/scrimlet/create-softnpu-zone.sh b/tools/scrimlet/create-softnpu-zone.sh new file mode 100755 index 0000000000..65f7a9d56f --- /dev/null +++ b/tools/scrimlet/create-softnpu-zone.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +set -x +set -e + +mkdir -p /softnpu-zone +mkdir -p /opt/oxide/softnpu/stuff +cp tools/scrimlet/softnpu.toml /opt/oxide/softnpu/stuff/ +cp tools/scrimlet/softnpu-init.sh /opt/oxide/softnpu/stuff/ +cp out/softnpu/libsidecar_lite.so /opt/oxide/softnpu/stuff/ +cp out/softnpu/softnpu /opt/oxide/softnpu/stuff/ +cp out/softnpu/scadm /opt/oxide/softnpu/stuff/ + +zfs create -p -o mountpoint=/softnpu-zone rpool/softnpu-zone + +pkg set-publisher --search-first helios-dev + +zonecfg -z softnpu -f tools/scrimlet/softnpu-zone.txt +zoneadm -z softnpu install +zoneadm -z softnpu boot + +pkg set-publisher --search-first helios-netdev diff --git a/tools/scrimlet/destroy-softnpu-zone.sh b/tools/scrimlet/destroy-softnpu-zone.sh new file mode 100755 index 0000000000..2baba6ccaa --- /dev/null +++ b/tools/scrimlet/destroy-softnpu-zone.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +set -x +set -e + +zoneadm -z softnpu halt +zoneadm -z softnpu uninstall +zonecfg -z softnpu delete + +zfs destroy -r rpool/softnpu-zone +rm -rf /opt/oxide/softnpu/stuff +rm -rf /softnpu-zone diff --git a/tools/scrimlet/softnpu-init.sh b/tools/scrimlet/softnpu-init.sh new file mode 100755 index 0000000000..e29f9bb348 --- /dev/null +++ b/tools/scrimlet/softnpu-init.sh @@ -0,0 +1,37 @@ +#!/bin/bash + +set -e +set -x + +# Gateway ip is automatically configured based on the default route on your development machine +# Can be overridden by setting GATEWAY_IP +GATEWAY_IP=${GATEWAY_IP:=$(netstat -rn -f inet | grep default | awk -F ' ' '{print $2}')} +echo "Using $GATEWAY_IP as gateway ip" + +# Gateway mac is determined automatically by inspecting the arp table on the development machine +gateway_mac=$(arp "$GATEWAY_IP" | awk -F ' ' '{print $4}') + +# Sidecar Interface facing "sled" +./out/softnpu/swadm -h "[fd00:1122:3344:101::2]" port create 1:0 100G RS + +# Sidecar Interface facing external network +./out/softnpu/swadm -h "[fd00:1122:3344:101::2]" port create 2:0 100G RS + +# Configure sidecar local ipv6 addresses +./out/softnpu/swadm -h "[fd00:1122:3344:101::2]" addr add 1:0 fe80::aae1:deff:fe01:701c +./out/softnpu/swadm -h "[fd00:1122:3344:101::2]" addr add 2:0 fe80::aae1:deff:fe01:701d +./out/softnpu/swadm -h "[fd00:1122:3344:101::2]" addr add 1:0 fd00:99::1 + +# Configure route to oxide rack +./out/softnpu/swadm -h "[fd00:1122:3344:101::2]" route add fd00:1122:3344:0101::/64 1:0 fe80::aae1:deff:fe00:1 +./out/softnpu/swadm -h "[fd00:1122:3344:101::2]" arp add fe80::aae1:deff:fe00:1 a8:e1:de:00:00:01 + +# Configure default route +./out/softnpu/swadm -h "[fd00:1122:3344:101::2]" route add 0.0.0.0/0 2:0 "$GATEWAY_IP" +./out/softnpu/swadm -h "[fd00:1122:3344:101::2]" arp add "$GATEWAY_IP" "$gateway_mac" + + +./out/softnpu/swadm -h "[fd00:1122:3344:101::2]" port list +./out/softnpu/swadm -h "[fd00:1122:3344:101::2]" addr list +./out/softnpu/swadm -h "[fd00:1122:3344:101::2]" route list +./out/softnpu/swadm -h "[fd00:1122:3344:101::2]" arp list diff --git a/tools/scrimlet/softnpu-zone.txt b/tools/scrimlet/softnpu-zone.txt new file mode 100644 index 0000000000..5e7c844754 --- /dev/null +++ b/tools/scrimlet/softnpu-zone.txt @@ -0,0 +1,23 @@ +create +set brand=omicron1 +set zonepath=/softnpu-zone +set ip-type=exclusive +set autoboot=false +add net + set physical=sc0_0 +end +add net + set physical=sr0_0 +end +add net + set physical=sr0_1 +end +add net + set physical=sc0_1 +end +add fs + set dir=/stuff + set special=/opt/oxide/softnpu/stuff + set type=lofs +end +commit diff --git a/tools/scrimlet/softnpu.toml b/tools/scrimlet/softnpu.toml new file mode 100644 index 0000000000..c1007ecbb5 --- /dev/null +++ b/tools/scrimlet/softnpu.toml @@ -0,0 +1,5 @@ +p4_program = "/stuff/libsidecar_lite.so" +ports = [ + { sidecar = "sc0_0", scrimlet = "sr0_0", mtu = 1600 }, + { sidecar = "sc0_1", scrimlet = "sr0_1", mtu = 1500 }, +] diff --git a/tools/setup_path.sh b/tools/setup_path.sh new file mode 100644 index 0000000000..5d7404ef59 --- /dev/null +++ b/tools/setup_path.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +export PATH="`pwd`/out/cockroachdb/bin:$PATH" +export PATH="`pwd`/out/clickhouse:$PATH"