diff --git a/.cargo/config.toml b/.cargo/config.toml index 9305fc8..0b0dad8 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -6,3 +6,7 @@ xtask = "run --package xtask --" # See https://doc.rust-lang.org/cargo/reference/config.html#target. [build] rustflags = ["--cfg", "tokio_unstable"] + +# Workaround for omicron's bootstore transitive dependency on pq-sys +[env] +DEP_PQ_LIBDIRS = "1" diff --git a/Cargo.lock b/Cargo.lock index 8612eae..d5e2212 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9,7 +9,7 @@ dependencies = [ "common 0.1.0", "oximeter", "oxnet", - "schemars", + "schemars 0.8.22", "serde", "serde_json", "thiserror 1.0.69", @@ -26,21 +26,22 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "addr2line" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" -dependencies = [ - "gimli", -] - [[package]] name = "adler2" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -124,12 +125,12 @@ checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "api_identity" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec600ca74e573a1926c1876e33b35" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#c197cca8225b09d7115f1ac24d9b35b078a3ac70" dependencies = [ "omicron-workspace-hack", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -179,7 +180,7 @@ dependencies = [ "oximeter", "propolis", "rand 0.9.2", - "schemars", + "schemars 0.8.22", "semver 1.0.27", "serde", "serde_json", @@ -189,7 +190,7 @@ dependencies = [ "softnpu 0.2.0 (git+https://github.com/oxidecomputer/softnpu?branch=main)", "strum 0.27.2", "thiserror 1.0.69", - "tofino", + "tofino 0.1.0 (git+https://github.com/oxidecomputer/tofino?branch=main)", "tokio", "transceiver-controller", "uuid", @@ -203,7 +204,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -225,7 +226,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -236,7 +237,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -348,19 +349,10 @@ dependencies = [ ] [[package]] -name = "backtrace" -version = "0.3.75" +name = "base16ct" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-targets 0.52.6", -] +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" [[package]] name = "base64" @@ -393,9 +385,9 @@ dependencies = [ [[package]] name = "bhyve_api" version = "0.0.0" -source = "git+https://github.com/oxidecomputer/propolis?rev=3f1752e6cee9a2f8ecdce6e2ad3326781182e2d9#3f1752e6cee9a2f8ecdce6e2ad3326781182e2d9" +source = "git+https://github.com/oxidecomputer/propolis?rev=2dc643742f82d2e072a1281dab23ba2bfdcee440#2dc643742f82d2e072a1281dab23ba2bfdcee440" dependencies = [ - "bhyve_api_sys 0.0.0 (git+https://github.com/oxidecomputer/propolis?rev=3f1752e6cee9a2f8ecdce6e2ad3326781182e2d9)", + "bhyve_api_sys 0.0.0 (git+https://github.com/oxidecomputer/propolis?rev=2dc643742f82d2e072a1281dab23ba2bfdcee440)", "libc", "strum 0.26.3", ] @@ -413,7 +405,7 @@ dependencies = [ [[package]] name = "bhyve_api_sys" version = "0.0.0" -source = "git+https://github.com/oxidecomputer/propolis?rev=3f1752e6cee9a2f8ecdce6e2ad3326781182e2d9#3f1752e6cee9a2f8ecdce6e2ad3326781182e2d9" +source = "git+https://github.com/oxidecomputer/propolis?rev=2dc643742f82d2e072a1281dab23ba2bfdcee440#2dc643742f82d2e072a1281dab23ba2bfdcee440" dependencies = [ "libc", "strum 0.26.3", @@ -445,7 +437,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -543,6 +535,36 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bootstore" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#c197cca8225b09d7115f1ac24d9b35b078a3ac70" +dependencies = [ + "bytes", + "camino", + "chacha20poly1305", + "ciborium", + "derive_more", + "hex", + "hkdf", + "omicron-common", + "omicron-rpaths", + "omicron-workspace-hack", + "pq-sys", + "rand 0.8.5", + "secrecy", + "serde", + "serde_with", + "sha3", + "sled-hardware-types", + "slog", + "thiserror 2.0.17", + "tokio", + "uuid", + "vsss-rs", + "zeroize", +] + [[package]] name = "bstr" version = "1.12.0" @@ -582,9 +604,9 @@ dependencies = [ [[package]] name = "camino" -version = "1.2.0" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1de8bc0aa9e9385ceb3bf0c152e3a9b9544f6c4a912c8ae504e80c1f0368603" +checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" dependencies = [ "serde_core", ] @@ -668,6 +690,30 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20", + "cipher", + "poly1305", + "zeroize", +] + [[package]] name = "chrono" version = "0.4.42" @@ -693,6 +739,44 @@ dependencies = [ "serde", ] +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", + "zeroize", +] + [[package]] name = "clang-sys" version = "1.8.1" @@ -736,7 +820,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -748,7 +832,16 @@ checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" [[package]] name = "clickhouse-admin-types" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec600ca74e573a1926c1876e33b35" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#c197cca8225b09d7115f1ac24d9b35b078a3ac70" +dependencies = [ + "clickhouse-admin-types-versions", + "omicron-workspace-hack", +] + +[[package]] +name = "clickhouse-admin-types-versions" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#c197cca8225b09d7115f1ac24d9b35b078a3ac70" dependencies = [ "anyhow", "atomicwrites", @@ -761,7 +854,7 @@ dependencies = [ "itertools 0.14.0", "omicron-common", "omicron-workspace-hack", - "schemars", + "schemars 0.8.22", "serde", "serde_json", "slog", @@ -776,7 +869,7 @@ dependencies = [ "camino", "clap", "derive_more", - "schemars", + "schemars 0.8.22", "serde", "serde_json", "thiserror 1.0.69", @@ -798,21 +891,31 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" dependencies = [ - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] name = "cockroach-admin-types" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec600ca74e573a1926c1876e33b35" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#c197cca8225b09d7115f1ac24d9b35b078a3ac70" +dependencies = [ + "cockroach-admin-types-versions", + "omicron-workspace-hack", + "serde", +] + +[[package]] +name = "cockroach-admin-types-versions" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#c197cca8225b09d7115f1ac24d9b35b078a3ac70" dependencies = [ "chrono", "csv", - "omicron-common", + "omicron-uuid-kinds", "omicron-workspace-hack", - "schemars", + "schemars 0.8.22", "serde", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] @@ -839,7 +942,7 @@ dependencies = [ "oximeter", "oxnet", "rand 0.9.2", - "schemars", + "schemars 0.8.22", "serde", "serde_json", "slog", @@ -861,7 +964,7 @@ dependencies = [ "oximeter", "oxnet", "rand 0.9.2", - "schemars", + "schemars 0.8.22", "serde", "serde_json", "slog", @@ -999,7 +1102,7 @@ source = "git+https://github.com/oxidecomputer/propolis#827e6615bfebfd94d41504dc dependencies = [ "bhyve_api 0.0.0 (git+https://github.com/oxidecomputer/propolis)", "bitflags 2.9.4", - "propolis_types", + "propolis_types 0.0.0 (git+https://github.com/oxidecomputer/propolis)", "thiserror 1.0.69", ] @@ -1058,6 +1161,19 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crucible-client-types" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/crucible?rev=7103cd3a3d7b0112d2949dd135db06fef0c156bb#7103cd3a3d7b0112d2949dd135db06fef0c156bb" +dependencies = [ + "base64 0.22.1", + "crucible-workspace-hack", + "schemars 0.8.22", + "serde", + "serde_json", + "uuid", +] + [[package]] name = "crucible-smf" version = "0.0.0" @@ -1067,7 +1183,7 @@ dependencies = [ "libc", "num-derive 0.4.2", "num-traits", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] @@ -1076,6 +1192,24 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbd293370c6cb9c334123675263de33fc9e53bbdfc8bdd5e329237cf0205fdc7" +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -1083,6 +1217,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", + "rand_core 0.6.4", "typenum", ] @@ -1147,6 +1282,34 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rand_core 0.6.4", + "rustc_version 0.4.1", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "daft" version = "0.1.4" @@ -1168,7 +1331,7 @@ checksum = "7ad40aef90652e771af668d28abcc3ef35fd0d39438706a76a61588cf8e8e84a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -1192,7 +1355,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -1203,7 +1366,7 @@ checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ "darling_core", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -1247,7 +1410,7 @@ dependencies = [ "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -1256,7 +1419,7 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" dependencies = [ - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] @@ -1279,6 +1442,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d630bccd429a5bb5a64b5e94f693bfc48c9f8566418fda4c494cc94f911f87cc" dependencies = [ "powerfmt", + "serde", ] [[package]] @@ -1290,7 +1454,7 @@ dependencies = [ "proc-macro2", "quote", "structmeta", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -1301,7 +1465,7 @@ checksum = "ef941ded77d15ca19b40374869ac6000af1c9f2a4c0f3d4c70926287e6364a8f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -1314,7 +1478,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version 0.4.1", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -1348,7 +1512,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -1369,20 +1533,20 @@ dependencies = [ "libdlpi-sys 0.1.0 (git+https://github.com/oxidecomputer/dlpi-sys?branch=main)", "num_enum 0.7.5", "pretty-hex", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", ] [[package]] name = "dlpi" version = "0.2.0" -source = "git+https://github.com/oxidecomputer/dlpi-sys#0a0b98721c2b789767c7b54217e3cb8e702fcc38" +source = "git+https://github.com/oxidecomputer/dlpi-sys#42b2bfeefdfb8c7b96fc6cfa9ec45ef4554c2714" dependencies = [ "libc", "libdlpi-sys 0.1.0 (git+https://github.com/oxidecomputer/dlpi-sys)", "num_enum 0.7.5", "pretty-hex", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] @@ -1409,7 +1573,7 @@ dependencies = [ "pretty-hex", "serde", "serde_json", - "thiserror 2.0.16", + "thiserror 2.0.17", "zerocopy 0.8.27", ] @@ -1457,7 +1621,7 @@ dependencies = [ "regex", "regress", "reqwest", - "schemars", + "schemars 0.8.22", "semver 1.0.27", "serde", "serde_json", @@ -1470,7 +1634,7 @@ dependencies = [ "strum 0.27.2", "thiserror 1.0.69", "tokio", - "toml 0.9.7", + "toml 0.9.8", "transceiver-controller", "usdt 0.6.0", "uuid", @@ -1486,7 +1650,7 @@ dependencies = [ "dropshot", "dropshot-api-manager-types", "oxnet", - "schemars", + "schemars 0.8.22", "serde", "transceiver-controller", ] @@ -1501,6 +1665,7 @@ dependencies = [ "chrono", "common 0.1.0", "crc8", + "dpd-types", "futures", "http", "lazy_static", @@ -1513,14 +1678,14 @@ dependencies = [ "rand 0.9.2", "regress", "reqwest", - "schemars", + "schemars 0.8.22", "serde", "serde_json", "slog", "slog-async", "slog-term", "tokio", - "toml 0.9.7", + "toml 0.9.8", "transceiver-controller", "uuid", ] @@ -1540,7 +1705,7 @@ dependencies = [ "progenitor 0.11.1", "regress", "reqwest", - "schemars", + "schemars 0.8.22", "serde", "serde_json", "slog", @@ -1558,7 +1723,7 @@ dependencies = [ "common 0.1.0", "omicron-common", "oxnet", - "schemars", + "schemars 0.8.22", "serde", "thiserror 1.0.69", "transceiver-controller", @@ -1572,7 +1737,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43eb40edecda6106744f5e4f3d4dc78b3adf19d3cfb2d81cc4faa007da91e527" dependencies = [ "anyhow", - "indexmap 2.11.4", + "indexmap 2.12.1", "openapiv3", "regex", "serde", @@ -1581,9 +1746,9 @@ dependencies = [ [[package]] name = "dropshot" -version = "0.16.4" +version = "0.16.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd9bdeafc752f117ed20e659b9763695ae5900adf3a32e93f9f6f4052fd5d66" +checksum = "4d0df98c06659ab85a454f32dc36ca5dbc6500bd2a58f25ede4dc1f1d478904e" dependencies = [ "async-stream", "async-trait", @@ -1595,19 +1760,19 @@ dependencies = [ "dropshot_endpoint", "form_urlencoded", "futures", - "hostname 0.4.1", + "hostname 0.4.2", "http", "http-body-util", "hyper", "hyper-util", - "indexmap 2.11.4", + "indexmap 2.12.1", "multer", "openapiv3", "paste", "percent-encoding", "rustls 0.22.4", "rustls-pemfile", - "schemars", + "schemars 0.8.22", "scopeguard", "semver 1.0.27", "serde", @@ -1620,11 +1785,11 @@ dependencies = [ "slog-bunyan", "slog-json", "slog-term", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "tokio-rustls 0.25.0", - "toml 0.9.7", - "usdt 0.5.0", + "toml 0.9.8", + "usdt 0.6.0", "uuid", "version_check", "waitgroup", @@ -1657,7 +1822,7 @@ dependencies = [ "similar", "supports-color", "textwrap", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] @@ -1675,9 +1840,9 @@ dependencies = [ [[package]] name = "dropshot_endpoint" -version = "0.16.4" +version = "0.16.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89d09440e73a9dcf8a0f7fbd6ab889a7751d59f0fe76e5082a0a6d5623ec6da3" +checksum = "7e53aef8838e0e341485590738ab180a6dceff3565ffcb198d5f365fea650378" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -1685,7 +1850,7 @@ dependencies = [ "semver 1.0.27", "serde", "serde_tokenstream", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -1707,7 +1872,7 @@ checksum = "dc09b90bda5770641457f1c0a42c8203c48f5a3d9799dcf1bafbd84e30ccf080" dependencies = [ "pest", "pest_derive", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] @@ -1728,6 +1893,24 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "hkdf", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + [[package]] name = "embedded-io" version = "0.4.0" @@ -1764,7 +1947,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -1773,6 +1956,15 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "erased-serde" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c138974f9d5e7fe373eb04df7cae98833802ae4b11c24ac7039a21d5af4b26c" +dependencies = [ + "serde", +] + [[package]] name = "erased-serde" version = "0.4.8" @@ -1787,15 +1979,15 @@ dependencies = [ [[package]] name = "ereport-types" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec600ca74e573a1926c1876e33b35" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#c197cca8225b09d7115f1ac24d9b35b078a3ac70" dependencies = [ "dropshot", "omicron-uuid-kinds", "omicron-workspace-hack", - "schemars", + "schemars 0.8.22", "serde", "serde_json", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] @@ -1826,6 +2018,22 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + [[package]] name = "filetime" version = "0.2.26" @@ -1917,7 +2125,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -2024,7 +2232,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -2060,7 +2268,7 @@ dependencies = [ [[package]] name = "gateway-client" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec600ca74e573a1926c1876e33b35" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#c197cca8225b09d7115f1ac24d9b35b078a3ac70" dependencies = [ "base64 0.22.1", "chrono", @@ -2073,11 +2281,11 @@ dependencies = [ "progenitor 0.10.0", "rand 0.9.2", "reqwest", - "schemars", + "schemars 0.8.22", "serde", "serde_json", "slog", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "uuid", ] @@ -2085,7 +2293,7 @@ dependencies = [ [[package]] name = "gateway-messages" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/management-gateway-service?rev=669fe557b66f44aed3c622bd17bc092f08797e0c#669fe557b66f44aed3c622bd17bc092f08797e0c" +source = "git+https://github.com/oxidecomputer/management-gateway-service?rev=ea2f39ccdea124b5affcad0ca17bc5dacf65823a#ea2f39ccdea124b5affcad0ca17bc5dacf65823a" dependencies = [ "bitflags 2.9.4", "hubpack", @@ -2102,18 +2310,26 @@ dependencies = [ [[package]] name = "gateway-types" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec600ca74e573a1926c1876e33b35" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#c197cca8225b09d7115f1ac24d9b35b078a3ac70" +dependencies = [ + "gateway-types-versions", + "omicron-workspace-hack", +] + +[[package]] +name = "gateway-types-versions" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#c197cca8225b09d7115f1ac24d9b35b078a3ac70" dependencies = [ "daft", "dropshot", "gateway-messages", "hex", - "omicron-common", "omicron-uuid-kinds", "omicron-workspace-hack", - "schemars", + "schemars 0.8.22", "serde", - "thiserror 2.0.16", + "thiserror 2.0.17", "tufaceous-artifact", "uuid", ] @@ -2126,6 +2342,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] @@ -2166,10 +2383,20 @@ dependencies = [ ] [[package]] -name = "gimli" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +name = "gfss" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#c197cca8225b09d7115f1ac24d9b35b078a3ac70" +dependencies = [ + "digest", + "omicron-workspace-hack", + "rand 0.9.2", + "schemars 0.8.22", + "secrecy", + "serde", + "subtle", + "thiserror 2.0.17", + "zeroize", +] [[package]] name = "git2" @@ -2225,6 +2452,17 @@ dependencies = [ "scroll 0.13.0", ] +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "h2" version = "0.4.12" @@ -2237,13 +2475,24 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap 2.11.4", + "indexmap 2.12.1", "slab", "tokio", "tokio-util", "tracing", ] +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy 0.8.27", +] + [[package]] name = "hash32" version = "0.3.1" @@ -2272,9 +2521,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ "allocator-api2", ] @@ -2325,9 +2574,6 @@ name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" -dependencies = [ - "serde", -] [[package]] name = "hex-literal" @@ -2377,7 +2623,7 @@ dependencies = [ "once_cell", "rand 0.9.2", "ring", - "thiserror 2.0.16", + "thiserror 2.0.17", "tinyvec", "tokio", "tracing", @@ -2421,7 +2667,7 @@ dependencies = [ "rand 0.9.2", "resolv-conf", "smallvec", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "tracing", ] @@ -2432,6 +2678,24 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9040319a6910b901d5d49cbada4a99db52836a1b63228a05f7e2b7f8feef89b1" +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + [[package]] name = "hostname" version = "0.3.1" @@ -2445,23 +2709,22 @@ dependencies = [ [[package]] name = "hostname" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a56f203cd1c76362b69e3863fd987520ac36cf70a8c92627449b2f64a8cf7d65" +checksum = "617aaa3557aef3810a6369d0a99fac8a080891b68bd9f9812a1eeda0c0730cbd" dependencies = [ "cfg-if", "libc", - "windows-link 0.1.3", + "windows-link 0.2.0", ] [[package]] name = "http" -version = "1.3.1" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" dependencies = [ "bytes", - "fnv", "itoa", ] @@ -2529,9 +2792,9 @@ checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" [[package]] name = "hyper" -version = "1.7.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" dependencies = [ "atomic-waker", "bytes", @@ -2598,9 +2861,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.17" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" +checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" dependencies = [ "base64 0.22.1", "bytes", @@ -2617,6 +2880,7 @@ dependencies = [ "socket2 0.6.0", "system-configuration", "tokio", + "tower-layer", "tower-service", "tracing", "windows-registry", @@ -2742,10 +3006,10 @@ dependencies = [ "daft", "equivalent", "foldhash 0.2.0", - "hashbrown 0.16.0", + "hashbrown 0.16.1", "ref-cast", "rustc-hash", - "schemars", + "schemars 0.8.22", "serde_core", "serde_json", ] @@ -2798,15 +3062,16 @@ dependencies = [ [[package]] name = "illumos-utils" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec600ca74e573a1926c1876e33b35" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#c197cca8225b09d7115f1ac24d9b35b078a3ac70" dependencies = [ "anyhow", "async-trait", - "bhyve_api 0.0.0 (git+https://github.com/oxidecomputer/propolis?rev=3f1752e6cee9a2f8ecdce6e2ad3326781182e2d9)", + "bhyve_api 0.0.0 (git+https://github.com/oxidecomputer/propolis?rev=2dc643742f82d2e072a1281dab23ba2bfdcee440)", "byteorder", "camino", "camino-tempfile", "cfg-if", + "chrono", "crucible-smf", "debug-ignore", "dropshot", @@ -2816,6 +3081,7 @@ dependencies = [ "itertools 0.14.0", "libc", "macaddr", + "nix", "omicron-common", "omicron-uuid-kinds", "omicron-workspace-hack", @@ -2823,12 +3089,15 @@ dependencies = [ "oxide-vpc", "oxlog", "oxnet", - "schemars", + "schemars 0.8.22", "serde", "slog", + "slog-async", "slog-error-chain", + "slog-term", "smf 0.2.3", - "thiserror 2.0.16", + "thiserror 2.0.17", + "tofino 0.1.0 (git+https://github.com/oxidecomputer/tofino)", "tokio", "uuid", "whoami", @@ -2849,16 +3118,17 @@ checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", "hashbrown 0.12.3", + "serde", ] [[package]] name = "indexmap" -version = "2.11.4" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" dependencies = [ "equivalent", - "hashbrown 0.15.5", + "hashbrown 0.16.1", "serde", "serde_core", ] @@ -2888,7 +3158,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -2902,6 +3172,15 @@ dependencies = [ "zerocopy 0.8.27", ] +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + [[package]] name = "instant" version = "0.1.13" @@ -2914,7 +3193,7 @@ dependencies = [ [[package]] name = "internal-dns-resolver" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec600ca74e573a1926c1876e33b35" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#c197cca8225b09d7115f1ac24d9b35b078a3ac70" dependencies = [ "futures", "hickory-proto 0.25.2", @@ -2926,41 +3205,44 @@ dependencies = [ "qorb", "reqwest", "slog", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] name = "internal-dns-types" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec600ca74e573a1926c1876e33b35" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#c197cca8225b09d7115f1ac24d9b35b078a3ac70" dependencies = [ "anyhow", "chrono", + "internal-dns-types-versions", "omicron-common", "omicron-uuid-kinds", "omicron-workspace-hack", - "schemars", + "schemars 0.8.22", "serde", "strum 0.27.2", ] +[[package]] +name = "internal-dns-types-versions" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#c197cca8225b09d7115f1ac24d9b35b078a3ac70" +dependencies = [ + "anyhow", + "chrono", + "omicron-common", + "omicron-workspace-hack", + "schemars 0.8.22", + "serde", +] + [[package]] name = "internet-checksum" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc6d6206008e25125b1f97fbe5d309eb7b85141cf9199d52dbd3729a1584dd16" -[[package]] -name = "io-uring" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" -dependencies = [ - "bitflags 2.9.4", - "cfg-if", - "libc", -] - [[package]] name = "ipconfig" version = "0.3.2" @@ -2985,7 +3267,7 @@ version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf370abdafd54d13e54a620e8c3e1145f28e46cc9d704bc6d94414559df41763" dependencies = [ - "schemars", + "schemars 0.8.22", "serde", ] @@ -3077,7 +3359,7 @@ checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -3115,13 +3397,22 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "keccak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" +dependencies = [ + "cpufeatures", +] + [[package]] name = "kstat-macro" version = "0.1.0" source = "git+https://github.com/oxidecomputer/opte?rev=795a1e0aeefb7a2c6fe4139779fdf66930d09b80#795a1e0aeefb7a2c6fe4139779fdf66930d09b80" dependencies = [ "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -3154,7 +3445,7 @@ source = "git+https://github.com/oxidecomputer/dlpi-sys?branch=main#555fa6e1315a [[package]] name = "libdlpi-sys" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/dlpi-sys#0a0b98721c2b789767c7b54217e3cb8e702fcc38" +source = "git+https://github.com/oxidecomputer/dlpi-sys#42b2bfeefdfb8c7b96fc6cfa9ec45ef4554c2714" [[package]] name = "libgit2-sys" @@ -3197,7 +3488,7 @@ checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" [[package]] name = "libnet" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/netadm-sys?branch=main#3eb1a6ad0b713660b367ce275e0a3896eabe19d4" +source = "git+https://github.com/oxidecomputer/netadm-sys?branch=main#24167d269038223b9d5d50c333ecaa34001d8f94" dependencies = [ "anyhow", "cfg-if", @@ -3211,7 +3502,7 @@ dependencies = [ "rand 0.9.2", "rusty-doors", "socket2 0.6.0", - "thiserror 2.0.16", + "thiserror 2.0.17", "tracing", "winnow 0.7.14", ] @@ -3233,7 +3524,7 @@ dependencies = [ "rand 0.9.2", "rusty-doors", "socket2 0.6.0", - "thiserror 2.0.16", + "thiserror 2.0.17", "tracing", "winnow 0.7.14", ] @@ -3332,7 +3623,7 @@ dependencies = [ "progenitor 0.11.1", "protocol", "reqwest", - "schemars", + "schemars 0.8.22", "serde", "serde_json", "slog", @@ -3346,7 +3637,7 @@ source = "git+https://github.com/oxidecomputer/lldp#61479b6922f9112fbe1e722414d2 dependencies = [ "anyhow", "dpd-client 0.1.0 (git+https://github.com/oxidecomputer/dendrite?branch=main)", - "schemars", + "schemars 0.8.22", "serde", "serde_json", "slog", @@ -3460,17 +3751,19 @@ dependencies = [ [[package]] name = "mg-admin-client" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/maghemite?rev=08f2a34d487658e87545ffbba3add632a82baf0d#08f2a34d487658e87545ffbba3add632a82baf0d" +source = "git+https://github.com/oxidecomputer/maghemite?rev=205b3ccf75b527ac7a565285fdcc0c78f4fcee95#205b3ccf75b527ac7a565285fdcc0c78f4fcee95" dependencies = [ - "anyhow", "chrono", - "percent-encoding", + "colored", "progenitor 0.11.1", + "rdb-types", "reqwest", - "schemars", + "schemars 0.8.22", "serde", "serde_json", "slog", + "tabwriter", + "uuid", ] [[package]] @@ -3528,7 +3821,7 @@ dependencies = [ "cfg-if", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -3598,7 +3891,7 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c012d14ef788ab066a347d19e3dda699916c92293b05b85ba2c76b8c82d2830" dependencies = [ - "schemars", + "schemars 0.8.22", "serde", "serde_json", "uuid", @@ -3615,7 +3908,7 @@ dependencies = [ "quote", "serde", "serde_tokenstream", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -3630,7 +3923,7 @@ dependencies = [ [[package]] name = "nexus-client" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec600ca74e573a1926c1876e33b35" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#c197cca8225b09d7115f1ac24d9b35b078a3ac70" dependencies = [ "chrono", "futures", @@ -3643,42 +3936,17 @@ dependencies = [ "progenitor 0.10.0", "regress", "reqwest", - "schemars", + "schemars 0.8.22", "serde", "serde_json", "slog", "uuid", ] -[[package]] -name = "nexus-sled-agent-shared" -version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec600ca74e573a1926c1876e33b35" -dependencies = [ - "camino", - "chrono", - "daft", - "iddqd", - "illumos-utils", - "indent_write", - "omicron-common", - "omicron-passwords", - "omicron-uuid-kinds", - "omicron-workspace-hack", - "schemars", - "serde", - "serde_json", - "sled-hardware-types", - "strum 0.27.2", - "thiserror 2.0.16", - "tufaceous-artifact", - "uuid", -] - [[package]] name = "nexus-types" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec600ca74e573a1926c1876e33b35" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#c197cca8225b09d7115f1ac24d9b35b078a3ac70" dependencies = [ "anyhow", "api_identity", @@ -3703,11 +3971,11 @@ dependencies = [ "illumos-utils", "indent_write", "internal-dns-types", + "ipnet", "ipnetwork", "itertools 0.14.0", "newtype-uuid", "newtype_derive", - "nexus-sled-agent-shared", "omicron-common", "omicron-passwords", "omicron-uuid-kinds", @@ -3718,11 +3986,13 @@ dependencies = [ "oxql-types", "parse-display", "regex", - "schemars", + "schemars 0.8.22", "semver 1.0.27", "serde", "serde_json", "serde_with", + "sled-agent-types", + "sled-agent-types-versions", "sled-hardware-types", "slog", "slog-error-chain", @@ -3732,9 +4002,11 @@ dependencies = [ "tabled 0.15.0", "test-strategy", "textwrap", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "tough", + "trust-quorum-protocol", + "trust-quorum-types", "tufaceous-artifact", "unicode-width 0.1.14", "update-engine", @@ -3825,7 +4097,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -3910,7 +4182,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -3954,15 +4226,6 @@ dependencies = [ "libc", ] -[[package]] -name = "object" -version = "0.36.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" -dependencies = [ - "memchr", -] - [[package]] name = "olpc-cjson" version = "0.1.4" @@ -3977,7 +4240,7 @@ dependencies = [ [[package]] name = "omicron-common" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec600ca74e573a1926c1876e33b35" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#c197cca8225b09d7115f1ac24d9b35b078a3ac70" dependencies = [ "anyhow", "api_identity", @@ -3992,6 +4255,7 @@ dependencies = [ "http", "iddqd", "ipnetwork", + "itertools 0.14.0", "macaddr", "mg-admin-client", "omicron-uuid-kinds", @@ -4003,7 +4267,7 @@ dependencies = [ "rand 0.9.2", "regress", "reqwest", - "schemars", + "schemars 0.8.22", "semver 1.0.27", "serde", "serde_human_bytes", @@ -4012,7 +4276,7 @@ dependencies = [ "slog", "slog-error-chain", "strum 0.27.2", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "tufaceous-artifact", "uuid", @@ -4021,28 +4285,36 @@ dependencies = [ [[package]] name = "omicron-passwords" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec600ca74e573a1926c1876e33b35" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#c197cca8225b09d7115f1ac24d9b35b078a3ac70" dependencies = [ "argon2", "omicron-workspace-hack", "rand 0.9.2", - "schemars", + "schemars 0.8.22", "secrecy", "serde", "serde_with", - "thiserror 2.0.16", + "thiserror 2.0.17", +] + +[[package]] +name = "omicron-rpaths" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#c197cca8225b09d7115f1ac24d9b35b078a3ac70" +dependencies = [ + "omicron-workspace-hack", ] [[package]] name = "omicron-uuid-kinds" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec600ca74e573a1926c1876e33b35" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#c197cca8225b09d7115f1ac24d9b35b078a3ac70" dependencies = [ "daft", "newtype-uuid", "newtype-uuid-macros", "paste", - "schemars", + "schemars 0.8.22", ] [[package]] @@ -4099,13 +4371,19 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + [[package]] name = "openapiv3" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c8d427828b22ae1fff2833a03d8486c2c881367f1c336349f307f321e7f4d05" dependencies = [ - "indexmap 2.11.4", + "indexmap 2.12.1", "serde", "serde_json", ] @@ -4133,7 +4411,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -4197,7 +4475,7 @@ dependencies = [ "oxide-vpc", "postcard", "serde", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] @@ -4234,7 +4512,7 @@ dependencies = [ [[package]] name = "oximeter" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec600ca74e573a1926c1876e33b35" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#c197cca8225b09d7115f1ac24d9b35b078a3ac70" dependencies = [ "anyhow", "chrono", @@ -4245,7 +4523,7 @@ dependencies = [ "oximeter-timeseries-macro", "oximeter-types", "prettyplease", - "syn 2.0.106", + "syn 2.0.111", "toml 0.8.23", "uuid", ] @@ -4253,7 +4531,7 @@ dependencies = [ [[package]] name = "oximeter-db" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec600ca74e573a1926c1876e33b35" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#c197cca8225b09d7115f1ac24d9b35b078a3ac70" dependencies = [ "anyhow", "async-recursion", @@ -4272,7 +4550,7 @@ dependencies = [ "gethostname", "highway", "iana-time-zone", - "indexmap 2.11.4", + "indexmap 2.12.1", "libc", "nom", "num", @@ -4286,7 +4564,7 @@ dependencies = [ "quote", "regex", "reqwest", - "schemars", + "schemars 0.8.22", "serde", "serde_json", "slog", @@ -4296,7 +4574,7 @@ dependencies = [ "slog-term", "strum 0.27.2", "termtree", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "tokio-util", "usdt 0.5.0", @@ -4306,7 +4584,7 @@ dependencies = [ [[package]] name = "oximeter-instruments" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec600ca74e573a1926c1876e33b35" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#c197cca8225b09d7115f1ac24d9b35b078a3ac70" dependencies = [ "cfg-if", "chrono", @@ -4316,7 +4594,7 @@ dependencies = [ "omicron-workspace-hack", "oximeter", "slog", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "uuid", ] @@ -4324,18 +4602,18 @@ dependencies = [ [[package]] name = "oximeter-macro-impl" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec600ca74e573a1926c1876e33b35" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#c197cca8225b09d7115f1ac24d9b35b078a3ac70" dependencies = [ "omicron-workspace-hack", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] name = "oximeter-producer" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec600ca74e573a1926c1876e33b35" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#c197cca8225b09d7115f1ac24d9b35b078a3ac70" dependencies = [ "chrono", "dropshot", @@ -4345,11 +4623,11 @@ dependencies = [ "omicron-common", "omicron-workspace-hack", "oximeter", - "schemars", + "schemars 0.8.22", "serde", "slog", "slog-dtrace", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "uuid", ] @@ -4357,7 +4635,7 @@ dependencies = [ [[package]] name = "oximeter-schema" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec600ca74e573a1926c1876e33b35" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#c197cca8225b09d7115f1ac24d9b35b078a3ac70" dependencies = [ "anyhow", "chrono", @@ -4368,50 +4646,64 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "schemars", + "schemars 0.8.22", "serde", "slog-error-chain", - "syn 2.0.106", + "syn 2.0.111", "toml 0.8.23", ] [[package]] name = "oximeter-timeseries-macro" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec600ca74e573a1926c1876e33b35" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#c197cca8225b09d7115f1ac24d9b35b078a3ac70" dependencies = [ "omicron-workspace-hack", "oximeter-schema", "oximeter-types", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", +] + +[[package]] +name = "oximeter-types" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#c197cca8225b09d7115f1ac24d9b35b078a3ac70" +dependencies = [ + "bytes", + "chrono", + "float-ord", + "num", + "omicron-common", + "omicron-workspace-hack", + "oximeter-types-versions", + "parse-display", + "regex", + "schemars 0.8.22", + "serde", + "strum 0.27.2", + "thiserror 2.0.17", + "uuid", ] [[package]] -name = "oximeter-types" +name = "oximeter-types-versions" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec600ca74e573a1926c1876e33b35" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#c197cca8225b09d7115f1ac24d9b35b078a3ac70" dependencies = [ - "bytes", "chrono", - "float-ord", - "num", "omicron-common", "omicron-workspace-hack", - "parse-display", - "regex", - "schemars", + "schemars 0.8.22", "serde", - "strum 0.27.2", - "thiserror 2.0.16", "uuid", ] [[package]] name = "oxlog" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec600ca74e573a1926c1876e33b35" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#c197cca8225b09d7115f1ac24d9b35b078a3ac70" dependencies = [ "anyhow", "camino", @@ -4427,12 +4719,12 @@ dependencies = [ [[package]] name = "oxnet" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8200429754152e6379fbb1dd06eea90156c3b67591f6e31d08e787d010ef0168" +checksum = "5dc6fb07ecd6d2a17ff1431bc5b3ce11036c0b6dd93a3c4904db5b910817b162" dependencies = [ "ipnetwork", - "schemars", + "schemars 0.8.22", "serde", "serde_json", ] @@ -4440,7 +4732,7 @@ dependencies = [ [[package]] name = "oxql-types" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec600ca74e573a1926c1876e33b35" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#c197cca8225b09d7115f1ac24d9b35b078a3ac70" dependencies = [ "anyhow", "chrono", @@ -4448,7 +4740,7 @@ dependencies = [ "num", "omicron-workspace-hack", "oximeter-types", - "schemars", + "schemars 0.8.22", "serde", "serde_json", "uuid", @@ -4485,7 +4777,7 @@ dependencies = [ "common 0.1.0", "hex-literal", "rand 0.9.2", - "schemars", + "schemars 0.8.22", "serde", "serde_json", "thiserror 1.0.69", @@ -4558,7 +4850,7 @@ dependencies = [ "regex", "regex-syntax", "structmeta", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -4609,7 +4901,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21e0a3a33733faeaf8651dfee72dd0f388f0c8e5ad496a3478fa5a922f49cfa8" dependencies = [ "memchr", - "thiserror 2.0.16", + "thiserror 2.0.17", "ucd-trie", ] @@ -4633,7 +4925,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -4653,7 +4945,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ "fixedbitset 0.4.2", - "indexmap 2.11.4", + "indexmap 2.12.1", "serde", "serde_derive", ] @@ -4666,7 +4958,7 @@ checksum = "54acf3a685220b533e437e264e4d932cfbdc4cc7ec0cd232ed73c08d03b8a7ca" dependencies = [ "fixedbitset 0.5.7", "hashbrown 0.15.5", - "indexmap 2.11.4", + "indexmap 2.12.1", "serde", ] @@ -4705,7 +4997,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -4732,6 +5024,17 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] + [[package]] name = "portable-atomic" version = "1.11.1" @@ -4783,6 +5086,17 @@ dependencies = [ "zerocopy 0.8.27", ] +[[package]] +name = "pq-sys" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "574ddd6a267294433f140b02a726b0640c43cf7c6f717084684aaa3b285aba61" +dependencies = [ + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "predicates" version = "3.1.3" @@ -4832,7 +5146,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -4888,7 +5202,7 @@ dependencies = [ "proc-macro-error-attr2", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -4960,16 +5274,16 @@ checksum = "b17e5363daa50bf1cccfade6b0fb970d2278758fd5cfa9ab69f25028e4b1afa3" dependencies = [ "heck 0.5.0", "http", - "indexmap 2.11.4", + "indexmap 2.12.1", "openapiv3", "proc-macro2", "quote", "regex", - "schemars", + "schemars 0.8.22", "serde", "serde_json", - "syn 2.0.106", - "thiserror 2.0.16", + "syn 2.0.111", + "thiserror 2.0.17", "typify", "unicode-ident", ] @@ -4982,16 +5296,16 @@ checksum = "8276d558f1dfd4cc7fc4cceee0a51dab482b5a4be2e69e7eab8c57fbfb1472f4" dependencies = [ "heck 0.5.0", "http", - "indexmap 2.11.4", + "indexmap 2.12.1", "openapiv3", "proc-macro2", "quote", "regex", - "schemars", + "schemars 0.8.22", "serde", "serde_json", - "syn 2.0.106", - "thiserror 2.0.16", + "syn 2.0.111", + "thiserror 2.0.17", "typify", "unicode-ident", ] @@ -5006,12 +5320,12 @@ dependencies = [ "proc-macro2", "progenitor-impl 0.10.0", "quote", - "schemars", + "schemars 0.8.22", "serde", "serde_json", "serde_tokenstream", "serde_yaml", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -5024,12 +5338,12 @@ dependencies = [ "proc-macro2", "progenitor-impl 0.11.1", "quote", - "schemars", + "schemars 0.8.22", "serde", "serde_json", "serde_tokenstream", "serde_yaml", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -5046,7 +5360,7 @@ dependencies = [ "cpuid_utils", "dladm", "dlpi 0.2.0 (git+https://github.com/oxidecomputer/dlpi-sys?branch=main)", - "erased-serde", + "erased-serde 0.4.8", "futures", "ispf", "lazy_static", @@ -5054,7 +5368,7 @@ dependencies = [ "libloading 0.7.4", "p9ds", "pin-project-lite", - "propolis_types", + "propolis_types 0.0.0 (git+https://github.com/oxidecomputer/propolis)", "rand 0.9.2", "rfb", "rgb_frame", @@ -5072,12 +5386,34 @@ dependencies = [ "zerocopy 0.8.27", ] +[[package]] +name = "propolis_api_types" +version = "0.0.0" +source = "git+https://github.com/oxidecomputer/propolis?rev=2dc643742f82d2e072a1281dab23ba2bfdcee440#2dc643742f82d2e072a1281dab23ba2bfdcee440" +dependencies = [ + "crucible-client-types", + "propolis_types 0.0.0 (git+https://github.com/oxidecomputer/propolis?rev=2dc643742f82d2e072a1281dab23ba2bfdcee440)", + "schemars 0.8.22", + "serde", + "thiserror 1.0.69", + "uuid", +] + +[[package]] +name = "propolis_types" +version = "0.0.0" +source = "git+https://github.com/oxidecomputer/propolis?rev=2dc643742f82d2e072a1281dab23ba2bfdcee440#2dc643742f82d2e072a1281dab23ba2bfdcee440" +dependencies = [ + "schemars 0.8.22", + "serde", +] + [[package]] name = "propolis_types" version = "0.0.0" source = "git+https://github.com/oxidecomputer/propolis#827e6615bfebfd94d41504dcd1517a0f22e3166a" dependencies = [ - "schemars", + "schemars 0.8.22", "serde", ] @@ -5121,7 +5457,7 @@ dependencies = [ "itertools 0.14.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -5136,10 +5472,10 @@ dependencies = [ [[package]] name = "protocol" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/lldp#61479b6922f9112fbe1e722414d2b8055212cb12" +source = "git+https://github.com/oxidecomputer/lldp#5d1210277b2966b16b7a34510255474ee4f2034c" dependencies = [ "anyhow", - "schemars", + "schemars 0.8.22", "serde", "thiserror 1.0.69", ] @@ -5158,7 +5494,7 @@ dependencies = [ "hickory-resolver 0.24.4", "rand 0.9.2", "serde", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "tokio-stream", "tracing", @@ -5185,7 +5521,7 @@ dependencies = [ "rustc-hash", "rustls 0.23.32", "socket2 0.6.0", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "tracing", "web-time", @@ -5206,7 +5542,7 @@ dependencies = [ "rustls 0.23.32", "rustls-pki-types", "slab", - "thiserror 2.0.16", + "thiserror 2.0.17", "tinyvec", "tracing", "web-time", @@ -5335,6 +5671,16 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "rdb-types" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/maghemite?rev=205b3ccf75b527ac7a565285fdcc0c78f4fcee95#205b3ccf75b527ac7a565285fdcc0c78f4fcee95" +dependencies = [ + "oxnet", + "schemars 0.8.22", + "serde", +] + [[package]] name = "redox_syscall" version = "0.5.17" @@ -5361,7 +5707,7 @@ checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -5494,12 +5840,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "rustc-demangle" -version = "0.1.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" - [[package]] name = "rustc-hash" version = "2.1.1" @@ -5699,6 +6039,30 @@ dependencies = [ "uuid", ] +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54e910108742c57a770f492731f99be216a52fadd361b06c8fb59d74ccc267d2" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + [[package]] name = "schemars_derive" version = "0.8.22" @@ -5708,7 +6072,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -5743,7 +6107,7 @@ checksum = "1783eabc414609e28a5ba76aee5ddd52199f7107a0b24c2e9746a1ecc34a683d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -5754,7 +6118,7 @@ checksum = "ed76efe62313ab6610570951494bdaa81568026e0318eaa55f167de70eeea67d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -5813,9 +6177,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.226" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dca6411025b24b60bfa7ec1fe1f8e710ac09782dca409ee8237ba74b51295fd" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ "serde_core", "serde_derive", @@ -5841,22 +6205,22 @@ dependencies = [ [[package]] name = "serde_core" -version = "1.0.226" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba2ba63999edb9dac981fb34b3e5c0d111a69b0924e253ed29d83f7c99e966a4" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.226" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8db53ae22f34573731bafa1db20f04027b2d25e02d8205921b569171699cdb33" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -5867,16 +6231,17 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] name = "serde_human_bytes" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/serde_human_bytes?branch=main#0a09794501b6208120528c3b457d5f3a8cb17424" +source = "git+https://github.com/oxidecomputer/serde_human_bytes?branch=main#70d3253a1acbe12a06152612ea85110acfe07317" dependencies = [ + "base64 0.22.1", "hex", - "serde", + "serde_core", ] [[package]] @@ -5920,7 +6285,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -5934,9 +6299,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5417783452c2be558477e104686f7de5dae53dba813c28435e0e70f82d9b04ee" +checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392" dependencies = [ "serde_core", ] @@ -5950,7 +6315,7 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -5974,6 +6339,11 @@ dependencies = [ "base64 0.22.1", "chrono", "hex", + "indexmap 1.9.3", + "indexmap 2.12.1", + "schemars 0.8.22", + "schemars 0.9.0", + "schemars 1.2.0", "serde", "serde_derive", "serde_json", @@ -5990,7 +6360,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -5999,7 +6369,7 @@ version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap 2.11.4", + "indexmap 2.12.1", "itoa", "ryu", "serde", @@ -6028,6 +6398,16 @@ dependencies = [ "digest", ] +[[package]] +name = "sha3" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +dependencies = [ + "digest", + "keccak", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -6104,23 +6484,98 @@ version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" +[[package]] +name = "sled-agent-types" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#c197cca8225b09d7115f1ac24d9b35b078a3ac70" +dependencies = [ + "anyhow", + "async-trait", + "bootstore", + "camino", + "chrono", + "daft", + "iddqd", + "omicron-common", + "omicron-uuid-kinds", + "omicron-workspace-hack", + "oxnet", + "schemars 0.8.22", + "serde", + "serde_human_bytes", + "serde_json", + "sled-agent-types-versions", + "sled-hardware-types", + "slog", + "slog-error-chain", + "strum 0.27.2", + "swrite", + "thiserror 2.0.17", + "toml 0.8.23", + "tufaceous-artifact", + "uuid", +] + +[[package]] +name = "sled-agent-types-versions" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#c197cca8225b09d7115f1ac24d9b35b078a3ac70" +dependencies = [ + "async-trait", + "bootstore", + "camino", + "chrono", + "daft", + "iddqd", + "illumos-utils", + "indent_write", + "omicron-common", + "omicron-passwords", + "omicron-uuid-kinds", + "omicron-workspace-hack", + "oxnet", + "propolis_api_types", + "schemars 0.8.22", + "serde", + "serde_human_bytes", + "serde_json", + "serde_with", + "sha3", + "sled-hardware-types", + "slog", + "strum 0.27.2", + "thiserror 2.0.17", + "trust-quorum-types-versions", + "tufaceous-artifact", + "uuid", +] + [[package]] name = "sled-hardware-types" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec600ca74e573a1926c1876e33b35" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#c197cca8225b09d7115f1ac24d9b35b078a3ac70" dependencies = [ + "daft", "illumos-utils", "omicron-common", "omicron-workspace-hack", - "schemars", + "schemars 0.8.22", "serde", + "slog", + "thiserror 2.0.17", ] [[package]] name = "slog" -version = "2.7.0" +version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8347046d4ebd943127157b94d63abb990fcf729dc4e9978927fdf4ac3c998d06" +checksum = "9b3b8565691b22d2bdfc066426ed48f837fc0c5f2c8cad8d9718f7f99d6995c1" +dependencies = [ + "anyhow", + "erased-serde 0.3.31", + "rustversion", + "serde_core", +] [[package]] name = "slog-async" @@ -6176,7 +6631,7 @@ source = "git+https://github.com/oxidecomputer/slog-error-chain?branch=main#15f6 dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -6271,7 +6726,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -6347,7 +6802,7 @@ dependencies = [ "lazy_static", "newtype_derive", "petgraph 0.6.5", - "schemars", + "schemars 0.8.22", "serde", "serde_json", "slog", @@ -6371,7 +6826,7 @@ dependencies = [ "proc-macro2", "quote", "structmeta-derive", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -6382,7 +6837,7 @@ checksum = "152a0b65a590ff6c3da95cabe2353ee04e6167c896b28e3b14478c2636c922fc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -6413,7 +6868,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -6425,7 +6880,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -6480,9 +6935,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.106" +version = "2.0.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" dependencies = [ "proc-macro2", "quote", @@ -6506,7 +6961,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -6575,7 +7030,7 @@ dependencies = [ "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -6664,7 +7119,7 @@ dependencies = [ "proc-macro2", "quote", "structmeta", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -6716,7 +7171,7 @@ dependencies = [ "packet", "pcap", "rand 0.9.2", - "schemars", + "schemars 0.8.22", "serde", "signal-hook", "slog", @@ -6738,11 +7193,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.16" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ - "thiserror-impl 2.0.16", + "thiserror-impl 2.0.17", ] [[package]] @@ -6753,18 +7208,38 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] name = "thiserror-impl" -version = "2.0.16" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "thiserror-impl-no-std" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" +checksum = "58e6318948b519ba6dc2b442a6d0b904ebfb8d411a3ad3e07843615a72249758" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 1.0.109", +] + +[[package]] +name = "thiserror-no-std" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3ad459d94dd517257cc96add8a43190ee620011bb6e6cdc82dafd97dfafafea" +dependencies = [ + "thiserror-impl-no-std", ] [[package]] @@ -6864,25 +7339,32 @@ dependencies = [ "illumos-devinfo", ] +[[package]] +name = "tofino" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/tofino#7e56ab6e9a64ebae27cd97cd6e10ebf2cfdc3a33" +dependencies = [ + "anyhow", + "cc", + "illumos-devinfo", +] + [[package]] name = "tokio" -version = "1.47.1" +version = "1.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" dependencies = [ - "backtrace", "bytes", - "io-uring", "libc", "mio", "parking_lot", "pin-project-lite", "signal-hook-registry", - "slab", "socket2 0.6.0", "tokio-macros", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.61.0", ] [[package]] @@ -6891,20 +7373,20 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f5eb4bcf85c373ff09a8beb87a477c2df185cd8842a770386a88bc3ff7ac5abb" dependencies = [ - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "usdt 0.5.0", ] [[package]] name = "tokio-macros" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -6990,14 +7472,14 @@ dependencies = [ [[package]] name = "toml" -version = "0.9.7" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00e5e5d9bf2475ac9d4f0d9edab68cc573dc2fd644b0dba36b0c30a92dd9eaa0" +checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" dependencies = [ - "indexmap 2.11.4", + "indexmap 2.12.1", "serde_core", - "serde_spanned 1.0.2", - "toml_datetime 0.7.2", + "serde_spanned 1.0.3", + "toml_datetime 0.7.3", "toml_parser", "toml_writer", "winnow 0.7.14", @@ -7014,9 +7496,9 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f1085dec27c2b6632b04c80b3bb1b4300d6495d1e129693bdda7d91e72eec1" +checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" dependencies = [ "serde_core", ] @@ -7027,7 +7509,7 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.11.4", + "indexmap 2.12.1", "serde", "serde_spanned 0.6.9", "toml_datetime 0.6.11", @@ -7040,7 +7522,7 @@ version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ - "indexmap 2.11.4", + "indexmap 2.12.1", "serde", "serde_spanned 0.6.9", "toml_datetime 0.6.11", @@ -7050,9 +7532,9 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cf893c33be71572e0e9aa6dd15e6677937abd686b066eac3f8cd3531688a627" +checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" dependencies = [ "winnow 0.7.14", ] @@ -7065,9 +7547,9 @@ checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" [[package]] name = "toml_writer" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d163a63c116ce562a22cda521fcc4d79152e7aba014456fb5eb442f6d6a10109" +checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" [[package]] name = "tonic" @@ -7224,7 +7706,7 @@ checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -7262,13 +7744,13 @@ dependencies = [ "hubpack", "itertools 0.14.0", "nix", - "schemars", + "schemars 0.8.22", "serde", "slog", "slog-async", "slog-term", "tabled 0.20.0", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "transceiver-decode", "transceiver-messages", @@ -7281,10 +7763,10 @@ name = "transceiver-decode" version = "0.1.0" source = "git+https://github.com/oxidecomputer/transceiver-control?branch=main#59b8432ec26c7a3725d5494937ca8bd6886c06a5" dependencies = [ - "schemars", + "schemars 0.8.22", "serde", "static_assertions", - "thiserror 2.0.16", + "thiserror 2.0.17", "transceiver-messages", ] @@ -7296,9 +7778,75 @@ dependencies = [ "bitflags 2.9.4", "clap", "hubpack", - "schemars", + "schemars 0.8.22", + "serde", + "thiserror 2.0.17", +] + +[[package]] +name = "trust-quorum-protocol" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#c197cca8225b09d7115f1ac24d9b35b078a3ac70" +dependencies = [ + "bootstore", + "bytes", + "camino", + "chacha20poly1305", + "ciborium", + "daft", + "derive_more", + "gfss", + "hex", + "hkdf", + "iddqd", + "omicron-uuid-kinds", + "omicron-workspace-hack", + "rand 0.9.2", + "secrecy", + "serde", + "serde_with", + "sha3", + "sled-agent-types", + "sled-hardware-types", + "slog", + "slog-error-chain", + "static_assertions", + "subtle", + "thiserror 2.0.17", + "trust-quorum-types", + "uuid", + "zeroize", +] + +[[package]] +name = "trust-quorum-types" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#c197cca8225b09d7115f1ac24d9b35b078a3ac70" +dependencies = [ + "omicron-workspace-hack", + "trust-quorum-types-versions", +] + +[[package]] +name = "trust-quorum-types-versions" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#c197cca8225b09d7115f1ac24d9b35b078a3ac70" +dependencies = [ + "daft", + "derive_more", + "gfss", + "iddqd", + "omicron-uuid-kinds", + "omicron-workspace-hack", + "rand 0.9.2", + "schemars 0.8.22", "serde", - "thiserror 2.0.16", + "serde_human_bytes", + "serde_with", + "sled-hardware-types", + "slog", + "slog-error-chain", + "thiserror 2.0.17", ] [[package]] @@ -7310,18 +7858,18 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "tufaceous-artifact" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/tufaceous?branch=main#db072743d0cfde918dcf981a064f225b0003b98d" +source = "git+https://github.com/oxidecomputer/tufaceous?branch=main#1eacfcf0cade44f77d433f31744dbee4abb96465" dependencies = [ "daft", "hex", "proptest", - "schemars", + "schemars 0.8.22", "semver 1.0.27", "serde", "serde_human_bytes", "strum 0.26.3", "test-strategy", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] @@ -7363,12 +7911,12 @@ dependencies = [ "proc-macro2", "quote", "regress", - "schemars", + "schemars 0.8.22", "semver 1.0.27", "serde", "serde_json", - "syn 2.0.106", - "thiserror 2.0.16", + "syn 2.0.111", + "thiserror 2.0.17", "unicode-ident", ] @@ -7380,12 +7928,12 @@ checksum = "9708a3ceb6660ba3f8d2b8f0567e7d4b8b198e2b94d093b8a6077a751425de9e" dependencies = [ "proc-macro2", "quote", - "schemars", + "schemars 0.8.22", "semver 1.0.27", "serde", "serde_json", "serde_tokenstream", - "syn 2.0.106", + "syn 2.0.111", "typify-impl", ] @@ -7446,6 +7994,16 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + [[package]] name = "unsafe-libyaml" version = "0.2.11" @@ -7467,7 +8025,7 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "update-engine" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec600ca74e573a1926c1876e33b35" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#c197cca8225b09d7115f1ac24d9b35b078a3ac70" dependencies = [ "anyhow", "cancel-safe-futures", @@ -7477,13 +8035,13 @@ dependencies = [ "either", "futures", "indent_write", - "indexmap 2.11.4", + "indexmap 2.12.1", "libsw", "linear-map", "omicron-workspace-hack", "owo-colors", "petgraph 0.8.2", - "schemars", + "schemars 0.8.22", "serde", "serde_json", "serde_with", @@ -7563,7 +8121,7 @@ dependencies = [ "proc-macro2", "quote", "serde_tokenstream", - "syn 2.0.106", + "syn 2.0.111", "usdt-impl 0.5.0", ] @@ -7577,7 +8135,7 @@ dependencies = [ "proc-macro2", "quote", "serde_tokenstream", - "syn 2.0.106", + "syn 2.0.111", "usdt-impl 0.6.0", ] @@ -7595,7 +8153,7 @@ dependencies = [ "quote", "serde", "serde_json", - "syn 2.0.106", + "syn 2.0.111", "thiserror 1.0.69", "thread-id 4.2.2", "version_check", @@ -7615,8 +8173,8 @@ dependencies = [ "quote", "serde", "serde_json", - "syn 2.0.106", - "thiserror 2.0.16", + "syn 2.0.111", + "thiserror 2.0.17", "thread-id 5.0.0", ] @@ -7630,7 +8188,7 @@ dependencies = [ "proc-macro2", "quote", "serde_tokenstream", - "syn 2.0.106", + "syn 2.0.111", "usdt-impl 0.5.0", ] @@ -7644,7 +8202,7 @@ dependencies = [ "proc-macro2", "quote", "serde_tokenstream", - "syn 2.0.106", + "syn 2.0.111", "usdt-impl 0.6.0", ] @@ -7662,13 +8220,13 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.18.1" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" +checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" dependencies = [ "getrandom 0.3.3", "js-sys", - "serde", + "serde_core", "wasm-bindgen", ] @@ -7724,6 +8282,24 @@ dependencies = [ "libc", ] +[[package]] +name = "vsss-rs" +version = "3.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "196bbee60607a195bc850e94f0e040bd090e45794ad8df0e9c5a422b9975a00f" +dependencies = [ + "curve25519-dalek", + "elliptic-curve", + "hex", + "rand 0.8.5", + "rand_chacha 0.3.1", + "rand_core 0.6.4", + "serde", + "subtle", + "thiserror-no-std", + "zeroize", +] + [[package]] name = "wait-timeout" version = "0.2.1" @@ -7814,7 +8390,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", "wasm-bindgen-shared", ] @@ -7849,7 +8425,7 @@ checksum = "ffc003a991398a8ee604a401e194b6b3a39677b3173d6e74495eb51b82e99a32" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -7974,7 +8550,7 @@ checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -7985,7 +8561,7 @@ checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -8379,7 +8955,7 @@ checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", "synstructure", ] @@ -8410,7 +8986,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -8421,7 +8997,7 @@ checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -8441,7 +9017,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", "synstructure", ] @@ -8450,6 +9026,20 @@ name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] [[package]] name = "zerotrie" @@ -8481,7 +9071,7 @@ checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 6162342..f9912c2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,7 +51,7 @@ omicron-common = { git = "https://github.com/oxidecomputer/omicron", branch= "ma oximeter = { git = "https://github.com/oxidecomputer/omicron", branch = "main" } oximeter-producer = { git = "https://github.com/oxidecomputer/omicron", branch = "main" } oximeter-instruments = { git = "https://github.com/oxidecomputer/omicron", branch = "main", default-features = false, features = ["kstat"] } -oxnet = { version = "0.1.3", default-features = false, features = ["schemars", "serde"] } +oxnet = { version = "0.1.4", default-features = false, features = ["schemars", "serde"] } propolis = { git = "https://github.com/oxidecomputer/propolis" } smf = { git = "https://github.com/illumos/smf-rs" } softnpu-lib = { git = "https://github.com/oxidecomputer/softnpu" , package = "softnpu" , branch = "main"} @@ -69,7 +69,7 @@ colored = "3" csv = "1.3" curl = "0.4" display-error-chain = "0.2" -dropshot = "0.16.4" +dropshot = "0.16.6" dropshot-api-manager = "0.2.2" dropshot-api-manager-types = "0.2.2" expectorate = "1" diff --git a/README.md b/README.md index 26c8088..98b8fa1 100644 --- a/README.md +++ b/README.md @@ -341,5 +341,14 @@ proxy_arp: 3. run `SDE=/opt/oxide/tofino_sde cargo test --features=` to execute the tests. -If regenerating the openapi specifications, set `EXPECTORATE=overwrite` when -runnning the tests with the `tofino_asic` feature. +### OpenAPI Generation + +`dpd-api/src/lib.rs` contains endpoint [dropshot][dropshot-gh] definitions and +controls API versioning for the `dpd` OpenAPI interface. If you add/remove or +edit API points and/or documentation, you can update the API version and +regenerate the latest OpenAPI specification bindings by running +`cargo xtask openapi generate`. Use `cargo xtask openapi check` to verify +specs are up-to-date. + + +[dropshot-gh]: https://github.com/oxidecomputer/dropshot diff --git a/dpd-api/src/lib.rs b/dpd-api/src/lib.rs index 9f2f9da..4fa4a5e 100644 --- a/dpd-api/src/lib.rs +++ b/dpd-api/src/lib.rs @@ -2,13 +2,18 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/ // -// Copyright 2025 Oxide Computer Company +// Copyright 2026 Oxide Computer Company //! DPD endpoint definitions. +pub mod v2; +pub mod v3; + use std::{ collections::{BTreeMap, HashMap, HashSet}, + fmt, net::{IpAddr, Ipv4Addr, Ipv6Addr}, + str::FromStr, }; use common::{ @@ -56,6 +61,8 @@ api_versions!([ // | example for the next person. // v // (next_int, IDENT), + (4, MCAST_STRICT_UNDERLAY), + (3, MCAST_SOURCE_FILTER_ANY), (2, DUAL_STACK_NAT_WORKFLOW), (1, INITIAL), ]); @@ -367,7 +374,7 @@ pub trait DpdApi { /// Get the set of available channels for all ports. /// /// This returns the unused MAC channels for each physical switch port. This can - /// be used to determine how many additional links can be crated on a physical + /// be used to determine how many additional links can be created on a physical /// switch port. #[endpoint { method = GET, @@ -1156,11 +1163,12 @@ pub trait DpdApi { /** * Clear all settings associated with a specific tag. * - * This removes: + * This removes all ARP or NDP table entries, all routes, and all links + * on all switch ports. * - * - All ARP or NDP table entries. - * - All routes - * - All links on all switch ports + * Note: Multicast groups are NOT cleared by this endpoint. Use the + * dedicated `/multicast/tags/{tag}` endpoint to clear multicast groups + * by tag. */ // TODO-security: This endpoint should probably not exist. #[endpoint { @@ -1175,7 +1183,10 @@ pub trait DpdApi { /** * Clear all settings. * - * This removes all data entirely. + * This removes all data entirely: ARP and NDP table entries, routes, + * links on all switch ports, NAT mappings, and multicast groups. + * + * Note: Unlike `reset_all_tagged`, this endpoint does clear multicast groups. */ // TODO-security: This endpoint should probably not exist. #[endpoint { @@ -1458,13 +1469,14 @@ pub trait DpdApi { /** * Create an external-only multicast group configuration. * - * External-only groups are used for IPv4 and non-admin-scoped IPv6 multicast + * External-only groups are used for IPv4 and non-admin-local IPv6 multicast * traffic that doesn't require replication infrastructure. These groups use * simple forwarding tables and require a NAT target. */ #[endpoint { method = POST, path = "/multicast/external-groups", + versions = VERSION_MCAST_STRICT_UNDERLAY.., }] async fn multicast_group_create_external( rqctx: RequestContext, @@ -1474,16 +1486,65 @@ pub trait DpdApi { HttpError, >; + /// Create an external-only multicast group configuration (API v3). + #[endpoint { + method = POST, + path = "/multicast/external-groups", + versions = VERSION_MCAST_SOURCE_FILTER_ANY..VERSION_MCAST_STRICT_UNDERLAY, + }] + async fn multicast_group_create_external_v3( + rqctx: RequestContext, + group: TypedBody, + ) -> Result< + HttpResponseCreated, + HttpError, + > { + match Self::multicast_group_create_external(rqctx, group).await { + Ok(HttpResponseCreated(resp)) => { + Ok(HttpResponseCreated(resp.into())) + } + Err(e) => Err(e), + } + } + + /// Create an external-only multicast group configuration (API v1-v2). + #[endpoint { + method = POST, + path = "/multicast/external-groups", + versions = ..VERSION_MCAST_SOURCE_FILTER_ANY, + }] + async fn multicast_group_create_external_v2( + rqctx: RequestContext, + group: TypedBody, + ) -> Result< + HttpResponseCreated, + HttpError, + > { + match Self::multicast_group_create_external( + rqctx, + group.map(Into::into), + ) + .await + { + Ok(HttpResponseCreated(resp)) => { + Ok(HttpResponseCreated(resp.into())) + } + Err(e) => Err(e), + } + } + /** * Create an underlay (internal) multicast group configuration. * - * Underlay groups are used for admin-scoped IPv6 multicast traffic that - * requires replication infrastructure. These groups support both external - * and underlay members with full replication capabilities. + * Underlay groups are used for admin-local IPv6 multicast traffic + * (ff04::/16, as defined in RFC 7346 and RFC 4291) that requires + * replication infrastructure. These groups support both external and + * underlay members with full replication capabilities. */ #[endpoint { method = POST, path = "/multicast/underlay-groups", + versions = VERSION_MCAST_STRICT_UNDERLAY.., }] async fn multicast_group_create_underlay( rqctx: RequestContext, @@ -1493,16 +1554,55 @@ pub trait DpdApi { HttpError, >; + /// Create an underlay (internal) multicast group configuration (API v1-v3). + #[endpoint { + method = POST, + path = "/multicast/underlay-groups", + versions = ..VERSION_MCAST_STRICT_UNDERLAY, + }] + async fn multicast_group_create_underlay_v3( + rqctx: RequestContext, + group: TypedBody, + ) -> Result< + HttpResponseCreated, + HttpError, + > { + match Self::multicast_group_create_underlay(rqctx, group).await { + Ok(HttpResponseCreated(resp)) => { + Ok(HttpResponseCreated(resp.into())) + } + Err(e) => Err(e), + } + } + /** - * Delete a multicast group configuration by IP address. + * Delete a multicast group configuration by IP address (API version 4+). + * + * All groups have tags (auto-generated if not provided at creation). + * The tag query parameter must match the group's existing tag. */ #[endpoint { method = DELETE, path = "/multicast/groups/{group_ip}", + versions = VERSION_MCAST_STRICT_UNDERLAY.., }] async fn multicast_group_delete( rqctx: RequestContext, path: Path, + query: Query, + ) -> Result; + + /** + * Delete a multicast group configuration by IP address (API versions 1-3). + */ + #[endpoint { + method = DELETE, + path = "/multicast/groups/{group_ip}", + versions = ..VERSION_MCAST_STRICT_UNDERLAY, + }] + async fn multicast_group_delete_v3( + rqctx: RequestContext, + path: Path, ) -> Result; /** @@ -1522,61 +1622,182 @@ pub trait DpdApi { #[endpoint { method = GET, path = "/multicast/groups/{group_ip}", + versions = VERSION_MCAST_STRICT_UNDERLAY.., }] async fn multicast_group_get( rqctx: RequestContext, path: Path, ) -> Result, HttpError>; + /// Get the multicast group configuration for a given group IP address (API v3). + #[endpoint { + method = GET, + path = "/multicast/groups/{group_ip}", + versions = VERSION_MCAST_SOURCE_FILTER_ANY..VERSION_MCAST_STRICT_UNDERLAY, + }] + async fn multicast_group_get_v3( + rqctx: RequestContext, + path: Path, + ) -> Result, HttpError> { + match Self::multicast_group_get(rqctx, path).await { + Ok(HttpResponseOk(resp)) => Ok(HttpResponseOk(resp.into())), + Err(e) => Err(e), + } + } + + /// Get the multicast group configuration for a given group IP address (API v1-v2). + #[endpoint { + method = GET, + path = "/multicast/groups/{group_ip}", + versions = ..VERSION_MCAST_SOURCE_FILTER_ANY, + }] + async fn multicast_group_get_v2( + rqctx: RequestContext, + path: Path, + ) -> Result, HttpError> { + match Self::multicast_group_get(rqctx, path).await { + Ok(HttpResponseOk(resp)) => Ok(HttpResponseOk(resp.into())), + Err(e) => Err(e), + } + } + /** - * Get an underlay (internal) multicast group configuration by admin-scoped - * IPv6 address. + * Get an underlay (internal) multicast group configuration. * - * Underlay groups handle admin-scoped IPv6 multicast traffic with + * Underlay groups handle admin-local IPv6 multicast traffic (ff04::/16) with * replication infrastructure for external and underlay members. */ #[endpoint { method = GET, path = "/multicast/underlay-groups/{group_ip}", + versions = VERSION_MCAST_STRICT_UNDERLAY.., }] async fn multicast_group_get_underlay( rqctx: RequestContext, path: Path, ) -> Result, HttpError>; + /// Get an underlay (internal) multicast group configuration (API v1-v3). + /// + /// Uses the broader ff04::/16 (admin-local) address validation for backward + /// compatibility. Delegates to v4 endpoint with path param conversion. + #[endpoint { + method = GET, + path = "/multicast/underlay-groups/{group_ip}", + versions = ..VERSION_MCAST_STRICT_UNDERLAY, + }] + async fn multicast_group_get_underlay_v3( + rqctx: RequestContext, + path: Path, + ) -> Result, HttpError> + { + let v4_path = path.try_map(|p| { + mcast::UnderlayMulticastIpv6::try_from(p.group_ip) + .map(|group_ip| MulticastUnderlayGroupIpParam { group_ip }) + .map_err(|e| { + HttpError::for_bad_request( + None, + format!("invalid group_ip: {e}"), + ) + }) + })?; + + match Self::multicast_group_get_underlay(rqctx, v4_path).await { + Ok(HttpResponseOk(resp)) => Ok(HttpResponseOk(resp.into())), + Err(e) => Err(e), + } + } + /** - * Update an underlay (internal) multicast group configuration for a given - * group IP address. + * Update an underlay (internal) multicast group configuration. * - * Underlay groups are used for admin-scoped IPv6 multicast traffic that - * requires replication infrastructure with external and underlay members. + * Underlay groups are used for admin-local IPv6 multicast traffic (ff04::/16) + * that requires replication infrastructure with external and underlay members. + * + * The `tag` query parameter must match the group's existing tag. */ #[endpoint { method = PUT, path = "/multicast/underlay-groups/{group_ip}", + versions = VERSION_MCAST_STRICT_UNDERLAY.., }] async fn multicast_group_update_underlay( rqctx: RequestContext, path: Path, + query: Query, group: TypedBody, ) -> Result, HttpError>; + /// Update an underlay (internal) multicast group configuration (API v1-v3). + /// + /// Uses the broader ff04::/16 (admin-local) address validation for backward + /// compatibility. Tags are optional in v3 for backward compatibility. + #[endpoint { + method = PUT, + path = "/multicast/underlay-groups/{group_ip}", + versions = ..VERSION_MCAST_STRICT_UNDERLAY, + }] + async fn multicast_group_update_underlay_v3( + rqctx: RequestContext, + path: Path, + group: TypedBody, + ) -> Result, HttpError>; + /** * Update an external-only multicast group configuration for a given group IP address. * - * External-only groups are used for IPv4 and non-admin-scoped IPv6 multicast + * External-only groups are used for IPv4 and non-admin-local IPv6 multicast * traffic that doesn't require replication infrastructure. + * + * The `tag` query parameter must match the group's existing tag. */ #[endpoint { method = PUT, path = "/multicast/external-groups/{group_ip}", + versions = VERSION_MCAST_STRICT_UNDERLAY.., }] async fn multicast_group_update_external( rqctx: RequestContext, path: Path, + query: Query, group: TypedBody, + ) -> Result, HttpError>; + + /** + * Update an external-only multicast group configuration (API v3). + * + * Tags are optional for backward compatibility. + */ + #[endpoint { + method = PUT, + path = "/multicast/external-groups/{group_ip}", + versions = VERSION_MCAST_SOURCE_FILTER_ANY..VERSION_MCAST_STRICT_UNDERLAY, + }] + async fn multicast_group_update_external_v3( + rqctx: RequestContext, + path: Path, + group: TypedBody, ) -> Result< - HttpResponseCreated, + HttpResponseCreated, + HttpError, + >; + + /** + * Update an external-only multicast group configuration (API v1/v2). + * + * Tags are optional for backward compatibility. + */ + #[endpoint { + method = PUT, + path = "/multicast/external-groups/{group_ip}", + versions = ..VERSION_MCAST_SOURCE_FILTER_ANY, + }] + async fn multicast_group_update_external_v2( + rqctx: RequestContext, + path: Path, + group: TypedBody, + ) -> Result< + HttpResponseCreated, HttpError, >; @@ -1586,6 +1807,7 @@ pub trait DpdApi { #[endpoint { method = GET, path = "/multicast/groups", + versions = VERSION_MCAST_STRICT_UNDERLAY.., }] async fn multicast_groups_list( rqctx: RequestContext, @@ -1597,16 +1819,69 @@ pub trait DpdApi { HttpError, >; + /// List all multicast groups (API v3). + #[endpoint { + method = GET, + path = "/multicast/groups", + versions = VERSION_MCAST_SOURCE_FILTER_ANY..VERSION_MCAST_STRICT_UNDERLAY, + }] + async fn multicast_groups_list_v3( + rqctx: RequestContext, + query_params: Query< + PaginationParams, + >, + ) -> Result< + HttpResponseOk>, + HttpError, + > { + match Self::multicast_groups_list(rqctx, query_params).await { + Ok(HttpResponseOk(page)) => Ok(HttpResponseOk(ResultsPage { + items: page.items.into_iter().map(Into::into).collect(), + next_page: page.next_page, + })), + Err(e) => Err(e), + } + } + + /// List all multicast groups (API v1/v2). + #[endpoint { + method = GET, + path = "/multicast/groups", + versions = ..VERSION_MCAST_SOURCE_FILTER_ANY, + }] + async fn multicast_groups_list_v2( + rqctx: RequestContext, + query_params: Query< + PaginationParams, + >, + ) -> Result< + HttpResponseOk>, + HttpError, + > { + match Self::multicast_groups_list(rqctx, query_params).await { + Ok(HttpResponseOk(page)) => Ok(HttpResponseOk(ResultsPage { + items: page.items.into_iter().map(Into::into).collect(), + next_page: page.next_page, + })), + Err(e) => Err(e), + } + } + /** * List all multicast groups with a given tag. + * + * Returns paginated multicast groups matching the specified tag. Tags are + * assigned at group creation and are immutable. Use this endpoint to find + * all groups associated with a specific client or component. */ #[endpoint { method = GET, path = "/multicast/tags/{tag}", + versions = VERSION_MCAST_STRICT_UNDERLAY.., }] async fn multicast_groups_list_by_tag( rqctx: RequestContext, - path: Path, + path: Path, query_params: Query< PaginationParams, >, @@ -1615,29 +1890,128 @@ pub trait DpdApi { HttpError, >; + /// List all multicast groups with a given tag (API v3). + #[endpoint { + method = GET, + path = "/multicast/tags/{tag}", + versions = VERSION_MCAST_SOURCE_FILTER_ANY..VERSION_MCAST_STRICT_UNDERLAY, + }] + async fn multicast_groups_list_by_tag_v3( + rqctx: RequestContext, + path: Path, + query_params: Query< + PaginationParams, + >, + ) -> Result< + HttpResponseOk>, + HttpError, + > { + match Self::multicast_groups_list_by_tag( + rqctx, + path.map(Into::into), + query_params, + ) + .await + { + Ok(HttpResponseOk(page)) => Ok(HttpResponseOk(ResultsPage { + items: page.items.into_iter().map(Into::into).collect(), + next_page: page.next_page, + })), + Err(e) => Err(e), + } + } + + /// List all multicast groups with a given tag (API v1/v2). + #[endpoint { + method = GET, + path = "/multicast/tags/{tag}", + versions = ..VERSION_MCAST_SOURCE_FILTER_ANY, + }] + async fn multicast_groups_list_by_tag_v2( + rqctx: RequestContext, + path: Path, + query_params: Query< + PaginationParams, + >, + ) -> Result< + HttpResponseOk>, + HttpError, + > { + match Self::multicast_groups_list_by_tag( + rqctx, + path.map(Into::into), + query_params, + ) + .await + { + Ok(HttpResponseOk(page)) => Ok(HttpResponseOk(ResultsPage { + items: page.items.into_iter().map(Into::into).collect(), + next_page: page.next_page, + })), + Err(e) => Err(e), + } + } + /** * Delete all multicast groups (and associated routes) with a given tag. + * + * This is idempotent: if no groups exist with the given tag, the operation + * returns success (the desired end state of "no groups with this tag" is + * achieved). Use this endpoint for bulk cleanup of all groups associated + * with a specific client or component. */ #[endpoint { method = DELETE, path = "/multicast/tags/{tag}", + versions = VERSION_MCAST_STRICT_UNDERLAY.., }] async fn multicast_reset_by_tag( rqctx: RequestContext, - path: Path, + path: Path, ) -> Result; + /// Delete all multicast groups (and associated routes) with a given tag + /// (API versions 1-3). + #[endpoint { + method = DELETE, + path = "/multicast/tags/{tag}", + versions = ..VERSION_MCAST_STRICT_UNDERLAY, + }] + async fn multicast_reset_by_tag_v3( + rqctx: RequestContext, + path: Path, + ) -> Result { + Self::multicast_reset_by_tag(rqctx, path.map(Into::into)).await + } + /** * Delete all multicast groups (and associated routes) without a tag. + * + * DEPRECATED: All groups have default tags generated at creation time. + * This endpoint returns HTTP 410 Gone. Use `multicast_reset_by_tag` + * with the tag returned from group creation instead. */ #[endpoint { method = DELETE, path = "/multicast/untagged", + versions = VERSION_MCAST_STRICT_UNDERLAY.., }] async fn multicast_reset_untagged( rqctx: RequestContext, ) -> Result; + /** + * Delete all multicast groups (and associated routes) without a tag. + */ + #[endpoint { + method = DELETE, + path = "/multicast/untagged", + versions = ..VERSION_MCAST_STRICT_UNDERLAY, + }] + async fn multicast_reset_untagged_v3( + rqctx: RequestContext, + ) -> Result; + /** * Get the physical coding sublayer (PCS) counters for all links. */ @@ -2216,6 +2590,7 @@ pub struct LinkFilter { pub filter: Option, } +/// Path parameter for tag-based operations. #[derive(Deserialize, Serialize, JsonSchema)] pub struct TagPath { pub tag: String, @@ -2297,11 +2672,114 @@ pub struct MulticastGroupIpParam { pub group_ip: IpAddr, } -/// Used to identify an underlay (internal) multicast group by admin-scoped IPv6 -/// address. +/// Tag for identifying and authorizing multicast group operations. +/// +/// Tag format: 1 to 80 ASCII bytes containing alphanumeric characters, +/// hyphens, underscores, colons, or periods. Default format is +/// `{uuid}:{group_ip}`. +#[derive( + Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize, JsonSchema, +)] +pub struct MulticastTag( + #[schemars( + length(min = 1, max = 80), + regex(pattern = r"^[a-zA-Z0-9_.:-]+$") + )] + pub String, +); + +impl AsRef for MulticastTag { + fn as_ref(&self) -> &str { + &self.0 + } +} + +impl From for String { + fn from(tag: MulticastTag) -> Self { + tag.0 + } +} + +impl From for MulticastTag { + fn from(tag: String) -> Self { + MulticastTag(tag) + } +} + +/// Maximum length for multicast tags. +pub const MAX_TAG_LENGTH: usize = 80; + +/// Error parsing a multicast tag from a string. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct MulticastTagParseError(String); + +impl fmt::Display for MulticastTagParseError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +impl std::error::Error for MulticastTagParseError {} + +impl FromStr for MulticastTag { + type Err = MulticastTagParseError; + + fn from_str(s: &str) -> Result { + if s.is_empty() { + return Err(MulticastTagParseError( + "tag cannot be empty".to_string(), + )); + } + if s.len() > MAX_TAG_LENGTH { + return Err(MulticastTagParseError(format!( + "tag cannot exceed {MAX_TAG_LENGTH} bytes" + ))); + } + if !s.bytes().all(|b| { + b.is_ascii_alphanumeric() || matches!(b, b'-' | b'_' | b':' | b'.') + }) { + return Err(MulticastTagParseError( + "tag must contain only ASCII alphanumeric characters, hyphens, \ + underscores, colons, or periods" + .to_string(), + )); + } + Ok(MulticastTag(s.to_string())) + } +} + +/// Path parameter for multicast tag-based operations (API version 4+). +#[derive(Deserialize, Serialize, JsonSchema)] +pub struct MulticastTagPath { + pub tag: MulticastTag, +} + +impl From for MulticastTagPath { + fn from(path: TagPath) -> Self { + Self { + tag: path.tag.into(), + } + } +} + +/// Tag for multicast group validation. +/// +/// All groups have tags (auto-generated at creation if not provided). +/// The provided tag must match the group's existing tag. +#[derive(Deserialize, Serialize, JsonSchema)] +pub struct MulticastGroupTagQuery { + /// Tag that must match the group's existing tag. + pub tag: MulticastTag, +} + +/// Used to identify an underlay (internal) multicast group by admin-local IPv6 +/// address (ff04::/16, as defined in [RFC 7346] and [RFC 4291]). +/// +/// [RFC 7346]: https://www.rfc-editor.org/rfc/rfc7346.html +/// [RFC 4291]: https://www.rfc-editor.org/rfc/rfc4291.html #[derive(Deserialize, Serialize, JsonSchema)] pub struct MulticastUnderlayGroupIpParam { - pub group_ip: mcast::AdminScopedIpv6, + pub group_ip: mcast::UnderlayMulticastIpv6, } /// Used to identify a multicast group by ID. diff --git a/dpd-api/src/v2.rs b/dpd-api/src/v2.rs new file mode 100644 index 0000000..4d6e37f --- /dev/null +++ b/dpd-api/src/v2.rs @@ -0,0 +1,195 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/ +// +// Copyright 2026 Oxide Computer Company + +//! Types from API version 2 that changed in version 3. +//! +//! The `IpSrc` enum changed from `{Exact, Subnet}` to `{Exact, Any}`. + +use std::{fmt, net::IpAddr}; + +use oxnet::Ipv4Net; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use dpd_types::mcast::{ + ExternalForwarding, InternalForwarding, MulticastGroupId, +}; + +// Use v3 underlay response which has Option tag +pub use crate::v3::MulticastGroupUnderlayResponse; + +/// Source filter match key for multicast traffic (API versions 1 and 2). +/// +/// This is the original `IpSrc` enum that used a single `Subnet` variant +/// (IPv4 only) rather than the `Any` variant added in version 3. +#[derive( + Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize, JsonSchema, +)] +pub enum IpSrc { + /// Exact match for the source IP address. + Exact(IpAddr), + /// Subnet match for the source IP address. + Subnet(Ipv4Net), +} + +impl fmt::Display for IpSrc { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + IpSrc::Exact(ip) => write!(f, "{ip}"), + IpSrc::Subnet(net) => write!(f, "{net}"), + } + } +} + +/// Convert from v3 IpSrc to v1/v2 IpSrc. +impl From for IpSrc { + fn from(src: dpd_types::mcast::IpSrc) -> Self { + match src { + dpd_types::mcast::IpSrc::Exact(ip) => IpSrc::Exact(ip), + dpd_types::mcast::IpSrc::Any => { + // v1/v2 API only supported IPv4 subnet matching. + IpSrc::Subnet( + Ipv4Net::new(std::net::Ipv4Addr::UNSPECIFIED, 0).unwrap(), + ) + } + } + } +} + +/// A multicast group configuration for POST requests for external (to the rack) +/// groups (API version 2). +#[derive(Debug, Deserialize, Serialize, JsonSchema)] +pub struct MulticastGroupCreateExternalEntry { + pub group_ip: IpAddr, + pub tag: Option, + pub internal_forwarding: InternalForwarding, + pub external_forwarding: ExternalForwarding, + pub sources: Option>, +} + +/// A multicast group update entry for PUT requests for external (to the rack) +/// groups (API version 2). +/// +/// Tag validation is optional in v2 for backward compatibility. +#[derive(Debug, Deserialize, Serialize, JsonSchema)] +pub struct MulticastGroupUpdateExternalEntry { + /// Tag for validating update requests. Optional in v2; if not provided, + /// tag validation is skipped. + pub tag: Option, + pub internal_forwarding: InternalForwarding, + pub external_forwarding: ExternalForwarding, + pub sources: Option>, +} + +/// Response structure for external multicast group operations (API version 2). +#[derive(Debug, Deserialize, Serialize, JsonSchema)] +pub struct MulticastGroupExternalResponse { + pub group_ip: IpAddr, + pub external_group_id: MulticastGroupId, + pub tag: Option, + pub internal_forwarding: InternalForwarding, + pub external_forwarding: ExternalForwarding, + pub sources: Option>, +} + +/// Convert from API v4 response to v2 response. +impl From + for MulticastGroupExternalResponse +{ + fn from(resp: dpd_types::mcast::MulticastGroupExternalResponse) -> Self { + Self { + group_ip: resp.group_ip, + external_group_id: resp.external_group_id, + tag: Some(resp.tag), + internal_forwarding: resp.internal_forwarding, + external_forwarding: resp.external_forwarding, + sources: resp + .sources + .map(|sources| sources.into_iter().map(IpSrc::from).collect()), + } + } +} + +/// Unified response type for operations that return mixed group types +/// (API version 2). +#[derive(Debug, Deserialize, Serialize, JsonSchema)] +#[serde(tag = "kind", rename_all = "snake_case")] +pub enum MulticastGroupResponse { + Underlay(MulticastGroupUnderlayResponse), + External(MulticastGroupExternalResponse), +} + +impl MulticastGroupResponse { + /// Get the multicast group IP address. + pub fn ip(&self) -> IpAddr { + match self { + Self::Underlay(resp) => resp.group_ip.into(), + Self::External(resp) => resp.group_ip, + } + } +} + +/// Convert from API v4 response to v2 response. +impl From for MulticastGroupResponse { + fn from(resp: dpd_types::mcast::MulticastGroupResponse) -> Self { + match resp { + dpd_types::mcast::MulticastGroupResponse::Underlay(u) => { + Self::Underlay(u.into()) + } + dpd_types::mcast::MulticastGroupResponse::External(e) => { + Self::External(e.into()) + } + } + } +} + +// ============================================================================ +// v2 → v3 conversions (for request types) +// ============================================================================ + +impl From for dpd_types::mcast::IpSrc { + fn from(src: IpSrc) -> Self { + match src { + IpSrc::Exact(ip) => dpd_types::mcast::IpSrc::Exact(ip), + IpSrc::Subnet(net) if net.width() == 0 => { + dpd_types::mcast::IpSrc::Any + } + IpSrc::Subnet(net) => { + dpd_types::mcast::IpSrc::Exact(IpAddr::V4(net.addr())) + } + } + } +} + +impl From + for dpd_types::mcast::MulticastGroupCreateExternalEntry +{ + fn from(entry: MulticastGroupCreateExternalEntry) -> Self { + Self { + group_ip: entry.group_ip, + tag: entry.tag, + internal_forwarding: entry.internal_forwarding, + external_forwarding: entry.external_forwarding, + sources: entry + .sources + .map(|s| s.into_iter().map(Into::into).collect()), + } + } +} + +impl From + for dpd_types::mcast::MulticastGroupUpdateExternalEntry +{ + fn from(entry: MulticastGroupUpdateExternalEntry) -> Self { + Self { + internal_forwarding: entry.internal_forwarding, + external_forwarding: entry.external_forwarding, + sources: entry + .sources + .map(|s| s.into_iter().map(Into::into).collect()), + } + } +} diff --git a/dpd-api/src/v3.rs b/dpd-api/src/v3.rs new file mode 100644 index 0000000..bff9f0e --- /dev/null +++ b/dpd-api/src/v3.rs @@ -0,0 +1,257 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/ +// +// Copyright 2026 Oxide Computer Company + +//! Types from API version 3 that changed in version 4. +//! +//! Changes in v4 (MCAST_STRICT_UNDERLAY): +//! - The `tag` field in response types changed from `Option` to `String` +//! since all groups now have default tags generated at creation time. +//! - Tag validation is now required for updates and deletes. +//! - `AdminScopedIpv6` was renamed to `UnderlayMulticastIpv6` and validation +//! was tightened from ff04::/16 to ff04::/64. + +use std::{ + fmt, + net::{IpAddr, Ipv6Addr}, + str::FromStr, +}; + +use oxnet::Ipv6Net; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use dpd_types::mcast::{ + ExternalForwarding, InternalForwarding, IpSrc, MulticastGroupId, + MulticastGroupMember, UnderlayMulticastIpv6, +}; + +/// A validated admin-local IPv6 multicast address (API version 3). +/// +/// In v3, admin-local addresses are validated against ff04::/16 (scope 4). +/// In v4+, this was renamed to `UnderlayMulticastIpv6` and tightened to +/// ff04::/64 to match Omicron's underlay multicast subnet allocation. +#[derive( + Clone, + Copy, + Debug, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, + Deserialize, + Serialize, + JsonSchema, +)] +#[serde(try_from = "Ipv6Addr", into = "Ipv6Addr")] +pub struct AdminScopedIpv6(Ipv6Addr); + +impl AdminScopedIpv6 { + /// Create a new AdminScopedIpv6 if the address is admin-local (ff04::/16). + pub fn new(addr: Ipv6Addr) -> Result { + if !Ipv6Net::new_unchecked(addr, 128).is_admin_local_multicast() { + return Err(format!( + "Address {} is not admin-local (must be ff04::/16)", + addr + )); + } + Ok(Self(addr)) + } +} + +impl TryFrom for AdminScopedIpv6 { + type Error = String; + + fn try_from(addr: Ipv6Addr) -> Result { + Self::new(addr) + } +} + +impl From for Ipv6Addr { + fn from(admin: AdminScopedIpv6) -> Self { + admin.0 + } +} + +impl From for IpAddr { + fn from(admin: AdminScopedIpv6) -> Self { + IpAddr::V6(admin.0) + } +} + +impl From for AdminScopedIpv6 { + fn from(underlay: UnderlayMulticastIpv6) -> Self { + // UnderlayMulticastIpv6 is a subset of AdminScopedIpv6, so this is safe + Self(underlay.into()) + } +} + +impl TryFrom for UnderlayMulticastIpv6 { + type Error = String; + + fn try_from(admin: AdminScopedIpv6) -> Result { + UnderlayMulticastIpv6::new(admin.0).map_err(|e| e.to_string()) + } +} + +impl fmt::Display for AdminScopedIpv6 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} + +impl FromStr for AdminScopedIpv6 { + type Err = String; + + fn from_str(s: &str) -> Result { + let addr: Ipv6Addr = + s.parse().map_err(|e| format!("invalid IPv6: {e}"))?; + Self::new(addr) + } +} + +/// Response structure for underlay/internal multicast group operations +/// (API version 3). +#[derive(Debug, Deserialize, Serialize, JsonSchema)] +pub struct MulticastGroupUnderlayResponse { + pub group_ip: AdminScopedIpv6, + pub external_group_id: MulticastGroupId, + pub underlay_group_id: MulticastGroupId, + pub tag: Option, + pub members: Vec, +} + +/// Convert from API v4 response to v3 response. +impl From + for MulticastGroupUnderlayResponse +{ + fn from(resp: dpd_types::mcast::MulticastGroupUnderlayResponse) -> Self { + Self { + group_ip: resp.group_ip.into(), + external_group_id: resp.external_group_id, + underlay_group_id: resp.underlay_group_id, + tag: Some(resp.tag), + members: resp.members, + } + } +} + +/// Response structure for external multicast group operations (API version 3). +#[derive(Debug, Deserialize, Serialize, JsonSchema)] +pub struct MulticastGroupExternalResponse { + pub group_ip: IpAddr, + pub external_group_id: MulticastGroupId, + pub tag: Option, + pub internal_forwarding: InternalForwarding, + pub external_forwarding: ExternalForwarding, + pub sources: Option>, +} + +/// Convert from API v4 response to v3 response. +impl From + for MulticastGroupExternalResponse +{ + fn from(resp: dpd_types::mcast::MulticastGroupExternalResponse) -> Self { + Self { + group_ip: resp.group_ip, + external_group_id: resp.external_group_id, + tag: Some(resp.tag), + internal_forwarding: resp.internal_forwarding, + external_forwarding: resp.external_forwarding, + sources: resp.sources, + } + } +} + +/// Unified response type for operations that return mixed group types +/// (API version 3). +#[derive(Debug, Deserialize, Serialize, JsonSchema)] +#[serde(tag = "kind", rename_all = "snake_case")] +pub enum MulticastGroupResponse { + Underlay(MulticastGroupUnderlayResponse), + External(MulticastGroupExternalResponse), +} + +impl MulticastGroupResponse { + /// Get the multicast group IP address. + pub fn ip(&self) -> IpAddr { + match self { + Self::Underlay(resp) => resp.group_ip.into(), + Self::External(resp) => resp.group_ip, + } + } +} + +/// Convert from API v4 response to v3 response. +impl From for MulticastGroupResponse { + fn from(resp: dpd_types::mcast::MulticastGroupResponse) -> Self { + match resp { + dpd_types::mcast::MulticastGroupResponse::Underlay(u) => { + Self::Underlay(u.into()) + } + dpd_types::mcast::MulticastGroupResponse::External(e) => { + Self::External(e.into()) + } + } + } +} + +/// A multicast group update entry for PUT requests for internal groups +/// (API version 3). +/// +/// Tags are optional in v3 for backward compatibility. If not provided, +/// the existing tag is preserved. +#[derive(Debug, Deserialize, Serialize, JsonSchema)] +pub struct MulticastGroupUpdateUnderlayEntry { + /// Tag for validating update requests. Optional in v3; if not provided, + /// tag validation is skipped. + pub tag: Option, + pub members: Vec, +} + +impl From + for dpd_types::mcast::MulticastGroupUpdateUnderlayEntry +{ + fn from(entry: MulticastGroupUpdateUnderlayEntry) -> Self { + Self { + members: entry.members, + } + } +} + +/// A multicast group update entry for PUT requests for external groups +/// (API version 3). +/// +/// Tag validation is optional in v3 for backward compatibility. +#[derive(Debug, Deserialize, Serialize, JsonSchema)] +pub struct MulticastGroupUpdateExternalEntry { + /// Tag for validating update requests. Optional in v3; if not provided, + /// tag validation is skipped. + pub tag: Option, + pub internal_forwarding: InternalForwarding, + pub external_forwarding: ExternalForwarding, + pub sources: Option>, +} + +impl From + for dpd_types::mcast::MulticastGroupUpdateExternalEntry +{ + fn from(entry: MulticastGroupUpdateExternalEntry) -> Self { + Self { + internal_forwarding: entry.internal_forwarding, + external_forwarding: entry.external_forwarding, + sources: entry.sources, + } + } +} + +/// Path parameter for underlay multicast group endpoints (API version 3). +/// +/// Uses `AdminScopedIpv6` which accepts the broader ff04::/16 range. +#[derive(Deserialize, Serialize, JsonSchema)] +pub struct MulticastUnderlayGroupIpParam { + pub group_ip: AdminScopedIpv6, +} diff --git a/dpd-client/Cargo.toml b/dpd-client/Cargo.toml index 9add026..1cba04c 100644 --- a/dpd-client/Cargo.toml +++ b/dpd-client/Cargo.toml @@ -33,6 +33,7 @@ packet = { path = "../packet" } pcap = { path = "../pcap" } asic = { path = "../asic" } anyhow.workspace = true +dpd-types.workspace = true lazy_static.workspace = true parking_lot.workspace = true rand.workspace = true diff --git a/dpd-client/tests/integration_tests/common.rs b/dpd-client/tests/integration_tests/common.rs index 5b315cb..56db9fd 100644 --- a/dpd-client/tests/integration_tests/common.rs +++ b/dpd-client/tests/integration_tests/common.rs @@ -2,7 +2,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/ // -// Copyright 2025 Oxide Computer Company +// Copyright 2026 Oxide Computer Company use std::fmt::Write; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; diff --git a/dpd-client/tests/integration_tests/mcast.rs b/dpd-client/tests/integration_tests/mcast.rs index 5a4a586..70e6f58 100644 --- a/dpd-client/tests/integration_tests/mcast.rs +++ b/dpd-client/tests/integration_tests/mcast.rs @@ -16,25 +16,38 @@ use ::common::network::MacAddr; use anyhow::anyhow; use dpd_client::{Error, types}; use futures::TryStreamExt; -use oxnet::{Ipv4Net, MulticastMac}; +use oxnet::MulticastMac; use packet::{Endpoint, eth, geneve, ipv4, ipv6, udp}; +/// Admin-local IPv6 multicast prefix (ff04::/16, scope 4). +const ADMIN_LOCAL_PREFIX: u16 = 0xFF04; + const MULTICAST_TEST_IPV4: Ipv4Addr = Ipv4Addr::new(224, 0, 1, 0); const MULTICAST_TEST_IPV6: Ipv6Addr = Ipv6Addr::new(0xff0e, 0, 0, 0, 0, 0, 1, 0x1010); const MULTICAST_TEST_IPV4_SSM: Ipv4Addr = Ipv4Addr::new(232, 123, 45, 67); const MULTICAST_TEST_IPV6_SSM: Ipv6Addr = Ipv6Addr::new(0xff3e, 0, 0, 0, 0, 0, 0, 0x1111); -const MULTICAST_NAT_IP: Ipv6Addr = Ipv6Addr::new(0xff05, 0, 0, 0, 0, 0, 0, 1); +const MULTICAST_NAT_IP: Ipv6Addr = + Ipv6Addr::new(ADMIN_LOCAL_PREFIX, 0, 0, 0, 0, 0, 0, 1); const GIMLET_MAC: &str = "11:22:33:44:55:66"; const GIMLET_IP: Ipv6Addr = Ipv6Addr::new(0xfd00, 0x1122, 0x7788, 0x0101, 0, 0, 0, 4); +/// Tag used for multicast group validation in tests. +const TEST_TAG: &str = "mcast_integration_test"; + +// Tag validation test consts +const TAG_A: &str = "tag_a"; +const TAG_B: &str = "tag_b"; +const TAG_WRONG: &str = "wrong_tag"; +const TAG_DIFFERENT: &str = "different_tag"; + trait ToIpAddr { fn to_ip_addr(&self) -> IpAddr; } -impl ToIpAddr for types::AdminScopedIpv6 { +impl ToIpAddr for types::UnderlayMulticastIpv6 { fn to_ip_addr(&self) -> IpAddr { IpAddr::V6(self.0) } @@ -121,12 +134,12 @@ async fn create_test_multicast_group( } IpAddr::V6(ipv6) => { if oxnet::Ipv6Net::new_unchecked(ipv6, 128) - .is_admin_scoped_multicast() + .is_admin_local_multicast() { - // Admin-scoped IPv6 groups are internal - let admin_scoped_ip = types::AdminScopedIpv6(ipv6); + // Admin-local IPv6 groups are internal + let admin_local_ip = types::UnderlayMulticastIpv6(ipv6); let internal_entry = types::MulticastGroupCreateUnderlayEntry { - group_ip: admin_scoped_ip, + group_ip: admin_local_ip, tag: tag.map(String::from), members, }; @@ -146,7 +159,7 @@ async fn create_test_multicast_group( underlay_group_id: resp.underlay_group_id, } } else { - // Non-admin-scoped IPv6 groups are external-only and require NAT targets + // Non-admin-local IPv6 groups are external-only and require NAT targets let external_entry = types::MulticastGroupCreateExternalEntry { group_ip, tag: tag.map(String::from), @@ -175,11 +188,20 @@ async fn create_test_multicast_group( } } +fn make_tag(tag: &str) -> types::MulticastTag { + tag.parse().expect("tag should parse") +} + /// Clean up a test group, failing if it cannot be deleted properly. -async fn cleanup_test_group(switch: &Switch, group_ip: IpAddr) -> TestResult { +async fn cleanup_test_group( + switch: &Switch, + group_ip: IpAddr, + tag: &str, +) -> TestResult { + let del_tag = make_tag(tag); switch .client - .multicast_group_delete(&group_ip) + .multicast_group_delete(&group_ip, &del_tag) .await .map_err(|e| { anyhow!("Failed to delete test group {}: {:?}", group_ip, e) @@ -252,7 +274,7 @@ fn get_nat_target( } } -fn get_tag(response: &types::MulticastGroupResponse) -> &Option { +fn get_tag(response: &types::MulticastGroupResponse) -> &String { match response { types::MulticastGroupResponse::Underlay { tag, .. } => tag, types::MulticastGroupResponse::External { tag, .. } => tag, @@ -475,7 +497,7 @@ async fn test_group_creation_with_validation() -> TestResult { let internal_group = create_test_multicast_group( switch, internal_multicast_ip, - Some("valid_internal_group"), + Some(TEST_TAG), &[(egress1, types::Direction::Underlay)], types::InternalForwarding { nat_target: None }, types::ExternalForwarding { vlan_id: None }, @@ -493,7 +515,7 @@ async fn test_group_creation_with_validation() -> TestResult { // IPv4 groups are always external let external_invalid = types::MulticastGroupCreateExternalEntry { group_ip: IpAddr::V4(MULTICAST_TEST_IPV4), - tag: Some("test_invalid".to_string()), + tag: Some(TEST_TAG.to_string()), internal_forwarding: types::InternalForwarding { nat_target: Some(nat_target.clone()), }, @@ -524,7 +546,7 @@ async fn test_group_creation_with_validation() -> TestResult { // IPv4 groups are always external let external_valid = types::MulticastGroupCreateExternalEntry { group_ip: IpAddr::V4(MULTICAST_TEST_IPV4_SSM), - tag: Some("test_valid".to_string()), + tag: Some(TEST_TAG.to_string()), internal_forwarding: types::InternalForwarding { nat_target: Some(nat_target.clone()), }, @@ -548,7 +570,7 @@ async fn test_group_creation_with_validation() -> TestResult { ); assert_eq!(created.group_ip, MULTICAST_TEST_IPV4_SSM); - assert_eq!(created.tag, Some("test_valid".to_string())); + assert_eq!(created.tag, TEST_TAG); assert_eq!( created.internal_forwarding.nat_target, Some(nat_target.clone()) @@ -561,7 +583,11 @@ async fn test_group_creation_with_validation() -> TestResult { )]) ); - cleanup_test_group(switch, created.group_ip).await + // Clean up external first (references internal via NAT target), then internal + cleanup_test_group(switch, created.group_ip, TEST_TAG) + .await + .unwrap(); + cleanup_test_group(switch, internal_multicast_ip, TEST_TAG).await } #[tokio::test] @@ -574,7 +600,7 @@ async fn test_internal_ipv6_validation() -> TestResult { // Admin-scoped IPv6 groups work correctly let internal_group = types::MulticastGroupCreateUnderlayEntry { group_ip: "ff04::2".parse().unwrap(), - tag: Some("test_admin_scoped".to_string()), + tag: Some(TEST_TAG.to_string()), members: vec![types::MulticastGroupMember { port_id: port_id.clone(), link_id, @@ -594,9 +620,8 @@ async fn test_internal_ipv6_validation() -> TestResult { "Group IDs should be different" ); - // Test update works correctly + // Test update works correctly (must use same tag for validation). let update_entry = types::MulticastGroupUpdateUnderlayEntry { - tag: Some("updated_tag".to_string()), members: vec![types::MulticastGroupMember { port_id, link_id, @@ -606,14 +631,18 @@ async fn test_internal_ipv6_validation() -> TestResult { let updated = switch .client - .multicast_group_update_underlay(&created.group_ip, &update_entry) + .multicast_group_update_underlay( + &created.group_ip, + &make_tag(TEST_TAG), + &update_entry, + ) .await .expect("Should update internal IPv6 group") .into_inner(); - assert_eq!(updated.tag, Some("updated_tag".to_string())); + assert_eq!(updated.tag, TEST_TAG); - cleanup_test_group(switch, created.group_ip.to_ip_addr()).await + cleanup_test_group(switch, created.group_ip.to_ip_addr(), TEST_TAG).await } #[tokio::test] @@ -626,7 +655,7 @@ async fn test_vlan_propagation_to_internal() -> TestResult { // Create internal IPv6 group first let internal_group_entry = types::MulticastGroupCreateUnderlayEntry { group_ip: "ff04::200".parse().unwrap(), - tag: Some("test_vlan_propagation".to_string()), + tag: Some(TEST_TAG.to_string()), members: vec![ types::MulticastGroupMember { port_id: port_id.clone(), @@ -657,7 +686,7 @@ async fn test_vlan_propagation_to_internal() -> TestResult { let external_group = types::MulticastGroupCreateExternalEntry { group_ip: IpAddr::V4("224.1.2.3".parse().unwrap()), - tag: Some("test_external_with_vlan".to_string()), + tag: Some(TEST_TAG.to_string()), internal_forwarding: types::InternalForwarding { nat_target: Some(nat_target), }, @@ -701,10 +730,12 @@ async fn test_vlan_propagation_to_internal() -> TestResult { "Admin-scoped group bitmap should have VLAN 42 from external group" ); - cleanup_test_group(switch, created_admin.group_ip.to_ip_addr()) + // Delete external group first since it references the internal group via NAT target + cleanup_test_group(switch, created_external.group_ip, TEST_TAG) .await .unwrap(); - cleanup_test_group(switch, created_external.group_ip).await + cleanup_test_group(switch, created_admin.group_ip.to_ip_addr(), TEST_TAG) + .await } #[tokio::test] @@ -717,7 +748,7 @@ async fn test_group_api_lifecycle() { create_test_multicast_group( switch, internal_multicast_ip, - Some("valid_underlay_group"), + Some(TEST_TAG), &[(egress1, types::Direction::Underlay)], types::InternalForwarding { nat_target: None }, types::ExternalForwarding { vlan_id: None }, @@ -731,7 +762,7 @@ async fn test_group_api_lifecycle() { let nat_target = create_nat_target_ipv4(); let external_create = types::MulticastGroupCreateExternalEntry { group_ip, - tag: Some("test_lifecycle".to_string()), + tag: Some(TEST_TAG.to_string()), internal_forwarding: types::InternalForwarding { nat_target: Some(nat_target.clone()), }, @@ -751,7 +782,7 @@ async fn test_group_api_lifecycle() { let external_group_id = created.external_group_id; assert_eq!(created.group_ip, MULTICAST_TEST_IPV4); - assert_eq!(created.tag, Some("test_lifecycle".to_string())); + assert_eq!(created.tag, TEST_TAG); assert_eq!( created.internal_forwarding.nat_target, Some(nat_target.clone()) @@ -774,7 +805,7 @@ async fn test_group_api_lifecycle() { // Get groups by tag let tagged_groups = switch .client - .multicast_groups_list_by_tag_stream("test_lifecycle", None) + .multicast_groups_list_by_tag_stream(&make_tag(TEST_TAG), None) .try_collect::>() .await .expect("Should be able to get groups by tag"); @@ -797,7 +828,7 @@ async fn test_group_api_lifecycle() { .expect("Should be able to get group by ID"); assert_eq!(get_external_group_id(&group[0]), external_group_id); - assert_eq!(get_tag(&group[0]), &Some("test_lifecycle".to_string())); + assert_eq!(get_tag(&group[0]), TEST_TAG); // Also test getting by IP address let group_by_ip = switch @@ -820,7 +851,6 @@ async fn test_group_api_lifecycle() { }; let external_update = types::MulticastGroupUpdateExternalEntry { - tag: Some("updated_lifecycle".to_string()), internal_forwarding: types::InternalForwarding { nat_target: Some(updated_nat_target.clone()), }, @@ -830,13 +860,17 @@ async fn test_group_api_lifecycle() { let updated = switch .client - .multicast_group_update_external(&group_ip, &external_update) + .multicast_group_update_external( + &group_ip, + &make_tag(TEST_TAG), + &external_update, + ) .await .expect("Should be able to update group") .into_inner(); assert_eq!(updated.external_group_id, external_group_id); - assert_eq!(updated.tag, Some("updated_lifecycle".to_string())); + assert_eq!(updated.tag, TEST_TAG); assert_eq!( updated.internal_forwarding.nat_target, Some(updated_nat_target) @@ -844,10 +878,11 @@ async fn test_group_api_lifecycle() { assert_eq!(updated.external_forwarding.vlan_id, Some(20)); assert_eq!(updated.sources, None); - // Delete the group + // Delete the group (must provide matching tag) + let del_tag = make_tag(TEST_TAG); switch .client - .multicast_group_delete(&group_ip) + .multicast_group_delete(&group_ip, &del_tag) .await .expect("Should be able to delete group"); @@ -885,6 +920,145 @@ async fn test_group_api_lifecycle() { !deleted_group_still_in_list, "Deleted group should not be in the list" ); + + // Clean up the internal group (external was already deleted above) + cleanup_test_group(switch, internal_multicast_ip, TEST_TAG) + .await + .unwrap(); +} + +/// Tests tag validation behavior on multicast group deletion. +/// +/// The tag parameter on delete serves as authorization: +/// - All groups have tags (auto-generated if not provided at creation) +/// - Deletion requires a matching tag for validation +/// - Mismatched tags result in a 400 Bad Request error +#[tokio::test] +#[ignore] +async fn test_multicast_del_tag_validation() -> TestResult { + let switch = &*get_switch().await; + + // Setup: create internal admin-scoped group for NAT target + let internal_multicast_ip = IpAddr::V6(MULTICAST_NAT_IP); + create_test_multicast_group( + switch, + internal_multicast_ip, + Some(TEST_TAG), + &[(PhysPort(11), types::Direction::Underlay)], + types::InternalForwarding { nat_target: None }, + types::ExternalForwarding { vlan_id: None }, + None, + ) + .await; + + // Test Case 1: Delete with mismatched tag should fail + let tagged_group_ip = IpAddr::V4(Ipv4Addr::new(224, 0, 10, 1)); + let external_tagged = types::MulticastGroupCreateExternalEntry { + group_ip: tagged_group_ip, + tag: Some(TAG_A.to_string()), + internal_forwarding: types::InternalForwarding { + nat_target: Some(create_nat_target_ipv4()), + }, + external_forwarding: types::ExternalForwarding { vlan_id: Some(10) }, + sources: None, + }; + + switch + .client + .multicast_group_create_external(&external_tagged) + .await + .expect("Should create tagged group"); + + // Attempt delete with wrong tag - should fail + let del_tag = make_tag(TAG_B); + let wrong_tag_result = switch + .client + .multicast_group_delete(&tagged_group_ip, &del_tag) + .await; + + match &wrong_tag_result { + Err(Error::ErrorResponse(resp)) => { + assert_eq!( + resp.status(), + 400, + "Wrong tag should return 400 Bad Request" + ); + } + _ => panic!( + "Expected ErrorResponse for tag mismatch, got: {:?}", + wrong_tag_result + ), + } + + // Case: Empty string should fail client-side validation (schema enforces minLength: 1) + assert!( + "".parse::().is_err(), + "Empty tag should fail client-side validation" + ); + + // Verify group still exists after failed delete attempts + let group_still_exists = + switch.client.multicast_group_get(&tagged_group_ip).await; + assert!( + group_still_exists.is_ok(), + "Group should still exist after failed delete attempts" + ); + + // Case: Delete with correct tag should succeed + let del_tag = make_tag(TAG_A); + switch + .client + .multicast_group_delete(&tagged_group_ip, &del_tag) + .await + .expect("Should delete group with matching tag"); + + // Verify group was deleted + let deleted_result = + switch.client.multicast_group_get(&tagged_group_ip).await; + match deleted_result { + Err(Error::ErrorResponse(resp)) => { + assert_eq!(resp.status(), 404, "Deleted group should return 404"); + } + _ => panic!("Expected 404 for deleted group"), + } + + // Case: Create group without explicit tag (uses default generated tag) + // then delete with the generated tag + let auto_tagged_group_ip = IpAddr::V4(Ipv4Addr::new(224, 0, 10, 2)); + let external_auto_tagged = types::MulticastGroupCreateExternalEntry { + group_ip: auto_tagged_group_ip, + tag: None, // Will get auto-generated tag + internal_forwarding: types::InternalForwarding { + nat_target: Some(create_nat_target_ipv4()), + }, + external_forwarding: types::ExternalForwarding { vlan_id: Some(10) }, + sources: None, + }; + + let created = switch + .client + .multicast_group_create_external(&external_auto_tagged) + .await + .expect("Should create group with auto-generated tag") + .into_inner(); + + // The group should have an auto-generated tag + assert!( + !created.tag.is_empty(), + "Group should have auto-generated tag" + ); + let auto_tag = created.tag.clone(); + + // Delete with correct auto-generated tag should succeed + let del_tag = make_tag(auto_tag.as_str()); + switch + .client + .multicast_group_delete(&auto_tagged_group_ip, &del_tag) + .await + .expect("Should delete group with matching auto-generated tag"); + + // Clean up internal group + cleanup_test_group(switch, internal_multicast_ip, TEST_TAG).await } #[tokio::test] @@ -950,7 +1124,7 @@ async fn test_multicast_tagged_groups_management() { // Create third IPv4 external group (different tag) let external_group3 = types::MulticastGroupCreateExternalEntry { group_ip: "224.0.1.3".parse().unwrap(), // Different IP - tag: Some("different_tag".to_string()), + tag: Some(TAG_DIFFERENT.to_string()), internal_forwarding: types::InternalForwarding { nat_target: Some(nat_target.clone()), }, @@ -968,7 +1142,7 @@ async fn test_multicast_tagged_groups_management() { // List groups by tag let tagged_groups = switch .client - .multicast_groups_list_by_tag_stream(tag, None) + .multicast_groups_list_by_tag_stream(&make_tag(tag), None) .try_collect::>() .await .expect("Should list groups by tag"); @@ -984,7 +1158,7 @@ async fn test_multicast_tagged_groups_management() { // Delete all groups with the tag switch .client - .multicast_reset_by_tag(tag) + .multicast_reset_by_tag(&make_tag(tag)) .await .expect("Should delete all groups with tag"); @@ -1003,85 +1177,6 @@ async fn test_multicast_tagged_groups_management() { assert!(remaining_ips.contains(&created3.group_ip)); } -#[tokio::test] -#[ignore] -async fn test_multicast_untagged_groups() { - let switch = &*get_switch().await; - - // First create the internal admin-scoped group that will be the NAT target - let internal_multicast_ip = IpAddr::V6(MULTICAST_NAT_IP); - create_test_multicast_group( - switch, - internal_multicast_ip, - None, // No tag for NAT target - &[(PhysPort(26), types::Direction::Underlay)], - types::InternalForwarding { nat_target: None }, - types::ExternalForwarding { vlan_id: None }, - None, - ) - .await; - - // Create a group without a tag - let group_ip = IpAddr::V4(MULTICAST_TEST_IPV4); - - // IPv4 groups are always external - create external entry directly - let external_untagged = types::MulticastGroupCreateExternalEntry { - group_ip, - tag: None, // No tag - internal_forwarding: types::InternalForwarding { - nat_target: Some(create_nat_target_ipv4()), - }, - external_forwarding: types::ExternalForwarding { vlan_id: Some(10) }, - sources: None, - }; - - let created_untagged = switch - .client - .multicast_group_create_external(&external_untagged) - .await - .expect("Should create untagged group") - .into_inner(); - - // Create a group with a tag - // IPv4 groups are always external - create external entry directly - let tagged_group = types::MulticastGroupCreateExternalEntry { - group_ip: "224.0.2.2".parse().unwrap(), // Different IP - tag: Some("some_tag".to_string()), - internal_forwarding: types::InternalForwarding { - nat_target: Some(create_nat_target_ipv4()), - }, - external_forwarding: types::ExternalForwarding { vlan_id: Some(10) }, - sources: None, - }; - - let created_tagged = switch - .client - .multicast_group_create_external(&tagged_group) - .await - .expect("Should create tagged group") - .into_inner(); - - // Delete all untagged groups - switch - .client - .multicast_reset_untagged() - .await - .expect("Should delete all untagged groups"); - - // Verify only the untagged group is gone - let remaining_groups = switch - .client - .multicast_groups_list_stream(None) - .try_collect::>() - .await - .expect("Should list remaining groups"); - - let remaining_ips: HashSet<_> = - remaining_groups.iter().map(get_group_ip).collect(); - assert!(!remaining_ips.contains(&created_untagged.group_ip)); - assert!(remaining_ips.contains(&created_tagged.group_ip)); -} - #[tokio::test] #[ignore] async fn test_api_internal_ipv6_bifurcated_replication() -> TestResult { @@ -1093,7 +1188,7 @@ async fn test_api_internal_ipv6_bifurcated_replication() -> TestResult { // Create admin-scoped IPv6 group with both external and underlay members let admin_scoped_group = types::MulticastGroupCreateUnderlayEntry { group_ip: "ff04::100".parse().unwrap(), - tag: Some("test_bifurcated".to_string()), + tag: Some(TEST_TAG.to_string()), members: vec![ types::MulticastGroupMember { port_id: port_id1.clone(), @@ -1144,7 +1239,7 @@ async fn test_api_internal_ipv6_bifurcated_replication() -> TestResult { assert_eq!(external_members.len(), 1); assert_eq!(underlay_members.len(), 1); - cleanup_test_group(switch, created.group_ip.to_ip_addr()).await + cleanup_test_group(switch, created.group_ip.to_ip_addr(), TEST_TAG).await } #[tokio::test] @@ -1154,10 +1249,10 @@ async fn test_api_internal_ipv6_underlay_only() -> TestResult { let (port_id, link_id) = switch.link_id(PhysPort(11)).unwrap(); - // Create admin-scoped IPv6 group with only underlay members + // Create admin-local IPv6 group with only underlay members let underlay_only_group = types::MulticastGroupCreateUnderlayEntry { - group_ip: "ff05::200".parse().unwrap(), - tag: Some("test_underlay_only".to_string()), + group_ip: "ff04::200".parse().unwrap(), + tag: Some(TEST_TAG.to_string()), members: vec![types::MulticastGroupMember { port_id: port_id.clone(), link_id, @@ -1169,14 +1264,14 @@ async fn test_api_internal_ipv6_underlay_only() -> TestResult { .client .multicast_group_create_underlay(&underlay_only_group) .await - .expect("Should create underlay-only admin-scoped group") + .expect("Should create underlay-only admin-local group") .into_inner(); // Verify only underlay members assert_eq!(created.members.len(), 1); assert_eq!(created.members[0].direction, types::Direction::Underlay); - cleanup_test_group(switch, created.group_ip.to_ip_addr()).await + cleanup_test_group(switch, created.group_ip.to_ip_addr(), TEST_TAG).await } #[tokio::test] @@ -1186,11 +1281,11 @@ async fn test_api_internal_ipv6_external_only() -> TestResult { let (port_id, link_id) = switch.link_id(PhysPort(11)).unwrap(); - // Create admin-scoped IPv6 group with only external members + // Create admin-local IPv6 group with only external members let external_members_only_group = types::MulticastGroupCreateUnderlayEntry { - group_ip: "ff08::300".parse().unwrap(), - tag: Some("test_external_members_only".to_string()), + group_ip: "ff04::300".parse().unwrap(), + tag: Some(TEST_TAG.to_string()), members: vec![types::MulticastGroupMember { port_id: port_id.clone(), link_id, @@ -1209,7 +1304,7 @@ async fn test_api_internal_ipv6_external_only() -> TestResult { assert_eq!(created.members.len(), 1); assert_eq!(created.members[0].direction, types::Direction::External); - cleanup_test_group(switch, created.group_ip.to_ip_addr()).await + cleanup_test_group(switch, created.group_ip.to_ip_addr(), TEST_TAG).await } #[tokio::test] @@ -1222,7 +1317,7 @@ async fn test_api_invalid_combinations() -> TestResult { create_test_multicast_group( switch, internal_multicast_ip, - Some("nat_target_for_invalid_combos"), + Some(TEST_TAG), &[(PhysPort(26), types::Direction::Underlay)], types::InternalForwarding { nat_target: None }, types::ExternalForwarding { vlan_id: None }, @@ -1233,7 +1328,7 @@ async fn test_api_invalid_combinations() -> TestResult { // IPv4 with underlay members should fail let ipv4_with_underlay = types::MulticastGroupCreateExternalEntry { group_ip: IpAddr::V4("224.1.0.200".parse().unwrap()), // Avoid 224.0.0.0/24 reserved range - tag: Some("test_invalid_ipv4".to_string()), + tag: Some(TEST_TAG.to_string()), internal_forwarding: types::InternalForwarding { nat_target: Some(create_nat_target_ipv4()), }, @@ -1252,7 +1347,7 @@ async fn test_api_invalid_combinations() -> TestResult { // Non-admin-scoped IPv6 should use external API let non_admin_ipv6 = types::MulticastGroupCreateExternalEntry { group_ip: "ff0e::400".parse().unwrap(), // Global scope, not admin-scoped - tag: Some("test_non_admin_ipv6".to_string()), + tag: Some(TEST_TAG.to_string()), internal_forwarding: types::InternalForwarding { nat_target: Some(create_nat_target_ipv6()), }, @@ -1271,7 +1366,7 @@ async fn test_api_invalid_combinations() -> TestResult { let admin_scoped_external_entry = types::MulticastGroupCreateExternalEntry { group_ip: "ff04::500".parse().unwrap(), // Admin-scoped - tag: Some("test_admin_external".to_string()), + tag: Some(TEST_TAG.to_string()), internal_forwarding: types::InternalForwarding { nat_target: Some(create_nat_target_ipv6()), }, @@ -1292,20 +1387,20 @@ async fn test_api_invalid_combinations() -> TestResult { match result { Error::ErrorResponse(inner) => { assert_eq!(inner.status(), 400); - assert!(inner.message.contains("admin-scoped multicast address")); + assert!(inner.message.contains("admin-local scope")); } _ => panic!( - "Expected ErrorResponse for admin-scoped external group creation" + "Expected ErrorResponse for admin-local external group creation" ), } - cleanup_test_group(switch, created_ipv4.group_ip) + cleanup_test_group(switch, created_ipv4.group_ip, TEST_TAG) .await .unwrap(); - cleanup_test_group(switch, created_non_admin.group_ip) + cleanup_test_group(switch, created_non_admin.group_ip, TEST_TAG) .await .unwrap(); - cleanup_test_group(switch, internal_multicast_ip).await + cleanup_test_group(switch, internal_multicast_ip, TEST_TAG).await } #[tokio::test] @@ -1324,7 +1419,7 @@ async fn test_ipv4_multicast_invalid_destination_mac() -> TestResult { create_test_multicast_group( switch, internal_multicast_ip, - Some("test_invalid_mac_underlay"), + Some(TEST_TAG), &[(egress1, types::Direction::Underlay)], types::InternalForwarding { nat_target: None }, types::ExternalForwarding { vlan_id: None }, @@ -1339,7 +1434,7 @@ async fn test_ipv4_multicast_invalid_destination_mac() -> TestResult { let created_group = create_test_multicast_group( switch, multicast_ip, - Some("test_invalid_mac"), + Some(TEST_TAG), &[], // External groups have no members types::InternalForwarding { nat_target: Some(create_nat_target_ipv4()), @@ -1403,10 +1498,10 @@ async fn test_ipv4_multicast_invalid_destination_mac() -> TestResult { .unwrap(); // Cleanup: Remove both external IPv4 group and underlay IPv6 group - cleanup_test_group(switch, get_group_ip(&created_group)) + cleanup_test_group(switch, get_group_ip(&created_group), TEST_TAG) .await .unwrap(); - cleanup_test_group(switch, internal_multicast_ip).await + cleanup_test_group(switch, internal_multicast_ip, TEST_TAG).await } #[tokio::test] @@ -1424,7 +1519,7 @@ async fn test_ipv6_multicast_invalid_destination_mac() -> TestResult { let created_group = create_test_multicast_group( switch, multicast_ip, - Some("test_ipv6_invalid_mac"), + Some(TEST_TAG), &[(egress1, types::Direction::External)], types::InternalForwarding { nat_target: None }, types::ExternalForwarding { vlan_id: None }, @@ -1497,7 +1592,7 @@ async fn test_ipv6_multicast_invalid_destination_mac() -> TestResult { .await .unwrap(); - cleanup_test_group(switch, get_group_ip(&created_group)).await + cleanup_test_group(switch, get_group_ip(&created_group), TEST_TAG).await } #[tokio::test] @@ -1514,7 +1609,7 @@ async fn test_multicast_ttl_zero() -> TestResult { create_test_multicast_group( switch, internal_multicast_ip, - Some("nat_target_for_ttl"), + Some(TEST_TAG), &[(egress1, types::Direction::Underlay)], types::InternalForwarding { nat_target: None }, types::ExternalForwarding { vlan_id: None }, @@ -1528,7 +1623,7 @@ async fn test_multicast_ttl_zero() -> TestResult { let created_group = create_test_multicast_group( switch, multicast_ip, - Some("test_ttl_drop"), + Some(TEST_TAG), &[], // External groups have no members types::InternalForwarding { nat_target: Some(create_nat_target_ipv4()), @@ -1578,10 +1673,10 @@ async fn test_multicast_ttl_zero() -> TestResult { .await .unwrap(); - cleanup_test_group(switch, get_group_ip(&created_group)) + cleanup_test_group(switch, get_group_ip(&created_group), TEST_TAG) .await .unwrap(); - cleanup_test_group(switch, internal_multicast_ip).await + cleanup_test_group(switch, internal_multicast_ip, TEST_TAG).await } #[tokio::test] @@ -1598,7 +1693,7 @@ async fn test_multicast_ttl_one() -> TestResult { create_test_multicast_group( switch, internal_multicast_ip, - Some("nat_target_for_ttl_one"), + Some(TEST_TAG), &[(egress1, types::Direction::Underlay)], types::InternalForwarding { nat_target: None }, types::ExternalForwarding { vlan_id: None }, @@ -1612,7 +1707,7 @@ async fn test_multicast_ttl_one() -> TestResult { let created_group = create_test_multicast_group( switch, multicast_ip, - Some("test_ttl_one_drop"), + Some(TEST_TAG), &[], // External groups have no members types::InternalForwarding { nat_target: Some(create_nat_target_ipv4()), @@ -1662,10 +1757,10 @@ async fn test_multicast_ttl_one() -> TestResult { .await .unwrap(); - cleanup_test_group(switch, get_group_ip(&created_group)) + cleanup_test_group(switch, get_group_ip(&created_group), TEST_TAG) .await .unwrap(); - cleanup_test_group(switch, internal_multicast_ip).await + cleanup_test_group(switch, internal_multicast_ip, TEST_TAG).await } #[tokio::test] @@ -1690,7 +1785,7 @@ async fn test_ipv4_multicast_basic_replication_nat_ingress() -> TestResult { create_test_multicast_group( switch, internal_multicast_ip, - Some("test_replication_internal"), + Some(TEST_TAG), &underlay_members, types::InternalForwarding { nat_target: None }, types::ExternalForwarding { vlan_id: None }, @@ -1710,7 +1805,7 @@ async fn test_ipv4_multicast_basic_replication_nat_ingress() -> TestResult { let created_group = create_test_multicast_group( switch, multicast_ip, - Some("test_ipv4_replication"), + Some(TEST_TAG), &external_members, types::InternalForwarding { nat_target: Some(create_nat_target_ipv4()), @@ -1800,7 +1895,11 @@ async fn test_ipv4_multicast_basic_replication_nat_ingress() -> TestResult { .await .unwrap(); - cleanup_test_group(switch, get_group_ip(&created_group)).await + // Cleanup external first, then internal + cleanup_test_group(switch, get_group_ip(&created_group), TEST_TAG) + .await + .unwrap(); + cleanup_test_group(switch, internal_multicast_ip, TEST_TAG).await } #[tokio::test] @@ -1824,7 +1923,7 @@ async fn test_encapped_multicast_geneve_mcast_tag_to_external_members() create_test_multicast_group( switch, internal_multicast_ip, - Some("test_geneve_mcast_tag_underlay"), + Some(TEST_TAG), &replication_members, types::InternalForwarding { nat_target: None }, types::ExternalForwarding { vlan_id: None }, // Admin-scoped groups don't need NAT targets @@ -1839,7 +1938,7 @@ async fn test_encapped_multicast_geneve_mcast_tag_to_external_members() let created_group = create_test_multicast_group( switch, multicast_ip, - Some("test_geneve_mcast_tag_0"), + Some(TEST_TAG), &[], // External groups have no members types::InternalForwarding { nat_target: Some(create_nat_target_ipv4()), @@ -1939,10 +2038,10 @@ async fn test_encapped_multicast_geneve_mcast_tag_to_external_members() .await .unwrap(); - cleanup_test_group(switch, get_group_ip(&created_group)) + cleanup_test_group(switch, get_group_ip(&created_group), TEST_TAG) .await .unwrap(); - cleanup_test_group(switch, MULTICAST_NAT_IP.into()).await + cleanup_test_group(switch, MULTICAST_NAT_IP.into(), TEST_TAG).await } #[tokio::test] @@ -1961,7 +2060,7 @@ async fn test_encapped_multicast_geneve_mcast_tag_to_underlay_members() create_test_multicast_group( switch, internal_multicast_ip, - Some("test_geneve_mcast_tag_underlay"), + Some(TEST_TAG), &[ (egress3, types::Direction::Underlay), (egress4, types::Direction::Underlay), @@ -1978,7 +2077,7 @@ async fn test_encapped_multicast_geneve_mcast_tag_to_underlay_members() let created_group = create_test_multicast_group( switch, multicast_ip, - Some("test_geneve_mcast_tag_1"), + Some(TEST_TAG), &[], // External groups have no members types::InternalForwarding { nat_target: Some(create_nat_target_ipv4()), @@ -2078,10 +2177,10 @@ async fn test_encapped_multicast_geneve_mcast_tag_to_underlay_members() .await .unwrap(); - cleanup_test_group(switch, get_group_ip(&created_group)) + cleanup_test_group(switch, get_group_ip(&created_group), TEST_TAG) .await .unwrap(); - cleanup_test_group(switch, MULTICAST_NAT_IP.into()).await + cleanup_test_group(switch, MULTICAST_NAT_IP.into(), TEST_TAG).await } #[tokio::test] @@ -2103,7 +2202,7 @@ async fn test_encapped_multicast_geneve_mcast_tag_to_underlay_and_external_membe create_test_multicast_group( switch, internal_multicast_ip, - Some("test_geneve_mcast_tag_bifurcated"), + Some(TEST_TAG), &[ (egress1, types::Direction::External), (egress2, types::Direction::External), @@ -2123,7 +2222,7 @@ async fn test_encapped_multicast_geneve_mcast_tag_to_underlay_and_external_membe let created_group = create_test_multicast_group( switch, multicast_ip, - Some("test_geneve_mcast_tag_1"), + Some(TEST_TAG), &[], // External groups have no members types::InternalForwarding { nat_target: Some(create_nat_target_ipv4()), @@ -2236,10 +2335,10 @@ async fn test_encapped_multicast_geneve_mcast_tag_to_underlay_and_external_membe .await .unwrap(); - cleanup_test_group(switch, get_group_ip(&created_group)) + cleanup_test_group(switch, get_group_ip(&created_group), TEST_TAG) .await .unwrap(); - cleanup_test_group(switch, MULTICAST_NAT_IP.into()).await + cleanup_test_group(switch, MULTICAST_NAT_IP.into(), TEST_TAG).await } #[tokio::test] @@ -2255,7 +2354,7 @@ async fn test_ipv4_multicast_drops_ingress_is_egress_port() -> TestResult { create_test_multicast_group( switch, internal_multicast_ip, - Some("test_drops_underlay"), + Some(TEST_TAG), &[(ingress, types::Direction::Underlay)], types::InternalForwarding { nat_target: None }, types::ExternalForwarding { vlan_id: None }, // No NAT target for admin-scoped group @@ -2269,7 +2368,7 @@ async fn test_ipv4_multicast_drops_ingress_is_egress_port() -> TestResult { let created_group = create_test_multicast_group( switch, multicast_ip, - Some("test_replication"), + Some(TEST_TAG), &[], // External groups have no members types::InternalForwarding { nat_target: Some(create_nat_target_ipv4()), @@ -2318,10 +2417,10 @@ async fn test_ipv4_multicast_drops_ingress_is_egress_port() -> TestResult { .await .unwrap(); - cleanup_test_group(switch, get_group_ip(&created_group)) + cleanup_test_group(switch, get_group_ip(&created_group), TEST_TAG) .await .unwrap(); - cleanup_test_group(switch, internal_multicast_ip).await + cleanup_test_group(switch, internal_multicast_ip, TEST_TAG).await } #[tokio::test] @@ -2339,7 +2438,7 @@ async fn test_ipv6_multicast_hop_limit_zero() -> TestResult { create_test_multicast_group( switch, internal_multicast_ip, - Some("test_ipv6_hop_limit_underlay"), + Some(TEST_TAG), &[ (egress1, types::Direction::External), (egress2, types::Direction::External), @@ -2356,7 +2455,7 @@ async fn test_ipv6_multicast_hop_limit_zero() -> TestResult { let created_group = create_test_multicast_group( switch, multicast_ip, - Some("test_ipv6_hop_limit_zero"), + Some(TEST_TAG), &[], // External groups have no members types::InternalForwarding { nat_target: Some(create_nat_target_ipv4()), @@ -2392,10 +2491,6 @@ async fn test_ipv6_multicast_hop_limit_zero() -> TestResult { switch.packet_test(vec![test_pkt], expected_pkts).unwrap(); - cleanup_test_group(switch, get_group_ip(&created_group)) - .await - .unwrap(); - check_counter_incremented( switch, "ipv6_ttl_invalid", @@ -2406,7 +2501,10 @@ async fn test_ipv6_multicast_hop_limit_zero() -> TestResult { .await .unwrap(); - Ok(()) + cleanup_test_group(switch, get_group_ip(&created_group), TEST_TAG) + .await + .unwrap(); + cleanup_test_group(switch, internal_multicast_ip, TEST_TAG).await } #[tokio::test] @@ -2424,7 +2522,7 @@ async fn test_ipv6_multicast_hop_limit_one() -> TestResult { create_test_multicast_group( switch, internal_multicast_ip, - Some("test_ipv6_hop_limit_one_underlay"), + Some(TEST_TAG), &[ (egress1, types::Direction::External), (egress2, types::Direction::External), @@ -2441,7 +2539,7 @@ async fn test_ipv6_multicast_hop_limit_one() -> TestResult { let created_group = create_test_multicast_group( switch, multicast_ip, - Some("test_ipv6_hop_limit_one"), + Some(TEST_TAG), &[], // External groups have no members types::InternalForwarding { nat_target: Some(create_nat_target_ipv4()), @@ -2491,7 +2589,10 @@ async fn test_ipv6_multicast_hop_limit_one() -> TestResult { .await .unwrap(); - cleanup_test_group(switch, get_group_ip(&created_group)).await + cleanup_test_group(switch, get_group_ip(&created_group), TEST_TAG) + .await + .unwrap(); + cleanup_test_group(switch, internal_multicast_ip, TEST_TAG).await } #[tokio::test] @@ -2510,7 +2611,7 @@ async fn test_ipv6_multicast_basic_replication_nat_ingress() -> TestResult { create_test_multicast_group( switch, internal_multicast_ip, - Some("test_replication_internal"), + Some(TEST_TAG), &underlay_members, types::InternalForwarding { nat_target: None }, types::ExternalForwarding { vlan_id: None }, // Admin-scoped groups don't need NAT targets @@ -2525,7 +2626,7 @@ async fn test_ipv6_multicast_basic_replication_nat_ingress() -> TestResult { let created_group = create_test_multicast_group( switch, multicast_ip, - Some("test_ipv6_replication"), + Some(TEST_TAG), &[], // External groups have no members types::InternalForwarding { nat_target: Some(create_nat_target_ipv4()), @@ -2592,7 +2693,10 @@ async fn test_ipv6_multicast_basic_replication_nat_ingress() -> TestResult { .await .unwrap(); - cleanup_test_group(switch, get_group_ip(&created_group)).await + cleanup_test_group(switch, get_group_ip(&created_group), TEST_TAG) + .await + .unwrap(); + cleanup_test_group(switch, internal_multicast_ip, TEST_TAG).await } #[tokio::test] @@ -2612,7 +2716,7 @@ async fn test_ipv4_multicast_source_filtering_exact_match() -> TestResult { create_test_multicast_group( switch, internal_multicast_ip, - Some("test_source_filtering_underlay"), + Some(TEST_TAG), &[ (egress1, types::Direction::External), (egress2, types::Direction::External), @@ -2625,14 +2729,14 @@ async fn test_ipv4_multicast_source_filtering_exact_match() -> TestResult { // Create IPv4 SSM external group with source filtering and NAT target (no members) let multicast_ip = IpAddr::V4(MULTICAST_TEST_IPV4_SSM); - let allowed_src_ip = "192.168.1.5".parse().unwrap(); + let allowed_src_ip: IpAddr = "192.168.1.5".parse().unwrap(); let filtered_src_ip: IpAddr = "192.168.1.6".parse().unwrap(); let allowed_src = types::IpSrc::Exact(allowed_src_ip); let created_group = create_test_multicast_group( switch, multicast_ip, - Some("test_source_filtering"), + Some(TEST_TAG), &[], // External groups have no members types::InternalForwarding { nat_target: Some(create_nat_target_ipv4()), @@ -2718,15 +2822,15 @@ async fn test_ipv4_multicast_source_filtering_exact_match() -> TestResult { .await .unwrap(); - cleanup_test_group(switch, get_group_ip(&created_group)) + cleanup_test_group(switch, get_group_ip(&created_group), TEST_TAG) .await .unwrap(); - cleanup_test_group(switch, internal_multicast_ip).await + cleanup_test_group(switch, internal_multicast_ip, TEST_TAG).await } #[tokio::test] #[ignore] -async fn test_ipv4_multicast_source_filtering_prefix_match() -> TestResult { +async fn test_ipv4_multicast_source_filtering_multiple_exact() -> TestResult { let switch = &*get_switch().await; // Define test ports @@ -2741,7 +2845,7 @@ async fn test_ipv4_multicast_source_filtering_prefix_match() -> TestResult { create_test_multicast_group( switch, internal_multicast_ip, - Some("test_source_filtering_prefix_underlay"), + Some(TEST_TAG), &[ (egress1, types::Direction::External), (egress2, types::Direction::External), @@ -2755,23 +2859,26 @@ async fn test_ipv4_multicast_source_filtering_prefix_match() -> TestResult { // Create multicast group with two egress ports and source filtering let multicast_ip = IpAddr::V4(MULTICAST_TEST_IPV4_SSM); - let allowed_src_ip1 = "192.168.1.5".parse().unwrap(); + let allowed_src_ip1: IpAddr = "192.168.1.5".parse().unwrap(); let allowed_src_ip2: IpAddr = "192.168.1.10".parse().unwrap(); let filtered_src_ip: IpAddr = "10.0.0.5".parse().unwrap(); - let allowed_src = - types::IpSrc::Subnet(Ipv4Net::new(allowed_src_ip1, 24).unwrap()); + // Allow both source IPs explicitly + let allowed_sources = vec![ + types::IpSrc::Exact(allowed_src_ip1), + types::IpSrc::Exact(allowed_src_ip2), + ]; let created_group = create_test_multicast_group( switch, multicast_ip, - Some("test_source_filtering"), + Some(TEST_TAG), &[], // External groups have no members types::InternalForwarding { nat_target: Some(create_nat_target_ipv4()), }, types::ExternalForwarding { vlan_id: Some(10) }, - Some(vec![allowed_src]), + Some(allowed_sources), ) .await; @@ -2887,10 +2994,10 @@ async fn test_ipv4_multicast_source_filtering_prefix_match() -> TestResult { .await .unwrap(); - cleanup_test_group(switch, get_group_ip(&created_group)) + cleanup_test_group(switch, get_group_ip(&created_group), TEST_TAG) .await .unwrap(); - cleanup_test_group(switch, internal_multicast_ip).await + cleanup_test_group(switch, internal_multicast_ip, TEST_TAG).await } #[tokio::test] @@ -2910,7 +3017,7 @@ async fn test_ipv6_multicast_multiple_source_filtering() -> TestResult { create_test_multicast_group( switch, internal_multicast_ip, - Some("test_ipv6_source_filtering_underlay"), + Some(TEST_TAG), &[ (egress1, types::Direction::External), (egress2, types::Direction::External), @@ -2936,7 +3043,7 @@ async fn test_ipv6_multicast_multiple_source_filtering() -> TestResult { let created_group = create_test_multicast_group( switch, multicast_ip, - Some("test_ipv6_source_filtering"), + Some(TEST_TAG), &[], // External groups have no members types::InternalForwarding { nat_target: Some(create_nat_target_ipv4()), @@ -3061,10 +3168,10 @@ async fn test_ipv6_multicast_multiple_source_filtering() -> TestResult { .await .unwrap(); - cleanup_test_group(switch, get_group_ip(&created_group)) + cleanup_test_group(switch, get_group_ip(&created_group), TEST_TAG) .await .unwrap(); - cleanup_test_group(switch, internal_multicast_ip).await + cleanup_test_group(switch, internal_multicast_ip, TEST_TAG).await } #[tokio::test] @@ -3083,7 +3190,7 @@ async fn test_multicast_dynamic_membership() -> TestResult { create_test_multicast_group( switch, internal_multicast_ip, - Some("test_dynamic_membership_internal"), + Some(TEST_TAG), &[ (egress1, types::Direction::External), (egress2, types::Direction::External), @@ -3101,7 +3208,7 @@ async fn test_multicast_dynamic_membership() -> TestResult { let created_group = create_test_multicast_group( switch, multicast_ip, - Some("test_dynamic_membership"), + Some(TEST_TAG), &[], // External groups have no members types::InternalForwarding { nat_target: Some(create_nat_target_ipv4()), @@ -3157,9 +3264,9 @@ async fn test_multicast_dynamic_membership() -> TestResult { assert!(result1.is_ok(), "Initial test failed: {:?}", result1); // Now update the external group - external groups don't have members to update, - // but we can update their NAT target, tag, vlan, and sources + // but we can update their NAT target, vlan, and sources. + // Must pass same tag for validation. let external_update_entry = types::MulticastGroupUpdateExternalEntry { - tag: None, internal_forwarding: types::InternalForwarding { nat_target: Some(create_nat_target_ipv4()), }, // Keep the same NAT target @@ -3171,6 +3278,7 @@ async fn test_multicast_dynamic_membership() -> TestResult { .client .multicast_group_update_external( &get_group_ip(&created_group), + &make_tag(TEST_TAG), &external_update_entry, ) .await @@ -3180,8 +3288,8 @@ async fn test_multicast_dynamic_membership() -> TestResult { let (port_id2, link_id2) = switch.link_id(egress2).unwrap(); let (port_id3, link_id3) = switch.link_id(egress3).unwrap(); + // Must pass same tag for validation. let internal_update_entry = types::MulticastGroupUpdateUnderlayEntry { - tag: None, members: vec![ types::MulticastGroupMember { port_id: port_id2, @@ -3204,7 +3312,8 @@ async fn test_multicast_dynamic_membership() -> TestResult { switch .client .multicast_group_update_underlay( - &types::AdminScopedIpv6(ipv6), + &types::UnderlayMulticastIpv6(ipv6), + &make_tag(TEST_TAG), &internal_update_entry, ) .await @@ -3246,10 +3355,10 @@ async fn test_multicast_dynamic_membership() -> TestResult { .packet_test(vec![test_pkt_new], expected_pkts_new) .unwrap(); - cleanup_test_group(switch, get_group_ip(&created_group)) + cleanup_test_group(switch, get_group_ip(&created_group), TEST_TAG) .await .unwrap(); - cleanup_test_group(switch, internal_multicast_ip).await + cleanup_test_group(switch, internal_multicast_ip, TEST_TAG).await } #[tokio::test] @@ -3269,7 +3378,7 @@ async fn test_multicast_multiple_groups() -> TestResult { create_test_multicast_group( switch, internal_multicast_ip, - Some("test_multi_group_underlay"), + Some(TEST_TAG), &[ (egress1, types::Direction::External), (egress2, types::Direction::External), @@ -3289,7 +3398,7 @@ async fn test_multicast_multiple_groups() -> TestResult { let created_group1 = create_test_multicast_group( switch, multicast_ip1, - Some("test_multi_group_1"), + Some(TEST_TAG), &[], // External groups have no members types::InternalForwarding { nat_target: Some(create_nat_target_ipv4()), @@ -3306,7 +3415,7 @@ async fn test_multicast_multiple_groups() -> TestResult { let created_group2 = create_test_multicast_group( switch, multicast_ip2, - Some("test_multi_group_2"), + Some(TEST_TAG), &[], // External groups have no members types::InternalForwarding { nat_target: Some(create_nat_target_ipv4()), @@ -3449,13 +3558,13 @@ async fn test_multicast_multiple_groups() -> TestResult { switch.packet_test(test_pkts, expected_pkts).unwrap(); - cleanup_test_group(switch, get_group_ip(&created_group1)) + cleanup_test_group(switch, get_group_ip(&created_group1), TEST_TAG) .await .unwrap(); - cleanup_test_group(switch, get_group_ip(&created_group2)) + cleanup_test_group(switch, get_group_ip(&created_group2), TEST_TAG) .await .unwrap(); - cleanup_test_group(switch, internal_multicast_ip).await + cleanup_test_group(switch, internal_multicast_ip, TEST_TAG).await } #[tokio::test] @@ -3474,7 +3583,7 @@ async fn test_multicast_reset_all_tables() -> TestResult { create_test_multicast_group( switch, internal_multicast_ip, - Some("test_reset_all_underlay"), + Some(TEST_TAG), &[ (egress1, types::Direction::External), (egress2, types::Direction::External), @@ -3492,7 +3601,7 @@ async fn test_multicast_reset_all_tables() -> TestResult { let created_group1 = create_test_multicast_group( switch, multicast_ip1, - Some("test_reset_all_1"), + Some(TEST_TAG), &[], // External groups have no members types::InternalForwarding { nat_target: Some(create_nat_target_ipv4()), @@ -3509,7 +3618,7 @@ async fn test_multicast_reset_all_tables() -> TestResult { let created_group2 = create_test_multicast_group( switch, multicast_ip2, - Some("test_reset_all_2"), + Some(TEST_TAG), &[], // External groups have no members types::InternalForwarding { nat_target: Some(create_nat_target_ipv6()), @@ -3519,12 +3628,12 @@ async fn test_multicast_reset_all_tables() -> TestResult { ) .await; - // 2b. Admin-scoped IPv6 group to test internal API with custom replication parameters - let ipv6 = Ipv6Addr::new(0xff04, 0, 0, 0, 0, 0, 0, 2); + // 2b. Admin-local IPv6 group to test internal API with custom replication parameters + let ipv6 = Ipv6Addr::new(ADMIN_LOCAL_PREFIX, 0, 0, 0, 0, 0, 0, 2); let group_entry2b = types::MulticastGroupCreateUnderlayEntry { - group_ip: types::AdminScopedIpv6(ipv6), - tag: Some("test_reset_all_2b".to_string()), + group_ip: types::UnderlayMulticastIpv6(ipv6), + tag: Some(TEST_TAG.to_string()), members: vec![types::MulticastGroupMember { port_id: switch.link_id(egress1).unwrap().0, link_id: switch.link_id(egress1).unwrap().1, @@ -3544,15 +3653,13 @@ async fn test_multicast_reset_all_tables() -> TestResult { let vlan3 = Some(30); let sources = Some(vec![ types::IpSrc::Exact("192.168.1.5".parse().unwrap()), - types::IpSrc::Subnet( - Ipv4Net::new("192.168.2.0".parse().unwrap(), 24).unwrap(), - ), + types::IpSrc::Exact("192.168.2.1".parse().unwrap()), ]); let created_group3 = create_test_multicast_group( switch, multicast_ip3, - Some("test_reset_all_3"), + Some(TEST_TAG), &[], // External groups have no members types::InternalForwarding { nat_target: Some(create_nat_target_ipv4()), @@ -3571,7 +3678,7 @@ async fn test_multicast_reset_all_tables() -> TestResult { let created_group4 = create_test_multicast_group( switch, multicast_ip4, - Some("test_reset_all_4"), + Some(TEST_TAG), &[], // External groups have no members types::InternalForwarding { nat_target: Some(create_nat_target_ipv6()), @@ -3801,7 +3908,7 @@ async fn test_multicast_vlan_translation_not_possible() -> TestResult { create_test_multicast_group( switch, internal_multicast_ip, - Some("test_vlan_underlay"), + Some(TEST_TAG), &[(egress1, types::Direction::External)], types::InternalForwarding { nat_target: None }, types::ExternalForwarding { vlan_id: None }, // Admin-scoped groups don't need NAT targets @@ -3816,7 +3923,7 @@ async fn test_multicast_vlan_translation_not_possible() -> TestResult { let created_group = create_test_multicast_group( switch, multicast_ip, - Some("test_vlan_behavior"), + Some(TEST_TAG), &[], // External groups have no members types::InternalForwarding { nat_target: Some(create_nat_target_ipv4()), @@ -3858,10 +3965,10 @@ async fn test_multicast_vlan_translation_not_possible() -> TestResult { switch.packet_test(vec![test_pkt], expected_pkts).unwrap(); - cleanup_test_group(switch, get_group_ip(&created_group)) + cleanup_test_group(switch, get_group_ip(&created_group), TEST_TAG) .await .unwrap(); - cleanup_test_group(switch, internal_multicast_ip).await + cleanup_test_group(switch, internal_multicast_ip, TEST_TAG).await } */ @@ -3881,7 +3988,7 @@ async fn test_multicast_multiple_packets() -> TestResult { create_test_multicast_group( switch, internal_multicast_ip, - Some("test_performance_underlay"), + Some(TEST_TAG), &[ (egress1, types::Direction::Underlay), (egress2, types::Direction::Underlay), @@ -3900,7 +4007,7 @@ async fn test_multicast_multiple_packets() -> TestResult { let created_group = create_test_multicast_group( switch, multicast_ip, - Some("test_performance"), + Some(TEST_TAG), &[], // External groups have no members types::InternalForwarding { nat_target: Some(create_nat_target_ipv4()), @@ -3993,10 +4100,10 @@ async fn test_multicast_multiple_packets() -> TestResult { .await .unwrap(); - cleanup_test_group(switch, get_group_ip(&created_group)) + cleanup_test_group(switch, get_group_ip(&created_group), TEST_TAG) .await .unwrap(); - cleanup_test_group(switch, internal_multicast_ip).await + cleanup_test_group(switch, internal_multicast_ip, TEST_TAG).await } #[tokio::test] @@ -4106,7 +4213,7 @@ async fn test_external_group_nat_target_validation() -> TestResult { let group_with_invalid_nat = types::MulticastGroupCreateExternalEntry { group_ip: IpAddr::V4("224.1.0.101".parse().unwrap()), - tag: Some("test_invalid_nat".to_string()), + tag: Some(TEST_TAG.to_string()), internal_forwarding: types::InternalForwarding { nat_target: Some(nonexistent_nat_target.clone()), }, @@ -4130,7 +4237,7 @@ async fn test_external_group_nat_target_validation() -> TestResult { // Create admin-scoped IPv6 group first, then external group with valid NAT target let admin_scoped_group = types::MulticastGroupCreateUnderlayEntry { group_ip: "ff04::1".parse().unwrap(), - tag: Some("test_admin_scoped".to_string()), + tag: Some(TEST_TAG.to_string()), members: vec![types::MulticastGroupMember { port_id: port_id.clone(), link_id, @@ -4168,7 +4275,7 @@ async fn test_external_group_nat_target_validation() -> TestResult { let group_with_valid_nat = types::MulticastGroupCreateExternalEntry { group_ip: IpAddr::V4("224.1.0.102".parse().unwrap()), - tag: Some("test_valid_nat".to_string()), + tag: Some(TEST_TAG.to_string()), internal_forwarding: types::InternalForwarding { nat_target: Some(valid_nat_target.clone()), }, @@ -4201,10 +4308,12 @@ async fn test_external_group_nat_target_validation() -> TestResult { "External group's NAT target should point to the correct internal IP" ); - cleanup_test_group(switch, created_admin.group_ip.to_ip_addr()) + // Delete external group first since it references the internal group via NAT target + cleanup_test_group(switch, created_external.group_ip, TEST_TAG) .await .unwrap(); - cleanup_test_group(switch, created_external.group_ip).await + cleanup_test_group(switch, created_admin.group_ip.to_ip_addr(), TEST_TAG) + .await } #[tokio::test] @@ -4218,7 +4327,7 @@ async fn test_ipv6_multicast_scope_validation() { // Admin-local scope (ff04::/16) - should work with internal API let admin_local_group = types::MulticastGroupCreateUnderlayEntry { group_ip: "ff04::100".parse().unwrap(), - tag: Some("test_admin_local".to_string()), + tag: Some(TEST_TAG.to_string()), members: vec![types::MulticastGroupMember { port_id: egress_port.clone(), link_id: egress_link, @@ -4235,10 +4344,10 @@ async fn test_ipv6_multicast_scope_validation() { "Admin-local scope (ff04::/16) should work with internal API" ); - // Site-local scope (ff05::/16) - should work with internal API + // Site-local scope (ff05::/16) - should be rejected (only admin-local ff04 allowed) let site_local_group = types::MulticastGroupCreateUnderlayEntry { group_ip: "ff05::200".parse().unwrap(), - tag: Some("test_site_local".to_string()), + tag: Some(TEST_TAG.to_string()), members: vec![types::MulticastGroupMember { port_id: egress_port.clone(), link_id: egress_link, @@ -4251,14 +4360,14 @@ async fn test_ipv6_multicast_scope_validation() { .multicast_group_create_underlay(&site_local_group) .await; assert!( - site_local_result.is_ok(), - "Site-local scope (ff05::/16) should work with internal API" + site_local_result.is_err(), + "Site-local scope (ff05::/16) should be rejected - only admin-local (ff04) allowed" ); - // Organization-local scope (ff08::/16) - should work with internal API + // Organization-local scope (ff08::/16) - should be rejected (only admin-local ff04 allowed) let org_local_group = types::MulticastGroupCreateUnderlayEntry { group_ip: "ff08::300".parse().unwrap(), - tag: Some("test_org_local".to_string()), + tag: Some(TEST_TAG.to_string()), members: vec![types::MulticastGroupMember { port_id: egress_port.clone(), link_id: egress_link, @@ -4271,14 +4380,14 @@ async fn test_ipv6_multicast_scope_validation() { .multicast_group_create_underlay(&org_local_group) .await; assert!( - org_local_result.is_ok(), - "Organization-local scope (ff08::/16) should work with internal API" + org_local_result.is_err(), + "Organization-local scope (ff08::/16) should be rejected - only admin-local (ff04) allowed" ); // Global scope (ff0e::/16) - should be rejected by server-side validation let global_scope_group = types::MulticastGroupCreateUnderlayEntry { group_ip: "ff0e::400".parse().unwrap(), - tag: Some("test_global".to_string()), + tag: Some(TEST_TAG.to_string()), members: vec![types::MulticastGroupMember { port_id: egress_port.clone(), link_id: egress_link, @@ -4299,7 +4408,7 @@ async fn test_ipv6_multicast_scope_validation() { // First create an admin-scoped group to reference let admin_target_group = types::MulticastGroupCreateUnderlayEntry { group_ip: "ff04::1000".parse().unwrap(), - tag: Some("test_target".to_string()), + tag: Some(TEST_TAG.to_string()), members: vec![types::MulticastGroupMember { port_id: egress_port.clone(), link_id: egress_link, @@ -4315,7 +4424,7 @@ async fn test_ipv6_multicast_scope_validation() { let admin_scoped_external = types::MulticastGroupCreateExternalEntry { group_ip: "ff04::500".parse().unwrap(), - tag: Some("test_admin_external".to_string()), + tag: Some(TEST_TAG.to_string()), internal_forwarding: types::InternalForwarding { nat_target: Some(types::NatTarget { internal_ip: "ff04::1000".parse().unwrap(), @@ -4339,34 +4448,28 @@ async fn test_ipv6_multicast_scope_validation() { let external_error_msg = format!("{:?}", admin_external_result.unwrap_err()); assert!( - external_error_msg.contains("admin-scoped multicast address"), - "Error should indicate admin-scoped addresses require internal API" + external_error_msg.contains("admin-local scope"), + "Error should indicate admin-local addresses require internal API" ); // Cleanup all created groups let admin_local_group = admin_local_result.unwrap().into_inner(); - let site_local_group = site_local_result.unwrap().into_inner(); - let org_local_group = org_local_result.unwrap().into_inner(); let target_group = target_result.into_inner(); + let del_tag = make_tag(TEST_TAG); switch .client - .multicast_group_delete(&admin_local_group.group_ip.to_ip_addr()) - .await - .ok(); - switch - .client - .multicast_group_delete(&site_local_group.group_ip.to_ip_addr()) - .await - .ok(); - switch - .client - .multicast_group_delete(&org_local_group.group_ip.to_ip_addr()) + .multicast_group_delete( + &admin_local_group.group_ip.to_ip_addr(), + &del_tag, + ) .await .ok(); + + let del_tag = make_tag(TEST_TAG); switch .client - .multicast_group_delete(&target_group.group_ip.to_ip_addr()) + .multicast_group_delete(&target_group.group_ip.to_ip_addr(), &del_tag) .await .ok(); } @@ -4377,15 +4480,18 @@ async fn test_multicast_group_id_recycling() -> TestResult { let switch = &*get_switch().await; // Use admin-scoped IPv6 addresses that get group IDs assigned - let group1_ip = IpAddr::V6(Ipv6Addr::new(0xff05, 0, 0, 0, 0, 0, 0, 10)); - let group2_ip = IpAddr::V6(Ipv6Addr::new(0xff05, 0, 0, 0, 0, 0, 0, 11)); - let group3_ip = IpAddr::V6(Ipv6Addr::new(0xff05, 0, 0, 0, 0, 0, 0, 12)); + let group1_ip = + IpAddr::V6(Ipv6Addr::new(ADMIN_LOCAL_PREFIX, 0, 0, 0, 0, 0, 0, 10)); + let group2_ip = + IpAddr::V6(Ipv6Addr::new(ADMIN_LOCAL_PREFIX, 0, 0, 0, 0, 0, 0, 11)); + let group3_ip = + IpAddr::V6(Ipv6Addr::new(ADMIN_LOCAL_PREFIX, 0, 0, 0, 0, 0, 0, 12)); // Create first group and capture its group IDs let group1 = create_test_multicast_group( switch, group1_ip, - Some("test_recycling_1"), + Some(TEST_TAG), &[(PhysPort(11), types::Direction::External)], types::InternalForwarding { nat_target: None }, types::ExternalForwarding { vlan_id: None }, @@ -4397,7 +4503,7 @@ async fn test_multicast_group_id_recycling() -> TestResult { let group2 = create_test_multicast_group( switch, group2_ip, - Some("test_recycling_2"), + Some(TEST_TAG), &[(PhysPort(12), types::Direction::External)], types::InternalForwarding { nat_target: None }, types::ExternalForwarding { vlan_id: None }, @@ -4411,9 +4517,10 @@ async fn test_multicast_group_id_recycling() -> TestResult { ); // Delete the first group + let del_tag = make_tag(TEST_TAG); switch .client - .multicast_group_delete(&group1_ip) + .multicast_group_delete(&group1_ip, &del_tag) .await .expect("Should be able to delete first group"); @@ -4435,7 +4542,7 @@ async fn test_multicast_group_id_recycling() -> TestResult { let group3 = create_test_multicast_group( switch, group3_ip, - Some("test_recycling_3"), + Some(TEST_TAG), &[(PhysPort(13), types::Direction::External)], types::InternalForwarding { nat_target: None }, types::ExternalForwarding { vlan_id: None }, @@ -4452,9 +4559,10 @@ async fn test_multicast_group_id_recycling() -> TestResult { ); // Create a fourth group after deleting group2, it should reuse group2's ID + let del_tag = make_tag(TEST_TAG); switch .client - .multicast_group_delete(&group2_ip) + .multicast_group_delete(&group2_ip, &del_tag) .await .expect("Should be able to delete second group"); @@ -4472,11 +4580,12 @@ async fn test_multicast_group_id_recycling() -> TestResult { "Group2 should be deleted" ); - let group4_ip = IpAddr::V6(Ipv6Addr::new(0xff05, 0, 0, 0, 0, 0, 0, 13)); + let group4_ip = + IpAddr::V6(Ipv6Addr::new(ADMIN_LOCAL_PREFIX, 0, 0, 0, 0, 0, 0, 13)); let group4 = create_test_multicast_group( switch, group4_ip, - Some("test_recycling_4"), + Some(TEST_TAG), &[(PhysPort(14), types::Direction::External)], types::InternalForwarding { nat_target: None }, types::ExternalForwarding { vlan_id: None }, @@ -4491,8 +4600,10 @@ async fn test_multicast_group_id_recycling() -> TestResult { "Fourth group should reuse Group2's underlay ID due to LIFO recycling" ); - cleanup_test_group(switch, group3_ip).await.unwrap(); - cleanup_test_group(switch, group4_ip).await + cleanup_test_group(switch, group3_ip, TEST_TAG) + .await + .unwrap(); + cleanup_test_group(switch, group4_ip, TEST_TAG).await } #[tokio::test] @@ -4501,7 +4612,7 @@ async fn test_multicast_empty_then_add_members_ipv6() -> TestResult { let switch = &*get_switch().await; let internal_group_ip = - IpAddr::V6(Ipv6Addr::new(0xff05, 0, 0, 0, 0, 0, 0, 100)); + IpAddr::V6(Ipv6Addr::new(ADMIN_LOCAL_PREFIX, 0, 0, 0, 0, 0, 0, 100)); let external_group_ip = IpAddr::V6(Ipv6Addr::new(0xff0e, 0, 0, 0, 0, 0, 0, 100)); @@ -4509,7 +4620,7 @@ async fn test_multicast_empty_then_add_members_ipv6() -> TestResult { create_test_multicast_group( &switch, internal_group_ip, - Some("empty_internal_ipv6_group"), + Some(TEST_TAG), &[], // No members (Omicron setup) types::InternalForwarding { nat_target: None }, types::ExternalForwarding { vlan_id: None }, @@ -4531,7 +4642,7 @@ async fn test_multicast_empty_then_add_members_ipv6() -> TestResult { let external_group = types::MulticastGroupCreateExternalEntry { group_ip: external_group_ip, - tag: Some("empty_external_ipv6_group".to_string()), + tag: Some(TEST_TAG.to_string()), internal_forwarding: types::InternalForwarding { nat_target: Some(nat_target.clone()), }, @@ -4664,18 +4775,21 @@ async fn test_multicast_empty_then_add_members_ipv6() -> TestResult { // Update the internal group to add members (2 external, 1 underlay) // Meaning: two decap/port-bitmap members. let update_entry = types::MulticastGroupUpdateUnderlayEntry { - tag: Some("empty_internal_ipv6_group".to_string()), members: vec![external_member1, external_member2, underlay_member], }; - let ipv6_update = types::AdminScopedIpv6(match internal_group_ip { + let ipv6_update = types::UnderlayMulticastIpv6(match internal_group_ip { IpAddr::V6(ipv6) => ipv6, _ => panic!("Expected IPv6 address"), }); switch .client - .multicast_group_update_underlay(&ipv6_update, &update_entry) + .multicast_group_update_underlay( + &ipv6_update, + &make_tag(TEST_TAG), + &update_entry, + ) .await .expect("Should update internal group with members"); @@ -4794,20 +4908,24 @@ async fn test_multicast_empty_then_add_members_ipv6() -> TestResult { "Bitmap table should have entry for external group ID when group has members" ); - // Test: Update internal group back to empty (remove all members) + // Test: Update internal group back to empty (remove all members). + // Must pass same tag for validation. let empty_update_entry = types::MulticastGroupUpdateUnderlayEntry { - tag: None, members: vec![], // Remove all members }; - let ipv6_update = types::AdminScopedIpv6(match internal_group_ip { + let ipv6_update = types::UnderlayMulticastIpv6(match internal_group_ip { IpAddr::V6(ipv6) => ipv6, _ => panic!("Expected IPv6 address"), }); switch .client - .multicast_group_update_underlay(&ipv6_update, &empty_update_entry) + .multicast_group_update_underlay( + &ipv6_update, + &make_tag(TEST_TAG), + &empty_update_entry, + ) .await .expect("Should update internal group back to empty"); @@ -4852,10 +4970,10 @@ async fn test_multicast_empty_then_add_members_ipv6() -> TestResult { switch.packet_test(vec![send_final], expected_final)?; - cleanup_test_group(&switch, external_group_ip) + cleanup_test_group(&switch, external_group_ip, TEST_TAG) .await .unwrap(); - cleanup_test_group(&switch, internal_group_ip).await + cleanup_test_group(&switch, internal_group_ip, TEST_TAG).await } #[tokio::test] @@ -4864,14 +4982,14 @@ async fn test_multicast_empty_then_add_members_ipv4() -> TestResult { let switch = &*get_switch().await; let internal_group_ip = - IpAddr::V6(Ipv6Addr::new(0xff05, 0, 0, 0, 0, 0, 0, 101)); + IpAddr::V6(Ipv6Addr::new(ADMIN_LOCAL_PREFIX, 0, 0, 0, 0, 0, 0, 101)); let external_group_ip = IpAddr::V4(Ipv4Addr::new(224, 1, 2, 100)); // Create internal admin-scoped group (empty, no members) create_test_multicast_group( &switch, internal_group_ip, - Some("empty_internal_ipv4_nat_target"), + Some(TEST_TAG), &[], // No members types::InternalForwarding { nat_target: None }, types::ExternalForwarding { vlan_id: None }, @@ -4891,7 +5009,7 @@ async fn test_multicast_empty_then_add_members_ipv4() -> TestResult { let external_group = types::MulticastGroupCreateExternalEntry { group_ip: external_group_ip, - tag: Some("empty_external_ipv4_group".to_string()), + tag: Some(TEST_TAG.to_string()), internal_forwarding: types::InternalForwarding { nat_target: Some(nat_target.clone()), }, @@ -5023,18 +5141,21 @@ async fn test_multicast_empty_then_add_members_ipv4() -> TestResult { // Update the internal group to add members (2 external, 1 underlay) let update_entry = types::MulticastGroupUpdateUnderlayEntry { - tag: Some("empty_internal_ipv4_nat_target".to_string()), members: vec![external_member1, external_member2, underlay_member], }; - let ipv6_update = types::AdminScopedIpv6(match internal_group_ip { + let ipv6_update = types::UnderlayMulticastIpv6(match internal_group_ip { IpAddr::V6(ipv6) => ipv6, _ => panic!("Expected IPv6 address"), }); switch .client - .multicast_group_update_underlay(&ipv6_update, &update_entry) + .multicast_group_update_underlay( + &ipv6_update, + &make_tag(TEST_TAG), + &update_entry, + ) .await .expect("Should update internal group with members"); @@ -5156,18 +5277,21 @@ async fn test_multicast_empty_then_add_members_ipv4() -> TestResult { // Test: Update internal group back to empty (remove all members) let empty_update_entry = types::MulticastGroupUpdateUnderlayEntry { - tag: None, members: vec![], // Remove all members }; - let ipv6_update = types::AdminScopedIpv6(match internal_group_ip { + let ipv6_update = types::UnderlayMulticastIpv6(match internal_group_ip { IpAddr::V6(ipv6) => ipv6, _ => panic!("Expected IPv6 address"), }); switch .client - .multicast_group_update_underlay(&ipv6_update, &empty_update_entry) + .multicast_group_update_underlay( + &ipv6_update, + &make_tag(TEST_TAG), + &empty_update_entry, + ) .await .expect("Should update internal group back to empty"); @@ -5213,16 +5337,12 @@ async fn test_multicast_empty_then_add_members_ipv4() -> TestResult { switch.packet_test(vec![send_final], expected_final)?; - cleanup_test_group(&switch, external_group_ip) + cleanup_test_group(&switch, external_group_ip, TEST_TAG) .await .unwrap(); - cleanup_test_group(&switch, internal_group_ip).await + cleanup_test_group(&switch, internal_group_ip, TEST_TAG).await } -// ============================================================================= -// ROLLBACK TESTS -// ============================================================================= - #[tokio::test] #[ignore] async fn test_multicast_rollback_external_group_creation_failure() -> TestResult @@ -5230,14 +5350,14 @@ async fn test_multicast_rollback_external_group_creation_failure() -> TestResult let switch = &*get_switch().await; let internal_group_ip = - IpAddr::V6(Ipv6Addr::new(0xff05, 0, 0, 0, 0, 0, 0, 102)); + IpAddr::V6(Ipv6Addr::new(ADMIN_LOCAL_PREFIX, 0, 0, 0, 0, 0, 0, 102)); let external_group_ip = IpAddr::V4(Ipv4Addr::new(224, 1, 2, 102)); // Create internal group with members first create_test_multicast_group( &switch, internal_group_ip, - Some("rollback_test_internal"), + Some(TEST_TAG), &[ (PhysPort(15), types::Direction::External), (PhysPort(17), types::Direction::Underlay), @@ -5272,6 +5392,11 @@ async fn test_multicast_rollback_external_group_creation_failure() -> TestResult .table_dump("pipe.Egress.mcast_egress.tbl_decap_ports") .await .expect("Should be able to dump bitmap table"); + let initial_src_filter_table = switch + .client + .table_dump(SOURCE_FILTER_IPV4_TABLE) + .await + .expect("Should be able to dump source filter table"); // Attempt to create external group that will cause failure during validation // Use a non-existent internal group IP to trigger "NAT target must be a tracked multicast group" error @@ -5375,7 +5500,19 @@ async fn test_multicast_rollback_external_group_creation_failure() -> TestResult "Bitmap table should be unchanged after rollback" ); - cleanup_test_group(&switch, internal_group_ip).await + let post_src_filter_table = switch + .client + .table_dump(SOURCE_FILTER_IPV4_TABLE) + .await + .expect("Should be able to dump source filter table"); + + assert_eq!( + post_src_filter_table.entries.len(), + initial_src_filter_table.entries.len(), + "Source filter table should be unchanged after rollback" + ); + + cleanup_test_group(&switch, internal_group_ip, TEST_TAG).await } #[tokio::test] @@ -5384,13 +5521,13 @@ async fn test_multicast_rollback_member_update_failure() -> TestResult { let switch = &*get_switch().await; let internal_group_ip = - IpAddr::V6(Ipv6Addr::new(0xff05, 0, 0, 0, 0, 0, 0, 103)); + IpAddr::V6(Ipv6Addr::new(ADMIN_LOCAL_PREFIX, 0, 0, 0, 0, 0, 0, 103)); // Create internal group with initial members create_test_multicast_group( &switch, internal_group_ip, - Some("rollback_member_test"), + Some(TEST_TAG), &[ (PhysPort(15), types::Direction::External), (PhysPort(17), types::Direction::Underlay), @@ -5422,10 +5559,9 @@ async fn test_multicast_rollback_member_update_failure() -> TestResult { let update_request = types::MulticastGroupUpdateUnderlayEntry { members: invalid_members, - tag: None, }; - let ipv6_update = types::AdminScopedIpv6(match internal_group_ip { + let ipv6_update = types::UnderlayMulticastIpv6(match internal_group_ip { IpAddr::V6(ipv6) => ipv6, _ => panic!("Expected IPv6 address"), }); @@ -5433,7 +5569,11 @@ async fn test_multicast_rollback_member_update_failure() -> TestResult { // This should fail and trigger rollback let result = switch .client - .multicast_group_update_underlay(&ipv6_update, &update_request) + .multicast_group_update_underlay( + &ipv6_update, + &make_tag(TEST_TAG), + &update_request, + ) .await; // Verify the update failed @@ -5455,7 +5595,7 @@ async fn test_multicast_rollback_member_update_failure() -> TestResult { "Member count should be unchanged after rollback" ); - cleanup_test_group(&switch, internal_group_ip).await + cleanup_test_group(&switch, internal_group_ip, TEST_TAG).await } #[tokio::test] @@ -5464,14 +5604,14 @@ async fn test_multicast_rollback_nat_transition_failure() -> TestResult { let switch = &*get_switch().await; let internal_group_ip = - IpAddr::V6(Ipv6Addr::new(0xff05, 0, 0, 0, 0, 0, 0, 104)); + IpAddr::V6(Ipv6Addr::new(ADMIN_LOCAL_PREFIX, 0, 0, 0, 0, 0, 0, 104)); let external_group_ip = IpAddr::V4(Ipv4Addr::new(224, 1, 2, 104)); // Create internal group create_test_multicast_group( &switch, internal_group_ip, - Some("nat_rollback_test"), + Some(TEST_TAG), &[(PhysPort(15), types::Direction::External)], types::InternalForwarding { nat_target: None }, types::ExternalForwarding { vlan_id: None }, @@ -5491,7 +5631,7 @@ async fn test_multicast_rollback_nat_transition_failure() -> TestResult { let external_entry = types::MulticastGroupCreateExternalEntry { group_ip: external_group_ip, - tag: None, + tag: Some(TEST_TAG.to_string()), internal_forwarding: types::InternalForwarding { nat_target: Some(nat_target.clone()), }, @@ -5531,7 +5671,6 @@ async fn test_multicast_rollback_nat_transition_failure() -> TestResult { }; let invalid_update = types::MulticastGroupUpdateExternalEntry { - tag: None, internal_forwarding: types::InternalForwarding { nat_target: Some(invalid_nat_target), }, @@ -5542,7 +5681,11 @@ async fn test_multicast_rollback_nat_transition_failure() -> TestResult { // This should fail and trigger NAT rollback let result = switch .client - .multicast_group_update_external(&external_group_ip, &invalid_update) + .multicast_group_update_external( + &external_group_ip, + &make_tag(TEST_TAG), + &invalid_update, + ) .await; // Verify the update failed @@ -5595,10 +5738,10 @@ async fn test_multicast_rollback_nat_transition_failure() -> TestResult { "NAT table should be unchanged after rollback" ); - cleanup_test_group(&switch, external_group_ip) + cleanup_test_group(&switch, external_group_ip, TEST_TAG) .await .unwrap(); - cleanup_test_group(&switch, internal_group_ip).await + cleanup_test_group(&switch, internal_group_ip, TEST_TAG).await } #[tokio::test] @@ -5607,14 +5750,14 @@ async fn test_multicast_rollback_vlan_propagation_consistency() { let switch = &*get_switch().await; let internal_group_ip = - IpAddr::V6(Ipv6Addr::new(0xff05, 0, 0, 0, 0, 0, 0, 105)); + IpAddr::V6(Ipv6Addr::new(ADMIN_LOCAL_PREFIX, 0, 0, 0, 0, 0, 0, 105)); let external_group_ip = IpAddr::V4(Ipv4Addr::new(224, 1, 2, 105)); // Create internal group with members (so bitmap entry get created) create_test_multicast_group( &switch, internal_group_ip, - Some("vlan_propagation_test"), + Some(TEST_TAG), &[ (PhysPort(15), types::Direction::External), (PhysPort(17), types::Direction::Underlay), @@ -5633,7 +5776,7 @@ async fn test_multicast_rollback_vlan_propagation_consistency() { .expect("Should be able to dump bitmap table"); // First, delete the internal group to break the NAT target reference - cleanup_test_group(&switch, internal_group_ip) + cleanup_test_group(&switch, internal_group_ip, TEST_TAG) .await .expect("Should cleanup internal group"); @@ -5733,7 +5876,7 @@ async fn test_multicast_rollback_source_filter_update() -> TestResult { create_test_multicast_group( switch, internal_multicast_ip, - Some("rollback_internal"), + Some(TEST_TAG), &[(egress1, types::Direction::External)], types::InternalForwarding { nat_target: None }, types::ExternalForwarding { vlan_id: None }, // No NAT needed for internal groups @@ -5757,7 +5900,7 @@ async fn test_multicast_rollback_source_filter_update() -> TestResult { let external_group = types::MulticastGroupCreateExternalEntry { group_ip, - tag: Some("source_filter_rollback_test".to_string()), + tag: Some(TEST_TAG.to_string()), internal_forwarding: types::InternalForwarding { nat_target: Some(nat_target), }, @@ -5788,13 +5931,16 @@ async fn test_multicast_rollback_source_filter_update() -> TestResult { sources: Some(invalid_sources), internal_forwarding: external_group.internal_forwarding.clone(), external_forwarding: types::ExternalForwarding { vlan_id: Some(10) }, - tag: None, }; // This update should fail due to invalid multicast source IP let result = switch .client - .multicast_group_update_external(&group_ip, &failing_update_entry) + .multicast_group_update_external( + &group_ip, + &make_tag(TEST_TAG), + &failing_update_entry, + ) .await; // Verify the update failed @@ -5833,8 +5979,11 @@ async fn test_multicast_rollback_source_filter_update() -> TestResult { "Source filter table should be unchanged after rollback" ); - // Clean up internal group - cleanup_test_group(&switch, internal_multicast_ip).await + // Clean up external group first (it references internal group via NAT target) + cleanup_test_group(&switch, group_ip, TEST_TAG) + .await + .unwrap(); + cleanup_test_group(&switch, internal_multicast_ip, TEST_TAG).await } #[tokio::test] @@ -5843,13 +5992,13 @@ async fn test_multicast_rollback_partial_member_addition() -> TestResult { let switch = &*get_switch().await; let internal_group_ip = - IpAddr::V6(Ipv6Addr::new(0xff05, 0, 0, 0, 0, 0, 0, 106)); + IpAddr::V6(Ipv6Addr::new(ADMIN_LOCAL_PREFIX, 0, 0, 0, 0, 0, 0, 106)); // Create internal group with initial members create_test_multicast_group( &switch, internal_group_ip, - Some("partial_add_rollback_test"), + Some(TEST_TAG), &[ (PhysPort(15), types::Direction::External), (PhysPort(16), types::Direction::Underlay), @@ -5897,10 +6046,9 @@ async fn test_multicast_rollback_partial_member_addition() -> TestResult { let update_request = types::MulticastGroupUpdateUnderlayEntry { members: mixed_members, - tag: None, }; - let ipv6_update = types::AdminScopedIpv6(match internal_group_ip { + let ipv6_update = types::UnderlayMulticastIpv6(match internal_group_ip { IpAddr::V6(ipv6) => ipv6, _ => panic!("Expected IPv6 address"), }); @@ -5908,7 +6056,11 @@ async fn test_multicast_rollback_partial_member_addition() -> TestResult { // This should fail after partially adding some members, triggering incremental rollback let result = switch .client - .multicast_group_update_underlay(&ipv6_update, &update_request) + .multicast_group_update_underlay( + &ipv6_update, + &make_tag(TEST_TAG), + &update_request, + ) .await; // Verify the update failed @@ -5933,7 +6085,7 @@ async fn test_multicast_rollback_partial_member_addition() -> TestResult { "Member count should be unchanged after partial addition rollback" ); - cleanup_test_group(&switch, internal_group_ip).await + cleanup_test_group(&switch, internal_group_ip, TEST_TAG).await } #[tokio::test] @@ -5942,14 +6094,14 @@ async fn test_multicast_rollback_table_operation_failure() { let switch = &*get_switch().await; let internal_group_ip = - IpAddr::V6(Ipv6Addr::new(0xff05, 0, 0, 0, 0, 0, 0, 107)); + IpAddr::V6(Ipv6Addr::new(ADMIN_LOCAL_PREFIX, 0, 0, 0, 0, 0, 0, 107)); let external_group_ip = IpAddr::V4(Ipv4Addr::new(224, 1, 2, 107)); // Create internal group first create_test_multicast_group( &switch, internal_group_ip, - Some("table_rollback_test"), + Some(TEST_TAG), &[ (PhysPort(15), types::Direction::External), (PhysPort(17), types::Direction::Underlay), @@ -5961,7 +6113,7 @@ async fn test_multicast_rollback_table_operation_failure() { .await; // Delete the internal group to break the NAT target reference - cleanup_test_group(&switch, internal_group_ip) + cleanup_test_group(&switch, internal_group_ip, TEST_TAG) .await .expect("Should cleanup internal group"); @@ -6077,13 +6229,13 @@ async fn test_multicast_group_get_underlay() -> TestResult { let switch = &*get_switch().await; let internal_group_ip = - IpAddr::V6(Ipv6Addr::new(0xff05, 0, 0, 0, 0, 0, 0, 200)); + IpAddr::V6(Ipv6Addr::new(ADMIN_LOCAL_PREFIX, 0, 0, 0, 0, 0, 0, 200)); // Create an internal/underlay group let _created_group = create_test_multicast_group( &switch, internal_group_ip, - Some("underlay_get_test"), + Some(TEST_TAG), &[ (PhysPort(10), types::Direction::External), (PhysPort(12), types::Direction::Underlay), @@ -6096,7 +6248,7 @@ async fn test_multicast_group_get_underlay() -> TestResult { let retrieved_underlay = switch .client - .multicast_group_get_underlay(&types::AdminScopedIpv6( + .multicast_group_get_underlay(&types::UnderlayMulticastIpv6( match internal_group_ip { IpAddr::V6(ipv6) => ipv6, _ => panic!("Expected IPv6 address"), @@ -6110,10 +6262,7 @@ async fn test_multicast_group_get_underlay() -> TestResult { // Verify the response matches what we created assert_eq!(retrieved_underlay.group_ip.to_ip_addr(), internal_group_ip); - assert_eq!( - retrieved_underlay.tag, - Some("underlay_get_test".to_string()) - ); + assert_eq!(retrieved_underlay.tag, TEST_TAG); assert_eq!(retrieved_underlay.members.len(), 2); // Compare with generic GET endpoint result @@ -6145,5 +6294,1039 @@ async fn test_multicast_group_get_underlay() -> TestResult { ); } } - cleanup_test_group(&switch, internal_group_ip).await + cleanup_test_group(&switch, internal_group_ip, TEST_TAG).await +} + +const SOURCE_FILTER_IPV4_TABLE: &str = + "pipe.Ingress.mcast_ingress.mcast_source_filter_ipv4"; +const SOURCE_FILTER_IPV6_TABLE: &str = + "pipe.Ingress.mcast_ingress.mcast_source_filter_ipv6"; + +/// Test that when `IpSrc::Any` is present in the sources list, only a single +/// /0 entry is added to the source filter table (not individual entries for +/// each specific source). +/// +/// This tests the ASM lifecycle where a group starts with specific sources +/// and later has an "any source" member join. +#[tokio::test] +#[ignore] +async fn test_source_filter_ipv4_collapses_to_any() -> TestResult { + let switch = &*get_switch().await; + + let internal_group_ip = + IpAddr::V6(Ipv6Addr::new(ADMIN_LOCAL_PREFIX, 0, 0, 0, 0, 0, 0, 0x300)); + let external_group_ip = IpAddr::V4(Ipv4Addr::new(239, 1, 1, 100)); + + // Create internal group first + let _internal = create_test_multicast_group( + switch, + internal_group_ip, + Some(TEST_TAG), + &[(PhysPort(10), types::Direction::External)], + types::InternalForwarding { nat_target: None }, + types::ExternalForwarding { vlan_id: None }, + None, + ) + .await; + + let nat_target = types::NatTarget { + internal_ip: match internal_group_ip { + IpAddr::V6(ipv6) => ipv6, + _ => panic!("Expected IPv6"), + }, + inner_mac: MacAddr::new(0x01, 0x00, 0x5e, 0x01, 0x01, 0x64).into(), + vni: 100.into(), + }; + + // Get baseline source filter table state + let baseline_table = switch + .client + .table_dump(SOURCE_FILTER_IPV4_TABLE) + .await + .expect("Should dump source filter table"); + let baseline_count = baseline_table.entries.len(); + + // Create external group with mixed sources: specific + `Any` + // The optimization should collapse this to just one /0 entry + let external_group = types::MulticastGroupCreateExternalEntry { + group_ip: external_group_ip, + tag: Some(TEST_TAG.to_string()), + internal_forwarding: types::InternalForwarding { + nat_target: Some(nat_target.clone()), + }, + external_forwarding: types::ExternalForwarding { vlan_id: Some(10) }, + sources: Some(vec![ + types::IpSrc::Exact("192.168.1.1".parse().unwrap()), + types::IpSrc::Exact("192.168.1.2".parse().unwrap()), + types::IpSrc::Any, + ]), + }; + + let created = switch + .client + .multicast_group_create_external(&external_group) + .await + .expect("Should create external group with sources") + .into_inner(); + + // Verify sources are normalized to None when `Any` is present + // (`Any` subsumes all other sources, so they collapse to `None` in + // the response) + assert_eq!( + created.sources, None, + "Sources containing Any should be normalized to None" + ); + + // Check source filter table - should only have 1 new entry (the /0) + let after_create_table = switch + .client + .table_dump(SOURCE_FILTER_IPV4_TABLE) + .await + .expect("Should dump source filter table after create"); + + assert_eq!( + after_create_table.entries.len(), + baseline_count + 1, + "Should have exactly 1 new entry (the /0), not 3 entries" + ); + + // Verify deletion ordering: attempting to delete internal group first should fail + // because the external group still references it via NAT target + let del_tag = make_tag(TEST_TAG); + let delete_internal_first_result = switch + .client + .multicast_group_delete(&internal_group_ip, &del_tag) + .await; + + assert!( + delete_internal_first_result.is_err(), + "Deleting internal group while still referenced by external group should fail" + ); + + if let Err(Error::ErrorResponse(resp)) = &delete_internal_first_result { + let error_msg = format!("{resp:?}"); + assert!( + error_msg.contains("still referenced"), + "Error should mention the group is still referenced: {error_msg}" + ); + } else { + panic!("Expected ErrorResponse, got: {delete_internal_first_result:?}"); + } + + // Cleanup in correct order: external first, then internal + cleanup_test_group(switch, external_group_ip, TEST_TAG) + .await + .unwrap(); + cleanup_test_group(switch, internal_group_ip, TEST_TAG).await +} + +/// Test IPv6 source filter collapsing when `IpSrc::Any` is present. +#[tokio::test] +#[ignore] +async fn test_source_filter_ipv6_collapses_to_any() -> TestResult { + let switch = &*get_switch().await; + + let internal_group_ip = + IpAddr::V6(Ipv6Addr::new(ADMIN_LOCAL_PREFIX, 0, 0, 0, 0, 0, 0, 0x310)); + // Non-admin-local IPv6 multicast address for external group + let external_group_ip = + IpAddr::V6(Ipv6Addr::new(0xff0e, 0, 0, 0, 0, 0, 0, 0x100)); + + // Create internal group first + let _internal = create_test_multicast_group( + switch, + internal_group_ip, + Some(TEST_TAG), + &[(PhysPort(10), types::Direction::External)], + types::InternalForwarding { nat_target: None }, + types::ExternalForwarding { vlan_id: None }, + None, + ) + .await; + + let nat_target = types::NatTarget { + internal_ip: match internal_group_ip { + IpAddr::V6(ipv6) => ipv6, + _ => panic!("Expected IPv6"), + }, + inner_mac: MacAddr::new(0x33, 0x33, 0x00, 0x00, 0x01, 0x00).into(), + vni: 100.into(), + }; + + let baseline_table = switch + .client + .table_dump(SOURCE_FILTER_IPV6_TABLE) + .await + .expect("Should dump IPv6 source filter table"); + let baseline_count = baseline_table.entries.len(); + + // Create external group with mixed sources: specific + `Any` + let external_group = types::MulticastGroupCreateExternalEntry { + group_ip: external_group_ip, + tag: Some(TEST_TAG.to_string()), + internal_forwarding: types::InternalForwarding { + nat_target: Some(nat_target.clone()), + }, + external_forwarding: types::ExternalForwarding { vlan_id: Some(10) }, + sources: Some(vec![ + types::IpSrc::Exact("2001:db8::1".parse().unwrap()), + types::IpSrc::Exact("2001:db8::2".parse().unwrap()), + types::IpSrc::Any, + ]), + }; + + let created = switch + .client + .multicast_group_create_external(&external_group) + .await + .expect("Should create external group with sources") + .into_inner(); + + // Verify sources are normalized to `None` when `Any` is present + assert_eq!( + created.sources, None, + "Sources containing Any should be normalized to None" + ); + + // Should only have 1 new entry (the ::/0) + let after_create_table = switch + .client + .table_dump(SOURCE_FILTER_IPV6_TABLE) + .await + .expect("Should dump IPv6 source filter table after create"); + + assert_eq!( + after_create_table.entries.len(), + baseline_count + 1, + "Should have exactly 1 new entry (the ::/0), not 3 entries" + ); + + // Verify deletion ordering: attempting to delete internal group first should fail + // because the external group still references it via NAT target. + // This is particularly important for IPv6 where external groups (ff0e::*) + // sort AFTER internal groups (ff04::*) in BTreeMap iteration order. + let del_tag = make_tag(TEST_TAG); + let delete_internal_first_result = switch + .client + .multicast_group_delete(&internal_group_ip, &del_tag) + .await; + + assert!( + delete_internal_first_result.is_err(), + "Deleting internal group while still referenced by external group should fail" + ); + + if let Err(Error::ErrorResponse(resp)) = &delete_internal_first_result { + let error_msg = format!("{resp:?}"); + assert!( + error_msg.contains("still referenced"), + "Error should mention the group is still referenced: {error_msg}" + ); + } else { + panic!("Expected ErrorResponse, got: {delete_internal_first_result:?}"); + } + + // Cleanup in correct order: external first, then internal + cleanup_test_group(switch, external_group_ip, TEST_TAG) + .await + .unwrap(); + cleanup_test_group(switch, internal_group_ip, TEST_TAG).await +} + +/// Test that updating a group from specific sources to include `Any` +/// results in the source filter table being updated correctly. +#[tokio::test] +#[ignore] +async fn test_source_filter_update_to_any() -> TestResult { + let switch = &*get_switch().await; + + let internal_group_ip = + IpAddr::V6(Ipv6Addr::new(ADMIN_LOCAL_PREFIX, 0, 0, 0, 0, 0, 0, 0x301)); + let external_group_ip = IpAddr::V4(Ipv4Addr::new(239, 1, 1, 101)); + + // Create internal group + let _internal = create_test_multicast_group( + switch, + internal_group_ip, + Some(TEST_TAG), + &[(PhysPort(10), types::Direction::External)], + types::InternalForwarding { nat_target: None }, + types::ExternalForwarding { vlan_id: None }, + None, + ) + .await; + + let nat_target = types::NatTarget { + internal_ip: match internal_group_ip { + IpAddr::V6(ipv6) => ipv6, + _ => panic!("Expected IPv6"), + }, + inner_mac: MacAddr::new(0x01, 0x00, 0x5e, 0x01, 0x01, 0x65).into(), + vni: 100.into(), + }; + + let baseline_table = switch + .client + .table_dump(SOURCE_FILTER_IPV4_TABLE) + .await + .expect("Should dump source filter table"); + let baseline_count = baseline_table.entries.len(); + + // Create external group with only specific sources (no `Any`) + let external_group = types::MulticastGroupCreateExternalEntry { + group_ip: external_group_ip, + tag: Some(TEST_TAG.to_string()), + internal_forwarding: types::InternalForwarding { + nat_target: Some(nat_target.clone()), + }, + external_forwarding: types::ExternalForwarding { vlan_id: Some(10) }, + sources: Some(vec![ + types::IpSrc::Exact("192.168.1.1".parse().unwrap()), + types::IpSrc::Exact("192.168.1.2".parse().unwrap()), + ]), + }; + + switch + .client + .multicast_group_create_external(&external_group) + .await + .expect("Should create external group"); + + // Should have 2 specific entries + let after_create_table = switch + .client + .table_dump(SOURCE_FILTER_IPV4_TABLE) + .await + .expect("Should dump table after create"); + + assert_eq!( + after_create_table.entries.len(), + baseline_count + 2, + "Should have 2 specific source entries" + ); + + // Update to include `Any`, simulating an "any source" member joining + let update_entry = types::MulticastGroupUpdateExternalEntry { + internal_forwarding: types::InternalForwarding { + nat_target: Some(nat_target.clone()), + }, + external_forwarding: types::ExternalForwarding { vlan_id: Some(10) }, + sources: Some(vec![ + types::IpSrc::Exact("192.168.1.1".parse().unwrap()), + types::IpSrc::Exact("192.168.1.2".parse().unwrap()), + types::IpSrc::Any, + ]), + }; + + let updated = switch + .client + .multicast_group_update_external( + &external_group_ip, + &make_tag(TEST_TAG), + &update_entry, + ) + .await + .expect("Should update external group") + .into_inner(); + + // Verify sources are normalized to `None` when `Any` is present + assert_eq!( + updated.sources, None, + "Sources containing Any should be normalized to None after update" + ); + + // Should now have only 1 entry (the /0), replacing the 2 specific ones + let after_update_table = switch + .client + .table_dump(SOURCE_FILTER_IPV4_TABLE) + .await + .expect("Should dump table after update"); + + assert_eq!( + after_update_table.entries.len(), + baseline_count + 1, + "After update with Any, should have only 1 entry (the /0)" + ); + + // Cleanup in correct order: external first, then internal + cleanup_test_group(switch, external_group_ip, TEST_TAG) + .await + .unwrap(); + cleanup_test_group(switch, internal_group_ip, TEST_TAG).await +} + +/// Test that source filter entries are properly cleaned up when a group is deleted. +#[tokio::test] +#[ignore] +async fn test_source_filter_cleanup_on_delete() -> TestResult { + let switch = &*get_switch().await; + + let internal_group_ip = + IpAddr::V6(Ipv6Addr::new(ADMIN_LOCAL_PREFIX, 0, 0, 0, 0, 0, 0, 0x302)); + let external_group_ip = IpAddr::V4(Ipv4Addr::new(239, 1, 1, 102)); + + // Create internal group + let _internal = create_test_multicast_group( + switch, + internal_group_ip, + Some(TEST_TAG), + &[(PhysPort(10), types::Direction::External)], + types::InternalForwarding { nat_target: None }, + types::ExternalForwarding { vlan_id: None }, + None, + ) + .await; + + let nat_target = types::NatTarget { + internal_ip: match internal_group_ip { + IpAddr::V6(ipv6) => ipv6, + _ => panic!("Expected IPv6"), + }, + inner_mac: MacAddr::new(0x01, 0x00, 0x5e, 0x01, 0x01, 0x66).into(), + vni: 100.into(), + }; + + let baseline_table = switch + .client + .table_dump(SOURCE_FILTER_IPV4_TABLE) + .await + .expect("Should dump source filter table"); + let baseline_count = baseline_table.entries.len(); + + // Create external group with sources + let external_group = types::MulticastGroupCreateExternalEntry { + group_ip: external_group_ip, + tag: Some(TEST_TAG.to_string()), + internal_forwarding: types::InternalForwarding { + nat_target: Some(nat_target.clone()), + }, + external_forwarding: types::ExternalForwarding { vlan_id: Some(10) }, + sources: Some(vec![ + types::IpSrc::Exact("192.168.1.1".parse().unwrap()), + types::IpSrc::Exact("192.168.1.2".parse().unwrap()), + ]), + }; + + switch + .client + .multicast_group_create_external(&external_group) + .await + .expect("Should create external group"); + + // Verify entries were added + let after_create_table = switch + .client + .table_dump(SOURCE_FILTER_IPV4_TABLE) + .await + .expect("Should dump table after create"); + + assert_eq!( + after_create_table.entries.len(), + baseline_count + 2, + "Should have 2 source entries after create" + ); + + // Delete the external group + cleanup_test_group(switch, external_group_ip, TEST_TAG) + .await + .expect("Should delete external group"); + + // Verify source filter entries were cleaned up + let after_delete_table = switch + .client + .table_dump(SOURCE_FILTER_IPV4_TABLE) + .await + .expect("Should dump table after delete"); + + assert_eq!( + after_delete_table.entries.len(), + baseline_count, + "Source filter entries should be cleaned up after group deletion" + ); + + cleanup_test_group(switch, internal_group_ip, TEST_TAG).await +} + +/// Test that empty sources `Some(vec![])` is normalized to None and adds /0 entry. +#[tokio::test] +#[ignore] +async fn test_source_filter_empty_vec_normalizes_to_any() -> TestResult { + let switch = &*get_switch().await; + + let internal_group_ip = + IpAddr::V6(Ipv6Addr::new(ADMIN_LOCAL_PREFIX, 0, 0, 0, 0, 0, 0, 0x303)); + let external_group_ip = IpAddr::V4(Ipv4Addr::new(239, 1, 1, 103)); + + // Create internal group + let _internal = create_test_multicast_group( + switch, + internal_group_ip, + Some(TEST_TAG), + &[(PhysPort(10), types::Direction::External)], + types::InternalForwarding { nat_target: None }, + types::ExternalForwarding { vlan_id: None }, + None, + ) + .await; + + let nat_target = types::NatTarget { + internal_ip: match internal_group_ip { + IpAddr::V6(ipv6) => ipv6, + _ => panic!("Expected IPv6"), + }, + inner_mac: MacAddr::new(0x01, 0x00, 0x5e, 0x01, 0x01, 0x67).into(), + vni: 100.into(), + }; + + let baseline_table = switch + .client + .table_dump(SOURCE_FILTER_IPV4_TABLE) + .await + .expect("Should dump source filter table"); + let baseline_count = baseline_table.entries.len(); + + // Create external group with empty sources vec - should normalize to None + // and add a single /0 entry (allow any source) + let external_group = types::MulticastGroupCreateExternalEntry { + group_ip: external_group_ip, + tag: Some(TEST_TAG.to_string()), + internal_forwarding: types::InternalForwarding { + nat_target: Some(nat_target.clone()), + }, + external_forwarding: types::ExternalForwarding { vlan_id: Some(10) }, + sources: Some(vec![]), // Empty vec should normalize to None + }; + + let created = switch + .client + .multicast_group_create_external(&external_group) + .await + .expect("Should create external group with empty sources") + .into_inner(); + + // Verify sources are normalized to None + assert_eq!( + created.sources, None, + "Empty sources vec should be normalized to None" + ); + + // Should have exactly 1 new entry (the /0 for any source) + let after_create_table = switch + .client + .table_dump(SOURCE_FILTER_IPV4_TABLE) + .await + .expect("Should dump source filter table after create"); + + assert_eq!( + after_create_table.entries.len(), + baseline_count + 1, + "Empty sources should add exactly 1 entry (the /0)" + ); + + // Cleanup + cleanup_test_group(switch, external_group_ip, TEST_TAG) + .await + .unwrap(); + + // Verify the /0 entry was removed + let after_delete_table = switch + .client + .table_dump(SOURCE_FILTER_IPV4_TABLE) + .await + .expect("Should dump table after delete"); + + assert_eq!( + after_delete_table.entries.len(), + baseline_count, + "Source filter entry should be cleaned up after deletion" + ); + + cleanup_test_group(switch, internal_group_ip, TEST_TAG).await +} + +/// Test that updating non-existent groups returns 404. +#[tokio::test] +#[ignore] +async fn test_update_nonexistent_group_returns_404() -> TestResult { + let switch = &*get_switch().await; + + // Case: Update non-existent underlay group + let nonexistent_underlay: types::UnderlayMulticastIpv6 = + Ipv6Addr::new(ADMIN_LOCAL_PREFIX, 0, 0, 0, 0, 0, 0, 0xdead) + .try_into() + .unwrap(); + + let (port_id, link_id) = switch.link_id(PhysPort(15)).unwrap(); + let underlay_update = types::MulticastGroupUpdateUnderlayEntry { + members: vec![types::MulticastGroupMember { + port_id, + link_id, + direction: types::Direction::Underlay, + }], + }; + + let result = switch + .client + .multicast_group_update_underlay( + &nonexistent_underlay, + &make_tag("nonexistent_test"), + &underlay_update, + ) + .await; + + match result { + Err(Error::ErrorResponse(resp)) => { + assert_eq!(resp.status(), 404, "Expected 404 for underlay update"); + } + Ok(_) => panic!("Expected error for non-existent underlay group"), + Err(e) => panic!("Expected ErrorResponse, got {:?}", e), + } + + // Case: Update non-existent external group + let nonexistent_external = IpAddr::V4(Ipv4Addr::new(239, 255, 255, 254)); + + let external_update = types::MulticastGroupUpdateExternalEntry { + external_forwarding: types::ExternalForwarding { vlan_id: Some(100) }, + internal_forwarding: types::InternalForwarding { nat_target: None }, + sources: None, + }; + + let result = switch + .client + .multicast_group_update_external( + &nonexistent_external, + &make_tag("nonexistent_test"), + &external_update, + ) + .await; + + match result { + Err(Error::ErrorResponse(resp)) => { + assert_eq!(resp.status(), 404, "Expected 404 for external update"); + } + Ok(_) => panic!("Expected error for non-existent external group"), + Err(e) => panic!("Expected ErrorResponse, got {:?}", e), + } + + Ok(()) +} + +/// Test that deleting non-existent groups returns 404, even with a tag. +/// +/// Verifies that tag validation doesn't produce a misleading error when +/// the group doesn't exist in the first place. +#[tokio::test] +#[ignore] +async fn test_delete_nonexistent_group_returns_404() -> TestResult { + let switch = &*get_switch().await; + + // Case: Delete non-existent group with a tag provided + let nonexistent_ip = IpAddr::V4(Ipv4Addr::new(239, 255, 255, 253)); + + let del_tag = make_tag("some_tag"); + let result = switch + .client + .multicast_group_delete(&nonexistent_ip, &del_tag) + .await; + + match result { + Err(Error::ErrorResponse(resp)) => { + assert_eq!( + resp.status(), + 404, + "Expected 404 for non-existent group" + ); + } + Ok(_) => panic!("Expected error for non-existent group"), + Err(e) => panic!("Expected ErrorResponse, got {:?}", e), + } + + Ok(()) +} + +/// Test the delete+recreate recovery pattern for underlay groups. +/// +/// Simulates Omicron's recovery flow when it encounters a 404. +#[tokio::test] +#[ignore] +async fn test_underlay_delete_recreate_recovery_flow() -> TestResult { + let switch = &*get_switch().await; + + let group_ip: types::UnderlayMulticastIpv6 = + Ipv6Addr::new(ADMIN_LOCAL_PREFIX, 0, 0, 0, 0, 0, 0, 0x501) + .try_into() + .unwrap(); + let tag = "recovery_flow_test"; + + // Case: Create underlay group with initial member + let (port_id_1, link_id_1) = switch.link_id(PhysPort(15)).unwrap(); + let create_entry = types::MulticastGroupCreateUnderlayEntry { + group_ip: group_ip.clone(), + members: vec![types::MulticastGroupMember { + port_id: port_id_1.clone(), + link_id: link_id_1, + direction: types::Direction::Underlay, + }], + tag: Some(tag.to_string()), + }; + + let created = switch + .client + .multicast_group_create_underlay(&create_entry) + .await + .expect("Should create underlay group"); + + assert_eq!(created.into_inner().members.len(), 1); + + // Case: Delete the group (simulating recovery from stale state) + let del_tag = make_tag(tag); + switch + .client + .multicast_group_delete(&group_ip.to_ip_addr(), &del_tag) + .await + .expect("Should delete group during recovery"); + + // Case: Verify 404 on get after deletion + let get_result = + switch.client.multicast_group_get_underlay(&group_ip).await; + + match get_result { + Err(Error::ErrorResponse(resp)) if resp.status() == 404 => {} + _ => panic!("Expected 404 after delete, got {:?}", get_result), + } + + // Case: Recreate with updated members + let (port_id_2, link_id_2) = switch.link_id(PhysPort(17)).unwrap(); + let recreate_entry = types::MulticastGroupCreateUnderlayEntry { + group_ip: group_ip.clone(), + members: vec![ + types::MulticastGroupMember { + port_id: port_id_1.clone(), + link_id: link_id_1, + direction: types::Direction::Underlay, + }, + types::MulticastGroupMember { + port_id: port_id_2.clone(), + link_id: link_id_2, + direction: types::Direction::Underlay, + }, + ], + tag: Some(tag.to_string()), + }; + + let recreated = switch + .client + .multicast_group_create_underlay(&recreate_entry) + .await + .expect("Should recreate underlay group"); + + // Case: Verify recreated group has correct state + let recreated_inner = recreated.into_inner(); + assert_eq!( + recreated_inner.members.len(), + 2, + "Recreated group should have 2 members" + ); + assert_eq!(recreated_inner.tag, tag); + + // Verify we can fetch it + let fetched = switch + .client + .multicast_group_get_underlay(&group_ip) + .await + .expect("Should fetch recreated group"); + + assert_eq!(fetched.into_inner().members.len(), 2); + + // Cleanup + switch + .client + .multicast_reset_by_tag(&make_tag(tag)) + .await + .expect("Should cleanup by tag"); + + Ok(()) +} + +/// Tests that update operations validate tags. +/// +/// When updating a multicast group, the provided tag must match the existing +/// group's tag. This prevents unauthorized modifications. +#[tokio::test] +#[ignore] +async fn test_tag_immutability_on_update() -> TestResult { + let switch = &*get_switch().await; + + let (port_id, link_id) = switch.link_id(PhysPort(11)).unwrap(); + + // Create underlay group with explicit tag + let create_entry = types::MulticastGroupCreateUnderlayEntry { + group_ip: "ff04::700".parse().unwrap(), + tag: Some(TAG_A.to_string()), + members: vec![types::MulticastGroupMember { + port_id: port_id.clone(), + link_id, + direction: types::Direction::Underlay, + }], + }; + + let created = switch + .client + .multicast_group_create_underlay(&create_entry) + .await + .expect("Should create underlay group") + .into_inner(); + + let group_ip = created.group_ip; + + // Attempt update with wrong tag + let update_entry = types::MulticastGroupUpdateUnderlayEntry { + members: vec![types::MulticastGroupMember { + port_id: port_id.clone(), + link_id, + direction: types::Direction::Underlay, + }], + }; + + let result = switch + .client + .multicast_group_update_underlay( + &group_ip, + &make_tag(TAG_WRONG), + &update_entry, + ) + .await; + + match &result { + Err(Error::ErrorResponse(resp)) => { + assert_eq!( + resp.status(), + 400, + "Update with wrong tag should return 400" + ); + // Security: error should not reveal the correct tag + let body = format!("{:?}", resp); + assert!( + !body.contains(TAG_A), + "Error message should not reveal correct tag" + ); + } + _ => { + panic!("Expected ErrorResponse for tag mismatch, got: {:?}", result) + } + } + + // Cleanup + let del_tag = make_tag(TAG_A); + switch + .client + .multicast_group_delete(&group_ip.to_ip_addr(), &del_tag) + .await + .expect("Should delete with correct tag"); + + Ok(()) +} + +/// Tests tag validation on delete for underlay groups. +/// +/// Complements test_multicast_del_tag_validation which tests external groups. +/// This test verifies underlay-specific delete behavior and includes explicit +/// GET verification after failed delete attempts. +#[tokio::test] +#[ignore] +async fn test_tag_validation_on_delete() -> TestResult { + let switch = &*get_switch().await; + + let (port_id, link_id) = switch.link_id(PhysPort(11)).unwrap(); + + // Create underlay group + let create_entry = types::MulticastGroupCreateUnderlayEntry { + group_ip: "ff04::800".parse().unwrap(), + tag: Some(TAG_A.to_string()), + members: vec![types::MulticastGroupMember { + port_id: port_id.clone(), + link_id, + direction: types::Direction::Underlay, + }], + }; + + let created = switch + .client + .multicast_group_create_underlay(&create_entry) + .await + .expect("Should create underlay group") + .into_inner(); + + let group_ip = created.group_ip; + + // Attempt delete with wrong tag + let del_tag = make_tag(TAG_WRONG); + let result = switch + .client + .multicast_group_delete(&group_ip.to_ip_addr(), &del_tag) + .await; + + match &result { + Err(Error::ErrorResponse(resp)) => { + assert_eq!( + resp.status(), + 400, + "Delete with wrong tag should return 400" + ); + } + _ => { + panic!("Expected ErrorResponse for tag mismatch, got: {:?}", result) + } + } + + // Verify group still exists via GET + let fetched = switch + .client + .multicast_group_get_underlay(&group_ip) + .await + .expect("Group should still exist after failed delete"); + + assert_eq!(fetched.into_inner().tag, TAG_A, "Tag should be preserved"); + + // Delete with correct tag + let del_tag = make_tag(TAG_A); + switch + .client + .multicast_group_delete(&group_ip.to_ip_addr(), &del_tag) + .await + .expect("Should delete with correct tag"); + + Ok(()) +} + +/// Tests additional tag validation scenarios: +/// - External group update with wrong tag +/// - Case-sensitive tag matching +/// - Reset by non-existent tag (no-op) +#[tokio::test] +#[ignore] +async fn test_tag_validation() -> TestResult { + let switch = &*get_switch().await; + + // Case: External group update with wrong tag + + // Create internal group for NAT target + let internal_ip: IpAddr = MULTICAST_NAT_IP.into(); + create_test_multicast_group( + switch, + internal_ip, + Some(TEST_TAG), + &[(PhysPort(11), types::Direction::Underlay)], + types::InternalForwarding { nat_target: None }, + types::ExternalForwarding { vlan_id: None }, + None, + ) + .await; + + let external_ip = IpAddr::V4(Ipv4Addr::new(224, 0, 12, 1)); + let create_entry = types::MulticastGroupCreateExternalEntry { + group_ip: external_ip, + tag: Some(TAG_A.to_string()), + internal_forwarding: types::InternalForwarding { + nat_target: Some(create_nat_target_ipv4()), + }, + external_forwarding: types::ExternalForwarding { vlan_id: Some(10) }, + sources: None, + }; + + switch + .client + .multicast_group_create_external(&create_entry) + .await + .expect("Should create external group"); + + let update_entry = types::MulticastGroupUpdateExternalEntry { + internal_forwarding: types::InternalForwarding { + nat_target: Some(create_nat_target_ipv4()), + }, + external_forwarding: types::ExternalForwarding { vlan_id: Some(20) }, + sources: None, + }; + + let result = switch + .client + .multicast_group_update_external( + &external_ip, + &make_tag(TAG_WRONG), + &update_entry, + ) + .await; + + match &result { + Err(Error::ErrorResponse(resp)) => { + assert_eq!( + resp.status(), + 400, + "Update with wrong tag should return 400" + ); + assert!( + !format!("{:?}", resp).contains(TAG_A), + "Error should not reveal tag" + ); + } + _ => panic!("Expected ErrorResponse for tag mismatch"), + } + + cleanup_test_group(switch, external_ip, TAG_A) + .await + .unwrap(); + cleanup_test_group(switch, internal_ip, TEST_TAG) + .await + .unwrap(); + + // Case: Case-sensitive tag matching + + let (port_id, link_id) = switch.link_id(PhysPort(11)).unwrap(); + + let create_entry = types::MulticastGroupCreateUnderlayEntry { + group_ip: "ff04::900".parse().unwrap(), + tag: Some("CaseSensitiveTag".to_string()), + members: vec![types::MulticastGroupMember { + port_id: port_id.clone(), + link_id, + direction: types::Direction::Underlay, + }], + }; + + let created = switch + .client + .multicast_group_create_underlay(&create_entry) + .await + .expect("Should create group") + .into_inner(); + + let case_group_ip = created.group_ip; + + let del_tag = make_tag("casesensitivetag"); + let result = switch + .client + .multicast_group_delete(&case_group_ip.to_ip_addr(), &del_tag) + .await; + + assert!( + matches!(&result, Err(Error::ErrorResponse(resp)) if resp.status() == 400), + "Case-insensitive tag should fail" + ); + + let del_tag = make_tag("CaseSensitiveTag"); + switch + .client + .multicast_group_delete(&case_group_ip.to_ip_addr(), &del_tag) + .await + .expect("Should delete with correct case"); + + // Case: Reset by non-existent tag (no-op) + + switch + .client + .multicast_reset_by_tag(&make_tag("nonexistent_tag_xyz")) + .await + .expect("Reset with non-existent tag should succeed"); + + Ok(()) } diff --git a/dpd-client/tests/integration_tests/table_tests.rs b/dpd-client/tests/integration_tests/table_tests.rs index 88eb7a8..283c05f 100644 --- a/dpd-client/tests/integration_tests/table_tests.rs +++ b/dpd-client/tests/integration_tests/table_tests.rs @@ -2,7 +2,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/ // -// Copyright 2025 Oxide Computer Company +// Copyright 2026 Oxide Computer Company use std::net::IpAddr; use std::net::Ipv4Addr; @@ -14,11 +14,15 @@ use oxnet::Ipv4Net; use oxnet::Ipv6Net; use reqwest::StatusCode; -use crate::integration_tests::common::prelude::*; use dpd_client::ClientInfo; use dpd_client::ResponseValue; use dpd_client::types; +use crate::integration_tests::common::prelude::*; + +/// Admin-local IPv6 multicast prefix (ff04::/16, scope 4). +const ADMIN_LOCAL_PREFIX: u16 = 0xFF04; + // The expected sizes of each table. The values are copied from constants.p4. // // Note: Some tables appear to be 1 entry smaller than the p4 code would @@ -36,8 +40,8 @@ use dpd_client::types; // This table has further shrunk to 4022 entries with the open source // compiler. That is being tracked as issue #1092, which will presumably // subsume #1013. -// update: with the move to 8192 entries we're now at 8190 entries. -const IPV4_LPM_SIZE: usize = 8190; // ipv4 forwarding table +// update: with the move to 8192 entries we're now at 8191 entries. +const IPV4_LPM_SIZE: usize = 8191; // ipv4 forwarding table const IPV6_LPM_SIZE: usize = 1023; // ipv6 forwarding table const SWITCH_IPV4_ADDRS_SIZE: usize = 511; // ipv4 addrs assigned to our ports const SWITCH_IPV6_ADDRS_SIZE: usize = 511; // ipv6 addrs assigned to our ports @@ -46,7 +50,7 @@ const IPV6_NAT_TABLE_SIZE: usize = 1024; // nat routing table const IPV4_ARP_SIZE: usize = 512; // arp cache const IPV6_NEIGHBOR_SIZE: usize = 512; // ipv6 neighbor cache /// The size of the multicast table related to replication on -/// admin-scoped (internal) multicast groups. +/// admin-local (internal) multicast groups. const MULTICAST_TABLE_SIZE: usize = 1024; const MCAST_TAG: &str = "mcast_table_test"; // multicast group tag @@ -73,16 +77,11 @@ fn gen_ipv6_cidr(idx: usize) -> Ipv6Net { Ipv6Net::new(gen_ipv6_addr(idx), 128).unwrap() } -// Generates valid IPv6 multicast addresses that are admin-scoped. +// Generates valid IPv6 multicast addresses that are admin-local (scope 4). fn gen_ipv6_multicast_addr(idx: usize) -> Ipv6Addr { - // Use admin-scoped multicast addresses (ff04::/16, ff05::/16, ff08::/16) + // Use admin-local multicast addresses (ff04::/16) // This ensures they will be created as internal groups - let scope = match idx % 3 { - 0 => 0xFF04, // admin-scoped - 1 => 0xFF05, // admin-scoped - _ => 0xFF08, // admin-scoped - }; - Ipv6Addr::new(scope, 0, 0, 0, 0, 0, 0, (1000 + idx) as u16) + Ipv6Addr::new(ADMIN_LOCAL_PREFIX, 0, 0, 0, 0, 0, 0, (1000 + idx) as u16) } // For each table we want to test, we define functions to insert, delete, and @@ -295,7 +294,7 @@ impl TableTest for types::Ipv4Nat { let external_ip = Ipv4Addr::new(192, 168, 0, 1); let tgt = types::NatTarget { - internal_ip: Ipv6Addr::new(0xff05, 0, 0, 0, 0, 0, 0, 1), + internal_ip: Ipv6Addr::new(ADMIN_LOCAL_PREFIX, 0, 0, 0, 0, 0, 0, 1), inner_mac: MacAddr::new(0xe0, 0xd5, 0x5e, 0x67, 0x89, 0xab).into(), vni: 0.into(), }; @@ -469,12 +468,12 @@ impl TableTest let (port_id1, link_id1) = switch.link_id(PhysPort(11)).unwrap(); let (port_id2, link_id2) = switch.link_id(PhysPort(12)).unwrap(); - // Only IPv6 admin-scoped multicast addresses for replication table testing + // Only IPv6 admin-local multicast addresses for replication table testing let group_ip = gen_ipv6_multicast_addr(idx); - // Admin-scoped IPv6 groups are internal with replication info and members + // Admin-local IPv6 groups are internal with replication info and members let internal_entry = types::MulticastGroupCreateUnderlayEntry { - group_ip: types::AdminScopedIpv6(group_ip), + group_ip: types::UnderlayMulticastIpv6(group_ip), tag: Some(MCAST_TAG.to_string()), members: vec![ types::MulticastGroupMember { @@ -497,14 +496,19 @@ impl TableTest async fn delete_entry(switch: &Switch, idx: usize) -> OpResult<()> { let ip = IpAddr::V6(gen_ipv6_multicast_addr(idx)); - switch.client.multicast_group_delete(&ip).await + let del_tag: types::MulticastTag = + MCAST_TAG.parse().expect("tag should parse"); + switch.client.multicast_group_delete(&ip, &del_tag).await } async fn count_entries(switch: &Switch) -> usize { // Count only underlay groups with our test tag (since this tests replication table capacity) switch .client - .multicast_groups_list_by_tag_stream(MCAST_TAG, None) + .multicast_groups_list_by_tag_stream( + &MCAST_TAG.parse::().unwrap(), + None, + ) .try_collect::>() .await .expect("Should be able to list groups by tag paginated") diff --git a/dpd-types/src/link.rs b/dpd-types/src/link.rs index 622e473..20d78a4 100644 --- a/dpd-types/src/link.rs +++ b/dpd-types/src/link.rs @@ -2,9 +2,9 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/ // -// Copyright 2025 Oxide Computer Company +// Copyright 2026 Oxide Computer Company -use std::fmt; +use std::{fmt, str::FromStr}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -15,6 +15,8 @@ use crate::fault::Fault; /// /// A switch port identified by a [`PortId`] may have multiple links within it, /// each identified by a `LinkId`. These are unique within a switch port only. +/// +/// [`PortId`]: common::ports::PortId #[derive( Clone, Copy, @@ -54,6 +56,14 @@ impl fmt::Display for LinkId { } } +impl FromStr for LinkId { + type Err = std::num::ParseIntError; + + fn from_str(s: &str) -> Result { + s.parse::().map(LinkId) + } +} + /// The state of a data link with a peer. #[derive(Clone, Deserialize, JsonSchema, PartialEq, Serialize)] #[serde(rename_all = "snake_case")] diff --git a/dpd-types/src/mcast.rs b/dpd-types/src/mcast.rs index cf90b5f..c4acef3 100644 --- a/dpd-types/src/mcast.rs +++ b/dpd-types/src/mcast.rs @@ -2,17 +2,18 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/ // -// Copyright 2025 Oxide Computer Company +// Copyright 2026 Oxide Computer Company //! Public types for multicast group management. use std::{ fmt, net::{IpAddr, Ipv6Addr}, + str::FromStr, }; use common::{nat::NatTarget, ports::PortId}; -use oxnet::{Ipv4Net, Ipv6Net}; +use omicron_common::address::UNDERLAY_MULTICAST_SUBNET; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -21,10 +22,11 @@ use crate::link::LinkId; /// Type alias for multicast group IDs. pub type MulticastGroupId = u16; -/// A validated admin-scoped IPv6 multicast address. +/// A validated underlay multicast IPv6 address. /// -/// Admin-scoped addresses are ff04::/16, ff05::/16, or ff08::/16. -/// These are used for internal/underlay multicast groups. +/// Underlay multicast addresses must be within the subnet allocated by Omicron +/// for rack-internal multicast traffic (ff04::/64). This is a subset of the +/// admin-local scope (ff04::/16) defined in RFC 4291. #[derive( Clone, Copy, @@ -39,19 +41,20 @@ pub type MulticastGroupId = u16; JsonSchema, )] #[serde(try_from = "Ipv6Addr", into = "Ipv6Addr")] -pub struct AdminScopedIpv6(Ipv6Addr); +pub struct UnderlayMulticastIpv6(Ipv6Addr); -impl AdminScopedIpv6 { - /// Create a new AdminScopedIpv6 if the address is admin-scoped. +impl UnderlayMulticastIpv6 { + /// Create a new UnderlayMulticastIpv6 if the address is within the + /// underlay multicast subnet (ff04::/64). pub fn new(addr: Ipv6Addr) -> Result { - if !Ipv6Net::new_unchecked(addr, 128).is_admin_scoped_multicast() { - return Err(Error::InvalidIp(addr)); + if !UNDERLAY_MULTICAST_SUBNET.contains(addr) { + return Err(Error::InvalidUnderlayMulticastIp(addr)); } Ok(Self(addr)) } } -impl TryFrom for AdminScopedIpv6 { +impl TryFrom for UnderlayMulticastIpv6 { type Error = Error; fn try_from(addr: Ipv6Addr) -> Result { @@ -59,40 +62,54 @@ impl TryFrom for AdminScopedIpv6 { } } -impl From for Ipv6Addr { - fn from(admin: AdminScopedIpv6) -> Self { - admin.0 +impl From for Ipv6Addr { + fn from(addr: UnderlayMulticastIpv6) -> Self { + addr.0 } } -impl From for IpAddr { - fn from(admin: AdminScopedIpv6) -> Self { - IpAddr::V6(admin.0) +impl From for IpAddr { + fn from(addr: UnderlayMulticastIpv6) -> Self { + IpAddr::V6(addr.0) } } -impl fmt::Display for AdminScopedIpv6 { +impl fmt::Display for UnderlayMulticastIpv6 { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.0.fmt(f) } } +impl FromStr for UnderlayMulticastIpv6 { + type Err = Error; + + fn from_str(s: &str) -> Result { + let addr: Ipv6Addr = s + .parse() + .map_err(|e| Error::InvalidIpv6Address(s.to_string(), e))?; + Self::new(addr) + } +} + /// Source filter match key for multicast traffic. +/// +/// For SSM groups, use `Exact` with specific source addresses. +/// For ASM groups with any-source filtering, use `Any`. #[derive( Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize, JsonSchema, )] pub enum IpSrc { /// Exact match for the source IP address. Exact(IpAddr), - /// Subnet match for the source IP address. - Subnet(Ipv4Net), + /// Match any source address (0.0.0.0/0 or ::/0 depending on group IP version). + Any, } impl fmt::Display for IpSrc { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { IpSrc::Exact(ip) => write!(f, "{ip}"), - IpSrc::Subnet(subnet) => write!(f, "{subnet}"), + IpSrc::Any => write!(f, "any"), } } } @@ -101,7 +118,9 @@ impl fmt::Display for IpSrc { /// groups. #[derive(Debug, Deserialize, Serialize, JsonSchema)] pub struct MulticastGroupCreateUnderlayEntry { - pub group_ip: AdminScopedIpv6, + pub group_ip: UnderlayMulticastIpv6, + /// Tag for validating update/delete requests. If a tag is not provided, + /// one is auto-generated as `{uuid}:{group_ip}`. pub tag: Option, pub members: Vec, } @@ -111,6 +130,8 @@ pub struct MulticastGroupCreateUnderlayEntry { #[derive(Debug, Deserialize, Serialize, JsonSchema)] pub struct MulticastGroupCreateExternalEntry { pub group_ip: IpAddr, + /// Tag for validating update/delete requests. If a tag is not provided, + /// one is auto-generated as `{uuid}:{group_ip}`. pub tag: Option, pub internal_forwarding: InternalForwarding, pub external_forwarding: ExternalForwarding, @@ -119,40 +140,46 @@ pub struct MulticastGroupCreateExternalEntry { /// Represents a multicast replication entry for PUT requests for internal /// (to the rack) groups. +/// +/// Tag validation is performed via the `tag` query parameter. #[derive(Debug, Deserialize, Serialize, JsonSchema)] pub struct MulticastGroupUpdateUnderlayEntry { - pub tag: Option, pub members: Vec, } /// A multicast group update entry for PUT requests for external (to the rack) /// groups. +/// +/// Tag validation is performed via the `tag` query parameter. #[derive(Debug, Deserialize, Serialize, JsonSchema)] pub struct MulticastGroupUpdateExternalEntry { - pub tag: Option, pub internal_forwarding: InternalForwarding, pub external_forwarding: ExternalForwarding, pub sources: Option>, } /// Response structure for underlay/internal multicast group operations. -/// These groups handle admin-scoped IPv6 multicast with full replication. +/// These groups handle admin-local IPv6 multicast with full replication. #[derive(Debug, Deserialize, Serialize, JsonSchema)] pub struct MulticastGroupUnderlayResponse { - pub group_ip: AdminScopedIpv6, + pub group_ip: UnderlayMulticastIpv6, pub external_group_id: MulticastGroupId, pub underlay_group_id: MulticastGroupId, - pub tag: Option, + /// Tag for validating update/delete requests. Always present and generated + /// as `{uuid}:{group_ip}` if not provided at creation time. + pub tag: String, pub members: Vec, } /// Response structure for external multicast group operations. -/// These groups handle IPv4 and non-admin IPv6 multicast via NAT targets. +/// These groups handle IPv4 and non-admin-local IPv6 multicast via NAT targets. #[derive(Debug, Deserialize, Serialize, JsonSchema)] pub struct MulticastGroupExternalResponse { pub group_ip: IpAddr, pub external_group_id: MulticastGroupId, - pub tag: Option, + /// Tag for validating update/delete requests. Always present and generated + /// as `{uuid}:{group_ip}` if not provided at creation time. + pub tag: String, pub internal_forwarding: InternalForwarding, pub external_forwarding: ExternalForwarding, pub sources: Option>, @@ -174,6 +201,14 @@ impl MulticastGroupResponse { Self::External(resp) => resp.group_ip, } } + + /// Get the tag. + pub fn tag(&self) -> &str { + match self { + Self::Underlay(resp) => &resp.tag, + Self::External(resp) => &resp.tag, + } + } } /// Represents the NAT target for multicast traffic for internal/underlay @@ -214,7 +249,9 @@ pub enum Direction { #[derive(Clone, Debug, thiserror::Error)] pub enum Error { #[error( - "Address {0} is not admin-scoped (must be ff04::/16, ff05::/16, or ff08::/16)" + "Address {0} is not in underlay multicast subnet (must be ff04::/64)" )] - InvalidIp(Ipv6Addr), + InvalidUnderlayMulticastIp(Ipv6Addr), + #[error("Invalid IPv6 address '{0}': {1}")] + InvalidIpv6Address(String, std::net::AddrParseError), } diff --git a/dpd/p4/constants.p4 b/dpd/p4/constants.p4 index 860d995..8ebe0b7 100644 --- a/dpd/p4/constants.p4 +++ b/dpd/p4/constants.p4 @@ -2,7 +2,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/ // -// Copyright 2025 Oxide Computer Company +// Copyright 2026 Oxide Computer Company const bit<16> L2_ISOLATED_FLAG = 0x8000; @@ -54,15 +54,12 @@ const bit<2> MULTICAST_TAG_EXTERNAL = 0; const bit<2> MULTICAST_TAG_UNDERLAY = 1; const bit<2> MULTICAST_TAG_UNDERLAY_EXTERNAL = 2; -/* IPv6 Address Mask Constants */ -const bit<128> IPV6_SCOPE_MASK = 0xffff0000000000000000000000000000; // Match ff00::/16 -const bit<128> IPV6_ULA_MASK = 0xff000000000000000000000000000000; // Match fd00::/8 - -/* IPv6 Address Pattern Constants */ -const bit<128> IPV6_ADMIN_LOCAL_PATTERN = 0xff040000000000000000000000000000; // ff04::/16 -const bit<128> IPV6_SITE_LOCAL_PATTERN = 0xff050000000000000000000000000000; // ff05::/16 -const bit<128> IPV6_ORG_SCOPE_PATTERN = 0xff080000000000000000000000000000; // ff08::/16 -const bit<128> IPV6_ULA_PATTERN = 0xfd000000000000000000000000000000; // fd00::/8 +/* IPv6 Address Mask and Pattern Constants */ +// Reserved underlay multicast subnet (ff04::/64). This /64 within admin-local +// scope is reserved for internal underlay multicast allocation. Customer +// external groups may use other admin-local /64s (e.g., ff04:0:0:1::/64). +const bit<128> IPV6_UNDERLAY_MASK = 0xffffffffffffffff0000000000000000; // /64 prefix mask +const bit<128> IPV6_UNDERLAY_MULTICAST_PATTERN = 0xff040000000000000000000000000000; // ff04::/64 /* Reasons a packet may be dropped by the p4 pipeline */ const bit<8> DROP_IPV4_SWITCH_ADDR_MISS = 0x01; diff --git a/dpd/p4/metadata.p4 b/dpd/p4/metadata.p4 index e8c29d7..26eae6b 100644 --- a/dpd/p4/metadata.p4 +++ b/dpd/p4/metadata.p4 @@ -56,7 +56,7 @@ struct sidecar_egress_meta_t { bit<8> drop_reason; // reason a packet was dropped bridge_h bridge_hdr; // bridge header - // 256-bit port bitmap separated across 8 x 32-bit values + // 256-bit port bitmap for decap filtering, split across 8 x 32-bit fields. bit<32> decap_ports_0; // Ports 0-31 bit<32> decap_ports_1; // Ports 32-63 bit<32> decap_ports_2; // Ports 64-95 diff --git a/dpd/p4/port_bitmap_check.p4 b/dpd/p4/port_bitmap_check.p4 index 75a79ca..cf739f7 100644 --- a/dpd/p4/port_bitmap_check.p4 +++ b/dpd/p4/port_bitmap_check.p4 @@ -4,6 +4,54 @@ // // Copyright 2025 Oxide Computer Company +// Port Bitmap Check Table +// +// Per-port decapsulation filter for multicast egress. Included via +// `#include ` in MulticastEgress (see sidecar.p4). +// +// # Bitmap Structure +// +// 256-port bitmap split across 8 x 32-bit metadata fields: +// +// decap_ports_0: ports 0-31 (bit N = port N) +// decap_ports_1: ports 32-63 (bit N = port 32+N) +// decap_ports_2: ports 64-95 (bit N = port 64+N) +// decap_ports_3: ports 96-127 (bit N = port 96+N) +// decap_ports_4: ports 128-159 (bit N = port 128+N) +// decap_ports_5: ports 160-191 (bit N = port 160+N) +// decap_ports_6: ports 192-223 (bit N = port 192+N) +// decap_ports_7: ports 224-255 (bit N = port 224+N) +// +// # Design +// +// The table has const entries mapping each port (0-255) to an action that: +// 1. Selects the correct 32-bit segment (decap_ports_N); +// 2. Bitwise ANDs it with a single-bit mask for that port's position; +// 3. Then, stores result in meta.bitmap_result +// +// Prerequisite: `meta.port_number` is populated by the MulticastEgress +// `asic_id_to_port` table (keyed by `eg_intr_md.egress_port`) prior to +// invoking `port_bitmap_check.apply()`. +// +// If bitmap_result != 0, the port is in the decap set and outer headers +// are stripped (Geneve decapsulation). Otherwise, the packet is forwarded +// with encapsulation intact. +// +// # Use Case +// +// Multicast traffic replicated towards sleds remains encapsulated (OPTE on the +// destination sled handles decap). Traffic bound for customer networks (front +// panel ports) is decapsulated here before egress. The bitmap marks which ports +// require decapsulation (external/customer-facing) vs which keep encapsulation +// (underlay/sled-bound). +// +// ## Example +// Group with external ports 5, 12, 47 requiring decap: +// decap_ports_0 = 0x00001020 (bits 5 and 12 set) +// decap_ports_1 = 0x00008000 (bit 15 set = port 47) +// decap_ports_2..7 = 0x00000000 +// ... + action check_port_bitmap_0(bit<32> bit_mask) { meta.bitmap_result = meta.decap_ports_0 & bit_mask; } diff --git a/dpd/p4/sidecar.p4 b/dpd/p4/sidecar.p4 index 3f30fab..c2de0ed 100644 --- a/dpd/p4/sidecar.p4 +++ b/dpd/p4/sidecar.p4 @@ -2,7 +2,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/ // -// Copyright 2025 Oxide Computer Company +// Copyright 2026 Oxide Computer Company #if __TARGET_TOFINO__ == 2 #include @@ -877,7 +877,7 @@ control RouterLookupIndex6( res.nexthop = 0; index_ctr.count(); } - + /* * The select_route table contains 2048 pre-computed entries. * It lives in another file just to keep this one manageable. @@ -1388,7 +1388,7 @@ control MulticastRouter6 ( // to go out. } else { // Set the destination port to an invalid value - ig_tm_md.ucast_egress_port = (PortId_t)0x1ff; + ig_tm_md.ucast_egress_port = (PortId_t)0x1ff; hdr.ipv6.hop_limit = hdr.ipv6.hop_limit - 1; } } @@ -1638,7 +1638,7 @@ control MulticastIngress ( table mcast_source_filter_ipv6 { key = { - hdr.inner_ipv6.src_addr: exact; + hdr.inner_ipv6.src_addr: lpm; hdr.inner_ipv6.dst_addr: exact; } actions = { @@ -1699,19 +1699,20 @@ control MulticastIngress ( // Note: SSM tables currently take one extra stage in the pipeline (17->18). apply { if (hdr.geneve.isValid() && hdr.inner_ipv4.isValid()) { - // Check if the inner destination address is an IPv4 SSM multicast - // address. - if (hdr.inner_ipv4.dst_addr[31:24] == 8w0xe8) { + // Check if the inner destination address is an IPv4 multicast + // address (224.0.0.0/4). Apply source filtering for both SSM + // (232.0.0.0/8) and ASM ranges. + if (hdr.inner_ipv4.dst_addr[31:28] == 4w0xe) { mcast_source_filter_ipv4.apply(); } else { meta.allow_source_mcast = true; } } else if (hdr.geneve.isValid() && hdr.inner_ipv6.isValid()) { - // Check if the inner destination address is an IPv6 SSM multicast - // address. - if ((hdr.inner_ipv6.dst_addr[127:120] == 8w0xff) - && ((hdr.inner_ipv6.dst_addr[119:116] == 4w0x3))) { - mcast_source_filter_ipv6.apply(); + // Check if the inner destination address is an IPv6 multicast + // address (ff00::/8). Apply source filtering for both SSM + // (ff3x::/16) and ASM ranges. + if (hdr.inner_ipv6.dst_addr[127:120] == 8w0xff) { + mcast_source_filter_ipv6.apply(); } else { meta.allow_source_mcast = true; } @@ -1729,10 +1730,25 @@ control MulticastIngress ( } -/* This control is used to configure the egress port for multicast packets. - * It includes actions for setting the decap ports bitmap and VLAN ID - * (if necessary), as well as stripping headers and decrementing TTL or hop - * limit. +/* Multicast Egress - Per-Port Decapsulation + * + * Determines which replicated multicast copies should be decapsulated. + * Traffic bound for sleds remains encapsulated (OPTE on the destination sled + * handles decap). Traffic exiting via front panel ports may be decapsulated + * based on the per-group bitmap configuration. + * + * Flow: + * 1. mcast_tag_check : Match packets with reserved underlay multicast + * subnet (ff04::/64, within admin-local ff04::/16) + * and mcast_tag == UNDERLAY_EXTERNAL + * 2. tbl_decap_ports : Lookup by egress_rid to get 256-port decap bitmap + * 3. asic_id_to_port : Map ASIC port ID to logical port number (0-255) + * 4. port_bitmap_check : Test port's bit in bitmap (see port_bitmap_check.p4) + * 5. modify_hdr : If bitmap_result != 0, decapsulate packet (strip + * outer headers, decrement TTL/hop limit, handle VLAN) + * + * The bitmap marks which egress ports require decapsulation (typically external + * customer-facing ports) vs which keep encapsulation (underlay/sled-bound). */ control MulticastEgress ( inout sidecar_headers_t hdr, @@ -1768,6 +1784,12 @@ control MulticastEgress ( } + // Check if packet is destined to the reserved underlay multicast subnet + // (ff04::/64, within admin-local scope ff04::/16) with UNDERLAY_EXTERNAL tag. + // This determines whether decap/bitmap processing should occur. + // + // Uses a table rather than inline control flow due to Tofino PHV input + // limits on complex conditions. table mcast_tag_check { key = { hdr.ipv6.isValid(): exact; @@ -1780,22 +1802,10 @@ control MulticastEgress ( actions = { NoAction; } const entries = { - // Admin-local (scope value 4): Matches IPv6 multicast addresses - // with scope ff04::/16 - ( true, IPV6_ADMIN_LOCAL_PATTERN &&& IPV6_SCOPE_MASK, true, true, 2 ) : NoAction; - // Site-local (scope value 5): Matches IPv6 multicast addresses with - // scope ff05::/16 - ( true, IPV6_SITE_LOCAL_PATTERN &&& IPV6_SCOPE_MASK, true, true, 2 ) : NoAction; - // Organization-local (scope value 8): Matches IPv6 multicast - // addresses with scope ff08::/16 - ( true, IPV6_ORG_SCOPE_PATTERN &&& IPV6_SCOPE_MASK, true, true, 2 ) : NoAction; - // ULA (Unique Local Address): Matches IPv6 addresses that start - // with fc00::/7. This is not a multicast address, but it is used - // for other internal routing purposes. - ( true, IPV6_ULA_PATTERN &&& IPV6_ULA_MASK, true, true, 2 ) : NoAction; + ( true, IPV6_UNDERLAY_MULTICAST_PATTERN &&& IPV6_UNDERLAY_MASK, true, true, MULTICAST_TAG_UNDERLAY_EXTERNAL ) : NoAction; } - const size = 4; + const size = 1; } table tbl_decap_ports { diff --git a/dpd/src/api_server.rs b/dpd/src/api_server.rs index 8d94e64..302bfac 100644 --- a/dpd/src/api_server.rs +++ b/dpd/src/api_server.rs @@ -2,7 +2,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/ // -// Copyright 2025 Oxide Computer Company +// Copyright 2026 Oxide Computer Company //! Dendrite HTTP API types and endpoint functions. @@ -24,6 +24,7 @@ use dpd_types::mcast::MulticastGroupResponse; use dpd_types::mcast::MulticastGroupUnderlayResponse; use dpd_types::mcast::MulticastGroupUpdateExternalEntry; use dpd_types::mcast::MulticastGroupUpdateUnderlayEntry; +use dpd_types::mcast::UnderlayMulticastIpv6; use dpd_types::oxstats::OximeterMetadata; use dpd_types::port_map::BackplaneLink; use dpd_types::route::Ipv4Route; @@ -1883,11 +1884,32 @@ impl DpdApi for DpdApiImpl { async fn multicast_group_delete( rqctx: RequestContext>, path: Path, + query: Query, ) -> Result { let switch: &Switch = rqctx.context(); let ip = path.into_inner().group_ip; + let tag = query.into_inner().tag; - mcast::del_group(switch, ip) + mcast::del_group(switch, ip, tag.as_ref()) + .map(|_| HttpResponseDeleted()) + .map_err(HttpError::from) + } + + async fn multicast_group_delete_v3( + rqctx: RequestContext>, + path: Path, + ) -> Result { + let switch: &Switch = rqctx.context(); + let ip = path.into_inner().group_ip; + + // For backward compat: lookup current tag to pass validation + // (v1-v3 API did not require tag, so we allow deletion without caller knowing it) + let existing_tag = mcast::get_group(switch, ip) + .map_err(HttpError::from)? + .tag() + .to_string(); + + mcast::del_group(switch, ip, &existing_tag) .map(|_| HttpResponseDeleted()) .map_err(HttpError::from) } @@ -1918,13 +1940,54 @@ impl DpdApi for DpdApiImpl { async fn multicast_group_update_underlay( rqctx: RequestContext>, path: Path, + query: Query, group: TypedBody, ) -> Result, HttpError> { let switch: &Switch = rqctx.context(); let admin_scoped = path.into_inner().group_ip; + let tag = query.into_inner().tag; - mcast::modify_group_internal(switch, admin_scoped, group.into_inner()) - .map(HttpResponseOk) + mcast::modify_group_internal( + switch, + admin_scoped, + tag.as_ref(), + group.into_inner(), + ) + .map(HttpResponseOk) + .map_err(HttpError::from) + } + + async fn multicast_group_update_underlay_v3( + rqctx: RequestContext>, + path: Path, + group: TypedBody, + ) -> Result< + HttpResponseOk, + HttpError, + > { + let switch: &Switch = rqctx.context(); + let admin_scoped = path.into_inner().group_ip; + let underlay = + UnderlayMulticastIpv6::try_from(admin_scoped).map_err(|e| { + HttpError::for_bad_request( + None, + format!("invalid group_ip: {e}"), + ) + })?; + let entry = group.into_inner(); + + // Lookup current tag for backward compat (v1-v3 clients may omit tag) + let tag = match entry.tag.as_ref() { + Some(t) => t.clone(), + None => { + mcast::get_group_internal(switch, underlay) + .map_err(HttpError::from)? + .tag + } + }; + + mcast::modify_group_internal(switch, underlay, &tag, entry.into()) + .map(|resp| HttpResponseOk(resp.into())) .map_err(HttpError::from) } @@ -1933,9 +1996,9 @@ impl DpdApi for DpdApiImpl { path: Path, ) -> Result, HttpError> { let switch: &Switch = rqctx.context(); - let admin_scoped = path.into_inner().group_ip; + let underlay = path.into_inner().group_ip; - mcast::get_group_internal(switch, admin_scoped) + mcast::get_group_internal(switch, underlay) .map(HttpResponseOk) .map_err(HttpError::from) } @@ -1943,15 +2006,68 @@ impl DpdApi for DpdApiImpl { async fn multicast_group_update_external( rqctx: RequestContext>, path: Path, + query: Query, group: TypedBody, - ) -> Result, HttpError> - { + ) -> Result, HttpError> { let switch: &Switch = rqctx.context(); let entry = group.into_inner(); let ip = path.into_inner().group_ip; + let tag = query.into_inner().tag; - mcast::modify_group_external(switch, ip, entry) - .map(HttpResponseCreated) + mcast::modify_group_external(switch, ip, tag.as_ref(), entry) + .map(HttpResponseOk) + .map_err(HttpError::from) + } + + async fn multicast_group_update_external_v3( + rqctx: RequestContext>, + path: Path, + group: TypedBody, + ) -> Result< + HttpResponseCreated, + HttpError, + > { + let switch: &Switch = rqctx.context(); + let ip = path.into_inner().group_ip; + let entry = group.into_inner(); + + // Lookup current tag for backward compat (v3 clients may omit tag) + let tag = match entry.tag.as_ref() { + Some(t) => t.clone(), + None => mcast::get_group(switch, ip) + .map_err(HttpError::from)? + .tag() + .to_string(), + }; + + mcast::modify_group_external(switch, ip, &tag, entry.into()) + .map(|resp| HttpResponseCreated(resp.into())) + .map_err(HttpError::from) + } + + async fn multicast_group_update_external_v2( + rqctx: RequestContext>, + path: Path, + group: TypedBody, + ) -> Result< + HttpResponseCreated, + HttpError, + > { + let switch: &Switch = rqctx.context(); + let ip = path.into_inner().group_ip; + let entry = group.into_inner(); + + // Lookup current tag for backward compat (v1-v2 clients may omit tag) + let tag = match entry.tag.as_ref() { + Some(t) => t.clone(), + None => mcast::get_group(switch, ip) + .map_err(HttpError::from)? + .tag() + .to_string(), + }; + + mcast::modify_group_external(switch, ip, &tag, entry.into()) + .map(|resp| HttpResponseCreated(resp.into())) .map_err(HttpError::from) } @@ -1992,14 +2108,14 @@ impl DpdApi for DpdApiImpl { async fn multicast_groups_list_by_tag( rqctx: RequestContext>, - path: Path, + path: Path, query_params: Query< PaginationParams, >, ) -> Result>, HttpError> { let switch: &Switch = rqctx.context(); - let tag = path.into_inner().tag; + let tag: String = path.into_inner().tag.into(); let pag_params = query_params.into_inner(); let Ok(limit) = usize::try_from(rqctx.page_limit(&pag_params)?.get()) @@ -2028,10 +2144,10 @@ impl DpdApi for DpdApiImpl { async fn multicast_reset_by_tag( rqctx: RequestContext>, - path: Path, + path: Path, ) -> Result { let switch: &Switch = rqctx.context(); - let tag = path.into_inner().tag; + let tag: String = path.into_inner().tag.into(); mcast::reset_tag(switch, &tag) .map(|_| HttpResponseDeleted()) @@ -2039,6 +2155,22 @@ impl DpdApi for DpdApiImpl { } async fn multicast_reset_untagged( + _rqctx: RequestContext>, + ) -> Result { + // All groups now have default tags, making this endpoint obsolete. + // Groups are cleaned up via multicast_reset_by_tag using the tag + // returned from group creation. + Err(HttpError::for_client_error( + None, + ClientErrorStatusCode::GONE, + "multicast_reset_untagged is deprecated; all groups now have \ + default tags. Use multicast_reset_by_tag with the tag returned \ + from group creation." + .to_string(), + )) + } + + async fn multicast_reset_untagged_v3( rqctx: RequestContext>, ) -> Result { let switch: &Switch = rqctx.context(); diff --git a/dpd/src/mcast/mod.rs b/dpd/src/mcast/mod.rs index 801b179..42a6c9a 100644 --- a/dpd/src/mcast/mod.rs +++ b/dpd/src/mcast/mod.rs @@ -2,7 +2,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/ // -// Copyright 2025 Oxide Computer Company +// Copyright 2026 Oxide Computer Company //! Multicast group management and configuration. //! @@ -37,20 +37,24 @@ //! The multicast implementation uses a bifurcated design that separates //! external (customer) and (internal) underlay traffic: //! -//! 1. External-only groups (IPv4 and non-admin-scoped IPv6): +//! 1. External-only groups (IPv4 and non-admin-local IPv6): //! - Created from API control plane IPs for customer traffic //! - Handle customer traffic to/from outside the rack //! - Use the external multicast API (/multicast/external-groups) //! - Must have NAT targets pointing to internal groups for proper forwarding //! -//! 2. Internal groups (admin-scoped IPv6 multicast): -//! - Admin-scoped = admin-local, site-local, or organization-local scope (RFC 7346, RFC 4291) +//! 2. Internal groups (admin-local IPv6 multicast): +//! - Admin-local = scope 4 (ff04::/16) as defined in +//! [RFC 7346] and [RFC 4291] //! - Geneve encapsulated multicast traffic (NAT targets of external-only groups) //! - Use the internal multicast API (/multicast/underlay-groups) //! - Can replicate to: //! a) External group members (customer traffic) //! b) Underlay-only members (infrastructure traffic) //! c) Both external and underlay members (bifurcated replication) +//! +//! [RFC 7346]: https://www.rfc-editor.org/rfc/rfc7346.html +//! [RFC 4291]: https://www.rfc-editor.org/rfc/rfc4291.html use std::{ collections::{BTreeMap, HashSet}, @@ -64,16 +68,17 @@ use common::{nat::NatTarget, ports::PortId}; use dpd_types::{ link::LinkId, mcast::{ - AdminScopedIpv6, Direction, ExternalForwarding, InternalForwarding, - IpSrc, MulticastGroupCreateExternalEntry, - MulticastGroupCreateUnderlayEntry, MulticastGroupExternalResponse, - MulticastGroupId, MulticastGroupMember, MulticastGroupResponse, - MulticastGroupUnderlayResponse, MulticastGroupUpdateExternalEntry, - MulticastGroupUpdateUnderlayEntry, + Direction, ExternalForwarding, InternalForwarding, IpSrc, + MulticastGroupCreateExternalEntry, MulticastGroupCreateUnderlayEntry, + MulticastGroupExternalResponse, MulticastGroupId, MulticastGroupMember, + MulticastGroupResponse, MulticastGroupUnderlayResponse, + MulticastGroupUpdateExternalEntry, MulticastGroupUpdateUnderlayEntry, + UnderlayMulticastIpv6, }, }; -use oxnet::Ipv4Net; +use oxnet::{Ipv4Net, Ipv6Net}; use slog::{debug, error, warn}; +use uuid::Uuid; use crate::{ Switch, table, @@ -86,7 +91,7 @@ mod validate; use rollback::{GroupCreateRollbackContext, GroupUpdateRollbackContext}; use validate::{ validate_multicast_address, validate_nat_target, - validate_not_admin_scoped_ipv6, + validate_not_underlay_subnet, validate_tag, validate_tag_format, }; #[derive(Debug)] @@ -140,7 +145,9 @@ struct MulticastReplicationInfo { pub(crate) struct MulticastGroup { external_scoped_group: ScopedGroupId, underlay_scoped_group: ScopedGroupId, - pub(crate) tag: Option, + /// Tag for validating update/delete requests. Always present and generated + /// as `{uuid}:{group_ip}` if not provided at creation time. + pub(crate) tag: String, pub(crate) int_fwding: InternalForwarding, pub(crate) ext_fwding: ExternalForwarding, pub(crate) sources: Option>, @@ -181,7 +188,7 @@ impl MulticastGroup { fn to_underlay_response( &self, - group_ip: AdminScopedIpv6, + group_ip: UnderlayMulticastIpv6, ) -> MulticastGroupUnderlayResponse { MulticastGroupUnderlayResponse { group_ip, @@ -198,8 +205,8 @@ impl MulticastGroup { self.to_external_response(group_ip), ), IpAddr::V6(ipv6) => { - // Try to create AdminScopedIpv6 - if successful, it's an underlay group - match AdminScopedIpv6::new(ipv6) { + // Try to create UnderlayMulticastIpv6 - if successful, it's an underlay group + match UnderlayMulticastIpv6::new(ipv6) { Ok(admin_scoped) => MulticastGroupResponse::Underlay( self.to_underlay_response(admin_scoped), ), @@ -220,9 +227,9 @@ pub struct MulticastGroupData { /// Stack of available group IDs for O(1) allocation. /// Pre-populated with all IDs from GENERATOR_START to u16::MAX-1. free_group_ids: Arc>>, - /// 1:1 mapping from admin-scoped group IP to external group that uses it as NAT - /// target (admin_scoped_ip -> external_group_ip) - nat_target_refs: BTreeMap, + /// 1:1 mapping from admin-local group IP to external group that uses it as NAT + /// target (admin_local_ip -> external_group_ip) + nat_target_refs: BTreeMap, } impl MulticastGroupData { @@ -253,9 +260,12 @@ impl MulticastGroupData { /// Returns a ScopedGroupId that will automatically return the ID to the /// free pool when dropped. fn generate_group_id(&mut self) -> DpdResult { - let mut pool = self.free_group_ids.lock().unwrap(); + let mut pool = self + .free_group_ids + .lock() + .expect("group ID pool lock poisoned"); let id = pool.pop().ok_or_else(|| { - DpdError::McastGroupFailure( + DpdError::ResourceExhausted( "no free multicast group IDs available (exhausted range 100-65534)".to_string(), ) })?; @@ -263,18 +273,18 @@ impl MulticastGroupData { Ok(ScopedIdInner(id, Arc::downgrade(&self.free_group_ids)).into()) } - /// Add 1:1 forwarding reference from admin-scoped IP to external group's IP. + /// Add 1:1 forwarding reference from admin-local IP to external group's IP. fn add_forwarding_refs( &mut self, external_group_ip: IpAddr, - admin_scoped_ip: AdminScopedIpv6, + admin_scoped_ip: UnderlayMulticastIpv6, ) { self.nat_target_refs .insert(admin_scoped_ip, external_group_ip); } /// Remove 1:1 forwarding reference. - fn rm_forwarding_refs(&mut self, admin_scoped_ip: AdminScopedIpv6) { + fn rm_forwarding_refs(&mut self, admin_scoped_ip: UnderlayMulticastIpv6) { self.nat_target_refs.remove(&admin_scoped_ip); } @@ -282,7 +292,7 @@ impl MulticastGroupData { /// the referencing external group (1:1 mapping). fn get_vlan_for_internal_addr( &self, - internal_ip: AdminScopedIpv6, + internal_ip: UnderlayMulticastIpv6, ) -> Option { self.nat_target_refs .get(&internal_ip) @@ -310,7 +320,7 @@ pub(crate) fn add_group_external( // Acquire the lock to the multicast data structure at the start to ensure // deterministic operation order - let mut mcast = s.mcast.lock().unwrap(); + let mut mcast = s.mcast.lock().expect("multicast data lock poisoned"); let nat_target = group_info.internal_forwarding.nat_target.ok_or_else(|| { @@ -364,23 +374,41 @@ pub(crate) fn add_group_external( }) .map_err(|e| rollback_ctx.rollback_and_return_error(e))?; - // Validate the admin-scoped IP early to avoid partial state - let admin_scoped_ip = AdminScopedIpv6::new(nat_target.internal_ip)?; + // NOTE: If perform_vlan_propagation succeeded, it updated the internal group's + // bitmap entry with the VLAN. The remaining operations below are infallible + // so no rollback is needed. + // + // If adding fallible operations here, consider adding VLAN propagation rollback. + + // This validation already passed in validate_nat_target, so it cannot fail here + let admin_local_ip = UnderlayMulticastIpv6::new(nat_target.internal_ip)?; + + let tag = match &group_info.tag { + Some(t) => { + validate_tag_format(t)?; + t.clone() + } + None => { + let generated = generate_default_tag(group_ip); + validate_tag_format(&generated)?; + generated + } + }; let group = MulticastGroup { external_scoped_group: scoped_external_id, underlay_scoped_group: scoped_underlay_id, - tag: group_info.tag, + tag, int_fwding: group_info.internal_forwarding.clone(), ext_fwding: group_info.external_forwarding.clone(), - sources: group_info.sources.clone(), + sources: normalize_sources(group_info.sources.clone()), replication_info: None, // External groups are entry points only - actual members reside in referenced internal groups members: Vec::new(), }; mcast.groups.insert(group_ip, group.clone()); - mcast.add_forwarding_refs(group_ip, admin_scoped_ip); + mcast.add_forwarding_refs(group_ip, admin_local_ip); Ok(group.to_external_response(group_ip)) } @@ -398,7 +426,7 @@ pub(crate) fn add_group_internal( // Acquire the lock to the multicast data structure at the start to ensure // deterministic operation order - let mut mcast = s.mcast.lock().unwrap(); + let mut mcast = s.mcast.lock().expect("multicast data lock poisoned"); validate_internal_group_creation(&mcast, group_ip)?; @@ -462,11 +490,23 @@ pub(crate) fn add_group_internal( None }; + let tag = match &group_info.tag { + Some(t) => { + validate_tag_format(t)?; + t.clone() + } + None => { + let generated = generate_default_tag(group_ip.into()); + validate_tag_format(&generated)?; + generated + } + }; + // Generic internal datastructure (vs API interface) let group = MulticastGroup { external_scoped_group: scoped_external_id, underlay_scoped_group: scoped_underlay_id, - tag: group_info.tag, + tag, int_fwding: InternalForwarding { nat_target: None, // Internal groups don't have NAT targets }, @@ -485,14 +525,50 @@ pub(crate) fn add_group_internal( /// Delete a multicast group from the switch, including all associated tables /// and port mappings. -pub(crate) fn del_group(s: &Switch, group_ip: IpAddr) -> DpdResult<()> { - let mut mcast = s.mcast.lock().unwrap(); +/// +/// This operation is idempotent: deleting a non-existent group returns +/// `NotFound` rather than a tag mismatch error, making deletes safe to retry. +/// +/// # Arguments +/// +/// * `s` - Switch instance containing the multicast state. +/// * `group_ip` - IP address of the multicast group to delete. +/// * `tag` - Tag for validation. Must match the group's existing tag. +/// +/// # Errors +/// +/// Returns an error if: +/// - Attempting to delete an internal group that is still referenced by an +/// external group via NAT target +/// - The provided tag does not match the group's existing tag +pub(crate) fn del_group( + s: &Switch, + group_ip: IpAddr, + tag: &str, +) -> DpdResult<()> { + let mut mcast = s.mcast.lock().expect("multicast data lock poisoned"); + + // Check if this is an internal group referenced by an external group. + // Internal groups are identified by admin-scoped IPv6 addresses (ff04::/16). + if let IpAddr::V6(ipv6) = group_ip + && let Ok(admin_scoped) = UnderlayMulticastIpv6::new(ipv6) + && let Some(external_ip) = mcast.nat_target_refs.get(&admin_scoped) + { + return Err(DpdError::Invalid(format!( + "cannot delete internal group {group_ip}: still referenced \ + by external group {external_ip} via NAT target" + ))); + } - let group = mcast.groups.remove(&group_ip).ok_or_else(|| { + // Validate tag before removing the group. + let group_entry = mcast.groups.get(&group_ip).ok_or_else(|| { DpdError::Missing(format!( "Multicast group for IP {group_ip} not found", )) })?; + validate_tag(&group_entry.tag, tag)?; + + let group = mcast.groups.remove(&group_ip).unwrap(); let nat_target_to_remove = group .int_fwding @@ -515,19 +591,19 @@ pub(crate) fn del_group(s: &Switch, group_ip: IpAddr) -> DpdResult<()> { } if let Some(IpAddr::V6(ipv6)) = nat_target_to_remove { - mcast.rm_forwarding_refs(AdminScopedIpv6::new(ipv6)?); + mcast.rm_forwarding_refs(UnderlayMulticastIpv6::new(ipv6)?); } Ok(()) } -/// Get an internal multicast group configuration by admin-scoped IPv6 address. +/// Get an internal multicast group configuration by admin-local IPv6 address. pub(crate) fn get_group_internal( s: &Switch, - admin_scoped: AdminScopedIpv6, + admin_local: UnderlayMulticastIpv6, ) -> DpdResult { - let mcast = s.mcast.lock().unwrap(); - let group_ip = IpAddr::V6(admin_scoped.into()); + let mcast = s.mcast.lock().expect("multicast data lock poisoned"); + let group_ip = IpAddr::V6(admin_local.into()); let group = mcast.groups.get(&group_ip).ok_or_else(|| { DpdError::Missing(format!( @@ -535,7 +611,7 @@ pub(crate) fn get_group_internal( )) })?; - Ok(group.to_underlay_response(admin_scoped)) + Ok(group.to_underlay_response(admin_local)) } /// Get a multicast group configuration. @@ -543,7 +619,7 @@ pub(crate) fn get_group( s: &Switch, group_ip: IpAddr, ) -> DpdResult { - let mcast = s.mcast.lock().unwrap(); + let mcast = s.mcast.lock().expect("multicast data lock poisoned"); let group = mcast.groups.get(&group_ip).ok_or_else(|| { DpdError::Missing(format!( @@ -557,15 +633,18 @@ pub(crate) fn get_group( pub(crate) fn modify_group_external( s: &Switch, group_ip: IpAddr, + tag: &str, new_group_info: MulticastGroupUpdateExternalEntry, ) -> DpdResult { - let mut mcast = s.mcast.lock().unwrap(); + let mut mcast = s.mcast.lock().expect("multicast data lock poisoned"); - if !mcast.groups.contains_key(&group_ip) { - return Err(DpdError::Missing(format!( - "Multicast group for IP {group_ip} not found", - ))); - } + // Check existence and validate tag before making any changes + let existing_group = mcast.groups.get(&group_ip).ok_or_else(|| { + DpdError::Missing(format!( + "Multicast group for IP {group_ip} not found" + )) + })?; + validate_tag(&existing_group.tag, tag)?; let nat_target = new_group_info @@ -588,14 +667,18 @@ pub(crate) fn modify_group_external( let rollback_ctx = GroupUpdateRollbackContext::new(s, group_ip, &group_entry_for_rollback); + // Pre-compute normalized sources for rollback purposes + let normalized_sources = normalize_sources(new_group_info.sources.clone()); + // Try to update external tables first if let Err(e) = update_external_tables(s, group_ip, &group_entry, &new_group_info) { // Restore original group and return error mcast.groups.insert(group_ip, group_entry); - return Err(rollback_ctx - .rollback_external(e, new_group_info.sources.as_deref())); + return Err( + rollback_ctx.rollback_external(e, normalized_sources.as_deref()) + ); } let mut updated_group = group_entry.clone(); @@ -606,16 +689,15 @@ pub(crate) fn modify_group_external( let new_internal_ip = nat_target.internal_ip; if old_internal_ip != new_internal_ip { - mcast.rm_forwarding_refs(AdminScopedIpv6::new(old_internal_ip)?); - mcast.add_forwarding_refs( - group_ip, - AdminScopedIpv6::new(new_internal_ip)?, - ); + // Validate both IPs before mutating state to avoid partial updates + let old_admin = UnderlayMulticastIpv6::new(old_internal_ip)?; + let new_admin = UnderlayMulticastIpv6::new(new_internal_ip)?; + mcast.rm_forwarding_refs(old_admin); + mcast.add_forwarding_refs(group_ip, new_admin); } } - // Update the external group fields - updated_group.tag = new_group_info.tag.or(updated_group.tag); + // Tags are immutable (validated above, never changed) updated_group.int_fwding.nat_target = Some(nat_target); let old_vlan_id = updated_group.ext_fwding.vlan_id; @@ -623,7 +705,9 @@ pub(crate) fn modify_group_external( .external_forwarding .vlan_id .or(updated_group.ext_fwding.vlan_id); - updated_group.sources = new_group_info.sources.or(updated_group.sources); + updated_group.sources = normalize_sources( + new_group_info.sources.clone().or(updated_group.sources), + ); // Update bitmap tables with new VLAN if VLAN changed // Also, handles possible membership skew between update internal + external calls. @@ -655,9 +739,26 @@ pub(crate) fn modify_group_external( }; if let Err(e) = bitmap_result { - // Rollback the external table changes and return the error + // Rollback the external table changes and NAT target references mcast.groups.insert(group_ip, group_entry); + // Rollback NAT target references if they were changed + if let Some(old_nat) = old_nat_target { + let old_internal_ip = old_nat.internal_ip; + let new_internal_ip = nat_target.internal_ip; + + if old_internal_ip != new_internal_ip { + // Restore original references (reverse of what we did above) + if let (Ok(old_admin), Ok(new_admin)) = ( + UnderlayMulticastIpv6::new(old_internal_ip), + UnderlayMulticastIpv6::new(new_internal_ip), + ) { + mcast.rm_forwarding_refs(new_admin); + mcast.add_forwarding_refs(group_ip, old_admin); + } + } + } + error!( s.log, "failed to update bitmap table for external group {group_ip}: {e:?}" @@ -673,16 +774,20 @@ pub(crate) fn modify_group_external( pub(crate) fn modify_group_internal( s: &Switch, - group_ip: AdminScopedIpv6, + group_ip: UnderlayMulticastIpv6, + tag: &str, new_group_info: MulticastGroupUpdateUnderlayEntry, ) -> DpdResult { - let mut mcast = s.mcast.lock().unwrap(); + let mut mcast = s.mcast.lock().expect("multicast data lock poisoned"); - if !mcast.groups.contains_key(&group_ip.into()) { - return Err(DpdError::Missing(format!( - "Multicast group for IP {group_ip} not found", - ))); - } + // Check existence and validate tag before making any changes + let existing_group = + mcast.groups.get(&group_ip.into()).ok_or_else(|| { + DpdError::Missing(format!( + "Multicast group for IP {group_ip} not found" + )) + })?; + validate_tag(&existing_group.tag, tag)?; let mut group_entry = mcast.groups.remove(&group_ip.into()).unwrap(); @@ -728,9 +833,9 @@ pub(crate) fn modify_group_internal( } }; - // Early return for no-replication case - just update metadata + // Early return for no-replication case -> just update metadata + // Tags are immutable (validated above, never changed) if replication_info.is_none() { - group_entry.tag = new_group_info.tag.or(group_entry.tag.clone()); group_entry.sources = sources; group_entry.members = new_group_info.members; @@ -755,19 +860,36 @@ pub(crate) fn modify_group_internal( .map_err(|e| rollback_ctx.rollback_internal(e, &[], &[]))?; // Perform table updates - update_group_tables( + match update_group_tables( s, group_ip.into(), &group_entry, repl_info, &sources, &group_entry.sources, - ) - .map_err(|e| { - // Restore group to mcast data structure - mcast.groups.insert(group_ip.into(), group_entry.clone()); - rollback_ctx.rollback_internal(e, &added_members, &removed_members) - })?; + ) { + Ok(()) => {} + Err(DpdError::Switch(AsicError::Missing(ref msg))) => { + // ASIC entry not found -> don't restore soft state, let the + // caller recreate. When omicron gets 404, it will CREATE fresh. + warn!( + s.log, + "ASIC entry missing during update, removing group from soft state"; + "group_ip" => %group_ip, + "error" => %msg, + ); + return Err(DpdError::Switch(AsicError::Missing(msg.clone()))); + } + Err(e) => { + // Other error - restore group and rollback + mcast.groups.insert(group_ip.into(), group_entry.clone()); + return Err(rollback_ctx.rollback_internal( + e, + &added_members, + &removed_members, + )); + } + } let filter_by_direction = |members: &[MulticastGroupMember], direction: Direction| { @@ -788,22 +910,34 @@ pub(crate) fn modify_group_internal( // VLAN mapping maintained via add_forwarding_refs/rm_forwarding_refs let external_group_vlan_id = mcast.get_vlan_for_internal_addr(group_ip); - update_internal_group_bitmap_tables( + match update_internal_group_bitmap_tables( s, group_entry.external_group_id(), &new_group_info.members, &group_entry.members, external_group_vlan_id, - ) - .map_err(|e| { - // Restore group to mcast data structure - mcast.groups.insert(group_ip.into(), group_entry.clone()); - rollback_ctx.rollback_and_restore(e) - })?; + ) { + Ok(()) => {} + Err(DpdError::Switch(AsicError::Missing(ref msg))) => { + // ASIC entry not found -> don't restore soft state + warn!( + s.log, + "ASIC bitmap entry missing during update, removing group from soft state"; + "group_ip" => %group_ip, + "error" => %msg, + ); + return Err(DpdError::Switch(AsicError::Missing(msg.clone()))); + } + Err(e) => { + // Other error - restore group and rollback + mcast.groups.insert(group_ip.into(), group_entry.clone()); + return Err(rollback_ctx.rollback_and_restore(e)); + } + } } // Update group metadata and return success - group_entry.tag = new_group_info.tag.or(group_entry.tag.clone()); + // Tags are immutable (validated above, never changed) group_entry.sources = sources; group_entry.replication_info = replication_info; group_entry.members = new_group_info.members; @@ -821,7 +955,7 @@ pub(crate) fn get_range( limit: usize, tag: Option<&str>, ) -> Vec { - let mcast = s.mcast.lock().unwrap(); + let mcast = s.mcast.lock().expect("multicast data lock poisoned"); let lower_bound = match last { None => Bound::Unbounded, @@ -833,9 +967,7 @@ pub(crate) fn get_range( .range((lower_bound, Bound::Unbounded)) .filter(|&(_ip, group)| { // Filter by tag if specified - tag.is_none_or(|tag_filter| { - group.tag.as_deref() == Some(tag_filter) - }) + tag.is_none_or(|tag_filter| group.tag == tag_filter) }) .map(|(ip, group)| group.to_response(*ip)) .take(limit) @@ -844,19 +976,22 @@ pub(crate) fn get_range( /// Reset all multicast groups (and associated routes) for a given tag. pub(crate) fn reset_tag(s: &Switch, tag: &str) -> DpdResult<()> { - let groups_to_delete = { - let mcast = s.mcast.lock().unwrap(); + let (external_groups, internal_groups) = { + let mcast = s.mcast.lock().expect("multicast data lock poisoned"); mcast .groups .iter() .filter_map(|(ip, group)| { - (group.tag.as_deref() == Some(tag)).then_some(*ip) + (group.tag == tag) + .then_some((*ip, group.int_fwding.nat_target.is_some())) }) - .collect::>() + .partition::, _>(|(_, is_external)| *is_external) }; - for group_ip in groups_to_delete { - if let Err(e) = del_group(s, group_ip) { + // Delete external groups first since they reference internal groups + // via NAT targets. Pass the tag for validation. + for (group_ip, _) in external_groups.into_iter().chain(internal_groups) { + if let Err(e) = del_group(s, group_ip, tag) { error!( s.log, "failed to delete multicast group for IP {group_ip}: {e:?}" @@ -869,36 +1004,19 @@ pub(crate) fn reset_tag(s: &Switch, tag: &str) -> DpdResult<()> { } /// Reset all multicast groups (and associated routes) without a tag. +/// +/// DEPRECATED: All groups have default tags generated at creation time. +/// This function is a no-op since no untagged groups can exist. +/// Retained for API version compatibility (v3 and earlier). +#[allow(unused_variables)] pub(crate) fn reset_untagged(s: &Switch) -> DpdResult<()> { - let groups_to_delete = { - let mcast = s.mcast.lock().unwrap(); - mcast - .groups - .iter() - .filter_map( - |(ip, group)| { - if group.tag.is_none() { Some(*ip) } else { None } - }, - ) - .collect::>() - }; - - for group_ip in groups_to_delete { - if let Err(e) = del_group(s, group_ip) { - error!( - s.log, - "failed to delete multicast group for IP {group_ip}: {e:?}" - ); - return Err(e); - } - } - + // All groups have tags, so there are no untagged groups to delete. Ok(()) } /// Reset all multicast groups (and associated routes). pub(crate) fn reset(s: &Switch) -> DpdResult<()> { - let mut mcast = s.mcast.lock().unwrap(); + let mut mcast = s.mcast.lock().expect("multicast data lock poisoned"); // Destroy ASIC groups let group_ids = s.asic_hdl.mc_domains(); @@ -946,7 +1064,7 @@ fn perform_vlan_propagation( ); let internal_group = mcast.groups.get(&internal_ip).ok_or_else(|| { - DpdError::McastGroupFailure(format!( + DpdError::Missing(format!( "internal group not found during VLAN propagation: \ internal_group={internal_ip}, external_group={group_ip}" )) @@ -991,26 +1109,31 @@ fn remove_ipv4_source_filters( ipv4: Ipv4Addr, sources: Option<&[IpSrc]>, ) -> DpdResult<()> { - if let Some(srcs) = sources { - for src in srcs { - match src { - IpSrc::Exact(IpAddr::V4(src)) => { - table::mcast::mcast_src_filter::del_ipv4_entry( - s, - Ipv4Net::new(*src, 32).unwrap(), - ipv4, - )?; - } - IpSrc::Subnet(src) => { - table::mcast::mcast_src_filter::del_ipv4_entry( - s, *src, ipv4, - )?; - } - _ => {} - } - } + let Some(srcs) = sources else { + // No sources means a /0 "any source" entry was added + return table::mcast::mcast_src_filter::del_ipv4_entry( + s, + Ipv4Net::new(Ipv4Addr::UNSPECIFIED, 0).unwrap(), + ipv4, + ); + }; + + // If empty or `Any` was present, only a /0 entry was added + if srcs.is_empty() || sources_contain_any(srcs) { + return table::mcast::mcast_src_filter::del_ipv4_entry( + s, + Ipv4Net::new(Ipv4Addr::UNSPECIFIED, 0).unwrap(), + ipv4, + ); } + for src in srcs { + let prefix = match src { + IpSrc::Exact(IpAddr::V4(addr)) => Ipv4Net::new(*addr, 32).unwrap(), + _ => continue, + }; + table::mcast::mcast_src_filter::del_ipv4_entry(s, prefix, ipv4)?; + } Ok(()) } @@ -1019,28 +1142,102 @@ fn remove_ipv6_source_filters( ipv6: Ipv6Addr, sources: Option<&[IpSrc]>, ) -> DpdResult<()> { - if let Some(srcs) = sources { - for src in srcs { - if let IpSrc::Exact(IpAddr::V6(src)) = src { - table::mcast::mcast_src_filter::del_ipv6_entry(s, *src, ipv6)?; - } - } + let Some(srcs) = sources else { + // No sources means a ::/0 "any source" entry was added + return table::mcast::mcast_src_filter::del_ipv6_entry( + s, + Ipv6Net::new(Ipv6Addr::UNSPECIFIED, 0).unwrap(), + ipv6, + ); + }; + + // If empty or `Any` was present, only a ::/0 entry was added + if srcs.is_empty() || sources_contain_any(srcs) { + return table::mcast::mcast_src_filter::del_ipv6_entry( + s, + Ipv6Net::new(Ipv6Addr::UNSPECIFIED, 0).unwrap(), + ipv6, + ); } + for src in srcs { + let prefix = match src { + IpSrc::Exact(IpAddr::V6(addr)) => Ipv6Net::new(*addr, 128).unwrap(), + _ => continue, + }; + table::mcast::mcast_src_filter::del_ipv6_entry(s, prefix, ipv6)?; + } Ok(()) } -/// Add source filters for a multicast group. +/// Returns true if the source list contains `IpSrc::Any`. +/// +/// Used to optimize P4 source filter table entries. When `Any` is present, +/// we only add a single `/0` entry instead of individual entries for each +/// specific source, since `/0` allows all sources anyway. +/// +/// This handles the ASM lifecycle where a group may start with specific +/// sources (e.g., `[Exact(1.2.3.4), Exact(5.6.7.8)]`) and later have an +/// "any source" member join (becoming `[Exact(1.2.3.4), Exact(5.6.7.8), Any]`). +/// In the latter case, only the `/0` entry is added to the P4 table. +pub(super) fn sources_contain_any(sources: &[IpSrc]) -> bool { + sources.iter().any(|s| matches!(s, IpSrc::Any)) +} + +/// Generate a default tag for a multicast group if none is provided. +/// +/// Format: `{uuid}:{group_ip}` to match Omicron's tag format. +/// This ensures uniqueness across the group's lifecycle and prevents +/// tag collision when group IPs are reused after deletion. +fn generate_default_tag(group_ip: IpAddr) -> String { + format!("{}:{}", Uuid::new_v4(), group_ip) +} + +/// Multiple representations map to "allow any source" in P4: +/// - `None` (no sources specified) +/// - `Some([])` (empty source list) +/// - `Some([IpSrc::Any])` or `Some([..., IpSrc::Any, ...])` (explicit Any) +/// +/// This function normalizes all "any source" representations to `None`, +/// which is what omicron expects for ASM groups. This eliminates semantic +/// ambiguity and prevents unnecessary P4 table updates when toggling +/// between equivalent representations. +fn normalize_sources(sources: Option>) -> Option> { + match sources { + None => None, + Some(srcs) if srcs.is_empty() || sources_contain_any(&srcs) => None, + Some(srcs) => Some(srcs), + } +} + fn add_source_filters( s: &Switch, group_ip: IpAddr, sources: Option<&[IpSrc]>, ) -> DpdResult<()> { - let Some(srcs) = sources else { return Ok(()) }; - - match group_ip { - IpAddr::V4(ipv4) => add_ipv4_source_filters(s, srcs, ipv4), - IpAddr::V6(ipv6) => add_ipv6_source_filters(s, srcs, ipv6), + match (sources, group_ip) { + // No sources specified: add "any source" entry (0.0.0.0/0 or ::/0) + (None, IpAddr::V4(ipv4)) => { + table::mcast::mcast_src_filter::add_ipv4_entry( + s, + Ipv4Net::new(Ipv4Addr::UNSPECIFIED, 0).unwrap(), + ipv4, + ) + } + (None, IpAddr::V6(ipv6)) => { + table::mcast::mcast_src_filter::add_ipv6_entry( + s, + Ipv6Net::new(Ipv6Addr::UNSPECIFIED, 0).unwrap(), + ipv6, + ) + } + // Explicit sources: add the specified filter entries + (Some(srcs), IpAddr::V4(ipv4)) => { + add_ipv4_source_filters(s, srcs, ipv4) + } + (Some(srcs), IpAddr::V6(ipv6)) => { + add_ipv6_source_filters(s, srcs, ipv6) + } } } @@ -1049,24 +1246,36 @@ fn add_ipv4_source_filters( sources: &[IpSrc], dest_ip: Ipv4Addr, ) -> DpdResult<()> { + // If empty or `Any` is present, add the /0 entry to allow any source + if sources.is_empty() || sources_contain_any(sources) { + return table::mcast::mcast_src_filter::add_ipv4_entry( + s, + Ipv4Net::new(Ipv4Addr::UNSPECIFIED, 0).unwrap(), + dest_ip, + ); + } + + // Track successfully added entries for rollback on failure + let mut added_prefixes = Vec::new(); + for src in sources { - match src { - IpSrc::Exact(IpAddr::V4(src)) => { - table::mcast::mcast_src_filter::add_ipv4_entry( - s, - Ipv4Net::new(*src, 32).unwrap(), - dest_ip, - ) - } - IpSrc::Subnet(subnet) => { - table::mcast::mcast_src_filter::add_ipv4_entry( - s, *subnet, dest_ip, - ) + let prefix = match src { + IpSrc::Exact(IpAddr::V4(addr)) => Ipv4Net::new(*addr, 32).unwrap(), + _ => continue, + }; + if let Err(e) = + table::mcast::mcast_src_filter::add_ipv4_entry(s, prefix, dest_ip) + { + // Rollback: remove previously added entries + for added in added_prefixes { + let _ = table::mcast::mcast_src_filter::del_ipv4_entry( + s, added, dest_ip, + ); } - _ => Ok(()), - }?; + return Err(e); + } + added_prefixes.push(prefix); } - Ok(()) } @@ -1075,18 +1284,42 @@ fn add_ipv6_source_filters( sources: &[IpSrc], dest_ip: Ipv6Addr, ) -> DpdResult<()> { + // If empty or `Any` is present, add the ::/0 entry to allow any source + if sources.is_empty() || sources_contain_any(sources) { + return table::mcast::mcast_src_filter::add_ipv6_entry( + s, + Ipv6Net::new(Ipv6Addr::UNSPECIFIED, 0).unwrap(), + dest_ip, + ); + } + + // Track successfully added entries for rollback on failure + let mut added_prefixes = Vec::new(); + for src in sources { - if let IpSrc::Exact(IpAddr::V6(src)) = src { - table::mcast::mcast_src_filter::add_ipv6_entry(s, *src, dest_ip)?; + let prefix = match src { + IpSrc::Exact(IpAddr::V6(addr)) => Ipv6Net::new(*addr, 128).unwrap(), + _ => continue, + }; + if let Err(e) = + table::mcast::mcast_src_filter::add_ipv6_entry(s, prefix, dest_ip) + { + // Rollback: remove previously added entries + for added in added_prefixes { + let _ = table::mcast::mcast_src_filter::del_ipv6_entry( + s, added, dest_ip, + ); + } + return Err(e); } + added_prefixes.push(prefix); } - Ok(()) } fn validate_internal_group_creation( mcast: &MulticastGroupData, - group_ip: AdminScopedIpv6, + group_ip: UnderlayMulticastIpv6, ) -> DpdResult<()> { validate_group_exists(mcast, group_ip.into())?; Ok(()) @@ -1099,7 +1332,7 @@ fn validate_external_group_creation( ) -> DpdResult<()> { validate_group_exists(mcast, group_ip)?; validate_multicast_address(group_ip, group_info.sources.as_deref())?; - validate_not_admin_scoped_ipv6(group_ip)?; + validate_not_underlay_subnet(group_ip)?; Ok(()) } @@ -1460,10 +1693,11 @@ fn update_external_tables( group_entry: &MulticastGroup, new_group_info: &MulticastGroupUpdateExternalEntry, ) -> DpdResult<()> { - // Update sources if they changed - if new_group_info.sources != group_entry.sources { + // Update sources if they changed (normalize both sides for comparison) + let new_sources = normalize_sources(new_group_info.sources.clone()); + if new_sources != group_entry.sources { remove_source_filters(s, group_ip, group_entry.sources.as_deref())?; - add_source_filters(s, group_ip, new_group_info.sources.as_deref())?; + add_source_filters(s, group_ip, new_sources.as_deref())?; } // Update NAT target - external groups always have NAT targets @@ -1592,9 +1826,10 @@ fn delete_group_tables( ) -> DpdResult<()> { match group_ip { IpAddr::V4(ipv4) => { - remove_ipv4_source_filters(s, ipv4, group.sources.as_deref())?; - + // Source filters and NAT entries only exist for external groups + // (which have NAT targets). if group.int_fwding.nat_target.is_some() { + remove_ipv4_source_filters(s, ipv4, group.sources.as_deref())?; table::mcast::mcast_nat::del_ipv4_entry(s, ipv4)?; } @@ -1605,9 +1840,10 @@ fn delete_group_tables( IpAddr::V6(ipv6) => { delete_replication_entries(s, group_ip, group)?; - remove_ipv6_source_filters(s, ipv6, group.sources.as_deref())?; - + // Source filters only exist for external groups (which have + // NAT targets). Internal groups don't have source filtering. if group.int_fwding.nat_target.is_some() { + remove_ipv6_source_filters(s, ipv6, group.sources.as_deref())?; table::mcast::mcast_nat::del_ipv6_entry(s, ipv6)?; } @@ -1712,11 +1948,15 @@ fn update_internal_group_bitmap_tables( Ok(()) } +/// Update forwarding tables during rollback. +/// +/// Only updates the external bitmap entry since that's the only bitmap +/// entry created during group configuration. The underlay replication +/// is handled separately via the ASIC's multicast group primitives. fn update_fwding_tables( s: &Switch, group_ip: IpAddr, external_group_id: MulticastGroupId, - underlay_group_id: MulticastGroupId, members: &[MulticastGroupMember], vlan_id: Option, ) -> DpdResult<()> { @@ -1728,6 +1968,8 @@ fn update_fwding_tables( table::mcast::mcast_route::update_ipv6_entry(s, ipv6, vlan_id) .and_then(|_| { // Update external bitmap for external members + // (only external bitmap entries exist; underlay replication + // uses ASIC multicast groups directly) let external_port_bitmap = create_port_bitmap(members, Direction::External); table::mcast::mcast_egress::update_bitmap_entry( @@ -1737,17 +1979,6 @@ fn update_fwding_tables( vlan_id, ) }) - .and_then(|_| { - // Update underlay bitmap for underlay members - let underlay_port_bitmap = - create_port_bitmap(members, Direction::Underlay); - table::mcast::mcast_egress::update_bitmap_entry( - s, - underlay_group_id, - &underlay_port_bitmap, - vlan_id, - ) - }) } } } @@ -1769,6 +2000,7 @@ fn create_port_bitmap( #[cfg(test)] mod tests { use super::*; + use common::ports::RearPort; use std::thread; #[test] @@ -1836,10 +2068,10 @@ mod tests { assert!(result.is_err()); match result.unwrap_err() { - DpdError::McastGroupFailure(msg) => { + DpdError::ResourceExhausted(msg) => { assert!(msg.contains("no free multicast group IDs available")); } - _ => panic!("Expected McastGroupFailure error"), + _ => panic!("Expected ResourceExhausted error"), } } @@ -1973,4 +2205,147 @@ mod tests { // Pool should be back to original size assert_eq!(final_pool_size, initial_pool_size); } + + #[test] + fn test_sources_contain_any() { + // Empty slice has no Any + assert!(!sources_contain_any(&[])); + + // Slice with only exact sources has no Any + let exact_only = vec![ + IpSrc::Exact(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1))), + IpSrc::Exact(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1))), + ]; + assert!(!sources_contain_any(&exact_only)); + + // Slice with Any returns true + let with_any = vec![ + IpSrc::Exact(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1))), + IpSrc::Any, + ]; + assert!(sources_contain_any(&with_any)); + + // Slice with only Any returns true + let only_any = vec![IpSrc::Any]; + assert!(sources_contain_any(&only_any)); + + // Multiple Any entries still returns true + let multiple_any = vec![IpSrc::Any, IpSrc::Any]; + assert!(sources_contain_any(&multiple_any)); + } + + #[test] + fn test_create_port_bitmap_empty() { + let members: Vec = vec![]; + let bitmap = create_port_bitmap(&members, Direction::External); + // Empty bitmap should have no ports + assert!(!bitmap.contains_port(0)); + assert!(!bitmap.contains_port(1)); + } + + #[test] + fn test_create_port_bitmap_filters_by_direction() { + let members = vec![ + MulticastGroupMember { + port_id: RearPort::new(1).unwrap().into(), + link_id: LinkId(0), + direction: Direction::External, + }, + MulticastGroupMember { + port_id: RearPort::new(2).unwrap().into(), + link_id: LinkId(0), + direction: Direction::Underlay, + }, + MulticastGroupMember { + port_id: RearPort::new(3).unwrap().into(), + link_id: LinkId(0), + direction: Direction::External, + }, + ]; + + // External bitmap should only contain ports 1 and 3 + let external_bitmap = create_port_bitmap(&members, Direction::External); + assert!(external_bitmap.contains_port(1)); + assert!(!external_bitmap.contains_port(2)); + assert!(external_bitmap.contains_port(3)); + + // Underlay bitmap should only contain port 2 + let underlay_bitmap = create_port_bitmap(&members, Direction::Underlay); + assert!(!underlay_bitmap.contains_port(1)); + assert!(underlay_bitmap.contains_port(2)); + assert!(!underlay_bitmap.contains_port(3)); + } + + #[test] + fn test_create_port_bitmap_all_same_direction() { + let members = vec![ + MulticastGroupMember { + port_id: RearPort::new(5).unwrap().into(), + link_id: LinkId(0), + direction: Direction::External, + }, + MulticastGroupMember { + port_id: RearPort::new(10).unwrap().into(), + link_id: LinkId(0), + direction: Direction::External, + }, + MulticastGroupMember { + port_id: RearPort::new(15).unwrap().into(), + link_id: LinkId(0), + direction: Direction::External, + }, + ]; + + let bitmap = create_port_bitmap(&members, Direction::External); + assert!(bitmap.contains_port(5)); + assert!(bitmap.contains_port(10)); + assert!(bitmap.contains_port(15)); + assert!(!bitmap.contains_port(1)); // Not in members + + // Underlay bitmap should be empty + let underlay_bitmap = create_port_bitmap(&members, Direction::Underlay); + assert!(!underlay_bitmap.contains_port(5)); + assert!(!underlay_bitmap.contains_port(10)); + assert!(!underlay_bitmap.contains_port(15)); + } + + #[test] + fn test_normalize_sources() { + // None stays None + assert_eq!(normalize_sources(None), None); + + // Empty vec normalizes to None + assert_eq!(normalize_sources(Some(vec![])), None); + + // Vec with only IpSrc::Any normalizes to None + assert_eq!(normalize_sources(Some(vec![IpSrc::Any])), None); + + // Vec with Any mixed with Exact normalizes to None (Any subsumes all) + assert_eq!( + normalize_sources(Some(vec![ + IpSrc::Exact(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1))), + IpSrc::Any, + ])), + None + ); + + // Vec with only Exact sources stays as-is + let exact_sources = vec![ + IpSrc::Exact(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1))), + IpSrc::Exact(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1))), + ]; + assert_eq!( + normalize_sources(Some(exact_sources.clone())), + Some(exact_sources) + ); + + // Single Exact source stays as-is + let single_exact = vec![IpSrc::Exact(IpAddr::V6(Ipv6Addr::new( + 0x2001, 0xdb8, 0, 0, 0, 0, 0, 1, + )))]; + assert_eq!( + normalize_sources(Some(single_exact.clone())), + Some(single_exact) + ); + } } diff --git a/dpd/src/mcast/rollback.rs b/dpd/src/mcast/rollback.rs index f862f38..65fc218 100644 --- a/dpd/src/mcast/rollback.rs +++ b/dpd/src/mcast/rollback.rs @@ -2,7 +2,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/ // -// Copyright 2025 Oxide Computer Company +// Copyright 2026 Oxide Computer Company //! Rollback contexts for multicast group operations. //! @@ -11,10 +11,13 @@ //! rollback parameters once and provide reusable error handling throughout //! multi-step operations. -use std::{fmt, net::IpAddr}; +use std::{ + fmt, + net::{IpAddr, Ipv4Addr, Ipv6Addr}, +}; use aal::AsicOps; -use oxnet::Ipv4Net; +use oxnet::{Ipv4Net, Ipv6Net}; use slog::{debug, error}; use common::{nat::NatTarget, ports::PortId}; @@ -22,7 +25,8 @@ use common::{nat::NatTarget, ports::PortId}; use super::{ Direction, IpSrc, LinkId, MulticastGroup, MulticastGroupId, MulticastGroupMember, MulticastReplicationInfo, add_source_filters, - remove_source_filters, update_fwding_tables, update_replication_tables, + remove_source_filters, sources_contain_any, update_fwding_tables, + update_replication_tables, }; use crate::{Switch, table, types::DpdResult}; @@ -363,32 +367,48 @@ impl<'a> GroupCreateRollbackContext<'a> { // IPv4 groups are always external-only and never create bitmap entries // (only IPv6 internal groups with replication create bitmap entries) - if let Some(srcs) = self.sources { - for src in srcs { - match src { - IpSrc::Exact(IpAddr::V4(src)) => { - self.log_rollback_error( - "delete IPv4 source filter entry", - &format!("for source {src} and group {ipv4}"), - table::mcast::mcast_src_filter::del_ipv4_entry( - self.switch, - Ipv4Net::new(*src, 32).unwrap(), - ipv4, - ), - ); - } - IpSrc::Subnet(subnet) => { - self.log_rollback_error( - "delete IPv4 source filter subnet entry", - &format!("for subnet {subnet} and group {ipv4}"), - table::mcast::mcast_src_filter::del_ipv4_entry( - self.switch, *subnet, ipv4, - ), - ); - } - _ => {} + // If `Any` was present, only a /0 entry was added (collapsing) + match self.sources { + Some(srcs) if sources_contain_any(srcs) => { + self.log_rollback_error( + "delete IPv4 any-source filter entry", + &format!("for group {ipv4}"), + table::mcast::mcast_src_filter::del_ipv4_entry( + self.switch, + Ipv4Net::new(Ipv4Addr::UNSPECIFIED, 0).unwrap(), + ipv4, + ), + ); + } + Some(srcs) => { + for src in srcs { + let IpSrc::Exact(IpAddr::V4(addr)) = src else { + continue; + }; + self.log_rollback_error( + "delete IPv4 source filter entry", + &format!("for source {addr} and group {ipv4}"), + table::mcast::mcast_src_filter::del_ipv4_entry( + self.switch, + Ipv4Net::new(*addr, 32).unwrap(), + ipv4, + ), + ); } } + None => { + // Normalized None means "allow any source", which + // added a /0 entry. + self.log_rollback_error( + "delete IPv4 any-source filter entry", + &format!("for group {ipv4}"), + table::mcast::mcast_src_filter::del_ipv4_entry( + self.switch, + Ipv4Net::new(Ipv4Addr::UNSPECIFIED, 0).unwrap(), + ipv4, + ), + ); + } } if self.nat_target.is_some() { self.log_rollback_error( @@ -430,22 +450,57 @@ impl<'a> GroupCreateRollbackContext<'a> { ), ); - if let Some(srcs) = self.sources { - for src in srcs { - if let IpSrc::Exact(IpAddr::V6(src)) = src { + // Source filters only exist for external groups (which have + // NAT targets). Internal groups don't have source filtering. + if self.nat_target.is_some() { + // If `Any` was present, only a ::/0 entry was added (collapsing) + match self.sources { + Some(srcs) if sources_contain_any(srcs) => { self.log_rollback_error( - "delete IPv6 source filter entry", - &format!("for source {src} and group {ipv6}"), + "delete IPv6 any-source filter entry", + &format!("for group {ipv6}"), table::mcast::mcast_src_filter::del_ipv6_entry( self.switch, - *src, + Ipv6Net::new(Ipv6Addr::UNSPECIFIED, 0) + .unwrap(), + ipv6, + ), + ); + } + Some(srcs) => { + for src in srcs { + let IpSrc::Exact(IpAddr::V6(addr)) = src else { + continue; + }; + self.log_rollback_error( + "delete IPv6 source filter entry", + &format!( + "for source {addr} and group {ipv6}" + ), + table::mcast::mcast_src_filter::del_ipv6_entry( + self.switch, + Ipv6Net::new(*addr, 128).unwrap(), + ipv6, + ), + ); + } + } + None => { + // Normalized None means "allow any source", which + // added a /0 entry. + self.log_rollback_error( + "delete IPv6 any-source filter entry", + &format!("for group {ipv6}"), + table::mcast::mcast_src_filter::del_ipv6_entry( + self.switch, + Ipv6Net::new(Ipv6Addr::UNSPECIFIED, 0) + .unwrap(), ipv6, ), ); } } - } - if self.nat_target.is_some() { + self.log_rollback_error( "delete IPv6 NAT entry", &format!("for group {ipv6}"), @@ -747,7 +802,6 @@ impl<'a> GroupUpdateRollbackContext<'a> { self.switch, self.group_ip, external_group_id, - underlay_group_id, &prev_members, vlan_id, ), @@ -756,16 +810,20 @@ impl<'a> GroupUpdateRollbackContext<'a> { } /// Rollback external group updates. + /// + /// Note: `new_sources` should be the normalized sources that were actually + /// applied to the tables (not the raw request sources). pub(crate) fn rollback_external( &self, error: E, new_sources: Option<&[IpSrc]>, ) -> E { - if new_sources.is_some() { - self.collect_rollback_result("source filter restoration", || { - self.rollback_source_filters(new_sources, None) - }); - } + // Always try to rollback source filters; with normalization, even + // None represents a /0 "any source" entry that may need to be undone. + let orig_sources = self.original_group.sources.as_deref(); + self.collect_rollback_result("source filter restoration", || { + self.rollback_source_filters(new_sources, orig_sources) + }); error } diff --git a/dpd/src/mcast/validate.rs b/dpd/src/mcast/validate.rs index f039469..44f9199 100644 --- a/dpd/src/mcast/validate.rs +++ b/dpd/src/mcast/validate.rs @@ -2,15 +2,25 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/ // -// Copyright 2025 Oxide Computer Company +// Copyright 2026 Oxide Computer Company -use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; +//! Multicast address validation. +//! +//! Reserved multicast addresses are defined by IANA: +//! . -use common::nat::NatTarget; -use oxnet::{Ipv4Net, Ipv6Net}; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; use super::IpSrc; use crate::types::{DpdError, DpdResult}; +use common::nat::NatTarget; +use dpd_api::MulticastTag; +use omicron_common::address::{ + IPV4_LINK_LOCAL_MULTICAST_SUBNET, IPV4_SSM_SUBNET, + IPV6_INTERFACE_LOCAL_MULTICAST_SUBNET, IPV6_LINK_LOCAL_MULTICAST_SUBNET, + IPV6_RESERVED_SCOPE_MULTICAST_SUBNET, IPV6_SSM_SUBNET, + UNDERLAY_MULTICAST_SUBNET, +}; /// Check if an IP address is unicast (emulating the unstable std::net API). /// For IP addresses, unicast means simply "not multicast". @@ -35,7 +45,10 @@ pub(crate) fn validate_multicast_address( } } -/// Validates the NAT target inner MAC address. +/// Validates the NAT target inner MAC and internal IP address. +/// +/// NAT targets must use addresses from the reserved underlay multicast subnet +/// (ff04::/64) which is allocated by Omicron for internal multicast routing. pub(crate) fn validate_nat_target(nat_target: NatTarget) -> DpdResult<()> { if !nat_target.inner_mac.is_multicast() { return Err(DpdError::Invalid(format!( @@ -44,12 +57,10 @@ pub(crate) fn validate_nat_target(nat_target: NatTarget) -> DpdResult<()> { ))); } - let internal_nat_ip = Ipv6Net::new_unchecked(nat_target.internal_ip, 128); - - if !internal_nat_ip.is_admin_scoped_multicast() { + if !UNDERLAY_MULTICAST_SUBNET.contains(nat_target.internal_ip) { return Err(DpdError::Invalid(format!( - "NAT target internal IP address {} is not a valid \ - site/admin-local or org-scoped multicast address", + "NAT target internal IP address {} is not in the reserved \ + underlay multicast subnet (ff04::/64)", nat_target.internal_ip ))); } @@ -60,16 +71,8 @@ pub(crate) fn validate_nat_target(nat_target: NatTarget) -> DpdResult<()> { /// Check if an IP address is a Source-Specific Multicast (SSM) address. pub(crate) fn is_ssm(addr: IpAddr) -> bool { match addr { - IpAddr::V4(ipv4) => { - let subnet = Ipv4Net::new_unchecked(Ipv4Addr::new(232, 0, 0, 0), 8); - subnet.contains(ipv4) - } - // Check for Source-Specific Multicast (ff3x::/32) - // In IPv6 multicast, the second nibble (flag field) indicates SSM when set to 3 - IpAddr::V6(ipv6) => { - let flag_field = (ipv6.octets()[1] & 0xF0) >> 4; - flag_field == 3 - } + IpAddr::V4(ipv4) => IPV4_SSM_SUBNET.contains(ipv4), + IpAddr::V6(ipv6) => IPV6_SSM_SUBNET.contains(ipv6), } } @@ -93,44 +96,13 @@ fn validate_ipv4_multicast( requires at least one source to be defined", ))); } - // If sources are defined for an SSM address, it's valid return Ok(()); - } else if sources.is_some() && !sources.unwrap().is_empty() { - // If this is not SSM but sources are defined, it's invalid - return Err(DpdError::Invalid(format!( - "{addr} is not a Source-Specific Multicast address but sources were provided", - ))); } - // Define reserved IPv4 multicast subnets - let reserved_subnets = [ - // Local network control block (link-local) - Ipv4Net::new_unchecked(Ipv4Addr::new(224, 0, 0, 0), 24), // 224.0.0.0/24 - // GLOP addressing - Ipv4Net::new_unchecked(Ipv4Addr::new(233, 0, 0, 0), 8), // 233.0.0.0/8 - // Administrative scoped addresses - Ipv4Net::new_unchecked(Ipv4Addr::new(239, 0, 0, 0), 8), // 239.0.0.0/8 (administratively scoped) - ]; - // Check reserved subnets - for subnet in &reserved_subnets { - if subnet.contains(addr) { - return Err(DpdError::Invalid(format!( - "{addr} is in the reserved multicast subnet {subnet}", - ))); - } - } - - // Check specific reserved addresses that may not fall within entire subnets - let specific_reserved = [ - Ipv4Addr::new(224, 0, 1, 1), // NTP (Network Time Protocol) - Ipv4Addr::new(224, 0, 1, 129), // Cisco Auto-RP-Announce - Ipv4Addr::new(224, 0, 1, 130), // Cisco Auto-RP-Discovery - ]; - - if specific_reserved.contains(&addr) { + if IPV4_LINK_LOCAL_MULTICAST_SUBNET.contains(addr) { return Err(DpdError::Invalid(format!( - "{addr} is a specifically reserved multicast address", + "{addr} is in the reserved link-local multicast subnet", ))); } @@ -156,26 +128,16 @@ fn validate_ipv6_multicast( and requires at least one source to be defined", ))); } - // If sources are defined for an IPv6 SSM address, it's valid return Ok(()); - } else if sources.is_some() && !sources.unwrap().is_empty() { - // If this is not SSM but sources are defined, it's invalid - return Err(DpdError::Invalid(format!( - "{addr} is not a Source-Specific Multicast address but sources were provided", - ))); } - // Define reserved IPv6 multicast subnets + // Check reserved subnets let reserved_subnets = [ - // Link-local scope - Ipv6Net::new_unchecked(Ipv6Addr::new(0xff02, 0, 0, 0, 0, 0, 0, 0), 16), // ff02::/16 - // Interface-local scope - Ipv6Net::new_unchecked(Ipv6Addr::new(0xff01, 0, 0, 0, 0, 0, 0, 0), 16), // ff01::/16 - // Node-local scope (deprecated) - Ipv6Net::new_unchecked(Ipv6Addr::new(0xff00, 0, 0, 0, 0, 0, 0, 0), 16), // ff00::/16 + IPV6_LINK_LOCAL_MULTICAST_SUBNET, + IPV6_INTERFACE_LOCAL_MULTICAST_SUBNET, + IPV6_RESERVED_SCOPE_MULTICAST_SUBNET, ]; - // Check reserved subnets for subnet in &reserved_subnets { if subnet.contains(addr) { return Err(DpdError::Invalid(format!( @@ -187,14 +149,19 @@ fn validate_ipv6_multicast( Ok(()) } -/// Validates that IPv6 addresses are not admin-scoped for external group creation. -pub(crate) fn validate_not_admin_scoped_ipv6(addr: IpAddr) -> DpdResult<()> { +/// Validates that IPv6 addresses are not in the reserved underlay subnet. +/// +/// External groups may use admin-local addresses (ff04::/16) but not the +/// reserved underlay subnet (ff04::/64), which is used for internal underlay +/// multicast group allocation. +pub(crate) fn validate_not_underlay_subnet(addr: IpAddr) -> DpdResult<()> { if let IpAddr::V6(ipv6) = addr - && oxnet::Ipv6Net::new_unchecked(ipv6, 128).is_admin_scoped_multicast() + && UNDERLAY_MULTICAST_SUBNET.contains(ipv6) { return Err(DpdError::Invalid(format!( - "{addr} is an admin-scoped multicast address and \ - must be created via the internal multicast API", + "{addr} is in the reserved underlay multicast subnet (ff04::/64, \ + within admin-local scope ff04::/16) and must be created via the \ + internal multicast API", ))); } Ok(()) @@ -212,7 +179,7 @@ pub(crate) fn validate_source_addresses( for source in sources { match source { IpSrc::Exact(ip) => validate_exact_source_address(*ip)?, - IpSrc::Subnet(subnet) => validate_ipv4_source_subnet(*subnet)?, + IpSrc::Any => {} // Any-source is always valid } } Ok(()) @@ -263,25 +230,33 @@ fn validate_ipv6_source_address(ipv6: Ipv6Addr) -> DpdResult<()> { Ok(()) } -/// Validates IPv4 source subnets for problematic address ranges. -fn validate_ipv4_source_subnet(subnet: Ipv4Net) -> DpdResult<()> { - let addr = subnet.addr(); - - // Reject subnets that contain multicast addresses - if addr.is_multicast() { - return Err(DpdError::Invalid(format!( - "Source subnet {subnet} contains multicast addresses and cannot be used as a source filter", - ))); - } +/// Validates tag format for group creation. +/// +/// Delegates to [`MulticastTag::from_str`] which enforces: +/// - Length: 1-80 ASCII bytes +/// - Characters: alphanumeric, hyphens, underscores, colons, or periods +/// +/// Auto-generated tags use the format `{uuid}:{group_ip}`. +pub(crate) fn validate_tag_format(tag: &str) -> DpdResult<()> { + tag.parse::() + .map(|_| ()) + .map_err(|e| DpdError::Invalid(e.to_string())) +} - // Reject subnets with loopback or broadcast addresses - if addr.is_loopback() || addr.is_broadcast() { - return Err(DpdError::Invalid(format!( - "Source subnet {subnet} contains invalid address types \ - (loopback/broadcast) for source filtering", - ))); +/// Validates that the request tag matches the existing group's tag. +/// +/// Tags are immutable after group creation. This validation ensures the caller +/// created the group before allowing mutations. +pub(crate) fn validate_tag( + existing_tag: &str, + request_tag: &str, +) -> DpdResult<()> { + if request_tag != existing_tag { + return Err(DpdError::Invalid( + "tag mismatch: provided tag does not match the group's tag" + .to_string(), + )); } - Ok(()) } @@ -289,9 +264,10 @@ fn validate_ipv4_source_subnet(subnet: Ipv4Net) -> DpdResult<()> { mod tests { use super::*; use common::{nat::Vni, network::MacAddr}; - use oxnet::Ipv4Net; + use dpd_api::MAX_TAG_LENGTH; - use std::str::FromStr; + /// Admin-local IPv6 multicast prefix (ff04::/16, scope 4). + const ADMIN_LOCAL_PREFIX: u16 = 0xff04; #[test] fn test_ipv4_validation() { @@ -370,89 +346,67 @@ mod tests { #[test] fn test_ipv4_ssm_with_sources() { - // Create test data for source specifications let ssm_addr = Ipv4Addr::new(232, 1, 2, 3); - let non_ssm_addr = Ipv4Addr::new(224, 1, 2, 3); + let asm_addr = Ipv4Addr::new(224, 1, 2, 3); - // Test with exact source IP let exact_sources = vec![IpSrc::Exact(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)))]; + let any_source = vec![IpSrc::Any]; - // Test with subnet source specification - let subnet_sources = - vec![IpSrc::Subnet(Ipv4Net::from_str("192.168.1.0/24").unwrap())]; - - // Test with mixed source specifications - let mixed_sources = vec![ - IpSrc::Exact(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1))), - IpSrc::Subnet(Ipv4Net::from_str("10.0.0.0/8").unwrap()), - ]; - - // Empty sources - should fail for SSM + // SSM requires sources assert!(validate_ipv4_multicast(ssm_addr, Some(&[])).is_err()); + assert!(validate_ipv4_multicast(ssm_addr, None).is_err()); - // SSM address with exact source - should pass + // SSM with exact source assert!( validate_ipv4_multicast(ssm_addr, Some(&exact_sources)).is_ok() ); - // SSM address with subnet source - should pass - assert!( - validate_ipv4_multicast(ssm_addr, Some(&subnet_sources)).is_ok() - ); + // SSM with any-source + assert!(validate_ipv4_multicast(ssm_addr, Some(&any_source)).is_ok()); - // SSM address with mixed sources - should pass - assert!( - validate_ipv4_multicast(ssm_addr, Some(&mixed_sources)).is_ok() - ); + // ASM without sources + assert!(validate_ipv4_multicast(asm_addr, None).is_ok()); + assert!(validate_ipv4_multicast(asm_addr, Some(&[])).is_ok()); - // Non-SSM address with sources - should fail as source specs only allowed for SSM - assert!( - validate_ipv4_multicast(non_ssm_addr, Some(&exact_sources)) - .is_err() - ); - assert!( - validate_ipv4_multicast(non_ssm_addr, Some(&subnet_sources)) - .is_err() - ); + // ASM with sources assert!( - validate_ipv4_multicast(non_ssm_addr, Some(&mixed_sources)) - .is_err() + validate_ipv4_multicast(asm_addr, Some(&exact_sources)).is_ok() ); - - // Non-SSM address without sources - should pass - assert!(validate_ipv4_multicast(non_ssm_addr, None).is_ok()); - assert!(validate_ipv4_multicast(non_ssm_addr, Some(&[])).is_ok()); + assert!(validate_ipv4_multicast(asm_addr, Some(&any_source)).is_ok()); } #[test] fn test_ipv6_ssm_with_sources() { - // IPv6 SSM addresses (ff3x::/32) - let ssm_global = Ipv6Addr::new(0xff3e, 0, 0, 0, 0, 0, 0, 0x1234); // Global scope (e) - let non_ssm_global = Ipv6Addr::new(0xff0e, 0, 0, 0, 0, 0, 0, 0x1234); // Non-SSM global + let ssm_addr = Ipv6Addr::new(0xff3e, 0, 0, 0, 0, 0, 0, 0x1234); + let asm_addr = Ipv6Addr::new(0xff0e, 0, 0, 0, 0, 0, 0, 0x1234); - // Create test sources for IPv6 - let ip6_sources = vec![IpSrc::Exact(IpAddr::V6(Ipv6Addr::new( + let exact_sources = vec![IpSrc::Exact(IpAddr::V6(Ipv6Addr::new( 0x2001, 0xdb8, 0, 0, 0, 0, 0, 0x1, )))]; + let any_source = vec![IpSrc::Any]; - // Empty sources - should fail for SSM - assert!(validate_ipv6_multicast(ssm_global, Some(&[])).is_err()); + // SSM requires sources + assert!(validate_ipv6_multicast(ssm_addr, Some(&[])).is_err()); + assert!(validate_ipv6_multicast(ssm_addr, None).is_err()); - // SSM address with IPv6 source - should pass + // SSM with exact source assert!( - validate_ipv6_multicast(ssm_global, Some(&ip6_sources)).is_ok() + validate_ipv6_multicast(ssm_addr, Some(&exact_sources)).is_ok() ); - // Non-SSM address with IPv6 source - should fail + // SSM with any-source + assert!(validate_ipv6_multicast(ssm_addr, Some(&any_source)).is_ok()); + + // ASM without sources + assert!(validate_ipv6_multicast(asm_addr, None).is_ok()); + assert!(validate_ipv6_multicast(asm_addr, Some(&[])).is_ok()); + + // ASM with sources assert!( - validate_ipv6_multicast(non_ssm_global, Some(&ip6_sources)) - .is_err() + validate_ipv6_multicast(asm_addr, Some(&exact_sources)).is_ok() ); - - // Non-SSM address without sources - should pass - assert!(validate_ipv6_multicast(non_ssm_global, None).is_ok()); - assert!(validate_ipv6_multicast(non_ssm_global, Some(&[])).is_ok()); + assert!(validate_ipv6_multicast(asm_addr, Some(&any_source)).is_ok()); } #[test] @@ -499,7 +453,7 @@ mod tests { // Valid IPv4 SSM address with sources let sources = vec![ IpSrc::Exact(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1))), - IpSrc::Subnet(Ipv4Net::from_str("10.0.0.0/8").unwrap()), + IpSrc::Any, ]; assert!( validate_multicast_address( @@ -550,13 +504,13 @@ mod tests { .is_err() ); - // IPv4 non-SSM with sources + // IPv4 ASM with sources assert!( validate_multicast_address( IpAddr::V4(Ipv4Addr::new(224, 1, 2, 3)), Some(&sources) ) - .is_err() + .is_ok() ); // IPv6 SSM without sources @@ -568,36 +522,60 @@ mod tests { .is_err() ); - // IPv6 non-SSM with sources + // IPv6 ASM with sources assert!( validate_multicast_address( IpAddr::V6(Ipv6Addr::new(0xff0e, 0, 0, 0, 0, 0, 0, 0x1234)), Some(&ip6_sources) ) - .is_err() + .is_ok() ); } #[test] fn test_validate_nat_target() { + // Unicast internal IP should be rejected let ucast_nat_target = NatTarget { internal_ip: Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1), - // Not a multicast MAC inner_mac: MacAddr::new(0x00, 0x00, 0x00, 0x00, 0x00, 0x01), vni: Vni::new(100).unwrap(), }; - assert!(validate_nat_target(ucast_nat_target).is_err()); - let mcast_nat_target = NatTarget { - // org-scoped multicast - internal_ip: Ipv6Addr::new(0xff08, 0, 0, 0, 0, 0, 0, 0x1234), - // Multicast MAC + // Valid NAT target in reserved underlay subnet (ff04::/64) + let valid_nat_target = NatTarget { + internal_ip: Ipv6Addr::new( + ADMIN_LOCAL_PREFIX, + 0, + 0, + 0, + 0, + 0, + 0, + 0x1234, + ), inner_mac: MacAddr::new(0x01, 0x00, 0x5e, 0x00, 0x00, 0x01), vni: Vni::new(100).unwrap(), }; - - assert!(validate_nat_target(mcast_nat_target).is_ok()); + assert!(validate_nat_target(valid_nat_target).is_ok()); + + // Admin-local address outside ff04::/64 should be rejected + // ff04:0:0:1::1234 is in ff04::/16 but not in ff04::/64 + let outside_underlay_nat_target = NatTarget { + internal_ip: Ipv6Addr::new( + ADMIN_LOCAL_PREFIX, + 0, + 0, + 1, // This puts it outside ff04::/64 + 0, + 0, + 0, + 0x1234, + ), + inner_mac: MacAddr::new(0x01, 0x00, 0x5e, 0x00, 0x00, 0x01), + vni: Vni::new(100).unwrap(), + }; + assert!(validate_nat_target(outside_underlay_nat_target).is_err()); } #[test] @@ -615,12 +593,9 @@ mod tests { )))]; assert!(validate_source_addresses(Some(&valid_ipv6_sources)).is_ok()); - // Valid subnet sources - let valid_subnet_sources = vec![ - IpSrc::Subnet(Ipv4Net::from_str("192.168.1.0/24").unwrap()), - IpSrc::Subnet(Ipv4Net::from_str("10.0.0.0/8").unwrap()), - ]; - assert!(validate_source_addresses(Some(&valid_subnet_sources)).is_ok()); + // Any-source is valid + let any_source = vec![IpSrc::Any]; + assert!(validate_source_addresses(Some(&any_source)).is_ok()); // Invalid multicast IPv4 source let invalid_mcast_ipv4 = @@ -653,20 +628,6 @@ mod tests { validate_source_addresses(Some(&invalid_loopback_ipv6)).is_err() ); - // Invalid multicast subnet - let invalid_mcast_subnet = - vec![IpSrc::Subnet(Ipv4Net::from_str("224.0.0.0/24").unwrap())]; - assert!( - validate_source_addresses(Some(&invalid_mcast_subnet)).is_err() - ); - - // Invalid loopback subnet - let invalid_loopback_subnet = - vec![IpSrc::Subnet(Ipv4Net::from_str("127.0.0.0/8").unwrap())]; - assert!( - validate_source_addresses(Some(&invalid_loopback_subnet)).is_err() - ); - // No sources should be valid assert!(validate_source_addresses(None).is_ok()); @@ -676,8 +637,6 @@ mod tests { #[test] fn test_address_validation_with_source_validation() { - // Test that multicast address validation now includes source validation - // Valid case: SSM address with valid unicast sources let valid_sources = vec![IpSrc::Exact(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)))]; @@ -719,4 +678,88 @@ mod tests { .contains("is not a valid source address") ); } + + #[test] + fn test_validate_not_underlay_subnet() { + // Reserved underlay subnet (ff04::/64) should be rejected + let underlay_addr = + IpAddr::V6(Ipv6Addr::new(0xff04, 0, 0, 0, 0, 0, 0, 1)); + assert!(validate_not_underlay_subnet(underlay_addr).is_err()); + + // Another address in ff04::/64 + let underlay_addr2 = + IpAddr::V6(Ipv6Addr::new(0xff04, 0, 0, 0, 0xdead, 0xbeef, 0, 1)); + assert!(validate_not_underlay_subnet(underlay_addr2).is_err()); + + // Other admin-local /64s should be allowed (e.g., ff04:0:0:1::/64) + let other_admin_local = + IpAddr::V6(Ipv6Addr::new(0xff04, 0, 0, 1, 0, 0, 0, 1)); + assert!(validate_not_underlay_subnet(other_admin_local).is_ok()); + + // ff04:0:0:2::/64 should also be allowed + let other_admin_local2 = + IpAddr::V6(Ipv6Addr::new(0xff04, 0, 0, 2, 0, 0, 0, 1)); + assert!(validate_not_underlay_subnet(other_admin_local2).is_ok()); + + // IPv4 multicast should always be allowed (not in underlay subnet) + let ipv4_mcast = IpAddr::V4(Ipv4Addr::new(224, 1, 2, 3)); + assert!(validate_not_underlay_subnet(ipv4_mcast).is_ok()); + + // Non-admin-local IPv6 multicast should be allowed + let global_mcast = + IpAddr::V6(Ipv6Addr::new(0xff0e, 0, 0, 0, 0, 0, 0, 1)); + assert!(validate_not_underlay_subnet(global_mcast).is_ok()); + + // Site-local multicast should be allowed + let site_local = IpAddr::V6(Ipv6Addr::new(0xff05, 0, 0, 0, 0, 0, 0, 1)); + assert!(validate_not_underlay_subnet(site_local).is_ok()); + } + + #[test] + fn test_validate_tag() { + // Existing tag matches request tag + assert!(validate_tag("my-tag", "my-tag").is_ok()); + + // Existing tag but request has different tag + assert!(validate_tag("owner-a", "owner-b").is_err()); + assert!(validate_tag("owner-a", "").is_err()); + assert!(validate_tag("owner-a", "tag/with/slashes").is_err()); + } + + #[test] + fn test_validate_tag_format() { + use super::validate_tag_format; + + // Valid tags + assert!(validate_tag_format("my-tag").is_ok()); + assert!(validate_tag_format("nexus").is_ok()); + assert!(validate_tag_format("a1b2c3").is_ok()); + assert!(validate_tag_format("tag_with_underscore").is_ok()); + assert!(validate_tag_format("tag.with.periods").is_ok()); + assert!(validate_tag_format("tag:with:colons").is_ok()); + assert!(validate_tag_format("mixed-tag_v1.0:test").is_ok()); + + // Auto-generated tag format (uuid:ip) + assert!( + validate_tag_format( + "550e8400-e29b-41d4-a716-446655440000:224.1.2.3" + ) + .is_ok() + ); + + // Tag at exactly MAX_TAG_LENGTH characters is valid + assert!(validate_tag_format(&"a".repeat(MAX_TAG_LENGTH)).is_ok()); + + // Empty tag rejected + assert!(validate_tag_format("").is_err()); + + // Tag exceeding MAX_TAG_LENGTH characters rejected + assert!(validate_tag_format(&"a".repeat(MAX_TAG_LENGTH + 1)).is_err()); + + // Invalid characters rejected + assert!(validate_tag_format("tag with spaces").is_err()); + assert!(validate_tag_format("tag/with/slashes").is_err()); + assert!(validate_tag_format("tag@with@at").is_err()); + assert!(validate_tag_format("tag#with#hash").is_err()); + } } diff --git a/dpd/src/port_map.rs b/dpd/src/port_map.rs index 2f00119..b167daf 100644 --- a/dpd/src/port_map.rs +++ b/dpd/src/port_map.rs @@ -2,7 +2,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/ // -// Copyright 2025 Oxide Computer Company +// Copyright 2026 Oxide Computer Company //! Types for mapping physical switch ports to Tofino-specific handles. //! @@ -111,7 +111,7 @@ impl FromStr for SidecarRevision { /// A mapping between a physical switch port and a Tofino `Connector`. /// /// These objects cannot be constructed externally. A reference to the static -/// maps can be returned via the [`port_map`] function. +/// maps can be returned via the `port_map` function. #[derive(Clone, Debug)] pub struct PortMap { _revision: SidecarRevision, diff --git a/dpd/src/table/mcast/mcast_egress.rs b/dpd/src/table/mcast/mcast_egress.rs index e050c2a..79b7e1b 100644 --- a/dpd/src/table/mcast/mcast_egress.rs +++ b/dpd/src/table/mcast/mcast_egress.rs @@ -2,7 +2,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/ // -// Copyright 2025 Oxide Computer Company +// Copyright 2026 Oxide Computer Company //! Table operations for multicast egress entries. @@ -283,10 +283,9 @@ pub(crate) fn port_mapping_counter_fetch( ) } -/// Structure to hold and manipulate the 256-bit port bitmap. +/// 256-port bitmap (8 × 32-bit) for multicast egress decapsulation filtering. #[derive(Debug, Clone, Default)] pub(crate) struct PortBitmap { - // 8 x 32-bit values representing all 256 ports ports: [u32; 8], } @@ -304,27 +303,21 @@ impl PortBitmap { self.ports[array_idx] |= mask; // Set the bit } - /// Remove a port from the bitmap + /// Remove a port from the bitmap. #[allow(dead_code)] - pub(crate) fn remove_port(&mut self, port: u16) { + pub(crate) fn remove_port(&mut self, port: u8) { let array_idx = (port >> 5) as usize; let bit_pos = port & 0x1F; let mask = 1u32 << bit_pos; - - self.ports[array_idx] &= !mask; // Clear the bit + self.ports[array_idx] &= !mask; } - /// Check if a port is in the bitmap + /// Check if a port is in the bitmap. #[allow(dead_code)] - fn contains_port(&self, port: u16) -> bool { - if port >= 256 { - return false; - } - + pub(crate) fn contains_port(&self, port: u8) -> bool { let array_idx = (port >> 5) as usize; let bit_pos = port & 0x1F; let mask = 1u32 << bit_pos; - (self.ports[array_idx] & mask) != 0 } @@ -378,9 +371,20 @@ mod tests { assert!(bitmap.contains_port(5)); assert!(bitmap.contains_port(10)); assert!(bitmap.contains_port(255)); - assert!(!bitmap.contains_port(256)); bitmap.remove_port(10); assert!(!bitmap.contains_port(10)); + + // Test boundary conditions + bitmap.add_port(0); + assert!(bitmap.contains_port(0)); + bitmap.remove_port(0); + assert!(!bitmap.contains_port(0)); + + // Test port at 32-bit boundary + bitmap.add_port(32); + assert!(bitmap.contains_port(32)); + bitmap.add_port(64); + assert!(bitmap.contains_port(64)); } } diff --git a/dpd/src/table/mcast/mcast_route.rs b/dpd/src/table/mcast/mcast_route.rs index 8fcf789..bd097e7 100644 --- a/dpd/src/table/mcast/mcast_route.rs +++ b/dpd/src/table/mcast/mcast_route.rs @@ -2,7 +2,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/ // -// Copyright 2025 Oxide Computer Company +// Copyright 2026 Oxide Computer Company //! Table operations for multicast routing entries (on Ingress to the switch). @@ -14,7 +14,7 @@ use super::{Ipv4MatchKey, Ipv6MatchKey}; use aal::ActionParse; use aal_macros::*; -use oxnet::Ipv6Net; +use omicron_common::address::UNDERLAY_MULTICAST_SUBNET; use slog::debug; /// IPv4 Table for multicast routing entries. @@ -124,13 +124,12 @@ pub(crate) fn add_ipv6_entry( vlan_id: Option, ) -> DpdResult<()> { let match_key = Ipv6MatchKey::new(route); - let internal_ip = Ipv6Net::new_unchecked(route, 128); - // Admin-scoped multicast and unique local addresses are internal to the rack - // and don't require VLAN tagging, so always use Forward action - let action_data: Ipv6Action = if internal_ip.is_admin_scoped_multicast() - || internal_ip.is_unique_local() - { + // Reserved underlay multicast subnet (ff04::/64) is internal to the rack + // and doesn't require VLAN tagging. Other admin-local addresses + // (e.g., ff04:0:0:1::/64) may be used by customer external groups and + // can receive VLAN tagging. + let action_data: Ipv6Action = if UNDERLAY_MULTICAST_SUBNET.contains(route) { Ipv6Action::Forward } else { match vlan_id { @@ -157,13 +156,12 @@ pub(crate) fn update_ipv6_entry( vlan_id: Option, ) -> DpdResult<()> { let match_key = Ipv6MatchKey::new(route); - let internal_ip = Ipv6Net::new_unchecked(route, 128); - // Admin-scoped multicast and unique local addresses are internal to the rack - // and don't require VLAN tagging, so always use Forward action - let action_data: Ipv6Action = if internal_ip.is_admin_scoped_multicast() - || internal_ip.is_unique_local() - { + // Reserved underlay multicast subnet (ff04::/64) is internal to the rack + // and doesn't require VLAN tagging. Other admin-local addresses + // (e.g., ff04:0:0:1::/64) may be used by customer external groups and + // can receive VLAN tagging. + let action_data: Ipv6Action = if UNDERLAY_MULTICAST_SUBNET.contains(route) { Ipv6Action::Forward } else { match vlan_id { diff --git a/dpd/src/table/mcast/mcast_src_filter.rs b/dpd/src/table/mcast/mcast_src_filter.rs index 133de4b..c3c2243 100644 --- a/dpd/src/table/mcast/mcast_src_filter.rs +++ b/dpd/src/table/mcast/mcast_src_filter.rs @@ -2,7 +2,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/ // -// Copyright 2025 Oxide Computer Company +// Copyright 2026 Oxide Computer Company //! Table operations for multicast source filter entries. @@ -15,7 +15,7 @@ use crate::{Switch, table::*}; use aal::{ActionParse, MatchParse}; use aal_macros::*; -use oxnet::Ipv4Net; +use oxnet::{Ipv4Net, Ipv6Net}; use slog::debug; /// IPv4 Table for multicast source filter entries. @@ -46,12 +46,13 @@ impl fmt::Display for Ipv4MatchKey { #[derive(MatchParse, Hash)] struct Ipv6MatchKey { - src_addr: Ipv6Addr, + #[match_xlate(name = "src_addr", type = "lpm")] + src_addr: Ipv6Net, dst_addr: Ipv6Addr, } impl Ipv6MatchKey { - fn new(src_addr: Ipv6Addr, dst_addr: Ipv6Addr) -> Self { + fn new(src_addr: Ipv6Net, dst_addr: Ipv6Addr) -> Self { Self { src_addr, dst_addr } } } @@ -131,7 +132,7 @@ pub(crate) fn reset_ipv4(s: &Switch) -> DpdResult<()> { /// `src_addr, dst_addr -> allow_source_mcastv6`. pub(crate) fn add_ipv6_entry( s: &Switch, - src_addr: Ipv6Addr, + src_addr: Ipv6Net, dst_addr: Ipv6Addr, ) -> DpdResult<()> { let match_key = Ipv6MatchKey::new(src_addr, dst_addr); @@ -149,7 +150,7 @@ pub(crate) fn add_ipv6_entry( /// `src_addr, dst_addr`. pub(crate) fn del_ipv6_entry( s: &Switch, - src_addr: Ipv6Addr, + src_addr: Ipv6Net, dst_addr: Ipv6Addr, ) -> DpdResult<()> { let match_key = Ipv6MatchKey::new(src_addr, dst_addr); diff --git a/dpd/src/types.rs b/dpd/src/types.rs index d466d96..685c4f6 100644 --- a/dpd/src/types.rs +++ b/dpd/src/types.rs @@ -2,7 +2,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/ // -// Copyright 2025 Oxide Computer Company +// Copyright 2026 Oxide Computer Company //! General types used throughout Dendrite. @@ -93,6 +93,8 @@ pub enum DpdError { McastGroupFailure(String), #[error("Resource exhausted: {}", .0)] ResourceExhausted(String), + #[error("Tag is required for idempotent validation")] + MissingTag, } impl From for DpdError { @@ -178,6 +180,14 @@ impl convert::From for dropshot::HttpError { message, ) } + DpdError::Switch(AsicError::Missing(ref msg)) => { + // ASIC entry not found - return 404 so caller can handle + // (e.g., omicron delete+recreate pattern) + dropshot::HttpError::for_not_found( + None, + format!("ASIC entry not found: {msg}"), + ) + } DpdError::TableFull(e) => dropshot::HttpError { status_code: dropshot::ErrorStatusCode::INSUFFICIENT_STORAGE, error_code: None, @@ -281,6 +291,9 @@ impl convert::From for dropshot::HttpError { DpdError::ResourceExhausted(e) => { dropshot::HttpError::for_unavail(None, e) } + e @ DpdError::MissingTag => { + dropshot::HttpError::for_bad_request(None, format!("{e}")) + } } } } diff --git a/openapi/dpd/dpd-3.0.0-27dfd0.json b/openapi/dpd/dpd-3.0.0-27dfd0.json new file mode 100644 index 0000000..16d1847 --- /dev/null +++ b/openapi/dpd/dpd-3.0.0-27dfd0.json @@ -0,0 +1,9648 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "Oxide Switch Dataplane Controller", + "description": "API for managing the Oxide rack switch", + "contact": { + "url": "https://oxide.computer", + "email": "api@oxide.computer" + }, + "version": "3.0.0" + }, + "paths": { + "/all-settings": { + "delete": { + "summary": "Clear all settings.", + "description": "This removes all data entirely: ARP and NDP table entries, routes, links on all switch ports, NAT mappings, and multicast groups.\n\nNote: Unlike `reset_all_tagged`, this endpoint does clear multicast groups.", + "operationId": "reset_all", + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/all-settings/{tag}": { + "delete": { + "summary": "Clear all settings associated with a specific tag.", + "description": "This removes all ARP or NDP table entries, all routes, and all links on all switch ports.\n\nNote: Multicast groups are NOT cleared by this endpoint. Use the dedicated `/multicast/tags/{tag}` endpoint to clear multicast groups by tag.", + "operationId": "reset_all_tagged", + "parameters": [ + { + "in": "path", + "name": "tag", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/arp": { + "get": { + "summary": "Fetch the configured IPv4 ARP table entries.", + "operationId": "arp_list", + "parameters": [ + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ArpEntryResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + }, + "post": { + "summary": "Add an IPv4 ARP table entry, mapping an IPv4 address to a MAC address.", + "operationId": "arp_create", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ArpEntry" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "summary": "Remove all entries in the IPv4 ARP tables.", + "operationId": "arp_reset", + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/arp/{ip}": { + "get": { + "summary": "Get a single IPv4 ARP table entry, by its IPv4 address.", + "operationId": "arp_get", + "parameters": [ + { + "in": "path", + "name": "ip", + "required": true, + "schema": { + "type": "string", + "format": "ipv4" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ArpEntry" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "summary": "Remove a single IPv4 ARP entry, by its IPv4 address.", + "operationId": "arp_delete", + "parameters": [ + { + "in": "path", + "name": "ip", + "required": true, + "schema": { + "type": "string", + "format": "ipv4" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/backplane-map": { + "get": { + "summary": "Return the full backplane map.", + "description": "This returns the entire mapping of all cubbies in a rack, through the cabled backplane, and into the Sidecar main board. It also includes the Tofino \"connector\", which is included in some contexts such as reporting counters.", + "operationId": "backplane_map", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Map_of_BackplaneLink", + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/BackplaneLink" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/backplane-map/{port_id}": { + "get": { + "summary": "Return the backplane mapping for a single switch port.", + "operationId": "port_backplane_link", + "parameters": [ + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BackplaneLink" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/build-info": { + "get": { + "summary": "Return detailed build information about the `dpd` server itself.", + "operationId": "build_info", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BuildInfo" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/channels": { + "get": { + "summary": "Get the set of available channels for all ports.", + "description": "This returns the unused MAC channels for each physical switch port. This can be used to determine how many additional links can be created on a physical switch port.", + "operationId": "channels_list", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_FreeChannels", + "type": "array", + "items": { + "$ref": "#/components/schemas/FreeChannels" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/counters/fec": { + "get": { + "summary": "Get the FEC RS counters for all links.", + "operationId": "fec_rs_counters_list", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_LinkFecRSCounters", + "type": "array", + "items": { + "$ref": "#/components/schemas/LinkFecRSCounters" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/counters/fec/{port_id}/{link_id}": { + "get": { + "summary": "Get the FEC RS counters for the given link.", + "operationId": "fec_rs_counters_get", + "parameters": [ + { + "in": "path", + "name": "link_id", + "description": "The link in the switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LinkFecRSCounters" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/counters/fsm/{port_id}/{link_id}": { + "get": { + "summary": "Get the autonegotiation FSM counters for the given link.", + "operationId": "link_fsm_counters_get", + "parameters": [ + { + "in": "path", + "name": "link_id", + "description": "The link in the switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LinkFsmCounters" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/counters/linkup": { + "get": { + "summary": "Get the LinkUp counters for all links.", + "operationId": "link_up_counters_list", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_LinkUpCounter", + "type": "array", + "items": { + "$ref": "#/components/schemas/LinkUpCounter" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/counters/linkup/{port_id}/{link_id}": { + "get": { + "summary": "Get the LinkUp counters for the given link.", + "operationId": "link_up_counters_get", + "parameters": [ + { + "in": "path", + "name": "link_id", + "description": "The link in the switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LinkUpCounter" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/counters/p4": { + "get": { + "summary": "Get a list of all the available p4-defined counters.", + "operationId": "counter_list", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_String", + "type": "array", + "items": { + "type": "string" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/counters/p4/{counter}": { + "get": { + "summary": "Get the values for a given counter.", + "description": "The name of the counter should match one of those returned by the `counter_list()` call.", + "operationId": "counter_get", + "parameters": [ + { + "in": "query", + "name": "force_sync", + "description": "Force a sync of the counters from the ASIC to memory, even if the default refresh timeout hasn't been reached.", + "required": true, + "schema": { + "type": "boolean" + } + }, + { + "in": "path", + "name": "counter", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_TableCounterEntry", + "type": "array", + "items": { + "$ref": "#/components/schemas/TableCounterEntry" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/counters/p4/{counter}/reset": { + "post": { + "summary": "Reset a single p4-defined counter.", + "description": "The name of the counter should match one of those returned by the `counter_list()` call.", + "operationId": "counter_reset", + "parameters": [ + { + "in": "path", + "name": "counter", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/counters/pcs": { + "get": { + "summary": "Get the physical coding sublayer (PCS) counters for all links.", + "operationId": "pcs_counters_list", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_LinkPcsCounters", + "type": "array", + "items": { + "$ref": "#/components/schemas/LinkPcsCounters" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/counters/pcs/{port_id}/{link_id}": { + "get": { + "summary": "Get the Physical Coding Sublayer (PCS) counters for the given link.", + "operationId": "pcs_counters_get", + "parameters": [ + { + "in": "path", + "name": "link_id", + "description": "The link in the switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LinkPcsCounters" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/counters/rmon/{port_id}/{link_id}/all": { + "get": { + "summary": "Get the full set of traffic counters for the given link.", + "operationId": "rmon_counters_get_all", + "parameters": [ + { + "in": "path", + "name": "link_id", + "description": "The link in the switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LinkRMonCountersAll" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/counters/rmon/{port_id}/{link_id}/subset": { + "get": { + "summary": "Get the most relevant subset of traffic counters for the given link.", + "operationId": "rmon_counters_get", + "parameters": [ + { + "in": "path", + "name": "link_id", + "description": "The link in the switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LinkRMonCounters" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/dpd-uptime": { + "get": { + "summary": "Return the server uptime.", + "operationId": "dpd_uptime", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "int64", + "type": "integer", + "format": "int64" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/dpd-version": { + "get": { + "summary": "Return the version of the `dpd` server itself.", + "operationId": "dpd_version", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "String", + "type": "string" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/leds": { + "get": { + "summary": "Return the state of all attention LEDs on the Sidecar QSFP ports.", + "operationId": "leds_list", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Map_of_Led", + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/Led" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/links": { + "get": { + "summary": "List all links, on all switch ports.", + "operationId": "link_list_all", + "parameters": [ + { + "in": "query", + "name": "filter", + "description": "Filter links to those whose name contains the provided string.\n\nIf not provided, then all links are returned.", + "schema": { + "nullable": true, + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_Link", + "type": "array", + "items": { + "$ref": "#/components/schemas/Link" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/links/tfport_data": { + "get": { + "summary": "Collect the link data consumed by `tfportd`. This app-specific convenience", + "description": "routine is meant to reduce the time and traffic expended on this once-per-second operation, by consolidating multiple per-link requests into a single per-switch request.", + "operationId": "tfport_data", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_TfportData", + "type": "array", + "items": { + "$ref": "#/components/schemas/TfportData" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/loopback/ipv4": { + "get": { + "summary": "Get loopback IPv4 addresses.", + "operationId": "loopback_ipv4_list", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_Ipv4Entry", + "type": "array", + "items": { + "$ref": "#/components/schemas/Ipv4Entry" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "post": { + "summary": "Add a loopback IPv4.", + "operationId": "loopback_ipv4_create", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Ipv4Entry" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/loopback/ipv4/{ipv4}": { + "delete": { + "summary": "Remove one loopback IPv4 address.", + "operationId": "loopback_ipv4_delete", + "parameters": [ + { + "in": "path", + "name": "ipv4", + "required": true, + "schema": { + "type": "string", + "format": "ipv4" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/loopback/ipv6": { + "get": { + "summary": "Get loopback IPv6 addresses.", + "operationId": "loopback_ipv6_list", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_Ipv6Entry", + "type": "array", + "items": { + "$ref": "#/components/schemas/Ipv6Entry" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "post": { + "summary": "Add a loopback IPv6.", + "operationId": "loopback_ipv6_create", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Ipv6Entry" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/loopback/ipv6/{ipv6}": { + "delete": { + "summary": "Remove one loopback IPv6 address.", + "operationId": "loopback_ipv6_delete", + "parameters": [ + { + "in": "path", + "name": "ipv6", + "required": true, + "schema": { + "type": "string", + "format": "ipv6" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/multicast/external-groups": { + "post": { + "summary": "Create an external-only multicast group configuration (API v3).", + "operationId": "multicast_group_create_external_v3", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MulticastGroupCreateExternalEntry" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MulticastGroupExternalResponse" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/multicast/external-groups/{group_ip}": { + "put": { + "summary": "Update an external-only multicast group configuration (API v3).", + "description": "Tags are optional for backward compatibility.", + "operationId": "multicast_group_update_external_v3", + "parameters": [ + { + "in": "path", + "name": "group_ip", + "required": true, + "schema": { + "type": "string", + "format": "ip" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MulticastGroupUpdateExternalEntry" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MulticastGroupExternalResponse" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/multicast/groups": { + "get": { + "summary": "List all multicast groups (API v3).", + "operationId": "multicast_groups_list_v3", + "parameters": [ + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MulticastGroupResponseResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + }, + "delete": { + "summary": "Reset all multicast group configurations.", + "operationId": "multicast_reset", + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/multicast/groups/{group_ip}": { + "get": { + "summary": "Get the multicast group configuration for a given group IP address (API v3).", + "operationId": "multicast_group_get_v3", + "parameters": [ + { + "in": "path", + "name": "group_ip", + "required": true, + "schema": { + "type": "string", + "format": "ip" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MulticastGroupResponse" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "summary": "Delete a multicast group configuration by IP address (API versions 1-3).", + "operationId": "multicast_group_delete_v3", + "parameters": [ + { + "in": "path", + "name": "group_ip", + "required": true, + "schema": { + "type": "string", + "format": "ip" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/multicast/tags/{tag}": { + "get": { + "summary": "List all multicast groups with a given tag (API v3).", + "operationId": "multicast_groups_list_by_tag_v3", + "parameters": [ + { + "in": "path", + "name": "tag", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MulticastGroupResponseResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + }, + "delete": { + "summary": "Delete all multicast groups (and associated routes) with a given tag", + "description": "(API versions 1-3).", + "operationId": "multicast_reset_by_tag_v3", + "parameters": [ + { + "in": "path", + "name": "tag", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/multicast/underlay-groups": { + "post": { + "summary": "Create an underlay (internal) multicast group configuration (API v1-v3).", + "operationId": "multicast_group_create_underlay_v3", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MulticastGroupCreateUnderlayEntry" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MulticastGroupUnderlayResponse" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/multicast/underlay-groups/{group_ip}": { + "get": { + "summary": "Get an underlay (internal) multicast group configuration (API v1-v3).", + "description": "Uses the broader ff04::/16 (admin-local) address validation for backward compatibility. Delegates to v4 endpoint with path param conversion.", + "operationId": "multicast_group_get_underlay_v3", + "parameters": [ + { + "in": "path", + "name": "group_ip", + "required": true, + "schema": { + "$ref": "#/components/schemas/AdminScopedIpv6" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MulticastGroupUnderlayResponse" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "summary": "Update an underlay (internal) multicast group configuration (API v1-v3).", + "description": "Uses the broader ff04::/16 (admin-local) address validation for backward compatibility. Tags are optional in v3 for backward compatibility.", + "operationId": "multicast_group_update_underlay_v3", + "parameters": [ + { + "in": "path", + "name": "group_ip", + "required": true, + "schema": { + "$ref": "#/components/schemas/AdminScopedIpv6" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MulticastGroupUpdateUnderlayEntry" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MulticastGroupUnderlayResponse" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/multicast/untagged": { + "delete": { + "summary": "Delete all multicast groups (and associated routes) without a tag.", + "operationId": "multicast_reset_untagged_v3", + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/nat/ipv4": { + "get": { + "summary": "Get all of the external addresses in use for IPv4 NAT mappings.", + "operationId": "nat_ipv4_addresses_list", + "parameters": [ + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ipv4ResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + }, + "delete": { + "summary": "Clear all IPv4 NAT mappings.", + "operationId": "nat_ipv4_reset", + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/nat/ipv4/{ipv4}": { + "get": { + "summary": "Get all of the external->internal NAT mappings for a given IPv4 address.", + "operationId": "nat_ipv4_list", + "parameters": [ + { + "in": "path", + "name": "ipv4", + "required": true, + "schema": { + "type": "string", + "format": "ipv4" + } + }, + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Ipv4NatResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + } + }, + "/nat/ipv4/{ipv4}/{low}": { + "get": { + "summary": "Get the external->internal NAT mapping for the given address/port", + "operationId": "nat_ipv4_get", + "parameters": [ + { + "in": "path", + "name": "ipv4", + "required": true, + "schema": { + "type": "string", + "format": "ipv4" + } + }, + { + "in": "path", + "name": "low", + "required": true, + "schema": { + "type": "integer", + "format": "uint16", + "minimum": 0 + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NatTarget" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "summary": "Clear the NAT mappings for an IPv4 address and starting L3 port.", + "operationId": "nat_ipv4_delete", + "parameters": [ + { + "in": "path", + "name": "ipv4", + "required": true, + "schema": { + "type": "string", + "format": "ipv4" + } + }, + { + "in": "path", + "name": "low", + "required": true, + "schema": { + "type": "integer", + "format": "uint16", + "minimum": 0 + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/nat/ipv4/{ipv4}/{low}/{high}": { + "put": { + "summary": "Add an external->internal NAT mapping for the given address/port range", + "description": "This maps an external IPv6 address and L3 port range to: - A gimlet's IPv6 address - A gimlet's MAC address - A Geneve VNI\n\nThese identify the gimlet on which a guest is running, and gives OPTE the information it needs to identify the guest VM that uses the external IPv6 and port range when making connections outside of an Oxide rack.", + "operationId": "nat_ipv4_create", + "parameters": [ + { + "in": "path", + "name": "high", + "required": true, + "schema": { + "type": "integer", + "format": "uint16", + "minimum": 0 + } + }, + { + "in": "path", + "name": "ipv4", + "required": true, + "schema": { + "type": "string", + "format": "ipv4" + } + }, + { + "in": "path", + "name": "low", + "required": true, + "schema": { + "type": "integer", + "format": "uint16", + "minimum": 0 + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NatTarget" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/nat/ipv6": { + "get": { + "summary": "Get all of the external addresses in use for NAT mappings.", + "operationId": "nat_ipv6_addresses_list", + "parameters": [ + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ipv6ResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + }, + "delete": { + "summary": "Clear all IPv6 NAT mappings.", + "operationId": "nat_ipv6_reset", + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/nat/ipv6/{ipv6}": { + "get": { + "summary": "Get all of the external->internal NAT mappings for a given address.", + "operationId": "nat_ipv6_list", + "parameters": [ + { + "in": "path", + "name": "ipv6", + "required": true, + "schema": { + "type": "string", + "format": "ipv6" + } + }, + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Ipv6NatResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + } + }, + "/nat/ipv6/{ipv6}/{low}": { + "get": { + "summary": "Get the external->internal NAT mapping for the given address and starting L3", + "description": "port.", + "operationId": "nat_ipv6_get", + "parameters": [ + { + "in": "path", + "name": "ipv6", + "required": true, + "schema": { + "type": "string", + "format": "ipv6" + } + }, + { + "in": "path", + "name": "low", + "required": true, + "schema": { + "type": "integer", + "format": "uint16", + "minimum": 0 + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NatTarget" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "summary": "Delete the NAT mapping for an IPv6 address and starting L3 port.", + "operationId": "nat_ipv6_delete", + "parameters": [ + { + "in": "path", + "name": "ipv6", + "required": true, + "schema": { + "type": "string", + "format": "ipv6" + } + }, + { + "in": "path", + "name": "low", + "required": true, + "schema": { + "type": "integer", + "format": "uint16", + "minimum": 0 + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/nat/ipv6/{ipv6}/{low}/{high}": { + "put": { + "summary": "Add an external->internal NAT mapping for the given address and L3 port", + "description": "range.\n\nThis maps an external IPv6 address and L3 port range to: - A gimlet's IPv6 address - A gimlet's MAC address - A Geneve VNI\n\nThese identify the gimlet on which a guest is running, and gives OPTE the information it needs to identify the guest VM that uses the external IPv6 and port range when making connections outside of an Oxide rack.", + "operationId": "nat_ipv6_create", + "parameters": [ + { + "in": "path", + "name": "high", + "required": true, + "schema": { + "type": "integer", + "format": "uint16", + "minimum": 0 + } + }, + { + "in": "path", + "name": "ipv6", + "required": true, + "schema": { + "type": "string", + "format": "ipv6" + } + }, + { + "in": "path", + "name": "low", + "required": true, + "schema": { + "type": "integer", + "format": "uint16", + "minimum": 0 + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NatTarget" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/ndp": { + "get": { + "summary": "Fetch the IPv6 NDP table entries.", + "description": "This returns a paginated list of all IPv6 neighbors directly connected to the switch.", + "operationId": "ndp_list", + "parameters": [ + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ArpEntryResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + }, + "post": { + "summary": "Add an IPv6 NDP entry, mapping an IPv6 address to a MAC address.", + "operationId": "ndp_create", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ArpEntry" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "summary": "Remove all entries in the the IPv6 NDP tables.", + "operationId": "ndp_reset", + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/ndp/{ip}": { + "get": { + "summary": "Get a single IPv6 NDP table entry, by its IPv6 address.", + "operationId": "ndp_get", + "parameters": [ + { + "in": "path", + "name": "ip", + "required": true, + "schema": { + "type": "string", + "format": "ipv6" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ArpEntry" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "summary": "Remove an IPv6 NDP entry, by its IPv6 address.", + "operationId": "ndp_delete", + "parameters": [ + { + "in": "path", + "name": "ip", + "required": true, + "schema": { + "type": "string", + "format": "ipv6" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/port/{port_id}/settings": { + "get": { + "summary": "Get port settings atomically.", + "operationId": "port_settings_get", + "parameters": [ + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + }, + { + "in": "query", + "name": "tag", + "description": "Restrict operations on this port to the provided tag.", + "schema": { + "nullable": true, + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PortSettings" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "post": { + "summary": "Apply port settings atomically.", + "description": "These settings will be applied holistically, and to the extent possible atomically to a given port. In the event of a failure a rollback is attempted. If the rollback fails there will be inconsistent state. This failure mode returns the error code \"rollback failure\". For more details see the docs on the [`PortSettings`] type.", + "operationId": "port_settings_apply", + "parameters": [ + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + }, + { + "in": "query", + "name": "tag", + "description": "Restrict operations on this port to the provided tag.", + "schema": { + "nullable": true, + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PortSettings" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PortSettings" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "summary": "Clear port settings atomically.", + "operationId": "port_settings_clear", + "parameters": [ + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + }, + { + "in": "query", + "name": "tag", + "description": "Restrict operations on this port to the provided tag.", + "schema": { + "nullable": true, + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PortSettings" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/ports": { + "get": { + "summary": "List all switch ports on the system.", + "operationId": "port_list", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_PortId", + "type": "array", + "items": { + "$ref": "#/components/schemas/PortId" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/ports/{port_id}": { + "get": { + "summary": "Return information about a single switch port.", + "operationId": "port_get", + "parameters": [ + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SwitchPort" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/ports/{port_id}/led": { + "get": { + "summary": "Return the current state of the attention LED on a front-facing QSFP port.", + "operationId": "led_get", + "parameters": [ + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Led" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "summary": "Override the current state of the attention LED on a front-facing QSFP port.", + "description": "The attention LED normally follows the state of the port itself. For example, if a transceiver is powered and operating normally, then the LED is solid on. An unexpected power fault would then be reflected by powering off the LED.\n\nThe client may override this behavior, explicitly setting the LED to a specified state. This can be undone, sending the LED back to its default policy, with the endpoint `/ports/{port_id}/led/auto`.", + "operationId": "led_set", + "parameters": [ + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LedState" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/ports/{port_id}/led/auto": { + "put": { + "summary": "Set the LED policy to automatic.", + "description": "The automatic LED policy ensures that the state of the LED follows the state of the switch port itself.", + "operationId": "led_set_auto", + "parameters": [ + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/ports/{port_id}/links": { + "get": { + "summary": "List the links within a single switch port.", + "operationId": "link_list", + "parameters": [ + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_Link", + "type": "array", + "items": { + "$ref": "#/components/schemas/Link" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "post": { + "summary": "Create a link on a switch port.", + "description": "Create an interface that can be used for sending Ethernet frames on the provided switch port. This will use the first available lanes in the physical port to create an interface of the desired speed, if possible.", + "operationId": "link_create", + "parameters": [ + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LinkCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LinkId" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/ports/{port_id}/links/{link_id}": { + "get": { + "summary": "Get an existing link by ID.", + "operationId": "link_get", + "parameters": [ + { + "in": "path", + "name": "link_id", + "description": "The link in the switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Link" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "summary": "Delete a link from a switch port.", + "operationId": "link_delete", + "parameters": [ + { + "in": "path", + "name": "link_id", + "description": "The link in the switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/ports/{port_id}/links/{link_id}/autoneg": { + "get": { + "summary": "Return whether the link is configured to use autonegotiation with its peer", + "description": "link.", + "operationId": "link_autoneg_get", + "parameters": [ + { + "in": "path", + "name": "link_id", + "description": "The link in the switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Boolean", + "type": "boolean" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "summary": "Set whether a port is configured to use autonegotation with its peer link.", + "operationId": "link_autoneg_set", + "parameters": [ + { + "in": "path", + "name": "link_id", + "description": "The link in the switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "title": "Boolean", + "type": "boolean" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/ports/{port_id}/links/{link_id}/ber": { + "get": { + "summary": "Return the estimated bit-error rate (BER) for a link.", + "operationId": "link_ber_get", + "parameters": [ + { + "in": "path", + "name": "link_id", + "description": "The link in the switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Ber" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/ports/{port_id}/links/{link_id}/enabled": { + "get": { + "summary": "Return whether the link is enabled.", + "operationId": "link_enabled_get", + "parameters": [ + { + "in": "path", + "name": "link_id", + "description": "The link in the switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Boolean", + "type": "boolean" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "summary": "Enable or disable a link.", + "operationId": "link_enabled_set", + "parameters": [ + { + "in": "path", + "name": "link_id", + "description": "The link in the switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "title": "Boolean", + "type": "boolean" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/ports/{port_id}/links/{link_id}/fault": { + "get": { + "summary": "Return any fault currently set on this link", + "operationId": "link_fault_get", + "parameters": [ + { + "in": "path", + "name": "link_id", + "description": "The link in the switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FaultCondition" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "post": { + "summary": "Inject a fault on this link", + "operationId": "link_fault_inject", + "parameters": [ + { + "in": "path", + "name": "link_id", + "description": "The link in the switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "title": "String", + "type": "string" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "summary": "Clear any fault currently set on this link", + "operationId": "link_fault_clear", + "parameters": [ + { + "in": "path", + "name": "link_id", + "description": "The link in the switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/ports/{port_id}/links/{link_id}/history": { + "get": { + "summary": "Get the event history for the given link.", + "operationId": "link_history_get", + "parameters": [ + { + "in": "path", + "name": "link_id", + "description": "The link in the switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LinkHistory" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/ports/{port_id}/links/{link_id}/ipv4": { + "get": { + "summary": "List the IPv4 addresses associated with a link.", + "operationId": "link_ipv4_list", + "parameters": [ + { + "in": "path", + "name": "link_id", + "description": "The link in the switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + }, + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Ipv4EntryResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + }, + "post": { + "summary": "Add an IPv4 address to a link.", + "operationId": "link_ipv4_create", + "parameters": [ + { + "in": "path", + "name": "link_id", + "description": "The link in the switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Ipv4Entry" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "summary": "Clear all IPv4 addresses from a link.", + "operationId": "link_ipv4_reset", + "parameters": [ + { + "in": "path", + "name": "link_id", + "description": "The link in the switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/ports/{port_id}/links/{link_id}/ipv4/{address}": { + "delete": { + "summary": "Remove an IPv4 address from a link.", + "operationId": "link_ipv4_delete", + "parameters": [ + { + "in": "path", + "name": "address", + "description": "The IPv4 address on which to operate.", + "required": true, + "schema": { + "type": "string", + "format": "ipv4" + } + }, + { + "in": "path", + "name": "link_id", + "description": "The link in the switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/ports/{port_id}/links/{link_id}/ipv6": { + "get": { + "summary": "List the IPv6 addresses associated with a link.", + "operationId": "link_ipv6_list", + "parameters": [ + { + "in": "path", + "name": "link_id", + "description": "The link in the switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + }, + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Ipv6EntryResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + }, + "post": { + "summary": "Add an IPv6 address to a link.", + "operationId": "link_ipv6_create", + "parameters": [ + { + "in": "path", + "name": "link_id", + "description": "The link in the switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Ipv6Entry" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "summary": "Clear all IPv6 addresses from a link.", + "operationId": "link_ipv6_reset", + "parameters": [ + { + "in": "path", + "name": "link_id", + "description": "The link in the switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/ports/{port_id}/links/{link_id}/ipv6/{address}": { + "delete": { + "summary": "Remove an IPv6 address from a link.", + "operationId": "link_ipv6_delete", + "parameters": [ + { + "in": "path", + "name": "address", + "description": "The IPv6 address on which to operate.", + "required": true, + "schema": { + "type": "string", + "format": "ipv6" + } + }, + { + "in": "path", + "name": "link_id", + "description": "The link in the switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/ports/{port_id}/links/{link_id}/ipv6_enabled": { + "get": { + "summary": "Return whether the link is configured to act as an IPv6 endpoint", + "operationId": "link_ipv6_enabled_get", + "parameters": [ + { + "in": "path", + "name": "link_id", + "description": "The link in the switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Boolean", + "type": "boolean" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "summary": "Set whether a port is configured to act as an IPv6 endpoint", + "operationId": "link_ipv6_enabled_set", + "parameters": [ + { + "in": "path", + "name": "link_id", + "description": "The link in the switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "title": "Boolean", + "type": "boolean" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/ports/{port_id}/links/{link_id}/kr": { + "get": { + "summary": "Return whether the link is in KR mode.", + "description": "\"KR\" refers to the Ethernet standard for the link, which are defined in various clauses of the IEEE 802.3 specification. \"K\" is used to denote a link over an electrical cabled backplane, and \"R\" refers to \"scrambled encoding\", a 64B/66B bit-encoding scheme.\n\nThus this should be true iff a link is on the cabled backplane.", + "operationId": "link_kr_get", + "parameters": [ + { + "in": "path", + "name": "link_id", + "description": "The link in the switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Boolean", + "type": "boolean" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "summary": "Enable or disable a link.", + "operationId": "link_kr_set", + "parameters": [ + { + "in": "path", + "name": "link_id", + "description": "The link in the switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "title": "Boolean", + "type": "boolean" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/ports/{port_id}/links/{link_id}/linkup": { + "get": { + "summary": "Return whether a link is up.", + "operationId": "link_linkup_get", + "parameters": [ + { + "in": "path", + "name": "link_id", + "description": "The link in the switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Boolean", + "type": "boolean" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/ports/{port_id}/links/{link_id}/mac": { + "get": { + "summary": "Get a link's MAC address.", + "operationId": "link_mac_get", + "parameters": [ + { + "in": "path", + "name": "link_id", + "description": "The link in the switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MacAddr" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "summary": "Set a link's MAC address.", + "operationId": "link_mac_set", + "parameters": [ + { + "in": "path", + "name": "link_id", + "description": "The link in the switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MacAddr" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/ports/{port_id}/links/{link_id}/nat_only": { + "get": { + "summary": "Return whether the link is configured to drop non-nat traffic", + "operationId": "link_nat_only_get", + "parameters": [ + { + "in": "path", + "name": "link_id", + "description": "The link in the switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Boolean", + "type": "boolean" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "summary": "Set whether a port is configured to use drop non-nat traffic", + "operationId": "link_nat_only_set", + "parameters": [ + { + "in": "path", + "name": "link_id", + "description": "The link in the switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "title": "Boolean", + "type": "boolean" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/ports/{port_id}/links/{link_id}/prbs": { + "get": { + "summary": "Return the link's PRBS speed and mode.", + "description": "During link training, a pseudorandom bit sequence (PRBS) is used to allow each side to synchronize their clocks and set various parameters on the underlying circuitry (such as filter gains).", + "operationId": "link_prbs_get", + "parameters": [ + { + "in": "path", + "name": "link_id", + "description": "The link in the switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PortPrbsMode" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "summary": "Set a link's PRBS speed and mode.", + "operationId": "link_prbs_set", + "parameters": [ + { + "in": "path", + "name": "link_id", + "description": "The link in the switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PortPrbsMode" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/ports/{port_id}/links/{link_id}/serdes/adapt": { + "get": { + "summary": "Get the per-lane adaptation counts for each lane on this link", + "operationId": "link_rx_adapt_count_get", + "parameters": [ + { + "in": "path", + "name": "link_id", + "description": "The link in the switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_DfeAdaptationState", + "type": "array", + "items": { + "$ref": "#/components/schemas/DfeAdaptationState" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/ports/{port_id}/links/{link_id}/serdes/anlt_status": { + "get": { + "summary": "Get the per-lane AN/LT status for each lane on this link", + "operationId": "link_an_lt_get", + "parameters": [ + { + "in": "path", + "name": "link_id", + "description": "The link in the switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AnLtStatus" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/ports/{port_id}/links/{link_id}/serdes/enc_speed": { + "get": { + "summary": "Get the per-lane speed and encoding for each lane on this link", + "operationId": "link_enc_speed_get", + "parameters": [ + { + "in": "path", + "name": "link_id", + "description": "The link in the switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_EncSpeed", + "type": "array", + "items": { + "$ref": "#/components/schemas/EncSpeed" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/ports/{port_id}/links/{link_id}/serdes/eye": { + "get": { + "summary": "Get the per-lane eye measurements for each lane on this link", + "operationId": "link_eye_get", + "parameters": [ + { + "in": "path", + "name": "link_id", + "description": "The link in the switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_SerdesEye", + "type": "array", + "items": { + "$ref": "#/components/schemas/SerdesEye" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/ports/{port_id}/links/{link_id}/serdes/lane_map": { + "get": { + "summary": "Get the logical->physical mappings for each lane in this port", + "operationId": "lane_map_get", + "parameters": [ + { + "in": "path", + "name": "link_id", + "description": "The link in the switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LaneMap" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/ports/{port_id}/links/{link_id}/serdes/rx_sig": { + "get": { + "summary": "Get the per-lane rx signal info for each lane on this link", + "operationId": "link_rx_sig_info_get", + "parameters": [ + { + "in": "path", + "name": "link_id", + "description": "The link in the switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_RxSigInfo", + "type": "array", + "items": { + "$ref": "#/components/schemas/RxSigInfo" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/ports/{port_id}/links/{link_id}/serdes/tx_eq": { + "get": { + "summary": "Get the per-lane tx eq settings for each lane on this link", + "operationId": "link_tx_eq_get", + "parameters": [ + { + "in": "path", + "name": "link_id", + "description": "The link in the switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_TxEqSwHw", + "type": "array", + "items": { + "$ref": "#/components/schemas/TxEqSwHw" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "summary": "Update the per-lane tx eq settings for all lanes on this link", + "operationId": "link_tx_eq_set", + "parameters": [ + { + "in": "path", + "name": "link_id", + "description": "The link in the switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TxEq" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/ports/{port_id}/management-mode": { + "get": { + "summary": "Return the current management mode of a QSFP switch port.", + "operationId": "management_mode_get", + "parameters": [ + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ManagementMode" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "summary": "Set the current management mode of a QSFP switch port.", + "operationId": "management_mode_set", + "parameters": [ + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ManagementMode" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/ports/{port_id}/transceiver": { + "get": { + "summary": "Return the information about a port's transceiver.", + "description": "This returns the status (presence, power state, etc) of the transceiver along with its identifying information. If the port is an optical switch port, but has no transceiver, then the identifying information is empty.\n\nIf the switch port is not a QSFP port, and thus could never have a transceiver, then \"Not Found\" is returned.", + "operationId": "transceiver_get", + "parameters": [ + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Transceiver" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/ports/{port_id}/transceiver/datapath": { + "get": { + "summary": "Fetch the state of the datapath for the provided transceiver.", + "operationId": "transceiver_datapath_get", + "parameters": [ + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Datapath" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/ports/{port_id}/transceiver/monitors": { + "get": { + "summary": "Fetch the monitored environmental information for the provided transceiver.", + "operationId": "transceiver_monitors_get", + "parameters": [ + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Monitors" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/ports/{port_id}/transceiver/power": { + "get": { + "summary": "Return the power state of a transceiver.", + "operationId": "transceiver_power_get", + "parameters": [ + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PowerState" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "summary": "Control the power state of a transceiver.", + "operationId": "transceiver_power_set", + "parameters": [ + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PowerState" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/ports/{port_id}/transceiver/reset": { + "post": { + "summary": "Effect a module-level reset of a QSFP transceiver.", + "description": "If the QSFP port has no transceiver or is not a QSFP port, then a client error is returned.", + "operationId": "transceiver_reset", + "parameters": [ + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/route/ipv4": { + "get": { + "summary": "Fetch the configured IPv4 routes, mapping IPv4 CIDR blocks to the switch port", + "description": "used for sending out that traffic, and optionally a gateway.", + "operationId": "route_ipv4_list", + "parameters": [ + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Ipv4RoutesResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + }, + "put": { + "summary": "Route an IPv4 subnet to a link and a nexthop gateway.", + "description": "This call can be used to create a new single-path route or to replace any existing routes with a new single-path route.", + "operationId": "route_ipv4_set", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Ipv4RouteUpdate" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "post": { + "summary": "Route an IPv4 subnet to a link and a nexthop gateway.", + "description": "This call can be used to create a new single-path route or to add new targets to a multipath route.", + "operationId": "route_ipv4_add", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Ipv4RouteUpdate" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/route/ipv4/{cidr}": { + "get": { + "summary": "Get the configured route for the given IPv4 subnet.", + "operationId": "route_ipv4_get", + "parameters": [ + { + "in": "path", + "name": "cidr", + "description": "The IPv4 subnet in CIDR notation whose route entry is returned.", + "required": true, + "schema": { + "$ref": "#/components/schemas/Ipv4Net" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_Ipv4Route", + "type": "array", + "items": { + "$ref": "#/components/schemas/Ipv4Route" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "summary": "Remove all targets for the given subnet", + "operationId": "route_ipv4_delete", + "parameters": [ + { + "in": "path", + "name": "cidr", + "description": "The IPv4 subnet in CIDR notation whose route entry is returned.", + "required": true, + "schema": { + "$ref": "#/components/schemas/Ipv4Net" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/route/ipv4/{cidr}/{port_id}/{link_id}/{tgt_ip}": { + "delete": { + "summary": "Remove a single target for the given IPv4 subnet", + "operationId": "route_ipv4_delete_target", + "parameters": [ + { + "in": "path", + "name": "cidr", + "description": "The subnet being routed", + "required": true, + "schema": { + "$ref": "#/components/schemas/Ipv4Net" + } + }, + { + "in": "path", + "name": "link_id", + "description": "The link to which packets should be sent", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port to which packets should be sent", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + }, + { + "in": "path", + "name": "tgt_ip", + "description": "The next hop in the IPv4 route", + "required": true, + "schema": { + "type": "string", + "format": "ipv4" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/route/ipv6": { + "get": { + "summary": "Fetch the configured IPv6 routes, mapping IPv6 CIDR blocks to the switch port", + "description": "used for sending out that traffic, and optionally a gateway.", + "operationId": "route_ipv6_list", + "parameters": [ + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Ipv6RoutesResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + }, + "put": { + "summary": "Route an IPv6 subnet to a link and a nexthop gateway.", + "description": "This call can be used to create a new single-path route or to replace any existing routes with a new single-path route.", + "operationId": "route_ipv6_set", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Ipv6RouteUpdate" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "post": { + "summary": "Route an IPv6 subnet to a link and a nexthop gateway.", + "description": "This call can be used to create a new single-path route or to add new targets to a multipath route.", + "operationId": "route_ipv6_add", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Ipv6RouteUpdate" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/route/ipv6/{cidr}": { + "get": { + "summary": "Get a single IPv6 route, by its IPv6 CIDR block.", + "operationId": "route_ipv6_get", + "parameters": [ + { + "in": "path", + "name": "cidr", + "description": "The IPv6 subnet in CIDR notation whose route entry is returned.", + "required": true, + "schema": { + "$ref": "#/components/schemas/Ipv6Net" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_Ipv6Route", + "type": "array", + "items": { + "$ref": "#/components/schemas/Ipv6Route" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "summary": "Remove an IPv6 route, by its IPv6 CIDR block.", + "operationId": "route_ipv6_delete", + "parameters": [ + { + "in": "path", + "name": "cidr", + "description": "The IPv6 subnet in CIDR notation whose route entry is returned.", + "required": true, + "schema": { + "$ref": "#/components/schemas/Ipv6Net" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/route/ipv6/{cidr}/{port_id}/{link_id}/{tgt_ip}": { + "delete": { + "summary": "Remove a single target for the given IPv6 subnet", + "operationId": "route_ipv6_delete_target", + "parameters": [ + { + "in": "path", + "name": "cidr", + "description": "The subnet being routed", + "required": true, + "schema": { + "$ref": "#/components/schemas/Ipv6Net" + } + }, + { + "in": "path", + "name": "link_id", + "description": "The link to which packets should be sent", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port to which packets should be sent", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + }, + { + "in": "path", + "name": "tgt_ip", + "description": "The next hop in the IPv4 route", + "required": true, + "schema": { + "type": "string", + "format": "ipv6" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/rpw/nat/gen": { + "get": { + "summary": "Get NAT generation number", + "operationId": "nat_generation", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "int64", + "type": "integer", + "format": "int64" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/rpw/nat/trigger": { + "post": { + "summary": "Trigger NAT Reconciliation", + "operationId": "nat_trigger_update", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Null", + "type": "string", + "enum": [ + null + ] + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/switch/identifiers": { + "get": { + "summary": "Get switch identifiers.", + "description": "This endpoint returns the switch identifiers, which can be used for consistent field definitions across oximeter time series schemas.", + "operationId": "switch_identifiers", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SwitchIdentifiers" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/table": { + "get": { + "summary": "Get the list of P4 tables", + "operationId": "table_list", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_String", + "type": "array", + "items": { + "type": "string" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/table/{table}/counters": { + "get": { + "summary": "Get any counter data from a single P4 match-action table.", + "description": "The name of the table should match one of those returned by the `table_list()` call.", + "operationId": "table_counters", + "parameters": [ + { + "in": "query", + "name": "force_sync", + "description": "Force a sync of the counters from the ASIC to memory, even if the default refresh timeout hasn't been reached.", + "required": true, + "schema": { + "type": "boolean" + } + }, + { + "in": "path", + "name": "table", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_TableCounterEntry", + "type": "array", + "items": { + "$ref": "#/components/schemas/TableCounterEntry" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/table/{table}/dump": { + "get": { + "summary": "Get the contents of a single P4 table.", + "description": "The name of the table should match one of those returned by the `table_list()` call.", + "operationId": "table_dump", + "parameters": [ + { + "in": "path", + "name": "table", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Table" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/transceivers": { + "get": { + "summary": "Return information about all QSFP transceivers.", + "operationId": "transceivers_list", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Map_of_Transceiver", + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/Transceiver" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + } + }, + "components": { + "schemas": { + "AdminScopedIpv6": { + "description": "A validated admin-local IPv6 multicast address (API version 3).\n\nIn v3, admin-local addresses are validated against ff04::/16 (scope 4). In v4+, this was renamed to `UnderlayMulticastIpv6` and tightened to ff04::/64 to match Omicron's underlay multicast subnet allocation.", + "type": "string", + "format": "ipv6" + }, + "AnLtStatus": { + "description": "A collection of the data involved in the autonegiation/link-training process", + "type": "object", + "properties": { + "lanes": { + "description": "The per-lane status", + "type": "array", + "items": { + "$ref": "#/components/schemas/LaneStatus" + } + }, + "lp_pages": { + "description": "The base and extended pages received from the link partner", + "allOf": [ + { + "$ref": "#/components/schemas/LpPages" + } + ] + } + }, + "required": [ + "lanes", + "lp_pages" + ] + }, + "AnStatus": { + "description": "State of a single lane during autonegotiation", + "type": "object", + "properties": { + "an_ability": { + "description": "Are we capable of AN?", + "type": "boolean" + }, + "an_complete": { + "description": "Is autonegotiation complete?", + "type": "boolean" + }, + "ext_np_status": { + "description": "Is extended page format supported?", + "type": "boolean" + }, + "link_status": { + "description": "Allegedly: is the link up? In practice, this always seems to be false? TODO: investigate this", + "type": "boolean" + }, + "lp_an_ability": { + "description": "Can the link partner perform AN?", + "type": "boolean" + }, + "page_rcvd": { + "description": "has a base page been received?", + "type": "boolean" + }, + "parallel_detect_fault": { + "description": "A fault has been detected via the parallel detection function", + "type": "boolean" + }, + "remote_fault": { + "description": "Remote fault detected", + "type": "boolean" + } + }, + "required": [ + "an_ability", + "an_complete", + "ext_np_status", + "link_status", + "lp_an_ability", + "page_rcvd", + "parallel_detect_fault", + "remote_fault" + ] + }, + "ApplicationDescriptor": { + "description": "An Application Descriptor describes the supported datapath configurations.\n\nThis is a CMIS-specific concept. It's used for modules to advertise how it can be used by the host. Each application describes the host-side electrical interface; the media-side interface; the number of lanes required; etc.\n\nHost-side software can select one of these applications to instruct the module to use a specific set of lanes, with the interface on either side of the module.", + "type": "object", + "properties": { + "host_id": { + "description": "The electrical interface with the host side.", + "type": "string" + }, + "host_lane_assignment_options": { + "description": "The lanes on the host-side supporting this application.\n\nThis is a bit mask with a 1 identifying the lowest lane in a consecutive group of lanes to which the application can be assigned. This must be used with the `host_lane_count`. For example a value of `0b0000_0001` with a host lane count of 4 indicates that the first 4 lanes may be used in this application.\n\nAn application may support starting from multiple lanes.", + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "host_lane_count": { + "description": "The number of host-side lanes.", + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "media_id": { + "description": "The interface, optical or copper, with the media side.", + "allOf": [ + { + "$ref": "#/components/schemas/MediaInterfaceId" + } + ] + }, + "media_lane_assignment_options": { + "description": "The lanes on the media-side supporting this application.\n\nSee `host_lane_assignment_options` for details.", + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "media_lane_count": { + "description": "The number of media-side lanes.", + "type": "integer", + "format": "uint8", + "minimum": 0 + } + }, + "required": [ + "host_id", + "host_lane_assignment_options", + "host_lane_count", + "media_id", + "media_lane_assignment_options", + "media_lane_count" + ] + }, + "ArpEntry": { + "description": "Represents the mapping of an IP address to a MAC address.", + "type": "object", + "properties": { + "ip": { + "description": "The IP address for the entry.", + "type": "string", + "format": "ip" + }, + "mac": { + "description": "The MAC address to which `ip` maps.", + "allOf": [ + { + "$ref": "#/components/schemas/MacAddr" + } + ] + }, + "tag": { + "description": "A tag used to associate this entry with a client.", + "type": "string" + }, + "update": { + "description": "The time the entry was updated", + "type": "string" + } + }, + "required": [ + "ip", + "mac", + "tag", + "update" + ] + }, + "ArpEntryResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/ArpEntry" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "Aux1Monitor": { + "description": "The first auxlliary CMIS monitor.", + "oneOf": [ + { + "description": "The monitored property is custom, i.e., part-specific.", + "type": "object", + "properties": { + "custom": { + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "minItems": 2, + "maxItems": 2 + } + }, + "required": [ + "custom" + ], + "additionalProperties": false + }, + { + "description": "The current of the laser thermoelectric cooler.\n\nFor actively-cooled laser systems, this specifies the percentage of the maximum current the thermoelectric cooler supports. If the percentage is positive, the cooler is heating the laser. If negative, the cooler is cooling the laser.", + "type": "object", + "properties": { + "tec_current": { + "type": "number", + "format": "float" + } + }, + "required": [ + "tec_current" + ], + "additionalProperties": false + } + ] + }, + "Aux2Monitor": { + "description": "The second auxlliary CMIS monitor.", + "oneOf": [ + { + "description": "The temperature of the laser itself (degrees C).", + "type": "object", + "properties": { + "laser_temperature": { + "type": "number", + "format": "float" + } + }, + "required": [ + "laser_temperature" + ], + "additionalProperties": false + }, + { + "description": "The current of the laser thermoelectric cooler.\n\nFor actively-cooled laser systems, this specifies the percentage of the maximum current the thermoelectric cooler supports. If the percentage is positive, the cooler is heating the laser. If negative, the cooler is cooling the laser.", + "type": "object", + "properties": { + "tec_current": { + "type": "number", + "format": "float" + } + }, + "required": [ + "tec_current" + ], + "additionalProperties": false + } + ] + }, + "Aux3Monitor": { + "description": "The third auxlliary CMIS monitor.", + "oneOf": [ + { + "description": "The temperature of the laser itself (degrees C).", + "type": "object", + "properties": { + "laser_temperature": { + "type": "number", + "format": "float" + } + }, + "required": [ + "laser_temperature" + ], + "additionalProperties": false + }, + { + "description": "Measured voltage of an additional power supply (Volts).", + "type": "object", + "properties": { + "additional_supply_voltage": { + "type": "number", + "format": "float" + } + }, + "required": [ + "additional_supply_voltage" + ], + "additionalProperties": false + } + ] + }, + "AuxMonitors": { + "description": "Auxlliary monitored values for CMIS modules.", + "type": "object", + "properties": { + "aux1": { + "nullable": true, + "description": "Auxlliary monitor 1, either a custom value or TEC current.", + "allOf": [ + { + "$ref": "#/components/schemas/Aux1Monitor" + } + ] + }, + "aux2": { + "nullable": true, + "description": "Auxlliary monitor 1, either laser temperature or TEC current.", + "allOf": [ + { + "$ref": "#/components/schemas/Aux2Monitor" + } + ] + }, + "aux3": { + "nullable": true, + "description": "Auxlliary monitor 1, either laser temperature or additional supply voltage.", + "allOf": [ + { + "$ref": "#/components/schemas/Aux3Monitor" + } + ] + }, + "custom": { + "nullable": true, + "description": "A custom monitor. The value here is entirely vendor- and part-specific, so the part's data sheet must be consulted. The value may be either a signed or unsigned 16-bit integer, and so is included as raw bytes.", + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "minItems": 2, + "maxItems": 2 + } + } + }, + "BackplaneCableLeg": { + "description": "The leg of the backplane cable.\n\nThis describes the leg on the actual backplane cable that connects the Sidecar chassis connector to a cubby endpoint.", + "type": "string", + "enum": [ + "A", + "B", + "C", + "D" + ] + }, + "BackplaneLink": { + "description": "A single point-to-point connection on the cabled backplane.\n\nThis describes a single link from the Sidecar switch to a cubby, via the cabled backplane. It ultimately maps the Tofino ASIC pins to the cubby at which that link terminates. This path follows the Sidecar internal cable; the Sidecar chassis connector; and the backplane cable itself. This is used to map the Tofino driver's \"connector\" number (an index in its possible pinouts) through the backplane to our logical cubby numbering.", + "type": "object", + "properties": { + "backplane_leg": { + "$ref": "#/components/schemas/BackplaneCableLeg" + }, + "cubby": { + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "sidecar_connector": { + "$ref": "#/components/schemas/SidecarConnector" + }, + "sidecar_leg": { + "$ref": "#/components/schemas/SidecarCableLeg" + }, + "tofino_connector": { + "type": "integer", + "format": "uint8", + "minimum": 0 + } + }, + "required": [ + "backplane_leg", + "cubby", + "sidecar_connector", + "sidecar_leg", + "tofino_connector" + ] + }, + "Ber": { + "description": "Reports the bit-error rate (BER) for a link.", + "type": "object", + "properties": { + "ber": { + "description": "Estimated BER per-lane.", + "type": "array", + "items": { + "type": "number", + "format": "float" + } + }, + "symbol_errors": { + "description": "Counters of symbol errors per-lane.", + "type": "array", + "items": { + "type": "integer", + "format": "uint64", + "minimum": 0 + } + }, + "total_ber": { + "description": "Aggregate BER on the link.", + "type": "number", + "format": "float" + } + }, + "required": [ + "ber", + "symbol_errors", + "total_ber" + ] + }, + "BuildInfo": { + "description": "Detailed build information about `dpd`.", + "type": "object", + "properties": { + "cargo_triple": { + "type": "string" + }, + "debug": { + "type": "boolean" + }, + "git_branch": { + "type": "string" + }, + "git_commit_timestamp": { + "type": "string" + }, + "git_sha": { + "type": "string" + }, + "opt_level": { + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "rustc_channel": { + "type": "string" + }, + "rustc_commit_sha": { + "type": "string" + }, + "rustc_host_triple": { + "type": "string" + }, + "rustc_semver": { + "type": "string" + }, + "sde_commit_sha": { + "type": "string" + }, + "version": { + "type": "string" + } + }, + "required": [ + "cargo_triple", + "debug", + "git_branch", + "git_commit_timestamp", + "git_sha", + "opt_level", + "rustc_channel", + "rustc_commit_sha", + "rustc_host_triple", + "rustc_semver", + "sde_commit_sha", + "version" + ] + }, + "CmisDatapath": { + "description": "A datapath in a CMIS module.\n\nIn contrast to SFF-8636, CMIS makes first-class the concept of a datapath: a set of lanes and all the associated machinery involved in the transfer of data. This includes:\n\n- The \"application descriptor\" which is the host and media interfaces, and the lanes on each side used to transfer data; - The state of the datapath in a well-defined finite state machine (see CMIS 5.0 section 6.3.3); - The flags indicating how the datapath components are operating, such as receiving an input Rx signal or whether the transmitter is disabled.", + "type": "object", + "properties": { + "application": { + "description": "The application descriptor for this datapath.", + "allOf": [ + { + "$ref": "#/components/schemas/ApplicationDescriptor" + } + ] + }, + "lane_status": { + "description": "The status bits for each lane in the datapath.", + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/CmisLaneStatus" + } + } + }, + "required": [ + "application", + "lane_status" + ] + }, + "CmisLaneStatus": { + "description": "The status of a single CMIS lane.\n\nIf any particular control or status value is unsupported by a module, it is `None`.", + "type": "object", + "properties": { + "rx_auto_squelch_disable": { + "nullable": true, + "description": "Whether the host-side has disabled the Rx auto-squelch.\n\nThe module can implement automatic squelching of the Rx output, if the media-side input signal isn't valid. This indicates whether the host has disabled such a setting.", + "type": "boolean" + }, + "rx_lol": { + "nullable": true, + "description": "Media-side loss of lock flag.\n\nThis is true if the module is not able to extract a clock signal from the media-side signal (usually optical).", + "type": "boolean" + }, + "rx_los": { + "nullable": true, + "description": "Media-side loss of signal flag.\n\nThis is true if there is no detected input signal from the media-side (usually optical).", + "type": "boolean" + }, + "rx_output_enabled": { + "nullable": true, + "description": "Whether the Rx output is enabled.\n\nThe host may control this to disable the electrical output from the module to the host.", + "type": "boolean" + }, + "rx_output_polarity": { + "nullable": true, + "description": "The Rx output polarity.\n\nThis indicates a host-side control that flips the polarity of the host-side output signal.", + "allOf": [ + { + "$ref": "#/components/schemas/LanePolarity" + } + ] + }, + "rx_output_status": { + "description": "Status of host-side Rx output.\n\nThis indicates whether the Rx output is sending a valid signal to the host. Note that this is `Invalid` if the output is either muted (such as squelched) or explicitly disabled.", + "allOf": [ + { + "$ref": "#/components/schemas/OutputStatus" + } + ] + }, + "state": { + "description": "The datapath state of this lane.\n\nSee CMIS 5.0 section 8.9.1 for details.", + "type": "string" + }, + "tx_adaptive_eq_fail": { + "nullable": true, + "description": "A failure in the Tx adaptive input equalization.", + "type": "boolean" + }, + "tx_auto_squelch_disable": { + "nullable": true, + "description": "Whether the host-side has disabled the Tx auto-squelch.\n\nThe module can implement automatic squelching of the Tx output, if the host-side input signal isn't valid. This indicates whether the host has disabled such a setting.", + "type": "boolean" + }, + "tx_failure": { + "nullable": true, + "description": "General Tx failure flag.\n\nThis indicates that an internal and unspecified malfunction has occurred on the Tx lane.", + "type": "boolean" + }, + "tx_force_squelch": { + "nullable": true, + "description": "Whether the host-side has force-squelched the Tx output.\n\nThis indicates that the host can _force_ squelching the output if the signal is not valid.", + "type": "boolean" + }, + "tx_input_polarity": { + "nullable": true, + "description": "The Tx input polarity.\n\nThis indicates a host-side control that flips the polarity of the host-side input signal.", + "allOf": [ + { + "$ref": "#/components/schemas/LanePolarity" + } + ] + }, + "tx_lol": { + "nullable": true, + "description": "Host-side loss of lock flag.\n\nThis is true if the module is not able to extract a clock signal from the host-side electrical signal.", + "type": "boolean" + }, + "tx_los": { + "nullable": true, + "description": "Host-side loss of signal flag.\n\nThis is true if there is no detected electrical signal from the host-side serdes.", + "type": "boolean" + }, + "tx_output_enabled": { + "nullable": true, + "description": "Whether the Tx output is enabled.", + "type": "boolean" + }, + "tx_output_status": { + "description": "Status of media-side Tx output.\n\nThis indicates whether the Rx output is sending a valid signal to the media itself. Note that this is `Invalid` if the output is either muted (such as squelched) or explicitly disabled.", + "allOf": [ + { + "$ref": "#/components/schemas/OutputStatus" + } + ] + } + }, + "required": [ + "rx_output_status", + "state", + "tx_output_status" + ] + }, + "CounterData": { + "description": "For a counter, this contains the number of bytes, packets, or both that were counted. XXX: Ideally this would be a data-bearing enum, with variants for Pkts, Bytes, and PktsAndBytes. However OpenApi doesn't yet have the necessary support, so we're left with this clumsier representation.", + "type": "object", + "properties": { + "bytes": { + "nullable": true, + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "pkts": { + "nullable": true, + "type": "integer", + "format": "uint64", + "minimum": 0 + } + } + }, + "Datapath": { + "description": "Information about a transceiver's datapath.\n\nThis includes state related to the low-level eletrical and optical path through which bits flow. This includes flags like loss-of-signal / loss-of-lock; transmitter enablement state; and equalization parameters.", + "oneOf": [ + { + "description": "A number of datapaths in a CMIS module.\n\nCMIS modules may have a large number of supported configurations of their various lanes, each called an \"application\". These are described by the `ApplicationDescriptor` type, which mirrors CMIS 5.0 table 8-18. Each descriptor is identified by an \"Application Selector Code\", which is just its index in the section of the memory map describing them.\n\nEach lane can be used in zero or more applications, however, it may exist in at most one application at a time. These active applications, of which there may be more than one, are keyed by their codes in the contained mapping.", + "type": "object", + "properties": { + "cmis": { + "type": "object", + "properties": { + "connector": { + "description": "The type of free-side connector", + "type": "string" + }, + "datapaths": { + "description": "Mapping from \"application selector\" ID to its datapath information.\n\nThe datapath inclues the lanes used; host electrical interface; media interface; and a lot more about the state of the path.", + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/CmisDatapath" + } + }, + "supported_lanes": { + "description": "A bit mask with a 1 in bit `i` if the `i`th lane is supported.", + "type": "integer", + "format": "uint8", + "minimum": 0 + } + }, + "required": [ + "connector", + "datapaths", + "supported_lanes" + ] + } + }, + "required": [ + "cmis" + ], + "additionalProperties": false + }, + { + "description": "Datapath state about each lane in an SFF-8636 module.", + "type": "object", + "properties": { + "sff8636": { + "type": "object", + "properties": { + "connector": { + "description": "The type of a media-side connector.\n\nThese values come from SFF-8024 Rev 4.10 Table 4-3.", + "type": "string" + }, + "lanes": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Sff8636Datapath" + }, + "minItems": 4, + "maxItems": 4 + }, + "specification": { + "$ref": "#/components/schemas/SffComplianceCode" + } + }, + "required": [ + "connector", + "lanes", + "specification" + ] + } + }, + "required": [ + "sff8636" + ], + "additionalProperties": false + } + ] + }, + "DfeAdaptationState": { + "description": "Rx DFE adaptation information", + "type": "object", + "properties": { + "adapt_cnt": { + "description": "Total DFE attempts", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "adapt_done": { + "description": "DFE complete", + "type": "boolean" + }, + "link_lost_cnt": { + "description": "Times the signal was lost since the last read", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "readapt_cnt": { + "description": "DFE attempts since the last read", + "type": "integer", + "format": "uint32", + "minimum": 0 + } + }, + "required": [ + "adapt_cnt", + "adapt_done", + "link_lost_cnt", + "readapt_cnt" + ] + }, + "Direction": { + "description": "Direction a multicast group member is reached by.\n\n`External` group members must have any packet encapsulation removed before packet delivery.", + "type": "string", + "enum": [ + "Underlay", + "External" + ] + }, + "ElectricalMode": { + "description": "The electrical mode of a QSFP-capable port.\n\nQSFP ports can be broken out into one of several different electrical configurations or modes. This describes how the transmit/receive lanes are grouped into a single, logical link.\n\nNote that the electrical mode may only be changed if there are no links within the port, _and_ if the inserted QSFP module actually supports this mode.", + "oneOf": [ + { + "description": "All transmit/receive lanes are used for a single link.", + "type": "string", + "enum": [ + "Single" + ] + } + ] + }, + "EncSpeed": { + "description": "Signal speed and encoding for a single lane", + "type": "object", + "properties": { + "encoding": { + "$ref": "#/components/schemas/LaneEncoding" + }, + "gigabits": { + "type": "integer", + "format": "uint32", + "minimum": 0 + } + }, + "required": [ + "encoding", + "gigabits" + ] + }, + "Error": { + "description": "Error information from a response.", + "type": "object", + "properties": { + "error_code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "request_id": { + "type": "string" + } + }, + "required": [ + "message", + "request_id" + ] + }, + "ExternalForwarding": { + "description": "Represents the forwarding configuration for external multicast traffic.", + "type": "object", + "properties": { + "vlan_id": { + "nullable": true, + "type": "integer", + "format": "uint16", + "minimum": 0 + } + } + }, + "Fault": { + "description": "A Fault represents a specific kind of failure, and carries some additional context. Currently Faults are only used to describe Link failures, but there is no reason they couldn't be used elsewhere.", + "oneOf": [ + { + "type": "object", + "properties": { + "LinkFlap": { + "type": "string" + } + }, + "required": [ + "LinkFlap" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "Autoneg": { + "type": "string" + } + }, + "required": [ + "Autoneg" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "Injected": { + "type": "string" + } + }, + "required": [ + "Injected" + ], + "additionalProperties": false + } + ] + }, + "FaultCondition": { + "description": "Represents a potential fault condtion on a link", + "type": "object", + "properties": { + "fault": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/Fault" + } + ] + } + } + }, + "FaultReason": { + "description": "The cause of a fault on a transceiver.", + "oneOf": [ + { + "description": "An error occurred accessing the transceiver.", + "type": "string", + "enum": [ + "failed" + ] + }, + { + "description": "Power was enabled, but did not come up in the requisite time.", + "type": "string", + "enum": [ + "power_timeout" + ] + }, + { + "description": "Power was enabled and later lost.", + "type": "string", + "enum": [ + "power_lost" + ] + }, + { + "description": "The service processor disabled the transceiver.\n\nThe SP is responsible for monitoring the thermal data from the transceivers, and controlling the fans to compensate. If a module's thermal data cannot be read, the SP may completely disable the transceiver to ensure it cannot overheat the Sidecar.", + "type": "string", + "enum": [ + "disabled_by_sp" + ] + } + ] + }, + "FecRSCounters": { + "description": "Per-port RS FEC counters", + "type": "object", + "properties": { + "fec_align_status": { + "description": "All lanes synced and aligned", + "type": "boolean" + }, + "fec_corr_cnt": { + "description": "FEC corrected blocks", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "fec_ser_lane_0": { + "description": "FEC symbol errors on lane 0", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "fec_ser_lane_1": { + "description": "FEC symbol errors on lane 1", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "fec_ser_lane_2": { + "description": "FEC symbol errors on lane 2", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "fec_ser_lane_3": { + "description": "FEC symbol errors on lane 3", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "fec_ser_lane_4": { + "description": "FEC symbol errors on lane 4", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "fec_ser_lane_5": { + "description": "FEC symbol errors on lane 5", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "fec_ser_lane_6": { + "description": "FEC symbol errors on lane 6", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "fec_ser_lane_7": { + "description": "FEC symbol errors on lane 7", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "fec_uncorr_cnt": { + "description": "FEC uncorrected blocks", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "hi_ser": { + "description": "symbol errors exceeds threshhold", + "type": "boolean" + }, + "port": { + "description": "Port being tracked", + "type": "string" + } + }, + "required": [ + "fec_align_status", + "fec_corr_cnt", + "fec_ser_lane_0", + "fec_ser_lane_1", + "fec_ser_lane_2", + "fec_ser_lane_3", + "fec_ser_lane_4", + "fec_ser_lane_5", + "fec_ser_lane_6", + "fec_ser_lane_7", + "fec_uncorr_cnt", + "hi_ser", + "port" + ] + }, + "FreeChannels": { + "description": "Represents the free MAC channels on a single physical port.", + "type": "object", + "properties": { + "channels": { + "description": "The set of available channels (lanes) on this connector.", + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0 + } + }, + "connector": { + "description": "The Tofino connector for this port.\n\nThis describes the set of electrical connections representing this port object, which are defined by the pinout and board design of the Sidecar.", + "type": "string" + }, + "port_id": { + "description": "The switch port.", + "allOf": [ + { + "$ref": "#/components/schemas/PortId" + } + ] + } + }, + "required": [ + "channels", + "connector", + "port_id" + ] + }, + "InternalForwarding": { + "description": "Represents the NAT target for multicast traffic for internal/underlay forwarding.", + "type": "object", + "properties": { + "nat_target": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/NatTarget" + } + ] + } + } + }, + "IpSrc": { + "description": "Source filter match key for multicast traffic.\n\nFor SSM groups, use `Exact` with specific source addresses. For ASM groups with any-source filtering, use `Any`.", + "oneOf": [ + { + "description": "Exact match for the source IP address.", + "type": "object", + "properties": { + "Exact": { + "type": "string", + "format": "ip" + } + }, + "required": [ + "Exact" + ], + "additionalProperties": false + }, + { + "description": "Match any source address (0.0.0.0/0 or ::/0 depending on group IP version).", + "type": "string", + "enum": [ + "Any" + ] + } + ] + }, + "Ipv4Entry": { + "description": "An IPv4 address assigned to a link.", + "type": "object", + "properties": { + "addr": { + "description": "The IP address.", + "type": "string", + "format": "ipv4" + }, + "tag": { + "description": "Client-side tag for this object.", + "type": "string" + } + }, + "required": [ + "addr", + "tag" + ] + }, + "Ipv4EntryResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/Ipv4Entry" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "Ipv4Nat": { + "description": "represents an IPv4 NAT reservation", + "type": "object", + "properties": { + "external": { + "type": "string", + "format": "ipv4" + }, + "high": { + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "low": { + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "target": { + "$ref": "#/components/schemas/NatTarget" + } + }, + "required": [ + "external", + "high", + "low", + "target" + ] + }, + "Ipv4NatResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/Ipv4Nat" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "Ipv4Net": { + "example": "192.168.1.0/24", + "title": "An IPv4 subnet", + "description": "An IPv4 subnet, including prefix and prefix length", + "x-rust-type": { + "crate": "oxnet", + "path": "oxnet::Ipv4Net", + "version": "0.1.0" + }, + "type": "string", + "pattern": "^(([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])/([0-9]|1[0-9]|2[0-9]|3[0-2])$" + }, + "Ipv4Route": { + "description": "A route for an IPv4 subnet.", + "type": "object", + "properties": { + "link_id": { + "$ref": "#/components/schemas/LinkId" + }, + "port_id": { + "$ref": "#/components/schemas/PortId" + }, + "tag": { + "type": "string" + }, + "tgt_ip": { + "type": "string", + "format": "ipv4" + }, + "vlan_id": { + "nullable": true, + "type": "integer", + "format": "uint16", + "minimum": 0 + } + }, + "required": [ + "link_id", + "port_id", + "tag", + "tgt_ip" + ] + }, + "Ipv4RouteUpdate": { + "description": "Represents a new or replacement mapping of a subnet to a single IPv4 RouteTarget nexthop target.", + "type": "object", + "properties": { + "cidr": { + "description": "Traffic destined for any address within the CIDR block is routed using this information.", + "allOf": [ + { + "$ref": "#/components/schemas/Ipv4Net" + } + ] + }, + "replace": { + "description": "Should this route replace any existing route? If a route exists and this parameter is false, then the call will fail.", + "type": "boolean" + }, + "target": { + "description": "A single Route associated with this CIDR", + "allOf": [ + { + "$ref": "#/components/schemas/Ipv4Route" + } + ] + } + }, + "required": [ + "cidr", + "replace", + "target" + ] + }, + "Ipv4Routes": { + "description": "Represents all mappings of an IPv4 subnet to a its nexthop target(s).", + "type": "object", + "properties": { + "cidr": { + "description": "Traffic destined for any address within the CIDR block is routed using this information.", + "allOf": [ + { + "$ref": "#/components/schemas/Ipv4Net" + } + ] + }, + "targets": { + "description": "All RouteTargets associated with this CIDR", + "type": "array", + "items": { + "$ref": "#/components/schemas/Ipv4Route" + } + } + }, + "required": [ + "cidr", + "targets" + ] + }, + "Ipv4RoutesResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/Ipv4Routes" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "Ipv6Entry": { + "description": "An IPv6 address assigned to a link.", + "type": "object", + "properties": { + "addr": { + "description": "The IP address.", + "type": "string", + "format": "ipv6" + }, + "tag": { + "description": "Client-side tag for this object.", + "type": "string" + } + }, + "required": [ + "addr", + "tag" + ] + }, + "Ipv6EntryResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/Ipv6Entry" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "Ipv6Nat": { + "description": "represents an IPv6 NAT reservation", + "type": "object", + "properties": { + "external": { + "type": "string", + "format": "ipv6" + }, + "high": { + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "low": { + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "target": { + "$ref": "#/components/schemas/NatTarget" + } + }, + "required": [ + "external", + "high", + "low", + "target" + ] + }, + "Ipv6NatResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/Ipv6Nat" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "Ipv6Net": { + "example": "fd12:3456::/64", + "title": "An IPv6 subnet", + "description": "An IPv6 subnet, including prefix and subnet mask", + "x-rust-type": { + "crate": "oxnet", + "path": "oxnet::Ipv6Net", + "version": "0.1.0" + }, + "type": "string", + "pattern": "^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))\\/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8])$" + }, + "Ipv6Route": { + "description": "A route for an IPv6 subnet.", + "type": "object", + "properties": { + "link_id": { + "$ref": "#/components/schemas/LinkId" + }, + "port_id": { + "$ref": "#/components/schemas/PortId" + }, + "tag": { + "type": "string" + }, + "tgt_ip": { + "type": "string", + "format": "ipv6" + }, + "vlan_id": { + "nullable": true, + "type": "integer", + "format": "uint16", + "minimum": 0 + } + }, + "required": [ + "link_id", + "port_id", + "tag", + "tgt_ip" + ] + }, + "Ipv6RouteUpdate": { + "description": "Represents a new or replacement mapping of a subnet to a single IPv6 RouteTarget nexthop target.", + "type": "object", + "properties": { + "cidr": { + "description": "Traffic destined for any address within the CIDR block is routed using this information.", + "allOf": [ + { + "$ref": "#/components/schemas/Ipv6Net" + } + ] + }, + "replace": { + "description": "Should this route replace any existing route? If a route exists and this parameter is false, then the call will fail.", + "type": "boolean" + }, + "target": { + "description": "A single RouteTarget associated with this CIDR", + "allOf": [ + { + "$ref": "#/components/schemas/Ipv6Route" + } + ] + } + }, + "required": [ + "cidr", + "replace", + "target" + ] + }, + "Ipv6Routes": { + "description": "Represents all mappings of an IPv6 subnet to a its nexthop target(s).", + "type": "object", + "properties": { + "cidr": { + "description": "Traffic destined for any address within the CIDR block is routed using this information.", + "allOf": [ + { + "$ref": "#/components/schemas/Ipv6Net" + } + ] + }, + "targets": { + "description": "All RouteTargets associated with this CIDR", + "type": "array", + "items": { + "$ref": "#/components/schemas/Ipv6Route" + } + } + }, + "required": [ + "cidr", + "targets" + ] + }, + "Ipv6RoutesResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/Ipv6Routes" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "LaneEncoding": { + "description": "Signal encoding", + "oneOf": [ + { + "description": "Pulse Amplitude Modulation 4-level", + "type": "string", + "enum": [ + "Pam4" + ] + }, + { + "description": "Non-Return-to-Zero encoding", + "type": "string", + "enum": [ + "Nrz" + ] + }, + { + "description": "No encoding selected", + "type": "string", + "enum": [ + "None" + ] + } + ] + }, + "LaneMap": { + "description": "Mapping of the logical lanes in a link to their physical instantiation in the MAC/serdes interface.", + "type": "object", + "properties": { + "logical_lane": { + "description": "logical lane within the mac block for each lane", + "type": "array", + "items": { + "type": "integer", + "format": "uint32", + "minimum": 0 + } + }, + "mac_block": { + "description": "MAC block in the tofino ASIC", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "rx_phys": { + "description": "Rx logical->physical mapping", + "type": "array", + "items": { + "type": "integer", + "format": "uint32", + "minimum": 0 + } + }, + "rx_polarity": { + "description": "Rx polarity", + "type": "array", + "items": { + "$ref": "#/components/schemas/Polarity" + } + }, + "tx_phys": { + "description": "Tx logical->physical mapping", + "type": "array", + "items": { + "type": "integer", + "format": "uint32", + "minimum": 0 + } + }, + "tx_polarity": { + "description": "Tx polarity", + "type": "array", + "items": { + "$ref": "#/components/schemas/Polarity" + } + } + }, + "required": [ + "logical_lane", + "mac_block", + "rx_phys", + "rx_polarity", + "tx_phys", + "tx_polarity" + ] + }, + "LanePolarity": { + "description": "The polarity of a transceiver lane.", + "type": "string", + "enum": [ + "normal", + "flipped" + ] + }, + "LaneStatus": { + "description": "The combined status of a lane, with respect to the autonegotiation / link-training process.", + "type": "object", + "properties": { + "lane_an_status": { + "description": "Detailed autonegotiation status", + "allOf": [ + { + "$ref": "#/components/schemas/AnStatus" + } + ] + }, + "lane_done": { + "description": "Has a lane successfully completed autoneg and link training?", + "type": "boolean" + }, + "lane_lt_status": { + "description": "Detailed link-training status", + "allOf": [ + { + "$ref": "#/components/schemas/LtStatus" + } + ] + } + }, + "required": [ + "lane_an_status", + "lane_done", + "lane_lt_status" + ] + }, + "Led": { + "description": "Information about a QSFP port's LED.", + "type": "object", + "properties": { + "policy": { + "description": "The policy by which the LED is controlled.", + "allOf": [ + { + "$ref": "#/components/schemas/LedPolicy" + } + ] + }, + "state": { + "description": "The state of the LED.", + "allOf": [ + { + "$ref": "#/components/schemas/LedState" + } + ] + } + }, + "required": [ + "policy", + "state" + ] + }, + "LedPolicy": { + "description": "The policy by which a port's LED is controlled.", + "oneOf": [ + { + "description": "The default policy is for the LED to reflect the port's state itself.\n\nIf the port is operating normally, the LED will be solid on. Without a transceiver, the LED will be solid off. A blinking LED is used to indicate an unsupported module or other failure on that port.", + "type": "string", + "enum": [ + "automatic" + ] + }, + { + "description": "The LED is explicitly overridden by client requests.", + "type": "string", + "enum": [ + "override" + ] + } + ] + }, + "LedState": { + "description": "The state of a module's attention LED, on the Sidecar front IO panel.", + "oneOf": [ + { + "description": "The LED is off.\n\nThis indicates that the port is disabled or not working at all.", + "type": "string", + "enum": [ + "off" + ] + }, + { + "description": "The LED is solid on.\n\nThis indicates that the port is working as expected and enabled.", + "type": "string", + "enum": [ + "on" + ] + }, + { + "description": "The LED is blinking.\n\nThis is used to draw attention to the port, such as to indicate a fault or to locate a port for servicing.", + "type": "string", + "enum": [ + "blink" + ] + } + ] + }, + "Link": { + "description": "An Ethernet-capable link within a switch port.", + "type": "object", + "properties": { + "address": { + "description": "The MAC address for the link.", + "allOf": [ + { + "$ref": "#/components/schemas/MacAddr" + } + ] + }, + "asic_id": { + "description": "The lower-level ASIC ID used to refer to this object in the switch driver software.", + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "autoneg": { + "description": "True if this link is configured to autonegotiate with its peer.", + "type": "boolean" + }, + "enabled": { + "description": "True if this link is enabled.", + "type": "boolean" + }, + "fec": { + "nullable": true, + "description": "The error-correction scheme for this link.", + "allOf": [ + { + "$ref": "#/components/schemas/PortFec" + } + ] + }, + "fsm_state": { + "description": "Current state in the autonegotiation/link-training finite state machine", + "type": "string" + }, + "ipv6_enabled": { + "description": "The link is configured for IPv6 use", + "type": "boolean" + }, + "kr": { + "description": "True if this link is in KR mode, i.e., is on a cabled backplane.", + "type": "boolean" + }, + "link_id": { + "description": "The `LinkId` within the switch port for this link.", + "allOf": [ + { + "$ref": "#/components/schemas/LinkId" + } + ] + }, + "link_state": { + "description": "The state of the Ethernet link.", + "allOf": [ + { + "$ref": "#/components/schemas/LinkState" + } + ] + }, + "media": { + "description": "The physical media underlying this link.", + "allOf": [ + { + "$ref": "#/components/schemas/PortMedia" + } + ] + }, + "port_id": { + "description": "The switch port on which this link exists.", + "allOf": [ + { + "$ref": "#/components/schemas/PortId" + } + ] + }, + "prbs": { + "description": "The PRBS mode.", + "allOf": [ + { + "$ref": "#/components/schemas/PortPrbsMode" + } + ] + }, + "presence": { + "description": "True if the transceiver module has detected a media presence.", + "type": "boolean" + }, + "speed": { + "description": "The speed of the link.", + "allOf": [ + { + "$ref": "#/components/schemas/PortSpeed" + } + ] + }, + "tofino_connector": { + "description": "The Tofino connector number associated with this link.", + "type": "integer", + "format": "uint16", + "minimum": 0 + } + }, + "required": [ + "address", + "asic_id", + "autoneg", + "enabled", + "fsm_state", + "ipv6_enabled", + "kr", + "link_id", + "link_state", + "media", + "port_id", + "prbs", + "presence", + "speed", + "tofino_connector" + ] + }, + "LinkCreate": { + "description": "Parameters used to create a link on a switch port.", + "type": "object", + "properties": { + "autoneg": { + "description": "Whether the link is configured to autonegotiate with its peer during link training.\n\nThis is generally only true for backplane links, and defaults to `false`.", + "default": false, + "type": "boolean" + }, + "fec": { + "nullable": true, + "description": "The requested forward-error correction method. If this is None, the standard FEC for the underlying media will be applied if it can be determined.", + "allOf": [ + { + "$ref": "#/components/schemas/PortFec" + } + ] + }, + "kr": { + "description": "Whether the link is configured in KR mode, an electrical specification generally only true for backplane link.\n\nThis defaults to `false`.", + "default": false, + "type": "boolean" + }, + "lane": { + "nullable": true, + "description": "The first lane of the port to use for the new link", + "allOf": [ + { + "$ref": "#/components/schemas/LinkId" + } + ] + }, + "speed": { + "description": "The requested speed of the link.", + "allOf": [ + { + "$ref": "#/components/schemas/PortSpeed" + } + ] + }, + "tx_eq": { + "nullable": true, + "description": "Transceiver equalization adjustment parameters. This defaults to `None`.", + "default": null, + "allOf": [ + { + "$ref": "#/components/schemas/TxEq" + } + ] + } + }, + "required": [ + "speed" + ] + }, + "LinkEvent": { + "type": "object", + "properties": { + "channel": { + "nullable": true, + "description": "Channel ID for sub-link-level events", + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "class": { + "description": "Event class", + "type": "string" + }, + "details": { + "nullable": true, + "description": "Optionally, additional details about the event", + "type": "string" + }, + "subclass": { + "description": "Event subclass", + "type": "string" + }, + "timestamp": { + "description": "Time the event occurred. The time is represented in milliseconds, starting at an undefined time in the past. This means that timestamps can be used to measure the time between events, but not to determine the wall-clock time at which the event occurred.", + "type": "integer", + "format": "int64" + } + }, + "required": [ + "class", + "subclass", + "timestamp" + ] + }, + "LinkFecRSCounters": { + "description": "The FEC counters for a specific link, including its link ID.", + "type": "object", + "properties": { + "counters": { + "description": "The FEC counter data.", + "allOf": [ + { + "$ref": "#/components/schemas/FecRSCounters" + } + ] + }, + "link_id": { + "description": "The link ID.", + "allOf": [ + { + "$ref": "#/components/schemas/LinkId" + } + ] + }, + "port_id": { + "description": "The switch port ID.", + "allOf": [ + { + "$ref": "#/components/schemas/PortId" + } + ] + } + }, + "required": [ + "counters", + "link_id", + "port_id" + ] + }, + "LinkFsmCounter": { + "description": "Reports how many times a given autoneg/link-training state has been entered", + "type": "object", + "properties": { + "current": { + "description": "Times entered since the link was last enabled", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "state_name": { + "description": "FSM state being counted", + "type": "string" + }, + "total": { + "description": "Times entered since the link was created", + "type": "integer", + "format": "uint32", + "minimum": 0 + } + }, + "required": [ + "current", + "state_name", + "total" + ] + }, + "LinkFsmCounters": { + "description": "Reports all the autoneg/link-training states a link has transitioned into.", + "type": "object", + "properties": { + "counters": { + "description": "All the states this link has entered, along with counts of how many times each state was entered.", + "type": "array", + "items": { + "$ref": "#/components/schemas/LinkFsmCounter" + } + }, + "link_path": { + "description": "Link being reported", + "type": "string" + } + }, + "required": [ + "counters", + "link_path" + ] + }, + "LinkHistory": { + "type": "object", + "properties": { + "events": { + "description": "The set of historical events recorded", + "type": "array", + "items": { + "$ref": "#/components/schemas/LinkEvent" + } + }, + "timestamp": { + "description": "The timestamp in milliseconds at which this history was collected.", + "type": "integer", + "format": "int64" + } + }, + "required": [ + "events", + "timestamp" + ] + }, + "LinkId": { + "description": "An identifier for a link within a switch port.\n\nA switch port identified by a [`PortId`] may have multiple links within it, each identified by a `LinkId`. These are unique within a switch port only.\n\n[`PortId`]: common::ports::PortId", + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "LinkPcsCounters": { + "description": "The Physical Coding Sublayer (PCS) counters for a specific link.", + "type": "object", + "properties": { + "counters": { + "description": "The PCS counter data.", + "allOf": [ + { + "$ref": "#/components/schemas/PcsCounters" + } + ] + }, + "link_id": { + "description": "The link ID.", + "allOf": [ + { + "$ref": "#/components/schemas/LinkId" + } + ] + }, + "port_id": { + "description": "The switch port ID.", + "allOf": [ + { + "$ref": "#/components/schemas/PortId" + } + ] + } + }, + "required": [ + "counters", + "link_id", + "port_id" + ] + }, + "LinkRMonCounters": { + "description": "The RMON counters (traffic counters) for a specific link.", + "type": "object", + "properties": { + "counters": { + "description": "The RMON counter data.", + "allOf": [ + { + "$ref": "#/components/schemas/RMonCounters" + } + ] + }, + "link_id": { + "description": "The link ID.", + "allOf": [ + { + "$ref": "#/components/schemas/LinkId" + } + ] + }, + "port_id": { + "description": "The switch port ID.", + "allOf": [ + { + "$ref": "#/components/schemas/PortId" + } + ] + } + }, + "required": [ + "counters", + "link_id", + "port_id" + ] + }, + "LinkRMonCountersAll": { + "description": "The complete RMON counters (traffic counters) for a specific link.", + "type": "object", + "properties": { + "counters": { + "description": "The RMON counter data.", + "allOf": [ + { + "$ref": "#/components/schemas/RMonCountersAll" + } + ] + }, + "link_id": { + "description": "The link ID.", + "allOf": [ + { + "$ref": "#/components/schemas/LinkId" + } + ] + }, + "port_id": { + "description": "The switch port ID.", + "allOf": [ + { + "$ref": "#/components/schemas/PortId" + } + ] + } + }, + "required": [ + "counters", + "link_id", + "port_id" + ] + }, + "LinkSettings": { + "description": "An object with link settings used in concert with [`PortSettings`].", + "type": "object", + "properties": { + "addrs": { + "type": "array", + "items": { + "type": "string", + "format": "ip" + }, + "uniqueItems": true + }, + "params": { + "$ref": "#/components/schemas/LinkCreate" + } + }, + "required": [ + "addrs", + "params" + ] + }, + "LinkState": { + "description": "The state of a data link with a peer.", + "oneOf": [ + { + "description": "An error was encountered while trying to configure the link in the switch hardware.", + "type": "object", + "properties": { + "config_error": { + "type": "string" + } + }, + "required": [ + "config_error" + ], + "additionalProperties": false + }, + { + "description": "The link is up.", + "type": "string", + "enum": [ + "up" + ] + }, + { + "description": "The link is down.", + "type": "string", + "enum": [ + "down" + ] + }, + { + "description": "The Link is offline due to a fault", + "type": "object", + "properties": { + "faulted": { + "$ref": "#/components/schemas/Fault" + } + }, + "required": [ + "faulted" + ], + "additionalProperties": false + }, + { + "description": "The link's state is not known.", + "type": "string", + "enum": [ + "unknown" + ] + } + ] + }, + "LinkUpCounter": { + "description": "Reports how many times a link has transitioned from Down to Up.", + "type": "object", + "properties": { + "current": { + "description": "LinkUp transitions since the link was last enabled", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "link_path": { + "description": "Link being reported", + "type": "string" + }, + "total": { + "description": "LinkUp transitions since the link was created", + "type": "integer", + "format": "uint32", + "minimum": 0 + } + }, + "required": [ + "current", + "link_path", + "total" + ] + }, + "LpPages": { + "description": "Set of AN pages sent by our link partner", + "type": "object", + "properties": { + "base_page": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "next_page1": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "next_page2": { + "type": "integer", + "format": "uint64", + "minimum": 0 + } + }, + "required": [ + "base_page", + "next_page1", + "next_page2" + ] + }, + "LtStatus": { + "description": "Link-training status for a single lane", + "type": "object", + "properties": { + "frame_lock": { + "description": "Frame lock state", + "type": "boolean" + }, + "readout_state": { + "description": "Readout for frame lock state", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "readout_training_state": { + "description": "Training state readout", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "readout_txstate": { + "description": "State machine readout for training arbiter", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "rx_trained": { + "description": "Local training finished", + "type": "boolean" + }, + "sig_det": { + "description": "Signal detect for PCS", + "type": "boolean" + }, + "training_failure": { + "description": "Link training failed", + "type": "boolean" + }, + "tx_training_data_en": { + "description": "TX control to send training pattern", + "type": "boolean" + } + }, + "required": [ + "frame_lock", + "readout_state", + "readout_training_state", + "readout_txstate", + "rx_trained", + "sig_det", + "training_failure", + "tx_training_data_en" + ] + }, + "MacAddr": { + "description": "An EUI-48 MAC address, used for layer-2 addressing.", + "type": "object", + "properties": { + "a": { + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "minItems": 6, + "maxItems": 6 + } + }, + "required": [ + "a" + ] + }, + "ManagementMode": { + "description": "How a switch port is managed.\n\nThe free-side devices in QSFP ports are complex devices, whose operation usually involves coordinated steps through one or more state machines. For example, when bringing up an optical link, a signal from the peer link must be detected; then a signal recovered; equalizer gains set; etc. In `Automatic` mode, all these kinds of steps are managed autonomously by switch driver software. In `Manual` mode, none of these will occur -- a switch port will only change in response to explicit requests from the operator or Oxide control plane.", + "oneOf": [ + { + "description": "A port is managed manually, by either the Oxide control plane or an operator.", + "type": "string", + "enum": [ + "manual" + ] + }, + { + "description": "A port is managed automatically by the switch software.", + "type": "string", + "enum": [ + "automatic" + ] + } + ] + }, + "MediaInterfaceId": { + "oneOf": [ + { + "type": "object", + "properties": { + "id": { + "description": "Media interface ID for multi-mode fiber media.\n\nSee SFF-8024 Table 4-6.", + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "mmf" + ] + } + }, + "required": [ + "id", + "type" + ] + }, + { + "type": "object", + "properties": { + "id": { + "description": "Media interface ID for single-mode fiber.\n\nSee SFF-8024 Table 4-7.", + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "smf" + ] + } + }, + "required": [ + "id", + "type" + ] + }, + { + "type": "object", + "properties": { + "id": { + "description": "Media interface ID for passive copper cables.\n\nSee SFF-8024 Table 4-8.", + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "passive_copper" + ] + } + }, + "required": [ + "id", + "type" + ] + }, + { + "type": "object", + "properties": { + "id": { + "description": "Media interface ID for active cable assemblies.\n\nSee SFF-8024 Table 4-9.", + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "active_cable" + ] + } + }, + "required": [ + "id", + "type" + ] + }, + { + "type": "object", + "properties": { + "id": { + "description": "Media interface ID for BASE-T.\n\nSee SFF-8024 Table 4-10.", + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "base_t" + ] + } + }, + "required": [ + "id", + "type" + ] + } + ] + }, + "Monitors": { + "description": "Free-side device monitoring information.\n\nNote that all values are optional, as some specifications do not require that modules implement monitoring of those values.", + "type": "object", + "properties": { + "aux_monitors": { + "nullable": true, + "description": "Auxiliary monitoring values.\n\nThese are only available on CMIS-compatible transceivers, e.g., QSFP-DD.", + "allOf": [ + { + "$ref": "#/components/schemas/AuxMonitors" + } + ] + }, + "receiver_power": { + "nullable": true, + "description": "The measured input optical power (milliwatts);\n\nNote that due to a limitation in the SFF-8636 specification, it's possible for receiver power to be zero. See [`ReceiverPower`] for details.", + "type": "array", + "items": { + "$ref": "#/components/schemas/ReceiverPower" + } + }, + "supply_voltage": { + "nullable": true, + "description": "The measured input supply voltage (Volts).", + "type": "number", + "format": "float" + }, + "temperature": { + "nullable": true, + "description": "The measured cage temperature (degrees C);", + "type": "number", + "format": "float" + }, + "transmitter_bias_current": { + "nullable": true, + "description": "The output laser bias current (milliamps).", + "type": "array", + "items": { + "type": "number", + "format": "float" + } + }, + "transmitter_power": { + "nullable": true, + "description": "The measured output optical power (milliwatts).", + "type": "array", + "items": { + "type": "number", + "format": "float" + } + } + } + }, + "MulticastGroupCreateExternalEntry": { + "description": "A multicast group configuration for POST requests for external (to the rack) groups.", + "type": "object", + "properties": { + "external_forwarding": { + "$ref": "#/components/schemas/ExternalForwarding" + }, + "group_ip": { + "type": "string", + "format": "ip" + }, + "internal_forwarding": { + "$ref": "#/components/schemas/InternalForwarding" + }, + "sources": { + "nullable": true, + "type": "array", + "items": { + "$ref": "#/components/schemas/IpSrc" + } + }, + "tag": { + "nullable": true, + "description": "Tag for validating update/delete requests. If a tag is not provided, one is auto-generated as `{uuid}:{group_ip}`.", + "type": "string" + } + }, + "required": [ + "external_forwarding", + "group_ip", + "internal_forwarding" + ] + }, + "MulticastGroupCreateUnderlayEntry": { + "description": "A multicast group configuration for POST requests for internal (to the rack) groups.", + "type": "object", + "properties": { + "group_ip": { + "$ref": "#/components/schemas/UnderlayMulticastIpv6" + }, + "members": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MulticastGroupMember" + } + }, + "tag": { + "nullable": true, + "description": "Tag for validating update/delete requests. If a tag is not provided, one is auto-generated as `{uuid}:{group_ip}`.", + "type": "string" + } + }, + "required": [ + "group_ip", + "members" + ] + }, + "MulticastGroupExternalResponse": { + "description": "Response structure for external multicast group operations (API version 3).", + "type": "object", + "properties": { + "external_forwarding": { + "$ref": "#/components/schemas/ExternalForwarding" + }, + "external_group_id": { + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "group_ip": { + "type": "string", + "format": "ip" + }, + "internal_forwarding": { + "$ref": "#/components/schemas/InternalForwarding" + }, + "sources": { + "nullable": true, + "type": "array", + "items": { + "$ref": "#/components/schemas/IpSrc" + } + }, + "tag": { + "nullable": true, + "type": "string" + } + }, + "required": [ + "external_forwarding", + "external_group_id", + "group_ip", + "internal_forwarding" + ] + }, + "MulticastGroupMember": { + "description": "Represents a member of a multicast group.", + "type": "object", + "properties": { + "direction": { + "$ref": "#/components/schemas/Direction" + }, + "link_id": { + "$ref": "#/components/schemas/LinkId" + }, + "port_id": { + "$ref": "#/components/schemas/PortId" + } + }, + "required": [ + "direction", + "link_id", + "port_id" + ] + }, + "MulticastGroupResponse": { + "description": "Unified response type for operations that return mixed group types (API version 3).", + "oneOf": [ + { + "description": "Response structure for underlay/internal multicast group operations (API version 3).", + "type": "object", + "properties": { + "external_group_id": { + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "group_ip": { + "$ref": "#/components/schemas/AdminScopedIpv6" + }, + "kind": { + "type": "string", + "enum": [ + "underlay" + ] + }, + "members": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MulticastGroupMember" + } + }, + "tag": { + "nullable": true, + "type": "string" + }, + "underlay_group_id": { + "type": "integer", + "format": "uint16", + "minimum": 0 + } + }, + "required": [ + "external_group_id", + "group_ip", + "kind", + "members", + "underlay_group_id" + ] + }, + { + "description": "Response structure for external multicast group operations (API version 3).", + "type": "object", + "properties": { + "external_forwarding": { + "$ref": "#/components/schemas/ExternalForwarding" + }, + "external_group_id": { + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "group_ip": { + "type": "string", + "format": "ip" + }, + "internal_forwarding": { + "$ref": "#/components/schemas/InternalForwarding" + }, + "kind": { + "type": "string", + "enum": [ + "external" + ] + }, + "sources": { + "nullable": true, + "type": "array", + "items": { + "$ref": "#/components/schemas/IpSrc" + } + }, + "tag": { + "nullable": true, + "type": "string" + } + }, + "required": [ + "external_forwarding", + "external_group_id", + "group_ip", + "internal_forwarding", + "kind" + ] + } + ] + }, + "MulticastGroupResponseResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/MulticastGroupResponse" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "MulticastGroupUnderlayResponse": { + "description": "Response structure for underlay/internal multicast group operations (API version 3).", + "type": "object", + "properties": { + "external_group_id": { + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "group_ip": { + "$ref": "#/components/schemas/AdminScopedIpv6" + }, + "members": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MulticastGroupMember" + } + }, + "tag": { + "nullable": true, + "type": "string" + }, + "underlay_group_id": { + "type": "integer", + "format": "uint16", + "minimum": 0 + } + }, + "required": [ + "external_group_id", + "group_ip", + "members", + "underlay_group_id" + ] + }, + "MulticastGroupUpdateExternalEntry": { + "description": "A multicast group update entry for PUT requests for external groups (API version 3).\n\nTag validation is optional in v3 for backward compatibility.", + "type": "object", + "properties": { + "external_forwarding": { + "$ref": "#/components/schemas/ExternalForwarding" + }, + "internal_forwarding": { + "$ref": "#/components/schemas/InternalForwarding" + }, + "sources": { + "nullable": true, + "type": "array", + "items": { + "$ref": "#/components/schemas/IpSrc" + } + }, + "tag": { + "nullable": true, + "description": "Tag for validating update requests. Optional in v3; if not provided, tag validation is skipped.", + "type": "string" + } + }, + "required": [ + "external_forwarding", + "internal_forwarding" + ] + }, + "MulticastGroupUpdateUnderlayEntry": { + "description": "A multicast group update entry for PUT requests for internal groups (API version 3).\n\nTags are optional in v3 for backward compatibility. If not provided, the existing tag is preserved.", + "type": "object", + "properties": { + "members": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MulticastGroupMember" + } + }, + "tag": { + "nullable": true, + "description": "Tag for validating update requests. Optional in v3; if not provided, tag validation is skipped.", + "type": "string" + } + }, + "required": [ + "members" + ] + }, + "NatTarget": { + "description": "represents an internal NAT target", + "type": "object", + "properties": { + "inner_mac": { + "$ref": "#/components/schemas/MacAddr" + }, + "internal_ip": { + "type": "string", + "format": "ipv6" + }, + "vni": { + "$ref": "#/components/schemas/Vni" + } + }, + "required": [ + "inner_mac", + "internal_ip", + "vni" + ] + }, + "Oui": { + "description": "An Organization Unique Identifier.", + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "minItems": 3, + "maxItems": 3 + }, + "OutputStatus": { + "type": "string", + "enum": [ + "valid", + "invalid" + ] + }, + "PcsCounters": { + "description": "Per-port PCS counters", + "type": "object", + "properties": { + "bad_sync_headers": { + "description": "Count of bad sync headers", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "bip_errors_per_pcs_lane": { + "description": "Bit Inteleaved Parity errors (per lane)", + "type": "array", + "items": { + "type": "integer", + "format": "uint32", + "minimum": 0 + } + }, + "block_lock_loss": { + "description": "Count of block-lock loss detections", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "errored_blocks": { + "description": "Count of errored blocks", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "hi_ber": { + "description": "Count of high bit error rate events", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "invalid_errors": { + "description": "Count of invalid error events", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "port": { + "description": "Port being tracked", + "type": "string" + }, + "sync_loss": { + "description": "Count of sync loss detections", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "unknown_errors": { + "description": "Count of unknown error events", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "valid_errors": { + "description": "Count of valid error events", + "type": "integer", + "format": "uint32", + "minimum": 0 + } + }, + "required": [ + "bad_sync_headers", + "bip_errors_per_pcs_lane", + "block_lock_loss", + "errored_blocks", + "hi_ber", + "invalid_errors", + "port", + "sync_loss", + "unknown_errors", + "valid_errors" + ] + }, + "Polarity": { + "type": "string", + "enum": [ + "Normal", + "Inverted" + ] + }, + "PortFec": { + "type": "string", + "enum": [ + "None", + "Firecode", + "RS" + ] + }, + "PortId": { + "example": "qsfp0", + "title": "PortId", + "description": "Physical switch port identifier", + "oneOf": [ + { + "title": "internal", + "type": "string", + "pattern": "(^[iI][nN][tT]0$)" + }, + { + "title": "rear", + "type": "string", + "pattern": "(^[rR][eE][aA][rR](([0-9])|([1-2][0-9])|(3[0-1]))$)" + }, + { + "title": "qsfp", + "type": "string", + "pattern": "(^[qQ][sS][fF][pP](([0-9])|([1-2][0-9])|(3[0-1]))$)" + } + ] + }, + "PortMedia": { + "type": "string", + "enum": [ + "Copper", + "Optical", + "CPU", + "None", + "Unknown" + ] + }, + "PortPrbsMode": { + "description": "Legal PRBS modes", + "type": "string", + "enum": [ + "Mode31", + "Mode23", + "Mode15", + "Mode13", + "Mode11", + "Mode9", + "Mode7", + "Mission" + ] + }, + "PortSettings": { + "description": "A port settings transaction object. When posted to the `/port-settings/{port_id}` API endpoint, these settings will be applied holistically, and to the extent possible atomically to a given port.", + "type": "object", + "properties": { + "links": { + "description": "The link settings to apply to the port on a per-link basis. Any links not in this map that are resident on the switch port will be removed. Any links that are in this map that are not resident on the switch port will be added. Any links that are resident on the switch port and in this map, and are different, will be modified. Links are indexed by spatial index within the port.", + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/LinkSettings" + } + } + }, + "required": [ + "links" + ] + }, + "PortSpeed": { + "description": "Speeds with which a single port may be configured", + "type": "string", + "enum": [ + "Speed0G", + "Speed1G", + "Speed10G", + "Speed25G", + "Speed40G", + "Speed50G", + "Speed100G", + "Speed200G", + "Speed400G" + ] + }, + "PowerMode": { + "description": "The power mode of a module.", + "type": "object", + "properties": { + "software_override": { + "nullable": true, + "description": "Whether the module is configured for software override of power control.\n\nIf the module is in `PowerState::Off`, this can't be determined, and `None` is returned.", + "type": "boolean" + }, + "state": { + "description": "The actual power state.", + "allOf": [ + { + "$ref": "#/components/schemas/PowerState" + } + ] + } + }, + "required": [ + "state" + ] + }, + "PowerState": { + "description": "An allowed power state for the module.", + "oneOf": [ + { + "description": "A module is entirely powered off, using the EFuse.", + "type": "string", + "enum": [ + "off" + ] + }, + { + "description": "Power is enabled to the module, but module remains in low-power mode.\n\nIn this state, modules will not establish a link or transmit traffic, but they may be managed and queried for information through their memory maps.", + "type": "string", + "enum": [ + "low" + ] + }, + { + "description": "The module is in high-power mode.\n\nNote that additional configuration may be required to correctly configure the module, such as described in SFF-8636 rev 2.10a table 6-10, and that the _host side_ is responsible for ensuring that the relevant configuration is applied.", + "type": "string", + "enum": [ + "high" + ] + } + ] + }, + "RMonCounters": { + "description": "High level subset of the RMon counters maintained by the Tofino ASIC", + "type": "object", + "properties": { + "crc_error_stomped": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "fragments_rx": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frame_too_long": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_dropped_buffer_full": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_rx_all": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_rx_ok": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_tx_all": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_tx_ok": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_tx_with_error": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_with_any_error": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "octets_rx": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "octets_rx_in_good_frames": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "octets_tx_total": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "octets_tx_without_error": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "port": { + "type": "string" + } + }, + "required": [ + "crc_error_stomped", + "fragments_rx", + "frame_too_long", + "frames_dropped_buffer_full", + "frames_rx_all", + "frames_rx_ok", + "frames_tx_all", + "frames_tx_ok", + "frames_tx_with_error", + "frames_with_any_error", + "octets_rx", + "octets_rx_in_good_frames", + "octets_tx_total", + "octets_tx_without_error", + "port" + ] + }, + "RMonCountersAll": { + "description": "All of the RMon counters maintained by the Tofino ASIC", + "type": "object", + "properties": { + "crc_error_stomped": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "fragments_rx": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frame_too_long": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_dropped_buffer_full": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_rx_all": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_rx_indersized": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_rx_length_1024_1518": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_rx_length_128_255": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_rx_length_1519_2047": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_rx_length_2048_4095": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_rx_length_256_511": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_rx_length_4096_8191": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_rx_length_512_1023": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_rx_length_65_127": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_rx_length_8192_9215": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_rx_length_9216": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_rx_length_eq_64": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_rx_length_lt_64": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_rx_oftype_pause": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_rx_ok": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_rx_oversized": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_rx_with_any_error": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_rx_with_broadcast_addresses": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_rx_with_fcs_error": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_rx_with_length_error": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_rx_with_multicast_addresses": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_rx_with_unicast_addresses": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_truncated": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_tx_all": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_tx_broadcast": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_tx_length_1024_1518": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_tx_length_128_255": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_tx_length_1519_2047": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_tx_length_2048_4095": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_tx_length_256_511": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_tx_length_4096_8191": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_tx_length_512_1023": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_tx_length_65_127": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_tx_length_8192_9215": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_tx_length_9216": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_tx_length_eq_64": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_tx_length_lt_64": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_tx_multicast": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_tx_ok": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_tx_pause": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_tx_pri_pause": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_tx_unicast": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_tx_vlan": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_tx_with_error": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "jabber_rx": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "octets_rx": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "octets_rx_in_good_frames": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "octets_tx_total": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "octets_tx_without_error": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "port": { + "type": "string" + }, + "pri0_frames_rx": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "pri0_framex_tx": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "pri1_frames_rx": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "pri1_frames_tx": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "pri2_frames_rx": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "pri2_frames_tx": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "pri3_frames_rx": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "pri3_frames_tx": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "pri4_frames_rx": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "pri4_frames_tx": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "pri5_frames_rx": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "pri5_frames_tx": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "pri6_frames_rx": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "pri6_frames_tx": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "pri7_frames_rx": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "pri7_frames_tx": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "priority_pause_frames": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "rx_pri0_pause_1us_count": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "rx_pri1_pause_1us_count": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "rx_pri2_pause_1us_count": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "rx_pri3_pause_1us_count": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "rx_pri4_pause_1us_count": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "rx_pri5_pause_1us_count": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "rx_pri6_pause_1us_count": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "rx_pri7_pause_1us_count": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "rx_standard_pause_1us_count": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "rx_vlan_frames_good": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "tx_pri0_pause_1us_count": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "tx_pri1_pause_1us_count": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "tx_pri2_pause_1us_count": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "tx_pri3_pause_1us_count": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "tx_pri4_pause_1us_count": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "tx_pri5_pause_1us_count": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "tx_pri6_pause_1us_count": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "tx_pri7_pause_1us_count": { + "type": "integer", + "format": "uint64", + "minimum": 0 + } + }, + "required": [ + "crc_error_stomped", + "fragments_rx", + "frame_too_long", + "frames_dropped_buffer_full", + "frames_rx_all", + "frames_rx_indersized", + "frames_rx_length_1024_1518", + "frames_rx_length_128_255", + "frames_rx_length_1519_2047", + "frames_rx_length_2048_4095", + "frames_rx_length_256_511", + "frames_rx_length_4096_8191", + "frames_rx_length_512_1023", + "frames_rx_length_65_127", + "frames_rx_length_8192_9215", + "frames_rx_length_9216", + "frames_rx_length_eq_64", + "frames_rx_length_lt_64", + "frames_rx_oftype_pause", + "frames_rx_ok", + "frames_rx_oversized", + "frames_rx_with_any_error", + "frames_rx_with_broadcast_addresses", + "frames_rx_with_fcs_error", + "frames_rx_with_length_error", + "frames_rx_with_multicast_addresses", + "frames_rx_with_unicast_addresses", + "frames_truncated", + "frames_tx_all", + "frames_tx_broadcast", + "frames_tx_length_1024_1518", + "frames_tx_length_128_255", + "frames_tx_length_1519_2047", + "frames_tx_length_2048_4095", + "frames_tx_length_256_511", + "frames_tx_length_4096_8191", + "frames_tx_length_512_1023", + "frames_tx_length_65_127", + "frames_tx_length_8192_9215", + "frames_tx_length_9216", + "frames_tx_length_eq_64", + "frames_tx_length_lt_64", + "frames_tx_multicast", + "frames_tx_ok", + "frames_tx_pause", + "frames_tx_pri_pause", + "frames_tx_unicast", + "frames_tx_vlan", + "frames_tx_with_error", + "jabber_rx", + "octets_rx", + "octets_rx_in_good_frames", + "octets_tx_total", + "octets_tx_without_error", + "port", + "pri0_frames_rx", + "pri0_framex_tx", + "pri1_frames_rx", + "pri1_frames_tx", + "pri2_frames_rx", + "pri2_frames_tx", + "pri3_frames_rx", + "pri3_frames_tx", + "pri4_frames_rx", + "pri4_frames_tx", + "pri5_frames_rx", + "pri5_frames_tx", + "pri6_frames_rx", + "pri6_frames_tx", + "pri7_frames_rx", + "pri7_frames_tx", + "priority_pause_frames", + "rx_pri0_pause_1us_count", + "rx_pri1_pause_1us_count", + "rx_pri2_pause_1us_count", + "rx_pri3_pause_1us_count", + "rx_pri4_pause_1us_count", + "rx_pri5_pause_1us_count", + "rx_pri6_pause_1us_count", + "rx_pri7_pause_1us_count", + "rx_standard_pause_1us_count", + "rx_vlan_frames_good", + "tx_pri0_pause_1us_count", + "tx_pri1_pause_1us_count", + "tx_pri2_pause_1us_count", + "tx_pri3_pause_1us_count", + "tx_pri4_pause_1us_count", + "tx_pri5_pause_1us_count", + "tx_pri6_pause_1us_count", + "tx_pri7_pause_1us_count" + ] + }, + "ReceiverPower": { + "description": "Measured receiver optical power.\n\nThe SFF specifications allow for devices to monitor input optical power in several ways. It may either be an average power, over some unspecified time, or a peak-to-peak power. The latter is often abbreviated OMA, for Optical Modulation Amplitude. Again the time interval for peak-to-peak measurments are not specified.\n\nDetails -------\n\nThe SFF-8636 specification has an unfortunate limitation. There is no separate advertisement for whether a module supports measurements of receiver power. Instead, the _kind_ of measurement is advertised. The _same bit value_ could mean that either a peak-to-peak measurement is supported, or the measurements are not supported at all. Thus values of `PeakToPeak(0.0)` may mean that power measurements are not supported.", + "oneOf": [ + { + "description": "The measurement is represents average optical power, in mW.", + "type": "object", + "properties": { + "average": { + "type": "number", + "format": "float" + } + }, + "required": [ + "average" + ], + "additionalProperties": false + }, + { + "description": "The measurement represents a peak-to-peak, in mW.", + "type": "object", + "properties": { + "peak_to_peak": { + "type": "number", + "format": "float" + } + }, + "required": [ + "peak_to_peak" + ], + "additionalProperties": false + } + ] + }, + "RxSigInfo": { + "description": "Per-lane Rx signal information", + "type": "object", + "properties": { + "phy_ready": { + "description": "CDR lock achieved", + "type": "boolean" + }, + "ppm": { + "description": "Apparent PPM difference between local and remote", + "type": "integer", + "format": "int32" + }, + "sig_detect": { + "description": "Rx signal detected", + "type": "boolean" + } + }, + "required": [ + "phy_ready", + "ppm", + "sig_detect" + ] + }, + "SerdesEye": { + "description": "Eye height(s) for a single lane in mv", + "oneOf": [ + { + "type": "object", + "properties": { + "Nrz": { + "type": "number", + "format": "float" + } + }, + "required": [ + "Nrz" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "Pam4": { + "type": "object", + "properties": { + "eye1": { + "type": "number", + "format": "float" + }, + "eye2": { + "type": "number", + "format": "float" + }, + "eye3": { + "type": "number", + "format": "float" + } + }, + "required": [ + "eye1", + "eye2", + "eye3" + ] + } + }, + "required": [ + "Pam4" + ], + "additionalProperties": false + } + ] + }, + "Sff8636Datapath": { + "description": "The datapath of an SFF-8636 module.\n\nThis describes the state of a single lane in an SFF module. It includes information about input and output signals, faults, and controls.", + "type": "object", + "properties": { + "rx_cdr_enabled": { + "description": "Media-side transmit Clock and Data Recovery (CDR) enable status.\n\nCDR is the process by which the module enages an internal retimer function, through which the module attempts to recovery a clock signal directly from the input bitstream.", + "type": "boolean" + }, + "rx_lol": { + "description": "Media-side loss of lock flag.\n\nThis is true if the module is not able to extract a clock signal from the media-side signal (usually optical).", + "type": "boolean" + }, + "rx_los": { + "description": "Media-side loss of signal flag.\n\nThis is true if there is no detected input signal from the media-side (usually optical).", + "type": "boolean" + }, + "tx_adaptive_eq_fault": { + "description": "Flag indicating a fault in adaptive transmit equalization.", + "type": "boolean" + }, + "tx_cdr_enabled": { + "description": "Host-side transmit Clock and Data Recovery (CDR) enable status.\n\nCDR is the process by which the module enages an internal retimer function, through which the module attempts to recovery a clock signal directly from the input bitstream.", + "type": "boolean" + }, + "tx_enabled": { + "description": "Software control of output transmitter.", + "type": "boolean" + }, + "tx_fault": { + "description": "Flag indicating a fault in the transmitter and/or laser.", + "type": "boolean" + }, + "tx_lol": { + "description": "Host-side loss of lock flag.\n\nThis is true if the module is not able to extract a clock signal from the host-side electrical signal.", + "type": "boolean" + }, + "tx_los": { + "description": "Host-side loss of signal flag.\n\nThis is true if there is no detected electrical signal from the host-side serdes.", + "type": "boolean" + } + }, + "required": [ + "rx_cdr_enabled", + "rx_lol", + "rx_los", + "tx_adaptive_eq_fault", + "tx_cdr_enabled", + "tx_enabled", + "tx_fault", + "tx_lol", + "tx_los" + ] + }, + "SffComplianceCode": { + "description": "The compliance code for an SFF-8636 module.\n\nThese values record a specification compliance code, from SFF-8636 Table 6-17, or an extended specification compliance code, from SFF-8024 Table 4-4.", + "oneOf": [ + { + "type": "object", + "properties": { + "code": { + "description": "Extended electrical or optical interface codes", + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "extended" + ] + } + }, + "required": [ + "code", + "type" + ] + }, + { + "type": "object", + "properties": { + "code": { + "description": "The Ethernet specification implemented by a module.", + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "ethernet" + ] + } + }, + "required": [ + "code", + "type" + ] + } + ] + }, + "SidecarCableLeg": { + "description": "The leg of the Sidecar-internal cable.\n\nThis describes the leg on the cabling that connects the pins on the Tofino ASIC to the Sidecar chassis connector.", + "type": "string", + "enum": [ + "A", + "C" + ] + }, + "SidecarConnector": { + "description": "The Sidecar chassis connector mating the backplane and internal cabling.\n\nThis describes the \"group\" of backplane links that all terminate in one connector on the Sidecar itself. This is the connection point between a cable on the backplane itself and the Sidecar chassis.", + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "SwitchIdentifiers": { + "description": "Identifiers for a switch.", + "type": "object", + "properties": { + "asic_backend": { + "description": "Asic backend (compiler target) responsible for these identifiers.", + "type": "string" + }, + "fab": { + "nullable": true, + "description": "Fabrication plant identifier.", + "type": "string", + "minLength": 1, + "maxLength": 1 + }, + "lot": { + "nullable": true, + "description": "Lot identifier.", + "type": "string", + "minLength": 1, + "maxLength": 1 + }, + "model": { + "description": "The model number of the switch being managed.", + "type": "string" + }, + "revision": { + "description": "The revision number of the switch being managed.", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "serial": { + "description": "The serial number of the switch being managed.", + "type": "string" + }, + "sidecar_id": { + "description": "Unique identifier for the chip.", + "type": "string", + "format": "uuid" + }, + "slot": { + "description": "The slot number of the switch being managed.\n\nMGS uses u16 for this internally.", + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "wafer": { + "nullable": true, + "description": "Wafer number within the lot.", + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "wafer_loc": { + "nullable": true, + "description": "The wafer location as (x, y) coordinates on the wafer, represented as an array due to the lack of tuple support in OpenAPI.", + "type": "array", + "items": { + "type": "integer", + "format": "int16" + }, + "minItems": 2, + "maxItems": 2 + } + }, + "required": [ + "asic_backend", + "model", + "revision", + "serial", + "sidecar_id", + "slot" + ] + }, + "SwitchPort": { + "description": "A physical port on the Sidecar switch.", + "type": "object", + "properties": { + "management_mode": { + "description": "How the QSFP device is managed.\n\nSee `ManagementMode` for details.", + "allOf": [ + { + "$ref": "#/components/schemas/ManagementMode" + } + ] + }, + "port_id": { + "description": "The identifier for the switch port.", + "allOf": [ + { + "$ref": "#/components/schemas/PortId" + } + ] + }, + "transceiver": { + "nullable": true, + "description": "Details about a transceiver module inserted into the switch port.\n\nIf there is no transceiver at all, this will be `None`.", + "allOf": [ + { + "$ref": "#/components/schemas/Transceiver" + } + ] + } + }, + "required": [ + "port_id" + ] + }, + "Table": { + "description": "Represents the contents of a P4 table", + "type": "object", + "properties": { + "entries": { + "description": "There will be an entry for each populated slot in the table", + "type": "array", + "items": { + "$ref": "#/components/schemas/TableEntry" + } + }, + "name": { + "description": "A user-friendly name for the table", + "type": "string" + }, + "size": { + "description": "The maximum number of entries the table can hold", + "type": "integer", + "format": "uint", + "minimum": 0 + } + }, + "required": [ + "entries", + "name", + "size" + ] + }, + "TableCounterEntry": { + "type": "object", + "properties": { + "data": { + "description": "Counter values", + "allOf": [ + { + "$ref": "#/components/schemas/CounterData" + } + ] + }, + "keys": { + "description": "Names and values of each of the key fields.", + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "required": [ + "data", + "keys" + ] + }, + "TableEntry": { + "description": "Each entry in a P4 table is addressed by matching against a set of key values. If an entry is found, an action is taken with an action-specific set of arguments.\n\nNote: each entry will have the same key fields and each instance of any given action will have the same argument names, so a vector of TableEntry structs will contain a signficant amount of redundant data. We could consider tightening this up by including a schema of sorts in the \"struct Table\".", + "type": "object", + "properties": { + "action": { + "description": "Name of the action to take on a match", + "type": "string" + }, + "action_args": { + "description": "Names and values for the arguments to the action implementation.", + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "keys": { + "description": "Names and values of each of the key fields.", + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "required": [ + "action", + "action_args", + "keys" + ] + }, + "TfportData": { + "description": "The per-link data consumed by tfportd", + "type": "object", + "properties": { + "asic_id": { + "description": "The lower-level ASIC ID used to refer to this object in the switch driver software.", + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "ipv6_enabled": { + "description": "Is ipv6 enabled for this link", + "type": "boolean" + }, + "link_id": { + "description": "The link ID for this link.", + "allOf": [ + { + "$ref": "#/components/schemas/LinkId" + } + ] + }, + "link_local": { + "nullable": true, + "description": "The IPv6 link-local address of the link, if it exists.", + "type": "string", + "format": "ipv6" + }, + "mac": { + "description": "The MAC address for the link.", + "allOf": [ + { + "$ref": "#/components/schemas/MacAddr" + } + ] + }, + "port_id": { + "description": "The switch port ID for this link.", + "allOf": [ + { + "$ref": "#/components/schemas/PortId" + } + ] + } + }, + "required": [ + "asic_id", + "ipv6_enabled", + "link_id", + "mac", + "port_id" + ] + }, + "Transceiver": { + "description": "The state of a transceiver in a QSFP switch port.", + "oneOf": [ + { + "description": "The transceiver could not be managed due to a power fault.", + "type": "object", + "properties": { + "info": { + "$ref": "#/components/schemas/FaultReason" + }, + "state": { + "type": "string", + "enum": [ + "faulted" + ] + } + }, + "required": [ + "info", + "state" + ] + }, + { + "description": "A transceiver was present, but unsupported and automatically disabled.", + "type": "object", + "properties": { + "state": { + "type": "string", + "enum": [ + "unsupported" + ] + } + }, + "required": [ + "state" + ] + }, + { + "description": "A transceiver is present and supported.", + "type": "object", + "properties": { + "info": { + "$ref": "#/components/schemas/TransceiverInfo" + }, + "state": { + "type": "string", + "enum": [ + "supported" + ] + } + }, + "required": [ + "info", + "state" + ] + } + ] + }, + "TransceiverInfo": { + "description": "Information about a QSFP transceiver.\n\nThis stores the most relevant information about a transceiver module, such as vendor info or power. Each field may be missing, indicating it could not be determined.", + "type": "object", + "properties": { + "electrical_mode": { + "description": "The electrical mode of the transceiver.\n\nSee [`ElectricalMode`] for details.", + "allOf": [ + { + "$ref": "#/components/schemas/ElectricalMode" + } + ] + }, + "in_reset": { + "nullable": true, + "description": "True if the module is currently in reset.", + "type": "boolean" + }, + "interrupt_pending": { + "nullable": true, + "description": "True if there is a pending interrupt on the module.", + "type": "boolean" + }, + "power_mode": { + "nullable": true, + "description": "The power mode of the transceiver.", + "allOf": [ + { + "$ref": "#/components/schemas/PowerMode" + } + ] + }, + "vendor_info": { + "nullable": true, + "description": "Vendor and part identifying information.\n\nThe information will not be populated if it could not be read.", + "allOf": [ + { + "$ref": "#/components/schemas/VendorInfo" + } + ] + } + }, + "required": [ + "electrical_mode" + ] + }, + "TxEq": { + "description": "Parameters to adjust the transceiver equalization settings for a link on a switch. These parameters match those available on a tofino-based sidecar, and may need to be adapted when we move to a new switch ASIC.", + "type": "object", + "properties": { + "main": { + "nullable": true, + "type": "integer", + "format": "int32" + }, + "post1": { + "nullable": true, + "type": "integer", + "format": "int32" + }, + "post2": { + "nullable": true, + "type": "integer", + "format": "int32" + }, + "pre1": { + "nullable": true, + "type": "integer", + "format": "int32" + }, + "pre2": { + "nullable": true, + "type": "integer", + "format": "int32" + } + } + }, + "TxEqSwHw": { + "description": "This represents the software-determined equalization value initially assigned to the transceiver and the value actually being used by the hardware. The values may differ on transceivers that are capable of tuning their own settings at run time.", + "type": "object", + "properties": { + "hw": { + "$ref": "#/components/schemas/TxEq" + }, + "sw": { + "$ref": "#/components/schemas/TxEq" + } + }, + "required": [ + "hw", + "sw" + ] + }, + "UnderlayMulticastIpv6": { + "description": "A validated underlay multicast IPv6 address.\n\nUnderlay multicast addresses must be within the subnet allocated by Omicron for rack-internal multicast traffic (ff04::/64). This is a subset of the admin-local scope (ff04::/16) defined in RFC 4291.", + "type": "string", + "format": "ipv6" + }, + "Vendor": { + "description": "Vendor-specific information about a transceiver module.", + "type": "object", + "properties": { + "date": { + "nullable": true, + "type": "string" + }, + "name": { + "type": "string" + }, + "oui": { + "$ref": "#/components/schemas/Oui" + }, + "part": { + "type": "string" + }, + "revision": { + "type": "string" + }, + "serial": { + "type": "string" + } + }, + "required": [ + "name", + "oui", + "part", + "revision", + "serial" + ] + }, + "VendorInfo": { + "description": "The vendor information for a transceiver module.", + "type": "object", + "properties": { + "identifier": { + "description": "The SFF-8024 identifier.", + "type": "string" + }, + "vendor": { + "description": "The vendor information.", + "allOf": [ + { + "$ref": "#/components/schemas/Vendor" + } + ] + } + }, + "required": [ + "identifier", + "vendor" + ] + }, + "Vni": { + "description": "A Geneve Virtual Network Identifier.\n\nA Geneve VNI is a 24-bit value used to identify virtual networks encapsulated using the Generic Network Virtualization Encapsulation (Geneve) protocol (RFC 8926).", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "ipv4ResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "type": "string", + "format": "ipv4" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "ipv6ResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "type": "string", + "format": "ipv6" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + } + }, + "responses": { + "Error": { + "description": "Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } +} diff --git a/openapi/dpd/dpd-4.0.0-7b2800.json b/openapi/dpd/dpd-4.0.0-7b2800.json new file mode 100644 index 0000000..f427060 --- /dev/null +++ b/openapi/dpd/dpd-4.0.0-7b2800.json @@ -0,0 +1,9676 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "Oxide Switch Dataplane Controller", + "description": "API for managing the Oxide rack switch", + "contact": { + "url": "https://oxide.computer", + "email": "api@oxide.computer" + }, + "version": "4.0.0" + }, + "paths": { + "/all-settings": { + "delete": { + "summary": "Clear all settings.", + "description": "This removes all data entirely: ARP and NDP table entries, routes, links on all switch ports, NAT mappings, and multicast groups.\n\nNote: Unlike `reset_all_tagged`, this endpoint does clear multicast groups.", + "operationId": "reset_all", + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/all-settings/{tag}": { + "delete": { + "summary": "Clear all settings associated with a specific tag.", + "description": "This removes all ARP or NDP table entries, all routes, and all links on all switch ports.\n\nNote: Multicast groups are NOT cleared by this endpoint. Use the dedicated `/multicast/tags/{tag}` endpoint to clear multicast groups by tag.", + "operationId": "reset_all_tagged", + "parameters": [ + { + "in": "path", + "name": "tag", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/arp": { + "get": { + "summary": "Fetch the configured IPv4 ARP table entries.", + "operationId": "arp_list", + "parameters": [ + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ArpEntryResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + }, + "post": { + "summary": "Add an IPv4 ARP table entry, mapping an IPv4 address to a MAC address.", + "operationId": "arp_create", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ArpEntry" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "summary": "Remove all entries in the IPv4 ARP tables.", + "operationId": "arp_reset", + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/arp/{ip}": { + "get": { + "summary": "Get a single IPv4 ARP table entry, by its IPv4 address.", + "operationId": "arp_get", + "parameters": [ + { + "in": "path", + "name": "ip", + "required": true, + "schema": { + "type": "string", + "format": "ipv4" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ArpEntry" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "summary": "Remove a single IPv4 ARP entry, by its IPv4 address.", + "operationId": "arp_delete", + "parameters": [ + { + "in": "path", + "name": "ip", + "required": true, + "schema": { + "type": "string", + "format": "ipv4" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/backplane-map": { + "get": { + "summary": "Return the full backplane map.", + "description": "This returns the entire mapping of all cubbies in a rack, through the cabled backplane, and into the Sidecar main board. It also includes the Tofino \"connector\", which is included in some contexts such as reporting counters.", + "operationId": "backplane_map", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Map_of_BackplaneLink", + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/BackplaneLink" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/backplane-map/{port_id}": { + "get": { + "summary": "Return the backplane mapping for a single switch port.", + "operationId": "port_backplane_link", + "parameters": [ + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BackplaneLink" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/build-info": { + "get": { + "summary": "Return detailed build information about the `dpd` server itself.", + "operationId": "build_info", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BuildInfo" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/channels": { + "get": { + "summary": "Get the set of available channels for all ports.", + "description": "This returns the unused MAC channels for each physical switch port. This can be used to determine how many additional links can be created on a physical switch port.", + "operationId": "channels_list", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_FreeChannels", + "type": "array", + "items": { + "$ref": "#/components/schemas/FreeChannels" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/counters/fec": { + "get": { + "summary": "Get the FEC RS counters for all links.", + "operationId": "fec_rs_counters_list", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_LinkFecRSCounters", + "type": "array", + "items": { + "$ref": "#/components/schemas/LinkFecRSCounters" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/counters/fec/{port_id}/{link_id}": { + "get": { + "summary": "Get the FEC RS counters for the given link.", + "operationId": "fec_rs_counters_get", + "parameters": [ + { + "in": "path", + "name": "link_id", + "description": "The link in the switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LinkFecRSCounters" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/counters/fsm/{port_id}/{link_id}": { + "get": { + "summary": "Get the autonegotiation FSM counters for the given link.", + "operationId": "link_fsm_counters_get", + "parameters": [ + { + "in": "path", + "name": "link_id", + "description": "The link in the switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LinkFsmCounters" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/counters/linkup": { + "get": { + "summary": "Get the LinkUp counters for all links.", + "operationId": "link_up_counters_list", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_LinkUpCounter", + "type": "array", + "items": { + "$ref": "#/components/schemas/LinkUpCounter" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/counters/linkup/{port_id}/{link_id}": { + "get": { + "summary": "Get the LinkUp counters for the given link.", + "operationId": "link_up_counters_get", + "parameters": [ + { + "in": "path", + "name": "link_id", + "description": "The link in the switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LinkUpCounter" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/counters/p4": { + "get": { + "summary": "Get a list of all the available p4-defined counters.", + "operationId": "counter_list", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_String", + "type": "array", + "items": { + "type": "string" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/counters/p4/{counter}": { + "get": { + "summary": "Get the values for a given counter.", + "description": "The name of the counter should match one of those returned by the `counter_list()` call.", + "operationId": "counter_get", + "parameters": [ + { + "in": "query", + "name": "force_sync", + "description": "Force a sync of the counters from the ASIC to memory, even if the default refresh timeout hasn't been reached.", + "required": true, + "schema": { + "type": "boolean" + } + }, + { + "in": "path", + "name": "counter", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_TableCounterEntry", + "type": "array", + "items": { + "$ref": "#/components/schemas/TableCounterEntry" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/counters/p4/{counter}/reset": { + "post": { + "summary": "Reset a single p4-defined counter.", + "description": "The name of the counter should match one of those returned by the `counter_list()` call.", + "operationId": "counter_reset", + "parameters": [ + { + "in": "path", + "name": "counter", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/counters/pcs": { + "get": { + "summary": "Get the physical coding sublayer (PCS) counters for all links.", + "operationId": "pcs_counters_list", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_LinkPcsCounters", + "type": "array", + "items": { + "$ref": "#/components/schemas/LinkPcsCounters" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/counters/pcs/{port_id}/{link_id}": { + "get": { + "summary": "Get the Physical Coding Sublayer (PCS) counters for the given link.", + "operationId": "pcs_counters_get", + "parameters": [ + { + "in": "path", + "name": "link_id", + "description": "The link in the switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LinkPcsCounters" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/counters/rmon/{port_id}/{link_id}/all": { + "get": { + "summary": "Get the full set of traffic counters for the given link.", + "operationId": "rmon_counters_get_all", + "parameters": [ + { + "in": "path", + "name": "link_id", + "description": "The link in the switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LinkRMonCountersAll" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/counters/rmon/{port_id}/{link_id}/subset": { + "get": { + "summary": "Get the most relevant subset of traffic counters for the given link.", + "operationId": "rmon_counters_get", + "parameters": [ + { + "in": "path", + "name": "link_id", + "description": "The link in the switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LinkRMonCounters" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/dpd-uptime": { + "get": { + "summary": "Return the server uptime.", + "operationId": "dpd_uptime", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "int64", + "type": "integer", + "format": "int64" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/dpd-version": { + "get": { + "summary": "Return the version of the `dpd` server itself.", + "operationId": "dpd_version", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "String", + "type": "string" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/leds": { + "get": { + "summary": "Return the state of all attention LEDs on the Sidecar QSFP ports.", + "operationId": "leds_list", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Map_of_Led", + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/Led" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/links": { + "get": { + "summary": "List all links, on all switch ports.", + "operationId": "link_list_all", + "parameters": [ + { + "in": "query", + "name": "filter", + "description": "Filter links to those whose name contains the provided string.\n\nIf not provided, then all links are returned.", + "schema": { + "nullable": true, + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_Link", + "type": "array", + "items": { + "$ref": "#/components/schemas/Link" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/links/tfport_data": { + "get": { + "summary": "Collect the link data consumed by `tfportd`. This app-specific convenience", + "description": "routine is meant to reduce the time and traffic expended on this once-per-second operation, by consolidating multiple per-link requests into a single per-switch request.", + "operationId": "tfport_data", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_TfportData", + "type": "array", + "items": { + "$ref": "#/components/schemas/TfportData" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/loopback/ipv4": { + "get": { + "summary": "Get loopback IPv4 addresses.", + "operationId": "loopback_ipv4_list", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_Ipv4Entry", + "type": "array", + "items": { + "$ref": "#/components/schemas/Ipv4Entry" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "post": { + "summary": "Add a loopback IPv4.", + "operationId": "loopback_ipv4_create", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Ipv4Entry" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/loopback/ipv4/{ipv4}": { + "delete": { + "summary": "Remove one loopback IPv4 address.", + "operationId": "loopback_ipv4_delete", + "parameters": [ + { + "in": "path", + "name": "ipv4", + "required": true, + "schema": { + "type": "string", + "format": "ipv4" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/loopback/ipv6": { + "get": { + "summary": "Get loopback IPv6 addresses.", + "operationId": "loopback_ipv6_list", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_Ipv6Entry", + "type": "array", + "items": { + "$ref": "#/components/schemas/Ipv6Entry" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "post": { + "summary": "Add a loopback IPv6.", + "operationId": "loopback_ipv6_create", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Ipv6Entry" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/loopback/ipv6/{ipv6}": { + "delete": { + "summary": "Remove one loopback IPv6 address.", + "operationId": "loopback_ipv6_delete", + "parameters": [ + { + "in": "path", + "name": "ipv6", + "required": true, + "schema": { + "type": "string", + "format": "ipv6" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/multicast/external-groups": { + "post": { + "summary": "Create an external-only multicast group configuration.", + "description": "External-only groups are used for IPv4 and non-admin-local IPv6 multicast traffic that doesn't require replication infrastructure. These groups use simple forwarding tables and require a NAT target.", + "operationId": "multicast_group_create_external", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MulticastGroupCreateExternalEntry" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MulticastGroupExternalResponse" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/multicast/external-groups/{group_ip}": { + "put": { + "summary": "Update an external-only multicast group configuration for a given group IP address.", + "description": "External-only groups are used for IPv4 and non-admin-local IPv6 multicast traffic that doesn't require replication infrastructure.\n\nThe `tag` query parameter must match the group's existing tag.", + "operationId": "multicast_group_update_external", + "parameters": [ + { + "in": "path", + "name": "group_ip", + "required": true, + "schema": { + "type": "string", + "format": "ip" + } + }, + { + "in": "query", + "name": "tag", + "description": "Tag that must match the group's existing tag.", + "required": true, + "schema": { + "$ref": "#/components/schemas/MulticastTag" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MulticastGroupUpdateExternalEntry" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MulticastGroupExternalResponse" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/multicast/groups": { + "get": { + "summary": "List all multicast groups.", + "operationId": "multicast_groups_list", + "parameters": [ + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MulticastGroupResponseResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + }, + "delete": { + "summary": "Reset all multicast group configurations.", + "operationId": "multicast_reset", + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/multicast/groups/{group_ip}": { + "get": { + "summary": "Get the multicast group configuration for a given group IP address.", + "operationId": "multicast_group_get", + "parameters": [ + { + "in": "path", + "name": "group_ip", + "required": true, + "schema": { + "type": "string", + "format": "ip" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MulticastGroupResponse" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "summary": "Delete a multicast group configuration by IP address (API version 4+).", + "description": "All groups have tags (auto-generated if not provided at creation). The tag query parameter must match the group's existing tag.", + "operationId": "multicast_group_delete", + "parameters": [ + { + "in": "path", + "name": "group_ip", + "required": true, + "schema": { + "type": "string", + "format": "ip" + } + }, + { + "in": "query", + "name": "tag", + "description": "Tag that must match the group's existing tag.", + "required": true, + "schema": { + "$ref": "#/components/schemas/MulticastTag" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/multicast/tags/{tag}": { + "get": { + "summary": "List all multicast groups with a given tag.", + "description": "Returns paginated multicast groups matching the specified tag. Tags are assigned at group creation and are immutable. Use this endpoint to find all groups associated with a specific client or component.", + "operationId": "multicast_groups_list_by_tag", + "parameters": [ + { + "in": "path", + "name": "tag", + "required": true, + "schema": { + "$ref": "#/components/schemas/MulticastTag" + } + }, + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MulticastGroupResponseResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + }, + "delete": { + "summary": "Delete all multicast groups (and associated routes) with a given tag.", + "description": "This is idempotent: if no groups exist with the given tag, the operation returns success (the desired end state of \"no groups with this tag\" is achieved). Use this endpoint for bulk cleanup of all groups associated with a specific client or component.", + "operationId": "multicast_reset_by_tag", + "parameters": [ + { + "in": "path", + "name": "tag", + "required": true, + "schema": { + "$ref": "#/components/schemas/MulticastTag" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/multicast/underlay-groups": { + "post": { + "summary": "Create an underlay (internal) multicast group configuration.", + "description": "Underlay groups are used for admin-local IPv6 multicast traffic (ff04::/16, as defined in RFC 7346 and RFC 4291) that requires replication infrastructure. These groups support both external and underlay members with full replication capabilities.", + "operationId": "multicast_group_create_underlay", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MulticastGroupCreateUnderlayEntry" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MulticastGroupUnderlayResponse" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/multicast/underlay-groups/{group_ip}": { + "get": { + "summary": "Get an underlay (internal) multicast group configuration.", + "description": "Underlay groups handle admin-local IPv6 multicast traffic (ff04::/16) with replication infrastructure for external and underlay members.", + "operationId": "multicast_group_get_underlay", + "parameters": [ + { + "in": "path", + "name": "group_ip", + "required": true, + "schema": { + "$ref": "#/components/schemas/UnderlayMulticastIpv6" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MulticastGroupUnderlayResponse" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "summary": "Update an underlay (internal) multicast group configuration.", + "description": "Underlay groups are used for admin-local IPv6 multicast traffic (ff04::/16) that requires replication infrastructure with external and underlay members.\n\nThe `tag` query parameter must match the group's existing tag.", + "operationId": "multicast_group_update_underlay", + "parameters": [ + { + "in": "path", + "name": "group_ip", + "required": true, + "schema": { + "$ref": "#/components/schemas/UnderlayMulticastIpv6" + } + }, + { + "in": "query", + "name": "tag", + "description": "Tag that must match the group's existing tag.", + "required": true, + "schema": { + "$ref": "#/components/schemas/MulticastTag" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MulticastGroupUpdateUnderlayEntry" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MulticastGroupUnderlayResponse" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/multicast/untagged": { + "delete": { + "summary": "Delete all multicast groups (and associated routes) without a tag.", + "description": "DEPRECATED: All groups have default tags generated at creation time. This endpoint returns HTTP 410 Gone. Use `multicast_reset_by_tag` with the tag returned from group creation instead.", + "operationId": "multicast_reset_untagged", + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/nat/ipv4": { + "get": { + "summary": "Get all of the external addresses in use for IPv4 NAT mappings.", + "operationId": "nat_ipv4_addresses_list", + "parameters": [ + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ipv4ResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + }, + "delete": { + "summary": "Clear all IPv4 NAT mappings.", + "operationId": "nat_ipv4_reset", + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/nat/ipv4/{ipv4}": { + "get": { + "summary": "Get all of the external->internal NAT mappings for a given IPv4 address.", + "operationId": "nat_ipv4_list", + "parameters": [ + { + "in": "path", + "name": "ipv4", + "required": true, + "schema": { + "type": "string", + "format": "ipv4" + } + }, + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Ipv4NatResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + } + }, + "/nat/ipv4/{ipv4}/{low}": { + "get": { + "summary": "Get the external->internal NAT mapping for the given address/port", + "operationId": "nat_ipv4_get", + "parameters": [ + { + "in": "path", + "name": "ipv4", + "required": true, + "schema": { + "type": "string", + "format": "ipv4" + } + }, + { + "in": "path", + "name": "low", + "required": true, + "schema": { + "type": "integer", + "format": "uint16", + "minimum": 0 + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NatTarget" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "summary": "Clear the NAT mappings for an IPv4 address and starting L3 port.", + "operationId": "nat_ipv4_delete", + "parameters": [ + { + "in": "path", + "name": "ipv4", + "required": true, + "schema": { + "type": "string", + "format": "ipv4" + } + }, + { + "in": "path", + "name": "low", + "required": true, + "schema": { + "type": "integer", + "format": "uint16", + "minimum": 0 + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/nat/ipv4/{ipv4}/{low}/{high}": { + "put": { + "summary": "Add an external->internal NAT mapping for the given address/port range", + "description": "This maps an external IPv6 address and L3 port range to: - A gimlet's IPv6 address - A gimlet's MAC address - A Geneve VNI\n\nThese identify the gimlet on which a guest is running, and gives OPTE the information it needs to identify the guest VM that uses the external IPv6 and port range when making connections outside of an Oxide rack.", + "operationId": "nat_ipv4_create", + "parameters": [ + { + "in": "path", + "name": "high", + "required": true, + "schema": { + "type": "integer", + "format": "uint16", + "minimum": 0 + } + }, + { + "in": "path", + "name": "ipv4", + "required": true, + "schema": { + "type": "string", + "format": "ipv4" + } + }, + { + "in": "path", + "name": "low", + "required": true, + "schema": { + "type": "integer", + "format": "uint16", + "minimum": 0 + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NatTarget" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/nat/ipv6": { + "get": { + "summary": "Get all of the external addresses in use for NAT mappings.", + "operationId": "nat_ipv6_addresses_list", + "parameters": [ + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ipv6ResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + }, + "delete": { + "summary": "Clear all IPv6 NAT mappings.", + "operationId": "nat_ipv6_reset", + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/nat/ipv6/{ipv6}": { + "get": { + "summary": "Get all of the external->internal NAT mappings for a given address.", + "operationId": "nat_ipv6_list", + "parameters": [ + { + "in": "path", + "name": "ipv6", + "required": true, + "schema": { + "type": "string", + "format": "ipv6" + } + }, + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Ipv6NatResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + } + }, + "/nat/ipv6/{ipv6}/{low}": { + "get": { + "summary": "Get the external->internal NAT mapping for the given address and starting L3", + "description": "port.", + "operationId": "nat_ipv6_get", + "parameters": [ + { + "in": "path", + "name": "ipv6", + "required": true, + "schema": { + "type": "string", + "format": "ipv6" + } + }, + { + "in": "path", + "name": "low", + "required": true, + "schema": { + "type": "integer", + "format": "uint16", + "minimum": 0 + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NatTarget" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "summary": "Delete the NAT mapping for an IPv6 address and starting L3 port.", + "operationId": "nat_ipv6_delete", + "parameters": [ + { + "in": "path", + "name": "ipv6", + "required": true, + "schema": { + "type": "string", + "format": "ipv6" + } + }, + { + "in": "path", + "name": "low", + "required": true, + "schema": { + "type": "integer", + "format": "uint16", + "minimum": 0 + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/nat/ipv6/{ipv6}/{low}/{high}": { + "put": { + "summary": "Add an external->internal NAT mapping for the given address and L3 port", + "description": "range.\n\nThis maps an external IPv6 address and L3 port range to: - A gimlet's IPv6 address - A gimlet's MAC address - A Geneve VNI\n\nThese identify the gimlet on which a guest is running, and gives OPTE the information it needs to identify the guest VM that uses the external IPv6 and port range when making connections outside of an Oxide rack.", + "operationId": "nat_ipv6_create", + "parameters": [ + { + "in": "path", + "name": "high", + "required": true, + "schema": { + "type": "integer", + "format": "uint16", + "minimum": 0 + } + }, + { + "in": "path", + "name": "ipv6", + "required": true, + "schema": { + "type": "string", + "format": "ipv6" + } + }, + { + "in": "path", + "name": "low", + "required": true, + "schema": { + "type": "integer", + "format": "uint16", + "minimum": 0 + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NatTarget" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/ndp": { + "get": { + "summary": "Fetch the IPv6 NDP table entries.", + "description": "This returns a paginated list of all IPv6 neighbors directly connected to the switch.", + "operationId": "ndp_list", + "parameters": [ + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ArpEntryResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + }, + "post": { + "summary": "Add an IPv6 NDP entry, mapping an IPv6 address to a MAC address.", + "operationId": "ndp_create", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ArpEntry" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "summary": "Remove all entries in the the IPv6 NDP tables.", + "operationId": "ndp_reset", + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/ndp/{ip}": { + "get": { + "summary": "Get a single IPv6 NDP table entry, by its IPv6 address.", + "operationId": "ndp_get", + "parameters": [ + { + "in": "path", + "name": "ip", + "required": true, + "schema": { + "type": "string", + "format": "ipv6" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ArpEntry" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "summary": "Remove an IPv6 NDP entry, by its IPv6 address.", + "operationId": "ndp_delete", + "parameters": [ + { + "in": "path", + "name": "ip", + "required": true, + "schema": { + "type": "string", + "format": "ipv6" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/port/{port_id}/settings": { + "get": { + "summary": "Get port settings atomically.", + "operationId": "port_settings_get", + "parameters": [ + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + }, + { + "in": "query", + "name": "tag", + "description": "Restrict operations on this port to the provided tag.", + "schema": { + "nullable": true, + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PortSettings" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "post": { + "summary": "Apply port settings atomically.", + "description": "These settings will be applied holistically, and to the extent possible atomically to a given port. In the event of a failure a rollback is attempted. If the rollback fails there will be inconsistent state. This failure mode returns the error code \"rollback failure\". For more details see the docs on the [`PortSettings`] type.", + "operationId": "port_settings_apply", + "parameters": [ + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + }, + { + "in": "query", + "name": "tag", + "description": "Restrict operations on this port to the provided tag.", + "schema": { + "nullable": true, + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PortSettings" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PortSettings" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "summary": "Clear port settings atomically.", + "operationId": "port_settings_clear", + "parameters": [ + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + }, + { + "in": "query", + "name": "tag", + "description": "Restrict operations on this port to the provided tag.", + "schema": { + "nullable": true, + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PortSettings" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/ports": { + "get": { + "summary": "List all switch ports on the system.", + "operationId": "port_list", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_PortId", + "type": "array", + "items": { + "$ref": "#/components/schemas/PortId" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/ports/{port_id}": { + "get": { + "summary": "Return information about a single switch port.", + "operationId": "port_get", + "parameters": [ + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SwitchPort" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/ports/{port_id}/led": { + "get": { + "summary": "Return the current state of the attention LED on a front-facing QSFP port.", + "operationId": "led_get", + "parameters": [ + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Led" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "summary": "Override the current state of the attention LED on a front-facing QSFP port.", + "description": "The attention LED normally follows the state of the port itself. For example, if a transceiver is powered and operating normally, then the LED is solid on. An unexpected power fault would then be reflected by powering off the LED.\n\nThe client may override this behavior, explicitly setting the LED to a specified state. This can be undone, sending the LED back to its default policy, with the endpoint `/ports/{port_id}/led/auto`.", + "operationId": "led_set", + "parameters": [ + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LedState" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/ports/{port_id}/led/auto": { + "put": { + "summary": "Set the LED policy to automatic.", + "description": "The automatic LED policy ensures that the state of the LED follows the state of the switch port itself.", + "operationId": "led_set_auto", + "parameters": [ + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/ports/{port_id}/links": { + "get": { + "summary": "List the links within a single switch port.", + "operationId": "link_list", + "parameters": [ + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_Link", + "type": "array", + "items": { + "$ref": "#/components/schemas/Link" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "post": { + "summary": "Create a link on a switch port.", + "description": "Create an interface that can be used for sending Ethernet frames on the provided switch port. This will use the first available lanes in the physical port to create an interface of the desired speed, if possible.", + "operationId": "link_create", + "parameters": [ + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LinkCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LinkId" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/ports/{port_id}/links/{link_id}": { + "get": { + "summary": "Get an existing link by ID.", + "operationId": "link_get", + "parameters": [ + { + "in": "path", + "name": "link_id", + "description": "The link in the switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Link" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "summary": "Delete a link from a switch port.", + "operationId": "link_delete", + "parameters": [ + { + "in": "path", + "name": "link_id", + "description": "The link in the switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/ports/{port_id}/links/{link_id}/autoneg": { + "get": { + "summary": "Return whether the link is configured to use autonegotiation with its peer", + "description": "link.", + "operationId": "link_autoneg_get", + "parameters": [ + { + "in": "path", + "name": "link_id", + "description": "The link in the switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Boolean", + "type": "boolean" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "summary": "Set whether a port is configured to use autonegotation with its peer link.", + "operationId": "link_autoneg_set", + "parameters": [ + { + "in": "path", + "name": "link_id", + "description": "The link in the switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "title": "Boolean", + "type": "boolean" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/ports/{port_id}/links/{link_id}/ber": { + "get": { + "summary": "Return the estimated bit-error rate (BER) for a link.", + "operationId": "link_ber_get", + "parameters": [ + { + "in": "path", + "name": "link_id", + "description": "The link in the switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Ber" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/ports/{port_id}/links/{link_id}/enabled": { + "get": { + "summary": "Return whether the link is enabled.", + "operationId": "link_enabled_get", + "parameters": [ + { + "in": "path", + "name": "link_id", + "description": "The link in the switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Boolean", + "type": "boolean" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "summary": "Enable or disable a link.", + "operationId": "link_enabled_set", + "parameters": [ + { + "in": "path", + "name": "link_id", + "description": "The link in the switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "title": "Boolean", + "type": "boolean" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/ports/{port_id}/links/{link_id}/fault": { + "get": { + "summary": "Return any fault currently set on this link", + "operationId": "link_fault_get", + "parameters": [ + { + "in": "path", + "name": "link_id", + "description": "The link in the switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FaultCondition" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "post": { + "summary": "Inject a fault on this link", + "operationId": "link_fault_inject", + "parameters": [ + { + "in": "path", + "name": "link_id", + "description": "The link in the switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "title": "String", + "type": "string" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "summary": "Clear any fault currently set on this link", + "operationId": "link_fault_clear", + "parameters": [ + { + "in": "path", + "name": "link_id", + "description": "The link in the switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/ports/{port_id}/links/{link_id}/history": { + "get": { + "summary": "Get the event history for the given link.", + "operationId": "link_history_get", + "parameters": [ + { + "in": "path", + "name": "link_id", + "description": "The link in the switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LinkHistory" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/ports/{port_id}/links/{link_id}/ipv4": { + "get": { + "summary": "List the IPv4 addresses associated with a link.", + "operationId": "link_ipv4_list", + "parameters": [ + { + "in": "path", + "name": "link_id", + "description": "The link in the switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + }, + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Ipv4EntryResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + }, + "post": { + "summary": "Add an IPv4 address to a link.", + "operationId": "link_ipv4_create", + "parameters": [ + { + "in": "path", + "name": "link_id", + "description": "The link in the switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Ipv4Entry" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "summary": "Clear all IPv4 addresses from a link.", + "operationId": "link_ipv4_reset", + "parameters": [ + { + "in": "path", + "name": "link_id", + "description": "The link in the switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/ports/{port_id}/links/{link_id}/ipv4/{address}": { + "delete": { + "summary": "Remove an IPv4 address from a link.", + "operationId": "link_ipv4_delete", + "parameters": [ + { + "in": "path", + "name": "address", + "description": "The IPv4 address on which to operate.", + "required": true, + "schema": { + "type": "string", + "format": "ipv4" + } + }, + { + "in": "path", + "name": "link_id", + "description": "The link in the switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/ports/{port_id}/links/{link_id}/ipv6": { + "get": { + "summary": "List the IPv6 addresses associated with a link.", + "operationId": "link_ipv6_list", + "parameters": [ + { + "in": "path", + "name": "link_id", + "description": "The link in the switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + }, + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Ipv6EntryResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + }, + "post": { + "summary": "Add an IPv6 address to a link.", + "operationId": "link_ipv6_create", + "parameters": [ + { + "in": "path", + "name": "link_id", + "description": "The link in the switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Ipv6Entry" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "summary": "Clear all IPv6 addresses from a link.", + "operationId": "link_ipv6_reset", + "parameters": [ + { + "in": "path", + "name": "link_id", + "description": "The link in the switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/ports/{port_id}/links/{link_id}/ipv6/{address}": { + "delete": { + "summary": "Remove an IPv6 address from a link.", + "operationId": "link_ipv6_delete", + "parameters": [ + { + "in": "path", + "name": "address", + "description": "The IPv6 address on which to operate.", + "required": true, + "schema": { + "type": "string", + "format": "ipv6" + } + }, + { + "in": "path", + "name": "link_id", + "description": "The link in the switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/ports/{port_id}/links/{link_id}/ipv6_enabled": { + "get": { + "summary": "Return whether the link is configured to act as an IPv6 endpoint", + "operationId": "link_ipv6_enabled_get", + "parameters": [ + { + "in": "path", + "name": "link_id", + "description": "The link in the switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Boolean", + "type": "boolean" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "summary": "Set whether a port is configured to act as an IPv6 endpoint", + "operationId": "link_ipv6_enabled_set", + "parameters": [ + { + "in": "path", + "name": "link_id", + "description": "The link in the switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "title": "Boolean", + "type": "boolean" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/ports/{port_id}/links/{link_id}/kr": { + "get": { + "summary": "Return whether the link is in KR mode.", + "description": "\"KR\" refers to the Ethernet standard for the link, which are defined in various clauses of the IEEE 802.3 specification. \"K\" is used to denote a link over an electrical cabled backplane, and \"R\" refers to \"scrambled encoding\", a 64B/66B bit-encoding scheme.\n\nThus this should be true iff a link is on the cabled backplane.", + "operationId": "link_kr_get", + "parameters": [ + { + "in": "path", + "name": "link_id", + "description": "The link in the switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Boolean", + "type": "boolean" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "summary": "Enable or disable a link.", + "operationId": "link_kr_set", + "parameters": [ + { + "in": "path", + "name": "link_id", + "description": "The link in the switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "title": "Boolean", + "type": "boolean" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/ports/{port_id}/links/{link_id}/linkup": { + "get": { + "summary": "Return whether a link is up.", + "operationId": "link_linkup_get", + "parameters": [ + { + "in": "path", + "name": "link_id", + "description": "The link in the switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Boolean", + "type": "boolean" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/ports/{port_id}/links/{link_id}/mac": { + "get": { + "summary": "Get a link's MAC address.", + "operationId": "link_mac_get", + "parameters": [ + { + "in": "path", + "name": "link_id", + "description": "The link in the switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MacAddr" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "summary": "Set a link's MAC address.", + "operationId": "link_mac_set", + "parameters": [ + { + "in": "path", + "name": "link_id", + "description": "The link in the switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MacAddr" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/ports/{port_id}/links/{link_id}/nat_only": { + "get": { + "summary": "Return whether the link is configured to drop non-nat traffic", + "operationId": "link_nat_only_get", + "parameters": [ + { + "in": "path", + "name": "link_id", + "description": "The link in the switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Boolean", + "type": "boolean" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "summary": "Set whether a port is configured to use drop non-nat traffic", + "operationId": "link_nat_only_set", + "parameters": [ + { + "in": "path", + "name": "link_id", + "description": "The link in the switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "title": "Boolean", + "type": "boolean" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/ports/{port_id}/links/{link_id}/prbs": { + "get": { + "summary": "Return the link's PRBS speed and mode.", + "description": "During link training, a pseudorandom bit sequence (PRBS) is used to allow each side to synchronize their clocks and set various parameters on the underlying circuitry (such as filter gains).", + "operationId": "link_prbs_get", + "parameters": [ + { + "in": "path", + "name": "link_id", + "description": "The link in the switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PortPrbsMode" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "summary": "Set a link's PRBS speed and mode.", + "operationId": "link_prbs_set", + "parameters": [ + { + "in": "path", + "name": "link_id", + "description": "The link in the switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PortPrbsMode" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/ports/{port_id}/links/{link_id}/serdes/adapt": { + "get": { + "summary": "Get the per-lane adaptation counts for each lane on this link", + "operationId": "link_rx_adapt_count_get", + "parameters": [ + { + "in": "path", + "name": "link_id", + "description": "The link in the switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_DfeAdaptationState", + "type": "array", + "items": { + "$ref": "#/components/schemas/DfeAdaptationState" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/ports/{port_id}/links/{link_id}/serdes/anlt_status": { + "get": { + "summary": "Get the per-lane AN/LT status for each lane on this link", + "operationId": "link_an_lt_get", + "parameters": [ + { + "in": "path", + "name": "link_id", + "description": "The link in the switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AnLtStatus" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/ports/{port_id}/links/{link_id}/serdes/enc_speed": { + "get": { + "summary": "Get the per-lane speed and encoding for each lane on this link", + "operationId": "link_enc_speed_get", + "parameters": [ + { + "in": "path", + "name": "link_id", + "description": "The link in the switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_EncSpeed", + "type": "array", + "items": { + "$ref": "#/components/schemas/EncSpeed" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/ports/{port_id}/links/{link_id}/serdes/eye": { + "get": { + "summary": "Get the per-lane eye measurements for each lane on this link", + "operationId": "link_eye_get", + "parameters": [ + { + "in": "path", + "name": "link_id", + "description": "The link in the switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_SerdesEye", + "type": "array", + "items": { + "$ref": "#/components/schemas/SerdesEye" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/ports/{port_id}/links/{link_id}/serdes/lane_map": { + "get": { + "summary": "Get the logical->physical mappings for each lane in this port", + "operationId": "lane_map_get", + "parameters": [ + { + "in": "path", + "name": "link_id", + "description": "The link in the switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LaneMap" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/ports/{port_id}/links/{link_id}/serdes/rx_sig": { + "get": { + "summary": "Get the per-lane rx signal info for each lane on this link", + "operationId": "link_rx_sig_info_get", + "parameters": [ + { + "in": "path", + "name": "link_id", + "description": "The link in the switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_RxSigInfo", + "type": "array", + "items": { + "$ref": "#/components/schemas/RxSigInfo" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/ports/{port_id}/links/{link_id}/serdes/tx_eq": { + "get": { + "summary": "Get the per-lane tx eq settings for each lane on this link", + "operationId": "link_tx_eq_get", + "parameters": [ + { + "in": "path", + "name": "link_id", + "description": "The link in the switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_TxEqSwHw", + "type": "array", + "items": { + "$ref": "#/components/schemas/TxEqSwHw" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "summary": "Update the per-lane tx eq settings for all lanes on this link", + "operationId": "link_tx_eq_set", + "parameters": [ + { + "in": "path", + "name": "link_id", + "description": "The link in the switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TxEq" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/ports/{port_id}/management-mode": { + "get": { + "summary": "Return the current management mode of a QSFP switch port.", + "operationId": "management_mode_get", + "parameters": [ + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ManagementMode" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "summary": "Set the current management mode of a QSFP switch port.", + "operationId": "management_mode_set", + "parameters": [ + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ManagementMode" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/ports/{port_id}/transceiver": { + "get": { + "summary": "Return the information about a port's transceiver.", + "description": "This returns the status (presence, power state, etc) of the transceiver along with its identifying information. If the port is an optical switch port, but has no transceiver, then the identifying information is empty.\n\nIf the switch port is not a QSFP port, and thus could never have a transceiver, then \"Not Found\" is returned.", + "operationId": "transceiver_get", + "parameters": [ + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Transceiver" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/ports/{port_id}/transceiver/datapath": { + "get": { + "summary": "Fetch the state of the datapath for the provided transceiver.", + "operationId": "transceiver_datapath_get", + "parameters": [ + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Datapath" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/ports/{port_id}/transceiver/monitors": { + "get": { + "summary": "Fetch the monitored environmental information for the provided transceiver.", + "operationId": "transceiver_monitors_get", + "parameters": [ + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Monitors" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/ports/{port_id}/transceiver/power": { + "get": { + "summary": "Return the power state of a transceiver.", + "operationId": "transceiver_power_get", + "parameters": [ + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PowerState" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "summary": "Control the power state of a transceiver.", + "operationId": "transceiver_power_set", + "parameters": [ + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PowerState" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/ports/{port_id}/transceiver/reset": { + "post": { + "summary": "Effect a module-level reset of a QSFP transceiver.", + "description": "If the QSFP port has no transceiver or is not a QSFP port, then a client error is returned.", + "operationId": "transceiver_reset", + "parameters": [ + { + "in": "path", + "name": "port_id", + "description": "The switch port on which to operate.", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + } + ], + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/route/ipv4": { + "get": { + "summary": "Fetch the configured IPv4 routes, mapping IPv4 CIDR blocks to the switch port", + "description": "used for sending out that traffic, and optionally a gateway.", + "operationId": "route_ipv4_list", + "parameters": [ + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Ipv4RoutesResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + }, + "put": { + "summary": "Route an IPv4 subnet to a link and a nexthop gateway.", + "description": "This call can be used to create a new single-path route or to replace any existing routes with a new single-path route.", + "operationId": "route_ipv4_set", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Ipv4RouteUpdate" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "post": { + "summary": "Route an IPv4 subnet to a link and a nexthop gateway.", + "description": "This call can be used to create a new single-path route or to add new targets to a multipath route.", + "operationId": "route_ipv4_add", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Ipv4RouteUpdate" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/route/ipv4/{cidr}": { + "get": { + "summary": "Get the configured route for the given IPv4 subnet.", + "operationId": "route_ipv4_get", + "parameters": [ + { + "in": "path", + "name": "cidr", + "description": "The IPv4 subnet in CIDR notation whose route entry is returned.", + "required": true, + "schema": { + "$ref": "#/components/schemas/Ipv4Net" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_Ipv4Route", + "type": "array", + "items": { + "$ref": "#/components/schemas/Ipv4Route" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "summary": "Remove all targets for the given subnet", + "operationId": "route_ipv4_delete", + "parameters": [ + { + "in": "path", + "name": "cidr", + "description": "The IPv4 subnet in CIDR notation whose route entry is returned.", + "required": true, + "schema": { + "$ref": "#/components/schemas/Ipv4Net" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/route/ipv4/{cidr}/{port_id}/{link_id}/{tgt_ip}": { + "delete": { + "summary": "Remove a single target for the given IPv4 subnet", + "operationId": "route_ipv4_delete_target", + "parameters": [ + { + "in": "path", + "name": "cidr", + "description": "The subnet being routed", + "required": true, + "schema": { + "$ref": "#/components/schemas/Ipv4Net" + } + }, + { + "in": "path", + "name": "link_id", + "description": "The link to which packets should be sent", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port to which packets should be sent", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + }, + { + "in": "path", + "name": "tgt_ip", + "description": "The next hop in the IPv4 route", + "required": true, + "schema": { + "type": "string", + "format": "ipv4" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/route/ipv6": { + "get": { + "summary": "Fetch the configured IPv6 routes, mapping IPv6 CIDR blocks to the switch port", + "description": "used for sending out that traffic, and optionally a gateway.", + "operationId": "route_ipv6_list", + "parameters": [ + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Ipv6RoutesResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + }, + "put": { + "summary": "Route an IPv6 subnet to a link and a nexthop gateway.", + "description": "This call can be used to create a new single-path route or to replace any existing routes with a new single-path route.", + "operationId": "route_ipv6_set", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Ipv6RouteUpdate" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "post": { + "summary": "Route an IPv6 subnet to a link and a nexthop gateway.", + "description": "This call can be used to create a new single-path route or to add new targets to a multipath route.", + "operationId": "route_ipv6_add", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Ipv6RouteUpdate" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/route/ipv6/{cidr}": { + "get": { + "summary": "Get a single IPv6 route, by its IPv6 CIDR block.", + "operationId": "route_ipv6_get", + "parameters": [ + { + "in": "path", + "name": "cidr", + "description": "The IPv6 subnet in CIDR notation whose route entry is returned.", + "required": true, + "schema": { + "$ref": "#/components/schemas/Ipv6Net" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_Ipv6Route", + "type": "array", + "items": { + "$ref": "#/components/schemas/Ipv6Route" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "summary": "Remove an IPv6 route, by its IPv6 CIDR block.", + "operationId": "route_ipv6_delete", + "parameters": [ + { + "in": "path", + "name": "cidr", + "description": "The IPv6 subnet in CIDR notation whose route entry is returned.", + "required": true, + "schema": { + "$ref": "#/components/schemas/Ipv6Net" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/route/ipv6/{cidr}/{port_id}/{link_id}/{tgt_ip}": { + "delete": { + "summary": "Remove a single target for the given IPv6 subnet", + "operationId": "route_ipv6_delete_target", + "parameters": [ + { + "in": "path", + "name": "cidr", + "description": "The subnet being routed", + "required": true, + "schema": { + "$ref": "#/components/schemas/Ipv6Net" + } + }, + { + "in": "path", + "name": "link_id", + "description": "The link to which packets should be sent", + "required": true, + "schema": { + "$ref": "#/components/schemas/LinkId" + } + }, + { + "in": "path", + "name": "port_id", + "description": "The switch port to which packets should be sent", + "required": true, + "schema": { + "$ref": "#/components/schemas/PortId" + } + }, + { + "in": "path", + "name": "tgt_ip", + "description": "The next hop in the IPv4 route", + "required": true, + "schema": { + "type": "string", + "format": "ipv6" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/rpw/nat/gen": { + "get": { + "summary": "Get NAT generation number", + "operationId": "nat_generation", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "int64", + "type": "integer", + "format": "int64" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/rpw/nat/trigger": { + "post": { + "summary": "Trigger NAT Reconciliation", + "operationId": "nat_trigger_update", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Null", + "type": "string", + "enum": [ + null + ] + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/switch/identifiers": { + "get": { + "summary": "Get switch identifiers.", + "description": "This endpoint returns the switch identifiers, which can be used for consistent field definitions across oximeter time series schemas.", + "operationId": "switch_identifiers", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SwitchIdentifiers" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/table": { + "get": { + "summary": "Get the list of P4 tables", + "operationId": "table_list", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_String", + "type": "array", + "items": { + "type": "string" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/table/{table}/counters": { + "get": { + "summary": "Get any counter data from a single P4 match-action table.", + "description": "The name of the table should match one of those returned by the `table_list()` call.", + "operationId": "table_counters", + "parameters": [ + { + "in": "query", + "name": "force_sync", + "description": "Force a sync of the counters from the ASIC to memory, even if the default refresh timeout hasn't been reached.", + "required": true, + "schema": { + "type": "boolean" + } + }, + { + "in": "path", + "name": "table", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_TableCounterEntry", + "type": "array", + "items": { + "$ref": "#/components/schemas/TableCounterEntry" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/table/{table}/dump": { + "get": { + "summary": "Get the contents of a single P4 table.", + "description": "The name of the table should match one of those returned by the `table_list()` call.", + "operationId": "table_dump", + "parameters": [ + { + "in": "path", + "name": "table", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Table" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/transceivers": { + "get": { + "summary": "Return information about all QSFP transceivers.", + "operationId": "transceivers_list", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Map_of_Transceiver", + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/Transceiver" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + } + }, + "components": { + "schemas": { + "AnLtStatus": { + "description": "A collection of the data involved in the autonegiation/link-training process", + "type": "object", + "properties": { + "lanes": { + "description": "The per-lane status", + "type": "array", + "items": { + "$ref": "#/components/schemas/LaneStatus" + } + }, + "lp_pages": { + "description": "The base and extended pages received from the link partner", + "allOf": [ + { + "$ref": "#/components/schemas/LpPages" + } + ] + } + }, + "required": [ + "lanes", + "lp_pages" + ] + }, + "AnStatus": { + "description": "State of a single lane during autonegotiation", + "type": "object", + "properties": { + "an_ability": { + "description": "Are we capable of AN?", + "type": "boolean" + }, + "an_complete": { + "description": "Is autonegotiation complete?", + "type": "boolean" + }, + "ext_np_status": { + "description": "Is extended page format supported?", + "type": "boolean" + }, + "link_status": { + "description": "Allegedly: is the link up? In practice, this always seems to be false? TODO: investigate this", + "type": "boolean" + }, + "lp_an_ability": { + "description": "Can the link partner perform AN?", + "type": "boolean" + }, + "page_rcvd": { + "description": "has a base page been received?", + "type": "boolean" + }, + "parallel_detect_fault": { + "description": "A fault has been detected via the parallel detection function", + "type": "boolean" + }, + "remote_fault": { + "description": "Remote fault detected", + "type": "boolean" + } + }, + "required": [ + "an_ability", + "an_complete", + "ext_np_status", + "link_status", + "lp_an_ability", + "page_rcvd", + "parallel_detect_fault", + "remote_fault" + ] + }, + "ApplicationDescriptor": { + "description": "An Application Descriptor describes the supported datapath configurations.\n\nThis is a CMIS-specific concept. It's used for modules to advertise how it can be used by the host. Each application describes the host-side electrical interface; the media-side interface; the number of lanes required; etc.\n\nHost-side software can select one of these applications to instruct the module to use a specific set of lanes, with the interface on either side of the module.", + "type": "object", + "properties": { + "host_id": { + "description": "The electrical interface with the host side.", + "type": "string" + }, + "host_lane_assignment_options": { + "description": "The lanes on the host-side supporting this application.\n\nThis is a bit mask with a 1 identifying the lowest lane in a consecutive group of lanes to which the application can be assigned. This must be used with the `host_lane_count`. For example a value of `0b0000_0001` with a host lane count of 4 indicates that the first 4 lanes may be used in this application.\n\nAn application may support starting from multiple lanes.", + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "host_lane_count": { + "description": "The number of host-side lanes.", + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "media_id": { + "description": "The interface, optical or copper, with the media side.", + "allOf": [ + { + "$ref": "#/components/schemas/MediaInterfaceId" + } + ] + }, + "media_lane_assignment_options": { + "description": "The lanes on the media-side supporting this application.\n\nSee `host_lane_assignment_options` for details.", + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "media_lane_count": { + "description": "The number of media-side lanes.", + "type": "integer", + "format": "uint8", + "minimum": 0 + } + }, + "required": [ + "host_id", + "host_lane_assignment_options", + "host_lane_count", + "media_id", + "media_lane_assignment_options", + "media_lane_count" + ] + }, + "ArpEntry": { + "description": "Represents the mapping of an IP address to a MAC address.", + "type": "object", + "properties": { + "ip": { + "description": "The IP address for the entry.", + "type": "string", + "format": "ip" + }, + "mac": { + "description": "The MAC address to which `ip` maps.", + "allOf": [ + { + "$ref": "#/components/schemas/MacAddr" + } + ] + }, + "tag": { + "description": "A tag used to associate this entry with a client.", + "type": "string" + }, + "update": { + "description": "The time the entry was updated", + "type": "string" + } + }, + "required": [ + "ip", + "mac", + "tag", + "update" + ] + }, + "ArpEntryResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/ArpEntry" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "Aux1Monitor": { + "description": "The first auxlliary CMIS monitor.", + "oneOf": [ + { + "description": "The monitored property is custom, i.e., part-specific.", + "type": "object", + "properties": { + "custom": { + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "minItems": 2, + "maxItems": 2 + } + }, + "required": [ + "custom" + ], + "additionalProperties": false + }, + { + "description": "The current of the laser thermoelectric cooler.\n\nFor actively-cooled laser systems, this specifies the percentage of the maximum current the thermoelectric cooler supports. If the percentage is positive, the cooler is heating the laser. If negative, the cooler is cooling the laser.", + "type": "object", + "properties": { + "tec_current": { + "type": "number", + "format": "float" + } + }, + "required": [ + "tec_current" + ], + "additionalProperties": false + } + ] + }, + "Aux2Monitor": { + "description": "The second auxlliary CMIS monitor.", + "oneOf": [ + { + "description": "The temperature of the laser itself (degrees C).", + "type": "object", + "properties": { + "laser_temperature": { + "type": "number", + "format": "float" + } + }, + "required": [ + "laser_temperature" + ], + "additionalProperties": false + }, + { + "description": "The current of the laser thermoelectric cooler.\n\nFor actively-cooled laser systems, this specifies the percentage of the maximum current the thermoelectric cooler supports. If the percentage is positive, the cooler is heating the laser. If negative, the cooler is cooling the laser.", + "type": "object", + "properties": { + "tec_current": { + "type": "number", + "format": "float" + } + }, + "required": [ + "tec_current" + ], + "additionalProperties": false + } + ] + }, + "Aux3Monitor": { + "description": "The third auxlliary CMIS monitor.", + "oneOf": [ + { + "description": "The temperature of the laser itself (degrees C).", + "type": "object", + "properties": { + "laser_temperature": { + "type": "number", + "format": "float" + } + }, + "required": [ + "laser_temperature" + ], + "additionalProperties": false + }, + { + "description": "Measured voltage of an additional power supply (Volts).", + "type": "object", + "properties": { + "additional_supply_voltage": { + "type": "number", + "format": "float" + } + }, + "required": [ + "additional_supply_voltage" + ], + "additionalProperties": false + } + ] + }, + "AuxMonitors": { + "description": "Auxlliary monitored values for CMIS modules.", + "type": "object", + "properties": { + "aux1": { + "nullable": true, + "description": "Auxlliary monitor 1, either a custom value or TEC current.", + "allOf": [ + { + "$ref": "#/components/schemas/Aux1Monitor" + } + ] + }, + "aux2": { + "nullable": true, + "description": "Auxlliary monitor 1, either laser temperature or TEC current.", + "allOf": [ + { + "$ref": "#/components/schemas/Aux2Monitor" + } + ] + }, + "aux3": { + "nullable": true, + "description": "Auxlliary monitor 1, either laser temperature or additional supply voltage.", + "allOf": [ + { + "$ref": "#/components/schemas/Aux3Monitor" + } + ] + }, + "custom": { + "nullable": true, + "description": "A custom monitor. The value here is entirely vendor- and part-specific, so the part's data sheet must be consulted. The value may be either a signed or unsigned 16-bit integer, and so is included as raw bytes.", + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "minItems": 2, + "maxItems": 2 + } + } + }, + "BackplaneCableLeg": { + "description": "The leg of the backplane cable.\n\nThis describes the leg on the actual backplane cable that connects the Sidecar chassis connector to a cubby endpoint.", + "type": "string", + "enum": [ + "A", + "B", + "C", + "D" + ] + }, + "BackplaneLink": { + "description": "A single point-to-point connection on the cabled backplane.\n\nThis describes a single link from the Sidecar switch to a cubby, via the cabled backplane. It ultimately maps the Tofino ASIC pins to the cubby at which that link terminates. This path follows the Sidecar internal cable; the Sidecar chassis connector; and the backplane cable itself. This is used to map the Tofino driver's \"connector\" number (an index in its possible pinouts) through the backplane to our logical cubby numbering.", + "type": "object", + "properties": { + "backplane_leg": { + "$ref": "#/components/schemas/BackplaneCableLeg" + }, + "cubby": { + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "sidecar_connector": { + "$ref": "#/components/schemas/SidecarConnector" + }, + "sidecar_leg": { + "$ref": "#/components/schemas/SidecarCableLeg" + }, + "tofino_connector": { + "type": "integer", + "format": "uint8", + "minimum": 0 + } + }, + "required": [ + "backplane_leg", + "cubby", + "sidecar_connector", + "sidecar_leg", + "tofino_connector" + ] + }, + "Ber": { + "description": "Reports the bit-error rate (BER) for a link.", + "type": "object", + "properties": { + "ber": { + "description": "Estimated BER per-lane.", + "type": "array", + "items": { + "type": "number", + "format": "float" + } + }, + "symbol_errors": { + "description": "Counters of symbol errors per-lane.", + "type": "array", + "items": { + "type": "integer", + "format": "uint64", + "minimum": 0 + } + }, + "total_ber": { + "description": "Aggregate BER on the link.", + "type": "number", + "format": "float" + } + }, + "required": [ + "ber", + "symbol_errors", + "total_ber" + ] + }, + "BuildInfo": { + "description": "Detailed build information about `dpd`.", + "type": "object", + "properties": { + "cargo_triple": { + "type": "string" + }, + "debug": { + "type": "boolean" + }, + "git_branch": { + "type": "string" + }, + "git_commit_timestamp": { + "type": "string" + }, + "git_sha": { + "type": "string" + }, + "opt_level": { + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "rustc_channel": { + "type": "string" + }, + "rustc_commit_sha": { + "type": "string" + }, + "rustc_host_triple": { + "type": "string" + }, + "rustc_semver": { + "type": "string" + }, + "sde_commit_sha": { + "type": "string" + }, + "version": { + "type": "string" + } + }, + "required": [ + "cargo_triple", + "debug", + "git_branch", + "git_commit_timestamp", + "git_sha", + "opt_level", + "rustc_channel", + "rustc_commit_sha", + "rustc_host_triple", + "rustc_semver", + "sde_commit_sha", + "version" + ] + }, + "CmisDatapath": { + "description": "A datapath in a CMIS module.\n\nIn contrast to SFF-8636, CMIS makes first-class the concept of a datapath: a set of lanes and all the associated machinery involved in the transfer of data. This includes:\n\n- The \"application descriptor\" which is the host and media interfaces, and the lanes on each side used to transfer data; - The state of the datapath in a well-defined finite state machine (see CMIS 5.0 section 6.3.3); - The flags indicating how the datapath components are operating, such as receiving an input Rx signal or whether the transmitter is disabled.", + "type": "object", + "properties": { + "application": { + "description": "The application descriptor for this datapath.", + "allOf": [ + { + "$ref": "#/components/schemas/ApplicationDescriptor" + } + ] + }, + "lane_status": { + "description": "The status bits for each lane in the datapath.", + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/CmisLaneStatus" + } + } + }, + "required": [ + "application", + "lane_status" + ] + }, + "CmisLaneStatus": { + "description": "The status of a single CMIS lane.\n\nIf any particular control or status value is unsupported by a module, it is `None`.", + "type": "object", + "properties": { + "rx_auto_squelch_disable": { + "nullable": true, + "description": "Whether the host-side has disabled the Rx auto-squelch.\n\nThe module can implement automatic squelching of the Rx output, if the media-side input signal isn't valid. This indicates whether the host has disabled such a setting.", + "type": "boolean" + }, + "rx_lol": { + "nullable": true, + "description": "Media-side loss of lock flag.\n\nThis is true if the module is not able to extract a clock signal from the media-side signal (usually optical).", + "type": "boolean" + }, + "rx_los": { + "nullable": true, + "description": "Media-side loss of signal flag.\n\nThis is true if there is no detected input signal from the media-side (usually optical).", + "type": "boolean" + }, + "rx_output_enabled": { + "nullable": true, + "description": "Whether the Rx output is enabled.\n\nThe host may control this to disable the electrical output from the module to the host.", + "type": "boolean" + }, + "rx_output_polarity": { + "nullable": true, + "description": "The Rx output polarity.\n\nThis indicates a host-side control that flips the polarity of the host-side output signal.", + "allOf": [ + { + "$ref": "#/components/schemas/LanePolarity" + } + ] + }, + "rx_output_status": { + "description": "Status of host-side Rx output.\n\nThis indicates whether the Rx output is sending a valid signal to the host. Note that this is `Invalid` if the output is either muted (such as squelched) or explicitly disabled.", + "allOf": [ + { + "$ref": "#/components/schemas/OutputStatus" + } + ] + }, + "state": { + "description": "The datapath state of this lane.\n\nSee CMIS 5.0 section 8.9.1 for details.", + "type": "string" + }, + "tx_adaptive_eq_fail": { + "nullable": true, + "description": "A failure in the Tx adaptive input equalization.", + "type": "boolean" + }, + "tx_auto_squelch_disable": { + "nullable": true, + "description": "Whether the host-side has disabled the Tx auto-squelch.\n\nThe module can implement automatic squelching of the Tx output, if the host-side input signal isn't valid. This indicates whether the host has disabled such a setting.", + "type": "boolean" + }, + "tx_failure": { + "nullable": true, + "description": "General Tx failure flag.\n\nThis indicates that an internal and unspecified malfunction has occurred on the Tx lane.", + "type": "boolean" + }, + "tx_force_squelch": { + "nullable": true, + "description": "Whether the host-side has force-squelched the Tx output.\n\nThis indicates that the host can _force_ squelching the output if the signal is not valid.", + "type": "boolean" + }, + "tx_input_polarity": { + "nullable": true, + "description": "The Tx input polarity.\n\nThis indicates a host-side control that flips the polarity of the host-side input signal.", + "allOf": [ + { + "$ref": "#/components/schemas/LanePolarity" + } + ] + }, + "tx_lol": { + "nullable": true, + "description": "Host-side loss of lock flag.\n\nThis is true if the module is not able to extract a clock signal from the host-side electrical signal.", + "type": "boolean" + }, + "tx_los": { + "nullable": true, + "description": "Host-side loss of signal flag.\n\nThis is true if there is no detected electrical signal from the host-side serdes.", + "type": "boolean" + }, + "tx_output_enabled": { + "nullable": true, + "description": "Whether the Tx output is enabled.", + "type": "boolean" + }, + "tx_output_status": { + "description": "Status of media-side Tx output.\n\nThis indicates whether the Rx output is sending a valid signal to the media itself. Note that this is `Invalid` if the output is either muted (such as squelched) or explicitly disabled.", + "allOf": [ + { + "$ref": "#/components/schemas/OutputStatus" + } + ] + } + }, + "required": [ + "rx_output_status", + "state", + "tx_output_status" + ] + }, + "CounterData": { + "description": "For a counter, this contains the number of bytes, packets, or both that were counted. XXX: Ideally this would be a data-bearing enum, with variants for Pkts, Bytes, and PktsAndBytes. However OpenApi doesn't yet have the necessary support, so we're left with this clumsier representation.", + "type": "object", + "properties": { + "bytes": { + "nullable": true, + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "pkts": { + "nullable": true, + "type": "integer", + "format": "uint64", + "minimum": 0 + } + } + }, + "Datapath": { + "description": "Information about a transceiver's datapath.\n\nThis includes state related to the low-level eletrical and optical path through which bits flow. This includes flags like loss-of-signal / loss-of-lock; transmitter enablement state; and equalization parameters.", + "oneOf": [ + { + "description": "A number of datapaths in a CMIS module.\n\nCMIS modules may have a large number of supported configurations of their various lanes, each called an \"application\". These are described by the `ApplicationDescriptor` type, which mirrors CMIS 5.0 table 8-18. Each descriptor is identified by an \"Application Selector Code\", which is just its index in the section of the memory map describing them.\n\nEach lane can be used in zero or more applications, however, it may exist in at most one application at a time. These active applications, of which there may be more than one, are keyed by their codes in the contained mapping.", + "type": "object", + "properties": { + "cmis": { + "type": "object", + "properties": { + "connector": { + "description": "The type of free-side connector", + "type": "string" + }, + "datapaths": { + "description": "Mapping from \"application selector\" ID to its datapath information.\n\nThe datapath inclues the lanes used; host electrical interface; media interface; and a lot more about the state of the path.", + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/CmisDatapath" + } + }, + "supported_lanes": { + "description": "A bit mask with a 1 in bit `i` if the `i`th lane is supported.", + "type": "integer", + "format": "uint8", + "minimum": 0 + } + }, + "required": [ + "connector", + "datapaths", + "supported_lanes" + ] + } + }, + "required": [ + "cmis" + ], + "additionalProperties": false + }, + { + "description": "Datapath state about each lane in an SFF-8636 module.", + "type": "object", + "properties": { + "sff8636": { + "type": "object", + "properties": { + "connector": { + "description": "The type of a media-side connector.\n\nThese values come from SFF-8024 Rev 4.10 Table 4-3.", + "type": "string" + }, + "lanes": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Sff8636Datapath" + }, + "minItems": 4, + "maxItems": 4 + }, + "specification": { + "$ref": "#/components/schemas/SffComplianceCode" + } + }, + "required": [ + "connector", + "lanes", + "specification" + ] + } + }, + "required": [ + "sff8636" + ], + "additionalProperties": false + } + ] + }, + "DfeAdaptationState": { + "description": "Rx DFE adaptation information", + "type": "object", + "properties": { + "adapt_cnt": { + "description": "Total DFE attempts", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "adapt_done": { + "description": "DFE complete", + "type": "boolean" + }, + "link_lost_cnt": { + "description": "Times the signal was lost since the last read", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "readapt_cnt": { + "description": "DFE attempts since the last read", + "type": "integer", + "format": "uint32", + "minimum": 0 + } + }, + "required": [ + "adapt_cnt", + "adapt_done", + "link_lost_cnt", + "readapt_cnt" + ] + }, + "Direction": { + "description": "Direction a multicast group member is reached by.\n\n`External` group members must have any packet encapsulation removed before packet delivery.", + "type": "string", + "enum": [ + "Underlay", + "External" + ] + }, + "ElectricalMode": { + "description": "The electrical mode of a QSFP-capable port.\n\nQSFP ports can be broken out into one of several different electrical configurations or modes. This describes how the transmit/receive lanes are grouped into a single, logical link.\n\nNote that the electrical mode may only be changed if there are no links within the port, _and_ if the inserted QSFP module actually supports this mode.", + "oneOf": [ + { + "description": "All transmit/receive lanes are used for a single link.", + "type": "string", + "enum": [ + "Single" + ] + } + ] + }, + "EncSpeed": { + "description": "Signal speed and encoding for a single lane", + "type": "object", + "properties": { + "encoding": { + "$ref": "#/components/schemas/LaneEncoding" + }, + "gigabits": { + "type": "integer", + "format": "uint32", + "minimum": 0 + } + }, + "required": [ + "encoding", + "gigabits" + ] + }, + "Error": { + "description": "Error information from a response.", + "type": "object", + "properties": { + "error_code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "request_id": { + "type": "string" + } + }, + "required": [ + "message", + "request_id" + ] + }, + "ExternalForwarding": { + "description": "Represents the forwarding configuration for external multicast traffic.", + "type": "object", + "properties": { + "vlan_id": { + "nullable": true, + "type": "integer", + "format": "uint16", + "minimum": 0 + } + } + }, + "Fault": { + "description": "A Fault represents a specific kind of failure, and carries some additional context. Currently Faults are only used to describe Link failures, but there is no reason they couldn't be used elsewhere.", + "oneOf": [ + { + "type": "object", + "properties": { + "LinkFlap": { + "type": "string" + } + }, + "required": [ + "LinkFlap" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "Autoneg": { + "type": "string" + } + }, + "required": [ + "Autoneg" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "Injected": { + "type": "string" + } + }, + "required": [ + "Injected" + ], + "additionalProperties": false + } + ] + }, + "FaultCondition": { + "description": "Represents a potential fault condtion on a link", + "type": "object", + "properties": { + "fault": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/Fault" + } + ] + } + } + }, + "FaultReason": { + "description": "The cause of a fault on a transceiver.", + "oneOf": [ + { + "description": "An error occurred accessing the transceiver.", + "type": "string", + "enum": [ + "failed" + ] + }, + { + "description": "Power was enabled, but did not come up in the requisite time.", + "type": "string", + "enum": [ + "power_timeout" + ] + }, + { + "description": "Power was enabled and later lost.", + "type": "string", + "enum": [ + "power_lost" + ] + }, + { + "description": "The service processor disabled the transceiver.\n\nThe SP is responsible for monitoring the thermal data from the transceivers, and controlling the fans to compensate. If a module's thermal data cannot be read, the SP may completely disable the transceiver to ensure it cannot overheat the Sidecar.", + "type": "string", + "enum": [ + "disabled_by_sp" + ] + } + ] + }, + "FecRSCounters": { + "description": "Per-port RS FEC counters", + "type": "object", + "properties": { + "fec_align_status": { + "description": "All lanes synced and aligned", + "type": "boolean" + }, + "fec_corr_cnt": { + "description": "FEC corrected blocks", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "fec_ser_lane_0": { + "description": "FEC symbol errors on lane 0", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "fec_ser_lane_1": { + "description": "FEC symbol errors on lane 1", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "fec_ser_lane_2": { + "description": "FEC symbol errors on lane 2", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "fec_ser_lane_3": { + "description": "FEC symbol errors on lane 3", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "fec_ser_lane_4": { + "description": "FEC symbol errors on lane 4", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "fec_ser_lane_5": { + "description": "FEC symbol errors on lane 5", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "fec_ser_lane_6": { + "description": "FEC symbol errors on lane 6", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "fec_ser_lane_7": { + "description": "FEC symbol errors on lane 7", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "fec_uncorr_cnt": { + "description": "FEC uncorrected blocks", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "hi_ser": { + "description": "symbol errors exceeds threshhold", + "type": "boolean" + }, + "port": { + "description": "Port being tracked", + "type": "string" + } + }, + "required": [ + "fec_align_status", + "fec_corr_cnt", + "fec_ser_lane_0", + "fec_ser_lane_1", + "fec_ser_lane_2", + "fec_ser_lane_3", + "fec_ser_lane_4", + "fec_ser_lane_5", + "fec_ser_lane_6", + "fec_ser_lane_7", + "fec_uncorr_cnt", + "hi_ser", + "port" + ] + }, + "FreeChannels": { + "description": "Represents the free MAC channels on a single physical port.", + "type": "object", + "properties": { + "channels": { + "description": "The set of available channels (lanes) on this connector.", + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0 + } + }, + "connector": { + "description": "The Tofino connector for this port.\n\nThis describes the set of electrical connections representing this port object, which are defined by the pinout and board design of the Sidecar.", + "type": "string" + }, + "port_id": { + "description": "The switch port.", + "allOf": [ + { + "$ref": "#/components/schemas/PortId" + } + ] + } + }, + "required": [ + "channels", + "connector", + "port_id" + ] + }, + "InternalForwarding": { + "description": "Represents the NAT target for multicast traffic for internal/underlay forwarding.", + "type": "object", + "properties": { + "nat_target": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/NatTarget" + } + ] + } + } + }, + "IpSrc": { + "description": "Source filter match key for multicast traffic.\n\nFor SSM groups, use `Exact` with specific source addresses. For ASM groups with any-source filtering, use `Any`.", + "oneOf": [ + { + "description": "Exact match for the source IP address.", + "type": "object", + "properties": { + "Exact": { + "type": "string", + "format": "ip" + } + }, + "required": [ + "Exact" + ], + "additionalProperties": false + }, + { + "description": "Match any source address (0.0.0.0/0 or ::/0 depending on group IP version).", + "type": "string", + "enum": [ + "Any" + ] + } + ] + }, + "Ipv4Entry": { + "description": "An IPv4 address assigned to a link.", + "type": "object", + "properties": { + "addr": { + "description": "The IP address.", + "type": "string", + "format": "ipv4" + }, + "tag": { + "description": "Client-side tag for this object.", + "type": "string" + } + }, + "required": [ + "addr", + "tag" + ] + }, + "Ipv4EntryResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/Ipv4Entry" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "Ipv4Nat": { + "description": "represents an IPv4 NAT reservation", + "type": "object", + "properties": { + "external": { + "type": "string", + "format": "ipv4" + }, + "high": { + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "low": { + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "target": { + "$ref": "#/components/schemas/NatTarget" + } + }, + "required": [ + "external", + "high", + "low", + "target" + ] + }, + "Ipv4NatResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/Ipv4Nat" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "Ipv4Net": { + "example": "192.168.1.0/24", + "title": "An IPv4 subnet", + "description": "An IPv4 subnet, including prefix and prefix length", + "x-rust-type": { + "crate": "oxnet", + "path": "oxnet::Ipv4Net", + "version": "0.1.0" + }, + "type": "string", + "pattern": "^(([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])/([0-9]|1[0-9]|2[0-9]|3[0-2])$" + }, + "Ipv4Route": { + "description": "A route for an IPv4 subnet.", + "type": "object", + "properties": { + "link_id": { + "$ref": "#/components/schemas/LinkId" + }, + "port_id": { + "$ref": "#/components/schemas/PortId" + }, + "tag": { + "type": "string" + }, + "tgt_ip": { + "type": "string", + "format": "ipv4" + }, + "vlan_id": { + "nullable": true, + "type": "integer", + "format": "uint16", + "minimum": 0 + } + }, + "required": [ + "link_id", + "port_id", + "tag", + "tgt_ip" + ] + }, + "Ipv4RouteUpdate": { + "description": "Represents a new or replacement mapping of a subnet to a single IPv4 RouteTarget nexthop target.", + "type": "object", + "properties": { + "cidr": { + "description": "Traffic destined for any address within the CIDR block is routed using this information.", + "allOf": [ + { + "$ref": "#/components/schemas/Ipv4Net" + } + ] + }, + "replace": { + "description": "Should this route replace any existing route? If a route exists and this parameter is false, then the call will fail.", + "type": "boolean" + }, + "target": { + "description": "A single Route associated with this CIDR", + "allOf": [ + { + "$ref": "#/components/schemas/Ipv4Route" + } + ] + } + }, + "required": [ + "cidr", + "replace", + "target" + ] + }, + "Ipv4Routes": { + "description": "Represents all mappings of an IPv4 subnet to a its nexthop target(s).", + "type": "object", + "properties": { + "cidr": { + "description": "Traffic destined for any address within the CIDR block is routed using this information.", + "allOf": [ + { + "$ref": "#/components/schemas/Ipv4Net" + } + ] + }, + "targets": { + "description": "All RouteTargets associated with this CIDR", + "type": "array", + "items": { + "$ref": "#/components/schemas/Ipv4Route" + } + } + }, + "required": [ + "cidr", + "targets" + ] + }, + "Ipv4RoutesResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/Ipv4Routes" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "Ipv6Entry": { + "description": "An IPv6 address assigned to a link.", + "type": "object", + "properties": { + "addr": { + "description": "The IP address.", + "type": "string", + "format": "ipv6" + }, + "tag": { + "description": "Client-side tag for this object.", + "type": "string" + } + }, + "required": [ + "addr", + "tag" + ] + }, + "Ipv6EntryResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/Ipv6Entry" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "Ipv6Nat": { + "description": "represents an IPv6 NAT reservation", + "type": "object", + "properties": { + "external": { + "type": "string", + "format": "ipv6" + }, + "high": { + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "low": { + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "target": { + "$ref": "#/components/schemas/NatTarget" + } + }, + "required": [ + "external", + "high", + "low", + "target" + ] + }, + "Ipv6NatResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/Ipv6Nat" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "Ipv6Net": { + "example": "fd12:3456::/64", + "title": "An IPv6 subnet", + "description": "An IPv6 subnet, including prefix and subnet mask", + "x-rust-type": { + "crate": "oxnet", + "path": "oxnet::Ipv6Net", + "version": "0.1.0" + }, + "type": "string", + "pattern": "^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))\\/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8])$" + }, + "Ipv6Route": { + "description": "A route for an IPv6 subnet.", + "type": "object", + "properties": { + "link_id": { + "$ref": "#/components/schemas/LinkId" + }, + "port_id": { + "$ref": "#/components/schemas/PortId" + }, + "tag": { + "type": "string" + }, + "tgt_ip": { + "type": "string", + "format": "ipv6" + }, + "vlan_id": { + "nullable": true, + "type": "integer", + "format": "uint16", + "minimum": 0 + } + }, + "required": [ + "link_id", + "port_id", + "tag", + "tgt_ip" + ] + }, + "Ipv6RouteUpdate": { + "description": "Represents a new or replacement mapping of a subnet to a single IPv6 RouteTarget nexthop target.", + "type": "object", + "properties": { + "cidr": { + "description": "Traffic destined for any address within the CIDR block is routed using this information.", + "allOf": [ + { + "$ref": "#/components/schemas/Ipv6Net" + } + ] + }, + "replace": { + "description": "Should this route replace any existing route? If a route exists and this parameter is false, then the call will fail.", + "type": "boolean" + }, + "target": { + "description": "A single RouteTarget associated with this CIDR", + "allOf": [ + { + "$ref": "#/components/schemas/Ipv6Route" + } + ] + } + }, + "required": [ + "cidr", + "replace", + "target" + ] + }, + "Ipv6Routes": { + "description": "Represents all mappings of an IPv6 subnet to a its nexthop target(s).", + "type": "object", + "properties": { + "cidr": { + "description": "Traffic destined for any address within the CIDR block is routed using this information.", + "allOf": [ + { + "$ref": "#/components/schemas/Ipv6Net" + } + ] + }, + "targets": { + "description": "All RouteTargets associated with this CIDR", + "type": "array", + "items": { + "$ref": "#/components/schemas/Ipv6Route" + } + } + }, + "required": [ + "cidr", + "targets" + ] + }, + "Ipv6RoutesResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/Ipv6Routes" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "LaneEncoding": { + "description": "Signal encoding", + "oneOf": [ + { + "description": "Pulse Amplitude Modulation 4-level", + "type": "string", + "enum": [ + "Pam4" + ] + }, + { + "description": "Non-Return-to-Zero encoding", + "type": "string", + "enum": [ + "Nrz" + ] + }, + { + "description": "No encoding selected", + "type": "string", + "enum": [ + "None" + ] + } + ] + }, + "LaneMap": { + "description": "Mapping of the logical lanes in a link to their physical instantiation in the MAC/serdes interface.", + "type": "object", + "properties": { + "logical_lane": { + "description": "logical lane within the mac block for each lane", + "type": "array", + "items": { + "type": "integer", + "format": "uint32", + "minimum": 0 + } + }, + "mac_block": { + "description": "MAC block in the tofino ASIC", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "rx_phys": { + "description": "Rx logical->physical mapping", + "type": "array", + "items": { + "type": "integer", + "format": "uint32", + "minimum": 0 + } + }, + "rx_polarity": { + "description": "Rx polarity", + "type": "array", + "items": { + "$ref": "#/components/schemas/Polarity" + } + }, + "tx_phys": { + "description": "Tx logical->physical mapping", + "type": "array", + "items": { + "type": "integer", + "format": "uint32", + "minimum": 0 + } + }, + "tx_polarity": { + "description": "Tx polarity", + "type": "array", + "items": { + "$ref": "#/components/schemas/Polarity" + } + } + }, + "required": [ + "logical_lane", + "mac_block", + "rx_phys", + "rx_polarity", + "tx_phys", + "tx_polarity" + ] + }, + "LanePolarity": { + "description": "The polarity of a transceiver lane.", + "type": "string", + "enum": [ + "normal", + "flipped" + ] + }, + "LaneStatus": { + "description": "The combined status of a lane, with respect to the autonegotiation / link-training process.", + "type": "object", + "properties": { + "lane_an_status": { + "description": "Detailed autonegotiation status", + "allOf": [ + { + "$ref": "#/components/schemas/AnStatus" + } + ] + }, + "lane_done": { + "description": "Has a lane successfully completed autoneg and link training?", + "type": "boolean" + }, + "lane_lt_status": { + "description": "Detailed link-training status", + "allOf": [ + { + "$ref": "#/components/schemas/LtStatus" + } + ] + } + }, + "required": [ + "lane_an_status", + "lane_done", + "lane_lt_status" + ] + }, + "Led": { + "description": "Information about a QSFP port's LED.", + "type": "object", + "properties": { + "policy": { + "description": "The policy by which the LED is controlled.", + "allOf": [ + { + "$ref": "#/components/schemas/LedPolicy" + } + ] + }, + "state": { + "description": "The state of the LED.", + "allOf": [ + { + "$ref": "#/components/schemas/LedState" + } + ] + } + }, + "required": [ + "policy", + "state" + ] + }, + "LedPolicy": { + "description": "The policy by which a port's LED is controlled.", + "oneOf": [ + { + "description": "The default policy is for the LED to reflect the port's state itself.\n\nIf the port is operating normally, the LED will be solid on. Without a transceiver, the LED will be solid off. A blinking LED is used to indicate an unsupported module or other failure on that port.", + "type": "string", + "enum": [ + "automatic" + ] + }, + { + "description": "The LED is explicitly overridden by client requests.", + "type": "string", + "enum": [ + "override" + ] + } + ] + }, + "LedState": { + "description": "The state of a module's attention LED, on the Sidecar front IO panel.", + "oneOf": [ + { + "description": "The LED is off.\n\nThis indicates that the port is disabled or not working at all.", + "type": "string", + "enum": [ + "off" + ] + }, + { + "description": "The LED is solid on.\n\nThis indicates that the port is working as expected and enabled.", + "type": "string", + "enum": [ + "on" + ] + }, + { + "description": "The LED is blinking.\n\nThis is used to draw attention to the port, such as to indicate a fault or to locate a port for servicing.", + "type": "string", + "enum": [ + "blink" + ] + } + ] + }, + "Link": { + "description": "An Ethernet-capable link within a switch port.", + "type": "object", + "properties": { + "address": { + "description": "The MAC address for the link.", + "allOf": [ + { + "$ref": "#/components/schemas/MacAddr" + } + ] + }, + "asic_id": { + "description": "The lower-level ASIC ID used to refer to this object in the switch driver software.", + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "autoneg": { + "description": "True if this link is configured to autonegotiate with its peer.", + "type": "boolean" + }, + "enabled": { + "description": "True if this link is enabled.", + "type": "boolean" + }, + "fec": { + "nullable": true, + "description": "The error-correction scheme for this link.", + "allOf": [ + { + "$ref": "#/components/schemas/PortFec" + } + ] + }, + "fsm_state": { + "description": "Current state in the autonegotiation/link-training finite state machine", + "type": "string" + }, + "ipv6_enabled": { + "description": "The link is configured for IPv6 use", + "type": "boolean" + }, + "kr": { + "description": "True if this link is in KR mode, i.e., is on a cabled backplane.", + "type": "boolean" + }, + "link_id": { + "description": "The `LinkId` within the switch port for this link.", + "allOf": [ + { + "$ref": "#/components/schemas/LinkId" + } + ] + }, + "link_state": { + "description": "The state of the Ethernet link.", + "allOf": [ + { + "$ref": "#/components/schemas/LinkState" + } + ] + }, + "media": { + "description": "The physical media underlying this link.", + "allOf": [ + { + "$ref": "#/components/schemas/PortMedia" + } + ] + }, + "port_id": { + "description": "The switch port on which this link exists.", + "allOf": [ + { + "$ref": "#/components/schemas/PortId" + } + ] + }, + "prbs": { + "description": "The PRBS mode.", + "allOf": [ + { + "$ref": "#/components/schemas/PortPrbsMode" + } + ] + }, + "presence": { + "description": "True if the transceiver module has detected a media presence.", + "type": "boolean" + }, + "speed": { + "description": "The speed of the link.", + "allOf": [ + { + "$ref": "#/components/schemas/PortSpeed" + } + ] + }, + "tofino_connector": { + "description": "The Tofino connector number associated with this link.", + "type": "integer", + "format": "uint16", + "minimum": 0 + } + }, + "required": [ + "address", + "asic_id", + "autoneg", + "enabled", + "fsm_state", + "ipv6_enabled", + "kr", + "link_id", + "link_state", + "media", + "port_id", + "prbs", + "presence", + "speed", + "tofino_connector" + ] + }, + "LinkCreate": { + "description": "Parameters used to create a link on a switch port.", + "type": "object", + "properties": { + "autoneg": { + "description": "Whether the link is configured to autonegotiate with its peer during link training.\n\nThis is generally only true for backplane links, and defaults to `false`.", + "default": false, + "type": "boolean" + }, + "fec": { + "nullable": true, + "description": "The requested forward-error correction method. If this is None, the standard FEC for the underlying media will be applied if it can be determined.", + "allOf": [ + { + "$ref": "#/components/schemas/PortFec" + } + ] + }, + "kr": { + "description": "Whether the link is configured in KR mode, an electrical specification generally only true for backplane link.\n\nThis defaults to `false`.", + "default": false, + "type": "boolean" + }, + "lane": { + "nullable": true, + "description": "The first lane of the port to use for the new link", + "allOf": [ + { + "$ref": "#/components/schemas/LinkId" + } + ] + }, + "speed": { + "description": "The requested speed of the link.", + "allOf": [ + { + "$ref": "#/components/schemas/PortSpeed" + } + ] + }, + "tx_eq": { + "nullable": true, + "description": "Transceiver equalization adjustment parameters. This defaults to `None`.", + "default": null, + "allOf": [ + { + "$ref": "#/components/schemas/TxEq" + } + ] + } + }, + "required": [ + "speed" + ] + }, + "LinkEvent": { + "type": "object", + "properties": { + "channel": { + "nullable": true, + "description": "Channel ID for sub-link-level events", + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "class": { + "description": "Event class", + "type": "string" + }, + "details": { + "nullable": true, + "description": "Optionally, additional details about the event", + "type": "string" + }, + "subclass": { + "description": "Event subclass", + "type": "string" + }, + "timestamp": { + "description": "Time the event occurred. The time is represented in milliseconds, starting at an undefined time in the past. This means that timestamps can be used to measure the time between events, but not to determine the wall-clock time at which the event occurred.", + "type": "integer", + "format": "int64" + } + }, + "required": [ + "class", + "subclass", + "timestamp" + ] + }, + "LinkFecRSCounters": { + "description": "The FEC counters for a specific link, including its link ID.", + "type": "object", + "properties": { + "counters": { + "description": "The FEC counter data.", + "allOf": [ + { + "$ref": "#/components/schemas/FecRSCounters" + } + ] + }, + "link_id": { + "description": "The link ID.", + "allOf": [ + { + "$ref": "#/components/schemas/LinkId" + } + ] + }, + "port_id": { + "description": "The switch port ID.", + "allOf": [ + { + "$ref": "#/components/schemas/PortId" + } + ] + } + }, + "required": [ + "counters", + "link_id", + "port_id" + ] + }, + "LinkFsmCounter": { + "description": "Reports how many times a given autoneg/link-training state has been entered", + "type": "object", + "properties": { + "current": { + "description": "Times entered since the link was last enabled", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "state_name": { + "description": "FSM state being counted", + "type": "string" + }, + "total": { + "description": "Times entered since the link was created", + "type": "integer", + "format": "uint32", + "minimum": 0 + } + }, + "required": [ + "current", + "state_name", + "total" + ] + }, + "LinkFsmCounters": { + "description": "Reports all the autoneg/link-training states a link has transitioned into.", + "type": "object", + "properties": { + "counters": { + "description": "All the states this link has entered, along with counts of how many times each state was entered.", + "type": "array", + "items": { + "$ref": "#/components/schemas/LinkFsmCounter" + } + }, + "link_path": { + "description": "Link being reported", + "type": "string" + } + }, + "required": [ + "counters", + "link_path" + ] + }, + "LinkHistory": { + "type": "object", + "properties": { + "events": { + "description": "The set of historical events recorded", + "type": "array", + "items": { + "$ref": "#/components/schemas/LinkEvent" + } + }, + "timestamp": { + "description": "The timestamp in milliseconds at which this history was collected.", + "type": "integer", + "format": "int64" + } + }, + "required": [ + "events", + "timestamp" + ] + }, + "LinkId": { + "description": "An identifier for a link within a switch port.\n\nA switch port identified by a [`PortId`] may have multiple links within it, each identified by a `LinkId`. These are unique within a switch port only.\n\n[`PortId`]: common::ports::PortId", + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "LinkPcsCounters": { + "description": "The Physical Coding Sublayer (PCS) counters for a specific link.", + "type": "object", + "properties": { + "counters": { + "description": "The PCS counter data.", + "allOf": [ + { + "$ref": "#/components/schemas/PcsCounters" + } + ] + }, + "link_id": { + "description": "The link ID.", + "allOf": [ + { + "$ref": "#/components/schemas/LinkId" + } + ] + }, + "port_id": { + "description": "The switch port ID.", + "allOf": [ + { + "$ref": "#/components/schemas/PortId" + } + ] + } + }, + "required": [ + "counters", + "link_id", + "port_id" + ] + }, + "LinkRMonCounters": { + "description": "The RMON counters (traffic counters) for a specific link.", + "type": "object", + "properties": { + "counters": { + "description": "The RMON counter data.", + "allOf": [ + { + "$ref": "#/components/schemas/RMonCounters" + } + ] + }, + "link_id": { + "description": "The link ID.", + "allOf": [ + { + "$ref": "#/components/schemas/LinkId" + } + ] + }, + "port_id": { + "description": "The switch port ID.", + "allOf": [ + { + "$ref": "#/components/schemas/PortId" + } + ] + } + }, + "required": [ + "counters", + "link_id", + "port_id" + ] + }, + "LinkRMonCountersAll": { + "description": "The complete RMON counters (traffic counters) for a specific link.", + "type": "object", + "properties": { + "counters": { + "description": "The RMON counter data.", + "allOf": [ + { + "$ref": "#/components/schemas/RMonCountersAll" + } + ] + }, + "link_id": { + "description": "The link ID.", + "allOf": [ + { + "$ref": "#/components/schemas/LinkId" + } + ] + }, + "port_id": { + "description": "The switch port ID.", + "allOf": [ + { + "$ref": "#/components/schemas/PortId" + } + ] + } + }, + "required": [ + "counters", + "link_id", + "port_id" + ] + }, + "LinkSettings": { + "description": "An object with link settings used in concert with [`PortSettings`].", + "type": "object", + "properties": { + "addrs": { + "type": "array", + "items": { + "type": "string", + "format": "ip" + }, + "uniqueItems": true + }, + "params": { + "$ref": "#/components/schemas/LinkCreate" + } + }, + "required": [ + "addrs", + "params" + ] + }, + "LinkState": { + "description": "The state of a data link with a peer.", + "oneOf": [ + { + "description": "An error was encountered while trying to configure the link in the switch hardware.", + "type": "object", + "properties": { + "config_error": { + "type": "string" + } + }, + "required": [ + "config_error" + ], + "additionalProperties": false + }, + { + "description": "The link is up.", + "type": "string", + "enum": [ + "up" + ] + }, + { + "description": "The link is down.", + "type": "string", + "enum": [ + "down" + ] + }, + { + "description": "The Link is offline due to a fault", + "type": "object", + "properties": { + "faulted": { + "$ref": "#/components/schemas/Fault" + } + }, + "required": [ + "faulted" + ], + "additionalProperties": false + }, + { + "description": "The link's state is not known.", + "type": "string", + "enum": [ + "unknown" + ] + } + ] + }, + "LinkUpCounter": { + "description": "Reports how many times a link has transitioned from Down to Up.", + "type": "object", + "properties": { + "current": { + "description": "LinkUp transitions since the link was last enabled", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "link_path": { + "description": "Link being reported", + "type": "string" + }, + "total": { + "description": "LinkUp transitions since the link was created", + "type": "integer", + "format": "uint32", + "minimum": 0 + } + }, + "required": [ + "current", + "link_path", + "total" + ] + }, + "LpPages": { + "description": "Set of AN pages sent by our link partner", + "type": "object", + "properties": { + "base_page": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "next_page1": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "next_page2": { + "type": "integer", + "format": "uint64", + "minimum": 0 + } + }, + "required": [ + "base_page", + "next_page1", + "next_page2" + ] + }, + "LtStatus": { + "description": "Link-training status for a single lane", + "type": "object", + "properties": { + "frame_lock": { + "description": "Frame lock state", + "type": "boolean" + }, + "readout_state": { + "description": "Readout for frame lock state", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "readout_training_state": { + "description": "Training state readout", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "readout_txstate": { + "description": "State machine readout for training arbiter", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "rx_trained": { + "description": "Local training finished", + "type": "boolean" + }, + "sig_det": { + "description": "Signal detect for PCS", + "type": "boolean" + }, + "training_failure": { + "description": "Link training failed", + "type": "boolean" + }, + "tx_training_data_en": { + "description": "TX control to send training pattern", + "type": "boolean" + } + }, + "required": [ + "frame_lock", + "readout_state", + "readout_training_state", + "readout_txstate", + "rx_trained", + "sig_det", + "training_failure", + "tx_training_data_en" + ] + }, + "MacAddr": { + "description": "An EUI-48 MAC address, used for layer-2 addressing.", + "type": "object", + "properties": { + "a": { + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "minItems": 6, + "maxItems": 6 + } + }, + "required": [ + "a" + ] + }, + "ManagementMode": { + "description": "How a switch port is managed.\n\nThe free-side devices in QSFP ports are complex devices, whose operation usually involves coordinated steps through one or more state machines. For example, when bringing up an optical link, a signal from the peer link must be detected; then a signal recovered; equalizer gains set; etc. In `Automatic` mode, all these kinds of steps are managed autonomously by switch driver software. In `Manual` mode, none of these will occur -- a switch port will only change in response to explicit requests from the operator or Oxide control plane.", + "oneOf": [ + { + "description": "A port is managed manually, by either the Oxide control plane or an operator.", + "type": "string", + "enum": [ + "manual" + ] + }, + { + "description": "A port is managed automatically by the switch software.", + "type": "string", + "enum": [ + "automatic" + ] + } + ] + }, + "MediaInterfaceId": { + "oneOf": [ + { + "type": "object", + "properties": { + "id": { + "description": "Media interface ID for multi-mode fiber media.\n\nSee SFF-8024 Table 4-6.", + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "mmf" + ] + } + }, + "required": [ + "id", + "type" + ] + }, + { + "type": "object", + "properties": { + "id": { + "description": "Media interface ID for single-mode fiber.\n\nSee SFF-8024 Table 4-7.", + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "smf" + ] + } + }, + "required": [ + "id", + "type" + ] + }, + { + "type": "object", + "properties": { + "id": { + "description": "Media interface ID for passive copper cables.\n\nSee SFF-8024 Table 4-8.", + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "passive_copper" + ] + } + }, + "required": [ + "id", + "type" + ] + }, + { + "type": "object", + "properties": { + "id": { + "description": "Media interface ID for active cable assemblies.\n\nSee SFF-8024 Table 4-9.", + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "active_cable" + ] + } + }, + "required": [ + "id", + "type" + ] + }, + { + "type": "object", + "properties": { + "id": { + "description": "Media interface ID for BASE-T.\n\nSee SFF-8024 Table 4-10.", + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "base_t" + ] + } + }, + "required": [ + "id", + "type" + ] + } + ] + }, + "Monitors": { + "description": "Free-side device monitoring information.\n\nNote that all values are optional, as some specifications do not require that modules implement monitoring of those values.", + "type": "object", + "properties": { + "aux_monitors": { + "nullable": true, + "description": "Auxiliary monitoring values.\n\nThese are only available on CMIS-compatible transceivers, e.g., QSFP-DD.", + "allOf": [ + { + "$ref": "#/components/schemas/AuxMonitors" + } + ] + }, + "receiver_power": { + "nullable": true, + "description": "The measured input optical power (milliwatts);\n\nNote that due to a limitation in the SFF-8636 specification, it's possible for receiver power to be zero. See [`ReceiverPower`] for details.", + "type": "array", + "items": { + "$ref": "#/components/schemas/ReceiverPower" + } + }, + "supply_voltage": { + "nullable": true, + "description": "The measured input supply voltage (Volts).", + "type": "number", + "format": "float" + }, + "temperature": { + "nullable": true, + "description": "The measured cage temperature (degrees C);", + "type": "number", + "format": "float" + }, + "transmitter_bias_current": { + "nullable": true, + "description": "The output laser bias current (milliamps).", + "type": "array", + "items": { + "type": "number", + "format": "float" + } + }, + "transmitter_power": { + "nullable": true, + "description": "The measured output optical power (milliwatts).", + "type": "array", + "items": { + "type": "number", + "format": "float" + } + } + } + }, + "MulticastGroupCreateExternalEntry": { + "description": "A multicast group configuration for POST requests for external (to the rack) groups.", + "type": "object", + "properties": { + "external_forwarding": { + "$ref": "#/components/schemas/ExternalForwarding" + }, + "group_ip": { + "type": "string", + "format": "ip" + }, + "internal_forwarding": { + "$ref": "#/components/schemas/InternalForwarding" + }, + "sources": { + "nullable": true, + "type": "array", + "items": { + "$ref": "#/components/schemas/IpSrc" + } + }, + "tag": { + "nullable": true, + "description": "Tag for validating update/delete requests. If a tag is not provided, one is auto-generated as `{uuid}:{group_ip}`.", + "type": "string" + } + }, + "required": [ + "external_forwarding", + "group_ip", + "internal_forwarding" + ] + }, + "MulticastGroupCreateUnderlayEntry": { + "description": "A multicast group configuration for POST requests for internal (to the rack) groups.", + "type": "object", + "properties": { + "group_ip": { + "$ref": "#/components/schemas/UnderlayMulticastIpv6" + }, + "members": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MulticastGroupMember" + } + }, + "tag": { + "nullable": true, + "description": "Tag for validating update/delete requests. If a tag is not provided, one is auto-generated as `{uuid}:{group_ip}`.", + "type": "string" + } + }, + "required": [ + "group_ip", + "members" + ] + }, + "MulticastGroupExternalResponse": { + "description": "Response structure for external multicast group operations. These groups handle IPv4 and non-admin-local IPv6 multicast via NAT targets.", + "type": "object", + "properties": { + "external_forwarding": { + "$ref": "#/components/schemas/ExternalForwarding" + }, + "external_group_id": { + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "group_ip": { + "type": "string", + "format": "ip" + }, + "internal_forwarding": { + "$ref": "#/components/schemas/InternalForwarding" + }, + "sources": { + "nullable": true, + "type": "array", + "items": { + "$ref": "#/components/schemas/IpSrc" + } + }, + "tag": { + "description": "Tag for validating update/delete requests. Always present and generated as `{uuid}:{group_ip}` if not provided at creation time.", + "type": "string" + } + }, + "required": [ + "external_forwarding", + "external_group_id", + "group_ip", + "internal_forwarding", + "tag" + ] + }, + "MulticastGroupMember": { + "description": "Represents a member of a multicast group.", + "type": "object", + "properties": { + "direction": { + "$ref": "#/components/schemas/Direction" + }, + "link_id": { + "$ref": "#/components/schemas/LinkId" + }, + "port_id": { + "$ref": "#/components/schemas/PortId" + } + }, + "required": [ + "direction", + "link_id", + "port_id" + ] + }, + "MulticastGroupResponse": { + "description": "Unified response type for operations that return mixed group types.", + "oneOf": [ + { + "description": "Response structure for underlay/internal multicast group operations. These groups handle admin-local IPv6 multicast with full replication.", + "type": "object", + "properties": { + "external_group_id": { + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "group_ip": { + "$ref": "#/components/schemas/UnderlayMulticastIpv6" + }, + "kind": { + "type": "string", + "enum": [ + "underlay" + ] + }, + "members": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MulticastGroupMember" + } + }, + "tag": { + "description": "Tag for validating update/delete requests. Always present and generated as `{uuid}:{group_ip}` if not provided at creation time.", + "type": "string" + }, + "underlay_group_id": { + "type": "integer", + "format": "uint16", + "minimum": 0 + } + }, + "required": [ + "external_group_id", + "group_ip", + "kind", + "members", + "tag", + "underlay_group_id" + ] + }, + { + "description": "Response structure for external multicast group operations. These groups handle IPv4 and non-admin-local IPv6 multicast via NAT targets.", + "type": "object", + "properties": { + "external_forwarding": { + "$ref": "#/components/schemas/ExternalForwarding" + }, + "external_group_id": { + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "group_ip": { + "type": "string", + "format": "ip" + }, + "internal_forwarding": { + "$ref": "#/components/schemas/InternalForwarding" + }, + "kind": { + "type": "string", + "enum": [ + "external" + ] + }, + "sources": { + "nullable": true, + "type": "array", + "items": { + "$ref": "#/components/schemas/IpSrc" + } + }, + "tag": { + "description": "Tag for validating update/delete requests. Always present and generated as `{uuid}:{group_ip}` if not provided at creation time.", + "type": "string" + } + }, + "required": [ + "external_forwarding", + "external_group_id", + "group_ip", + "internal_forwarding", + "kind", + "tag" + ] + } + ] + }, + "MulticastGroupResponseResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/MulticastGroupResponse" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "MulticastGroupUnderlayResponse": { + "description": "Response structure for underlay/internal multicast group operations. These groups handle admin-local IPv6 multicast with full replication.", + "type": "object", + "properties": { + "external_group_id": { + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "group_ip": { + "$ref": "#/components/schemas/UnderlayMulticastIpv6" + }, + "members": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MulticastGroupMember" + } + }, + "tag": { + "description": "Tag for validating update/delete requests. Always present and generated as `{uuid}:{group_ip}` if not provided at creation time.", + "type": "string" + }, + "underlay_group_id": { + "type": "integer", + "format": "uint16", + "minimum": 0 + } + }, + "required": [ + "external_group_id", + "group_ip", + "members", + "tag", + "underlay_group_id" + ] + }, + "MulticastGroupUpdateExternalEntry": { + "description": "A multicast group update entry for PUT requests for external (to the rack) groups.\n\nTag validation is performed via the `tag` query parameter.", + "type": "object", + "properties": { + "external_forwarding": { + "$ref": "#/components/schemas/ExternalForwarding" + }, + "internal_forwarding": { + "$ref": "#/components/schemas/InternalForwarding" + }, + "sources": { + "nullable": true, + "type": "array", + "items": { + "$ref": "#/components/schemas/IpSrc" + } + } + }, + "required": [ + "external_forwarding", + "internal_forwarding" + ] + }, + "MulticastGroupUpdateUnderlayEntry": { + "description": "Represents a multicast replication entry for PUT requests for internal (to the rack) groups.\n\nTag validation is performed via the `tag` query parameter.", + "type": "object", + "properties": { + "members": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MulticastGroupMember" + } + } + }, + "required": [ + "members" + ] + }, + "NatTarget": { + "description": "represents an internal NAT target", + "type": "object", + "properties": { + "inner_mac": { + "$ref": "#/components/schemas/MacAddr" + }, + "internal_ip": { + "type": "string", + "format": "ipv6" + }, + "vni": { + "$ref": "#/components/schemas/Vni" + } + }, + "required": [ + "inner_mac", + "internal_ip", + "vni" + ] + }, + "Oui": { + "description": "An Organization Unique Identifier.", + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "minItems": 3, + "maxItems": 3 + }, + "OutputStatus": { + "type": "string", + "enum": [ + "valid", + "invalid" + ] + }, + "PcsCounters": { + "description": "Per-port PCS counters", + "type": "object", + "properties": { + "bad_sync_headers": { + "description": "Count of bad sync headers", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "bip_errors_per_pcs_lane": { + "description": "Bit Inteleaved Parity errors (per lane)", + "type": "array", + "items": { + "type": "integer", + "format": "uint32", + "minimum": 0 + } + }, + "block_lock_loss": { + "description": "Count of block-lock loss detections", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "errored_blocks": { + "description": "Count of errored blocks", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "hi_ber": { + "description": "Count of high bit error rate events", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "invalid_errors": { + "description": "Count of invalid error events", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "port": { + "description": "Port being tracked", + "type": "string" + }, + "sync_loss": { + "description": "Count of sync loss detections", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "unknown_errors": { + "description": "Count of unknown error events", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "valid_errors": { + "description": "Count of valid error events", + "type": "integer", + "format": "uint32", + "minimum": 0 + } + }, + "required": [ + "bad_sync_headers", + "bip_errors_per_pcs_lane", + "block_lock_loss", + "errored_blocks", + "hi_ber", + "invalid_errors", + "port", + "sync_loss", + "unknown_errors", + "valid_errors" + ] + }, + "Polarity": { + "type": "string", + "enum": [ + "Normal", + "Inverted" + ] + }, + "PortFec": { + "type": "string", + "enum": [ + "None", + "Firecode", + "RS" + ] + }, + "PortId": { + "example": "qsfp0", + "title": "PortId", + "description": "Physical switch port identifier", + "oneOf": [ + { + "title": "internal", + "type": "string", + "pattern": "(^[iI][nN][tT]0$)" + }, + { + "title": "rear", + "type": "string", + "pattern": "(^[rR][eE][aA][rR](([0-9])|([1-2][0-9])|(3[0-1]))$)" + }, + { + "title": "qsfp", + "type": "string", + "pattern": "(^[qQ][sS][fF][pP](([0-9])|([1-2][0-9])|(3[0-1]))$)" + } + ] + }, + "PortMedia": { + "type": "string", + "enum": [ + "Copper", + "Optical", + "CPU", + "None", + "Unknown" + ] + }, + "PortPrbsMode": { + "description": "Legal PRBS modes", + "type": "string", + "enum": [ + "Mode31", + "Mode23", + "Mode15", + "Mode13", + "Mode11", + "Mode9", + "Mode7", + "Mission" + ] + }, + "PortSettings": { + "description": "A port settings transaction object. When posted to the `/port-settings/{port_id}` API endpoint, these settings will be applied holistically, and to the extent possible atomically to a given port.", + "type": "object", + "properties": { + "links": { + "description": "The link settings to apply to the port on a per-link basis. Any links not in this map that are resident on the switch port will be removed. Any links that are in this map that are not resident on the switch port will be added. Any links that are resident on the switch port and in this map, and are different, will be modified. Links are indexed by spatial index within the port.", + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/LinkSettings" + } + } + }, + "required": [ + "links" + ] + }, + "PortSpeed": { + "description": "Speeds with which a single port may be configured", + "type": "string", + "enum": [ + "Speed0G", + "Speed1G", + "Speed10G", + "Speed25G", + "Speed40G", + "Speed50G", + "Speed100G", + "Speed200G", + "Speed400G" + ] + }, + "PowerMode": { + "description": "The power mode of a module.", + "type": "object", + "properties": { + "software_override": { + "nullable": true, + "description": "Whether the module is configured for software override of power control.\n\nIf the module is in `PowerState::Off`, this can't be determined, and `None` is returned.", + "type": "boolean" + }, + "state": { + "description": "The actual power state.", + "allOf": [ + { + "$ref": "#/components/schemas/PowerState" + } + ] + } + }, + "required": [ + "state" + ] + }, + "PowerState": { + "description": "An allowed power state for the module.", + "oneOf": [ + { + "description": "A module is entirely powered off, using the EFuse.", + "type": "string", + "enum": [ + "off" + ] + }, + { + "description": "Power is enabled to the module, but module remains in low-power mode.\n\nIn this state, modules will not establish a link or transmit traffic, but they may be managed and queried for information through their memory maps.", + "type": "string", + "enum": [ + "low" + ] + }, + { + "description": "The module is in high-power mode.\n\nNote that additional configuration may be required to correctly configure the module, such as described in SFF-8636 rev 2.10a table 6-10, and that the _host side_ is responsible for ensuring that the relevant configuration is applied.", + "type": "string", + "enum": [ + "high" + ] + } + ] + }, + "RMonCounters": { + "description": "High level subset of the RMon counters maintained by the Tofino ASIC", + "type": "object", + "properties": { + "crc_error_stomped": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "fragments_rx": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frame_too_long": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_dropped_buffer_full": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_rx_all": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_rx_ok": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_tx_all": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_tx_ok": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_tx_with_error": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_with_any_error": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "octets_rx": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "octets_rx_in_good_frames": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "octets_tx_total": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "octets_tx_without_error": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "port": { + "type": "string" + } + }, + "required": [ + "crc_error_stomped", + "fragments_rx", + "frame_too_long", + "frames_dropped_buffer_full", + "frames_rx_all", + "frames_rx_ok", + "frames_tx_all", + "frames_tx_ok", + "frames_tx_with_error", + "frames_with_any_error", + "octets_rx", + "octets_rx_in_good_frames", + "octets_tx_total", + "octets_tx_without_error", + "port" + ] + }, + "RMonCountersAll": { + "description": "All of the RMon counters maintained by the Tofino ASIC", + "type": "object", + "properties": { + "crc_error_stomped": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "fragments_rx": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frame_too_long": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_dropped_buffer_full": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_rx_all": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_rx_indersized": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_rx_length_1024_1518": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_rx_length_128_255": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_rx_length_1519_2047": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_rx_length_2048_4095": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_rx_length_256_511": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_rx_length_4096_8191": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_rx_length_512_1023": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_rx_length_65_127": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_rx_length_8192_9215": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_rx_length_9216": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_rx_length_eq_64": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_rx_length_lt_64": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_rx_oftype_pause": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_rx_ok": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_rx_oversized": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_rx_with_any_error": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_rx_with_broadcast_addresses": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_rx_with_fcs_error": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_rx_with_length_error": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_rx_with_multicast_addresses": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_rx_with_unicast_addresses": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_truncated": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_tx_all": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_tx_broadcast": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_tx_length_1024_1518": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_tx_length_128_255": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_tx_length_1519_2047": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_tx_length_2048_4095": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_tx_length_256_511": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_tx_length_4096_8191": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_tx_length_512_1023": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_tx_length_65_127": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_tx_length_8192_9215": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_tx_length_9216": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_tx_length_eq_64": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_tx_length_lt_64": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_tx_multicast": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_tx_ok": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_tx_pause": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_tx_pri_pause": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_tx_unicast": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_tx_vlan": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "frames_tx_with_error": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "jabber_rx": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "octets_rx": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "octets_rx_in_good_frames": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "octets_tx_total": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "octets_tx_without_error": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "port": { + "type": "string" + }, + "pri0_frames_rx": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "pri0_framex_tx": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "pri1_frames_rx": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "pri1_frames_tx": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "pri2_frames_rx": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "pri2_frames_tx": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "pri3_frames_rx": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "pri3_frames_tx": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "pri4_frames_rx": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "pri4_frames_tx": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "pri5_frames_rx": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "pri5_frames_tx": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "pri6_frames_rx": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "pri6_frames_tx": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "pri7_frames_rx": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "pri7_frames_tx": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "priority_pause_frames": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "rx_pri0_pause_1us_count": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "rx_pri1_pause_1us_count": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "rx_pri2_pause_1us_count": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "rx_pri3_pause_1us_count": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "rx_pri4_pause_1us_count": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "rx_pri5_pause_1us_count": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "rx_pri6_pause_1us_count": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "rx_pri7_pause_1us_count": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "rx_standard_pause_1us_count": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "rx_vlan_frames_good": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "tx_pri0_pause_1us_count": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "tx_pri1_pause_1us_count": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "tx_pri2_pause_1us_count": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "tx_pri3_pause_1us_count": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "tx_pri4_pause_1us_count": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "tx_pri5_pause_1us_count": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "tx_pri6_pause_1us_count": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "tx_pri7_pause_1us_count": { + "type": "integer", + "format": "uint64", + "minimum": 0 + } + }, + "required": [ + "crc_error_stomped", + "fragments_rx", + "frame_too_long", + "frames_dropped_buffer_full", + "frames_rx_all", + "frames_rx_indersized", + "frames_rx_length_1024_1518", + "frames_rx_length_128_255", + "frames_rx_length_1519_2047", + "frames_rx_length_2048_4095", + "frames_rx_length_256_511", + "frames_rx_length_4096_8191", + "frames_rx_length_512_1023", + "frames_rx_length_65_127", + "frames_rx_length_8192_9215", + "frames_rx_length_9216", + "frames_rx_length_eq_64", + "frames_rx_length_lt_64", + "frames_rx_oftype_pause", + "frames_rx_ok", + "frames_rx_oversized", + "frames_rx_with_any_error", + "frames_rx_with_broadcast_addresses", + "frames_rx_with_fcs_error", + "frames_rx_with_length_error", + "frames_rx_with_multicast_addresses", + "frames_rx_with_unicast_addresses", + "frames_truncated", + "frames_tx_all", + "frames_tx_broadcast", + "frames_tx_length_1024_1518", + "frames_tx_length_128_255", + "frames_tx_length_1519_2047", + "frames_tx_length_2048_4095", + "frames_tx_length_256_511", + "frames_tx_length_4096_8191", + "frames_tx_length_512_1023", + "frames_tx_length_65_127", + "frames_tx_length_8192_9215", + "frames_tx_length_9216", + "frames_tx_length_eq_64", + "frames_tx_length_lt_64", + "frames_tx_multicast", + "frames_tx_ok", + "frames_tx_pause", + "frames_tx_pri_pause", + "frames_tx_unicast", + "frames_tx_vlan", + "frames_tx_with_error", + "jabber_rx", + "octets_rx", + "octets_rx_in_good_frames", + "octets_tx_total", + "octets_tx_without_error", + "port", + "pri0_frames_rx", + "pri0_framex_tx", + "pri1_frames_rx", + "pri1_frames_tx", + "pri2_frames_rx", + "pri2_frames_tx", + "pri3_frames_rx", + "pri3_frames_tx", + "pri4_frames_rx", + "pri4_frames_tx", + "pri5_frames_rx", + "pri5_frames_tx", + "pri6_frames_rx", + "pri6_frames_tx", + "pri7_frames_rx", + "pri7_frames_tx", + "priority_pause_frames", + "rx_pri0_pause_1us_count", + "rx_pri1_pause_1us_count", + "rx_pri2_pause_1us_count", + "rx_pri3_pause_1us_count", + "rx_pri4_pause_1us_count", + "rx_pri5_pause_1us_count", + "rx_pri6_pause_1us_count", + "rx_pri7_pause_1us_count", + "rx_standard_pause_1us_count", + "rx_vlan_frames_good", + "tx_pri0_pause_1us_count", + "tx_pri1_pause_1us_count", + "tx_pri2_pause_1us_count", + "tx_pri3_pause_1us_count", + "tx_pri4_pause_1us_count", + "tx_pri5_pause_1us_count", + "tx_pri6_pause_1us_count", + "tx_pri7_pause_1us_count" + ] + }, + "ReceiverPower": { + "description": "Measured receiver optical power.\n\nThe SFF specifications allow for devices to monitor input optical power in several ways. It may either be an average power, over some unspecified time, or a peak-to-peak power. The latter is often abbreviated OMA, for Optical Modulation Amplitude. Again the time interval for peak-to-peak measurments are not specified.\n\nDetails -------\n\nThe SFF-8636 specification has an unfortunate limitation. There is no separate advertisement for whether a module supports measurements of receiver power. Instead, the _kind_ of measurement is advertised. The _same bit value_ could mean that either a peak-to-peak measurement is supported, or the measurements are not supported at all. Thus values of `PeakToPeak(0.0)` may mean that power measurements are not supported.", + "oneOf": [ + { + "description": "The measurement is represents average optical power, in mW.", + "type": "object", + "properties": { + "average": { + "type": "number", + "format": "float" + } + }, + "required": [ + "average" + ], + "additionalProperties": false + }, + { + "description": "The measurement represents a peak-to-peak, in mW.", + "type": "object", + "properties": { + "peak_to_peak": { + "type": "number", + "format": "float" + } + }, + "required": [ + "peak_to_peak" + ], + "additionalProperties": false + } + ] + }, + "RxSigInfo": { + "description": "Per-lane Rx signal information", + "type": "object", + "properties": { + "phy_ready": { + "description": "CDR lock achieved", + "type": "boolean" + }, + "ppm": { + "description": "Apparent PPM difference between local and remote", + "type": "integer", + "format": "int32" + }, + "sig_detect": { + "description": "Rx signal detected", + "type": "boolean" + } + }, + "required": [ + "phy_ready", + "ppm", + "sig_detect" + ] + }, + "SerdesEye": { + "description": "Eye height(s) for a single lane in mv", + "oneOf": [ + { + "type": "object", + "properties": { + "Nrz": { + "type": "number", + "format": "float" + } + }, + "required": [ + "Nrz" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "Pam4": { + "type": "object", + "properties": { + "eye1": { + "type": "number", + "format": "float" + }, + "eye2": { + "type": "number", + "format": "float" + }, + "eye3": { + "type": "number", + "format": "float" + } + }, + "required": [ + "eye1", + "eye2", + "eye3" + ] + } + }, + "required": [ + "Pam4" + ], + "additionalProperties": false + } + ] + }, + "Sff8636Datapath": { + "description": "The datapath of an SFF-8636 module.\n\nThis describes the state of a single lane in an SFF module. It includes information about input and output signals, faults, and controls.", + "type": "object", + "properties": { + "rx_cdr_enabled": { + "description": "Media-side transmit Clock and Data Recovery (CDR) enable status.\n\nCDR is the process by which the module enages an internal retimer function, through which the module attempts to recovery a clock signal directly from the input bitstream.", + "type": "boolean" + }, + "rx_lol": { + "description": "Media-side loss of lock flag.\n\nThis is true if the module is not able to extract a clock signal from the media-side signal (usually optical).", + "type": "boolean" + }, + "rx_los": { + "description": "Media-side loss of signal flag.\n\nThis is true if there is no detected input signal from the media-side (usually optical).", + "type": "boolean" + }, + "tx_adaptive_eq_fault": { + "description": "Flag indicating a fault in adaptive transmit equalization.", + "type": "boolean" + }, + "tx_cdr_enabled": { + "description": "Host-side transmit Clock and Data Recovery (CDR) enable status.\n\nCDR is the process by which the module enages an internal retimer function, through which the module attempts to recovery a clock signal directly from the input bitstream.", + "type": "boolean" + }, + "tx_enabled": { + "description": "Software control of output transmitter.", + "type": "boolean" + }, + "tx_fault": { + "description": "Flag indicating a fault in the transmitter and/or laser.", + "type": "boolean" + }, + "tx_lol": { + "description": "Host-side loss of lock flag.\n\nThis is true if the module is not able to extract a clock signal from the host-side electrical signal.", + "type": "boolean" + }, + "tx_los": { + "description": "Host-side loss of signal flag.\n\nThis is true if there is no detected electrical signal from the host-side serdes.", + "type": "boolean" + } + }, + "required": [ + "rx_cdr_enabled", + "rx_lol", + "rx_los", + "tx_adaptive_eq_fault", + "tx_cdr_enabled", + "tx_enabled", + "tx_fault", + "tx_lol", + "tx_los" + ] + }, + "SffComplianceCode": { + "description": "The compliance code for an SFF-8636 module.\n\nThese values record a specification compliance code, from SFF-8636 Table 6-17, or an extended specification compliance code, from SFF-8024 Table 4-4.", + "oneOf": [ + { + "type": "object", + "properties": { + "code": { + "description": "Extended electrical or optical interface codes", + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "extended" + ] + } + }, + "required": [ + "code", + "type" + ] + }, + { + "type": "object", + "properties": { + "code": { + "description": "The Ethernet specification implemented by a module.", + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "ethernet" + ] + } + }, + "required": [ + "code", + "type" + ] + } + ] + }, + "SidecarCableLeg": { + "description": "The leg of the Sidecar-internal cable.\n\nThis describes the leg on the cabling that connects the pins on the Tofino ASIC to the Sidecar chassis connector.", + "type": "string", + "enum": [ + "A", + "C" + ] + }, + "SidecarConnector": { + "description": "The Sidecar chassis connector mating the backplane and internal cabling.\n\nThis describes the \"group\" of backplane links that all terminate in one connector on the Sidecar itself. This is the connection point between a cable on the backplane itself and the Sidecar chassis.", + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "SwitchIdentifiers": { + "description": "Identifiers for a switch.", + "type": "object", + "properties": { + "asic_backend": { + "description": "Asic backend (compiler target) responsible for these identifiers.", + "type": "string" + }, + "fab": { + "nullable": true, + "description": "Fabrication plant identifier.", + "type": "string", + "minLength": 1, + "maxLength": 1 + }, + "lot": { + "nullable": true, + "description": "Lot identifier.", + "type": "string", + "minLength": 1, + "maxLength": 1 + }, + "model": { + "description": "The model number of the switch being managed.", + "type": "string" + }, + "revision": { + "description": "The revision number of the switch being managed.", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "serial": { + "description": "The serial number of the switch being managed.", + "type": "string" + }, + "sidecar_id": { + "description": "Unique identifier for the chip.", + "type": "string", + "format": "uuid" + }, + "slot": { + "description": "The slot number of the switch being managed.\n\nMGS uses u16 for this internally.", + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "wafer": { + "nullable": true, + "description": "Wafer number within the lot.", + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "wafer_loc": { + "nullable": true, + "description": "The wafer location as (x, y) coordinates on the wafer, represented as an array due to the lack of tuple support in OpenAPI.", + "type": "array", + "items": { + "type": "integer", + "format": "int16" + }, + "minItems": 2, + "maxItems": 2 + } + }, + "required": [ + "asic_backend", + "model", + "revision", + "serial", + "sidecar_id", + "slot" + ] + }, + "SwitchPort": { + "description": "A physical port on the Sidecar switch.", + "type": "object", + "properties": { + "management_mode": { + "description": "How the QSFP device is managed.\n\nSee `ManagementMode` for details.", + "allOf": [ + { + "$ref": "#/components/schemas/ManagementMode" + } + ] + }, + "port_id": { + "description": "The identifier for the switch port.", + "allOf": [ + { + "$ref": "#/components/schemas/PortId" + } + ] + }, + "transceiver": { + "nullable": true, + "description": "Details about a transceiver module inserted into the switch port.\n\nIf there is no transceiver at all, this will be `None`.", + "allOf": [ + { + "$ref": "#/components/schemas/Transceiver" + } + ] + } + }, + "required": [ + "port_id" + ] + }, + "Table": { + "description": "Represents the contents of a P4 table", + "type": "object", + "properties": { + "entries": { + "description": "There will be an entry for each populated slot in the table", + "type": "array", + "items": { + "$ref": "#/components/schemas/TableEntry" + } + }, + "name": { + "description": "A user-friendly name for the table", + "type": "string" + }, + "size": { + "description": "The maximum number of entries the table can hold", + "type": "integer", + "format": "uint", + "minimum": 0 + } + }, + "required": [ + "entries", + "name", + "size" + ] + }, + "TableCounterEntry": { + "type": "object", + "properties": { + "data": { + "description": "Counter values", + "allOf": [ + { + "$ref": "#/components/schemas/CounterData" + } + ] + }, + "keys": { + "description": "Names and values of each of the key fields.", + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "required": [ + "data", + "keys" + ] + }, + "TableEntry": { + "description": "Each entry in a P4 table is addressed by matching against a set of key values. If an entry is found, an action is taken with an action-specific set of arguments.\n\nNote: each entry will have the same key fields and each instance of any given action will have the same argument names, so a vector of TableEntry structs will contain a signficant amount of redundant data. We could consider tightening this up by including a schema of sorts in the \"struct Table\".", + "type": "object", + "properties": { + "action": { + "description": "Name of the action to take on a match", + "type": "string" + }, + "action_args": { + "description": "Names and values for the arguments to the action implementation.", + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "keys": { + "description": "Names and values of each of the key fields.", + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "required": [ + "action", + "action_args", + "keys" + ] + }, + "TfportData": { + "description": "The per-link data consumed by tfportd", + "type": "object", + "properties": { + "asic_id": { + "description": "The lower-level ASIC ID used to refer to this object in the switch driver software.", + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "ipv6_enabled": { + "description": "Is ipv6 enabled for this link", + "type": "boolean" + }, + "link_id": { + "description": "The link ID for this link.", + "allOf": [ + { + "$ref": "#/components/schemas/LinkId" + } + ] + }, + "link_local": { + "nullable": true, + "description": "The IPv6 link-local address of the link, if it exists.", + "type": "string", + "format": "ipv6" + }, + "mac": { + "description": "The MAC address for the link.", + "allOf": [ + { + "$ref": "#/components/schemas/MacAddr" + } + ] + }, + "port_id": { + "description": "The switch port ID for this link.", + "allOf": [ + { + "$ref": "#/components/schemas/PortId" + } + ] + } + }, + "required": [ + "asic_id", + "ipv6_enabled", + "link_id", + "mac", + "port_id" + ] + }, + "Transceiver": { + "description": "The state of a transceiver in a QSFP switch port.", + "oneOf": [ + { + "description": "The transceiver could not be managed due to a power fault.", + "type": "object", + "properties": { + "info": { + "$ref": "#/components/schemas/FaultReason" + }, + "state": { + "type": "string", + "enum": [ + "faulted" + ] + } + }, + "required": [ + "info", + "state" + ] + }, + { + "description": "A transceiver was present, but unsupported and automatically disabled.", + "type": "object", + "properties": { + "state": { + "type": "string", + "enum": [ + "unsupported" + ] + } + }, + "required": [ + "state" + ] + }, + { + "description": "A transceiver is present and supported.", + "type": "object", + "properties": { + "info": { + "$ref": "#/components/schemas/TransceiverInfo" + }, + "state": { + "type": "string", + "enum": [ + "supported" + ] + } + }, + "required": [ + "info", + "state" + ] + } + ] + }, + "TransceiverInfo": { + "description": "Information about a QSFP transceiver.\n\nThis stores the most relevant information about a transceiver module, such as vendor info or power. Each field may be missing, indicating it could not be determined.", + "type": "object", + "properties": { + "electrical_mode": { + "description": "The electrical mode of the transceiver.\n\nSee [`ElectricalMode`] for details.", + "allOf": [ + { + "$ref": "#/components/schemas/ElectricalMode" + } + ] + }, + "in_reset": { + "nullable": true, + "description": "True if the module is currently in reset.", + "type": "boolean" + }, + "interrupt_pending": { + "nullable": true, + "description": "True if there is a pending interrupt on the module.", + "type": "boolean" + }, + "power_mode": { + "nullable": true, + "description": "The power mode of the transceiver.", + "allOf": [ + { + "$ref": "#/components/schemas/PowerMode" + } + ] + }, + "vendor_info": { + "nullable": true, + "description": "Vendor and part identifying information.\n\nThe information will not be populated if it could not be read.", + "allOf": [ + { + "$ref": "#/components/schemas/VendorInfo" + } + ] + } + }, + "required": [ + "electrical_mode" + ] + }, + "TxEq": { + "description": "Parameters to adjust the transceiver equalization settings for a link on a switch. These parameters match those available on a tofino-based sidecar, and may need to be adapted when we move to a new switch ASIC.", + "type": "object", + "properties": { + "main": { + "nullable": true, + "type": "integer", + "format": "int32" + }, + "post1": { + "nullable": true, + "type": "integer", + "format": "int32" + }, + "post2": { + "nullable": true, + "type": "integer", + "format": "int32" + }, + "pre1": { + "nullable": true, + "type": "integer", + "format": "int32" + }, + "pre2": { + "nullable": true, + "type": "integer", + "format": "int32" + } + } + }, + "TxEqSwHw": { + "description": "This represents the software-determined equalization value initially assigned to the transceiver and the value actually being used by the hardware. The values may differ on transceivers that are capable of tuning their own settings at run time.", + "type": "object", + "properties": { + "hw": { + "$ref": "#/components/schemas/TxEq" + }, + "sw": { + "$ref": "#/components/schemas/TxEq" + } + }, + "required": [ + "hw", + "sw" + ] + }, + "UnderlayMulticastIpv6": { + "description": "A validated underlay multicast IPv6 address.\n\nUnderlay multicast addresses must be within the subnet allocated by Omicron for rack-internal multicast traffic (ff04::/64). This is a subset of the admin-local scope (ff04::/16) defined in RFC 4291.", + "type": "string", + "format": "ipv6" + }, + "Vendor": { + "description": "Vendor-specific information about a transceiver module.", + "type": "object", + "properties": { + "date": { + "nullable": true, + "type": "string" + }, + "name": { + "type": "string" + }, + "oui": { + "$ref": "#/components/schemas/Oui" + }, + "part": { + "type": "string" + }, + "revision": { + "type": "string" + }, + "serial": { + "type": "string" + } + }, + "required": [ + "name", + "oui", + "part", + "revision", + "serial" + ] + }, + "VendorInfo": { + "description": "The vendor information for a transceiver module.", + "type": "object", + "properties": { + "identifier": { + "description": "The SFF-8024 identifier.", + "type": "string" + }, + "vendor": { + "description": "The vendor information.", + "allOf": [ + { + "$ref": "#/components/schemas/Vendor" + } + ] + } + }, + "required": [ + "identifier", + "vendor" + ] + }, + "Vni": { + "description": "A Geneve Virtual Network Identifier.\n\nA Geneve VNI is a 24-bit value used to identify virtual networks encapsulated using the Generic Network Virtualization Encapsulation (Geneve) protocol (RFC 8926).", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "ipv4ResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "type": "string", + "format": "ipv4" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "ipv6ResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "type": "string", + "format": "ipv6" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, + "MulticastTag": { + "description": "Tag for identifying and authorizing multicast group operations.\n\nTag format: 1 to 80 ASCII bytes containing alphanumeric characters, hyphens, underscores, colons, or periods. Default format is `{uuid}:{group_ip}`.", + "type": "string", + "pattern": "^[a-zA-Z0-9_.:-]+$", + "minLength": 1, + "maxLength": 80 + } + }, + "responses": { + "Error": { + "description": "Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } +} diff --git a/openapi/dpd/dpd-latest.json b/openapi/dpd/dpd-latest.json index a8d9fb4..872b94b 120000 --- a/openapi/dpd/dpd-latest.json +++ b/openapi/dpd/dpd-latest.json @@ -1 +1 @@ -dpd-2.0.0-74a45c.json \ No newline at end of file +dpd-4.0.0-7b2800.json \ No newline at end of file diff --git a/swadm/src/link.rs b/swadm/src/link.rs index f04d4d2..769e6ed 100644 --- a/swadm/src/link.rs +++ b/swadm/src/link.rs @@ -2,7 +2,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/ // -// Copyright 2025 Oxide Computer Company +// Copyright 2026 Oxide Computer Company use std::collections::HashMap; use std::convert::From; diff --git a/xtask/src/linux.rs b/xtask/src/linux.rs index 0f032aa..96288d0 100644 --- a/xtask/src/linux.rs +++ b/xtask/src/linux.rs @@ -2,7 +2,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/ // -// Copyright 2025 Oxide Computer Company +// Copyright 2026 Oxide Computer Company use std::fs; use std::io::Write;