diff --git a/.github/buildomat/jobs/a4x2-deploy.sh b/.github/buildomat/jobs/a4x2-deploy.sh new file mode 100755 index 0000000000..a07f953281 --- /dev/null +++ b/.github/buildomat/jobs/a4x2-deploy.sh @@ -0,0 +1,205 @@ +#!/bin/bash +#: +#: name = "a4x2-deploy" +#: variety = "basic" +#: target = "lab-2.0-opte-0.27" +#: rust_toolchain = "stable" +#: output_rules = [ +#: "/out/falcon/*.log", +#: "/out/falcon/*.err", +#: "/out/connectivity-report.json", +#: "/ci/out/*-sled-agent.log", +#: "/ci/out/*cockroach*.log", +#: "%/out/dhcp-server.log", +#: ] +#: skip_clone = true +#: enable = false +#: +#: [dependencies.a4x2] +#: job = "a4x2-prepare" + +set -o errexit +set -o pipefail +set -o xtrace + +pfexec mkdir -p /out +pfexec chown "$UID" /out + +# +# If we fail, try to collect some debugging information +# +_exit_trap() { + local status=$? + [[ $status -eq 0 ]] && exit 0 + + set +o errexit + + df -h + + # show what services have issues + for gimlet in g0 g1 g2 g3; do + ./a4x2 exec $gimlet "svcs -xvZ" + done + + mkdir -p /out/falcon + cp .falcon/* /out/falcon/ + for x in ce cr1 cr2 g0 g1 g2 g3; do + mv /out/falcon/$x.out /out/falcon/$x.log + done + cp connectivity-report.json /out/ + + mkdir -p /ci/out + + for gimlet in g0 g1 g2 g3; do + ./a4x2 exec \ + $gimlet \ + "cat /var/svc/log/oxide-sled-agent:default.log" > \ + /ci/out/$gimlet-sled-agent.log + done + + # collect cockroachdb logs + mkdir -p /ci/log + for gimlet in g0 g1 g2 g3; do + ./a4x2 exec $gimlet 'cat /pool/ext/*/crypt/zone/oxz_cockroachdb*/root/data/logs/cockroach.log' > \ + /ci/out/$gimlet-cockroach.log + + ./a4x2 exec $gimlet 'cat /pool/ext/*/crypt/zone/oxz_cockroachdb*/root/data/logs/cockroach-stderr.log' > \ + /ci/out/$gimlet-cockroach-stderr.log + + ./a4x2 exec $gimlet 'cat /pool/ext/*/crypt/zone/oxz_cockroachdb*/root/data/logs/cockroach-health.log' > \ + /ci/out/$gimlet-cockroach-health.log + + ./a4x2 exec $gimlet 'cat /pool/ext/*/crypt/zone/oxz_cockroachdb*/root/var/svc/log/oxide-cockroachdb:default.log*' > \ + /ci/out/$gimlet-oxide-cockroachdb.log + done +} +trap _exit_trap EXIT + +# +# Install propolis +# +curl -fOL https://buildomat.eng.oxide.computer/wg/0/artefact/01HJ4BJJY2Q9EKXHYV6HQZ8XPN/qQS2fnkS9LebcL4cDLeHRWdleSiXaGKEXGLDucRoab8pwBSi/01HJ4BJY5F995ET252YSD4NJWV/01HJ4CGFH946THBF0ZRH6SRM8X/propolis-server +chmod +x propolis-server +pfexec mv propolis-server /usr/bin/ + +# +# Make space for CI work +# +export DISK=${DISK:-c1t1d0} +pfexec diskinfo +pfexec zpool create -f cpool $DISK +pfexec zfs create -o mountpoint=/ci cpool/ci + +if [[ $(curl -s http://catacomb.eng.oxide.computer:12346/trim-me) =~ "true" ]]; then + pfexec zpool trim cpool + while [[ ! $(zpool status -t cpool) =~ "100%" ]]; do sleep 10; done +fi + +pfexec chown "$UID" /ci +cd /ci + +# +# Fetch and decompress the cargo bay from the a4x2-prepeare job +# +for x in ce cr1 cr2 omicron-common g0 g1 g2 g3 tools; do + tar -xvzf /input/a4x2/out/cargo-bay-$x.tgz +done + +for sled in g0 g1 g2 g3; do + cp -r cargo-bay/omicron-common/omicron/out/* cargo-bay/$sled/omicron/out/ +done +ls -R + +# +# Fetch the a4x2 topology manager program +# +buildomat_url=https://buildomat.eng.oxide.computer +testbed_artifact_path=public/file/oxidecomputer/testbed/topo/ +testbed_rev=677559e30b4dfc65c374b24336ac23d40102de81 +curl -fOL $buildomat_url/$testbed_artifact_path/$testbed_rev/a4x2 +chmod +x a4x2 + +# +# Create a zpool for falcon images and disks +# + +# +# Install falcon base images +# +export FALCON_DATASET=cpool/falcon +images="debian-11.0_0 helios-2.0_0" +for img in $images; do + file=$img.raw.xz + curl -OL http://catacomb.eng.oxide.computer:12346/falcon/$file + unxz --keep -T 0 $file + + file=$img.raw + name=${img%_*} + fsize=`ls -l $img.raw | awk '{print $5}'` + let vsize=(fsize + 4096 - size%4096) + + pfexec zfs create -p -V $vsize -o volblocksize=4k "$FALCON_DATASET/img/$name" + pfexec dd if=$img.raw of="/dev/zvol/rdsk/$FALCON_DATASET/img/$name" bs=1024k status=progress + pfexec zfs snapshot "$FALCON_DATASET/img/$name@base" +done + +# +# Install OVMF +# +curl -fOL http://catacomb.eng.oxide.computer:12346/falcon/OVMF_CODE.fd +pfexec mkdir -p /var/ovmf +pfexec cp OVMF_CODE.fd /var/ovmf/OVMF_CODE.fd + +# +# Fetch the arista image +# +curl -fOL http://catacomb.eng.oxide.computer:12346/falcon/arista.gz.xz +unxz arista.gz.xz +pfexec zfs receive cpool/falcon/img/arista@base < arista.gz + +# +# Run the VM dhcp server +# +export EXT_INTERFACE=${EXT_INTERFACE:-igb0} + +cp /input/a4x2/out/dhcp-server . +chmod +x dhcp-server +first=`bmat address ls -f extra -Ho first` +last=`bmat address ls -f extra -Ho last` +gw=`bmat address ls -f extra -Ho gateway` +server=`ipadm show-addr $EXT_INTERFACE/dhcp -po ADDR | sed 's#/.*##g'` +pfexec ./dhcp-server $first $last $gw $server &> /out/dhcp-server.log & + +# +# Run the topology +# +pfexec ./a4x2 launch + +# +# Add a route to the rack ip pool +# + +# Get the DHCP address for the external interface of the customer edge VM. This +# VM interface is attached to the host machine's external interface via viona. +customer_edge_addr=$(./a4x2 exec ce \ + "ip -4 -j addr show enp0s10 | jq -r '.[0].addr_info[] | select(.dynamic == true) | .local'") + +# Add the route to the rack via the customer edge VM +pfexec dladm +pfexec ipadm +pfexec netstat -nr +pfexec route add 198.51.100.0/24 $customer_edge_addr + +# +# Run the communications test program +# +cp /input/a4x2/out/commtest . +chmod +x commtest +pfexec ./commtest http://198.51.100.23 run \ + --ip-pool-begin 198.51.100.40 \ + --ip-pool-end 198.51.100.70 \ + --icmp-loss-tolerance 10 \ + --test-duration 300s \ + --packet-rate 30 + +cp connectivity-report.json /out/ diff --git a/.github/buildomat/jobs/a4x2-prepare.sh b/.github/buildomat/jobs/a4x2-prepare.sh new file mode 100755 index 0000000000..bc88ddd4c0 --- /dev/null +++ b/.github/buildomat/jobs/a4x2-prepare.sh @@ -0,0 +1,94 @@ +#!/bin/bash +#: +#: name = "a4x2-prepare" +#: variety = "basic" +#: target = "helios-2.0" +#: rust_toolchain = "stable" +#: output_rules = [ +#: "=/out/cargo-bay-ce.tgz", +#: "=/out/cargo-bay-cr1.tgz", +#: "=/out/cargo-bay-cr2.tgz", +#: "=/out/cargo-bay-g0.tgz", +#: "=/out/cargo-bay-g1.tgz", +#: "=/out/cargo-bay-g2.tgz", +#: "=/out/cargo-bay-g3.tgz", +#: "=/out/cargo-bay-tools.tgz", +#: "=/out/cargo-bay-omicron-common.tgz", +#: "=/out/commtest", +#: "=/out/dhcp-server", +#: ] +#: access_repos = [ +#: "oxidecomputer/testbed", +#: ] +#: enable = false + +source ./env.sh + +set -o errexit +set -o pipefail +set -o xtrace + +pfexec mkdir -p /out +pfexec chown "$UID" /out + +# +# Prep to build omicron +# +banner "prerequisites" +set -o xtrace +./tools/install_builder_prerequisites.sh -y + +# +# Build the commtest program and place in the output +# +banner "commtest" +cargo build -p end-to-end-tests --bin commtest --bin dhcp-server --release +cp target/release/commtest /out/ +cp target/release/dhcp-server /out/ + +# +# Clone the testbed repo +# +banner "testbed" +cd /work/oxidecomputer +rm -rf testbed +git clone https://github.com/oxidecomputer/testbed +cd testbed/a4x2 + +# +# Build the a4x2 cargo bay using the omicron sources in this branch, fetch the +# softnpu artifacts into the cargo bay, zip up the cargo bay and place it in the +# output. +# +OMICRON=/work/oxidecomputer/omicron ./config/build-packages.sh + +# Create an omicron archive that captures common assets + +pushd cargo-bay +mkdir -p omicron-common/omicron/ +cp -r g0/omicron/out omicron-common/omicron/ +# sled agent, gateway and switch archives are sled-specific +rm omicron-common/omicron/out/omicron-sled-agent.tar +rm omicron-common/omicron/out/omicron-gateway* +rm omicron-common/omicron/out/switch-softnpu.tar.gz +popd + +# Remove everything in $sled/omicron/out except sled-agent, mgs (gateway), and +# switch tar archives, these common elements are in the omicron-common archive +for sled in g0 g1 g2 g3; do + find cargo-bay/$sled/omicron/out/ -maxdepth 1 -mindepth 1 \ + | grep -v sled-agent \ + | grep -v omicron-gateway \ + | grep -v switch-softnpu \ + | xargs -l rm -rf +done + +# Put the softnpu artifacts in place. +./config/fetch-softnpu-artifacts.sh + +# Archive everything up and place it in the output +for x in ce cr1 cr2 g0 g1 g2 g3 tools omicron-common; do + tar -czf cargo-bay-$x.tgz cargo-bay/$x + mv cargo-bay-$x.tgz /out/ +done + diff --git a/.github/buildomat/jobs/ci-tools.sh b/.github/buildomat/jobs/ci-tools.sh index 07a63af30c..ce17d4fb30 100755 --- a/.github/buildomat/jobs/ci-tools.sh +++ b/.github/buildomat/jobs/ci-tools.sh @@ -8,6 +8,7 @@ #: "=/work/end-to-end-tests/*.gz", #: "=/work/caboose-util.gz", #: "=/work/tufaceous.gz", +#: "=/work/commtest", #: ] set -o errexit @@ -33,6 +34,10 @@ export CARGO_INCREMENTAL=0 ptime -m cargo build --locked -p end-to-end-tests --tests --bin bootstrap \ --message-format json-render-diagnostics >/tmp/output.end-to-end.json +mkdir -p /work +ptime -m cargo build --locked -p end-to-end-tests --tests --bin commtest +cp target/debug/commtest /work/commtest + mkdir -p /work/end-to-end-tests for p in target/debug/bootstrap $(/opt/ooce/bin/jq -r 'select(.profile.test) | .executable' /tmp/output.end-to-end.json); do # shellcheck disable=SC2094 diff --git a/.github/buildomat/jobs/package.sh b/.github/buildomat/jobs/package.sh index dc89bc787b..d290976d9f 100755 --- a/.github/buildomat/jobs/package.sh +++ b/.github/buildomat/jobs/package.sh @@ -124,6 +124,7 @@ zones=( out/omicron-gateway-softnpu.tar.gz out/omicron-gateway-asic.tar.gz out/overlay.tar.gz + out/probe.tar.gz ) cp "${zones[@]}" /work/zones/ diff --git a/.github/buildomat/jobs/tuf-repo.sh b/.github/buildomat/jobs/tuf-repo.sh old mode 100644 new mode 100755 diff --git a/.gitignore b/.gitignore index fc5fd5f297..a13536aab7 100644 --- a/.gitignore +++ b/.gitignore @@ -13,4 +13,5 @@ debug.out rusty-tags.vi *.sw* tags -.direnv \ No newline at end of file +.direnv +connectivity-report.json diff --git a/Cargo.lock b/Cargo.lock index 416766b9cb..0cfc7b4500 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -54,12 +54,12 @@ dependencies = [ [[package]] name = "ahash" -version = "0.8.7" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77c3a9648d43b9cd48db467b3f87fdd6e146bcc88ab0180006cef2179fe11d01" +checksum = "42cd52102d3df161c77a887b608d7a4897d7cc112886a9537b738a887a03aaff" dependencies = [ "cfg-if", - "getrandom 0.2.10", + "getrandom 0.2.12", "once_cell", "version_check", "zerocopy 0.7.32", @@ -67,9 +67,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.0.4" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6748e8def348ed4d14996fa801f4122cd763fff530258cdc03f64b25f89d3a5a" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" dependencies = [ "memchr", ] @@ -160,9 +160,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.75" +version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" +checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca" dependencies = [ "backtrace", ] @@ -241,7 +241,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed72493ac66d5804837f480ab3766c72bdfab91a65e565fc54fa9e42db0073a8" dependencies = [ "anstyle", - "bstr 1.6.0", + "bstr 1.9.0", "doc-comment", "predicates", "predicates-core", @@ -377,7 +377,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b62ddb9cb1ec0a098ad4bbf9344d0713fa193ae1a80af55febcff2627b6a00c1" dependencies = [ "futures-core", - "getrandom 0.2.10", + "getrandom 0.2.12", "instant", "pin-project-lite", "rand 0.8.5", @@ -485,7 +485,7 @@ version = "0.69.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4c69fae65a523209d34240b60abe0c42d33d1045d445c0839d8a4894a736e2d" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.4.2", "cexpr", "clang-sys", "lazy_static", @@ -531,9 +531,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.0" +version = "2.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" +checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" dependencies = [ "serde", ] @@ -678,12 +678,12 @@ dependencies = [ [[package]] name = "bstr" -version = "1.6.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6798148dccfbff0fae41c7574d2fa8f1ef3492fba0face179de5d8d447d67b05" +checksum = "c48f0051a4b4c5e0b6d365cd04af53aeaa209e3cc15ec2cdb69e73cc87fbd0dc" dependencies = [ "memchr", - "regex-automata 0.3.8", + "regex-automata 0.4.5", "serde", ] @@ -909,9 +909,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.31" +version = "0.4.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" +checksum = "5bc015644b92d5890fab7489e49d21f879d5c990186827d42ec511919404f38b" dependencies = [ "android-tzdata", "iana-time-zone", @@ -919,7 +919,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-targets 0.48.5", + "windows-targets 0.52.0", ] [[package]] @@ -988,9 +988,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.0" +version = "4.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80c21025abd42669a92efc996ef13cfb2c5c627858421ea58d5c3b331a6c134f" +checksum = "c918d541ef2913577a0f9566e9ce27cb35b6df072075769e0b26cb5a554520da" dependencies = [ "clap_builder", "clap_derive", @@ -998,9 +998,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.0" +version = "4.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "458bf1f341769dfcf849846f65dffdf9146daa56bcd2a47cb4e1de9915567c99" +checksum = "9f3e7391dad68afb0c2ede1bf619f579a3dc9c2ec67f089baa397123a2f3d1eb" dependencies = [ "anstream", "anstyle", @@ -1050,11 +1050,10 @@ checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" [[package]] name = "colored" -version = "2.0.4" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2674ec482fbc38012cf31e6c42ba0177b431a0cb6f15fe40efa5aab1bda516f6" +checksum = "cbf2150cce219b664a8a70df7a1f933836724b503f8a413af9365b4dcc4d90b8" dependencies = [ - "is-terminal", "lazy_static", "windows-sys 0.48.0", ] @@ -1087,9 +1086,9 @@ dependencies = [ [[package]] name = "const-oid" -version = "0.9.5" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28c122c3980598d243d63d9a704629a2d748d101f278052ff068be5a4423ab6f" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" [[package]] name = "constant_time_eq" @@ -1109,6 +1108,17 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" +[[package]] +name = "cookie" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7efb37c3e1ccb1ff97164ad95ac1606e8ccd35b3fa0a7d99a304c7f4a428cc24" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + [[package]] name = "cookie" version = "0.18.0" @@ -1119,6 +1129,23 @@ dependencies = [ "version_check", ] +[[package]] +name = "cookie_store" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "387461abbc748185c3a6e1673d826918b450b87ff22639429c694619a83b6cf6" +dependencies = [ + "cookie 0.17.0", + "idna 0.3.0", + "log", + "publicsuffix", + "serde", + "serde_derive", + "serde_json", + "time", + "url", +] + [[package]] name = "core-foundation" version = "0.9.3" @@ -1201,7 +1228,7 @@ dependencies = [ "anes", "cast", "ciborium", - "clap 4.5.0", + "clap 4.5.1", "criterion-plot", "futures", "is-terminal", @@ -1259,25 +1286,18 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.15" +version = "0.9.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" dependencies = [ - "autocfg", - "cfg-if", "crossbeam-utils", - "memoffset 0.9.0", - "scopeguard", ] [[package]] name = "crossbeam-utils" -version = "0.8.16" +version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" -dependencies = [ - "cfg-if", -] +checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" [[package]] name = "crossterm" @@ -1285,7 +1305,7 @@ version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.4.2", "crossterm_winapi", "futures-core", "libc", @@ -1646,10 +1666,11 @@ dependencies = [ [[package]] name = "deranged" -version = "0.3.8" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2696e8a945f658fd14dc3b87242e6b80cd0f36ff04ea560fa39082368847946" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" dependencies = [ + "powerfmt", "serde", ] @@ -1719,13 +1740,34 @@ dependencies = [ "syn 2.0.51", ] +[[package]] +name = "dhcproto" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "000717b4f6913807b6195419e0bacb008d449ba6023ca26abf349c4ff2f1866b" +dependencies = [ + "dhcproto-macros", + "hex", + "ipnet", + "rand 0.8.5", + "thiserror", + "trust-dns-proto", + "url", +] + +[[package]] +name = "dhcproto-macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7993efb860416547839c115490d4951c6d0f8ec04a3594d9dd99d50ed7ec170" + [[package]] name = "diesel" version = "2.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62c6fcf842f17f8c78ecf7c81d75c5ce84436b41ee07e03f490fbb5f5a8731d8" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.4.2", "byteorder", "chrono", "diesel_derives", @@ -1863,7 +1905,7 @@ dependencies = [ "anyhow", "camino", "chrono", - "clap 4.5.0", + "clap 4.5.1", "dns-service-client", "dropshot", "expectorate", @@ -1988,7 +2030,7 @@ dependencies = [ "futures", "hostname", "http 0.2.12", - "hyper 0.14.27", + "hyper 0.14.28", "indexmap 2.2.5", "multer", "openapiv3", @@ -2165,12 +2207,20 @@ dependencies = [ name = "end-to-end-tests" version = "0.1.0" dependencies = [ + "anstyle", "anyhow", "async-trait", "base64", "chrono", + "clap 4.5.1", + "colored", + "dhcproto", "http 0.2.12", - "hyper 0.14.27", + "humantime", + "hyper 0.14.28", + "internet-checksum", + "ispf", + "macaddr", "omicron-sled-agent", "omicron-test-utils", "omicron-workspace-hack", @@ -2181,6 +2231,7 @@ dependencies = [ "russh-keys", "serde", "serde_json", + "socket2 0.5.5", "tokio", "toml 0.8.10", "trust-dns-resolver", @@ -2569,7 +2620,7 @@ name = "gateway-cli" version = "0.1.0" dependencies = [ "anyhow", - "clap 4.5.0", + "clap 4.5.1", "futures", "gateway-client", "gateway-messages", @@ -2711,9 +2762,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.10" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" dependencies = [ "cfg-if", "js-sys", @@ -2751,7 +2802,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "759c97c1e17c55525b57192c06a267cda0ac5210b222d6b82189a2338fa1c13d" dependencies = [ "aho-corasick", - "bstr 1.6.0", + "bstr 1.9.0", "fnv", "log", "regex", @@ -3093,7 +3144,7 @@ dependencies = [ "form_urlencoded", "futures", "http 0.2.12", - "hyper 0.14.27", + "hyper 0.14.28", "log", "once_cell", "regex", @@ -3170,9 +3221,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.27" +version = "0.14.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" +checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" dependencies = [ "bytes", "futures-channel", @@ -3185,7 +3236,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "socket2 0.4.9", + "socket2 0.5.5", "tokio", "tower-service", "tracing", @@ -3218,7 +3269,7 @@ checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" dependencies = [ "futures-util", "http 0.2.12", - "hyper 0.14.27", + "hyper 0.14.28", "rustls 0.21.9", "tokio", "tokio-rustls 0.24.1", @@ -3253,7 +3304,7 @@ dependencies = [ "http 0.2.12", "http-range", "httpdate", - "hyper 0.14.27", + "hyper 0.14.28", "mime_guess", "percent-encoding", "rand 0.8.5", @@ -3269,7 +3320,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" dependencies = [ "bytes", - "hyper 0.14.27", + "hyper 0.14.28", "native-tls", "tokio", "tokio-native-tls", @@ -3335,6 +3386,16 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "idna" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "idna" version = "0.5.0" @@ -3474,7 +3535,7 @@ dependencies = [ "bytes", "camino", "cancel-safe-futures", - "clap 4.5.0", + "clap 4.5.1", "ddm-admin-client", "display-error-chain", "futures", @@ -3534,10 +3595,10 @@ version = "0.1.0" dependencies = [ "anyhow", "async-trait", - "clap 4.5.0", + "clap 4.5.1", "dropshot", "expectorate", - "hyper 0.14.27", + "hyper 0.14.28", "installinator-common", "omicron-common", "omicron-test-utils", @@ -3593,7 +3654,7 @@ dependencies = [ "dropshot", "expectorate", "futures", - "hyper 0.14.27", + "hyper 0.14.28", "omicron-common", "omicron-test-utils", "omicron-workspace-hack", @@ -3615,7 +3676,7 @@ name = "internal-dns-cli" version = "0.1.0" dependencies = [ "anyhow", - "clap 4.5.0", + "clap 4.5.1", "dropshot", "internal-dns", "omicron-common", @@ -3625,6 +3686,12 @@ dependencies = [ "trust-dns-resolver", ] +[[package]] +name = "internet-checksum" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6d6206008e25125b1f97fbe5d309eb7b85141cf9199d52dbd3729a1584dd16" + [[package]] name = "ipcc" version = "0.1.0" @@ -3686,6 +3753,14 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45" +[[package]] +name = "ispf" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/ispf#f78443a98397f7818b1e7a487dbb7d5cad625496" +dependencies = [ + "serde", +] + [[package]] name = "itertools" version = "0.10.5" @@ -3884,7 +3959,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d8de370f98a6cb8a4606618e53e802f93b094ddec0f96988eaec2c27e6e9ce7" dependencies = [ - "clap 4.5.0", + "clap 4.5.1", "termcolor", "threadpool", ] @@ -3940,7 +4015,7 @@ version = "0.2.4" source = "git+https://github.com/oxidecomputer/lpc55_support#96f064eaae5e95930efaab6c29fd1b2e22225dac" dependencies = [ "bitfield", - "clap 4.5.0", + "clap 4.5.1", "packed_struct", "serde", ] @@ -4044,9 +4119,9 @@ checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" [[package]] name = "memchr" -version = "2.6.3" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" +checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" [[package]] name = "memmap" @@ -4076,15 +4151,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "memoffset" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" -dependencies = [ - "autocfg", -] - [[package]] name = "mg-admin-client" version = "0.1.0" @@ -4211,7 +4277,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" dependencies = [ - "getrandom 0.2.10", + "getrandom 0.2.12", ] [[package]] @@ -4366,7 +4432,7 @@ dependencies = [ "camino", "camino-tempfile", "chrono", - "cookie", + "cookie 0.18.0", "db-macros", "diesel", "diesel-dtrace", @@ -4376,7 +4442,7 @@ dependencies = [ "gateway-client", "headers", "http 0.2.12", - "hyper 0.14.27", + "hyper 0.14.28", "hyper-rustls 0.26.0", "illumos-utils", "internal-dns", @@ -4414,6 +4480,7 @@ dependencies = [ "regex", "rustls 0.22.2", "samael", + "schemars", "serde", "serde_json", "serde_urlencoded", @@ -4580,7 +4647,7 @@ dependencies = [ "gateway-test-utils", "headers", "http 0.2.12", - "hyper 0.14.27", + "hyper 0.14.28", "internal-dns", "nexus-config", "nexus-db-queries", @@ -4659,7 +4726,7 @@ dependencies = [ "bitflags 1.3.2", "cfg-if", "libc", - "memoffset 0.7.1", + "memoffset", "pin-utils", "static_assertions", ] @@ -4670,7 +4737,7 @@ version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.4.2", "cfg-if", "libc", ] @@ -4758,6 +4825,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-derive" version = "0.4.0" @@ -4780,9 +4853,9 @@ dependencies = [ [[package]] name = "num-iter" -version = "0.1.43" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" +checksum = "d869c01cc0c455284163fd0092f1f93835385ccab5a98a0dcc497b2f8bf055a9" dependencies = [ "autocfg", "num-integer", @@ -4802,9 +4875,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.16" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" +checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" dependencies = [ "autocfg", "libm", @@ -4968,7 +5041,7 @@ dependencies = [ "anyhow", "camino", "camino-tempfile", - "clap 4.5.0", + "clap 4.5.1", "dropshot", "expectorate", "futures", @@ -5002,7 +5075,7 @@ dependencies = [ "anyhow", "base64", "camino", - "clap 4.5.0", + "clap 4.5.1", "dropshot", "expectorate", "futures", @@ -5011,7 +5084,7 @@ dependencies = [ "gateway-test-utils", "hex", "http 0.2.12", - "hyper 0.14.27", + "hyper 0.14.28", "illumos-utils", "ipcc", "omicron-common", @@ -5053,7 +5126,7 @@ dependencies = [ "camino-tempfile", "cancel-safe-futures", "chrono", - "clap 4.5.0", + "clap 4.5.1", "criterion", "crucible-agent-client", "crucible-pantry-client", @@ -5073,7 +5146,7 @@ dependencies = [ "http 0.2.12", "httptest", "hubtools", - "hyper 0.14.27", + "hyper 0.14.28", "hyper-rustls 0.26.0", "illumos-utils", "internal-dns", @@ -5167,7 +5240,7 @@ dependencies = [ "camino", "camino-tempfile", "chrono", - "clap 4.5.0", + "clap 4.5.1", "crossterm", "crucible-agent-client", "csv", @@ -5219,7 +5292,7 @@ version = "0.1.0" dependencies = [ "anyhow", "camino", - "clap 4.5.0", + "clap 4.5.1", "expectorate", "futures", "hex", @@ -5286,7 +5359,7 @@ dependencies = [ "cancel-safe-futures", "cfg-if", "chrono", - "clap 4.5.0", + "clap 4.5.1", "crucible-agent-client", "ddm-admin-client", "derive_more", @@ -5304,7 +5377,7 @@ dependencies = [ "guppy", "hex", "http 0.2.12", - "hyper 0.14.27", + "hyper 0.14.28", "hyper-staticfile", "illumos-utils", "installinator-common", @@ -5418,14 +5491,14 @@ dependencies = [ "bit-set", "bit-vec", "bitflags 1.3.2", - "bitflags 2.4.0", + "bitflags 2.4.2", "bstr 0.2.17", - "bstr 1.6.0", + "bstr 1.9.0", "byteorder", "bytes", "chrono", "cipher", - "clap 4.5.0", + "clap 4.5.1", "clap_builder", "console", "const-oid", @@ -5450,12 +5523,12 @@ dependencies = [ "futures-util", "gateway-messages", "generic-array", - "getrandom 0.2.10", + "getrandom 0.2.12", "group", "hashbrown 0.14.3", "hex", "hmac", - "hyper 0.14.27", + "hyper 0.14.28", "indexmap 2.2.5", "inout", "ipnetwork", @@ -5483,7 +5556,7 @@ dependencies = [ "rand 0.8.5", "rand_chacha 0.3.1", "regex", - "regex-automata 0.4.4", + "regex-automata 0.4.5", "regex-syntax 0.8.2", "reqwest", "ring 0.17.8", @@ -5495,7 +5568,6 @@ dependencies = [ "sha2", "similar", "slog", - "socket2 0.5.5", "spin 0.9.8", "string_cache", "subtle", @@ -5603,7 +5675,7 @@ version = "0.10.60" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79a4c6c3a2b158f7f8f2a2fc5a969fa3a068df6fc9dbb4a43845436e3af7c800" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.4.2", "cfg-if", "foreign-types 0.3.2", "libc", @@ -5731,7 +5803,7 @@ dependencies = [ "chrono", "futures", "http 0.2.12", - "hyper 0.14.27", + "hyper 0.14.28", "omicron-workspace-hack", "progenitor", "rand 0.8.5", @@ -5804,11 +5876,11 @@ dependencies = [ "anyhow", "camino", "chrono", - "clap 4.5.0", + "clap 4.5.1", "dropshot", "expectorate", "futures", - "hyper 0.14.27", + "hyper 0.14.28", "internal-dns", "nexus-client", "nexus-types", @@ -5847,7 +5919,7 @@ dependencies = [ "bytes", "camino", "chrono", - "clap 4.5.0", + "clap 4.5.1", "dropshot", "expectorate", "futures", @@ -5917,7 +5989,7 @@ version = "0.1.0" dependencies = [ "anyhow", "chrono", - "clap 4.5.0", + "clap 4.5.1", "dropshot", "nexus-client", "omicron-common", @@ -5939,7 +6011,7 @@ dependencies = [ "anyhow", "camino", "chrono", - "clap 4.5.0", + "clap 4.5.1", "omicron-workspace-hack", "uuid", ] @@ -6466,6 +6538,12 @@ dependencies = [ "uuid", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -6693,10 +6771,10 @@ dependencies = [ "anyhow", "atty", "base64", - "clap 4.5.0", + "clap 4.5.1", "dropshot", "futures", - "hyper 0.14.27", + "hyper 0.14.28", "progenitor", "propolis_types", "rand 0.8.5", @@ -6732,7 +6810,7 @@ checksum = "31b476131c3c86cb68032fdc5cb6d5a1045e3e42d96b69fa599fd77701e1f5bf" dependencies = [ "bit-set", "bit-vec", - "bitflags 2.4.0", + "bitflags 2.4.2", "lazy_static", "num-traits", "rand 0.8.5", @@ -6744,6 +6822,22 @@ dependencies = [ "unarray", ] +[[package]] +name = "psl-types" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" + +[[package]] +name = "publicsuffix" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96a8c1bda5ae1af7f99a2962e49df150414a43d62404644d98dd5c3a93d07457" +dependencies = [ + "idna 0.3.0", + "psl-types", +] + [[package]] name = "quick-error" version = "1.2.3" @@ -6883,7 +6977,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.10", + "getrandom 0.2.12", ] [[package]] @@ -6910,7 +7004,7 @@ version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bcb12f8fbf6c62614b0d56eb352af54f6a22410c3b079eb53ee93c7b97dd31d8" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.4.2", "cassowary", "compact_str", "crossterm", @@ -6972,7 +7066,7 @@ dependencies = [ "anyhow", "camino", "camino-tempfile", - "clap 4.5.0", + "clap 4.5.1", "dropshot", "expectorate", "humantime", @@ -7027,7 +7121,7 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" dependencies = [ - "getrandom 0.2.10", + "getrandom 0.2.12", "redox_syscall 0.2.16", "thiserror", ] @@ -7080,7 +7174,7 @@ checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.4", + "regex-automata 0.4.5", "regex-syntax 0.8.2", ] @@ -7092,15 +7186,9 @@ checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" [[package]] name = "regex-automata" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2f401f4955220693b56f8ec66ee9c78abffd8d1c4f23dc41a23839eb88f0795" - -[[package]] -name = "regex-automata" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b7fa1134405e2ec9353fd416b17f8dacd46c473d7d3fd1cf202706a14eb792a" +checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd" dependencies = [ "aho-corasick", "memchr", @@ -7152,13 +7240,15 @@ checksum = "c6920094eb85afde5e4a138be3f2de8bbdf28000f0029e72c45025a56b042251" dependencies = [ "base64", "bytes", + "cookie 0.17.0", + "cookie_store", "encoding_rs", "futures-core", "futures-util", "h2", "http 0.2.12", "http-body 0.4.5", - "hyper 0.14.27", + "hyper 0.14.28", "hyper-rustls 0.24.2", "hyper-tls", "ipnet", @@ -7233,7 +7323,7 @@ checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.10", + "getrandom 0.2.12", "libc", "spin 0.9.8", "untrusted 0.9.0", @@ -7247,7 +7337,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" dependencies = [ "base64", - "bitflags 2.4.0", + "bitflags 2.4.2", "serde", "serde_derive", ] @@ -7335,7 +7425,7 @@ dependencies = [ "aes", "aes-gcm", "async-trait", - "bitflags 2.4.0", + "bitflags 2.4.2", "byteorder", "chacha20", "ctr", @@ -7471,7 +7561,7 @@ version = "0.38.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.4.2", "errno", "libc", "linux-raw-sys", @@ -7605,7 +7695,7 @@ version = "13.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02a2d683a4ac90aeef5b1013933f6d977bd37d51ff3f4dad829d4931a7e6be86" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.4.2", "cfg-if", "clipboard-win", "fd-lock 4.0.2", @@ -8134,9 +8224,9 @@ dependencies = [ [[package]] name = "similar" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2aeaf503862c419d66959f5d7ca015337d864e9c49485d771b732e2a20453597" +checksum = "32fea41aca09ee824cc9724996433064c89f7777e60762749a4170a14abbfa21" dependencies = [ "bstr 0.2.17", "unicode-segmentation", @@ -8488,7 +8578,7 @@ version = "0.1.0" dependencies = [ "anyhow", "async-trait", - "clap 4.5.0", + "clap 4.5.1", "dropshot", "futures", "gateway-messages", @@ -9145,14 +9235,16 @@ dependencies = [ [[package]] name = "time" -version = "0.3.27" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bb39ee79a6d8de55f48f2293a830e040392f1c5f16e336bdd1788cd0aadce07" +checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749" dependencies = [ "deranged", "itoa", "libc", + "num-conv", "num_threads", + "powerfmt", "serde", "time-core", "time-macros", @@ -9160,16 +9252,17 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.13" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "733d258752e9303d392b94b75230d07b0b9c489350c69b851fc6c065fde3e8f9" +checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774" dependencies = [ + "num-conv", "time-core", ] @@ -9525,11 +9618,10 @@ checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" [[package]] name = "tracing" -version = "0.1.37" +version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ - "cfg-if", "log", "pin-project-lite", "tracing-attributes", @@ -9538,9 +9630,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.26" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", @@ -9549,9 +9641,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.31" +version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", ] @@ -9672,7 +9764,7 @@ dependencies = [ "assert_cmd", "camino", "chrono", - "clap 4.5.0", + "clap 4.5.1", "console", "datatest-stable", "fs-err", @@ -9832,9 +9924,9 @@ dependencies = [ [[package]] name = "unicode-bidi" -version = "0.3.13" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" [[package]] name = "unicode-ident" @@ -9918,7 +10010,7 @@ dependencies = [ "camino", "camino-tempfile", "chrono", - "clap 4.5.0", + "clap 4.5.1", "debug-ignore", "display-error-chain", "dropshot", @@ -9949,7 +10041,7 @@ dependencies = [ "camino", "camino-tempfile", "cancel-safe-futures", - "clap 4.5.0", + "clap 4.5.1", "debug-ignore", "derive-where", "either", @@ -10129,7 +10221,7 @@ version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f00cc9702ca12d3c81455259621e676d0f7251cec66a21e98fe2e9a37db93b2a" dependencies = [ - "getrandom 0.2.10", + "getrandom 0.2.12", "serde", ] @@ -10370,7 +10462,7 @@ dependencies = [ "buf-list", "camino", "ciborium", - "clap 4.5.0", + "clap 4.5.1", "crossterm", "futures", "humantime", @@ -10431,7 +10523,7 @@ dependencies = [ "bytes", "camino", "ciborium", - "clap 4.5.0", + "clap 4.5.1", "crossterm", "omicron-workspace-hack", "reedline", @@ -10456,7 +10548,7 @@ dependencies = [ "bytes", "camino", "camino-tempfile", - "clap 4.5.0", + "clap 4.5.1", "ddm-admin-client", "debug-ignore", "display-error-chain", @@ -10474,7 +10566,7 @@ dependencies = [ "hex", "http 0.2.12", "hubtools", - "hyper 0.14.27", + "hyper 0.14.28", "illumos-utils", "installinator", "installinator-artifact-client", @@ -10782,7 +10874,7 @@ dependencies = [ "camino", "cargo_metadata", "cargo_toml", - "clap 4.5.0", + "clap 4.5.1", ] [[package]] @@ -10901,8 +10993,7 @@ dependencies = [ [[package]] name = "zone" version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a62a428a79ea2224ce8ab05d6d8a21bdd7b4b68a8dbc1230511677a56e72ef22" +source = "git+https://github.com/oxidecomputer/zone?branch=state-derive-eq-hash#f1920d5636c69ea8179f8ec659702dcdef43268c" dependencies = [ "itertools 0.10.5", "thiserror", @@ -10915,7 +11006,7 @@ name = "zone-network-setup" version = "0.1.0" dependencies = [ "anyhow", - "clap 4.5.0", + "clap 4.5.1", "dropshot", "illumos-utils", "omicron-common", @@ -10928,8 +11019,7 @@ dependencies = [ [[package]] name = "zone_cfg_derive" version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5c4f01d3785e222d5aca11c9813e9c46b69abfe258756c99c9b628683626cc8" +source = "git+https://github.com/oxidecomputer/zone?branch=state-derive-eq-hash#f1920d5636c69ea8179f8ec659702dcdef43268c" dependencies = [ "heck 0.4.1", "proc-macro-error", diff --git a/Cargo.toml b/Cargo.toml index 299d715d67..96f47be86f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -158,6 +158,7 @@ resolver = "2" [workspace.dependencies] anyhow = "1.0" +anstyle = "1.0" api_identity = { path = "api_identity" } approx = "0.5.1" assert_matches = "1.5.0" @@ -184,6 +185,7 @@ ciborium = "0.2.2" cfg-if = "1.0" chrono = { version = "0.4", features = [ "serde" ] } clap = { version = "4.5", features = ["cargo", "derive", "env", "wrap_help"] } +colored = "2.1" cookie = "0.18" criterion = { version = "0.5.1", features = [ "async_tokio" ] } crossbeam = "0.8" @@ -246,8 +248,11 @@ installinator-common = { path = "installinator-common" } internal-dns = { path = "internal-dns" } ipcc = { path = "ipcc" } ipnet = "2.9" -ipnetwork = { version = "0.20", features = ["schemars"] } itertools = "0.12.1" +internet-checksum = "0.2" +ipcc-key-value = { path = "ipcc-key-value" } +ipnetwork = { version = "0.20", features = ["schemars"] } +ispf = { git = "https://github.com/oxidecomputer/ispf" } key-manager = { path = "key-manager" } kstat-rs = "0.2.3" libc = "0.2.153" @@ -373,6 +378,8 @@ slog-envlogger = "2.2" slog-error-chain = { git = "https://github.com/oxidecomputer/slog-error-chain", branch = "main", features = ["derive"] } slog-term = "2.9" smf = "0.2" +snafu = "0.7" +socket2 = { version = "0.5", features = ["all"] } sp-sim = { path = "sp-sim" } sprockets-common = { git = "http://github.com/oxidecomputer/sprockets", rev = "77df31efa5619d0767ffc837ef7468101608aee9" } sprockets-host = { git = "http://github.com/oxidecomputer/sprockets", rev = "77df31efa5619d0767ffc837ef7468101608aee9" } @@ -654,3 +661,7 @@ branch = "oxide/omicron" # See also: uuid-kinds/README.adoc. [patch."https://github.com/oxidecomputer/omicron"] omicron-uuid-kinds = { path = "uuid-kinds" } + +[patch.crates-io.zone] +git = 'https://github.com/oxidecomputer/zone' +branch = 'state-derive-eq-hash' diff --git a/clients/nexus-client/src/lib.rs b/clients/nexus-client/src/lib.rs index 55bdf3d0aa..85c67ddbfd 100644 --- a/clients/nexus-client/src/lib.rs +++ b/clients/nexus-client/src/lib.rs @@ -33,6 +33,8 @@ progenitor::generate_api!( MacAddr = omicron_common::api::external::MacAddr, Name = omicron_common::api::external::Name, NewPasswordHash = omicron_passwords::NewPasswordHash, + NetworkInterface = omicron_common::api::internal::shared::NetworkInterface, + NetworkInterfaceKind = omicron_common::api::internal::shared::NetworkInterfaceKind, }, patch = { SledAgentInfo = { derives = [PartialEq, Eq] }, diff --git a/clients/sled-agent-client/src/lib.rs b/clients/sled-agent-client/src/lib.rs index eb1e57b11f..b2bc232ef5 100644 --- a/clients/sled-agent-client/src/lib.rs +++ b/clients/sled-agent-client/src/lib.rs @@ -6,6 +6,7 @@ use anyhow::Context; use async_trait::async_trait; +use omicron_common::api::internal::shared::NetworkInterface; use std::convert::TryFrom; use std::net::IpAddr; use std::net::SocketAddr; @@ -39,6 +40,7 @@ progenitor::generate_api!( PortSpeed = omicron_common::api::internal::shared::PortSpeed, SourceNatConfig = omicron_common::api::internal::shared::SourceNatConfig, Vni = omicron_common::api::external::Vni, + NetworkInterface = omicron_common::api::internal::shared::NetworkInterface, } ); @@ -141,7 +143,7 @@ impl types::OmicronZoneType { } /// The service vNIC providing external connectivity to this zone - pub fn service_vnic(&self) -> Option<&types::NetworkInterface> { + pub fn service_vnic(&self) -> Option<&NetworkInterface> { match self { types::OmicronZoneType::Nexus { nic, .. } | types::OmicronZoneType::ExternalDns { nic, .. } @@ -556,26 +558,7 @@ impl From match s { Instance { id } => Self::Instance(id), Service { id } => Self::Service(id), - } - } -} - -impl From - for types::NetworkInterface -{ - fn from( - s: omicron_common::api::internal::shared::NetworkInterface, - ) -> Self { - Self { - id: s.id, - kind: s.kind.into(), - name: s.name, - ip: s.ip, - mac: s.mac, - subnet: s.subnet.into(), - vni: s.vni, - primary: s.primary, - slot: s.slot, + Probe { id } => Self::Probe(id), } } } diff --git a/common/src/api/external/mod.rs b/common/src/api/external/mod.rs index 17b4826f8c..58bc82c825 100644 --- a/common/src/api/external/mod.rs +++ b/common/src/api/external/mod.rs @@ -879,6 +879,8 @@ pub enum ResourceType { Vmm, Ipv4NatEntry, FloatingIp, + Probe, + ProbeNetworkInterface, } // IDENTITY METADATA @@ -2827,6 +2829,15 @@ pub struct TufRepoGetResponse { pub description: TufRepoDescription, } +#[derive( + Clone, Debug, Deserialize, JsonSchema, Serialize, PartialEq, ObjectIdentity, +)] +pub struct Probe { + #[serde(flatten)] + pub identity: IdentityMetadata, + pub sled: Uuid, +} + #[cfg(test)] mod test { use serde::Deserialize; diff --git a/common/src/api/internal/shared.rs b/common/src/api/internal/shared.rs index c8d8b1c786..bf825fd2e7 100644 --- a/common/src/api/internal/shared.rs +++ b/common/src/api/internal/shared.rs @@ -36,6 +36,8 @@ pub enum NetworkInterfaceKind { Instance { id: Uuid }, /// A vNIC associated with an internal service Service { id: Uuid }, + /// A vNIC associated with a probe + Probe { id: Uuid }, } /// Information required to construct a virtual network interface diff --git a/dev-tools/omdb/src/bin/omdb/db.rs b/dev-tools/omdb/src/bin/omdb/db.rs index 339c257d8e..98e7748054 100644 --- a/dev-tools/omdb/src/bin/omdb/db.rs +++ b/dev-tools/omdb/src/bin/omdb/db.rs @@ -54,6 +54,7 @@ use nexus_db_model::IpAttachState; use nexus_db_model::IpKind; use nexus_db_model::NetworkInterface; use nexus_db_model::NetworkInterfaceKind; +use nexus_db_model::Probe; use nexus_db_model::Project; use nexus_db_model::Region; use nexus_db_model::RegionSnapshot; @@ -711,7 +712,25 @@ async fn lookup_service_kind( Ok(Some(service_kind)) } -/// Helper function to look up a project with the given ID. +/// Helper function to looks up a probe with the given ID. +async fn lookup_probe( + datastore: &DataStore, + probe_id: Uuid, +) -> anyhow::Result> { + use db::schema::probe::dsl; + + let conn = datastore.pool_connection_for_tests().await?; + dsl::probe + .filter(dsl::id.eq(probe_id)) + .limit(1) + .select(Probe::as_select()) + .get_result_async(&*conn) + .await + .optional() + .with_context(|| format!("loading probe {probe_id}")) +} + +/// Helper function to looks up a project with the given ID. async fn lookup_project( datastore: &DataStore, project_id: Uuid, @@ -2122,6 +2141,28 @@ async fn cmd_db_network_list_vnics( } } } + NetworkInterfaceKind::Probe => { + match lookup_probe(datastore, nic.parent_id).await? { + Some(probe) => { + match lookup_project(datastore, probe.project_id) + .await? + { + Some(project) => ( + "probe", + format!("{}/{}", project.name(), probe.name()), + ), + None => { + eprintln!( + "project with id {} not found", + probe.project_id + ); + continue; + } + } + } + None => ("probe?", "parent probe not found".to_string()), + } + } NetworkInterfaceKind::Service => { // We create service NICs named after the service, so we can use // the nic name instead of looking up the service. diff --git a/end-to-end-tests/Cargo.toml b/end-to-end-tests/Cargo.toml index fee32a344e..0fb9efd5cc 100644 --- a/end-to-end-tests/Cargo.toml +++ b/end-to-end-tests/Cargo.toml @@ -15,7 +15,7 @@ omicron-sled-agent.workspace = true omicron-test-utils.workspace = true oxide-client.workspace = true rand.workspace = true -reqwest.workspace = true +reqwest = { workspace = true, features = ["cookies"] } russh = "0.42.0" russh-keys = "0.42.0" serde.workspace = true @@ -25,3 +25,12 @@ toml.workspace = true trust-dns-resolver.workspace = true uuid.workspace = true omicron-workspace-hack.workspace = true +ispf.workspace = true +internet-checksum.workspace = true +humantime.workspace = true +socket2.workspace = true +colored.workspace = true +anstyle.workspace = true +clap.workspace = true +dhcproto = "0.11" +macaddr.workspace = true diff --git a/end-to-end-tests/src/bin/commtest.rs b/end-to-end-tests/src/bin/commtest.rs new file mode 100644 index 0000000000..27ca4633ce --- /dev/null +++ b/end-to-end-tests/src/bin/commtest.rs @@ -0,0 +1,443 @@ +use anyhow::{anyhow, Result}; +use clap::{Parser, Subcommand}; +use end_to_end_tests::helpers::cli::oxide_cli_style; +use end_to_end_tests::helpers::icmp::ping4_test_run; +use oxide_client::{ + types::{ + IpPoolCreate, IpPoolLinkSilo, IpRange, Ipv4Range, Name, NameOrId, + PingStatus, ProbeCreate, ProbeInfo, ProjectCreate, + UsernamePasswordCredentials, + }, + ClientHiddenExt, ClientLoginExt, ClientProjectsExt, + ClientSystemHardwareExt, ClientSystemNetworkingExt, ClientSystemStatusExt, + ClientVpcsExt, +}; +use std::{ + net::{IpAddr, Ipv4Addr}, + time::{Duration, Instant}, +}; +use tokio::time::sleep; +use uuid::Uuid; + +#[derive(Parser, Debug)] +#[clap(version, about, long_about = None, styles = oxide_cli_style())] +struct Cli { + /// Oxide API address i.e., http://198.51.100.20 + oxide_api: String, + + /// How long to wait for the API to become available + #[arg(long, default_value = "60m")] + api_timeout: humantime::Duration, + + #[clap(subcommand)] + command: Commands, +} + +#[derive(Debug, Subcommand)] +enum Commands { + Run(RunArgs), + Cleanup, +} + +#[derive(Parser, Debug)] +struct RunArgs { + /// Test Duration + #[arg(long, default_value = "100s")] + test_duration: humantime::Duration, + + /// Test packet rate in packets per second + #[arg(long, default_value_t = 10)] + packet_rate: usize, + + /// How many lost ICMP packets may be tolerated + #[arg(long, default_value_t = 0)] + icmp_loss_tolerance: usize, + + /// First address in the IP pool to use for testing + #[arg(long)] + ip_pool_begin: Ipv4Addr, + + /// Last address in the IP pool to use for testing + #[arg(long)] + ip_pool_end: Ipv4Addr, +} + +const API_RETRY_ATTEMPTS: usize = 15; + +#[tokio::main] +pub async fn main() -> Result<()> { + let cli = Cli::parse(); + match cli.command { + Commands::Run(ref args) => run(&cli, args).await, + Commands::Cleanup => cleanup(&cli).await, + } +} + +async fn run(cli: &Cli, args: &RunArgs) -> Result<()> { + wait_until_oxide_api_is_available(cli).await?; + let (sleds, oxide) = rack_prepare(cli, args).await?; + let addrs = launch_probes(sleds, &oxide).await?; + test_connectivity(args, addrs)?; + Ok(()) +} + +async fn cleanup(cli: &Cli) -> Result<()> { + wait_until_oxide_api_is_available(cli).await?; + let oxide = cleanup_probes(cli).await?; + rack_cleanup(&oxide).await?; + Ok(()) +} + +async fn wait_until_oxide_api_is_available(cli: &Cli) -> Result<()> { + let oxide = oxide_client::Client::new(&cli.oxide_api); + let start = Instant::now(); + loop { + if let Ok(result) = oxide.ping().send().await.map(|x| x.into_inner()) { + if result.status == PingStatus::Ok { + println!("the api is up"); + break; + } + } + if Instant::now().duration_since(start) + > Into::::into(cli.api_timeout) + { + return Err(anyhow!( + "One hour deadline for system startup exceeded" + )); + } + println!("no api response yet, wating 3s ..."); + sleep(Duration::from_secs(3)).await; + } + Ok(()) +} + +macro_rules! api_retry { + ($call:expr) => {{ + let mut limit = API_RETRY_ATTEMPTS; + loop { + match $call { + res @ Ok(_) => break res, + Err(e) => { + limit -= 1; + if limit == 0 { + break Err(e); + } + println!("API call error: {e}, retrying in 3 s"); + sleep(Duration::from_secs(3)).await; + } + } + } + }}; +} + +async fn cleanup_probes(cli: &Cli) -> Result { + let rqb = reqwest::ClientBuilder::new() + .cookie_store(true) + .timeout(Duration::from_secs(15)) + .connect_timeout(Duration::from_secs(15)) + .build() + .unwrap(); + let oxide = oxide_client::Client::new_with_client(&cli.oxide_api, rqb); + + print!("logging in ... "); + api_retry!( + oxide + .login_local() + .silo_name(Name::try_from("recovery").unwrap()) + .body(UsernamePasswordCredentials { + password: "oxide".parse().unwrap(), + username: "recovery".parse().unwrap(), + }) + .send() + .await + )?; + println!("done"); + + let probes: Vec = api_retry!( + oxide + .probe_list() + .project(Name::try_from("classone").unwrap()) + .limit(u32::MAX) + .send() + .await + )? + .into_inner() + .items; + + for probe in &probes { + print!("deleting probe {} ... ", *probe.name); + api_retry!( + oxide + .probe_delete() + .project(Name::try_from("classone").unwrap()) + .probe(probe.id) + .send() + .await + )?; + println!("done"); + } + + Ok(oxide) +} + +async fn rack_cleanup(oxide: &oxide_client::Client) -> Result<()> { + if let Err(e) = oxide + .project_view() + .project(Name::try_from("classone").unwrap()) + .send() + .await + { + if let Some(reqwest::StatusCode::NOT_FOUND) = e.status() { + print!("project does not exist"); + } else { + Err(e)?; + } + } else { + print!("deleting classone subnet ... "); + api_retry!( + oxide + .vpc_subnet_delete() + .project(Name::try_from("classone").unwrap()) + .vpc(Name::try_from("default").unwrap()) + .subnet(Name::try_from("default").unwrap()) + .send() + .await + )?; + println!("done"); + + print!("deleting classone vpc ... "); + api_retry!( + oxide + .vpc_delete() + .project(Name::try_from("classone").unwrap()) + .vpc(Name::try_from("default").unwrap()) + .send() + .await + )?; + println!("done"); + + print!("deleting classone project ... "); + api_retry!( + oxide + .project_delete() + .project(Name::try_from("classone").unwrap()) + .send() + .await + )?; + println!("done"); + } + Ok(()) +} + +async fn rack_prepare( + cli: &Cli, + args: &RunArgs, +) -> Result<(Vec, oxide_client::Client)> { + let rqb = reqwest::ClientBuilder::new().cookie_store(true).build().unwrap(); + + let oxide = oxide_client::Client::new_with_client(&cli.oxide_api, rqb); + + print!("logging in ... "); + api_retry!( + oxide + .login_local() + .silo_name(Name::try_from("recovery").unwrap()) + .body(UsernamePasswordCredentials { + password: "oxide".parse().unwrap(), + username: "recovery".parse().unwrap(), + }) + .send() + .await + )?; + println!("done"); + + api_retry!(if let Err(e) = oxide + .project_view() + .project(Name::try_from("classone").unwrap()) + .send() + .await + { + if let Some(reqwest::StatusCode::NOT_FOUND) = e.status() { + print!("project does not exist, creating ... "); + oxide + .project_create() + .body(ProjectCreate { + description: "A project for probes".into(), + name: "classone".parse().unwrap(), + }) + .send() + .await?; + println!("done"); + Ok(()) + } else { + Err(e) + } + } else { + println!("classone project already exists"); + Ok(()) + })?; + + let pool_name = "default"; + api_retry!( + if let Err(e) = oxide.ip_pool_view().pool("default").send().await { + if let Some(reqwest::StatusCode::NOT_FOUND) = e.status() { + print!("default ip pool does not exist, creating ..."); + oxide + .ip_pool_create() + .body(IpPoolCreate { + name: pool_name.parse().unwrap(), + description: "Default IP pool".to_string(), + }) + .send() + .await?; + oxide + .ip_pool_silo_link() + .pool(pool_name) + .body(IpPoolLinkSilo { + silo: NameOrId::Name("recovery".parse().unwrap()), + is_default: true, + }) + .send() + .await?; + println!("done"); + Ok(()) + } else { + Err(e) + } + } else { + println!("default ip pool already exists"); + Ok(()) + } + )?; + + let pool = api_retry!( + oxide + .ip_pool_range_list() + .limit(u32::MAX) + .pool(Name::try_from("default").unwrap()) + .send() + .await + )? + .into_inner() + .items; + + let range = Ipv4Range { first: args.ip_pool_begin, last: args.ip_pool_end }; + + let range_exists = pool + .iter() + .filter_map(|x| match &x.range { + IpRange::V4(r) => Some(r), + IpRange::V6(_) => None, + }) + .any(|x| x.first == range.first && x.last == range.last); + + if !range_exists { + print!("ip range does not exist, creating ... "); + api_retry!( + oxide + .ip_pool_range_add() + .pool(Name::try_from("default").unwrap()) + .body(IpRange::V4(range.clone())) + .send() + .await + )?; + println!("done"); + } else { + println!("ip range already exists"); + } + + print!("getting sled ids ... "); + let sleds = api_retry!(oxide.sled_list().limit(u32::MAX).send().await)? + .into_inner() + .items + .iter() + .map(|x| x.id) + .collect(); + println!("done"); + + Ok((sleds, oxide)) +} + +async fn launch_probes( + sleds: Vec, + oxide: &oxide_client::Client, +) -> Result> { + for (i, sled) in sleds.into_iter().enumerate() { + println!("checking if probe{i} exists"); + api_retry!(if let Err(e) = oxide + .probe_view() + .project(Name::try_from("classone").unwrap()) + .probe(Name::try_from(format!("probe{i}")).unwrap()) + .send() + .await + { + if let Some(reqwest::StatusCode::NOT_FOUND) = e.status() { + print!("probe{i} does not exist, creating ... "); + oxide + .probe_create() + .project(Name::try_from("classone").unwrap()) + .body(ProbeCreate { + description: format!("probe {i}"), + ip_pool: Some("default".parse().unwrap()), + name: format!("probe{i}").parse().unwrap(), + sled, + }) + .send() + .await?; + println!("done"); + Ok(()) + } else { + Err(e) + } + } else { + println!("probe{i} already exists"); + Ok(()) + })?; + } + + Ok(api_retry!( + oxide + .probe_list() + .project(Name::try_from("classone").unwrap()) + .limit(u32::MAX) + .send() + .await + )? + .into_inner() + .items + .iter() + .map(|x| x.external_ips.get(0).unwrap().ip) + .filter_map(|x| match x { + IpAddr::V4(ip) => Some(ip), + IpAddr::V6(_) => None, + }) + .collect()) +} + +fn test_connectivity(args: &RunArgs, addrs: Vec) -> Result<()> { + let ttl = 255; + println!("testing connectivity to probes"); + let report = ping4_test_run( + &addrs, + ttl, + args.packet_rate, + args.test_duration.into(), + ); + + let out = serde_json::to_string_pretty(&report).unwrap(); + std::fs::write("connectivity-report.json", out.as_str()).unwrap(); + + for state in report.v4.iter() { + if state.lost > args.icmp_loss_tolerance { + panic!( + "{} has loss = {} packets which is greater than tolerance {}", + state.dest, state.lost, args.icmp_loss_tolerance, + ); + } + if state.rx_count == 0 { + panic!("received no responses from {}", state.dest); + } + } + println!("all connectivity tests within loss tolerance"); + Ok(()) +} diff --git a/end-to-end-tests/src/bin/dhcp-server.rs b/end-to-end-tests/src/bin/dhcp-server.rs new file mode 100644 index 0000000000..69681fa413 --- /dev/null +++ b/end-to-end-tests/src/bin/dhcp-server.rs @@ -0,0 +1,125 @@ +//! This is a dirt simple DHCP server for handing out addresses in a given +//! range. Leases do not expire. If the server runs out of addresses, it +//! panics. This is a stopgap program to hand out addresses to VMs in CI. It's +//! in no way meant to be a generic DHCP server solution. + +use anyhow::Result; +use clap::Parser; +use dhcproto::{ + v4::{ + self, Decodable, Decoder, DhcpOptions, Encodable, Message, Opcode, + OptionCode, + }, + Encoder, +}; +use end_to_end_tests::helpers::cli::oxide_cli_style; +use macaddr::MacAddr6; +use std::{ + collections::HashMap, + net::{Ipv4Addr, SocketAddrV4, UdpSocket}, +}; + +#[derive(Parser, Debug)] +#[clap(version, about, long_about = None, styles = oxide_cli_style())] +struct Cli { + /// First address in DHCP range. + begin: Ipv4Addr, + /// Last address in DHCP range. + end: Ipv4Addr, + /// Default router to advertise. + router: Ipv4Addr, + /// Server address to advertise. + server: Ipv4Addr, +} + +pub fn main() -> Result<()> { + let cli = Cli::parse(); + let mut current = cli.begin; + let mut assignments = HashMap::::new(); + + let sock = UdpSocket::bind("0.0.0.0:67")?; + loop { + let mut buf = [0; 8192]; + let (n, src) = sock.recv_from(&mut buf)?; + + let mut msg = match Message::decode(&mut Decoder::new(&buf[..n])) { + Ok(msg) => msg, + Err(e) => { + eprintln!("message decode error {e}"); + continue; + } + }; + + println!("request: {msg:#?}"); + + if msg.opcode() != Opcode::BootRequest { + continue; + } + + let mac: [u8; 6] = msg.chaddr()[0..6].try_into().unwrap(); + let mac = MacAddr6::from(mac); + + let ip = match assignments.get(&mac) { + Some(ip) => *ip, + None => { + assignments.insert(mac, current); + let ip = current; + current = Ipv4Addr::from(u32::from(current) + 1); + if u32::from(current) > u32::from(cli.end) { + panic!("address exhaustion"); + } + ip + } + }; + + let mut opts = DhcpOptions::new(); + match msg.opts().get(OptionCode::MessageType) { + Some(v4::DhcpOption::MessageType(v4::MessageType::Discover)) => { + opts.insert(v4::DhcpOption::MessageType( + v4::MessageType::Offer, + )); + } + Some(v4::DhcpOption::MessageType(v4::MessageType::Request)) => { + opts.insert(v4::DhcpOption::MessageType(v4::MessageType::Ack)); + } + Some(mtype) => eprintln!("unexpected message type {mtype:?}"), + None => { + eprintln!("no message type"); + } + }; + // hardcode to /24 + opts.insert(v4::DhcpOption::SubnetMask(Ipv4Addr::new( + 255, 255, 255, 0, + ))); + // hardcode to something stable + opts.insert(v4::DhcpOption::DomainNameServer(vec![Ipv4Addr::new( + 1, 1, 1, 1, + )])); + opts.insert(v4::DhcpOption::ServerIdentifier(cli.server)); + // just something big enough to last CI runs + opts.insert(v4::DhcpOption::AddressLeaseTime(60 * 60 * 24 * 30)); + opts.insert(v4::DhcpOption::Router(vec![cli.router])); + if let Some(opt) = msg.opts().get(OptionCode::ClientIdentifier) { + opts.insert(opt.clone()); + } + + msg.set_opcode(Opcode::BootReply); + msg.set_siaddr(cli.server); + msg.set_yiaddr(ip); + msg.set_opts(opts); + + let mut buf = Vec::new(); + let mut e = Encoder::new(&mut buf); + if let Err(e) = msg.encode(&mut e) { + eprintln!("encode reply error: {e}"); + continue; + } + + // always blast replys bcast + let dst = + SocketAddrV4::new(Ipv4Addr::new(255, 255, 255, 255), src.port()); + if let Err(e) = sock.send_to(&buf, dst) { + eprintln!("send reply error: {e}"); + } + } +} diff --git a/end-to-end-tests/src/helpers/cli.rs b/end-to-end-tests/src/helpers/cli.rs new file mode 100644 index 0000000000..049ab5a8b7 --- /dev/null +++ b/end-to-end-tests/src/helpers/cli.rs @@ -0,0 +1,22 @@ +/// An Oxide color theme for your clap-based CLIs +pub fn oxide_cli_style() -> clap::builder::Styles { + clap::builder::Styles::styled() + .header(anstyle::Style::new().bold().underline().fg_color(Some( + anstyle::Color::Rgb(anstyle::RgbColor(245, 207, 101)), + ))) + .literal(anstyle::Style::new().bold().fg_color(Some( + anstyle::Color::Rgb(anstyle::RgbColor(72, 213, 151)), + ))) + .invalid(anstyle::Style::new().bold().fg_color(Some( + anstyle::Color::Rgb(anstyle::RgbColor(72, 213, 151)), + ))) + .valid(anstyle::Style::new().bold().fg_color(Some( + anstyle::Color::Rgb(anstyle::RgbColor(72, 213, 151)), + ))) + .usage(anstyle::Style::new().bold().fg_color(Some( + anstyle::Color::Rgb(anstyle::RgbColor(245, 207, 101)), + ))) + .error(anstyle::Style::new().bold().fg_color(Some( + anstyle::Color::Rgb(anstyle::RgbColor(232, 104, 134)), + ))) +} diff --git a/end-to-end-tests/src/helpers/icmp.rs b/end-to-end-tests/src/helpers/icmp.rs new file mode 100644 index 0000000000..42617801d8 --- /dev/null +++ b/end-to-end-tests/src/helpers/icmp.rs @@ -0,0 +1,306 @@ +use colored::*; +use internet_checksum::Checksum; +use serde::{Deserialize, Serialize}; +use socket2::{Domain, Protocol, SockAddr, Socket, Type}; +use std::collections::BTreeMap; +use std::mem::MaybeUninit; +use std::net::{Ipv4Addr, SocketAddrV4}; +use std::sync::{Arc, Mutex}; +use std::thread::{sleep, spawn}; +use std::time::{Duration, Instant}; + +const HIDE_CURSOR: &str = "\x1b[?25l"; +const SHOW_CURSOR: &str = "\x1b[?25h"; +const MOVE_CURSOR_UP: &str = "\x1b[A"; + +const ICMP_ECHO_TYPE: u8 = 8; +const ICMP_ECHO_CODE: u8 = 0; + +#[derive(Debug, Serialize, Deserialize)] +struct EchoRequest { + typ: u8, + code: u8, + checksum: u16, + identifier: u16, + sequence_number: u16, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Report { + pub v4: Vec, +} + +/// Run a ping test against the provided destination addreses, with the +/// specified time-to-live (ttl) at a given rate in packets per second +/// (pps) for the specified duration. +pub fn ping4_test_run( + dst: &[Ipv4Addr], + ttl: u32, + pps: usize, + duration: Duration, +) -> Report { + let p = Pinger4::new(ttl); + for dst in dst { + // use a random number for the ICMP id + p.add_target(rand::random(), *dst, pps, duration); + } + // Use an ASCII code to hide the blinking cursor as it makes the output hard + // to read. + print!("{HIDE_CURSOR}"); + p.clone().show(); + // wait for the test to conclude plus a bit of buffer time for packets in + // flight. + sleep(duration + Duration::from_millis(250)); + for _ in 0..p.targets.lock().unwrap().len() { + println!(); + } + // turn the blinky cursor back on + print!("{SHOW_CURSOR}"); + + // return a report to the caller + let v4 = p.targets.lock().unwrap().values().copied().collect(); + Report { v4 } +} + +struct Pinger4 { + sock: Socket, + targets: Mutex>, +} + +/// Running results for an IPv4 ping test. +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub struct Ping4State { + /// Destination address of the ping test. + pub dest: Ipv4Addr, + /// Low water mark for ping round trip times. + pub low: Duration, + /// High water mark for ping round trip times. + pub high: Duration, + /// Summation of ping round trip times. + pub sum: Duration, + /// The last recorded ping round trip time. + pub current: Option, + /// The number of ICMP packets considered lost. Does not start ticking + /// until at least one reply has been received. + pub lost: usize, + /// The number of packets sent. + pub tx_count: u16, + /// The number of packets received. + pub rx_count: u16, + /// The last time a packet was sent. + #[serde(skip)] + pub sent: Option, + /// The transmit counter value when we received the first reply. + #[serde(skip)] + pub first: u16, +} + +impl Ping4State { + fn new(addr: Ipv4Addr) -> Self { + Self { + dest: addr, + low: Duration::default(), + high: Duration::default(), + sum: Duration::default(), + current: None, + lost: 0, + tx_count: 0, + rx_count: 0, + sent: None, + first: 0, + } + } +} + +impl Pinger4 { + fn new(ttl: u32) -> Arc { + let sock = Socket::new(Domain::IPV4, Type::RAW, Some(Protocol::ICMPV4)) + .unwrap(); + sock.set_ttl(ttl).unwrap(); + let s = Arc::new(Self { sock, targets: Mutex::new(BTreeMap::new()) }); + s.clone().rx(); + s.clone().count_lost(); + s + } + + fn show(self: Arc) { + println!( + "{:15} {:7} {:7} {:7} {:7} {:7} {:7} {}", + "addr".dimmed(), + "low".dimmed(), + "avg".dimmed(), + "high".dimmed(), + "last".dimmed(), + "sent".dimmed(), + "received".dimmed(), + "lost".dimmed() + ); + // run the reporting on a background thread + spawn(move || loop { + // print a status line for each target + for (_id, t) in self.targets.lock().unwrap().iter() { + println!( + "{:15} {:7} {:7} {:7} {:7} {:7} {:7} {:<7}", + t.dest.to_string().cyan(), + format!("{:.3}", (t.low.as_micros() as f32 / 1000.0)), + if t.rx_count == 0 { + format!("{:.3}", 0.0) + } else { + format!( + "{:.3}", + (t.sum.as_micros() as f32 + / 1000.0 + / t.rx_count as f32) + ) + }, + format!("{:.3}", (t.high.as_micros() as f32 / 1000.0)), + match t.current { + Some(dt) => + format!("{:.3}", (dt.as_micros() as f32 / 1000.0)), + None => format!("{:.3}", 0.0), + }, + t.tx_count.to_string(), + t.rx_count.to_string(), + if t.lost == 0 { + t.lost.to_string().green() + } else { + t.lost.to_string().red() + }, + ); + } + // move the cursor back to the top for another round of reporting + for _ in 0..self.targets.lock().unwrap().len() { + print!("{MOVE_CURSOR_UP}"); + } + print!("\r"); + + sleep(Duration::from_millis(100)); + }); + } + + fn add_target( + self: &Arc, + id: u16, + addr: Ipv4Addr, + pps: usize, + duration: Duration, + ) { + self.targets.lock().unwrap().insert(id, Ping4State::new(addr)); + let interval = Duration::from_secs_f64(1.0 / pps as f64); + self.clone().tx(id, addr, interval, duration); + } + + fn tx( + self: Arc, + id: u16, + dst: Ipv4Addr, + interval: Duration, + duration: Duration, + ) { + let mut seq = 0u16; + let stop = Instant::now() + duration; + // send ICMP test packets on a background thread + spawn(move || loop { + if Instant::now() >= stop { + break; + } + let mut c = Checksum::new(); + c.add_bytes(&[ICMP_ECHO_TYPE, ICMP_ECHO_CODE]); + c.add_bytes(&id.to_be_bytes()); + c.add_bytes(&seq.to_be_bytes()); + let pkt = EchoRequest { + typ: ICMP_ECHO_TYPE, + code: ICMP_ECHO_CODE, + checksum: u16::from_be_bytes(c.checksum()), + identifier: id, + sequence_number: seq, + }; + let msg = ispf::to_bytes_be(&pkt).unwrap(); + + match self.targets.lock().unwrap().get_mut(&id) { + Some(ref mut tgt) => { + tgt.sent = Some(Instant::now()); + tgt.tx_count = seq; + let sa: SockAddr = SocketAddrV4::new(dst, 0).into(); + self.sock.send_to(&msg, &sa).unwrap(); + } + None => continue, + } + + seq += 1; + sleep(interval); + }); + } + + // At the end of the day this is not strictly necessary for the final + // report. But it's really nice for interactive use to have a live + // ticker for lost packet count. + fn count_lost(self: Arc) { + spawn(move || loop { + for (_, tgt) in self.targets.lock().unwrap().iter_mut() { + // Only start considering packets lost after the first packet + // is received. This allows the remote endpoint time to come + // online without considering initial packets lost while it's + // coming up. + if tgt.first != 0 { + tgt.lost = tgt + .tx_count + .saturating_sub(tgt.first) + .saturating_sub(tgt.rx_count) + as usize; + } + } + sleep(Duration::from_millis(10)); + }); + } + + fn rx(self: Arc) { + // Spawn a background thread to receive ICMP replies and do the + // necessary accounting. + spawn(move || loop { + let mut ubuf = [MaybeUninit::new(0); 10240]; + if let Ok((sz, _)) = self.sock.recv_from(&mut ubuf) { + let buf = unsafe { &slice_assume_init_ref(&ubuf[..sz]) }; + let msg: EchoRequest = match ispf::from_bytes_be(&buf[20..sz]) { + Ok(msg) => msg, + Err(_) => { + continue; + } + }; + // correlate the ICMP id with a target + match self.targets.lock().unwrap().get_mut(&msg.identifier) { + Some(ref mut target) => match target.sent { + Some(ref mut sent) => { + let t1 = Instant::now(); + let dt = t1 - *sent; + target.current = Some(dt); + if target.low == Duration::ZERO || dt < target.low { + target.low = dt; + } + if dt > target.high { + target.high = dt; + } + target.sum += dt; + target.current = Some(dt); + target.rx_count += 1; + if target.first == 0 { + target.first = target.tx_count; + } + } + None => { + println!("no sent"); + } + }, + None => { + println!("no target {}", msg.identifier); + } + } + } + }); + } +} + +// TODO: Use `MaybeUninit::slice_assume_init_ref` once it has stabilized +unsafe fn slice_assume_init_ref(slice: &[MaybeUninit]) -> &[T] { + unsafe { &*(slice as *const [MaybeUninit] as *const [T]) } +} diff --git a/end-to-end-tests/src/helpers/mod.rs b/end-to-end-tests/src/helpers/mod.rs index db03973555..b7cd6d5574 100644 --- a/end-to-end-tests/src/helpers/mod.rs +++ b/end-to-end-tests/src/helpers/mod.rs @@ -1,4 +1,6 @@ +pub mod cli; pub mod ctx; +pub mod icmp; use self::ctx::nexus_addr; use anyhow::{bail, Result}; diff --git a/end-to-end-tests/src/instance_launch.rs b/end-to-end-tests/src/instance_launch.rs index f27261e82d..1aae46fe98 100644 --- a/end-to-end-tests/src/instance_launch.rs +++ b/end-to-end-tests/src/instance_launch.rs @@ -92,6 +92,7 @@ async fn instance_launch() -> Result<()> { .first() .context("no external IPs")? .clone(); + let ExternalIp::Ephemeral { ip: ip_addr } = ip_addr else { anyhow::bail!("IP bound to instance was not ephemeral as required.") }; diff --git a/illumos-utils/src/running_zone.rs b/illumos-utils/src/running_zone.rs index 02302347cd..0dd8f85e4e 100644 --- a/illumos-utils/src/running_zone.rs +++ b/illumos-utils/src/running_zone.rs @@ -912,10 +912,16 @@ impl RunningZone { Ok(()) } + /// Return a reference to the links for this zone. pub fn links(&self) -> &Vec { &self.inner.links } + /// Return a mutable reference to the links for this zone. + pub fn links_mut(&mut self) -> &mut Vec { + &mut self.inner.links + } + /// Return the running processes associated with all the SMF services this /// zone is intended to run. pub fn service_processes( diff --git a/installinator/src/dispatch.rs b/installinator/src/dispatch.rs index 9bec14664c..1fcf351a9b 100644 --- a/installinator/src/dispatch.rs +++ b/installinator/src/dispatch.rs @@ -151,13 +151,13 @@ struct InstallOpts { #[clap(long)] install_on_gimlet: bool, - //TODO(ry) this probably needs to get plumbed somewhere instead of relying + //TODO this probably needs to get plumbed somewhere instead of relying //on a default. /// The first gimlet data link to use. #[clap(long, default_value = "cxgbe0")] data_link0: String, - //TODO(ry) this probably needs to get plumbed somewhere instead of relying + //TODO this probably needs to get plumbed somewhere instead of relying //on a default. /// The second gimlet data link to use. #[clap(long, default_value = "cxgbe1")] diff --git a/nexus/Cargo.toml b/nexus/Cargo.toml index 45d1c34382..0712ba6743 100644 --- a/nexus/Cargo.toml +++ b/nexus/Cargo.toml @@ -35,6 +35,7 @@ hyper.workspace = true illumos-utils.workspace = true internal-dns.workspace = true ipnetwork.workspace = true +itertools.workspace = true macaddr.workspace = true mime_guess.workspace = true # Not under "dev-dependencies"; these also need to be implemented for @@ -104,7 +105,6 @@ diesel.workspace = true dns-server.workspace = true expectorate.workspace = true hyper-rustls.workspace = true -itertools.workspace = true gateway-messages.workspace = true gateway-test-utils.workspace = true hubtools.workspace = true diff --git a/nexus/db-model/src/deployment.rs b/nexus/db-model/src/deployment.rs index a1f285fbef..e9a650812b 100644 --- a/nexus/db-model/src/deployment.rs +++ b/nexus/db-model/src/deployment.rs @@ -15,6 +15,7 @@ use crate::{ipv6, Generation, MacAddr, Name, SqlU16, SqlU32, SqlU8}; use chrono::{DateTime, Utc}; use ipnetwork::IpNetwork; use nexus_types::deployment::BlueprintTarget; +use omicron_common::api::internal::shared::NetworkInterface; use uuid::Uuid; /// See [`nexus_types::deployment::Blueprint`]. @@ -249,7 +250,7 @@ impl BpOmicronZoneNic { pub fn into_network_interface_for_zone( self, zone_id: Uuid, - ) -> Result { + ) -> Result { let zone_nic = OmicronZoneNic::from(self); zone_nic.into_network_interface_for_zone(zone_id) } diff --git a/nexus/db-model/src/external_ip.rs b/nexus/db-model/src/external_ip.rs index 0f484f7610..337e7ef2a7 100644 --- a/nexus/db-model/src/external_ip.rs +++ b/nexus/db-model/src/external_ip.rs @@ -22,6 +22,7 @@ use nexus_types::external_api::views; use omicron_common::address::NUM_SOURCE_NAT_PORTS; use omicron_common::api::external::Error; use omicron_common::api::external::IdentityMetadata; +use schemars::JsonSchema; use serde::Deserialize; use serde::Serialize; use sled_agent_client::types::InstanceExternalIpBody; @@ -34,7 +35,7 @@ impl_enum_type!( #[diesel(postgres_type(name = "ip_kind", schema = "public"))] pub struct IpKindEnum; - #[derive(Clone, Copy, Debug, AsExpression, FromSqlRow, PartialEq, Eq, Deserialize, Serialize)] + #[derive(Clone, Copy, Debug, AsExpression, FromSqlRow, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[diesel(sql_type = IpKindEnum)] pub enum IpKind; @@ -48,7 +49,7 @@ impl_enum_type!( #[diesel(postgres_type(name = "ip_attach_state"))] pub struct IpAttachStateEnum; - #[derive(Clone, Copy, Debug, AsExpression, FromSqlRow, PartialEq, Eq, Deserialize, Serialize)] + #[derive(Clone, Copy, Debug, AsExpression, FromSqlRow, PartialEq, Eq, Deserialize, Serialize, JsonSchema)] #[diesel(sql_type = IpAttachStateEnum)] pub enum IpAttachState; @@ -95,10 +96,11 @@ impl std::fmt::Display for IpKind { Selectable, Queryable, Insertable, - Deserialize, - Serialize, PartialEq, Eq, + Serialize, + Deserialize, + JsonSchema, )] #[diesel(table_name = external_ip)] pub struct ExternalIp { @@ -125,6 +127,7 @@ pub struct ExternalIp { // Only Some(_) for instance Floating IPs pub project_id: Option, pub state: IpAttachState, + pub is_probe: bool, } /// A view type constructed from `ExternalIp` used to represent Floating IP @@ -171,6 +174,7 @@ pub struct IncompleteExternalIp { time_created: DateTime, kind: IpKind, is_service: bool, + is_probe: bool, parent_id: Option, pool_id: Uuid, project_id: Option, @@ -195,6 +199,7 @@ impl IncompleteExternalIp { time_created: Utc::now(), kind, is_service: false, + is_probe: false, parent_id: Some(instance_id), pool_id, project_id: None, @@ -214,6 +219,30 @@ impl IncompleteExternalIp { kind, is_service: false, parent_id: None, + is_probe: false, + pool_id, + project_id: None, + explicit_ip: None, + explicit_port_range: None, + state: kind.initial_state(), + } + } + + pub fn for_ephemeral_probe( + id: Uuid, + instance_id: Uuid, + pool_id: Uuid, + ) -> Self { + let kind = IpKind::Ephemeral; + Self { + id, + name: None, + description: None, + time_created: Utc::now(), + kind: IpKind::Ephemeral, + is_service: false, + is_probe: true, + parent_id: Some(instance_id), pool_id, project_id: None, explicit_ip: None, @@ -237,6 +266,7 @@ impl IncompleteExternalIp { time_created: Utc::now(), kind, is_service: false, + is_probe: false, parent_id: None, pool_id, project_id: Some(project_id), @@ -262,6 +292,7 @@ impl IncompleteExternalIp { time_created: Utc::now(), kind, is_service: false, + is_probe: false, parent_id: None, pool_id, project_id: Some(project_id), @@ -286,6 +317,7 @@ impl IncompleteExternalIp { time_created: Utc::now(), kind: IpKind::Floating, is_service: true, + is_probe: false, parent_id: Some(service_id), pool_id, project_id: None, @@ -317,6 +349,7 @@ impl IncompleteExternalIp { time_created: Utc::now(), kind, is_service: true, + is_probe: false, parent_id: Some(service_id), pool_id, project_id: None, @@ -341,6 +374,7 @@ impl IncompleteExternalIp { time_created: Utc::now(), kind, is_service: true, + is_probe: false, parent_id: Some(service_id), pool_id, project_id: None, @@ -359,6 +393,7 @@ impl IncompleteExternalIp { time_created: Utc::now(), kind, is_service: true, + is_probe: false, parent_id: Some(service_id), pool_id, project_id: None, @@ -392,6 +427,10 @@ impl IncompleteExternalIp { &self.is_service } + pub fn is_probe(&self) -> &bool { + &self.is_probe + } + pub fn parent_id(&self) -> &Option { &self.parent_id } diff --git a/nexus/db-model/src/inventory.rs b/nexus/db-model/src/inventory.rs index 0992eb60b5..2f69fd998c 100644 --- a/nexus/db-model/src/inventory.rs +++ b/nexus/db-model/src/inventory.rs @@ -28,6 +28,7 @@ use ipnetwork::IpNetwork; use nexus_types::inventory::{ BaseboardId, Caboose, Collection, PowerState, RotPage, RotSlot, }; +use omicron_common::api::internal::shared::NetworkInterface; use uuid::Uuid; // See [`nexus_types::inventory::PowerState`]. @@ -872,7 +873,7 @@ impl InvOmicronZoneNic { pub fn into_network_interface_for_zone( self, zone_id: Uuid, - ) -> Result { + ) -> Result { let zone_nic = OmicronZoneNic::from(self); zone_nic.into_network_interface_for_zone(zone_id) } diff --git a/nexus/db-model/src/lib.rs b/nexus/db-model/src/lib.rs index 07c9f5eec6..5c89134b78 100644 --- a/nexus/db-model/src/lib.rs +++ b/nexus/db-model/src/lib.rs @@ -45,6 +45,7 @@ mod network_interface; mod oximeter_info; mod physical_disk; mod physical_disk_kind; +mod probe; mod producer_endpoint; mod project; mod semver_version; @@ -145,6 +146,7 @@ pub use network_interface::*; pub use oximeter_info::*; pub use physical_disk::*; pub use physical_disk_kind::*; +pub use probe::*; pub use producer_endpoint::*; pub use project::*; pub use quota::*; diff --git a/nexus/db-model/src/network_interface.rs b/nexus/db-model/src/network_interface.rs index 72752ae3f8..fdcfcbf588 100644 --- a/nexus/db-model/src/network_interface.rs +++ b/nexus/db-model/src/network_interface.rs @@ -14,7 +14,7 @@ use db_macros::Resource; use diesel::AsChangeset; use nexus_types::external_api::params; use nexus_types::identity::Resource; -use omicron_common::api::external; +use omicron_common::api::{external, internal}; use uuid::Uuid; /// The max number of interfaces that may be associated with a resource, @@ -35,6 +35,7 @@ impl_enum_type! { Instance => b"instance" Service => b"service" + Probe => b"probe" } /// Generic Network Interface DB model. @@ -63,6 +64,41 @@ pub struct NetworkInterface { pub primary: bool, } +impl NetworkInterface { + pub fn into_internal( + self, + subnet: external::IpNet, + ) -> internal::shared::NetworkInterface { + internal::shared::NetworkInterface { + id: self.id(), + kind: match self.kind { + NetworkInterfaceKind::Instance => { + internal::shared::NetworkInterfaceKind::Instance { + id: self.parent_id, + } + } + NetworkInterfaceKind::Service => { + internal::shared::NetworkInterfaceKind::Service { + id: self.parent_id, + } + } + NetworkInterfaceKind::Probe => { + internal::shared::NetworkInterfaceKind::Probe { + id: self.parent_id, + } + } + }, + name: self.name().clone(), + ip: self.ip.ip(), + mac: self.mac.into(), + subnet: subnet, + vni: external::Vni::try_from(0).unwrap(), + primary: self.primary, + slot: self.slot.try_into().unwrap(), + } + } +} + /// Instance Network Interface DB model. /// /// The underlying "table" (`instance_network_interface`) is actually a view @@ -244,6 +280,13 @@ impl IncompleteNetworkInterface { ))); } } + NetworkInterfaceKind::Probe => { + if !mac.is_guest() { + return Err(external::Error::invalid_request(format!( + "invalid MAC address {mac} for probe NIC", + ))); + } + } NetworkInterfaceKind::Service => { if !mac.is_system() { return Err(external::Error::invalid_request(format!( @@ -312,6 +355,26 @@ impl IncompleteNetworkInterface { Some(slot), ) } + + pub fn new_probe( + interface_id: Uuid, + probe_id: Uuid, + subnet: VpcSubnet, + identity: external::IdentityMetadataCreateParams, + ip: Option, + mac: Option, + ) -> Result { + Self::new( + interface_id, + NetworkInterfaceKind::Probe, + probe_id, + subnet, + identity, + ip, + mac, + None, + ) + } } /// Describes a set of updates for the [`NetworkInterface`] model. diff --git a/nexus/db-model/src/omicron_zone_config.rs b/nexus/db-model/src/omicron_zone_config.rs index f4726ccd92..ce3127a9b3 100644 --- a/nexus/db-model/src/omicron_zone_config.rs +++ b/nexus/db-model/src/omicron_zone_config.rs @@ -18,6 +18,9 @@ use crate::{ipv6, MacAddr, Name, SqlU16, SqlU32, SqlU8}; use anyhow::{anyhow, bail, ensure, Context}; use ipnetwork::IpNetwork; use nexus_types::inventory::OmicronZoneType; +use omicron_common::api::internal::shared::{ + NetworkInterface, NetworkInterfaceKind, +}; use uuid::Uuid; #[derive(Debug)] @@ -410,9 +413,7 @@ impl OmicronZoneNic { ensure!( matches!( nic.kind, - nexus_types::inventory::NetworkInterfaceKind::Service( - id - ) if id == zone.id + NetworkInterfaceKind::Service{ id } if id == zone.id ), "expected zone's NIC kind to be \"service\" and the \ id to match the zone's id ({})", @@ -424,7 +425,7 @@ impl OmicronZoneNic { name: Name::from(nic.name.clone()), ip: IpNetwork::from(nic.ip), mac: MacAddr::from(nic.mac), - subnet: IpNetwork::from(nic.subnet.clone()), + subnet: IpNetwork::from(nic.subnet), vni: SqlU32::from(u32::from(nic.vni)), is_primary: nic.primary, slot: SqlU8::from(nic.slot), @@ -437,13 +438,11 @@ impl OmicronZoneNic { pub(crate) fn into_network_interface_for_zone( self, zone_id: Uuid, - ) -> anyhow::Result { - Ok(nexus_types::inventory::NetworkInterface { + ) -> anyhow::Result { + Ok(NetworkInterface { id: self.id, ip: self.ip.ip(), - kind: nexus_types::inventory::NetworkInterfaceKind::Service( - zone_id, - ), + kind: NetworkInterfaceKind::Service { id: zone_id }, mac: *self.mac, name: self.name.into(), primary: self.is_primary, diff --git a/nexus/db-model/src/probe.rs b/nexus/db-model/src/probe.rs new file mode 100644 index 0000000000..be3576dfa0 --- /dev/null +++ b/nexus/db-model/src/probe.rs @@ -0,0 +1,50 @@ +use crate::schema::probe; +use db_macros::Resource; +use nexus_types::external_api::params; +use nexus_types::identity::Resource; +use omicron_common::api::external; +use omicron_common::api::external::IdentityMetadataCreateParams; +use serde::Deserialize; +use serde::Serialize; +use uuid::Uuid; + +#[derive( + Queryable, + Insertable, + Selectable, + Clone, + Debug, + Resource, + Serialize, + Deserialize, +)] +#[diesel(table_name = probe)] +pub struct Probe { + #[diesel(embed)] + pub identity: ProbeIdentity, + + pub project_id: Uuid, + pub sled: Uuid, +} + +impl Probe { + pub fn from_create(p: ¶ms::ProbeCreate, project_id: Uuid) -> Self { + Self { + identity: ProbeIdentity::new( + Uuid::new_v4(), + IdentityMetadataCreateParams { + name: p.identity.name.clone(), + description: p.identity.description.clone(), + }, + ), + project_id, + sled: p.sled, + } + } +} + +impl Into for Probe { + fn into(self) -> external::Probe { + external::Probe { identity: self.identity().clone(), sled: self.sled } + } +} diff --git a/nexus/db-model/src/schema.rs b/nexus/db-model/src/schema.rs index 09bc963936..e2b918d805 100644 --- a/nexus/db-model/src/schema.rs +++ b/nexus/db-model/src/schema.rs @@ -13,7 +13,7 @@ use omicron_common::api::external::SemverVersion; /// /// This should be updated whenever the schema is changed. For more details, /// refer to: schema/crdb/README.adoc -pub const SCHEMA_VERSION: SemverVersion = SemverVersion::new(39, 0, 0); +pub const SCHEMA_VERSION: SemverVersion = SemverVersion::new(40, 0, 0); table! { disk (id) { @@ -591,6 +591,7 @@ table! { project_id -> Nullable, state -> crate::IpAttachStateEnum, + is_probe -> Bool, } } @@ -1520,6 +1521,19 @@ table! { } } +table! { + probe (id) { + id -> Uuid, + name -> Text, + description -> Text, + time_created -> Timestamptz, + time_modified -> Timestamptz, + time_deleted -> Nullable, + project_id -> Uuid, + sled -> Uuid, + } +} + table! { db_metadata (singleton) { singleton -> Bool, diff --git a/nexus/db-model/src/unsigned.rs b/nexus/db-model/src/unsigned.rs index b4e9db2308..920cad1cff 100644 --- a/nexus/db-model/src/unsigned.rs +++ b/nexus/db-model/src/unsigned.rs @@ -7,6 +7,7 @@ use diesel::deserialize::{self, FromSql}; use diesel::pg::Pg; use diesel::serialize::{self, ToSql}; use diesel::sql_types; +use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::convert::TryFrom; @@ -76,6 +77,7 @@ where FromSqlRow, Serialize, Deserialize, + JsonSchema, )] #[diesel(sql_type = sql_types::Int4)] #[repr(transparent)] diff --git a/nexus/db-queries/Cargo.toml b/nexus/db-queries/Cargo.toml index e673003036..a33ca24c84 100644 --- a/nexus/db-queries/Cargo.toml +++ b/nexus/db-queries/Cargo.toml @@ -35,6 +35,7 @@ pq-sys = "*" rand.workspace = true ref-cast.workspace = true samael.workspace = true +schemars.workspace = true serde.workspace = true serde_json.workspace = true serde_urlencoded.workspace = true diff --git a/nexus/db-queries/src/db/datastore/external_ip.rs b/nexus/db-queries/src/db/datastore/external_ip.rs index f561a024e8..017d2f22d2 100644 --- a/nexus/db-queries/src/db/datastore/external_ip.rs +++ b/nexus/db-queries/src/db/datastore/external_ip.rs @@ -46,6 +46,7 @@ use omicron_common::api::external::Error; use omicron_common::api::external::IdentityMetadataCreateParams; use omicron_common::api::external::ListResultVec; use omicron_common::api::external::LookupResult; +use omicron_common::api::external::NameOrId; use omicron_common::api::external::ResourceType; use omicron_common::api::external::UpdateResult; use ref_cast::RefCast; @@ -71,6 +72,42 @@ impl DataStore { self.allocate_external_ip(opctx, data).await } + /// Create an Ephemeral IP address for a probe. + pub async fn allocate_probe_ephemeral_ip( + &self, + opctx: &OpContext, + ip_id: Uuid, + probe_id: Uuid, + pool_name: Option, + ) -> CreateResult { + let pool = match pool_name { + Some(NameOrId::Name(name)) => { + let (.., pool) = LookupPath::new(opctx, &self) + .ip_pool_name(&name.into()) + .fetch_for(authz::Action::CreateChild) + .await?; + pool + } + Some(NameOrId::Id(id)) => { + let (.., pool) = LookupPath::new(opctx, &self) + .ip_pool_id(id) + .fetch_for(authz::Action::CreateChild) + .await?; + pool + } + // If no name given, use the default pool + None => { + let (.., pool) = self.ip_pools_fetch_default(&opctx).await?; + pool + } + }; + + let pool_id = pool.identity.id; + let data = + IncompleteExternalIp::for_ephemeral_probe(ip_id, probe_id, pool_id); + self.allocate_external_ip(opctx, data).await + } + /// Create an Ephemeral IP address for an instance. /// /// For consistency between instance create and External IP attach/detach @@ -725,6 +762,7 @@ impl DataStore { diesel::update(dsl::external_ip) .filter(dsl::time_deleted.is_null()) .filter(dsl::is_service.eq(false)) + .filter(dsl::is_probe.eq(false)) .filter(dsl::parent_id.eq(instance_id)) .filter(dsl::kind.ne(IpKind::Floating)) .set(( @@ -736,7 +774,31 @@ impl DataStore { .map_err(|e| public_error_from_diesel(e, ErrorHandler::Server)) } - /// Detach all Floating IP address from their parent instance. + /// Delete all external IP addresses associated with the provided probe + /// ID. + /// + /// This method returns the number of records deleted, rather than the usual + /// `DeleteResult`. That's mostly useful for tests, but could be important + /// if callers have some invariants they'd like to check. + pub async fn deallocate_external_ip_by_probe_id( + &self, + opctx: &OpContext, + probe_id: Uuid, + ) -> Result { + use db::schema::external_ip::dsl; + let now = Utc::now(); + diesel::update(dsl::external_ip) + .filter(dsl::time_deleted.is_null()) + .filter(dsl::is_probe.eq(true)) + .filter(dsl::parent_id.eq(probe_id)) + .filter(dsl::kind.ne(IpKind::Ephemeral)) + .set(dsl::time_deleted.eq(now)) + .execute_async(&*self.pool_connection_authorized(opctx).await?) + .await + .map_err(|e| public_error_from_diesel(e, ErrorHandler::Server)) + } + + /// Detach an individual Floating IP address from their parent instance. /// /// As in `deallocate_external_ip_by_instance_id`, this method returns the /// number of records altered, rather than an `UpdateResult`. @@ -774,6 +836,7 @@ impl DataStore { use db::schema::external_ip::dsl; dsl::external_ip .filter(dsl::is_service.eq(false)) + .filter(dsl::is_probe.eq(false)) .filter(dsl::parent_id.eq(instance_id)) .filter(dsl::time_deleted.is_null()) .select(ExternalIp::as_select()) @@ -796,6 +859,23 @@ impl DataStore { .find(|v| v.kind == IpKind::Ephemeral)) } + /// Fetch all external IP addresses of any kind for the provided probe + pub async fn probe_lookup_external_ips( + &self, + opctx: &OpContext, + probe_id: Uuid, + ) -> LookupResult> { + use db::schema::external_ip::dsl; + dsl::external_ip + .filter(dsl::is_probe.eq(true)) + .filter(dsl::parent_id.eq(probe_id)) + .filter(dsl::time_deleted.is_null()) + .select(ExternalIp::as_select()) + .get_results_async(&*self.pool_connection_authorized(opctx).await?) + .await + .map_err(|e| public_error_from_diesel(e, ErrorHandler::Server)) + } + /// Fetch all Floating IP addresses for the provided project. pub async fn floating_ips_list( &self, diff --git a/nexus/db-queries/src/db/datastore/mod.rs b/nexus/db-queries/src/db/datastore/mod.rs index b164186fdf..475fb27df1 100644 --- a/nexus/db-queries/src/db/datastore/mod.rs +++ b/nexus/db-queries/src/db/datastore/mod.rs @@ -70,6 +70,7 @@ mod ipv4_nat_entry; mod network_interface; mod oximeter; mod physical_disk; +mod probe; mod project; mod quota; mod rack; @@ -106,6 +107,7 @@ pub use db_metadata::{ pub use dns::DnsVersionUpdateBuilder; pub use instance::InstanceAndActiveVmm; pub use inventory::DataStoreInventoryTest; +pub use probe::ProbeInfo; pub use rack::RackInit; pub use silo::Discoverability; pub use switch_port::SwitchPortSettingsCombinedResult; @@ -1681,6 +1683,7 @@ mod test { first_port: crate::db::model::SqlU16(0), last_port: crate::db::model::SqlU16(10), state: nexus_db_model::IpAttachState::Attached, + is_probe: false, }) .collect::>(); diesel::insert_into(dsl::external_ip) @@ -1743,6 +1746,7 @@ mod test { first_port: crate::db::model::SqlU16(0), last_port: crate::db::model::SqlU16(10), state: nexus_db_model::IpAttachState::Attached, + is_probe: false, }; diesel::insert_into(dsl::external_ip) .values(ip.clone()) @@ -1814,6 +1818,7 @@ mod test { first_port: crate::db::model::SqlU16(0), last_port: crate::db::model::SqlU16(10), state: nexus_db_model::IpAttachState::Attached, + is_probe: false, }; // Combinations of NULL and non-NULL for: diff --git a/nexus/db-queries/src/db/datastore/network_interface.rs b/nexus/db-queries/src/db/datastore/network_interface.rs index f2782e8f67..1bccca4e97 100644 --- a/nexus/db-queries/src/db/datastore/network_interface.rs +++ b/nexus/db-queries/src/db/datastore/network_interface.rs @@ -35,11 +35,11 @@ use omicron_common::api::external::http_pagination::PaginatedBy; use omicron_common::api::external::DeleteResult; use omicron_common::api::external::Error; use omicron_common::api::external::ListResultVec; +use omicron_common::api::external::LookupResult; use omicron_common::api::external::LookupType; use omicron_common::api::external::ResourceType; use omicron_common::api::external::UpdateResult; use ref_cast::RefCast; -use sled_agent_client::types as sled_client_types; use uuid::Uuid; /// OPTE requires information that's currently split across the network @@ -59,8 +59,10 @@ struct NicInfo { slot: i16, } -impl From for sled_client_types::NetworkInterface { - fn from(nic: NicInfo) -> sled_client_types::NetworkInterface { +impl From for omicron_common::api::internal::shared::NetworkInterface { + fn from( + nic: NicInfo, + ) -> omicron_common::api::internal::shared::NetworkInterface { let ip_subnet = if nic.ip.is_ipv4() { external::IpNet::V4(nic.ipv4_block.0) } else { @@ -68,19 +70,22 @@ impl From for sled_client_types::NetworkInterface { }; let kind = match nic.kind { NetworkInterfaceKind::Instance => { - sled_client_types::NetworkInterfaceKind::Instance(nic.parent_id) + omicron_common::api::internal::shared::NetworkInterfaceKind::Instance{ id: nic.parent_id } } NetworkInterfaceKind::Service => { - sled_client_types::NetworkInterfaceKind::Service(nic.parent_id) + omicron_common::api::internal::shared::NetworkInterfaceKind::Service{ id: nic.parent_id } + } + NetworkInterfaceKind::Probe => { + omicron_common::api::internal::shared::NetworkInterfaceKind::Probe{ id: nic.parent_id } } }; - sled_client_types::NetworkInterface { + omicron_common::api::internal::shared::NetworkInterface { id: nic.id, kind, name: nic.name.into(), ip: nic.ip.ip(), mac: nic.mac.0, - subnet: sled_client_types::IpNet::from(ip_subnet), + subnet: ip_subnet, vni: nic.vni.0, primary: nic.primary, slot: u8::try_from(nic.slot).unwrap(), @@ -108,6 +113,14 @@ impl DataStore { self.instance_create_network_interface_raw(&opctx, interface).await } + pub async fn probe_create_network_interface( + &self, + opctx: &OpContext, + interface: IncompleteNetworkInterface, + ) -> Result { + self.create_network_interface_raw(&opctx, interface).await + } + pub(crate) async fn instance_create_network_interface_raw( &self, opctx: &OpContext, @@ -271,6 +284,33 @@ impl DataStore { Ok(()) } + /// Delete all network interfaces attached to the given probe. + pub async fn probe_delete_all_network_interfaces( + &self, + opctx: &OpContext, + probe_id: Uuid, + ) -> DeleteResult { + use db::schema::network_interface::dsl; + let now = Utc::now(); + diesel::update(dsl::network_interface) + .filter(dsl::parent_id.eq(probe_id)) + .filter(dsl::kind.eq(NetworkInterfaceKind::Probe)) + .filter(dsl::time_deleted.is_null()) + .set(dsl::time_deleted.eq(now)) + .execute_async(&*self.pool_connection_authorized(opctx).await?) + .await + .map_err(|e| { + public_error_from_diesel( + e, + ErrorHandler::NotFoundByLookup( + ResourceType::Probe, + LookupType::ById(probe_id), + ), + ) + })?; + Ok(()) + } + /// Delete an `InstanceNetworkInterface` attached to a provided instance. /// /// Note that the primary interface for an instance cannot be deleted if @@ -313,7 +353,8 @@ impl DataStore { &self, opctx: &OpContext, partial_query: BoxedQuery, - ) -> ListResultVec { + ) -> ListResultVec + { use db::schema::network_interface; use db::schema::vpc; use db::schema::vpc_subnet; @@ -349,7 +390,7 @@ impl DataStore { .map_err(|e| public_error_from_diesel(e, ErrorHandler::Server))?; Ok(rows .into_iter() - .map(sled_client_types::NetworkInterface::from) + .map(omicron_common::api::internal::shared::NetworkInterface::from) .collect()) } @@ -359,7 +400,8 @@ impl DataStore { &self, opctx: &OpContext, authz_instance: &authz::Instance, - ) -> ListResultVec { + ) -> ListResultVec + { opctx.authorize(authz::Action::ListChildren, authz_instance).await?; use db::schema::network_interface; @@ -375,13 +417,31 @@ impl DataStore { .await } + pub async fn derive_probe_network_interface_info( + &self, + opctx: &OpContext, + probe_id: Uuid, + ) -> ListResultVec + { + use db::schema::network_interface; + self.derive_network_interface_info( + opctx, + network_interface::table + .filter(network_interface::parent_id.eq(probe_id)) + .filter(network_interface::kind.eq(NetworkInterfaceKind::Probe)) + .into_boxed(), + ) + .await + } + /// Return information about all VNICs connected to a VPC required /// for the sled agent to instantiate firewall rules via OPTE. pub async fn derive_vpc_network_interface_info( &self, opctx: &OpContext, authz_vpc: &authz::Vpc, - ) -> ListResultVec { + ) -> ListResultVec + { opctx.authorize(authz::Action::ListChildren, authz_vpc).await?; use db::schema::network_interface; @@ -400,7 +460,8 @@ impl DataStore { &self, opctx: &OpContext, authz_subnet: &authz::VpcSubnet, - ) -> ListResultVec { + ) -> ListResultVec + { opctx.authorize(authz::Action::ListChildren, authz_subnet).await?; use db::schema::network_interface; @@ -443,6 +504,25 @@ impl DataStore { .map_err(|e| public_error_from_diesel(e, ErrorHandler::Server)) } + /// Get network interface associated with a given probe. + pub async fn probe_get_network_interface( + &self, + opctx: &OpContext, + probe_id: Uuid, + ) -> LookupResult { + use db::schema::network_interface::dsl; + + dsl::network_interface + .filter(dsl::time_deleted.is_null()) + .filter(dsl::parent_id.eq(probe_id)) + .select(NetworkInterface::as_select()) + .first_async::( + &*self.pool_connection_authorized(opctx).await?, + ) + .await + .map_err(|e| public_error_from_diesel(e, ErrorHandler::Server)) + } + /// Update a network interface associated with a given instance. pub async fn instance_update_network_interface( &self, diff --git a/nexus/db-queries/src/db/datastore/probe.rs b/nexus/db-queries/src/db/datastore/probe.rs new file mode 100644 index 0000000000..f1e737e353 --- /dev/null +++ b/nexus/db-queries/src/db/datastore/probe.rs @@ -0,0 +1,390 @@ +use std::net::IpAddr; + +use crate::authz; +use crate::context::OpContext; +use crate::db; +use crate::db::datastore::DataStoreConnection; +use crate::db::error::public_error_from_diesel; +use crate::db::error::ErrorHandler; +use crate::db::lookup::LookupPath; +use crate::db::model::Name; +use crate::db::pagination::paginated; +use async_bb8_diesel::AsyncRunQueryDsl; +use chrono::Utc; +use diesel::{ExpressionMethods, QueryDsl, SelectableHelper}; +use nexus_db_model::IncompleteNetworkInterface; +use nexus_db_model::Probe; +use nexus_db_model::VpcSubnet; +use nexus_types::external_api::params; +use nexus_types::identity::Resource; +use omicron_common::api::external::http_pagination::PaginatedBy; +use omicron_common::api::external::CreateResult; +use omicron_common::api::external::DataPageParams; +use omicron_common::api::external::DeleteResult; +use omicron_common::api::external::IdentityMetadataCreateParams; +use omicron_common::api::external::ListResultVec; +use omicron_common::api::external::LookupResult; +use omicron_common::api::external::LookupType; +use omicron_common::api::external::NameOrId; +use omicron_common::api::external::ResourceType; +use omicron_common::api::internal::shared::NetworkInterface; +use ref_cast::RefCast; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +#[derive(Debug, Clone, JsonSchema, Serialize, Deserialize)] +pub struct ProbeInfo { + pub id: Uuid, + pub name: Name, + sled: Uuid, + pub external_ips: Vec, + pub interface: NetworkInterface, +} + +#[derive(Debug, Clone, JsonSchema, Serialize, Deserialize)] +pub struct ProbeExternalIp { + ip: IpAddr, + first_port: u16, + last_port: u16, + kind: IpKind, +} + +impl From for ProbeExternalIp { + fn from(value: nexus_db_model::ExternalIp) -> Self { + Self { + ip: value.ip.ip(), + first_port: value.first_port.0, + last_port: value.last_port.0, + kind: value.kind.into(), + } + } +} + +#[derive(Debug, Clone, JsonSchema, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum IpKind { + Snat, + Floating, + Ephemeral, +} + +impl From for IpKind { + fn from(value: nexus_db_model::IpKind) -> Self { + match value { + nexus_db_model::IpKind::SNat => Self::Snat, + nexus_db_model::IpKind::Ephemeral => Self::Ephemeral, + nexus_db_model::IpKind::Floating => Self::Floating, + } + } +} + +impl super::DataStore { + /// List the probes for the given project. + pub async fn probe_list( + &self, + opctx: &OpContext, + authz_project: &authz::Project, + pagparams: &PaginatedBy<'_>, + ) -> ListResultVec { + opctx.authorize(authz::Action::ListChildren, authz_project).await?; + + use db::schema::probe::dsl; + use db::schema::vpc_subnet::dsl as vpc_subnet_dsl; + + let pool = self.pool_connection_authorized(opctx).await?; + + let probes = match pagparams { + PaginatedBy::Id(pagparams) => { + paginated(dsl::probe, dsl::id, &pagparams) + } + PaginatedBy::Name(pagparams) => paginated( + dsl::probe, + dsl::name, + &pagparams.map_name(|n| Name::ref_cast(n)), + ), + } + .filter(dsl::project_id.eq(authz_project.id())) + .filter(dsl::time_deleted.is_null()) + .select(Probe::as_select()) + .load_async(&*pool) + .await + .map_err(|e| public_error_from_diesel(e, ErrorHandler::Server))?; + + let mut result = Vec::with_capacity(probes.len()); + + for probe in probes.into_iter() { + let external_ips = self + .probe_lookup_external_ips(opctx, probe.id()) + .await? + .into_iter() + .map(Into::into) + .collect(); + + let interface = + self.probe_get_network_interface(opctx, probe.id()).await?; + + let vni = self.resolve_vpc_to_vni(opctx, interface.vpc_id).await?; + + let db_subnet = vpc_subnet_dsl::vpc_subnet + .filter(vpc_subnet_dsl::id.eq(interface.subnet_id)) + .select(VpcSubnet::as_select()) + .first_async(&*pool) + .await + .map_err(|e| { + public_error_from_diesel(e, ErrorHandler::Server) + })?; + + let mut interface: NetworkInterface = + interface.into_internal(db_subnet.ipv4_block.0.into()); + + interface.vni = vni.0; + + result.push(ProbeInfo { + id: probe.id(), + name: probe.name().clone().into(), + sled: probe.sled, + interface, + external_ips, + }) + } + + Ok(result) + } + + async fn resolve_probe_info( + &self, + opctx: &OpContext, + probe: &Probe, + pool: &DataStoreConnection<'_>, + ) -> LookupResult { + use db::schema::vpc_subnet::dsl as vpc_subnet_dsl; + + let external_ips = self + .probe_lookup_external_ips(opctx, probe.id()) + .await? + .into_iter() + .map(Into::into) + .collect(); + + let interface = + self.probe_get_network_interface(opctx, probe.id()).await?; + + let db_subnet = vpc_subnet_dsl::vpc_subnet + .filter(vpc_subnet_dsl::id.eq(interface.subnet_id)) + .select(VpcSubnet::as_select()) + .first_async(&**pool) + .await + .map_err(|e| public_error_from_diesel(e, ErrorHandler::Server))?; + + let vni = self.resolve_vpc_to_vni(opctx, interface.vpc_id).await?; + + let mut interface: NetworkInterface = + interface.into_internal(db_subnet.ipv4_block.0.into()); + interface.vni = vni.0; + + Ok(ProbeInfo { + id: probe.id(), + name: probe.name().clone().into(), + sled: probe.sled, + interface, + external_ips, + }) + } + + /// List the probes for a given sled. This is used by sled agents for + /// determining what probes they should be running. + pub async fn probe_list_for_sled( + &self, + sled: Uuid, + opctx: &OpContext, + pagparams: &DataPageParams<'_, Uuid>, + ) -> ListResultVec { + use db::schema::probe::dsl; + + let pool = self.pool_connection_authorized(opctx).await?; + + let probes = paginated(dsl::probe, dsl::id, pagparams) + .filter(dsl::time_deleted.is_null()) + .filter(dsl::sled.eq(sled)) + .select(Probe::as_select()) + .load_async(&*pool) + .await + .map_err(|e| public_error_from_diesel(e, ErrorHandler::Server))?; + + let mut result = Vec::with_capacity(probes.len()); + + for probe in probes.into_iter() { + result.push(self.resolve_probe_info(opctx, &probe, &pool).await?); + } + + Ok(result) + } + + /// Get information about a particular probe given its name or id. + pub async fn probe_get( + &self, + opctx: &OpContext, + authz_project: &authz::Project, + name_or_id: &NameOrId, + ) -> LookupResult { + use db::schema::probe; + use db::schema::probe::dsl; + let pool = self.pool_connection_authorized(opctx).await?; + + let name_or_id = name_or_id.clone(); + + let probe = match name_or_id { + NameOrId::Name(name) => dsl::probe + .filter(probe::name.eq(name.to_string())) + .filter(probe::time_deleted.is_null()) + .filter(probe::project_id.eq(authz_project.id())) + .select(Probe::as_select()) + .limit(1) + .first_async::(&*pool) + .await + .map_err(|e| { + public_error_from_diesel( + e, + ErrorHandler::NotFoundByLookup( + ResourceType::Probe, + LookupType::ByName(name.to_string()), + ), + ) + }), + NameOrId::Id(id) => dsl::probe + .filter(probe::id.eq(id)) + .filter(probe::project_id.eq(authz_project.id())) + .select(Probe::as_select()) + .limit(1) + .first_async::(&*pool) + .await + .map_err(|e| { + public_error_from_diesel( + e, + ErrorHandler::NotFoundByLookup( + ResourceType::Probe, + LookupType::ById(id), + ), + ) + }), + }?; + + self.resolve_probe_info(opctx, &probe, &pool).await + } + + /// Add a probe to the data store. + pub async fn probe_create( + &self, + opctx: &OpContext, + authz_project: &authz::Project, + new_probe: ¶ms::ProbeCreate, + ) -> CreateResult { + //TODO in transaction + use db::schema::probe::dsl; + let pool = self.pool_connection_authorized(opctx).await?; + + let probe = Probe::from_create(new_probe, authz_project.id()); + + let _eip = self + .allocate_probe_ephemeral_ip( + opctx, + Uuid::new_v4(), + probe.id(), + new_probe.ip_pool.clone().map(Into::into), + ) + .await?; + + let default_name = omicron_common::api::external::Name::try_from( + "default".to_string(), + ) + .unwrap(); + let internal_default_name = db::model::Name::from(default_name.clone()); + + let (.., db_subnet) = LookupPath::new(opctx, self) + .project_id(authz_project.id()) + .vpc_name(&internal_default_name) + .vpc_subnet_name(&internal_default_name) + .fetch() + .await?; + + let incomplete = IncompleteNetworkInterface::new_probe( + Uuid::new_v4(), + probe.id(), + db_subnet, + IdentityMetadataCreateParams { + name: probe.name().clone(), + description: format!( + "default primary interface for {}", + probe.name(), + ), + }, + None, //Request IP address assignment + None, //Request MAC address assignment + )?; + + let _ifx = self + .probe_create_network_interface(opctx, incomplete) + .await + .map_err(|e| { + omicron_common::api::external::Error::InternalError { + internal_message: format!( + "create network interface: {e:?}" + ), + } + })?; + + let result = diesel::insert_into(dsl::probe) + .values(probe.clone()) + .returning(Probe::as_returning()) + .get_result_async(&*pool) + .await + .map_err(|e| public_error_from_diesel(e, ErrorHandler::Server))?; + + Ok(result) + } + + /// Remove a probe from the data store. + pub async fn probe_delete( + &self, + opctx: &OpContext, + authz_project: &authz::Project, + name_or_id: &NameOrId, + ) -> DeleteResult { + use db::schema::probe; + use db::schema::probe::dsl; + let pool = self.pool_connection_authorized(opctx).await?; + + let name_or_id = name_or_id.clone(); + + //TODO in transaction + let id = match name_or_id { + NameOrId::Name(name) => dsl::probe + .filter(probe::name.eq(name.to_string())) + .filter(probe::time_deleted.is_null()) + .filter(probe::project_id.eq(authz_project.id())) + .select(probe::id) + .limit(1) + .first_async::(&*pool) + .await + .map_err(|e| { + public_error_from_diesel(e, ErrorHandler::Server) + })?, + NameOrId::Id(id) => id, + }; + + self.deallocate_external_ip_by_probe_id(opctx, id).await?; + + self.probe_delete_all_network_interfaces(opctx, id).await?; + + diesel::update(dsl::probe) + .filter(dsl::id.eq(id)) + .filter(dsl::project_id.eq(authz_project.id())) + .set(dsl::time_deleted.eq(Utc::now())) + .execute_async(&*pool) + .await + .map_err(|e| public_error_from_diesel(e, ErrorHandler::Server))?; + + Ok(()) + } +} diff --git a/nexus/db-queries/src/db/datastore/vpc.rs b/nexus/db-queries/src/db/datastore/vpc.rs index cd972c0941..8c5d5f4f46 100644 --- a/nexus/db-queries/src/db/datastore/vpc.rs +++ b/nexus/db-queries/src/db/datastore/vpc.rs @@ -1212,6 +1212,30 @@ impl DataStore { ) }) } + + /// Look up a VNI by VPC. + pub async fn resolve_vpc_to_vni( + &self, + opctx: &OpContext, + vpc_id: Uuid, + ) -> LookupResult { + use db::schema::vpc::dsl; + dsl::vpc + .filter(dsl::id.eq(vpc_id)) + .filter(dsl::time_deleted.is_null()) + .select(dsl::vni) + .get_result_async(&*self.pool_connection_authorized(opctx).await?) + .await + .map_err(|e| { + public_error_from_diesel( + e, + ErrorHandler::NotFoundByLookup( + ResourceType::Vpc, + LookupType::ByCompositeId("VNI".to_string()), + ), + ) + }) + } } #[cfg(test)] @@ -1228,8 +1252,6 @@ mod tests { use nexus_test_utils::db::test_setup_database; use nexus_types::deployment::Blueprint; use nexus_types::deployment::BlueprintTarget; - use nexus_types::deployment::NetworkInterface; - use nexus_types::deployment::NetworkInterfaceKind; use nexus_types::deployment::OmicronZoneConfig; use nexus_types::deployment::OmicronZoneType; use nexus_types::deployment::OmicronZonesConfig; @@ -1241,6 +1263,8 @@ mod tests { use omicron_common::api::external::IpNet; use omicron_common::api::external::MacAddr; use omicron_common::api::external::Vni; + use omicron_common::api::internal::shared::NetworkInterface; + use omicron_common::api::internal::shared::NetworkInterfaceKind; use omicron_test_utils::dev; use slog::info; use std::collections::BTreeMap; @@ -1558,13 +1582,15 @@ mod tests { external_ip: "::1".parse().unwrap(), nic: NetworkInterface { id: nic.identity.id, - kind: NetworkInterfaceKind::Service(service.id()), + kind: NetworkInterfaceKind::Service { + id: service.id(), + }, name: format!("test-nic-{}", nic.identity.id) .parse() .unwrap(), ip: nic.ip.unwrap(), mac: nic.mac.unwrap(), - subnet: IpNet::from(*NEXUS_OPTE_IPV4_SUBNET).into(), + subnet: IpNet::from(*NEXUS_OPTE_IPV4_SUBNET), vni: Vni::SERVICES_VNI, primary: true, slot: nic.slot.unwrap(), diff --git a/nexus/db-queries/src/db/queries/external_ip.rs b/nexus/db-queries/src/db/queries/external_ip.rs index 739c0b0809..0502450121 100644 --- a/nexus/db-queries/src/db/queries/external_ip.rs +++ b/nexus/db-queries/src/db/queries/external_ip.rs @@ -132,6 +132,7 @@ const MAX_PORT: u16 = u16::MAX; /// CAST(candidate_first_port AS INT4) AS first_port, /// CAST(candidate_last_port AS INT4) AS last_port, /// AS project_id, +/// AS is_probe, /// AS state /// FROM /// SELECT * FROM ( @@ -419,6 +420,12 @@ impl NextExternalIp { )?; out.push_sql(" AS "); out.push_identifier(dsl::state::NAME)?; + out.push_sql(", "); + + // is_probe flag + out.push_bind_param::(self.ip.is_probe())?; + out.push_sql(" AS "); + out.push_identifier(dsl::is_probe::NAME)?; out.push_sql(" FROM ("); self.push_address_sequence_subquery(out.reborrow())?; diff --git a/nexus/db-queries/src/db/queries/network_interface.rs b/nexus/db-queries/src/db/queries/network_interface.rs index f6ce3e31e3..c0fc18aca1 100644 --- a/nexus/db-queries/src/db/queries/network_interface.rs +++ b/nexus/db-queries/src/db/queries/network_interface.rs @@ -159,6 +159,9 @@ impl InsertError { InsertError::InterfaceAlreadyExists(_name, NetworkInterfaceKind::Service) => { unimplemented!("service network interface") } + InsertError::InterfaceAlreadyExists(_name, NetworkInterfaceKind::Probe) => { + unimplemented!("probe network interface") + } InsertError::NoAvailableIpAddresses => { external::Error::invalid_request( "No available IP addresses for interface", @@ -408,6 +411,9 @@ fn decode_database_error( NetworkInterfaceKind::Service => { external::ResourceType::ServiceNetworkInterface } + NetworkInterfaceKind::Probe => { + external::ResourceType::ProbeNetworkInterface + } }; InsertError::External(error::public_error_from_diesel( err, @@ -647,7 +653,7 @@ impl NextMacShifts { impl NextMacAddress { pub fn new(vpc_id: Uuid, kind: NetworkInterfaceKind) -> Self { let (base, max_shift, min_shift) = match kind { - NetworkInterfaceKind::Instance => { + NetworkInterfaceKind::Instance | NetworkInterfaceKind::Probe => { let NextMacShifts { base, min_shift, max_shift } = NextMacShifts::for_guest(); (base.into(), max_shift, min_shift) @@ -2730,6 +2736,7 @@ mod tests { NetworkInterfaceKind::Service => { (inserted.mac.is_system(), "system") } + NetworkInterfaceKind::Probe => (inserted.mac.is_system(), "probe"), }; assert!( mac_in_range, diff --git a/nexus/reconfigurator/execution/src/resource_allocation.rs b/nexus/reconfigurator/execution/src/resource_allocation.rs index 8ca44df39e..83a484baa4 100644 --- a/nexus/reconfigurator/execution/src/resource_allocation.rs +++ b/nexus/reconfigurator/execution/src/resource_allocation.rs @@ -15,12 +15,12 @@ use nexus_db_queries::db::fixed_data::vpc_subnet::DNS_VPC_SUBNET; use nexus_db_queries::db::fixed_data::vpc_subnet::NEXUS_VPC_SUBNET; use nexus_db_queries::db::fixed_data::vpc_subnet::NTP_VPC_SUBNET; use nexus_db_queries::db::DataStore; -use nexus_types::deployment::NetworkInterface; -use nexus_types::deployment::NetworkInterfaceKind; use nexus_types::deployment::OmicronZoneType; use nexus_types::deployment::OmicronZonesConfig; use nexus_types::deployment::SourceNatConfig; use omicron_common::api::external::IdentityMetadataCreateParams; +use omicron_common::api::internal::shared::NetworkInterface; +use omicron_common::api::internal::shared::NetworkInterfaceKind; use slog::info; use slog::warn; use std::collections::BTreeMap; @@ -345,6 +345,7 @@ impl<'a> ResourceAllocator<'a> { bail!("invalid NIC kind (expected service, got instance)") } NetworkInterfaceKind::Service { .. } => (), + NetworkInterfaceKind::Probe { .. } => (), } // Only attempt to allocate `nic` if it isn't already assigned to this @@ -546,7 +547,7 @@ mod tests { external_ips.next().expect("exhausted external_ips"); let nexus_nic = NetworkInterface { id: Uuid::new_v4(), - kind: NetworkInterfaceKind::Service(nexus_id), + kind: NetworkInterfaceKind::Service { id: nexus_id }, name: "test-nexus".parse().expect("bad name"), ip: NEXUS_OPTE_IPV4_SUBNET .iter() @@ -554,7 +555,7 @@ mod tests { .unwrap() .into(), mac: MacAddr::random_system(), - subnet: IpNet::from(*NEXUS_OPTE_IPV4_SUBNET).into(), + subnet: IpNet::from(*NEXUS_OPTE_IPV4_SUBNET), vni: Vni::SERVICES_VNI, primary: true, slot: 0, @@ -566,7 +567,7 @@ mod tests { external_ips.next().expect("exhausted external_ips"); let dns_nic = NetworkInterface { id: Uuid::new_v4(), - kind: NetworkInterfaceKind::Service(dns_id), + kind: NetworkInterfaceKind::Service { id: dns_id }, name: "test-external-dns".parse().expect("bad name"), ip: DNS_OPTE_IPV4_SUBNET .iter() @@ -574,7 +575,7 @@ mod tests { .unwrap() .into(), mac: MacAddr::random_system(), - subnet: IpNet::from(*DNS_OPTE_IPV4_SUBNET).into(), + subnet: IpNet::from(*DNS_OPTE_IPV4_SUBNET), vni: Vni::SERVICES_VNI, primary: true, slot: 0, @@ -589,7 +590,7 @@ mod tests { }; let ntp_nic = NetworkInterface { id: Uuid::new_v4(), - kind: NetworkInterfaceKind::Service(ntp_id), + kind: NetworkInterfaceKind::Service { id: ntp_id }, name: "test-external-ntp".parse().expect("bad name"), ip: NTP_OPTE_IPV4_SUBNET .iter() @@ -597,7 +598,7 @@ mod tests { .unwrap() .into(), mac: MacAddr::random_system(), - subnet: IpNet::from(*NTP_OPTE_IPV4_SUBNET).into(), + subnet: IpNet::from(*NTP_OPTE_IPV4_SUBNET), vni: Vni::SERVICES_VNI, primary: true, slot: 0, @@ -888,9 +889,14 @@ mod tests { "invalid NIC kind (expected service, got instance)" ) } - NetworkInterfaceKind::Service(id) => { + NetworkInterfaceKind::Probe { .. } => { + panic!( + "invalid NIC kind (expected service, got instance)" + ) + } + NetworkInterfaceKind::Service { id } => { let id = *id; - nic.kind = NetworkInterfaceKind::Instance(id); + nic.kind = NetworkInterfaceKind::Instance { id }; } } "invalid NIC kind".to_string() diff --git a/nexus/reconfigurator/planning/src/blueprint_builder.rs b/nexus/reconfigurator/planning/src/blueprint_builder.rs index d541e112d5..26d0d1e23f 100644 --- a/nexus/reconfigurator/planning/src/blueprint_builder.rs +++ b/nexus/reconfigurator/planning/src/blueprint_builder.rs @@ -13,8 +13,6 @@ use ipnet::IpAdd; use nexus_config::NUM_INITIAL_RESERVED_IP_ADDRESSES; use nexus_inventory::now_db_precision; use nexus_types::deployment::Blueprint; -use nexus_types::deployment::NetworkInterface; -use nexus_types::deployment::NetworkInterfaceKind; use nexus_types::deployment::OmicronZoneConfig; use nexus_types::deployment::OmicronZoneDataset; use nexus_types::deployment::OmicronZoneType; @@ -35,6 +33,8 @@ use omicron_common::api::external::Generation; use omicron_common::api::external::IpNet; use omicron_common::api::external::MacAddr; use omicron_common::api::external::Vni; +use omicron_common::api::internal::shared::NetworkInterface; +use omicron_common::api::internal::shared::NetworkInterfaceKind; use slog::o; use slog::Logger; use std::collections::BTreeMap; @@ -532,14 +532,14 @@ impl<'a> BlueprintBuilder<'a> { .next() .ok_or(Error::ExhaustedNexusIps)? .into(), - IpNet::from(*NEXUS_OPTE_IPV4_SUBNET).into(), + IpNet::from(*NEXUS_OPTE_IPV4_SUBNET), ), IpAddr::V6(_) => ( self.nexus_v6_ips .next() .ok_or(Error::ExhaustedNexusIps)? .into(), - IpNet::from(*NEXUS_OPTE_IPV6_SUBNET).into(), + IpNet::from(*NEXUS_OPTE_IPV6_SUBNET), ), }; let mac = self @@ -548,7 +548,7 @@ impl<'a> BlueprintBuilder<'a> { .ok_or(Error::NoSystemMacAddressAvailable)?; NetworkInterface { id: Uuid::new_v4(), - kind: NetworkInterfaceKind::Service(nexus_id), + kind: NetworkInterfaceKind::Service { id: nexus_id }, name: format!("nexus-{nexus_id}").parse().unwrap(), ip, mac, diff --git a/nexus/src/app/instance_network.rs b/nexus/src/app/instance_network.rs index c0bc5d237b..eb5f83470f 100644 --- a/nexus/src/app/instance_network.rs +++ b/nexus/src/app/instance_network.rs @@ -22,11 +22,13 @@ use omicron_common::api::external::Error; use omicron_common::api::external::Ipv4Net; use omicron_common::api::external::Ipv6Net; use omicron_common::api::internal::nexus; +use omicron_common::api::internal::shared::NetworkInterface; use omicron_common::api::internal::shared::SwitchLocation; use sled_agent_client::types::DeleteVirtualNetworkInterfaceHost; use sled_agent_client::types::SetVirtualNetworkInterfaceHost; use std::collections::HashSet; use std::str::FromStr; +use std::sync::Arc; use uuid::Uuid; impl super::Nexus { @@ -452,11 +454,105 @@ impl super::Nexus { Ok(nat_entries) } + // The logic of this function should follow very closely what + // `instance_ensure_dpd_config` does. However, there are enough differences + // in the mechanics of how the logic is being carried out to justify having + // this separate function, it seems. + pub(crate) async fn probe_ensure_dpd_config( + &self, + opctx: &OpContext, + probe_id: Uuid, + sled_ip_address: std::net::Ipv6Addr, + ip_index_filter: Option, + dpd_client: &Arc, + ) -> Result<(), Error> { + let log = &self.log; + + // All external IPs map to the primary network interface, so find that + // interface. If there is no such interface, there's no way to route + // traffic destined to those IPs, so there's nothing to configure and + // it's safe to return early. + let network_interface = match self + .db_datastore + .derive_probe_network_interface_info(&opctx, probe_id) + .await? + .into_iter() + .find(|interface| interface.primary) + { + Some(interface) => interface, + None => { + info!(log, "probe has no primary network interface"; + "probe_id" => %probe_id); + return Ok(()); + } + }; + + let mac_address = + macaddr::MacAddr6::from_str(&network_interface.mac.to_string()) + .map_err(|e| { + Error::internal_error(&format!( + "failed to convert mac address: {e}" + )) + })?; + + info!(log, "looking up probe's external IPs"; + "probe_id" => %probe_id); + + let ips = self + .db_datastore + .probe_lookup_external_ips(&opctx, probe_id) + .await?; + + if let Some(wanted_index) = ip_index_filter { + if let None = ips.get(wanted_index) { + return Err(Error::internal_error(&format!( + "failed to find external ip address at index: {}", + wanted_index + ))); + } + } + + let sled_address = + Ipv6Net(Ipv6Network::new(sled_ip_address, 128).unwrap()); + + for target_ip in ips + .iter() + .enumerate() + .filter(|(index, _)| { + if let Some(wanted_index) = ip_index_filter { + *index == wanted_index + } else { + true + } + }) + .map(|(_, ip)| ip) + { + // For each external ip, add a nat entry to the database + self.ensure_nat_entry( + target_ip, + sled_address, + &network_interface, + mac_address, + opctx, + ) + .await?; + } + + // Notify dendrite that there are changes for it to reconcile. + // In the event of a failure to notify dendrite, we'll log an error + // and rely on dendrite's RPW timer to catch it up. + if let Err(e) = dpd_client.ipv4_nat_trigger_update().await { + error!(self.log, "failed to notify dendrite of nat updates"; "error" => ?e); + }; + + Ok(()) + } + async fn ensure_nat_entry( &self, target_ip: &nexus_db_model::ExternalIp, sled_address: Ipv6Net, - network_interface: &sled_agent_client::types::NetworkInterface, + network_interface: &NetworkInterface, mac_address: macaddr::MacAddr6, opctx: &OpContext, ) -> Result { @@ -675,6 +771,87 @@ impl super::Nexus { Ok(()) } + // The logic of this function should follow very closely what + // `instance_delete_dpd_config` does. However, there are enough differences + // in the mechanics of how the logic is being carried out to justify having + // this separate function, it seems. + pub(crate) async fn probe_delete_dpd_config( + &self, + opctx: &OpContext, + probe_id: Uuid, + ) -> Result<(), Error> { + let log = &self.log; + + info!(log, "deleting probe dpd configuration"; + "probe_id" => %probe_id); + + let external_ips = self + .db_datastore + .probe_lookup_external_ips(opctx, probe_id) + .await?; + + let mut errors = vec![]; + for entry in external_ips { + // Soft delete the NAT entry + match self + .db_datastore + .ipv4_nat_delete_by_external_ip(&opctx, &entry) + .await + { + Ok(_) => Ok(()), + Err(err) => match err { + Error::ObjectNotFound { .. } => { + warn!(log, "no matching nat entries to soft delete"); + Ok(()) + } + _ => { + let message = format!( + "failed to delete nat entry due to error: {err:?}" + ); + error!(log, "{}", message); + Err(Error::internal_error(&message)) + } + }, + }?; + } + + let boundary_switches = + self.boundary_switches(&self.opctx_alloc).await?; + + for switch in &boundary_switches { + debug!(&self.log, "notifying dendrite of updates"; + "probe_id" => %probe_id, + "switch" => switch.to_string()); + + let client_result = self.dpd_clients.get(switch).ok_or_else(|| { + Error::internal_error(&format!( + "unable to find dendrite client for {switch}" + )) + }); + + let dpd_client = match client_result { + Ok(client) => client, + Err(new_error) => { + errors.push(new_error); + continue; + } + }; + + // Notify dendrite that there are changes for it to reconcile. + // In the event of a failure to notify dendrite, we'll log an error + // and rely on dendrite's RPW timer to catch it up. + if let Err(e) = dpd_client.ipv4_nat_trigger_update().await { + error!(self.log, "failed to notify dendrite of nat updates"; "error" => ?e); + }; + } + + if let Some(e) = errors.into_iter().nth(0) { + return Err(e); + } + + Ok(()) + } + /// Deletes an instance's OPTE V2P mappings and the boundary switch NAT /// entries for its external IPs. /// diff --git a/nexus/src/app/mod.rs b/nexus/src/app/mod.rs index 781e39ac83..68f57d31f3 100644 --- a/nexus/src/app/mod.rs +++ b/nexus/src/app/mod.rs @@ -55,6 +55,7 @@ mod ip_pool; mod metrics; mod network_interface; mod oximeter; +mod probe; mod project; mod quota; mod rack; diff --git a/nexus/src/app/probe.rs b/nexus/src/app/probe.rs new file mode 100644 index 0000000000..0fce9d3431 --- /dev/null +++ b/nexus/src/app/probe.rs @@ -0,0 +1,109 @@ +use nexus_db_model::Probe; +use nexus_db_queries::authz; +use nexus_db_queries::context::OpContext; +use nexus_db_queries::db::datastore::ProbeInfo; +use nexus_db_queries::db::lookup; +use nexus_types::external_api::params; +use nexus_types::identity::Resource; +use omicron_common::api::external::Error; +use omicron_common::api::external::{ + http_pagination::PaginatedBy, CreateResult, DataPageParams, DeleteResult, + ListResultVec, LookupResult, NameOrId, +}; +use uuid::Uuid; + +impl super::Nexus { + /// List the probes in the given project. + pub(crate) async fn probe_list( + &self, + opctx: &OpContext, + project_lookup: &lookup::Project<'_>, + pagparams: &PaginatedBy<'_>, + ) -> ListResultVec { + let (.., authz_project) = + project_lookup.lookup_for(authz::Action::ListChildren).await?; + self.db_datastore.probe_list(opctx, &authz_project, pagparams).await + } + + /// List the probes for the given sled. This is used by sled agents to + /// determine what probes they should be running. + pub(crate) async fn probe_list_for_sled( + &self, + opctx: &OpContext, + pagparams: &DataPageParams<'_, Uuid>, + sled: Uuid, + ) -> ListResultVec { + self.db_datastore.probe_list_for_sled(sled, opctx, pagparams).await + } + + /// Get info about a particular probe. + pub(crate) async fn probe_get( + &self, + opctx: &OpContext, + project_lookup: &lookup::Project<'_>, + name_or_id: &NameOrId, + ) -> LookupResult { + let (.., authz_project) = + project_lookup.lookup_for(authz::Action::CreateChild).await?; + self.db_datastore.probe_get(opctx, &authz_project, &name_or_id).await + } + + /// Create a probe. This adds the probe to the data store and sets up the + /// NAT state on the switch. Actual launching of the probe is done by the + /// target sled agent asynchronously. + pub(crate) async fn probe_create( + &self, + opctx: &OpContext, + project_lookup: &lookup::Project<'_>, + new_probe_params: ¶ms::ProbeCreate, + ) -> CreateResult { + let (.., authz_project) = + project_lookup.lookup_for(authz::Action::CreateChild).await?; + + let probe = self + .db_datastore + .probe_create(opctx, &authz_project, new_probe_params) + .await?; + + let (.., sled) = + self.sled_lookup(opctx, &new_probe_params.sled)?.fetch().await?; + + let boundary_switches = + self.boundary_switches(&self.opctx_alloc).await?; + + for switch in &boundary_switches { + let dpd_client = self.dpd_clients.get(switch).ok_or_else(|| { + Error::internal_error(&format!( + "could not find dpd client for {switch}" + )) + })?; + self.probe_ensure_dpd_config( + opctx, + probe.id(), + sled.ip.into(), + None, + dpd_client, + ) + .await?; + } + + Ok(probe) + } + + /// Delete a probe. This deletes the probe from the data store and tears + /// down the associated NAT state. + pub(crate) async fn probe_delete( + &self, + opctx: &OpContext, + project_lookup: &lookup::Project<'_>, + name_or_id: NameOrId, + ) -> DeleteResult { + let probe = self.probe_get(opctx, project_lookup, &name_or_id).await?; + + self.probe_delete_dpd_config(opctx, probe.id).await?; + + let (.., authz_project) = + project_lookup.lookup_for(authz::Action::CreateChild).await?; + self.db_datastore.probe_delete(opctx, &authz_project, &name_or_id).await + } +} diff --git a/nexus/src/app/rack.rs b/nexus/src/app/rack.rs index a137f19434..4030fce31d 100644 --- a/nexus/src/app/rack.rs +++ b/nexus/src/app/rack.rs @@ -760,8 +760,9 @@ impl super::Nexus { ntp_servers: Vec::new(), //TODO rack_network_config: Some(RackNetworkConfigV1 { rack_subnet: subnet, - //TODO(ry) you are here. We need to remove these too. They are - // inconsistent with a generic set of addresses on ports. + //TODO: We need to remove these. They are inconsistent with + // a generic set of addresses on ports that may not be + // contiguous. infra_ip_first: Ipv4Addr::UNSPECIFIED, infra_ip_last: Ipv4Addr::UNSPECIFIED, ports, diff --git a/nexus/src/app/sagas/project_create.rs b/nexus/src/app/sagas/project_create.rs index 40acc822c0..b31dd821f0 100644 --- a/nexus/src/app/sagas/project_create.rs +++ b/nexus/src/app/sagas/project_create.rs @@ -245,7 +245,6 @@ mod test { .filter(dsl::collection_type.eq(nexus_db_queries::db::model::CollectionTypeProvisioned::Project.to_string())) // ignore built-in services project .filter(dsl::id.ne(*SERVICES_PROJECT_ID)) - .select(VirtualProvisioningCollection::as_select()) .get_results_async::(&conn) .await diff --git a/nexus/src/app/switch_port.rs b/nexus/src/app/switch_port.rs index b9f0f94fa0..fc9ad2866a 100644 --- a/nexus/src/app/switch_port.rs +++ b/nexus/src/app/switch_port.rs @@ -40,9 +40,9 @@ impl super::Nexus { ) -> CreateResult { opctx.authorize(authz::Action::Modify, &authz::FLEET).await?; - //TODO(ry) race conditions on exists check versus update/create. - // Normally I would use a DB lock here, but not sure what - // the Omicron way of doing things here is. + //TODO race conditions on exists check versus update/create. + // Normally I would use a DB lock here, but not sure what + // the Omicron way of doing things here is. match self .db_datastore diff --git a/nexus/src/app/vpc.rs b/nexus/src/app/vpc.rs index 3a6278053a..44b676b853 100644 --- a/nexus/src/app/vpc.rs +++ b/nexus/src/app/vpc.rs @@ -30,10 +30,10 @@ use omicron_common::api::external::NameOrId; use omicron_common::api::external::UpdateResult; use omicron_common::api::external::VpcFirewallRuleUpdateParams; use omicron_common::api::internal::nexus::HostIdentifier; -use sled_agent_client::types::NetworkInterface; use futures::future::join_all; use ipnetwork::IpNetwork; +use omicron_common::api::internal::shared::NetworkInterface; use std::collections::{HashMap, HashSet}; use std::net::IpAddr; use std::sync::Arc; diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index 8fe02a0fd8..aa037e072f 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -15,7 +15,6 @@ use super::{ }; use crate::external_api::shared; use crate::ServerContext; -use dropshot::EmptyScanParams; use dropshot::HttpError; use dropshot::HttpResponseAccepted; use dropshot::HttpResponseCreated; @@ -34,18 +33,20 @@ use dropshot::{ channel, endpoint, WebsocketChannelResult, WebsocketConnection, }; use dropshot::{ApiDescription, StreamingBody}; +use dropshot::{ApiEndpoint, EmptyScanParams}; use ipnetwork::IpNetwork; -use nexus_db_queries::authz; use nexus_db_queries::db; use nexus_db_queries::db::identity::Resource; use nexus_db_queries::db::lookup::ImageLookup; use nexus_db_queries::db::lookup::ImageParentLookup; use nexus_db_queries::db::model::Name; +use nexus_db_queries::{authz, db::datastore::ProbeInfo}; use nexus_types::external_api::shared::BfdStatus; use omicron_common::api::external::http_pagination::data_page_params_for; use omicron_common::api::external::http_pagination::marker_for_name; use omicron_common::api::external::http_pagination::marker_for_name_or_id; use omicron_common::api::external::http_pagination::name_or_id_pagination; +use omicron_common::api::external::http_pagination::PaginatedBy; use omicron_common::api::external::http_pagination::PaginatedById; use omicron_common::api::external::http_pagination::PaginatedByName; use omicron_common::api::external::http_pagination::PaginatedByNameOrId; @@ -69,6 +70,7 @@ use omicron_common::api::external::InstanceNetworkInterface; use omicron_common::api::external::InternalContext; use omicron_common::api::external::LoopbackAddress; use omicron_common::api::external::NameOrId; +use omicron_common::api::external::Probe; use omicron_common::api::external::RouterRoute; use omicron_common::api::external::RouterRouteKind; use omicron_common::api::external::SwitchPort; @@ -353,12 +355,40 @@ pub(crate) fn external_api() -> NexusApiDescription { Ok(()) } + fn register_experimental( + api: &mut NexusApiDescription, + endpoint: T, + ) -> Result<(), String> + where + T: Into>>, + { + let mut ep: ApiEndpoint> = endpoint.into(); + // only one tag is allowed + ep.tags = vec![String::from("hidden")]; + ep.path = String::from("/experimental") + &ep.path; + api.register(ep) + } + + fn register_experimental_endpoints( + api: &mut NexusApiDescription, + ) -> Result<(), String> { + register_experimental(api, probe_list)?; + register_experimental(api, probe_view)?; + register_experimental(api, probe_create)?; + register_experimental(api, probe_delete)?; + + Ok(()) + } + let conf = serde_json::from_str(include_str!("./tag-config.json")).unwrap(); let mut api = NexusApiDescription::new().tag_config(conf); if let Err(err) = register_endpoints(&mut api) { panic!("failed to register entrypoints: {}", err); } + if let Err(err) = register_experimental_endpoints(&mut api) { + panic!("failed to register experimental entrypoints: {}", err); + } api } @@ -5999,6 +6029,125 @@ async fn current_user_ssh_key_delete( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } +/// List instrumentation probes +#[endpoint { + method = GET, + path = "/v1/probes", + tags = ["system/probes"], +}] +async fn probe_list( + rqctx: RequestContext>, + query_params: Query>, +) -> Result>, HttpError> { + let apictx = rqctx.context(); + let handler = async { + let opctx = crate::context::op_context_for_external_api(&rqctx).await?; + opctx.authorize(authz::Action::ListChildren, &authz::FLEET).await?; + + let nexus = &apictx.nexus; + let query = query_params.into_inner(); + let pag_params = data_page_params_for(&rqctx, &query)?; + let scan_params = ScanByNameOrId::from_query(&query)?; + let paginated_by = name_or_id_pagination(&pag_params, scan_params)?; + let project_lookup = + nexus.project_lookup(&opctx, scan_params.selector.clone())?; + + let probes = + nexus.probe_list(&opctx, &project_lookup, &paginated_by).await?; + + Ok(HttpResponseOk(ScanByNameOrId::results_page( + &query, + probes, + &|_, p: &ProbeInfo| match paginated_by { + PaginatedBy::Id(_) => NameOrId::Id(p.id), + PaginatedBy::Name(_) => NameOrId::Name(p.name.clone().into()), + }, + )?)) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + +/// View instrumentation probe +#[endpoint { + method = GET, + path = "/v1/probes/{probe}", + tags = ["system/probes"], +}] +async fn probe_view( + rqctx: RequestContext>, + path_params: Path, + query_params: Query, +) -> Result, HttpError> { + let apictx = rqctx.context(); + let handler = async { + let opctx = crate::context::op_context_for_external_api(&rqctx).await?; + opctx.authorize(authz::Action::ListChildren, &authz::FLEET).await?; + + let nexus = &apictx.nexus; + let path = path_params.into_inner(); + let project_selector = query_params.into_inner(); + let project_lookup = nexus.project_lookup(&opctx, project_selector)?; + let probe = + nexus.probe_get(&opctx, &project_lookup, &path.probe).await?; + Ok(HttpResponseOk(probe)) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + +/// Create instrumentation probe +#[endpoint { + method = POST, + path = "/v1/probes", + tags = ["system/probes"], +}] +async fn probe_create( + rqctx: RequestContext>, + query_params: Query, + new_probe: TypedBody, +) -> Result, HttpError> { + let apictx = rqctx.context(); + let handler = async { + let opctx = crate::context::op_context_for_external_api(&rqctx).await?; + opctx.authorize(authz::Action::Modify, &authz::FLEET).await?; + + let nexus = &apictx.nexus; + let new_probe_params = &new_probe.into_inner(); + let project_selector = query_params.into_inner(); + let project_lookup = nexus.project_lookup(&opctx, project_selector)?; + let probe = nexus + .probe_create(&opctx, &project_lookup, &new_probe_params) + .await?; + Ok(HttpResponseCreated(probe.into())) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + +/// Delete instrumentation probe +#[endpoint { + method = DELETE, + path = "/v1/probes/{probe}", + tags = ["system/probes"], +}] +async fn probe_delete( + rqctx: RequestContext>, + query_params: Query, + path_params: Path, +) -> Result { + let apictx = rqctx.context(); + let handler = async { + let opctx = crate::context::op_context_for_external_api(&rqctx).await?; + opctx.authorize(authz::Action::Modify, &authz::FLEET).await?; + + let nexus = &apictx.nexus; + let path = path_params.into_inner(); + let project_selector = query_params.into_inner(); + let project_lookup = nexus.project_lookup(&opctx, project_selector)?; + nexus.probe_delete(&opctx, &project_lookup, path.probe).await?; + Ok(HttpResponseDeleted()) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + #[cfg(test)] mod test { use super::external_api; diff --git a/nexus/src/external_api/tag-config.json b/nexus/src/external_api/tag-config.json index 3bc8006cee..6974906507 100644 --- a/nexus/src/external_api/tag-config.json +++ b/nexus/src/external_api/tag-config.json @@ -86,6 +86,12 @@ "url": "http://docs.oxide.computer/api/vpcs" } }, + "system/probes": { + "description": "Probes for testing network connectivity", + "external_docs": { + "url": "http://docs.oxide.computer/api/probes" + } + }, "system/status": { "description": "Endpoints related to system health", "external_docs": { diff --git a/nexus/src/internal_api/http_entrypoints.rs b/nexus/src/internal_api/http_entrypoints.rs index e298935fee..7f8211dc8e 100644 --- a/nexus/src/internal_api/http_entrypoints.rs +++ b/nexus/src/internal_api/http_entrypoints.rs @@ -25,6 +25,7 @@ use dropshot::ResultsPage; use dropshot::TypedBody; use hyper::Body; use nexus_db_model::Ipv4NatEntryView; +use nexus_db_queries::db::datastore::ProbeInfo; use nexus_types::deployment::Blueprint; use nexus_types::deployment::BlueprintMetadata; use nexus_types::deployment::BlueprintTarget; @@ -94,6 +95,8 @@ pub(crate) fn internal_api() -> NexusApiDescription { api.register(sled_list_uninitialized)?; api.register(sled_add)?; + api.register(probes_get)?; + Ok(()) } @@ -635,7 +638,7 @@ struct RpwNatQueryParam { /// change or until the `limit` is reached. If there are no changes, an /// empty vec is returned. #[endpoint { - method = GET, + method = GET, path = "/nat/ipv4/changeset/{from_gen}" }] async fn ipv4_nat_changeset( @@ -864,3 +867,33 @@ async fn sled_add( }; apictx.internal_latencies.instrument_dropshot_handler(&rqctx, handler).await } + +/// Path parameters for probes +#[derive(Deserialize, JsonSchema)] +struct ProbePathParam { + sled: Uuid, +} + +/// Get all the probes associated with a given sled. +#[endpoint { + method = GET, + path = "/probes/{sled}" +}] +async fn probes_get( + rqctx: RequestContext>, + path_params: Path, + query_params: Query, +) -> Result>, HttpError> { + let apictx = rqctx.context(); + let handler = async { + let query = query_params.into_inner(); + let path = path_params.into_inner(); + let nexus = &apictx.nexus; + let opctx = crate::context::op_context_for_internal_api(&rqctx).await; + let pagparams = data_page_params_for(&rqctx, &query)?; + Ok(HttpResponseOk( + nexus.probe_list_for_sled(&opctx, &pagparams, path.sled).await?, + )) + }; + apictx.internal_latencies.instrument_dropshot_handler(&rqctx, handler).await +} diff --git a/nexus/tests/integration_tests/mod.rs b/nexus/tests/integration_tests/mod.rs index 4b68a6c4f2..84b867252f 100644 --- a/nexus/tests/integration_tests/mod.rs +++ b/nexus/tests/integration_tests/mod.rs @@ -23,6 +23,7 @@ mod metrics; mod oximeter; mod pantry; mod password_login; +mod probe; mod projects; mod quotas; mod rack; diff --git a/nexus/tests/integration_tests/oximeter.rs b/nexus/tests/integration_tests/oximeter.rs index 7dc453d713..9663e10fa0 100644 --- a/nexus/tests/integration_tests/oximeter.rs +++ b/nexus/tests/integration_tests/oximeter.rs @@ -152,7 +152,7 @@ async fn test_oximeter_reregistration() { // Timeouts for checks const POLL_INTERVAL: Duration = Duration::from_millis(100); - const POLL_DURATION: Duration = Duration::from_secs(30); + const POLL_DURATION: Duration = Duration::from_secs(60); // We must have at exactly one timeseries, with at least one sample. let timeseries = diff --git a/nexus/tests/integration_tests/probe.rs b/nexus/tests/integration_tests/probe.rs new file mode 100644 index 0000000000..71a695bf8c --- /dev/null +++ b/nexus/tests/integration_tests/probe.rs @@ -0,0 +1,127 @@ +use dropshot::HttpErrorResponseBody; +use http::{Method, StatusCode}; +use nexus_db_queries::db::datastore::ProbeInfo; +use nexus_test_utils::{ + http_testing::{AuthnMode, NexusRequest}, + resource_helpers::{create_default_ip_pool, create_project}, + SLED_AGENT_UUID, +}; +use nexus_test_utils_macros::nexus_test; +use nexus_types::external_api::params::ProbeCreate; +use omicron_common::api::external::{IdentityMetadataCreateParams, Probe}; + +type ControlPlaneTestContext = + nexus_test_utils::ControlPlaneTestContext; + +#[nexus_test] +async fn test_probe_basic_crud(ctx: &ControlPlaneTestContext) { + let client = &ctx.external_client; + + create_default_ip_pool(&client).await; + create_project(&client, "nebula").await; + + let probes = NexusRequest::iter_collection_authn::( + client, + "/experimental/v1/probes?project=nebula", + "", + None, + ) + .await + .expect("Failed to list probes") + .all_items; + + assert_eq!(probes.len(), 0, "Expected zero probes"); + + let params = ProbeCreate { + identity: IdentityMetadataCreateParams { + name: "class1".parse().unwrap(), + description: "subspace relay probe".to_owned(), + }, + ip_pool: None, + sled: SLED_AGENT_UUID.parse().unwrap(), + }; + + let created: Probe = NexusRequest::objects_post( + client, + "/experimental/v1/probes?project=nebula", + ¶ms, + ) + .authn_as(AuthnMode::PrivilegedUser) + .execute() + .await + .unwrap() + .parsed_body() + .unwrap(); + + let probes = NexusRequest::iter_collection_authn::( + client, + "/experimental/v1/probes?project=nebula", + "", + None, + ) + .await + .expect("Failed to list probes") + .all_items; + + assert_eq!(probes.len(), 1, "Expected one probe"); + assert_eq!(probes[0].id, created.identity.id); + + let error: HttpErrorResponseBody = NexusRequest::expect_failure( + client, + StatusCode::NOT_FOUND, + Method::GET, + "/experimental/v1/probes/class2?project=nebula", + ) + .authn_as(AuthnMode::PrivilegedUser) + .execute() + .await + .unwrap() + .parsed_body() + .unwrap(); + assert_eq!(error.message, "not found: probe with name \"class2\""); + + NexusRequest::object_get( + client, + "/experimental/v1/probes/class1?project=nebula", + ) + .authn_as(AuthnMode::PrivilegedUser) + .execute() + .await + .expect("failed to view probe") + .parsed_body::() + .expect("failed to parse probe info"); + + let fetched: ProbeInfo = NexusRequest::object_get( + client, + "/experimental/v1/probes/class1?project=nebula", + ) + .authn_as(AuthnMode::PrivilegedUser) + .execute() + .await + .unwrap() + .parsed_body() + .unwrap(); + + assert_eq!(fetched.id, created.identity.id); + + NexusRequest::object_delete( + client, + "/experimental/v1/probes/class1?project=nebula", + ) + .authn_as(AuthnMode::PrivilegedUser) + .execute() + .await + .unwrap(); + + let probes = NexusRequest::iter_collection_authn::( + client, + "/experimental/v1/probes?project=nebula", + "", + None, + ) + .await + .expect("Failed to list probes after delete") + .all_items; + + assert_eq!(probes.len(), 0, "Expected zero probes"); +} diff --git a/nexus/tests/output/nexus_tags.txt b/nexus/tests/output/nexus_tags.txt index ecffadcb4d..3b954ec6ec 100644 --- a/nexus/tests/output/nexus_tags.txt +++ b/nexus/tests/output/nexus_tags.txt @@ -26,6 +26,10 @@ device_access_token POST /device/token device_auth_confirm POST /device/confirm device_auth_request POST /device/auth logout POST /v1/logout +probe_create POST /experimental/v1/probes +probe_delete DELETE /experimental/v1/probes/{probe} +probe_list GET /experimental/v1/probes +probe_view GET /experimental/v1/probes/{probe} API operations found with tag "images" OPERATION ID METHOD URL PATH diff --git a/nexus/tests/output/uncovered-authz-endpoints.txt b/nexus/tests/output/uncovered-authz-endpoints.txt index d76d9c5495..d19c7970d0 100644 --- a/nexus/tests/output/uncovered-authz-endpoints.txt +++ b/nexus/tests/output/uncovered-authz-endpoints.txt @@ -1,8 +1,12 @@ API endpoints with no coverage in authz tests: +probe_delete (delete "/experimental/v1/probes/{probe}") +probe_list (get "/experimental/v1/probes") +probe_view (get "/experimental/v1/probes/{probe}") ping (get "/v1/ping") device_auth_request (post "/device/auth") device_auth_confirm (post "/device/confirm") device_access_token (post "/device/token") +probe_create (post "/experimental/v1/probes") login_saml (post "/login/{silo_name}/saml/{provider_name}") login_local (post "/v1/login/{silo_name}/local") logout (post "/v1/logout") diff --git a/nexus/types/src/deployment.rs b/nexus/types/src/deployment.rs index ac950d2ca3..7dcb843b7f 100644 --- a/nexus/types/src/deployment.rs +++ b/nexus/types/src/deployment.rs @@ -15,8 +15,6 @@ use crate::external_api::views::SledPolicy; use crate::external_api::views::SledState; use crate::inventory::Collection; -pub use crate::inventory::NetworkInterface; -pub use crate::inventory::NetworkInterfaceKind; pub use crate::inventory::OmicronZoneConfig; pub use crate::inventory::OmicronZoneDataset; pub use crate::inventory::OmicronZoneType; diff --git a/nexus/types/src/external_api/params.rs b/nexus/types/src/external_api/params.rs index 31cb1d3e5c..c6ae47d27c 100644 --- a/nexus/types/src/external_api/params.rs +++ b/nexus/types/src/external_api/params.rs @@ -80,6 +80,7 @@ path_param!(ProviderPath, provider, "SAML identity provider"); path_param!(IpPoolPath, pool, "IP pool"); path_param!(SshKeyPath, ssh_key, "SSH key"); path_param!(AddressLotPath, address_lot, "address lot"); +path_param!(ProbePath, probe, "probe"); id_path_param!(GroupPath, group_id, "group"); @@ -2042,3 +2043,21 @@ pub struct UpdatesGetRepositoryParams { /// The version to get. pub system_version: SemverVersion, } + +// Probes + +/// Create time parameters for probes. +#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] +pub struct ProbeCreate { + #[serde(flatten)] + pub identity: IdentityMetadataCreateParams, + pub sled: Uuid, + pub ip_pool: Option, +} + +/// List probes with an optional name or id. +#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, PartialEq)] +pub struct ProbeListSelector { + /// A name or id to use when selecting a probe. + pub name_or_id: Option, +} diff --git a/nexus/types/src/inventory.rs b/nexus/types/src/inventory.rs index 57fc7bd647..92985081dc 100644 --- a/nexus/types/src/inventory.rs +++ b/nexus/types/src/inventory.rs @@ -17,11 +17,11 @@ pub use gateway_client::types::PowerState; pub use gateway_client::types::RotSlot; pub use gateway_client::types::SpType; use omicron_common::api::external::ByteCount; +pub use omicron_common::api::internal::shared::NetworkInterface; +pub use omicron_common::api::internal::shared::NetworkInterfaceKind; pub use omicron_common::api::internal::shared::SourceNatConfig; use serde::{Deserialize, Serialize}; use serde_with::serde_as; -pub use sled_agent_client::types::NetworkInterface; -pub use sled_agent_client::types::NetworkInterfaceKind; pub use sled_agent_client::types::OmicronZoneConfig; pub use sled_agent_client::types::OmicronZoneDataset; pub use sled_agent_client::types::OmicronZoneType; diff --git a/openapi/nexus-internal.json b/openapi/nexus-internal.json index fb3ff976ae..9e18c7d6bc 100644 --- a/openapi/nexus-internal.json +++ b/openapi/nexus-internal.json @@ -667,6 +667,75 @@ } } }, + "/probes/{sled}": { + "get": { + "summary": "Get all the probes associated with a given sled.", + "operationId": "probes_get", + "parameters": [ + { + "in": "path", + "name": "sled", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/IdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_ProbeInfo", + "type": "array", + "items": { + "$ref": "#/components/schemas/ProbeInfo" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + } + }, "/racks/{rack_id}/initialization-complete": { "put": { "summary": "Report that the Rack Setup Service initialization is complete", @@ -4358,14 +4427,31 @@ } ] }, + "IpKind": { + "type": "string", + "enum": [ + "snat", + "floating", + "ephemeral" + ] + }, "IpNet": { - "description": "IpNet\n\n
JSON schema\n\n```json { \"oneOf\": [ { \"title\": \"v4\", \"allOf\": [ { \"$ref\": \"#/components/schemas/Ipv4Net\" } ] }, { \"title\": \"v6\", \"allOf\": [ { \"$ref\": \"#/components/schemas/Ipv6Net\" } ] } ] } ```
", - "anyOf": [ + "oneOf": [ { - "$ref": "#/components/schemas/Ipv4Net" + "title": "v4", + "allOf": [ + { + "$ref": "#/components/schemas/Ipv4Net" + } + ] }, { - "$ref": "#/components/schemas/Ipv6Net" + "title": "v6", + "allOf": [ + { + "$ref": "#/components/schemas/Ipv6Net" + } + ] } ] }, @@ -4457,8 +4543,11 @@ ] }, "Ipv4Net": { - "description": "An IPv4 subnet, including prefix and subnet mask\n\n
JSON schema\n\n```json { \"title\": \"An IPv4 subnet\", \"description\": \"An IPv4 subnet, including prefix and subnet mask\", \"examples\": [ \"192.168.1.0/24\" ], \"type\": \"string\", \"pattern\": \"^(([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\\\\.){3}([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])/([0-9]|1[0-9]|2[0-9]|3[0-2])$\" } ```
", - "type": "string" + "example": "192.168.1.0/24", + "title": "An IPv4 subnet", + "description": "An IPv4 subnet, including prefix and subnet mask", + "type": "string", + "pattern": "^(([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])/([0-9]|1[0-9]|2[0-9]|3[0-2])$" }, "Ipv4Network": { "type": "string", @@ -4483,8 +4572,11 @@ ] }, "Ipv6Net": { - "description": "An IPv6 subnet, including prefix and subnet mask\n\n
JSON schema\n\n```json { \"title\": \"An IPv6 subnet\", \"description\": \"An IPv6 subnet, including prefix and subnet mask\", \"examples\": [ \"fd12:3456::/64\" ], \"type\": \"string\", \"pattern\": \"^([fF][dD])[0-9a-fA-F]{2}:(([0-9a-fA-F]{1,4}:){6}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,6}:)([0-9a-fA-F]{1,4})?\\\\/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8])$\" } ```
", - "type": "string" + "example": "fd12:3456::/64", + "title": "An IPv6 subnet", + "description": "An IPv6 subnet, including prefix and subnet mask", + "type": "string", + "pattern": "^([fF][dD])[0-9a-fA-F]{2}:(([0-9a-fA-F]{1,4}:){6}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,6}:)([0-9a-fA-F]{1,4})?\\/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8])$" }, "Ipv6Network": { "type": "string", @@ -4825,7 +4917,7 @@ "maxLength": 63 }, "NetworkInterface": { - "description": "Information required to construct a virtual network interface\n\n
JSON schema\n\n```json { \"description\": \"Information required to construct a virtual network interface\", \"type\": \"object\", \"required\": [ \"id\", \"ip\", \"kind\", \"mac\", \"name\", \"primary\", \"slot\", \"subnet\", \"vni\" ], \"properties\": { \"id\": { \"type\": \"string\", \"format\": \"uuid\" }, \"ip\": { \"type\": \"string\", \"format\": \"ip\" }, \"kind\": { \"$ref\": \"#/components/schemas/NetworkInterfaceKind\" }, \"mac\": { \"$ref\": \"#/components/schemas/MacAddr\" }, \"name\": { \"$ref\": \"#/components/schemas/Name\" }, \"primary\": { \"type\": \"boolean\" }, \"slot\": { \"type\": \"integer\", \"format\": \"uint8\", \"minimum\": 0.0 }, \"subnet\": { \"$ref\": \"#/components/schemas/IpNet\" }, \"vni\": { \"$ref\": \"#/components/schemas/Vni\" } } } ```
", + "description": "Information required to construct a virtual network interface", "type": "object", "properties": { "id": { @@ -4873,7 +4965,7 @@ ] }, "NetworkInterfaceKind": { - "description": "The type of network interface\n\n
JSON schema\n\n```json { \"description\": \"The type of network interface\", \"oneOf\": [ { \"description\": \"A vNIC attached to a guest instance\", \"type\": \"object\", \"required\": [ \"id\", \"type\" ], \"properties\": { \"id\": { \"type\": \"string\", \"format\": \"uuid\" }, \"type\": { \"type\": \"string\", \"enum\": [ \"instance\" ] } } }, { \"description\": \"A vNIC associated with an internal service\", \"type\": \"object\", \"required\": [ \"id\", \"type\" ], \"properties\": { \"id\": { \"type\": \"string\", \"format\": \"uuid\" }, \"type\": { \"type\": \"string\", \"enum\": [ \"service\" ] } } } ] } ```
", + "description": "The type of network interface", "oneOf": [ { "description": "A vNIC attached to a guest instance", @@ -4914,6 +5006,26 @@ "id", "type" ] + }, + { + "description": "A vNIC associated with a probe", + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "type": { + "type": "string", + "enum": [ + "probe" + ] + } + }, + "required": [ + "id", + "type" + ] } ] }, @@ -5504,6 +5616,66 @@ "speed400_g" ] }, + "ProbeExternalIp": { + "type": "object", + "properties": { + "first_port": { + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "ip": { + "type": "string", + "format": "ip" + }, + "kind": { + "$ref": "#/components/schemas/IpKind" + }, + "last_port": { + "type": "integer", + "format": "uint16", + "minimum": 0 + } + }, + "required": [ + "first_port", + "ip", + "kind", + "last_port" + ] + }, + "ProbeInfo": { + "type": "object", + "properties": { + "external_ips": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ProbeExternalIp" + } + }, + "id": { + "type": "string", + "format": "uuid" + }, + "interface": { + "$ref": "#/components/schemas/NetworkInterface" + }, + "name": { + "$ref": "#/components/schemas/Name" + }, + "sled": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "external_ips", + "id", + "interface", + "name", + "sled" + ] + }, "ProducerEndpoint": { "description": "Information announced by a metric server, used so that clients can contact it and collect available metric data from it.", "type": "object", diff --git a/openapi/nexus.json b/openapi/nexus.json index 3d31331a90..cacd875acc 100644 --- a/openapi/nexus.json +++ b/openapi/nexus.json @@ -101,6 +101,206 @@ } } }, + "/experimental/v1/probes": { + "get": { + "tags": [ + "hidden" + ], + "summary": "List instrumentation probes", + "operationId": "probe_list", + "parameters": [ + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProbeInfoResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [ + "project" + ] + } + }, + "post": { + "tags": [ + "hidden" + ], + "summary": "Create instrumentation probe", + "operationId": "probe_create", + "parameters": [ + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProbeCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Probe" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/experimental/v1/probes/{probe}": { + "get": { + "tags": [ + "hidden" + ], + "summary": "View instrumentation probe", + "operationId": "probe_view", + "parameters": [ + { + "in": "path", + "name": "probe", + "description": "Name or ID of the probe", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProbeInfo" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "hidden" + ], + "summary": "Delete instrumentation probe", + "operationId": "probe_delete", + "parameters": [ + { + "in": "query", + "name": "project", + "description": "Name or ID of the project", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "path", + "name": "probe", + "description": "Name or ID of the probe", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, "/login/{silo_name}/saml/{provider_name}": { "post": { "tags": [ @@ -13210,6 +13410,14 @@ } ] }, + "IpKind": { + "type": "string", + "enum": [ + "snat", + "floating", + "ephemeral" + ] + }, "IpNet": { "oneOf": [ { @@ -13914,6 +14122,119 @@ } ] }, + "NetworkInterface": { + "description": "Information required to construct a virtual network interface", + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "ip": { + "type": "string", + "format": "ip" + }, + "kind": { + "$ref": "#/components/schemas/NetworkInterfaceKind" + }, + "mac": { + "$ref": "#/components/schemas/MacAddr" + }, + "name": { + "$ref": "#/components/schemas/Name" + }, + "primary": { + "type": "boolean" + }, + "slot": { + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "subnet": { + "$ref": "#/components/schemas/IpNet" + }, + "vni": { + "$ref": "#/components/schemas/Vni" + } + }, + "required": [ + "id", + "ip", + "kind", + "mac", + "name", + "primary", + "slot", + "subnet", + "vni" + ] + }, + "NetworkInterfaceKind": { + "description": "The type of network interface", + "oneOf": [ + { + "description": "A vNIC attached to a guest instance", + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "type": { + "type": "string", + "enum": [ + "instance" + ] + } + }, + "required": [ + "id", + "type" + ] + }, + { + "description": "A vNIC associated with an internal service", + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "type": { + "type": "string", + "enum": [ + "service" + ] + } + }, + "required": [ + "id", + "type" + ] + }, + { + "description": "A vNIC associated with a probe", + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "type": { + "type": "string", + "enum": [ + "probe" + ] + } + }, + "required": [ + "id", + "type" + ] + } + ] + }, "Password": { "title": "A password used to authenticate a user", "description": "Passwords may be subject to additional constraints.", @@ -14019,6 +14340,161 @@ "ok" ] }, + "Probe": { + "description": "Identity-related metadata that's included in nearly all public API objects", + "type": "object", + "properties": { + "description": { + "description": "human-readable free-form text about a resource", + "type": "string" + }, + "id": { + "description": "unique, immutable, system-controlled identifier for each resource", + "type": "string", + "format": "uuid" + }, + "name": { + "description": "unique, mutable, user-controlled identifier for each resource", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "sled": { + "type": "string", + "format": "uuid" + }, + "time_created": { + "description": "timestamp when this resource was created", + "type": "string", + "format": "date-time" + }, + "time_modified": { + "description": "timestamp when this resource was last modified", + "type": "string", + "format": "date-time" + } + }, + "required": [ + "description", + "id", + "name", + "sled", + "time_created", + "time_modified" + ] + }, + "ProbeCreate": { + "description": "Create time parameters for probes.", + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "ip_pool": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/NameOrId" + } + ] + }, + "name": { + "$ref": "#/components/schemas/Name" + }, + "sled": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "description", + "name", + "sled" + ] + }, + "ProbeExternalIp": { + "type": "object", + "properties": { + "first_port": { + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "ip": { + "type": "string", + "format": "ip" + }, + "kind": { + "$ref": "#/components/schemas/IpKind" + }, + "last_port": { + "type": "integer", + "format": "uint16", + "minimum": 0 + } + }, + "required": [ + "first_port", + "ip", + "kind", + "last_port" + ] + }, + "ProbeInfo": { + "type": "object", + "properties": { + "external_ips": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ProbeExternalIp" + } + }, + "id": { + "type": "string", + "format": "uuid" + }, + "interface": { + "$ref": "#/components/schemas/NetworkInterface" + }, + "name": { + "$ref": "#/components/schemas/Name" + }, + "sled": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "external_ips", + "id", + "interface", + "name", + "sled" + ] + }, + "ProbeInfoResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/ProbeInfo" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, "Project": { "description": "View of a Project", "type": "object", @@ -16503,6 +16979,12 @@ "storage" ] }, + "Vni": { + "description": "A Geneve Virtual Network Identifier", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, "Vpc": { "description": "View of a VPC", "type": "object", @@ -17452,6 +17934,13 @@ "url": "http://docs.oxide.computer/api/system-networking" } }, + { + "name": "system/probes", + "description": "Probes for testing network connectivity", + "externalDocs": { + "url": "http://docs.oxide.computer/api/probes" + } + }, { "name": "system/silos", "description": "Silos represent a logical partition of users and resources.", diff --git a/openapi/sled-agent.json b/openapi/sled-agent.json index 238b5832ca..43fac710fc 100644 --- a/openapi/sled-agent.json +++ b/openapi/sled-agent.json @@ -5670,6 +5670,26 @@ "id", "type" ] + }, + { + "description": "A vNIC associated with a probe", + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "type": { + "type": "string", + "enum": [ + "probe" + ] + } + }, + "required": [ + "id", + "type" + ] } ] }, diff --git a/package-manifest.toml b/package-manifest.toml index c6f39d2ecd..b7e96935f1 100644 --- a/package-manifest.toml +++ b/package-manifest.toml @@ -626,6 +626,15 @@ output.type = "zone" output.intermediate_only = true setup_hint = "Run `./tools/ci_download_transceiver_control` to download the necessary binaries" +[package.thundermuffin] +service_name = "thundermuffin" +source.type = "prebuilt" +source.repo = "thundermuffin" +source.commit = "a4a6108d7c9aac2464a0b6898e88132a8f701a13" +source.sha256 = "dc55a2accd33a347df4cbdc0026cbaccea2c004940c3fec8cadcdd633d440dfa" +output.type = "zone" +output.intermediate_only = true + # To package and install the asic variant of the switch, do: # # $ cargo run --release --bin omicron-package -- -t default target create -i standard -m gimlet -s asic @@ -740,3 +749,11 @@ source.type = "local" source.rust.binary_names = ["oxlog"] source.rust.release = true output.type = "tarball" + +[package.probe] +service_name = "probe" +source.type = "composite" +source.packages = [ + "thundermuffin.tar.gz", +] +output.type = "zone" diff --git a/schema/all-zone-requests.json b/schema/all-zone-requests.json index 8c324a15bd..e37fbfde59 100644 --- a/schema/all-zone-requests.json +++ b/schema/all-zone-requests.json @@ -302,6 +302,26 @@ ] } } + }, + { + "description": "A vNIC associated with a probe", + "type": "object", + "required": [ + "id", + "type" + ], + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "type": { + "type": "string", + "enum": [ + "probe" + ] + } + } } ] }, diff --git a/schema/all-zones-requests.json b/schema/all-zones-requests.json index 7a07e2f9ae..0ac9e760a8 100644 --- a/schema/all-zones-requests.json +++ b/schema/all-zones-requests.json @@ -186,6 +186,26 @@ ] } } + }, + { + "description": "A vNIC associated with a probe", + "type": "object", + "required": [ + "id", + "type" + ], + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "type": { + "type": "string", + "enum": [ + "probe" + ] + } + } } ] }, diff --git a/schema/crdb/40.0.0/up1.sql b/schema/crdb/40.0.0/up1.sql new file mode 100644 index 0000000000..7fc8c01713 --- /dev/null +++ b/schema/crdb/40.0.0/up1.sql @@ -0,0 +1,10 @@ +CREATE TABLE IF NOT EXISTS omicron.public.probe ( + id UUID NOT NULL PRIMARY KEY, + name STRING(63) NOT NULL, + description STRING(512) NOT NULL, + time_created TIMESTAMPTZ NOT NULL, + time_modified TIMESTAMPTZ NOT NULL, + time_deleted TIMESTAMPTZ, + project_id UUID NOT NULL, + sled UUID NOT NULL +); diff --git a/schema/crdb/40.0.0/up2.sql b/schema/crdb/40.0.0/up2.sql new file mode 100644 index 0000000000..6c070463a4 --- /dev/null +++ b/schema/crdb/40.0.0/up2.sql @@ -0,0 +1,4 @@ +CREATE UNIQUE INDEX IF NOT EXISTS lookup_probe_by_name ON omicron.public.probe ( + name +) WHERE + time_deleted IS NULL; diff --git a/schema/crdb/40.0.0/up3.sql b/schema/crdb/40.0.0/up3.sql new file mode 100644 index 0000000000..3b71ba4313 --- /dev/null +++ b/schema/crdb/40.0.0/up3.sql @@ -0,0 +1 @@ +ALTER TABLE omicron.public.external_ip ADD COLUMN IF NOT EXISTS is_probe BOOL NOT NULL DEFAULT false; diff --git a/schema/crdb/40.0.0/up4.sql b/schema/crdb/40.0.0/up4.sql new file mode 100644 index 0000000000..6c989cf8c8 --- /dev/null +++ b/schema/crdb/40.0.0/up4.sql @@ -0,0 +1 @@ +ALTER TYPE omicron.public.network_interface_kind ADD VALUE IF NOT EXISTS 'probe'; diff --git a/schema/crdb/dbinit.sql b/schema/crdb/dbinit.sql index 79b6131d85..27faf3f79f 100644 --- a/schema/crdb/dbinit.sql +++ b/schema/crdb/dbinit.sql @@ -3548,6 +3548,26 @@ SELECT deleted FROM interleaved_versions; +CREATE TABLE IF NOT EXISTS omicron.public.probe ( + id UUID NOT NULL PRIMARY KEY, + name STRING(63) NOT NULL, + description STRING(512) NOT NULL, + time_created TIMESTAMPTZ NOT NULL, + time_modified TIMESTAMPTZ NOT NULL, + time_deleted TIMESTAMPTZ, + project_id UUID NOT NULL, + sled UUID NOT NULL +); + +CREATE UNIQUE INDEX IF NOT EXISTS lookup_probe_by_name ON omicron.public.probe ( + name +) WHERE + time_deleted IS NULL; + +ALTER TABLE omicron.public.external_ip ADD COLUMN IF NOT EXISTS is_probe BOOL NOT NULL DEFAULT false; + +ALTER TYPE omicron.public.network_interface_kind ADD VALUE IF NOT EXISTS 'probe'; + INSERT INTO omicron.public.db_metadata ( singleton, time_created, @@ -3555,7 +3575,7 @@ INSERT INTO omicron.public.db_metadata ( version, target_version ) VALUES - ( TRUE, NOW(), NOW(), '39.0.0', NULL) + ( TRUE, NOW(), NOW(), '40.0.0', NULL) ON CONFLICT DO NOTHING; COMMIT; diff --git a/schema/rss-service-plan-v2.json b/schema/rss-service-plan-v2.json index 10d8f8ab95..ee0b21af81 100644 --- a/schema/rss-service-plan-v2.json +++ b/schema/rss-service-plan-v2.json @@ -274,6 +274,26 @@ ] } } + }, + { + "description": "A vNIC associated with a probe", + "type": "object", + "required": [ + "id", + "type" + ], + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "type": { + "type": "string", + "enum": [ + "probe" + ] + } + } } ] }, diff --git a/sled-agent/src/lib.rs b/sled-agent/src/lib.rs index bfc23b248d..a4686bdb88 100644 --- a/sled-agent/src/lib.rs +++ b/sled-agent/src/lib.rs @@ -30,6 +30,7 @@ mod long_running_tasks; mod metrics; mod nexus; pub mod params; +mod probe_manager; mod profile; pub mod rack_setup; pub mod server; diff --git a/sled-agent/src/params.rs b/sled-agent/src/params.rs index d192f745f6..f30c910efc 100644 --- a/sled-agent/src/params.rs +++ b/sled-agent/src/params.rs @@ -742,7 +742,7 @@ impl From for sled_agent_client::types::OmicronZoneType { domain, ntp_servers, snat_cfg, - nic: nic.into(), + nic: nic, }, OmicronZoneType::Clickhouse { address, dataset } => { Other::Clickhouse { @@ -778,7 +778,7 @@ impl From for sled_agent_client::types::OmicronZoneType { dataset: dataset.into(), http_address: http_address.to_string(), dns_address: dns_address.to_string(), - nic: nic.into(), + nic: nic, }, OmicronZoneType::InternalDns { dataset, @@ -815,7 +815,7 @@ impl From for sled_agent_client::types::OmicronZoneType { external_ip, external_tls, internal_address: internal_address.to_string(), - nic: nic.into(), + nic: nic, }, OmicronZoneType::Oximeter { address } => { Other::Oximeter { address: address.to_string() } diff --git a/sled-agent/src/probe_manager.rs b/sled-agent/src/probe_manager.rs new file mode 100644 index 0000000000..8481dc4b79 --- /dev/null +++ b/sled-agent/src/probe_manager.rs @@ -0,0 +1,383 @@ +use crate::nexus::NexusClientWithResolver; +use anyhow::{anyhow, Result}; +use illumos_utils::dladm::Etherstub; +use illumos_utils::link::VnicAllocator; +use illumos_utils::opte::params::VpcFirewallRule; +use illumos_utils::opte::{DhcpCfg, PortManager}; +use illumos_utils::running_zone::{RunningZone, ZoneBuilderFactory}; +use illumos_utils::zone::Zones; +use nexus_client::types::{ProbeExternalIp, ProbeInfo}; +use omicron_common::api::external::{ + VpcFirewallRuleAction, VpcFirewallRuleDirection, VpcFirewallRulePriority, + VpcFirewallRuleStatus, +}; +use omicron_common::api::internal::shared::NetworkInterface; +use rand::prelude::SliceRandom; +use rand::SeedableRng; +use sled_storage::dataset::ZONE_DATASET; +use sled_storage::manager::StorageHandle; +use slog::{error, warn, Logger}; +use std::collections::{HashMap, HashSet}; +use std::hash::{Hash, Hasher}; +use std::sync::Arc; +use std::time::Duration; +use tokio::sync::Mutex; +use tokio::task::JoinHandle; +use tokio::time::sleep; +use uuid::Uuid; +use zone::Zone; + +/// Prefix used for probe zone names +const PROBE_ZONE_PREFIX: &str = "oxz_probe"; + +/// How long to wait between check-ins with nexus +const RECONCILIATION_INTERVAL: Duration = Duration::from_secs(1); + +/// The scope to use when allocating VNICs +const VNIC_ALLOCATOR_SCOPE: &str = "probe"; + +/// The probe manager periodically asks nexus what probes it should be running. +/// It checks the probes it should be running versus the probes it's actually +/// running and reconciles any differences. +pub(crate) struct ProbeManager { + inner: Arc, +} + +pub(crate) struct ProbeManagerInner { + join_handle: Mutex>>, + nexus_client: NexusClientWithResolver, + log: Logger, + sled_id: Uuid, + vnic_allocator: VnicAllocator, + storage: StorageHandle, + port_manager: PortManager, + running_probes: Mutex>, +} + +impl ProbeManager { + pub(crate) fn new( + sled_id: Uuid, + nexus_client: NexusClientWithResolver, + etherstub: Etherstub, + storage: StorageHandle, + port_manager: PortManager, + log: Logger, + ) -> Self { + Self { + inner: Arc::new(ProbeManagerInner { + join_handle: Mutex::new(None), + vnic_allocator: VnicAllocator::new( + VNIC_ALLOCATOR_SCOPE, + etherstub, + ), + running_probes: Mutex::new(HashMap::new()), + nexus_client, + log, + sled_id, + storage, + port_manager, + }), + } + } + + pub(crate) async fn run(&self) { + self.inner.run().await; + } +} + +/// State information about a probe. This is a common representation that +/// captures elements from both the nexus and running-zone representation of a +/// probe. +#[derive(Debug, Clone)] +struct ProbeState { + /// Id as determined by nexus + id: Uuid, + /// Runtime state on this sled + status: zone::State, + /// The external IP addresses the probe has been assigned. + external_ips: Vec, + /// The probes networking interface. + interface: Option, +} + +impl PartialEq for ProbeState { + fn eq(&self, other: &Self) -> bool { + self.id.eq(&other.id) + } +} + +impl Eq for ProbeState {} + +impl Hash for ProbeState { + fn hash(&self, state: &mut H) { + self.id.hash(state) + } +} + +/// Translate from the nexus API `ProbeInfo` into a `ProbeState` +impl From for ProbeState { + fn from(value: ProbeInfo) -> Self { + Self { + id: value.id, + status: zone::State::Running, + external_ips: value.external_ips, + interface: Some(value.interface), + } + } +} + +/// Translate from running zone state into a `ProbeState` +impl TryFrom for ProbeState { + type Error = String; + fn try_from(value: Zone) -> std::result::Result { + Ok(Self { + id: value + .name() + .strip_prefix(&format!("{PROBE_ZONE_PREFIX}_")) + .ok_or(String::from("not a probe prefix"))? + .parse() + .map_err(|e| format!("invalid uuid: {e}"))?, + status: value.state(), + external_ips: Vec::new(), + interface: None, + }) + } +} + +impl ProbeManagerInner { + /// Run the probe manager. If it's already running this is a no-op. + async fn run(self: &Arc) { + let mut join_handle = self.join_handle.lock().await; + if join_handle.is_none() { + *join_handle = Some(self.clone().reconciler()) + } + } + + /// Run the reconciler loop on a background thread. + fn reconciler(self: Arc) -> JoinHandle<()> { + tokio::spawn(async move { + loop { + sleep(RECONCILIATION_INTERVAL).await; + + // Collect the target and current state. Use set operations + // to determine what probes need to be added, removed and/or + // modified. + + let target = match self.target_state().await { + Ok(state) => state, + Err(e) => { + error!(self.log, "get target probe state: {e}"); + continue; + } + }; + + let current = match self.current_state().await { + Ok(state) => state, + Err(e) => { + error!(self.log, "get current probe state: {e}"); + continue; + } + }; + + self.add(target.difference(¤t)).await; + self.remove(current.difference(&target)).await; + self.check(current.intersection(&target)).await; + } + }) + } + + /// Add a set of probes to this sled. + async fn add<'a, I>(self: &Arc, probes: I) + where + I: Iterator, + { + for probe in probes { + info!(self.log, "adding probe {}", probe.id); + if let Err(e) = self.add_probe(probe).await { + error!(self.log, "add probe: {e}"); + } + } + } + + /// Add a probe to this sled. This sets up resources for the probe zone + /// such as storage and networking. Then it configures, installs and + /// boots the probe zone. + async fn add_probe(self: &Arc, probe: &ProbeState) -> Result<()> { + let mut rng = rand::rngs::StdRng::from_entropy(); + let root = self + .storage + .get_latest_resources() + .await + .all_u2_mountpoints(ZONE_DATASET) + .choose(&mut rng) + .ok_or_else(|| anyhow!("u2 not found"))? + .clone(); + + let nic = probe + .interface + .as_ref() + .ok_or(anyhow!("no interface specified for probe"))?; + + let eip = probe + .external_ips + .get(0) + .ok_or(anyhow!("expected an external ip"))?; + + let port = self.port_manager.create_port( + &nic, + None, + Some(eip.ip), + &[], // floating ips + &[VpcFirewallRule { + status: VpcFirewallRuleStatus::Enabled, + direction: VpcFirewallRuleDirection::Inbound, + targets: vec![nic.clone()], + filter_hosts: None, + filter_ports: None, + filter_protocols: None, + action: VpcFirewallRuleAction::Allow, + priority: VpcFirewallRulePriority(100), + }], + DhcpCfg::default(), + )?; + + let installed_zone = ZoneBuilderFactory::default() + .builder() + .with_log(self.log.clone()) + .with_underlay_vnic_allocator(&self.vnic_allocator) + .with_zone_root_path(&root) + .with_zone_image_paths(&["/opt/oxide".into()]) + .with_zone_type("probe") + .with_unique_name(probe.id) + .with_datasets(&[]) + .with_filesystems(&[]) + .with_data_links(&[]) + .with_devices(&[]) + .with_opte_ports(vec![port]) + .with_links(vec![]) + .with_limit_priv(vec![]) + .install() + .await?; + + info!(self.log, "installed probe {}", probe.id); + + //TODO SMF properties for probe services? + + let rz = RunningZone::boot(installed_zone).await?; + rz.ensure_address_for_port("overlay", 0).await?; + info!(self.log, "started probe {}", probe.id); + + self.running_probes.lock().await.insert(probe.id, rz); + + Ok(()) + } + + /// Remove a set of probes from this sled. + async fn remove<'a, I>(self: &Arc, probes: I) + where + I: Iterator, + { + for probe in probes { + info!(self.log, "removing probe {}", probe.id); + self.remove_probe(probe.id).await; + } + } + + /// Remove a probe from this sled. This tears down the zone and it's + /// network resources. + async fn remove_probe(self: &Arc, id: Uuid) { + match self.running_probes.lock().await.remove(&id) { + Some(mut running_zone) => { + for l in running_zone.links_mut() { + if let Err(e) = l.delete() { + error!(self.log, "delete probe link {}: {e}", l.name()); + } + } + running_zone.release_opte_ports(); + if let Err(e) = running_zone.stop().await { + error!(self.log, "stop probe: {e}") + } + // TODO are there storage resources that need to be cleared + // out here too? + } + None => { + warn!(self.log, "attempt to stop non-running probe: {id}") + } + } + } + + /// Check that probes that should be running are running, and with the + /// correct configuration. + async fn check<'a, I>(self: &Arc, probes: I) + where + I: Iterator, + { + for probe in probes { + if probe.status == zone::State::Running { + continue; + } + warn!( + self.log, + "probe {} found in unexpected state {:?}", + probe.id, + probe.status + ) + //TODO somehow handle the hooligans here? + } + } + + /// Collect target probe state from the nexus internal API. + async fn target_state(self: &Arc) -> Result> { + Ok(self + .nexus_client + .client() + .probes_get( + &self.sled_id, + None, //limit + None, //page token + None, //sort by + ) + .await? + .into_inner() + .into_iter() + .map(Into::into) + .collect()) + } + + /// Collect the current probe state from the running zones on this sled. + async fn current_state(self: &Arc) -> Result> { + Ok(Zones::get() + .await? + .into_iter() + .filter_map(|z| ProbeState::try_from(z).ok()) + .collect()) + } +} + +#[cfg(test)] +mod test { + use super::*; + use uuid::Uuid; + + #[test] + fn probe_state_set_ops() { + let a = ProbeState { + id: Uuid::new_v4(), + status: zone::State::Configured, + external_ips: Vec::new(), + interface: None, + }; + + let mut b = a.clone(); + b.status = zone::State::Running; + + let target = HashSet::from([a]); + let current = HashSet::from([b]); + + let to_add = target.difference(¤t); + let to_remove = current.difference(&target); + + assert_eq!(to_add.count(), 0); + assert_eq!(to_remove.count(), 0); + } +} diff --git a/sled-agent/src/sled_agent.rs b/sled-agent/src/sled_agent.rs index 8f737f879b..4a21a6fe89 100644 --- a/sled-agent/src/sled_agent.rs +++ b/sled-agent/src/sled_agent.rs @@ -25,6 +25,7 @@ use crate::params::{ OmicronZonesConfig, SledRole, TimeSync, VpcFirewallRule, ZoneBundleMetadata, Zpool, }; +use crate::probe_manager::ProbeManager; use crate::services::{self, ServiceManager}; use crate::storage_monitor::UnderlayAccess; use crate::updates::{ConfigUpdates, UpdateManager}; @@ -309,6 +310,9 @@ struct SledAgentInner { // Handle to the traffic manager for writing OS updates to our boot disks. boot_disk_os_writer: BootDiskOsWriter, + + // Component of Sled Agent responsible for managing instrumentation probes. + probes: ProbeManager, } impl SledAgentInner { @@ -571,6 +575,15 @@ impl SledAgent { nexus_notifier_task.run().await; }); + let probes = ProbeManager::new( + request.body.id, + nexus_client.clone(), + etherstub.clone(), + storage_manager.clone(), + port_manager.clone(), + log.new(o!("component" => "ProbeManager")), + ); + let sled_agent = SledAgent { inner: Arc::new(SledAgentInner { id: request.body.id, @@ -578,6 +591,7 @@ impl SledAgent { start_request: request, storage: long_running_task_handles.storage_manager.clone(), instances, + probes, hardware: long_running_task_handles.hardware_manager.clone(), updates, port_manager, @@ -593,6 +607,8 @@ impl SledAgent { log: log.clone(), }; + sled_agent.inner.probes.run().await; + // We immediately add a notification to the request queue about our // existence. If inspection of the hardware later informs us that we're // actually running on a scrimlet, that's fine, the updated value will diff --git a/tools/ci_download_maghemite_mgd b/tools/ci_download_maghemite_mgd index 9890e4505e..bf6be1d5b1 100755 --- a/tools/ci_download_maghemite_mgd +++ b/tools/ci_download_maghemite_mgd @@ -29,6 +29,8 @@ PACKAGE_BASE_URL="$ARTIFACT_URL/$REPO/image/$COMMIT" function main { + rm -rf $DOWNLOAD_DIR/root + # # Process command-line arguments. We generally don't expect any, but # we allow callers to specify a value to override OSTYPE, just for diff --git a/tools/ci_download_maghemite_openapi b/tools/ci_download_maghemite_openapi index 56ce640a76..7255e57cf4 100755 --- a/tools/ci_download_maghemite_openapi +++ b/tools/ci_download_maghemite_openapi @@ -15,10 +15,10 @@ TARGET_DIR="out" # Location where intermediate artifacts are downloaded / unpacked. DOWNLOAD_DIR="$TARGET_DIR/downloads" - - function main { + rm -rf $DOWNLOAD_DIR/root + if [[ $# != 0 ]]; then echo "unexpected arguments" >&2 exit 2 diff --git a/tools/ci_download_thundermuffin b/tools/ci_download_thundermuffin new file mode 100755 index 0000000000..014d1b30b2 --- /dev/null +++ b/tools/ci_download_thundermuffin @@ -0,0 +1,153 @@ +#!/bin/bash + +# +# ci_download_probe_packages: fetches thundermuffin binary tarball package, +# unpacks it, and creates a copy, all in the current directory +# + +set -o pipefail +set -o xtrace +set -o errexit + +SOURCE_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" +ARG0="$(basename "${BASH_SOURCE[0]}")" + +source "$SOURCE_DIR/thundermuffin_checksums" +source "$SOURCE_DIR/thundermuffin_version" + +TARGET_DIR="out" +# Location where intermediate artifacts are downloaded / unpacked. +DOWNLOAD_DIR="$TARGET_DIR/downloads" +# Location where the final thundermuffin directory should end up. +DEST_DIR="./$TARGET_DIR/thundermuffin" +BIN_DIR="$DEST_DIR/root/opt/oxide/thundermuffin/bin" + +ARTIFACT_URL="https://buildomat.eng.oxide.computer/public/file" + +REPO='oxidecomputer/thundermuffin' +PACKAGE_BASE_URL="$ARTIFACT_URL/$REPO/image/$COMMIT" + +function main +{ + rm -rf $DOWNLOAD_DIR/root + + # + # Process command-line arguments. We generally don't expect any, but + # we allow callers to specify a value to override OSTYPE, just for + # testing. + # + if [[ $# != 0 ]]; then + CIDL_OS="$1" + shift + else + CIDL_OS="$OSTYPE" + fi + + if [[ $# != 0 ]]; then + echo "unexpected arguments" >&2 + exit 2 + fi + + # Configure this program + configure_os "$CIDL_OS" + + CIDL_SHA256FUNC="do_sha256sum" + TARBALL_FILENAME="thundermuffin.tar.gz" + PACKAGE_URL="$PACKAGE_BASE_URL/$TARBALL_FILENAME" + TARBALL_FILE="$DOWNLOAD_DIR/$TARBALL_FILENAME" + + # Download the file. + echo "URL: $PACKAGE_URL" + echo "Local file: $TARBALL_FILE" + + mkdir -p "$DOWNLOAD_DIR" + mkdir -p "$DEST_DIR" + + fetch_and_verify + + do_untar "$TARBALL_FILE" + + do_assemble + + $SET_BINARIES +} + +function fail +{ + echo "$ARG0: $@" >&2 + exit 1 +} + +function configure_os +{ + echo "current directory: $PWD" + echo "configuring based on OS: \"$1\"" + case "$1" in + solaris*) + SET_BINARIES="" + ;; + *) + echo "WARNING: binaries for $1 are not published by thundermuffin" + SET_BINARIES="unsupported_os" + ;; + esac +} + +function do_download_curl +{ + curl --silent --show-error --fail --location --output "$2" "$1" +} + +function do_sha256sum +{ + sha256sum < "$1" | awk '{print $1}' +} + +function do_untar +{ + tar xzf "$1" -C "$DOWNLOAD_DIR" +} + +function do_assemble +{ + rm -r "$DEST_DIR" || true + mkdir "$DEST_DIR" + cp -r "$DOWNLOAD_DIR/root" "$DEST_DIR/root" +} + +function fetch_and_verify +{ + local DO_DOWNLOAD="true" + if [[ -f "$TARBALL_FILE" ]]; then + # If the file exists with a valid checksum, we can skip downloading. + calculated_sha256="$($CIDL_SHA256FUNC "$TARBALL_FILE")" || \ + fail "failed to calculate sha256sum" + if [[ "$calculated_sha256" == "$CIDL_SHA256" ]]; then + DO_DOWNLOAD="false" + fi + fi + + if [ "$DO_DOWNLOAD" == "true" ]; then + echo "Downloading..." + do_download_curl "$PACKAGE_URL" "$TARBALL_FILE" || \ + fail "failed to download file" + + # Verify the sha256sum. + calculated_sha256="$($CIDL_SHA256FUNC "$TARBALL_FILE")" || \ + fail "failed to calculate sha256sum" + if [[ "$calculated_sha256" != "$CIDL_SHA256" ]]; then + fail "sha256sum mismatch \ + (expected $CIDL_SHA256, found $calculated_sha256)" + fi + fi + +} + +function unsupported_os +{ + mkdir -p "$BIN_DIR" + echo "echo 'unsupported os' && exit 1" >> "$BIN_DIR/thundermuffin" + chmod +x "$BIN_DIR/thundermuffin" +} + +main "$@" diff --git a/tools/install_builder_prerequisites.sh b/tools/install_builder_prerequisites.sh index 5fa8ec11ba..09b8a27677 100755 --- a/tools/install_builder_prerequisites.sh +++ b/tools/install_builder_prerequisites.sh @@ -217,6 +217,9 @@ retry ./tools/ci_download_maghemite_mgd # xcvradm binary which is bundled with the switch zone. retry ./tools/ci_download_transceiver_control +# Download thundermuffin. This is required to launch network probes. +retry ./tools/ci_download_thundermuffin + # Validate the PATH: expected_in_path=( 'pg_config' diff --git a/tools/thundermuffin_checksums b/tools/thundermuffin_checksums new file mode 100644 index 0000000000..5e10539bdd --- /dev/null +++ b/tools/thundermuffin_checksums @@ -0,0 +1 @@ +CIDL_SHA256="dc55a2accd33a347df4cbdc0026cbaccea2c004940c3fec8cadcdd633d440dfa" diff --git a/tools/thundermuffin_version b/tools/thundermuffin_version new file mode 100644 index 0000000000..cbca739f5c --- /dev/null +++ b/tools/thundermuffin_version @@ -0,0 +1 @@ +COMMIT="a4a6108d7c9aac2464a0b6898e88132a8f701a13" diff --git a/workspace-hack/Cargo.toml b/workspace-hack/Cargo.toml index e70c7c329e..5efbb6c1f1 100644 --- a/workspace-hack/Cargo.toml +++ b/workspace-hack/Cargo.toml @@ -14,26 +14,26 @@ publish = false ### BEGIN HAKARI SECTION [dependencies] -ahash = { version = "0.8.7" } -aho-corasick = { version = "1.0.4" } -anyhow = { version = "1.0.75", features = ["backtrace"] } +ahash = { version = "0.8.8" } +aho-corasick = { version = "1.1.2" } +anyhow = { version = "1.0.79", features = ["backtrace"] } base16ct = { version = "0.2.0", default-features = false, features = ["alloc"] } bit-set = { version = "0.5.3" } bit-vec = { version = "0.6.3" } bitflags-dff4ba8e3ae991db = { package = "bitflags", version = "1.3.2" } -bitflags-f595c2ba2a3f28df = { package = "bitflags", version = "2.4.0", default-features = false, features = ["serde"] } +bitflags-f595c2ba2a3f28df = { package = "bitflags", version = "2.4.2", default-features = false, features = ["serde"] } bstr-6f8ce4dd05d13bba = { package = "bstr", version = "0.2.17" } -bstr-dff4ba8e3ae991db = { package = "bstr", version = "1.6.0" } +bstr-dff4ba8e3ae991db = { package = "bstr", version = "1.9.0" } byteorder = { version = "1.5.0" } bytes = { version = "1.5.0", features = ["serde"] } -chrono = { version = "0.4.31", features = ["alloc", "serde"] } +chrono = { version = "0.4.34", features = ["serde"] } cipher = { version = "0.4.4", default-features = false, features = ["block-padding", "zeroize"] } -clap = { version = "4.5.0", features = ["cargo", "derive", "env", "wrap_help"] } -clap_builder = { version = "4.5.0", default-features = false, features = ["cargo", "color", "env", "std", "suggestions", "usage", "wrap_help"] } +clap = { version = "4.5.1", features = ["cargo", "derive", "env", "wrap_help"] } +clap_builder = { version = "4.5.1", 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" } -crossbeam-utils = { version = "0.8.16" } +const-oid = { version = "0.9.6", default-features = false, features = ["db", "std"] } +crossbeam-epoch = { version = "0.9.18" } +crossbeam-utils = { version = "0.8.19" } crossterm = { version = "0.27.0", features = ["event-stream", "serde"] } crypto-common = { version = "0.1.6", default-features = false, features = ["getrandom", "std"] } der = { version = "0.7.8", default-features = false, features = ["derive", "flagset", "oid", "pem", "std"] } @@ -52,12 +52,12 @@ futures-task = { version = "0.3.30", default-features = false, features = ["std" futures-util = { version = "0.3.30", features = ["channel", "io", "sink"] } gateway-messages = { git = "https://github.com/oxidecomputer/management-gateway-service", rev = "2739c18e80697aa6bc235c935176d14b4d757ee9", features = ["std"] } generic-array = { version = "0.14.7", default-features = false, features = ["more_lengths", "zeroize"] } -getrandom = { version = "0.2.10", default-features = false, features = ["js", "rdrand", "std"] } +getrandom = { version = "0.2.12", default-features = false, features = ["js", "rdrand", "std"] } group = { version = "0.13.0", default-features = false, features = ["alloc"] } hashbrown = { version = "0.14.3", features = ["raw"] } hex = { version = "0.4.3", features = ["serde"] } hmac = { version = "0.12.1", default-features = false, features = ["reset"] } -hyper = { version = "0.14.27", features = ["full"] } +hyper = { version = "0.14.28", features = ["full"] } indexmap = { version = "2.2.5", features = ["serde"] } inout = { version = "0.1.3", default-features = false, features = ["std"] } ipnetwork = { version = "0.20.0", features = ["schemars"] } @@ -67,12 +67,12 @@ lazy_static = { version = "1.4.0", default-features = false, features = ["spin_n libc = { version = "0.2.153", features = ["extra_traits"] } log = { version = "0.4.20", default-features = false, features = ["std"] } managed = { version = "0.8.0", default-features = false, features = ["alloc", "map"] } -memchr = { version = "2.6.3" } +memchr = { version = "2.7.1" } nom = { version = "7.1.3" } num-bigint = { version = "0.4.4", features = ["rand"] } num-integer = { version = "0.1.46", features = ["i128"] } -num-iter = { version = "0.1.43", default-features = false, features = ["i128"] } -num-traits = { version = "0.2.16", features = ["i128", "libm"] } +num-iter = { version = "0.1.44", default-features = false, features = ["i128"] } +num-traits = { version = "0.2.18", features = ["i128", "libm"] } openapiv3 = { version = "2.0.0", default-features = false, features = ["skip_serializing_defaults"] } pem-rfc7468 = { version = "0.7.0", default-features = false, features = ["std"] } petgraph = { version = "0.6.4", features = ["serde-1"] } @@ -83,33 +83,32 @@ proc-macro2 = { version = "1.0.78" } rand = { version = "0.8.5" } rand_chacha = { version = "0.3.1", default-features = false, features = ["std"] } regex = { version = "1.10.3" } -regex-automata = { version = "0.4.4", default-features = false, features = ["dfa-onepass", "hybrid", "meta", "nfa-backtrack", "perf-inline", "perf-literal", "unicode"] } +regex-automata = { version = "0.4.5", default-features = false, features = ["dfa-onepass", "dfa-search", "hybrid", "meta", "nfa-backtrack", "perf-inline", "perf-literal", "unicode"] } regex-syntax = { version = "0.8.2" } -reqwest = { version = "0.11.24", features = ["blocking", "json", "rustls-tls", "stream"] } +reqwest = { version = "0.11.24", features = ["blocking", "cookies", "json", "rustls-tls", "stream"] } ring = { version = "0.17.8", features = ["std"] } schemars = { version = "0.8.16", features = ["bytes", "chrono", "uuid1"] } semver = { version = "1.0.22", features = ["serde"] } serde = { version = "1.0.197", features = ["alloc", "derive", "rc"] } serde_json = { version = "1.0.114", features = ["raw_value", "unbounded_depth"] } sha2 = { version = "0.10.8", features = ["oid"] } -similar = { version = "2.3.0", features = ["inline", "unicode"] } +similar = { version = "2.4.0", features = ["inline", "unicode"] } slog = { version = "2.7.0", features = ["dynamic-keys", "max_level_trace", "release_max_level_debug", "release_max_level_trace"] } -socket2 = { version = "0.5.5", default-features = false, features = ["all"] } spin = { version = "0.9.8" } string_cache = { version = "0.8.7" } subtle = { version = "2.5.0" } syn-dff4ba8e3ae991db = { package = "syn", version = "1.0.109", features = ["extra-traits", "fold", "full", "visit"] } syn-f595c2ba2a3f28df = { package = "syn", version = "2.0.51", features = ["extra-traits", "fold", "full", "visit", "visit-mut"] } -time = { version = "0.3.27", features = ["formatting", "local-offset", "macros", "parsing"] } +time = { version = "0.3.34", features = ["formatting", "local-offset", "macros", "parsing"] } tokio = { version = "1.36.0", features = ["full", "test-util"] } tokio-postgres = { version = "0.7.10", features = ["with-chrono-0_4", "with-serde_json-1", "with-uuid-1"] } tokio-stream = { version = "0.1.14", features = ["net"] } tokio-util = { version = "0.7.10", features = ["codec", "io-util"] } toml = { version = "0.7.8" } toml_edit-3c51e837cfc5589a = { package = "toml_edit", version = "0.22.6", features = ["serde"] } -tracing = { version = "0.1.37", features = ["log"] } +tracing = { version = "0.1.40", features = ["log"] } trust-dns-proto = { version = "0.22.0" } -unicode-bidi = { version = "0.3.13" } +unicode-bidi = { version = "0.3.15" } unicode-normalization = { version = "0.1.22" } usdt = { version = "0.3.5" } usdt-impl = { version = "0.5.0", default-features = false, features = ["asm", "des"] } @@ -120,26 +119,26 @@ zeroize = { version = "1.7.0", features = ["std", "zeroize_derive"] } zip = { version = "0.6.6", default-features = false, features = ["bzip2", "deflate"] } [build-dependencies] -ahash = { version = "0.8.7" } -aho-corasick = { version = "1.0.4" } -anyhow = { version = "1.0.75", features = ["backtrace"] } +ahash = { version = "0.8.8" } +aho-corasick = { version = "1.1.2" } +anyhow = { version = "1.0.79", features = ["backtrace"] } base16ct = { version = "0.2.0", default-features = false, features = ["alloc"] } bit-set = { version = "0.5.3" } bit-vec = { version = "0.6.3" } bitflags-dff4ba8e3ae991db = { package = "bitflags", version = "1.3.2" } -bitflags-f595c2ba2a3f28df = { package = "bitflags", version = "2.4.0", default-features = false, features = ["serde"] } +bitflags-f595c2ba2a3f28df = { package = "bitflags", version = "2.4.2", default-features = false, features = ["serde"] } bstr-6f8ce4dd05d13bba = { package = "bstr", version = "0.2.17" } -bstr-dff4ba8e3ae991db = { package = "bstr", version = "1.6.0" } +bstr-dff4ba8e3ae991db = { package = "bstr", version = "1.9.0" } byteorder = { version = "1.5.0" } bytes = { version = "1.5.0", features = ["serde"] } -chrono = { version = "0.4.31", features = ["alloc", "serde"] } +chrono = { version = "0.4.34", features = ["serde"] } cipher = { version = "0.4.4", default-features = false, features = ["block-padding", "zeroize"] } -clap = { version = "4.5.0", features = ["cargo", "derive", "env", "wrap_help"] } -clap_builder = { version = "4.5.0", default-features = false, features = ["cargo", "color", "env", "std", "suggestions", "usage", "wrap_help"] } +clap = { version = "4.5.1", features = ["cargo", "derive", "env", "wrap_help"] } +clap_builder = { version = "4.5.1", 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" } -crossbeam-utils = { version = "0.8.16" } +const-oid = { version = "0.9.6", default-features = false, features = ["db", "std"] } +crossbeam-epoch = { version = "0.9.18" } +crossbeam-utils = { version = "0.8.19" } crossterm = { version = "0.27.0", features = ["event-stream", "serde"] } crypto-common = { version = "0.1.6", default-features = false, features = ["getrandom", "std"] } der = { version = "0.7.8", default-features = false, features = ["derive", "flagset", "oid", "pem", "std"] } @@ -158,12 +157,12 @@ futures-task = { version = "0.3.30", default-features = false, features = ["std" futures-util = { version = "0.3.30", features = ["channel", "io", "sink"] } gateway-messages = { git = "https://github.com/oxidecomputer/management-gateway-service", rev = "2739c18e80697aa6bc235c935176d14b4d757ee9", features = ["std"] } generic-array = { version = "0.14.7", default-features = false, features = ["more_lengths", "zeroize"] } -getrandom = { version = "0.2.10", default-features = false, features = ["js", "rdrand", "std"] } +getrandom = { version = "0.2.12", default-features = false, features = ["js", "rdrand", "std"] } group = { version = "0.13.0", default-features = false, features = ["alloc"] } hashbrown = { version = "0.14.3", features = ["raw"] } hex = { version = "0.4.3", features = ["serde"] } hmac = { version = "0.12.1", default-features = false, features = ["reset"] } -hyper = { version = "0.14.27", features = ["full"] } +hyper = { version = "0.14.28", features = ["full"] } indexmap = { version = "2.2.5", features = ["serde"] } inout = { version = "0.1.3", default-features = false, features = ["std"] } ipnetwork = { version = "0.20.0", features = ["schemars"] } @@ -173,12 +172,12 @@ lazy_static = { version = "1.4.0", default-features = false, features = ["spin_n libc = { version = "0.2.153", features = ["extra_traits"] } log = { version = "0.4.20", default-features = false, features = ["std"] } managed = { version = "0.8.0", default-features = false, features = ["alloc", "map"] } -memchr = { version = "2.6.3" } +memchr = { version = "2.7.1" } nom = { version = "7.1.3" } num-bigint = { version = "0.4.4", features = ["rand"] } num-integer = { version = "0.1.46", features = ["i128"] } -num-iter = { version = "0.1.43", default-features = false, features = ["i128"] } -num-traits = { version = "0.2.16", features = ["i128", "libm"] } +num-iter = { version = "0.1.44", default-features = false, features = ["i128"] } +num-traits = { version = "0.2.18", features = ["i128", "libm"] } openapiv3 = { version = "2.0.0", default-features = false, features = ["skip_serializing_defaults"] } pem-rfc7468 = { version = "0.7.0", default-features = false, features = ["std"] } petgraph = { version = "0.6.4", features = ["serde-1"] } @@ -189,34 +188,33 @@ proc-macro2 = { version = "1.0.78" } rand = { version = "0.8.5" } rand_chacha = { version = "0.3.1", default-features = false, features = ["std"] } regex = { version = "1.10.3" } -regex-automata = { version = "0.4.4", default-features = false, features = ["dfa-onepass", "hybrid", "meta", "nfa-backtrack", "perf-inline", "perf-literal", "unicode"] } +regex-automata = { version = "0.4.5", default-features = false, features = ["dfa-onepass", "dfa-search", "hybrid", "meta", "nfa-backtrack", "perf-inline", "perf-literal", "unicode"] } regex-syntax = { version = "0.8.2" } -reqwest = { version = "0.11.24", features = ["blocking", "json", "rustls-tls", "stream"] } +reqwest = { version = "0.11.24", features = ["blocking", "cookies", "json", "rustls-tls", "stream"] } ring = { version = "0.17.8", features = ["std"] } schemars = { version = "0.8.16", features = ["bytes", "chrono", "uuid1"] } semver = { version = "1.0.22", features = ["serde"] } serde = { version = "1.0.197", features = ["alloc", "derive", "rc"] } serde_json = { version = "1.0.114", features = ["raw_value", "unbounded_depth"] } sha2 = { version = "0.10.8", features = ["oid"] } -similar = { version = "2.3.0", features = ["inline", "unicode"] } +similar = { version = "2.4.0", features = ["inline", "unicode"] } slog = { version = "2.7.0", features = ["dynamic-keys", "max_level_trace", "release_max_level_debug", "release_max_level_trace"] } -socket2 = { version = "0.5.5", default-features = false, features = ["all"] } spin = { version = "0.9.8" } string_cache = { version = "0.8.7" } subtle = { version = "2.5.0" } syn-dff4ba8e3ae991db = { package = "syn", version = "1.0.109", features = ["extra-traits", "fold", "full", "visit"] } syn-f595c2ba2a3f28df = { package = "syn", version = "2.0.51", features = ["extra-traits", "fold", "full", "visit", "visit-mut"] } -time = { version = "0.3.27", features = ["formatting", "local-offset", "macros", "parsing"] } -time-macros = { version = "0.2.13", default-features = false, features = ["formatting", "parsing"] } +time = { version = "0.3.34", features = ["formatting", "local-offset", "macros", "parsing"] } +time-macros = { version = "0.2.17", default-features = false, features = ["formatting", "parsing"] } tokio = { version = "1.36.0", features = ["full", "test-util"] } tokio-postgres = { version = "0.7.10", features = ["with-chrono-0_4", "with-serde_json-1", "with-uuid-1"] } tokio-stream = { version = "0.1.14", features = ["net"] } tokio-util = { version = "0.7.10", features = ["codec", "io-util"] } toml = { version = "0.7.8" } toml_edit-3c51e837cfc5589a = { package = "toml_edit", version = "0.22.6", features = ["serde"] } -tracing = { version = "0.1.37", features = ["log"] } +tracing = { version = "0.1.40", features = ["log"] } trust-dns-proto = { version = "0.22.0" } -unicode-bidi = { version = "0.3.13" } +unicode-bidi = { version = "0.3.15" } unicode-normalization = { version = "0.1.22" } usdt = { version = "0.3.5" } usdt-impl = { version = "0.5.0", default-features = false, features = ["asm", "des"] } @@ -227,45 +225,45 @@ zeroize = { version = "1.7.0", features = ["std", "zeroize_derive"] } zip = { version = "0.6.6", default-features = false, features = ["bzip2", "deflate"] } [target.x86_64-unknown-linux-gnu.dependencies] -bitflags-f595c2ba2a3f28df = { package = "bitflags", version = "2.4.0", default-features = false, features = ["std"] } +bitflags-f595c2ba2a3f28df = { package = "bitflags", version = "2.4.2", default-features = false, features = ["std"] } dof = { version = "0.3.0", default-features = false, features = ["des"] } mio = { version = "0.8.11", features = ["net", "os-ext"] } once_cell = { version = "1.19.0" } rustix = { version = "0.38.31", features = ["fs", "termios"] } [target.x86_64-unknown-linux-gnu.build-dependencies] -bitflags-f595c2ba2a3f28df = { package = "bitflags", version = "2.4.0", default-features = false, features = ["std"] } +bitflags-f595c2ba2a3f28df = { package = "bitflags", version = "2.4.2", default-features = false, features = ["std"] } dof = { version = "0.3.0", default-features = false, features = ["des"] } mio = { version = "0.8.11", features = ["net", "os-ext"] } once_cell = { version = "1.19.0" } rustix = { version = "0.38.31", features = ["fs", "termios"] } [target.x86_64-apple-darwin.dependencies] -bitflags-f595c2ba2a3f28df = { package = "bitflags", version = "2.4.0", default-features = false, features = ["std"] } +bitflags-f595c2ba2a3f28df = { package = "bitflags", version = "2.4.2", default-features = false, features = ["std"] } mio = { version = "0.8.11", features = ["net", "os-ext"] } once_cell = { version = "1.19.0" } rustix = { version = "0.38.31", features = ["fs", "termios"] } [target.x86_64-apple-darwin.build-dependencies] -bitflags-f595c2ba2a3f28df = { package = "bitflags", version = "2.4.0", default-features = false, features = ["std"] } +bitflags-f595c2ba2a3f28df = { package = "bitflags", version = "2.4.2", default-features = false, features = ["std"] } mio = { version = "0.8.11", features = ["net", "os-ext"] } once_cell = { version = "1.19.0" } rustix = { version = "0.38.31", features = ["fs", "termios"] } [target.aarch64-apple-darwin.dependencies] -bitflags-f595c2ba2a3f28df = { package = "bitflags", version = "2.4.0", default-features = false, features = ["std"] } +bitflags-f595c2ba2a3f28df = { package = "bitflags", version = "2.4.2", default-features = false, features = ["std"] } mio = { version = "0.8.11", features = ["net", "os-ext"] } once_cell = { version = "1.19.0" } rustix = { version = "0.38.31", features = ["fs", "termios"] } [target.aarch64-apple-darwin.build-dependencies] -bitflags-f595c2ba2a3f28df = { package = "bitflags", version = "2.4.0", default-features = false, features = ["std"] } +bitflags-f595c2ba2a3f28df = { package = "bitflags", version = "2.4.2", default-features = false, features = ["std"] } mio = { version = "0.8.11", features = ["net", "os-ext"] } once_cell = { version = "1.19.0" } rustix = { version = "0.38.31", features = ["fs", "termios"] } [target.x86_64-unknown-illumos.dependencies] -bitflags-f595c2ba2a3f28df = { package = "bitflags", version = "2.4.0", default-features = false, features = ["std"] } +bitflags-f595c2ba2a3f28df = { package = "bitflags", version = "2.4.2", default-features = false, features = ["std"] } dof = { version = "0.3.0", default-features = false, features = ["des"] } mio = { version = "0.8.11", features = ["net", "os-ext"] } once_cell = { version = "1.19.0" } @@ -274,7 +272,7 @@ toml_datetime = { version = "0.6.5", default-features = false, features = ["serd toml_edit-cdcf2f9584511fe6 = { package = "toml_edit", version = "0.19.15", features = ["serde"] } [target.x86_64-unknown-illumos.build-dependencies] -bitflags-f595c2ba2a3f28df = { package = "bitflags", version = "2.4.0", default-features = false, features = ["std"] } +bitflags-f595c2ba2a3f28df = { package = "bitflags", version = "2.4.2", default-features = false, features = ["std"] } dof = { version = "0.3.0", default-features = false, features = ["des"] } mio = { version = "0.8.11", features = ["net", "os-ext"] } once_cell = { version = "1.19.0" }