diff --git a/.gitignore b/.gitignore index e35ab47a430..2f0dcb535f0 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,4 @@ tags connectivity-report.json *.local .DS_Store +tools/check_mcast_drift.sh diff --git a/Cargo.lock b/Cargo.lock index dab633eda70..68cc6bdf66b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -677,7 +677,7 @@ dependencies = [ [[package]] name = "bhyve_api" version = "0.0.0" -source = "git+https://github.com/oxidecomputer/propolis?rev=979b72896bc39a876c98fb32fe706da37a4fc408#979b72896bc39a876c98fb32fe706da37a4fc408" +source = "git+https://github.com/oxidecomputer/propolis?rev=dcad23754c05c74644c9bb0954ca7ed13dca8381#dcad23754c05c74644c9bb0954ca7ed13dca8381" dependencies = [ "bhyve_api_sys", "libc", @@ -687,7 +687,7 @@ dependencies = [ [[package]] name = "bhyve_api_sys" version = "0.0.0" -source = "git+https://github.com/oxidecomputer/propolis?rev=979b72896bc39a876c98fb32fe706da37a4fc408#979b72896bc39a876c98fb32fe706da37a4fc408" +source = "git+https://github.com/oxidecomputer/propolis?rev=dcad23754c05c74644c9bb0954ca7ed13dca8381#dcad23754c05c74644c9bb0954ca7ed13dca8381" dependencies = [ "libc", "strum 0.26.3", @@ -1640,7 +1640,13 @@ dependencies = [ [[package]] name = "client-common" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/maghemite?rev=65cc70aa5507f779551a1d33c0089bb0a9b7ac72#65cc70aa5507f779551a1d33c0089bb0a9b7ac72" +source = "git+https://github.com/oxidecomputer/maghemite?rev=0509f50053149b5ab87e3e33bf291baab729fe5b#0509f50053149b5ab87e3e33bf291baab729fe5b" +dependencies = [ + "oxnet", + "schemars 0.8.22", + "serde", + "thiserror 2.0.18", +] [[package]] name = "clipboard-win" @@ -1749,7 +1755,7 @@ version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.48.0", ] [[package]] @@ -1783,6 +1789,27 @@ dependencies = [ "tokio", ] +[[package]] +name = "common" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/dendrite?rev=96483d0c52f0cfc4e22e70229466798254a99b6c#96483d0c52f0cfc4e22e70229466798254a99b6c" +dependencies = [ + "anyhow", + "chrono", + "oxnet", + "rand 0.9.2", + "schemars 0.8.22", + "serde", + "serde_json", + "slog", + "slog-async", + "slog-bunyan", + "slog-term", + "smf 0.10.0", + "thiserror 2.0.18", + "tokio", +] + [[package]] name = "common" version = "0.1.0" @@ -2675,7 +2702,7 @@ dependencies = [ [[package]] name = "ddm-admin-client" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/maghemite?rev=65cc70aa5507f779551a1d33c0089bb0a9b7ac72#65cc70aa5507f779551a1d33c0089bb0a9b7ac72" +source = "git+https://github.com/oxidecomputer/maghemite?rev=0509f50053149b5ab87e3e33bf291baab729fe5b#0509f50053149b5ab87e3e33bf291baab729fe5b" dependencies = [ "ddm-api-types-versions", "oxnet", @@ -2689,20 +2716,23 @@ dependencies = [ [[package]] name = "ddm-api-types-versions" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/maghemite?rev=65cc70aa5507f779551a1d33c0089bb0a9b7ac72#65cc70aa5507f779551a1d33c0089bb0a9b7ac72" +source = "git+https://github.com/oxidecomputer/maghemite?rev=0509f50053149b5ab87e3e33bf291baab729fe5b#0509f50053149b5ab87e3e33bf291baab729fe5b" dependencies = [ + "client-common", "ddm-protocol", "oxnet", "schemars 0.8.22", "serde", + "serde_json", "serde_repr", + "thiserror 2.0.18", "uuid", ] [[package]] name = "ddm-protocol" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/maghemite?rev=65cc70aa5507f779551a1d33c0089bb0a9b7ac72#65cc70aa5507f779551a1d33c0089bb0a9b7ac72" +source = "git+https://github.com/oxidecomputer/maghemite?rev=0509f50053149b5ab87e3e33bf291baab729fe5b#0509f50053149b5ab87e3e33bf291baab729fe5b" dependencies = [ "oxnet", "schemars 0.8.22", @@ -3326,6 +3356,30 @@ dependencies = [ "uuid", ] +[[package]] +name = "dpd-client" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/dendrite?rev=96483d0c52f0cfc4e22e70229466798254a99b6c#96483d0c52f0cfc4e22e70229466798254a99b6c" +dependencies = [ + "async-trait", + "chrono", + "common 0.1.0 (git+https://github.com/oxidecomputer/dendrite?rev=96483d0c52f0cfc4e22e70229466798254a99b6c)", + "crc8", + "futures", + "http", + "oxnet", + "progenitor 0.14.0", + "regress 0.11.1", + "reqwest 0.13.2", + "schemars 0.8.22", + "serde", + "serde_json", + "slog", + "tokio", + "transceiver-controller 0.1.1 (git+https://github.com/oxidecomputer/transceiver-control?branch=main)", + "uuid", +] + [[package]] name = "dpd-client" version = "0.1.0" @@ -3836,7 +3890,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -5392,7 +5446,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.3", + "socket2 0.5.10", "system-configuration", "tokio", "tower-layer", @@ -5601,7 +5655,7 @@ checksum = "a4b8219c9c8c2c844dfd5772ec0bda5cd2a81d78c4579aba97f699721d46ab24" [[package]] name = "illumos-sys-hdrs" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/opte?rev=ab2941cdf2ab067429a4a68ab2389d2960620b57#ab2941cdf2ab067429a4a68ab2389d2960620b57" +source = "git+https://github.com/oxidecomputer/opte?rev=f4433191fba909e99e9b27e9e9700d916b96f38d#f4433191fba909e99e9b27e9e9700d916b96f38d" dependencies = [ "bitflags 2.11.0", ] @@ -6095,7 +6149,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi 0.5.2", "libc", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -6172,7 +6226,7 @@ dependencies = [ "portable-atomic", "portable-atomic-util", "serde_core", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -6327,7 +6381,7 @@ dependencies = [ [[package]] name = "kstat-macro" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/opte?rev=ab2941cdf2ab067429a4a68ab2389d2960620b57#ab2941cdf2ab067429a4a68ab2389d2960620b57" +source = "git+https://github.com/oxidecomputer/opte?rev=f4433191fba909e99e9b27e9e9700d916b96f38d#f4433191fba909e99e9b27e9e9700d916b96f38d" dependencies = [ "quote", "syn 2.0.117", @@ -6877,7 +6931,7 @@ dependencies = [ [[package]] name = "mg-admin-client" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/maghemite?rev=65cc70aa5507f779551a1d33c0089bb0a9b7ac72#65cc70aa5507f779551a1d33c0089bb0a9b7ac72" +source = "git+https://github.com/oxidecomputer/maghemite?rev=0509f50053149b5ab87e3e33bf291baab729fe5b#0509f50053149b5ab87e3e33bf291baab729fe5b" dependencies = [ "chrono", "client-common", @@ -6897,7 +6951,7 @@ dependencies = [ [[package]] name = "mg-api-types" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/maghemite?rev=65cc70aa5507f779551a1d33c0089bb0a9b7ac72#65cc70aa5507f779551a1d33c0089bb0a9b7ac72" +source = "git+https://github.com/oxidecomputer/maghemite?rev=0509f50053149b5ab87e3e33bf291baab729fe5b#0509f50053149b5ab87e3e33bf291baab729fe5b" dependencies = [ "mg-api-types-versions", ] @@ -6905,14 +6959,16 @@ dependencies = [ [[package]] name = "mg-api-types-versions" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/maghemite?rev=65cc70aa5507f779551a1d33c0089bb0a9b7ac72#65cc70aa5507f779551a1d33c0089bb0a9b7ac72" +source = "git+https://github.com/oxidecomputer/maghemite?rev=0509f50053149b5ab87e3e33bf291baab729fe5b#0509f50053149b5ab87e3e33bf291baab729fe5b" dependencies = [ "chrono", + "client-common", "nom 8.0.0", "num_enum 0.7.6", "oxnet", "schemars 0.8.22", "serde", + "serde_json", "slog", "thiserror 2.0.18", "uuid", @@ -8131,7 +8187,7 @@ dependencies = [ "chrono", "crucible-agent-client", "dns-service-client", - "dpd-client 0.1.0 (git+https://github.com/oxidecomputer/dendrite?rev=cc0c307c617f2988aafdca4e3bd35ea178b64801)", + "dpd-client 0.1.0 (git+https://github.com/oxidecomputer/dendrite?rev=96483d0c52f0cfc4e22e70229466798254a99b6c)", "dropshot", "fmd-adm-sys", "futures", @@ -8442,7 +8498,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -9120,7 +9176,7 @@ dependencies = [ "display-error-chain", "dns-server", "dns-service-client", - "dpd-client 0.1.0 (git+https://github.com/oxidecomputer/dendrite?rev=cc0c307c617f2988aafdca4e3bd35ea178b64801)", + "dpd-client 0.1.0 (git+https://github.com/oxidecomputer/dendrite?rev=96483d0c52f0cfc4e22e70229466798254a99b6c)", "dropshot", "ereport-types", "expectorate", @@ -10106,7 +10162,7 @@ dependencies = [ [[package]] name = "opte" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/opte?rev=ab2941cdf2ab067429a4a68ab2389d2960620b57#ab2941cdf2ab067429a4a68ab2389d2960620b57" +source = "git+https://github.com/oxidecomputer/opte?rev=f4433191fba909e99e9b27e9e9700d916b96f38d#f4433191fba909e99e9b27e9e9700d916b96f38d" dependencies = [ "bitflags 2.11.0", "dyn-clone", @@ -10125,7 +10181,7 @@ dependencies = [ [[package]] name = "opte-api" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/opte?rev=ab2941cdf2ab067429a4a68ab2389d2960620b57#ab2941cdf2ab067429a4a68ab2389d2960620b57" +source = "git+https://github.com/oxidecomputer/opte?rev=f4433191fba909e99e9b27e9e9700d916b96f38d#f4433191fba909e99e9b27e9e9700d916b96f38d" dependencies = [ "illumos-sys-hdrs", "ingot", @@ -10138,7 +10194,7 @@ dependencies = [ [[package]] name = "opte-ioctl" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/opte?rev=ab2941cdf2ab067429a4a68ab2389d2960620b57#ab2941cdf2ab067429a4a68ab2389d2960620b57" +source = "git+https://github.com/oxidecomputer/opte?rev=f4433191fba909e99e9b27e9e9700d916b96f38d#f4433191fba909e99e9b27e9e9700d916b96f38d" dependencies = [ "libc", "libnet", @@ -10277,7 +10333,7 @@ dependencies = [ [[package]] name = "oxide-vpc" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/opte?rev=ab2941cdf2ab067429a4a68ab2389d2960620b57#ab2941cdf2ab067429a4a68ab2389d2960620b57" +source = "git+https://github.com/oxidecomputer/opte?rev=f4433191fba909e99e9b27e9e9700d916b96f38d#f4433191fba909e99e9b27e9e9700d916b96f38d" dependencies = [ "cfg-if", "illumos-sys-hdrs", @@ -11791,7 +11847,7 @@ dependencies = [ [[package]] name = "propolis-api-types-versions" version = "0.0.0" -source = "git+https://github.com/oxidecomputer/propolis?rev=979b72896bc39a876c98fb32fe706da37a4fc408#979b72896bc39a876c98fb32fe706da37a4fc408" +source = "git+https://github.com/oxidecomputer/propolis?rev=dcad23754c05c74644c9bb0954ca7ed13dca8381#dcad23754c05c74644c9bb0954ca7ed13dca8381" dependencies = [ "crucible-client-types", "propolis_types", @@ -11804,7 +11860,7 @@ dependencies = [ [[package]] name = "propolis-client" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/propolis?rev=979b72896bc39a876c98fb32fe706da37a4fc408#979b72896bc39a876c98fb32fe706da37a4fc408" +source = "git+https://github.com/oxidecomputer/propolis?rev=dcad23754c05c74644c9bb0954ca7ed13dca8381#dcad23754c05c74644c9bb0954ca7ed13dca8381" dependencies = [ "async-trait", "base64 0.21.7", @@ -11828,7 +11884,7 @@ dependencies = [ [[package]] name = "propolis-mock-server" version = "0.0.0" -source = "git+https://github.com/oxidecomputer/propolis?rev=979b72896bc39a876c98fb32fe706da37a4fc408#979b72896bc39a876c98fb32fe706da37a4fc408" +source = "git+https://github.com/oxidecomputer/propolis?rev=dcad23754c05c74644c9bb0954ca7ed13dca8381#dcad23754c05c74644c9bb0954ca7ed13dca8381" dependencies = [ "anyhow", "atty", @@ -11861,7 +11917,7 @@ dependencies = [ [[package]] name = "propolis_api_types" version = "0.0.0" -source = "git+https://github.com/oxidecomputer/propolis?rev=979b72896bc39a876c98fb32fe706da37a4fc408#979b72896bc39a876c98fb32fe706da37a4fc408" +source = "git+https://github.com/oxidecomputer/propolis?rev=dcad23754c05c74644c9bb0954ca7ed13dca8381#dcad23754c05c74644c9bb0954ca7ed13dca8381" dependencies = [ "crucible-client-types", "propolis-api-types-versions", @@ -11870,7 +11926,7 @@ dependencies = [ [[package]] name = "propolis_types" version = "0.0.0" -source = "git+https://github.com/oxidecomputer/propolis?rev=979b72896bc39a876c98fb32fe706da37a4fc408#979b72896bc39a876c98fb32fe706da37a4fc408" +source = "git+https://github.com/oxidecomputer/propolis?rev=dcad23754c05c74644c9bb0954ca7ed13dca8381#dcad23754c05c74644c9bb0954ca7ed13dca8381" dependencies = [ "schemars 0.8.22", "serde", @@ -11982,7 +12038,7 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls", - "socket2 0.6.3", + "socket2 0.5.10", "thiserror 2.0.18", "tokio", "tracing", @@ -12020,9 +12076,9 @@ dependencies = [ "cfg_aliases 0.2.1", "libc", "once_cell", - "socket2 0.6.3", + "socket2 0.5.10", "tracing", - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] @@ -12948,7 +13004,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.11.0", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -13015,7 +13071,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -14583,7 +14639,7 @@ version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1c97747dbf44bb1ca44a561ece23508e99cb592e862f22222dcf42f51d1e451" dependencies = [ - "heck 0.5.0", + "heck 0.4.1", "proc-macro2", "quote", "syn 2.0.117", @@ -15286,7 +15342,7 @@ dependencies = [ "getrandom 0.4.1", "once_cell", "rustix 1.1.3", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -15306,7 +15362,7 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8c27177b12a6399ffc08b98f76f7c9a1f4fe9fc967c784c5a071fa8d93cf7e1" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -17387,7 +17443,7 @@ name = "wicket-common" version = "0.1.0" dependencies = [ "anyhow", - "dpd-client 0.1.0 (git+https://github.com/oxidecomputer/dendrite?rev=cc0c307c617f2988aafdca4e3bd35ea178b64801)", + "dpd-client 0.1.0 (git+https://github.com/oxidecomputer/dendrite?rev=96483d0c52f0cfc4e22e70229466798254a99b6c)", "dropshot", "gateway-client", "gateway-types", @@ -17455,7 +17511,7 @@ dependencies = [ "clap", "debug-ignore", "display-error-chain", - "dpd-client 0.1.0 (git+https://github.com/oxidecomputer/dendrite?rev=cc0c307c617f2988aafdca4e3bd35ea178b64801)", + "dpd-client 0.1.0 (git+https://github.com/oxidecomputer/dendrite?rev=96483d0c52f0cfc4e22e70229466798254a99b6c)", "dropshot", "either", "expectorate", @@ -17635,7 +17691,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.48.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index ce3774b626a..c275ab5a77b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -428,7 +428,7 @@ cast_lossless = "warn" [workspace.dependencies] anyhow = "1.0" -anstyle = "1.0.11" +anstyle = "1.0.14" api_identity = { path = "api_identity" } approx = "0.5.1" assert_matches = "1.5.0" @@ -512,7 +512,7 @@ digest = "0.10.7" dns-server = { path = "dns-server" } dns-server-api = { path = "dns-server-api" } dns-service-client = { path = "clients/dns-service-client" } -dpd-client = { git = "https://github.com/oxidecomputer/dendrite", rev = "cc0c307c617f2988aafdca4e3bd35ea178b64801" } +dpd-client = { git = "https://github.com/oxidecomputer/dendrite", rev = "96483d0c52f0cfc4e22e70229466798254a99b6c" } dropshot = { version = "0.17.1", features = [ "usdt-probes" ] } dropshot-api-manager = "0.7.2" dropshot-api-manager-types = "0.7.2" @@ -622,8 +622,8 @@ ntp-admin-api = { path = "ntp-admin/api" } ntp-admin-client = { path = "clients/ntp-admin-client" } ntp-admin-types = { path = "ntp-admin/types" } ntp-admin-types-versions = { path = "ntp-admin/types/versions" } -mg-admin-client = { git = "https://github.com/oxidecomputer/maghemite", rev = "65cc70aa5507f779551a1d33c0089bb0a9b7ac72" } -ddm-admin-client = { git = "https://github.com/oxidecomputer/maghemite", rev = "65cc70aa5507f779551a1d33c0089bb0a9b7ac72" } +mg-admin-client = { git = "https://github.com/oxidecomputer/maghemite", rev = "0509f50053149b5ab87e3e33bf291baab729fe5b" } +ddm-admin-client = { git = "https://github.com/oxidecomputer/maghemite", rev = "0509f50053149b5ab87e3e33bf291baab729fe5b" } multimap = "0.10.1" nexus-auth = { path = "nexus/auth" } nexus-background-task-interface = { path = "nexus/background-task-interface" } @@ -691,16 +691,16 @@ oxide-tokio-rt = "0.1.4" oxide-update-engine = "0.1.2" oxide-update-engine-types = { version = "0.1.2", features = ["schemars08"] } oxide-update-engine-display = "0.1.2" -oxide-vpc = { git = "https://github.com/oxidecomputer/opte", rev = "ab2941cdf2ab067429a4a68ab2389d2960620b57", features = [ "api", "std" ] } +oxide-vpc = { git = "https://github.com/oxidecomputer/opte", rev = "f4433191fba909e99e9b27e9e9700d916b96f38d", features = [ "api", "std" ] } oxlog = { path = "dev-tools/oxlog" } -oxnet = "0.1.5" +oxnet = "0.1.6" once_cell = "1.21.3" openapi-lint = { git = "https://github.com/oxidecomputer/openapi-lint", branch = "main" } openapiv3 = "2.2.0" # must match samael's crate! openssl = "0.10" openssl-sys = "0.9" -opte-ioctl = { git = "https://github.com/oxidecomputer/opte", rev = "ab2941cdf2ab067429a4a68ab2389d2960620b57" } +opte-ioctl = { git = "https://github.com/oxidecomputer/opte", rev = "f4433191fba909e99e9b27e9e9700d916b96f38d" } oso = "0.27" owo-colors = "4.2.2" oximeter = { path = "oximeter/oximeter" } @@ -745,17 +745,17 @@ progenitor-extras = "0.2.0" # NOTE: if you change the pinned revision of the `bhyve_api` and propolis # dependencies, you must also update the references in package-manifest.toml to # match the new revision. -bhyve_api = { git = "https://github.com/oxidecomputer/propolis", rev = "979b72896bc39a876c98fb32fe706da37a4fc408" } -propolis-api-types-versions = { git = "https://github.com/oxidecomputer/propolis", rev = "979b72896bc39a876c98fb32fe706da37a4fc408" } -propolis_api_types = { git = "https://github.com/oxidecomputer/propolis", rev = "979b72896bc39a876c98fb32fe706da37a4fc408" } -propolis-client = { git = "https://github.com/oxidecomputer/propolis", rev = "979b72896bc39a876c98fb32fe706da37a4fc408" } -propolis-mock-server = { git = "https://github.com/oxidecomputer/propolis", rev = "979b72896bc39a876c98fb32fe706da37a4fc408" } +bhyve_api = { git = "https://github.com/oxidecomputer/propolis", rev = "dcad23754c05c74644c9bb0954ca7ed13dca8381" } +propolis-api-types-versions = { git = "https://github.com/oxidecomputer/propolis", rev = "dcad23754c05c74644c9bb0954ca7ed13dca8381" } +propolis_api_types = { git = "https://github.com/oxidecomputer/propolis", rev = "dcad23754c05c74644c9bb0954ca7ed13dca8381" } +propolis-client = { git = "https://github.com/oxidecomputer/propolis", rev = "dcad23754c05c74644c9bb0954ca7ed13dca8381" } +propolis-mock-server = { git = "https://github.com/oxidecomputer/propolis", rev = "dcad23754c05c74644c9bb0954ca7ed13dca8381" } # NOTE: see above! proptest = "1.7.0" qorb = "0.4.1" quote = "1.0" # Some dependencies still require rand 0.8.x. -rand08 = { package = "rand", version = "0.8.6" } +rand08 = { package = "rand", version = "0.8.5" } rand = "0.9.2" rand_core = "0.9.3" rand_distr = "0.5.1" @@ -766,7 +766,7 @@ rats-corim = { git = "https://github.com/oxidecomputer/rats-corim.git", rev = "f raw-cpuid = { git = "https://github.com/oxidecomputer/rust-cpuid.git", rev = "a4cf01df76f35430ff5d39dc2fe470bcb953503b" } rayon = "1.10" rcgen = { version = "0.12.1", default-features = false, features = ["aws_lc_rs", "pem"] } -mg-api-types = { git = "https://github.com/oxidecomputer/maghemite", rev = "65cc70aa5507f779551a1d33c0089bb0a9b7ac72" } +mg-api-types = { git = "https://github.com/oxidecomputer/maghemite", rev = "0509f50053149b5ab87e3e33bf291baab729fe5b" } reconfigurator-cli = { path = "dev-tools/reconfigurator-cli" } reedline = "0.40.0" ref-cast = "1.0" @@ -1153,3 +1153,17 @@ path = "workspace-hack" [patch."https://github.com/oxidecomputer/omicron"] omicron-uuid-kinds = { path = "uuid-kinds" } omicron-common = { path = "common" } +# oxlog is built as a binary by omicron-package. Without this patch, +# transitive git-pulls of oxlog (illumos-utils -> oxlog, via maghemite's +# branch=main omicron deps) leave both local and git copies in the dep graph, +# and cargo's `--package oxlog` fails as ambiguous. +# +# TODO: This patch is a symptom of the broader maghemite -> omicron +# self-reference loop (maghemite depends on oximeter-producer, which pulls +# nexus-client -> nexus-types from omicron@main and not a rev). @jgallagher +# flagged this in review; @bnaecker suggested extracting oximeter-producer +# out of omicron to break it. When that lands, this patch (and any +# duplicated path+git entries for illumos-utils, nexus-types, etc.) can be +# removed. +oxlog = { path = "dev-tools/oxlog" } + diff --git a/dev-tools/ls-apis/api-manifest.toml b/dev-tools/ls-apis/api-manifest.toml index d40b5177d40..2059231fb57 100644 --- a/dev-tools/ls-apis/api-manifest.toml +++ b/dev-tools/ls-apis/api-manifest.toml @@ -638,6 +638,15 @@ Sled Agent uses the Crucible Agent client types only, and only in the simulated sled agent. """ +[[dependency_filter_rules]] +ancestor = "omicron-sled-agent" +client = "crucible-pantry-client" +evaluation = "bogus" +note = """ +Sled Agent uses the Crucible Pantry client types only, and only in the simulated +sled agent. +""" + ################################################################################ # Intra-deployment-unit-only edges # diff --git a/dev-tools/ls-apis/tests/api_dependencies.out b/dev-tools/ls-apis/tests/api_dependencies.out index e0db8fcd1cd..95f6ca7627d 100644 --- a/dev-tools/ls-apis/tests/api_dependencies.out +++ b/dev-tools/ls-apis/tests/api_dependencies.out @@ -38,9 +38,9 @@ DNS Server (client: dns-service-client) consumed by: sled-agent-rack-setup (omicron/sled-agent/rack-setup) via 1 path [embedded in omicron-sled-agent; rack-init only] Dendrite DPD (client: dpd-client) - consumed by: ddmd (maghemite/ddmd) via 2 paths + consumed by: ddmd (maghemite/ddmd) via 3 paths consumed by: lldpd (lldp/lldpd) via 2 paths - consumed by: mgd (maghemite/mgd) via 1 path + consumed by: mgd (maghemite/mgd) via 2 paths consumed by: omicron-nexus (omicron/nexus) via 2 paths consumed by: omicron-sled-agent (omicron/sled-agent) via 2 paths consumed by: tfportd (dendrite/tfportd) via 2 paths diff --git a/dev-tools/omdb/Cargo.toml b/dev-tools/omdb/Cargo.toml index b8f44818fcf..86157d91da0 100644 --- a/dev-tools/omdb/Cargo.toml +++ b/dev-tools/omdb/Cargo.toml @@ -4,6 +4,10 @@ version = "0.1.0" edition.workspace = true license = "MPL-2.0" +[features] +default = ["multicast"] +multicast = [] + [lints] workspace = true diff --git a/dev-tools/omdb/src/bin/omdb/db.rs b/dev-tools/omdb/src/bin/omdb/db.rs index 1ff2acfa806..1bf4bf4d457 100644 --- a/dev-tools/omdb/src/bin/omdb/db.rs +++ b/dev-tools/omdb/src/bin/omdb/db.rs @@ -190,6 +190,7 @@ mod alert; mod blueprints; mod db_metadata; mod ereport; +mod multicast; mod saga; mod sitrep; mod target_release; @@ -419,6 +420,8 @@ enum DbCommands { /// Print information about migrations #[clap(alias = "migration")] Migrations(MigrationsArgs), + /// Print information about multicast groups + Multicast(multicast::MulticastArgs), /// Print information about snapshots Snapshots(SnapshotArgs), /// Validate the contents of the database @@ -1509,6 +1512,33 @@ impl DbArgs { }) => { cmd_db_migrations_list(&datastore, &fetch_opts, args).await } + DbCommands::Multicast(multicast::MulticastArgs { + command: multicast::MulticastCommands::Groups(args), + }) => { + multicast::cmd_db_multicast_groups( + &datastore, &fetch_opts, &args, + ) + .await + } + DbCommands::Multicast(multicast::MulticastArgs { + command: multicast::MulticastCommands::Members(args), + }) => { + multicast::cmd_db_multicast_members(&datastore, &fetch_opts, &args) + .await + } + DbCommands::Multicast(multicast::MulticastArgs { + command: multicast::MulticastCommands::Pools, + }) => { + multicast::cmd_db_multicast_pools(&datastore, &fetch_opts).await + } + DbCommands::Multicast(multicast::MulticastArgs { + command: multicast::MulticastCommands::Info(args), + }) => { + multicast::cmd_db_multicast_info( + &datastore, &fetch_opts, &args, + ) + .await + } DbCommands::Snapshots(SnapshotArgs { command: SnapshotCommands::Info(uuid), }) => cmd_db_snapshot_info(&opctx, &datastore, uuid).await, diff --git a/dev-tools/omdb/src/bin/omdb/db/multicast.rs b/dev-tools/omdb/src/bin/omdb/db/multicast.rs new file mode 100644 index 00000000000..68118b2ca80 --- /dev/null +++ b/dev-tools/omdb/src/bin/omdb/db/multicast.rs @@ -0,0 +1,940 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! `omdb db multicast` subcommands +//! +//! # Command Outputs +//! +//! ## `omdb db multicast pools` +//! +//! | Column | Description | +//! |---------------|----------------------------| +//! | POOL_ID | Pool UUID | +//! | POOL_NAME | Pool name | +//! | FIRST_ADDRESS | Range start IP | +//! | LAST_ADDRESS | Range end IP | +//! | CREATED | Creation timestamp | +//! +//! ## `omdb db multicast groups` +//! +//! | Column | Description | +//! |--------------|--------------------------------------| +//! | ID | Group UUID | +//! | NAME | Group name | +//! | STATE | Group state ("Active"/"Creating") | +//! | MULTICAST_IP | Allocated multicast IP | +//! | RANGE | ASM or SSM based on IP range | +//! | UNDERLAY_IP | Underlay group IP (blank if none) | +//! | SOURCES | Source allowlist, or "-" (any) | +//! | MEMBERS | Comma-separated "instance@sled" list | +//! | VNI | Virtual network ID | +//! | CREATED | Creation timestamp | +//! +//! Note: SOURCES "-" also appears when a group has no members; use MEMBERS +//! column or `info` command to distinguish from any-source members. +//! +//! Filters: `--state`, `--pool` +//! +//! ## `omdb db multicast members` +//! +//! | Column | Description | +//! |--------------|------------------------------------------| +//! | ID | Member UUID | +//! | GROUP_NAME | Parent group name | +//! | PARENT_ID | Instance UUID | +//! | STATE | Member state ("Joining"/"Joined"/"Left") | +//! | MULTICAST_IP | Group multicast IP | +//! | SOURCES | Source allowlist, or "-" (any) | +//! | SLED_ID | Assigned sled UUID (blank if none) | +//! | CREATED | Creation timestamp | +//! +//! Filters: `--group-id`, `--group-ip`, `--group-name`, `--state`, +//! `--sled-id`, `--source-ip` +//! +//! ## `omdb db multicast info` +//! +//! Detailed view of a single group with sections: +//! +//! **MULTICAST GROUP** +//! - id, name, state, multicast_ip, vni, source_ips (allowlist or "-") +//! - ip_pool (name + ID), underlay_group, tag, created +//! +//! **UNDERLAY GROUP** (if present) +//! - id, multicast_ip, tag, created +//! +//! **MEMBERS** (table) +//! +//! | Column | Description | +//! |--------------|--------------------------------| +//! | ID | Member UUID | +//! | INSTANCE | Instance name | +//! | STATE | Member state | +//! | MULTICAST_IP | Group multicast IP | +//! | SOURCES | Allowlist, or "-" (any) | +//! | SLED | Sled serial number, or "-" | +//! | CREATED | Creation timestamp | +//! +//! Lookup: `--group-id`, `--ip`, `--name` (exactly one required) + +use std::collections::{BTreeSet, HashMap}; + +use anyhow::Context; +use async_bb8_diesel::AsyncRunQueryDsl; +use chrono::{DateTime, Utc}; +use clap::{Args, Subcommand, ValueEnum}; +use diesel::prelude::*; +use tabled::Tabled; +use uuid::Uuid; + +use nexus_db_model::{ + ExternalMulticastGroup, IpPool, IpPoolRange, IpPoolType, + MulticastGroupMember, MulticastGroupMemberState, MulticastGroupState, + UnderlayMulticastGroup, +}; +use nexus_db_queries::db::DataStore; +use nexus_types::identity::Resource; +use omicron_common::address::is_ssm_address; +use omicron_uuid_kinds::{GenericUuid, SledUuid}; + +/// CLI wrapper for MulticastGroupState to support clap ValueEnum +#[derive(Debug, Clone, Copy, ValueEnum)] +pub enum CliGroupState { + Creating, + Active, + Deleting, + Deleted, +} + +impl From for MulticastGroupState { + fn from(cli: CliGroupState) -> Self { + match cli { + CliGroupState::Creating => MulticastGroupState::Creating, + CliGroupState::Active => MulticastGroupState::Active, + CliGroupState::Deleting => MulticastGroupState::Deleting, + CliGroupState::Deleted => MulticastGroupState::Deleted, + } + } +} + +/// CLI wrapper for MulticastGroupMemberState to support clap ValueEnum +#[derive(Debug, Clone, Copy, ValueEnum)] +pub enum CliMemberState { + Joining, + Joined, + Left, +} + +impl From for MulticastGroupMemberState { + fn from(cli: CliMemberState) -> Self { + match cli { + CliMemberState::Joining => MulticastGroupMemberState::Joining, + CliMemberState::Joined => MulticastGroupMemberState::Joined, + CliMemberState::Left => MulticastGroupMemberState::Left, + } + } +} + +use crate::db::{DbFetchOptions, check_limit}; +use crate::helpers::{datetime_rfc3339_concise, display_option_blank}; + +// Display labels for multicast address range classification +const RANGE_SSM: &str = "SSM"; +const RANGE_ASM: &str = "ASM"; + +/// `omdb db multicast` subcommand +#[derive(Debug, Args, Clone)] +pub(super) struct MulticastArgs { + #[command(subcommand)] + pub command: MulticastCommands, +} + +#[derive(Debug, Subcommand, Clone)] +pub(super) enum MulticastCommands { + /// List all multicast groups. + /// + /// Shows ID, name, state, multicast IP, address range type (ASM/SSM), + /// underlay IP, source IPs union, VNI, and creation time. + #[clap(alias = "ls")] + Groups(MulticastGroupsArgs), + + /// List all multicast group members. + /// + /// Shows member ID, group name, parent instance ID, state, multicast IP, + /// source IPs, sled ID, and creation time. + Members(MulticastMembersArgs), + + /// List multicast IP pools and their ranges. + /// + /// Shows pool ID, name, first/last addresses, and creation time. + Pools, + + /// Get detailed info for a multicast group. + /// + /// Shows group details, associated underlay group, and all members. + #[clap(alias = "show")] + Info(MulticastInfoArgs), +} + +#[derive(Debug, Args, Clone)] +pub(super) struct MulticastGroupsArgs { + /// Filter by state + #[arg(long, ignore_case = true, value_enum)] + state: Option, + /// Filter by pool name + #[arg(long)] + pool: Option, +} + +#[derive(Debug, Args, Clone)] +pub(super) struct MulticastMembersArgs { + /// Filter by group ID + #[arg(long)] + group_id: Option, + /// Filter by group IP address (e.g., 239.1.2.3) + #[arg(long)] + group_ip: Option, + /// Filter by group name + #[arg(long)] + group_name: Option, + /// Filter by state + #[arg(long, ignore_case = true, value_enum)] + state: Option, + /// Filter by sled ID + #[arg(long)] + sled_id: Option, + /// Filter by source IP (members subscribed to this source) + #[arg(long)] + source_ip: Option, +} + +#[derive(Debug, Args, Clone)] +#[group(required = true, multiple = false)] +pub(super) struct MulticastInfoArgs { + /// Multicast group ID + #[arg(long)] + group_id: Option, + /// Multicast IP address (e.g., 239.1.2.3) + #[arg(long)] + ip: Option, + /// Multicast group name + #[arg(long)] + name: Option, +} + +#[derive(Tabled)] +#[tabled(rename_all = "SCREAMING_SNAKE_CASE")] +struct MulticastGroupRow { + id: Uuid, + name: String, + state: MulticastGroupState, + multicast_ip: std::net::IpAddr, + /// ASM (any-source) or SSM (source-specific) based on IP range + range: &'static str, + #[tabled(display_with = "display_option_blank")] + underlay_ip: Option, + /// Source IPs union from members ("-" = any source) + sources: String, + /// Members formatted as "inst_1@sled, ..., inst_n@sled" + members: String, + vni: u32, + #[tabled(display_with = "datetime_rfc3339_concise")] + created: DateTime, +} + +#[derive(Tabled)] +#[tabled(rename_all = "SCREAMING_SNAKE_CASE")] +struct MulticastMemberRow { + id: Uuid, + group_name: String, + parent_id: Uuid, + state: MulticastGroupMemberState, + multicast_ip: std::net::IpAddr, + /// Source IPs for source filtering ("-" = any source) + sources: String, + #[tabled(display_with = "display_option_blank")] + sled_id: Option, + #[tabled(display_with = "datetime_rfc3339_concise")] + created: DateTime, +} + +#[derive(Tabled)] +#[tabled(rename_all = "SCREAMING_SNAKE_CASE")] +struct MulticastInfoMemberRow { + id: Uuid, + instance: String, + state: MulticastGroupMemberState, + multicast_ip: std::net::IpAddr, + /// Source IPs for source filtering ("-" = any source) + sources: String, + sled: String, + #[tabled(display_with = "datetime_rfc3339_concise")] + created: DateTime, +} + +// Build output combining pools and ranges +#[derive(Tabled)] +#[tabled(rename_all = "SCREAMING_SNAKE_CASE")] +struct MulticastPoolRow { + pool_id: Uuid, + pool_name: String, + first_address: std::net::IpAddr, + last_address: std::net::IpAddr, + #[tabled(display_with = "datetime_rfc3339_concise")] + created: chrono::DateTime, +} + +pub(super) async fn cmd_db_multicast_groups( + datastore: &DataStore, + fetch_opts: &DbFetchOptions, + args: &MulticastGroupsArgs, +) -> Result<(), anyhow::Error> { + use nexus_db_schema::schema::instance::dsl as instance_dsl; + use nexus_db_schema::schema::ip_pool::dsl as pool_dsl; + use nexus_db_schema::schema::multicast_group::dsl; + use nexus_db_schema::schema::multicast_group_member::dsl as member_dsl; + use nexus_db_schema::schema::sled::dsl as sled_dsl; + use nexus_db_schema::schema::underlay_multicast_group::dsl as underlay_dsl; + + let conn = datastore.pool_connection_for_tests().await?; + + let mut query = dsl::multicast_group.into_boxed(); + if !fetch_opts.include_deleted { + query = query.filter(dsl::time_deleted.is_null()); + } + if let Some(state) = args.state.map(MulticastGroupState::from) { + query = query.filter(dsl::state.eq(state)); + } + if let Some(ref pool_name) = args.pool { + let pool_id: Uuid = pool_dsl::ip_pool + .filter(pool_dsl::name.eq(pool_name.clone())) + .filter(pool_dsl::time_deleted.is_null()) + .select(pool_dsl::id) + .first_async(&*conn) + .await + .with_context(|| { + format!("no pool found with name '{pool_name}'") + })?; + query = query.filter(dsl::ip_pool_id.eq(pool_id)); + } + + let groups: Vec = query + .order_by(dsl::time_created.desc()) + .limit(i64::from(u32::from(fetch_opts.fetch_limit))) + .select(ExternalMulticastGroup::as_select()) + .get_results_async(&*conn) + .await?; + + check_limit(&groups, fetch_opts.fetch_limit, || { + String::from("listing multicast groups") + }); + + // Batch lookup underlay IPs for groups + let underlay_ids: Vec = + groups.iter().filter_map(|group| group.underlay_group_id).collect(); + let underlay_map: HashMap = + if underlay_ids.is_empty() { + HashMap::new() + } else { + underlay_dsl::underlay_multicast_group + .filter(underlay_dsl::id.eq_any(underlay_ids)) + .select((underlay_dsl::id, underlay_dsl::multicast_ip)) + .get_results_async::<(Uuid, ipnetwork::IpNetwork)>(&*conn) + .await + .context("fetching underlay groups")? + .into_iter() + .collect() + }; + + // Fetch members for all groups with deterministic ordering + let group_ids: Vec = groups.iter().map(|g| g.identity.id).collect(); + let members: Vec = if group_ids.is_empty() { + Vec::new() + } else { + let mut mq = member_dsl::multicast_group_member + .filter(member_dsl::external_group_id.eq_any(group_ids)) + .into_boxed(); + if !fetch_opts.include_deleted { + mq = mq.filter(member_dsl::time_deleted.is_null()); + } + mq.order_by(member_dsl::external_group_id.asc()) + .then_order_by(member_dsl::parent_id.asc()) + .then_order_by(member_dsl::id.asc()) + .select(MulticastGroupMember::as_select()) + .get_results_async(&*conn) + .await + .context("fetching multicast group members")? + }; + + // Derive effective source filtering state from members for each group. + // If any member has empty source_ips, the group effectively allows any source + // (filter disabled). Otherwise, show the union of all member source IPs + // (filter enabled with allowlist). None = any-source, Some(set) = filtered. + let source_filter_map: HashMap>> = { + let mut map: HashMap>> = + HashMap::new(); + for member in &members { + if member.source_ips.is_empty() { + // Any member with empty sources means any-source allowed + map.insert(member.external_group_id, None); + } else { + map.entry(member.external_group_id) + .and_modify(|v| { + if let Some(set) = v { + for ip in &member.source_ips { + set.insert(ip.ip()); + } + } + // If already None (any-source), leave it as None + }) + .or_insert_with(|| { + Some( + member + .source_ips + .iter() + .map(|ip| ip.ip()) + .collect(), + ) + }); + } + } + map + }; + + // Batch lookup instance names + let parent_ids: Vec = members.iter().map(|m| m.parent_id).collect(); + let instance_names: HashMap = if parent_ids.is_empty() { + HashMap::new() + } else { + instance_dsl::instance + .filter(instance_dsl::id.eq_any(parent_ids)) + .select((instance_dsl::id, instance_dsl::name)) + .get_results_async::<(Uuid, String)>(&*conn) + .await + .context("fetching instance names")? + .into_iter() + .collect() + }; + + // Batch lookup sled serials + let sled_ids: Vec = members + .iter() + .filter_map(|m| m.sled_id.map(|s| s.into_untyped_uuid())) + .collect(); + let sled_serials: HashMap = if sled_ids.is_empty() { + HashMap::new() + } else { + sled_dsl::sled + .filter(sled_dsl::id.eq_any(sled_ids)) + .select((sled_dsl::id, sled_dsl::serial_number)) + .get_results_async::<(Uuid, String)>(&*conn) + .await + .context("fetching sled serials")? + .into_iter() + .collect() + }; + + // Build group_id -> formatted members string + let mut members_map: HashMap> = HashMap::new(); + for member in &members { + let inst_name = instance_names + .get(&member.parent_id) + .cloned() + .unwrap_or_else(|| member.parent_id.to_string()); + let sled_serial = member + .sled_id + .and_then(|s| sled_serials.get(&s.into_untyped_uuid()).cloned()) + .unwrap_or_else(|| "-".to_string()); + let formatted = format!("{inst_name}@{sled_serial}"); + members_map + .entry(member.external_group_id) + .or_default() + .push(formatted); + } + + // Sort each group's members for deterministic output + for v in members_map.values_mut() { + v.sort_unstable(); + } + + let rows: Vec = groups + .into_iter() + .map(|group| { + let mcast_ip = group.multicast_ip.ip(); + let range = + if is_ssm_address(mcast_ip) { RANGE_SSM } else { RANGE_ASM }; + // Format effective source filter state (derived from members) + let sources = source_filter_map + .get(&group.identity.id) + .and_then(|opt| opt.as_ref()) + .filter(|set| !set.is_empty()) + .map(|set| { + set.iter() + .map(|ip| ip.to_string()) + .collect::>() + .join(",") + }) + .unwrap_or_else(|| "-".to_string()); + let underlay_ip = group + .underlay_group_id + .and_then(|id| underlay_map.get(&id)) + .map(|ip| ip.ip()); + let members = members_map + .get(&group.identity.id) + .map(|v| v.join(", ")) + .unwrap_or_else(|| "-".to_string()); + MulticastGroupRow { + id: group.identity.id, + name: group.identity.name.to_string(), + state: group.state, + multicast_ip: mcast_ip, + range, + underlay_ip, + sources, + members, + vni: u32::from(group.vni.0), + created: group.identity.time_created, + } + }) + .collect(); + + let table = tabled::Table::new(rows) + .with(tabled::settings::Style::empty()) + .with(tabled::settings::Padding::new(0, 1, 0, 0)) + .to_string(); + + println!("{table}"); + + Ok(()) +} + +pub(super) async fn cmd_db_multicast_members( + datastore: &DataStore, + fetch_opts: &DbFetchOptions, + args: &MulticastMembersArgs, +) -> Result<(), anyhow::Error> { + use nexus_db_schema::schema::multicast_group::dsl as group_dsl; + use nexus_db_schema::schema::multicast_group_member::dsl; + + let conn = datastore.pool_connection_for_tests().await?; + + // Resolve group_ip or group_name to a group_id if specified + let resolved_group_id = match (&args.group_ip, &args.group_name) { + (Some(ip), _) => { + let group: ExternalMulticastGroup = group_dsl::multicast_group + .filter(group_dsl::time_deleted.is_null()) + .filter( + group_dsl::multicast_ip.eq(ipnetwork::IpNetwork::from(*ip)), + ) + .select(ExternalMulticastGroup::as_select()) + .first_async(&*conn) + .await + .with_context(|| format!("no multicast group with IP {ip}"))?; + Some(group.id()) + } + (None, Some(name)) => { + let group: ExternalMulticastGroup = group_dsl::multicast_group + .filter(group_dsl::time_deleted.is_null()) + .filter(group_dsl::name.eq(name.clone())) + .select(ExternalMulticastGroup::as_select()) + .first_async(&*conn) + .await + .with_context(|| { + format!("no multicast group with name '{name}'") + })?; + Some(group.id()) + } + (None, None) => args.group_id, + }; + + let mut query = dsl::multicast_group_member.into_boxed(); + if !fetch_opts.include_deleted { + query = query.filter(dsl::time_deleted.is_null()); + } + if let Some(group_id) = resolved_group_id { + query = query.filter(dsl::external_group_id.eq(group_id)); + } + if let Some(state) = args.state.map(MulticastGroupMemberState::from) { + query = query.filter(dsl::state.eq(state)); + } + if let Some(sled_id) = args.sled_id { + query = query.filter(dsl::sled_id.eq(sled_id.into_untyped_uuid())); + } + if let Some(source_ip) = args.source_ip { + let ip_network = ipnetwork::IpNetwork::from(source_ip); + query = query.filter(dsl::source_ips.contains(vec![ip_network])); + } + + let members: Vec = query + .order_by(dsl::time_created.desc()) + .limit(i64::from(u32::from(fetch_opts.fetch_limit))) + .select(MulticastGroupMember::as_select()) + .get_results_async(&*conn) + .await?; + + check_limit(&members, fetch_opts.fetch_limit, || { + String::from("listing multicast group members") + }); + + // Batch lookup group names + let group_ids: Vec = + members.iter().map(|member| member.external_group_id).collect(); + let group_names: HashMap = if group_ids.is_empty() { + HashMap::new() + } else { + group_dsl::multicast_group + .filter(group_dsl::id.eq_any(group_ids)) + .select((group_dsl::id, group_dsl::name)) + .get_results_async::<(Uuid, String)>(&*conn) + .await + .context("fetching group names")? + .into_iter() + .collect() + }; + + let rows: Vec = members + .into_iter() + .map(|member| { + let group_name = group_names + .get(&member.external_group_id) + .cloned() + .unwrap_or_else(|| member.external_group_id.to_string()); + let sources = Some(&member.source_ips) + .filter(|ips| !ips.is_empty()) + .map(|ips| { + ips.iter() + .map(|ip| ip.ip().to_string()) + .collect::>() + .join(",") + }) + .unwrap_or_else(|| "-".to_string()); + MulticastMemberRow { + id: member.id, + group_name, + parent_id: member.parent_id, + state: member.state, + multicast_ip: member.multicast_ip.ip(), + sources, + sled_id: member.sled_id.map(SledUuid::from), + created: member.time_created, + } + }) + .collect(); + + let table = tabled::Table::new(rows) + .with(tabled::settings::Style::empty()) + .with(tabled::settings::Padding::new(0, 1, 0, 0)) + .to_string(); + + println!("{table}"); + + Ok(()) +} + +pub(super) async fn cmd_db_multicast_pools( + datastore: &DataStore, + fetch_opts: &DbFetchOptions, +) -> Result<(), anyhow::Error> { + use nexus_db_schema::schema::ip_pool::dsl as pool_dsl; + use nexus_db_schema::schema::ip_pool_range::dsl as range_dsl; + + let conn = datastore.pool_connection_for_tests().await?; + + // Get multicast pools + let mut query = pool_dsl::ip_pool.into_boxed(); + query = query.filter(pool_dsl::pool_type.eq(IpPoolType::Multicast)); + if !fetch_opts.include_deleted { + query = query.filter(pool_dsl::time_deleted.is_null()); + } + + let pools: Vec = query + .order_by(pool_dsl::time_created.desc()) + .limit(i64::from(u32::from(fetch_opts.fetch_limit))) + .select(IpPool::as_select()) + .get_results_async(&*conn) + .await?; + + check_limit(&pools, fetch_opts.fetch_limit, || { + String::from("listing multicast pools") + }); + + if pools.is_empty() { + println!("no multicast IP pools found"); + return Ok(()); + } + + // Get ranges for each pool + let pool_ids: Vec = pools.iter().map(|pool| pool.id()).collect(); + + let mut range_query = range_dsl::ip_pool_range.into_boxed(); + range_query = + range_query.filter(range_dsl::ip_pool_id.eq_any(pool_ids.clone())); + if !fetch_opts.include_deleted { + range_query = range_query.filter(range_dsl::time_deleted.is_null()); + } + + let ranges: Vec = range_query + .order_by(range_dsl::first_address) + .select(IpPoolRange::as_select()) + .get_results_async(&*conn) + .await?; + + let pool_map: HashMap = + pools.iter().map(|pool| (pool.id(), pool)).collect(); + + let rows: Vec = ranges + .into_iter() + .filter_map(|range| { + pool_map.get(&range.ip_pool_id).map(|pool| MulticastPoolRow { + pool_id: pool.id(), + pool_name: pool.name().to_string(), + first_address: range.first_address.ip(), + last_address: range.last_address.ip(), + created: range.time_created, + }) + }) + .collect(); + + if rows.is_empty() { + println!("no multicast IP pool ranges found"); + return Ok(()); + } + + let table = tabled::Table::new(rows) + .with(tabled::settings::Style::empty()) + .with(tabled::settings::Padding::new(0, 1, 0, 0)) + .to_string(); + + println!("{table}"); + + Ok(()) +} + +pub(super) async fn cmd_db_multicast_info( + datastore: &DataStore, + fetch_opts: &DbFetchOptions, + args: &MulticastInfoArgs, +) -> Result<(), anyhow::Error> { + use nexus_db_schema::schema::instance::dsl as instance_dsl; + use nexus_db_schema::schema::ip_pool::dsl as pool_dsl; + use nexus_db_schema::schema::multicast_group::dsl as group_dsl; + use nexus_db_schema::schema::multicast_group_member::dsl as member_dsl; + use nexus_db_schema::schema::sled::dsl as sled_dsl; + use nexus_db_schema::schema::underlay_multicast_group::dsl as underlay_dsl; + + let conn = datastore.pool_connection_for_tests().await?; + + // Find the group by ID, IP, or name, pairing filter with error message + let (mut query, not_found_msg) = + match (&args.group_id, &args.ip, &args.name) { + (Some(id), _, _) => ( + group_dsl::multicast_group + .filter(group_dsl::id.eq(*id)) + .into_boxed(), + format!("no multicast group found with ID {id}"), + ), + (None, Some(ip), _) => ( + group_dsl::multicast_group + .filter( + group_dsl::multicast_ip + .eq(ipnetwork::IpNetwork::from(*ip)), + ) + .into_boxed(), + format!("no multicast group found with IP {ip}"), + ), + (None, None, Some(name)) => ( + group_dsl::multicast_group + .filter(group_dsl::name.eq(name.clone())) + .into_boxed(), + format!("no multicast group found with name \"{name}\""), + ), + (None, None, None) => { + anyhow::bail!("must specify --group-id, --ip, or --name") + } + }; + + if !fetch_opts.include_deleted { + query = query.filter(group_dsl::time_deleted.is_null()); + } + + // Fetch group with underlay in single query using LEFT JOIN + let result: Option<( + ExternalMulticastGroup, + Option, + )> = + query + .left_join(underlay_dsl::underlay_multicast_group.on( + underlay_dsl::id.nullable().eq(group_dsl::underlay_group_id), + )) + .select(( + ExternalMulticastGroup::as_select(), + Option::::as_select(), + )) + .first_async(&*conn) + .await + .optional()?; + + let (group, underlay) = match result { + Some((grp, ulay)) => (grp, ulay), + None => { + println!("{not_found_msg}"); + return Ok(()); + } + }; + + // Look up the pool name + let pool_name: String = match pool_dsl::ip_pool + .filter(pool_dsl::id.eq(group.ip_pool_id)) + .select(pool_dsl::name) + .first_async(&*conn) + .await + .optional() + .context("fetching pool name")? + { + Some(name) => name, + None => { + eprintln!("warning: no pool found for id {}", group.ip_pool_id); + "".into() + } + }; + + // Find members for this group (fetch early to derive source_ips) + let mut member_query = member_dsl::multicast_group_member.into_boxed(); + member_query = member_query + .filter(member_dsl::external_group_id.eq(group.identity.id)); + if !fetch_opts.include_deleted { + member_query = member_query.filter(member_dsl::time_deleted.is_null()); + } + + let members: Vec = member_query + .order_by(member_dsl::time_created.desc()) + .select(MulticastGroupMember::as_select()) + .get_results_async(&*conn) + .await?; + + // Derive effective source filter state from members. + // If any member has empty source_ips, the group allows any source ("-"). + // Otherwise, show the union of all member source IPs. + let has_any_source_member = members.iter().any(|m| m.source_ips.is_empty()); + let source_ips_display = if has_any_source_member { + "-".to_string() + } else { + let source_ips: BTreeSet = members + .iter() + .flat_map(|m| m.source_ips.iter().map(|ip| ip.ip())) + .collect(); + if source_ips.is_empty() { + "-".to_string() + } else { + source_ips + .iter() + .map(|ip| ip.to_string()) + .collect::>() + .join(",") + } + }; + + // Print group details + println!("MULTICAST GROUP"); + println!(" id: {}", group.identity.id); + println!(" name: {}", group.identity.name); + println!(" state: {:?}", group.state); + println!(" multicast_ip: {}", group.multicast_ip); + println!(" vni: {}", u32::from(group.vni.0)); + println!(" source_ips: {source_ips_display}"); + println!(" ip_pool: {pool_name} ({})", group.ip_pool_id); + println!(" underlay_group: {:?}", group.underlay_group_id); + println!(" tag: {:?}", group.tag); + println!(" created: {}", group.identity.time_created); + if let Some(deleted) = group.identity.time_deleted { + println!(" deleted: {deleted}"); + } + + // Display underlay group if present + if let Some(underlay_group) = underlay { + println!("\nUNDERLAY GROUP"); + println!(" id: {}", underlay_group.id); + println!(" multicast_ip: {}", underlay_group.multicast_ip); + println!(" tag: {:?}", underlay_group.tag); + println!(" created: {}", underlay_group.time_created); + if let Some(deleted) = underlay_group.time_deleted { + println!(" deleted: {deleted}"); + } + } + + if members.is_empty() { + println!("\nMEMBERS: (none)"); + } else { + println!("\nMEMBERS ({}):", members.len()); + + // Batch lookup instance names (parent_id references instances) + let parent_ids: Vec = + members.iter().map(|member| member.parent_id).collect(); + let instances: Vec<(Uuid, String)> = instance_dsl::instance + .filter(instance_dsl::id.eq_any(parent_ids)) + .select((instance_dsl::id, instance_dsl::name)) + .get_results_async(&*conn) + .await + .context("fetching instance names")?; + let instance_map: HashMap = + instances.into_iter().collect(); + + // Batch lookup sled serials + let sled_ids: Vec = members + .iter() + .filter_map(|member| member.sled_id.map(|s| s.into_untyped_uuid())) + .collect(); + let sleds: Vec<(Uuid, String)> = if sled_ids.is_empty() { + Vec::new() + } else { + sled_dsl::sled + .filter(sled_dsl::id.eq_any(sled_ids)) + .select((sled_dsl::id, sled_dsl::serial_number)) + .get_results_async(&*conn) + .await + .context("fetching sled serials")? + }; + let sled_map: HashMap = sleds.into_iter().collect(); + + let rows: Vec = members + .into_iter() + .map(|member| { + let instance_name = instance_map + .get(&member.parent_id) + .cloned() + .unwrap_or_else(|| member.parent_id.to_string()); + let sled_serial = member + .sled_id + .and_then(|s| sled_map.get(&s.into_untyped_uuid()).cloned()) + .unwrap_or_else(|| "-".to_string()); + let sources = Some(&member.source_ips) + .filter(|ips| !ips.is_empty()) + .map(|ips| { + ips.iter() + .map(|ip| ip.ip().to_string()) + .collect::>() + .join(",") + }) + .unwrap_or_else(|| "-".to_string()); + MulticastInfoMemberRow { + id: member.id, + instance: instance_name, + state: member.state, + multicast_ip: member.multicast_ip.ip(), + sources, + sled: sled_serial, + created: member.time_created, + } + }) + .collect(); + + let table = tabled::Table::new(rows) + .with(tabled::settings::Style::empty()) + .with(tabled::settings::Padding::new(0, 1, 0, 0)) + .to_string(); + + println!("{table}"); + } + + Ok(()) +} diff --git a/dev-tools/omdb/src/bin/omdb/nexus.rs b/dev-tools/omdb/src/bin/omdb/nexus.rs index 2879f15c6d7..16950ae9f38 100644 --- a/dev-tools/omdb/src/bin/omdb/nexus.rs +++ b/dev-tools/omdb/src/bin/omdb/nexus.rs @@ -66,6 +66,7 @@ use nexus_types::internal_api::background::InstanceReincarnationStatus; use nexus_types::internal_api::background::InstanceUpdaterStatus; use nexus_types::internal_api::background::InventoryLoadStatus; use nexus_types::internal_api::background::LookupRegionPortStatus; +use nexus_types::internal_api::background::MulticastGroupReconcilerStatus; use nexus_types::internal_api::background::PhysicalDiskAdoptionStatus; use nexus_types::internal_api::background::ProbeDistributorStatus; use nexus_types::internal_api::background::ReadOnlyRegionReplacementStartStatus; @@ -1316,6 +1317,9 @@ fn print_task_details( "lookup_region_port" => { print_task_lookup_region_port(details); } + "multicast_reconciler" => { + print_task_multicast_reconciler(details); + } "phantom_disks" => { print_task_phantom_disks(details); } @@ -2265,6 +2269,76 @@ fn print_task_lookup_region_port(details: &serde_json::Value) { } } +fn print_task_multicast_reconciler(details: &serde_json::Value) { + let status = match serde_json::from_value::( + details.clone(), + ) { + Err(error) => { + eprintln!( + "warning: failed to interpret task details: {error:?}: {details:?}" + ); + return; + } + Ok(status) => status, + }; + + if status.disabled { + println!(" multicast feature is disabled"); + return; + } + + const GROUPS_CREATED: &str = "groups created (Creating->Active):"; + const GROUPS_DELETED: &str = "groups deleted (cleanup):"; + const GROUPS_VERIFIED: &str = "groups verified (Active):"; + const EMPTY_GROUPS_MARKED: &str = "empty groups marked for deletion:"; + const MEMBERS_PROCESSED: &str = "members processed:"; + const MEMBERS_DELETED: &str = "members deleted:"; + const WIDTH: usize = const_max_len(&[ + GROUPS_CREATED, + GROUPS_DELETED, + GROUPS_VERIFIED, + EMPTY_GROUPS_MARKED, + MEMBERS_PROCESSED, + MEMBERS_DELETED, + ]) + 1; + const NUM_WIDTH: usize = 3; + + if !status.errors.is_empty() { + println!( + " task did not complete successfully! ({} errors)", + status.errors.len() + ); + for error in &status.errors { + println!(" > {error}"); + } + } + + println!( + " {GROUPS_CREATED:NUM_WIDTH$}", + status.groups_created + ); + println!( + " {GROUPS_DELETED:NUM_WIDTH$}", + status.groups_deleted + ); + println!( + " {GROUPS_VERIFIED:NUM_WIDTH$}", + status.groups_verified + ); + println!( + " {EMPTY_GROUPS_MARKED:NUM_WIDTH$}", + status.empty_groups_marked + ); + println!( + " {MEMBERS_PROCESSED:NUM_WIDTH$}", + status.members_processed + ); + println!( + " {MEMBERS_DELETED:NUM_WIDTH$}", + status.members_deleted + ); +} + fn print_task_phantom_disks(details: &serde_json::Value) { #[derive(Deserialize)] struct TaskSuccess { diff --git a/dev-tools/omdb/tests/successes.out b/dev-tools/omdb/tests/successes.out index 6b4a9e198e2..3057f6b6f22 100644 --- a/dev-tools/omdb/tests/successes.out +++ b/dev-tools/omdb/tests/successes.out @@ -85,6 +85,36 @@ stderr: note: using database URL postgresql://root@[::1]:REDACTED_PORT/omicron?sslmode=disable note: database schema version matches expected () ============================================= +EXECUTING COMMAND: omdb ["db", "multicast", "groups"] +termination: Exited(0) +--------------------------------------------- +stdout: +ID NAME STATE MULTICAST_IP RANGE UNDERLAY_IP SOURCES MEMBERS VNI CREATED +--------------------------------------------- +stderr: +note: using database URL postgresql://root@[::1]:REDACTED_PORT/omicron?sslmode=disable +note: database schema version matches expected () +============================================= +EXECUTING COMMAND: omdb ["db", "multicast", "members"] +termination: Exited(0) +--------------------------------------------- +stdout: +ID GROUP_NAME PARENT_ID STATE MULTICAST_IP SOURCES SLED_ID CREATED +--------------------------------------------- +stderr: +note: using database URL postgresql://root@[::1]:REDACTED_PORT/omicron?sslmode=disable +note: database schema version matches expected () +============================================= +EXECUTING COMMAND: omdb ["db", "multicast", "pools"] +termination: Exited(0) +--------------------------------------------- +stdout: +no multicast IP pools found +--------------------------------------------- +stderr: +note: using database URL postgresql://root@[::1]:REDACTED_PORT/omicron?sslmode=disable +note: database schema version matches expected () +============================================= EXECUTING COMMAND: omdb ["db", "sleds"] termination: Exited(0) --------------------------------------------- @@ -866,7 +896,12 @@ task: "multicast_reconciler" configured period: every m last completed activation: , triggered by started at (s ago) and ran for ms -warning: unknown background task: "multicast_reconciler" (don't know how to interpret details: Object {"disabled": Bool(false), "empty_groups_marked": Number(0), "errors": Array [], "groups_created": Number(0), "groups_deleted": Number(0), "groups_verified": Number(0), "members_deleted": Number(0), "members_processed": Number(0)}) + groups created (Creating->Active): 0 + groups deleted (cleanup): 0 + groups verified (Active): 0 + empty groups marked for deletion: 0 + members processed: 0 + members deleted: 0 task: "phantom_disks" configured period: every s @@ -1579,7 +1614,12 @@ task: "multicast_reconciler" configured period: every m last completed activation: , triggered by started at (s ago) and ran for ms -warning: unknown background task: "multicast_reconciler" (don't know how to interpret details: Object {"disabled": Bool(false), "empty_groups_marked": Number(0), "errors": Array [], "groups_created": Number(0), "groups_deleted": Number(0), "groups_verified": Number(0), "members_deleted": Number(0), "members_processed": Number(0)}) + groups created (Creating->Active): 0 + groups deleted (cleanup): 0 + groups verified (Active): 0 + empty groups marked for deletion: 0 + members processed: 0 + members deleted: 0 task: "phantom_disks" configured period: every s diff --git a/dev-tools/omdb/tests/test_all_output.rs b/dev-tools/omdb/tests/test_all_output.rs index a00b80f823f..37d8ae03d84 100644 --- a/dev-tools/omdb/tests/test_all_output.rs +++ b/dev-tools/omdb/tests/test_all_output.rs @@ -111,6 +111,7 @@ async fn test_omdb_usage_errors() { &["db", "sitrep", "show", "v1.2.3"], &["db", "saga"], &["db", "snapshots"], + &["db", "multicast"], &["db", "network"], &["mgs"], &["nexus"], @@ -246,6 +247,9 @@ async fn test_omdb_success_cases() { &["db", "dns", "diff", "external", "2"], &["db", "dns", "names", "external", "2"], &["db", "instances"], + &["db", "multicast", "groups"], + &["db", "multicast", "members"], + &["db", "multicast", "pools"], &["db", "sleds"], &["db", "sleds", "-F", "discretionary"], &["mgs", "inventory"], diff --git a/dev-tools/omdb/tests/test_multicast.rs b/dev-tools/omdb/tests/test_multicast.rs new file mode 100644 index 00000000000..a4a4c24f285 --- /dev/null +++ b/dev-tools/omdb/tests/test_multicast.rs @@ -0,0 +1,728 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#![cfg(feature = "multicast")] +//! Tests for omdb multicast commands with real data. +//! +//! These tests verify that omdb correctly formats multicast data by creating +//! actual multicast pools, groups, and members, then running omdb commands +//! and checking the output. + +use std::net::IpAddr; +use std::process::Command; +use std::time::Duration; + +use dropshot::test_util::ClientTestContext; +use futures::future::join3; +use nexus_db_queries::db::fixed_data::silo::DEFAULT_SILO; +use nexus_test_utils::http_testing::{AuthnMode, NexusRequest}; +use nexus_test_utils::resource_helpers::{ + create_default_ip_pools, create_instance_with, create_multicast_ip_pool, + create_project, link_ip_pool, object_put_upsert, objects_list_page_authz, +}; +use nexus_test_utils_macros::nexus_test; +use nexus_types::external_api::instance::Instance; +use nexus_types::external_api::instance::InstanceNetworkInterfaceAttachment; +use nexus_types::external_api::multicast::{ + InstanceMulticastGroupJoin, MulticastGroup, MulticastGroupMember, +}; +use nexus_types::identity::Resource; +use omicron_common::address::{IpRange, Ipv4Range}; +use omicron_test_utils::dev::poll::{self, CondCheckError, wait_for_condition}; + +type ControlPlaneTestContext = + nexus_test_utils::ControlPlaneTestContext; + +const PROJECT_NAME: &str = "omdb-test-project"; + +/// Path to the omdb binary - set by cargo when running tests +const CMD_OMDB: &str = env!("CARGO_BIN_EXE_omdb"); + +// Timeout constants for test operations +const POLL_INTERVAL: Duration = Duration::from_millis(80); +const MULTICAST_OPERATION_TIMEOUT: Duration = Duration::from_secs(120); + +/// Run an omdb command and return its stdout. +fn run_omdb(db_url: &str, args: &[&str]) -> String { + let output = Command::new(CMD_OMDB) + .env("OMDB_DB_URL", db_url) + .args(args) + .output() + .expect("failed to execute omdb"); + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + panic!("omdb command failed with args {args:?}:\nstderr: {stderr}"); + } + + String::from_utf8_lossy(&output.stdout).to_string() +} + +/// Build URL for a specific multicast group by name. +fn mcast_group_url(group_name: &str) -> String { + format!("/v1/multicast-groups/{group_name}") +} + +/// Create a multicast IP pool and link it to the default silo. +async fn create_multicast_pool_linked( + client: &ClientTestContext, + pool_name: &str, + ip_range: Option, +) { + create_multicast_ip_pool(client, pool_name, ip_range).await; + link_ip_pool(client, pool_name, &DEFAULT_SILO.id(), false).await; +} + +/// Convenience function to wait for a group to become "Active". +async fn wait_for_group_active( + client: &ClientTestContext, + group_name: &str, +) -> MulticastGroup { + match wait_for_condition( + || async { + let group: MulticastGroup = + NexusRequest::object_get(client, &mcast_group_url(group_name)) + .authn_as(AuthnMode::PrivilegedUser) + .execute_and_parse_unwrap() + .await; + if group.state == "Active" { + Ok(group) + } else { + Err(CondCheckError::<()>::NotYet { status: None }) + } + }, + &POLL_INTERVAL, + &MULTICAST_OPERATION_TIMEOUT, + ) + .await + { + Ok(group) => group, + Err(poll::Error::TimedOut { elapsed, .. }) => { + panic!( + "group {group_name} did not reach state 'Active' within {elapsed:?}" + ); + } + Err(poll::Error::PermanentError(err)) => { + panic!( + "failed waiting for group {group_name} to reach 'Active': {err:?}" + ); + } + } +} + +/// Wait for a specific member to reach the expected state. +async fn wait_for_member_state( + cptestctx: &ControlPlaneTestContext, + group_name: &str, + instance_id: uuid::Uuid, + expected_state: nexus_db_model::MulticastGroupMemberState, +) -> MulticastGroupMember { + let client = &cptestctx.external_client; + let expected_state_str = expected_state.to_string(); + + match wait_for_condition( + || async { + let url = format!("/v1/multicast-groups/{group_name}/members"); + let members = + objects_list_page_authz::(client, &url) + .await + .items; + if let Some(member) = + members.iter().find(|m| m.instance_id == instance_id) + { + if member.state == expected_state_str { + Ok(member.clone()) + } else { + Err(CondCheckError::<()>::NotYet { status: None }) + } + } else { + Err(CondCheckError::<()>::NotYet { status: None }) + } + }, + &POLL_INTERVAL, + &MULTICAST_OPERATION_TIMEOUT, + ) + .await + { + Ok(member) => member, + Err(poll::Error::TimedOut { elapsed, .. }) => { + panic!( + "member {instance_id} in group {group_name} did not reach state '{expected_state_str}' within {elapsed:?}" + ); + } + Err(poll::Error::PermanentError(err)) => { + panic!( + "failed waiting for member {instance_id} in group {group_name}: {err:?}" + ); + } + } +} + +/// Create an instance for multicast testing. +async fn create_test_instance( + client: &ClientTestContext, + project_name: &str, + instance_name: &str, + start: bool, +) -> Instance { + create_instance_with( + client, + project_name, + instance_name, + &InstanceNetworkInterfaceAttachment::DefaultIpv4, + vec![], + vec![], + start, + None, + None, + vec![], + ) + .await +} + +/// Test omdb multicast pools command. +#[nexus_test] +async fn test_omdb_multicast_pools(cptestctx: &ControlPlaneTestContext) { + let db_url = cptestctx.database.listen_url().to_string(); + let client = &cptestctx.external_client; + + // Before creating any pools, should show "no multicast IP pools found" + let output = run_omdb(&db_url, &["db", "multicast", "pools"]); + assert!( + output.contains("no multicast IP pools found"), + "Expected empty pool message, got: {output}" + ); + + // Create a multicast pool (no silo linking needed for pools-only test) + create_multicast_ip_pool(client, "test-mcast-pool", None).await; + + // Now should show the pool with all columns + let output = run_omdb(&db_url, &["db", "multicast", "pools"]); + // pool name + assert!( + output.contains("test-mcast-pool"), + "Expected pool name in output, got: {output}" + ); + // first address (default range from test-utils: 224.1.0.0 - 224.1.255.255) + assert!( + output.contains("224.1.0.0"), + "Expected first address in output, got: {output}" + ); + // last address + assert!( + output.contains("224.1.255.255"), + "Expected last address in output, got: {output}" + ); +} + +/// Test omdb multicast groups, members, and info commands. +/// +/// This consolidated test verifies all multicast commands work with actual data. +#[nexus_test] +async fn test_omdb_multicast_commands(cptestctx: &ControlPlaneTestContext) { + let db_url = cptestctx.database.listen_url().to_string(); + let client = &cptestctx.external_client; + + // Setup: create pools and project + join3( + create_default_ip_pools(client), + create_project(client, PROJECT_NAME), + create_multicast_pool_linked(client, "test-mcast-pool", None), + ) + .await; + + // Create an instance without multicast groups first + let instance = create_test_instance( + client, + PROJECT_NAME, + "test-instance", + false, // don't start + ) + .await; + + // Add a multicast member via API (this implicitly creates the group) + // Use instance-centric join endpoint: PUT /v1/instances/{instance}/multicast-groups/{group} + let join_url = format!( + "/v1/instances/{}/multicast-groups/test-mcast-group?project={PROJECT_NAME}", + instance.identity.id + ); + + object_put_upsert::<_, MulticastGroupMember>( + client, + &join_url, + &InstanceMulticastGroupJoin { + source_ips: None, // ASM (Any-Source Multicast) + ip_version: None, + }, + ) + .await; + + // Wait for the group to become "Active" + wait_for_group_active(client, "test-mcast-group").await; + + // Get the group details for later tests + let group_url = mcast_group_url("test-mcast-group"); + let group: MulticastGroup = NexusRequest::object_get(client, &group_url) + .authn_as(AuthnMode::PrivilegedUser) + .execute() + .await + .expect("failed to get group") + .parsed_body() + .expect("failed to parse group"); + + // Test: omdb db multicast groups + let output = run_omdb(&db_url, &["db", "multicast", "groups"]); + // group id + assert!( + output.contains(&group.identity.id.to_string()), + "Expected group id in output, got: {output}" + ); + // group name + assert!( + output.contains("test-mcast-group"), + "Expected group name in output, got: {output}" + ); + // state + assert!( + output.contains("Active"), + "Expected state 'Active' in output, got: {output}" + ); + // multicast ip + assert!( + output.contains(&group.multicast_ip.to_string()), + "Expected multicast ip in output, got: {output}" + ); + // range (ASM for 224.x.x.x) + assert!( + output.contains("ASM"), + "Expected range 'ASM' in output, got: {output}" + ); + // vni (column exists but VNI is internal, not exposed in the external API) + assert!( + output.contains("VNI"), + "Expected VNI column in output, got: {output}" + ); + // members (instance@sled format, "-" when not started) + assert!( + output.contains("test-instance@-"), + "Expected member 'test-instance@-' in output, got: {output}" + ); + + // Test: omdb db multicast groups --state active + let output = + run_omdb(&db_url, &["db", "multicast", "groups", "--state", "active"]); + assert!( + output.contains("test-mcast-group"), + "Expected group name with state filter, got: {output}" + ); + + // Test: omdb db multicast groups --pool + let output = run_omdb( + &db_url, + &["db", "multicast", "groups", "--pool", "test-mcast-pool"], + ); + assert!( + output.contains("test-mcast-group"), + "Expected group name with pool filter, got: {output}" + ); + + // Test: omdb db multicast members + let output = run_omdb(&db_url, &["db", "multicast", "members"]); + // group name + assert!( + output.contains("test-mcast-group"), + "Expected group name in members output, got: {output}" + ); + // parent id (instance id) + assert!( + output.contains(&instance.identity.id.to_string()), + "Expected parent id in members output, got: {output}" + ); + // multicast ip + let group_ip = group.multicast_ip.to_string(); + assert!( + output.contains(&group_ip), + "Expected multicast ip in members output, got: {output}" + ); + // sources ("-" = any-source member) + let has_any_source = output.lines().any(|line| { + line.contains(&instance.identity.id.to_string()) && line.contains(" - ") + }); + assert!( + has_any_source, + "Expected '-' for any-source member, got: {output}" + ); + + // Test: omdb db multicast members --group-name + let output = run_omdb( + &db_url, + &["db", "multicast", "members", "--group-name", "test-mcast-group"], + ); + assert!( + output.contains(&instance.identity.id.to_string()), + "Expected instance ID with group-name filter, got: {output}" + ); + + // Test: omdb db multicast members --group-ip (reuses group_ip from above) + let output = run_omdb( + &db_url, + &["db", "multicast", "members", "--group-ip", &group_ip], + ); + assert!( + output.contains(&instance.identity.id.to_string()), + "Expected instance ID with group-ip filter, got: {output}" + ); + + // Test: omdb db multicast members --group-id + let group_id = group.identity.id.to_string(); + let output = run_omdb( + &db_url, + &["db", "multicast", "members", "--group-id", &group_id], + ); + assert!( + output.contains(&instance.identity.id.to_string()), + "Expected instance ID with group-id filter, got: {output}" + ); + + // Test: omdb db multicast members --state left + // Wait for the RPW reconciler to transition member to "Left" state + // (instance isn't running, so no sled_id assignment) + wait_for_member_state( + cptestctx, + "test-mcast-group", + instance.identity.id, + nexus_db_model::MulticastGroupMemberState::Left, + ) + .await; + let output = + run_omdb(&db_url, &["db", "multicast", "members", "--state", "left"]); + assert!( + output.contains(&instance.identity.id.to_string()), + "Expected instance ID with state=left filter, got: {output}" + ); + + // Test: omdb db multicast members --sled-id + // Create a started instance so the member gets a sled_id + let started_instance = create_test_instance( + client, + PROJECT_NAME, + "started-instance", + true, // start the instance + ) + .await; + + // Add member to a new group for the started instance + let sled_join_url = format!( + "/v1/instances/{}/multicast-groups/sled-test-group?project={PROJECT_NAME}", + started_instance.identity.id + ); + object_put_upsert::<_, MulticastGroupMember>( + client, + &sled_join_url, + &InstanceMulticastGroupJoin { source_ips: None, ip_version: None }, + ) + .await; + + wait_for_group_active(client, "sled-test-group").await; + + // Query members by sled_id - the started instance should be on first_sled + let sled_id = cptestctx.first_sled_id().to_string(); + let output = run_omdb( + &db_url, + &["db", "multicast", "members", "--sled-id", &sled_id], + ); + assert!( + output.contains(&started_instance.identity.id.to_string()), + "Expected started instance ID with sled-id filter, got: {output}" + ); + + // Test: omdb db multicast members --state joined + // Wait for the started instance's member to reach "Joined" state + wait_for_member_state( + cptestctx, + "sled-test-group", + started_instance.identity.id, + nexus_db_model::MulticastGroupMemberState::Joined, + ) + .await; + + // Now test the --state joined filter + let output_joined = + run_omdb(&db_url, &["db", "multicast", "members", "--state", "joined"]); + assert!( + output_joined.contains(&started_instance.identity.id.to_string()), + "Expected started instance in joined state, got: {output_joined}" + ); + // state column shows "Joined" + assert!( + output_joined.contains("Joined"), + "Expected 'Joined' state in members output, got: {output_joined}" + ); + // sled_id column shows the sled UUID + assert!( + output_joined.contains(&sled_id), + "Expected sled_id in members output, got: {output_joined}" + ); + + // Verify info for started instance shows sled serial (not "-") + let output_info = run_omdb( + &db_url, + &["db", "multicast", "info", "--name", "sled-test-group"], + ); + // member instance name + assert!( + output_info.contains("started-instance"), + "Expected 'started-instance' in info members, got: {output_info}" + ); + // underlay group should be present for active group + assert!( + output_info.contains("UNDERLAY GROUP"), + "Expected 'UNDERLAY GROUP' section in info output, got: {output_info}" + ); + + // Verify groups output shows started instance with sled serial (not "-") + let output_groups = run_omdb(&db_url, &["db", "multicast", "groups"]); + // sled-test-group should show "started-instance@" not "started-instance@-" + assert!( + output_groups.contains("started-instance@"), + "Expected 'started-instance@' in groups members column, got: {output_groups}" + ); + // The sled serial should appear (not just "-") + // Note: test sled serial is typically "serial0" or similar + assert!( + !output_groups.contains("started-instance@-"), + "Started instance should have sled serial, not '-', got: {output_groups}" + ); + + // Verify underlay_ip column shows an IP for active groups + // Active groups with joined members should have an underlay group assigned + // The underlay IP is in the ff04::/16 range (admin-local IPv6 multicast) + assert!( + output_groups.contains("ff04:"), + "Expected underlay_ip (ff04:*) for active group in groups output, got: {output_groups}" + ); + + // Verify started instance is not in "Left" state + let output_left = + run_omdb(&db_url, &["db", "multicast", "members", "--state", "left"]); + assert!( + !output_left.contains(&started_instance.identity.id.to_string()), + "Started instance should not be in 'Left' state, got: {output_left}" + ); + + // Test: combined filters (--group-name + --state) + // The started instance's member should appear when filtering by both + let output_combined = run_omdb( + &db_url, + &[ + "db", + "multicast", + "members", + "--group-name", + "sled-test-group", + "--state", + "joined", + ], + ); + assert!( + output_combined.contains(&started_instance.identity.id.to_string()), + "Expected started instance with combined filters, got: {output_combined}" + ); + + // Test: combined filters that should return empty (wrong group + state) + let output_combined_empty = run_omdb( + &db_url, + &[ + "db", + "multicast", + "members", + "--group-name", + "test-mcast-group", + "--state", + "joined", + ], + ); + // test-mcast-group has a non-started instance, so it should not be in "Joined" state + assert!( + !output_combined_empty + .contains(&started_instance.identity.id.to_string()), + "Started instance should not appear in wrong group filter, got: {output_combined_empty}" + ); + + // Test: omdb db multicast info --name + let output = run_omdb( + &db_url, + &["db", "multicast", "info", "--name", "test-mcast-group"], + ); + // section header + assert!( + output.contains("MULTICAST GROUP"), + "Expected 'MULTICAST GROUP' header in info output, got: {output}" + ); + // id + assert!( + output.contains(&group.identity.id.to_string()), + "Expected group id in info output, got: {output}" + ); + // name + assert!( + output.contains("test-mcast-group"), + "Expected group name in info output, got: {output}" + ); + // state + assert!( + output.contains("Active"), + "Expected state 'Active' in info output, got: {output}" + ); + // multicast ip + assert!( + output.contains(&group.multicast_ip.to_string()), + "Expected multicast ip in info output, got: {output}" + ); + // vni (field exists but VNI is internal, not exposed in the external API) + assert!( + output.contains("vni:"), + "Expected vni field in info output, got: {output}" + ); + // ip pool + assert!( + output.contains("test-mcast-pool"), + "Expected pool name in info output, got: {output}" + ); + // members section + assert!( + output.contains("MEMBERS"), + "Expected 'MEMBERS' section in info output, got: {output}" + ); + // member instance name + assert!( + output.contains("test-instance"), + "Expected instance name in info members, got: {output}" + ); + // member sled ("-" when not started) - check specific line pattern + // More specific than just contains("-") which could match table separators + let has_sled_dash = output + .lines() + .any(|line| line.contains("test-instance") && line.contains(" - ")); + assert!( + has_sled_dash, + "Expected sled '-' for non-started instance on same line as instance name, got: {output}" + ); + + // Test: omdb db multicast info --ip + let output = + run_omdb(&db_url, &["db", "multicast", "info", "--ip", &group_ip]); + assert!( + output.contains("test-mcast-group"), + "Expected group name when querying by IP, got: {output}" + ); + + // Test: omdb db multicast info --group-id (reuses group_id from members test) + let output = run_omdb( + &db_url, + &["db", "multicast", "info", "--group-id", &group_id], + ); + assert!( + output.contains("test-mcast-group"), + "Expected group name when querying by ID, got: {output}" + ); + + // Test SSM (Source-Specific Multicast) - group in 232/8 range + // SSM range is 232.0.0.0/8 for IPv4, ff3x::/32 for IPv6 + let ssm_range = IpRange::V4( + Ipv4Range::new( + std::net::Ipv4Addr::new(232, 1, 0, 0), + std::net::Ipv4Addr::new(232, 1, 0, 255), + ) + .unwrap(), + ); + create_multicast_pool_linked(client, "test-ssm-pool", Some(ssm_range)) + .await; + + let ssm_instance = + create_test_instance(client, PROJECT_NAME, "ssm-instance", false).await; + + let ssm_join_url = format!( + "/v1/instances/{}/multicast-groups/ssm-group?project={PROJECT_NAME}", + ssm_instance.identity.id + ); + object_put_upsert::<_, MulticastGroupMember>( + client, + &ssm_join_url, + &InstanceMulticastGroupJoin { + source_ips: Some(vec![ + "10.0.0.1".parse::().unwrap(), + "10.0.0.2".parse::().unwrap(), + ]), + ip_version: None, + }, + ) + .await; + + wait_for_group_active(client, "ssm-group").await; + + // Verify SSM group shows in groups list with sources + let output = run_omdb(&db_url, &["db", "multicast", "groups"]); + assert!( + output.contains("ssm-group"), + "Expected SSM group in output, got: {output}" + ); + // Verify SSM is shown in RANGE column (232.x.x.x = SSM range) + assert!( + output.contains("SSM"), + "Expected SSM in range column, got: {output}" + ); + // Verify ASM is shown for 224.x.x.x range (test-mcast-group) + assert!( + output.contains("ASM"), + "Expected ASM in range column, got: {output}" + ); + // Verify SSM source IPs + assert!( + output.contains("10.0.0.1") && output.contains("10.0.0.2"), + "Expected SSM source IPs in output, got: {output}" + ); + + // Verify SSM sources show in info command + let output = + run_omdb(&db_url, &["db", "multicast", "info", "--name", "ssm-group"]); + assert!( + output.contains("10.0.0.1") || output.contains("10.0.0.2"), + "Expected SSM source IPs in info output, got: {output}" + ); + + // Test: omdb db multicast members shows sources per member + let output = run_omdb(&db_url, &["db", "multicast", "members"]); + // SSM member should show its sources + assert!( + output.contains("10.0.0.1") || output.contains("10.0.0.2"), + "Expected SSM member sources in members output, got: {output}" + ); + + // Test: omdb db multicast members --source-ip + // Filter by SSM source IP - should find SSM member + let output = run_omdb( + &db_url, + &["db", "multicast", "members", "--source-ip", "10.0.0.1"], + ); + assert!( + output.contains(&ssm_instance.identity.id.to_string()), + "Expected SSM instance with source-ip filter, got: {output}" + ); + // Members without sources should not appear for any source-ip filter + assert!( + !output.contains(&instance.identity.id.to_string()), + "Member without sources should not appear, got: {output}" + ); + + // Test: --source-ip with non-existent IP returns no members + let output = run_omdb( + &db_url, + &["db", "multicast", "members", "--source-ip", "10.99.99.99"], + ); + assert!( + !output.contains(&ssm_instance.identity.id.to_string()), + "No members should match non-existent source IP, got: {output}" + ); +} diff --git a/dev-tools/omdb/tests/usage_errors.out b/dev-tools/omdb/tests/usage_errors.out index 0fe007398af..b3b3ea053f8 100644 --- a/dev-tools/omdb/tests/usage_errors.out +++ b/dev-tools/omdb/tests/usage_errors.out @@ -149,6 +149,7 @@ Commands: instances Alias to `omdb instance list` network Print information about the network migrations Print information about migrations + multicast Print information about multicast groups snapshots Print information about snapshots validate Validate the contents of the database volumes Print information about volumes @@ -218,6 +219,7 @@ Commands: instances Alias to `omdb instance list` network Print information about the network migrations Print information about migrations + multicast Print information about multicast groups snapshots Print information about snapshots validate Validate the contents of the database volumes Print information about volumes @@ -1302,6 +1304,41 @@ Database Options: --include-deleted whether to include soft-deleted records when enumerating objects that can be soft-deleted +Safety Options: + -w, --destructive Allow potentially-destructive subcommands +============================================= +EXECUTING COMMAND: omdb ["db", "multicast"] +termination: Exited(2) +--------------------------------------------- +stdout: +--------------------------------------------- +stderr: +Print information about multicast groups + +Usage: omdb db multicast [OPTIONS] + +Commands: + groups List all multicast groups + members List all multicast group members + pools List multicast IP pools and their ranges + info Get detailed info for a multicast group + help Print this message or the help of the given subcommand(s) + +Options: + --log-level log level filter [env: LOG_LEVEL=] [default: warn] + --color Color output [default: auto] [possible values: auto, always, never] + -h, --help Print help + +Connection Options: + --db-url URL of the database SQL interface [env: OMDB_DB_URL=] + --dns-server [env: OMDB_DNS_SERVER=] + +Database Options: + --fetch-limit limit to apply to queries that fetch rows [env: + OMDB_FETCH_LIMIT=] [default: 500] + --include-deleted whether to include soft-deleted records when enumerating objects + that can be soft-deleted + Safety Options: -w, --destructive Allow potentially-destructive subcommands ============================================= diff --git a/dev-tools/xtask/src/virtual_hardware.rs b/dev-tools/xtask/src/virtual_hardware.rs index 6ce61f13c4f..0f6c13ce735 100644 --- a/dev-tools/xtask/src/virtual_hardware.rs +++ b/dev-tools/xtask/src/virtual_hardware.rs @@ -117,8 +117,8 @@ const ZPOOL: &'static str = "/usr/sbin/zpool"; const ZONEADM: &'static str = "/usr/sbin/zoneadm"; const SIDECAR_LITE_COMMIT: &'static str = - "69fae2ac230cd29f4ea2bf3bcbd6d99554c7bc47"; -const SOFTNPU_COMMIT: &'static str = "e4c3130538b192988737161f4a4dc97272f02b9c"; + "6f3311e8acd7e7e95c167aab61188355a93afe72"; +const SOFTNPU_COMMIT: &'static str = "284c6830722548714128e63ea04bcca78ee27154"; const PXA_MAC_DEFAULT: &'static str = "a8:e1:de:01:70:1d"; const PXA_WARNING: &'static str = r#" You have not set up the proxy-ARP environment variables diff --git a/nexus/Cargo.toml b/nexus/Cargo.toml index 727a794c681..a94449e5e0e 100644 --- a/nexus/Cargo.toml +++ b/nexus/Cargo.toml @@ -8,6 +8,7 @@ license = "MPL-2.0" workspace = true [features] +default = ["multicast"] multicast = [] [build-dependencies] diff --git a/nexus/db-schema/src/schema.rs b/nexus/db-schema/src/schema.rs index accbfca1d23..9e35a444fa9 100644 --- a/nexus/db-schema/src/schema.rs +++ b/nexus/db-schema/src/schema.rs @@ -3090,7 +3090,11 @@ table! { } // Allow multicast tables to appear together for NOT EXISTS subqueries -allow_tables_to_appear_in_same_query!(multicast_group, multicast_group_member); +allow_tables_to_appear_in_same_query!( + multicast_group, + underlay_multicast_group, + multicast_group_member +); allow_tables_to_appear_in_same_query!(user_data_export, snapshot, image); diff --git a/nexus/examples/config-second.toml b/nexus/examples/config-second.toml index 3f255af6287..e3f81e6ac7d 100644 --- a/nexus/examples/config-second.toml +++ b/nexus/examples/config-second.toml @@ -207,6 +207,11 @@ audit_log_cleanup.retention_days = 90 audit_log_cleanup.max_deleted_per_activation = 10000 populate_switch_ports.period_secs = 30 +[multicast] +# Enable multicast switch programming and the group reconciler. The multicast +# API is served regardless of this setting. +enabled = true + [default_region_allocation_strategy] # allocate region on 3 random distinct zpools, on 3 random distinct sleds. type = "random_with_distinct_sleds" diff --git a/nexus/examples/config.toml b/nexus/examples/config.toml index 942e639d7b8..5aa6da6b1db 100644 --- a/nexus/examples/config.toml +++ b/nexus/examples/config.toml @@ -191,6 +191,11 @@ audit_log_cleanup.retention_days = 90 audit_log_cleanup.max_deleted_per_activation = 10000 populate_switch_ports.period_secs = 30 +[multicast] +# Enable multicast switch programming and the group reconciler. The multicast +# API is served regardless of this setting. +enabled = true + [default_region_allocation_strategy] # allocate region on 3 random distinct zpools, on 3 random distinct sleds. # type = "random_with_distinct_sleds" diff --git a/nexus/test-utils/src/resource_helpers.rs b/nexus/test-utils/src/resource_helpers.rs index dd624a90c42..f00af12f0ed 100644 --- a/nexus/test-utils/src/resource_helpers.rs +++ b/nexus/test-utils/src/resource_helpers.rs @@ -234,6 +234,29 @@ where .unwrap() } +/// Like [`object_put`], but expects 201 Created instead of 200 OK. +pub async fn object_put_upsert( + client: &ClientTestContext, + path: &str, + input: &InputType, +) -> OutputType +where + InputType: serde::Serialize, + OutputType: serde::de::DeserializeOwned, +{ + NexusRequest::new( + RequestBuilder::new(client, Method::PUT, path) + .body(Some(input)) + .expect_status(Some(StatusCode::CREATED)), + ) + .authn_as(AuthnMode::PrivilegedUser) + .execute() + .await + .unwrap_or_else(|e| panic!("failed to make \"PUT\" request to {path}: {e}")) + .parsed_body() + .unwrap() +} + pub async fn object_put_error( client: &ClientTestContext, path: &str, diff --git a/nexus/tests/integration_tests/instances.rs b/nexus/tests/integration_tests/instances.rs index bb3b6904765..08bbc73e071 100644 --- a/nexus/tests/integration_tests/instances.rs +++ b/nexus/tests/integration_tests/instances.rs @@ -9733,8 +9733,6 @@ pub async fn instance_simulate(nexus: &Arc, id: &InstanceUuid) { /// /// Returns an error instead of panicking if the sled agent communication fails. /// This is useful during test cleanup where the sled agent may be unavailable. -// This is currently only consumed by tests behind the multicast feature gate. -// If/when it has another consumer, this cfg can be removed. #[cfg(feature = "multicast")] pub async fn try_instance_simulate( nexus: &Arc, diff --git a/nexus/tests/integration_tests/multicast/api.rs b/nexus/tests/integration_tests/multicast/api.rs index 0f3cc677ce6..ae7a4d54545 100644 --- a/nexus/tests/integration_tests/multicast/api.rs +++ b/nexus/tests/integration_tests/multicast/api.rs @@ -33,16 +33,15 @@ use nexus_test_utils::resource_helpers::{ }; use nexus_test_utils_macros::nexus_test; use nexus_types::external_api::instance::{ - InstanceCreate, InstanceNetworkInterfaceAttachment, + InstanceCpuCount, InstanceCreate, InstanceNetworkInterfaceAttachment, }; use nexus_types::external_api::multicast::{ InstanceMulticastGroupJoin, MulticastGroup, MulticastGroupIdentifier, MulticastGroupJoinSpec, MulticastGroupMember, }; +use nexus_types_versions::latest::instance::Instance; use omicron_common::address::is_ssm_address; -use omicron_common::api::external::{ - ByteCount, IdentityMetadataCreateParams, Instance, InstanceCpuCount, -}; +use omicron_common::api::external::{ByteCount, IdentityMetadataCreateParams}; use super::*; @@ -83,6 +82,7 @@ async fn test_multicast_api_behavior(cptestctx: &ControlPlaneTestContext) { cpu_platform: None, auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), + enable_jumbo_frames: false, }; let instance_url = format!("/v1/instances?project={project_name}"); @@ -114,6 +114,7 @@ async fn test_multicast_api_behavior(cptestctx: &ControlPlaneTestContext) { cpu_platform: None, auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), + enable_jumbo_frames: false, }; let instance2: Instance = object_create(client, &instance_url, &instance2_params).await; @@ -196,6 +197,7 @@ async fn test_multicast_api_behavior(cptestctx: &ControlPlaneTestContext) { cpu_platform: None, auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), + enable_jumbo_frames: false, }; let (instance3, group) = ops::join2( @@ -331,6 +333,7 @@ async fn test_multicast_api_behavior(cptestctx: &ControlPlaneTestContext) { cpu_platform: None, auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), + enable_jumbo_frames: false, }; let err = object_create_error( client, diff --git a/nexus/tests/integration_tests/multicast/authorization.rs b/nexus/tests/integration_tests/multicast/authorization.rs index 88550abab40..c170b099221 100644 --- a/nexus/tests/integration_tests/multicast/authorization.rs +++ b/nexus/tests/integration_tests/multicast/authorization.rs @@ -21,7 +21,7 @@ use nexus_test_utils::resource_helpers::{ }; use nexus_test_utils_macros::nexus_test; use nexus_types::external_api::instance::{ - InstanceCreate, InstanceNetworkInterfaceAttachment, + InstanceCpuCount, InstanceCreate, InstanceNetworkInterfaceAttachment, }; use nexus_types::external_api::multicast::{ InstanceMulticastGroupJoin, MulticastGroup, MulticastGroupMember, @@ -31,9 +31,9 @@ use nexus_types::external_api::project::ProjectCreate; use nexus_types::external_api::silo::{ Silo, SiloCreate, SiloIdentityMode, SiloQuotasCreate, }; +use nexus_types_versions::latest::instance::Instance; use omicron_common::api::external::{ - ByteCount, Hostname, IdentityMetadataCreateParams, Instance, - InstanceCpuCount, + ByteCount, Hostname, IdentityMetadataCreateParams, }; use omicron_uuid_kinds::SiloUserUuid; @@ -94,6 +94,7 @@ async fn create_group_via_instance_join( start: false, auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), + enable_jumbo_frames: false, }; let instance_url = format!("/v1/instances?project={project_name}"); @@ -261,6 +262,7 @@ async fn test_silo_user_multicast_permissions( start: false, auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), + enable_jumbo_frames: false, }; let instance2: Instance = NexusRequest::new( @@ -440,6 +442,7 @@ async fn test_silo_user_multicast_permissions( start: false, auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), + enable_jumbo_frames: false, }; let project_instance: Instance = NexusRequest::new( @@ -510,6 +513,7 @@ async fn test_silo_user_multicast_permissions( start: false, auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), + enable_jumbo_frames: false, multicast_groups: Vec::new(), }; let instance2: Instance = NexusRequest::objects_post( @@ -1117,6 +1121,7 @@ async fn test_cross_silo_multicast_isolation( start: false, auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), + enable_jumbo_frames: false, }; let instance_a: Instance = NexusRequest::new( @@ -1155,6 +1160,7 @@ async fn test_cross_silo_multicast_isolation( start: false, auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), + enable_jumbo_frames: false, }; let instance_b: Instance = NexusRequest::new( @@ -1355,6 +1361,7 @@ async fn test_cross_silo_multicast_isolation( start: false, auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), + enable_jumbo_frames: false, }; NexusRequest::new( @@ -1486,6 +1493,7 @@ async fn test_both_member_endpoints_have_same_permissions( start: false, auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), + enable_jumbo_frames: false, }; let instance_url = format!("/v1/instances?project={}", project_a.identity.name); diff --git a/nexus/tests/integration_tests/multicast/cache_invalidation.rs b/nexus/tests/integration_tests/multicast/cache_invalidation.rs index ed058b8b0e3..299a5777624 100644 --- a/nexus/tests/integration_tests/multicast/cache_invalidation.rs +++ b/nexus/tests/integration_tests/multicast/cache_invalidation.rs @@ -13,7 +13,8 @@ use http::{Method, StatusCode}; -use gateway_client::types::{PowerState, RotState, SpState}; +use gateway_types::component::{PowerState, SpState}; +use gateway_types::rot::RotState; use nexus_db_lookup::LookupPath; use nexus_db_queries::context::OpContext; use nexus_test_utils::resource_helpers::{ diff --git a/nexus/tests/integration_tests/multicast/enablement.rs b/nexus/tests/integration_tests/multicast/enablement.rs index b46efc8bd2a..35a6ed6b5e7 100644 --- a/nexus/tests/integration_tests/multicast/enablement.rs +++ b/nexus/tests/integration_tests/multicast/enablement.rs @@ -9,7 +9,8 @@ use nexus_test_utils::resource_helpers::{ create_default_ip_pools, create_project, object_get, }; -use omicron_common::api::external::{Instance, InstanceState}; +use nexus_types::external_api::instance::InstanceState; +use nexus_types_versions::latest::instance::Instance; use omicron_uuid_kinds::{GenericUuid, InstanceUuid}; use super::*; diff --git a/nexus/tests/integration_tests/multicast/failures.rs b/nexus/tests/integration_tests/multicast/failures.rs index 1baaa1a0a0f..2c172cfb49e 100644 --- a/nexus/tests/integration_tests/multicast/failures.rs +++ b/nexus/tests/integration_tests/multicast/failures.rs @@ -1043,15 +1043,14 @@ async fn test_left_member_waits_for_group_active( /// Test underlay IP collision detection and salt-based retry mechanism. /// -/// This test verifies that when two external groups would map to the same -/// underlay IP, the reconciler detects the collision and retries with an +/// Verifies that when two external groups would map to the same underlay +/// IP, the reconciler detects the collision and retries with an /// incremented salt value. /// -/// Test setup: -/// 1. Create group A with external IP 224.5.5.5 (via instance join) -/// 2. Pre-occupy B's target underlay IP (ff04::e001:0203) using A's tag -/// 3. Create group B with external IP 224.1.2.3 (maps to ff04::e001:0203 with salt=0) -/// 4. When reconciler processes B, it hits collision and retries with salt=1 +/// Steps: a) create group A (224.5.5.5), let it reach Active, b) stop +/// DPD and pre-occupy B's target underlay (ff04::e001:0203) with A's +/// tag, c) create group B (224.1.2.3), d) restart DPD and reconcile, +/// e) verify B has salt > 0 and A/B have different underlays. #[nexus_test] async fn test_multicast_group_underlay_collision_retry( cptestctx: &ControlPlaneTestContext, @@ -1065,11 +1064,10 @@ async fn test_multicast_group_underlay_collision_retry( let project_name = "collision-test-project"; let instance_a_name = "collision-instance-a"; let instance_b_name = "collision-instance-b"; - // Use IP addresses as group identifiers for explicit IP allocation let group_a_ip = "224.5.5.5"; - let group_b_ip = "224.1.2.3"; // Known mapping: → ff04::e001:0203 with salt=0 + let group_b_ip = "224.1.2.3"; // Maps to ff04::e001:0203 at salt=0 - // Setup: project, pools + // Setup: project, pools, instances ops::join3( create_project(&client, project_name), create_default_ip_pools(&client), @@ -1082,17 +1080,14 @@ async fn test_multicast_group_underlay_collision_retry( ) .await; - // Create two instances for membership ops::join2( create_instance(client, project_name, instance_a_name), create_instance(client, project_name, instance_b_name), ) .await; - // Stop DPD to control when groups transition to "Active" - cptestctx.stop_dendrite(SwitchSlot::Switch0).await; - - // Create group A by joining instance to IP address (implicit group creation) + // Step 1: let group A reach Active so its underlay is deterministic + // before setting up the collision. multicast_group_attach( cptestctx, project_name, @@ -1101,18 +1096,34 @@ async fn test_multicast_group_underlay_collision_retry( ) .await; - // Fetch group A (created by attach) from API using IP address as identifier let group_a = get_multicast_group(client, group_a_ip).await; + wait_for_group_active(client, group_a_ip).await; + let group_a_model = datastore .multicast_group_fetch( &opctx, MulticastGroupUuid::from_untyped_uuid(group_a.identity.id), ) .await - .expect("Should fetch group A"); + .expect("Should fetch group A after Active"); + let underlay_a = datastore + .underlay_multicast_group_fetch( + &opctx, + group_a_model.underlay_group_id.unwrap(), + ) + .await + .expect("Should fetch A's underlay group"); + + assert_eq!( + underlay_a.multicast_ip.ip().to_string(), + "ff04::e005:505", + "A's underlay should be its natural mapping (224.5.5.5)" + ); + + // Step 2: stop DPD and pre-occupy B's target underlay IP using A's + // tag so B will collide at salt=0. + cptestctx.stop_dendrite(SwitchSlot::Switch0).await; - // Pre-occupy B's target underlay IP by creating an underlay group with A's tag - // B's target: ff04::e001:0203 (known mapping for 224.1.2.3 with salt=0) let collision_ip: ipnetwork::IpNetwork = "ff04::e001:0203".parse().unwrap(); let res = datastore .ensure_underlay_multicast_group( @@ -1123,7 +1134,6 @@ async fn test_multicast_group_underlay_collision_retry( .await .expect("Should create collision underlay"); - // Verify the underlay was created (for group A, but with B's target IP) assert!( matches!( res, @@ -1132,7 +1142,7 @@ async fn test_multicast_group_underlay_collision_retry( "Should have created underlay group with collision IP" ); - // Create group B by joining instance to IP address (implicit group creation) + // Step 3: create group B. multicast_group_attach( cptestctx, project_name, @@ -1141,14 +1151,15 @@ async fn test_multicast_group_underlay_collision_retry( ) .await; - // Fetch group B (created by attach) from API using IP address as identifier let group_b = get_multicast_group(client, group_b_ip).await; - // Restart DPD and run reconciler, triggering collision detection + // Step 4: restart DPD and run the reconciler. cptestctx.restart_dendrite(SwitchSlot::Switch0).await; activate_multicast_reconciler(&cptestctx.lockstep_client).await; - // Fetch group B after reconciliation + // Step 5: verify collision was handled correctly. + wait_for_group_active(client, group_b_ip).await; + let group_b_after = datastore .multicast_group_fetch( &opctx, @@ -1157,7 +1168,6 @@ async fn test_multicast_group_underlay_collision_retry( .await .expect("Should fetch group B after reconciliation"); - // Verify B got a salt value > 0 (collision was detected and retried) assert!( group_b_after.underlay_salt.is_some(), "Group B should have a salt value after collision retry" @@ -1168,36 +1178,32 @@ async fn test_multicast_group_underlay_collision_retry( "Salt should be > 0 after collision (got {})", *salt_value ); - - // Verify B got an underlay group (different from the collision IP) assert!( group_b_after.underlay_group_id.is_some(), "Group B should have an underlay group after collision retry" ); - // Fetch A's underlay group and verify it has the collision IP - let group_a_after = datastore + // Verify A and B both got underlay groups and they differ. + let group_a_final = datastore .multicast_group_fetch( &opctx, MulticastGroupUuid::from_untyped_uuid(group_a.identity.id), ) .await - .expect("Should fetch group A after reconciliation"); + .expect("Should fetch group A after full reconciliation"); + assert!( + group_a_final.underlay_group_id.is_some(), + "Group A should have an underlay group" + ); + let underlay_a = datastore .underlay_multicast_group_fetch( &opctx, - group_a_after.underlay_group_id.unwrap(), + group_a_final.underlay_group_id.unwrap(), ) .await .expect("Should fetch A's underlay group"); - assert_eq!( - underlay_a.multicast_ip.ip().to_string(), - "ff04::e001:203", - "A's underlay IP should be the collision IP we set" - ); - - // Fetch B's underlay group and verify it has a different IP than the collision IP let underlay_b = datastore .underlay_multicast_group_fetch( &opctx, @@ -1206,13 +1212,6 @@ async fn test_multicast_group_underlay_collision_retry( .await .expect("Should fetch B's underlay group"); - assert_ne!( - underlay_b.multicast_ip.ip().to_string(), - "ff04::e001:203", - "B's underlay IP should differ from collision IP due to salt" - ); - - // Verify A and B have different underlay IPs assert_ne!( underlay_a.multicast_ip, underlay_b.multicast_ip, "A and B should have different underlay IPs" diff --git a/nexus/tests/integration_tests/multicast/groups.rs b/nexus/tests/integration_tests/multicast/groups.rs index 6d5cb03de8d..b67789a092b 100644 --- a/nexus/tests/integration_tests/multicast/groups.rs +++ b/nexus/tests/integration_tests/multicast/groups.rs @@ -39,6 +39,7 @@ use nexus_test_utils::resource_helpers::{ object_get, object_get_error, object_put_error, }; use nexus_test_utils_macros::nexus_test; +use nexus_types::external_api::instance::InstanceState; use nexus_types::external_api::ip_pool::{ IpPool, IpPoolAssignment, IpPoolCreate, IpPoolRange, IpPoolType, IpRange, IpVersion, Ipv4Range, Ipv6Range, @@ -46,9 +47,7 @@ use nexus_types::external_api::ip_pool::{ use nexus_types::external_api::multicast::{ InstanceMulticastGroupJoin, MulticastGroup, MulticastGroupMember, }; -use omicron_common::api::external::{ - IdentityMetadataCreateParams, InstanceState, -}; +use omicron_common::api::external::IdentityMetadataCreateParams; use omicron_uuid_kinds::InstanceUuid; use super::*; diff --git a/nexus/tests/integration_tests/multicast/instances.rs b/nexus/tests/integration_tests/multicast/instances.rs index 314ff88f959..a60290db5c2 100644 --- a/nexus/tests/integration_tests/multicast/instances.rs +++ b/nexus/tests/integration_tests/multicast/instances.rs @@ -31,7 +31,8 @@ use nexus_test_utils::resource_helpers::{ }; use nexus_test_utils_macros::nexus_test; use nexus_types::external_api::instance::{ - InstanceCreate, InstanceNetworkInterfaceAttachment, InstanceUpdate, + InstanceCpuCount, InstanceCreate, InstanceNetworkInterfaceAttachment, + InstanceState, InstanceUpdate, }; use nexus_types::external_api::multicast::{ InstanceMulticastGroupJoin, MulticastGroup, MulticastGroupJoinSpec, @@ -39,9 +40,9 @@ use nexus_types::external_api::multicast::{ }; use nexus_types::internal_api::params::InstanceMigrateRequest; +use nexus_types_versions::latest::instance::Instance; use omicron_common::api::external::{ - ByteCount, IdentityMetadataCreateParams, Instance, InstanceCpuCount, - InstanceState, Nullable, + ByteCount, IdentityMetadataCreateParams, Nullable, }; use omicron_nexus::TestInterfaces; use omicron_uuid_kinds::{GenericUuid, InstanceUuid}; @@ -679,6 +680,7 @@ async fn test_multicast_member_cleanup_instance_never_started( start: false, // Don't start the instance auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), + enable_jumbo_frames: false, }; let instance_url = format!("/v1/instances?project={project_name}"); @@ -1240,6 +1242,7 @@ async fn test_source_ips_preserved_on_instance_reconfigure( start: false, auto_restart_policy: None, anti_affinity_groups: Vec::new(), + enable_jumbo_frames: false, cpu_platform: None, multicast_groups: Vec::new(), }, @@ -1303,6 +1306,7 @@ async fn test_source_ips_preserved_on_instance_reconfigure( "boot_disk": null, "auto_restart_policy": null, "cpu_platform": null, + "enable_jumbo_frames": false, "multicast_groups": [ // Existing group: source_ips=null to preserve existing sources { "group": ssm_ip, "source_ips": null }, @@ -1424,6 +1428,7 @@ async fn test_instance_create_with_ssm_multicast_groups( start: true, // Start the instance auto_restart_policy: None, anti_affinity_groups: Vec::new(), + enable_jumbo_frames: false, cpu_platform: None, // Key part: SSM group with source_ips via MulticastGroupJoinSpec multicast_groups: vec![MulticastGroupJoinSpec { @@ -1522,6 +1527,7 @@ async fn test_ssm_without_sources_fails_create_and_reconfigure( start: true, auto_restart_policy: None, anti_affinity_groups: Vec::new(), + enable_jumbo_frames: false, cpu_platform: None, multicast_groups: vec![MulticastGroupJoinSpec { group: ssm_ip.to_string().parse().unwrap(), @@ -1572,6 +1578,7 @@ async fn test_ssm_without_sources_fails_create_and_reconfigure( start: true, auto_restart_policy: None, anti_affinity_groups: Vec::new(), + enable_jumbo_frames: false, cpu_platform: None, multicast_groups: vec![], // No multicast groups init }; @@ -1596,6 +1603,7 @@ async fn test_ssm_without_sources_fails_create_and_reconfigure( source_ips: None, // Missing sources for new SSM group! ip_version: None, }]), + enable_jumbo_frames: false, }; let update_url = diff --git a/nexus/tests/integration_tests/multicast/mod.rs b/nexus/tests/integration_tests/multicast/mod.rs index 5142c90190f..5924995b36d 100644 --- a/nexus/tests/integration_tests/multicast/mod.rs +++ b/nexus/tests/integration_tests/multicast/mod.rs @@ -32,7 +32,8 @@ use nexus_test_utils::resource_helpers::{ }; use nexus_types::deployment::SledFilter; use nexus_types::external_api::instance::{ - InstanceCreate, InstanceNetworkInterfaceAttachment, + InstanceCpuCount, InstanceCreate, InstanceNetworkInterfaceAttachment, + InstanceState, }; use nexus_types::external_api::ip_pool::{ IpPool, IpPoolAssignment, IpPoolCreate, IpPoolRange, IpPoolType, IpRange, @@ -43,9 +44,9 @@ use nexus_types::external_api::multicast::{ MulticastGroupJoinSpec, MulticastGroupMember, }; use nexus_types::identity::{Asset, Resource}; +use nexus_types_versions::latest::instance::Instance; use omicron_common::api::external::{ ByteCount, DataPageParams, Hostname, IdentityMetadataCreateParams, - Instance, InstanceCpuCount, InstanceState, }; use omicron_nexus::TestInterfaces; use omicron_test_utils::dev::poll::{self, CondCheckError, wait_for_condition}; @@ -1202,6 +1203,7 @@ pub(crate) async fn instance_for_multicast_groups( start, auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), + enable_jumbo_frames: false, }, ) .await diff --git a/nexus/tests/integration_tests/multicast/networking_integration.rs b/nexus/tests/integration_tests/multicast/networking_integration.rs index da7c70b1eb9..57e90569cda 100644 --- a/nexus/tests/integration_tests/multicast/networking_integration.rs +++ b/nexus/tests/integration_tests/multicast/networking_integration.rs @@ -22,13 +22,13 @@ use nexus_types::external_api::floating_ip::{ AddressAllocator, FloatingIp, FloatingIpAttach, }; use nexus_types::external_api::instance::{ - EphemeralIpCreate, ExternalIpCreate, InstanceCreate, + EphemeralIpCreate, ExternalIpCreate, InstanceCpuCount, InstanceCreate, InstanceNetworkInterfaceAttachment, }; use nexus_types::external_api::ip_pool::{IpVersion, PoolSelector}; +use nexus_types_versions::latest::instance::Instance; use omicron_common::api::external::{ - ByteCount, IdentityMetadataCreateParams, Instance, InstanceCpuCount, - NameOrId, + ByteCount, IdentityMetadataCreateParams, NameOrId, }; use omicron_test_utils::dev::poll::{CondCheckError, wait_for_condition}; use omicron_uuid_kinds::{GenericUuid, InstanceUuid}; @@ -97,6 +97,7 @@ async fn test_multicast_external_ip_scenarios( start: true, // Start the instance auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), + enable_jumbo_frames: false, }; let instance_url = format!("/v1/instances?project={project_name}"); @@ -243,6 +244,7 @@ async fn test_multicast_external_ip_scenarios( start: true, auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), + enable_jumbo_frames: false, }; let instance_url = format!("/v1/instances?project={project_name}"); @@ -406,6 +408,7 @@ async fn test_multicast_external_ip_scenarios( start: true, auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), + enable_jumbo_frames: false, }; let instance_url = format!("/v1/instances?project={project_name}"); @@ -529,6 +532,7 @@ async fn test_multicast_with_floating_ip_basic( start: true, // Start the instance auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), + enable_jumbo_frames: false, }; let instance_url = format!("/v1/instances?project={project_name}"); diff --git a/package-manifest.toml b/package-manifest.toml index 4bfc1ce08e7..becd6ae69da 100644 --- a/package-manifest.toml +++ b/package-manifest.toml @@ -667,10 +667,10 @@ service_name = "propolis-server" only_for_targets.image = "standard" source.type = "prebuilt" source.repo = "propolis" -source.commit = "979b72896bc39a876c98fb32fe706da37a4fc408" +source.commit = "dcad23754c05c74644c9bb0954ca7ed13dca8381" # The SHA256 digest is automatically posted to: # https://buildomat.eng.oxide.computer/public/file/oxidecomputer/propolis/image//propolis-server.sha256.txt -source.sha256 = "b950c8da9e2bb61eeb3b7087c27b2edab6c1b341ae8e7db6e88e7c172af58aff" +source.sha256 = "1b89f32fcacd1881f4ce82d2c87bb3e8e4b9bd45e5f8c43434bea73b592a3929" output.type = "zone" [package.mg-ddm-gz] @@ -683,10 +683,10 @@ source.repo = "maghemite" # `tools/maghemite_openapi_version`. Failing to do so will cause a failure when # building `ddm-admin-client` (which will instruct you to update # `tools/maghemite_openapi_version`). -source.commit = "65cc70aa5507f779551a1d33c0089bb0a9b7ac72" +source.commit = "0509f50053149b5ab87e3e33bf291baab729fe5b" # The SHA256 digest is automatically posted to: # https://buildomat.eng.oxide.computer/public/file/oxidecomputer/maghemite/image//mg-ddm-gz.sha256.txt -source.sha256 = "8a4305550621611cbec322bce89521154468917c5aebb6da29846b513a062e83" +source.sha256 = "475cb8b5ded4a26bf0e431d29c0a2104da83e91609de10f7a0c3c7e038032874" output.type = "tarball" [package.mg-ddm] @@ -699,10 +699,10 @@ source.repo = "maghemite" # `tools/maghemite_openapi_version`. Failing to do so will cause a failure when # building `ddm-admin-client` (which will instruct you to update # `tools/maghemite_openapi_version`). -source.commit = "65cc70aa5507f779551a1d33c0089bb0a9b7ac72" +source.commit = "0509f50053149b5ab87e3e33bf291baab729fe5b" # The SHA256 digest is automatically posted to: # https://buildomat.eng.oxide.computer/public/file/oxidecomputer/maghemite/image//mg-ddm.sha256.txt -source.sha256 = "2bdede0e7d28463487317b3deb3312f2c38a9dd63c70b8d9acc2423243560d4f" +source.sha256 = "3d7ed45afd03049fcc3df620b65e17dfe139b909e98230e7d8fcb3b7380937b1" output.type = "zone" output.intermediate_only = true @@ -714,10 +714,10 @@ source.repo = "maghemite" # `tools/maghemite_openapi_version`. Failing to do so will cause a failure when # building `ddm-admin-client` (which will instruct you to update # `tools/maghemite_openapi_version`). -source.commit = "65cc70aa5507f779551a1d33c0089bb0a9b7ac72" +source.commit = "0509f50053149b5ab87e3e33bf291baab729fe5b" # The SHA256 digest is automatically posted to: # https://buildomat.eng.oxide.computer/public/file/oxidecomputer/maghemite/image//mgd.sha256.txt -source.sha256 = "910aba7568b3dbdadebb08f97a6726a2a8213f438c0dfce0d8164cc2d3b9aab8" +source.sha256 = "d3b6282fd1a42b1853f38f17f0bbea8220a3ffef56206ffc113f9df6118d8aab" output.type = "zone" output.intermediate_only = true @@ -765,8 +765,8 @@ only_for_targets.image = "standard" # the other `source.*` keys. source.type = "prebuilt" source.repo = "dendrite" -source.commit = "cc0c307c617f2988aafdca4e3bd35ea178b64801" -source.sha256 = "9c1cf3ec68ba2317c5f52b8dad7c6aa8bfe5e657e8510ca22033ec35ceb36ea5" +source.commit = "96483d0c52f0cfc4e22e70229466798254a99b6c" +source.sha256 = "40bc6c5951e13e1f6e2cdddf9cf4887ecf888b8e44133ea345357585da9e6785" output.type = "zone" output.intermediate_only = true @@ -792,8 +792,8 @@ only_for_targets.image = "standard" # the other `source.*` keys. source.type = "prebuilt" source.repo = "dendrite" -source.commit = "cc0c307c617f2988aafdca4e3bd35ea178b64801" -source.sha256 = "45883d2b9ab045daf71b8e3566671bdd352058bbe95f7f67bc7d7f52c2ef87de" +source.commit = "96483d0c52f0cfc4e22e70229466798254a99b6c" +source.sha256 = "ebe2929f6440116ffe9b02f55ab8e32203e03a7f61d50ff6871e9c957a947af4" output.type = "zone" output.intermediate_only = true @@ -812,8 +812,8 @@ only_for_targets.image = "standard" # the other `source.*` keys. source.type = "prebuilt" source.repo = "dendrite" -source.commit = "cc0c307c617f2988aafdca4e3bd35ea178b64801" -source.sha256 = "8b31347c6ac55395ed488dbbdb5cec392b536b3ef151c6e28b49c22c1b160def" +source.commit = "96483d0c52f0cfc4e22e70229466798254a99b6c" +source.sha256 = "7468635952167defc5d470473a2b0a2eca73b9480c1a2581bce991877b9ad340" output.type = "zone" output.intermediate_only = true @@ -833,8 +833,8 @@ setup_hint = "Run `cargo xtask download transceiver-control` to download the nec service_name = "thundermuffin" source.type = "prebuilt" source.repo = "thundermuffin" -source.commit = "a4a6108d7c9aac2464a0b6898e88132a8f701a13" -source.sha256 = "dc55a2accd33a347df4cbdc0026cbaccea2c004940c3fec8cadcdd633d440dfa" +source.commit = "fda704ee409c8ed50056f3ad96999951b2bbfa7f" +source.sha256 = "aa938be76cf2eb2071f0edc0ffc8a125d6c867d0a20da2e1c9178925b8a209a5" output.type = "zone" output.intermediate_only = true diff --git a/sled-agent/src/sim/collection.rs b/sled-agent/src/sim/collection.rs index 56bcb3aaf3d..84a31190cb8 100644 --- a/sled-agent/src/sim/collection.rs +++ b/sled-agent/src/sim/collection.rs @@ -245,7 +245,11 @@ impl SimCollection { // particular, we want to finish this work before calling out to // notify the nexus. let mut objects = self.objects.lock().await; - let mut object = objects.remove(&id).unwrap(); + let Some(mut object) = objects.remove(&id) else { + // Instance was already removed (e.g., destroyed by a + // concurrent transition). Nothing left to do. + break; + }; object.transition_finish(); let after = object.object.current().clone(); diff --git a/smf/nexus/multi-sled/config-partial.toml b/smf/nexus/multi-sled/config-partial.toml index 2cbbfc36034..ce2dc83cba0 100644 --- a/smf/nexus/multi-sled/config-partial.toml +++ b/smf/nexus/multi-sled/config-partial.toml @@ -129,6 +129,11 @@ audit_log_cleanup.retention_days = 90 audit_log_cleanup.max_deleted_per_activation = 10000 populate_switch_ports.period_secs = 30 +[multicast] +# Enable multicast switch programming and the group reconciler. The multicast +# API is served regardless of this setting. +enabled = true + [default_region_allocation_strategy] # by default, allocate across 3 distinct sleds # seed is omitted so a new seed will be chosen with every allocation. diff --git a/smf/nexus/single-sled/config-partial.toml b/smf/nexus/single-sled/config-partial.toml index 93e255dfbb0..5332183593c 100644 --- a/smf/nexus/single-sled/config-partial.toml +++ b/smf/nexus/single-sled/config-partial.toml @@ -129,6 +129,11 @@ audit_log_cleanup.retention_days = 90 audit_log_cleanup.max_deleted_per_activation = 10000 populate_switch_ports.period_secs = 30 +[multicast] +# Enable multicast switch programming and the group reconciler. The multicast +# API is served regardless of this setting. +enabled = true + [default_region_allocation_strategy] # by default, allocate without requirement for distinct sleds. # seed is omitted so a new seed will be chosen with every allocation. diff --git a/tools/dendrite_stub_checksums b/tools/dendrite_stub_checksums index 35e29d37b2a..065124802d9 100644 --- a/tools/dendrite_stub_checksums +++ b/tools/dendrite_stub_checksums @@ -1,3 +1,3 @@ -CIDL_SHA256_ILLUMOS="9c1cf3ec68ba2317c5f52b8dad7c6aa8bfe5e657e8510ca22033ec35ceb36ea5" -CIDL_SHA256_LINUX_DPD="1e721729ee74715458efd2f5202e02d1d086d733acf0b6f4c88cb1cb4ad8067e" -CIDL_SHA256_LINUX_SWADM="ed4b565ee8303f74eff059547a85573453c99252c5219058768ed076af44e247" +CIDL_SHA256_ILLUMOS="40bc6c5951e13e1f6e2cdddf9cf4887ecf888b8e44133ea345357585da9e6785" +CIDL_SHA256_LINUX_DPD="45d4db6707974925078f781919ba1eefca69c3f2874d52064712ea89ac966001" +CIDL_SHA256_LINUX_SWADM="e574bf7df20e77ada4da9d72dfc422eaaceeca26645af4d8c9b8db45f90d391f" diff --git a/tools/dendrite_version b/tools/dendrite_version index 39aabd660aa..f3bce0ef8b3 100644 --- a/tools/dendrite_version +++ b/tools/dendrite_version @@ -1 +1 @@ -COMMIT="cc0c307c617f2988aafdca4e3bd35ea178b64801" +COMMIT="96483d0c52f0cfc4e22e70229466798254a99b6c" diff --git a/tools/maghemite_ddm_openapi_version b/tools/maghemite_ddm_openapi_version index ddff44595ff..bba376c28ac 100644 --- a/tools/maghemite_ddm_openapi_version +++ b/tools/maghemite_ddm_openapi_version @@ -1 +1 @@ -COMMIT="65cc70aa5507f779551a1d33c0089bb0a9b7ac72" +COMMIT="0509f50053149b5ab87e3e33bf291baab729fe5b" diff --git a/tools/maghemite_mg_openapi_version b/tools/maghemite_mg_openapi_version index ddff44595ff..bba376c28ac 100644 --- a/tools/maghemite_mg_openapi_version +++ b/tools/maghemite_mg_openapi_version @@ -1 +1 @@ -COMMIT="65cc70aa5507f779551a1d33c0089bb0a9b7ac72" +COMMIT="0509f50053149b5ab87e3e33bf291baab729fe5b" diff --git a/tools/maghemite_mgd_checksums b/tools/maghemite_mgd_checksums index 73df8447bb8..862a4935f57 100644 --- a/tools/maghemite_mgd_checksums +++ b/tools/maghemite_mgd_checksums @@ -1,4 +1,4 @@ -CIDL_SHA256="910aba7568b3dbdadebb08f97a6726a2a8213f438c0dfce0d8164cc2d3b9aab8" -MGD_LINUX_SHA256="65d9afe892f042b74768d10da18867bdd2b7938c3592b8a5cd1a26a162c11132" -MG_DDM_SHA256="2bdede0e7d28463487317b3deb3312f2c38a9dd63c70b8d9acc2423243560d4f" -DDMD_LINUX_SHA256="c5e19f18cf30cc46a6d0e241acadc580c158950108943f86f38d8e9a18d97b1b" \ No newline at end of file +CIDL_SHA256="d3b6282fd1a42b1853f38f17f0bbea8220a3ffef56206ffc113f9df6118d8aab" +MGD_LINUX_SHA256="cb467dd3f57c8b20045d8ca155b35ce3be5064303329267f279b5bb98d347304" +MG_DDM_SHA256="3d7ed45afd03049fcc3df620b65e17dfe139b909e98230e7d8fcb3b7380937b1" +DDMD_LINUX_SHA256="87fcde154f0cdd06f394806288880bd6ba9fc6ba95ce500ecfe4f5d0a0579217" \ No newline at end of file diff --git a/tools/opte_version_override b/tools/opte_version_override index 288a0febf15..2454499745a 100644 --- a/tools/opte_version_override +++ b/tools/opte_version_override @@ -21,4 +21,4 @@ # with a matching Cargo bump. # # To deactivate (once the new version is published): set OPTE_COMMIT to "". -OPTE_COMMIT="" +OPTE_COMMIT="f4433191fba909e99e9b27e9e9700d916b96f38d" diff --git a/tools/softnpu_version b/tools/softnpu_version index 0fc9248f4f1..95f6d356e81 100644 --- a/tools/softnpu_version +++ b/tools/softnpu_version @@ -1,2 +1,2 @@ -COMMIT="e4c3130538b192988737161f4a4dc97272f02b9c" -SHA2="5776bfcd80b685080c383b19b04b46ba9b985de18f722f1523f456d7ce59d0c5" +COMMIT="284c6830722548714128e63ea04bcca78ee27154" +SHA2="9dc7bc632692d2f895296bc3d2af4b1a6e75c2d2942d7512c5792d5aab6d8411" diff --git a/tools/thundermuffin_checksums b/tools/thundermuffin_checksums index 5e10539bdd5..79f42c4f530 100644 --- a/tools/thundermuffin_checksums +++ b/tools/thundermuffin_checksums @@ -1 +1 @@ -CIDL_SHA256="dc55a2accd33a347df4cbdc0026cbaccea2c004940c3fec8cadcdd633d440dfa" +CIDL_SHA256="aa938be76cf2eb2071f0edc0ffc8a125d6c867d0a20da2e1c9178925b8a209a5" diff --git a/tools/thundermuffin_version b/tools/thundermuffin_version index cbca739f5c8..66765e1db2c 100644 --- a/tools/thundermuffin_version +++ b/tools/thundermuffin_version @@ -1 +1 @@ -COMMIT="a4a6108d7c9aac2464a0b6898e88132a8f701a13" +COMMIT="fda704ee409c8ed50056f3ad96999951b2bbfa7f" diff --git a/tools/update_crucible.sh b/tools/update_crucible.sh index be0b7d91e84..e0d4b913d03 100755 --- a/tools/update_crucible.sh +++ b/tools/update_crucible.sh @@ -29,6 +29,7 @@ CRATES=( "crucible-agent-client" "crucible-pantry-client" "crucible-smf" + "crucible-common" ) REPO="oxidecomputer/crucible"