diff --git a/Cargo.lock b/Cargo.lock index e83da904c92..668ec7b1a9a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -102,7 +102,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" dependencies = [ "anstyle", - "anstyle-parse", + "anstyle-parse 0.2.7", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse 1.0.0", "anstyle-query", "anstyle-wincon", "colorchoice", @@ -112,9 +127,9 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" @@ -125,6 +140,15 @@ dependencies = [ "utf8parse", ] +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + [[package]] name = "anstyle-query" version = "1.1.5" @@ -1041,7 +1065,7 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8c236e531d024b1524669ee2a56eca09ab8a40f3395dc2728cde9defa9c60d8" dependencies = [ - "anstream", + "anstream 0.6.21", "anstyle", "camino", "camino-tempfile", @@ -1233,7 +1257,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" dependencies = [ - "nom", + "nom 7.1.3", ] [[package]] @@ -1402,9 +1426,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.60" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" dependencies = [ "clap_builder", "clap_derive", @@ -1412,11 +1436,11 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.60" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ - "anstream", + "anstream 1.0.0", "anstyle", "clap_lex", "strsim", @@ -1425,9 +1449,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.55" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -1603,6 +1627,11 @@ dependencies = [ "tokio", ] +[[package]] +name = "client-common" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/maghemite?rev=b27bea5344fb21b3b88af6717b969e31ee6a9ccb#b27bea5344fb21b3b88af6717b969e31ee6a9ccb" + [[package]] name = "clipboard-win" version = "5.4.1" @@ -1720,7 +1749,7 @@ dependencies = [ [[package]] name = "common" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/dendrite?branch=main#f20f786e67c86388dfaaf0ef3aa9d2693dfe1da4" +source = "git+https://github.com/oxidecomputer/dendrite?branch=main#cc0c307c617f2988aafdca4e3bd35ea178b64801" dependencies = [ "anyhow", "chrono", @@ -2525,16 +2554,29 @@ dependencies = [ [[package]] name = "ddm-admin-client" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/maghemite?rev=7696ee48d5ee29a917dea459e281fe2e8ff20513#7696ee48d5ee29a917dea459e281fe2e8ff20513" +source = "git+https://github.com/oxidecomputer/maghemite?rev=b27bea5344fb21b3b88af6717b969e31ee6a9ccb#b27bea5344fb21b3b88af6717b969e31ee6a9ccb" dependencies = [ + "ddm-api-types-versions", "oxnet", - "progenitor 0.13.0", + "progenitor 0.14.0", "reqwest 0.13.2", "serde", "slog", "uuid", ] +[[package]] +name = "ddm-api-types-versions" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/maghemite?rev=b27bea5344fb21b3b88af6717b969e31ee6a9ccb#b27bea5344fb21b3b88af6717b969e31ee6a9ccb" +dependencies = [ + "oxnet", + "schemars 0.8.22", + "serde", + "serde_repr", + "uuid", +] + [[package]] name = "debug-ignore" version = "1.0.5" @@ -2941,7 +2983,7 @@ source = "git+https://github.com/oxidecomputer/dlpi-sys#d9645f8d61187e76384474b1 dependencies = [ "libc", "libdlpi-sys", - "num_enum 0.7.5", + "num_enum 0.7.6", "pretty-hex", "thiserror 2.0.18", ] @@ -3084,7 +3126,7 @@ checksum = "117240f60069e65410b3ae1bb213295bd828f707b5bec6596a1afc8793ce0cbc" [[package]] name = "dpd-client" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/dendrite?branch=main#f20f786e67c86388dfaaf0ef3aa9d2693dfe1da4" +source = "git+https://github.com/oxidecomputer/dendrite?branch=main#cc0c307c617f2988aafdca4e3bd35ea178b64801" dependencies = [ "async-trait", "chrono", @@ -3579,7 +3621,7 @@ version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2daee4ea451f429a58296525ddf28b45a3b64f1acf6587e2067437bb11e218d" dependencies = [ - "anstream", + "anstream 0.6.21", "anstyle", "env_filter", "log", @@ -5322,7 +5364,7 @@ checksum = "a4b8219c9c8c2c844dfd5772ec0bda5cd2a81d78c4579aba97f699721d46ab24" [[package]] name = "illumos-sys-hdrs" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/opte?rev=2c6efefe14321dafe7e9e80129d38316adb2d238#2c6efefe14321dafe7e9e80129d38316adb2d238" +source = "git+https://github.com/oxidecomputer/opte?rev=84e91b62621406f38e4d0870a92bafacad96536e#84e91b62621406f38e4d0870a92bafacad96536e" dependencies = [ "bitflags 2.11.0", ] @@ -6005,7 +6047,7 @@ dependencies = [ [[package]] name = "kstat-macro" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/opte?rev=2c6efefe14321dafe7e9e80129d38316adb2d238#2c6efefe14321dafe7e9e80129d38316adb2d238" +source = "git+https://github.com/oxidecomputer/opte?rev=84e91b62621406f38e4d0870a92bafacad96536e#84e91b62621406f38e4d0870a92bafacad96536e" dependencies = [ "quote", "syn 2.0.117", @@ -6166,7 +6208,7 @@ dependencies = [ "colored 3.1.1", "dlpi", "libc", - "num_enum 0.7.5", + "num_enum 0.7.6", "nvpair", "nvpair-sys", "oxnet", @@ -6243,7 +6285,7 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5297962ef19edda4ce33aaa484386e0a5b3d7f2f4e037cbeee00503ef6b29d33" dependencies = [ - "anstream", + "anstream 0.6.21", "anstyle", "clap", "escape8259", @@ -6340,14 +6382,15 @@ dependencies = [ [[package]] name = "lldpd-client" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/lldp#d22509dfdb051321b859e924948605115691b93c" +source = "git+https://github.com/oxidecomputer/lldp#54b266174d4de9628bca9c97b0db176e16f12154" dependencies = [ "chrono", "futures", "lldpd-common", - "progenitor 0.11.2", + "lldpd-types-versions", + "progenitor 0.13.0", "protocol", - "reqwest 0.12.28", + "reqwest 0.13.2", "schemars 0.8.22", "serde", "serde_json", @@ -6358,7 +6401,7 @@ dependencies = [ [[package]] name = "lldpd-common" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/lldp#d22509dfdb051321b859e924948605115691b93c" +source = "git+https://github.com/oxidecomputer/lldp#54b266174d4de9628bca9c97b0db176e16f12154" dependencies = [ "anyhow", "dpd-client 0.1.0 (git+https://github.com/oxidecomputer/dendrite?branch=main)", @@ -6372,6 +6415,18 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "lldpd-types-versions" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/lldp#54b266174d4de9628bca9c97b0db176e16f12154" +dependencies = [ + "chrono", + "protocol", + "schemars 0.8.22", + "serde", + "uuid", +] + [[package]] name = "lock_api" version = "0.4.14" @@ -6542,12 +6597,13 @@ dependencies = [ [[package]] name = "mg-admin-client" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/maghemite?rev=7696ee48d5ee29a917dea459e281fe2e8ff20513#7696ee48d5ee29a917dea459e281fe2e8ff20513" +source = "git+https://github.com/oxidecomputer/maghemite?rev=b27bea5344fb21b3b88af6717b969e31ee6a9ccb#b27bea5344fb21b3b88af6717b969e31ee6a9ccb" dependencies = [ "chrono", + "client-common", "colored 3.1.1", - "progenitor 0.13.0", - "rdb-types", + "mg-api-types-versions", + "progenitor 0.14.0", "reqwest 0.13.2", "schemars 0.8.22", "serde", @@ -6557,6 +6613,30 @@ dependencies = [ "uuid", ] +[[package]] +name = "mg-api-types" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/maghemite?rev=b27bea5344fb21b3b88af6717b969e31ee6a9ccb#b27bea5344fb21b3b88af6717b969e31ee6a9ccb" +dependencies = [ + "mg-api-types-versions", +] + +[[package]] +name = "mg-api-types-versions" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/maghemite?rev=b27bea5344fb21b3b88af6717b969e31ee6a9ccb#b27bea5344fb21b3b88af6717b969e31ee6a9ccb" +dependencies = [ + "chrono", + "nom 8.0.0", + "num_enum 0.7.6", + "oxnet", + "schemars 0.8.22", + "serde", + "slog", + "thiserror 2.0.18", + "uuid", +] + [[package]] name = "mgs-dev" version = "0.1.0" @@ -7848,6 +7928,7 @@ dependencies = [ "dropshot 0.17.0", "http", "mg-admin-client", + "mg-api-types", "omicron-common", "omicron-passwords", "omicron-uuid-kinds", @@ -7946,6 +8027,15 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + [[package]] name = "nonempty" version = "0.12.0" @@ -8156,11 +8246,11 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" dependencies = [ - "num_enum_derive 0.7.5", + "num_enum_derive 0.7.6", "rustversion", ] @@ -8178,9 +8268,9 @@ dependencies = [ [[package]] name = "num_enum_derive" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" dependencies = [ "proc-macro-crate 3.4.0", "proc-macro2", @@ -8723,6 +8813,7 @@ dependencies = [ "macaddr", "maplit", "mg-admin-client", + "mg-api-types", "nexus-auth", "nexus-background-task-interface", "nexus-client", @@ -8789,7 +8880,6 @@ dependencies = [ "range-requests", "raw-cpuid", "rcgen", - "rdb-types", "ref-cast", "regex", "reqwest 0.12.28", @@ -9360,6 +9450,7 @@ version = "0.1.0" dependencies = [ "ahash", "aho-corasick", + "anstream 0.6.21", "anyhow", "aws-lc-rs", "base16ct", @@ -9660,7 +9751,7 @@ dependencies = [ [[package]] name = "opte" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/opte?rev=2c6efefe14321dafe7e9e80129d38316adb2d238#2c6efefe14321dafe7e9e80129d38316adb2d238" +source = "git+https://github.com/oxidecomputer/opte?rev=84e91b62621406f38e4d0870a92bafacad96536e#84e91b62621406f38e4d0870a92bafacad96536e" dependencies = [ "bitflags 2.11.0", "dyn-clone", @@ -9679,7 +9770,7 @@ dependencies = [ [[package]] name = "opte-api" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/opte?rev=2c6efefe14321dafe7e9e80129d38316adb2d238#2c6efefe14321dafe7e9e80129d38316adb2d238" +source = "git+https://github.com/oxidecomputer/opte?rev=84e91b62621406f38e4d0870a92bafacad96536e#84e91b62621406f38e4d0870a92bafacad96536e" dependencies = [ "illumos-sys-hdrs", "ingot", @@ -9692,7 +9783,7 @@ dependencies = [ [[package]] name = "opte-ioctl" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/opte?rev=2c6efefe14321dafe7e9e80129d38316adb2d238#2c6efefe14321dafe7e9e80129d38316adb2d238" +source = "git+https://github.com/oxidecomputer/opte?rev=84e91b62621406f38e4d0870a92bafacad96536e#84e91b62621406f38e4d0870a92bafacad96536e" dependencies = [ "libc", "libnet", @@ -9789,7 +9880,7 @@ dependencies = [ [[package]] name = "oxide-vpc" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/opte?rev=2c6efefe14321dafe7e9e80129d38316adb2d238#2c6efefe14321dafe7e9e80129d38316adb2d238" +source = "git+https://github.com/oxidecomputer/opte?rev=84e91b62621406f38e4d0870a92bafacad96536e#84e91b62621406f38e4d0870a92bafacad96536e" dependencies = [ "cfg-if", "illumos-sys-hdrs", @@ -9946,7 +10037,7 @@ dependencies = [ "indexmap 2.14.0", "itertools 0.14.0", "libc", - "nom", + "nom 7.1.3", "num", "omicron-common", "omicron-test-utils", @@ -10242,9 +10333,9 @@ dependencies = [ [[package]] name = "oxnet" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dc6fb07ecd6d2a17ff1431bc5b3ce11036c0b6dd93a3c4904db5b910817b162" +checksum = "057865b45bb202b17ed475d8f22f0416412de2c317c168fefecf9d207faf048d" dependencies = [ "ipnetwork", "schemars 0.8.22", @@ -10657,7 +10748,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9cd31dcfdbbd7431a807ef4df6edd6473228e94d5c805e8cf671227a21bad068" dependencies = [ "anyhow", - "itertools 0.14.0", + "itertools 0.12.1", "proc-macro2", "quote", "rand 0.8.6", @@ -11521,7 +11612,7 @@ dependencies = [ [[package]] name = "protocol" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/lldp#d22509dfdb051321b859e924948605115691b93c" +source = "git+https://github.com/oxidecomputer/lldp#54b266174d4de9628bca9c97b0db176e16f12154" dependencies = [ "anyhow", "schemars 0.8.22", @@ -11904,16 +11995,6 @@ dependencies = [ "yasna", ] -[[package]] -name = "rdb-types" -version = "0.1.0" -source = "git+https://github.com/oxidecomputer/maghemite?rev=7696ee48d5ee29a917dea459e281fe2e8ff20513#7696ee48d5ee29a917dea459e281fe2e8ff20513" -dependencies = [ - "oxnet", - "schemars 0.8.22", - "serde", -] - [[package]] name = "reconfigurator-cli" version = "0.1.0" @@ -12238,6 +12319,7 @@ dependencies = [ "bytes", "cookie", "cookie_store", + "encoding_rs", "futures-channel", "futures-core", "futures-util", @@ -12250,6 +12332,7 @@ dependencies = [ "hyper-util", "js-sys", "log", + "mime", "percent-encoding", "pin-project-lite", "quinn", @@ -13630,11 +13713,11 @@ dependencies = [ "internal-dns-resolver", "internal-dns-types", "mg-admin-client", + "mg-api-types", "omicron-common", "omicron-ddm-admin-client", "omicron-workspace-hack", "oxnet", - "rdb-types", "sled-agent-types", "slog", "slog-error-chain", @@ -14188,7 +14271,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", @@ -15628,7 +15711,7 @@ dependencies = [ [[package]] name = "transceiver-controller" version = "0.1.1" -source = "git+https://github.com/oxidecomputer/transceiver-control?branch=main#11afc484d5957b13d3058e44db274aa720cea1c4" +source = "git+https://github.com/oxidecomputer/transceiver-control?branch=main#81167659157860d6587713b3362b7bc791dfb530" dependencies = [ "anyhow", "clap", @@ -15676,7 +15759,7 @@ dependencies = [ [[package]] name = "transceiver-decode" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/transceiver-control?branch=main#11afc484d5957b13d3058e44db274aa720cea1c4" +source = "git+https://github.com/oxidecomputer/transceiver-control?branch=main#81167659157860d6587713b3362b7bc791dfb530" dependencies = [ "schemars 0.8.22", "serde", @@ -15700,7 +15783,7 @@ dependencies = [ [[package]] name = "transceiver-messages" version = "0.1.1" -source = "git+https://github.com/oxidecomputer/transceiver-control?branch=main#11afc484d5957b13d3058e44db274aa720cea1c4" +source = "git+https://github.com/oxidecomputer/transceiver-control?branch=main#81167659157860d6587713b3362b7bc791dfb530" dependencies = [ "bitflags 2.11.0", "clap", diff --git a/Cargo.toml b/Cargo.toml index 0a9cc97e5c7..3fb98f6475f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -611,8 +611,8 @@ ntp-admin-client = { path = "clients/ntp-admin-client" } ntp-admin-v1-client = { path = "clients/ntp-admin-v1-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 = "7696ee48d5ee29a917dea459e281fe2e8ff20513" } -ddm-admin-client = { git = "https://github.com/oxidecomputer/maghemite", rev = "7696ee48d5ee29a917dea459e281fe2e8ff20513" } +mg-admin-client = { git = "https://github.com/oxidecomputer/maghemite", rev = "b27bea5344fb21b3b88af6717b969e31ee6a9ccb" } +ddm-admin-client = { git = "https://github.com/oxidecomputer/maghemite", rev = "b27bea5344fb21b3b88af6717b969e31ee6a9ccb" } multimap = "0.10.1" nexus-auth = { path = "nexus/auth" } nexus-background-task-interface = { path = "nexus/background-task-interface" } @@ -674,16 +674,16 @@ omicron-workspace-hack = "0.1.0" omicron-zone-package = "0.12.2" oxide-client = { path = "clients/oxide-client" } oxide-tokio-rt = "0.1.4" -oxide-vpc = { git = "https://github.com/oxidecomputer/opte", rev = "2c6efefe14321dafe7e9e80129d38316adb2d238", features = [ "api", "std" ] } +oxide-vpc = { git = "https://github.com/oxidecomputer/opte", rev = "84e91b62621406f38e4d0870a92bafacad96536e", features = [ "api", "std" ] } oxlog = { path = "dev-tools/oxlog" } -oxnet = "0.1.4" +oxnet = "0.1.5" 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 = "2c6efefe14321dafe7e9e80129d38316adb2d238" } +opte-ioctl = { git = "https://github.com/oxidecomputer/opte", rev = "84e91b62621406f38e4d0870a92bafacad96536e" } oso = "0.27" owo-colors = "4.2.2" oximeter = { path = "oximeter/oximeter" } @@ -749,7 +749,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 = "0.12.1" -rdb-types = { git = "https://github.com/oxidecomputer/maghemite", rev = "7696ee48d5ee29a917dea459e281fe2e8ff20513" } +mg-api-types = { git = "https://github.com/oxidecomputer/maghemite", rev = "b27bea5344fb21b3b88af6717b969e31ee6a9ccb" } reconfigurator-cli = { path = "dev-tools/reconfigurator-cli" } reedline = "0.40.0" ref-cast = "1.0" diff --git a/common/src/address.rs b/common/src/address.rs index af34bcff8f7..5aeca941e56 100644 --- a/common/src/address.rs +++ b/common/src/address.rs @@ -24,6 +24,16 @@ pub const AZ_PREFIX: u8 = 48; pub const RACK_PREFIX: u8 = 56; pub const SLED_PREFIX: u8 = 64; +/// Default Ethernet MTU for external-facing OPTE ports, in bytes. Used when +/// the per-instance jumbo-frames opt-in is unset or when the fleet-wide opt-in +/// is disabled. +pub const EXTERNAL_DEFAULT_MTU: u32 = 1500; + +/// Effective MTU for external-facing OPTE ports when jumbo frames have been +/// opted into. 500 bytes of headroom under the 9000 byte underlay MTU leaves +/// room for encapsulation overhead. +pub const EXTERNAL_JUMBO_FRAMES_MTU: u32 = 8500; + // Multicast constants /// IPv4 Source-Specific Multicast (SSM) subnet. diff --git a/end-to-end-tests/src/instance_launch.rs b/end-to-end-tests/src/instance_launch.rs index 308adb09e29..778e29af9e7 100644 --- a/end-to-end-tests/src/instance_launch.rs +++ b/end-to-end-tests/src/instance_launch.rs @@ -88,6 +88,7 @@ async fn instance_launch() -> Result<()> { anti_affinity_groups: Vec::new(), cpu_platform: None, multicast_groups: Vec::new(), + enable_jumbo_frames: false, }) .send() .await?; diff --git a/illumos-utils/src/opte/non_illumos.rs b/illumos-utils/src/opte/non_illumos.rs index 42487cde09c..8f0c4d3175a 100644 --- a/illumos-utils/src/opte/non_illumos.rs +++ b/illumos-utils/src/opte/non_illumos.rs @@ -209,7 +209,7 @@ impl Handle { &self, name: &str, cfg: VpcCfg, - _: bool, + _mtu: Option, ) -> Result { let name = name.to_string(); let IpCfg::Ipv4(ip_cfg) = cfg.ip_cfg else { diff --git a/illumos-utils/src/opte/port_manager.rs b/illumos-utils/src/opte/port_manager.rs index 638dd52de3d..f2b1b134c1d 100644 --- a/illumos-utils/src/opte/port_manager.rs +++ b/illumos-utils/src/opte/port_manager.rs @@ -147,6 +147,9 @@ pub struct PortCreateParams<'a> { pub firewall_rules: &'a [ResolvedVpcFirewallRule], pub dhcp_config: DhcpCfg, pub attached_subnets: Vec, + /// MTU to set on the xde device, in bytes. If `None`, OPTE applies its + /// default (1500). Used by jumbo-frame opt-in. + pub mtu: Option, } impl<'a> TryFrom<&PortCreateParams<'a>> for IpCfg { @@ -371,6 +374,7 @@ impl PortManager { firewall_rules, dhcp_config, attached_subnets: _, + mtu, } = params; let is_service = matches!(nic.kind, NetworkInterfaceKind::Service { .. }); @@ -410,7 +414,7 @@ impl PortManager { ); let hdl = { let hdl = Handle::new()?; - hdl.create_xde(&port_name, vpc_cfg, /* passthru = */ false)?; + hdl.create_xde(&port_name, vpc_cfg, mtu)?; hdl }; let (port, ticket) = { @@ -1335,6 +1339,7 @@ mod tests { dns6_servers: Vec::new(), }, attached_subnets: vec![], + mtu: None, }) .unwrap(); @@ -1514,6 +1519,7 @@ mod tests { dns6_servers: Vec::new(), }, attached_subnets: vec![], + mtu: None, }) .unwrap(); @@ -1685,6 +1691,7 @@ mod tests { dns6_servers: vec![], }, attached_subnets: vec![], + mtu: None, }; let IpCfg::Ipv4(oxide_vpc::api::Ipv4Cfg { vpc_subnet, @@ -1758,6 +1765,7 @@ mod tests { dns6_servers: vec![], }, attached_subnets: vec![], + mtu: None, }; let IpCfg::Ipv6(oxide_vpc::api::Ipv6Cfg { vpc_subnet, @@ -1842,6 +1850,7 @@ mod tests { dns6_servers: vec![], }, attached_subnets: vec![], + mtu: None, }; let IpCfg::DualStack { ipv4, ipv6 } = IpCfg::try_from(&prs).unwrap() else { @@ -1932,6 +1941,7 @@ mod tests { dns6_servers: vec![], }, attached_subnets: vec![], + mtu: None, }; let _ = IpCfg::try_from(&prs).expect_err( "Should fail to convert with public IPv6 and private IPv4", @@ -1978,6 +1988,7 @@ mod tests { dns6_servers: vec![], }, attached_subnets: vec![], + mtu: None, }; let _ = IpCfg::try_from(&prs).expect_err( "Should fail to convert with public IPv4 and private IPv6", diff --git a/nexus/Cargo.toml b/nexus/Cargo.toml index acb8784355c..8e1d291d216 100644 --- a/nexus/Cargo.toml +++ b/nexus/Cargo.toml @@ -96,7 +96,7 @@ qorb.workspace = true rand.workspace = true range-requests.workspace = true ref-cast.workspace = true -rdb-types.workspace = true +mg-api-types.workspace = true regex.workspace = true reqwest = { workspace = true, features = ["http2", "json"] } reqwest012 = { workspace = true } diff --git a/nexus/db-model/src/instance.rs b/nexus/db-model/src/instance.rs index 8e91ffed8da..ede9d6355e1 100644 --- a/nexus/db-model/src/instance.rs +++ b/nexus/db-model/src/instance.rs @@ -136,6 +136,13 @@ pub struct Instance { /// a sled by any other constraints the instance will be incarnated with the /// most general CPU platform supported by the selected sled. pub cpu_platform: Option, + + /// When true, the instance has opted in to jumbo frames (8500 byte MTU) + /// on its primary OPTE interface. The effective MTU also depends on the + /// fleet-wide setting in `system_networking_settings`; if that flag is off + /// the OPTE port is created with the default MTU regardless of this field. + /// Changes to this field only take effect on the next instance restart. + pub enable_jumbo_frames: bool, } impl Instance { @@ -185,6 +192,7 @@ impl Instance { boot_disk_id: None, intended_state, cpu_platform: params.cpu_platform.map(Into::into), + enable_jumbo_frames: params.enable_jumbo_frames, } } diff --git a/nexus/db-model/src/lib.rs b/nexus/db-model/src/lib.rs index bd78fd55f6d..b6c1cd8688c 100644 --- a/nexus/db-model/src/lib.rs +++ b/nexus/db-model/src/lib.rs @@ -195,6 +195,7 @@ mod serde_time_delta; mod silo_auth_settings; mod switch_interface; mod switch_port; +mod system_networking_settings; mod target_release; mod trust_quorum; mod v2p_mapping; @@ -373,6 +374,7 @@ pub use support_bundle::*; pub use switch::*; pub use switch_interface::*; pub use switch_port::*; +pub use system_networking_settings::*; pub use target_release::*; pub use trust_quorum::*; pub use tuf_repo::*; diff --git a/nexus/db-model/src/schema_versions.rs b/nexus/db-model/src/schema_versions.rs index 5caf71e9e28..d03570f70d8 100644 --- a/nexus/db-model/src/schema_versions.rs +++ b/nexus/db-model/src/schema_versions.rs @@ -16,7 +16,7 @@ use std::{collections::BTreeMap, sync::LazyLock}; /// /// This must be updated when you change the database schema. Refer to /// schema/crdb/README.adoc in the root of this repository for details. -pub const SCHEMA_VERSION: Version = Version::new(261, 0, 0); +pub const SCHEMA_VERSION: Version = Version::new(262, 0, 0); /// List of all past database schema versions, in *reverse* order /// @@ -28,6 +28,7 @@ pub static KNOWN_VERSIONS: LazyLock> = LazyLock::new(|| { // | leaving the first copy as an example for the next person. // v // KnownVersion::new(next_int, "unique-dirname-with-the-sql-files"), + KnownVersion::new(262, "external-jumbo-frames"), KnownVersion::new(261, "remove-add-zones-with-mupdate-override"), KnownVersion::new(260, "ereport-trim-serial-trailing-nulls"), KnownVersion::new(259, "vmm-failure-reason"), diff --git a/nexus/db-model/src/system_networking_settings.rs b/nexus/db-model/src/system_networking_settings.rs new file mode 100644 index 00000000000..d3212ac109e --- /dev/null +++ b/nexus/db-model/src/system_networking_settings.rs @@ -0,0 +1,37 @@ +// 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/. + +use chrono::{DateTime, Utc}; +use nexus_db_schema::schema::system_networking_settings; +use serde::{Deserialize, Serialize}; + +/// Singleton row holding fleet-wide networking settings. +#[derive( + Queryable, + Insertable, + Debug, + Clone, + Selectable, + Serialize, + Deserialize, + AsChangeset, +)] +#[diesel(table_name = system_networking_settings)] +pub struct SystemNetworkingSettings { + pub singleton: bool, + pub time_created: DateTime, + pub time_modified: DateTime, + /// When true, end users may opt in to jumbo frames on the primary + /// interface of an instance. When false, the per-instance opt-in is + /// ignored and OPTE ports are created with the default MTU. + pub external_jumbo_frames_opt_in_enabled: bool, +} + +/// Updates to the [`SystemNetworkingSettings`] singleton. +#[derive(AsChangeset)] +#[diesel(table_name = system_networking_settings)] +pub struct SystemNetworkingSettingsUpdate { + pub external_jumbo_frames_opt_in_enabled: Option, + pub time_modified: DateTime, +} diff --git a/nexus/db-queries/src/db/datastore/instance.rs b/nexus/db-queries/src/db/datastore/instance.rs index b6d199da917..d585b4aff85 100644 --- a/nexus/db-queries/src/db/datastore/instance.rs +++ b/nexus/db-queries/src/db/datastore/instance.rs @@ -280,6 +280,16 @@ impl From for external::Instance { } } +impl From + for nexus_types::external_api::instance::Instance +{ + fn from(value: InstanceAndActiveVmm) -> Self { + let enable_jumbo_frames = value.instance.enable_jumbo_frames; + let inner: external::Instance = value.into(); + Self { inner, enable_jumbo_frames } + } +} + /// The totality of database records describing the current state of /// an instance: the [`Instance`] record itself, along with its active [`Vmm`], /// target [`Vmm`], and current [`Migration`], if they exist. @@ -1203,6 +1213,26 @@ impl DataStore { Ok(instance_and_vmm) } + /// Update the per-instance jumbo-frames opt-in. Changes take effect on the + /// next instance restart. + pub async fn instance_set_enable_jumbo_frames( + &self, + opctx: &OpContext, + authz_instance: &authz::Instance, + enable_jumbo_frames: bool, + ) -> Result<(), Error> { + opctx.authorize(authz::Action::Modify, authz_instance).await?; + use nexus_db_schema::schema::instance::dsl as instance_dsl; + diesel::update(instance_dsl::instance) + .filter(instance_dsl::id.eq(authz_instance.id())) + .filter(instance_dsl::time_deleted.is_null()) + .set(instance_dsl::enable_jumbo_frames.eq(enable_jumbo_frames)) + .execute_async(&*self.pool_connection_authorized(opctx).await?) + .await + .map_err(|e| public_error_from_diesel(e, ErrorHandler::Server))?; + Ok(()) + } + /// Set the boot disk on an instance, bypassing the rest of an instance /// update. You probably don't need this; it's only used at the end of /// instance creation, since the boot disk can't be set until the new @@ -2376,6 +2406,7 @@ mod tests { auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), + enable_jumbo_frames: false, }, ), ) diff --git a/nexus/db-queries/src/db/datastore/migration.rs b/nexus/db-queries/src/db/datastore/migration.rs index e36789d3ed3..b05a2afd272 100644 --- a/nexus/db-queries/src/db/datastore/migration.rs +++ b/nexus/db-queries/src/db/datastore/migration.rs @@ -242,6 +242,7 @@ mod tests { auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), + enable_jumbo_frames: false, }, ), ) diff --git a/nexus/db-queries/src/db/datastore/mod.rs b/nexus/db-queries/src/db/datastore/mod.rs index 80d2e480584..a691c6836a3 100644 --- a/nexus/db-queries/src/db/datastore/mod.rs +++ b/nexus/db-queries/src/db/datastore/mod.rs @@ -137,6 +137,7 @@ mod support_bundle; mod switch; mod switch_interface; mod switch_port; +mod system_networking_settings; mod target_release; #[cfg(test)] pub(crate) mod test_utils; diff --git a/nexus/db-queries/src/db/datastore/sled.rs b/nexus/db-queries/src/db/datastore/sled.rs index 36872c074c6..13f7f3e88ae 100644 --- a/nexus/db-queries/src/db/datastore/sled.rs +++ b/nexus/db-queries/src/db/datastore/sled.rs @@ -4423,6 +4423,7 @@ pub(in crate::db::datastore) mod test { auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), + enable_jumbo_frames: false, }, ), ) diff --git a/nexus/db-queries/src/db/datastore/system_networking_settings.rs b/nexus/db-queries/src/db/datastore/system_networking_settings.rs new file mode 100644 index 00000000000..a742297b078 --- /dev/null +++ b/nexus/db-queries/src/db/datastore/system_networking_settings.rs @@ -0,0 +1,59 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! Datastore access for fleet-wide networking settings. + +use super::DataStore; +use crate::authz; +use crate::context::OpContext; +use async_bb8_diesel::AsyncRunQueryDsl; +use chrono::Utc; +use diesel::prelude::*; +use nexus_db_errors::ErrorHandler; +use nexus_db_errors::public_error_from_diesel; +use nexus_db_model::SystemNetworkingSettings; +use nexus_db_model::SystemNetworkingSettingsUpdate; +use omicron_common::api::external::Error; +use omicron_common::api::external::UpdateResult; + +impl DataStore { + /// Read the singleton fleet networking settings row. + pub async fn system_networking_settings_view( + &self, + opctx: &OpContext, + ) -> Result { + opctx.authorize(authz::Action::Read, &authz::FLEET).await?; + + use nexus_db_schema::schema::system_networking_settings::dsl; + dsl::system_networking_settings + .filter(dsl::singleton.eq(true)) + .first_async(&*self.pool_connection_authorized(opctx).await?) + .await + .map_err(|e| public_error_from_diesel(e, ErrorHandler::Server)) + } + + /// Update the singleton fleet networking settings row. Fields left as + /// `None` in the supplied update are not modified. + pub async fn system_networking_settings_update( + &self, + opctx: &OpContext, + external_jumbo_frames_opt_in_enabled: Option, + ) -> UpdateResult { + opctx.authorize(authz::Action::Modify, &authz::FLEET).await?; + + let updates = SystemNetworkingSettingsUpdate { + external_jumbo_frames_opt_in_enabled, + time_modified: Utc::now(), + }; + + use nexus_db_schema::schema::system_networking_settings::dsl; + diesel::update(dsl::system_networking_settings) + .filter(dsl::singleton.eq(true)) + .set(updates) + .returning(SystemNetworkingSettings::as_returning()) + .get_result_async(&*self.pool_connection_authorized(opctx).await?) + .await + .map_err(|e| public_error_from_diesel(e, ErrorHandler::Server)) + } +} diff --git a/nexus/db-queries/src/db/datastore/virtual_provisioning_collection.rs b/nexus/db-queries/src/db/datastore/virtual_provisioning_collection.rs index 1cf604d102b..9cba5b26186 100644 --- a/nexus/db-queries/src/db/datastore/virtual_provisioning_collection.rs +++ b/nexus/db-queries/src/db/datastore/virtual_provisioning_collection.rs @@ -462,6 +462,7 @@ mod test { auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), + enable_jumbo_frames: false, }, ), ) diff --git a/nexus/db-queries/src/db/datastore/vpc.rs b/nexus/db-queries/src/db/datastore/vpc.rs index 4a79499dda8..e1a594c646b 100644 --- a/nexus/db-queries/src/db/datastore/vpc.rs +++ b/nexus/db-queries/src/db/datastore/vpc.rs @@ -4023,6 +4023,7 @@ mod tests { auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), + enable_jumbo_frames: false, }, ), ) diff --git a/nexus/db-queries/src/db/pub_test_utils/helpers.rs b/nexus/db-queries/src/db/pub_test_utils/helpers.rs index 72b6a05cf47..44da3928a65 100644 --- a/nexus/db-queries/src/db/pub_test_utils/helpers.rs +++ b/nexus/db-queries/src/db/pub_test_utils/helpers.rs @@ -253,6 +253,7 @@ pub async fn create_stopped_instance_record( auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), + enable_jumbo_frames: false, }, ); diff --git a/nexus/db-queries/src/db/queries/external_ip.rs b/nexus/db-queries/src/db/queries/external_ip.rs index 34e9f06baac..909743070f9 100644 --- a/nexus/db-queries/src/db/queries/external_ip.rs +++ b/nexus/db-queries/src/db/queries/external_ip.rs @@ -1044,6 +1044,7 @@ mod tests { auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), + enable_jumbo_frames: false, }); let conn = self @@ -2426,6 +2427,7 @@ mod tests { auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), + enable_jumbo_frames: false, }); let conn = context diff --git a/nexus/db-queries/src/db/queries/network_interface.rs b/nexus/db-queries/src/db/queries/network_interface.rs index f4affda1547..4d43fcac7bc 100644 --- a/nexus/db-queries/src/db/queries/network_interface.rs +++ b/nexus/db-queries/src/db/queries/network_interface.rs @@ -2004,6 +2004,7 @@ mod tests { auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), + enable_jumbo_frames: false, }; let instance = Instance::new(instance_id, project_id, ¶ms); diff --git a/nexus/db-schema/src/schema.rs b/nexus/db-schema/src/schema.rs index 05d6c96b1db..428aeb57715 100644 --- a/nexus/db-schema/src/schema.rs +++ b/nexus/db-schema/src/schema.rs @@ -467,6 +467,7 @@ table! { boot_disk_id -> Nullable, intended_state -> crate::enums::InstanceIntendedStateEnum, cpu_platform -> Nullable, + enable_jumbo_frames -> Bool, } } @@ -595,6 +596,15 @@ table! { } } +table! { + system_networking_settings(singleton) { + singleton -> Bool, + time_created -> Timestamptz, + time_modified -> Timestamptz, + external_jumbo_frames_opt_in_enabled -> Bool, + } +} + table! { network_interface (id) { id -> Uuid, diff --git a/nexus/external-api/output/nexus_tags.txt b/nexus/external-api/output/nexus_tags.txt index 8035742b535..20f7c265064 100644 --- a/nexus/external-api/output/nexus_tags.txt +++ b/nexus/external-api/output/nexus_tags.txt @@ -295,6 +295,8 @@ networking_switch_port_settings_create POST /v1/system/networking/switch-p networking_switch_port_settings_delete DELETE /v1/system/networking/switch-port-settings networking_switch_port_settings_list GET /v1/system/networking/switch-port-settings networking_switch_port_settings_view GET /v1/system/networking/switch-port-settings/{port} +system_networking_settings_update PUT /v1/system/networking/settings +system_networking_settings_view GET /v1/system/networking/settings API operations found with tag "system/silos" OPERATION ID METHOD URL PATH diff --git a/nexus/external-api/src/lib.rs b/nexus/external-api/src/lib.rs index 9b81215750b..18dfdb8281e 100644 --- a/nexus/external-api/src/lib.rs +++ b/nexus/external-api/src/lib.rs @@ -33,6 +33,7 @@ use nexus_types_versions::v2026_01_16_00; use nexus_types_versions::v2026_01_16_01; use nexus_types_versions::v2026_01_22_00; use nexus_types_versions::v2026_01_30_01; +use nexus_types_versions::v2026_01_31_00; use nexus_types_versions::v2026_02_13_01; use nexus_types_versions::v2026_04_16_00; use omicron_common::address::IpRange; @@ -83,6 +84,7 @@ api_versions!([ // | date-based version should be at the top of the list. // v // (next_yyyy_mm_dd_nn, IDENT), + (2026_05_20_00, EXTERNAL_JUMBO_FRAMES), (2026_05_08_00, MANUAL_DISK_ADOPTION), (2026_05_07_00, REMOVE_DUPLICATED_NETWORKING_TYPES), (2026_04_30_00, PROBE_AND_SAML_DOCS), @@ -437,6 +439,37 @@ pub trait NexusExternalApi { HttpError, >; + /// Fetch fleet-wide networking settings + #[endpoint { + method = GET, + path = "/v1/system/networking/settings", + tags = ["system/networking"], + versions = VERSION_EXTERNAL_JUMBO_FRAMES.., + }] + async fn system_networking_settings_view( + rqctx: RequestContext, + ) -> Result< + HttpResponseOk, + HttpError, + >; + + /// Update fleet-wide networking settings + #[endpoint { + method = PUT, + path = "/v1/system/networking/settings", + tags = ["system/networking"], + versions = VERSION_EXTERNAL_JUMBO_FRAMES.., + }] + async fn system_networking_settings_update( + rqctx: RequestContext, + new_settings: TypedBody< + latest::system_networking::SystemNetworkingSettingsUpdate, + >, + ) -> Result< + HttpResponseOk, + HttpError, + >; + /// Fetch current silo's IAM policy #[endpoint { method = GET, @@ -3568,26 +3601,72 @@ pub trait NexusExternalApi { method = GET, path = "/v1/instances", tags = ["instances"], + versions = VERSION_EXTERNAL_JUMBO_FRAMES.., }] async fn instance_list( rqctx: RequestContext, query_params: Query< PaginatedByNameOrId, >, - ) -> Result>, HttpError>; + ) -> Result< + HttpResponseOk>, + HttpError, + >; + + #[endpoint { + operation_id = "instance_list", + method = GET, + path = "/v1/instances", + tags = ["instances"], + versions = ..VERSION_EXTERNAL_JUMBO_FRAMES, + }] + async fn instance_list_pre_jumbo( + rqctx: RequestContext, + query_params: Query< + PaginatedByNameOrId, + >, + ) -> Result>, HttpError> { + let resp = Self::instance_list(rqctx, query_params).await?; + let inner = resp.0; + Ok(HttpResponseOk(ResultsPage { + items: inner.items.into_iter().map(Into::into).collect(), + next_page: inner.next_page, + })) + } /// Create instance #[endpoint { method = POST, path = "/v1/instances", tags = ["instances"], - versions = VERSION_READ_ONLY_DISKS_NULLABLE.., + versions = VERSION_EXTERNAL_JUMBO_FRAMES.., }] async fn instance_create( rqctx: RequestContext, query_params: Query, new_instance: TypedBody, - ) -> Result, HttpError>; + ) -> Result, HttpError>; + + #[endpoint { + operation_id = "instance_create", + method = POST, + path = "/v1/instances", + tags = ["instances"], + versions = VERSION_READ_ONLY_DISKS_NULLABLE..VERSION_EXTERNAL_JUMBO_FRAMES, + }] + async fn instance_create_v2026_01_31_00( + rqctx: RequestContext, + query_params: Query, + new_instance: TypedBody, + ) -> Result, HttpError> { + let resp = Self::instance_create( + rqctx, + query_params, + new_instance.map(Into::into), + ) + .await?; + Ok(HttpResponseCreated(resp.0.into())) + } #[endpoint { operation_id = "instance_create", @@ -3601,8 +3680,12 @@ pub trait NexusExternalApi { query_params: Query, new_instance: TypedBody, ) -> Result, HttpError> { - Self::instance_create(rqctx, query_params, new_instance.map(Into::into)) - .await + Self::instance_create_v2026_01_31_00( + rqctx, + query_params, + new_instance.map(Into::into), + ) + .await } /// Create instance @@ -3731,12 +3814,30 @@ pub trait NexusExternalApi { method = GET, path = "/v1/instances/{instance}", tags = ["instances"], + versions = VERSION_EXTERNAL_JUMBO_FRAMES.., }] async fn instance_view( rqctx: RequestContext, query_params: Query, path_params: Path, - ) -> Result, HttpError>; + ) -> Result, HttpError>; + + #[endpoint { + operation_id = "instance_view", + method = GET, + path = "/v1/instances/{instance}", + tags = ["instances"], + versions = ..VERSION_EXTERNAL_JUMBO_FRAMES, + }] + async fn instance_view_pre_jumbo( + rqctx: RequestContext, + query_params: Query, + path_params: Path, + ) -> Result, HttpError> { + let resp = + Self::instance_view(rqctx, query_params, path_params).await?; + Ok(HttpResponseOk(resp.0.into())) + } /// Delete instance #[endpoint { @@ -3755,14 +3856,38 @@ pub trait NexusExternalApi { method = PUT, path = "/v1/instances/{instance}", tags = ["instances"], - versions = VERSION_MULTICAST_IMPLICIT_LIFECYCLE_UPDATES.., + versions = VERSION_EXTERNAL_JUMBO_FRAMES.., }] async fn instance_update( rqctx: RequestContext, query_params: Query, path_params: Path, instance_config: TypedBody, - ) -> Result, HttpError>; + ) -> Result, HttpError>; + + /// Update instance + #[endpoint { + operation_id = "instance_update", + method = PUT, + path = "/v1/instances/{instance}", + tags = ["instances"], + versions = VERSION_MULTICAST_IMPLICIT_LIFECYCLE_UPDATES..VERSION_EXTERNAL_JUMBO_FRAMES, + }] + async fn instance_update_v2026_01_08_00( + rqctx: RequestContext, + query_params: Query, + path_params: Path, + instance_config: TypedBody, + ) -> Result, HttpError> { + let resp = Self::instance_update( + rqctx, + query_params, + path_params, + instance_config.map(Into::into), + ) + .await?; + Ok(HttpResponseOk(resp.0.into())) + } /// Update instance #[endpoint { @@ -3778,7 +3903,7 @@ pub trait NexusExternalApi { path_params: Path, instance_config: TypedBody, ) -> Result, HttpError> { - Self::instance_update( + Self::instance_update_v2026_01_08_00( rqctx, query_params, path_params, @@ -3792,36 +3917,90 @@ pub trait NexusExternalApi { method = POST, path = "/v1/instances/{instance}/reboot", tags = ["instances"], + versions = VERSION_EXTERNAL_JUMBO_FRAMES.., }] async fn instance_reboot( rqctx: RequestContext, query_params: Query, path_params: Path, - ) -> Result, HttpError>; + ) -> Result, HttpError>; + + #[endpoint { + operation_id = "instance_reboot", + method = POST, + path = "/v1/instances/{instance}/reboot", + tags = ["instances"], + versions = ..VERSION_EXTERNAL_JUMBO_FRAMES, + }] + async fn instance_reboot_pre_jumbo( + rqctx: RequestContext, + query_params: Query, + path_params: Path, + ) -> Result, HttpError> { + let resp = + Self::instance_reboot(rqctx, query_params, path_params).await?; + Ok(HttpResponseAccepted(resp.0.into())) + } /// Boot instance #[endpoint { method = POST, path = "/v1/instances/{instance}/start", tags = ["instances"], + versions = VERSION_EXTERNAL_JUMBO_FRAMES.., }] async fn instance_start( rqctx: RequestContext, query_params: Query, path_params: Path, - ) -> Result, HttpError>; + ) -> Result, HttpError>; + + #[endpoint { + operation_id = "instance_start", + method = POST, + path = "/v1/instances/{instance}/start", + tags = ["instances"], + versions = ..VERSION_EXTERNAL_JUMBO_FRAMES, + }] + async fn instance_start_pre_jumbo( + rqctx: RequestContext, + query_params: Query, + path_params: Path, + ) -> Result, HttpError> { + let resp = + Self::instance_start(rqctx, query_params, path_params).await?; + Ok(HttpResponseAccepted(resp.0.into())) + } /// Stop instance #[endpoint { method = POST, path = "/v1/instances/{instance}/stop", tags = ["instances"], + versions = VERSION_EXTERNAL_JUMBO_FRAMES.., }] async fn instance_stop( rqctx: RequestContext, query_params: Query, path_params: Path, - ) -> Result, HttpError>; + ) -> Result, HttpError>; + + #[endpoint { + operation_id = "instance_stop", + method = POST, + path = "/v1/instances/{instance}/stop", + tags = ["instances"], + versions = ..VERSION_EXTERNAL_JUMBO_FRAMES, + }] + async fn instance_stop_pre_jumbo( + rqctx: RequestContext, + query_params: Query, + path_params: Path, + ) -> Result, HttpError> { + let resp = + Self::instance_stop(rqctx, query_params, path_params).await?; + Ok(HttpResponseAccepted(resp.0.into())) + } /// Fetch instance serial console #[endpoint { diff --git a/nexus/src/app/background/tasks/instance_reincarnation.rs b/nexus/src/app/background/tasks/instance_reincarnation.rs index 1b67e86d402..1a0fe380803 100644 --- a/nexus/src/app/background/tasks/instance_reincarnation.rs +++ b/nexus/src/app/background/tasks/instance_reincarnation.rs @@ -410,6 +410,7 @@ mod test { auto_restart_policy, anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), + enable_jumbo_frames: false, }, ) .await; diff --git a/nexus/src/app/background/tasks/sync_switch_configuration.rs b/nexus/src/app/background/tasks/sync_switch_configuration.rs index 91dcc66b070..2fb4aa26260 100644 --- a/nexus/src/app/background/tasks/sync_switch_configuration.rs +++ b/nexus/src/app/background/tasks/sync_switch_configuration.rs @@ -30,13 +30,25 @@ use dpd_client::{Client as DpdClient, types as DpdTypes}; use futures::FutureExt; use futures::future::BoxFuture; use mg_admin_client::types::{ - AddStaticRoute4Request, AddStaticRoute6Request, ApplyRequest, - BestpathFanoutRequest, BgpPeerConfig, CheckerSource, - DeleteStaticRoute4Request, DeleteStaticRoute6Request, + ApplyRequest, BgpPeerConfig, + UnnumberedBgpPeerConfig as MgUnnumberedBgpPeerConfig, +}; +use mg_api_types::bgp::policy::{ ImportExportPolicy4 as MgImportExportPolicy4, - ImportExportPolicy6 as MgImportExportPolicy6, Ipv4UnicastConfig, - Ipv6UnicastConfig, JitterRange, ShaperSource, StaticRoute4, - StaticRoute4List, StaticRoute6, StaticRoute6List, UnnumberedBgpPeerConfig, + ImportExportPolicy6 as MgImportExportPolicy6, +}; +use mg_api_types::rdb::prefix::{Prefix, Prefix4, Prefix6}; +use mg_api_types::rib::BestpathFanoutRequest; +use mg_api_types::static_routes::{ + AddStaticRoute4Request, AddStaticRoute6Request, StaticRoute4, + StaticRoute4List, StaticRoute6, StaticRoute6List, +}; +use mg_api_types::{ + bgp::config::{ + CheckerSource, Ipv4UnicastConfig, Ipv6UnicastConfig, JitterRange, + ShaperSource, + }, + static_routes::{DeleteStaticRoute4Request, DeleteStaticRoute6Request}, }; use nexus_db_queries::{ context::OpContext, @@ -49,7 +61,6 @@ use omicron_common::{ address::{Ipv6Subnet, get_sled_address}, api::external::{DataPageParams, Name}, }; -use rdb_types::{Prefix, Prefix4, Prefix6}; use serde_json::json; use sled_agent_client::types::HostPortConfig; use sled_agent_types::early_networking::BgpConfig as SledBgpConfig; @@ -78,7 +89,7 @@ use slog_error_chain::InlineErrorChain; use std::{ collections::{HashMap, HashSet, hash_map::Entry}, hash::Hash, - net::{IpAddr, Ipv4Addr, Ipv6Addr}, + net::{IpAddr, Ipv6Addr, SocketAddr}, str::FromStr, sync::{Arc, LazyLock}, }; @@ -549,7 +560,7 @@ impl BackgroundTask for SwitchPortSettingsManager { // desired peer configurations for a given switch port let mut peers: HashMap> = HashMap::new(); - let mut unnumbered_peers: HashMap> = HashMap::new(); + let mut unnumbered_peers: HashMap> = HashMap::new(); for peer in &settings.bgp_peers { let bgp_config_id = peer.bgp_config_id(); @@ -817,7 +828,7 @@ impl BackgroundTask for SwitchPortSettingsManager { // now that the peer passes the above validations, add it to the list for configuration let peer_config = BgpPeerConfig { name: format!("{ip}"), - host: format!("{ip}:179"), + host: SocketAddr::new(ip.into(), 179).to_string(), hold_time: peer.hold_time.into(), idle_hold_time: peer.idle_hold_time.into(), delay_open: peer.delay_open.into(), @@ -866,9 +877,10 @@ impl BackgroundTask for SwitchPortSettingsManager { } // Unnumbered peer - identified by interface RouterPeerType::Unnumbered { router_lifetime } => { - let peer_config = UnnumberedBgpPeerConfig { + let peer_config = MgUnnumberedBgpPeerConfig { name: format!("unnumbered-{}", port.port_name), interface: format!("tfport{}_0", port.port_name), + router_lifetime: router_lifetime.as_u16(), hold_time: peer.hold_time.into(), idle_hold_time: peer.idle_hold_time.into(), delay_open: peer.delay_open.into(), @@ -900,7 +912,6 @@ impl BackgroundTask for SwitchPortSettingsManager { }), deterministic_collision_resolution: false, idle_hold_jitter: None, - router_lifetime: router_lifetime.as_u16(), src_port: None, src_addr: None, }; @@ -998,7 +1009,7 @@ impl BackgroundTask for SwitchPortSettingsManager { "switch_slot" => ?switch_slot, "config" => ?config, ); - if let Err(e) = client.bgp_apply_v2(config).await { + if let Err(e) = client.bgp_apply(config).await { error!(log, "error while applying bgp configuration"; "error" => ?e); } @@ -1848,7 +1859,7 @@ fn build_sled_agent_clients( #[derive(PartialEq, Eq, Hash, Debug)] struct SwitchStaticRouteV4 { - nexthop: Ipv4Addr, + nexthop: IpAddr, prefix: Prefix4, vlan: Option, priority: u8, @@ -2031,7 +2042,7 @@ fn static_routes_in_db( None => DEFAULT_RIB_PRIORITY_STATIC, }; routes.insert(SwitchStaticRoute::V4(SwitchStaticRouteV4 { - nexthop, + nexthop: IpAddr::V4(nexthop), prefix: Prefix4 { value: dst, length: route.dst.prefix(), @@ -2060,6 +2071,21 @@ fn static_routes_in_db( priority, })); } + (IpAddr::V6(nexthop), IpAddr::V4(dst)) => { + let priority = match route.rib_priority { + Some(v) => v.0, + None => DEFAULT_RIB_PRIORITY_STATIC, + }; + routes.insert(SwitchStaticRoute::V4(SwitchStaticRouteV4 { + nexthop: IpAddr::V6(nexthop), + prefix: Prefix4 { + value: dst, + length: route.dst.prefix(), + }, + vlan: route.vid.map(|x| x.0), + priority, + })); + } (nexthop, dst) => { error!(log, "encountered route with ip version mismatch"; "nexthop" => nexthop.to_string(), @@ -2291,7 +2317,7 @@ async fn static_routes_on_switch( }; flattened.insert(SwitchStaticRoute::V4( SwitchStaticRouteV4 { - nexthop: addr, + nexthop: IpAddr::V4(addr), prefix: dst, vlan: p.vlan_id, priority: p.rib_priority, @@ -2299,22 +2325,33 @@ async fn static_routes_on_switch( )); } IpAddr::V6(addr) => { - let Ok(dst) = destination.parse() else { - error!( - log, - "failed to parse static route destination: \ - {destination}" - ); + if let Ok(dst) = destination.parse::() { + flattened.insert(SwitchStaticRoute::V6( + SwitchStaticRouteV6 { + nexthop: addr, + prefix: dst, + vlan: p.vlan_id, + priority: p.rib_priority, + }, + )); continue; }; - flattened.insert(SwitchStaticRoute::V6( - SwitchStaticRouteV6 { - nexthop: addr, - prefix: dst, - vlan: p.vlan_id, - priority: p.rib_priority, - }, - )); + if let Ok(dst) = destination.parse::() { + flattened.insert(SwitchStaticRoute::V4( + SwitchStaticRouteV4 { + nexthop: IpAddr::V6(addr), + prefix: dst, + vlan: p.vlan_id, + priority: p.rib_priority, + }, + )); + continue; + }; + error!( + log, + "failed to parse static route destination: \ + {destination}" + ); } }; } diff --git a/nexus/src/app/bgp.rs b/nexus/src/app/bgp.rs index 53cc41996c5..0ca95b2f294 100644 --- a/nexus/src/app/bgp.rs +++ b/nexus/src/app/bgp.rs @@ -177,7 +177,7 @@ impl super::Nexus { for r in &router_info { let asn = r.asn; - let selector = mg_admin_client::types::ExportedSelector { + let selector = mg_api_types::bgp::session::ExportedSelector { afi: None, asn, peer: None, @@ -199,12 +199,12 @@ impl super::Nexus { for (peer_id, exports) in exported { for ex in exports.iter() { let prefix = match ex { - rdb_types::Prefix::V4(v4) => { + mg_api_types::rdb::prefix::Prefix::V4(v4) => { oxnet::IpNet::V4(oxnet::Ipv4Net::new_unchecked( v4.value, v4.length, )) } - rdb_types::Prefix::V6(v6) => { + mg_api_types::rdb::prefix::Prefix::V6(v6) => { oxnet::IpNet::V6(oxnet::Ipv6Net::new_unchecked( v6.value, v6.length, )) diff --git a/nexus/src/app/instance.rs b/nexus/src/app/instance.rs index d0b77f6ff3f..5f08a0a2bee 100644 --- a/nexus/src/app/instance.rs +++ b/nexus/src/app/instance.rs @@ -569,10 +569,27 @@ impl super::Nexus { boot_disk, cpu_platform, multicast_groups, + enable_jumbo_frames, } = params; check_instance_cpu_memory_sizes(*ncpus, *memory)?; + // Opting in to jumbo frames requires the fleet-wide opt-in. Opting out + // (`Some(false)`) is always allowed. + if matches!(enable_jumbo_frames, Some(true)) { + let settings = self + .db_datastore + .system_networking_settings_view(opctx) + .await?; + if !settings.external_jumbo_frames_opt_in_enabled { + return Err(Error::invalid_request( + "enable_jumbo_frames may only be set to true when the \ + fleet-wide jumbo-frames opt-in is enabled by a fleet \ + administrator", + )); + } + } + let boot_disk_id = match boot_disk.as_ref() { Some(disk) => { let selector = disk::DiskSelector { @@ -611,6 +628,18 @@ impl super::Nexus { .instance_reconfigure(opctx, &authz_instance, update) .await; + // Persist the new jumbo-frames bit if it was supplied. Effective MTU is + // recomputed on the next instance start. + if let Some(enable) = enable_jumbo_frames { + self.datastore() + .instance_set_enable_jumbo_frames( + opctx, + &authz_instance, + *enable, + ) + .await?; + } + // Handle multicast group updates if specified if let Some(multicast_groups) = multicast_groups { self.handle_multicast_group_changes( @@ -675,6 +704,22 @@ impl super::Nexus { ))); } + // Requiring jumbo frames on a new instance requires the fleet-wide + // opt-in. + if params.enable_jumbo_frames { + let settings = self + .db_datastore + .system_networking_settings_view(opctx) + .await?; + if !settings.external_jumbo_frames_opt_in_enabled { + return Err(Error::invalid_request( + "enable_jumbo_frames may only be set on an instance when \ + the fleet-wide jumbo-frames opt-in is enabled by a \ + fleet administrator", + )); + } + } + // Collect ephemeral IP selectors for validation let ephemeral_selectors: Vec<_> = params .external_ips @@ -1649,6 +1694,24 @@ impl super::Nexus { }) .collect(); + // Compute the effective MTU for the instance's primary OPTE port. Jumbo + // frames are only applied when both the fleet-wide opt-in is enabled + // and the per-instance bit is set; otherwise we leave `primary_nic_mtu` + // unset and OPTE applies the default MTU. + let primary_nic_mtu = if db_instance.enable_jumbo_frames { + let settings = self + .db_datastore + .system_networking_settings_view(opctx) + .await?; + if settings.external_jumbo_frames_opt_in_enabled { + Some(omicron_common::address::EXTERNAL_JUMBO_FRAMES_MTU) + } else { + None + } + } else { + None + }; + let local_config = sled_agent_client::types::InstanceSledLocalConfig { hostname, nics, @@ -1663,6 +1726,7 @@ impl super::Nexus { }, delegated_zvols, attached_subnets, + primary_nic_mtu, }; let instance_id = InstanceUuid::from_untyped_uuid(db_instance.id()); @@ -3103,6 +3167,7 @@ mod tests { auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), + enable_jumbo_frames: false, }; let instance_id = InstanceUuid::from_untyped_uuid(Uuid::new_v4()); diff --git a/nexus/src/app/lldp.rs b/nexus/src/app/lldp.rs index 8ddb0f3ec07..ff150dedce8 100644 --- a/nexus/src/app/lldp.rs +++ b/nexus/src/app/lldp.rs @@ -85,12 +85,16 @@ impl super::Nexus { .get_neighbors_stream(&format!("{port}/0"), None) .try_collect() .await - .map_err(|e| { - Error::internal_error(&format!( + .map_err(|e| match e.status() { + Some(http::StatusCode::NOT_FOUND) => Error::not_found_by_name( + omicron_common::api::external::ResourceType::SwitchPort, + port, + ), + _ => Error::internal_error(&format!( "failed to get neighbor list for \ {switch_slot:?}/{port}: {}", InlineErrorChain::new(&e), - )) + )), })?; // Strip out any neighbors seen on previous pages prior to sorting the diff --git a/nexus/src/app/mod.rs b/nexus/src/app/mod.rs index 6778ea72598..f7c8e524a90 100644 --- a/nexus/src/app/mod.rs +++ b/nexus/src/app/mod.rs @@ -105,6 +105,7 @@ pub(crate) mod support_bundles; mod switch; mod switch_interface; mod switch_port; +mod system_networking; pub mod test_interfaces; mod trust_quorum; mod update; diff --git a/nexus/src/app/rack.rs b/nexus/src/app/rack.rs index 0ed1084c763..32df6156836 100644 --- a/nexus/src/app/rack.rs +++ b/nexus/src/app/rack.rs @@ -651,6 +651,15 @@ impl super::Nexus { ) .await?; + // Persist the fleet-wide jumbo-frames opt-in. The singleton row was + // seeded by the schema migration; we update it here in case RSS shipped + // a non-default value. + if request.external_jumbo_frames_opt_in_enabled { + self.db_datastore + .system_networking_settings_update(opctx, Some(true)) + .await?; + } + // Note: Service firewall rules are propagated on a best-effort basis // in Server::start() via attempt_ip_allowlist_plumbing(). OPTE's // default-deny policy ensures Nexus remains unreachable until the diff --git a/nexus/src/app/sagas/instance_create.rs b/nexus/src/app/sagas/instance_create.rs index afe911c95ca..8c3367e8412 100644 --- a/nexus/src/app/sagas/instance_create.rs +++ b/nexus/src/app/sagas/instance_create.rs @@ -1548,6 +1548,7 @@ pub mod test { auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), + enable_jumbo_frames: false, }, boundary_switches: HashSet::from([SwitchSlot::Switch0]), } diff --git a/nexus/src/app/sagas/instance_delete.rs b/nexus/src/app/sagas/instance_delete.rs index 86463c55537..9d24df7f363 100644 --- a/nexus/src/app/sagas/instance_delete.rs +++ b/nexus/src/app/sagas/instance_delete.rs @@ -315,6 +315,7 @@ mod test { auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), + enable_jumbo_frames: false, } } diff --git a/nexus/src/app/sagas/instance_migrate.rs b/nexus/src/app/sagas/instance_migrate.rs index a713a8335d3..058823b657c 100644 --- a/nexus/src/app/sagas/instance_migrate.rs +++ b/nexus/src/app/sagas/instance_migrate.rs @@ -667,6 +667,7 @@ mod tests { auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), + enable_jumbo_frames: false, }, ) .await diff --git a/nexus/src/app/sagas/instance_start.rs b/nexus/src/app/sagas/instance_start.rs index 0899ffcd474..2f11d9e1587 100644 --- a/nexus/src/app/sagas/instance_start.rs +++ b/nexus/src/app/sagas/instance_start.rs @@ -1227,6 +1227,7 @@ mod test { auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), + enable_jumbo_frames: false, }, ) .await diff --git a/nexus/src/app/sagas/instance_update/mod.rs b/nexus/src/app/sagas/instance_update/mod.rs index 6de40e3fc04..17debf22319 100644 --- a/nexus/src/app/sagas/instance_update/mod.rs +++ b/nexus/src/app/sagas/instance_update/mod.rs @@ -1642,6 +1642,7 @@ mod test { auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), + enable_jumbo_frames: false, }, ) .await diff --git a/nexus/src/app/sagas/snapshot_create.rs b/nexus/src/app/sagas/snapshot_create.rs index 1debdf8f33d..73d3c894be2 100644 --- a/nexus/src/app/sagas/snapshot_create.rs +++ b/nexus/src/app/sagas/snapshot_create.rs @@ -2166,6 +2166,7 @@ mod test { auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), + enable_jumbo_frames: false, }, ) .await; diff --git a/nexus/src/app/system_networking.rs b/nexus/src/app/system_networking.rs new file mode 100644 index 00000000000..217dac2e80a --- /dev/null +++ b/nexus/src/app/system_networking.rs @@ -0,0 +1,44 @@ +// 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/. + +//! Fleet-wide networking settings. + +use nexus_db_queries::authz; +use nexus_db_queries::context::OpContext; +use nexus_types::external_api::system_networking; +use omicron_common::api::external::Error; + +impl super::Nexus { + pub(crate) async fn system_networking_settings_view( + &self, + opctx: &OpContext, + ) -> Result { + opctx.authorize(authz::Action::Read, &authz::FLEET).await?; + let settings = + self.db_datastore.system_networking_settings_view(opctx).await?; + Ok(system_networking::SystemNetworkingSettings { + external_jumbo_frames_opt_in_enabled: settings + .external_jumbo_frames_opt_in_enabled, + }) + } + + pub(crate) async fn system_networking_settings_update( + &self, + opctx: &OpContext, + params: &system_networking::SystemNetworkingSettingsUpdate, + ) -> Result { + opctx.authorize(authz::Action::Modify, &authz::FLEET).await?; + let settings = self + .db_datastore + .system_networking_settings_update( + opctx, + params.external_jumbo_frames_opt_in_enabled, + ) + .await?; + Ok(system_networking::SystemNetworkingSettings { + external_jumbo_frames_opt_in_enabled: settings + .external_jumbo_frames_opt_in_enabled, + }) + } +} diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index b0fa2ab8408..e7287a53cb0 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -45,8 +45,8 @@ use nexus_types::external_api::{ external_subnet, floating_ip, hardware, identity_provider, image, instance, internet_gateway, ip_pool, metrics, multicast, networking, oxql, path_params, policy, probe, project, rack, scim, silo, sled, snapshot, - ssh_key, subnet_pool, support_bundle, switch, system, timeseries, update, - user, vpc, + ssh_key, subnet_pool, support_bundle, switch, system, system_networking, + timeseries, update, user, vpc, }; // Type imports for API implementations (per RFD 619) use nexus_types::external_api::bfd::BfdStatus; @@ -83,7 +83,6 @@ use omicron_common::api::external::AntiAffinityGroupMember; use omicron_common::api::external::DataPageParams; use omicron_common::api::external::Disk; use omicron_common::api::external::Error; -use omicron_common::api::external::Instance; use omicron_common::api::external::InstanceNetworkInterface; use omicron_common::api::external::InternalContext; use omicron_common::api::external::LldpNeighbor; @@ -170,6 +169,47 @@ impl NexusExternalApi for NexusExternalApiImpl { .await } + async fn system_networking_settings_view( + rqctx: RequestContext, + ) -> Result< + HttpResponseOk, + HttpError, + > { + let apictx = rqctx.context(); + let handler = async { + let nexus = &apictx.context.nexus; + let opctx = + crate::context::op_context_for_external_api(&rqctx).await?; + let settings = + nexus.system_networking_settings_view(&opctx).await?; + Ok(HttpResponseOk(settings)) + }; + apictx + .context + .external_latencies + .instrument_dropshot_handler(&rqctx, handler) + .await + } + + async fn system_networking_settings_update( + rqctx: RequestContext, + new_settings: TypedBody< + system_networking::SystemNetworkingSettingsUpdate, + >, + ) -> Result< + HttpResponseOk, + HttpError, + > { + audit_and_time(&rqctx, |opctx, nexus| async move { + let new_settings = new_settings.into_inner(); + let settings = nexus + .system_networking_settings_update(&opctx, &new_settings) + .await?; + Ok(HttpResponseOk(settings)) + }) + .await + } + async fn policy_view( rqctx: RequestContext, ) -> Result>, HttpError> @@ -2678,7 +2718,8 @@ impl NexusExternalApi for NexusExternalApiImpl { async fn instance_list( rqctx: RequestContext, query_params: Query>, - ) -> Result>, HttpError> { + ) -> Result>, HttpError> + { let apictx = rqctx.context(); let handler = async { let nexus = &apictx.context.nexus; @@ -2713,7 +2754,7 @@ impl NexusExternalApi for NexusExternalApiImpl { rqctx: RequestContext, query_params: Query, new_instance: TypedBody, - ) -> Result, HttpError> { + ) -> Result, HttpError> { audit_and_time(&rqctx, |opctx, nexus| async move { let project_selector = query_params.into_inner(); let new_instance_params = new_instance.into_inner(); @@ -2735,7 +2776,7 @@ impl NexusExternalApi for NexusExternalApiImpl { rqctx: RequestContext, query_params: Query, path_params: Path, - ) -> Result, HttpError> { + ) -> Result, HttpError> { let apictx = rqctx.context(); let nexus = &apictx.context.nexus; let path = path_params.into_inner(); @@ -2789,7 +2830,7 @@ impl NexusExternalApi for NexusExternalApiImpl { query_params: Query, path_params: Path, instance_config: TypedBody, - ) -> Result, HttpError> { + ) -> Result, HttpError> { let query = query_params.into_inner(); let path = path_params.into_inner(); let instance_config = instance_config.into_inner(); @@ -2816,7 +2857,7 @@ impl NexusExternalApi for NexusExternalApiImpl { rqctx: RequestContext, query_params: Query, path_params: Path, - ) -> Result, HttpError> { + ) -> Result, HttpError> { let path = path_params.into_inner(); let query = query_params.into_inner(); let instance_selector = instance::InstanceSelector { @@ -2837,7 +2878,7 @@ impl NexusExternalApi for NexusExternalApiImpl { rqctx: RequestContext, query_params: Query, path_params: Path, - ) -> Result, HttpError> { + ) -> Result, HttpError> { let path = path_params.into_inner(); let query = query_params.into_inner(); let instance_selector = instance::InstanceSelector { @@ -2863,7 +2904,7 @@ impl NexusExternalApi for NexusExternalApiImpl { rqctx: RequestContext, query_params: Query, path_params: Path, - ) -> Result, HttpError> { + ) -> Result, HttpError> { let path = path_params.into_inner(); let query = query_params.into_inner(); let instance_selector = instance::InstanceSelector { diff --git a/nexus/src/lib.rs b/nexus/src/lib.rs index 929342771a8..80fd53748f9 100644 --- a/nexus/src/lib.rs +++ b/nexus/src/lib.rs @@ -472,6 +472,7 @@ impl nexus_test_interface::NexusServer for Server { bfd: Vec::new(), }, allowed_source_ips: AllowedSourceIps::Any, + external_jumbo_frames_opt_in_enabled: false, // Insert a fake trust quorum config such that existing // sleds will never be present. // diff --git a/nexus/test-utils/src/resource_helpers.rs b/nexus/test-utils/src/resource_helpers.rs index ace0d2f265a..bd2d3f92322 100644 --- a/nexus/test-utils/src/resource_helpers.rs +++ b/nexus/test-utils/src/resource_helpers.rs @@ -940,6 +940,7 @@ pub async fn create_instance_with( auto_restart_policy, anti_affinity_groups: Vec::new(), multicast_groups, + enable_jumbo_frames: false, }, ) .await diff --git a/nexus/tests/integration_tests/endpoints.rs b/nexus/tests/integration_tests/endpoints.rs index 9a324be5d90..44d5ac7feb0 100644 --- a/nexus/tests/integration_tests/endpoints.rs +++ b/nexus/tests/integration_tests/endpoints.rs @@ -46,6 +46,7 @@ use nexus_types::external_api::ssh_key; use nexus_types::external_api::subnet_pool; use nexus_types::external_api::support_bundle; use nexus_types::external_api::system; +use nexus_types::external_api::system_networking; use nexus_types::external_api::timeseries; use nexus_types::external_api::update; use nexus_types::external_api::vpc; @@ -215,6 +216,8 @@ pub const DEMO_ACCESS_TOKEN_DELETE_URL: &str = // Global policy pub const SYSTEM_POLICY_URL: &'static str = "/v1/system/policy"; +pub const SYSTEM_NETWORKING_SETTINGS_URL: &'static str = + "/v1/system/networking/settings"; // Silo used for testing pub static DEMO_SILO_NAME: LazyLock = @@ -794,6 +797,7 @@ pub static DEMO_INSTANCE_CREATE: LazyLock = auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), + enable_jumbo_frames: false, }); pub static DEMO_STOPPED_INSTANCE_CREATE: LazyLock = LazyLock::new(|| instance::InstanceCreate { @@ -820,6 +824,7 @@ pub static DEMO_STOPPED_INSTANCE_CREATE: LazyLock = auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), + enable_jumbo_frames: false, }); pub static DEMO_INSTANCE_UPDATE: LazyLock = LazyLock::new(|| instance::InstanceUpdate { @@ -829,6 +834,7 @@ pub static DEMO_INSTANCE_UPDATE: LazyLock = ncpus: InstanceCpuCount(1), memory: ByteCount::from_gibibytes_u32(16), multicast_groups: None, + enable_jumbo_frames: None, }); // The instance needs a network interface, too. @@ -1810,6 +1816,25 @@ pub static VERIFY_ENDPOINTS: LazyLock> = LazyLock::new( ), ], }, + // Fleet-wide networking settings + VerifyEndpoint { + url: &SYSTEM_NETWORKING_SETTINGS_URL, + visibility: Visibility::Public, + unprivileged_access: UnprivilegedAccess::None, + allowed_methods: vec![ + AllowedMethod::Get, + AllowedMethod::Put( + serde_json::to_value( + &system_networking::SystemNetworkingSettingsUpdate { + external_jumbo_frames_opt_in_enabled: Some( + false, + ), + }, + ) + .unwrap(), + ), + ], + }, // IP Pools top-level endpoint VerifyEndpoint { url: &DEMO_IP_POOLS_URL, diff --git a/nexus/tests/integration_tests/external_ips.rs b/nexus/tests/integration_tests/external_ips.rs index 8d4a37198f9..627d0adef1a 100644 --- a/nexus/tests/integration_tests/external_ips.rs +++ b/nexus/tests/integration_tests/external_ips.rs @@ -1173,6 +1173,7 @@ async fn test_floating_ip_attach_fail_between_projects( auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), + enable_jumbo_frames: false, }, StatusCode::BAD_REQUEST, ) diff --git a/nexus/tests/integration_tests/instances.rs b/nexus/tests/integration_tests/instances.rs index 2aff4004804..2ebcf18dc85 100644 --- a/nexus/tests/integration_tests/instances.rs +++ b/nexus/tests/integration_tests/instances.rs @@ -280,6 +280,7 @@ async fn test_create_instance_with_bad_hostname_impl( auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), + enable_jumbo_frames: false, }; let mut body: serde_json::Value = serde_json::from_str(&serde_json::to_string(¶ms).unwrap()).unwrap(); @@ -389,6 +390,7 @@ async fn test_instances_create_reboot_halt( auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), + enable_jumbo_frames: false, })) .expect_status(Some(StatusCode::BAD_REQUEST)), ) @@ -2463,6 +2465,7 @@ async fn test_instances_create_stopped_start( multicast_groups: Vec::new(), auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), + enable_jumbo_frames: false, }, ) .await; @@ -2637,6 +2640,7 @@ async fn test_instance_using_image_from_other_project_fails( auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), + enable_jumbo_frames: false, })) .expect_status(Some(StatusCode::BAD_REQUEST)), ) @@ -2706,6 +2710,7 @@ async fn test_instance_create_saga_removes_instance_database_record( auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), + enable_jumbo_frames: false, }; let response = NexusRequest::objects_post( client, @@ -2739,6 +2744,7 @@ async fn test_instance_create_saga_removes_instance_database_record( auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), + enable_jumbo_frames: false, }; let _ = NexusRequest::objects_post( client, @@ -2895,6 +2901,7 @@ async fn test_instance_with_single_explicit_ip_address_impl( auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), + enable_jumbo_frames: false, }; let response = NexusRequest::objects_post( client, @@ -3075,6 +3082,7 @@ async fn test_instance_with_new_custom_network_interfaces( auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), + enable_jumbo_frames: false, }; let response = NexusRequest::objects_post( client, @@ -3207,6 +3215,7 @@ async fn test_instance_create_delete_network_interface( auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), + enable_jumbo_frames: false, }; let response = NexusRequest::objects_post( client, @@ -3485,6 +3494,7 @@ async fn test_instance_update_network_interfaces( auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), + enable_jumbo_frames: false, }; let response = NexusRequest::objects_post( client, @@ -3909,6 +3919,7 @@ async fn cannot_make_new_primary_nic_lacking_ip_stack_for_external_addresses( auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), + enable_jumbo_frames: false, }; let response = NexusRequest::objects_post( client, @@ -4290,6 +4301,7 @@ async fn test_instance_with_multiple_nics_unwinds_completely( auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), + enable_jumbo_frames: false, }; let builder = RequestBuilder::new(client, http::Method::POST, &get_instances_url()) @@ -4366,6 +4378,7 @@ async fn test_attach_one_disk_to_instance(cptestctx: &ControlPlaneTestContext) { auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), + enable_jumbo_frames: false, }; let builder = @@ -4464,6 +4477,7 @@ async fn test_instance_create_attach_disks( auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), + enable_jumbo_frames: false, }; let builder = @@ -4571,6 +4585,7 @@ async fn test_instance_create_attach_disks_undo( auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), + enable_jumbo_frames: false, }; let builder = @@ -4658,6 +4673,7 @@ async fn test_attach_eight_disks_to_instance( auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), + enable_jumbo_frames: false, }; let builder = @@ -4755,6 +4771,7 @@ async fn test_disk_attach_limit(cptestctx: &ControlPlaneTestContext) { auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), + enable_jumbo_frames: false, }; let url_instances = format!("/v1/instances?project={}", project_name); @@ -4860,6 +4877,7 @@ async fn test_cannot_attach_faulted_disks(cptestctx: &ControlPlaneTestContext) { auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), + enable_jumbo_frames: false, }; let builder = @@ -4954,6 +4972,7 @@ async fn test_disks_detached_when_instance_destroyed( auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), + enable_jumbo_frames: false, }; let builder = @@ -5052,6 +5071,7 @@ async fn test_disks_detached_when_instance_destroyed( auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), + enable_jumbo_frames: false, }; let builder = @@ -5139,6 +5159,7 @@ async fn test_duplicate_disk_attach_requests_ok( auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), + enable_jumbo_frames: false, }; let builder = @@ -5186,6 +5207,7 @@ async fn test_duplicate_disk_attach_requests_ok( auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), + enable_jumbo_frames: false, }; let builder = @@ -5246,6 +5268,7 @@ async fn test_cannot_detach_boot_disk(cptestctx: &ControlPlaneTestContext) { multicast_groups: Vec::new(), auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), + enable_jumbo_frames: false, }; let builder = @@ -5309,6 +5332,7 @@ async fn test_cannot_detach_boot_disk(cptestctx: &ControlPlaneTestContext) { ncpus: InstanceCpuCount::try_from(2).unwrap(), memory: ByteCount::from_gibibytes_u32(4), multicast_groups: None, + enable_jumbo_frames: None, }, ) .await; @@ -5385,6 +5409,7 @@ async fn test_updating_running_instance_boot_disk_is_conflict( auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), + enable_jumbo_frames: false, }; let builder = @@ -5417,6 +5442,7 @@ async fn test_updating_running_instance_boot_disk_is_conflict( ncpus: InstanceCpuCount::try_from(2).unwrap(), memory: ByteCount::from_gibibytes_u32(4), multicast_groups: None, + enable_jumbo_frames: None, }, http::StatusCode::CONFLICT, ) @@ -5439,6 +5465,7 @@ async fn test_updating_running_instance_boot_disk_is_conflict( ncpus: InstanceCpuCount::try_from(2).unwrap(), memory: ByteCount::from_gibibytes_u32(4), multicast_groups: None, + enable_jumbo_frames: None, }, ) .await; @@ -5463,6 +5490,7 @@ async fn test_updating_missing_instance_is_not_found( ncpus: InstanceCpuCount::try_from(0).unwrap(), memory: ByteCount::from_gibibytes_u32(0), multicast_groups: None, + enable_jumbo_frames: None, }, http::StatusCode::NOT_FOUND, ) @@ -5558,6 +5586,7 @@ async fn test_size_can_be_changed(cptestctx: &ControlPlaneTestContext) { auto_restart_policy: None, anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), + enable_jumbo_frames: false, }; let builder = @@ -5585,6 +5614,7 @@ async fn test_size_can_be_changed(cptestctx: &ControlPlaneTestContext) { ncpus: initial_ncpus, memory: initial_memory, multicast_groups: None, + enable_jumbo_frames: None, }; // Resizing the instance immediately will error; the instance is running. @@ -5777,6 +5807,7 @@ async fn test_auto_restart_policy_can_be_changed( auto_restart_policy: None, anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), + enable_jumbo_frames: false, }; let builder = @@ -5805,6 +5836,7 @@ async fn test_auto_restart_policy_can_be_changed( ncpus: InstanceCpuCount::try_from(2).unwrap(), memory: ByteCount::from_gibibytes_u32(4), multicast_groups: None, + enable_jumbo_frames: None, }), ) .await; @@ -5853,6 +5885,7 @@ async fn test_cpu_platform_can_be_changed(cptestctx: &ControlPlaneTestContext) { auto_restart_policy: None, anti_affinity_groups: Vec::new(), multicast_groups: vec![], + enable_jumbo_frames: false, }; let builder = @@ -5881,6 +5914,7 @@ async fn test_cpu_platform_can_be_changed(cptestctx: &ControlPlaneTestContext) { ncpus: InstanceCpuCount::try_from(2).unwrap(), memory: ByteCount::from_gibibytes_u32(4), multicast_groups: None, + enable_jumbo_frames: None, }), ) .await; @@ -5951,6 +5985,7 @@ async fn test_boot_disk_can_be_changed(cptestctx: &ControlPlaneTestContext) { auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), + enable_jumbo_frames: false, }; let builder = @@ -5979,6 +6014,7 @@ async fn test_boot_disk_can_be_changed(cptestctx: &ControlPlaneTestContext) { ncpus: InstanceCpuCount::try_from(2).unwrap(), memory: ByteCount::from_gibibytes_u32(4), multicast_groups: None, + enable_jumbo_frames: None, }, ) .await; @@ -6026,6 +6062,7 @@ async fn test_boot_disk_must_be_attached(cptestctx: &ControlPlaneTestContext) { auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), + enable_jumbo_frames: false, }; let builder = @@ -6051,6 +6088,7 @@ async fn test_boot_disk_must_be_attached(cptestctx: &ControlPlaneTestContext) { ncpus: InstanceCpuCount::try_from(2).unwrap(), memory: ByteCount::from_gibibytes_u32(4), multicast_groups: None, + enable_jumbo_frames: None, }, http::StatusCode::CONFLICT, ) @@ -6086,6 +6124,7 @@ async fn test_boot_disk_must_be_attached(cptestctx: &ControlPlaneTestContext) { ncpus: InstanceCpuCount::try_from(2).unwrap(), memory: ByteCount::from_gibibytes_u32(4), multicast_groups: None, + enable_jumbo_frames: None, }, ) .await; @@ -6125,6 +6164,7 @@ async fn test_instances_memory_rejected_less_than_min_memory_size( auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), + enable_jumbo_frames: false, }; let error = NexusRequest::new( @@ -6181,6 +6221,7 @@ async fn test_instances_memory_not_divisible_by_min_memory_size( auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), + enable_jumbo_frames: false, }; let error = NexusRequest::new( @@ -6237,6 +6278,7 @@ async fn test_instances_memory_greater_than_max_size( auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), + enable_jumbo_frames: false, }; let error = NexusRequest::new( @@ -6346,6 +6388,7 @@ async fn test_instance_create_with_anti_affinity_groups( cpu_platform: None, auto_restart_policy: Default::default(), anti_affinity_groups: anti_affinity_groups_param, + enable_jumbo_frames: false, }; let builder = @@ -6418,6 +6461,7 @@ async fn test_instance_create_with_duplicate_anti_affinity_groups( cpu_platform: None, auto_restart_policy: Default::default(), anti_affinity_groups: anti_affinity_groups_param, + enable_jumbo_frames: false, }; let builder = @@ -6491,6 +6535,7 @@ async fn test_instance_create_with_anti_affinity_groups_that_do_not_exist( cpu_platform: None, auto_restart_policy: Default::default(), anti_affinity_groups: anti_affinity_groups_param, + enable_jumbo_frames: false, }; let error = object_create_error( @@ -6577,6 +6622,7 @@ async fn test_instance_create_with_ssh_keys( cpu_platform: None, auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), + enable_jumbo_frames: false, }; let builder = @@ -6629,6 +6675,7 @@ async fn test_instance_create_with_ssh_keys( cpu_platform: None, auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), + enable_jumbo_frames: false, }; let builder = @@ -6680,6 +6727,7 @@ async fn test_instance_create_with_ssh_keys( cpu_platform: None, auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), + enable_jumbo_frames: false, }; let builder = @@ -6824,6 +6872,7 @@ async fn test_cannot_provision_instance_beyond_cpu_capacity( multicast_groups: Vec::new(), auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), + enable_jumbo_frames: false, }; let url_instances = get_instances_url(); @@ -6886,6 +6935,7 @@ async fn test_cannot_provision_instance_beyond_cpu_limit( multicast_groups: Vec::new(), auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), + enable_jumbo_frames: false, }; let url_instances = get_instances_url(); @@ -6944,6 +6994,7 @@ async fn test_cannot_provision_instance_beyond_ram_capacity( multicast_groups: Vec::new(), auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), + enable_jumbo_frames: false, }; let url_instances = get_instances_url(); @@ -7049,6 +7100,7 @@ async fn test_can_start_instance_with_cpu_platform( auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), multicast_groups: vec![], + enable_jumbo_frames: false, }; let url_instances = get_instances_url(); @@ -7090,6 +7142,7 @@ async fn test_can_start_instance_with_cpu_platform( ncpus: InstanceCpuCount::try_from(1).unwrap(), memory: ByteCount::from_gibibytes_u32(4), multicast_groups: None, + enable_jumbo_frames: None, }, ) .await; @@ -7164,6 +7217,7 @@ async fn test_cannot_start_instance_with_unsatisfiable_cpu_platform( auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), multicast_groups: vec![], + enable_jumbo_frames: false, }; let url_instances = get_instances_url(); @@ -7465,6 +7519,7 @@ async fn test_instance_ephemeral_ip_from_correct_pool( auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), + enable_jumbo_frames: false, }; let error = object_create_error( client, @@ -7540,6 +7595,7 @@ async fn test_instance_ephemeral_ip_from_orphan_pool( auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), + enable_jumbo_frames: false, }; // instance create 404s @@ -7607,6 +7663,7 @@ async fn test_instance_ephemeral_ip_no_default_pool_error( auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), + enable_jumbo_frames: false, }; let url = format!("/v1/instances?project={}", PROJECT_NAME); @@ -7642,6 +7699,7 @@ async fn test_instance_ephemeral_ip_no_default_pool_error( auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), + enable_jumbo_frames: false, }; let error = object_create_error(client, &url, &body, StatusCode::NOT_FOUND).await; @@ -7787,6 +7845,7 @@ async fn test_instance_rejects_three_ephemeral_ips( auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), + enable_jumbo_frames: false, }; let error = object_create_error( client, @@ -7838,6 +7897,7 @@ async fn test_instance_rejects_two_ephemeral_auto_without_version( auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), + enable_jumbo_frames: false, }; let error = object_create_error( client, @@ -7895,6 +7955,7 @@ async fn test_instance_rejects_two_ephemeral_auto_none_with_explicit( auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), + enable_jumbo_frames: false, }; let error = object_create_error( client, @@ -7952,6 +8013,7 @@ async fn test_instance_rejects_two_ephemeral_same_pool( auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), + enable_jumbo_frames: false, }; let error = object_create_error( client, @@ -8113,6 +8175,7 @@ async fn test_instance_create_in_silo(cptestctx: &ControlPlaneTestContext) { auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), + enable_jumbo_frames: false, }; let url_instances = format!("/v1/instances?project={}", PROJECT_NAME); NexusRequest::objects_post(client, &url_instances, &instance_params) @@ -8298,6 +8361,7 @@ async fn test_instance_create_with_cross_project_subnet( start: false, auto_restart_policy: None, anti_affinity_groups: Vec::new(), + enable_jumbo_frames: false, }; let instances_url_a = format!("/v1/instances?project={}", project_a_name); @@ -8427,6 +8491,7 @@ async fn test_silo_limited_collaborator_cross_project_subnet( start: false, auto_restart_policy: None, anti_affinity_groups: Vec::new(), + enable_jumbo_frames: false, }; let instances_url_a = format!("/v1/instances?project={}", project_a_name); @@ -8494,6 +8559,7 @@ async fn test_silo_limited_collaborator_cross_project_subnet( start: false, auto_restart_policy: None, anti_affinity_groups: Vec::new(), + enable_jumbo_frames: false, }; // Should get 404 Not Found because VPC/subnet lookups are scoped to the @@ -9245,6 +9311,7 @@ async fn test_instance_with_max_disks(cptestctx: &ControlPlaneTestContext) { auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), + enable_jumbo_frames: false, }; let instance: Instance = diff --git a/nexus/tests/integration_tests/projects.rs b/nexus/tests/integration_tests/projects.rs index abeef09479a..66dffcfe83c 100644 --- a/nexus/tests/integration_tests/projects.rs +++ b/nexus/tests/integration_tests/projects.rs @@ -184,6 +184,7 @@ async fn test_project_deletion_with_instance( auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), + enable_jumbo_frames: false, }, ) .await; diff --git a/nexus/tests/integration_tests/quotas.rs b/nexus/tests/integration_tests/quotas.rs index 4de4955af88..544ed7ebcd4 100644 --- a/nexus/tests/integration_tests/quotas.rs +++ b/nexus/tests/integration_tests/quotas.rs @@ -119,6 +119,7 @@ impl ResourceAllocator { auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), + enable_jumbo_frames: false, }, ) .authn_as(self.auth.clone()) diff --git a/nexus/tests/integration_tests/snapshots.rs b/nexus/tests/integration_tests/snapshots.rs index 8efc49ff6dd..6408252bcb8 100644 --- a/nexus/tests/integration_tests/snapshots.rs +++ b/nexus/tests/integration_tests/snapshots.rs @@ -143,6 +143,7 @@ async fn test_snapshot_basic(cptestctx: &ControlPlaneTestContext) { auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), + enable_jumbo_frames: false, }, ) .await; @@ -325,6 +326,7 @@ async fn test_snapshot_stopped_instance(cptestctx: &ControlPlaneTestContext) { auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), + enable_jumbo_frames: false, }, ) .await; diff --git a/nexus/tests/integration_tests/subnet_allocation.rs b/nexus/tests/integration_tests/subnet_allocation.rs index e5986d3b342..f86dfd6bfb0 100644 --- a/nexus/tests/integration_tests/subnet_allocation.rs +++ b/nexus/tests/integration_tests/subnet_allocation.rs @@ -71,6 +71,7 @@ async fn create_instance_expect_failure( auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), + enable_jumbo_frames: false, }; NexusRequest::new( diff --git a/nexus/tests/integration_tests/utilization.rs b/nexus/tests/integration_tests/utilization.rs index d7917d83b19..5756e6f337b 100644 --- a/nexus/tests/integration_tests/utilization.rs +++ b/nexus/tests/integration_tests/utilization.rs @@ -242,6 +242,7 @@ async fn create_resources_in_test_suite_silo( auto_restart_policy: Default::default(), anti_affinity_groups: Vec::new(), multicast_groups: Vec::new(), + enable_jumbo_frames: false, }; NexusRequest::objects_post( diff --git a/nexus/tests/integration_tests/vpcs.rs b/nexus/tests/integration_tests/vpcs.rs index 5f1fc37873b..e83a5a24c56 100644 --- a/nexus/tests/integration_tests/vpcs.rs +++ b/nexus/tests/integration_tests/vpcs.rs @@ -481,6 +481,7 @@ async fn test_limited_collaborator_can_create_instance( start: true, auto_restart_policy: None, anti_affinity_groups: vec![], + enable_jumbo_frames: false, }, ) .authn_as(AuthnMode::SiloUser(limited_user.id)) diff --git a/nexus/types/src/external_api/mod.rs b/nexus/types/src/external_api/mod.rs index a784bac7989..c83076809e7 100644 --- a/nexus/types/src/external_api/mod.rs +++ b/nexus/types/src/external_api/mod.rs @@ -46,6 +46,7 @@ pub mod subnet_pool; pub mod support_bundle; pub mod switch; pub mod system; +pub mod system_networking; pub mod timeseries; pub mod update; pub mod user; diff --git a/nexus/types/src/external_api/system_networking.rs b/nexus/types/src/external_api/system_networking.rs new file mode 100644 index 00000000000..b094793b152 --- /dev/null +++ b/nexus/types/src/external_api/system_networking.rs @@ -0,0 +1,7 @@ +// 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/. + +//! Fleet-wide networking settings. + +pub use nexus_types_versions::latest::system_networking::*; diff --git a/nexus/types/src/internal_api/params.rs b/nexus/types/src/internal_api/params.rs index 9313cfd2925..206ffd8fdc6 100644 --- a/nexus/types/src/internal_api/params.rs +++ b/nexus/types/src/internal_api/params.rs @@ -205,6 +205,10 @@ pub struct RackInitializationRequest { /// * Trust quorum is not fully complete yet, and we only want this to be /// used in production once it is complete. pub initial_trust_quorum_configuration: Option, + /// Fleet-wide jumbo-frames opt-in (defaults to false). Operators can change + /// this post-init via the Nexus API. + #[serde(default)] + pub external_jumbo_frames_opt_in_enabled: bool, } pub type DnsConfigParams = internal_dns_types::config::DnsConfigParams; diff --git a/nexus/types/versions/Cargo.toml b/nexus/types/versions/Cargo.toml index 6c0a2274c6e..ddffa6b880c 100644 --- a/nexus/types/versions/Cargo.toml +++ b/nexus/types/versions/Cargo.toml @@ -16,6 +16,7 @@ daft.workspace = true dropshot.workspace = true http.workspace = true mg-admin-client.workspace = true +mg-api-types.workspace = true omicron-common.workspace = true omicron-passwords.workspace = true omicron-uuid-kinds.workspace = true diff --git a/nexus/types/versions/src/external_jumbo_frames/instance.rs b/nexus/types/versions/src/external_jumbo_frames/instance.rs new file mode 100644 index 00000000000..399ac41a293 --- /dev/null +++ b/nexus/types/versions/src/external_jumbo_frames/instance.rs @@ -0,0 +1,251 @@ +// 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/. + +//! Instance types for version EXTERNAL_JUMBO_FRAMES. +//! +//! Adds `enable_jumbo_frames` to `InstanceCreate`, `InstanceUpdate`, and the +//! `Instance` view. For create the field is `bool` (defaults to `false`); for +//! update it's `Option` (omit to leave the value unchanged); for the +//! view it's `bool` reflecting the current configured value. + +use omicron_common::api::external::{ + self, ByteCount, Hostname, IdentityMetadataCreateParams, + InstanceAutoRestartPolicy, InstanceCpuCount, InstanceCpuPlatform, NameOrId, + Nullable, +}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::v2025_11_20_00::instance::{ + InstanceDiskAttach, UserData, bool_true, +}; +use crate::v2026_01_03_00::instance::InstanceNetworkInterfaceAttachment; +use crate::v2026_01_05_00::instance::ExternalIpCreate; +use crate::v2026_01_08_00::multicast::MulticastGroupJoinSpec; +use crate::v2026_01_31_00::disk::DiskCreate; + +use crate::v2026_01_08_00; +use crate::v2026_01_31_00; + +/// Describe the instance's disks at creation time +#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum InstanceDiskAttachment { + /// During instance creation, create and attach disks + Create(DiskCreate), + /// During instance creation, attach this disk + Attach(InstanceDiskAttach), +} + +impl From + for InstanceDiskAttachment +{ + fn from(old: v2026_01_31_00::instance::InstanceDiskAttachment) -> Self { + match old { + v2026_01_31_00::instance::InstanceDiskAttachment::Create( + create, + ) => Self::Create(create), + v2026_01_31_00::instance::InstanceDiskAttachment::Attach( + attach, + ) => Self::Attach(attach), + } + } +} + +/// Create-time parameters for an `Instance` +#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] +pub struct InstanceCreate { + #[serde(flatten)] + pub identity: IdentityMetadataCreateParams, + /// The number of vCPUs to be allocated to the instance + pub ncpus: InstanceCpuCount, + /// The amount of RAM (in bytes) to be allocated to the instance + pub memory: ByteCount, + /// The hostname to be assigned to the instance + pub hostname: Hostname, + /// User data for instance initialization systems (such as cloud-init). + /// Must be a Base64-encoded string, as specified in RFC 4648 § 4 (+ and / + /// characters with padding). Maximum 32 KiB unencoded data. + #[serde(default, with = "UserData")] + pub user_data: Vec, + /// The network interfaces to be created for this instance. + #[serde(default)] + pub network_interfaces: InstanceNetworkInterfaceAttachment, + /// The external IP addresses provided to this instance. + /// + /// By default, all instances have outbound connectivity, but no inbound + /// connectivity. These external addresses can be used to provide a fixed, + /// known IP address for making inbound connections to the instance. + #[serde(default)] + pub external_ips: Vec, + /// Multicast groups this instance should join at creation. + /// + /// Groups can be specified by name, UUID, or IP address. Non-existent + /// groups are created automatically. + #[serde(default)] + pub multicast_groups: Vec, + /// A list of disks to be attached to the instance. + /// + /// Disk attachments of type "create" will be created, while those of type + /// "attach" must already exist. + /// + /// The order of this list does not guarantee a boot order for the + /// instance. Use the boot_disk attribute to specify a boot disk. When + /// boot_disk is specified it will count against the disk attachment limit. + #[serde(default)] + pub disks: Vec, + /// The disk the instance is configured to boot from. + /// + /// This disk can either be attached if it already exists or created along + /// with the instance. + /// + /// Specifying a boot disk is optional but recommended to ensure + /// predictable boot behavior. The boot disk can be set during instance + /// creation or later if the instance is stopped. The boot disk counts + /// against the disk attachment limit. + /// + /// An instance that does not have a boot disk set will use the boot + /// options specified in its UEFI settings, which are controlled by both + /// the instance's UEFI firmware and the guest operating system. Boot + /// options can change as disks are attached and detached, which may + /// result in an instance that only boots to the EFI shell until a boot + /// disk is set. + #[serde(default)] + pub boot_disk: Option, + /// An allowlist of SSH public keys to be transferred to the instance via + /// cloud-init during instance creation. + /// + /// If not provided, all SSH public keys from the user's profile will be + /// sent. If an empty list is provided, no public keys will be transmitted + /// to the instance. + pub ssh_public_keys: Option>, + /// Should this instance be started upon creation; true by default. + #[serde(default = "bool_true")] + pub start: bool, + /// The auto-restart policy for this instance. + /// + /// This policy determines whether the instance should be automatically + /// restarted by the control plane on failure. If this is `null`, no + /// auto-restart policy will be explicitly configured for this instance, + /// and the control plane will select the default policy when determining + /// whether the instance can be automatically restarted. + /// + /// Currently, the global default auto-restart policy is "best-effort", + /// so instances with `null` auto-restart policies will be automatically + /// restarted. However, in the future, the default policy may be + /// configurable through other mechanisms, such as on a per-project + /// basis. In that case, any configured default policy will be used if + /// this is `null`. + #[serde(default)] + pub auto_restart_policy: Option, + /// Anti-affinity groups to which this instance should be added. + #[serde(default)] + pub anti_affinity_groups: Vec, + /// The CPU platform to be used for this instance. If this is `null`, the + /// instance requires no particular CPU platform; when it is started the + /// instance will have the most general CPU platform supported by the sled + /// it is initially placed on. + #[serde(default)] + pub cpu_platform: Option, + /// Enable jumbo frames (8500 byte MTU) on the instance's primary OPTE + /// interface. Requires the fleet-wide jumbo-frames opt-in to be enabled + /// by an operator; otherwise this field must be `false`. Changes only take + /// effect on the next instance restart. + #[serde(default)] + pub enable_jumbo_frames: bool, +} + +impl From for InstanceCreate { + fn from(old: v2026_01_31_00::instance::InstanceCreate) -> Self { + Self { + identity: old.identity, + ncpus: old.ncpus, + memory: old.memory, + hostname: old.hostname, + user_data: old.user_data, + network_interfaces: old.network_interfaces, + external_ips: old.external_ips, + multicast_groups: old.multicast_groups, + disks: old.disks.into_iter().map(Into::into).collect(), + boot_disk: old.boot_disk.map(Into::into), + ssh_public_keys: old.ssh_public_keys, + start: old.start, + auto_restart_policy: old.auto_restart_policy, + anti_affinity_groups: old.anti_affinity_groups, + cpu_platform: old.cpu_platform, + enable_jumbo_frames: false, + } + } +} + +/// Parameters of an `Instance` that can be reconfigured after creation. +#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] +pub struct InstanceUpdate { + /// The number of vCPUs to be allocated to the instance + pub ncpus: InstanceCpuCount, + + /// The amount of RAM (in bytes) to be allocated to the instance + pub memory: ByteCount, + + /// The disk the instance is configured to boot from. + pub boot_disk: Nullable, + + /// The auto-restart policy for this instance. + pub auto_restart_policy: Nullable, + + /// The CPU platform to be used for this instance. + pub cpu_platform: Nullable, + + /// Multicast groups this instance should join. + #[serde(default)] + pub multicast_groups: Option>, + + /// Update the per-instance jumbo-frames opt-in. Setting this to `true` + /// requires the fleet-wide jumbo-frames opt-in to be enabled. Changes only + /// take effect on the next instance restart. + #[serde(default)] + pub enable_jumbo_frames: Option, +} + +impl From for InstanceUpdate { + fn from(old: v2026_01_08_00::instance::InstanceUpdate) -> Self { + Self { + ncpus: old.ncpus, + memory: old.memory, + boot_disk: old.boot_disk, + auto_restart_policy: old.auto_restart_policy, + cpu_platform: old.cpu_platform, + multicast_groups: old.multicast_groups, + enable_jumbo_frames: None, + } + } +} + +/// View of an Instance, including the per-instance jumbo-frames opt-in. +#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] +pub struct Instance { + /// All fields of the prior `Instance` view, unchanged. + #[serde(flatten)] + pub inner: external::Instance, + + /// When true, this instance has opted in to jumbo frames (8500 byte MTU) + /// on its primary network interface. The effective MTU also depends on + /// the fleet-wide jumbo-frames opt-in; if that is disabled, the primary + /// interface uses the default MTU regardless of this value. Changes only + /// take effect on the next instance restart. + pub enable_jumbo_frames: bool, +} + +impl From for external::Instance { + fn from(v: Instance) -> Self { + v.inner + } +} + +// Delegated so paginated listings can derive a marker from the inner identity. +impl external::ObjectIdentity for Instance { + fn identity(&self) -> &external::IdentityMetadata { + self.inner.identity() + } +} diff --git a/nexus/types/versions/src/external_jumbo_frames/mod.rs b/nexus/types/versions/src/external_jumbo_frames/mod.rs new file mode 100644 index 00000000000..137d59791f3 --- /dev/null +++ b/nexus/types/versions/src/external_jumbo_frames/mod.rs @@ -0,0 +1,18 @@ +// 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/. + +//! Version `EXTERNAL_JUMBO_FRAMES` of the external Nexus API. +//! +//! This version adds: +//! +//! - `InstanceCreate.enable_jumbo_frames` — per-instance opt-in for jumbo +//! frames (8500 byte MTU) on the primary OPTE interface. +//! - `InstanceUpdate.enable_jumbo_frames` — same disposition, mutable via +//! the existing update endpoint. Changes only take effect on the next +//! instance restart. +//! - `SystemNetworkingSettings` view/update — fleet-wide opt-in gate for +//! the per-instance bit, controlled by fleet admins. + +pub mod instance; +pub mod system_networking; diff --git a/nexus/types/versions/src/external_jumbo_frames/system_networking.rs b/nexus/types/versions/src/external_jumbo_frames/system_networking.rs new file mode 100644 index 00000000000..3834566992a --- /dev/null +++ b/nexus/types/versions/src/external_jumbo_frames/system_networking.rs @@ -0,0 +1,27 @@ +// 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/. + +//! Fleet-wide networking settings. + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +/// Fleet-wide networking settings. Only fleet admins may view or modify these +/// settings. +#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, PartialEq, Eq)] +pub struct SystemNetworkingSettings { + /// When true, end users may opt in to jumbo frames (8500 byte MTU) on the + /// primary interface of an instance. When false, instance-level opt-in is + /// ignored and OPTE ports are created with the default MTU. + pub external_jumbo_frames_opt_in_enabled: bool, +} + +/// Parameters for updating the fleet-wide networking settings. +#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, PartialEq, Eq)] +pub struct SystemNetworkingSettingsUpdate { + /// Toggle the fleet-wide external jumbo-frames opt-in. Omit to leave the + /// current value unchanged. + #[serde(default)] + pub external_jumbo_frames_opt_in_enabled: Option, +} diff --git a/nexus/types/versions/src/impls/networking.rs b/nexus/types/versions/src/impls/networking.rs index 0831b35c920..ddbcec8479f 100644 --- a/nexus/types/versions/src/impls/networking.rs +++ b/nexus/types/versions/src/impls/networking.rs @@ -21,11 +21,11 @@ impl From for latest::networking::AddressLotBlockCreate { } } -impl From +impl From for latest::networking::BgpPeerState { - fn from(s: mg_admin_client::types::FsmStateKind) -> Self { - use mg_admin_client::types::FsmStateKind; + fn from(s: mg_api_types::bgp::session::FsmStateKind) -> Self { + use mg_api_types::bgp::session::FsmStateKind; match s { FsmStateKind::Idle => Self::Idle, FsmStateKind::Connect => Self::Connect, diff --git a/nexus/types/versions/src/latest.rs b/nexus/types/versions/src/latest.rs index a223cc453d5..04faf97ab55 100644 --- a/nexus/types/versions/src/latest.rs +++ b/nexus/types/versions/src/latest.rs @@ -173,13 +173,13 @@ pub mod instance { pub use crate::v2026_01_05_00::instance::EphemeralIpCreate; pub use crate::v2026_01_05_00::instance::ExternalIpCreate; - pub use crate::v2026_01_08_00::instance::InstanceUpdate; - pub use crate::v2026_01_23_00::instance::EphemeralIpDetachSelector; pub use crate::v2026_01_23_00::instance::ExternalIpDetach; - pub use crate::v2026_01_31_00::instance::InstanceCreate; - pub use crate::v2026_01_31_00::instance::InstanceDiskAttachment; + pub use crate::v2026_05_20_00::instance::Instance; + pub use crate::v2026_05_20_00::instance::InstanceCreate; + pub use crate::v2026_05_20_00::instance::InstanceDiskAttachment; + pub use crate::v2026_05_20_00::instance::InstanceUpdate; } pub mod internet_gateway { @@ -402,6 +402,11 @@ pub mod system { pub use crate::v2025_11_20_00::system::PingStatus; } +pub mod system_networking { + pub use crate::v2026_05_20_00::system_networking::SystemNetworkingSettings; + pub use crate::v2026_05_20_00::system_networking::SystemNetworkingSettingsUpdate; +} + pub mod timeseries { pub use crate::v2025_11_20_00::timeseries::TimeseriesQuery; } diff --git a/nexus/types/versions/src/lib.rs b/nexus/types/versions/src/lib.rs index cc106e01acd..4f97ccbe2ec 100644 --- a/nexus/types/versions/src/lib.rs +++ b/nexus/types/versions/src/lib.rs @@ -85,3 +85,5 @@ pub mod v2026_04_16_00; pub mod v2026_05_07_00; #[path = "manual_disk_adoption/mod.rs"] pub mod v2026_05_08_00; +#[path = "external_jumbo_frames/mod.rs"] +pub mod v2026_05_20_00; diff --git a/openapi/bootstrap-agent-lockstep.json b/openapi/bootstrap-agent-lockstep.json index dd93582928d..c2125f53b31 100644 --- a/openapi/bootstrap-agent-lockstep.json +++ b/openapi/bootstrap-agent-lockstep.json @@ -1000,6 +1000,11 @@ "description": "DNS name for the DNS zone delegated to the rack for external DNS", "type": "string" }, + "external_jumbo_frames_opt_in_enabled": { + "description": "When true, end users may opt in to jumbo frames on the primary interface of an instance, and control plane services with external-facing OPTE NICs are brought up with an 8500 byte MTU. Operators can toggle this at runtime via the fleet networking settings API.", + "default": false, + "type": "boolean" + }, "internal_services_ip_pool_ranges": { "description": "Ranges of the service IP pool which may be used for internal services.", "type": "array", diff --git a/openapi/nexus-lockstep.json b/openapi/nexus-lockstep.json index f2cf1bdef6e..8751ab313b3 100644 --- a/openapi/nexus-lockstep.json +++ b/openapi/nexus-lockstep.json @@ -8284,6 +8284,11 @@ "description": "delegated DNS name for external DNS", "type": "string" }, + "external_jumbo_frames_opt_in_enabled": { + "description": "Fleet-wide jumbo-frames opt-in (defaults to false). Operators can change this post-init via the Nexus API.", + "default": false, + "type": "boolean" + }, "initial_trust_quorum_configuration": { "nullable": true, "description": "Data used to write the initial trust quorum configuration to CRDB\n\nThis is optional for two reasons: * For clusters fewer than 3 nodes, we don't support trust quorum. * Trust quorum is not fully complete yet, and we only want this to be used in production once it is complete.", diff --git a/openapi/nexus/nexus-2026050800.0.0-d2276f.json.gitstub b/openapi/nexus/nexus-2026050800.0.0-d2276f.json.gitstub new file mode 100644 index 00000000000..f90adfcd6f7 --- /dev/null +++ b/openapi/nexus/nexus-2026050800.0.0-d2276f.json.gitstub @@ -0,0 +1 @@ +7a911ab47e46b902fe7acc5e98dca5f79138fbf4:openapi/nexus/nexus-2026050800.0.0-d2276f.json diff --git a/openapi/nexus/nexus-2026050800.0.0-d2276f.json b/openapi/nexus/nexus-2026052000.0.0-8543ef.json similarity index 99% rename from openapi/nexus/nexus-2026050800.0.0-d2276f.json rename to openapi/nexus/nexus-2026052000.0.0-8543ef.json index 71ee60f8ce9..503187a6f8e 100644 --- a/openapi/nexus/nexus-2026050800.0.0-d2276f.json +++ b/openapi/nexus/nexus-2026052000.0.0-8543ef.json @@ -7,7 +7,7 @@ "url": "https://oxide.computer", "email": "api@oxide.computer" }, - "version": "2026050800.0.0" + "version": "2026052000.0.0" }, "paths": { "/device/auth": { @@ -11269,6 +11269,68 @@ } } }, + "/v1/system/networking/settings": { + "get": { + "tags": [ + "system/networking" + ], + "summary": "Fetch fleet-wide networking settings", + "operationId": "system_networking_settings_view", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SystemNetworkingSettings" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "tags": [ + "system/networking" + ], + "summary": "Update fleet-wide networking settings", + "operationId": "system_networking_settings_update", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SystemNetworkingSettingsUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SystemNetworkingSettings" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, "/v1/system/networking/switch-port-settings": { "get": { "tags": [ @@ -22612,7 +22674,7 @@ ] }, "Instance": { - "description": "View of an Instance", + "description": "View of an Instance, including the per-instance jumbo-frames opt-in.", "type": "object", "properties": { "auto_restart_cooldown_expiration": { @@ -22653,6 +22715,10 @@ "description": "Human-readable free-form text about a resource", "type": "string" }, + "enable_jumbo_frames": { + "description": "When true, this instance has opted in to jumbo frames (8500 byte MTU) on its primary network interface. The effective MTU also depends on the fleet-wide jumbo-frames opt-in; if that is disabled, the primary interface uses the default MTU regardless of this value. Changes only take effect on the next instance restart.", + "type": "boolean" + }, "hostname": { "description": "RFC1035-compliant hostname for the instance", "type": "string" @@ -22718,6 +22784,7 @@ "required": [ "auto_restart_enabled", "description", + "enable_jumbo_frames", "hostname", "id", "memory", @@ -22827,6 +22894,11 @@ "$ref": "#/components/schemas/InstanceDiskAttachment" } }, + "enable_jumbo_frames": { + "description": "Enable jumbo frames (8500 byte MTU) on the instance's primary OPTE interface. Requires the fleet-wide jumbo-frames opt-in to be enabled by an operator; otherwise this field must be `false`. Changes only take effect on the next instance restart.", + "default": false, + "type": "boolean" + }, "external_ips": { "description": "The external IP addresses provided to this instance.\n\nBy default, all instances have outbound connectivity, but no inbound connectivity. These external addresses can be used to provide a fixed, known IP address for making inbound connections to the instance.", "default": [], @@ -23411,7 +23483,7 @@ "properties": { "auto_restart_policy": { "nullable": true, - "description": "The auto-restart policy for this instance.\n\nThis policy determines whether the instance should be automatically restarted by the control plane on failure. If this is `null`, any explicitly configured auto-restart policy will be unset, and the control plane will select the default policy when determining whether the instance can be automatically restarted.\n\nCurrently, the global default auto-restart policy is \"best-effort\", so instances with `null` auto-restart policies will be automatically restarted. However, in the future, the default policy may be configurable through other mechanisms, such as on a per-project basis. In that case, any configured default policy will be used if this is `null`.", + "description": "The auto-restart policy for this instance.", "allOf": [ { "$ref": "#/components/schemas/InstanceAutoRestartPolicy" @@ -23420,7 +23492,7 @@ }, "boot_disk": { "nullable": true, - "description": "The disk the instance is configured to boot from.\n\nSetting a boot disk is optional but recommended to ensure predictable boot behavior. The boot disk can be set during instance creation or later if the instance is stopped. The boot disk counts against the disk attachment limit.\n\nAn instance that does not have a boot disk set will use the boot options specified in its UEFI settings, which are controlled by both the instance's UEFI firmware and the guest operating system. Boot options can change as disks are attached and detached, which may result in an instance that only boots to the EFI shell until a boot disk is set.", + "description": "The disk the instance is configured to boot from.", "allOf": [ { "$ref": "#/components/schemas/NameOrId" @@ -23429,13 +23501,19 @@ }, "cpu_platform": { "nullable": true, - "description": "The CPU platform to be used for this instance. If this is `null`, the instance requires no particular CPU platform; when it is started the instance will have the most general CPU platform supported by the sled it is initially placed on.", + "description": "The CPU platform to be used for this instance.", "allOf": [ { "$ref": "#/components/schemas/InstanceCpuPlatform" } ] }, + "enable_jumbo_frames": { + "nullable": true, + "description": "Update the per-instance jumbo-frames opt-in. Setting this to `true` requires the fleet-wide jumbo-frames opt-in to be enabled. Changes only take effect on the next instance restart.", + "default": null, + "type": "boolean" + }, "memory": { "description": "The amount of RAM (in bytes) to be allocated to the instance", "allOf": [ @@ -23446,7 +23524,7 @@ }, "multicast_groups": { "nullable": true, - "description": "Multicast groups this instance should join.\n\nWhen specified, this replaces the instance's current multicast group membership with the new set of groups. The instance will leave any groups not listed here and join any new groups that are specified.\n\nEach entry can specify the group by name, UUID, or IP address, along with optional source IP filtering for SSM (Source-Specific Multicast). When a group doesn't exist, it will be implicitly created using the default multicast pool (or you can specify `ip_version` to disambiguate if needed).\n\nIf not provided, the instance's multicast group membership will not be changed.", + "description": "Multicast groups this instance should join.", "default": null, "type": "array", "items": { @@ -29582,6 +29660,31 @@ } ] }, + "SystemNetworkingSettings": { + "description": "Fleet-wide networking settings. Only fleet admins may view or modify these settings.", + "type": "object", + "properties": { + "external_jumbo_frames_opt_in_enabled": { + "description": "When true, end users may opt in to jumbo frames (8500 byte MTU) on the primary interface of an instance. When false, instance-level opt-in is ignored and OPTE ports are created with the default MTU.", + "type": "boolean" + } + }, + "required": [ + "external_jumbo_frames_opt_in_enabled" + ] + }, + "SystemNetworkingSettingsUpdate": { + "description": "Parameters for updating the fleet-wide networking settings.", + "type": "object", + "properties": { + "external_jumbo_frames_opt_in_enabled": { + "nullable": true, + "description": "Toggle the fleet-wide external jumbo-frames opt-in. Omit to leave the current value unchanged.", + "default": null, + "type": "boolean" + } + } + }, "TargetRelease": { "description": "View of a system software target release", "type": "object", diff --git a/openapi/nexus/nexus-latest.json b/openapi/nexus/nexus-latest.json index 47e667754f5..14a009c5c00 120000 --- a/openapi/nexus/nexus-latest.json +++ b/openapi/nexus/nexus-latest.json @@ -1 +1 @@ -nexus-2026050800.0.0-d2276f.json \ No newline at end of file +nexus-2026052000.0.0-8543ef.json \ No newline at end of file diff --git a/openapi/sled-agent/sled-agent-40.0.0-600e45.json.gitstub b/openapi/sled-agent/sled-agent-40.0.0-600e45.json.gitstub new file mode 100644 index 00000000000..23b7f916608 --- /dev/null +++ b/openapi/sled-agent/sled-agent-40.0.0-600e45.json.gitstub @@ -0,0 +1 @@ +a3a58cc520fcb12bfad1a946eae784ec08cf8718:openapi/sled-agent/sled-agent-40.0.0-600e45.json diff --git a/openapi/sled-agent/sled-agent-40.0.0-600e45.json b/openapi/sled-agent/sled-agent-41.0.0-d909db.json similarity index 99% rename from openapi/sled-agent/sled-agent-40.0.0-600e45.json rename to openapi/sled-agent/sled-agent-41.0.0-d909db.json index fa6c04f9104..c9997880e35 100644 --- a/openapi/sled-agent/sled-agent-40.0.0-600e45.json +++ b/openapi/sled-agent/sled-agent-41.0.0-d909db.json @@ -7,7 +7,7 @@ "url": "https://oxide.computer", "email": "api@oxide.computer" }, - "version": "40.0.0" + "version": "41.0.0" }, "paths": { "/artifacts": { @@ -6082,6 +6082,13 @@ "items": { "$ref": "#/components/schemas/NetworkInterface" } + }, + "primary_nic_mtu": { + "nullable": true, + "description": "The MTU to apply to the instance's primary OPTE port, in bytes. If `None`, the OPTE default is used (1500). Set when the fleet has enabled external jumbo frames and the instance has opted in.", + "type": "integer", + "format": "uint32", + "minimum": 0 } }, "required": [ diff --git a/openapi/sled-agent/sled-agent-latest.json b/openapi/sled-agent/sled-agent-latest.json index 5f70b30a1ee..9e97a1678ca 120000 --- a/openapi/sled-agent/sled-agent-latest.json +++ b/openapi/sled-agent/sled-agent-latest.json @@ -1 +1 @@ -sled-agent-40.0.0-600e45.json \ No newline at end of file +sled-agent-41.0.0-d909db.json \ No newline at end of file diff --git a/openapi/wicketd.json b/openapi/wicketd.json index 08e0548e44e..5219fb03ab1 100644 --- a/openapi/wicketd.json +++ b/openapi/wicketd.json @@ -1636,6 +1636,11 @@ "external_dns_zone_name": { "type": "string" }, + "external_jumbo_frames_opt_in_enabled": { + "description": "Enable the fleet-wide jumbo-frames opt-in. Operators can also toggle this at runtime via the Nexus API after handoff.", + "default": false, + "type": "boolean" + }, "internal_services_ip_pool_ranges": { "type": "array", "items": { @@ -3403,6 +3408,11 @@ "external_dns_zone_name": { "type": "string" }, + "external_jumbo_frames_opt_in_enabled": { + "description": "Enable the fleet-wide jumbo-frames opt-in.", + "default": false, + "type": "boolean" + }, "internal_services_ip_pool_ranges": { "type": "array", "items": { diff --git a/package-manifest.toml b/package-manifest.toml index 204a5436de3..c1e1d223af1 100644 --- a/package-manifest.toml +++ b/package-manifest.toml @@ -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 = "7696ee48d5ee29a917dea459e281fe2e8ff20513" +source.commit = "b27bea5344fb21b3b88af6717b969e31ee6a9ccb" # The SHA256 digest is automatically posted to: # https://buildomat.eng.oxide.computer/public/file/oxidecomputer/maghemite/image//mg-ddm-gz.sha256.txt -source.sha256 = "ce52b9094adf0ed567bd3ed1e3ac48ac1c983cc7859adacf4f392e415a1189ad" +source.sha256 = "3df7654970bfa71a654c060b8381c3ef507f991a204923b36828258b42724344" 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 = "7696ee48d5ee29a917dea459e281fe2e8ff20513" +source.commit = "b27bea5344fb21b3b88af6717b969e31ee6a9ccb" # The SHA256 digest is automatically posted to: # https://buildomat.eng.oxide.computer/public/file/oxidecomputer/maghemite/image//mg-ddm.sha256.txt -source.sha256 = "23950a4e73a07fa7f087ba3312e4bc5a8981fd9ebad54af2350baaa86ad6bbf3" +source.sha256 = "c72502346a4fe1f3762bc15c2d755caaeef3275ad81b4130d4609f23340762a0" 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 = "7696ee48d5ee29a917dea459e281fe2e8ff20513" +source.commit = "b27bea5344fb21b3b88af6717b969e31ee6a9ccb" # The SHA256 digest is automatically posted to: # https://buildomat.eng.oxide.computer/public/file/oxidecomputer/maghemite/image//mgd.sha256.txt -source.sha256 = "301d31ca481e4822f69484feacca31dd08a7c4aae87d96641d384bda3178d2f3" +source.sha256 = "350a6684051132b3d18f618f81d4c5cbf5dd1d9e93d522b9fb5c2305ae19b358" output.type = "zone" output.intermediate_only = true @@ -725,8 +725,8 @@ output.intermediate_only = true service_name = "lldp" source.type = "prebuilt" source.repo = "lldp" -source.commit = "61479b6922f9112fbe1e722414d2b8055212cb12" -source.sha256 = "8f988c0b0fa3ad4121ab0e825298601035e56c5c054bdc3a1dfb4d6c8fd5b300" +source.commit = "54b266174d4de9628bca9c97b0db176e16f12154" +source.sha256 = "0c56ec13dcb11f724e158281947062aa4e70160b202728e85b05ca765673c819" output.type = "zone" output.intermediate_only = true diff --git a/schema/crdb/dbinit.sql b/schema/crdb/dbinit.sql index 308379c85c5..76aa53ad55e 100644 --- a/schema/crdb/dbinit.sql +++ b/schema/crdb/dbinit.sql @@ -1208,6 +1208,33 @@ CREATE TABLE IF NOT EXISTS omicron.public.silo_auth_settings ( -- null means no max: users can tokens that never expire device_token_max_ttl_seconds INT8 CHECK (device_token_max_ttl_seconds > 0) ); + +/* + * Fleet-wide networking settings. Singleton row; see `db_metadata` for an + * explanation of the singleton pattern. + */ +CREATE TABLE IF NOT EXISTS omicron.public.system_networking_settings ( + singleton BOOL NOT NULL PRIMARY KEY, + time_created TIMESTAMPTZ NOT NULL, + time_modified TIMESTAMPTZ NOT NULL, + + -- When true, end users may opt in to jumbo frames (8500 byte MTU) on the + -- primary interface of an instance. When false, the per-instance bit is + -- ignored and ports are created with the default MTU. + external_jumbo_frames_opt_in_enabled BOOL NOT NULL, + + CHECK (singleton = true) +); + +INSERT INTO omicron.public.system_networking_settings ( + singleton, + time_created, + time_modified, + external_jumbo_frames_opt_in_enabled +) VALUES ( + TRUE, NOW(), NOW(), FALSE +) ON CONFLICT DO NOTHING; + /* * Projects */ @@ -1423,6 +1450,15 @@ CREATE TABLE IF NOT EXISTS omicron.public.instance ( */ cpu_platform omicron.public.instance_cpu_platform, + /* + * When set this instance has opted in to jumbo frames (8500 byte MTU) + * on its primary OPTE interface. The effective MTU also depends on the + * fleet-wide setting in `system_networking_settings`; if that flag is off, + * the OPTE port is created with the default MTU regardless of this column. + * Changes to this column only take effect on the next instance restart. + */ + enable_jumbo_frames BOOL NOT NULL, + CONSTRAINT vmm_iff_active_propolis CHECK ( ((state = 'vmm') AND (active_propolis_id IS NOT NULL)) OR ((state != 'vmm') AND (active_propolis_id IS NULL)) @@ -8623,7 +8659,7 @@ INSERT INTO omicron.public.db_metadata ( version, target_version ) VALUES - (TRUE, NOW(), NOW(), '261.0.0', NULL) + (TRUE, NOW(), NOW(), '262.0.0', NULL) ON CONFLICT DO NOTHING; COMMIT; diff --git a/schema/crdb/external-jumbo-frames/up1.sql b/schema/crdb/external-jumbo-frames/up1.sql new file mode 100644 index 00000000000..51c1623c22a --- /dev/null +++ b/schema/crdb/external-jumbo-frames/up1.sql @@ -0,0 +1,14 @@ +CREATE TABLE IF NOT EXISTS omicron.public.system_networking_settings ( + -- Singleton row for fleet-wide networking settings. See `db_metadata` + -- for an explanation of the singleton pattern. + singleton BOOL NOT NULL PRIMARY KEY, + time_created TIMESTAMPTZ NOT NULL, + time_modified TIMESTAMPTZ NOT NULL, + + -- When true, end users may opt in to jumbo frames (8500 byte MTU) on + -- the primary interface of an instance. When false, the per-instance + -- bit is ignored and ports are created with the default MTU. + external_jumbo_frames_opt_in_enabled BOOL NOT NULL, + + CHECK (singleton = true) +); diff --git a/schema/crdb/external-jumbo-frames/up2.sql b/schema/crdb/external-jumbo-frames/up2.sql new file mode 100644 index 00000000000..6bc442ddc8b --- /dev/null +++ b/schema/crdb/external-jumbo-frames/up2.sql @@ -0,0 +1,8 @@ +INSERT INTO omicron.public.system_networking_settings ( + singleton, + time_created, + time_modified, + external_jumbo_frames_opt_in_enabled +) VALUES ( + TRUE, NOW(), NOW(), FALSE +) ON CONFLICT DO NOTHING; diff --git a/schema/crdb/external-jumbo-frames/up3.sql b/schema/crdb/external-jumbo-frames/up3.sql new file mode 100644 index 00000000000..df72d4bad3b --- /dev/null +++ b/schema/crdb/external-jumbo-frames/up3.sql @@ -0,0 +1,2 @@ +ALTER TABLE omicron.public.instance + ADD COLUMN IF NOT EXISTS enable_jumbo_frames BOOL NOT NULL DEFAULT FALSE; diff --git a/schema/crdb/external-jumbo-frames/up4.sql b/schema/crdb/external-jumbo-frames/up4.sql new file mode 100644 index 00000000000..2c881302854 --- /dev/null +++ b/schema/crdb/external-jumbo-frames/up4.sql @@ -0,0 +1 @@ +ALTER TABLE omicron.public.instance ALTER COLUMN enable_jumbo_frames DROP DEFAULT; diff --git a/sled-agent/api/src/lib.rs b/sled-agent/api/src/lib.rs index 0ee2567f5e1..0b80d2f0300 100644 --- a/sled-agent/api/src/lib.rs +++ b/sled-agent/api/src/lib.rs @@ -21,7 +21,7 @@ use omicron_common::api::internal::{ }; use sled_agent_types_versions::{ latest, v1, v4, v6, v7, v9, v10, v11, v12, v14, v16, v17, v18, v20, v22, - v24, v25, v26, v28, v29, v30, v31, v33, v34, v37, v39, + v24, v25, v26, v28, v29, v30, v31, v32, v33, v34, v37, v39, }; use sled_diagnostics::SledDiagnosticsQueryOutput; use slog_error_chain::InlineErrorChain; @@ -38,6 +38,7 @@ api_versions!([ // | example for the next person. // v // (next_int, IDENT), + (41, ADD_INSTANCE_PRIMARY_NIC_MTU), (40, ADD_FMD_TO_INVENTORY), (39, BOOTSTORE_SERVICE_NAT_GENERATION), (38, RENAME_PORT_FEC_SPEED_TO_LINK_FEC_SPEED), @@ -444,7 +445,7 @@ pub trait SledAgentApi { operation_id = "vmm_register", method = PUT, path = "/vmms/{propolis_id}", - versions = VERSION_MAKE_ALL_EXTERNAL_IP_FIELDS_OPTIONAL.. + versions = VERSION_ADD_INSTANCE_PRIMARY_NIC_MTU.. }] async fn vmm_register( rqctx: RequestContext, @@ -452,6 +453,20 @@ pub trait SledAgentApi { body: TypedBody, ) -> Result, HttpError>; + #[endpoint { + operation_id = "vmm_register", + method = PUT, + path = "/vmms/{propolis_id}", + versions = VERSION_MAKE_ALL_EXTERNAL_IP_FIELDS_OPTIONAL..VERSION_ADD_INSTANCE_PRIMARY_NIC_MTU + }] + async fn vmm_register_v32( + rqctx: RequestContext, + path_params: Path, + body: TypedBody, + ) -> Result, HttpError> { + Self::vmm_register(rqctx, path_params, body.map(Into::into)).await + } + #[endpoint { operation_id = "vmm_register", method = PUT, @@ -463,7 +478,7 @@ pub trait SledAgentApi { path_params: Path, body: TypedBody, ) -> Result, HttpError> { - Self::vmm_register(rqctx, path_params, body.map(Into::into)).await + Self::vmm_register_v32(rqctx, path_params, body.map(Into::into)).await } #[endpoint { diff --git a/sled-agent/bootstrap-agent-lockstep-types/src/lib.rs b/sled-agent/bootstrap-agent-lockstep-types/src/lib.rs index 0f6d3b64d55..7813224da4c 100644 --- a/sled-agent/bootstrap-agent-lockstep-types/src/lib.rs +++ b/sled-agent/bootstrap-agent-lockstep-types/src/lib.rs @@ -78,6 +78,13 @@ pub struct RackInitializeRequest { /// IPs or subnets allowed to make requests to user-facing services pub allowed_source_ips: AllowedSourceIps, + + /// When true, end users may opt in to jumbo frames on the primary interface + /// of an instance, and control plane services with external-facing OPTE + /// NICs are brought up with an 8500 byte MTU. Operators can toggle this at + /// runtime via the fleet networking settings API. + #[serde(default)] + pub external_jumbo_frames_opt_in_enabled: bool, } // This custom debug implementation hides the private keys. @@ -97,6 +104,7 @@ impl std::fmt::Debug for RackInitializeRequest { recovery_silo, rack_network_config, allowed_source_ips, + external_jumbo_frames_opt_in_enabled, } = &self; f.debug_struct("RackInitializeRequest") @@ -114,6 +122,10 @@ impl std::fmt::Debug for RackInitializeRequest { .field("recovery_silo", recovery_silo) .field("rack_network_config", rack_network_config) .field("allowed_source_ips", allowed_source_ips) + .field( + "external_jumbo_frames_opt_in_enabled", + external_jumbo_frames_opt_in_enabled, + ) .finish() } } @@ -156,6 +168,8 @@ struct UnvalidatedRackInitializeRequest { // passing this field. #[serde(default = "default_allowed_source_ips")] allowed_source_ips: AllowedSourceIps, + #[serde(default)] + external_jumbo_frames_opt_in_enabled: bool, } impl TryFrom for RackInitializeRequest { @@ -182,6 +196,8 @@ impl TryFrom for RackInitializeRequest { recovery_silo: value.recovery_silo, rack_network_config: value.rack_network_config, allowed_source_ips: value.allowed_source_ips, + external_jumbo_frames_opt_in_enabled: value + .external_jumbo_frames_opt_in_enabled, }) } } diff --git a/sled-agent/early-networking/Cargo.toml b/sled-agent/early-networking/Cargo.toml index 0ae4cc1428c..4d662b6ea03 100644 --- a/sled-agent/early-networking/Cargo.toml +++ b/sled-agent/early-networking/Cargo.toml @@ -23,7 +23,7 @@ omicron-common.workspace = true omicron-ddm-admin-client.workspace = true omicron-workspace-hack.workspace = true oxnet.workspace = true -rdb-types.workspace = true +mg-api-types.workspace = true sled-agent-types.workspace = true slog.workspace = true slog-error-chain.workspace = true diff --git a/sled-agent/early-networking/src/lib.rs b/sled-agent/early-networking/src/lib.rs index b4e9e6f768b..3cb3421ebdc 100644 --- a/sled-agent/early-networking/src/lib.rs +++ b/sled-agent/early-networking/src/lib.rs @@ -16,18 +16,23 @@ use internal_dns_resolver::{ResolveError, Resolver as DnsResolver}; use internal_dns_types::names::ServiceName; use mg_admin_client::Client as MgdClient; use mg_admin_client::types::{ - AddStaticRoute4Request, AddStaticRoute6Request, ApplyRequest, - BestpathFanoutRequest, CheckerSource, - ImportExportPolicy4 as MgImportExportPolicy4, - ImportExportPolicy6 as MgImportExportPolicy6, JitterRange, ShaperSource, - StaticRoute4, StaticRoute4List, StaticRoute6, StaticRoute6List, + ApplyRequest, BfdPeerConfig as MgBfdPeerConfig, + BgpPeerConfig as MgBgpPeerConfig, + UnnumberedBgpPeerConfig as MgUnnumberedBgpPeerConfig, }; -use mg_admin_client::types::{ - BfdPeerConfig as MgBfdPeerConfig, Ipv4UnicastConfig, +use mg_api_types::bgp::config::{ + CheckerSource, Ipv4UnicastConfig, Ipv6UnicastConfig, JitterRange, + ShaperSource, }; -use mg_admin_client::types::{ - BgpPeerConfig as MgBgpPeerConfig, Ipv6UnicastConfig, - UnnumberedBgpPeerConfig as MgUnnumberedBgpPeerConfig, +use mg_api_types::bgp::policy::{ + ImportExportPolicy4 as MgImportExportPolicy4, + ImportExportPolicy6 as MgImportExportPolicy6, +}; +use mg_api_types::rdb::prefix::{Prefix, Prefix4, Prefix6}; +use mg_api_types::rib::BestpathFanoutRequest; +use mg_api_types::static_routes::{ + AddStaticRoute4Request, AddStaticRoute6Request, StaticRoute4, + StaticRoute4List, StaticRoute6, StaticRoute6List, }; use omicron_common::OMICRON_DPD_TAG; use omicron_common::address::DENDRITE_PORT; @@ -37,7 +42,6 @@ use omicron_common::backoff::{ }; use omicron_ddm_admin_client::DdmError; use oxnet::IpNet; -use rdb_types::{Prefix, Prefix4, Prefix6}; use sled_agent_types::early_networking::{ BfdMode, BgpConfig, BgpPeerConfig, ImportExportPolicy, LinkFec, LinkSpeed, PortConfig, RouterPeerType, SwitchSlot, UplinkAddress, @@ -712,7 +716,7 @@ impl<'a> EarlyNetworkSetup<'a> { fanout: config.max_paths.as_nonzero_u8(), }; - if let Err(e) = mgd.bgp_apply_v2(&request).await { + if let Err(e) = mgd.bgp_apply(&request).await { error!( self.log, "BGP peer configuration failed"; @@ -754,7 +758,20 @@ impl<'a> EarlyNetworkSetup<'a> { length: r.destination.width(), }; let sr = StaticRoute4 { - nexthop, + nexthop: IpAddr::V4(nexthop), + prefix, + vlan_id, + rib_priority, + }; + rq.routes.list.push(sr); + } + (IpAddr::V6(nexthop), IpAddr::V4(dest_addr)) => { + let prefix = Prefix4 { + value: dest_addr, + length: r.destination.width(), + }; + let sr = StaticRoute4 { + nexthop: IpAddr::V6(nexthop), prefix, vlan_id, rib_priority, @@ -774,10 +791,10 @@ impl<'a> EarlyNetworkSetup<'a> { }; rqv6.routes.list.push(sr); } - _ => { + (IpAddr::V4(_), IpAddr::V6(_)) => { error!( self.log, - "nexthop and destination are different address types"; + "v6 destination over v4 nexthop not supported"; "nexthop" => ?r.nexthop, "destination" => ?r.destination.addr(), ); @@ -800,7 +817,7 @@ impl<'a> EarlyNetworkSetup<'a> { self.log, "static route configuration failed"; "error" => ?e, - "configuration" => ?rq, + "configuration" => ?rqv6, ); }; diff --git a/sled-agent/rack-setup/src/plan/service.rs b/sled-agent/rack-setup/src/plan/service.rs index 48de2b1d3e9..d0ea30fc4fa 100644 --- a/sled-agent/rack-setup/src/plan/service.rs +++ b/sled-agent/rack-setup/src/plan/service.rs @@ -1482,6 +1482,7 @@ mod tests { bfd: Vec::new(), }, allowed_source_ips: AllowedSourceIps::Any, + external_jumbo_frames_opt_in_enabled: false, }; (dns_ips.to_vec(), config) diff --git a/sled-agent/rack-setup/src/service.rs b/sled-agent/rack-setup/src/service.rs index a9147694cc6..fe2444cc754 100644 --- a/sled-agent/rack-setup/src/service.rs +++ b/sled-agent/rack-setup/src/service.rs @@ -1063,6 +1063,8 @@ impl ServiceInner { rack_network_config, allowed_source_ips, initial_trust_quorum_configuration, + external_jumbo_frames_opt_in_enabled: config + .external_jumbo_frames_opt_in_enabled, }; let notify_nexus = || async { @@ -2211,6 +2213,7 @@ mod test { bfd: Vec::new(), }, allowed_source_ips: AllowedSourceIps::Any, + external_jumbo_frames_opt_in_enabled: false, }; assert_eq!( diff --git a/sled-agent/src/instance.rs b/sled-agent/src/instance.rs index a009e7f3843..2812d3d533d 100644 --- a/sled-agent/src/instance.rs +++ b/sled-agent/src/instance.rs @@ -568,6 +568,11 @@ struct InstanceRunner { firewall_rules: Vec, dhcp_config: DhcpCfg, + // Effective MTU for the primary NIC's OPTE port. `None` means use the OPTE + // default (1500). Populated by Nexus based on jumbo-frame opt-in (fleet + // flag AND instance bit). + primary_nic_mtu: Option, + // Internal State management state: InstanceStates, running_state: Option, @@ -1925,6 +1930,7 @@ impl Instance { multicast_groups: local_config.multicast_groups, firewall_rules: local_config.firewall_rules, dhcp_config, + primary_nic_mtu: local_config.primary_nic_mtu, state: InstanceStates::new(vmm_runtime, migration_id), running_state: None, nexus_client, @@ -2337,6 +2343,7 @@ impl InstanceRunner { .copied() .map(Into::into) .collect(), + mtu: if nic.primary { self.primary_nic_mtu } else { None }, })?; opte_port_names.push(port.0.name().to_string()); opte_ports.push(port); @@ -3004,6 +3011,7 @@ mod tests { }, delegated_zvols: vec![], attached_subnets: vec![], + primary_nic_mtu: None, }; InstanceInitialState { @@ -3633,6 +3641,7 @@ mod tests { multicast_groups: local_config.multicast_groups, firewall_rules: local_config.firewall_rules, dhcp_config, + primary_nic_mtu: local_config.primary_nic_mtu, state: InstanceStates::new(vmm_runtime, migration_id), running_state: None, nexus_client, diff --git a/sled-agent/src/probe_manager.rs b/sled-agent/src/probe_manager.rs index 2f2d5421204..c0494f2b136 100644 --- a/sled-agent/src/probe_manager.rs +++ b/sled-agent/src/probe_manager.rs @@ -382,6 +382,7 @@ impl ProbeManagerInner { // but probes are supposed to mimic instances as closely as // possible. We should consider if we want to support them here. attached_subnets: vec![], + mtu: None, })?; let installed_zone = ZoneBuilderFactory::new() diff --git a/sled-agent/src/services.rs b/sled-agent/src/services.rs index bd2ad97dc02..ee0a9dc0021 100644 --- a/sled-agent/src/services.rs +++ b/sled-agent/src/services.rs @@ -1177,6 +1177,17 @@ impl ServiceManager { dhcp_config: DhcpCfg::default(), // Services do not use attached subnets, only instances. attached_subnets: vec![], + // TODO(RFD 689): plumb the fleet-wide jumbo-frames opt-in + // (`system_networking_settings.external_jumbo_frames_opt_in_enabled`) + // through blueprints / reconfigurator-execution to this point + // so that external-facing service zones (Nexus, ExternalDns, + // BoundaryNtp) are brought up with `EXTERNAL_JUMBO_FRAMES_MTU` + // when the operator has enabled the opt-in. + // + // For now, services always use the default MTU. The + // per-instance jumbo opt-in (the primary user-facing feature + // from RFD 689) is fully wired through `instance_ensure_registered`. + mtu: None, }) .map_err(|err| Error::ServicePortCreation { service: zone_kind, diff --git a/sled-agent/src/sim/server.rs b/sled-agent/src/sim/server.rs index a20acd0ee3c..1eb0b1d1c74 100644 --- a/sled-agent/src/sim/server.rs +++ b/sled-agent/src/sim/server.rs @@ -658,6 +658,7 @@ pub async fn run_standalone_server( }, allowed_source_ips: AllowedSourceIps::Any, initial_trust_quorum_configuration: None, + external_jumbo_frames_opt_in_enabled: false, }; let mut nexus_lockstep_address = config.nexus_address; diff --git a/sled-agent/types/versions/src/add_instance_primary_nic_mtu/instance.rs b/sled-agent/types/versions/src/add_instance_primary_nic_mtu/instance.rs new file mode 100644 index 00000000000..31cb7684ebd --- /dev/null +++ b/sled-agent/types/versions/src/add_instance_primary_nic_mtu/instance.rs @@ -0,0 +1,104 @@ +// 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/. + +//! Instance types for version `ADD_INSTANCE_PRIMARY_NIC_MTU`. + +use std::net::SocketAddr; + +use omicron_common::api::external::Hostname; +use omicron_common::api::internal::shared::DelegatedZvol; +use omicron_common::api::internal::shared::DhcpConfig; +use omicron_uuid_kinds::InstanceUuid; +use schemars::JsonSchema; +use serde::Deserialize; +use serde::Serialize; +use uuid::Uuid; + +use crate::v1::instance::InstanceMetadata; +use crate::v1::instance::VmmRuntimeState; +use crate::v7::instance::InstanceMulticastMembership; +use crate::v10::inventory::NetworkInterface; +use crate::v18::attached_subnet::AttachedSubnet; +use crate::v29::instance::VmmSpec; +use crate::v31::instance::ResolvedVpcFirewallRule; +use crate::v32; +use crate::v32::instance::ExternalIpConfig; + +/// The body of a request to ensure that a instance and VMM are known to a sled +/// agent. +#[derive(Serialize, Deserialize, JsonSchema)] +pub struct InstanceEnsureBody { + /// The virtual hardware configuration this virtual machine should have when + /// it is started. + pub vmm_spec: VmmSpec, + + /// Information about the sled-local configuration that needs to be + /// established to make the VM's virtual hardware fully functional. + pub local_config: InstanceSledLocalConfig, + + /// The initial VMM runtime state for the VMM being registered. + pub vmm_runtime: VmmRuntimeState, + + /// The ID of the instance for which this VMM is being created. + pub instance_id: InstanceUuid, + + /// The ID of the migration in to this VMM, if this VMM is being + /// ensured is part of a migration in. If this is `None`, the VMM is not + /// being created due to a migration. + pub migration_id: Option, + + /// The address at which this VMM should serve a Propolis server API. + pub propolis_addr: SocketAddr, + + /// Metadata used to track instance statistics. + pub metadata: InstanceMetadata, +} + +/// Describes sled-local configuration that a sled-agent must establish to make +/// the instance's virtual hardware fully functional. +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] +pub struct InstanceSledLocalConfig { + pub hostname: Hostname, + pub nics: Vec, + pub external_ips: ExternalIpConfig, + pub attached_subnets: Vec, + pub multicast_groups: Vec, + pub firewall_rules: Vec, + pub dhcp_config: DhcpConfig, + pub delegated_zvols: Vec, + /// The MTU to apply to the instance's primary OPTE port, in bytes. If + /// `None`, the OPTE default is used (1500). Set when the fleet has + /// enabled external jumbo frames and the instance has opted in. + pub primary_nic_mtu: Option, +} + +impl From for InstanceEnsureBody { + fn from(old: v32::instance::InstanceEnsureBody) -> Self { + Self { + vmm_spec: old.vmm_spec, + local_config: old.local_config.into(), + vmm_runtime: old.vmm_runtime, + instance_id: old.instance_id, + migration_id: old.migration_id, + propolis_addr: old.propolis_addr, + metadata: old.metadata, + } + } +} + +impl From for InstanceSledLocalConfig { + fn from(old: v32::instance::InstanceSledLocalConfig) -> Self { + Self { + hostname: old.hostname, + nics: old.nics, + external_ips: old.external_ips, + attached_subnets: old.attached_subnets, + multicast_groups: old.multicast_groups, + firewall_rules: old.firewall_rules, + dhcp_config: old.dhcp_config, + delegated_zvols: old.delegated_zvols, + primary_nic_mtu: None, + } + } +} diff --git a/sled-agent/types/versions/src/add_instance_primary_nic_mtu/mod.rs b/sled-agent/types/versions/src/add_instance_primary_nic_mtu/mod.rs new file mode 100644 index 00000000000..14ea508c0e7 --- /dev/null +++ b/sled-agent/types/versions/src/add_instance_primary_nic_mtu/mod.rs @@ -0,0 +1,10 @@ +// 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/. + +//! Version `ADD_INSTANCE_PRIMARY_NIC_MTU` of the Sled Agent API. +//! +//! This version adds an MTU field for the instance's primary OPTE NIC so +//! Nexus can request an 8500 byte MTU when jumbo frames are enabled. + +pub mod instance; diff --git a/sled-agent/types/versions/src/latest.rs b/sled-agent/types/versions/src/latest.rs index b4529cf1d27..63fa725d91a 100644 --- a/sled-agent/types/versions/src/latest.rs +++ b/sled-agent/types/versions/src/latest.rs @@ -118,8 +118,8 @@ pub mod instance { pub use crate::v32::instance::ExternalIps; pub use crate::v32::instance::ExternalIpv4Config; pub use crate::v32::instance::ExternalIpv6Config; - pub use crate::v32::instance::InstanceEnsureBody; - pub use crate::v32::instance::InstanceSledLocalConfig; + pub use crate::v41::instance::InstanceEnsureBody; + pub use crate::v41::instance::InstanceSledLocalConfig; } pub mod inventory { diff --git a/sled-agent/types/versions/src/lib.rs b/sled-agent/types/versions/src/lib.rs index 27b112414ab..25a268b12bc 100644 --- a/sled-agent/types/versions/src/lib.rs +++ b/sled-agent/types/versions/src/lib.rs @@ -87,6 +87,8 @@ pub mod v39; pub mod v4; #[path = "add_fmd_to_inventory/mod.rs"] pub mod v40; +#[path = "add_instance_primary_nic_mtu/mod.rs"] +pub mod v41; #[path = "add_probe_put_endpoint/mod.rs"] pub mod v6; #[path = "multicast_support/mod.rs"] diff --git a/smf/sled-agent/non-gimlet/config-rss.toml b/smf/sled-agent/non-gimlet/config-rss.toml index c121ed4fb09..d6583a98827 100644 --- a/smf/sled-agent/non-gimlet/config-rss.toml +++ b/smf/sled-agent/non-gimlet/config-rss.toml @@ -146,6 +146,13 @@ allow = "any" # Note that single IP addresses must include the netmask as well, so `/32` or # `/128`. +# Enable the fleet-wide jumbo-frames opt-in. When true, end users may opt +# in to jumbo frames on the primary interface of an instance, and control +# plane services with external-facing OPTE NICs are brought up with an 8500 +# byte MTU. Defaults to false. Can also be toggled at runtime via the Nexus +# `/v1/system/networking/settings` endpoint after handoff. +external_jumbo_frames_opt_in_enabled = false + # Configuration for the initial Silo, user, and password. # # You don't need to change the silo or user names. diff --git a/tools/install_opte.sh b/tools/install_opte.sh index 5b32a8c472e..ec13294f948 100755 --- a/tools/install_opte.sh +++ b/tools/install_opte.sh @@ -88,7 +88,7 @@ if [[ "x$OPTE_COMMIT" != "x" ]]; then curl -fL -o "$P5P_PATH" "$P5P_URL" RC=0 - pfexec pkg install -g "$P5P_PATH" driver/network/opte || RC=$? + pfexec pkg install -g "$P5P_PATH" pkg://helios-dev/driver/network/opte || RC=$? if [[ "$RC" -eq 0 ]]; then echo "xde driver installed from override p5p" elif [[ "$RC" -eq 4 ]]; then diff --git a/tools/maghemite_ddm_openapi_version b/tools/maghemite_ddm_openapi_version index 060b3a13efb..9cf6d1d3862 100644 --- a/tools/maghemite_ddm_openapi_version +++ b/tools/maghemite_ddm_openapi_version @@ -1 +1 @@ -COMMIT="7696ee48d5ee29a917dea459e281fe2e8ff20513" +COMMIT="b27bea5344fb21b3b88af6717b969e31ee6a9ccb" diff --git a/tools/maghemite_mg_openapi_version b/tools/maghemite_mg_openapi_version index 060b3a13efb..9cf6d1d3862 100644 --- a/tools/maghemite_mg_openapi_version +++ b/tools/maghemite_mg_openapi_version @@ -1 +1 @@ -COMMIT="7696ee48d5ee29a917dea459e281fe2e8ff20513" +COMMIT="b27bea5344fb21b3b88af6717b969e31ee6a9ccb" diff --git a/tools/maghemite_mgd_checksums b/tools/maghemite_mgd_checksums index 470facaa671..d545a32bf70 100644 --- a/tools/maghemite_mgd_checksums +++ b/tools/maghemite_mgd_checksums @@ -1,2 +1,2 @@ -CIDL_SHA256="301d31ca481e4822f69484feacca31dd08a7c4aae87d96641d384bda3178d2f3" -MGD_LINUX_SHA256="95f9759a5fde2784d148c81df2218d29adde1d27fb72d5dbcf534de6450f0f7c" \ No newline at end of file +CIDL_SHA256="350a6684051132b3d18f618f81d4c5cbf5dd1d9e93d522b9fb5c2305ae19b358" +MGD_LINUX_SHA256="fc043a56800a112d0ccc8292e1fb518b8aec0d7e5f17ed25a53c4018892f5546" \ No newline at end of file diff --git a/tools/opte_version b/tools/opte_version index a126f3ac29f..51014b25280 100644 --- a/tools/opte_version +++ b/tools/opte_version @@ -1 +1 @@ -0.40.479 +0.41.477 diff --git a/tools/opte_version_override b/tools/opte_version_override index 288a0febf15..8be6ee5749d 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="84e91b62621406f38e4d0870a92bafacad96536e" diff --git a/wicket-common/src/example.rs b/wicket-common/src/example.rs index 5abf054d7e8..cb0ea73eecf 100644 --- a/wicket-common/src/example.rs +++ b/wicket-common/src/example.rs @@ -271,6 +271,7 @@ impl ExampleRackSetupData { ntp_servers, rack_network_config: Some(rack_network_config), allowed_source_ips: Some(AllowedSourceIps::Any), + external_jumbo_frames_opt_in_enabled: false, }; for tweak in tweaks { @@ -305,6 +306,7 @@ impl ExampleRackSetupData { .clone() .unwrap(), allowed_source_ips: AllowedSourceIps::Any, + external_jumbo_frames_opt_in_enabled: false, }; Self { diff --git a/wicket-common/src/rack_setup.rs b/wicket-common/src/rack_setup.rs index c2ff6377e01..f44eba185b8 100644 --- a/wicket-common/src/rack_setup.rs +++ b/wicket-common/src/rack_setup.rs @@ -54,6 +54,10 @@ pub struct CurrentRssUserConfigInsensitive { pub external_dns_zone_name: String, pub rack_network_config: Option, pub allowed_source_ips: Option, + /// Enable the fleet-wide jumbo-frames opt-in. Operators can also toggle + /// this at runtime via the Nexus API after handoff. + #[serde(default)] + pub external_jumbo_frames_opt_in_enabled: bool, } /// The portion of `CurrentRssUserConfig` that can be posted in one shot; it is @@ -77,6 +81,9 @@ pub struct PutRssUserConfigInsensitive { pub external_dns_zone_name: String, pub rack_network_config: UserSpecifiedRackNetworkConfig, pub allowed_source_ips: AllowedSourceIps, + /// Enable the fleet-wide jumbo-frames opt-in. + #[serde(default)] + pub external_jumbo_frames_opt_in_enabled: bool, } #[derive( diff --git a/wicket/src/cli/rack_setup/config_template.toml b/wicket/src/cli/rack_setup/config_template.toml index b5bcaa202c8..189d76a3309 100644 --- a/wicket/src/cli/rack_setup/config_template.toml +++ b/wicket/src/cli/rack_setup/config_template.toml @@ -38,6 +38,13 @@ internal_services_ip_pool_ranges = [] # Confirm this list contains all expected sleds before continuing! bootstrap_sleds = [] +# When true, end users may opt in to jumbo frames (8500 byte MTU) on the primary +# interface of an instance, and control plane services with external-facing OPTE +# NICs are brought up with an 8500 byte MTU. Operators can also toggle this at +# runtime via the Nexus `/v1/system/networking/settings` endpoint after rack +# initialization. +external_jumbo_frames_opt_in_enabled = false + # Allowlist of source IPs that can make requests to user-facing services. # # Use the key: diff --git a/wicket/src/cli/rack_setup/config_toml.rs b/wicket/src/cli/rack_setup/config_toml.rs index 97e3f65ca48..c24ef73c9f0 100644 --- a/wicket/src/cli/rack_setup/config_toml.rs +++ b/wicket/src/cli/rack_setup/config_toml.rs @@ -107,6 +107,13 @@ impl TomlTemplate { config.allowed_source_ips.as_ref(), ); + *doc.get_mut("external_jumbo_frames_opt_in_enabled") + .unwrap() + .as_value_mut() + .unwrap() = Value::Boolean(Formatted::new( + config.external_jumbo_frames_opt_in_enabled, + )); + *doc.get_mut("bootstrap_sleds").unwrap().as_array_mut().unwrap() = build_sleds_array(&config.bootstrap_sleds); diff --git a/wicket/src/ui/panes/rack_setup.rs b/wicket/src/ui/panes/rack_setup.rs index 76a1b391441..b1b1963f86c 100644 --- a/wicket/src/ui/panes/rack_setup.rs +++ b/wicket/src/ui/panes/rack_setup.rs @@ -692,6 +692,7 @@ fn rss_config_text<'a>( external_dns_zone_name, rack_network_config, allowed_source_ips, + external_jumbo_frames_opt_in_enabled, } = &config.insensitive; // Special single-line values, where we convert some kind of condition into @@ -707,6 +708,10 @@ fn rss_config_text<'a>( Span::styled("Recovery password set: ", label_style), dyn_span(*recovery_silo_password_set, "Yes", "No"), ])); + spans.push(Line::from(vec![ + Span::styled("External jumbo frames opt-in: ", label_style), + dyn_span(*external_jumbo_frames_opt_in_enabled, "Enabled", "Disabled"), + ])); // List of single-line values, each of which may or may not be set; if it's // set we show its value, and if not we show "Not set" in bad_style. diff --git a/wicket/tests/output/example_non_empty.toml b/wicket/tests/output/example_non_empty.toml index 0537bfdeae4..0182b82c0dc 100644 --- a/wicket/tests/output/example_non_empty.toml +++ b/wicket/tests/output/example_non_empty.toml @@ -50,6 +50,13 @@ bootstrap_sleds = [ 5, # serial 4 5 6 (model model2 revision 5, ::1) ] +# When true, end users may opt in to jumbo frames (8500 byte MTU) on the primary +# interface of an instance, and control plane services with external-facing OPTE +# NICs are brought up with an 8500 byte MTU. Operators can also toggle this at +# runtime via the Nexus `/v1/system/networking/settings` endpoint after rack +# initialization. +external_jumbo_frames_opt_in_enabled = false + # Allowlist of source IPs that can make requests to user-facing services. # # Use the key: diff --git a/wicketd/src/rss_config.rs b/wicketd/src/rss_config.rs index 22ce64baf4f..f1bcaebcd8e 100644 --- a/wicketd/src/rss_config.rs +++ b/wicketd/src/rss_config.rs @@ -86,6 +86,7 @@ pub(crate) struct CurrentRssConfig { // Currently these are always TCP-MD5 keys, bgp_auth_keys: BTreeMap>, allowed_source_ips: Option, + external_jumbo_frames_opt_in_enabled: bool, // External certificates are uploaded in two separate actions (cert then // key, or vice versa). Here we store a partial certificate; once we have // both parts, we validate it and promote it to be a member of @@ -310,6 +311,8 @@ impl CurrentRssConfig { .allowed_source_ips .clone() .unwrap_or(AllowedSourceIps::Any), + external_jumbo_frames_opt_in_enabled: self + .external_jumbo_frames_opt_in_enabled, }; Ok(request) @@ -534,6 +537,8 @@ impl CurrentRssConfig { self.external_dns_ips = value.external_dns_ips; self.external_dns_zone_name = value.external_dns_zone_name; self.allowed_source_ips = Some(value.allowed_source_ips); + self.external_jumbo_frames_opt_in_enabled = + value.external_jumbo_frames_opt_in_enabled; // Build a new auth key map, dropping all old keys from the map. let new_bgp_auth_key_ids = @@ -589,6 +594,8 @@ impl From<&'_ CurrentRssConfig> for CurrentRssUserConfig { external_dns_zone_name: rss.external_dns_zone_name.clone(), rack_network_config: rss.rack_network_config.clone(), allowed_source_ips: rss.allowed_source_ips.clone(), + external_jumbo_frames_opt_in_enabled: rss + .external_jumbo_frames_opt_in_enabled, }, } } diff --git a/workspace-hack/Cargo.toml b/workspace-hack/Cargo.toml index e04f06954c6..2002be2dbbc 100644 --- a/workspace-hack/Cargo.toml +++ b/workspace-hack/Cargo.toml @@ -20,6 +20,7 @@ workspace = true [dependencies] ahash = { version = "0.8.12" } aho-corasick = { version = "1.1.4" } +anstream = { version = "0.6.21" } anyhow = { version = "1.0.102", features = ["backtrace"] } base16ct = { version = "0.2.0", default-features = false, features = ["alloc"] } base64 = { version = "0.22.1" } @@ -32,8 +33,8 @@ bytes = { version = "1.11.1", features = ["serde"] } camino = { version = "1.2.2", default-features = false, features = ["serde1"] } chrono = { version = "0.4.44", features = ["serde"] } cipher = { version = "0.4.4", default-features = false, features = ["block-padding", "zeroize"] } -clap = { version = "4.5.60", features = ["cargo", "derive", "env", "wrap_help"] } -clap_builder = { version = "4.5.60", default-features = false, features = ["cargo", "color", "env", "std", "suggestions", "usage", "wrap_help"] } +clap = { version = "4.6.1", features = ["cargo", "derive", "env", "wrap_help"] } +clap_builder = { version = "4.6.0", default-features = false, features = ["cargo", "color", "env", "std", "suggestions", "usage", "wrap_help"] } const-oid = { version = "0.9.6", default-features = false, features = ["db", "std"] } crossbeam-epoch = { version = "0.9.18" } crossbeam-utils = { version = "0.8.21" } @@ -111,7 +112,7 @@ rand_chacha-468e82937335b1c9 = { package = "rand_chacha", version = "0.3.1", def regex = { version = "1.12.3" } regex-automata = { version = "0.4.14", default-features = false, features = ["dfa", "hybrid", "meta", "nfa", "perf", "std", "unicode"] } regex-syntax = { version = "0.8.10" } -reqwest-594e8ee84c453af0 = { package = "reqwest", version = "0.13.2", default-features = false, features = ["blocking", "cookies", "http2", "json", "query", "rustls", "stream"] } +reqwest-594e8ee84c453af0 = { package = "reqwest", version = "0.13.2", features = ["blocking", "cookies", "json", "query", "stream"] } reqwest-5ef9efb8ec2df382 = { package = "reqwest", version = "0.12.28", features = ["blocking", "json", "rustls-tls", "stream"] } rsa = { version = "0.9.10", features = ["serde", "sha2"] } rustls = { version = "0.23.37", features = ["ring"] } @@ -165,6 +166,7 @@ zip-3b31131e45eafb45 = { package = "zip", version = "0.6.6", default-features = [build-dependencies] ahash = { version = "0.8.12" } aho-corasick = { version = "1.1.4" } +anstream = { version = "0.6.21" } anyhow = { version = "1.0.102", features = ["backtrace"] } base16ct = { version = "0.2.0", default-features = false, features = ["alloc"] } base64 = { version = "0.22.1" } @@ -178,8 +180,8 @@ camino = { version = "1.2.2", default-features = false, features = ["serde1"] } cc = { version = "1.2.56", default-features = false, features = ["parallel"] } chrono = { version = "0.4.44", features = ["serde"] } cipher = { version = "0.4.4", default-features = false, features = ["block-padding", "zeroize"] } -clap = { version = "4.5.60", features = ["cargo", "derive", "env", "wrap_help"] } -clap_builder = { version = "4.5.60", default-features = false, features = ["cargo", "color", "env", "std", "suggestions", "usage", "wrap_help"] } +clap = { version = "4.6.1", features = ["cargo", "derive", "env", "wrap_help"] } +clap_builder = { version = "4.6.0", default-features = false, features = ["cargo", "color", "env", "std", "suggestions", "usage", "wrap_help"] } const-oid = { version = "0.9.6", default-features = false, features = ["db", "std"] } crossbeam-epoch = { version = "0.9.18" } crossbeam-utils = { version = "0.8.21" } @@ -258,7 +260,7 @@ rand_chacha-468e82937335b1c9 = { package = "rand_chacha", version = "0.3.1", def regex = { version = "1.12.3" } regex-automata = { version = "0.4.14", default-features = false, features = ["dfa", "hybrid", "meta", "nfa", "perf", "std", "unicode"] } regex-syntax = { version = "0.8.10" } -reqwest-594e8ee84c453af0 = { package = "reqwest", version = "0.13.2", default-features = false, features = ["blocking", "cookies", "http2", "json", "query", "rustls", "stream"] } +reqwest-594e8ee84c453af0 = { package = "reqwest", version = "0.13.2", features = ["blocking", "cookies", "json", "query", "stream"] } reqwest-5ef9efb8ec2df382 = { package = "reqwest", version = "0.12.28", features = ["blocking", "json", "rustls-tls", "stream"] } rsa = { version = "0.9.10", features = ["serde", "sha2"] } rustls = { version = "0.23.37", features = ["ring"] }