From 0aef4d543a85cb4d4a9e359cd022c93e5b76b2db Mon Sep 17 00:00:00 2001 From: Zeeshan Lakhani Date: Mon, 1 Dec 2025 01:35:40 +0000 Subject: [PATCH 01/22] [deps] Update Omicron `main` related deps to 64b40cb This PR is a precursor to a follow-up PR that leverages updated code in `omicron-common`. This gets dendrite in line with Omicron `main`, capturing an upstream type change that we needed to accomodate. --- Cargo.lock | 168 ++++++++++++++-------------------- Cargo.toml | 2 +- dpd/Cargo.toml | 1 + dpd/src/switch_identifiers.rs | 4 +- 4 files changed, 72 insertions(+), 103 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 70b3cc12..abe303a4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -124,7 +124,7 @@ checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "api_identity" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#f41bd73be02538b43ab0c65585b703a1cb216d03" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec600ca74e573a1926c1876e33b35" dependencies = [ "omicron-workspace-hack", "proc-macro2", @@ -393,9 +393,9 @@ dependencies = [ [[package]] name = "bhyve_api" version = "0.0.0" -source = "git+https://github.com/oxidecomputer/propolis?rev=23b06c2f452a97fac1dc12561d8451ce876d7c5a#23b06c2f452a97fac1dc12561d8451ce876d7c5a" +source = "git+https://github.com/oxidecomputer/propolis?rev=3f1752e6cee9a2f8ecdce6e2ad3326781182e2d9#3f1752e6cee9a2f8ecdce6e2ad3326781182e2d9" dependencies = [ - "bhyve_api_sys 0.0.0 (git+https://github.com/oxidecomputer/propolis?rev=23b06c2f452a97fac1dc12561d8451ce876d7c5a)", + "bhyve_api_sys 0.0.0 (git+https://github.com/oxidecomputer/propolis?rev=3f1752e6cee9a2f8ecdce6e2ad3326781182e2d9)", "libc", "strum 0.26.3", ] @@ -413,7 +413,7 @@ dependencies = [ [[package]] name = "bhyve_api_sys" version = "0.0.0" -source = "git+https://github.com/oxidecomputer/propolis?rev=23b06c2f452a97fac1dc12561d8451ce876d7c5a#23b06c2f452a97fac1dc12561d8451ce876d7c5a" +source = "git+https://github.com/oxidecomputer/propolis?rev=3f1752e6cee9a2f8ecdce6e2ad3326781182e2d9#3f1752e6cee9a2f8ecdce6e2ad3326781182e2d9" dependencies = [ "libc", "strum 0.26.3", @@ -748,7 +748,7 @@ checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" [[package]] name = "clickhouse-admin-types" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#e76962a5f67b8c5bf813ee24a7600634de708b35" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec600ca74e573a1926c1876e33b35" dependencies = [ "anyhow", "atomicwrites", @@ -804,7 +804,7 @@ dependencies = [ [[package]] name = "cockroach-admin-types" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#e76962a5f67b8c5bf813ee24a7600634de708b35" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec600ca74e573a1926c1876e33b35" dependencies = [ "chrono", "csv", @@ -1061,7 +1061,7 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crucible-smf" version = "0.0.0" -source = "git+https://github.com/oxidecomputer/crucible?rev=779775d5130ff7a4836f52f48b7e64d1479ee104#779775d5130ff7a4836f52f48b7e64d1479ee104" +source = "git+https://github.com/oxidecomputer/crucible?rev=7103cd3a3d7b0112d2949dd135db06fef0c156bb#7103cd3a3d7b0112d2949dd135db06fef0c156bb" dependencies = [ "crucible-workspace-hack", "libc", @@ -1367,7 +1367,7 @@ source = "git+https://github.com/oxidecomputer/dlpi-sys?branch=main#555fa6e1315a dependencies = [ "libc", "libdlpi-sys 0.1.0 (git+https://github.com/oxidecomputer/dlpi-sys?branch=main)", - "num_enum 0.7.4", + "num_enum 0.7.5", "pretty-hex", "thiserror 2.0.16", "tokio", @@ -1376,11 +1376,11 @@ dependencies = [ [[package]] name = "dlpi" version = "0.2.0" -source = "git+https://github.com/oxidecomputer/dlpi-sys#555fa6e1315a64f40c72716e4d168697c03795c6" +source = "git+https://github.com/oxidecomputer/dlpi-sys#0a0b98721c2b789767c7b54217e3cb8e702fcc38" dependencies = [ "libc", "libdlpi-sys 0.1.0 (git+https://github.com/oxidecomputer/dlpi-sys)", - "num_enum 0.7.4", + "num_enum 0.7.5", "pretty-hex", "thiserror 2.0.16", ] @@ -1441,6 +1441,7 @@ dependencies = [ "expectorate", "futures", "gateway-client", + "gateway-types", "internal-dns-resolver", "internal-dns-types", "libc", @@ -1786,7 +1787,7 @@ dependencies = [ [[package]] name = "ereport-types" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#e76962a5f67b8c5bf813ee24a7600634de708b35" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec600ca74e573a1926c1876e33b35" dependencies = [ "dropshot", "omicron-uuid-kinds", @@ -2059,7 +2060,7 @@ dependencies = [ [[package]] name = "gateway-client" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#e76962a5f67b8c5bf813ee24a7600634de708b35" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec600ca74e573a1926c1876e33b35" dependencies = [ "base64 0.22.1", "chrono", @@ -2084,7 +2085,7 @@ dependencies = [ [[package]] name = "gateway-messages" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/management-gateway-service?rev=77e316c812aa057b9714d0d99c4a7bdd36d45be2#77e316c812aa057b9714d0d99c4a7bdd36d45be2" +source = "git+https://github.com/oxidecomputer/management-gateway-service?rev=669fe557b66f44aed3c622bd17bc092f08797e0c#669fe557b66f44aed3c622bd17bc092f08797e0c" dependencies = [ "bitflags 2.9.4", "hubpack", @@ -2101,7 +2102,7 @@ dependencies = [ [[package]] name = "gateway-types" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#e76962a5f67b8c5bf813ee24a7600634de708b35" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec600ca74e573a1926c1876e33b35" dependencies = [ "daft", "dropshot", @@ -2731,23 +2732,11 @@ dependencies = [ "zerovec", ] -[[package]] -name = "id-map" -version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#f41bd73be02538b43ab0c65585b703a1cb216d03" -dependencies = [ - "daft", - "derive-where", - "omicron-workspace-hack", - "schemars", - "serde", -] - [[package]] name = "iddqd" -version = "0.3.13" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f73e38c7e0c1b237e00655516f8f633c584c0bd59ce63fedac6f49efeba15613" +checksum = "6b215e67ed1d1a4b1702acd787c487d16e4c977c5dcbcc4587bdb5ea26b6ce06" dependencies = [ "allocator-api2", "daft", @@ -2801,7 +2790,7 @@ dependencies = [ [[package]] name = "illumos-sys-hdrs" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/opte?rev=77f1ff9ceea1afe5733f0003f73f806b8c8c58ae#77f1ff9ceea1afe5733f0003f73f806b8c8c58ae" +source = "git+https://github.com/oxidecomputer/opte?rev=795a1e0aeefb7a2c6fe4139779fdf66930d09b80#795a1e0aeefb7a2c6fe4139779fdf66930d09b80" dependencies = [ "bitflags 2.9.4", ] @@ -2809,11 +2798,11 @@ dependencies = [ [[package]] name = "illumos-utils" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#e76962a5f67b8c5bf813ee24a7600634de708b35" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec600ca74e573a1926c1876e33b35" dependencies = [ "anyhow", "async-trait", - "bhyve_api 0.0.0 (git+https://github.com/oxidecomputer/propolis?rev=23b06c2f452a97fac1dc12561d8451ce876d7c5a)", + "bhyve_api 0.0.0 (git+https://github.com/oxidecomputer/propolis?rev=3f1752e6cee9a2f8ecdce6e2ad3326781182e2d9)", "byteorder", "camino", "camino-tempfile", @@ -2925,7 +2914,7 @@ dependencies = [ [[package]] name = "internal-dns-resolver" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#e76962a5f67b8c5bf813ee24a7600634de708b35" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec600ca74e573a1926c1876e33b35" dependencies = [ "futures", "hickory-proto 0.25.2", @@ -2943,7 +2932,7 @@ dependencies = [ [[package]] name = "internal-dns-types" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#e76962a5f67b8c5bf813ee24a7600634de708b35" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec600ca74e573a1926c1876e33b35" dependencies = [ "anyhow", "chrono", @@ -2952,6 +2941,7 @@ dependencies = [ "omicron-workspace-hack", "schemars", "serde", + "strum 0.27.2", ] [[package]] @@ -3128,7 +3118,7 @@ dependencies = [ [[package]] name = "kstat-macro" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/opte?rev=77f1ff9ceea1afe5733f0003f73f806b8c8c58ae#77f1ff9ceea1afe5733f0003f73f806b8c8c58ae" +source = "git+https://github.com/oxidecomputer/opte?rev=795a1e0aeefb7a2c6fe4139779fdf66930d09b80#795a1e0aeefb7a2c6fe4139779fdf66930d09b80" dependencies = [ "quote", "syn 2.0.106", @@ -3164,7 +3154,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#555fa6e1315a64f40c72716e4d168697c03795c6" +source = "git+https://github.com/oxidecomputer/dlpi-sys#0a0b98721c2b789767c7b54217e3cb8e702fcc38" [[package]] name = "libgit2-sys" @@ -3207,14 +3197,14 @@ checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" [[package]] name = "libnet" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/netadm-sys?branch=main#aeabd725ad7b4626042e536c9e3d452cd4a122bb" +source = "git+https://github.com/oxidecomputer/netadm-sys?branch=main#3eb1a6ad0b713660b367ce275e0a3896eabe19d4" dependencies = [ "anyhow", "cfg-if", "colored", "dlpi 0.2.0 (git+https://github.com/oxidecomputer/dlpi-sys)", "libc", - "num_enum 0.7.4", + "num_enum 0.7.5", "nvpair 0.5.0", "nvpair-sys", "oxnet", @@ -3223,7 +3213,7 @@ dependencies = [ "socket2 0.6.0", "thiserror 2.0.16", "tracing", - "winnow 0.7.13", + "winnow 0.7.14", ] [[package]] @@ -3582,9 +3572,9 @@ dependencies = [ [[package]] name = "newtype-uuid" -version = "1.3.1" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74d1216f62e63be5fb25a9ecd1e2b37b1556a9b8c02f4831770f5d01df85c226" +checksum = "5c012d14ef788ab066a347d19e3dda699916c92293b05b85ba2c76b8c82d2830" dependencies = [ "schemars", "serde", @@ -3618,15 +3608,13 @@ dependencies = [ [[package]] name = "nexus-client" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#e76962a5f67b8c5bf813ee24a7600634de708b35" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec600ca74e573a1926c1876e33b35" dependencies = [ "chrono", "futures", "iddqd", - "nexus-sled-agent-shared", "nexus-types", "omicron-common", - "omicron-passwords", "omicron-uuid-kinds", "omicron-workspace-hack", "oxnet", @@ -3643,12 +3631,11 @@ dependencies = [ [[package]] name = "nexus-sled-agent-shared" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#e76962a5f67b8c5bf813ee24a7600634de708b35" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec600ca74e573a1926c1876e33b35" dependencies = [ "camino", "chrono", "daft", - "id-map", "iddqd", "illumos-utils", "indent_write", @@ -3669,7 +3656,7 @@ dependencies = [ [[package]] name = "nexus-types" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#e76962a5f67b8c5bf813ee24a7600634de708b35" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec600ca74e573a1926c1876e33b35" dependencies = [ "anyhow", "api_identity", @@ -3684,12 +3671,12 @@ dependencies = [ "derive-where", "derive_more", "dropshot", + "ereport-types", "futures", "gateway-client", "gateway-types", "http", "humantime", - "id-map", "iddqd", "illumos-utils", "indent_write", @@ -3714,11 +3701,14 @@ dependencies = [ "serde", "serde_json", "serde_with", + "sled-hardware-types", "slog", "slog-error-chain", "steno", "strum 0.27.2", + "swrite", "tabled 0.15.0", + "test-strategy", "textwrap", "thiserror 2.0.16", "tokio", @@ -3869,11 +3859,11 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a973b4e44ce6cad84ce69d797acf9a044532e4184c4f267913d1b546a0727b7a" +checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" dependencies = [ - "num_enum_derive 0.7.4", + "num_enum_derive 0.7.5", "rustversion", ] @@ -3883,7 +3873,7 @@ version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799" dependencies = [ - "proc-macro-crate 1.3.1", + "proc-macro-crate", "proc-macro2", "quote", "syn 1.0.109", @@ -3891,11 +3881,11 @@ dependencies = [ [[package]] name = "num_enum_derive" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d" +checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" dependencies = [ - "proc-macro-crate 3.4.0", + "proc-macro-crate", "proc-macro2", "quote", "syn 2.0.106", @@ -3965,7 +3955,7 @@ dependencies = [ [[package]] name = "omicron-common" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#f41bd73be02538b43ab0c65585b703a1cb216d03" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec600ca74e573a1926c1876e33b35" dependencies = [ "anyhow", "api_identity", @@ -3978,7 +3968,6 @@ dependencies = [ "futures", "hex", "http", - "id-map", "iddqd", "ipnetwork", "macaddr", @@ -4010,7 +3999,7 @@ dependencies = [ [[package]] name = "omicron-passwords" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#e76962a5f67b8c5bf813ee24a7600634de708b35" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec600ca74e573a1926c1876e33b35" dependencies = [ "argon2", "omicron-workspace-hack", @@ -4025,7 +4014,7 @@ dependencies = [ [[package]] name = "omicron-uuid-kinds" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#f41bd73be02538b43ab0c65585b703a1cb216d03" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec600ca74e573a1926c1876e33b35" dependencies = [ "daft", "newtype-uuid", @@ -4146,7 +4135,7 @@ dependencies = [ [[package]] name = "opte" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/opte?rev=77f1ff9ceea1afe5733f0003f73f806b8c8c58ae#77f1ff9ceea1afe5733f0003f73f806b8c8c58ae" +source = "git+https://github.com/oxidecomputer/opte?rev=795a1e0aeefb7a2c6fe4139779fdf66930d09b80#795a1e0aeefb7a2c6fe4139779fdf66930d09b80" dependencies = [ "bitflags 2.9.4", "dyn-clone", @@ -4165,7 +4154,7 @@ dependencies = [ [[package]] name = "opte-api" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/opte?rev=77f1ff9ceea1afe5733f0003f73f806b8c8c58ae#77f1ff9ceea1afe5733f0003f73f806b8c8c58ae" +source = "git+https://github.com/oxidecomputer/opte?rev=795a1e0aeefb7a2c6fe4139779fdf66930d09b80#795a1e0aeefb7a2c6fe4139779fdf66930d09b80" dependencies = [ "illumos-sys-hdrs", "ingot", @@ -4178,7 +4167,7 @@ dependencies = [ [[package]] name = "opte-ioctl" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/opte?rev=77f1ff9ceea1afe5733f0003f73f806b8c8c58ae#77f1ff9ceea1afe5733f0003f73f806b8c8c58ae" +source = "git+https://github.com/oxidecomputer/opte?rev=795a1e0aeefb7a2c6fe4139779fdf66930d09b80#795a1e0aeefb7a2c6fe4139779fdf66930d09b80" dependencies = [ "libc", "libnet", @@ -4209,7 +4198,7 @@ dependencies = [ [[package]] name = "oxide-vpc" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/opte?rev=77f1ff9ceea1afe5733f0003f73f806b8c8c58ae#77f1ff9ceea1afe5733f0003f73f806b8c8c58ae" +source = "git+https://github.com/oxidecomputer/opte?rev=795a1e0aeefb7a2c6fe4139779fdf66930d09b80#795a1e0aeefb7a2c6fe4139779fdf66930d09b80" dependencies = [ "cfg-if", "illumos-sys-hdrs", @@ -4223,7 +4212,7 @@ dependencies = [ [[package]] name = "oximeter" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#f41bd73be02538b43ab0c65585b703a1cb216d03" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec600ca74e573a1926c1876e33b35" dependencies = [ "anyhow", "chrono", @@ -4242,7 +4231,7 @@ dependencies = [ [[package]] name = "oximeter-db" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#e76962a5f67b8c5bf813ee24a7600634de708b35" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec600ca74e573a1926c1876e33b35" dependencies = [ "anyhow", "async-recursion", @@ -4295,7 +4284,7 @@ dependencies = [ [[package]] name = "oximeter-instruments" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#e76962a5f67b8c5bf813ee24a7600634de708b35" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec600ca74e573a1926c1876e33b35" dependencies = [ "cfg-if", "chrono", @@ -4313,7 +4302,7 @@ dependencies = [ [[package]] name = "oximeter-macro-impl" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#f41bd73be02538b43ab0c65585b703a1cb216d03" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec600ca74e573a1926c1876e33b35" dependencies = [ "omicron-workspace-hack", "proc-macro2", @@ -4324,7 +4313,7 @@ dependencies = [ [[package]] name = "oximeter-producer" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#e76962a5f67b8c5bf813ee24a7600634de708b35" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec600ca74e573a1926c1876e33b35" dependencies = [ "chrono", "dropshot", @@ -4346,7 +4335,7 @@ dependencies = [ [[package]] name = "oximeter-schema" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#f41bd73be02538b43ab0c65585b703a1cb216d03" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec600ca74e573a1926c1876e33b35" dependencies = [ "anyhow", "chrono", @@ -4367,7 +4356,7 @@ dependencies = [ [[package]] name = "oximeter-timeseries-macro" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#f41bd73be02538b43ab0c65585b703a1cb216d03" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec600ca74e573a1926c1876e33b35" dependencies = [ "omicron-workspace-hack", "oximeter-schema", @@ -4380,7 +4369,7 @@ dependencies = [ [[package]] name = "oximeter-types" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#f41bd73be02538b43ab0c65585b703a1cb216d03" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec600ca74e573a1926c1876e33b35" dependencies = [ "bytes", "chrono", @@ -4400,7 +4389,7 @@ dependencies = [ [[package]] name = "oxlog" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#e76962a5f67b8c5bf813ee24a7600634de708b35" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec600ca74e573a1926c1876e33b35" dependencies = [ "anyhow", "camino", @@ -4429,7 +4418,7 @@ dependencies = [ [[package]] name = "oxql-types" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#e76962a5f67b8c5bf813ee24a7600634de708b35" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec600ca74e573a1926c1876e33b35" dependencies = [ "anyhow", "chrono", @@ -4834,15 +4823,6 @@ dependencies = [ "toml_edit 0.19.15", ] -[[package]] -name = "proc-macro-crate" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" -dependencies = [ - "toml_edit 0.23.6", -] - [[package]] name = "proc-macro-error" version = "1.0.4" @@ -6105,7 +6085,7 @@ checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" [[package]] name = "sled-hardware-types" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#e76962a5f67b8c5bf813ee24a7600634de708b35" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec600ca74e573a1926c1876e33b35" dependencies = [ "illumos-utils", "omicron-common", @@ -6997,7 +6977,7 @@ dependencies = [ "toml_datetime 0.7.2", "toml_parser", "toml_writer", - "winnow 0.7.13", + "winnow 0.7.14", ] [[package]] @@ -7042,19 +7022,7 @@ dependencies = [ "serde_spanned 0.6.9", "toml_datetime 0.6.11", "toml_write", - "winnow 0.7.13", -] - -[[package]] -name = "toml_edit" -version = "0.23.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3effe7c0e86fdff4f69cdd2ccc1b96f933e24811c5441d44904e8683e27184b" -dependencies = [ - "indexmap 2.11.4", - "toml_datetime 0.7.2", - "toml_parser", - "winnow 0.7.13", + "winnow 0.7.14", ] [[package]] @@ -7063,7 +7031,7 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cf893c33be71572e0e9aa6dd15e6677937abd686b066eac3f8cd3531688a627" dependencies = [ - "winnow 0.7.13", + "winnow 0.7.14", ] [[package]] @@ -7476,7 +7444,7 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "update-engine" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#e76962a5f67b8c5bf813ee24a7600634de708b35" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec600ca74e573a1926c1876e33b35" dependencies = [ "anyhow", "cancel-safe-futures", @@ -8298,9 +8266,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.7.13" +version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" dependencies = [ "memchr", ] diff --git a/Cargo.toml b/Cargo.toml index 5842737d..84e446e5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,6 +44,7 @@ internal-dns-resolver = { git = "https://github.com/oxidecomputer/omicron", bran internal-dns-types = { git = "https://github.com/oxidecomputer/omicron", branch = "main" } ispf = { git = "https://github.com/oxidecomputer/ispf" } gateway-client = { git = "https://github.com/oxidecomputer/omicron", branch = "main" } +gateway-types = { git = "https://github.com/oxidecomputer/omicron", branch = "main" } nexus-client = { git = "https://github.com/oxidecomputer/omicron", branch = "main" } omicron-common = { git = "https://github.com/oxidecomputer/omicron", branch= "main" } oximeter = { git = "https://github.com/oxidecomputer/omicron", branch = "main" } @@ -51,7 +52,6 @@ oximeter-producer = { git = "https://github.com/oxidecomputer/omicron", branch = 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"] } propolis = { git = "https://github.com/oxidecomputer/propolis" } -sled-agent-client = { git = "https://github.com/oxidecomputer/omicron", branch = "main" } smf = { git = "https://github.com/illumos/smf-rs" } softnpu-lib = { git = "https://github.com/oxidecomputer/softnpu" , package = "softnpu" , branch = "main"} tofino = { git = "https://github.com/oxidecomputer/tofino", branch = "main" } diff --git a/dpd/Cargo.toml b/dpd/Cargo.toml index 404081bd..eb9ee426 100644 --- a/dpd/Cargo.toml +++ b/dpd/Cargo.toml @@ -57,6 +57,7 @@ reqwest.workspace = true dropshot = { workspace = true, features = [ "usdt-probes" ] } gateway-client.workspace = true +gateway-types.workspace = true internal-dns-resolver.workspace = true internal-dns-types.workspace = true nexus-client.workspace = true diff --git a/dpd/src/switch_identifiers.rs b/dpd/src/switch_identifiers.rs index 1fcb7e01..04c0897d 100644 --- a/dpd/src/switch_identifiers.rs +++ b/dpd/src/switch_identifiers.rs @@ -69,13 +69,13 @@ pub(crate) async fn fetch_switch_identifiers_loop( BackoffError::transient(DisplayErrorChain::new(&e).to_string()) })? .into_inner(); - if type_ != gateway_client::types::SpType::Switch { + if type_ != gateway_types::component::SpType::Switch { return Err(BackoffError::transient(format!( "expected a switch SP, but found one of type: {type_:?}" ))); }; let sp = client - .sp_get(type_, slot) + .sp_get(&type_, slot) .await .map_err(|e| { BackoffError::transient(DisplayErrorChain::new(&e).to_string()) From 94e91ca52dfd521e9658249ebcc69f942ca234e7 Mon Sep 17 00:00:00 2001 From: Zeeshan Lakhani Date: Mon, 1 Dec 2025 03:37:22 +0000 Subject: [PATCH 02/22] [multicast] Narrow admin-scoped to admin-local only (ff04::/16) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, internal multicast groups accepted admin-scoped addresses including admin-local (ff04), site-local (ff05), and org-local (ff08). This narrows the scope to only admin-local (ff04::/16), which is what Omicron *now* dictates. - [ ] This should be merged after https://github.com/oxidecomputer/omicron/pull/9450 is reviewed and merged into Omicron. We now make Dendrite/Dpd match Omicron consistently for validation. Key changes: - Remove IPV6_SITE_LOCAL_PATTERN and IPV6_ORG_SCOPE_PATTERN from P4 - Update P4 table entries to only match admin-local (size 4→2) - Add ADMIN_LOCAL_PREFIX const to dpd-types with RFC doc links - Update validation to use `is_admin_local_multicast()` from oxnet v0.1.4 - Bump to API version 2 for doc changes (only) - Update README with OpenAPI generation instructions - Use new multicast subnet constants from `omicron-common` for validation --- Cargo.lock | 750 +- Cargo.toml | 10 +- README.md | 13 +- dpd-api/src/lib.rs | 27 +- dpd-client/Cargo.toml | 1 + dpd-client/tests/integration_tests/mcast.rs | 90 +- .../tests/integration_tests/table_tests.rs | 20 +- dpd-types/src/mcast.rs | 20 +- dpd/p4/constants.p4 | 2 - dpd/p4/sidecar.p4 | 11 +- dpd/src/mcast/mod.rs | 34 +- dpd/src/mcast/validate.rs | 84 +- dpd/src/table/mcast/mcast_route.rs | 8 +- openapi/dpd/dpd-2.0.0-4ba80a.json | 9646 +++++++++++++++++ openapi/dpd/dpd-latest.json | 2 +- 15 files changed, 10475 insertions(+), 243 deletions(-) create mode 100644 openapi/dpd/dpd-2.0.0-4ba80a.json diff --git a/Cargo.lock b/Cargo.lock index abe303a4..7ac714ce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7,7 +7,7 @@ name = "aal" version = "0.1.0" dependencies = [ "common 0.1.0", - "oximeter", + "oximeter 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "oxnet", "schemars", "serde", @@ -132,6 +132,17 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "api_identity" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#1c9d172568fdb149daaab76338574232d3703f2b" +dependencies = [ + "omicron-workspace-hack", + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "argon2" version = "0.5.3" @@ -176,7 +187,7 @@ dependencies = [ "dpd-api", "lazy_static", "libc", - "oximeter", + "oximeter 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "propolis", "rand 0.9.2", "schemars", @@ -759,7 +770,29 @@ dependencies = [ "derive_more", "expectorate", "itertools 0.14.0", - "omicron-common", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-workspace-hack", + "schemars", + "serde", + "serde_json", + "slog", +] + +[[package]] +name = "clickhouse-admin-types" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#1c9d172568fdb149daaab76338574232d3703f2b" +dependencies = [ + "anyhow", + "atomicwrites", + "camino", + "camino-tempfile", + "chrono", + "daft", + "derive_more", + "expectorate", + "itertools 0.14.0", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "omicron-workspace-hack", "schemars", "serde", @@ -808,7 +841,21 @@ source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec dependencies = [ "chrono", "csv", - "omicron-common", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-workspace-hack", + "schemars", + "serde", + "thiserror 2.0.16", +] + +[[package]] +name = "cockroach-admin-types" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#1c9d172568fdb149daaab76338574232d3703f2b" +dependencies = [ + "chrono", + "csv", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "omicron-workspace-hack", "schemars", "serde", @@ -836,7 +883,7 @@ version = "0.1.0" dependencies = [ "anyhow", "chrono", - "oximeter", + "oximeter 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "oxnet", "rand 0.9.2", "schemars", @@ -858,7 +905,7 @@ source = "git+https://github.com/oxidecomputer/dendrite?branch=main#606c0be888f4 dependencies = [ "anyhow", "chrono", - "oximeter", + "oximeter 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", "oxnet", "rand 0.9.2", "schemars", @@ -1440,17 +1487,17 @@ dependencies = [ "dropshot", "expectorate", "futures", - "gateway-client", - "gateway-types", - "internal-dns-resolver", - "internal-dns-types", + "gateway-client 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "gateway-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "internal-dns-resolver 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "internal-dns-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", "libc", "mockall", - "nexus-client", - "omicron-common", + "nexus-client 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "openssl", "oxide-tokio-rt", - "oximeter", + "oximeter 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "oximeter-producer", "oxnet", "rand 0.9.2", @@ -1501,6 +1548,7 @@ dependencies = [ "chrono", "common 0.1.0", "crc8", + "dpd-types", "futures", "http", "lazy_static", @@ -1556,7 +1604,7 @@ dependencies = [ "aal", "chrono", "common 0.1.0", - "omicron-common", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "oxnet", "schemars", "serde", @@ -1790,7 +1838,21 @@ version = "0.1.0" source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec600ca74e573a1926c1876e33b35" dependencies = [ "dropshot", - "omicron-uuid-kinds", + "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-workspace-hack", + "schemars", + "serde", + "serde_json", + "thiserror 2.0.16", +] + +[[package]] +name = "ereport-types" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#1c9d172568fdb149daaab76338574232d3703f2b" +dependencies = [ + "dropshot", + "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "omicron-workspace-hack", "schemars", "serde", @@ -2065,10 +2127,35 @@ dependencies = [ "base64 0.22.1", "chrono", "daft", - "ereport-types", + "ereport-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "gateway-messages", + "gateway-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-workspace-hack", + "progenitor 0.10.0", + "rand 0.9.2", + "reqwest", + "schemars", + "serde", + "serde_json", + "slog", + "thiserror 2.0.16", + "tokio", + "uuid", +] + +[[package]] +name = "gateway-client" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#1c9d172568fdb149daaab76338574232d3703f2b" +dependencies = [ + "base64 0.22.1", + "chrono", + "daft", + "ereport-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "gateway-messages", - "gateway-types", - "omicron-uuid-kinds", + "gateway-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "omicron-workspace-hack", "progenitor 0.10.0", "rand 0.9.2", @@ -2108,8 +2195,27 @@ dependencies = [ "dropshot", "gateway-messages", "hex", - "omicron-common", - "omicron-uuid-kinds", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-workspace-hack", + "schemars", + "serde", + "thiserror 2.0.16", + "tufaceous-artifact", + "uuid", +] + +[[package]] +name = "gateway-types" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#1c9d172568fdb149daaab76338574232d3703f2b" +dependencies = [ + "daft", + "dropshot", + "gateway-messages", + "hex", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "omicron-workspace-hack", "schemars", "serde", @@ -2816,12 +2922,52 @@ dependencies = [ "itertools 0.14.0", "libc", "macaddr", - "omicron-common", - "omicron-uuid-kinds", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", "omicron-workspace-hack", "opte-ioctl", "oxide-vpc", - "oxlog", + "oxlog 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "oxnet", + "schemars", + "serde", + "slog", + "slog-error-chain", + "smf 0.2.3", + "thiserror 2.0.16", + "tokio", + "uuid", + "whoami", + "zone", +] + +[[package]] +name = "illumos-utils" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#1c9d172568fdb149daaab76338574232d3703f2b" +dependencies = [ + "anyhow", + "async-trait", + "bhyve_api 0.0.0 (git+https://github.com/oxidecomputer/propolis?rev=3f1752e6cee9a2f8ecdce6e2ad3326781182e2d9)", + "byteorder", + "camino", + "camino-tempfile", + "cfg-if", + "crucible-smf", + "debug-ignore", + "dropshot", + "futures", + "http", + "ipnetwork", + "itertools 0.14.0", + "libc", + "macaddr", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "omicron-workspace-hack", + "opte-ioctl", + "oxide-vpc", + "oxlog 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "oxnet", "schemars", "serde", @@ -2919,9 +3065,27 @@ dependencies = [ "futures", "hickory-proto 0.25.2", "hickory-resolver 0.25.2", - "internal-dns-types", - "omicron-common", - "omicron-uuid-kinds", + "internal-dns-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-workspace-hack", + "qorb", + "reqwest", + "slog", + "thiserror 2.0.16", +] + +[[package]] +name = "internal-dns-resolver" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#1c9d172568fdb149daaab76338574232d3703f2b" +dependencies = [ + "futures", + "hickory-proto 0.25.2", + "hickory-resolver 0.25.2", + "internal-dns-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "omicron-workspace-hack", "qorb", "reqwest", @@ -2936,8 +3100,23 @@ source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec dependencies = [ "anyhow", "chrono", - "omicron-common", - "omicron-uuid-kinds", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-workspace-hack", + "schemars", + "serde", + "strum 0.27.2", +] + +[[package]] +name = "internal-dns-types" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#1c9d172568fdb149daaab76338574232d3703f2b" +dependencies = [ + "anyhow", + "chrono", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "omicron-workspace-hack", "schemars", "serde", @@ -3613,9 +3792,32 @@ dependencies = [ "chrono", "futures", "iddqd", - "nexus-types", - "omicron-common", - "omicron-uuid-kinds", + "nexus-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-workspace-hack", + "oxnet", + "progenitor 0.10.0", + "regress", + "reqwest", + "schemars", + "serde", + "serde_json", + "slog", + "uuid", +] + +[[package]] +name = "nexus-client" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#1c9d172568fdb149daaab76338574232d3703f2b" +dependencies = [ + "chrono", + "futures", + "iddqd", + "nexus-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "omicron-workspace-hack", "oxnet", "progenitor 0.10.0", @@ -3637,16 +3839,41 @@ dependencies = [ "chrono", "daft", "iddqd", - "illumos-utils", + "illumos-utils 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", "indent_write", - "omicron-common", - "omicron-passwords", - "omicron-uuid-kinds", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-passwords 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", "omicron-workspace-hack", "schemars", "serde", "serde_json", - "sled-hardware-types", + "sled-hardware-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "strum 0.27.2", + "thiserror 2.0.16", + "tufaceous-artifact", + "uuid", +] + +[[package]] +name = "nexus-sled-agent-shared" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#1c9d172568fdb149daaab76338574232d3703f2b" +dependencies = [ + "camino", + "chrono", + "daft", + "iddqd", + "illumos-utils 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "indent_write", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "omicron-passwords 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "omicron-workspace-hack", + "schemars", + "serde", + "serde_json", + "sled-hardware-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "strum 0.27.2", "thiserror 2.0.16", "tufaceous-artifact", @@ -3659,41 +3886,108 @@ version = "0.1.0" source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec600ca74e573a1926c1876e33b35" dependencies = [ "anyhow", - "api_identity", + "api_identity 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "async-trait", + "base64 0.22.1", + "chrono", + "clap", + "clickhouse-admin-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "cockroach-admin-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "cookie", + "daft", + "derive-where", + "derive_more", + "dropshot", + "ereport-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "futures", + "gateway-client 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "gateway-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "http", + "humantime", + "iddqd", + "illumos-utils 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "indent_write", + "internal-dns-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "ipnetwork", + "itertools 0.14.0", + "newtype-uuid", + "newtype_derive", + "nexus-sled-agent-shared 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-passwords 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-workspace-hack", + "openssl", + "oximeter-db 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "oxnet", + "oxql-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "parse-display", + "regex", + "schemars", + "semver 1.0.27", + "serde", + "serde_json", + "serde_with", + "sled-hardware-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "slog", + "slog-error-chain", + "steno", + "strum 0.27.2", + "swrite", + "tabled 0.15.0", + "test-strategy", + "textwrap", + "thiserror 2.0.16", + "tokio", + "tough", + "tufaceous-artifact", + "unicode-width 0.1.14", + "update-engine 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "url", + "uuid", +] + +[[package]] +name = "nexus-types" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#1c9d172568fdb149daaab76338574232d3703f2b" +dependencies = [ + "anyhow", + "api_identity 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "async-trait", "base64 0.22.1", "chrono", "clap", - "clickhouse-admin-types", - "cockroach-admin-types", + "clickhouse-admin-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "cockroach-admin-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "cookie", "daft", "derive-where", "derive_more", "dropshot", - "ereport-types", + "ereport-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "futures", - "gateway-client", - "gateway-types", + "gateway-client 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "gateway-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "http", "humantime", "iddqd", - "illumos-utils", + "illumos-utils 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "indent_write", - "internal-dns-types", + "internal-dns-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "ipnetwork", "itertools 0.14.0", "newtype-uuid", "newtype_derive", - "nexus-sled-agent-shared", - "omicron-common", - "omicron-passwords", - "omicron-uuid-kinds", + "nexus-sled-agent-shared 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "omicron-passwords 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "omicron-workspace-hack", "openssl", - "oximeter-db", + "oximeter-db 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "oxnet", - "oxql-types", + "oxql-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "parse-display", "regex", "schemars", @@ -3701,7 +3995,7 @@ dependencies = [ "serde", "serde_json", "serde_with", - "sled-hardware-types", + "sled-hardware-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "slog", "slog-error-chain", "steno", @@ -3715,7 +4009,7 @@ dependencies = [ "tough", "tufaceous-artifact", "unicode-width 0.1.14", - "update-engine", + "update-engine 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "url", "uuid", ] @@ -3958,7 +4252,7 @@ version = "0.1.0" source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec600ca74e573a1926c1876e33b35" dependencies = [ "anyhow", - "api_identity", + "api_identity 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", "async-trait", "backoff", "camino", @@ -3972,7 +4266,51 @@ dependencies = [ "ipnetwork", "macaddr", "mg-admin-client", - "omicron-uuid-kinds", + "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-workspace-hack", + "oxnet", + "parse-display", + "progenitor-client 0.10.0", + "protocol", + "rand 0.9.2", + "regress", + "reqwest", + "schemars", + "semver 1.0.27", + "serde", + "serde_human_bytes", + "serde_json", + "serde_with", + "slog", + "slog-error-chain", + "strum 0.27.2", + "thiserror 2.0.16", + "tokio", + "tufaceous-artifact", + "uuid", +] + +[[package]] +name = "omicron-common" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#1c9d172568fdb149daaab76338574232d3703f2b" +dependencies = [ + "anyhow", + "api_identity 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "async-trait", + "backoff", + "camino", + "chrono", + "daft", + "dropshot", + "futures", + "hex", + "http", + "iddqd", + "ipnetwork", + "macaddr", + "mg-admin-client", + "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "omicron-workspace-hack", "oxnet", "parse-display", @@ -4011,6 +4349,21 @@ dependencies = [ "thiserror 2.0.16", ] +[[package]] +name = "omicron-passwords" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#1c9d172568fdb149daaab76338574232d3703f2b" +dependencies = [ + "argon2", + "omicron-workspace-hack", + "rand 0.9.2", + "schemars", + "secrecy", + "serde", + "serde_with", + "thiserror 2.0.16", +] + [[package]] name = "omicron-uuid-kinds" version = "0.1.0" @@ -4023,6 +4376,18 @@ dependencies = [ "schemars", ] +[[package]] +name = "omicron-uuid-kinds" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#1c9d172568fdb149daaab76338574232d3703f2b" +dependencies = [ + "daft", + "newtype-uuid", + "newtype-uuid-macros", + "paste", + "schemars", +] + [[package]] name = "omicron-workspace-hack" version = "0.1.0" @@ -4218,10 +4583,29 @@ dependencies = [ "chrono", "clap", "omicron-workspace-hack", - "oximeter-macro-impl", - "oximeter-schema", - "oximeter-timeseries-macro", - "oximeter-types", + "oximeter-macro-impl 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "oximeter-schema 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "oximeter-timeseries-macro 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "oximeter-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "prettyplease", + "syn 2.0.106", + "toml 0.8.23", + "uuid", +] + +[[package]] +name = "oximeter" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#1c9d172568fdb149daaab76338574232d3703f2b" +dependencies = [ + "anyhow", + "chrono", + "clap", + "omicron-workspace-hack", + "oximeter-macro-impl 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "oximeter-schema 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "oximeter-timeseries-macro 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "oximeter-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "prettyplease", "syn 2.0.106", "toml 0.8.23", @@ -4254,11 +4638,64 @@ dependencies = [ "libc", "nom", "num", - "omicron-common", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-workspace-hack", + "oxide-tokio-rt", + "oximeter 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "oxql-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "parse-display", + "qorb", + "quote", + "regex", + "reqwest", + "schemars", + "serde", + "serde_json", + "slog", + "slog-async", + "slog-dtrace", + "slog-error-chain", + "slog-term", + "strum 0.27.2", + "termtree", + "thiserror 2.0.16", + "tokio", + "tokio-util", + "usdt 0.5.0", + "uuid", +] + +[[package]] +name = "oximeter-db" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#1c9d172568fdb149daaab76338574232d3703f2b" +dependencies = [ + "anyhow", + "async-recursion", + "async-trait", + "bcs", + "bytes", + "camino", + "chrono", + "chrono-tz", + "clap", + "clickward", + "const_format", + "debug-ignore", + "dropshot", + "futures", + "gethostname", + "highway", + "iana-time-zone", + "indexmap 2.11.4", + "libc", + "nom", + "num", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "omicron-workspace-hack", "oxide-tokio-rt", - "oximeter", - "oxql-types", + "oximeter 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "oxql-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "parse-display", "qorb", "quote", @@ -4284,7 +4721,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=zl%2Fmcast-implicit-lifecycle#1c9d172568fdb149daaab76338574232d3703f2b" dependencies = [ "cfg-if", "chrono", @@ -4292,7 +4729,7 @@ dependencies = [ "kstat-rs", "libc", "omicron-workspace-hack", - "oximeter", + "oximeter 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "slog", "thiserror 2.0.16", "tokio", @@ -4310,19 +4747,30 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "oximeter-macro-impl" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#1c9d172568fdb149daaab76338574232d3703f2b" +dependencies = [ + "omicron-workspace-hack", + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[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=zl%2Fmcast-implicit-lifecycle#1c9d172568fdb149daaab76338574232d3703f2b" dependencies = [ "chrono", "dropshot", - "internal-dns-resolver", - "internal-dns-types", - "nexus-client", - "omicron-common", + "internal-dns-resolver 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "internal-dns-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "nexus-client 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "omicron-workspace-hack", - "oximeter", + "oximeter 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "schemars", "serde", "slog", @@ -4342,7 +4790,28 @@ dependencies = [ "clap", "heck 0.5.0", "omicron-workspace-hack", - "oximeter-types", + "oximeter-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "prettyplease", + "proc-macro2", + "quote", + "schemars", + "serde", + "slog-error-chain", + "syn 2.0.106", + "toml 0.8.23", +] + +[[package]] +name = "oximeter-schema" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#1c9d172568fdb149daaab76338574232d3703f2b" +dependencies = [ + "anyhow", + "chrono", + "clap", + "heck 0.5.0", + "omicron-workspace-hack", + "oximeter-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "prettyplease", "proc-macro2", "quote", @@ -4359,8 +4828,21 @@ version = "0.1.0" source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec600ca74e573a1926c1876e33b35" dependencies = [ "omicron-workspace-hack", - "oximeter-schema", - "oximeter-types", + "oximeter-schema 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "oximeter-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "oximeter-timeseries-macro" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#1c9d172568fdb149daaab76338574232d3703f2b" +dependencies = [ + "omicron-workspace-hack", + "oximeter-schema 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "oximeter-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "proc-macro2", "quote", "syn 2.0.106", @@ -4375,7 +4857,27 @@ dependencies = [ "chrono", "float-ord", "num", - "omicron-common", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-workspace-hack", + "parse-display", + "regex", + "schemars", + "serde", + "strum 0.27.2", + "thiserror 2.0.16", + "uuid", +] + +[[package]] +name = "oximeter-types" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#1c9d172568fdb149daaab76338574232d3703f2b" +dependencies = [ + "bytes", + "chrono", + "float-ord", + "num", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "omicron-workspace-hack", "parse-display", "regex", @@ -4403,11 +4905,28 @@ dependencies = [ "uuid", ] +[[package]] +name = "oxlog" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#1c9d172568fdb149daaab76338574232d3703f2b" +dependencies = [ + "anyhow", + "camino", + "chrono", + "clap", + "glob", + "jiff", + "omicron-workspace-hack", + "rayon", + "sigpipe", + "uuid", +] + [[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", @@ -4425,7 +4944,24 @@ dependencies = [ "highway", "num", "omicron-workspace-hack", - "oximeter-types", + "oximeter-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "schemars", + "serde", + "serde_json", + "uuid", +] + +[[package]] +name = "oxql-types" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#1c9d172568fdb149daaab76338574232d3703f2b" +dependencies = [ + "anyhow", + "chrono", + "highway", + "num", + "omicron-workspace-hack", + "oximeter-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "schemars", "serde", "serde_json", @@ -5791,9 +6327,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", @@ -5819,18 +6355,18 @@ 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", @@ -6087,8 +6623,20 @@ name = "sled-hardware-types" version = "0.1.0" source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec600ca74e573a1926c1876e33b35" dependencies = [ - "illumos-utils", - "omicron-common", + "illumos-utils 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-workspace-hack", + "schemars", + "serde", +] + +[[package]] +name = "sled-hardware-types" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#1c9d172568fdb149daaab76338574232d3703f2b" +dependencies = [ + "illumos-utils 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "omicron-workspace-hack", "schemars", "serde", @@ -6684,9 +7232,9 @@ dependencies = [ "kstat-rs", "libc", "lldpd-client", - "omicron-common", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "oxide-tokio-rt", - "oximeter", + "oximeter 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "oximeter-instruments", "oximeter-producer", "oxnet", @@ -7471,6 +8019,36 @@ dependencies = [ "uuid", ] +[[package]] +name = "update-engine" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#1c9d172568fdb149daaab76338574232d3703f2b" +dependencies = [ + "anyhow", + "cancel-safe-futures", + "chrono", + "debug-ignore", + "derive-where", + "either", + "futures", + "indent_write", + "indexmap 2.11.4", + "libsw", + "linear-map", + "omicron-workspace-hack", + "owo-colors", + "petgraph 0.8.2", + "schemars", + "serde", + "serde_json", + "serde_with", + "slog", + "swrite", + "tokio", + "unicode-width 0.1.14", + "uuid", +] + [[package]] name = "uplinkd" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 84e446e5..154933a0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,11 +46,11 @@ ispf = { git = "https://github.com/oxidecomputer/ispf" } gateway-client = { git = "https://github.com/oxidecomputer/omicron", branch = "main" } gateway-types = { git = "https://github.com/oxidecomputer/omicron", branch = "main" } nexus-client = { git = "https://github.com/oxidecomputer/omicron", branch = "main" } -omicron-common = { git = "https://github.com/oxidecomputer/omicron", branch= "main" } -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"] } +omicron-common = { git = "https://github.com/oxidecomputer/omicron", branch= "zl/mcast-implicit-lifecycle" } +oximeter = { git = "https://github.com/oxidecomputer/omicron", branch = "zl/mcast-implicit-lifecycle" } +oximeter-producer = { git = "https://github.com/oxidecomputer/omicron", branch = "zl/mcast-implicit-lifecycle" } +oximeter-instruments = { git = "https://github.com/oxidecomputer/omicron", branch = "zl/mcast-implicit-lifecycle", default-features = false, features = ["kstat"] } +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"} diff --git a/README.md b/README.md index 26c80885..98b8fa1e 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 e2d9a31d..0b0a80be 100644 --- a/dpd-api/src/lib.rs +++ b/dpd-api/src/lib.rs @@ -56,6 +56,7 @@ api_versions!([ // | example for the next person. // v // (next_int, IDENT), + (2, MCAST_DOCS_ADMIN_LOCAL), (1, INITIAL), ]); @@ -1431,7 +1432,7 @@ 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. */ @@ -1450,9 +1451,10 @@ pub trait DpdApi { /** * 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, @@ -1502,10 +1504,10 @@ pub trait DpdApi { ) -> Result, HttpError>; /** - * Get an underlay (internal) multicast group configuration by admin-scoped + * Get an underlay (internal) multicast group configuration by admin-local * IPv6 address. * - * 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 { @@ -1521,8 +1523,8 @@ pub trait DpdApi { * Update an underlay (internal) multicast group configuration for a given * group IP address. * - * 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. */ #[endpoint { method = PUT, @@ -1537,7 +1539,7 @@ pub trait DpdApi { /** * 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. */ #[endpoint { @@ -2270,8 +2272,11 @@ pub struct MulticastGroupIpParam { pub group_ip: IpAddr, } -/// Used to identify an underlay (internal) multicast group by admin-scoped IPv6 -/// address. +/// 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, diff --git a/dpd-client/Cargo.toml b/dpd-client/Cargo.toml index 9add026b..1cba04cf 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/mcast.rs b/dpd-client/tests/integration_tests/mcast.rs index 10f0e9d5..abfe8e96 100644 --- a/dpd-client/tests/integration_tests/mcast.rs +++ b/dpd-client/tests/integration_tests/mcast.rs @@ -15,6 +15,7 @@ use crate::integration_tests::common::prelude::*; use ::common::network::MacAddr; use anyhow::anyhow; use dpd_client::{Error, types}; +use dpd_types::mcast::ADMIN_LOCAL_PREFIX; use futures::TryStreamExt; use oxnet::{Ipv4Net, MulticastMac}; use packet::{Endpoint, eth, geneve, ipv4, ipv6, udp}; @@ -25,7 +26,8 @@ const MULTICAST_TEST_IPV6: Ipv6Addr = 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); @@ -121,12 +123,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::AdminScopedIpv6(ipv6); let internal_entry = types::MulticastGroupCreateUnderlayEntry { - group_ip: admin_scoped_ip, + group_ip: admin_local_ip, tag: tag.map(String::from), members, }; @@ -146,7 +148,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), @@ -1154,9 +1156,9 @@ 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(), + group_ip: "ff04::200".parse().unwrap(), tag: Some("test_underlay_only".to_string()), members: vec![types::MulticastGroupMember { port_id: port_id.clone(), @@ -1169,7 +1171,7 @@ 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 @@ -1186,10 +1188,10 @@ 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(), + group_ip: "ff04::300".parse().unwrap(), tag: Some("test_external_members_only".to_string()), members: vec![types::MulticastGroupMember { port_id: port_id.clone(), @@ -1292,10 +1294,10 @@ 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 multicast address")); } _ => panic!( - "Expected ErrorResponse for admin-scoped external group creation" + "Expected ErrorResponse for admin-local external group creation" ), } @@ -3538,8 +3540,8 @@ 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), @@ -4249,7 +4251,7 @@ 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()), @@ -4265,11 +4267,11 @@ 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()), @@ -4285,8 +4287,8 @@ 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 @@ -4353,14 +4355,12 @@ 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 multicast address"), + "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(); switch @@ -4368,16 +4368,6 @@ async fn test_ipv6_multicast_scope_validation() { .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()) - .await - .ok(); switch .client .multicast_group_delete(&target_group.group_ip.to_ip_addr()) @@ -4391,9 +4381,12 @@ 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( @@ -4486,7 +4479,8 @@ 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, @@ -4515,7 +4509,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)); @@ -4878,7 +4872,7 @@ 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) @@ -5244,7 +5238,7 @@ 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 @@ -5398,7 +5392,7 @@ 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( @@ -5478,7 +5472,7 @@ 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 @@ -5621,7 +5615,7 @@ 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) @@ -5857,7 +5851,7 @@ 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( @@ -5956,7 +5950,7 @@ 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 @@ -6091,7 +6085,7 @@ 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( diff --git a/dpd-client/tests/integration_tests/table_tests.rs b/dpd-client/tests/integration_tests/table_tests.rs index 88eb7a8e..3491f369 100644 --- a/dpd-client/tests/integration_tests/table_tests.rs +++ b/dpd-client/tests/integration_tests/table_tests.rs @@ -18,6 +18,7 @@ use crate::integration_tests::common::prelude::*; use dpd_client::ClientInfo; use dpd_client::ResponseValue; use dpd_client::types; +use dpd_types::mcast::ADMIN_LOCAL_PREFIX; // The expected sizes of each table. The values are copied from constants.p4. // @@ -46,7 +47,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 +74,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 +291,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,10 +465,10 @@ 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), tag: Some(MCAST_TAG.to_string()), diff --git a/dpd-types/src/mcast.rs b/dpd-types/src/mcast.rs index cf90b5f4..706ce91d 100644 --- a/dpd-types/src/mcast.rs +++ b/dpd-types/src/mcast.rs @@ -21,9 +21,17 @@ use crate::link::LinkId; /// Type alias for multicast group IDs. pub type MulticastGroupId = u16; -/// A validated admin-scoped IPv6 multicast address. +/// Admin-local IPv6 multicast prefix (ff04::/16, scope 4). /// -/// Admin-scoped addresses are ff04::/16, ff05::/16, or ff08::/16. +/// 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 +pub const ADMIN_LOCAL_PREFIX: u16 = 0xFF04; + +/// A validated admin-local IPv6 multicast address. +/// +/// Admin-local addresses are ff04::/16 (scope 4). /// These are used for internal/underlay multicast groups. #[derive( Clone, @@ -42,9 +50,9 @@ pub type MulticastGroupId = u16; pub struct AdminScopedIpv6(Ipv6Addr); impl AdminScopedIpv6 { - /// Create a new AdminScopedIpv6 if the address is admin-scoped. + /// Create a new AdminScopedIpv6 if the address is admin-local. pub fn new(addr: Ipv6Addr) -> Result { - if !Ipv6Net::new_unchecked(addr, 128).is_admin_scoped_multicast() { + if !Ipv6Net::new_unchecked(addr, 128).is_admin_local_multicast() { return Err(Error::InvalidIp(addr)); } Ok(Self(addr)) @@ -213,8 +221,6 @@ 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)" - )] + #[error("Address {0} is not admin-local (must be ff04::/16)")] InvalidIp(Ipv6Addr), } diff --git a/dpd/p4/constants.p4 b/dpd/p4/constants.p4 index 860d9951..2696c7f1 100644 --- a/dpd/p4/constants.p4 +++ b/dpd/p4/constants.p4 @@ -60,8 +60,6 @@ const bit<128> IPV6_ULA_MASK = 0xff000000000000000000000000000000; // Match /* 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 /* Reasons a packet may be dropped by the p4 pipeline */ diff --git a/dpd/p4/sidecar.p4 b/dpd/p4/sidecar.p4 index 7c723632..53e0acf8 100644 --- a/dpd/p4/sidecar.p4 +++ b/dpd/p4/sidecar.p4 @@ -1813,21 +1813,16 @@ control MulticastEgress ( const entries = { // Admin-local (scope value 4): Matches IPv6 multicast addresses - // with scope ff04::/16 + // with scope ff04::/16. This is the only multicast scope used for + // internal/underlay traffic (RFC 7346, RFC 4291). ( 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; } - const size = 4; + const size = 2; } table tbl_decap_ports { diff --git a/dpd/src/mcast/mod.rs b/dpd/src/mcast/mod.rs index 801b1799..6db806c9 100644 --- a/dpd/src/mcast/mod.rs +++ b/dpd/src/mcast/mod.rs @@ -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}, @@ -86,7 +90,7 @@ mod validate; use rollback::{GroupCreateRollbackContext, GroupUpdateRollbackContext}; use validate::{ validate_multicast_address, validate_nat_target, - validate_not_admin_scoped_ipv6, + validate_not_admin_local_ipv6, }; #[derive(Debug)] @@ -220,8 +224,8 @@ 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) + /// 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, } @@ -263,7 +267,7 @@ 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, @@ -364,8 +368,8 @@ 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)?; + // Validate the admin-local IP early to avoid partial state + let admin_local_ip = AdminScopedIpv6::new(nat_target.internal_ip)?; let group = MulticastGroup { external_scoped_group: scoped_external_id, @@ -380,7 +384,7 @@ pub(crate) fn add_group_external( }; 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)) } @@ -521,13 +525,13 @@ pub(crate) fn del_group(s: &Switch, group_ip: IpAddr) -> DpdResult<()> { 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: AdminScopedIpv6, ) -> DpdResult { let mcast = s.mcast.lock().unwrap(); - let group_ip = IpAddr::V6(admin_scoped.into()); + let group_ip = IpAddr::V6(admin_local.into()); let group = mcast.groups.get(&group_ip).ok_or_else(|| { DpdError::Missing(format!( @@ -535,7 +539,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. @@ -1099,7 +1103,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_admin_local_ipv6(group_ip)?; Ok(()) } diff --git a/dpd/src/mcast/validate.rs b/dpd/src/mcast/validate.rs index f0394698..a0f430f6 100644 --- a/dpd/src/mcast/validate.rs +++ b/dpd/src/mcast/validate.rs @@ -4,9 +4,21 @@ // // Copyright 2025 Oxide Computer Company +//! Multicast address validation. +//! +//! Reserved multicast addresses are defined by IANA: +//! . + use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; use common::nat::NatTarget; +use omicron_common::address::{ + IPV4_ADMIN_SCOPED_MULTICAST_SUBNET, IPV4_GLOP_MULTICAST_SUBNET, + IPV4_LINK_LOCAL_MULTICAST_SUBNET, IPV4_SPECIFIC_RESERVED_MULTICAST_ADDRS, + IPV4_SSM_SUBNET, IPV6_INTERFACE_LOCAL_MULTICAST_SUBNET, + IPV6_LINK_LOCAL_MULTICAST_SUBNET, IPV6_RESERVED_SCOPE_MULTICAST_SUBNET, + IPV6_SSM_SUBNET, +}; use oxnet::{Ipv4Net, Ipv6Net}; use super::IpSrc; @@ -46,10 +58,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 !internal_nat_ip.is_admin_local_multicast() { return Err(DpdError::Invalid(format!( "NAT target internal IP address {} is not a valid \ - site/admin-local or org-scoped multicast address", + admin-local multicast address (must be ff04::/16)", nat_target.internal_ip ))); } @@ -60,16 +72,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), } } @@ -102,17 +106,13 @@ fn validate_ipv4_multicast( ))); } - // Define reserved IPv4 multicast subnets + // Check reserved 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) + IPV4_LINK_LOCAL_MULTICAST_SUBNET, + IPV4_GLOP_MULTICAST_SUBNET, + IPV4_ADMIN_SCOPED_MULTICAST_SUBNET, ]; - // Check reserved subnets for subnet in &reserved_subnets { if subnet.contains(addr) { return Err(DpdError::Invalid(format!( @@ -121,14 +121,8 @@ fn validate_ipv4_multicast( } } - // 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) { + // Check specific reserved addresses + if IPV4_SPECIFIC_RESERVED_MULTICAST_ADDRS.contains(&addr) { return Err(DpdError::Invalid(format!( "{addr} is a specifically reserved multicast address", ))); @@ -165,17 +159,13 @@ fn validate_ipv6_multicast( ))); } - // 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,13 +177,13 @@ 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 admin-local for external group creation. +pub(crate) fn validate_not_admin_local_ipv6(addr: IpAddr) -> DpdResult<()> { if let IpAddr::V6(ipv6) = addr - && oxnet::Ipv6Net::new_unchecked(ipv6, 128).is_admin_scoped_multicast() + && oxnet::Ipv6Net::new_unchecked(ipv6, 128).is_admin_local_multicast() { return Err(DpdError::Invalid(format!( - "{addr} is an admin-scoped multicast address and \ + "{addr} is an admin-local multicast address and \ must be created via the internal multicast API", ))); } @@ -289,6 +279,7 @@ fn validate_ipv4_source_subnet(subnet: Ipv4Net) -> DpdResult<()> { mod tests { use super::*; use common::{nat::Vni, network::MacAddr}; + use dpd_types::mcast::ADMIN_LOCAL_PREFIX; use oxnet::Ipv4Net; use std::str::FromStr; @@ -590,8 +581,17 @@ mod tests { 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), + // admin-local multicast (ff04::/16) + internal_ip: Ipv6Addr::new( + ADMIN_LOCAL_PREFIX, + 0, + 0, + 0, + 0, + 0, + 0, + 0x1234, + ), // Multicast MAC inner_mac: MacAddr::new(0x01, 0x00, 0x5e, 0x00, 0x00, 0x01), vni: Vni::new(100).unwrap(), diff --git a/dpd/src/table/mcast/mcast_route.rs b/dpd/src/table/mcast/mcast_route.rs index 8fcf7895..43492129 100644 --- a/dpd/src/table/mcast/mcast_route.rs +++ b/dpd/src/table/mcast/mcast_route.rs @@ -126,9 +126,9 @@ pub(crate) fn add_ipv6_entry( 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 + // Admin-local 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() + let action_data: Ipv6Action = if internal_ip.is_admin_local_multicast() || internal_ip.is_unique_local() { Ipv6Action::Forward @@ -159,9 +159,9 @@ pub(crate) fn update_ipv6_entry( 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 + // Admin-local 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() + let action_data: Ipv6Action = if internal_ip.is_admin_local_multicast() || internal_ip.is_unique_local() { Ipv6Action::Forward diff --git a/openapi/dpd/dpd-2.0.0-4ba80a.json b/openapi/dpd/dpd-2.0.0-4ba80a.json new file mode 100644 index 00000000..63df87ff --- /dev/null +++ b/openapi/dpd/dpd-2.0.0-4ba80a.json @@ -0,0 +1,9646 @@ +{ + "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": "2.0.0" + }, + "paths": { + "/all-settings": { + "delete": { + "summary": "Clear all settings.", + "description": "This removes all data entirely.", + "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:\n\n- All ARP or NDP table entries. - All routes - All links on all switch ports", + "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 crated 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.", + "operationId": "multicast_group_update_external", + "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.", + "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.", + "operationId": "multicast_group_delete", + "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.", + "operationId": "multicast_groups_list_by_tag", + "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.", + "operationId": "multicast_reset_by_tag", + "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.", + "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 by admin-local", + "description": "IPv6 address.\n\nUnderlay 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/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 for a given", + "description": "group IP address.\n\nUnderlay groups are used for admin-local IPv6 multicast traffic (ff04::/16) that requires replication infrastructure with external and underlay members.", + "operationId": "multicast_group_update_underlay", + "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", + "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/ipv4/gen": { + "get": { + "summary": "Get NATv4 generation number", + "operationId": "ipv4_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/ipv4/trigger": { + "post": { + "summary": "Trigger NATv4 Reconciliation", + "operationId": "ipv4_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.\n\nAdmin-local addresses are ff04::/16 (scope 4). These are used for internal/underlay multicast groups.", + "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.", + "oneOf": [ + { + "description": "Exact match for the source IP address.", + "type": "object", + "properties": { + "Exact": { + "type": "string", + "format": "ip" + } + }, + "required": [ + "Exact" + ], + "additionalProperties": false + }, + { + "description": "Subnet match for the source IP address.", + "type": "object", + "properties": { + "Subnet": { + "$ref": "#/components/schemas/Ipv4Net" + } + }, + "required": [ + "Subnet" + ], + "additionalProperties": false + } + ] + }, + "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.", + "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, + "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/AdminScopedIpv6" + }, + "members": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MulticastGroupMember" + } + }, + "tag": { + "nullable": true, + "type": "string" + } + }, + "required": [ + "group_ip", + "members" + ] + }, + "MulticastGroupExternalResponse": { + "description": "Response structure for external multicast group operations. These groups handle IPv4 and non-admin 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": { + "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.", + "oneOf": [ + { + "description": "Response structure for underlay/internal multicast group operations. These groups handle admin-scoped IPv6 multicast with full replication.", + "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. These groups handle IPv4 and non-admin 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": { + "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. These groups handle admin-scoped IPv6 multicast with full replication.", + "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 (to the rack) groups.", + "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, + "type": "string" + } + }, + "required": [ + "external_forwarding", + "internal_forwarding" + ] + }, + "MulticastGroupUpdateUnderlayEntry": { + "description": "Represents a multicast replication entry for PUT requests for internal (to the rack) groups.", + "type": "object", + "properties": { + "members": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MulticastGroupMember" + } + }, + "tag": { + "nullable": true, + "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" + ] + }, + "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-latest.json b/openapi/dpd/dpd-latest.json index a3945e55..437c9096 120000 --- a/openapi/dpd/dpd-latest.json +++ b/openapi/dpd/dpd-latest.json @@ -1 +1 @@ -dpd-1.0.0-40fbc6.json \ No newline at end of file +dpd-2.0.0-4ba80a.json \ No newline at end of file From 622fe6266cf69adbb5b5d14af1046629cffa25c9 Mon Sep 17 00:00:00 2001 From: Zeeshan Lakhani Date: Thu, 11 Dec 2025 05:35:37 +0000 Subject: [PATCH 03/22] [merge] fixes --- Cargo.lock | 1241 +++++++++++++++++++++++++++---------- dpd/src/mcast/validate.rs | 27 +- 2 files changed, 907 insertions(+), 361 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index abe303a4..c5e6407f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7,7 +7,7 @@ name = "aal" version = "0.1.0" dependencies = [ "common 0.1.0", - "oximeter", + "oximeter 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "oxnet", "schemars", "serde", @@ -26,15 +26,6 @@ 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" @@ -129,7 +120,18 @@ dependencies = [ "omicron-workspace-hack", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", +] + +[[package]] +name = "api_identity" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#07a36a1dabacaec9ebfe9fa48ab51adf99a67278" +dependencies = [ + "omicron-workspace-hack", + "proc-macro2", + "quote", + "syn 2.0.111", ] [[package]] @@ -176,7 +178,7 @@ dependencies = [ "dpd-api", "lazy_static", "libc", - "oximeter", + "oximeter 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "propolis", "rand 0.9.2", "schemars", @@ -189,7 +191,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 +205,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -225,7 +227,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -236,7 +238,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -347,21 +349,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "backtrace" -version = "0.3.75" -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", -] - [[package]] name = "base64" version = "0.21.7" @@ -445,7 +432,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -582,9 +569,9 @@ dependencies = [ [[package]] name = "camino" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1de8bc0aa9e9385ceb3bf0c152e3a9b9544f6c4a912c8ae504e80c1f0368603" +checksum = "276a59bf2b2c967788139340c9f0c5b12d7fd6630315c15c217e559de85d2609" dependencies = [ "serde_core", ] @@ -736,7 +723,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -759,7 +746,29 @@ dependencies = [ "derive_more", "expectorate", "itertools 0.14.0", - "omicron-common", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-workspace-hack", + "schemars", + "serde", + "serde_json", + "slog", +] + +[[package]] +name = "clickhouse-admin-types" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#07a36a1dabacaec9ebfe9fa48ab51adf99a67278" +dependencies = [ + "anyhow", + "atomicwrites", + "camino", + "camino-tempfile", + "chrono", + "daft", + "derive_more", + "expectorate", + "itertools 0.14.0", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "omicron-workspace-hack", "schemars", "serde", @@ -798,7 +807,7 @@ 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]] @@ -808,11 +817,25 @@ source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec dependencies = [ "chrono", "csv", - "omicron-common", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", "omicron-workspace-hack", "schemars", "serde", - "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=zl%2Fmcast-implicit-lifecycle#07a36a1dabacaec9ebfe9fa48ab51adf99a67278" +dependencies = [ + "chrono", + "csv", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "omicron-workspace-hack", + "schemars", + "serde", + "thiserror 2.0.17", ] [[package]] @@ -836,7 +859,7 @@ version = "0.1.0" dependencies = [ "anyhow", "chrono", - "oximeter", + "oximeter 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "oxnet", "rand 0.9.2", "schemars", @@ -858,7 +881,7 @@ source = "git+https://github.com/oxidecomputer/dendrite?branch=main#606c0be888f4 dependencies = [ "anyhow", "chrono", - "oximeter", + "oximeter 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", "oxnet", "rand 0.9.2", "schemars", @@ -1067,7 +1090,7 @@ dependencies = [ "libc", "num-derive 0.4.2", "num-traits", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] @@ -1168,7 +1191,7 @@ checksum = "7ad40aef90652e771af668d28abcc3ef35fd0d39438706a76a61588cf8e8e84a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -1192,7 +1215,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -1203,7 +1226,7 @@ checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ "darling_core", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -1247,7 +1270,7 @@ dependencies = [ "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -1256,7 +1279,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]] @@ -1290,7 +1313,7 @@ dependencies = [ "proc-macro2", "quote", "structmeta", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -1301,7 +1324,7 @@ checksum = "ef941ded77d15ca19b40374869ac6000af1c9f2a4c0f3d4c70926287e6364a8f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -1314,7 +1337,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version 0.4.1", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -1348,7 +1371,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -1369,7 +1392,7 @@ 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", ] @@ -1382,7 +1405,7 @@ dependencies = [ "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 +1432,7 @@ dependencies = [ "pretty-hex", "serde", "serde_json", - "thiserror 2.0.16", + "thiserror 2.0.17", "zerocopy 0.8.27", ] @@ -1440,17 +1463,17 @@ dependencies = [ "dropshot", "expectorate", "futures", - "gateway-client", - "gateway-types", - "internal-dns-resolver", - "internal-dns-types", + "gateway-client 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "gateway-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "internal-dns-resolver 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "internal-dns-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", "libc", "mockall", - "nexus-client", - "omicron-common", + "nexus-client 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "openssl", "oxide-tokio-rt", - "oximeter", + "oximeter 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "oximeter-producer", "oxnet", "rand 0.9.2", @@ -1470,7 +1493,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", @@ -1501,6 +1524,7 @@ dependencies = [ "chrono", "common 0.1.0", "crc8", + "dpd-types", "futures", "http", "lazy_static", @@ -1520,7 +1544,7 @@ dependencies = [ "slog-async", "slog-term", "tokio", - "toml 0.9.7", + "toml 0.9.8", "transceiver-controller", "uuid", ] @@ -1556,7 +1580,7 @@ dependencies = [ "aal", "chrono", "common 0.1.0", - "omicron-common", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "oxnet", "schemars", "serde", @@ -1572,7 +1596,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 +1605,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,12 +1619,12 @@ 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", @@ -1620,11 +1644,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 +1681,7 @@ dependencies = [ "similar", "supports-color", "textwrap", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] @@ -1675,9 +1699,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 +1709,7 @@ dependencies = [ "semver 1.0.27", "serde", "serde_tokenstream", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -1707,7 +1731,7 @@ checksum = "dc09b90bda5770641457f1c0a42c8203c48f5a3d9799dcf1bafbd84e30ccf080" dependencies = [ "pest", "pest_derive", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] @@ -1764,7 +1788,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -1773,6 +1797,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" @@ -1790,12 +1823,26 @@ version = "0.1.0" source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec600ca74e573a1926c1876e33b35" dependencies = [ "dropshot", - "omicron-uuid-kinds", + "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-workspace-hack", + "schemars", + "serde", + "serde_json", + "thiserror 2.0.17", +] + +[[package]] +name = "ereport-types" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#07a36a1dabacaec9ebfe9fa48ab51adf99a67278" +dependencies = [ + "dropshot", + "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "omicron-workspace-hack", "schemars", "serde", "serde_json", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] @@ -1917,7 +1964,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -2024,7 +2071,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -2065,10 +2112,35 @@ dependencies = [ "base64 0.22.1", "chrono", "daft", - "ereport-types", - "gateway-messages", - "gateway-types", - "omicron-uuid-kinds", + "ereport-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "gateway-messages 0.1.0 (git+https://github.com/oxidecomputer/management-gateway-service?rev=669fe557b66f44aed3c622bd17bc092f08797e0c)", + "gateway-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-workspace-hack", + "progenitor 0.10.0", + "rand 0.9.2", + "reqwest", + "schemars", + "serde", + "serde_json", + "slog", + "thiserror 2.0.17", + "tokio", + "uuid", +] + +[[package]] +name = "gateway-client" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#07a36a1dabacaec9ebfe9fa48ab51adf99a67278" +dependencies = [ + "base64 0.22.1", + "chrono", + "daft", + "ereport-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "gateway-messages 0.1.0 (git+https://github.com/oxidecomputer/management-gateway-service?rev=ea2f39ccdea124b5affcad0ca17bc5dacf65823a)", + "gateway-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "omicron-workspace-hack", "progenitor 0.10.0", "rand 0.9.2", @@ -2077,7 +2149,7 @@ dependencies = [ "serde", "serde_json", "slog", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "uuid", ] @@ -2099,6 +2171,23 @@ dependencies = [ "zerocopy 0.8.27", ] +[[package]] +name = "gateway-messages" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/management-gateway-service?rev=ea2f39ccdea124b5affcad0ca17bc5dacf65823a#ea2f39ccdea124b5affcad0ca17bc5dacf65823a" +dependencies = [ + "bitflags 2.9.4", + "hubpack", + "serde", + "serde-big-array", + "serde_repr", + "static_assertions", + "strum 0.27.2", + "strum_macros 0.27.2", + "uuid", + "zerocopy 0.8.27", +] + [[package]] name = "gateway-types" version = "0.1.0" @@ -2106,14 +2195,33 @@ source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec dependencies = [ "daft", "dropshot", - "gateway-messages", + "gateway-messages 0.1.0 (git+https://github.com/oxidecomputer/management-gateway-service?rev=669fe557b66f44aed3c622bd17bc092f08797e0c)", + "hex", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-workspace-hack", + "schemars", + "serde", + "thiserror 2.0.17", + "tufaceous-artifact", + "uuid", +] + +[[package]] +name = "gateway-types" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#07a36a1dabacaec9ebfe9fa48ab51adf99a67278" +dependencies = [ + "daft", + "dropshot", + "gateway-messages 0.1.0 (git+https://github.com/oxidecomputer/management-gateway-service?rev=ea2f39ccdea124b5affcad0ca17bc5dacf65823a)", "hex", - "omicron-common", - "omicron-uuid-kinds", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "omicron-workspace-hack", "schemars", "serde", - "thiserror 2.0.16", + "thiserror 2.0.17", "tufaceous-artifact", "uuid", ] @@ -2165,12 +2273,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "gimli" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" - [[package]] name = "git2" version = "0.19.0" @@ -2237,7 +2339,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap 2.11.4", + "indexmap 2.12.1", "slab", "tokio", "tokio-util", @@ -2272,9 +2374,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", ] @@ -2377,7 +2479,7 @@ dependencies = [ "once_cell", "rand 0.9.2", "ring", - "thiserror 2.0.16", + "thiserror 2.0.17", "tinyvec", "tokio", "tracing", @@ -2421,7 +2523,7 @@ dependencies = [ "rand 0.9.2", "resolv-conf", "smallvec", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "tracing", ] @@ -2445,23 +2547,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 +2630,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 +2699,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 +2718,7 @@ dependencies = [ "socket2 0.6.0", "system-configuration", "tokio", + "tower-layer", "tower-service", "tracing", "windows-registry", @@ -2742,7 +2844,7 @@ dependencies = [ "daft", "equivalent", "foldhash 0.2.0", - "hashbrown 0.16.0", + "hashbrown 0.16.1", "ref-cast", "rustc-hash", "schemars", @@ -2816,19 +2918,61 @@ dependencies = [ "itertools 0.14.0", "libc", "macaddr", - "omicron-common", - "omicron-uuid-kinds", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-workspace-hack", + "opte-ioctl", + "oxide-vpc", + "oxlog 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "oxnet", + "schemars", + "serde", + "slog", + "slog-error-chain", + "smf 0.2.3", + "thiserror 2.0.17", + "tokio", + "uuid", + "whoami", + "zone", +] + +[[package]] +name = "illumos-utils" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#07a36a1dabacaec9ebfe9fa48ab51adf99a67278" +dependencies = [ + "anyhow", + "async-trait", + "bhyve_api 0.0.0 (git+https://github.com/oxidecomputer/propolis?rev=3f1752e6cee9a2f8ecdce6e2ad3326781182e2d9)", + "byteorder", + "camino", + "camino-tempfile", + "cfg-if", + "crucible-smf", + "debug-ignore", + "dropshot", + "futures", + "http", + "ipnetwork", + "itertools 0.14.0", + "libc", + "macaddr", + "nix", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "omicron-workspace-hack", "opte-ioctl", "oxide-vpc", - "oxlog", + "oxlog 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "oxnet", "schemars", "serde", "slog", "slog-error-chain", "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", @@ -2853,12 +2997,12 @@ dependencies = [ [[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.16.0", + "hashbrown 0.16.1", "serde", "serde_core", ] @@ -2888,7 +3032,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -2919,14 +3063,32 @@ dependencies = [ "futures", "hickory-proto 0.25.2", "hickory-resolver 0.25.2", - "internal-dns-types", - "omicron-common", - "omicron-uuid-kinds", + "internal-dns-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", "omicron-workspace-hack", "qorb", "reqwest", "slog", - "thiserror 2.0.16", + "thiserror 2.0.17", +] + +[[package]] +name = "internal-dns-resolver" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#07a36a1dabacaec9ebfe9fa48ab51adf99a67278" +dependencies = [ + "futures", + "hickory-proto 0.25.2", + "hickory-resolver 0.25.2", + "internal-dns-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "omicron-workspace-hack", + "qorb", + "reqwest", + "slog", + "thiserror 2.0.17", ] [[package]] @@ -2936,8 +3098,23 @@ source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec dependencies = [ "anyhow", "chrono", - "omicron-common", - "omicron-uuid-kinds", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-workspace-hack", + "schemars", + "serde", + "strum 0.27.2", +] + +[[package]] +name = "internal-dns-types" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#07a36a1dabacaec9ebfe9fa48ab51adf99a67278" +dependencies = [ + "anyhow", + "chrono", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "omicron-workspace-hack", "schemars", "serde", @@ -2950,17 +3127,6 @@ 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" @@ -3077,7 +3243,7 @@ checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -3121,7 +3287,7 @@ 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]] @@ -3211,7 +3377,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", ] @@ -3506,7 +3672,7 @@ dependencies = [ "cfg-if", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -3593,7 +3759,7 @@ dependencies = [ "quote", "serde", "serde_tokenstream", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -3613,9 +3779,32 @@ dependencies = [ "chrono", "futures", "iddqd", - "nexus-types", - "omicron-common", - "omicron-uuid-kinds", + "nexus-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-workspace-hack", + "oxnet", + "progenitor 0.10.0", + "regress", + "reqwest", + "schemars", + "serde", + "serde_json", + "slog", + "uuid", +] + +[[package]] +name = "nexus-client" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#07a36a1dabacaec9ebfe9fa48ab51adf99a67278" +dependencies = [ + "chrono", + "futures", + "iddqd", + "nexus-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "omicron-workspace-hack", "oxnet", "progenitor 0.10.0", @@ -3637,18 +3826,43 @@ dependencies = [ "chrono", "daft", "iddqd", - "illumos-utils", + "illumos-utils 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "indent_write", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-passwords 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-workspace-hack", + "schemars", + "serde", + "serde_json", + "sled-hardware-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "strum 0.27.2", + "thiserror 2.0.17", + "tufaceous-artifact", + "uuid", +] + +[[package]] +name = "nexus-sled-agent-shared" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#07a36a1dabacaec9ebfe9fa48ab51adf99a67278" +dependencies = [ + "camino", + "chrono", + "daft", + "iddqd", + "illumos-utils 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "indent_write", - "omicron-common", - "omicron-passwords", - "omicron-uuid-kinds", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "omicron-passwords 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "omicron-workspace-hack", "schemars", "serde", "serde_json", - "sled-hardware-types", + "sled-hardware-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "strum 0.27.2", - "thiserror 2.0.16", + "thiserror 2.0.17", "tufaceous-artifact", "uuid", ] @@ -3659,41 +3873,108 @@ version = "0.1.0" source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec600ca74e573a1926c1876e33b35" dependencies = [ "anyhow", - "api_identity", + "api_identity 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "async-trait", + "base64 0.22.1", + "chrono", + "clap", + "clickhouse-admin-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "cockroach-admin-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "cookie", + "daft", + "derive-where", + "derive_more", + "dropshot", + "ereport-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "futures", + "gateway-client 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "gateway-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "http", + "humantime", + "iddqd", + "illumos-utils 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "indent_write", + "internal-dns-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "ipnetwork", + "itertools 0.14.0", + "newtype-uuid", + "newtype_derive", + "nexus-sled-agent-shared 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-passwords 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-workspace-hack", + "openssl", + "oximeter-db 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "oxnet", + "oxql-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "parse-display", + "regex", + "schemars", + "semver 1.0.27", + "serde", + "serde_json", + "serde_with", + "sled-hardware-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "slog", + "slog-error-chain", + "steno", + "strum 0.27.2", + "swrite", + "tabled 0.15.0", + "test-strategy", + "textwrap", + "thiserror 2.0.17", + "tokio", + "tough", + "tufaceous-artifact", + "unicode-width 0.1.14", + "update-engine 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "url", + "uuid", +] + +[[package]] +name = "nexus-types" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#07a36a1dabacaec9ebfe9fa48ab51adf99a67278" +dependencies = [ + "anyhow", + "api_identity 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "async-trait", "base64 0.22.1", "chrono", "clap", - "clickhouse-admin-types", - "cockroach-admin-types", + "clickhouse-admin-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "cockroach-admin-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "cookie", "daft", "derive-where", "derive_more", "dropshot", - "ereport-types", + "ereport-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "futures", - "gateway-client", - "gateway-types", + "gateway-client 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "gateway-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "http", "humantime", "iddqd", - "illumos-utils", + "illumos-utils 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "indent_write", - "internal-dns-types", + "internal-dns-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "ipnetwork", "itertools 0.14.0", "newtype-uuid", "newtype_derive", - "nexus-sled-agent-shared", - "omicron-common", - "omicron-passwords", - "omicron-uuid-kinds", + "nexus-sled-agent-shared 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "omicron-passwords 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "omicron-workspace-hack", "openssl", - "oximeter-db", + "oximeter-db 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "oxnet", - "oxql-types", + "oxql-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "parse-display", "regex", "schemars", @@ -3701,7 +3982,7 @@ dependencies = [ "serde", "serde_json", "serde_with", - "sled-hardware-types", + "sled-hardware-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "slog", "slog-error-chain", "steno", @@ -3710,12 +3991,12 @@ dependencies = [ "tabled 0.15.0", "test-strategy", "textwrap", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "tough", "tufaceous-artifact", "unicode-width 0.1.14", - "update-engine", + "update-engine 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "url", "uuid", ] @@ -3803,7 +4084,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -3888,7 +4169,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -3933,32 +4214,67 @@ dependencies = [ ] [[package]] -name = "object" -version = "0.36.7" +name = "olpc-cjson" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +checksum = "696183c9b5fe81a7715d074fd632e8bd46f4ccc0231a3ed7fc580a80de5f7083" dependencies = [ - "memchr", + "serde", + "serde_json", + "unicode-normalization", ] [[package]] -name = "olpc-cjson" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "696183c9b5fe81a7715d074fd632e8bd46f4ccc0231a3ed7fc580a80de5f7083" +name = "omicron-common" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec600ca74e573a1926c1876e33b35" dependencies = [ + "anyhow", + "api_identity 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "async-trait", + "backoff", + "camino", + "chrono", + "daft", + "dropshot", + "futures", + "hex", + "http", + "iddqd", + "ipnetwork", + "macaddr", + "mg-admin-client", + "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-workspace-hack", + "oxnet", + "parse-display", + "progenitor-client 0.10.0", + "protocol", + "rand 0.9.2", + "regress", + "reqwest", + "schemars", + "semver 1.0.27", "serde", + "serde_human_bytes", "serde_json", - "unicode-normalization", + "serde_with", + "slog", + "slog-error-chain", + "strum 0.27.2", + "thiserror 2.0.17", + "tokio", + "tufaceous-artifact", + "uuid", ] [[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=zl%2Fmcast-implicit-lifecycle#07a36a1dabacaec9ebfe9fa48ab51adf99a67278" dependencies = [ "anyhow", - "api_identity", + "api_identity 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "async-trait", "backoff", "camino", @@ -3972,7 +4288,7 @@ dependencies = [ "ipnetwork", "macaddr", "mg-admin-client", - "omicron-uuid-kinds", + "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "omicron-workspace-hack", "oxnet", "parse-display", @@ -3990,7 +4306,7 @@ dependencies = [ "slog", "slog-error-chain", "strum 0.27.2", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "tufaceous-artifact", "uuid", @@ -4008,7 +4324,22 @@ dependencies = [ "secrecy", "serde", "serde_with", - "thiserror 2.0.16", + "thiserror 2.0.17", +] + +[[package]] +name = "omicron-passwords" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#07a36a1dabacaec9ebfe9fa48ab51adf99a67278" +dependencies = [ + "argon2", + "omicron-workspace-hack", + "rand 0.9.2", + "schemars", + "secrecy", + "serde", + "serde_with", + "thiserror 2.0.17", ] [[package]] @@ -4023,6 +4354,18 @@ dependencies = [ "schemars", ] +[[package]] +name = "omicron-uuid-kinds" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#07a36a1dabacaec9ebfe9fa48ab51adf99a67278" +dependencies = [ + "daft", + "newtype-uuid", + "newtype-uuid-macros", + "paste", + "schemars", +] + [[package]] name = "omicron-workspace-hack" version = "0.1.0" @@ -4083,7 +4426,7 @@ 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", ] @@ -4111,7 +4454,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -4175,7 +4518,7 @@ dependencies = [ "oxide-vpc", "postcard", "serde", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] @@ -4218,12 +4561,31 @@ dependencies = [ "chrono", "clap", "omicron-workspace-hack", - "oximeter-macro-impl", - "oximeter-schema", - "oximeter-timeseries-macro", - "oximeter-types", + "oximeter-macro-impl 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "oximeter-schema 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "oximeter-timeseries-macro 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "oximeter-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "prettyplease", + "syn 2.0.111", + "toml 0.8.23", + "uuid", +] + +[[package]] +name = "oximeter" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#07a36a1dabacaec9ebfe9fa48ab51adf99a67278" +dependencies = [ + "anyhow", + "chrono", + "clap", + "omicron-workspace-hack", + "oximeter-macro-impl 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "oximeter-schema 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "oximeter-timeseries-macro 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "oximeter-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "prettyplease", - "syn 2.0.106", + "syn 2.0.111", "toml 0.8.23", "uuid", ] @@ -4250,15 +4612,68 @@ dependencies = [ "gethostname", "highway", "iana-time-zone", - "indexmap 2.11.4", + "indexmap 2.12.1", + "libc", + "nom", + "num", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-workspace-hack", + "oxide-tokio-rt", + "oximeter 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "oxql-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "parse-display", + "qorb", + "quote", + "regex", + "reqwest", + "schemars", + "serde", + "serde_json", + "slog", + "slog-async", + "slog-dtrace", + "slog-error-chain", + "slog-term", + "strum 0.27.2", + "termtree", + "thiserror 2.0.17", + "tokio", + "tokio-util", + "usdt 0.5.0", + "uuid", +] + +[[package]] +name = "oximeter-db" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#07a36a1dabacaec9ebfe9fa48ab51adf99a67278" +dependencies = [ + "anyhow", + "async-recursion", + "async-trait", + "bcs", + "bytes", + "camino", + "chrono", + "chrono-tz", + "clap", + "clickward", + "const_format", + "debug-ignore", + "dropshot", + "futures", + "gethostname", + "highway", + "iana-time-zone", + "indexmap 2.12.1", "libc", "nom", "num", - "omicron-common", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "omicron-workspace-hack", "oxide-tokio-rt", - "oximeter", - "oxql-types", + "oximeter 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "oxql-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "parse-display", "qorb", "quote", @@ -4274,7 +4689,7 @@ dependencies = [ "slog-term", "strum 0.27.2", "termtree", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "tokio-util", "usdt 0.5.0", @@ -4284,7 +4699,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=zl%2Fmcast-implicit-lifecycle#07a36a1dabacaec9ebfe9fa48ab51adf99a67278" dependencies = [ "cfg-if", "chrono", @@ -4292,9 +4707,9 @@ dependencies = [ "kstat-rs", "libc", "omicron-workspace-hack", - "oximeter", + "oximeter 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "slog", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "uuid", ] @@ -4307,27 +4722,38 @@ dependencies = [ "omicron-workspace-hack", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", +] + +[[package]] +name = "oximeter-macro-impl" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#07a36a1dabacaec9ebfe9fa48ab51adf99a67278" +dependencies = [ + "omicron-workspace-hack", + "proc-macro2", + "quote", + "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=zl%2Fmcast-implicit-lifecycle#07a36a1dabacaec9ebfe9fa48ab51adf99a67278" dependencies = [ "chrono", "dropshot", - "internal-dns-resolver", - "internal-dns-types", - "nexus-client", - "omicron-common", + "internal-dns-resolver 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "internal-dns-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "nexus-client 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "omicron-workspace-hack", - "oximeter", + "oximeter 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "schemars", "serde", "slog", "slog-dtrace", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "uuid", ] @@ -4342,14 +4768,35 @@ dependencies = [ "clap", "heck 0.5.0", "omicron-workspace-hack", - "oximeter-types", + "oximeter-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "prettyplease", + "proc-macro2", + "quote", + "schemars", + "serde", + "slog-error-chain", + "syn 2.0.111", + "toml 0.8.23", +] + +[[package]] +name = "oximeter-schema" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#07a36a1dabacaec9ebfe9fa48ab51adf99a67278" +dependencies = [ + "anyhow", + "chrono", + "clap", + "heck 0.5.0", + "omicron-workspace-hack", + "oximeter-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "prettyplease", "proc-macro2", "quote", "schemars", "serde", "slog-error-chain", - "syn 2.0.106", + "syn 2.0.111", "toml 0.8.23", ] @@ -4359,11 +4806,24 @@ version = "0.1.0" source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec600ca74e573a1926c1876e33b35" dependencies = [ "omicron-workspace-hack", - "oximeter-schema", - "oximeter-types", + "oximeter-schema 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "oximeter-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "oximeter-timeseries-macro" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#07a36a1dabacaec9ebfe9fa48ab51adf99a67278" +dependencies = [ + "omicron-workspace-hack", + "oximeter-schema 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "oximeter-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -4375,14 +4835,34 @@ dependencies = [ "chrono", "float-ord", "num", - "omicron-common", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-workspace-hack", + "parse-display", + "regex", + "schemars", + "serde", + "strum 0.27.2", + "thiserror 2.0.17", + "uuid", +] + +[[package]] +name = "oximeter-types" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#07a36a1dabacaec9ebfe9fa48ab51adf99a67278" +dependencies = [ + "bytes", + "chrono", + "float-ord", + "num", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "omicron-workspace-hack", "parse-display", "regex", "schemars", "serde", "strum 0.27.2", - "thiserror 2.0.16", + "thiserror 2.0.17", "uuid", ] @@ -4403,11 +4883,28 @@ dependencies = [ "uuid", ] +[[package]] +name = "oxlog" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#07a36a1dabacaec9ebfe9fa48ab51adf99a67278" +dependencies = [ + "anyhow", + "camino", + "chrono", + "clap", + "glob", + "jiff", + "omicron-workspace-hack", + "rayon", + "sigpipe", + "uuid", +] + [[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", @@ -4425,7 +4922,24 @@ dependencies = [ "highway", "num", "omicron-workspace-hack", - "oximeter-types", + "oximeter-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "schemars", + "serde", + "serde_json", + "uuid", +] + +[[package]] +name = "oxql-types" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#07a36a1dabacaec9ebfe9fa48ab51adf99a67278" +dependencies = [ + "anyhow", + "chrono", + "highway", + "num", + "omicron-workspace-hack", + "oximeter-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "schemars", "serde", "serde_json", @@ -4536,7 +5050,7 @@ dependencies = [ "regex", "regex-syntax", "structmeta", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -4587,7 +5101,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", ] @@ -4611,7 +5125,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -4631,7 +5145,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", ] @@ -4644,7 +5158,7 @@ checksum = "54acf3a685220b533e437e264e4d932cfbdc4cc7ec0cd232ed73c08d03b8a7ca" dependencies = [ "fixedbitset 0.5.7", "hashbrown 0.15.5", - "indexmap 2.11.4", + "indexmap 2.12.1", "serde", ] @@ -4683,7 +5197,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -4810,7 +5324,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]] @@ -4866,7 +5380,7 @@ dependencies = [ "proc-macro-error-attr2", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -4938,7 +5452,7 @@ checksum = "b17e5363daa50bf1cccfade6b0fb970d2278758fd5cfa9ab69f25028e4b1afa3" dependencies = [ "heck 0.5.0", "http", - "indexmap 2.11.4", + "indexmap 2.12.1", "openapiv3", "proc-macro2", "quote", @@ -4946,8 +5460,8 @@ dependencies = [ "schemars", "serde", "serde_json", - "syn 2.0.106", - "thiserror 2.0.16", + "syn 2.0.111", + "thiserror 2.0.17", "typify", "unicode-ident", ] @@ -4960,7 +5474,7 @@ checksum = "8276d558f1dfd4cc7fc4cceee0a51dab482b5a4be2e69e7eab8c57fbfb1472f4" dependencies = [ "heck 0.5.0", "http", - "indexmap 2.11.4", + "indexmap 2.12.1", "openapiv3", "proc-macro2", "quote", @@ -4968,8 +5482,8 @@ dependencies = [ "schemars", "serde", "serde_json", - "syn 2.0.106", - "thiserror 2.0.16", + "syn 2.0.111", + "thiserror 2.0.17", "typify", "unicode-ident", ] @@ -4989,7 +5503,7 @@ dependencies = [ "serde_json", "serde_tokenstream", "serde_yaml", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -5007,7 +5521,7 @@ dependencies = [ "serde_json", "serde_tokenstream", "serde_yaml", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -5024,7 +5538,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", @@ -5099,7 +5613,7 @@ dependencies = [ "itertools 0.14.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -5136,7 +5650,7 @@ dependencies = [ "hickory-resolver 0.24.4", "rand 0.9.2", "serde", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "tokio-stream", "tracing", @@ -5163,7 +5677,7 @@ dependencies = [ "rustc-hash", "rustls 0.23.32", "socket2 0.6.0", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "tracing", "web-time", @@ -5184,7 +5698,7 @@ dependencies = [ "rustls 0.23.32", "rustls-pki-types", "slab", - "thiserror 2.0.16", + "thiserror 2.0.17", "tinyvec", "tracing", "web-time", @@ -5339,7 +5853,7 @@ checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -5472,12 +5986,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" @@ -5686,7 +6194,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -5721,7 +6229,7 @@ checksum = "1783eabc414609e28a5ba76aee5ddd52199f7107a0b24c2e9746a1ecc34a683d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -5732,7 +6240,7 @@ checksum = "ed76efe62313ab6610570951494bdaa81568026e0318eaa55f167de70eeea67d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -5791,9 +6299,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", @@ -5819,22 +6327,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]] @@ -5845,7 +6353,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -5898,7 +6406,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -5912,9 +6420,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", ] @@ -5928,7 +6436,7 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -5968,7 +6476,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -5977,7 +6485,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", @@ -6087,8 +6595,20 @@ name = "sled-hardware-types" version = "0.1.0" source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec600ca74e573a1926c1876e33b35" dependencies = [ - "illumos-utils", - "omicron-common", + "illumos-utils 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-workspace-hack", + "schemars", + "serde", +] + +[[package]] +name = "sled-hardware-types" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#07a36a1dabacaec9ebfe9fa48ab51adf99a67278" +dependencies = [ + "illumos-utils 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "omicron-workspace-hack", "schemars", "serde", @@ -6096,9 +6616,15 @@ dependencies = [ [[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" @@ -6154,7 +6680,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]] @@ -6249,7 +6775,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -6349,7 +6875,7 @@ dependencies = [ "proc-macro2", "quote", "structmeta-derive", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -6360,7 +6886,7 @@ checksum = "152a0b65a590ff6c3da95cabe2353ee04e6167c896b28e3b14478c2636c922fc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -6391,7 +6917,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -6403,7 +6929,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -6458,9 +6984,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", @@ -6484,7 +7010,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -6553,7 +7079,7 @@ dependencies = [ "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -6642,7 +7168,7 @@ dependencies = [ "proc-macro2", "quote", "structmeta", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -6684,9 +7210,9 @@ dependencies = [ "kstat-rs", "libc", "lldpd-client", - "omicron-common", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "oxide-tokio-rt", - "oximeter", + "oximeter 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "oximeter-instruments", "oximeter-producer", "oxnet", @@ -6715,11 +7241,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]] @@ -6730,18 +7256,18 @@ 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 = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -6841,25 +7367,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]] @@ -6868,20 +7401,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]] @@ -6967,14 +7500,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", @@ -6991,9 +7524,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", ] @@ -7004,7 +7537,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", @@ -7017,7 +7550,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", @@ -7027,9 +7560,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", ] @@ -7042,9 +7575,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" @@ -7201,7 +7734,7 @@ checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -7245,7 +7778,7 @@ dependencies = [ "slog-async", "slog-term", "tabled 0.20.0", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "transceiver-decode", "transceiver-messages", @@ -7261,7 +7794,7 @@ dependencies = [ "schemars", "serde", "static_assertions", - "thiserror 2.0.16", + "thiserror 2.0.17", "transceiver-messages", ] @@ -7275,7 +7808,7 @@ dependencies = [ "hubpack", "schemars", "serde", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] @@ -7298,7 +7831,7 @@ dependencies = [ "serde_human_bytes", "strum 0.26.3", "test-strategy", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] @@ -7344,8 +7877,8 @@ dependencies = [ "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", ] @@ -7362,7 +7895,7 @@ dependencies = [ "serde", "serde_json", "serde_tokenstream", - "syn 2.0.106", + "syn 2.0.111", "typify-impl", ] @@ -7454,7 +7987,37 @@ 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", + "serde", + "serde_json", + "serde_with", + "slog", + "swrite", + "tokio", + "unicode-width 0.1.14", + "uuid", +] + +[[package]] +name = "update-engine" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#07a36a1dabacaec9ebfe9fa48ab51adf99a67278" +dependencies = [ + "anyhow", + "cancel-safe-futures", + "chrono", + "debug-ignore", + "derive-where", + "either", + "futures", + "indent_write", + "indexmap 2.12.1", "libsw", "linear-map", "omicron-workspace-hack", @@ -7540,7 +8103,7 @@ dependencies = [ "proc-macro2", "quote", "serde_tokenstream", - "syn 2.0.106", + "syn 2.0.111", "usdt-impl 0.5.0", ] @@ -7554,7 +8117,7 @@ dependencies = [ "proc-macro2", "quote", "serde_tokenstream", - "syn 2.0.106", + "syn 2.0.111", "usdt-impl 0.6.0", ] @@ -7572,7 +8135,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", @@ -7592,8 +8155,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", ] @@ -7607,7 +8170,7 @@ dependencies = [ "proc-macro2", "quote", "serde_tokenstream", - "syn 2.0.106", + "syn 2.0.111", "usdt-impl 0.5.0", ] @@ -7621,7 +8184,7 @@ dependencies = [ "proc-macro2", "quote", "serde_tokenstream", - "syn 2.0.106", + "syn 2.0.111", "usdt-impl 0.6.0", ] @@ -7639,13 +8202,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", ] @@ -7791,7 +8354,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", "wasm-bindgen-shared", ] @@ -7826,7 +8389,7 @@ checksum = "ffc003a991398a8ee604a401e194b6b3a39677b3173d6e74495eb51b82e99a32" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -7951,7 +8514,7 @@ checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -7962,7 +8525,7 @@ checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -8356,7 +8919,7 @@ checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", "synstructure", ] @@ -8387,7 +8950,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -8398,7 +8961,7 @@ checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -8418,7 +8981,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", "synstructure", ] @@ -8458,7 +9021,7 @@ checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] diff --git a/dpd/src/mcast/validate.rs b/dpd/src/mcast/validate.rs index a0f430f6..94a81d04 100644 --- a/dpd/src/mcast/validate.rs +++ b/dpd/src/mcast/validate.rs @@ -13,11 +13,9 @@ use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; use common::nat::NatTarget; use omicron_common::address::{ - IPV4_ADMIN_SCOPED_MULTICAST_SUBNET, IPV4_GLOP_MULTICAST_SUBNET, - IPV4_LINK_LOCAL_MULTICAST_SUBNET, IPV4_SPECIFIC_RESERVED_MULTICAST_ADDRS, - IPV4_SSM_SUBNET, IPV6_INTERFACE_LOCAL_MULTICAST_SUBNET, - IPV6_LINK_LOCAL_MULTICAST_SUBNET, IPV6_RESERVED_SCOPE_MULTICAST_SUBNET, - IPV6_SSM_SUBNET, + 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, }; use oxnet::{Ipv4Net, Ipv6Net}; @@ -107,24 +105,9 @@ fn validate_ipv4_multicast( } // Check reserved subnets - let reserved_subnets = [ - IPV4_LINK_LOCAL_MULTICAST_SUBNET, - IPV4_GLOP_MULTICAST_SUBNET, - IPV4_ADMIN_SCOPED_MULTICAST_SUBNET, - ]; - - 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 - if IPV4_SPECIFIC_RESERVED_MULTICAST_ADDRS.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", ))); } From 793b4438c825d37e32b3d89dfb5ac38e932be7d4 Mon Sep 17 00:00:00 2001 From: Zeeshan Lakhani Date: Mon, 15 Dec 2025 03:10:17 +0000 Subject: [PATCH 04/22] [review] document port_bitmap used in multicastegress --- dpd/p4/metadata.p4 | 2 +- dpd/p4/port_bitmap_check.p4 | 47 +++++++++++++++++++++++++++++ dpd/p4/sidecar.p4 | 22 +++++++++++--- dpd/src/table/mcast/mcast_egress.rs | 3 +- 4 files changed, 66 insertions(+), 8 deletions(-) diff --git a/dpd/p4/metadata.p4 b/dpd/p4/metadata.p4 index f8c7799c..099c8480 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 75a79ca4..1bd47a69 100644 --- a/dpd/p4/port_bitmap_check.p4 +++ b/dpd/p4/port_bitmap_check.p4 @@ -4,6 +4,53 @@ // // 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 +// +// External multicast groups have members on specific sleds. When a multicast +// packet is replicated to all ports in the group, only ports connected to +// member sleds should decapsulate. Other ports (e.g., uplinks forwarding to +// peer switches) keep the Geneve encapsulation. +// +// ## Example +// Group with members on sleds connected to ports 5, 12, 47 +// 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 53e0acf8..d1bebe6a 100644 --- a/dpd/p4/sidecar.p4 +++ b/dpd/p4/sidecar.p4 @@ -908,7 +908,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. @@ -1761,10 +1761,22 @@ 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. + * Only packets destined for member sleds have their Geneve headers stripped, + * while copies forwarded to peer switches or uplinks remain encapsulated. + * + * Flow: + * 1. mcast_tag_check : Match packets with admin-local (ff04::/16) or ULA + * destination, AND mcast_tag=2 (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 is populated by DPD based on which ports connect to member sleds. */ control MulticastEgress ( inout sidecar_headers_t hdr, diff --git a/dpd/src/table/mcast/mcast_egress.rs b/dpd/src/table/mcast/mcast_egress.rs index e050c2aa..5750996f 100644 --- a/dpd/src/table/mcast/mcast_egress.rs +++ b/dpd/src/table/mcast/mcast_egress.rs @@ -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], } From 87dd7f47d8de493c5ac5af63a8a43a46b23b5595 Mon Sep 17 00:00:00 2001 From: Zeeshan Lakhani Date: Tue, 16 Dec 2025 01:18:07 +0000 Subject: [PATCH 05/22] [review] fix direction, not being dumb --- dpd/p4/port_bitmap_check.p4 | 11 ++++++----- dpd/p4/sidecar.p4 | 8 +++++--- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/dpd/p4/port_bitmap_check.p4 b/dpd/p4/port_bitmap_check.p4 index 1bd47a69..cf739f7b 100644 --- a/dpd/p4/port_bitmap_check.p4 +++ b/dpd/p4/port_bitmap_check.p4 @@ -39,13 +39,14 @@ // // # Use Case // -// External multicast groups have members on specific sleds. When a multicast -// packet is replicated to all ports in the group, only ports connected to -// member sleds should decapsulate. Other ports (e.g., uplinks forwarding to -// peer switches) keep the Geneve encapsulation. +// 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 members on sleds connected to ports 5, 12, 47 +// 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 diff --git a/dpd/p4/sidecar.p4 b/dpd/p4/sidecar.p4 index d1bebe6a..fd687de4 100644 --- a/dpd/p4/sidecar.p4 +++ b/dpd/p4/sidecar.p4 @@ -1764,8 +1764,9 @@ control MulticastIngress ( /* Multicast Egress - Per-Port Decapsulation * * Determines which replicated multicast copies should be decapsulated. - * Only packets destined for member sleds have their Geneve headers stripped, - * while copies forwarded to peer switches or uplinks remain encapsulated. + * 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 admin-local (ff04::/16) or ULA @@ -1776,7 +1777,8 @@ control MulticastIngress ( * 5. modify_hdr : If bitmap_result != 0, decapsulate packet (strip * outer headers, decrement TTL/hop limit, handle VLAN) * - * The bitmap is populated by DPD based on which ports connect to member sleds. + * 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, From c76f78ccadf303bbf0a27249896765bd25e6fb40 Mon Sep 17 00:00:00 2001 From: Zeeshan Lakhani Date: Wed, 17 Dec 2025 05:15:48 +0000 Subject: [PATCH 06/22] [post-merge] updates --- .cargo/config.toml | 4 + Cargo.lock | 1703 +++++++++++++++++++++++++++++++++++--------- 2 files changed, 1354 insertions(+), 353 deletions(-) diff --git a/.cargo/config.toml b/.cargo/config.toml index 9305fc8c..bf041c98 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"] + +# TODO: remove once we're targetting omicron `main` +[env] +DEP_PQ_LIBDIRS = "1" diff --git a/Cargo.lock b/Cargo.lock index 8612eaee..5a41bd6a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7,7 +7,7 @@ name = "aal" version = "0.1.0" dependencies = [ "common 0.1.0", - "oximeter", + "oximeter 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "oxnet", "schemars", "serde", @@ -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" @@ -129,7 +130,18 @@ dependencies = [ "omicron-workspace-hack", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", +] + +[[package]] +name = "api_identity" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#9794876a45bda0d20c2d545b4f7f134ffd7d2086" +dependencies = [ + "omicron-workspace-hack", + "proc-macro2", + "quote", + "syn 2.0.111", ] [[package]] @@ -176,7 +188,7 @@ dependencies = [ "dpd-api", "lazy_static", "libc", - "oximeter", + "oximeter 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "propolis", "rand 0.9.2", "schemars", @@ -189,7 +201,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 +215,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -225,7 +237,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -236,7 +248,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -348,19 +360,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" @@ -445,7 +448,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -543,6 +546,36 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bootstore" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#9794876a45bda0d20c2d545b4f7f134ffd7d2086" +dependencies = [ + "bytes", + "camino", + "chacha20poly1305", + "ciborium", + "derive_more", + "hex", + "hkdf", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "omicron-rpaths", + "omicron-workspace-hack", + "pq-sys", + "rand 0.8.5", + "secrecy", + "serde", + "serde_with", + "sha3", + "sled-hardware-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "slog", + "thiserror 2.0.17", + "tokio", + "uuid", + "vsss-rs", + "zeroize", +] + [[package]] name = "bstr" version = "1.12.0" @@ -582,9 +615,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 +701,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 +750,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 +831,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -759,7 +854,29 @@ dependencies = [ "derive_more", "expectorate", "itertools 0.14.0", - "omicron-common", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-workspace-hack", + "schemars", + "serde", + "serde_json", + "slog", +] + +[[package]] +name = "clickhouse-admin-types" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#9794876a45bda0d20c2d545b4f7f134ffd7d2086" +dependencies = [ + "anyhow", + "atomicwrites", + "camino", + "camino-tempfile", + "chrono", + "daft", + "derive_more", + "expectorate", + "itertools 0.14.0", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "omicron-workspace-hack", "schemars", "serde", @@ -798,7 +915,7 @@ 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]] @@ -808,11 +925,25 @@ source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec dependencies = [ "chrono", "csv", - "omicron-common", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-workspace-hack", + "schemars", + "serde", + "thiserror 2.0.17", +] + +[[package]] +name = "cockroach-admin-types" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#9794876a45bda0d20c2d545b4f7f134ffd7d2086" +dependencies = [ + "chrono", + "csv", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "omicron-workspace-hack", "schemars", "serde", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] @@ -836,7 +967,7 @@ version = "0.1.0" dependencies = [ "anyhow", "chrono", - "oximeter", + "oximeter 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "oxnet", "rand 0.9.2", "schemars", @@ -858,7 +989,7 @@ source = "git+https://github.com/oxidecomputer/dendrite?branch=main#606c0be888f4 dependencies = [ "anyhow", "chrono", - "oximeter", + "oximeter 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", "oxnet", "rand 0.9.2", "schemars", @@ -999,7 +1130,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 +1189,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", + "serde", + "serde_json", + "uuid", +] + [[package]] name = "crucible-smf" version = "0.0.0" @@ -1067,7 +1211,7 @@ dependencies = [ "libc", "num-derive 0.4.2", "num-traits", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] @@ -1076,6 +1220,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 +1245,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", + "rand_core 0.6.4", "typenum", ] @@ -1147,6 +1310,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 +1359,7 @@ checksum = "7ad40aef90652e771af668d28abcc3ef35fd0d39438706a76a61588cf8e8e84a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -1192,7 +1383,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -1203,7 +1394,7 @@ checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ "darling_core", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -1247,7 +1438,7 @@ dependencies = [ "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -1256,7 +1447,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]] @@ -1290,7 +1481,7 @@ dependencies = [ "proc-macro2", "quote", "structmeta", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -1301,7 +1492,7 @@ checksum = "ef941ded77d15ca19b40374869ac6000af1c9f2a4c0f3d4c70926287e6364a8f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -1314,7 +1505,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version 0.4.1", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -1348,7 +1539,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -1369,7 +1560,7 @@ 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", ] @@ -1382,7 +1573,7 @@ dependencies = [ "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 +1600,7 @@ dependencies = [ "pretty-hex", "serde", "serde_json", - "thiserror 2.0.16", + "thiserror 2.0.17", "zerocopy 0.8.27", ] @@ -1440,17 +1631,17 @@ dependencies = [ "dropshot", "expectorate", "futures", - "gateway-client", - "gateway-types", - "internal-dns-resolver", - "internal-dns-types", + "gateway-client 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "gateway-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "internal-dns-resolver 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "internal-dns-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", "libc", "mockall", - "nexus-client", - "omicron-common", + "nexus-client 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "openssl", "oxide-tokio-rt", - "oximeter", + "oximeter 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "oximeter-producer", "oxnet", "rand 0.9.2", @@ -1470,7 +1661,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", @@ -1501,6 +1692,7 @@ dependencies = [ "chrono", "common 0.1.0", "crc8", + "dpd-types", "futures", "http", "lazy_static", @@ -1520,7 +1712,7 @@ dependencies = [ "slog-async", "slog-term", "tokio", - "toml 0.9.7", + "toml 0.9.8", "transceiver-controller", "uuid", ] @@ -1556,7 +1748,7 @@ dependencies = [ "aal", "chrono", "common 0.1.0", - "omicron-common", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "oxnet", "schemars", "serde", @@ -1572,7 +1764,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 +1773,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,12 +1787,12 @@ 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", @@ -1620,11 +1812,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 +1849,7 @@ dependencies = [ "similar", "supports-color", "textwrap", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] @@ -1675,9 +1867,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 +1877,7 @@ dependencies = [ "semver 1.0.27", "serde", "serde_tokenstream", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -1707,7 +1899,7 @@ checksum = "dc09b90bda5770641457f1c0a42c8203c48f5a3d9799dcf1bafbd84e30ccf080" dependencies = [ "pest", "pest_derive", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] @@ -1728,6 +1920,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 +1974,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -1773,6 +1983,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" @@ -1790,12 +2009,26 @@ version = "0.1.0" source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec600ca74e573a1926c1876e33b35" dependencies = [ "dropshot", - "omicron-uuid-kinds", + "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-workspace-hack", + "schemars", + "serde", + "serde_json", + "thiserror 2.0.17", +] + +[[package]] +name = "ereport-types" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#9794876a45bda0d20c2d545b4f7f134ffd7d2086" +dependencies = [ + "dropshot", + "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "omicron-workspace-hack", "schemars", "serde", "serde_json", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] @@ -1826,6 +2059,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 +2166,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -2024,7 +2273,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -2065,10 +2314,35 @@ dependencies = [ "base64 0.22.1", "chrono", "daft", - "ereport-types", - "gateway-messages", - "gateway-types", - "omicron-uuid-kinds", + "ereport-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "gateway-messages 0.1.0 (git+https://github.com/oxidecomputer/management-gateway-service?rev=669fe557b66f44aed3c622bd17bc092f08797e0c)", + "gateway-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-workspace-hack", + "progenitor 0.10.0", + "rand 0.9.2", + "reqwest", + "schemars", + "serde", + "serde_json", + "slog", + "thiserror 2.0.17", + "tokio", + "uuid", +] + +[[package]] +name = "gateway-client" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#9794876a45bda0d20c2d545b4f7f134ffd7d2086" +dependencies = [ + "base64 0.22.1", + "chrono", + "daft", + "ereport-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "gateway-messages 0.1.0 (git+https://github.com/oxidecomputer/management-gateway-service?rev=ea2f39ccdea124b5affcad0ca17bc5dacf65823a)", + "gateway-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "omicron-workspace-hack", "progenitor 0.10.0", "rand 0.9.2", @@ -2077,7 +2351,7 @@ dependencies = [ "serde", "serde_json", "slog", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "uuid", ] @@ -2099,6 +2373,23 @@ dependencies = [ "zerocopy 0.8.27", ] +[[package]] +name = "gateway-messages" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/management-gateway-service?rev=ea2f39ccdea124b5affcad0ca17bc5dacf65823a#ea2f39ccdea124b5affcad0ca17bc5dacf65823a" +dependencies = [ + "bitflags 2.9.4", + "hubpack", + "serde", + "serde-big-array", + "serde_repr", + "static_assertions", + "strum 0.27.2", + "strum_macros 0.27.2", + "uuid", + "zerocopy 0.8.27", +] + [[package]] name = "gateway-types" version = "0.1.0" @@ -2106,32 +2397,60 @@ source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec dependencies = [ "daft", "dropshot", - "gateway-messages", + "gateway-messages 0.1.0 (git+https://github.com/oxidecomputer/management-gateway-service?rev=669fe557b66f44aed3c622bd17bc092f08797e0c)", "hex", - "omicron-common", - "omicron-uuid-kinds", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", "omicron-workspace-hack", "schemars", "serde", - "thiserror 2.0.16", + "thiserror 2.0.17", "tufaceous-artifact", "uuid", ] [[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +name = "gateway-types" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#9794876a45bda0d20c2d545b4f7f134ffd7d2086" dependencies = [ - "typenum", - "version_check", + "gateway-types-versions", + "omicron-workspace-hack", ] [[package]] -name = "gethostname" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" +name = "gateway-types-versions" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#9794876a45bda0d20c2d545b4f7f134ffd7d2086" +dependencies = [ + "daft", + "dropshot", + "gateway-messages 0.1.0 (git+https://github.com/oxidecomputer/management-gateway-service?rev=ea2f39ccdea124b5affcad0ca17bc5dacf65823a)", + "hex", + "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "omicron-workspace-hack", + "schemars", + "serde", + "thiserror 2.0.17", + "tufaceous-artifact", + "uuid", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "gethostname" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc3655aa6818d65bc620d6911f05aa7b6aeb596291e1e9f79e52df85583d1e30" dependencies = [ "rustix 0.38.44", @@ -2165,12 +2484,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "gimli" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" - [[package]] name = "git2" version = "0.19.0" @@ -2225,6 +2538,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 +2561,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 +2607,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", ] @@ -2377,7 +2712,7 @@ dependencies = [ "once_cell", "rand 0.9.2", "ring", - "thiserror 2.0.16", + "thiserror 2.0.17", "tinyvec", "tokio", "tracing", @@ -2421,7 +2756,7 @@ dependencies = [ "rand 0.9.2", "resolv-conf", "smallvec", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "tracing", ] @@ -2432,6 +2767,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 +2798,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 +2881,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 +2950,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 +2969,7 @@ dependencies = [ "socket2 0.6.0", "system-configuration", "tokio", + "tower-layer", "tower-service", "tracing", "windows-registry", @@ -2742,7 +3095,7 @@ dependencies = [ "daft", "equivalent", "foldhash 0.2.0", - "hashbrown 0.16.0", + "hashbrown 0.16.1", "ref-cast", "rustc-hash", "schemars", @@ -2816,19 +3169,61 @@ dependencies = [ "itertools 0.14.0", "libc", "macaddr", - "omicron-common", - "omicron-uuid-kinds", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-workspace-hack", + "opte-ioctl", + "oxide-vpc", + "oxlog 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "oxnet", + "schemars", + "serde", + "slog", + "slog-error-chain", + "smf 0.2.3", + "thiserror 2.0.17", + "tokio", + "uuid", + "whoami", + "zone", +] + +[[package]] +name = "illumos-utils" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#9794876a45bda0d20c2d545b4f7f134ffd7d2086" +dependencies = [ + "anyhow", + "async-trait", + "bhyve_api 0.0.0 (git+https://github.com/oxidecomputer/propolis?rev=3f1752e6cee9a2f8ecdce6e2ad3326781182e2d9)", + "byteorder", + "camino", + "camino-tempfile", + "cfg-if", + "crucible-smf", + "debug-ignore", + "dropshot", + "futures", + "http", + "ipnetwork", + "itertools 0.14.0", + "libc", + "macaddr", + "nix", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "omicron-workspace-hack", "opte-ioctl", "oxide-vpc", - "oxlog", + "oxlog 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "oxnet", "schemars", "serde", "slog", "slog-error-chain", "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", @@ -2853,12 +3248,12 @@ dependencies = [ [[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 +3283,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -2902,6 +3297,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" @@ -2919,14 +3323,32 @@ dependencies = [ "futures", "hickory-proto 0.25.2", "hickory-resolver 0.25.2", - "internal-dns-types", - "omicron-common", - "omicron-uuid-kinds", + "internal-dns-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", "omicron-workspace-hack", "qorb", "reqwest", "slog", - "thiserror 2.0.16", + "thiserror 2.0.17", +] + +[[package]] +name = "internal-dns-resolver" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#9794876a45bda0d20c2d545b4f7f134ffd7d2086" +dependencies = [ + "futures", + "hickory-proto 0.25.2", + "hickory-resolver 0.25.2", + "internal-dns-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "omicron-workspace-hack", + "qorb", + "reqwest", + "slog", + "thiserror 2.0.17", ] [[package]] @@ -2936,8 +3358,23 @@ source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec dependencies = [ "anyhow", "chrono", - "omicron-common", - "omicron-uuid-kinds", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-workspace-hack", + "schemars", + "serde", + "strum 0.27.2", +] + +[[package]] +name = "internal-dns-types" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#9794876a45bda0d20c2d545b4f7f134ffd7d2086" +dependencies = [ + "anyhow", + "chrono", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "omicron-workspace-hack", "schemars", "serde", @@ -2950,17 +3387,6 @@ 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" @@ -3077,7 +3503,7 @@ checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -3115,13 +3541,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]] @@ -3211,7 +3646,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 +3668,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", ] @@ -3473,6 +3908,24 @@ dependencies = [ "slog", ] +[[package]] +name = "mg-admin-client" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/maghemite?rev=0df320d42b356e689a3c7a7600eec9b16770237a#0df320d42b356e689a3c7a7600eec9b16770237a" +dependencies = [ + "chrono", + "colored", + "progenitor 0.11.1", + "rdb-types", + "reqwest", + "schemars", + "serde", + "serde_json", + "slog", + "tabwriter", + "uuid", +] + [[package]] name = "mime" version = "0.3.17" @@ -3528,7 +3981,7 @@ dependencies = [ "cfg-if", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -3615,7 +4068,7 @@ dependencies = [ "quote", "serde", "serde_tokenstream", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -3635,9 +4088,32 @@ dependencies = [ "chrono", "futures", "iddqd", - "nexus-types", - "omicron-common", - "omicron-uuid-kinds", + "nexus-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-workspace-hack", + "oxnet", + "progenitor 0.10.0", + "regress", + "reqwest", + "schemars", + "serde", + "serde_json", + "slog", + "uuid", +] + +[[package]] +name = "nexus-client" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#9794876a45bda0d20c2d545b4f7f134ffd7d2086" +dependencies = [ + "chrono", + "futures", + "iddqd", + "nexus-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "omicron-workspace-hack", "oxnet", "progenitor 0.10.0", @@ -3659,18 +4135,18 @@ dependencies = [ "chrono", "daft", "iddqd", - "illumos-utils", + "illumos-utils 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", "indent_write", - "omicron-common", - "omicron-passwords", - "omicron-uuid-kinds", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-passwords 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", "omicron-workspace-hack", "schemars", "serde", "serde_json", - "sled-hardware-types", + "sled-hardware-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", "strum 0.27.2", - "thiserror 2.0.16", + "thiserror 2.0.17", "tufaceous-artifact", "uuid", ] @@ -3681,41 +4157,107 @@ version = "0.1.0" source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec600ca74e573a1926c1876e33b35" dependencies = [ "anyhow", - "api_identity", + "api_identity 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", "async-trait", "base64 0.22.1", "chrono", "clap", - "clickhouse-admin-types", - "cockroach-admin-types", + "clickhouse-admin-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "cockroach-admin-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", "cookie", "daft", "derive-where", "derive_more", "dropshot", - "ereport-types", + "ereport-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", "futures", - "gateway-client", - "gateway-types", + "gateway-client 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "gateway-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", "http", "humantime", "iddqd", - "illumos-utils", + "illumos-utils 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", "indent_write", - "internal-dns-types", + "internal-dns-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", "ipnetwork", "itertools 0.14.0", "newtype-uuid", "newtype_derive", "nexus-sled-agent-shared", - "omicron-common", - "omicron-passwords", - "omicron-uuid-kinds", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-passwords 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-workspace-hack", + "openssl", + "oximeter-db 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "oxnet", + "oxql-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "parse-display", + "regex", + "schemars", + "semver 1.0.27", + "serde", + "serde_json", + "serde_with", + "sled-hardware-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "slog", + "slog-error-chain", + "steno", + "strum 0.27.2", + "swrite", + "tabled 0.15.0", + "test-strategy", + "textwrap", + "thiserror 2.0.17", + "tokio", + "tough", + "tufaceous-artifact", + "unicode-width 0.1.14", + "update-engine 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "url", + "uuid", +] + +[[package]] +name = "nexus-types" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#9794876a45bda0d20c2d545b4f7f134ffd7d2086" +dependencies = [ + "anyhow", + "api_identity 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "async-trait", + "base64 0.22.1", + "chrono", + "clap", + "clickhouse-admin-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "cockroach-admin-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "cookie", + "daft", + "derive-where", + "derive_more", + "dropshot", + "ereport-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "futures", + "gateway-client 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "gateway-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "http", + "humantime", + "iddqd", + "illumos-utils 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "indent_write", + "internal-dns-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "ipnetwork", + "itertools 0.14.0", + "newtype-uuid", + "newtype_derive", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "omicron-passwords 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "omicron-workspace-hack", "openssl", - "oximeter-db", + "oximeter-db 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "oxnet", - "oxql-types", + "oxql-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "parse-display", "regex", "schemars", @@ -3723,7 +4265,8 @@ dependencies = [ "serde", "serde_json", "serde_with", - "sled-hardware-types", + "sled-agent-types-versions", + "sled-hardware-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "slog", "slog-error-chain", "steno", @@ -3732,12 +4275,12 @@ dependencies = [ "tabled 0.15.0", "test-strategy", "textwrap", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "tough", "tufaceous-artifact", "unicode-width 0.1.14", - "update-engine", + "update-engine 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "url", "uuid", ] @@ -3825,7 +4368,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -3910,7 +4453,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -3954,15 +4497,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" @@ -3980,7 +4514,51 @@ version = "0.1.0" source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec600ca74e573a1926c1876e33b35" dependencies = [ "anyhow", - "api_identity", + "api_identity 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "async-trait", + "backoff", + "camino", + "chrono", + "daft", + "dropshot", + "futures", + "hex", + "http", + "iddqd", + "ipnetwork", + "macaddr", + "mg-admin-client 0.1.0 (git+https://github.com/oxidecomputer/maghemite?rev=08f2a34d487658e87545ffbba3add632a82baf0d)", + "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-workspace-hack", + "oxnet", + "parse-display", + "progenitor-client 0.10.0", + "protocol", + "rand 0.9.2", + "regress", + "reqwest", + "schemars", + "semver 1.0.27", + "serde", + "serde_human_bytes", + "serde_json", + "serde_with", + "slog", + "slog-error-chain", + "strum 0.27.2", + "thiserror 2.0.17", + "tokio", + "tufaceous-artifact", + "uuid", +] + +[[package]] +name = "omicron-common" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#9794876a45bda0d20c2d545b4f7f134ffd7d2086" +dependencies = [ + "anyhow", + "api_identity 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "async-trait", "backoff", "camino", @@ -3992,9 +4570,10 @@ dependencies = [ "http", "iddqd", "ipnetwork", + "itertools 0.14.0", "macaddr", - "mg-admin-client", - "omicron-uuid-kinds", + "mg-admin-client 0.1.0 (git+https://github.com/oxidecomputer/maghemite?rev=0df320d42b356e689a3c7a7600eec9b16770237a)", + "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "omicron-workspace-hack", "oxnet", "parse-display", @@ -4012,7 +4591,7 @@ dependencies = [ "slog", "slog-error-chain", "strum 0.27.2", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "tufaceous-artifact", "uuid", @@ -4030,7 +4609,30 @@ dependencies = [ "secrecy", "serde", "serde_with", - "thiserror 2.0.16", + "thiserror 2.0.17", +] + +[[package]] +name = "omicron-passwords" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#9794876a45bda0d20c2d545b4f7f134ffd7d2086" +dependencies = [ + "argon2", + "omicron-workspace-hack", + "rand 0.9.2", + "schemars", + "secrecy", + "serde", + "serde_with", + "thiserror 2.0.17", +] + +[[package]] +name = "omicron-rpaths" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#9794876a45bda0d20c2d545b4f7f134ffd7d2086" +dependencies = [ + "omicron-workspace-hack", ] [[package]] @@ -4045,6 +4647,18 @@ dependencies = [ "schemars", ] +[[package]] +name = "omicron-uuid-kinds" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#9794876a45bda0d20c2d545b4f7f134ffd7d2086" +dependencies = [ + "daft", + "newtype-uuid", + "newtype-uuid-macros", + "paste", + "schemars", +] + [[package]] name = "omicron-workspace-hack" version = "0.1.0" @@ -4099,13 +4713,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 +4753,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -4197,7 +4817,7 @@ dependencies = [ "oxide-vpc", "postcard", "serde", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] @@ -4240,12 +4860,31 @@ dependencies = [ "chrono", "clap", "omicron-workspace-hack", - "oximeter-macro-impl", - "oximeter-schema", - "oximeter-timeseries-macro", - "oximeter-types", + "oximeter-macro-impl 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "oximeter-schema 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "oximeter-timeseries-macro 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "oximeter-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "prettyplease", + "syn 2.0.111", + "toml 0.8.23", + "uuid", +] + +[[package]] +name = "oximeter" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#9794876a45bda0d20c2d545b4f7f134ffd7d2086" +dependencies = [ + "anyhow", + "chrono", + "clap", + "omicron-workspace-hack", + "oximeter-macro-impl 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "oximeter-schema 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "oximeter-timeseries-macro 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "oximeter-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "prettyplease", - "syn 2.0.106", + "syn 2.0.111", "toml 0.8.23", "uuid", ] @@ -4272,15 +4911,68 @@ dependencies = [ "gethostname", "highway", "iana-time-zone", - "indexmap 2.11.4", + "indexmap 2.12.1", + "libc", + "nom", + "num", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-workspace-hack", + "oxide-tokio-rt", + "oximeter 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "oxql-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "parse-display", + "qorb", + "quote", + "regex", + "reqwest", + "schemars", + "serde", + "serde_json", + "slog", + "slog-async", + "slog-dtrace", + "slog-error-chain", + "slog-term", + "strum 0.27.2", + "termtree", + "thiserror 2.0.17", + "tokio", + "tokio-util", + "usdt 0.5.0", + "uuid", +] + +[[package]] +name = "oximeter-db" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#9794876a45bda0d20c2d545b4f7f134ffd7d2086" +dependencies = [ + "anyhow", + "async-recursion", + "async-trait", + "bcs", + "bytes", + "camino", + "chrono", + "chrono-tz", + "clap", + "clickward", + "const_format", + "debug-ignore", + "dropshot", + "futures", + "gethostname", + "highway", + "iana-time-zone", + "indexmap 2.12.1", "libc", "nom", "num", - "omicron-common", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "omicron-workspace-hack", "oxide-tokio-rt", - "oximeter", - "oxql-types", + "oximeter 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "oxql-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "parse-display", "qorb", "quote", @@ -4296,7 +4988,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 +4998,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=zl%2Fmcast-implicit-lifecycle#9794876a45bda0d20c2d545b4f7f134ffd7d2086" dependencies = [ "cfg-if", "chrono", @@ -4314,9 +5006,9 @@ dependencies = [ "kstat-rs", "libc", "omicron-workspace-hack", - "oximeter", + "oximeter 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "slog", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "uuid", ] @@ -4329,49 +5021,81 @@ dependencies = [ "omicron-workspace-hack", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", +] + +[[package]] +name = "oximeter-macro-impl" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#9794876a45bda0d20c2d545b4f7f134ffd7d2086" +dependencies = [ + "omicron-workspace-hack", + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "oximeter-producer" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#9794876a45bda0d20c2d545b4f7f134ffd7d2086" +dependencies = [ + "chrono", + "dropshot", + "internal-dns-resolver 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "internal-dns-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "nexus-client 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "omicron-workspace-hack", + "oximeter 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "schemars", + "serde", + "slog", + "slog-dtrace", + "thiserror 2.0.17", + "tokio", + "uuid", ] [[package]] -name = "oximeter-producer" +name = "oximeter-schema" version = "0.1.0" source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec600ca74e573a1926c1876e33b35" dependencies = [ + "anyhow", "chrono", - "dropshot", - "internal-dns-resolver", - "internal-dns-types", - "nexus-client", - "omicron-common", + "clap", + "heck 0.5.0", "omicron-workspace-hack", - "oximeter", + "oximeter-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "prettyplease", + "proc-macro2", + "quote", "schemars", "serde", - "slog", - "slog-dtrace", - "thiserror 2.0.16", - "tokio", - "uuid", + "slog-error-chain", + "syn 2.0.111", + "toml 0.8.23", ] [[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=zl%2Fmcast-implicit-lifecycle#9794876a45bda0d20c2d545b4f7f134ffd7d2086" dependencies = [ "anyhow", "chrono", "clap", "heck 0.5.0", "omicron-workspace-hack", - "oximeter-types", + "oximeter-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "prettyplease", "proc-macro2", "quote", "schemars", "serde", "slog-error-chain", - "syn 2.0.106", + "syn 2.0.111", "toml 0.8.23", ] @@ -4381,11 +5105,24 @@ version = "0.1.0" source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec600ca74e573a1926c1876e33b35" dependencies = [ "omicron-workspace-hack", - "oximeter-schema", - "oximeter-types", + "oximeter-schema 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "oximeter-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", +] + +[[package]] +name = "oximeter-timeseries-macro" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#9794876a45bda0d20c2d545b4f7f134ffd7d2086" +dependencies = [ + "omicron-workspace-hack", + "oximeter-schema 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "oximeter-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "proc-macro2", + "quote", + "syn 2.0.111", ] [[package]] @@ -4397,14 +5134,34 @@ dependencies = [ "chrono", "float-ord", "num", - "omicron-common", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-workspace-hack", + "parse-display", + "regex", + "schemars", + "serde", + "strum 0.27.2", + "thiserror 2.0.17", + "uuid", +] + +[[package]] +name = "oximeter-types" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#9794876a45bda0d20c2d545b4f7f134ffd7d2086" +dependencies = [ + "bytes", + "chrono", + "float-ord", + "num", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "omicron-workspace-hack", "parse-display", "regex", "schemars", "serde", "strum 0.27.2", - "thiserror 2.0.16", + "thiserror 2.0.17", "uuid", ] @@ -4425,11 +5182,28 @@ dependencies = [ "uuid", ] +[[package]] +name = "oxlog" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#9794876a45bda0d20c2d545b4f7f134ffd7d2086" +dependencies = [ + "anyhow", + "camino", + "chrono", + "clap", + "glob", + "jiff", + "omicron-workspace-hack", + "rayon", + "sigpipe", + "uuid", +] + [[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", @@ -4447,7 +5221,24 @@ dependencies = [ "highway", "num", "omicron-workspace-hack", - "oximeter-types", + "oximeter-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "schemars", + "serde", + "serde_json", + "uuid", +] + +[[package]] +name = "oxql-types" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#9794876a45bda0d20c2d545b4f7f134ffd7d2086" +dependencies = [ + "anyhow", + "chrono", + "highway", + "num", + "omicron-workspace-hack", + "oximeter-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "schemars", "serde", "serde_json", @@ -4558,7 +5349,7 @@ dependencies = [ "regex", "regex-syntax", "structmeta", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -4609,7 +5400,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 +5424,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -4653,7 +5444,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 +5457,7 @@ checksum = "54acf3a685220b533e437e264e4d932cfbdc4cc7ec0cd232ed73c08d03b8a7ca" dependencies = [ "fixedbitset 0.5.7", "hashbrown 0.15.5", - "indexmap 2.11.4", + "indexmap 2.12.1", "serde", ] @@ -4705,7 +5496,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -4732,6 +5523,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 +5585,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 +5645,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 +5701,7 @@ dependencies = [ "proc-macro-error-attr2", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -4960,7 +5773,7 @@ checksum = "b17e5363daa50bf1cccfade6b0fb970d2278758fd5cfa9ab69f25028e4b1afa3" dependencies = [ "heck 0.5.0", "http", - "indexmap 2.11.4", + "indexmap 2.12.1", "openapiv3", "proc-macro2", "quote", @@ -4968,8 +5781,8 @@ dependencies = [ "schemars", "serde", "serde_json", - "syn 2.0.106", - "thiserror 2.0.16", + "syn 2.0.111", + "thiserror 2.0.17", "typify", "unicode-ident", ] @@ -4982,7 +5795,7 @@ checksum = "8276d558f1dfd4cc7fc4cceee0a51dab482b5a4be2e69e7eab8c57fbfb1472f4" dependencies = [ "heck 0.5.0", "http", - "indexmap 2.11.4", + "indexmap 2.12.1", "openapiv3", "proc-macro2", "quote", @@ -4990,8 +5803,8 @@ dependencies = [ "schemars", "serde", "serde_json", - "syn 2.0.106", - "thiserror 2.0.16", + "syn 2.0.111", + "thiserror 2.0.17", "typify", "unicode-ident", ] @@ -5011,7 +5824,7 @@ dependencies = [ "serde_json", "serde_tokenstream", "serde_yaml", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -5029,7 +5842,7 @@ dependencies = [ "serde_json", "serde_tokenstream", "serde_yaml", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -5046,7 +5859,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 +5867,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,6 +5885,28 @@ dependencies = [ "zerocopy 0.8.27", ] +[[package]] +name = "propolis_api_types" +version = "0.0.0" +source = "git+https://github.com/oxidecomputer/propolis?rev=3f1752e6cee9a2f8ecdce6e2ad3326781182e2d9#3f1752e6cee9a2f8ecdce6e2ad3326781182e2d9" +dependencies = [ + "crucible-client-types", + "propolis_types 0.0.0 (git+https://github.com/oxidecomputer/propolis?rev=3f1752e6cee9a2f8ecdce6e2ad3326781182e2d9)", + "schemars", + "serde", + "thiserror 1.0.69", + "uuid", +] + +[[package]] +name = "propolis_types" +version = "0.0.0" +source = "git+https://github.com/oxidecomputer/propolis?rev=3f1752e6cee9a2f8ecdce6e2ad3326781182e2d9#3f1752e6cee9a2f8ecdce6e2ad3326781182e2d9" +dependencies = [ + "schemars", + "serde", +] + [[package]] name = "propolis_types" version = "0.0.0" @@ -5121,7 +5956,7 @@ dependencies = [ "itertools 0.14.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -5158,7 +5993,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 +6020,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 +6041,7 @@ dependencies = [ "rustls 0.23.32", "rustls-pki-types", "slab", - "thiserror 2.0.16", + "thiserror 2.0.17", "tinyvec", "tracing", "web-time", @@ -5335,6 +6170,16 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "rdb-types" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/maghemite?rev=0df320d42b356e689a3c7a7600eec9b16770237a#0df320d42b356e689a3c7a7600eec9b16770237a" +dependencies = [ + "oxnet", + "schemars", + "serde", +] + [[package]] name = "redox_syscall" version = "0.5.17" @@ -5361,7 +6206,7 @@ checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -5494,12 +6339,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" @@ -5708,7 +6547,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -5743,7 +6582,7 @@ checksum = "1783eabc414609e28a5ba76aee5ddd52199f7107a0b24c2e9746a1ecc34a683d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -5754,7 +6593,7 @@ checksum = "ed76efe62313ab6610570951494bdaa81568026e0318eaa55f167de70eeea67d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -5813,9 +6652,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 +6680,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,7 +6706,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -5920,7 +6759,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -5934,9 +6773,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 +6789,7 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -5990,7 +6829,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -5999,7 +6838,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 +6867,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,13 +6953,56 @@ version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" +[[package]] +name = "sled-agent-types-versions" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#9794876a45bda0d20c2d545b4f7f134ffd7d2086" +dependencies = [ + "async-trait", + "bootstore", + "camino", + "chrono", + "daft", + "iddqd", + "illumos-utils 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "indent_write", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "omicron-passwords 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "omicron-workspace-hack", + "oxnet", + "propolis_api_types", + "schemars", + "serde", + "serde_json", + "sha3", + "sled-hardware-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "slog", + "strum 0.27.2", + "thiserror 2.0.17", + "tufaceous-artifact", + "uuid", +] + [[package]] name = "sled-hardware-types" version = "0.1.0" source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec600ca74e573a1926c1876e33b35" dependencies = [ - "illumos-utils", - "omicron-common", + "illumos-utils 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-workspace-hack", + "schemars", + "serde", +] + +[[package]] +name = "sled-hardware-types" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#9794876a45bda0d20c2d545b4f7f134ffd7d2086" +dependencies = [ + "illumos-utils 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "omicron-workspace-hack", "schemars", "serde", @@ -6118,9 +7010,15 @@ dependencies = [ [[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 +7074,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 +7169,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -6371,7 +7269,7 @@ dependencies = [ "proc-macro2", "quote", "structmeta-derive", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -6382,7 +7280,7 @@ checksum = "152a0b65a590ff6c3da95cabe2353ee04e6167c896b28e3b14478c2636c922fc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -6413,7 +7311,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -6425,7 +7323,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -6480,9 +7378,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 +7404,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -6575,7 +7473,7 @@ dependencies = [ "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -6664,7 +7562,7 @@ dependencies = [ "proc-macro2", "quote", "structmeta", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -6707,9 +7605,9 @@ dependencies = [ "libc", "libnet 0.1.0 (git+https://github.com/oxidecomputer/netadm-sys)", "lldpd-client", - "omicron-common", + "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "oxide-tokio-rt", - "oximeter", + "oximeter 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", "oximeter-instruments", "oximeter-producer", "oxnet", @@ -6738,11 +7636,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 +7651,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 +7782,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 +7816,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 +7915,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 +7939,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 +7952,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 +7965,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 +7975,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 +7990,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 +8149,7 @@ checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -7268,7 +8193,7 @@ dependencies = [ "slog-async", "slog-term", "tabled 0.20.0", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "transceiver-decode", "transceiver-messages", @@ -7284,7 +8209,7 @@ dependencies = [ "schemars", "serde", "static_assertions", - "thiserror 2.0.16", + "thiserror 2.0.17", "transceiver-messages", ] @@ -7298,7 +8223,7 @@ dependencies = [ "hubpack", "schemars", "serde", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] @@ -7321,7 +8246,7 @@ dependencies = [ "serde_human_bytes", "strum 0.26.3", "test-strategy", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] @@ -7367,8 +8292,8 @@ dependencies = [ "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", ] @@ -7385,7 +8310,7 @@ dependencies = [ "serde", "serde_json", "serde_tokenstream", - "syn 2.0.106", + "syn 2.0.111", "typify-impl", ] @@ -7446,6 +8371,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" @@ -7477,7 +8412,37 @@ 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", + "serde", + "serde_json", + "serde_with", + "slog", + "swrite", + "tokio", + "unicode-width 0.1.14", + "uuid", +] + +[[package]] +name = "update-engine" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#9794876a45bda0d20c2d545b4f7f134ffd7d2086" +dependencies = [ + "anyhow", + "cancel-safe-futures", + "chrono", + "debug-ignore", + "derive-where", + "either", + "futures", + "indent_write", + "indexmap 2.12.1", "libsw", "linear-map", "omicron-workspace-hack", @@ -7563,7 +8528,7 @@ dependencies = [ "proc-macro2", "quote", "serde_tokenstream", - "syn 2.0.106", + "syn 2.0.111", "usdt-impl 0.5.0", ] @@ -7577,7 +8542,7 @@ dependencies = [ "proc-macro2", "quote", "serde_tokenstream", - "syn 2.0.106", + "syn 2.0.111", "usdt-impl 0.6.0", ] @@ -7595,7 +8560,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 +8580,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 +8595,7 @@ dependencies = [ "proc-macro2", "quote", "serde_tokenstream", - "syn 2.0.106", + "syn 2.0.111", "usdt-impl 0.5.0", ] @@ -7644,7 +8609,7 @@ dependencies = [ "proc-macro2", "quote", "serde_tokenstream", - "syn 2.0.106", + "syn 2.0.111", "usdt-impl 0.6.0", ] @@ -7662,13 +8627,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 +8689,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 +8797,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", "wasm-bindgen-shared", ] @@ -7849,7 +8832,7 @@ checksum = "ffc003a991398a8ee604a401e194b6b3a39677b3173d6e74495eb51b82e99a32" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -7974,7 +8957,7 @@ checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -7985,7 +8968,7 @@ checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -8379,7 +9362,7 @@ checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", "synstructure", ] @@ -8410,7 +9393,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -8421,7 +9404,7 @@ checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -8441,7 +9424,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", "synstructure", ] @@ -8450,6 +9433,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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] [[package]] name = "zerotrie" @@ -8481,7 +9478,7 @@ checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] From da0ccaed219992939e78d63f288c33c30ee916cb Mon Sep 17 00:00:00 2001 From: Zeeshan Lakhani Date: Wed, 7 Jan 2026 18:55:47 +0000 Subject: [PATCH 07/22] [multicast]: match-up with updates in Omicron/mcast-lifecycle work This diffset handles our move toward ASM groups being able to have source IPs. Includes: - Replace IpSrc::Subnet with IpSrc::Any for any-source multicast filtering - Add source filter normalization: when Any is present, collapse to single /0 entry; empty sources treated as allow-any - API versioning - Add tag ownership validation for group updates and deletes (v4 API) - Add v2/v3 API version adapters for backward compatibility - Fix test_service_ipv4_unknown_address to set NAT-only on correct port - https://github.com/oxidecomputer/dendrite/issues/172 - I had to fix this here, as it was failing consistently locally. --- .cargo/config.toml | 3 - Cargo.lock | 1112 +- Cargo.toml | 62 +- dpd-api/src/lib.rs | 290 +- dpd-api/src/v2.rs | 191 + dpd-api/src/v3.rs | 105 + dpd-client/tests/integration_tests/mcast.rs | 1721 ++- dpd-client/tests/integration_tests/service.rs | 21 +- .../tests/integration_tests/table_tests.rs | 12 +- dpd-types/src/link.rs | 2 + dpd-types/src/mcast.rs | 19 +- dpd/p4/sidecar.p4 | 21 +- dpd/src/api_server.rs | 37 +- dpd/src/mcast/mod.rs | 682 +- dpd/src/mcast/rollback.rs | 140 +- dpd/src/mcast/validate.rs | 188 +- dpd/src/port_map.rs | 2 +- dpd/src/table/mcast/mcast_egress.rs | 29 +- dpd/src/table/mcast/mcast_route.rs | 4 +- dpd/src/table/mcast/mcast_src_filter.rs | 11 +- dpd/src/types.rs | 8 + openapi/dpd/dpd-2.0.0-e62a18.json | 9643 ++++++++++++++++ ....0.0-4ba80a.json => dpd-3.0.0-6ed477.json} | 69 +- openapi/dpd/dpd-4.0.0-af315d.json | 9655 +++++++++++++++++ openapi/dpd/dpd-latest.json | 2 +- 25 files changed, 22593 insertions(+), 1436 deletions(-) create mode 100644 dpd-api/src/v2.rs create mode 100644 dpd-api/src/v3.rs create mode 100644 openapi/dpd/dpd-2.0.0-e62a18.json rename openapi/dpd/{dpd-2.0.0-4ba80a.json => dpd-3.0.0-6ed477.json} (99%) create mode 100644 openapi/dpd/dpd-4.0.0-af315d.json diff --git a/.cargo/config.toml b/.cargo/config.toml index bf041c98..10f68fd8 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -7,6 +7,3 @@ xtask = "run --package xtask --" [build] rustflags = ["--cfg", "tokio_unstable"] -# TODO: remove once we're targetting omicron `main` -[env] -DEP_PQ_LIBDIRS = "1" diff --git a/Cargo.lock b/Cargo.lock index 5a41bd6a..3166ec41 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7,9 +7,9 @@ name = "aal" version = "0.1.0" dependencies = [ "common 0.1.0", - "oximeter 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "oximeter", "oxnet", - "schemars", + "schemars 0.8.22", "serde", "serde_json", "thiserror 1.0.69", @@ -125,18 +125,7 @@ checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "api_identity" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec600ca74e573a1926c1876e33b35" -dependencies = [ - "omicron-workspace-hack", - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "api_identity" -version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#9794876a45bda0d20c2d545b4f7f134ffd7d2086" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#75fe22d9f9c7815e3ece509b4f7409eb5252e0d1" dependencies = [ "omicron-workspace-hack", "proc-macro2", @@ -188,10 +177,10 @@ dependencies = [ "dpd-api", "lazy_static", "libc", - "oximeter 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "oximeter", "propolis", "rand 0.9.2", - "schemars", + "schemars 0.8.22", "semver 1.0.27", "serde", "serde_json", @@ -396,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", ] @@ -416,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", @@ -549,7 +538,7 @@ dependencies = [ [[package]] name = "bootstore" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#9794876a45bda0d20c2d545b4f7f134ffd7d2086" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#75fe22d9f9c7815e3ece509b4f7409eb5252e0d1" dependencies = [ "bytes", "camino", @@ -558,7 +547,7 @@ dependencies = [ "derive_more", "hex", "hkdf", - "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "omicron-common", "omicron-rpaths", "omicron-workspace-hack", "pq-sys", @@ -567,7 +556,7 @@ dependencies = [ "serde", "serde_with", "sha3", - "sled-hardware-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "sled-hardware-types", "slog", "thiserror 2.0.17", "tokio", @@ -843,29 +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#75fe22d9f9c7815e3ece509b4f7409eb5252e0d1" dependencies = [ - "anyhow", - "atomicwrites", - "camino", - "camino-tempfile", - "chrono", - "daft", - "derive_more", - "expectorate", - "itertools 0.14.0", - "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "clickhouse-admin-types-versions", "omicron-workspace-hack", - "schemars", - "serde", - "serde_json", - "slog", ] [[package]] -name = "clickhouse-admin-types" +name = "clickhouse-admin-types-versions" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#9794876a45bda0d20c2d545b4f7f134ffd7d2086" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#75fe22d9f9c7815e3ece509b4f7409eb5252e0d1" dependencies = [ "anyhow", "atomicwrites", @@ -876,9 +852,9 @@ dependencies = [ "derive_more", "expectorate", "itertools 0.14.0", - "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "omicron-common", "omicron-workspace-hack", - "schemars", + "schemars 0.8.22", "serde", "serde_json", "slog", @@ -893,7 +869,7 @@ dependencies = [ "camino", "clap", "derive_more", - "schemars", + "schemars 0.8.22", "serde", "serde_json", "thiserror 1.0.69", @@ -921,27 +897,23 @@ dependencies = [ [[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#75fe22d9f9c7815e3ece509b4f7409eb5252e0d1" dependencies = [ - "chrono", - "csv", - "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "cockroach-admin-types-versions", "omicron-workspace-hack", - "schemars", "serde", - "thiserror 2.0.17", ] [[package]] -name = "cockroach-admin-types" +name = "cockroach-admin-types-versions" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#9794876a45bda0d20c2d545b4f7f134ffd7d2086" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#75fe22d9f9c7815e3ece509b4f7409eb5252e0d1" dependencies = [ "chrono", "csv", - "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "omicron-uuid-kinds", "omicron-workspace-hack", - "schemars", + "schemars 0.8.22", "serde", "thiserror 2.0.17", ] @@ -967,10 +939,10 @@ version = "0.1.0" dependencies = [ "anyhow", "chrono", - "oximeter 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "oximeter", "oxnet", "rand 0.9.2", - "schemars", + "schemars 0.8.22", "serde", "serde_json", "slog", @@ -989,10 +961,10 @@ source = "git+https://github.com/oxidecomputer/dendrite?branch=main#606c0be888f4 dependencies = [ "anyhow", "chrono", - "oximeter 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "oximeter", "oxnet", "rand 0.9.2", - "schemars", + "schemars 0.8.22", "serde", "serde_json", "slog", @@ -1196,7 +1168,7 @@ source = "git+https://github.com/oxidecomputer/crucible?rev=7103cd3a3d7b0112d294 dependencies = [ "base64 0.22.1", "crucible-workspace-hack", - "schemars", + "schemars 0.8.22", "serde", "serde_json", "uuid", @@ -1470,6 +1442,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d630bccd429a5bb5a64b5e94f693bfc48c9f8566418fda4c494cc94f911f87cc" dependencies = [ "powerfmt", + "serde", ] [[package]] @@ -1567,7 +1540,7 @@ dependencies = [ [[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)", @@ -1631,24 +1604,24 @@ dependencies = [ "dropshot", "expectorate", "futures", - "gateway-client 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", - "gateway-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", - "internal-dns-resolver 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", - "internal-dns-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "gateway-client", + "gateway-types", + "internal-dns-resolver", + "internal-dns-types", "libc", "mockall", - "nexus-client 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", - "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "nexus-client", + "omicron-common", "openssl", "oxide-tokio-rt", - "oximeter 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "oximeter", "oximeter-producer", "oxnet", "rand 0.9.2", "regex", "regress", "reqwest", - "schemars", + "schemars 0.8.22", "semver 1.0.27", "serde", "serde_json", @@ -1677,7 +1650,7 @@ dependencies = [ "dropshot", "dropshot-api-manager-types", "oxnet", - "schemars", + "schemars 0.8.22", "serde", "transceiver-controller", ] @@ -1705,7 +1678,7 @@ dependencies = [ "rand 0.9.2", "regress", "reqwest", - "schemars", + "schemars 0.8.22", "serde", "serde_json", "slog", @@ -1732,7 +1705,7 @@ dependencies = [ "progenitor 0.11.1", "regress", "reqwest", - "schemars", + "schemars 0.8.22", "serde", "serde_json", "slog", @@ -1748,9 +1721,9 @@ dependencies = [ "aal", "chrono", "common 0.1.0", - "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "omicron-common", "oxnet", - "schemars", + "schemars 0.8.22", "serde", "thiserror 1.0.69", "transceiver-controller", @@ -1799,7 +1772,7 @@ dependencies = [ "percent-encoding", "rustls 0.22.4", "rustls-pemfile", - "schemars", + "schemars 0.8.22", "scopeguard", "semver 1.0.27", "serde", @@ -2006,26 +1979,12 @@ dependencies = [ [[package]] name = "ereport-types" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec600ca74e573a1926c1876e33b35" -dependencies = [ - "dropshot", - "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", - "omicron-workspace-hack", - "schemars", - "serde", - "serde_json", - "thiserror 2.0.17", -] - -[[package]] -name = "ereport-types" -version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#9794876a45bda0d20c2d545b4f7f134ffd7d2086" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#75fe22d9f9c7815e3ece509b4f7409eb5252e0d1" dependencies = [ "dropshot", - "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "omicron-uuid-kinds", "omicron-workspace-hack", - "schemars", + "schemars 0.8.22", "serde", "serde_json", "thiserror 2.0.17", @@ -2309,45 +2268,20 @@ dependencies = [ [[package]] name = "gateway-client" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec600ca74e573a1926c1876e33b35" -dependencies = [ - "base64 0.22.1", - "chrono", - "daft", - "ereport-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", - "gateway-messages 0.1.0 (git+https://github.com/oxidecomputer/management-gateway-service?rev=669fe557b66f44aed3c622bd17bc092f08797e0c)", - "gateway-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", - "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", - "omicron-workspace-hack", - "progenitor 0.10.0", - "rand 0.9.2", - "reqwest", - "schemars", - "serde", - "serde_json", - "slog", - "thiserror 2.0.17", - "tokio", - "uuid", -] - -[[package]] -name = "gateway-client" -version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#9794876a45bda0d20c2d545b4f7f134ffd7d2086" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#75fe22d9f9c7815e3ece509b4f7409eb5252e0d1" dependencies = [ "base64 0.22.1", "chrono", "daft", - "ereport-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", - "gateway-messages 0.1.0 (git+https://github.com/oxidecomputer/management-gateway-service?rev=ea2f39ccdea124b5affcad0ca17bc5dacf65823a)", - "gateway-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", - "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "ereport-types", + "gateway-messages", + "gateway-types", + "omicron-uuid-kinds", "omicron-workspace-hack", "progenitor 0.10.0", "rand 0.9.2", "reqwest", - "schemars", + "schemars 0.8.22", "serde", "serde_json", "slog", @@ -2356,23 +2290,6 @@ dependencies = [ "uuid", ] -[[package]] -name = "gateway-messages" -version = "0.1.0" -source = "git+https://github.com/oxidecomputer/management-gateway-service?rev=669fe557b66f44aed3c622bd17bc092f08797e0c#669fe557b66f44aed3c622bd17bc092f08797e0c" -dependencies = [ - "bitflags 2.9.4", - "hubpack", - "serde", - "serde-big-array", - "serde_repr", - "static_assertions", - "strum 0.27.2", - "strum_macros 0.27.2", - "uuid", - "zerocopy 0.8.27", -] - [[package]] name = "gateway-messages" version = "0.1.0" @@ -2393,26 +2310,7 @@ dependencies = [ [[package]] name = "gateway-types" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec600ca74e573a1926c1876e33b35" -dependencies = [ - "daft", - "dropshot", - "gateway-messages 0.1.0 (git+https://github.com/oxidecomputer/management-gateway-service?rev=669fe557b66f44aed3c622bd17bc092f08797e0c)", - "hex", - "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", - "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", - "omicron-workspace-hack", - "schemars", - "serde", - "thiserror 2.0.17", - "tufaceous-artifact", - "uuid", -] - -[[package]] -name = "gateway-types" -version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#9794876a45bda0d20c2d545b4f7f134ffd7d2086" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#75fe22d9f9c7815e3ece509b4f7409eb5252e0d1" dependencies = [ "gateway-types-versions", "omicron-workspace-hack", @@ -2421,15 +2319,15 @@ dependencies = [ [[package]] name = "gateway-types-versions" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#9794876a45bda0d20c2d545b4f7f134ffd7d2086" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#75fe22d9f9c7815e3ece509b4f7409eb5252e0d1" dependencies = [ "daft", "dropshot", - "gateway-messages 0.1.0 (git+https://github.com/oxidecomputer/management-gateway-service?rev=ea2f39ccdea124b5affcad0ca17bc5dacf65823a)", + "gateway-messages", "hex", - "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "omicron-uuid-kinds", "omicron-workspace-hack", - "schemars", + "schemars 0.8.22", "serde", "thiserror 2.0.17", "tufaceous-artifact", @@ -2484,6 +2382,22 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "gfss" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#75fe22d9f9c7815e3ece509b4f7409eb5252e0d1" +dependencies = [ + "digest", + "omicron-workspace-hack", + "rand 0.9.2", + "schemars 0.8.22", + "secrecy", + "serde", + "subtle", + "thiserror 2.0.17", + "zeroize", +] + [[package]] name = "git2" version = "0.19.0" @@ -3098,7 +3012,7 @@ dependencies = [ "hashbrown 0.16.1", "ref-cast", "rustc-hash", - "schemars", + "schemars 0.8.22", "serde_core", "serde_json", ] @@ -3151,55 +3065,16 @@ dependencies = [ [[package]] name = "illumos-utils" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec600ca74e573a1926c1876e33b35" -dependencies = [ - "anyhow", - "async-trait", - "bhyve_api 0.0.0 (git+https://github.com/oxidecomputer/propolis?rev=3f1752e6cee9a2f8ecdce6e2ad3326781182e2d9)", - "byteorder", - "camino", - "camino-tempfile", - "cfg-if", - "crucible-smf", - "debug-ignore", - "dropshot", - "futures", - "http", - "ipnetwork", - "itertools 0.14.0", - "libc", - "macaddr", - "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", - "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", - "omicron-workspace-hack", - "opte-ioctl", - "oxide-vpc", - "oxlog 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", - "oxnet", - "schemars", - "serde", - "slog", - "slog-error-chain", - "smf 0.2.3", - "thiserror 2.0.17", - "tokio", - "uuid", - "whoami", - "zone", -] - -[[package]] -name = "illumos-utils" -version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#9794876a45bda0d20c2d545b4f7f134ffd7d2086" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#75fe22d9f9c7815e3ece509b4f7409eb5252e0d1" 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", @@ -3210,17 +3085,19 @@ dependencies = [ "libc", "macaddr", "nix", - "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", - "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "omicron-common", + "omicron-uuid-kinds", "omicron-workspace-hack", "opte-ioctl", "oxide-vpc", - "oxlog 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "oxlog", "oxnet", - "schemars", + "schemars 0.8.22", "serde", "slog", + "slog-async", "slog-error-chain", + "slog-term", "smf 0.2.3", "thiserror 2.0.17", "tofino 0.1.0 (git+https://github.com/oxidecomputer/tofino)", @@ -3244,6 +3121,7 @@ checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", "hashbrown 0.12.3", + "serde", ] [[package]] @@ -3318,32 +3196,14 @@ dependencies = [ [[package]] name = "internal-dns-resolver" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec600ca74e573a1926c1876e33b35" -dependencies = [ - "futures", - "hickory-proto 0.25.2", - "hickory-resolver 0.25.2", - "internal-dns-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", - "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", - "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", - "omicron-workspace-hack", - "qorb", - "reqwest", - "slog", - "thiserror 2.0.17", -] - -[[package]] -name = "internal-dns-resolver" -version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#9794876a45bda0d20c2d545b4f7f134ffd7d2086" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#75fe22d9f9c7815e3ece509b4f7409eb5252e0d1" dependencies = [ "futures", "hickory-proto 0.25.2", "hickory-resolver 0.25.2", - "internal-dns-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", - "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", - "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "internal-dns-types", + "omicron-common", + "omicron-uuid-kinds", "omicron-workspace-hack", "qorb", "reqwest", @@ -3354,31 +3214,30 @@ dependencies = [ [[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#75fe22d9f9c7815e3ece509b4f7409eb5252e0d1" dependencies = [ "anyhow", "chrono", - "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", - "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "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" +name = "internal-dns-types-versions" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#9794876a45bda0d20c2d545b4f7f134ffd7d2086" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#75fe22d9f9c7815e3ece509b4f7409eb5252e0d1" dependencies = [ "anyhow", "chrono", - "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", - "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "omicron-common", "omicron-workspace-hack", - "schemars", + "schemars 0.8.22", "serde", - "strum 0.27.2", ] [[package]] @@ -3411,7 +3270,7 @@ version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf370abdafd54d13e54a620e8c3e1145f28e46cc9d704bc6d94414559df41763" dependencies = [ - "schemars", + "schemars 0.8.22", "serde", ] @@ -3589,7 +3448,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" @@ -3632,7 +3491,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", @@ -3767,7 +3626,7 @@ dependencies = [ "progenitor 0.11.1", "protocol", "reqwest", - "schemars", + "schemars 0.8.22", "serde", "serde_json", "slog", @@ -3781,7 +3640,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", @@ -3895,30 +3754,14 @@ dependencies = [ [[package]] name = "mg-admin-client" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/maghemite?rev=08f2a34d487658e87545ffbba3add632a82baf0d#08f2a34d487658e87545ffbba3add632a82baf0d" -dependencies = [ - "anyhow", - "chrono", - "percent-encoding", - "progenitor 0.11.1", - "reqwest", - "schemars", - "serde", - "serde_json", - "slog", -] - -[[package]] -name = "mg-admin-client" -version = "0.1.0" -source = "git+https://github.com/oxidecomputer/maghemite?rev=0df320d42b356e689a3c7a7600eec9b16770237a#0df320d42b356e689a3c7a7600eec9b16770237a" +source = "git+https://github.com/oxidecomputer/maghemite?rev=205b3ccf75b527ac7a565285fdcc0c78f4fcee95#205b3ccf75b527ac7a565285fdcc0c78f4fcee95" dependencies = [ "chrono", "colored", "progenitor 0.11.1", "rdb-types", "reqwest", - "schemars", + "schemars 0.8.22", "serde", "serde_json", "slog", @@ -4051,7 +3894,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", @@ -4083,190 +3926,77 @@ dependencies = [ [[package]] name = "nexus-client" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec600ca74e573a1926c1876e33b35" -dependencies = [ - "chrono", - "futures", - "iddqd", - "nexus-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", - "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", - "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", - "omicron-workspace-hack", - "oxnet", - "progenitor 0.10.0", - "regress", - "reqwest", - "schemars", - "serde", - "serde_json", - "slog", - "uuid", -] - -[[package]] -name = "nexus-client" -version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#9794876a45bda0d20c2d545b4f7f134ffd7d2086" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#75fe22d9f9c7815e3ece509b4f7409eb5252e0d1" dependencies = [ "chrono", "futures", "iddqd", - "nexus-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", - "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", - "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "nexus-types", + "omicron-common", + "omicron-uuid-kinds", "omicron-workspace-hack", "oxnet", "progenitor 0.10.0", "regress", "reqwest", - "schemars", - "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 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", - "indent_write", - "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", - "omicron-passwords 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", - "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", - "omicron-workspace-hack", - "schemars", - "serde", - "serde_json", - "sled-hardware-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", - "strum 0.27.2", - "thiserror 2.0.17", - "tufaceous-artifact", - "uuid", -] - -[[package]] -name = "nexus-types" -version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec600ca74e573a1926c1876e33b35" -dependencies = [ - "anyhow", - "api_identity 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", - "async-trait", - "base64 0.22.1", - "chrono", - "clap", - "clickhouse-admin-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", - "cockroach-admin-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", - "cookie", - "daft", - "derive-where", - "derive_more", - "dropshot", - "ereport-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", - "futures", - "gateway-client 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", - "gateway-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", - "http", - "humantime", - "iddqd", - "illumos-utils 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", - "indent_write", - "internal-dns-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", - "ipnetwork", - "itertools 0.14.0", - "newtype-uuid", - "newtype_derive", - "nexus-sled-agent-shared", - "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", - "omicron-passwords 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", - "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", - "omicron-workspace-hack", - "openssl", - "oximeter-db 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", - "oxnet", - "oxql-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", - "parse-display", - "regex", - "schemars", - "semver 1.0.27", + "schemars 0.8.22", "serde", "serde_json", - "serde_with", - "sled-hardware-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", "slog", - "slog-error-chain", - "steno", - "strum 0.27.2", - "swrite", - "tabled 0.15.0", - "test-strategy", - "textwrap", - "thiserror 2.0.17", - "tokio", - "tough", - "tufaceous-artifact", - "unicode-width 0.1.14", - "update-engine 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", - "url", "uuid", ] [[package]] name = "nexus-types" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#9794876a45bda0d20c2d545b4f7f134ffd7d2086" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#75fe22d9f9c7815e3ece509b4f7409eb5252e0d1" dependencies = [ "anyhow", - "api_identity 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "api_identity", "async-trait", "base64 0.22.1", "chrono", "clap", - "clickhouse-admin-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", - "cockroach-admin-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "clickhouse-admin-types", + "cockroach-admin-types", "cookie", "daft", "derive-where", "derive_more", "dropshot", - "ereport-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "ereport-types", "futures", - "gateway-client 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", - "gateway-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "gateway-client", + "gateway-types", "http", "humantime", "iddqd", - "illumos-utils 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "illumos-utils", "indent_write", - "internal-dns-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "internal-dns-types", + "ipnet", "ipnetwork", "itertools 0.14.0", "newtype-uuid", "newtype_derive", - "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", - "omicron-passwords 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", - "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "omicron-common", + "omicron-passwords", + "omicron-uuid-kinds", "omicron-workspace-hack", "openssl", - "oximeter-db 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "oximeter-db", "oxnet", - "oxql-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "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 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "sled-hardware-types", "slog", "slog-error-chain", "steno", @@ -4278,9 +4008,11 @@ dependencies = [ "thiserror 2.0.17", "tokio", "tough", + "trust-quorum-protocol", + "trust-quorum-types", "tufaceous-artifact", "unicode-width 0.1.14", - "update-engine 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "update-engine", "url", "uuid", ] @@ -4511,10 +4243,10 @@ 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#75fe22d9f9c7815e3ece509b4f7409eb5252e0d1" dependencies = [ "anyhow", - "api_identity 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "api_identity", "async-trait", "backoff", "camino", @@ -4526,9 +4258,10 @@ dependencies = [ "http", "iddqd", "ipnetwork", + "itertools 0.14.0", "macaddr", - "mg-admin-client 0.1.0 (git+https://github.com/oxidecomputer/maghemite?rev=08f2a34d487658e87545ffbba3add632a82baf0d)", - "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "mg-admin-client", + "omicron-uuid-kinds", "omicron-workspace-hack", "oxnet", "parse-display", @@ -4537,7 +4270,7 @@ dependencies = [ "rand 0.9.2", "regress", "reqwest", - "schemars", + "schemars 0.8.22", "semver 1.0.27", "serde", "serde_human_bytes", @@ -4552,75 +4285,15 @@ dependencies = [ "uuid", ] -[[package]] -name = "omicron-common" -version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#9794876a45bda0d20c2d545b4f7f134ffd7d2086" -dependencies = [ - "anyhow", - "api_identity 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", - "async-trait", - "backoff", - "camino", - "chrono", - "daft", - "dropshot", - "futures", - "hex", - "http", - "iddqd", - "ipnetwork", - "itertools 0.14.0", - "macaddr", - "mg-admin-client 0.1.0 (git+https://github.com/oxidecomputer/maghemite?rev=0df320d42b356e689a3c7a7600eec9b16770237a)", - "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", - "omicron-workspace-hack", - "oxnet", - "parse-display", - "progenitor-client 0.10.0", - "protocol", - "rand 0.9.2", - "regress", - "reqwest", - "schemars", - "semver 1.0.27", - "serde", - "serde_human_bytes", - "serde_json", - "serde_with", - "slog", - "slog-error-chain", - "strum 0.27.2", - "thiserror 2.0.17", - "tokio", - "tufaceous-artifact", - "uuid", -] - -[[package]] -name = "omicron-passwords" -version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec600ca74e573a1926c1876e33b35" -dependencies = [ - "argon2", - "omicron-workspace-hack", - "rand 0.9.2", - "schemars", - "secrecy", - "serde", - "serde_with", - "thiserror 2.0.17", -] - [[package]] name = "omicron-passwords" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#9794876a45bda0d20c2d545b4f7f134ffd7d2086" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#75fe22d9f9c7815e3ece509b4f7409eb5252e0d1" dependencies = [ "argon2", "omicron-workspace-hack", "rand 0.9.2", - "schemars", + "schemars 0.8.22", "secrecy", "serde", "serde_with", @@ -4630,7 +4303,7 @@ dependencies = [ [[package]] name = "omicron-rpaths" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#9794876a45bda0d20c2d545b4f7f134ffd7d2086" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#75fe22d9f9c7815e3ece509b4f7409eb5252e0d1" dependencies = [ "omicron-workspace-hack", ] @@ -4638,25 +4311,13 @@ dependencies = [ [[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#75fe22d9f9c7815e3ece509b4f7409eb5252e0d1" dependencies = [ "daft", "newtype-uuid", "newtype-uuid-macros", "paste", - "schemars", -] - -[[package]] -name = "omicron-uuid-kinds" -version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#9794876a45bda0d20c2d545b4f7f134ffd7d2086" -dependencies = [ - "daft", - "newtype-uuid", - "newtype-uuid-macros", - "paste", - "schemars", + "schemars 0.8.22", ] [[package]] @@ -4854,35 +4515,16 @@ dependencies = [ [[package]] name = "oximeter" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec600ca74e573a1926c1876e33b35" -dependencies = [ - "anyhow", - "chrono", - "clap", - "omicron-workspace-hack", - "oximeter-macro-impl 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", - "oximeter-schema 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", - "oximeter-timeseries-macro 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", - "oximeter-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", - "prettyplease", - "syn 2.0.111", - "toml 0.8.23", - "uuid", -] - -[[package]] -name = "oximeter" -version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#9794876a45bda0d20c2d545b4f7f134ffd7d2086" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#75fe22d9f9c7815e3ece509b4f7409eb5252e0d1" dependencies = [ "anyhow", "chrono", "clap", "omicron-workspace-hack", - "oximeter-macro-impl 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", - "oximeter-schema 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", - "oximeter-timeseries-macro 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", - "oximeter-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "oximeter-macro-impl", + "oximeter-schema", + "oximeter-timeseries-macro", + "oximeter-types", "prettyplease", "syn 2.0.111", "toml 0.8.23", @@ -4892,60 +4534,7 @@ dependencies = [ [[package]] name = "oximeter-db" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec600ca74e573a1926c1876e33b35" -dependencies = [ - "anyhow", - "async-recursion", - "async-trait", - "bcs", - "bytes", - "camino", - "chrono", - "chrono-tz", - "clap", - "clickward", - "const_format", - "debug-ignore", - "dropshot", - "futures", - "gethostname", - "highway", - "iana-time-zone", - "indexmap 2.12.1", - "libc", - "nom", - "num", - "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", - "omicron-workspace-hack", - "oxide-tokio-rt", - "oximeter 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", - "oxql-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", - "parse-display", - "qorb", - "quote", - "regex", - "reqwest", - "schemars", - "serde", - "serde_json", - "slog", - "slog-async", - "slog-dtrace", - "slog-error-chain", - "slog-term", - "strum 0.27.2", - "termtree", - "thiserror 2.0.17", - "tokio", - "tokio-util", - "usdt 0.5.0", - "uuid", -] - -[[package]] -name = "oximeter-db" -version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#9794876a45bda0d20c2d545b4f7f134ffd7d2086" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#75fe22d9f9c7815e3ece509b4f7409eb5252e0d1" dependencies = [ "anyhow", "async-recursion", @@ -4968,17 +4557,17 @@ dependencies = [ "libc", "nom", "num", - "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "omicron-common", "omicron-workspace-hack", "oxide-tokio-rt", - "oximeter 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", - "oxql-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "oximeter", + "oxql-types", "parse-display", "qorb", "quote", "regex", "reqwest", - "schemars", + "schemars 0.8.22", "serde", "serde_json", "slog", @@ -4998,7 +4587,7 @@ dependencies = [ [[package]] name = "oximeter-instruments" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#9794876a45bda0d20c2d545b4f7f134ffd7d2086" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#75fe22d9f9c7815e3ece509b4f7409eb5252e0d1" dependencies = [ "cfg-if", "chrono", @@ -5006,7 +4595,7 @@ dependencies = [ "kstat-rs", "libc", "omicron-workspace-hack", - "oximeter 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "oximeter", "slog", "thiserror 2.0.17", "tokio", @@ -5016,18 +4605,7 @@ dependencies = [ [[package]] name = "oximeter-macro-impl" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec600ca74e573a1926c1876e33b35" -dependencies = [ - "omicron-workspace-hack", - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "oximeter-macro-impl" -version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#9794876a45bda0d20c2d545b4f7f134ffd7d2086" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#75fe22d9f9c7815e3ece509b4f7409eb5252e0d1" dependencies = [ "omicron-workspace-hack", "proc-macro2", @@ -5038,17 +4616,17 @@ dependencies = [ [[package]] name = "oximeter-producer" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#9794876a45bda0d20c2d545b4f7f134ffd7d2086" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#75fe22d9f9c7815e3ece509b4f7409eb5252e0d1" dependencies = [ "chrono", "dropshot", - "internal-dns-resolver 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", - "internal-dns-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", - "nexus-client 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", - "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "internal-dns-resolver", + "internal-dns-types", + "nexus-client", + "omicron-common", "omicron-workspace-hack", - "oximeter 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", - "schemars", + "oximeter", + "schemars 0.8.22", "serde", "slog", "slog-dtrace", @@ -5060,39 +4638,18 @@ dependencies = [ [[package]] name = "oximeter-schema" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec600ca74e573a1926c1876e33b35" -dependencies = [ - "anyhow", - "chrono", - "clap", - "heck 0.5.0", - "omicron-workspace-hack", - "oximeter-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", - "prettyplease", - "proc-macro2", - "quote", - "schemars", - "serde", - "slog-error-chain", - "syn 2.0.111", - "toml 0.8.23", -] - -[[package]] -name = "oximeter-schema" -version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#9794876a45bda0d20c2d545b4f7f134ffd7d2086" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#75fe22d9f9c7815e3ece509b4f7409eb5252e0d1" dependencies = [ "anyhow", "chrono", "clap", "heck 0.5.0", "omicron-workspace-hack", - "oximeter-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "oximeter-types", "prettyplease", "proc-macro2", "quote", - "schemars", + "schemars 0.8.22", "serde", "slog-error-chain", "syn 2.0.111", @@ -5102,24 +4659,11 @@ dependencies = [ [[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#75fe22d9f9c7815e3ece509b4f7409eb5252e0d1" dependencies = [ "omicron-workspace-hack", - "oximeter-schema 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", - "oximeter-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "oximeter-timeseries-macro" -version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#9794876a45bda0d20c2d545b4f7f134ffd7d2086" -dependencies = [ - "omicron-workspace-hack", - "oximeter-schema 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", - "oximeter-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "oximeter-schema", + "oximeter-types", "proc-macro2", "quote", "syn 2.0.111", @@ -5128,17 +4672,18 @@ dependencies = [ [[package]] name = "oximeter-types" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec600ca74e573a1926c1876e33b35" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#75fe22d9f9c7815e3ece509b4f7409eb5252e0d1" dependencies = [ "bytes", "chrono", "float-ord", "num", - "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-common", "omicron-workspace-hack", + "oximeter-types-versions", "parse-display", "regex", - "schemars", + "schemars 0.8.22", "serde", "strum 0.27.2", "thiserror 2.0.17", @@ -5146,46 +4691,22 @@ dependencies = [ ] [[package]] -name = "oximeter-types" +name = "oximeter-types-versions" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#9794876a45bda0d20c2d545b4f7f134ffd7d2086" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#75fe22d9f9c7815e3ece509b4f7409eb5252e0d1" dependencies = [ - "bytes", "chrono", - "float-ord", - "num", - "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "omicron-common", "omicron-workspace-hack", - "parse-display", - "regex", - "schemars", + "schemars 0.8.22", "serde", - "strum 0.27.2", - "thiserror 2.0.17", "uuid", ] [[package]] name = "oxlog" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec600ca74e573a1926c1876e33b35" -dependencies = [ - "anyhow", - "camino", - "chrono", - "clap", - "glob", - "jiff", - "omicron-workspace-hack", - "rayon", - "sigpipe", - "uuid", -] - -[[package]] -name = "oxlog" -version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#9794876a45bda0d20c2d545b4f7f134ffd7d2086" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#75fe22d9f9c7815e3ece509b4f7409eb5252e0d1" dependencies = [ "anyhow", "camino", @@ -5206,40 +4727,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5dc6fb07ecd6d2a17ff1431bc5b3ce11036c0b6dd93a3c4904db5b910817b162" dependencies = [ "ipnetwork", - "schemars", - "serde", - "serde_json", -] - -[[package]] -name = "oxql-types" -version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec600ca74e573a1926c1876e33b35" -dependencies = [ - "anyhow", - "chrono", - "highway", - "num", - "omicron-workspace-hack", - "oximeter-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", - "schemars", + "schemars 0.8.22", "serde", "serde_json", - "uuid", ] [[package]] name = "oxql-types" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#9794876a45bda0d20c2d545b4f7f134ffd7d2086" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#75fe22d9f9c7815e3ece509b4f7409eb5252e0d1" dependencies = [ "anyhow", "chrono", "highway", "num", "omicron-workspace-hack", - "oximeter-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", - "schemars", + "oximeter-types", + "schemars 0.8.22", "serde", "serde_json", "uuid", @@ -5276,7 +4780,7 @@ dependencies = [ "common 0.1.0", "hex-literal", "rand 0.9.2", - "schemars", + "schemars 0.8.22", "serde", "serde_json", "thiserror 1.0.69", @@ -5778,7 +5282,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "schemars", + "schemars 0.8.22", "serde", "serde_json", "syn 2.0.111", @@ -5800,7 +5304,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "schemars", + "schemars 0.8.22", "serde", "serde_json", "syn 2.0.111", @@ -5819,7 +5323,7 @@ dependencies = [ "proc-macro2", "progenitor-impl 0.10.0", "quote", - "schemars", + "schemars 0.8.22", "serde", "serde_json", "serde_tokenstream", @@ -5837,7 +5341,7 @@ dependencies = [ "proc-macro2", "progenitor-impl 0.11.1", "quote", - "schemars", + "schemars 0.8.22", "serde", "serde_json", "serde_tokenstream", @@ -5888,11 +5392,11 @@ dependencies = [ [[package]] name = "propolis_api_types" 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 = [ "crucible-client-types", - "propolis_types 0.0.0 (git+https://github.com/oxidecomputer/propolis?rev=3f1752e6cee9a2f8ecdce6e2ad3326781182e2d9)", - "schemars", + "propolis_types 0.0.0 (git+https://github.com/oxidecomputer/propolis?rev=2dc643742f82d2e072a1281dab23ba2bfdcee440)", + "schemars 0.8.22", "serde", "thiserror 1.0.69", "uuid", @@ -5901,9 +5405,9 @@ dependencies = [ [[package]] name = "propolis_types" 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 = [ - "schemars", + "schemars 0.8.22", "serde", ] @@ -5912,7 +5416,7 @@ name = "propolis_types" version = "0.0.0" source = "git+https://github.com/oxidecomputer/propolis#827e6615bfebfd94d41504dcd1517a0f22e3166a" dependencies = [ - "schemars", + "schemars 0.8.22", "serde", ] @@ -5971,10 +5475,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", ] @@ -6173,10 +5677,10 @@ dependencies = [ [[package]] name = "rdb-types" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/maghemite?rev=0df320d42b356e689a3c7a7600eec9b16770237a#0df320d42b356e689a3c7a7600eec9b16770237a" +source = "git+https://github.com/oxidecomputer/maghemite?rev=205b3ccf75b527ac7a565285fdcc0c78f4fcee95#205b3ccf75b527ac7a565285fdcc0c78f4fcee95" dependencies = [ "oxnet", - "schemars", + "schemars 0.8.22", "serde", ] @@ -6538,6 +6042,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" @@ -6813,6 +6341,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", @@ -6954,58 +6487,83 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" [[package]] -name = "sled-agent-types-versions" +name = "sled-agent-types" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#9794876a45bda0d20c2d545b4f7f134ffd7d2086" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#75fe22d9f9c7815e3ece509b4f7409eb5252e0d1" dependencies = [ + "anyhow", "async-trait", "bootstore", "camino", "chrono", "daft", "iddqd", - "illumos-utils 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", - "indent_write", - "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", - "omicron-passwords 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", - "omicron-uuid-kinds 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "omicron-common", + "omicron-uuid-kinds", "omicron-workspace-hack", "oxnet", - "propolis_api_types", - "schemars", + "schemars 0.8.22", "serde", + "serde_human_bytes", "serde_json", - "sha3", - "sled-hardware-types 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "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-hardware-types" +name = "sled-agent-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#75fe22d9f9c7815e3ece509b4f7409eb5252e0d1" dependencies = [ - "illumos-utils 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", - "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "async-trait", + "bootstore", + "camino", + "chrono", + "daft", + "iddqd", + "illumos-utils", + "indent_write", + "omicron-common", + "omicron-passwords", + "omicron-uuid-kinds", "omicron-workspace-hack", - "schemars", + "oxnet", + "propolis_api_types", + "schemars 0.8.22", "serde", + "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=zl%2Fmcast-implicit-lifecycle#9794876a45bda0d20c2d545b4f7f134ffd7d2086" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#75fe22d9f9c7815e3ece509b4f7409eb5252e0d1" dependencies = [ - "illumos-utils 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", - "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "daft", + "illumos-utils", + "omicron-common", "omicron-workspace-hack", - "schemars", + "schemars 0.8.22", "serde", + "slog", + "thiserror 2.0.17", ] [[package]] @@ -7245,7 +6803,7 @@ dependencies = [ "lazy_static", "newtype_derive", "petgraph 0.6.5", - "schemars", + "schemars 0.8.22", "serde", "serde_json", "slog", @@ -7605,16 +7163,16 @@ dependencies = [ "libc", "libnet 0.1.0 (git+https://github.com/oxidecomputer/netadm-sys)", "lldpd-client", - "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "omicron-common", "oxide-tokio-rt", - "oximeter 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle)", + "oximeter", "oximeter-instruments", "oximeter-producer", "oxnet", "packet", "pcap", "rand 0.9.2", - "schemars", + "schemars 0.8.22", "serde", "signal-hook", "slog", @@ -8187,7 +7745,7 @@ dependencies = [ "hubpack", "itertools 0.14.0", "nix", - "schemars", + "schemars 0.8.22", "serde", "slog", "slog-async", @@ -8206,7 +7764,7 @@ 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.17", @@ -8221,8 +7779,74 @@ 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#75fe22d9f9c7815e3ece509b4f7409eb5252e0d1" +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#75fe22d9f9c7815e3ece509b4f7409eb5252e0d1" +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#75fe22d9f9c7815e3ece509b4f7409eb5252e0d1" +dependencies = [ + "daft", + "derive_more", + "gfss", + "iddqd", + "omicron-uuid-kinds", + "omicron-workspace-hack", + "rand 0.9.2", + "schemars 0.8.22", + "serde", + "serde_human_bytes", + "serde_with", + "sled-hardware-types", + "slog", + "slog-error-chain", "thiserror 2.0.17", ] @@ -8235,12 +7859,12 @@ 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", @@ -8288,7 +7912,7 @@ dependencies = [ "proc-macro2", "quote", "regress", - "schemars", + "schemars 0.8.22", "semver 1.0.27", "serde", "serde_json", @@ -8305,7 +7929,7 @@ checksum = "9708a3ceb6660ba3f8d2b8f0567e7d4b8b198e2b94d093b8a6077a751425de9e" dependencies = [ "proc-macro2", "quote", - "schemars", + "schemars 0.8.22", "semver 1.0.27", "serde", "serde_json", @@ -8402,37 +8026,7 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "update-engine" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#64b40cb0a98ec600ca74e573a1926c1876e33b35" -dependencies = [ - "anyhow", - "cancel-safe-futures", - "chrono", - "debug-ignore", - "derive-where", - "either", - "futures", - "indent_write", - "indexmap 2.12.1", - "libsw", - "linear-map", - "omicron-workspace-hack", - "owo-colors", - "petgraph 0.8.2", - "schemars", - "serde", - "serde_json", - "serde_with", - "slog", - "swrite", - "tokio", - "unicode-width 0.1.14", - "uuid", -] - -[[package]] -name = "update-engine" -version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=zl%2Fmcast-implicit-lifecycle#9794876a45bda0d20c2d545b4f7f134ffd7d2086" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#75fe22d9f9c7815e3ece509b4f7409eb5252e0d1" dependencies = [ "anyhow", "cancel-safe-futures", @@ -8448,7 +8042,7 @@ dependencies = [ "omicron-workspace-hack", "owo-colors", "petgraph 0.8.2", - "schemars", + "schemars 0.8.22", "serde", "serde_json", "serde_with", @@ -9439,9 +9033,9 @@ dependencies = [ [[package]] name = "zeroize_derive" -version = "1.4.2" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 170238f5..b182d09a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,21 +1,21 @@ [workspace] members = [ - "aal", - "asic", - "aal_macros", - "common", - "dpd", - "dpd-api", - "dpd-client", - "dpd-types", - "dropshot-apis", - "packet", - "pcap", - "swadm", - "tfportd", - "uplinkd", - "xtask", + "aal", + "asic", + "aal_macros", + "common", + "dpd", + "dpd-api", + "dpd-client", + "dpd-types", + "dropshot-apis", + "packet", + "pcap", + "swadm", + "tfportd", + "uplinkd", + "xtask", ] resolver = "2" @@ -47,14 +47,19 @@ gateway-client = { git = "https://github.com/oxidecomputer/omicron", branch = "m gateway-types = { git = "https://github.com/oxidecomputer/omicron", branch = "main" } libnet = { git = "https://github.com/oxidecomputer/netadm-sys" } nexus-client = { git = "https://github.com/oxidecomputer/omicron", branch = "main" } -omicron-common = { git = "https://github.com/oxidecomputer/omicron", branch= "zl/mcast-implicit-lifecycle" } -oximeter = { git = "https://github.com/oxidecomputer/omicron", branch = "zl/mcast-implicit-lifecycle" } -oximeter-producer = { git = "https://github.com/oxidecomputer/omicron", branch = "zl/mcast-implicit-lifecycle" } -oximeter-instruments = { git = "https://github.com/oxidecomputer/omicron", branch = "zl/mcast-implicit-lifecycle", default-features = false, features = ["kstat"] } -oxnet = { version = "0.1.4", default-features = false, features = ["schemars", "serde"] } +omicron-common = { git = "https://github.com/oxidecomputer/omicron", branch = "main" } +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.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"} +softnpu-lib = { git = "https://github.com/oxidecomputer/softnpu", package = "softnpu", branch = "main" } tofino = { git = "https://github.com/oxidecomputer/tofino", branch = "main" } transceiver-controller = { git = "https://github.com/oxidecomputer/transceiver-control", branch = "main" } @@ -64,7 +69,7 @@ bytes = "1.6" camino = { version = "1.1", features = ["serde1"] } cfg-if = "1" chrono = "0.4" -clap = { version = "4.5.50", features = [ "derive" ] } +clap = { version = "4.5.50", features = ["derive"] } colored = "3" csv = "1.3" curl = "0.4" @@ -95,20 +100,23 @@ semver = "1.0" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" signal-hook = "0.3" -signal-hook-tokio = { version = "0.3", features = [ "futures-v0_3" ] } -slog = { version = "2.7", features = [ "release_max_level_debug", "max_level_trace" ] } +signal-hook-tokio = { version = "0.3", features = ["futures-v0_3"] } +slog = { version = "2.7", features = [ + "release_max_level_debug", + "max_level_trace", +] } slog-async = "2.8" slog-bunyan = "2.5" slog-term = "2.9" socket2 = { version = "0.6", features = ["all"] } -strum = { version = "0.27", features = [ "derive" ] } -syn = { version = "2.0", features = ["extra-traits"]} +strum = { version = "0.27", features = ["derive"] } +syn = { version = "2.0", features = ["extra-traits"] } tabwriter = { version = "1", features = ["ansi_formatting"] } thiserror = "1.0" tokio = "1.37" toml = "0.9" usdt = "0.6" -uuid = { version = "1.10", features = [ "v4", "serde" ] } +uuid = { version = "1.10", features = ["v4", "serde"] } internet-checksum = "0.2" # diff --git a/dpd-api/src/lib.rs b/dpd-api/src/lib.rs index 0b0a80be..c4cd0561 100644 --- a/dpd-api/src/lib.rs +++ b/dpd-api/src/lib.rs @@ -6,6 +6,9 @@ //! DPD endpoint definitions. +pub mod v2; +pub mod v3; + use std::{ collections::{BTreeMap, HashMap, HashSet}, net::{IpAddr, Ipv4Addr, Ipv6Addr}, @@ -41,7 +44,7 @@ use oxnet::{Ipv4Net, Ipv6Net}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use transceiver_controller::{ - Datapath, Monitors, PowerState, message::LedState, + message::LedState, Datapath, Monitors, PowerState, }; api_versions!([ @@ -56,6 +59,8 @@ api_versions!([ // | example for the next person. // v // (next_int, IDENT), + (4, MCAST_TAG_OWNERSHIP), + (3, MCAST_SOURCE_FILTER_ANY), (2, MCAST_DOCS_ADMIN_LOCAL), (1, INITIAL), ]); @@ -1161,6 +1166,10 @@ pub trait DpdApi { * - 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 +1184,15 @@ pub trait DpdApi { /** * Clear all settings. * - * This removes all data entirely. + * This removes all data entirely, including: + * + * - All ARP and NDP table entries + * - All routes + * - All links on all switch ports + * - All NAT mappings + * - All multicast groups + * + * Note: Unlike `reset_all_tagged`, this endpoint does clear multicast groups. */ // TODO-security: This endpoint should probably not exist. #[endpoint { @@ -1439,6 +1456,7 @@ pub trait DpdApi { #[endpoint { method = POST, path = "/multicast/external-groups", + versions = VERSION_MCAST_SOURCE_FILTER_ANY.., }] async fn multicast_group_create_external( rqctx: RequestContext, @@ -1448,6 +1466,32 @@ pub trait DpdApi { HttpError, >; + /// 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. * @@ -1459,6 +1503,7 @@ pub trait DpdApi { #[endpoint { method = POST, path = "/multicast/underlay-groups", + versions = VERSION_MCAST_TAG_OWNERSHIP.., }] async fn multicast_group_create_underlay( rqctx: RequestContext, @@ -1468,16 +1513,58 @@ pub trait DpdApi { HttpError, >; - /** - * Delete a multicast group configuration by IP address. - */ + /// Create an underlay (internal) multicast group configuration (API v1-v3). + #[endpoint { + method = POST, + path = "/multicast/underlay-groups", + versions = ..VERSION_MCAST_TAG_OWNERSHIP, + }] + 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 (API version 4+). + /// + /// All groups have tags (auto-generated if not provided at creation). + /// When a tag is provided in the query, it must match the group's + /// existing tag to prove ownership. Omitting the tag skips validation. #[endpoint { method = DELETE, path = "/multicast/groups/{group_ip}", + versions = VERSION_MCAST_TAG_OWNERSHIP.., }] async fn multicast_group_delete( rqctx: RequestContext, path: Path, + query: Query, + ) -> Result; + + /// Delete a multicast group configuration by IP address (API versions 1-3). + /// + /// Does not include tag validation. + /// + // We cannot provide a default implementation that delegates to + // `multicast_group_delete` because Dropshot's `Query` extractor is opaque + // and cannot be constructed outside the framework. + #[endpoint { + method = DELETE, + path = "/multicast/groups/{group_ip}", + versions = ..VERSION_MCAST_TAG_OWNERSHIP, + }] + async fn multicast_group_delete_v3( + rqctx: RequestContext, + path: Path, ) -> Result; /** @@ -1497,15 +1584,31 @@ pub trait DpdApi { #[endpoint { method = GET, path = "/multicast/groups/{group_ip}", + versions = VERSION_MCAST_SOURCE_FILTER_ANY.., }] async fn multicast_group_get( rqctx: RequestContext, path: Path, ) -> Result, HttpError>; + /// 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-local - * IPv6 address. + * Get an underlay (internal) multicast group configuration. * * Underlay groups handle admin-local IPv6 multicast traffic (ff04::/16) with * replication infrastructure for external and underlay members. @@ -1513,15 +1616,32 @@ pub trait DpdApi { #[endpoint { method = GET, path = "/multicast/underlay-groups/{group_ip}", + versions = VERSION_MCAST_TAG_OWNERSHIP.., }] async fn multicast_group_get_underlay( rqctx: RequestContext, path: Path, ) -> Result, HttpError>; + /// Get an underlay (internal) multicast group configuration (API v1-v3). + #[endpoint { + method = GET, + path = "/multicast/underlay-groups/{group_ip}", + versions = ..VERSION_MCAST_TAG_OWNERSHIP, + }] + async fn multicast_group_get_underlay_v3( + rqctx: RequestContext, + path: Path, + ) -> Result, HttpError> + { + match Self::multicast_group_get_underlay(rqctx, 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-local IPv6 multicast traffic (ff04::/16) * that requires replication infrastructure with external and underlay members. @@ -1529,6 +1649,7 @@ pub trait DpdApi { #[endpoint { method = PUT, path = "/multicast/underlay-groups/{group_ip}", + versions = VERSION_MCAST_TAG_OWNERSHIP.., }] async fn multicast_group_update_underlay( rqctx: RequestContext, @@ -1536,6 +1657,24 @@ pub trait DpdApi { group: TypedBody, ) -> Result, HttpError>; + /// Update an underlay (internal) multicast group configuration (API v1-v3). + #[endpoint { + method = PUT, + path = "/multicast/underlay-groups/{group_ip}", + versions = ..VERSION_MCAST_TAG_OWNERSHIP, + }] + async fn multicast_group_update_underlay_v3( + rqctx: RequestContext, + path: Path, + group: TypedBody, + ) -> Result, HttpError> + { + match Self::multicast_group_update_underlay(rqctx, path, group).await { + Ok(HttpResponseOk(resp)) => Ok(HttpResponseOk(resp.into())), + Err(e) => Err(e), + } + } + /** * Update an external-only multicast group configuration for a given group IP address. * @@ -1545,15 +1684,63 @@ pub trait DpdApi { #[endpoint { method = PUT, path = "/multicast/external-groups/{group_ip}", + versions = VERSION_MCAST_TAG_OWNERSHIP.., }] async fn multicast_group_update_external( rqctx: RequestContext, path: Path, group: TypedBody, + ) -> Result, HttpError>; + + /// Update an external-only multicast group configuration (API v3). + /// + /// Returns 201 Created (v4+ returns 200 OK). + #[endpoint { + method = PUT, + path = "/multicast/external-groups/{group_ip}", + versions = VERSION_MCAST_SOURCE_FILTER_ANY..VERSION_MCAST_TAG_OWNERSHIP, + }] + async fn multicast_group_update_external_v3( + rqctx: RequestContext, + path: Path, + group: TypedBody, ) -> Result< HttpResponseCreated, HttpError, - >; + > { + match Self::multicast_group_update_external(rqctx, path, group).await { + Ok(HttpResponseOk(resp)) => Ok(HttpResponseCreated(resp)), + Err(e) => Err(e), + } + } + + /// Update an external-only multicast group configuration (API v1/v2). + /// + /// Returns 201 Created (v4+ returns 200 OK). + #[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, + > { + match Self::multicast_group_update_external( + rqctx, + path, + group.map(Into::into), + ) + .await + { + Ok(HttpResponseOk(resp)) => Ok(HttpResponseCreated(resp.into())), + Err(e) => Err(e), + } + } /** * List all multicast groups. @@ -1561,6 +1748,7 @@ pub trait DpdApi { #[endpoint { method = GET, path = "/multicast/groups", + versions = VERSION_MCAST_SOURCE_FILTER_ANY.., }] async fn multicast_groups_list( rqctx: RequestContext, @@ -1572,12 +1760,37 @@ pub trait DpdApi { HttpError, >; + /// 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. */ #[endpoint { method = GET, path = "/multicast/tags/{tag}", + versions = VERSION_MCAST_SOURCE_FILTER_ANY.., }] async fn multicast_groups_list_by_tag( rqctx: RequestContext, @@ -1590,6 +1803,33 @@ pub trait DpdApi { HttpError, >; + /// 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, 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. */ @@ -1604,15 +1844,32 @@ pub trait DpdApi { /** * 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_TAG_OWNERSHIP.., }] 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_TAG_OWNERSHIP, + }] + async fn multicast_reset_untagged_v3( + rqctx: RequestContext, + ) -> Result; + /** * Get the physical coding sublayer (PCS) counters for all links. */ @@ -2272,6 +2529,19 @@ pub struct MulticastGroupIpParam { pub group_ip: IpAddr, } +/// Tag for multicast group ownership validation. +/// +/// All groups have tags (auto-generated at creation if not provided). +/// Omit the field to skip validation, or provide a tag that must match +/// the group's existing tag. +#[derive(Deserialize, Serialize, JsonSchema)] +pub struct MulticastGroupTagQuery { + /// Tag that must match the group's existing tag for ownership validation. + /// Omit to skip validation. + #[serde(default)] + pub tag: Option, +} + /// Used to identify an underlay (internal) multicast group by admin-local IPv6 /// address (ff04::/16, as defined in [RFC 7346] and [RFC 4291]). /// diff --git a/dpd-api/src/v2.rs b/dpd-api/src/v2.rs new file mode 100644 index 00000000..d197ec26 --- /dev/null +++ b/dpd-api/src/v2.rs @@ -0,0 +1,191 @@ +// 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, SubnetV4, SubnetV6}` to `{Exact, Any}`. + +use std::{fmt, net::IpAddr}; + +use dpd_types::mcast::{ + ExternalForwarding, InternalForwarding, MulticastGroupId, +}; +use oxnet::Ipv4Net; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +// 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). +#[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 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 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 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 { + 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()), + } + } +} diff --git a/dpd-api/src/v3.rs b/dpd-api/src/v3.rs new file mode 100644 index 00000000..a4bc0249 --- /dev/null +++ b/dpd-api/src/v3.rs @@ -0,0 +1,105 @@ +// 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 2025 Oxide Computer Company + +//! Types from API version 3 that changed in version 4. +//! +//! The `tag` field in response types changed from `Option` to `String` +//! since all groups now have default tags generated at creation time. + +use std::net::IpAddr; + +use dpd_types::mcast::{ + AdminScopedIpv6, ExternalForwarding, InternalForwarding, IpSrc, + MulticastGroupId, MulticastGroupMember, +}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +/// 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 v4 response to v3 response. +impl From + for MulticastGroupUnderlayResponse +{ + fn from(resp: dpd_types::mcast::MulticastGroupUnderlayResponse) -> Self { + Self { + group_ip: resp.group_ip, + 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 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 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()) + } + } + } +} diff --git a/dpd-client/tests/integration_tests/mcast.rs b/dpd-client/tests/integration_tests/mcast.rs index b7720597..98399ad9 100644 --- a/dpd-client/tests/integration_tests/mcast.rs +++ b/dpd-client/tests/integration_tests/mcast.rs @@ -17,7 +17,7 @@ use anyhow::anyhow; use dpd_client::{Error, types}; use dpd_types::mcast::ADMIN_LOCAL_PREFIX; use futures::TryStreamExt; -use oxnet::{Ipv4Net, MulticastMac}; +use oxnet::MulticastMac; use packet::{Endpoint, eth, geneve, ipv4, ipv6, udp}; const MULTICAST_TEST_IPV4: Ipv4Addr = Ipv4Addr::new(224, 0, 1, 0); @@ -178,10 +178,14 @@ async fn create_test_multicast_group( } /// 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: Option<&str>, +) -> TestResult { switch .client - .multicast_group_delete(&group_ip) + .multicast_group_delete(&group_ip, tag) .await .map_err(|e| { anyhow!("Failed to delete test group {}: {:?}", group_ip, e) @@ -254,7 +258,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, @@ -550,7 +554,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_valid"); assert_eq!( created.internal_forwarding.nat_target, Some(nat_target.clone()) @@ -563,7 +567,16 @@ 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, Some("test_valid")) + .await + .unwrap(); + cleanup_test_group( + switch, + internal_multicast_ip, + Some("valid_internal_group"), + ) + .await } #[tokio::test] @@ -596,9 +609,9 @@ 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 ownership validation) let update_entry = types::MulticastGroupUpdateUnderlayEntry { - tag: Some("updated_tag".to_string()), + tag: Some("test_admin_scoped".to_string()), members: vec![types::MulticastGroupMember { port_id, link_id, @@ -613,9 +626,14 @@ async fn test_internal_ipv6_validation() -> TestResult { .expect("Should update internal IPv6 group") .into_inner(); - assert_eq!(updated.tag, Some("updated_tag".to_string())); + assert_eq!(updated.tag, "test_admin_scoped"); - cleanup_test_group(switch, created.group_ip.to_ip_addr()).await + cleanup_test_group( + switch, + created.group_ip.to_ip_addr(), + Some("test_admin_scoped"), + ) + .await } #[tokio::test] @@ -703,10 +721,20 @@ 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()) - .await - .unwrap(); - cleanup_test_group(switch, created_external.group_ip).await + // Delete external group first since it references the internal group via NAT target + cleanup_test_group( + switch, + created_external.group_ip, + Some("test_external_with_vlan"), + ) + .await + .unwrap(); + cleanup_test_group( + switch, + created_admin.group_ip.to_ip_addr(), + Some("test_vlan_propagation"), + ) + .await } #[tokio::test] @@ -753,7 +781,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_lifecycle"); assert_eq!( created.internal_forwarding.nat_target, Some(nat_target.clone()) @@ -799,7 +827,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_lifecycle"); // Also test getting by IP address let group_by_ip = switch @@ -822,7 +850,7 @@ async fn test_group_api_lifecycle() { }; let external_update = types::MulticastGroupUpdateExternalEntry { - tag: Some("updated_lifecycle".to_string()), + tag: Some("test_lifecycle".to_string()), internal_forwarding: types::InternalForwarding { nat_target: Some(updated_nat_target.clone()), }, @@ -838,7 +866,7 @@ async fn test_group_api_lifecycle() { .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_lifecycle"); assert_eq!( updated.internal_forwarding.nat_target, Some(updated_nat_target) @@ -846,10 +874,10 @@ 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) switch .client - .multicast_group_delete(&group_ip) + .multicast_group_delete(&group_ip, Some("test_lifecycle")) .await .expect("Should be able to delete group"); @@ -887,6 +915,206 @@ 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, + Some("valid_underlay_group"), + ) + .await + .unwrap(); +} + +/// Tests tag validation behavior on multicast group deletion. +/// +/// The tag parameter on delete serves as proof of ownership: +/// - All groups have tags (auto-generated if not provided at creation) +/// - Deletion requires a matching tag for ownership validation +/// - Mismatched tags result in a 400 Bad Request error +#[tokio::test] +#[ignore] +async fn test_multicast_delete_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("tag_validation_internal"), + &[(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("owner_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 wrong_tag_result = switch + .client + .multicast_group_delete(&tagged_group_ip, Some("owner_b")) + .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: Delete tagged group with empty string should fail + let empty_tag_result = switch + .client + .multicast_group_delete(&tagged_group_ip, Some("")) + .await; + + match &empty_tag_result { + Err(Error::ErrorResponse(resp)) => { + assert_eq!( + resp.status(), + 400, + "Empty tag for tagged group should return 400 Bad Request" + ); + } + _ => panic!( + "Expected ErrorResponse for missing tag on tagged group, got: {:?}", + empty_tag_result + ), + } + + // 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 + switch + .client + .multicast_group_delete(&tagged_group_ip, Some("owner_a")) + .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(); + + // Attempt delete with empty string on auto-tagged group - should fail + let empty_on_auto_result = switch + .client + .multicast_group_delete(&auto_tagged_group_ip, Some("")) + .await; + + match &empty_on_auto_result { + Err(Error::ErrorResponse(resp)) => { + assert_eq!( + resp.status(), + 400, + "Empty tag for auto-tagged group should return 400" + ); + } + _ => panic!( + "Expected ErrorResponse for empty tag on auto-tagged group, got: {:?}", + empty_on_auto_result + ), + } + + // Delete with correct auto-generated tag should succeed + switch + .client + .multicast_group_delete(&auto_tagged_group_ip, Some(auto_tag.as_str())) + .await + .expect("Should delete group with matching auto-generated tag"); + + // Case: Delete with None skips validation + let skip_validation_group_ip = IpAddr::V4(Ipv4Addr::new(224, 0, 10, 3)); + create_test_multicast_group( + switch, + skip_validation_group_ip, + Some("will_be_ignored"), + &[(PhysPort(11), types::Direction::External)], + types::InternalForwarding { + nat_target: Some(create_nat_target_ipv4()), + }, + types::ExternalForwarding { vlan_id: Some(10) }, + None, + ) + .await; + + switch + .client + .multicast_group_delete(&skip_validation_group_ip, None) + .await + .expect("Should delete group when tag is None"); + + // Clean up internal group + cleanup_test_group( + switch, + internal_multicast_ip, + Some("tag_validation_internal"), + ) + .await } #[tokio::test] @@ -1005,85 +1233,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 { @@ -1146,7 +1295,12 @@ 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(), + Some("test_bifurcated"), + ) + .await } #[tokio::test] @@ -1178,7 +1332,12 @@ async fn test_api_internal_ipv6_underlay_only() -> TestResult { 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(), + Some("test_underlay_only"), + ) + .await } #[tokio::test] @@ -1211,7 +1370,12 @@ 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(), + Some("test_external_members_only"), + ) + .await } #[tokio::test] @@ -1301,13 +1465,26 @@ async fn test_api_invalid_combinations() -> TestResult { ), } - cleanup_test_group(switch, created_ipv4.group_ip) - .await - .unwrap(); - cleanup_test_group(switch, created_non_admin.group_ip) - .await - .unwrap(); - cleanup_test_group(switch, internal_multicast_ip).await + cleanup_test_group( + switch, + created_ipv4.group_ip, + Some("test_invalid_ipv4"), + ) + .await + .unwrap(); + cleanup_test_group( + switch, + created_non_admin.group_ip, + Some("test_non_admin_ipv6"), + ) + .await + .unwrap(); + cleanup_test_group( + switch, + internal_multicast_ip, + Some("nat_target_for_invalid_combos"), + ) + .await } #[tokio::test] @@ -1405,10 +1582,19 @@ 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)) - .await - .unwrap(); - cleanup_test_group(switch, internal_multicast_ip).await + cleanup_test_group( + switch, + get_group_ip(&created_group), + Some("test_invalid_mac"), + ) + .await + .unwrap(); + cleanup_test_group( + switch, + internal_multicast_ip, + Some("test_invalid_mac_underlay"), + ) + .await } #[tokio::test] @@ -1499,7 +1685,12 @@ 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), + Some("test_ipv6_invalid_mac"), + ) + .await } #[tokio::test] @@ -1580,10 +1771,19 @@ async fn test_multicast_ttl_zero() -> TestResult { .await .unwrap(); - cleanup_test_group(switch, get_group_ip(&created_group)) - .await - .unwrap(); - cleanup_test_group(switch, internal_multicast_ip).await + cleanup_test_group( + switch, + get_group_ip(&created_group), + Some("test_ttl_drop"), + ) + .await + .unwrap(); + cleanup_test_group( + switch, + internal_multicast_ip, + Some("nat_target_for_ttl"), + ) + .await } #[tokio::test] @@ -1664,13 +1864,22 @@ async fn test_multicast_ttl_one() -> TestResult { .await .unwrap(); - cleanup_test_group(switch, get_group_ip(&created_group)) - .await - .unwrap(); - cleanup_test_group(switch, internal_multicast_ip).await -} - -#[tokio::test] + cleanup_test_group( + switch, + get_group_ip(&created_group), + Some("test_ttl_one_drop"), + ) + .await + .unwrap(); + cleanup_test_group( + switch, + internal_multicast_ip, + Some("nat_target_for_ttl_one"), + ) + .await +} + +#[tokio::test] #[ignore] async fn test_ipv4_multicast_basic_replication_nat_ingress() -> TestResult { let switch = &*get_switch().await; @@ -1802,7 +2011,20 @@ 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), + Some("test_ipv4_replication"), + ) + .await + .unwrap(); + cleanup_test_group( + switch, + internal_multicast_ip, + Some("test_replication_internal"), + ) + .await } #[tokio::test] @@ -1941,10 +2163,19 @@ async fn test_encapped_multicast_geneve_mcast_tag_to_external_members() .await .unwrap(); - cleanup_test_group(switch, get_group_ip(&created_group)) - .await - .unwrap(); - cleanup_test_group(switch, MULTICAST_NAT_IP.into()).await + cleanup_test_group( + switch, + get_group_ip(&created_group), + Some("test_geneve_mcast_tag_0"), + ) + .await + .unwrap(); + cleanup_test_group( + switch, + MULTICAST_NAT_IP.into(), + Some("test_geneve_mcast_tag_underlay"), + ) + .await } #[tokio::test] @@ -2080,10 +2311,19 @@ async fn test_encapped_multicast_geneve_mcast_tag_to_underlay_members() .await .unwrap(); - cleanup_test_group(switch, get_group_ip(&created_group)) - .await - .unwrap(); - cleanup_test_group(switch, MULTICAST_NAT_IP.into()).await + cleanup_test_group( + switch, + get_group_ip(&created_group), + Some("test_geneve_mcast_tag_1"), + ) + .await + .unwrap(); + cleanup_test_group( + switch, + MULTICAST_NAT_IP.into(), + Some("test_geneve_mcast_tag_underlay"), + ) + .await } #[tokio::test] @@ -2238,10 +2478,19 @@ async fn test_encapped_multicast_geneve_mcast_tag_to_underlay_and_external_membe .await .unwrap(); - cleanup_test_group(switch, get_group_ip(&created_group)) - .await - .unwrap(); - cleanup_test_group(switch, MULTICAST_NAT_IP.into()).await + cleanup_test_group( + switch, + get_group_ip(&created_group), + Some("test_geneve_mcast_tag_1"), + ) + .await + .unwrap(); + cleanup_test_group( + switch, + MULTICAST_NAT_IP.into(), + Some("test_geneve_mcast_tag_bifurcated"), + ) + .await } #[tokio::test] @@ -2320,10 +2569,19 @@ async fn test_ipv4_multicast_drops_ingress_is_egress_port() -> TestResult { .await .unwrap(); - cleanup_test_group(switch, get_group_ip(&created_group)) - .await - .unwrap(); - cleanup_test_group(switch, internal_multicast_ip).await + cleanup_test_group( + switch, + get_group_ip(&created_group), + Some("test_replication"), + ) + .await + .unwrap(); + cleanup_test_group( + switch, + internal_multicast_ip, + Some("test_drops_underlay"), + ) + .await } #[tokio::test] @@ -2394,10 +2652,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", @@ -2408,7 +2662,19 @@ async fn test_ipv6_multicast_hop_limit_zero() -> TestResult { .await .unwrap(); - Ok(()) + cleanup_test_group( + switch, + get_group_ip(&created_group), + Some("test_ipv6_hop_limit_zero"), + ) + .await + .unwrap(); + cleanup_test_group( + switch, + internal_multicast_ip, + Some("test_ipv6_hop_limit_underlay"), + ) + .await } #[tokio::test] @@ -2493,7 +2759,19 @@ 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), + Some("test_ipv6_hop_limit_one"), + ) + .await + .unwrap(); + cleanup_test_group( + switch, + internal_multicast_ip, + Some("test_ipv6_hop_limit_one_underlay"), + ) + .await } #[tokio::test] @@ -2594,7 +2872,19 @@ 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), + Some("test_ipv6_replication"), + ) + .await + .unwrap(); + cleanup_test_group( + switch, + internal_multicast_ip, + Some("test_replication_internal"), + ) + .await } #[tokio::test] @@ -2627,7 +2917,7 @@ 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); @@ -2720,15 +3010,24 @@ async fn test_ipv4_multicast_source_filtering_exact_match() -> TestResult { .await .unwrap(); - cleanup_test_group(switch, get_group_ip(&created_group)) - .await - .unwrap(); - cleanup_test_group(switch, internal_multicast_ip).await + cleanup_test_group( + switch, + get_group_ip(&created_group), + Some("test_source_filtering"), + ) + .await + .unwrap(); + cleanup_test_group( + switch, + internal_multicast_ip, + Some("test_source_filtering_underlay"), + ) + .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 @@ -2743,7 +3042,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_source_filtering_multi_underlay"), &[ (egress1, types::Direction::External), (egress2, types::Direction::External), @@ -2757,12 +3056,15 @@ 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, @@ -2773,7 +3075,7 @@ async fn test_ipv4_multicast_source_filtering_prefix_match() -> TestResult { nat_target: Some(create_nat_target_ipv4()), }, types::ExternalForwarding { vlan_id: Some(10) }, - Some(vec![allowed_src]), + Some(allowed_sources), ) .await; @@ -2889,10 +3191,19 @@ async fn test_ipv4_multicast_source_filtering_prefix_match() -> TestResult { .await .unwrap(); - cleanup_test_group(switch, get_group_ip(&created_group)) - .await - .unwrap(); - cleanup_test_group(switch, internal_multicast_ip).await + cleanup_test_group( + switch, + get_group_ip(&created_group), + Some("test_source_filtering"), + ) + .await + .unwrap(); + cleanup_test_group( + switch, + internal_multicast_ip, + Some("test_source_filtering_multi_underlay"), + ) + .await } #[tokio::test] @@ -3063,10 +3374,19 @@ async fn test_ipv6_multicast_multiple_source_filtering() -> TestResult { .await .unwrap(); - cleanup_test_group(switch, get_group_ip(&created_group)) - .await - .unwrap(); - cleanup_test_group(switch, internal_multicast_ip).await + cleanup_test_group( + switch, + get_group_ip(&created_group), + Some("test_ipv6_source_filtering"), + ) + .await + .unwrap(); + cleanup_test_group( + switch, + internal_multicast_ip, + Some("test_ipv6_source_filtering_underlay"), + ) + .await } #[tokio::test] @@ -3159,9 +3479,10 @@ 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 ownership validation. let external_update_entry = types::MulticastGroupUpdateExternalEntry { - tag: None, + tag: Some("test_dynamic_membership".to_string()), internal_forwarding: types::InternalForwarding { nat_target: Some(create_nat_target_ipv4()), }, // Keep the same NAT target @@ -3182,8 +3503,9 @@ 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 ownership validation let internal_update_entry = types::MulticastGroupUpdateUnderlayEntry { - tag: None, + tag: Some("test_dynamic_membership_internal".to_string()), members: vec![ types::MulticastGroupMember { port_id: port_id2, @@ -3248,10 +3570,19 @@ 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)) - .await - .unwrap(); - cleanup_test_group(switch, internal_multicast_ip).await + cleanup_test_group( + switch, + get_group_ip(&created_group), + Some("test_dynamic_membership"), + ) + .await + .unwrap(); + cleanup_test_group( + switch, + internal_multicast_ip, + Some("test_dynamic_membership_internal"), + ) + .await } #[tokio::test] @@ -3451,13 +3782,26 @@ async fn test_multicast_multiple_groups() -> TestResult { switch.packet_test(test_pkts, expected_pkts).unwrap(); - cleanup_test_group(switch, get_group_ip(&created_group1)) - .await - .unwrap(); - cleanup_test_group(switch, get_group_ip(&created_group2)) - .await - .unwrap(); - cleanup_test_group(switch, internal_multicast_ip).await + cleanup_test_group( + switch, + get_group_ip(&created_group1), + Some("test_multi_group_1"), + ) + .await + .unwrap(); + cleanup_test_group( + switch, + get_group_ip(&created_group2), + Some("test_multi_group_2"), + ) + .await + .unwrap(); + cleanup_test_group( + switch, + internal_multicast_ip, + Some("test_multi_group_underlay"), + ) + .await } #[tokio::test] @@ -3546,9 +3890,7 @@ 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( @@ -3856,10 +4198,19 @@ 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)) - .await - .unwrap(); - cleanup_test_group(switch, internal_multicast_ip).await + cleanup_test_group( + switch, + get_group_ip(&created_group), + Some("test_vlan_behavior"), + ) + .await + .unwrap(); + cleanup_test_group( + switch, + internal_multicast_ip, + Some("test_vlan_underlay"), + ) + .await } #[tokio::test] @@ -3990,10 +4341,19 @@ async fn test_multicast_multiple_packets() -> TestResult { .await .unwrap(); - cleanup_test_group(switch, get_group_ip(&created_group)) - .await - .unwrap(); - cleanup_test_group(switch, internal_multicast_ip).await + cleanup_test_group( + switch, + get_group_ip(&created_group), + Some("test_performance"), + ) + .await + .unwrap(); + cleanup_test_group( + switch, + internal_multicast_ip, + Some("test_performance_underlay"), + ) + .await } #[tokio::test] @@ -4198,10 +4558,20 @@ 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()) - .await - .unwrap(); - cleanup_test_group(switch, created_external.group_ip).await + // Delete external group first since it references the internal group via NAT target + cleanup_test_group( + switch, + created_external.group_ip, + Some("test_valid_nat"), + ) + .await + .unwrap(); + cleanup_test_group( + switch, + created_admin.group_ip.to_ip_addr(), + Some("test_admin_scoped"), + ) + .await } #[tokio::test] @@ -4346,12 +4716,18 @@ async fn test_ipv6_multicast_scope_validation() { switch .client - .multicast_group_delete(&admin_local_group.group_ip.to_ip_addr()) + .multicast_group_delete( + &admin_local_group.group_ip.to_ip_addr(), + Some("test_admin_local"), + ) .await .ok(); switch .client - .multicast_group_delete(&target_group.group_ip.to_ip_addr()) + .multicast_group_delete( + &target_group.group_ip.to_ip_addr(), + Some("test_target"), + ) .await .ok(); } @@ -4401,7 +4777,7 @@ async fn test_multicast_group_id_recycling() -> TestResult { // Delete the first group switch .client - .multicast_group_delete(&group1_ip) + .multicast_group_delete(&group1_ip, Some("test_recycling_1")) .await .expect("Should be able to delete first group"); @@ -4442,7 +4818,7 @@ async fn test_multicast_group_id_recycling() -> TestResult { // Create a fourth group after deleting group2, it should reuse group2's ID switch .client - .multicast_group_delete(&group2_ip) + .multicast_group_delete(&group2_ip, Some("test_recycling_2")) .await .expect("Should be able to delete second group"); @@ -4480,8 +4856,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, Some("test_recycling_3")) + .await + .unwrap(); + cleanup_test_group(switch, group4_ip, Some("test_recycling_4")).await } #[tokio::test] @@ -4784,8 +5162,9 @@ async fn test_multicast_empty_then_add_members_ipv6() -> TestResult { ); // Test: Update internal group back to empty (remove all members) + // Must pass same tag for ownership validation let empty_update_entry = types::MulticastGroupUpdateUnderlayEntry { - tag: None, + tag: Some("empty_internal_ipv6_group".to_string()), members: vec![], // Remove all members }; @@ -4841,10 +5220,19 @@ 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) - .await - .unwrap(); - cleanup_test_group(&switch, internal_group_ip).await + cleanup_test_group( + &switch, + external_group_ip, + Some("empty_external_ipv6_group"), + ) + .await + .unwrap(); + cleanup_test_group( + &switch, + internal_group_ip, + Some("empty_internal_ipv6_group"), + ) + .await } #[tokio::test] @@ -5145,7 +5533,7 @@ 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, + tag: Some("empty_internal_ipv4_nat_target".to_string()), members: vec![], // Remove all members }; @@ -5202,16 +5590,21 @@ 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) - .await - .unwrap(); - cleanup_test_group(&switch, internal_group_ip).await + cleanup_test_group( + &switch, + external_group_ip, + Some("empty_external_ipv4_group"), + ) + .await + .unwrap(); + cleanup_test_group( + &switch, + internal_group_ip, + Some("empty_internal_ipv4_nat_target"), + ) + .await } -// ============================================================================= -// ROLLBACK TESTS -// ============================================================================= - #[tokio::test] #[ignore] async fn test_multicast_rollback_external_group_creation_failure() -> TestResult @@ -5261,6 +5654,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 @@ -5364,15 +5762,32 @@ 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 -} - -#[tokio::test] -#[ignore] -async fn test_multicast_rollback_member_update_failure() -> TestResult { - let switch = &*get_switch().await; + let post_src_filter_table = switch + .client + .table_dump(SOURCE_FILTER_IPV4_TABLE) + .await + .expect("Should be able to dump source filter table"); - let internal_group_ip = + 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, + Some("rollback_test_internal"), + ) + .await +} + +#[tokio::test] +#[ignore] +async fn test_multicast_rollback_member_update_failure() -> TestResult { + let switch = &*get_switch().await; + + let internal_group_ip = IpAddr::V6(Ipv6Addr::new(ADMIN_LOCAL_PREFIX, 0, 0, 0, 0, 0, 0, 103)); // Create internal group with initial members @@ -5444,7 +5859,8 @@ 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, Some("rollback_member_test")) + .await } #[tokio::test] @@ -5584,10 +6000,11 @@ 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, None) .await .unwrap(); - cleanup_test_group(&switch, internal_group_ip).await + cleanup_test_group(&switch, internal_group_ip, Some("nat_rollback_test")) + .await } #[tokio::test] @@ -5622,9 +6039,13 @@ 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) - .await - .expect("Should cleanup internal group"); + cleanup_test_group( + &switch, + internal_group_ip, + Some("vlan_propagation_test"), + ) + .await + .expect("Should cleanup internal group"); let nat_target = types::NatTarget { internal_ip: match internal_group_ip { @@ -5822,8 +6243,16 @@ 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, Some("source_filter_rollback_test")) + .await + .unwrap(); + cleanup_test_group( + &switch, + internal_multicast_ip, + Some("rollback_internal"), + ) + .await } #[tokio::test] @@ -5922,7 +6351,12 @@ 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, + Some("partial_add_rollback_test"), + ) + .await } #[tokio::test] @@ -5950,7 +6384,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, Some("table_rollback_test")) .await .expect("Should cleanup internal group"); @@ -6099,10 +6533,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, "underlay_get_test"); assert_eq!(retrieved_underlay.members.len(), 2); // Compare with generic GET endpoint result @@ -6134,5 +6565,815 @@ async fn test_multicast_group_get_underlay() -> TestResult { ); } } - cleanup_test_group(&switch, internal_group_ip).await + cleanup_test_group(&switch, internal_group_ip, Some("underlay_get_test")) + .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("source_filter_test_internal"), + &[(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("source_filter_collapse_test".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 delete_internal_first_result = switch + .client + .multicast_group_delete( + &internal_group_ip, + Some("source_filter_test_internal"), + ) + .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, + Some("source_filter_collapse_test"), + ) + .await + .unwrap(); + cleanup_test_group( + switch, + internal_group_ip, + Some("source_filter_test_internal"), + ) + .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("source_filter_ipv6_internal"), + &[(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("source_filter_ipv6_collapse_test".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 delete_internal_first_result = switch + .client + .multicast_group_delete( + &internal_group_ip, + Some("source_filter_ipv6_internal"), + ) + .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, + Some("source_filter_ipv6_collapse_test"), + ) + .await + .unwrap(); + cleanup_test_group( + switch, + internal_group_ip, + Some("source_filter_ipv6_internal"), + ) + .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("source_filter_update_internal"), + &[(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("source_filter_update_test".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 { + tag: Some("source_filter_update_test".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 updated = switch + .client + .multicast_group_update_external(&external_group_ip, &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, + Some("source_filter_update_test"), + ) + .await + .unwrap(); + cleanup_test_group( + switch, + internal_group_ip, + Some("source_filter_update_internal"), + ) + .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("source_filter_cleanup_internal"), + &[(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("source_filter_cleanup_test".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, + Some("source_filter_cleanup_test"), + ) + .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, + Some("source_filter_cleanup_internal"), + ) + .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("empty_sources_internal"), + &[(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("empty_sources_test".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, Some("empty_sources_test")) + .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, + Some("empty_sources_internal"), + ) + .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::AdminScopedIpv6 = + 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, + }], + tag: Some("nonexistent_test".to_string()), + }; + + let result = switch + .client + .multicast_group_update_underlay( + &nonexistent_underlay, + &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 }, + tag: Some("nonexistent_test".to_string()), + sources: None, + }; + + let result = switch + .client + .multicast_group_update_external( + &nonexistent_external, + &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 result = switch + .client + .multicast_group_delete(&nonexistent_ip, Some("some_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), + } + + // Case: Delete non-existent group with None + let result = switch + .client + .multicast_group_delete(&nonexistent_ip, None) + .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::AdminScopedIpv6 = + 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) + switch + .client + .multicast_group_delete(&group_ip.to_ip_addr(), Some(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(tag) + .await + .expect("Should cleanup by tag"); + + Ok(()) } diff --git a/dpd-client/tests/integration_tests/service.rs b/dpd-client/tests/integration_tests/service.rs index 514261aa..bbff9782 100644 --- a/dpd-client/tests/integration_tests/service.rs +++ b/dpd-client/tests/integration_tests/service.rs @@ -174,7 +174,9 @@ async fn test_service_ipv4_wrong_port() -> TestResult { } // Packets sent to an IP address not assigned to a switch port should be -// dropped. +// dropped when the port is configured as NAT-only (see issue #172). +// Without NAT-only, such packets would instead trigger ICMP_DEST_UNREACHABLE +// via the router. #[tokio::test] #[ignore] async fn test_service_ipv4_unknown_address() -> TestResult { @@ -205,7 +207,22 @@ async fn test_service_ipv4_unknown_address() -> TestResult { port: PhysPort(ingress + 1), }; - switch.packet_test(vec![send], Vec::new()) + // Mark the ingress port as NAT-only so packets to unknown addresses are + // dropped rather than triggering ICMP_UNREACHABLE (see issue #172). + let (ingress_port_id, ingress_link_id) = + switch.link_id(PhysPort(ingress + 1)).unwrap(); + switch + .client + .link_nat_only_set(&ingress_port_id, &ingress_link_id, true) + .await + .unwrap(); + let result = switch.packet_test(vec![send], Vec::new()); + switch + .client + .link_nat_only_set(&ingress_port_id, &ingress_link_id, false) + .await + .unwrap(); + result } #[tokio::test] diff --git a/dpd-client/tests/integration_tests/table_tests.rs b/dpd-client/tests/integration_tests/table_tests.rs index 3491f369..9ed977b2 100644 --- a/dpd-client/tests/integration_tests/table_tests.rs +++ b/dpd-client/tests/integration_tests/table_tests.rs @@ -14,12 +14,13 @@ 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 dpd_types::mcast::ADMIN_LOCAL_PREFIX; +use crate::integration_tests::common::prelude::*; + // 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 @@ -37,8 +38,8 @@ use dpd_types::mcast::ADMIN_LOCAL_PREFIX; // 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 @@ -493,7 +494,10 @@ 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 + switch + .client + .multicast_group_delete(&ip, Some(MCAST_TAG)) + .await } async fn count_entries(switch: &Switch) -> usize { diff --git a/dpd-types/src/link.rs b/dpd-types/src/link.rs index 622e473d..e5a1ac75 100644 --- a/dpd-types/src/link.rs +++ b/dpd-types/src/link.rs @@ -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, diff --git a/dpd-types/src/mcast.rs b/dpd-types/src/mcast.rs index 706ce91d..a4a3e939 100644 --- a/dpd-types/src/mcast.rs +++ b/dpd-types/src/mcast.rs @@ -12,7 +12,7 @@ use std::{ }; use common::{nat::NatTarget, ports::PortId}; -use oxnet::{Ipv4Net, Ipv6Net}; +use oxnet::Ipv6Net; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -86,21 +86,24 @@ impl fmt::Display for AdminScopedIpv6 { } /// 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"), } } } @@ -150,7 +153,9 @@ pub struct MulticastGroupUnderlayResponse { pub group_ip: AdminScopedIpv6, pub external_group_id: MulticastGroupId, pub underlay_group_id: MulticastGroupId, - pub tag: Option, + /// Tag for ownership validation. Always present; generated as + /// `{uuid}:{group_ip}` if not provided at creation time. + pub tag: String, pub members: Vec, } @@ -160,7 +165,9 @@ pub struct MulticastGroupUnderlayResponse { pub struct MulticastGroupExternalResponse { pub group_ip: IpAddr, pub external_group_id: MulticastGroupId, - pub tag: Option, + /// Tag for ownership validation. Always present; 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>, diff --git a/dpd/p4/sidecar.p4 b/dpd/p4/sidecar.p4 index d05c1754..68803a40 100644 --- a/dpd/p4/sidecar.p4 +++ b/dpd/p4/sidecar.p4 @@ -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; } diff --git a/dpd/src/api_server.rs b/dpd/src/api_server.rs index e1558efc..458f90d6 100644 --- a/dpd/src/api_server.rs +++ b/dpd/src/api_server.rs @@ -1871,11 +1871,25 @@ 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_deref()) + .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; + + mcast::del_group(switch, ip, None) .map(|_| HttpResponseDeleted()) .map_err(HttpError::from) } @@ -1932,14 +1946,13 @@ impl DpdApi for DpdApiImpl { rqctx: RequestContext>, path: Path, group: TypedBody, - ) -> Result, HttpError> - { + ) -> Result, HttpError> { let switch: &Switch = rqctx.context(); let entry = group.into_inner(); let ip = path.into_inner().group_ip; mcast::modify_group_external(switch, ip, entry) - .map(HttpResponseCreated) + .map(HttpResponseOk) .map_err(HttpError::from) } @@ -2027,6 +2040,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 6db806c9..e99eb4cc 100644 --- a/dpd/src/mcast/mod.rs +++ b/dpd/src/mcast/mod.rs @@ -76,8 +76,9 @@ use dpd_types::{ MulticastGroupUpdateUnderlayEntry, }, }; -use oxnet::Ipv4Net; +use oxnet::{Ipv4Net, Ipv6Net}; use slog::{debug, error, warn}; +use uuid::Uuid; use crate::{ Switch, table, @@ -90,7 +91,7 @@ mod validate; use rollback::{GroupCreateRollbackContext, GroupUpdateRollbackContext}; use validate::{ validate_multicast_address, validate_nat_target, - validate_not_admin_local_ipv6, + validate_not_admin_local_ipv6, validate_tag_for_update, }; #[derive(Debug)] @@ -144,7 +145,9 @@ struct MulticastReplicationInfo { pub(crate) struct MulticastGroup { external_scoped_group: ScopedGroupId, underlay_scoped_group: ScopedGroupId, - pub(crate) tag: Option, + /// Tag for ownership validation. Always present; 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>, @@ -257,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(), ) })?; @@ -314,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(|| { @@ -368,16 +374,26 @@ pub(crate) fn add_group_external( }) .map_err(|e| rollback_ctx.rollback_and_return_error(e))?; - // Validate the admin-local IP early to avoid partial state + // 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 = AdminScopedIpv6::new(nat_target.internal_ip)?; + let tag = group_info + .tag + .unwrap_or_else(|| generate_default_tag(group_ip)); + 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(), @@ -402,7 +418,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)?; @@ -466,11 +482,15 @@ pub(crate) fn add_group_internal( None }; + let tag = group_info + .tag + .unwrap_or_else(|| generate_default_tag(group_ip.into())); + // 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 }, @@ -489,8 +509,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(); +/// +/// # Arguments +/// +/// * `s` - Switch instance containing the multicast state. +/// * `group_ip` - IP address of the multicast group to delete. +/// * `tag` - Optional tag for ownership validation. If provided, it must +/// match the group's existing tag for the deletion to succeed. +/// +/// # 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: Option<&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) = AdminScopedIpv6::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" + ))); + } + + // Validate tag ownership before removing the group. + // If tag is None (v3 API), skip validation for backward compatibility. + // If tag is Some, it must match the group's existing tag. + if let (Some(group), Some(request_tag)) = (mcast.groups.get(&group_ip), tag) + && group.tag != request_tag + { + return Err(DpdError::Invalid(format!( + "tag mismatch: group has tag '{}' but request has tag '{request_tag}'", + group.tag + ))); + } let group = mcast.groups.remove(&group_ip).ok_or_else(|| { DpdError::Missing(format!( @@ -530,7 +592,7 @@ pub(crate) fn get_group_internal( s: &Switch, admin_local: AdminScopedIpv6, ) -> DpdResult { - let mcast = s.mcast.lock().unwrap(); + 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(|| { @@ -547,7 +609,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!( @@ -563,13 +625,15 @@ pub(crate) fn modify_group_external( group_ip: IpAddr, 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_for_update(&existing_group.tag, &new_group_info.tag)?; let nat_target = new_group_info @@ -592,14 +656,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(); @@ -610,16 +678,18 @@ 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 = AdminScopedIpv6::new(old_internal_ip)?; + let new_admin = AdminScopedIpv6::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); + // Update the external group fields (use new tag if provided, else keep existing) + updated_group.tag = new_group_info + .tag + .unwrap_or_else(|| updated_group.tag.clone()); updated_group.int_fwding.nat_target = Some(nat_target); let old_vlan_id = updated_group.ext_fwding.vlan_id; @@ -627,7 +697,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. @@ -659,9 +731,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)) = ( + AdminScopedIpv6::new(old_internal_ip), + AdminScopedIpv6::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:?}" @@ -680,13 +769,16 @@ pub(crate) fn modify_group_internal( group_ip: AdminScopedIpv6, 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_for_update(&existing_group.tag, &new_group_info.tag)?; let mut group_entry = mcast.groups.remove(&group_ip.into()).unwrap(); @@ -734,7 +826,9 @@ pub(crate) fn modify_group_internal( // Early return for no-replication case - just update metadata if replication_info.is_none() { - group_entry.tag = new_group_info.tag.or(group_entry.tag.clone()); + group_entry.tag = new_group_info + .tag + .unwrap_or_else(|| group_entry.tag.clone()); group_entry.sources = sources; group_entry.members = new_group_info.members; @@ -759,19 +853,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| { @@ -792,22 +903,36 @@ 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()); + group_entry.tag = new_group_info + .tag + .unwrap_or_else(|| group_entry.tag.clone()); group_entry.sources = sources; group_entry.replication_info = replication_info; group_entry.members = new_group_info.members; @@ -825,7 +950,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, @@ -837,9 +962,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) @@ -848,19 +971,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 ownership validation. + for (group_ip, _) in external_groups.into_iter().chain(internal_groups) { + if let Err(e) = del_group(s, group_ip, Some(tag)) { error!( s.log, "failed to delete multicast group for IP {group_ip}: {e:?}" @@ -873,36 +999,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(); @@ -950,7 +1059,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}" )) @@ -995,26 +1104,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(()) } @@ -1023,28 +1137,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}:{multicast_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) + } } } @@ -1053,24 +1241,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(()) } @@ -1079,12 +1279,36 @@ 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(()) } @@ -1464,10 +1688,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 @@ -1596,9 +1821,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)?; } @@ -1609,9 +1835,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)?; } @@ -1716,11 +1943,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<()> { @@ -1732,6 +1963,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( @@ -1741,17 +1974,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, - ) - }) } } } @@ -1773,6 +1995,7 @@ fn create_port_bitmap( #[cfg(test)] mod tests { use super::*; + use common::ports::RearPort; use std::thread; #[test] @@ -1840,10 +2063,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"), } } @@ -1977,4 +2200,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 f862f38f..663c8f43 100644 --- a/dpd/src/mcast/rollback.rs +++ b/dpd/src/mcast/rollback.rs @@ -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 94a81d04..e131dd45 100644 --- a/dpd/src/mcast/validate.rs +++ b/dpd/src/mcast/validate.rs @@ -17,7 +17,7 @@ use omicron_common::address::{ IPV6_INTERFACE_LOCAL_MULTICAST_SUBNET, IPV6_LINK_LOCAL_MULTICAST_SUBNET, IPV6_RESERVED_SCOPE_MULTICAST_SUBNET, IPV6_SSM_SUBNET, }; -use oxnet::{Ipv4Net, Ipv6Net}; +use oxnet::Ipv6Net; use super::IpSrc; use crate::types::{DpdError, DpdResult}; @@ -95,14 +95,9 @@ 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", - ))); } + // ASM: sources are optional (filtering or any-source) // Check reserved subnets if IPV4_LINK_LOCAL_MULTICAST_SUBNET.contains(addr) { @@ -133,13 +128,7 @@ 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", - ))); } // Check reserved subnets @@ -185,7 +174,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(()) @@ -236,25 +225,22 @@ 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", - ))); - } - - // Reject subnets with loopback or broadcast addresses - if addr.is_loopback() || addr.is_broadcast() { +/// Validates that the update request tag matches the existing group's tag. +/// +/// If the request tag is `None`, the group's existing tag (set or generated +/// at creation) is used as the default and validation succeeds. If provided, +/// it must match the group's existing tag. +pub(crate) fn validate_tag_for_update( + existing_tag: &str, + request_tag: &Option, +) -> DpdResult<()> { + if let Some(request) = request_tag + && request != existing_tag + { return Err(DpdError::Invalid(format!( - "Source subnet {subnet} contains invalid address types \ - (loopback/broadcast) for source filtering", + "tag mismatch: group has tag '{existing_tag}' but request has tag '{request}'" ))); } - Ok(()) } @@ -263,9 +249,6 @@ mod tests { use super::*; use common::{nat::Vni, network::MacAddr}; use dpd_types::mcast::ADMIN_LOCAL_PREFIX; - use oxnet::Ipv4Net; - - use std::str::FromStr; #[test] fn test_ipv4_validation() { @@ -344,89 +327,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 + // ASM with sources assert!( - validate_ipv4_multicast(non_ssm_addr, Some(&exact_sources)) - .is_err() + validate_ipv4_multicast(asm_addr, Some(&exact_sources)).is_ok() ); - assert!( - validate_ipv4_multicast(non_ssm_addr, Some(&subnet_sources)) - .is_err() - ); - assert!( - validate_ipv4_multicast(non_ssm_addr, Some(&mixed_sources)) - .is_err() - ); - - // 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] @@ -473,7 +434,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( @@ -524,13 +485,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 @@ -542,13 +503,13 @@ 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() ); } @@ -598,12 +559,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 = @@ -636,20 +594,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()); @@ -659,8 +603,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)))]; @@ -702,4 +644,22 @@ mod tests { .contains("is not a valid source address") ); } + + #[test] + fn test_validate_tag_for_update() { + // Existing tag matches request tag + assert!( + validate_tag_for_update("my-tag", &Some("my-tag".to_string())) + .is_ok() + ); + + // Existing tag but request has different tag + let result = + validate_tag_for_update("owner-a", &Some("owner-b".to_string())); + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("tag mismatch")); + + // No tag provided uses existing tag as default (validation passes) + assert!(validate_tag_for_update("my-tag", &None).is_ok()); + } } diff --git a/dpd/src/port_map.rs b/dpd/src/port_map.rs index 2f00119b..f2bafb2f 100644 --- a/dpd/src/port_map.rs +++ b/dpd/src/port_map.rs @@ -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 5750996f..c32cf67c 100644 --- a/dpd/src/table/mcast/mcast_egress.rs +++ b/dpd/src/table/mcast/mcast_egress.rs @@ -303,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 } @@ -377,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 43492129..6c83b1ba 100644 --- a/dpd/src/table/mcast/mcast_route.rs +++ b/dpd/src/table/mcast/mcast_route.rs @@ -127,7 +127,7 @@ pub(crate) fn add_ipv6_entry( let internal_ip = Ipv6Net::new_unchecked(route, 128); // Admin-local multicast and unique local addresses are internal to the rack - // and don't require VLAN tagging, so always use Forward action + // and don't require VLAN tagging, so always use Forward action. let action_data: Ipv6Action = if internal_ip.is_admin_local_multicast() || internal_ip.is_unique_local() { @@ -160,7 +160,7 @@ pub(crate) fn update_ipv6_entry( let internal_ip = Ipv6Net::new_unchecked(route, 128); // Admin-local multicast and unique local addresses are internal to the rack - // and don't require VLAN tagging, so always use Forward action + // and don't require VLAN tagging, so always use Forward action. let action_data: Ipv6Action = if internal_ip.is_admin_local_multicast() || internal_ip.is_unique_local() { diff --git a/dpd/src/table/mcast/mcast_src_filter.rs b/dpd/src/table/mcast/mcast_src_filter.rs index 133de4b5..aa870cf9 100644 --- a/dpd/src/table/mcast/mcast_src_filter.rs +++ b/dpd/src/table/mcast/mcast_src_filter.rs @@ -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 d466d963..5c7f9a81 100644 --- a/dpd/src/types.rs +++ b/dpd/src/types.rs @@ -178,6 +178,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, diff --git a/openapi/dpd/dpd-2.0.0-e62a18.json b/openapi/dpd/dpd-2.0.0-e62a18.json new file mode 100644 index 00000000..454626bc --- /dev/null +++ b/openapi/dpd/dpd-2.0.0-e62a18.json @@ -0,0 +1,9643 @@ +{ + "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": "2.0.0" + }, + "paths": { + "/all-settings": { + "delete": { + "summary": "Clear all settings.", + "description": "This removes all data entirely, including:\n\n- All ARP and NDP table entries - All routes - All links on all switch ports - All NAT mappings - All 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:\n\n- All ARP or NDP table entries. - All routes - 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 crated 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 v1/v2).", + "operationId": "multicast_group_create_external_v2", + "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 v1/v2).", + "description": "Returns 201 Created (v4+ returns 200 OK).", + "operationId": "multicast_group_update_external_v2", + "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 v1/v2).", + "operationId": "multicast_groups_list_v2", + "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 v1/v2).", + "operationId": "multicast_group_get_v2", + "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).", + "description": "Does not include tag validation.", + "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 v1/v2).", + "operationId": "multicast_groups_list_by_tag_v2", + "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.", + "operationId": "multicast_reset_by_tag", + "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).", + "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).", + "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/ipv4/gen": { + "get": { + "summary": "Get NATv4 generation number", + "operationId": "ipv4_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/ipv4/trigger": { + "post": { + "summary": "Trigger NATv4 Reconciliation", + "operationId": "ipv4_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.\n\nAdmin-local addresses are ff04::/16 (scope 4). These are used for internal/underlay multicast groups.", + "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 (API versions 1 and 2).\n\nThis is the original `IpSrc` enum that used a single `Subnet` variant (IPv4 only) rather than the `Any` variant added in version 3.", + "oneOf": [ + { + "description": "Exact match for the source IP address.", + "type": "object", + "properties": { + "Exact": { + "type": "string", + "format": "ip" + } + }, + "required": [ + "Exact" + ], + "additionalProperties": false + }, + { + "description": "Subnet match for the source IP address.", + "type": "object", + "properties": { + "Subnet": { + "$ref": "#/components/schemas/Ipv4Net" + } + }, + "required": [ + "Subnet" + ], + "additionalProperties": false + } + ] + }, + "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 (API version 2).", + "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, + "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/AdminScopedIpv6" + }, + "members": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MulticastGroupMember" + } + }, + "tag": { + "nullable": true, + "type": "string" + } + }, + "required": [ + "group_ip", + "members" + ] + }, + "MulticastGroupExternalResponse": { + "description": "Response structure for external multicast group operations (API version 2).", + "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 2).", + "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 2).", + "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 (to the rack) groups (API version 2).", + "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, + "type": "string" + } + }, + "required": [ + "external_forwarding", + "internal_forwarding" + ] + }, + "MulticastGroupUpdateUnderlayEntry": { + "description": "Represents a multicast replication entry for PUT requests for internal (to the rack) groups.", + "type": "object", + "properties": { + "members": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MulticastGroupMember" + } + }, + "tag": { + "nullable": true, + "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" + ] + }, + "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-2.0.0-4ba80a.json b/openapi/dpd/dpd-3.0.0-6ed477.json similarity index 99% rename from openapi/dpd/dpd-2.0.0-4ba80a.json rename to openapi/dpd/dpd-3.0.0-6ed477.json index 63df87ff..a66e9a10 100644 --- a/openapi/dpd/dpd-2.0.0-4ba80a.json +++ b/openapi/dpd/dpd-3.0.0-6ed477.json @@ -7,13 +7,13 @@ "url": "https://oxide.computer", "email": "api@oxide.computer" }, - "version": "2.0.0" + "version": "3.0.0" }, "paths": { "/all-settings": { "delete": { "summary": "Clear all settings.", - "description": "This removes all data entirely.", + "description": "This removes all data entirely, including:\n\n- All ARP and NDP table entries - All routes - All links on all switch ports - All NAT mappings - All multicast groups\n\nNote: Unlike `reset_all_tagged`, this endpoint DOES clear multicast groups.", "operationId": "reset_all", "responses": { "204": { @@ -31,7 +31,7 @@ "/all-settings/{tag}": { "delete": { "summary": "Clear all settings associated with a specific tag.", - "description": "This removes:\n\n- All ARP or NDP table entries. - All routes - All links on all switch ports", + "description": "This removes:\n\n- All ARP or NDP table entries. - All routes - 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": [ { @@ -1121,9 +1121,9 @@ }, "/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.", - "operationId": "multicast_group_update_external", + "summary": "Update an external-only multicast group configuration (API v3).", + "description": "Returns 201 Created (v4+ returns 200 OK).", + "operationId": "multicast_group_update_external_v3", "parameters": [ { "in": "path", @@ -1264,8 +1264,9 @@ } }, "delete": { - "summary": "Delete a multicast group configuration by IP address.", - "operationId": "multicast_group_delete", + "summary": "Delete a multicast group configuration by IP address (API versions 1-3).", + "description": "Does not include tag validation.", + "operationId": "multicast_group_delete_v3", "parameters": [ { "in": "path", @@ -1374,9 +1375,8 @@ }, "/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", + "summary": "Create an underlay (internal) multicast group configuration (API v1-v3).", + "operationId": "multicast_group_create_underlay_v3", "requestBody": { "content": { "application/json": { @@ -1409,9 +1409,8 @@ }, "/multicast/underlay-groups/{group_ip}": { "get": { - "summary": "Get an underlay (internal) multicast group configuration by admin-local", - "description": "IPv6 address.\n\nUnderlay groups handle admin-local IPv6 multicast traffic (ff04::/16) with replication infrastructure for external and underlay members.", - "operationId": "multicast_group_get_underlay", + "summary": "Get an underlay (internal) multicast group configuration (API v1-v3).", + "operationId": "multicast_group_get_underlay_v3", "parameters": [ { "in": "path", @@ -1442,9 +1441,8 @@ } }, "put": { - "summary": "Update an underlay (internal) multicast group configuration for a given", - "description": "group IP address.\n\nUnderlay groups are used for admin-local IPv6 multicast traffic (ff04::/16) that requires replication infrastructure with external and underlay members.", - "operationId": "multicast_group_update_underlay", + "summary": "Update an underlay (internal) multicast group configuration (API v1-v3).", + "operationId": "multicast_group_update_underlay_v3", "parameters": [ { "in": "path", @@ -1488,7 +1486,7 @@ "/multicast/untagged": { "delete": { "summary": "Delete all multicast groups (and associated routes) without a tag.", - "operationId": "multicast_reset_untagged", + "operationId": "multicast_reset_untagged_v3", "responses": { "204": { "description": "successful deletion" @@ -6262,7 +6260,7 @@ } }, "IpSrc": { - "description": "Source filter match key for multicast traffic.", + "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.", @@ -6279,17 +6277,11 @@ "additionalProperties": false }, { - "description": "Subnet match for the source IP address.", - "type": "object", - "properties": { - "Subnet": { - "$ref": "#/components/schemas/Ipv4Net" - } - }, - "required": [ - "Subnet" - ], - "additionalProperties": false + "description": "Match any source address (0.0.0.0/0 or ::/0 depending on group IP version).", + "type": "string", + "enum": [ + "Any" + ] } ] }, @@ -7229,7 +7221,7 @@ ] }, "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.", + "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 @@ -7791,7 +7783,7 @@ } }, "tag": { - "nullable": true, + "description": "Tag for ownership validation. Always present; generated as `{uuid}:{group_ip}` if not provided at creation time.", "type": "string" } }, @@ -7799,7 +7791,8 @@ "external_forwarding", "external_group_id", "group_ip", - "internal_forwarding" + "internal_forwarding", + "tag" ] }, "MulticastGroupMember": { @@ -7850,7 +7843,7 @@ } }, "tag": { - "nullable": true, + "description": "Tag for ownership validation. Always present; generated as `{uuid}:{group_ip}` if not provided at creation time.", "type": "string" }, "underlay_group_id": { @@ -7864,6 +7857,7 @@ "group_ip", "kind", "members", + "tag", "underlay_group_id" ] }, @@ -7900,7 +7894,7 @@ } }, "tag": { - "nullable": true, + "description": "Tag for ownership validation. Always present; generated as `{uuid}:{group_ip}` if not provided at creation time.", "type": "string" } }, @@ -7909,7 +7903,8 @@ "external_group_id", "group_ip", "internal_forwarding", - "kind" + "kind", + "tag" ] } ] @@ -7936,7 +7931,7 @@ ] }, "MulticastGroupUnderlayResponse": { - "description": "Response structure for underlay/internal multicast group operations. These groups handle admin-scoped IPv6 multicast with full replication.", + "description": "Response structure for underlay/internal multicast group operations (API version 3).", "type": "object", "properties": { "external_group_id": { diff --git a/openapi/dpd/dpd-4.0.0-af315d.json b/openapi/dpd/dpd-4.0.0-af315d.json new file mode 100644 index 00000000..0dad3ee6 --- /dev/null +++ b/openapi/dpd/dpd-4.0.0-af315d.json @@ -0,0 +1,9655 @@ +{ + "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, including:\n\n- All ARP and NDP table entries - All routes - All links on all switch ports - All NAT mappings - All 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:\n\n- All ARP or NDP table entries. - All routes - 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 crated 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.", + "operationId": "multicast_group_update_external", + "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": { + "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). When a tag is provided in the query, it must match the group's existing tag to prove ownership. Omitting the tag skips validation.", + "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 for ownership validation. Omit to skip validation.", + "schema": { + "nullable": true, + "type": "string" + } + } + ], + "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.", + "operationId": "multicast_groups_list_by_tag", + "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.", + "operationId": "multicast_reset_by_tag", + "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.", + "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/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.", + "description": "Underlay groups are used for admin-local IPv6 multicast traffic (ff04::/16) that requires replication infrastructure with external and underlay members.", + "operationId": "multicast_group_update_underlay", + "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.", + "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/ipv4/gen": { + "get": { + "summary": "Get NATv4 generation number", + "operationId": "ipv4_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/ipv4/trigger": { + "post": { + "summary": "Trigger NATv4 Reconciliation", + "operationId": "ipv4_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.\n\nAdmin-local addresses are ff04::/16 (scope 4). These are used for internal/underlay multicast groups.", + "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, + "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/AdminScopedIpv6" + }, + "members": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MulticastGroupMember" + } + }, + "tag": { + "nullable": true, + "type": "string" + } + }, + "required": [ + "group_ip", + "members" + ] + }, + "MulticastGroupExternalResponse": { + "description": "Response structure for external multicast group operations. These groups handle IPv4 and non-admin 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 ownership validation. Always present; 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-scoped IPv6 multicast with full replication.", + "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": { + "description": "Tag for ownership validation. Always present; 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 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 ownership validation. Always present; 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-scoped IPv6 multicast with full replication.", + "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": { + "description": "Tag for ownership validation. Always present; 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.", + "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, + "type": "string" + } + }, + "required": [ + "external_forwarding", + "internal_forwarding" + ] + }, + "MulticastGroupUpdateUnderlayEntry": { + "description": "Represents a multicast replication entry for PUT requests for internal (to the rack) groups.", + "type": "object", + "properties": { + "members": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MulticastGroupMember" + } + }, + "tag": { + "nullable": true, + "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" + ] + }, + "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-latest.json b/openapi/dpd/dpd-latest.json index 437c9096..add67937 120000 --- a/openapi/dpd/dpd-latest.json +++ b/openapi/dpd/dpd-latest.json @@ -1 +1 @@ -dpd-2.0.0-4ba80a.json \ No newline at end of file +dpd-4.0.0-af315d.json \ No newline at end of file From db21e5064fa65231034470dfa330e1382294e809 Mon Sep 17 00:00:00 2001 From: Zeeshan Lakhani Date: Wed, 14 Jan 2026 04:29:16 +0000 Subject: [PATCH 08/22] [fix] update for apis test and copyrights! --- .github/buildomat/illumos.sh | 12 +--- dpd-api/src/lib.rs | 2 +- dpd-api/src/v3.rs | 2 +- dpd-client/tests/integration_tests/mcast.rs | 2 +- dpd-client/tests/integration_tests/service.rs | 2 +- .../tests/integration_tests/table_tests.rs | 2 +- dpd-types/src/link.rs | 2 +- dpd-types/src/mcast.rs | 2 +- dpd/p4/sidecar.p4 | 2 +- dpd/src/api_server.rs | 2 +- dpd/src/mcast/mod.rs | 2 +- dpd/src/mcast/rollback.rs | 2 +- dpd/src/mcast/validate.rs | 2 +- dpd/src/port_map.rs | 2 +- dpd/src/table/mcast/mcast_egress.rs | 2 +- dpd/src/table/mcast/mcast_route.rs | 2 +- dpd/src/table/mcast/mcast_src_filter.rs | 2 +- dpd/src/types.rs | 2 +- ....0.0-c75a52.json => dpd-2.0.0-74a45c.json} | 69 ++++++++++--------- swadm/src/link.rs | 2 +- 20 files changed, 57 insertions(+), 60 deletions(-) rename openapi/dpd/{dpd-2.0.0-c75a52.json => dpd-2.0.0-74a45c.json} (99%) diff --git a/.github/buildomat/illumos.sh b/.github/buildomat/illumos.sh index ed7f7039..73e2e382 100644 --- a/.github/buildomat/illumos.sh +++ b/.github/buildomat/illumos.sh @@ -2,21 +2,15 @@ banner "sde setup" export PKG=tofino_sde.p5p -curl -OL $SDE_DIR/$PKG -SDE_CALC=`digest -a sha256 $PKG` -if [ $SDE_CALC != $SDE_PKG_SHA256 ]; then - echo "downloaded tofino_sde has a bad checksum" - exit 1 -fi -pfexec pkg install -g $PKG tofino_sde +#pfexec pkg install -g $PKG tofino_sde export SDE=/opt/oxide/tofino_sde export LD_LIBRARY_PATH="$SDE/lib:$LD_LIBRARY_PATH" # Install a couple of non-standard packages needed to build dendrite banner "packages" -pfexec pkg install clang-15 pcap -pfexec pkg set-mediator -V 15 clang llvm +#pfexec pkg install clang-15 pcap +#pfexec pkg set-mediator -V 15 clang llvm cargo --version rustc --version diff --git a/dpd-api/src/lib.rs b/dpd-api/src/lib.rs index 303c939d..c5196cd0 100644 --- a/dpd-api/src/lib.rs +++ b/dpd-api/src/lib.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 //! DPD endpoint definitions. diff --git a/dpd-api/src/v3.rs b/dpd-api/src/v3.rs index a4bc0249..24df4d46 100644 --- a/dpd-api/src/v3.rs +++ b/dpd-api/src/v3.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 from API version 3 that changed in version 4. //! diff --git a/dpd-client/tests/integration_tests/mcast.rs b/dpd-client/tests/integration_tests/mcast.rs index 98399ad9..9492c22a 100644 --- a/dpd-client/tests/integration_tests/mcast.rs +++ b/dpd-client/tests/integration_tests/mcast.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::HashSet, diff --git a/dpd-client/tests/integration_tests/service.rs b/dpd-client/tests/integration_tests/service.rs index bbff9782..3c5c6a19 100644 --- a/dpd-client/tests/integration_tests/service.rs +++ b/dpd-client/tests/integration_tests/service.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::sync::Arc; diff --git a/dpd-client/tests/integration_tests/table_tests.rs b/dpd-client/tests/integration_tests/table_tests.rs index 9ed977b2..f79b18f6 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; diff --git a/dpd-types/src/link.rs b/dpd-types/src/link.rs index e5a1ac75..3e13d7b4 100644 --- a/dpd-types/src/link.rs +++ b/dpd-types/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::fmt; diff --git a/dpd-types/src/mcast.rs b/dpd-types/src/mcast.rs index a4a3e939..5830cee1 100644 --- a/dpd-types/src/mcast.rs +++ b/dpd-types/src/mcast.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 //! Public types for multicast group management. diff --git a/dpd/p4/sidecar.p4 b/dpd/p4/sidecar.p4 index 68803a40..1b30a855 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 diff --git a/dpd/src/api_server.rs b/dpd/src/api_server.rs index 1d408733..133f5c62 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. diff --git a/dpd/src/mcast/mod.rs b/dpd/src/mcast/mod.rs index e99eb4cc..c753acec 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. //! diff --git a/dpd/src/mcast/rollback.rs b/dpd/src/mcast/rollback.rs index 165b0864..65fc2184 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. //! diff --git a/dpd/src/mcast/validate.rs b/dpd/src/mcast/validate.rs index d1f68173..97ab4e44 100644 --- a/dpd/src/mcast/validate.rs +++ b/dpd/src/mcast/validate.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 address validation. //! diff --git a/dpd/src/port_map.rs b/dpd/src/port_map.rs index f2bafb2f..b167daf2 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. //! diff --git a/dpd/src/table/mcast/mcast_egress.rs b/dpd/src/table/mcast/mcast_egress.rs index c32cf67c..79b7e1b1 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. diff --git a/dpd/src/table/mcast/mcast_route.rs b/dpd/src/table/mcast/mcast_route.rs index 6c83b1ba..71281d9c 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). diff --git a/dpd/src/table/mcast/mcast_src_filter.rs b/dpd/src/table/mcast/mcast_src_filter.rs index aa870cf9..c3c22434 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. diff --git a/dpd/src/types.rs b/dpd/src/types.rs index 5c7f9a81..4bd54503 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. diff --git a/openapi/dpd/dpd-2.0.0-c75a52.json b/openapi/dpd/dpd-2.0.0-74a45c.json similarity index 99% rename from openapi/dpd/dpd-2.0.0-c75a52.json rename to openapi/dpd/dpd-2.0.0-74a45c.json index 37df7672..d876aebc 100644 --- a/openapi/dpd/dpd-2.0.0-c75a52.json +++ b/openapi/dpd/dpd-2.0.0-74a45c.json @@ -13,7 +13,7 @@ "/all-settings": { "delete": { "summary": "Clear all settings.", - "description": "This removes all data entirely, including:\n\n- All ARP and NDP table entries - All routes - All links on all switch ports - All NAT mappings - All multicast groups\n\nNote: Unlike `reset_all_tagged`, this endpoint does clear multicast groups.", + "description": "This removes all data entirely.", "operationId": "reset_all", "responses": { "204": { @@ -31,7 +31,7 @@ "/all-settings/{tag}": { "delete": { "summary": "Clear all settings associated with a specific tag.", - "description": "This removes:\n\n- All ARP or NDP table entries. - All routes - 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.", + "description": "This removes:\n\n- All ARP or NDP table entries. - All routes - All links on all switch ports", "operationId": "reset_all_tagged", "parameters": [ { @@ -1086,8 +1086,9 @@ }, "/multicast/external-groups": { "post": { - "summary": "Create an external-only multicast group configuration (API v1/v2).", - "operationId": "multicast_group_create_external_v2", + "summary": "Create an external-only multicast group configuration.", + "description": "External-only groups are used for IPv4 and non-admin-scoped 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": { @@ -1120,9 +1121,9 @@ }, "/multicast/external-groups/{group_ip}": { "put": { - "summary": "Update an external-only multicast group configuration (API v1/v2).", - "description": "Returns 201 Created (v4+ returns 200 OK).", - "operationId": "multicast_group_update_external_v2", + "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-scoped IPv6 multicast traffic that doesn't require replication infrastructure.", + "operationId": "multicast_group_update_external", "parameters": [ { "in": "path", @@ -1166,8 +1167,8 @@ }, "/multicast/groups": { "get": { - "summary": "List all multicast groups (API v1/v2).", - "operationId": "multicast_groups_list_v2", + "summary": "List all multicast groups.", + "operationId": "multicast_groups_list", "parameters": [ { "in": "query", @@ -1230,8 +1231,8 @@ }, "/multicast/groups/{group_ip}": { "get": { - "summary": "Get the multicast group configuration for a given group IP address (API v1/v2).", - "operationId": "multicast_group_get_v2", + "summary": "Get the multicast group configuration for a given group IP address.", + "operationId": "multicast_group_get", "parameters": [ { "in": "path", @@ -1263,9 +1264,8 @@ } }, "delete": { - "summary": "Delete a multicast group configuration by IP address (API versions 1-3).", - "description": "Does not include tag validation.", - "operationId": "multicast_group_delete_v3", + "summary": "Delete a multicast group configuration by IP address.", + "operationId": "multicast_group_delete", "parameters": [ { "in": "path", @@ -1292,8 +1292,8 @@ }, "/multicast/tags/{tag}": { "get": { - "summary": "List all multicast groups with a given tag (API v1/v2).", - "operationId": "multicast_groups_list_by_tag_v2", + "summary": "List all multicast groups with a given tag.", + "operationId": "multicast_groups_list_by_tag", "parameters": [ { "in": "path", @@ -1374,8 +1374,9 @@ }, "/multicast/underlay-groups": { "post": { - "summary": "Create an underlay (internal) multicast group configuration (API v1-v3).", - "operationId": "multicast_group_create_underlay_v3", + "summary": "Create an underlay (internal) multicast group configuration.", + "description": "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.", + "operationId": "multicast_group_create_underlay", "requestBody": { "content": { "application/json": { @@ -1408,8 +1409,9 @@ }, "/multicast/underlay-groups/{group_ip}": { "get": { - "summary": "Get an underlay (internal) multicast group configuration (API v1-v3).", - "operationId": "multicast_group_get_underlay_v3", + "summary": "Get an underlay (internal) multicast group configuration by admin-scoped", + "description": "IPv6 address.\n\nUnderlay groups handle admin-scoped IPv6 multicast traffic with replication infrastructure for external and underlay members.", + "operationId": "multicast_group_get_underlay", "parameters": [ { "in": "path", @@ -1440,8 +1442,9 @@ } }, "put": { - "summary": "Update an underlay (internal) multicast group configuration (API v1-v3).", - "operationId": "multicast_group_update_underlay_v3", + "summary": "Update an underlay (internal) multicast group configuration for a given", + "description": "group IP address.\n\nUnderlay groups are used for admin-scoped IPv6 multicast traffic that requires replication infrastructure with external and underlay members.", + "operationId": "multicast_group_update_underlay", "parameters": [ { "in": "path", @@ -1485,7 +1488,7 @@ "/multicast/untagged": { "delete": { "summary": "Delete all multicast groups (and associated routes) without a tag.", - "operationId": "multicast_reset_untagged_v3", + "operationId": "multicast_reset_untagged", "responses": { "204": { "description": "successful deletion" @@ -5240,7 +5243,7 @@ "components": { "schemas": { "AdminScopedIpv6": { - "description": "A validated admin-local IPv6 multicast address.\n\nAdmin-local addresses are ff04::/16 (scope 4). These are used for internal/underlay multicast groups.", + "description": "A validated admin-scoped IPv6 multicast address.\n\nAdmin-scoped addresses are ff04::/16, ff05::/16, or ff08::/16. These are used for internal/underlay multicast groups.", "type": "string", "format": "ipv6" }, @@ -6259,7 +6262,7 @@ } }, "IpSrc": { - "description": "Source filter match key for multicast traffic (API versions 1 and 2).\n\nThis is the original `IpSrc` enum that used a single `Subnet` variant (IPv4 only) rather than the `Any` variant added in version 3.", + "description": "Source filter match key for multicast traffic.", "oneOf": [ { "description": "Exact match for the source IP address.", @@ -7226,7 +7229,7 @@ ] }, "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", + "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.", "type": "integer", "format": "uint8", "minimum": 0 @@ -7707,7 +7710,7 @@ } }, "MulticastGroupCreateExternalEntry": { - "description": "A multicast group configuration for POST requests for external (to the rack) groups (API version 2).", + "description": "A multicast group configuration for POST requests for external (to the rack) groups.", "type": "object", "properties": { "external_forwarding": { @@ -7762,7 +7765,7 @@ ] }, "MulticastGroupExternalResponse": { - "description": "Response structure for external multicast group operations (API version 2).", + "description": "Response structure for external multicast group operations. These groups handle IPv4 and non-admin IPv6 multicast via NAT targets.", "type": "object", "properties": { "external_forwarding": { @@ -7820,10 +7823,10 @@ ] }, "MulticastGroupResponse": { - "description": "Unified response type for operations that return mixed group types (API version 2).", + "description": "Unified response type for operations that return mixed group types.", "oneOf": [ { - "description": "Response structure for underlay/internal multicast group operations (API version 3).", + "description": "Response structure for underlay/internal multicast group operations. These groups handle admin-scoped IPv6 multicast with full replication.", "type": "object", "properties": { "external_group_id": { @@ -7865,7 +7868,7 @@ ] }, { - "description": "Response structure for external multicast group operations (API version 2).", + "description": "Response structure for external multicast group operations. These groups handle IPv4 and non-admin IPv6 multicast via NAT targets.", "type": "object", "properties": { "external_forwarding": { @@ -7933,7 +7936,7 @@ ] }, "MulticastGroupUnderlayResponse": { - "description": "Response structure for underlay/internal multicast group operations (API version 3).", + "description": "Response structure for underlay/internal multicast group operations. These groups handle admin-scoped IPv6 multicast with full replication.", "type": "object", "properties": { "external_group_id": { @@ -7968,7 +7971,7 @@ ] }, "MulticastGroupUpdateExternalEntry": { - "description": "A multicast group update entry for PUT requests for external (to the rack) groups (API version 2).", + "description": "A multicast group update entry for PUT requests for external (to the rack) groups.", "type": "object", "properties": { "external_forwarding": { diff --git a/swadm/src/link.rs b/swadm/src/link.rs index f04d4d25..769e6ed4 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; From e8ab513df6e2e692404564093fa7cddd7d4bf517 Mon Sep 17 00:00:00 2001 From: Zeeshan Lakhani Date: Wed, 14 Jan 2026 04:36:58 +0000 Subject: [PATCH 09/22] .. --- .github/buildomat/illumos.sh | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/buildomat/illumos.sh b/.github/buildomat/illumos.sh index 73e2e382..ed7f7039 100644 --- a/.github/buildomat/illumos.sh +++ b/.github/buildomat/illumos.sh @@ -2,15 +2,21 @@ banner "sde setup" export PKG=tofino_sde.p5p -#pfexec pkg install -g $PKG tofino_sde +curl -OL $SDE_DIR/$PKG +SDE_CALC=`digest -a sha256 $PKG` +if [ $SDE_CALC != $SDE_PKG_SHA256 ]; then + echo "downloaded tofino_sde has a bad checksum" + exit 1 +fi +pfexec pkg install -g $PKG tofino_sde export SDE=/opt/oxide/tofino_sde export LD_LIBRARY_PATH="$SDE/lib:$LD_LIBRARY_PATH" # Install a couple of non-standard packages needed to build dendrite banner "packages" -#pfexec pkg install clang-15 pcap -#pfexec pkg set-mediator -V 15 clang llvm +pfexec pkg install clang-15 pcap +pfexec pkg set-mediator -V 15 clang llvm cargo --version rustc --version From 51c162bbb0ab337d4d86ed6450a383f3109f3b02 Mon Sep 17 00:00:00 2001 From: Zeeshan Lakhani Date: Wed, 14 Jan 2026 12:47:51 -0500 Subject: [PATCH 10/22] [multicast, apis]: fix API versioning consistency & tag validation / specification Multicast groups now require tag-based validation for mutations (delete/update) in API v4 (current). Tags are assigned at creation (user-provided or auto-generated as `{uuid}:{group_ip}`) and are immutable. This prevents accidental modification or deletion of groups associated with other components, and better works with Omicron's retry model. This commit also addresses several issues with this branch's multicast API versioning scheme, including restructuring versions on this branch to follow this ordering: - v4 = TAG_OWNERSHIP: required tags for delete/update as query params, IpSrc::Any - v3 = SOURCE_FILTER_ANY: optional tags, IpSrc::Any - (MCAST_DOCS_ADMIN_LOCAL was removed) - v2 = DUAL_STACK_NAT_WORKFLOW: optional tags, IpSrc::Subnet - v1 = INITIAL API version 4 changes: - DELETE requires tag query parameter that must match the group's tag - PUT validates that the provided tag matches the existing group's tag - All response types have `tag: String` (always present, never null) Backward compatibility (v1-v3): - DELETE does not require tag (handler looks up existing tag internally) - PUT tag is optional; if omitted, existing tag is preserved - Response types have `tag: Option` for v3, converted from v4 Tag format: 1-80 ASCII bytes, alphanumeric plus hyphens, underscores, colons, and periods. Constraint matches Omicron's database schema (post-update there). --- dpd-api/src/lib.rs | 317 +- dpd-api/src/v2.rs | 11 +- dpd-api/src/v3.rs | 58 +- dpd-client/tests/integration_tests/mcast.rs | 1356 ++- .../tests/integration_tests/table_tests.rs | 7 +- dpd-types/src/mcast.rs | 30 +- dpd/src/api_server.rs | 113 +- dpd/src/mcast/mod.rs | 87 +- dpd/src/mcast/validate.rs | 110 +- dpd/src/types.rs | 5 + ....0.0-5e9839.json => dpd-3.0.0-19eaa3.json} | 62 +- openapi/dpd/dpd-3.0.0-f810b8.json | 9643 ----------------- ....0.0-c7d322.json => dpd-4.0.0-00cf42.json} | 68 +- openapi/dpd/dpd-latest.json | 2 +- 14 files changed, 1300 insertions(+), 10569 deletions(-) rename openapi/dpd/{dpd-4.0.0-5e9839.json => dpd-3.0.0-19eaa3.json} (99%) delete mode 100644 openapi/dpd/dpd-3.0.0-f810b8.json rename openapi/dpd/{dpd-5.0.0-c7d322.json => dpd-4.0.0-00cf42.json} (98%) diff --git a/dpd-api/src/lib.rs b/dpd-api/src/lib.rs index c5196cd0..0ca1aff3 100644 --- a/dpd-api/src/lib.rs +++ b/dpd-api/src/lib.rs @@ -59,9 +59,8 @@ api_versions!([ // | example for the next person. // v // (next_int, IDENT), - (5, MCAST_TAG_OWNERSHIP), - (4, MCAST_SOURCE_FILTER_ANY), - (3, MCAST_DOCS_ADMIN_LOCAL), + (4, MCAST_TAG_OWNERSHIP), + (3, MCAST_SOURCE_FILTER_ANY), (2, DUAL_STACK_NAT_WORKFLOW), (1, INITIAL), ]); @@ -373,7 +372,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, @@ -1162,11 +1161,8 @@ pub trait DpdApi { /** * Clear all settings associated with a specific tag. * - * This removes: - * - * - All ARP or NDP table entries. - * - All routes - * - All links on all switch ports + * This removes all ARP or NDP table entries, all routes, and 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 @@ -1185,13 +1181,8 @@ pub trait DpdApi { /** * Clear all settings. * - * This removes all data entirely, including: - * - * - All ARP and NDP table entries - * - All routes - * - All links on all switch ports - * - All NAT mappings - * - All multicast groups + * 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. */ @@ -1483,7 +1474,7 @@ pub trait DpdApi { #[endpoint { method = POST, path = "/multicast/external-groups", - versions = VERSION_MCAST_SOURCE_FILTER_ANY.., + versions = VERSION_MCAST_TAG_OWNERSHIP.., }] async fn multicast_group_create_external( rqctx: RequestContext, @@ -1493,7 +1484,28 @@ pub trait DpdApi { HttpError, >; - /// Create an external-only multicast group configuration (API v1/v2). + /// Create an external-only multicast group configuration (API v3). + #[endpoint { + method = POST, + path = "/multicast/external-groups", + versions = VERSION_MCAST_SOURCE_FILTER_ANY..VERSION_MCAST_TAG_OWNERSHIP, + }] + 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", @@ -1561,11 +1573,12 @@ pub trait DpdApi { } } - /// Delete a multicast group configuration by IP address (API version 4+). - /// - /// All groups have tags (auto-generated if not provided at creation). - /// When a tag is provided in the query, it must match the group's - /// existing tag to prove ownership. Omitting the tag skips validation. + /** + * 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}", @@ -1577,10 +1590,11 @@ pub trait DpdApi { query: Query, ) -> Result; - /// Delete a multicast group configuration by IP address (API versions 1-3). - /// - /// Does not include tag validation. - /// + /** + * Delete a multicast group configuration by IP address (API versions 1-3). + * + * Does not include tag validation. + */ // We cannot provide a default implementation that delegates to // `multicast_group_delete` because Dropshot's `Query` extractor is opaque // and cannot be constructed outside the framework. @@ -1611,14 +1625,30 @@ pub trait DpdApi { #[endpoint { method = GET, path = "/multicast/groups/{group_ip}", - versions = VERSION_MCAST_SOURCE_FILTER_ANY.., + versions = VERSION_MCAST_TAG_OWNERSHIP.., }] async fn multicast_group_get( rqctx: RequestContext, path: Path, ) -> Result, HttpError>; - /// Get the multicast group configuration for a given group IP address (API v1/v2). + /// 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_TAG_OWNERSHIP, + }] + 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}", @@ -1672,6 +1702,8 @@ pub trait DpdApi { * * 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, @@ -1681,10 +1713,18 @@ pub trait DpdApi { 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). + /** + * Update an underlay (internal) multicast group configuration (API v1-v3). + * + * Tags are optional. If a tag is not provided, the existing tag is preserved. + */ + // We cannot provide a default implementation that delegates to + // `multicast_group_update_underlay` because the implementor must look up + // the existing tag if not provided in the request. #[endpoint { method = PUT, path = "/multicast/underlay-groups/{group_ip}", @@ -1693,20 +1733,16 @@ pub trait DpdApi { async fn multicast_group_update_underlay_v3( rqctx: RequestContext, path: Path, - group: TypedBody, - ) -> Result, HttpError> - { - match Self::multicast_group_update_underlay(rqctx, path, group).await { - Ok(HttpResponseOk(resp)) => Ok(HttpResponseOk(resp.into())), - Err(e) => Err(e), - } - } + 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-local IPv6 multicast * traffic that doesn't require replication infrastructure. + * + * The `tag` query parameter must match the group's existing tag. */ #[endpoint { method = PUT, @@ -1716,12 +1752,19 @@ pub trait DpdApi { 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). - /// - /// Returns 201 Created (v4+ returns 200 OK). + /** + * Update an external-only multicast group configuration (API v3). + * + * Tags are optional. If a tag is not provided, the existing tag is preserved. + * Returns 201 Created (API v4+ returns 200 OK). + */ + // We cannot provide a default implementation that delegates to + // `multicast_group_update_external` because the implementor must look up + // the existing tag if not provided in the request. #[endpoint { method = PUT, path = "/multicast/external-groups/{group_ip}", @@ -1730,20 +1773,21 @@ pub trait DpdApi { async fn multicast_group_update_external_v3( rqctx: RequestContext, path: Path, - group: TypedBody, + group: TypedBody, ) -> Result< - HttpResponseCreated, + HttpResponseCreated, HttpError, - > { - match Self::multicast_group_update_external(rqctx, path, group).await { - Ok(HttpResponseOk(resp)) => Ok(HttpResponseCreated(resp)), - Err(e) => Err(e), - } - } + >; - /// Update an external-only multicast group configuration (API v1/v2). - /// - /// Returns 201 Created (v4+ returns 200 OK). + /** + * Update an external-only multicast group configuration (API v1/v2). + * + * Tags are optional. If a tag is not provided, the existing tag is preserved. + * Returns 201 Created (API v4+ returns 200 OK). + */ + // We cannot provide a default implementation that delegates to + // `multicast_group_update_external` because the implementor must look up + // the existing tag if not provided in the request. #[endpoint { method = PUT, path = "/multicast/external-groups/{group_ip}", @@ -1756,18 +1800,7 @@ pub trait DpdApi { ) -> Result< HttpResponseCreated, HttpError, - > { - match Self::multicast_group_update_external( - rqctx, - path, - group.map(Into::into), - ) - .await - { - Ok(HttpResponseOk(resp)) => Ok(HttpResponseCreated(resp.into())), - Err(e) => Err(e), - } - } + >; /** * List all multicast groups. @@ -1775,7 +1808,7 @@ pub trait DpdApi { #[endpoint { method = GET, path = "/multicast/groups", - versions = VERSION_MCAST_SOURCE_FILTER_ANY.., + versions = VERSION_MCAST_TAG_OWNERSHIP.., }] async fn multicast_groups_list( rqctx: RequestContext, @@ -1787,6 +1820,30 @@ pub trait DpdApi { HttpError, >; + /// List all multicast groups (API v3). + #[endpoint { + method = GET, + path = "/multicast/groups", + versions = VERSION_MCAST_SOURCE_FILTER_ANY..VERSION_MCAST_TAG_OWNERSHIP, + }] + 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, @@ -1813,15 +1870,19 @@ pub trait DpdApi { /** * 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_SOURCE_FILTER_ANY.., + versions = VERSION_MCAST_TAG_OWNERSHIP.., }] async fn multicast_groups_list_by_tag( rqctx: RequestContext, - path: Path, + path: Path, query_params: Query< PaginationParams, >, @@ -1830,6 +1891,37 @@ 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_TAG_OWNERSHIP, + }] + 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, @@ -1846,8 +1938,12 @@ pub trait DpdApi { HttpResponseOk>, HttpError, > { - match Self::multicast_groups_list_by_tag(rqctx, path, query_params) - .await + 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(), @@ -1859,16 +1955,36 @@ pub trait DpdApi { /** * 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_TAG_OWNERSHIP.., }] 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_TAG_OWNERSHIP, + }] + 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. * @@ -2475,6 +2591,7 @@ pub struct LinkFilter { pub filter: Option, } +/// Path parameter for tag-based operations. #[derive(Deserialize, Serialize, JsonSchema)] pub struct TagPath { pub tag: String, @@ -2556,17 +2673,63 @@ pub struct MulticastGroupIpParam { pub group_ip: IpAddr, } -/// Tag for multicast group ownership validation. +/// 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, +)] +#[schemars(transparent)] +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) + } +} + +/// 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). -/// Omit the field to skip validation, or provide a tag that must match -/// the group's existing tag. +/// 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 for ownership validation. - /// Omit to skip validation. - #[serde(default)] - pub tag: Option, + /// Tag that must match the group's existing tag. + pub tag: MulticastTag, } /// Used to identify an underlay (internal) multicast group by admin-local IPv6 diff --git a/dpd-api/src/v2.rs b/dpd-api/src/v2.rs index d197ec26..b5990e9d 100644 --- a/dpd-api/src/v2.rs +++ b/dpd-api/src/v2.rs @@ -6,7 +6,7 @@ //! Types from API version 2 that changed in version 3. //! -//! The `IpSrc` enum changed from `{Exact, SubnetV4, SubnetV6}` to `{Exact, Any}`. +//! The `IpSrc` enum changed from `{Exact, Subnet}` to `{Exact, Any}`. use std::{fmt, net::IpAddr}; @@ -71,8 +71,12 @@ pub struct MulticastGroupCreateExternalEntry { /// 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, @@ -90,7 +94,7 @@ pub struct MulticastGroupExternalResponse { pub sources: Option>, } -/// Convert from v4 response to v2 response. +/// Convert from API v4 response to v2 response. impl From for MulticastGroupExternalResponse { @@ -127,7 +131,7 @@ impl MulticastGroupResponse { } } -/// Convert from v4 response to v2 response. +/// Convert from API v4 response to v2 response. impl From for MulticastGroupResponse { fn from(resp: dpd_types::mcast::MulticastGroupResponse) -> Self { match resp { @@ -180,7 +184,6 @@ impl From { fn from(entry: MulticastGroupUpdateExternalEntry) -> Self { Self { - tag: entry.tag, internal_forwarding: entry.internal_forwarding, external_forwarding: entry.external_forwarding, sources: entry diff --git a/dpd-api/src/v3.rs b/dpd-api/src/v3.rs index 24df4d46..1ee01389 100644 --- a/dpd-api/src/v3.rs +++ b/dpd-api/src/v3.rs @@ -7,7 +7,8 @@ //! Types from API version 3 that changed in version 4. //! //! The `tag` field in response types changed from `Option` to `String` -//! since all groups now have default tags generated at creation time. +//! since all groups now have default tags generated at creation time, and API +//! version 4 introduced tag validation for updates and deletes. use std::net::IpAddr; @@ -29,7 +30,7 @@ pub struct MulticastGroupUnderlayResponse { pub members: Vec, } -/// Convert from v4 response to v3 response. +/// Convert from API v4 response to v3 response. impl From for MulticastGroupUnderlayResponse { @@ -55,7 +56,7 @@ pub struct MulticastGroupExternalResponse { pub sources: Option>, } -/// Convert from v4 response to v3 response. +/// Convert from API v4 response to v3 response. impl From for MulticastGroupExternalResponse { @@ -90,7 +91,7 @@ impl MulticastGroupResponse { } } -/// Convert from v4 response to v3 response. +/// Convert from API v4 response to v3 response. impl From for MulticastGroupResponse { fn from(resp: dpd_types::mcast::MulticastGroupResponse) -> Self { match resp { @@ -103,3 +104,52 @@ impl From for MulticastGroupResponse { } } } + +/// 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, + } + } +} diff --git a/dpd-client/tests/integration_tests/mcast.rs b/dpd-client/tests/integration_tests/mcast.rs index 9492c22a..8a7ee3e8 100644 --- a/dpd-client/tests/integration_tests/mcast.rs +++ b/dpd-client/tests/integration_tests/mcast.rs @@ -32,6 +32,15 @@ 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; } @@ -177,15 +186,32 @@ async fn create_test_multicast_group( } } +fn make_del_tag(tag: &str) -> types::MulticastGroupDeleteTag { + tag.parse().expect("tag should parse") +} + +fn make_underlay_update_tag( + tag: &str, +) -> types::MulticastGroupUpdateUnderlayTag { + tag.parse().expect("tag should parse") +} + +fn make_external_update_tag( + tag: &str, +) -> types::MulticastGroupUpdateExternalTag { + 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, - tag: Option<&str>, + tag: &str, ) -> TestResult { + let del_tag = make_del_tag(tag); switch .client - .multicast_group_delete(&group_ip, tag) + .multicast_group_delete(&group_ip, &del_tag) .await .map_err(|e| { anyhow!("Failed to delete test group {}: {:?}", group_ip, e) @@ -481,7 +507,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 }, @@ -499,7 +525,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()), }, @@ -530,7 +556,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()), }, @@ -554,7 +580,7 @@ async fn test_group_creation_with_validation() -> TestResult { ); assert_eq!(created.group_ip, MULTICAST_TEST_IPV4_SSM); - assert_eq!(created.tag, "test_valid"); + assert_eq!(created.tag, TEST_TAG); assert_eq!( created.internal_forwarding.nat_target, Some(nat_target.clone()) @@ -568,15 +594,10 @@ async fn test_group_creation_with_validation() -> TestResult { ); // Clean up external first (references internal via NAT target), then internal - cleanup_test_group(switch, created.group_ip, Some("test_valid")) + cleanup_test_group(switch, created.group_ip, TEST_TAG) .await .unwrap(); - cleanup_test_group( - switch, - internal_multicast_ip, - Some("valid_internal_group"), - ) - .await + cleanup_test_group(switch, internal_multicast_ip, TEST_TAG).await } #[tokio::test] @@ -589,7 +610,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, @@ -609,9 +630,8 @@ async fn test_internal_ipv6_validation() -> TestResult { "Group IDs should be different" ); - // Test update works correctly (must use same tag for ownership validation) + // Test update works correctly (must use same tag for validation). let update_entry = types::MulticastGroupUpdateUnderlayEntry { - tag: Some("test_admin_scoped".to_string()), members: vec![types::MulticastGroupMember { port_id, link_id, @@ -621,19 +641,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_underlay_update_tag(TEST_TAG), + &update_entry, + ) .await .expect("Should update internal IPv6 group") .into_inner(); - assert_eq!(updated.tag, "test_admin_scoped"); + assert_eq!(updated.tag, TEST_TAG); - cleanup_test_group( - switch, - created.group_ip.to_ip_addr(), - Some("test_admin_scoped"), - ) - .await + cleanup_test_group(switch, created.group_ip.to_ip_addr(), TEST_TAG).await } #[tokio::test] @@ -646,7 +665,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(), @@ -677,7 +696,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), }, @@ -722,19 +741,11 @@ async fn test_vlan_propagation_to_internal() -> TestResult { ); // Delete external group first since it references the internal group via NAT target - cleanup_test_group( - switch, - created_external.group_ip, - Some("test_external_with_vlan"), - ) - .await - .unwrap(); - cleanup_test_group( - switch, - created_admin.group_ip.to_ip_addr(), - Some("test_vlan_propagation"), - ) - .await + cleanup_test_group(switch, created_external.group_ip, TEST_TAG) + .await + .unwrap(); + cleanup_test_group(switch, created_admin.group_ip.to_ip_addr(), TEST_TAG) + .await } #[tokio::test] @@ -747,7 +758,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 }, @@ -761,7 +772,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()), }, @@ -781,7 +792,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, "test_lifecycle"); + assert_eq!(created.tag, TEST_TAG); assert_eq!( created.internal_forwarding.nat_target, Some(nat_target.clone()) @@ -804,7 +815,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(TEST_TAG, None) .try_collect::>() .await .expect("Should be able to get groups by tag"); @@ -827,7 +838,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]), "test_lifecycle"); + assert_eq!(get_tag(&group[0]), TEST_TAG); // Also test getting by IP address let group_by_ip = switch @@ -850,7 +861,6 @@ async fn test_group_api_lifecycle() { }; let external_update = types::MulticastGroupUpdateExternalEntry { - tag: Some("test_lifecycle".to_string()), internal_forwarding: types::InternalForwarding { nat_target: Some(updated_nat_target.clone()), }, @@ -860,13 +870,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_external_update_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, "test_lifecycle"); + assert_eq!(updated.tag, TEST_TAG); assert_eq!( updated.internal_forwarding.nat_target, Some(updated_nat_target) @@ -875,9 +889,10 @@ async fn test_group_api_lifecycle() { assert_eq!(updated.sources, None); // Delete the group (must provide matching tag) + let del_tag = make_del_tag(TEST_TAG); switch .client - .multicast_group_delete(&group_ip, Some("test_lifecycle")) + .multicast_group_delete(&group_ip, &del_tag) .await .expect("Should be able to delete group"); @@ -917,24 +932,20 @@ async fn test_group_api_lifecycle() { ); // Clean up the internal group (external was already deleted above) - cleanup_test_group( - switch, - internal_multicast_ip, - Some("valid_underlay_group"), - ) - .await - .unwrap(); + 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 proof of ownership: +/// 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 ownership validation +/// - Deletion requires a matching tag for validation /// - Mismatched tags result in a 400 Bad Request error #[tokio::test] #[ignore] -async fn test_multicast_delete_tag_validation() -> TestResult { +async fn test_multicast_del_tag_validation() -> TestResult { let switch = &*get_switch().await; // Setup: create internal admin-scoped group for NAT target @@ -942,7 +953,7 @@ async fn test_multicast_delete_tag_validation() -> TestResult { create_test_multicast_group( switch, internal_multicast_ip, - Some("tag_validation_internal"), + Some(TEST_TAG), &[(PhysPort(11), types::Direction::Underlay)], types::InternalForwarding { nat_target: None }, types::ExternalForwarding { vlan_id: None }, @@ -954,7 +965,7 @@ async fn test_multicast_delete_tag_validation() -> TestResult { let tagged_group_ip = IpAddr::V4(Ipv4Addr::new(224, 0, 10, 1)); let external_tagged = types::MulticastGroupCreateExternalEntry { group_ip: tagged_group_ip, - tag: Some("owner_a".to_string()), + tag: Some(TAG_A.to_string()), internal_forwarding: types::InternalForwarding { nat_target: Some(create_nat_target_ipv4()), }, @@ -969,9 +980,10 @@ async fn test_multicast_delete_tag_validation() -> TestResult { .expect("Should create tagged group"); // Attempt delete with wrong tag - should fail + let del_tag = make_del_tag(TAG_B); let wrong_tag_result = switch .client - .multicast_group_delete(&tagged_group_ip, Some("owner_b")) + .multicast_group_delete(&tagged_group_ip, &del_tag) .await; match &wrong_tag_result { @@ -988,25 +1000,11 @@ async fn test_multicast_delete_tag_validation() -> TestResult { ), } - // Case: Delete tagged group with empty string should fail - let empty_tag_result = switch - .client - .multicast_group_delete(&tagged_group_ip, Some("")) - .await; - - match &empty_tag_result { - Err(Error::ErrorResponse(resp)) => { - assert_eq!( - resp.status(), - 400, - "Empty tag for tagged group should return 400 Bad Request" - ); - } - _ => panic!( - "Expected ErrorResponse for missing tag on tagged group, got: {:?}", - empty_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 = @@ -1017,9 +1015,10 @@ async fn test_multicast_delete_tag_validation() -> TestResult { ); // Case: Delete with correct tag should succeed + let del_tag = make_del_tag(TAG_A); switch .client - .multicast_group_delete(&tagged_group_ip, Some("owner_a")) + .multicast_group_delete(&tagged_group_ip, &del_tag) .await .expect("Should delete group with matching tag"); @@ -1060,61 +1059,16 @@ async fn test_multicast_delete_tag_validation() -> TestResult { ); let auto_tag = created.tag.clone(); - // Attempt delete with empty string on auto-tagged group - should fail - let empty_on_auto_result = switch - .client - .multicast_group_delete(&auto_tagged_group_ip, Some("")) - .await; - - match &empty_on_auto_result { - Err(Error::ErrorResponse(resp)) => { - assert_eq!( - resp.status(), - 400, - "Empty tag for auto-tagged group should return 400" - ); - } - _ => panic!( - "Expected ErrorResponse for empty tag on auto-tagged group, got: {:?}", - empty_on_auto_result - ), - } - // Delete with correct auto-generated tag should succeed + let del_tag = make_del_tag(auto_tag.as_str()); switch .client - .multicast_group_delete(&auto_tagged_group_ip, Some(auto_tag.as_str())) + .multicast_group_delete(&auto_tagged_group_ip, &del_tag) .await .expect("Should delete group with matching auto-generated tag"); - // Case: Delete with None skips validation - let skip_validation_group_ip = IpAddr::V4(Ipv4Addr::new(224, 0, 10, 3)); - create_test_multicast_group( - switch, - skip_validation_group_ip, - Some("will_be_ignored"), - &[(PhysPort(11), types::Direction::External)], - types::InternalForwarding { - nat_target: Some(create_nat_target_ipv4()), - }, - types::ExternalForwarding { vlan_id: Some(10) }, - None, - ) - .await; - - switch - .client - .multicast_group_delete(&skip_validation_group_ip, None) - .await - .expect("Should delete group when tag is None"); - // Clean up internal group - cleanup_test_group( - switch, - internal_multicast_ip, - Some("tag_validation_internal"), - ) - .await + cleanup_test_group(switch, internal_multicast_ip, TEST_TAG).await } #[tokio::test] @@ -1180,7 +1134,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()), }, @@ -1244,7 +1198,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(), @@ -1295,12 +1249,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(), - Some("test_bifurcated"), - ) - .await + cleanup_test_group(switch, created.group_ip.to_ip_addr(), TEST_TAG).await } #[tokio::test] @@ -1313,7 +1262,7 @@ async fn test_api_internal_ipv6_underlay_only() -> TestResult { // Create admin-local IPv6 group with only underlay members let underlay_only_group = types::MulticastGroupCreateUnderlayEntry { group_ip: "ff04::200".parse().unwrap(), - tag: Some("test_underlay_only".to_string()), + tag: Some(TEST_TAG.to_string()), members: vec![types::MulticastGroupMember { port_id: port_id.clone(), link_id, @@ -1332,12 +1281,7 @@ async fn test_api_internal_ipv6_underlay_only() -> TestResult { 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(), - Some("test_underlay_only"), - ) - .await + cleanup_test_group(switch, created.group_ip.to_ip_addr(), TEST_TAG).await } #[tokio::test] @@ -1351,7 +1295,7 @@ async fn test_api_internal_ipv6_external_only() -> TestResult { let external_members_only_group = types::MulticastGroupCreateUnderlayEntry { group_ip: "ff04::300".parse().unwrap(), - tag: Some("test_external_members_only".to_string()), + tag: Some(TEST_TAG.to_string()), members: vec![types::MulticastGroupMember { port_id: port_id.clone(), link_id, @@ -1370,12 +1314,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(), - Some("test_external_members_only"), - ) - .await + cleanup_test_group(switch, created.group_ip.to_ip_addr(), TEST_TAG).await } #[tokio::test] @@ -1388,7 +1327,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 }, @@ -1399,7 +1338,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()), }, @@ -1418,7 +1357,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()), }, @@ -1437,7 +1376,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()), }, @@ -1465,26 +1404,13 @@ async fn test_api_invalid_combinations() -> TestResult { ), } - cleanup_test_group( - switch, - created_ipv4.group_ip, - Some("test_invalid_ipv4"), - ) - .await - .unwrap(); - cleanup_test_group( - switch, - created_non_admin.group_ip, - Some("test_non_admin_ipv6"), - ) - .await - .unwrap(); - cleanup_test_group( - switch, - internal_multicast_ip, - Some("nat_target_for_invalid_combos"), - ) - .await + cleanup_test_group(switch, created_ipv4.group_ip, TEST_TAG) + .await + .unwrap(); + cleanup_test_group(switch, created_non_admin.group_ip, TEST_TAG) + .await + .unwrap(); + cleanup_test_group(switch, internal_multicast_ip, TEST_TAG).await } #[tokio::test] @@ -1503,7 +1429,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 }, @@ -1518,7 +1444,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()), @@ -1582,19 +1508,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), - Some("test_invalid_mac"), - ) - .await - .unwrap(); - cleanup_test_group( - switch, - internal_multicast_ip, - Some("test_invalid_mac_underlay"), - ) - .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] @@ -1612,7 +1529,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 }, @@ -1685,12 +1602,7 @@ async fn test_ipv6_multicast_invalid_destination_mac() -> TestResult { .await .unwrap(); - cleanup_test_group( - switch, - get_group_ip(&created_group), - Some("test_ipv6_invalid_mac"), - ) - .await + cleanup_test_group(switch, get_group_ip(&created_group), TEST_TAG).await } #[tokio::test] @@ -1707,7 +1619,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 }, @@ -1721,7 +1633,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()), @@ -1771,19 +1683,10 @@ async fn test_multicast_ttl_zero() -> TestResult { .await .unwrap(); - cleanup_test_group( - switch, - get_group_ip(&created_group), - Some("test_ttl_drop"), - ) - .await - .unwrap(); - cleanup_test_group( - switch, - internal_multicast_ip, - Some("nat_target_for_ttl"), - ) - .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] @@ -1800,7 +1703,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 }, @@ -1814,7 +1717,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()), @@ -1864,19 +1767,10 @@ async fn test_multicast_ttl_one() -> TestResult { .await .unwrap(); - cleanup_test_group( - switch, - get_group_ip(&created_group), - Some("test_ttl_one_drop"), - ) - .await - .unwrap(); - cleanup_test_group( - switch, - internal_multicast_ip, - Some("nat_target_for_ttl_one"), - ) - .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] @@ -1901,7 +1795,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 }, @@ -1921,7 +1815,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()), @@ -2012,19 +1906,10 @@ async fn test_ipv4_multicast_basic_replication_nat_ingress() -> TestResult { .unwrap(); // Cleanup external first, then internal - cleanup_test_group( - switch, - get_group_ip(&created_group), - Some("test_ipv4_replication"), - ) - .await - .unwrap(); - cleanup_test_group( - switch, - internal_multicast_ip, - Some("test_replication_internal"), - ) - .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] @@ -2048,7 +1933,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 @@ -2063,7 +1948,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()), @@ -2163,19 +2048,10 @@ async fn test_encapped_multicast_geneve_mcast_tag_to_external_members() .await .unwrap(); - cleanup_test_group( - switch, - get_group_ip(&created_group), - Some("test_geneve_mcast_tag_0"), - ) - .await - .unwrap(); - cleanup_test_group( - switch, - MULTICAST_NAT_IP.into(), - Some("test_geneve_mcast_tag_underlay"), - ) - .await + cleanup_test_group(switch, get_group_ip(&created_group), TEST_TAG) + .await + .unwrap(); + cleanup_test_group(switch, MULTICAST_NAT_IP.into(), TEST_TAG).await } #[tokio::test] @@ -2194,7 +2070,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), @@ -2211,7 +2087,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()), @@ -2311,19 +2187,10 @@ async fn test_encapped_multicast_geneve_mcast_tag_to_underlay_members() .await .unwrap(); - cleanup_test_group( - switch, - get_group_ip(&created_group), - Some("test_geneve_mcast_tag_1"), - ) - .await - .unwrap(); - cleanup_test_group( - switch, - MULTICAST_NAT_IP.into(), - Some("test_geneve_mcast_tag_underlay"), - ) - .await + cleanup_test_group(switch, get_group_ip(&created_group), TEST_TAG) + .await + .unwrap(); + cleanup_test_group(switch, MULTICAST_NAT_IP.into(), TEST_TAG).await } #[tokio::test] @@ -2345,7 +2212,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), @@ -2365,7 +2232,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()), @@ -2478,19 +2345,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), - Some("test_geneve_mcast_tag_1"), - ) - .await - .unwrap(); - cleanup_test_group( - switch, - MULTICAST_NAT_IP.into(), - Some("test_geneve_mcast_tag_bifurcated"), - ) - .await + cleanup_test_group(switch, get_group_ip(&created_group), TEST_TAG) + .await + .unwrap(); + cleanup_test_group(switch, MULTICAST_NAT_IP.into(), TEST_TAG).await } #[tokio::test] @@ -2506,7 +2364,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 @@ -2520,7 +2378,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()), @@ -2569,19 +2427,10 @@ async fn test_ipv4_multicast_drops_ingress_is_egress_port() -> TestResult { .await .unwrap(); - cleanup_test_group( - switch, - get_group_ip(&created_group), - Some("test_replication"), - ) - .await - .unwrap(); - cleanup_test_group( - switch, - internal_multicast_ip, - Some("test_drops_underlay"), - ) - .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] @@ -2599,7 +2448,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), @@ -2616,7 +2465,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()), @@ -2662,19 +2511,10 @@ async fn test_ipv6_multicast_hop_limit_zero() -> TestResult { .await .unwrap(); - cleanup_test_group( - switch, - get_group_ip(&created_group), - Some("test_ipv6_hop_limit_zero"), - ) - .await - .unwrap(); - cleanup_test_group( - switch, - internal_multicast_ip, - Some("test_ipv6_hop_limit_underlay"), - ) - .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] @@ -2692,7 +2532,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), @@ -2709,7 +2549,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()), @@ -2759,19 +2599,10 @@ async fn test_ipv6_multicast_hop_limit_one() -> TestResult { .await .unwrap(); - cleanup_test_group( - switch, - get_group_ip(&created_group), - Some("test_ipv6_hop_limit_one"), - ) - .await - .unwrap(); - cleanup_test_group( - switch, - internal_multicast_ip, - Some("test_ipv6_hop_limit_one_underlay"), - ) - .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] @@ -2790,7 +2621,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 @@ -2805,7 +2636,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()), @@ -2872,19 +2703,10 @@ async fn test_ipv6_multicast_basic_replication_nat_ingress() -> TestResult { .await .unwrap(); - cleanup_test_group( - switch, - get_group_ip(&created_group), - Some("test_ipv6_replication"), - ) - .await - .unwrap(); - cleanup_test_group( - switch, - internal_multicast_ip, - Some("test_replication_internal"), - ) - .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] @@ -2904,7 +2726,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), @@ -2924,7 +2746,7 @@ async fn test_ipv4_multicast_source_filtering_exact_match() -> TestResult { 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()), @@ -3010,19 +2832,10 @@ async fn test_ipv4_multicast_source_filtering_exact_match() -> TestResult { .await .unwrap(); - cleanup_test_group( - switch, - get_group_ip(&created_group), - Some("test_source_filtering"), - ) - .await - .unwrap(); - cleanup_test_group( - switch, - internal_multicast_ip, - Some("test_source_filtering_underlay"), - ) - .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] @@ -3042,7 +2855,7 @@ async fn test_ipv4_multicast_source_filtering_multiple_exact() -> TestResult { create_test_multicast_group( switch, internal_multicast_ip, - Some("test_source_filtering_multi_underlay"), + Some(TEST_TAG), &[ (egress1, types::Direction::External), (egress2, types::Direction::External), @@ -3069,7 +2882,7 @@ async fn test_ipv4_multicast_source_filtering_multiple_exact() -> TestResult { 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()), @@ -3191,19 +3004,10 @@ async fn test_ipv4_multicast_source_filtering_multiple_exact() -> TestResult { .await .unwrap(); - cleanup_test_group( - switch, - get_group_ip(&created_group), - Some("test_source_filtering"), - ) - .await - .unwrap(); - cleanup_test_group( - switch, - internal_multicast_ip, - Some("test_source_filtering_multi_underlay"), - ) - .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] @@ -3223,7 +3027,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), @@ -3249,7 +3053,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()), @@ -3374,19 +3178,10 @@ async fn test_ipv6_multicast_multiple_source_filtering() -> TestResult { .await .unwrap(); - cleanup_test_group( - switch, - get_group_ip(&created_group), - Some("test_ipv6_source_filtering"), - ) - .await - .unwrap(); - cleanup_test_group( - switch, - internal_multicast_ip, - Some("test_ipv6_source_filtering_underlay"), - ) - .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] @@ -3405,7 +3200,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), @@ -3423,7 +3218,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()), @@ -3480,9 +3275,8 @@ async fn test_multicast_dynamic_membership() -> TestResult { // Now update the external group - external groups don't have members to update, // but we can update their NAT target, vlan, and sources. - // Must pass same tag for ownership validation. + // Must pass same tag for validation. let external_update_entry = types::MulticastGroupUpdateExternalEntry { - tag: Some("test_dynamic_membership".to_string()), internal_forwarding: types::InternalForwarding { nat_target: Some(create_nat_target_ipv4()), }, // Keep the same NAT target @@ -3494,6 +3288,7 @@ async fn test_multicast_dynamic_membership() -> TestResult { .client .multicast_group_update_external( &get_group_ip(&created_group), + &make_external_update_tag(TEST_TAG), &external_update_entry, ) .await @@ -3503,9 +3298,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 ownership validation + // Must pass same tag for validation. let internal_update_entry = types::MulticastGroupUpdateUnderlayEntry { - tag: Some("test_dynamic_membership_internal".to_string()), members: vec![ types::MulticastGroupMember { port_id: port_id2, @@ -3529,6 +3323,7 @@ async fn test_multicast_dynamic_membership() -> TestResult { .client .multicast_group_update_underlay( &types::AdminScopedIpv6(ipv6), + &make_underlay_update_tag(TEST_TAG), &internal_update_entry, ) .await @@ -3570,19 +3365,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), - Some("test_dynamic_membership"), - ) - .await - .unwrap(); - cleanup_test_group( - switch, - internal_multicast_ip, - Some("test_dynamic_membership_internal"), - ) - .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] @@ -3602,7 +3388,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), @@ -3622,7 +3408,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()), @@ -3639,7 +3425,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()), @@ -3782,26 +3568,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), - Some("test_multi_group_1"), - ) - .await - .unwrap(); - cleanup_test_group( - switch, - get_group_ip(&created_group2), - Some("test_multi_group_2"), - ) - .await - .unwrap(); - cleanup_test_group( - switch, - internal_multicast_ip, - Some("test_multi_group_underlay"), - ) - .await + cleanup_test_group(switch, get_group_ip(&created_group1), TEST_TAG) + .await + .unwrap(); + cleanup_test_group(switch, get_group_ip(&created_group2), TEST_TAG) + .await + .unwrap(); + cleanup_test_group(switch, internal_multicast_ip, TEST_TAG).await } #[tokio::test] @@ -3820,7 +3593,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), @@ -3838,7 +3611,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()), @@ -3855,7 +3628,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()), @@ -3870,7 +3643,7 @@ async fn test_multicast_reset_all_tables() -> TestResult { let group_entry2b = types::MulticastGroupCreateUnderlayEntry { group_ip: types::AdminScopedIpv6(ipv6), - tag: Some("test_reset_all_2b".to_string()), + 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, @@ -3896,7 +3669,7 @@ async fn test_multicast_reset_all_tables() -> TestResult { 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()), @@ -3915,7 +3688,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()), @@ -4141,7 +3914,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 @@ -4156,7 +3929,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()), @@ -4198,19 +3971,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), - Some("test_vlan_behavior"), - ) - .await - .unwrap(); - cleanup_test_group( - switch, - internal_multicast_ip, - Some("test_vlan_underlay"), - ) - .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] @@ -4229,7 +3993,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), @@ -4248,7 +4012,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()), @@ -4341,19 +4105,10 @@ async fn test_multicast_multiple_packets() -> TestResult { .await .unwrap(); - cleanup_test_group( - switch, - get_group_ip(&created_group), - Some("test_performance"), - ) - .await - .unwrap(); - cleanup_test_group( - switch, - internal_multicast_ip, - Some("test_performance_underlay"), - ) - .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] @@ -4463,7 +4218,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()), }, @@ -4487,7 +4242,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, @@ -4525,7 +4280,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()), }, @@ -4559,19 +4314,11 @@ async fn test_external_group_nat_target_validation() -> TestResult { ); // Delete external group first since it references the internal group via NAT target - cleanup_test_group( - switch, - created_external.group_ip, - Some("test_valid_nat"), - ) - .await - .unwrap(); - cleanup_test_group( - switch, - created_admin.group_ip.to_ip_addr(), - Some("test_admin_scoped"), - ) - .await + cleanup_test_group(switch, created_external.group_ip, TEST_TAG) + .await + .unwrap(); + cleanup_test_group(switch, created_admin.group_ip.to_ip_addr(), TEST_TAG) + .await } #[tokio::test] @@ -4585,7 +4332,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, @@ -4605,7 +4352,7 @@ async fn test_ipv6_multicast_scope_validation() { // 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, @@ -4625,7 +4372,7 @@ async fn test_ipv6_multicast_scope_validation() { // 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, @@ -4645,7 +4392,7 @@ async fn test_ipv6_multicast_scope_validation() { // 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, @@ -4666,7 +4413,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, @@ -4682,7 +4429,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(), @@ -4714,20 +4461,20 @@ async fn test_ipv6_multicast_scope_validation() { let admin_local_group = admin_local_result.unwrap().into_inner(); let target_group = target_result.into_inner(); + let del_tag = make_del_tag(TEST_TAG); switch .client .multicast_group_delete( &admin_local_group.group_ip.to_ip_addr(), - Some("test_admin_local"), + &del_tag, ) .await .ok(); + + let del_tag = make_del_tag(TEST_TAG); switch .client - .multicast_group_delete( - &target_group.group_ip.to_ip_addr(), - Some("test_target"), - ) + .multicast_group_delete(&target_group.group_ip.to_ip_addr(), &del_tag) .await .ok(); } @@ -4749,7 +4496,7 @@ async fn test_multicast_group_id_recycling() -> TestResult { 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 }, @@ -4761,7 +4508,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 }, @@ -4775,9 +4522,10 @@ async fn test_multicast_group_id_recycling() -> TestResult { ); // Delete the first group + let del_tag = make_del_tag(TEST_TAG); switch .client - .multicast_group_delete(&group1_ip, Some("test_recycling_1")) + .multicast_group_delete(&group1_ip, &del_tag) .await .expect("Should be able to delete first group"); @@ -4799,7 +4547,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 }, @@ -4816,9 +4564,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_del_tag(TEST_TAG); switch .client - .multicast_group_delete(&group2_ip, Some("test_recycling_2")) + .multicast_group_delete(&group2_ip, &del_tag) .await .expect("Should be able to delete second group"); @@ -4841,7 +4590,7 @@ async fn test_multicast_group_id_recycling() -> TestResult { 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 }, @@ -4856,10 +4605,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, Some("test_recycling_3")) + cleanup_test_group(switch, group3_ip, TEST_TAG) .await .unwrap(); - cleanup_test_group(switch, group4_ip, Some("test_recycling_4")).await + cleanup_test_group(switch, group4_ip, TEST_TAG).await } #[tokio::test] @@ -4876,7 +4625,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 }, @@ -4898,7 +4647,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()), }, @@ -5031,7 +4780,6 @@ 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], }; @@ -5042,7 +4790,11 @@ async fn test_multicast_empty_then_add_members_ipv6() -> TestResult { switch .client - .multicast_group_update_underlay(&ipv6_update, &update_entry) + .multicast_group_update_underlay( + &ipv6_update, + &make_underlay_update_tag(TEST_TAG), + &update_entry, + ) .await .expect("Should update internal group with members"); @@ -5161,10 +4913,9 @@ 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) - // Must pass same tag for ownership validation + // Test: Update internal group back to empty (remove all members). + // Must pass same tag for validation. let empty_update_entry = types::MulticastGroupUpdateUnderlayEntry { - tag: Some("empty_internal_ipv6_group".to_string()), members: vec![], // Remove all members }; @@ -5175,7 +4926,11 @@ async fn test_multicast_empty_then_add_members_ipv6() -> TestResult { switch .client - .multicast_group_update_underlay(&ipv6_update, &empty_update_entry) + .multicast_group_update_underlay( + &ipv6_update, + &make_underlay_update_tag(TEST_TAG), + &empty_update_entry, + ) .await .expect("Should update internal group back to empty"); @@ -5220,19 +4975,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, - Some("empty_external_ipv6_group"), - ) - .await - .unwrap(); - cleanup_test_group( - &switch, - internal_group_ip, - Some("empty_internal_ipv6_group"), - ) - .await + cleanup_test_group(&switch, external_group_ip, TEST_TAG) + .await + .unwrap(); + cleanup_test_group(&switch, internal_group_ip, TEST_TAG).await } #[tokio::test] @@ -5248,7 +4994,7 @@ async fn test_multicast_empty_then_add_members_ipv4() -> TestResult { 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 }, @@ -5268,7 +5014,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()), }, @@ -5400,7 +5146,6 @@ 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], }; @@ -5411,7 +5156,11 @@ async fn test_multicast_empty_then_add_members_ipv4() -> TestResult { switch .client - .multicast_group_update_underlay(&ipv6_update, &update_entry) + .multicast_group_update_underlay( + &ipv6_update, + &make_underlay_update_tag(TEST_TAG), + &update_entry, + ) .await .expect("Should update internal group with members"); @@ -5533,7 +5282,6 @@ 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: Some("empty_internal_ipv4_nat_target".to_string()), members: vec![], // Remove all members }; @@ -5544,7 +5292,11 @@ async fn test_multicast_empty_then_add_members_ipv4() -> TestResult { switch .client - .multicast_group_update_underlay(&ipv6_update, &empty_update_entry) + .multicast_group_update_underlay( + &ipv6_update, + &make_underlay_update_tag(TEST_TAG), + &empty_update_entry, + ) .await .expect("Should update internal group back to empty"); @@ -5590,19 +5342,10 @@ 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, - Some("empty_external_ipv4_group"), - ) - .await - .unwrap(); - cleanup_test_group( - &switch, - internal_group_ip, - Some("empty_internal_ipv4_nat_target"), - ) - .await + cleanup_test_group(&switch, external_group_ip, TEST_TAG) + .await + .unwrap(); + cleanup_test_group(&switch, internal_group_ip, TEST_TAG).await } #[tokio::test] @@ -5619,7 +5362,7 @@ async fn test_multicast_rollback_external_group_creation_failure() -> TestResult 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), @@ -5774,12 +5517,7 @@ async fn test_multicast_rollback_external_group_creation_failure() -> TestResult "Source filter table should be unchanged after rollback" ); - cleanup_test_group( - &switch, - internal_group_ip, - Some("rollback_test_internal"), - ) - .await + cleanup_test_group(&switch, internal_group_ip, TEST_TAG).await } #[tokio::test] @@ -5794,7 +5532,7 @@ async fn test_multicast_rollback_member_update_failure() -> TestResult { 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), @@ -5826,7 +5564,6 @@ 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 { @@ -5837,7 +5574,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_underlay_update_tag(TEST_TAG), + &update_request, + ) .await; // Verify the update failed @@ -5859,8 +5600,7 @@ async fn test_multicast_rollback_member_update_failure() -> TestResult { "Member count should be unchanged after rollback" ); - cleanup_test_group(&switch, internal_group_ip, Some("rollback_member_test")) - .await + cleanup_test_group(&switch, internal_group_ip, TEST_TAG).await } #[tokio::test] @@ -5876,7 +5616,7 @@ async fn test_multicast_rollback_nat_transition_failure() -> TestResult { 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 }, @@ -5896,7 +5636,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()), }, @@ -5936,7 +5676,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), }, @@ -5947,7 +5686,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_external_update_tag(TEST_TAG), + &invalid_update, + ) .await; // Verify the update failed @@ -6000,11 +5743,10 @@ async fn test_multicast_rollback_nat_transition_failure() -> TestResult { "NAT table should be unchanged after rollback" ); - cleanup_test_group(&switch, external_group_ip, None) + cleanup_test_group(&switch, external_group_ip, TEST_TAG) .await .unwrap(); - cleanup_test_group(&switch, internal_group_ip, Some("nat_rollback_test")) - .await + cleanup_test_group(&switch, internal_group_ip, TEST_TAG).await } #[tokio::test] @@ -6020,7 +5762,7 @@ async fn test_multicast_rollback_vlan_propagation_consistency() { 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), @@ -6039,13 +5781,9 @@ 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, - Some("vlan_propagation_test"), - ) - .await - .expect("Should cleanup internal group"); + cleanup_test_group(&switch, internal_group_ip, TEST_TAG) + .await + .expect("Should cleanup internal group"); let nat_target = types::NatTarget { internal_ip: match internal_group_ip { @@ -6143,7 +5881,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 @@ -6167,7 +5905,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), }, @@ -6198,13 +5936,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_external_update_tag(TEST_TAG), + &failing_update_entry, + ) .await; // Verify the update failed @@ -6244,15 +5985,10 @@ async fn test_multicast_rollback_source_filter_update() -> TestResult { ); // Clean up external group first (it references internal group via NAT target) - cleanup_test_group(&switch, group_ip, Some("source_filter_rollback_test")) + cleanup_test_group(&switch, group_ip, TEST_TAG) .await .unwrap(); - cleanup_test_group( - &switch, - internal_multicast_ip, - Some("rollback_internal"), - ) - .await + cleanup_test_group(&switch, internal_multicast_ip, TEST_TAG).await } #[tokio::test] @@ -6267,7 +6003,7 @@ async fn test_multicast_rollback_partial_member_addition() -> TestResult { 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), @@ -6315,7 +6051,6 @@ 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 { @@ -6326,7 +6061,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_underlay_update_tag(TEST_TAG), + &update_request, + ) .await; // Verify the update failed @@ -6351,12 +6090,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, - Some("partial_add_rollback_test"), - ) - .await + cleanup_test_group(&switch, internal_group_ip, TEST_TAG).await } #[tokio::test] @@ -6372,7 +6106,7 @@ async fn test_multicast_rollback_table_operation_failure() { 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), @@ -6384,7 +6118,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, Some("table_rollback_test")) + cleanup_test_group(&switch, internal_group_ip, TEST_TAG) .await .expect("Should cleanup internal group"); @@ -6506,7 +6240,7 @@ async fn test_multicast_group_get_underlay() -> TestResult { 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), @@ -6533,7 +6267,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, "underlay_get_test"); + assert_eq!(retrieved_underlay.tag, TEST_TAG); assert_eq!(retrieved_underlay.members.len(), 2); // Compare with generic GET endpoint result @@ -6565,8 +6299,7 @@ async fn test_multicast_group_get_underlay() -> TestResult { ); } } - cleanup_test_group(&switch, internal_group_ip, Some("underlay_get_test")) - .await + cleanup_test_group(&switch, internal_group_ip, TEST_TAG).await } const SOURCE_FILTER_IPV4_TABLE: &str = @@ -6593,7 +6326,7 @@ async fn test_source_filter_ipv4_collapses_to_any() -> TestResult { let _internal = create_test_multicast_group( switch, internal_group_ip, - Some("source_filter_test_internal"), + Some(TEST_TAG), &[(PhysPort(10), types::Direction::External)], types::InternalForwarding { nat_target: None }, types::ExternalForwarding { vlan_id: None }, @@ -6622,7 +6355,7 @@ async fn test_source_filter_ipv4_collapses_to_any() -> TestResult { // The optimization should collapse this to just one /0 entry let external_group = types::MulticastGroupCreateExternalEntry { group_ip: external_group_ip, - tag: Some("source_filter_collapse_test".to_string()), + tag: Some(TEST_TAG.to_string()), internal_forwarding: types::InternalForwarding { nat_target: Some(nat_target.clone()), }, @@ -6664,12 +6397,10 @@ async fn test_source_filter_ipv4_collapses_to_any() -> TestResult { // 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_del_tag(TEST_TAG); let delete_internal_first_result = switch .client - .multicast_group_delete( - &internal_group_ip, - Some("source_filter_test_internal"), - ) + .multicast_group_delete(&internal_group_ip, &del_tag) .await; assert!( @@ -6688,19 +6419,10 @@ async fn test_source_filter_ipv4_collapses_to_any() -> TestResult { } // Cleanup in correct order: external first, then internal - cleanup_test_group( - switch, - external_group_ip, - Some("source_filter_collapse_test"), - ) - .await - .unwrap(); - cleanup_test_group( - switch, - internal_group_ip, - Some("source_filter_test_internal"), - ) - .await + 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. @@ -6719,7 +6441,7 @@ async fn test_source_filter_ipv6_collapses_to_any() -> TestResult { let _internal = create_test_multicast_group( switch, internal_group_ip, - Some("source_filter_ipv6_internal"), + Some(TEST_TAG), &[(PhysPort(10), types::Direction::External)], types::InternalForwarding { nat_target: None }, types::ExternalForwarding { vlan_id: None }, @@ -6746,7 +6468,7 @@ async fn test_source_filter_ipv6_collapses_to_any() -> TestResult { // Create external group with mixed sources: specific + `Any` let external_group = types::MulticastGroupCreateExternalEntry { group_ip: external_group_ip, - tag: Some("source_filter_ipv6_collapse_test".to_string()), + tag: Some(TEST_TAG.to_string()), internal_forwarding: types::InternalForwarding { nat_target: Some(nat_target.clone()), }, @@ -6788,12 +6510,10 @@ async fn test_source_filter_ipv6_collapses_to_any() -> TestResult { // 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_del_tag(TEST_TAG); let delete_internal_first_result = switch .client - .multicast_group_delete( - &internal_group_ip, - Some("source_filter_ipv6_internal"), - ) + .multicast_group_delete(&internal_group_ip, &del_tag) .await; assert!( @@ -6812,19 +6532,10 @@ async fn test_source_filter_ipv6_collapses_to_any() -> TestResult { } // Cleanup in correct order: external first, then internal - cleanup_test_group( - switch, - external_group_ip, - Some("source_filter_ipv6_collapse_test"), - ) - .await - .unwrap(); - cleanup_test_group( - switch, - internal_group_ip, - Some("source_filter_ipv6_internal"), - ) - .await + 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` @@ -6842,7 +6553,7 @@ async fn test_source_filter_update_to_any() -> TestResult { let _internal = create_test_multicast_group( switch, internal_group_ip, - Some("source_filter_update_internal"), + Some(TEST_TAG), &[(PhysPort(10), types::Direction::External)], types::InternalForwarding { nat_target: None }, types::ExternalForwarding { vlan_id: None }, @@ -6869,7 +6580,7 @@ async fn test_source_filter_update_to_any() -> TestResult { // Create external group with only specific sources (no `Any`) let external_group = types::MulticastGroupCreateExternalEntry { group_ip: external_group_ip, - tag: Some("source_filter_update_test".to_string()), + tag: Some(TEST_TAG.to_string()), internal_forwarding: types::InternalForwarding { nat_target: Some(nat_target.clone()), }, @@ -6901,7 +6612,6 @@ async fn test_source_filter_update_to_any() -> TestResult { // Update to include `Any`, simulating an "any source" member joining let update_entry = types::MulticastGroupUpdateExternalEntry { - tag: Some("source_filter_update_test".to_string()), internal_forwarding: types::InternalForwarding { nat_target: Some(nat_target.clone()), }, @@ -6915,7 +6625,11 @@ async fn test_source_filter_update_to_any() -> TestResult { let updated = switch .client - .multicast_group_update_external(&external_group_ip, &update_entry) + .multicast_group_update_external( + &external_group_ip, + &make_external_update_tag(TEST_TAG), + &update_entry, + ) .await .expect("Should update external group") .into_inner(); @@ -6940,19 +6654,10 @@ async fn test_source_filter_update_to_any() -> TestResult { ); // Cleanup in correct order: external first, then internal - cleanup_test_group( - switch, - external_group_ip, - Some("source_filter_update_test"), - ) - .await - .unwrap(); - cleanup_test_group( - switch, - internal_group_ip, - Some("source_filter_update_internal"), - ) - .await + 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. @@ -6969,7 +6674,7 @@ async fn test_source_filter_cleanup_on_delete() -> TestResult { let _internal = create_test_multicast_group( switch, internal_group_ip, - Some("source_filter_cleanup_internal"), + Some(TEST_TAG), &[(PhysPort(10), types::Direction::External)], types::InternalForwarding { nat_target: None }, types::ExternalForwarding { vlan_id: None }, @@ -6996,7 +6701,7 @@ async fn test_source_filter_cleanup_on_delete() -> TestResult { // Create external group with sources let external_group = types::MulticastGroupCreateExternalEntry { group_ip: external_group_ip, - tag: Some("source_filter_cleanup_test".to_string()), + tag: Some(TEST_TAG.to_string()), internal_forwarding: types::InternalForwarding { nat_target: Some(nat_target.clone()), }, @@ -7027,13 +6732,9 @@ async fn test_source_filter_cleanup_on_delete() -> TestResult { ); // Delete the external group - cleanup_test_group( - switch, - external_group_ip, - Some("source_filter_cleanup_test"), - ) - .await - .expect("Should delete 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 @@ -7048,12 +6749,7 @@ async fn test_source_filter_cleanup_on_delete() -> TestResult { "Source filter entries should be cleaned up after group deletion" ); - cleanup_test_group( - switch, - internal_group_ip, - Some("source_filter_cleanup_internal"), - ) - .await + cleanup_test_group(switch, internal_group_ip, TEST_TAG).await } /// Test that empty sources `Some(vec![])` is normalized to None and adds /0 entry. @@ -7070,7 +6766,7 @@ async fn test_source_filter_empty_vec_normalizes_to_any() -> TestResult { let _internal = create_test_multicast_group( switch, internal_group_ip, - Some("empty_sources_internal"), + Some(TEST_TAG), &[(PhysPort(10), types::Direction::External)], types::InternalForwarding { nat_target: None }, types::ExternalForwarding { vlan_id: None }, @@ -7098,7 +6794,7 @@ async fn test_source_filter_empty_vec_normalizes_to_any() -> TestResult { // and add a single /0 entry (allow any source) let external_group = types::MulticastGroupCreateExternalEntry { group_ip: external_group_ip, - tag: Some("empty_sources_test".to_string()), + tag: Some(TEST_TAG.to_string()), internal_forwarding: types::InternalForwarding { nat_target: Some(nat_target.clone()), }, @@ -7133,7 +6829,7 @@ async fn test_source_filter_empty_vec_normalizes_to_any() -> TestResult { ); // Cleanup - cleanup_test_group(switch, external_group_ip, Some("empty_sources_test")) + cleanup_test_group(switch, external_group_ip, TEST_TAG) .await .unwrap(); @@ -7150,12 +6846,7 @@ async fn test_source_filter_empty_vec_normalizes_to_any() -> TestResult { "Source filter entry should be cleaned up after deletion" ); - cleanup_test_group( - switch, - internal_group_ip, - Some("empty_sources_internal"), - ) - .await + cleanup_test_group(switch, internal_group_ip, TEST_TAG).await } /// Test that updating non-existent groups returns 404. @@ -7177,13 +6868,13 @@ async fn test_update_nonexistent_group_returns_404() -> TestResult { link_id, direction: types::Direction::Underlay, }], - tag: Some("nonexistent_test".to_string()), }; let result = switch .client .multicast_group_update_underlay( &nonexistent_underlay, + &make_underlay_update_tag("nonexistent_test"), &underlay_update, ) .await; @@ -7202,7 +6893,6 @@ async fn test_update_nonexistent_group_returns_404() -> TestResult { let external_update = types::MulticastGroupUpdateExternalEntry { external_forwarding: types::ExternalForwarding { vlan_id: Some(100) }, internal_forwarding: types::InternalForwarding { nat_target: None }, - tag: Some("nonexistent_test".to_string()), sources: None, }; @@ -7210,6 +6900,7 @@ async fn test_update_nonexistent_group_returns_404() -> TestResult { .client .multicast_group_update_external( &nonexistent_external, + &make_external_update_tag("nonexistent_test"), &external_update, ) .await; @@ -7237,27 +6928,10 @@ async fn test_delete_nonexistent_group_returns_404() -> TestResult { // Case: Delete non-existent group with a tag provided let nonexistent_ip = IpAddr::V4(Ipv4Addr::new(239, 255, 255, 253)); + let del_tag = make_del_tag("some_tag"); let result = switch .client - .multicast_group_delete(&nonexistent_ip, Some("some_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), - } - - // Case: Delete non-existent group with None - let result = switch - .client - .multicast_group_delete(&nonexistent_ip, None) + .multicast_group_delete(&nonexistent_ip, &del_tag) .await; match result { @@ -7310,9 +6984,10 @@ async fn test_underlay_delete_recreate_recovery_flow() -> TestResult { assert_eq!(created.into_inner().members.len(), 1); // Case: Delete the group (simulating recovery from stale state) + let del_tag = make_del_tag(tag); switch .client - .multicast_group_delete(&group_ip.to_ip_addr(), Some(tag)) + .multicast_group_delete(&group_ip.to_ip_addr(), &del_tag) .await .expect("Should delete group during recovery"); @@ -7377,3 +7052,286 @@ async fn test_underlay_delete_recreate_recovery_flow() -> TestResult { 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_underlay_update_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_del_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_del_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_del_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_external_update_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_del_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_del_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("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 f79b18f6..8668ca74 100644 --- a/dpd-client/tests/integration_tests/table_tests.rs +++ b/dpd-client/tests/integration_tests/table_tests.rs @@ -494,10 +494,9 @@ 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, Some(MCAST_TAG)) - .await + let del_tag: types::MulticastGroupDeleteTag = + MCAST_TAG.parse().expect("tag should parse"); + switch.client.multicast_group_delete(&ip, &del_tag).await } async fn count_entries(switch: &Switch) -> usize { diff --git a/dpd-types/src/mcast.rs b/dpd-types/src/mcast.rs index 5830cee1..ac87a2b8 100644 --- a/dpd-types/src/mcast.rs +++ b/dpd-types/src/mcast.rs @@ -113,6 +113,8 @@ impl fmt::Display for IpSrc { #[derive(Debug, Deserialize, Serialize, JsonSchema)] pub struct MulticastGroupCreateUnderlayEntry { pub group_ip: AdminScopedIpv6, + /// 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, } @@ -122,6 +124,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, @@ -130,43 +134,45 @@ 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 external_group_id: MulticastGroupId, pub underlay_group_id: MulticastGroupId, - /// Tag for ownership validation. Always present; generated as - /// `{uuid}:{group_ip}` if not provided at creation time. + /// 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, - /// Tag for ownership validation. Always present; generated as - /// `{uuid}:{group_ip}` if not provided at creation time. + /// 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, @@ -189,6 +195,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 diff --git a/dpd/src/api_server.rs b/dpd/src/api_server.rs index 133f5c62..fc811a16 100644 --- a/dpd/src/api_server.rs +++ b/dpd/src/api_server.rs @@ -1889,7 +1889,7 @@ impl DpdApi for DpdApiImpl { let ip = path.into_inner().group_ip; let tag = query.into_inner().tag; - mcast::del_group(switch, ip, tag.as_deref()) + mcast::del_group(switch, ip, tag.as_ref()) .map(|_| HttpResponseDeleted()) .map_err(HttpError::from) } @@ -1901,7 +1901,14 @@ impl DpdApi for DpdApiImpl { let switch: &Switch = rqctx.context(); let ip = path.into_inner().group_ip; - mcast::del_group(switch, ip, None) + // 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) } @@ -1932,13 +1939,47 @@ 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 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, admin_scoped) + .map_err(HttpError::from)? + .tag + } + }; + + mcast::modify_group_internal(switch, admin_scoped, &tag, entry.into()) + .map(|resp| HttpResponseOk(resp.into())) .map_err(HttpError::from) } @@ -1957,17 +1998,71 @@ impl DpdApi for DpdApiImpl { async fn multicast_group_update_external( rqctx: RequestContext>, path: Path, + query: Query, group: TypedBody, ) -> 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) + 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) + } + async fn multicast_groups_list( rqctx: RequestContext>, query_params: Query< @@ -2005,14 +2100,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()) @@ -2041,10 +2136,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()) diff --git a/dpd/src/mcast/mod.rs b/dpd/src/mcast/mod.rs index c753acec..3fcfdda5 100644 --- a/dpd/src/mcast/mod.rs +++ b/dpd/src/mcast/mod.rs @@ -91,7 +91,7 @@ mod validate; use rollback::{GroupCreateRollbackContext, GroupUpdateRollbackContext}; use validate::{ validate_multicast_address, validate_nat_target, - validate_not_admin_local_ipv6, validate_tag_for_update, + validate_not_admin_local_ipv6, validate_tag, validate_tag_format, }; #[derive(Debug)] @@ -145,8 +145,8 @@ struct MulticastReplicationInfo { pub(crate) struct MulticastGroup { external_scoped_group: ScopedGroupId, underlay_scoped_group: ScopedGroupId, - /// Tag for ownership validation. Always present; generated as - /// `{uuid}:{group_ip}` if not provided at creation time. + /// 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, @@ -383,9 +383,17 @@ pub(crate) fn add_group_external( // This validation already passed in validate_nat_target, so it cannot fail here let admin_local_ip = AdminScopedIpv6::new(nat_target.internal_ip)?; - let tag = group_info - .tag - .unwrap_or_else(|| generate_default_tag(group_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, @@ -482,9 +490,17 @@ pub(crate) fn add_group_internal( None }; - let tag = group_info - .tag - .unwrap_or_else(|| generate_default_tag(group_ip.into())); + 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 { @@ -510,12 +526,14 @@ pub(crate) fn add_group_internal( /// Delete a multicast group from the switch, including all associated tables /// and port mappings. /// +/// 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` - Optional tag for ownership validation. If provided, it must -/// match the group's existing tag for the deletion to succeed. +/// * `tag` - Tag for validation. Must match the group's existing tag. /// /// # Errors /// @@ -526,7 +544,7 @@ pub(crate) fn add_group_internal( pub(crate) fn del_group( s: &Switch, group_ip: IpAddr, - tag: Option<&str>, + tag: &str, ) -> DpdResult<()> { let mut mcast = s.mcast.lock().expect("multicast data lock poisoned"); @@ -542,23 +560,15 @@ pub(crate) fn del_group( ))); } - // Validate tag ownership before removing the group. - // If tag is None (v3 API), skip validation for backward compatibility. - // If tag is Some, it must match the group's existing tag. - if let (Some(group), Some(request_tag)) = (mcast.groups.get(&group_ip), tag) - && group.tag != request_tag - { - return Err(DpdError::Invalid(format!( - "tag mismatch: group has tag '{}' but request has tag '{request_tag}'", - group.tag - ))); - } - - 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 @@ -623,6 +633,7 @@ 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().expect("multicast data lock poisoned"); @@ -633,7 +644,7 @@ pub(crate) fn modify_group_external( "Multicast group for IP {group_ip} not found" )) })?; - validate_tag_for_update(&existing_group.tag, &new_group_info.tag)?; + validate_tag(&existing_group.tag, tag)?; let nat_target = new_group_info @@ -686,10 +697,7 @@ pub(crate) fn modify_group_external( } } - // Update the external group fields (use new tag if provided, else keep existing) - updated_group.tag = new_group_info - .tag - .unwrap_or_else(|| updated_group.tag.clone()); + // 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; @@ -767,6 +775,7 @@ pub(crate) fn modify_group_external( pub(crate) fn modify_group_internal( s: &Switch, group_ip: AdminScopedIpv6, + tag: &str, new_group_info: MulticastGroupUpdateUnderlayEntry, ) -> DpdResult { let mut mcast = s.mcast.lock().expect("multicast data lock poisoned"); @@ -778,7 +787,7 @@ pub(crate) fn modify_group_internal( "Multicast group for IP {group_ip} not found" )) })?; - validate_tag_for_update(&existing_group.tag, &new_group_info.tag)?; + validate_tag(&existing_group.tag, tag)?; let mut group_entry = mcast.groups.remove(&group_ip.into()).unwrap(); @@ -824,11 +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 - .unwrap_or_else(|| group_entry.tag.clone()); group_entry.sources = sources; group_entry.members = new_group_info.members; @@ -930,9 +937,7 @@ pub(crate) fn modify_group_internal( } // Update group metadata and return success - group_entry.tag = new_group_info - .tag - .unwrap_or_else(|| 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; @@ -984,9 +989,9 @@ pub(crate) fn reset_tag(s: &Switch, tag: &str) -> DpdResult<()> { }; // Delete external groups first since they reference internal groups - // via NAT targets. Pass the tag for ownership validation. + // 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, Some(tag)) { + if let Err(e) = del_group(s, group_ip, tag) { error!( s.log, "failed to delete multicast group for IP {group_ip}: {e:?}" @@ -1181,7 +1186,7 @@ pub(super) fn sources_contain_any(sources: &[IpSrc]) -> bool { /// Generate a default tag for a multicast group if none is provided. /// -/// Format: `{uuid}:{multicast_ip}` to match Omicron's tag format. +/// 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 { diff --git a/dpd/src/mcast/validate.rs b/dpd/src/mcast/validate.rs index 97ab4e44..c608c8ae 100644 --- a/dpd/src/mcast/validate.rs +++ b/dpd/src/mcast/validate.rs @@ -224,22 +224,58 @@ fn validate_ipv6_source_address(ipv6: Ipv6Addr) -> DpdResult<()> { Ok(()) } -/// Validates that the update request tag matches the existing group's tag. +/// Maximum length for multicast group tags. /// -/// If the request tag is `None`, the group's existing tag (set or generated -/// at creation) is used as the default and validation succeeds. If provided, -/// it must match the group's existing tag. -pub(crate) fn validate_tag_for_update( - existing_tag: &str, - request_tag: &Option, -) -> DpdResult<()> { - if let Some(request) = request_tag - && request != existing_tag - { +/// Keep in sync with Omicron's database schema column type for multicast group +/// tags. This is sized to accommodate the auto-generated format +/// `{uuid}:{group_ip}` for both IPv4 and IPv6 group IPs. +const MAX_TAG_LENGTH: usize = 80; + +/// Validates tag format for group creation. +/// +/// Tags must be 1-80 ASCII bytes containing only alphanumeric characters, +/// hyphens, underscores, colons, or periods. +/// +/// This character set is compatible with URL path segments, though colons are +/// RFC 3986 reserved characters and may require percent-encoding in some HTTP +/// client contexts. +/// +/// Auto-generated tags use the format `{uuid}:{group_ip}`. +pub(crate) fn validate_tag_format(tag: &str) -> DpdResult<()> { + if tag.is_empty() { + return Err(DpdError::Invalid("tag cannot be empty".to_string())); + } + if tag.len() > MAX_TAG_LENGTH { return Err(DpdError::Invalid(format!( - "tag mismatch: group has tag '{existing_tag}' but request has tag '{request}'" + "tag cannot exceed {MAX_TAG_LENGTH} bytes" ))); } + if !tag.bytes().all(|b| { + b.is_ascii_alphanumeric() || matches!(b, b'-' | b'_' | b':' | b'.') + }) { + return Err(DpdError::Invalid( + "tag must contain only ASCII alphanumeric characters, hyphens, \ + underscores, colons, or periods" + .to_string(), + )); + } + Ok(()) +} + +/// 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(()) } @@ -645,20 +681,50 @@ mod tests { } #[test] - fn test_validate_tag_for_update() { + 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_for_update("my-tag", &Some("my-tag".to_string())) - .is_ok() + validate_tag_format( + "550e8400-e29b-41d4-a716-446655440000:224.1.2.3" + ) + .is_ok() ); - // Existing tag but request has different tag - let result = - validate_tag_for_update("owner-a", &Some("owner-b".to_string())); - assert!(result.is_err()); - assert!(result.unwrap_err().to_string().contains("tag mismatch")); + // 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()); - // No tag provided uses existing tag as default (validation passes) - assert!(validate_tag_for_update("my-tag", &None).is_ok()); + // 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/types.rs b/dpd/src/types.rs index 4bd54503..685c4f66 100644 --- a/dpd/src/types.rs +++ b/dpd/src/types.rs @@ -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 { @@ -289,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-4.0.0-5e9839.json b/openapi/dpd/dpd-3.0.0-19eaa3.json similarity index 99% rename from openapi/dpd/dpd-4.0.0-5e9839.json rename to openapi/dpd/dpd-3.0.0-19eaa3.json index b76dd06a..82b85c9a 100644 --- a/openapi/dpd/dpd-4.0.0-5e9839.json +++ b/openapi/dpd/dpd-3.0.0-19eaa3.json @@ -7,13 +7,13 @@ "url": "https://oxide.computer", "email": "api@oxide.computer" }, - "version": "4.0.0" + "version": "3.0.0" }, "paths": { "/all-settings": { "delete": { "summary": "Clear all settings.", - "description": "This removes all data entirely, including:\n\n- All ARP and NDP table entries - All routes - All links on all switch ports - All NAT mappings - All multicast groups\n\nNote: Unlike `reset_all_tagged`, this endpoint does clear multicast groups.", + "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": { @@ -31,7 +31,7 @@ "/all-settings/{tag}": { "delete": { "summary": "Clear all settings associated with a specific tag.", - "description": "This removes:\n\n- All ARP or NDP table entries. - All routes - 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.", + "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": [ { @@ -297,7 +297,7 @@ "/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 crated on a physical switch port.", + "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": { @@ -1086,9 +1086,8 @@ }, "/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", + "summary": "Create an external-only multicast group configuration (API v3).", + "operationId": "multicast_group_create_external_v3", "requestBody": { "content": { "application/json": { @@ -1122,7 +1121,7 @@ "/multicast/external-groups/{group_ip}": { "put": { "summary": "Update an external-only multicast group configuration (API v3).", - "description": "Returns 201 Created (v4+ returns 200 OK).", + "description": "Tags are optional. If a tag is not provided, the existing tag is preserved. Returns 201 Created (API v4+ returns 200 OK).", "operationId": "multicast_group_update_external_v3", "parameters": [ { @@ -1167,8 +1166,8 @@ }, "/multicast/groups": { "get": { - "summary": "List all multicast groups.", - "operationId": "multicast_groups_list", + "summary": "List all multicast groups (API v3).", + "operationId": "multicast_groups_list_v3", "parameters": [ { "in": "query", @@ -1231,8 +1230,8 @@ }, "/multicast/groups/{group_ip}": { "get": { - "summary": "Get the multicast group configuration for a given group IP address.", - "operationId": "multicast_group_get", + "summary": "Get the multicast group configuration for a given group IP address (API v3).", + "operationId": "multicast_group_get_v3", "parameters": [ { "in": "path", @@ -1293,8 +1292,8 @@ }, "/multicast/tags/{tag}": { "get": { - "summary": "List all multicast groups with a given tag.", - "operationId": "multicast_groups_list_by_tag", + "summary": "List all multicast groups with a given tag (API v3).", + "operationId": "multicast_groups_list_by_tag_v3", "parameters": [ { "in": "path", @@ -1348,8 +1347,9 @@ } }, "delete": { - "summary": "Delete all multicast groups (and associated routes) with a given tag.", - "operationId": "multicast_reset_by_tag", + "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", @@ -1442,6 +1442,7 @@ }, "put": { "summary": "Update an underlay (internal) multicast group configuration (API v1-v3).", + "description": "Tags are optional. If a tag is not provided, the existing tag is preserved.", "operationId": "multicast_group_update_underlay_v3", "parameters": [ { @@ -7724,6 +7725,7 @@ }, "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" } }, @@ -7748,6 +7750,7 @@ }, "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" } }, @@ -7757,7 +7760,7 @@ ] }, "MulticastGroupExternalResponse": { - "description": "Response structure for external multicast group operations. These groups handle IPv4 and non-admin IPv6 multicast via NAT targets.", + "description": "Response structure for external multicast group operations (API version 3).", "type": "object", "properties": { "external_forwarding": { @@ -7783,7 +7786,7 @@ } }, "tag": { - "description": "Tag for ownership validation. Always present; generated as `{uuid}:{group_ip}` if not provided at creation time.", + "nullable": true, "type": "string" } }, @@ -7791,8 +7794,7 @@ "external_forwarding", "external_group_id", "group_ip", - "internal_forwarding", - "tag" + "internal_forwarding" ] }, "MulticastGroupMember": { @@ -7816,10 +7818,10 @@ ] }, "MulticastGroupResponse": { - "description": "Unified response type for operations that return mixed group types.", + "description": "Unified response type for operations that return mixed group types (API version 3).", "oneOf": [ { - "description": "Response structure for underlay/internal multicast group operations. These groups handle admin-scoped IPv6 multicast with full replication.", + "description": "Response structure for underlay/internal multicast group operations (API version 3).", "type": "object", "properties": { "external_group_id": { @@ -7843,7 +7845,7 @@ } }, "tag": { - "description": "Tag for ownership validation. Always present; generated as `{uuid}:{group_ip}` if not provided at creation time.", + "nullable": true, "type": "string" }, "underlay_group_id": { @@ -7857,12 +7859,11 @@ "group_ip", "kind", "members", - "tag", "underlay_group_id" ] }, { - "description": "Response structure for external multicast group operations. These groups handle IPv4 and non-admin IPv6 multicast via NAT targets.", + "description": "Response structure for external multicast group operations (API version 3).", "type": "object", "properties": { "external_forwarding": { @@ -7894,7 +7895,7 @@ } }, "tag": { - "description": "Tag for ownership validation. Always present; generated as `{uuid}:{group_ip}` if not provided at creation time.", + "nullable": true, "type": "string" } }, @@ -7903,8 +7904,7 @@ "external_group_id", "group_ip", "internal_forwarding", - "kind", - "tag" + "kind" ] } ] @@ -7966,7 +7966,7 @@ ] }, "MulticastGroupUpdateExternalEntry": { - "description": "A multicast group update entry for PUT requests for external (to the rack) groups.", + "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": { @@ -7984,6 +7984,7 @@ }, "tag": { "nullable": true, + "description": "Tag for validating update requests. Optional in v3; if not provided, tag validation is skipped.", "type": "string" } }, @@ -7993,7 +7994,7 @@ ] }, "MulticastGroupUpdateUnderlayEntry": { - "description": "Represents a multicast replication entry for PUT requests for internal (to the rack) groups.", + "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": { @@ -8004,6 +8005,7 @@ }, "tag": { "nullable": true, + "description": "Tag for validating update requests. Optional in v3; if not provided, tag validation is skipped.", "type": "string" } }, diff --git a/openapi/dpd/dpd-3.0.0-f810b8.json b/openapi/dpd/dpd-3.0.0-f810b8.json deleted file mode 100644 index d692b087..00000000 --- a/openapi/dpd/dpd-3.0.0-f810b8.json +++ /dev/null @@ -1,9643 +0,0 @@ -{ - "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, including:\n\n- All ARP and NDP table entries - All routes - All links on all switch ports - All NAT mappings - All 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:\n\n- All ARP or NDP table entries. - All routes - 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 crated 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 v1/v2).", - "operationId": "multicast_group_create_external_v2", - "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 v1/v2).", - "description": "Returns 201 Created (v4+ returns 200 OK).", - "operationId": "multicast_group_update_external_v2", - "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 v1/v2).", - "operationId": "multicast_groups_list_v2", - "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 v1/v2).", - "operationId": "multicast_group_get_v2", - "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).", - "description": "Does not include tag validation.", - "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 v1/v2).", - "operationId": "multicast_groups_list_by_tag_v2", - "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.", - "operationId": "multicast_reset_by_tag", - "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).", - "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).", - "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.\n\nAdmin-local addresses are ff04::/16 (scope 4). These are used for internal/underlay multicast groups.", - "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 (API versions 1 and 2).\n\nThis is the original `IpSrc` enum that used a single `Subnet` variant (IPv4 only) rather than the `Any` variant added in version 3.", - "oneOf": [ - { - "description": "Exact match for the source IP address.", - "type": "object", - "properties": { - "Exact": { - "type": "string", - "format": "ip" - } - }, - "required": [ - "Exact" - ], - "additionalProperties": false - }, - { - "description": "Subnet match for the source IP address.", - "type": "object", - "properties": { - "Subnet": { - "$ref": "#/components/schemas/Ipv4Net" - } - }, - "required": [ - "Subnet" - ], - "additionalProperties": false - } - ] - }, - "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 (API version 2).", - "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, - "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/AdminScopedIpv6" - }, - "members": { - "type": "array", - "items": { - "$ref": "#/components/schemas/MulticastGroupMember" - } - }, - "tag": { - "nullable": true, - "type": "string" - } - }, - "required": [ - "group_ip", - "members" - ] - }, - "MulticastGroupExternalResponse": { - "description": "Response structure for external multicast group operations (API version 2).", - "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 2).", - "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 2).", - "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 (to the rack) groups (API version 2).", - "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, - "type": "string" - } - }, - "required": [ - "external_forwarding", - "internal_forwarding" - ] - }, - "MulticastGroupUpdateUnderlayEntry": { - "description": "Represents a multicast replication entry for PUT requests for internal (to the rack) groups.", - "type": "object", - "properties": { - "members": { - "type": "array", - "items": { - "$ref": "#/components/schemas/MulticastGroupMember" - } - }, - "tag": { - "nullable": true, - "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" - ] - }, - "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-5.0.0-c7d322.json b/openapi/dpd/dpd-4.0.0-00cf42.json similarity index 98% rename from openapi/dpd/dpd-5.0.0-c7d322.json rename to openapi/dpd/dpd-4.0.0-00cf42.json index 46aabe04..51044a00 100644 --- a/openapi/dpd/dpd-5.0.0-c7d322.json +++ b/openapi/dpd/dpd-4.0.0-00cf42.json @@ -7,13 +7,13 @@ "url": "https://oxide.computer", "email": "api@oxide.computer" }, - "version": "5.0.0" + "version": "4.0.0" }, "paths": { "/all-settings": { "delete": { "summary": "Clear all settings.", - "description": "This removes all data entirely, including:\n\n- All ARP and NDP table entries - All routes - All links on all switch ports - All NAT mappings - All multicast groups\n\nNote: Unlike `reset_all_tagged`, this endpoint does clear multicast groups.", + "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": { @@ -31,7 +31,7 @@ "/all-settings/{tag}": { "delete": { "summary": "Clear all settings associated with a specific tag.", - "description": "This removes:\n\n- All ARP or NDP table entries. - All routes - 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.", + "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": [ { @@ -297,7 +297,7 @@ "/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 crated on a physical switch port.", + "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": { @@ -1122,7 +1122,7 @@ "/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.", + "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": [ { @@ -1133,6 +1133,15 @@ "type": "string", "format": "ip" } + }, + { + "in": "query", + "name": "tag", + "description": "Tag that must match the group's existing tag.", + "required": true, + "schema": { + "type": "string" + } } ], "requestBody": { @@ -1265,7 +1274,7 @@ }, "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). When a tag is provided in the query, it must match the group's existing tag to prove ownership. Omitting the tag skips validation.", + "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": [ { @@ -1280,9 +1289,9 @@ { "in": "query", "name": "tag", - "description": "Tag that must match the group's existing tag for ownership validation. Omit to skip validation.", + "description": "Tag that must match the group's existing tag.", + "required": true, "schema": { - "nullable": true, "type": "string" } } @@ -1303,6 +1312,7 @@ "/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": [ { @@ -1358,6 +1368,7 @@ }, "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": [ { @@ -1453,7 +1464,7 @@ }, "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.", + "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": [ { @@ -1463,6 +1474,15 @@ "schema": { "$ref": "#/components/schemas/AdminScopedIpv6" } + }, + { + "in": "query", + "name": "tag", + "description": "Tag that must match the group's existing tag.", + "required": true, + "schema": { + "type": "string" + } } ], "requestBody": { @@ -7737,6 +7757,7 @@ }, "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" } }, @@ -7761,6 +7782,7 @@ }, "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" } }, @@ -7770,7 +7792,7 @@ ] }, "MulticastGroupExternalResponse": { - "description": "Response structure for external multicast group operations. These groups handle IPv4 and non-admin IPv6 multicast via NAT targets.", + "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": { @@ -7796,7 +7818,7 @@ } }, "tag": { - "description": "Tag for ownership validation. Always present; generated as `{uuid}:{group_ip}` if not provided at creation time.", + "description": "Tag for validating update/delete requests. Always present and generated as `{uuid}:{group_ip}` if not provided at creation time.", "type": "string" } }, @@ -7832,7 +7854,7 @@ "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-scoped IPv6 multicast with full replication.", + "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": { @@ -7856,7 +7878,7 @@ } }, "tag": { - "description": "Tag for ownership validation. Always present; generated as `{uuid}:{group_ip}` if not provided at creation time.", + "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": { @@ -7875,7 +7897,7 @@ ] }, { - "description": "Response structure for external multicast group operations. These groups handle IPv4 and non-admin IPv6 multicast via NAT targets.", + "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": { @@ -7907,7 +7929,7 @@ } }, "tag": { - "description": "Tag for ownership validation. Always present; generated as `{uuid}:{group_ip}` if not provided at creation time.", + "description": "Tag for validating update/delete requests. Always present and generated as `{uuid}:{group_ip}` if not provided at creation time.", "type": "string" } }, @@ -7944,7 +7966,7 @@ ] }, "MulticastGroupUnderlayResponse": { - "description": "Response structure for underlay/internal multicast group operations. These groups handle admin-scoped IPv6 multicast with full replication.", + "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": { @@ -7962,7 +7984,7 @@ } }, "tag": { - "description": "Tag for ownership validation. Always present; generated as `{uuid}:{group_ip}` if not provided at creation time.", + "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": { @@ -7980,7 +8002,7 @@ ] }, "MulticastGroupUpdateExternalEntry": { - "description": "A multicast group update entry for PUT requests for external (to the rack) groups.", + "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": { @@ -7995,10 +8017,6 @@ "items": { "$ref": "#/components/schemas/IpSrc" } - }, - "tag": { - "nullable": true, - "type": "string" } }, "required": [ @@ -8007,7 +8025,7 @@ ] }, "MulticastGroupUpdateUnderlayEntry": { - "description": "Represents a multicast replication entry for PUT requests for internal (to the rack) groups.", + "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": { @@ -8015,10 +8033,6 @@ "items": { "$ref": "#/components/schemas/MulticastGroupMember" } - }, - "tag": { - "nullable": true, - "type": "string" } }, "required": [ diff --git a/openapi/dpd/dpd-latest.json b/openapi/dpd/dpd-latest.json index 316a4ba0..ec2849af 120000 --- a/openapi/dpd/dpd-latest.json +++ b/openapi/dpd/dpd-latest.json @@ -1 +1 @@ -dpd-5.0.0-c7d322.json \ No newline at end of file +dpd-4.0.0-00cf42.json \ No newline at end of file From a253044de5a59cb711979fcb3e35dc0d41cee4f0 Mon Sep 17 00:00:00 2001 From: Zeeshan Lakhani Date: Wed, 14 Jan 2026 15:33:14 -0500 Subject: [PATCH 11/22] [post-merge] update to correct tag type, align with main --- dpd-api/src/lib.rs | 23 +--- dpd-client/tests/integration_tests/mcast.rs | 101 ++++++++---------- dpd-client/tests/integration_tests/service.rs | 26 +++-- .../tests/integration_tests/table_tests.rs | 4 +- ....0.0-19eaa3.json => dpd-3.0.0-70f546.json} | 5 +- ....0.0-00cf42.json => dpd-4.0.0-56f11d.json} | 17 ++- openapi/dpd/dpd-latest.json | 2 +- 7 files changed, 79 insertions(+), 99 deletions(-) rename openapi/dpd/{dpd-3.0.0-19eaa3.json => dpd-3.0.0-70f546.json} (99%) rename openapi/dpd/{dpd-4.0.0-00cf42.json => dpd-4.0.0-56f11d.json} (99%) diff --git a/dpd-api/src/lib.rs b/dpd-api/src/lib.rs index 0ca1aff3..88356d19 100644 --- a/dpd-api/src/lib.rs +++ b/dpd-api/src/lib.rs @@ -1592,12 +1592,7 @@ pub trait DpdApi { /** * Delete a multicast group configuration by IP address (API versions 1-3). - * - * Does not include tag validation. */ - // We cannot provide a default implementation that delegates to - // `multicast_group_delete` because Dropshot's `Query` extractor is opaque - // and cannot be constructed outside the framework. #[endpoint { method = DELETE, path = "/multicast/groups/{group_ip}", @@ -1720,11 +1715,8 @@ pub trait DpdApi { /** * Update an underlay (internal) multicast group configuration (API v1-v3). * - * Tags are optional. If a tag is not provided, the existing tag is preserved. + * Tags are optional for backward compatibility. */ - // We cannot provide a default implementation that delegates to - // `multicast_group_update_underlay` because the implementor must look up - // the existing tag if not provided in the request. #[endpoint { method = PUT, path = "/multicast/underlay-groups/{group_ip}", @@ -1759,12 +1751,8 @@ pub trait DpdApi { /** * Update an external-only multicast group configuration (API v3). * - * Tags are optional. If a tag is not provided, the existing tag is preserved. - * Returns 201 Created (API v4+ returns 200 OK). + * Tags are optional for backward compatibility. */ - // We cannot provide a default implementation that delegates to - // `multicast_group_update_external` because the implementor must look up - // the existing tag if not provided in the request. #[endpoint { method = PUT, path = "/multicast/external-groups/{group_ip}", @@ -1782,12 +1770,8 @@ pub trait DpdApi { /** * Update an external-only multicast group configuration (API v1/v2). * - * Tags are optional. If a tag is not provided, the existing tag is preserved. - * Returns 201 Created (API v4+ returns 200 OK). + * Tags are optional for backward compatibility. */ - // We cannot provide a default implementation that delegates to - // `multicast_group_update_external` because the implementor must look up - // the existing tag if not provided in the request. #[endpoint { method = PUT, path = "/multicast/external-groups/{group_ip}", @@ -2681,7 +2665,6 @@ pub struct MulticastGroupIpParam { #[derive( Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize, JsonSchema, )] -#[schemars(transparent)] pub struct MulticastTag( #[schemars( length(min = 1, max = 80), diff --git a/dpd-client/tests/integration_tests/mcast.rs b/dpd-client/tests/integration_tests/mcast.rs index 8a7ee3e8..1ac8038f 100644 --- a/dpd-client/tests/integration_tests/mcast.rs +++ b/dpd-client/tests/integration_tests/mcast.rs @@ -186,19 +186,7 @@ async fn create_test_multicast_group( } } -fn make_del_tag(tag: &str) -> types::MulticastGroupDeleteTag { - tag.parse().expect("tag should parse") -} - -fn make_underlay_update_tag( - tag: &str, -) -> types::MulticastGroupUpdateUnderlayTag { - tag.parse().expect("tag should parse") -} - -fn make_external_update_tag( - tag: &str, -) -> types::MulticastGroupUpdateExternalTag { +fn make_tag(tag: &str) -> types::MulticastTag { tag.parse().expect("tag should parse") } @@ -208,7 +196,7 @@ async fn cleanup_test_group( group_ip: IpAddr, tag: &str, ) -> TestResult { - let del_tag = make_del_tag(tag); + let del_tag = make_tag(tag); switch .client .multicast_group_delete(&group_ip, &del_tag) @@ -643,7 +631,7 @@ async fn test_internal_ipv6_validation() -> TestResult { .client .multicast_group_update_underlay( &created.group_ip, - &make_underlay_update_tag(TEST_TAG), + &make_tag(TEST_TAG), &update_entry, ) .await @@ -815,7 +803,7 @@ async fn test_group_api_lifecycle() { // Get groups by tag let tagged_groups = switch .client - .multicast_groups_list_by_tag_stream(TEST_TAG, None) + .multicast_groups_list_by_tag_stream(&make_tag(TEST_TAG), None) .try_collect::>() .await .expect("Should be able to get groups by tag"); @@ -872,7 +860,7 @@ async fn test_group_api_lifecycle() { .client .multicast_group_update_external( &group_ip, - &make_external_update_tag(TEST_TAG), + &make_tag(TEST_TAG), &external_update, ) .await @@ -889,7 +877,7 @@ async fn test_group_api_lifecycle() { assert_eq!(updated.sources, None); // Delete the group (must provide matching tag) - let del_tag = make_del_tag(TEST_TAG); + let del_tag = make_tag(TEST_TAG); switch .client .multicast_group_delete(&group_ip, &del_tag) @@ -980,7 +968,7 @@ async fn test_multicast_del_tag_validation() -> TestResult { .expect("Should create tagged group"); // Attempt delete with wrong tag - should fail - let del_tag = make_del_tag(TAG_B); + let del_tag = make_tag(TAG_B); let wrong_tag_result = switch .client .multicast_group_delete(&tagged_group_ip, &del_tag) @@ -1002,7 +990,7 @@ async fn test_multicast_del_tag_validation() -> TestResult { // Case: Empty string should fail client-side validation (schema enforces minLength: 1) assert!( - "".parse::().is_err(), + "".parse::().is_err(), "Empty tag should fail client-side validation" ); @@ -1015,7 +1003,7 @@ async fn test_multicast_del_tag_validation() -> TestResult { ); // Case: Delete with correct tag should succeed - let del_tag = make_del_tag(TAG_A); + let del_tag = make_tag(TAG_A); switch .client .multicast_group_delete(&tagged_group_ip, &del_tag) @@ -1060,7 +1048,7 @@ async fn test_multicast_del_tag_validation() -> TestResult { let auto_tag = created.tag.clone(); // Delete with correct auto-generated tag should succeed - let del_tag = make_del_tag(auto_tag.as_str()); + let del_tag = make_tag(auto_tag.as_str()); switch .client .multicast_group_delete(&auto_tagged_group_ip, &del_tag) @@ -1152,7 +1140,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"); @@ -1168,7 +1156,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"); @@ -3288,7 +3276,7 @@ async fn test_multicast_dynamic_membership() -> TestResult { .client .multicast_group_update_external( &get_group_ip(&created_group), - &make_external_update_tag(TEST_TAG), + &make_tag(TEST_TAG), &external_update_entry, ) .await @@ -3323,7 +3311,7 @@ async fn test_multicast_dynamic_membership() -> TestResult { .client .multicast_group_update_underlay( &types::AdminScopedIpv6(ipv6), - &make_underlay_update_tag(TEST_TAG), + &make_tag(TEST_TAG), &internal_update_entry, ) .await @@ -3900,6 +3888,10 @@ async fn test_multicast_reset_all_tables() -> TestResult { Ok(()) } +/* + * Commented out untl https://github.com/oxidecomputer/dendrite/issues/107 is + * fixed + * #[tokio::test] #[ignore] async fn test_multicast_vlan_translation_not_possible() -> TestResult { @@ -3976,6 +3968,7 @@ async fn test_multicast_vlan_translation_not_possible() -> TestResult { .unwrap(); cleanup_test_group(switch, internal_multicast_ip, TEST_TAG).await } +*/ #[tokio::test] #[ignore] @@ -4461,7 +4454,7 @@ async fn test_ipv6_multicast_scope_validation() { let admin_local_group = admin_local_result.unwrap().into_inner(); let target_group = target_result.into_inner(); - let del_tag = make_del_tag(TEST_TAG); + let del_tag = make_tag(TEST_TAG); switch .client .multicast_group_delete( @@ -4471,7 +4464,7 @@ async fn test_ipv6_multicast_scope_validation() { .await .ok(); - let del_tag = make_del_tag(TEST_TAG); + let del_tag = make_tag(TEST_TAG); switch .client .multicast_group_delete(&target_group.group_ip.to_ip_addr(), &del_tag) @@ -4522,7 +4515,7 @@ async fn test_multicast_group_id_recycling() -> TestResult { ); // Delete the first group - let del_tag = make_del_tag(TEST_TAG); + let del_tag = make_tag(TEST_TAG); switch .client .multicast_group_delete(&group1_ip, &del_tag) @@ -4564,7 +4557,7 @@ 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_del_tag(TEST_TAG); + let del_tag = make_tag(TEST_TAG); switch .client .multicast_group_delete(&group2_ip, &del_tag) @@ -4792,7 +4785,7 @@ async fn test_multicast_empty_then_add_members_ipv6() -> TestResult { .client .multicast_group_update_underlay( &ipv6_update, - &make_underlay_update_tag(TEST_TAG), + &make_tag(TEST_TAG), &update_entry, ) .await @@ -4928,7 +4921,7 @@ async fn test_multicast_empty_then_add_members_ipv6() -> TestResult { .client .multicast_group_update_underlay( &ipv6_update, - &make_underlay_update_tag(TEST_TAG), + &make_tag(TEST_TAG), &empty_update_entry, ) .await @@ -5158,7 +5151,7 @@ async fn test_multicast_empty_then_add_members_ipv4() -> TestResult { .client .multicast_group_update_underlay( &ipv6_update, - &make_underlay_update_tag(TEST_TAG), + &make_tag(TEST_TAG), &update_entry, ) .await @@ -5294,7 +5287,7 @@ async fn test_multicast_empty_then_add_members_ipv4() -> TestResult { .client .multicast_group_update_underlay( &ipv6_update, - &make_underlay_update_tag(TEST_TAG), + &make_tag(TEST_TAG), &empty_update_entry, ) .await @@ -5576,7 +5569,7 @@ async fn test_multicast_rollback_member_update_failure() -> TestResult { .client .multicast_group_update_underlay( &ipv6_update, - &make_underlay_update_tag(TEST_TAG), + &make_tag(TEST_TAG), &update_request, ) .await; @@ -5688,7 +5681,7 @@ async fn test_multicast_rollback_nat_transition_failure() -> TestResult { .client .multicast_group_update_external( &external_group_ip, - &make_external_update_tag(TEST_TAG), + &make_tag(TEST_TAG), &invalid_update, ) .await; @@ -5943,7 +5936,7 @@ async fn test_multicast_rollback_source_filter_update() -> TestResult { .client .multicast_group_update_external( &group_ip, - &make_external_update_tag(TEST_TAG), + &make_tag(TEST_TAG), &failing_update_entry, ) .await; @@ -6063,7 +6056,7 @@ async fn test_multicast_rollback_partial_member_addition() -> TestResult { .client .multicast_group_update_underlay( &ipv6_update, - &make_underlay_update_tag(TEST_TAG), + &make_tag(TEST_TAG), &update_request, ) .await; @@ -6397,7 +6390,7 @@ async fn test_source_filter_ipv4_collapses_to_any() -> TestResult { // 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_del_tag(TEST_TAG); + let del_tag = make_tag(TEST_TAG); let delete_internal_first_result = switch .client .multicast_group_delete(&internal_group_ip, &del_tag) @@ -6510,7 +6503,7 @@ async fn test_source_filter_ipv6_collapses_to_any() -> TestResult { // 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_del_tag(TEST_TAG); + let del_tag = make_tag(TEST_TAG); let delete_internal_first_result = switch .client .multicast_group_delete(&internal_group_ip, &del_tag) @@ -6627,7 +6620,7 @@ async fn test_source_filter_update_to_any() -> TestResult { .client .multicast_group_update_external( &external_group_ip, - &make_external_update_tag(TEST_TAG), + &make_tag(TEST_TAG), &update_entry, ) .await @@ -6874,7 +6867,7 @@ async fn test_update_nonexistent_group_returns_404() -> TestResult { .client .multicast_group_update_underlay( &nonexistent_underlay, - &make_underlay_update_tag("nonexistent_test"), + &make_tag("nonexistent_test"), &underlay_update, ) .await; @@ -6900,7 +6893,7 @@ async fn test_update_nonexistent_group_returns_404() -> TestResult { .client .multicast_group_update_external( &nonexistent_external, - &make_external_update_tag("nonexistent_test"), + &make_tag("nonexistent_test"), &external_update, ) .await; @@ -6928,7 +6921,7 @@ async fn test_delete_nonexistent_group_returns_404() -> TestResult { // Case: Delete non-existent group with a tag provided let nonexistent_ip = IpAddr::V4(Ipv4Addr::new(239, 255, 255, 253)); - let del_tag = make_del_tag("some_tag"); + let del_tag = make_tag("some_tag"); let result = switch .client .multicast_group_delete(&nonexistent_ip, &del_tag) @@ -6984,7 +6977,7 @@ async fn test_underlay_delete_recreate_recovery_flow() -> TestResult { assert_eq!(created.into_inner().members.len(), 1); // Case: Delete the group (simulating recovery from stale state) - let del_tag = make_del_tag(tag); + let del_tag = make_tag(tag); switch .client .multicast_group_delete(&group_ip.to_ip_addr(), &del_tag) @@ -7046,7 +7039,7 @@ async fn test_underlay_delete_recreate_recovery_flow() -> TestResult { // Cleanup switch .client - .multicast_reset_by_tag(tag) + .multicast_reset_by_tag(&make_tag(tag)) .await .expect("Should cleanup by tag"); @@ -7097,7 +7090,7 @@ async fn test_tag_immutability_on_update() -> TestResult { .client .multicast_group_update_underlay( &group_ip, - &make_underlay_update_tag(TAG_WRONG), + &make_tag(TAG_WRONG), &update_entry, ) .await; @@ -7122,7 +7115,7 @@ async fn test_tag_immutability_on_update() -> TestResult { } // Cleanup - let del_tag = make_del_tag(TAG_A); + let del_tag = make_tag(TAG_A); switch .client .multicast_group_delete(&group_ip.to_ip_addr(), &del_tag) @@ -7165,7 +7158,7 @@ async fn test_tag_validation_on_delete() -> TestResult { let group_ip = created.group_ip; // Attempt delete with wrong tag - let del_tag = make_del_tag(TAG_WRONG); + let del_tag = make_tag(TAG_WRONG); let result = switch .client .multicast_group_delete(&group_ip.to_ip_addr(), &del_tag) @@ -7194,7 +7187,7 @@ async fn test_tag_validation_on_delete() -> TestResult { assert_eq!(fetched.into_inner().tag, TAG_A, "Tag should be preserved"); // Delete with correct tag - let del_tag = make_del_tag(TAG_A); + let del_tag = make_tag(TAG_A); switch .client .multicast_group_delete(&group_ip.to_ip_addr(), &del_tag) @@ -7257,7 +7250,7 @@ async fn test_tag_validation() -> TestResult { .client .multicast_group_update_external( &external_ip, - &make_external_update_tag(TAG_WRONG), + &make_tag(TAG_WRONG), &update_entry, ) .await; @@ -7307,7 +7300,7 @@ async fn test_tag_validation() -> TestResult { let case_group_ip = created.group_ip; - let del_tag = make_del_tag("casesensitivetag"); + let del_tag = make_tag("casesensitivetag"); let result = switch .client .multicast_group_delete(&case_group_ip.to_ip_addr(), &del_tag) @@ -7318,7 +7311,7 @@ async fn test_tag_validation() -> TestResult { "Case-insensitive tag should fail" ); - let del_tag = make_del_tag("CaseSensitiveTag"); + let del_tag = make_tag("CaseSensitiveTag"); switch .client .multicast_group_delete(&case_group_ip.to_ip_addr(), &del_tag) @@ -7329,7 +7322,7 @@ async fn test_tag_validation() -> TestResult { switch .client - .multicast_reset_by_tag("nonexistent_tag_xyz") + .multicast_reset_by_tag(&make_tag("nonexistent_tag_xyz")) .await .expect("Reset with non-existent tag should succeed"); diff --git a/dpd-client/tests/integration_tests/service.rs b/dpd-client/tests/integration_tests/service.rs index 3c5c6a19..20a72701 100644 --- a/dpd-client/tests/integration_tests/service.rs +++ b/dpd-client/tests/integration_tests/service.rs @@ -174,9 +174,7 @@ async fn test_service_ipv4_wrong_port() -> TestResult { } // Packets sent to an IP address not assigned to a switch port should be -// dropped when the port is configured as NAT-only (see issue #172). -// Without NAT-only, such packets would instead trigger ICMP_DEST_UNREACHABLE -// via the router. +// dropped. #[tokio::test] #[ignore] async fn test_service_ipv4_unknown_address() -> TestResult { @@ -197,6 +195,13 @@ async fn test_service_ipv4_unknown_address() -> TestResult { .await .unwrap(); + // Mark the port as NAT-only + switch + .client + .link_nat_only_set(&port_id, &link_id, true) + .await + .unwrap(); + let send_pkt = common::gen_udp_packet( Endpoint::parse("e0:d5:5e:67:89:ab", "10.10.10.10", 3333).unwrap(), Endpoint::parse(router_mac, "192.10.12.1", 4444).unwrap(), @@ -204,24 +209,17 @@ async fn test_service_ipv4_unknown_address() -> TestResult { let send = TestPacket { packet: Arc::new(send_pkt), - port: PhysPort(ingress + 1), + port: PhysPort(ingress), }; - // Mark the ingress port as NAT-only so packets to unknown addresses are - // dropped rather than triggering ICMP_UNREACHABLE (see issue #172). - let (ingress_port_id, ingress_link_id) = - switch.link_id(PhysPort(ingress + 1)).unwrap(); - switch - .client - .link_nat_only_set(&ingress_port_id, &ingress_link_id, true) - .await - .unwrap(); let result = switch.packet_test(vec![send], Vec::new()); + // Clear the port's NAT-only property switch .client - .link_nat_only_set(&ingress_port_id, &ingress_link_id, false) + .link_nat_only_set(&port_id, &link_id, false) .await .unwrap(); + result } diff --git a/dpd-client/tests/integration_tests/table_tests.rs b/dpd-client/tests/integration_tests/table_tests.rs index 8668ca74..33b2fd49 100644 --- a/dpd-client/tests/integration_tests/table_tests.rs +++ b/dpd-client/tests/integration_tests/table_tests.rs @@ -494,7 +494,7 @@ impl TableTest async fn delete_entry(switch: &Switch, idx: usize) -> OpResult<()> { let ip = IpAddr::V6(gen_ipv6_multicast_addr(idx)); - let del_tag: types::MulticastGroupDeleteTag = + let del_tag: types::MulticastTag = MCAST_TAG.parse().expect("tag should parse"); switch.client.multicast_group_delete(&ip, &del_tag).await } @@ -503,7 +503,7 @@ impl TableTest // 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/openapi/dpd/dpd-3.0.0-19eaa3.json b/openapi/dpd/dpd-3.0.0-70f546.json similarity index 99% rename from openapi/dpd/dpd-3.0.0-19eaa3.json rename to openapi/dpd/dpd-3.0.0-70f546.json index 82b85c9a..caf801ea 100644 --- a/openapi/dpd/dpd-3.0.0-19eaa3.json +++ b/openapi/dpd/dpd-3.0.0-70f546.json @@ -1121,7 +1121,7 @@ "/multicast/external-groups/{group_ip}": { "put": { "summary": "Update an external-only multicast group configuration (API v3).", - "description": "Tags are optional. If a tag is not provided, the existing tag is preserved. Returns 201 Created (API v4+ returns 200 OK).", + "description": "Tags are optional for backward compatibility.", "operationId": "multicast_group_update_external_v3", "parameters": [ { @@ -1264,7 +1264,6 @@ }, "delete": { "summary": "Delete a multicast group configuration by IP address (API versions 1-3).", - "description": "Does not include tag validation.", "operationId": "multicast_group_delete_v3", "parameters": [ { @@ -1442,7 +1441,7 @@ }, "put": { "summary": "Update an underlay (internal) multicast group configuration (API v1-v3).", - "description": "Tags are optional. If a tag is not provided, the existing tag is preserved.", + "description": "Tags are optional for backward compatibility.", "operationId": "multicast_group_update_underlay_v3", "parameters": [ { diff --git a/openapi/dpd/dpd-4.0.0-00cf42.json b/openapi/dpd/dpd-4.0.0-56f11d.json similarity index 99% rename from openapi/dpd/dpd-4.0.0-00cf42.json rename to openapi/dpd/dpd-4.0.0-56f11d.json index 51044a00..ac24ef25 100644 --- a/openapi/dpd/dpd-4.0.0-00cf42.json +++ b/openapi/dpd/dpd-4.0.0-56f11d.json @@ -1140,7 +1140,7 @@ "description": "Tag that must match the group's existing tag.", "required": true, "schema": { - "type": "string" + "$ref": "#/components/schemas/MulticastTag" } } ], @@ -1292,7 +1292,7 @@ "description": "Tag that must match the group's existing tag.", "required": true, "schema": { - "type": "string" + "$ref": "#/components/schemas/MulticastTag" } } ], @@ -1320,7 +1320,7 @@ "name": "tag", "required": true, "schema": { - "type": "string" + "$ref": "#/components/schemas/MulticastTag" } }, { @@ -1376,7 +1376,7 @@ "name": "tag", "required": true, "schema": { - "type": "string" + "$ref": "#/components/schemas/MulticastTag" } } ], @@ -1481,7 +1481,7 @@ "description": "Tag that must match the group's existing tag.", "required": true, "schema": { - "type": "string" + "$ref": "#/components/schemas/MulticastTag" } } ], @@ -9651,6 +9651,13 @@ "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": { diff --git a/openapi/dpd/dpd-latest.json b/openapi/dpd/dpd-latest.json index ec2849af..4554c6c1 120000 --- a/openapi/dpd/dpd-latest.json +++ b/openapi/dpd/dpd-latest.json @@ -1 +1 @@ -dpd-4.0.0-00cf42.json \ No newline at end of file +dpd-4.0.0-56f11d.json \ No newline at end of file From ac8fefbbcf835402404069a45bbbdb039d6fcf0f Mon Sep 17 00:00:00 2001 From: Zeeshan Lakhani Date: Wed, 14 Jan 2026 21:27:08 +0000 Subject: [PATCH 12/22] [fmt,2026] --- dpd-client/tests/integration_tests/table_tests.rs | 5 ++++- xtask/src/linux.rs | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/dpd-client/tests/integration_tests/table_tests.rs b/dpd-client/tests/integration_tests/table_tests.rs index 33b2fd49..ef1dc6ca 100644 --- a/dpd-client/tests/integration_tests/table_tests.rs +++ b/dpd-client/tests/integration_tests/table_tests.rs @@ -503,7 +503,10 @@ impl TableTest // Count only underlay groups with our test tag (since this tests replication table capacity) switch .client - .multicast_groups_list_by_tag_stream(&MCAST_TAG.parse::().unwrap(), 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/xtask/src/linux.rs b/xtask/src/linux.rs index 0f032aaf..96288d0a 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; From e68a2a6afa74f163742d6adc361cab0964d7e1ed Mon Sep 17 00:00:00 2001 From: Zeeshan Lakhani Date: Thu, 15 Jan 2026 01:04:04 -0500 Subject: [PATCH 13/22] [match omicron] restrict underlay subnet to ff04::/64, not all of ff04::/16 As per Omicron, customers can use admin-local IPv6 multicast addresses (ff04::/16) for external groups, except for the reserved underlay subnet (ff04::/64) which is used for internal underlay multicast allocation. Changes: - switch external-group validation from validate_not_admin_local_ipv6() to validate_not_underlay_subnet() - validate_not_underlay_subnet() now only rejects ff04::/64 - validate_nat_target() now requires NAT IPs in UNDERLAY_MULTICAST_SUBNET - VLAN tagging logic uses UNDERLAY_MULTICAST_SUBNET, removed dead is_unique_local() check (multicast IPs can't be ULA) - removed unused IPV6_SCOPE_MASK, IPV6_ULA_MASK/PATTERN from P4 - removed dead ULA entry from mcast_tag_check table (const size = 1) - Updated integration test assertions, added unit tests --- dpd-client/tests/integration_tests/mcast.rs | 4 +- dpd/p4/constants.p4 | 13 ++- dpd/p4/sidecar.p4 | 22 ++--- dpd/src/mcast/mod.rs | 4 +- dpd/src/mcast/validate.rs | 98 ++++++++++++++++----- dpd/src/table/mcast/mcast_route.rs | 24 +++-- 6 files changed, 109 insertions(+), 56 deletions(-) diff --git a/dpd-client/tests/integration_tests/mcast.rs b/dpd-client/tests/integration_tests/mcast.rs index 1ac8038f..ff222cfb 100644 --- a/dpd-client/tests/integration_tests/mcast.rs +++ b/dpd-client/tests/integration_tests/mcast.rs @@ -1385,7 +1385,7 @@ async fn test_api_invalid_combinations() -> TestResult { match result { Error::ErrorResponse(inner) => { assert_eq!(inner.status(), 400); - assert!(inner.message.contains("admin-local multicast address")); + assert!(inner.message.contains("admin-local scope")); } _ => panic!( "Expected ErrorResponse for admin-local external group creation" @@ -4446,7 +4446,7 @@ async fn test_ipv6_multicast_scope_validation() { let external_error_msg = format!("{:?}", admin_external_result.unwrap_err()); assert!( - external_error_msg.contains("admin-local multicast address"), + external_error_msg.contains("admin-local scope"), "Error should indicate admin-local addresses require internal API" ); diff --git a/dpd/p4/constants.p4 b/dpd/p4/constants.p4 index 2696c7f1..343187ec 100644 --- a/dpd/p4/constants.p4 +++ b/dpd/p4/constants.p4 @@ -54,13 +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_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/sidecar.p4 b/dpd/p4/sidecar.p4 index 1b30a855..c2de0edf 100644 --- a/dpd/p4/sidecar.p4 +++ b/dpd/p4/sidecar.p4 @@ -1738,8 +1738,9 @@ control MulticastIngress ( * based on the per-group bitmap configuration. * * Flow: - * 1. mcast_tag_check : Match packets with admin-local (ff04::/16) or ULA - * destination, AND mcast_tag=2 (UNDERLAY_EXTERNAL) + * 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) @@ -1783,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; @@ -1795,17 +1802,10 @@ control MulticastEgress ( actions = { NoAction; } const entries = { - // Admin-local (scope value 4): Matches IPv6 multicast addresses - // with scope ff04::/16. This is the only multicast scope used for - // internal/underlay traffic (RFC 7346, RFC 4291). - ( true, IPV6_ADMIN_LOCAL_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 = 2; + const size = 1; } table tbl_decap_ports { diff --git a/dpd/src/mcast/mod.rs b/dpd/src/mcast/mod.rs index 3fcfdda5..27f33dcd 100644 --- a/dpd/src/mcast/mod.rs +++ b/dpd/src/mcast/mod.rs @@ -91,7 +91,7 @@ mod validate; use rollback::{GroupCreateRollbackContext, GroupUpdateRollbackContext}; use validate::{ validate_multicast_address, validate_nat_target, - validate_not_admin_local_ipv6, validate_tag, validate_tag_format, + validate_not_underlay_subnet, validate_tag, validate_tag_format, }; #[derive(Debug)] @@ -1332,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_local_ipv6(group_ip)?; + validate_not_underlay_subnet(group_ip)?; Ok(()) } diff --git a/dpd/src/mcast/validate.rs b/dpd/src/mcast/validate.rs index c608c8ae..52bf21f0 100644 --- a/dpd/src/mcast/validate.rs +++ b/dpd/src/mcast/validate.rs @@ -11,16 +11,15 @@ use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; +use super::IpSrc; +use crate::types::{DpdError, DpdResult}; use common::nat::NatTarget; 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, }; -use oxnet::Ipv6Net; - -use super::IpSrc; -use crate::types::{DpdError, DpdResult}; /// Check if an IP address is unicast (emulating the unstable std::net API). /// For IP addresses, unicast means simply "not multicast". @@ -45,7 +44,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!( @@ -54,12 +56,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_local_multicast() { + if !UNDERLAY_MULTICAST_SUBNET.contains(nat_target.internal_ip) { return Err(DpdError::Invalid(format!( - "NAT target internal IP address {} is not a valid \ - admin-local multicast address (must be ff04::/16)", + "NAT target internal IP address {} is not in the reserved \ + underlay multicast subnet (ff04::/64)", nat_target.internal_ip ))); } @@ -148,14 +148,19 @@ fn validate_ipv6_multicast( Ok(()) } -/// Validates that IPv6 addresses are not admin-local for external group creation. -pub(crate) fn validate_not_admin_local_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_local_multicast() + && UNDERLAY_MULTICAST_SUBNET.contains(ipv6) { return Err(DpdError::Invalid(format!( - "{addr} is an admin-local 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(()) @@ -550,17 +555,16 @@ mod tests { #[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 { - // admin-local multicast (ff04::/16) + // Valid NAT target in reserved underlay subnet (ff04::/64) + let valid_nat_target = NatTarget { internal_ip: Ipv6Addr::new( ADMIN_LOCAL_PREFIX, 0, @@ -571,12 +575,28 @@ mod tests { 0, 0x1234, ), - // Multicast MAC inner_mac: MacAddr::new(0x01, 0x00, 0x5e, 0x00, 0x00, 0x01), vni: Vni::new(100).unwrap(), }; + assert!(validate_nat_target(valid_nat_target).is_ok()); - assert!(validate_nat_target(mcast_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] @@ -680,6 +700,42 @@ mod tests { ); } + #[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 diff --git a/dpd/src/table/mcast/mcast_route.rs b/dpd/src/table/mcast/mcast_route.rs index 71281d9c..bd097e7c 100644 --- a/dpd/src/table/mcast/mcast_route.rs +++ b/dpd/src/table/mcast/mcast_route.rs @@ -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-local 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_local_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-local 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_local_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 { From a7febbecf84b5ca1e1799915cf0ee45516a7fb46 Mon Sep 17 00:00:00 2001 From: Zeeshan Lakhani Date: Thu, 15 Jan 2026 07:29:37 +0000 Subject: [PATCH 14/22] [multicast] restrict underlay multicast type to ff04::/64 API v4 (MCAST_STRICT_UNDERLAY) changes: - Renamed AdminScopedIpv6 to UnderlayMulticastIpv6 to better reflect its purpose as the underlay multicast subnet type - Tightened validation from ff04::/16 (admin-local scope) to ff04::/64 to match Omicron's UNDERLAY_MULTICAST_SUBNET allocation - Tag validation now required for update/delete operations (as before) API v3 backward compatibility: - Added v3::AdminScopedIpv6 type that accepts the broader ff04::/16 range - v3 endpoints use v3::MulticastUnderlayGroupIpParam and convert to the v4 type with appropriate error handling for out-of-range addresses --- dpd-api/src/lib.rs | 72 ++++++------ dpd-api/src/v2.rs | 7 +- dpd-api/src/v3.rs | 109 ++++++++++++++++-- dpd-client/tests/integration_tests/mcast.rs | 30 ++--- .../tests/integration_tests/table_tests.rs | 6 +- dpd-types/src/mcast.rs | 54 ++++----- dpd/src/api_server.rs | 40 ++++++- dpd/src/mcast/mod.rs | 46 ++++---- dpd/src/mcast/validate.rs | 4 +- ....0.0-70f546.json => dpd-3.0.0-e0be17.json} | 12 +- ....0.0-56f11d.json => dpd-4.0.0-7b2800.json} | 20 ++-- openapi/dpd/dpd-latest.json | 2 +- 12 files changed, 264 insertions(+), 138 deletions(-) rename openapi/dpd/{dpd-3.0.0-70f546.json => dpd-3.0.0-e0be17.json} (99%) rename openapi/dpd/{dpd-4.0.0-56f11d.json => dpd-4.0.0-7b2800.json} (99%) diff --git a/dpd-api/src/lib.rs b/dpd-api/src/lib.rs index 88356d19..c5c8209a 100644 --- a/dpd-api/src/lib.rs +++ b/dpd-api/src/lib.rs @@ -59,7 +59,7 @@ api_versions!([ // | example for the next person. // v // (next_int, IDENT), - (4, MCAST_TAG_OWNERSHIP), + (4, MCAST_STRICT_UNDERLAY), (3, MCAST_SOURCE_FILTER_ANY), (2, DUAL_STACK_NAT_WORKFLOW), (1, INITIAL), @@ -1474,7 +1474,7 @@ pub trait DpdApi { #[endpoint { method = POST, path = "/multicast/external-groups", - versions = VERSION_MCAST_TAG_OWNERSHIP.., + versions = VERSION_MCAST_STRICT_UNDERLAY.., }] async fn multicast_group_create_external( rqctx: RequestContext, @@ -1488,7 +1488,7 @@ pub trait DpdApi { #[endpoint { method = POST, path = "/multicast/external-groups", - versions = VERSION_MCAST_SOURCE_FILTER_ANY..VERSION_MCAST_TAG_OWNERSHIP, + versions = VERSION_MCAST_SOURCE_FILTER_ANY..VERSION_MCAST_STRICT_UNDERLAY, }] async fn multicast_group_create_external_v3( rqctx: RequestContext, @@ -1542,7 +1542,7 @@ pub trait DpdApi { #[endpoint { method = POST, path = "/multicast/underlay-groups", - versions = VERSION_MCAST_TAG_OWNERSHIP.., + versions = VERSION_MCAST_STRICT_UNDERLAY.., }] async fn multicast_group_create_underlay( rqctx: RequestContext, @@ -1556,7 +1556,7 @@ pub trait DpdApi { #[endpoint { method = POST, path = "/multicast/underlay-groups", - versions = ..VERSION_MCAST_TAG_OWNERSHIP, + versions = ..VERSION_MCAST_STRICT_UNDERLAY, }] async fn multicast_group_create_underlay_v3( rqctx: RequestContext, @@ -1582,7 +1582,7 @@ pub trait DpdApi { #[endpoint { method = DELETE, path = "/multicast/groups/{group_ip}", - versions = VERSION_MCAST_TAG_OWNERSHIP.., + versions = VERSION_MCAST_STRICT_UNDERLAY.., }] async fn multicast_group_delete( rqctx: RequestContext, @@ -1596,7 +1596,7 @@ pub trait DpdApi { #[endpoint { method = DELETE, path = "/multicast/groups/{group_ip}", - versions = ..VERSION_MCAST_TAG_OWNERSHIP, + versions = ..VERSION_MCAST_STRICT_UNDERLAY, }] async fn multicast_group_delete_v3( rqctx: RequestContext, @@ -1620,7 +1620,7 @@ pub trait DpdApi { #[endpoint { method = GET, path = "/multicast/groups/{group_ip}", - versions = VERSION_MCAST_TAG_OWNERSHIP.., + versions = VERSION_MCAST_STRICT_UNDERLAY.., }] async fn multicast_group_get( rqctx: RequestContext, @@ -1631,7 +1631,7 @@ pub trait DpdApi { #[endpoint { method = GET, path = "/multicast/groups/{group_ip}", - versions = VERSION_MCAST_SOURCE_FILTER_ANY..VERSION_MCAST_TAG_OWNERSHIP, + versions = VERSION_MCAST_SOURCE_FILTER_ANY..VERSION_MCAST_STRICT_UNDERLAY, }] async fn multicast_group_get_v3( rqctx: RequestContext, @@ -1668,7 +1668,7 @@ pub trait DpdApi { #[endpoint { method = GET, path = "/multicast/underlay-groups/{group_ip}", - versions = VERSION_MCAST_TAG_OWNERSHIP.., + versions = VERSION_MCAST_STRICT_UNDERLAY.., }] async fn multicast_group_get_underlay( rqctx: RequestContext, @@ -1676,21 +1676,18 @@ pub trait DpdApi { ) -> Result, HttpError>; /// Get an underlay (internal) multicast group configuration (API v1-v3). + /// + /// Uses the broader ff04::/16 (admin-local) address validation for backward + /// compatibility. #[endpoint { method = GET, path = "/multicast/underlay-groups/{group_ip}", - versions = ..VERSION_MCAST_TAG_OWNERSHIP, + versions = ..VERSION_MCAST_STRICT_UNDERLAY, }] async fn multicast_group_get_underlay_v3( rqctx: RequestContext, - path: Path, - ) -> Result, HttpError> - { - match Self::multicast_group_get_underlay(rqctx, path).await { - Ok(HttpResponseOk(resp)) => Ok(HttpResponseOk(resp.into())), - Err(e) => Err(e), - } - } + path: Path, + ) -> Result, HttpError>; /** * Update an underlay (internal) multicast group configuration. @@ -1703,7 +1700,7 @@ pub trait DpdApi { #[endpoint { method = PUT, path = "/multicast/underlay-groups/{group_ip}", - versions = VERSION_MCAST_TAG_OWNERSHIP.., + versions = VERSION_MCAST_STRICT_UNDERLAY.., }] async fn multicast_group_update_underlay( rqctx: RequestContext, @@ -1712,19 +1709,18 @@ pub trait DpdApi { group: TypedBody, ) -> Result, HttpError>; - /** - * Update an underlay (internal) multicast group configuration (API v1-v3). - * - * Tags are optional for backward compatibility. - */ + /// 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_TAG_OWNERSHIP, + versions = ..VERSION_MCAST_STRICT_UNDERLAY, }] async fn multicast_group_update_underlay_v3( rqctx: RequestContext, - path: Path, + path: Path, group: TypedBody, ) -> Result, HttpError>; @@ -1739,7 +1735,7 @@ pub trait DpdApi { #[endpoint { method = PUT, path = "/multicast/external-groups/{group_ip}", - versions = VERSION_MCAST_TAG_OWNERSHIP.., + versions = VERSION_MCAST_STRICT_UNDERLAY.., }] async fn multicast_group_update_external( rqctx: RequestContext, @@ -1756,7 +1752,7 @@ pub trait DpdApi { #[endpoint { method = PUT, path = "/multicast/external-groups/{group_ip}", - versions = VERSION_MCAST_SOURCE_FILTER_ANY..VERSION_MCAST_TAG_OWNERSHIP, + versions = VERSION_MCAST_SOURCE_FILTER_ANY..VERSION_MCAST_STRICT_UNDERLAY, }] async fn multicast_group_update_external_v3( rqctx: RequestContext, @@ -1792,7 +1788,7 @@ pub trait DpdApi { #[endpoint { method = GET, path = "/multicast/groups", - versions = VERSION_MCAST_TAG_OWNERSHIP.., + versions = VERSION_MCAST_STRICT_UNDERLAY.., }] async fn multicast_groups_list( rqctx: RequestContext, @@ -1808,7 +1804,7 @@ pub trait DpdApi { #[endpoint { method = GET, path = "/multicast/groups", - versions = VERSION_MCAST_SOURCE_FILTER_ANY..VERSION_MCAST_TAG_OWNERSHIP, + versions = VERSION_MCAST_SOURCE_FILTER_ANY..VERSION_MCAST_STRICT_UNDERLAY, }] async fn multicast_groups_list_v3( rqctx: RequestContext, @@ -1862,7 +1858,7 @@ pub trait DpdApi { #[endpoint { method = GET, path = "/multicast/tags/{tag}", - versions = VERSION_MCAST_TAG_OWNERSHIP.., + versions = VERSION_MCAST_STRICT_UNDERLAY.., }] async fn multicast_groups_list_by_tag( rqctx: RequestContext, @@ -1879,7 +1875,7 @@ pub trait DpdApi { #[endpoint { method = GET, path = "/multicast/tags/{tag}", - versions = VERSION_MCAST_SOURCE_FILTER_ANY..VERSION_MCAST_TAG_OWNERSHIP, + versions = VERSION_MCAST_SOURCE_FILTER_ANY..VERSION_MCAST_STRICT_UNDERLAY, }] async fn multicast_groups_list_by_tag_v3( rqctx: RequestContext, @@ -1948,7 +1944,7 @@ pub trait DpdApi { #[endpoint { method = DELETE, path = "/multicast/tags/{tag}", - versions = VERSION_MCAST_TAG_OWNERSHIP.., + versions = VERSION_MCAST_STRICT_UNDERLAY.., }] async fn multicast_reset_by_tag( rqctx: RequestContext, @@ -1960,7 +1956,7 @@ pub trait DpdApi { #[endpoint { method = DELETE, path = "/multicast/tags/{tag}", - versions = ..VERSION_MCAST_TAG_OWNERSHIP, + versions = ..VERSION_MCAST_STRICT_UNDERLAY, }] async fn multicast_reset_by_tag_v3( rqctx: RequestContext, @@ -1979,7 +1975,7 @@ pub trait DpdApi { #[endpoint { method = DELETE, path = "/multicast/untagged", - versions = VERSION_MCAST_TAG_OWNERSHIP.., + versions = VERSION_MCAST_STRICT_UNDERLAY.., }] async fn multicast_reset_untagged( rqctx: RequestContext, @@ -1991,7 +1987,7 @@ pub trait DpdApi { #[endpoint { method = DELETE, path = "/multicast/untagged", - versions = ..VERSION_MCAST_TAG_OWNERSHIP, + versions = ..VERSION_MCAST_STRICT_UNDERLAY, }] async fn multicast_reset_untagged_v3( rqctx: RequestContext, @@ -2722,7 +2718,7 @@ pub struct MulticastGroupTagQuery { /// [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 index b5990e9d..4d6e37f1 100644 --- a/dpd-api/src/v2.rs +++ b/dpd-api/src/v2.rs @@ -10,13 +10,14 @@ use std::{fmt, net::IpAddr}; -use dpd_types::mcast::{ - ExternalForwarding, InternalForwarding, MulticastGroupId, -}; 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; diff --git a/dpd-api/src/v3.rs b/dpd-api/src/v3.rs index 1ee01389..90f98b6c 100644 --- a/dpd-api/src/v3.rs +++ b/dpd-api/src/v3.rs @@ -6,19 +6,102 @@ //! Types from API version 3 that changed in version 4. //! -//! The `tag` field in response types changed from `Option` to `String` -//! since all groups now have default tags generated at creation time, and API -//! version 4 introduced tag validation for updates and deletes. +//! 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::net::IpAddr; - -use dpd_types::mcast::{ - AdminScopedIpv6, ExternalForwarding, InternalForwarding, IpSrc, - MulticastGroupId, MulticastGroupMember, +use std::{ + fmt, + net::{IpAddr, Ipv6Addr}, }; + +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) + } +} + /// Response structure for underlay/internal multicast group operations /// (API version 3). #[derive(Debug, Deserialize, Serialize, JsonSchema)] @@ -36,7 +119,7 @@ impl From { fn from(resp: dpd_types::mcast::MulticastGroupUnderlayResponse) -> Self { Self { - group_ip: resp.group_ip, + group_ip: resp.group_ip.into(), external_group_id: resp.external_group_id, underlay_group_id: resp.underlay_group_id, tag: Some(resp.tag), @@ -153,3 +236,11 @@ impl From } } } + +/// 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/tests/integration_tests/mcast.rs b/dpd-client/tests/integration_tests/mcast.rs index ff222cfb..70e6f587 100644 --- a/dpd-client/tests/integration_tests/mcast.rs +++ b/dpd-client/tests/integration_tests/mcast.rs @@ -15,11 +15,13 @@ use crate::integration_tests::common::prelude::*; use ::common::network::MacAddr; use anyhow::anyhow; use dpd_client::{Error, types}; -use dpd_types::mcast::ADMIN_LOCAL_PREFIX; use futures::TryStreamExt; 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); @@ -45,7 +47,7 @@ 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) } @@ -135,7 +137,7 @@ async fn create_test_multicast_group( .is_admin_local_multicast() { // Admin-local IPv6 groups are internal - let admin_local_ip = types::AdminScopedIpv6(ipv6); + let admin_local_ip = types::UnderlayMulticastIpv6(ipv6); let internal_entry = types::MulticastGroupCreateUnderlayEntry { group_ip: admin_local_ip, tag: tag.map(String::from), @@ -3310,7 +3312,7 @@ 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, ) @@ -3630,7 +3632,7 @@ async fn test_multicast_reset_all_tables() -> TestResult { let ipv6 = Ipv6Addr::new(ADMIN_LOCAL_PREFIX, 0, 0, 0, 0, 0, 0, 2); let group_entry2b = types::MulticastGroupCreateUnderlayEntry { - group_ip: types::AdminScopedIpv6(ipv6), + group_ip: types::UnderlayMulticastIpv6(ipv6), tag: Some(TEST_TAG.to_string()), members: vec![types::MulticastGroupMember { port_id: switch.link_id(egress1).unwrap().0, @@ -4776,7 +4778,7 @@ async fn test_multicast_empty_then_add_members_ipv6() -> TestResult { 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"), }); @@ -4912,7 +4914,7 @@ async fn test_multicast_empty_then_add_members_ipv6() -> TestResult { 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"), }); @@ -5142,7 +5144,7 @@ async fn test_multicast_empty_then_add_members_ipv4() -> TestResult { 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"), }); @@ -5278,7 +5280,7 @@ async fn test_multicast_empty_then_add_members_ipv4() -> TestResult { 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"), }); @@ -5559,7 +5561,7 @@ async fn test_multicast_rollback_member_update_failure() -> TestResult { members: invalid_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"), }); @@ -6046,7 +6048,7 @@ async fn test_multicast_rollback_partial_member_addition() -> TestResult { members: mixed_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"), }); @@ -6246,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"), @@ -6849,7 +6851,7 @@ async fn test_update_nonexistent_group_returns_404() -> TestResult { let switch = &*get_switch().await; // Case: Update non-existent underlay group - let nonexistent_underlay: types::AdminScopedIpv6 = + let nonexistent_underlay: types::UnderlayMulticastIpv6 = Ipv6Addr::new(ADMIN_LOCAL_PREFIX, 0, 0, 0, 0, 0, 0, 0xdead) .try_into() .unwrap(); @@ -6950,7 +6952,7 @@ async fn test_delete_nonexistent_group_returns_404() -> TestResult { async fn test_underlay_delete_recreate_recovery_flow() -> TestResult { let switch = &*get_switch().await; - let group_ip: types::AdminScopedIpv6 = + let group_ip: types::UnderlayMulticastIpv6 = Ipv6Addr::new(ADMIN_LOCAL_PREFIX, 0, 0, 0, 0, 0, 0, 0x501) .try_into() .unwrap(); diff --git a/dpd-client/tests/integration_tests/table_tests.rs b/dpd-client/tests/integration_tests/table_tests.rs index ef1dc6ca..283c05f4 100644 --- a/dpd-client/tests/integration_tests/table_tests.rs +++ b/dpd-client/tests/integration_tests/table_tests.rs @@ -17,10 +17,12 @@ use reqwest::StatusCode; use dpd_client::ClientInfo; use dpd_client::ResponseValue; use dpd_client::types; -use dpd_types::mcast::ADMIN_LOCAL_PREFIX; 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 @@ -471,7 +473,7 @@ impl TableTest // 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 { diff --git a/dpd-types/src/mcast.rs b/dpd-types/src/mcast.rs index ac87a2b8..cdc59256 100644 --- a/dpd-types/src/mcast.rs +++ b/dpd-types/src/mcast.rs @@ -12,7 +12,7 @@ use std::{ }; use common::{nat::NatTarget, ports::PortId}; -use oxnet::Ipv6Net; +use omicron_common::address::UNDERLAY_MULTICAST_SUBNET; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -21,18 +21,11 @@ use crate::link::LinkId; /// Type alias for multicast group IDs. pub type MulticastGroupId = u16; -/// Admin-local IPv6 multicast prefix (ff04::/16, scope 4). +/// A validated underlay multicast IPv6 address. /// -/// 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 -pub const ADMIN_LOCAL_PREFIX: u16 = 0xFF04; - -/// A validated admin-local IPv6 multicast address. -/// -/// Admin-local addresses are ff04::/16 (scope 4). -/// 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, @@ -47,19 +40,20 @@ pub const ADMIN_LOCAL_PREFIX: u16 = 0xFF04; 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-local. +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_local_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 { @@ -67,19 +61,19 @@ 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) } @@ -112,7 +106,7 @@ 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, @@ -156,7 +150,7 @@ pub struct MulticastGroupUpdateExternalEntry { /// 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, /// Tag for validating update/delete requests. Always present and generated @@ -242,6 +236,8 @@ pub enum Direction { #[derive(Clone, Debug, thiserror::Error)] pub enum Error { - #[error("Address {0} is not admin-local (must be ff04::/16)")] - InvalidIp(Ipv6Addr), + #[error( + "Address {0} is not in underlay multicast subnet (must be ff04::/64)" + )] + InvalidUnderlayMulticastIp(Ipv6Addr), } diff --git a/dpd/src/api_server.rs b/dpd/src/api_server.rs index fc811a16..94e0aa99 100644 --- a/dpd/src/api_server.rs +++ b/dpd/src/api_server.rs @@ -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; @@ -1958,7 +1959,7 @@ impl DpdApi for DpdApiImpl { async fn multicast_group_update_underlay_v3( rqctx: RequestContext>, - path: Path, + path: Path, group: TypedBody, ) -> Result< HttpResponseOk, @@ -1966,19 +1967,48 @@ impl DpdApi for DpdApiImpl { > { 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, admin_scoped) + mcast::get_group_internal(switch, underlay) .map_err(HttpError::from)? .tag } }; - mcast::modify_group_internal(switch, admin_scoped, &tag, entry.into()) + mcast::modify_group_internal(switch, underlay, &tag, entry.into()) + .map(|resp| HttpResponseOk(resp.into())) + .map_err(HttpError::from) + } + + async fn multicast_group_get_underlay_v3( + rqctx: RequestContext>, + path: Path, + ) -> 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}"), + ) + })?; + + mcast::get_group_internal(switch, underlay) .map(|resp| HttpResponseOk(resp.into())) .map_err(HttpError::from) } @@ -1988,9 +2018,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) } diff --git a/dpd/src/mcast/mod.rs b/dpd/src/mcast/mod.rs index 27f33dcd..42a6c9a9 100644 --- a/dpd/src/mcast/mod.rs +++ b/dpd/src/mcast/mod.rs @@ -68,12 +68,12 @@ 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, Ipv6Net}; @@ -188,7 +188,7 @@ impl MulticastGroup { fn to_underlay_response( &self, - group_ip: AdminScopedIpv6, + group_ip: UnderlayMulticastIpv6, ) -> MulticastGroupUnderlayResponse { MulticastGroupUnderlayResponse { group_ip, @@ -205,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), ), @@ -229,7 +229,7 @@ pub struct MulticastGroupData { free_group_ids: Arc>>, /// 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, + nat_target_refs: BTreeMap, } impl MulticastGroupData { @@ -277,14 +277,14 @@ impl MulticastGroupData { 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); } @@ -292,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) @@ -381,7 +381,7 @@ pub(crate) fn add_group_external( // 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 = AdminScopedIpv6::new(nat_target.internal_ip)?; + let admin_local_ip = UnderlayMulticastIpv6::new(nat_target.internal_ip)?; let tag = match &group_info.tag { Some(t) => { @@ -551,7 +551,7 @@ pub(crate) fn del_group( // 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) = AdminScopedIpv6::new(ipv6) + && let Ok(admin_scoped) = UnderlayMulticastIpv6::new(ipv6) && let Some(external_ip) = mcast.nat_target_refs.get(&admin_scoped) { return Err(DpdError::Invalid(format!( @@ -591,7 +591,7 @@ pub(crate) fn del_group( } 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(()) @@ -600,7 +600,7 @@ pub(crate) fn del_group( /// Get an internal multicast group configuration by admin-local IPv6 address. pub(crate) fn get_group_internal( s: &Switch, - admin_local: AdminScopedIpv6, + admin_local: UnderlayMulticastIpv6, ) -> DpdResult { let mcast = s.mcast.lock().expect("multicast data lock poisoned"); let group_ip = IpAddr::V6(admin_local.into()); @@ -690,8 +690,8 @@ pub(crate) fn modify_group_external( if old_internal_ip != new_internal_ip { // Validate both IPs before mutating state to avoid partial updates - let old_admin = AdminScopedIpv6::new(old_internal_ip)?; - let new_admin = AdminScopedIpv6::new(new_internal_ip)?; + 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); } @@ -750,8 +750,8 @@ pub(crate) fn modify_group_external( if old_internal_ip != new_internal_ip { // Restore original references (reverse of what we did above) if let (Ok(old_admin), Ok(new_admin)) = ( - AdminScopedIpv6::new(old_internal_ip), - AdminScopedIpv6::new(new_internal_ip), + UnderlayMulticastIpv6::new(old_internal_ip), + UnderlayMulticastIpv6::new(new_internal_ip), ) { mcast.rm_forwarding_refs(new_admin); mcast.add_forwarding_refs(group_ip, old_admin); @@ -774,7 +774,7 @@ 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 { @@ -1319,7 +1319,7 @@ fn add_ipv6_source_filters( fn validate_internal_group_creation( mcast: &MulticastGroupData, - group_ip: AdminScopedIpv6, + group_ip: UnderlayMulticastIpv6, ) -> DpdResult<()> { validate_group_exists(mcast, group_ip.into())?; Ok(()) diff --git a/dpd/src/mcast/validate.rs b/dpd/src/mcast/validate.rs index 52bf21f0..0eadfe26 100644 --- a/dpd/src/mcast/validate.rs +++ b/dpd/src/mcast/validate.rs @@ -288,7 +288,9 @@ pub(crate) fn validate_tag( mod tests { use super::*; use common::{nat::Vni, network::MacAddr}; - use dpd_types::mcast::ADMIN_LOCAL_PREFIX; + + /// Admin-local IPv6 multicast prefix (ff04::/16, scope 4). + const ADMIN_LOCAL_PREFIX: u16 = 0xff04; #[test] fn test_ipv4_validation() { diff --git a/openapi/dpd/dpd-3.0.0-70f546.json b/openapi/dpd/dpd-3.0.0-e0be17.json similarity index 99% rename from openapi/dpd/dpd-3.0.0-70f546.json rename to openapi/dpd/dpd-3.0.0-e0be17.json index caf801ea..1c8b8603 100644 --- a/openapi/dpd/dpd-3.0.0-70f546.json +++ b/openapi/dpd/dpd-3.0.0-e0be17.json @@ -1409,6 +1409,7 @@ "/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.", "operationId": "multicast_group_get_underlay_v3", "parameters": [ { @@ -1441,7 +1442,7 @@ }, "put": { "summary": "Update an underlay (internal) multicast group configuration (API v1-v3).", - "description": "Tags are optional for backward compatibility.", + "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": [ { @@ -5241,7 +5242,7 @@ "components": { "schemas": { "AdminScopedIpv6": { - "description": "A validated admin-local IPv6 multicast address.\n\nAdmin-local addresses are ff04::/16 (scope 4). These are used for internal/underlay multicast groups.", + "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" }, @@ -7739,7 +7740,7 @@ "type": "object", "properties": { "group_ip": { - "$ref": "#/components/schemas/AdminScopedIpv6" + "$ref": "#/components/schemas/UnderlayMulticastIpv6" }, "members": { "type": "array", @@ -9521,6 +9522,11 @@ "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", diff --git a/openapi/dpd/dpd-4.0.0-56f11d.json b/openapi/dpd/dpd-4.0.0-7b2800.json similarity index 99% rename from openapi/dpd/dpd-4.0.0-56f11d.json rename to openapi/dpd/dpd-4.0.0-7b2800.json index ac24ef25..f4270606 100644 --- a/openapi/dpd/dpd-4.0.0-56f11d.json +++ b/openapi/dpd/dpd-4.0.0-7b2800.json @@ -1439,7 +1439,7 @@ "name": "group_ip", "required": true, "schema": { - "$ref": "#/components/schemas/AdminScopedIpv6" + "$ref": "#/components/schemas/UnderlayMulticastIpv6" } } ], @@ -1472,7 +1472,7 @@ "name": "group_ip", "required": true, "schema": { - "$ref": "#/components/schemas/AdminScopedIpv6" + "$ref": "#/components/schemas/UnderlayMulticastIpv6" } }, { @@ -5273,11 +5273,6 @@ }, "components": { "schemas": { - "AdminScopedIpv6": { - "description": "A validated admin-local IPv6 multicast address.\n\nAdmin-local addresses are ff04::/16 (scope 4). These are used for internal/underlay multicast groups.", - "type": "string", - "format": "ipv6" - }, "AnLtStatus": { "description": "A collection of the data involved in the autonegiation/link-training process", "type": "object", @@ -7772,7 +7767,7 @@ "type": "object", "properties": { "group_ip": { - "$ref": "#/components/schemas/AdminScopedIpv6" + "$ref": "#/components/schemas/UnderlayMulticastIpv6" }, "members": { "type": "array", @@ -7863,7 +7858,7 @@ "minimum": 0 }, "group_ip": { - "$ref": "#/components/schemas/AdminScopedIpv6" + "$ref": "#/components/schemas/UnderlayMulticastIpv6" }, "kind": { "type": "string", @@ -7975,7 +7970,7 @@ "minimum": 0 }, "group_ip": { - "$ref": "#/components/schemas/AdminScopedIpv6" + "$ref": "#/components/schemas/UnderlayMulticastIpv6" }, "members": { "type": "array", @@ -9548,6 +9543,11 @@ "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", diff --git a/openapi/dpd/dpd-latest.json b/openapi/dpd/dpd-latest.json index 4554c6c1..872b94b0 120000 --- a/openapi/dpd/dpd-latest.json +++ b/openapi/dpd/dpd-latest.json @@ -1 +1 @@ -dpd-4.0.0-56f11d.json \ No newline at end of file +dpd-4.0.0-7b2800.json \ No newline at end of file From d99c56a217b2e0a9aec7f261945dbc347ee2ef26 Mon Sep 17 00:00:00 2001 From: Zeeshan Lakhani Date: Wed, 14 Jan 2026 06:48:44 -0500 Subject: [PATCH 15/22] [multicast] prevent VLAN translation via match key enforcement Fixes https://github.com/oxidecomputer/dendrite/issues/107, which now properly passes. This adds VLAN-aware NAT ingress matching to *actually* prevent cross VLAN translation. Previously, a packet arriving with VLAN 100 destined to a multicast group configured for VLAN 200 would be NAT encapsulated and forwarded, effectively translating the packet to the wrong customer's network. Includes: - NAT ingress table matching (mcast_nat.rs, mod.rs): - Add Ipv4VlanMatchKey and Ipv6VlanMatchKey that match on destination address, VLAN header validity, and VLAN ID - For groups with VLAN, install two entries: untagged (for decapsulated Geneve from underlay) and correctly tagged (for customer packets) - Packets with wrong VLAN miss both entries and are not NAT encapsulated - Multicast router VLAN handling (sidecar.p4): - Strip incoming VLAN tag before routing lookup in MulticastRouter4/6 - forward_vlan action re-adds the group's configured VLAN on egress - Prevents unintended VLAN translation at the routing stage - Rollback changes: - Remove dead NAT rollback branches for internal groups (no NAT entries) - Add rollback support for VLAN changes in NAT and route tables - Counter(s): - The underlay multicast counter condition was unreachable for packets tagged MULTICAST_TAG_UNDERLAY_EXTERNAL that weren't decapped. The check for `== MULTICAST_TAG_UNDERLAY` excluded these packets, causing them to fall through to the external counter. --- dpd-client/tests/integration_tests/mcast.rs | 402 ++++++++++++++- dpd/p4/sidecar.p4 | 56 ++- dpd/src/counters.rs | 7 +- dpd/src/mcast/mod.rs | 183 ++++--- dpd/src/mcast/rollback.rs | 122 +++-- dpd/src/mcast/validate.rs | 8 +- dpd/src/table/mcast/mcast_nat.rs | 513 +++++++++++++++++--- dpd/src/table/mcast/mcast_route.rs | 139 +++--- dpd/src/table/mcast/mod.rs | 82 ++++ 9 files changed, 1232 insertions(+), 280 deletions(-) diff --git a/dpd-client/tests/integration_tests/mcast.rs b/dpd-client/tests/integration_tests/mcast.rs index 70e6f587..8b2085f9 100644 --- a/dpd-client/tests/integration_tests/mcast.rs +++ b/dpd-client/tests/integration_tests/mcast.rs @@ -53,6 +53,30 @@ impl ToIpAddr for types::UnderlayMulticastIpv6 { } } +/// Count table entries matching a specific IP address in any key field. +fn count_entries_for_ip(entries: &[types::TableEntry], ip: &str) -> usize { + entries + .iter() + .filter(|e| e.keys.values().any(|v| v.contains(ip))) + .count() +} + +/// Check if any table entry for the given IP has a `forward_vlan` action with +/// the specified VLAN ID. +fn has_vlan_action_for_ip( + entries: &[types::TableEntry], + ip: &str, + vlan: u16, +) -> bool { + entries.iter().any(|e| { + e.keys.values().any(|v| v.contains(ip)) + && e.action_args + .get("vlan_id") + .map(|v| v == &vlan.to_string()) + .unwrap_or(false) + }) +} + async fn check_counter_incremented( switch: &Switch, counter_name: &str, @@ -542,6 +566,66 @@ async fn test_group_creation_with_validation() -> TestResult { _ => panic!("Expected ErrorResponse for invalid group ID"), } + // Test with reserved VLAN ID 0 (also invalid) + let external_vlan0 = types::MulticastGroupCreateExternalEntry { + group_ip: IpAddr::V4(MULTICAST_TEST_IPV4), + tag: Some(TEST_TAG.to_string()), + internal_forwarding: types::InternalForwarding { + nat_target: Some(nat_target.clone()), + }, + external_forwarding: types::ExternalForwarding { + vlan_id: Some(0), // Invalid: VLAN 0 is reserved + }, + sources: None, + }; + + let res = switch + .client + .multicast_group_create_external(&external_vlan0) + .await + .expect_err("Should fail with reserved VLAN ID 0"); + + match res { + Error::ErrorResponse(inner) => { + assert_eq!( + inner.status(), + 400, + "Expected 400 Bad Request status code for VLAN 0" + ); + } + _ => panic!("Expected ErrorResponse for reserved VLAN ID 0"), + } + + // Test with reserved VLAN ID 1 (also invalid) + let external_vlan1 = types::MulticastGroupCreateExternalEntry { + group_ip: IpAddr::V4(MULTICAST_TEST_IPV4), + tag: Some(TEST_TAG.to_string()), + internal_forwarding: types::InternalForwarding { + nat_target: Some(nat_target.clone()), + }, + external_forwarding: types::ExternalForwarding { + vlan_id: Some(1), // Invalid: VLAN 1 is reserved + }, + sources: None, + }; + + let res = switch + .client + .multicast_group_create_external(&external_vlan1) + .await + .expect_err("Should fail with reserved VLAN ID 1"); + + match res { + Error::ErrorResponse(inner) => { + assert_eq!( + inner.status(), + 400, + "Expected 400 Bad Request status code for VLAN 1" + ); + } + _ => panic!("Expected ErrorResponse for reserved VLAN ID 1"), + } + // Test with valid parameters // IPv4 groups are always external let external_valid = types::MulticastGroupCreateExternalEntry { @@ -712,8 +796,52 @@ async fn test_vlan_propagation_to_internal() -> TestResult { "ff04::200".parse::().unwrap() ); - // Verify the admin-scoped group now has access to the VLAN via NAT target reference - // Check the bitmap table to see if VLAN 42 is properly set (this is where VLAN matters for P4) + // Verify IPv4 route table has ONE entry for the VLAN group. + // Route tables use simple dst_addr matching with forward_vlan(vid) action. + // VLAN isolation (preventing translation) is handled by NAT ingress tables. + let route_table = switch + .client + .table_dump("pipe.Ingress.l3_router.MulticastRouter4.tbl") + .await + .expect("Should dump IPv4 route table") + .into_inner(); + let group_entries: Vec<_> = route_table + .entries + .iter() + .filter(|e| e.keys.values().any(|v| v.contains("224.1.2.3"))) + .collect(); + assert_eq!( + group_entries.len(), + 1, + "Route table uses dst_addr only - 1 entry per group" + ); + // Verify the action is forward_vlan with VLAN 42 + assert!( + group_entries[0] + .action_args + .get("vlan_id") + .map(|v| v == "42") + .unwrap_or(false), + "Route entry should have forward_vlan(42) action" + ); + + // Verify NAT ingress table has TWO entries for VLAN isolation: + // 1. Untagged match -> forward (for decapsulated Geneve) + // 2. Tagged match with VLAN 42 -> forward (for already-tagged) + let nat_table = switch + .client + .table_dump("pipe.Ingress.nat_ingress.ingress_ipv4_mcast") + .await + .expect("Should dump NAT ingress table") + .into_inner(); + assert_eq!( + count_entries_for_ip(&nat_table.entries, "224.1.2.3"), + 2, + "NAT table should have 2 entries for VLAN isolation (untagged + tagged)" + ); + + // Verify the admin-scoped group now has access to the VLAN via NAT target + // reference let bitmap_table = switch .client .table_dump("pipe.Egress.mcast_egress.tbl_decap_ports") @@ -721,7 +849,8 @@ async fn test_vlan_propagation_to_internal() -> TestResult { .expect("Should clean up internal group") .into_inner(); - // Verify the admin-scoped group's bitmap entry has VLAN 42 from external group propagation + // Verify the admin-scoped group's bitmap entry has VLAN 42 from external + // group propagation assert!( bitmap_table .entries @@ -730,7 +859,8 @@ async fn test_vlan_propagation_to_internal() -> TestResult { "Admin-scoped group bitmap should have VLAN 42 from external group" ); - // Delete external group first since it references the internal group via NAT target + // 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(); @@ -789,6 +919,42 @@ async fn test_group_api_lifecycle() { ); assert_eq!(created.external_forwarding.vlan_id, Some(vlan_id)); + // Route table: 1 entry (dst_addr only, VLAN via action) + let route_table = switch + .client + .table_dump("pipe.Ingress.l3_router.MulticastRouter4.tbl") + .await + .expect("Should dump route table") + .into_inner(); + let route_entries: Vec<_> = route_table + .entries + .iter() + .filter(|e| e.keys.values().any(|v| v.contains("224.0.1.0"))) + .collect(); + assert_eq!( + route_entries.len(), + 1, + "Route table should have 1 entry per group (dst_addr only)" + ); + + // NAT table: 2 entries for VLAN groups (untagged + tagged match keys) + let nat_table = switch + .client + .table_dump("pipe.Ingress.nat_ingress.ingress_ipv4_mcast") + .await + .expect("Should dump NAT table") + .into_inner(); + let nat_entries: Vec<_> = nat_table + .entries + .iter() + .filter(|e| e.keys.values().any(|v| v.contains("224.0.1.0"))) + .collect(); + assert_eq!( + nat_entries.len(), + 2, + "NAT table should have 2 entries for VLAN group (untagged + tagged)" + ); + // Get all groups and verify our group is included let groups = switch .client @@ -3890,10 +4056,6 @@ async fn test_multicast_reset_all_tables() -> TestResult { Ok(()) } -/* - * Commented out untl https://github.com/oxidecomputer/dendrite/issues/107 is - * fixed - * #[tokio::test] #[ignore] async fn test_multicast_vlan_translation_not_possible() -> TestResult { @@ -3970,7 +4132,6 @@ async fn test_multicast_vlan_translation_not_possible() -> TestResult { .unwrap(); cleanup_test_group(switch, internal_multicast_ip, TEST_TAG).await } -*/ #[tokio::test] #[ignore] @@ -7048,6 +7209,229 @@ async fn test_underlay_delete_recreate_recovery_flow() -> TestResult { Ok(()) } +/// Tests the full VLAN lifecycle for multicast groups, verifying route table +/// entries are correctly managed through all VLAN transitions. +/// +/// Route tables use dst_addr only matching with action-based VLAN tagging: +/// - 1 entry per group: forward (no VLAN) or forward_vlan(vid) +/// +/// NAT tables use VLAN-aware matching for isolation: +/// - 2 entries for VLAN groups: untagged + tagged match keys +#[tokio::test] +#[ignore] +async fn test_vlan_lifecycle_route_entries() -> TestResult { + let switch = &*get_switch().await; + let tag = "vlan_lifecycle_test"; + + // Setup: create internal admin-scoped group for NAT target + let internal_ip = IpAddr::V6(MULTICAST_NAT_IP); + let egress_port = PhysPort(28); + create_test_multicast_group( + switch, + internal_ip, + Some(tag), + &[(egress_port, types::Direction::Underlay)], + types::InternalForwarding { nat_target: None }, + types::ExternalForwarding { vlan_id: None }, + None, + ) + .await; + + let group_ip = IpAddr::V4(MULTICAST_TEST_IPV4); + let nat_target = create_nat_target_ipv4(); + let test_ip = "224.0.1.0"; + + // Case: Create with NO VLAN - should have 1 route entry + let create_no_vlan = types::MulticastGroupCreateExternalEntry { + group_ip, + tag: Some(tag.to_string()), + internal_forwarding: types::InternalForwarding { + nat_target: Some(nat_target.clone()), + }, + external_forwarding: types::ExternalForwarding { vlan_id: None }, + sources: None, + }; + + switch + .client + .multicast_group_create_external(&create_no_vlan) + .await + .expect("Should create group without VLAN"); + + let table = switch + .client + .table_dump("pipe.Ingress.l3_router.MulticastRouter4.tbl") + .await? + .into_inner(); + assert_eq!( + count_entries_for_ip(&table.entries, test_ip), + 1, + "Group without VLAN should have 1 route entry" + ); + + // Case: Update to ADD VLAN 10 + // Route: 1 entry with forward_vlan(10) action + // NAT: 2 entries (untagged + tagged) for VLAN isolation + let update_add_vlan = types::MulticastGroupUpdateExternalEntry { + internal_forwarding: types::InternalForwarding { + nat_target: Some(nat_target.clone()), + }, + external_forwarding: types::ExternalForwarding { vlan_id: Some(10) }, + sources: None, + }; + + let updated = switch + .client + .multicast_group_update_external( + &group_ip, + &make_tag(tag), + &update_add_vlan, + ) + .await + .expect("Should update group to add VLAN"); + assert_eq!(updated.into_inner().external_forwarding.vlan_id, Some(10)); + + let table = switch + .client + .table_dump("pipe.Ingress.l3_router.MulticastRouter4.tbl") + .await? + .into_inner(); + assert_eq!( + count_entries_for_ip(&table.entries, test_ip), + 1, + "Route table uses dst_addr only - 1 entry per group" + ); + assert!( + has_vlan_action_for_ip(&table.entries, test_ip, 10), + "Route entry should have forward_vlan(10) action" + ); + + // Verify NAT table has 2 entries for VLAN isolation + let nat_table = switch + .client + .table_dump("pipe.Ingress.nat_ingress.ingress_ipv4_mcast") + .await? + .into_inner(); + assert_eq!( + count_entries_for_ip(&nat_table.entries, test_ip), + 2, + "NAT table should have 2 entries for VLAN group (untagged + tagged)" + ); + + // Case: Update to CHANGE VLAN 10 -> 20 + // Route: same 1 entry, action changes to forward_vlan(20) + // NAT: 2 entries with new VLAN, no stale entries + let update_change_vlan = types::MulticastGroupUpdateExternalEntry { + internal_forwarding: types::InternalForwarding { + nat_target: Some(nat_target.clone()), + }, + external_forwarding: types::ExternalForwarding { vlan_id: Some(20) }, + sources: None, + }; + + let updated = switch + .client + .multicast_group_update_external( + &group_ip, + &make_tag(tag), + &update_change_vlan, + ) + .await + .expect("Should update group to change VLAN"); + assert_eq!(updated.into_inner().external_forwarding.vlan_id, Some(20)); + + let table = switch + .client + .table_dump("pipe.Ingress.l3_router.MulticastRouter4.tbl") + .await? + .into_inner(); + assert_eq!( + count_entries_for_ip(&table.entries, test_ip), + 1, + "Route table uses dst_addr only - 1 entry per group" + ); + assert!( + has_vlan_action_for_ip(&table.entries, test_ip, 20), + "Route entry should have forward_vlan(20) action" + ); + assert!( + !has_vlan_action_for_ip(&table.entries, test_ip, 10), + "Should NOT have stale forward_vlan(10) action" + ); + + // NAT table should still have 2 entries + let nat_table = switch + .client + .table_dump("pipe.Ingress.nat_ingress.ingress_ipv4_mcast") + .await? + .into_inner(); + assert_eq!( + count_entries_for_ip(&nat_table.entries, test_ip), + 2, + "NAT table should have 2 entries for VLAN group" + ); + + // Case: Update to REMOVE VLAN + // Route: 1 entry with forward action (no VLAN) + // NAT: 1 entry (untagged only) + let update_remove_vlan = types::MulticastGroupUpdateExternalEntry { + internal_forwarding: types::InternalForwarding { + nat_target: Some(nat_target.clone()), + }, + external_forwarding: types::ExternalForwarding { vlan_id: None }, + sources: None, + }; + + let updated = switch + .client + .multicast_group_update_external( + &group_ip, + &make_tag(tag), + &update_remove_vlan, + ) + .await + .expect("Should update group to remove VLAN"); + assert_eq!(updated.into_inner().external_forwarding.vlan_id, None); + + let table = switch + .client + .table_dump("pipe.Ingress.l3_router.MulticastRouter4.tbl") + .await? + .into_inner(); + + assert_eq!( + count_entries_for_ip(&table.entries, test_ip), + 1, + "Route table uses dst_addr only - 1 entry per group" + ); + + assert!( + !has_vlan_action_for_ip(&table.entries, test_ip, 20), + "Should NOT have forward_vlan action after VLAN removal" + ); + + // NAT table should have 1 entry now (no VLAN = untagged only) + let nat_table = switch + .client + .table_dump("pipe.Ingress.nat_ingress.ingress_ipv4_mcast") + .await? + .into_inner(); + assert_eq!( + count_entries_for_ip(&nat_table.entries, test_ip), + 1, + "NAT table should have 1 entry after VLAN removal" + ); + + // 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 diff --git a/dpd/p4/sidecar.p4 b/dpd/p4/sidecar.p4 index c2de0edf..55c691e7 100644 --- a/dpd/p4/sidecar.p4 +++ b/dpd/p4/sidecar.p4 @@ -560,8 +560,16 @@ control NatIngress ( } // Separate table for IPv4 multicast packets that need to be encapsulated. + // Groups without VLAN match untagged only. Groups with VLAN match both + // untagged (for decapsulated Geneve from underlay) and correctly tagged. + // Packets with wrong VLAN miss and are not NAT encapsulated. + // When hdr.vlan.isValid()==false, vlan_id matches as 0. table ingress_ipv4_mcast { - key = { hdr.ipv4.dst_addr : exact; } + key = { + hdr.ipv4.dst_addr : exact; + hdr.vlan.isValid() : exact; + hdr.vlan.vlan_id : exact; + } actions = { mcast_forward_ipv4_to; } const size = IPV4_MULTICAST_TABLE_SIZE; counters = mcast_ipv4_ingress_ctr; @@ -577,8 +585,16 @@ control NatIngress ( } // Separate table for IPv6 multicast packets that need to be encapsulated. + // Groups without VLAN match untagged only. Groups with VLAN match both + // untagged (for decapsulated Geneve from underlay) and correctly tagged. + // Packets with wrong VLAN miss and are not NAT encapsulated. + // When hdr.vlan.isValid()==false, vlan_id matches as 0. table ingress_ipv6_mcast { - key = { hdr.ipv6.dst_addr : exact; } + key = { + hdr.ipv6.dst_addr : exact; + hdr.vlan.isValid() : exact; + hdr.vlan.vlan_id : exact; + } actions = { mcast_forward_ipv6_to; } const size = IPV6_MULTICAST_TABLE_SIZE; counters = mcast_ipv6_ingress_ctr; @@ -1220,9 +1236,7 @@ control MulticastRouter4( } table tbl { - key = { - hdr.ipv4.dst_addr : exact; - } + key = { hdr.ipv4.dst_addr : exact; } actions = { forward; forward_vlan; unreachable; } default_action = unreachable; const size = IPV4_MULTICAST_TABLE_SIZE; @@ -1231,16 +1245,18 @@ control MulticastRouter4( apply { // If the packet came in with a VLAN tag, we need to invalidate - // the VLAN header before we do the lookup. The VLAN header - // will be re-attached if set in the forward_vlan action. + // the VLAN header before we do the lookup. The VLAN header + // will be re-attached if set in the forward_vlan action (or + // untagged for groups without VLAN). This prevents unintended + // VLAN translation. if (hdr.vlan.isValid()) { hdr.ethernet.ether_type = hdr.vlan.ether_type; hdr.vlan.setInvalid(); } if (!tbl.apply().hit) { - icmp_error(ICMP6_DST_UNREACH, ICMP6_DST_UNREACH_NOROUTE); - meta.drop_reason = DROP_IPV6_UNROUTEABLE; + icmp_error(ICMP_DEST_UNREACH, ICMP_DST_UNREACH_NET); + meta.drop_reason = DROP_IPV4_UNROUTEABLE; // Dont set meta.dropped because we want an error packet // to go out. } else if (hdr.ipv4.ttl == 1 && !meta.service_routed) { @@ -1358,9 +1374,7 @@ control MulticastRouter6 ( } table tbl { - key = { - hdr.ipv6.dst_addr : exact; - } + key = { hdr.ipv6.dst_addr : exact; } actions = { forward; forward_vlan; unreachable; } default_action = unreachable; const size = IPV6_MULTICAST_TABLE_SIZE; @@ -1369,8 +1383,10 @@ control MulticastRouter6 ( apply { // If the packet came in with a VLAN tag, we need to invalidate - // the VLAN header before we do the lookup. The VLAN header - // will be re-attached if set in the forward_vlan action. + // the VLAN header before we do the lookup. The VLAN header + // will be re-attached if set in the forward_vlan action (or + // untagged for groups without VLAN). This prevents unintended + // VLAN translation. if (hdr.vlan.isValid()) { hdr.ethernet.ether_type = hdr.vlan.ether_type; hdr.vlan.setInvalid(); @@ -2193,12 +2209,18 @@ control Egress( if (is_link_local_ipv6_mcast) { link_local_mcast_ctr.count(eg_intr_md.egress_port); - } else if (hdr.geneve.isValid()) { - external_mcast_ctr.count(eg_intr_md.egress_port); } else if (hdr.geneve.isValid() && hdr.geneve_opts.oxg_mcast.isValid() && - hdr.geneve_opts.oxg_mcast.mcast_tag == MULTICAST_TAG_UNDERLAY) { + hdr.geneve_opts.oxg_mcast.mcast_tag != MULTICAST_TAG_EXTERNAL) { + // Encapsulated multicast going to sleds. Includes both + // MULTICAST_TAG_UNDERLAY and MULTICAST_TAG_UNDERLAY_EXTERNAL + // packets that were not decapped for this egress port. underlay_mcast_ctr.count(eg_intr_md.egress_port); + } else { + // Decapped external multicast going to front panel ports. + // Either originally tagged EXTERNAL, or UNDERLAY_EXTERNAL + // packets that were decapped based on port bitmap. + external_mcast_ctr.count(eg_intr_md.egress_port); } } else { // non-multicast packets should bypass the egress diff --git a/dpd/src/counters.rs b/dpd/src/counters.rs index dfad527f..eb61c25c 100644 --- a/dpd/src/counters.rs +++ b/dpd/src/counters.rs @@ -284,6 +284,7 @@ enum DropReason { GeneveOptionsTooLong, GeneveOptionMalformed, GeneveOptionUnknown, + VlanMismatch, } impl TryFrom for DropReason { @@ -317,6 +318,7 @@ impl TryFrom for DropReason { 23 => Ok(DropReason::GeneveOptionsTooLong), 24 => Ok(DropReason::GeneveOptionMalformed), 25 => Ok(DropReason::GeneveOptionUnknown), + 26 => Ok(DropReason::VlanMismatch), x => Err(format!("Unrecognized drop reason: {x}")), } } @@ -343,8 +345,8 @@ fn reason_label(ctr: u8) -> Result, String> { DropReason::Ipv4TtlExceeded => "ipv4_ttl_exceeded".to_string(), DropReason::Ipv6TtlInvalid => "ipv6_ttl_invalid".to_string(), DropReason::Ipv6TtlExceeded => "ipv6_ttl_exceeded".to_string(), - DropReason::Ipv4Unrouteable => "ipv6_unrouteable".to_string(), - DropReason::Ipv6Unrouteable => "ipv4_unrouteable".to_string(), + DropReason::Ipv4Unrouteable => "ipv4_unrouteable".to_string(), + DropReason::Ipv6Unrouteable => "ipv6_unrouteable".to_string(), DropReason::NatIngressMiss => "nat_ingress_miss".to_string(), DropReason::MulticastNoGroup => "multicast_no_group".to_string(), DropReason::MulticastInvalidMac => "multicast_invalid_mac".to_string(), @@ -362,6 +364,7 @@ fn reason_label(ctr: u8) -> Result, String> { "geneve_option_malformed".to_string() } DropReason::GeneveOptionUnknown => "geneve_option_unknown".to_string(), + DropReason::VlanMismatch => "vlan_mismatch".to_string(), }; Ok(Some(label)) } diff --git a/dpd/src/mcast/mod.rs b/dpd/src/mcast/mod.rs index 42a6c9a9..18f2d63d 100644 --- a/dpd/src/mcast/mod.rs +++ b/dpd/src/mcast/mod.rs @@ -354,6 +354,7 @@ pub(crate) fn add_group_external( scoped_underlay_id.id(), nat_target, group_info.sources.as_deref(), + group_info.external_forwarding.vlan_id, ); // Configure external tables and handle VLAN propagation @@ -664,8 +665,12 @@ pub(crate) fn modify_group_external( // Create rollback context for external group update let group_entry_for_rollback = group_entry.clone(); - let rollback_ctx = - GroupUpdateRollbackContext::new(s, group_ip, &group_entry_for_rollback); + let rollback_ctx = GroupUpdateRollbackContext::new( + s, + group_ip, + &group_entry_for_rollback, + new_group_info.external_forwarding.vlan_id, + ); // Pre-compute normalized sources for rollback purposes let normalized_sources = normalize_sources(new_group_info.sources.clone()); @@ -701,10 +706,9 @@ pub(crate) fn modify_group_external( updated_group.int_fwding.nat_target = Some(nat_target); let old_vlan_id = updated_group.ext_fwding.vlan_id; - updated_group.ext_fwding.vlan_id = new_group_info - .external_forwarding - .vlan_id - .or(updated_group.ext_fwding.vlan_id); + // VLAN is assigned directly -> Some(x) sets VLAN, None removes VLAN + updated_group.ext_fwding.vlan_id = + new_group_info.external_forwarding.vlan_id; updated_group.sources = normalize_sources( new_group_info.sources.clone().or(updated_group.sources), ); @@ -797,9 +801,11 @@ pub(crate) fn modify_group_internal( s, group_ip.into(), &group_entry_for_rollback, + // Internal groups don't have VLANs, so `attempted_vlan_id` is None. + None, ); - // Internal groups don't update sources - always `None` + // Internal groups don't update sources, so always `None` debug_assert!( group_entry.sources.is_none(), "Internal groups should not have sources" @@ -1190,7 +1196,7 @@ pub(super) fn sources_contain_any(sources: &[IpSrc]) -> bool { /// 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) + format!("{}:{group_ip}", Uuid::new_v4()) } /// Multiple representations map to "allow any source" in P4: @@ -1365,12 +1371,17 @@ fn configure_external_tables( add_source_filters(s, group_ip, group_info.sources.as_deref())?; // Add NAT entry + let vlan_id = group_info.external_forwarding.vlan_id; match group_ip { IpAddr::V4(ipv4) => { - table::mcast::mcast_nat::add_ipv4_entry(s, ipv4, nat_target)?; + table::mcast::mcast_nat::add_ipv4_entry( + s, ipv4, nat_target, vlan_id, + )?; } IpAddr::V6(ipv6) => { - table::mcast::mcast_nat::add_ipv6_entry(s, ipv6, nat_target)?; + table::mcast::mcast_nat::add_ipv6_entry( + s, ipv6, nat_target, vlan_id, + )?; } } @@ -1700,46 +1711,48 @@ fn update_external_tables( add_source_filters(s, group_ip, new_sources.as_deref())?; } + let old_vlan_id = group_entry.ext_fwding.vlan_id; + let new_vlan_id = new_group_info.external_forwarding.vlan_id; + // Update NAT target - external groups always have NAT targets - if Some( - new_group_info - .internal_forwarding - .nat_target - .ok_or_else(|| { - DpdError::Invalid( - "external groups must have NAT target".to_string(), - ) - })?, - ) != group_entry.int_fwding.nat_target + // Also handles VLAN changes since VLAN is part of the NAT match key + let new_nat_target = new_group_info + .internal_forwarding + .nat_target + .ok_or_else(|| { + DpdError::Invalid( + "external groups must have NAT target".to_string(), + ) + })?; + + if Some(new_nat_target) != group_entry.int_fwding.nat_target + || old_vlan_id != new_vlan_id { update_nat_tables( s, group_ip, - Some(new_group_info.internal_forwarding.nat_target.ok_or_else( - || { - DpdError::Invalid( - "external groups must have NAT target".to_string(), - ) - }, - )?), + Some(new_nat_target), group_entry.int_fwding.nat_target, + old_vlan_id, + new_vlan_id, )?; } - // Update VLAN if it changed - if new_group_info.external_forwarding.vlan_id - != group_entry.ext_fwding.vlan_id - { + // Update route table VLAN if it changed + // Route tables use simple dst_addr matching but select forward vs forward_vlan action + if old_vlan_id != new_vlan_id { match group_ip { IpAddr::V4(ipv4) => table::mcast::mcast_route::update_ipv4_entry( s, ipv4, - new_group_info.external_forwarding.vlan_id, + old_vlan_id, + new_vlan_id, ), IpAddr::V6(ipv6) => table::mcast::mcast_route::update_ipv6_entry( s, ipv6, - new_group_info.external_forwarding.vlan_id, + old_vlan_id, + new_vlan_id, ), }?; } @@ -1830,7 +1843,11 @@ fn delete_group_tables( // (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)?; + table::mcast::mcast_nat::del_ipv4_entry( + s, + ipv4, + group.ext_fwding.vlan_id, + )?; } delete_group_bitmap_entries(s, group)?; @@ -1844,7 +1861,11 @@ fn delete_group_tables( // 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)?; + table::mcast::mcast_nat::del_ipv6_entry( + s, + ipv6, + group.ext_fwding.vlan_id, + )?; } delete_group_bitmap_entries(s, group)?; @@ -1882,31 +1903,47 @@ fn update_nat_tables( group_ip: IpAddr, new_nat_target: Option, old_nat_target: Option, + old_vlan_id: Option, + new_vlan_id: Option, ) -> DpdResult<()> { match (group_ip, new_nat_target, old_nat_target) { - (IpAddr::V4(ipv4), Some(nat), Some(_)) => { - // NAT to NAT - update existing entry - table::mcast::mcast_nat::update_ipv4_entry(s, ipv4, nat) + (IpAddr::V4(ipv4), Some(new_nat), Some(old_nat)) => { + // NAT to NAT - update existing entry (handles VLAN changes internally) + table::mcast::mcast_nat::update_ipv4_entry( + s, + ipv4, + new_nat, + old_nat, + old_vlan_id, + new_vlan_id, + ) } - (IpAddr::V6(ipv6), Some(nat), Some(_)) => { - // NAT to NAT - update existing entry - table::mcast::mcast_nat::update_ipv6_entry(s, ipv6, nat) + (IpAddr::V6(ipv6), Some(new_nat), Some(old_nat)) => { + // NAT to NAT - update existing entry (handles VLAN changes internally) + table::mcast::mcast_nat::update_ipv6_entry( + s, + ipv6, + new_nat, + old_nat, + old_vlan_id, + new_vlan_id, + ) } (IpAddr::V4(ipv4), Some(nat), None) => { // No NAT to NAT - add new entry - table::mcast::mcast_nat::add_ipv4_entry(s, ipv4, nat) + table::mcast::mcast_nat::add_ipv4_entry(s, ipv4, nat, new_vlan_id) } (IpAddr::V6(ipv6), Some(nat), None) => { // No NAT to NAT - add new entry - table::mcast::mcast_nat::add_ipv6_entry(s, ipv6, nat) + table::mcast::mcast_nat::add_ipv6_entry(s, ipv6, nat, new_vlan_id) } (IpAddr::V4(ipv4), None, Some(_)) => { // NAT to no NAT - delete entry - table::mcast::mcast_nat::del_ipv4_entry(s, ipv4) + table::mcast::mcast_nat::del_ipv4_entry(s, ipv4, old_vlan_id) } (IpAddr::V6(ipv6), None, Some(_)) => { // NAT to no NAT - delete entry - table::mcast::mcast_nat::del_ipv6_entry(s, ipv6) + table::mcast::mcast_nat::del_ipv6_entry(s, ipv6, old_vlan_id) } _ => Ok(()), // No change (None → None) } @@ -1953,33 +1990,49 @@ fn update_internal_group_bitmap_tables( /// 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. +/// +/// # Arguments +/// +/// * `s` - Switch instance for table operations. +/// * `group_ip` - IP address of the multicast group. +/// * `external_group_id` - ID of the external multicast group for bitmap updates. +/// * `members` - Group members used to recreate port bitmap. +/// * `current_vlan_id` - VLAN currently in the table (may be the attempted new VLAN). +/// * `target_vlan_id` - VLAN to restore to (the original VLAN). fn update_fwding_tables( s: &Switch, group_ip: IpAddr, external_group_id: MulticastGroupId, members: &[MulticastGroupMember], - vlan_id: Option, + current_vlan_id: Option, + target_vlan_id: Option, ) -> DpdResult<()> { match group_ip { - IpAddr::V4(ipv4) => { - table::mcast::mcast_route::update_ipv4_entry(s, ipv4, vlan_id) - } - IpAddr::V6(ipv6) => { - 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( - s, - external_group_id, - &external_port_bitmap, - vlan_id, - ) - }) - } + IpAddr::V4(ipv4) => table::mcast::mcast_route::update_ipv4_entry( + s, + ipv4, + current_vlan_id, + target_vlan_id, + ), + IpAddr::V6(ipv6) => table::mcast::mcast_route::update_ipv6_entry( + s, + ipv6, + current_vlan_id, + target_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( + s, + external_group_id, + &external_port_bitmap, + target_vlan_id, + ) + }), } } diff --git a/dpd/src/mcast/rollback.rs b/dpd/src/mcast/rollback.rs index 65fc2184..c27189d9 100644 --- a/dpd/src/mcast/rollback.rs +++ b/dpd/src/mcast/rollback.rs @@ -57,12 +57,12 @@ trait RollbackOps { ) -> DpdResult<()> { self.log_rollback_error( "remove new source filters", - &format!("for group {}", self.group_ip()), + &format!("for group {group_ip}", group_ip = self.group_ip()), remove_source_filters(self.switch(), self.group_ip(), new_sources), ); self.log_rollback_error( "restore original source filters", - &format!("for group {}", self.group_ip()), + &format!("for group {group_ip}", group_ip = self.group_ip()), add_source_filters(self.switch(), self.group_ip(), orig_sources), ); Ok(()) @@ -128,6 +128,7 @@ pub(crate) struct GroupCreateRollbackContext<'a> { underlay_id: MulticastGroupId, nat_target: Option, sources: Option<&'a [IpSrc]>, + vlan_id: Option, } impl RollbackOps for GroupCreateRollbackContext<'_> { @@ -253,6 +254,7 @@ impl<'a> GroupCreateRollbackContext<'a> { underlay_id: MulticastGroupId, nat_target: NatTarget, sources: Option<&'a [IpSrc]>, + vlan_id: Option, ) -> Self { Self { switch, @@ -261,6 +263,7 @@ impl<'a> GroupCreateRollbackContext<'a> { underlay_id, nat_target: Some(nat_target), sources, + vlan_id, } } @@ -278,6 +281,7 @@ impl<'a> GroupCreateRollbackContext<'a> { underlay_id, nat_target: None, sources: None, + vlan_id: None, } } @@ -343,16 +347,18 @@ impl<'a> GroupCreateRollbackContext<'a> { self.log_rollback_error( "remove external multicast group", &format!( - "for IP {} with ID {}", - self.group_ip, self.external_id + "for IP {group_ip} with ID {external_id}", + group_ip = self.group_ip, + external_id = self.external_id ), self.switch.asic_hdl.mc_group_destroy(self.external_id), ); self.log_rollback_error( "remove underlay multicast group", &format!( - "for IP {} with ID {}", - self.group_ip, self.underlay_id + "for IP {group_ip} with ID {underlay_id}", + group_ip = self.group_ip, + underlay_id = self.underlay_id ), self.switch.asic_hdl.mc_group_destroy(self.underlay_id), ); @@ -417,6 +423,7 @@ impl<'a> GroupCreateRollbackContext<'a> { table::mcast::mcast_nat::del_ipv4_entry( self.switch, ipv4, + self.vlan_id, ), ); } @@ -434,7 +441,10 @@ impl<'a> GroupCreateRollbackContext<'a> { // (bitmap entries are only created for internal groups with both group types) self.log_rollback_error( "delete IPv6 egress bitmap entry", - &format!("for external group {}", self.external_id), + &format!( + "for external group {external_id}", + external_id = self.external_id + ), table::mcast::mcast_egress::del_bitmap_entry( self.switch, self.external_id, @@ -507,6 +517,7 @@ impl<'a> GroupCreateRollbackContext<'a> { table::mcast::mcast_nat::del_ipv6_entry( self.switch, ipv6, + self.vlan_id, ), ); } @@ -529,6 +540,10 @@ pub(crate) struct GroupUpdateRollbackContext<'a> { switch: &'a Switch, group_ip: IpAddr, original_group: &'a MulticastGroup, + /// The VLAN that the update is attempting to set. This may differ from + /// `original_group.ext_fwding.vlan_id` when a VLAN change is in progress. + /// Used during rollback to delete entries that may have been added. + attempted_vlan_id: Option, } impl RollbackOps for GroupUpdateRollbackContext<'_> { @@ -559,7 +574,7 @@ impl RollbackOps for GroupUpdateRollbackContext<'_> { return Ok(()); } - // Internal group - perform actual port rollback + // Internal group -> perform actual port rollback debug!( self.switch.log, "rolling back multicast group update"; @@ -707,15 +722,25 @@ impl RollbackOps for GroupUpdateRollbackContext<'_> { impl<'a> GroupUpdateRollbackContext<'a> { /// Create rollback context for group update operations. + /// + /// # Arguments + /// + /// * `switch` - Switch instance for table operations. + /// * `group_ip` - IP address of the multicast group being updated. + /// * `original_group` - Reference to the group's state before the update. + /// * `attempted_vlan_id` - The VLAN that the update is attempting to set. + /// Used during rollback to delete entries that may have been added. pub(crate) fn new( switch: &'a Switch, group_ip: IpAddr, original_group: &'a MulticastGroup, + attempted_vlan_id: Option, ) -> Self { Self { switch, group_ip, original_group, + attempted_vlan_id, } } @@ -744,7 +769,7 @@ impl<'a> GroupUpdateRollbackContext<'a> { if let Some(replication_info) = replication_info { self.log_rollback_error( "restore replication settings", - &format!("for group {}", self.group_ip), + &format!("for group {group_ip}", group_ip = self.group_ip), update_replication_tables( self.switch, self.group_ip, @@ -755,54 +780,57 @@ impl<'a> GroupUpdateRollbackContext<'a> { ); } - // Restore NAT settings - match (self.group_ip, nat_target) { - (IpAddr::V4(ipv4), Some(nat)) => { - self.log_rollback_error( - "restore IPv4 NAT settings", - &format!("for group {}", self.group_ip), - table::mcast::mcast_nat::update_ipv4_entry( - self.switch, - ipv4, - nat, - ), - ); - } - (IpAddr::V6(ipv6), Some(nat)) => { - self.log_rollback_error( - "restore IPv6 NAT settings", - &format!("for group {}", self.group_ip), - table::mcast::mcast_nat::update_ipv6_entry( - self.switch, - ipv6, - nat, - ), - ); - } - (IpAddr::V4(ipv4), None) => { - self.log_rollback_error( - "remove IPv4 NAT settings", - &format!("for group {}", self.group_ip), - table::mcast::mcast_nat::del_ipv4_entry(self.switch, ipv4), - ); - } - (IpAddr::V6(ipv6), None) => { - self.log_rollback_error( - "remove IPv6 NAT settings", - &format!("for group {}", self.group_ip), - table::mcast::mcast_nat::del_ipv6_entry(self.switch, ipv6), - ); + // Restore NAT settings for external groups (internal groups have no + // NAT entries). Both new_tgt and old_tgt are the same original NAT + // target since we're restoring to the original state. + if let Some(nat) = nat_target { + match self.group_ip { + IpAddr::V4(ipv4) => { + self.log_rollback_error( + "restore IPv4 NAT settings", + &format!( + "for group {group_ip}", + group_ip = self.group_ip + ), + table::mcast::mcast_nat::update_ipv4_entry( + self.switch, + ipv4, + nat, + nat, + self.attempted_vlan_id, + vlan_id, + ), + ); + } + IpAddr::V6(ipv6) => { + self.log_rollback_error( + "restore IPv6 NAT settings", + &format!( + "for group {group_ip}", + group_ip = self.group_ip + ), + table::mcast::mcast_nat::update_ipv6_entry( + self.switch, + ipv6, + nat, + nat, + self.attempted_vlan_id, + vlan_id, + ), + ); + } } } self.log_rollback_error( "restore VLAN settings", - &format!("for group {}", self.group_ip), + &format!("for group {group_ip}", group_ip = self.group_ip), update_fwding_tables( self.switch, self.group_ip, external_group_id, &prev_members, + self.attempted_vlan_id, vlan_id, ), ); diff --git a/dpd/src/mcast/validate.rs b/dpd/src/mcast/validate.rs index 0eadfe26..57956431 100644 --- a/dpd/src/mcast/validate.rs +++ b/dpd/src/mcast/validate.rs @@ -51,16 +51,16 @@ pub(crate) fn validate_multicast_address( pub(crate) fn validate_nat_target(nat_target: NatTarget) -> DpdResult<()> { if !nat_target.inner_mac.is_multicast() { return Err(DpdError::Invalid(format!( - "NAT target inner MAC address {} is not a multicast MAC address", - nat_target.inner_mac + "NAT target inner MAC address {inner_mac} is not a multicast MAC address", + inner_mac = nat_target.inner_mac ))); } if !UNDERLAY_MULTICAST_SUBNET.contains(nat_target.internal_ip) { return Err(DpdError::Invalid(format!( - "NAT target internal IP address {} is not in the reserved \ + "NAT target internal IP address {internal_ip} is not in the reserved \ underlay multicast subnet (ff04::/64)", - nat_target.internal_ip + internal_ip = nat_target.internal_ip ))); } diff --git a/dpd/src/table/mcast/mcast_nat.rs b/dpd/src/table/mcast/mcast_nat.rs index 044c7cb2..75517073 100644 --- a/dpd/src/table/mcast/mcast_nat.rs +++ b/dpd/src/table/mcast/mcast_nat.rs @@ -10,8 +10,7 @@ use std::net::{Ipv4Addr, Ipv6Addr}; use crate::{Switch, table::*}; -use super::{Ipv4MatchKey, Ipv6MatchKey}; - +use super::{Ipv4VlanMatchKey, Ipv6VlanMatchKey}; use aal::ActionParse; use aal_macros::*; use common::{nat::NatTarget, network::MacAddr}; @@ -44,64 +43,246 @@ enum Ipv6Action { }, } -/// Add a NAT entry for IPv4 multicast traffic, keyed on `ip`. +/// Add NAT entries for IPv4 multicast traffic. +/// +/// For groups with a VLAN, two entries are added: +/// 1. Untagged ingress match -> forward (for decapsulated Geneve packets) +/// 2. Correctly tagged ingress match -> forward (for already-tagged packets) +/// +/// This allows both packet types to match, while packets with the wrong VLAN +/// will miss both entries and not be NAT encapsulated. pub(crate) fn add_ipv4_entry( s: &Switch, ip: Ipv4Addr, tgt: NatTarget, + vlan_id: Option, ) -> DpdResult<()> { - let match_key = Ipv4MatchKey::new(ip); let action_key = Ipv4Action::Forward { target: tgt.internal_ip, inner_mac: tgt.inner_mac, vni: tgt.vni.as_u32(), }; - debug!( - s.log, - "add ingress mcast entry {} -> {:?}", match_key, action_key - ); - - s.table_entry_add(TableType::NatIngressIpv4Mcast, &match_key, &action_key) + match vlan_id { + None => { + // Untagged only + let match_key = Ipv4VlanMatchKey::new(ip, None); + debug!( + s.log, + "add ingress mcast entry {} -> {:?}", match_key, action_key + ); + s.table_entry_add( + TableType::NatIngressIpv4Mcast, + &match_key, + &action_key, + ) + } + Some(vid) => { + common::network::validate_vlan(vid)?; + + // Untagged entry + let match_key_untagged = Ipv4VlanMatchKey::new(ip, None); + debug!( + s.log, + "add ingress mcast entry {} -> {:?}", + match_key_untagged, + action_key + ); + s.table_entry_add( + TableType::NatIngressIpv4Mcast, + &match_key_untagged, + &action_key, + )?; + + // Tagged entry + let match_key_tagged = Ipv4VlanMatchKey::new(ip, Some(vid)); + debug!( + s.log, + "add ingress mcast entry {} -> {:?}", + match_key_tagged, + action_key + ); + if let Err(e) = s.table_entry_add( + TableType::NatIngressIpv4Mcast, + &match_key_tagged, + &action_key, + ) { + // Rollback untagged entry + debug!(s.log, "rollback: removing untagged entry"); + let _ = s.table_entry_del( + TableType::NatIngressIpv4Mcast, + &match_key_untagged, + ); + return Err(e); + } + Ok(()) + } + } } -/// Update a NAT entry for IPv4 multicast traffic. +/// Update NAT entries for IPv4 multicast traffic. +/// +/// When VLAN changes, old entries are deleted and new ones added because +/// the VLAN is part of the match key and cannot be updated in place. +/// +/// # Arguments +/// +/// * `new_tgt` - NAT target for the new entries. +/// * `old_tgt` - NAT target for restoring entries on failure. Required when +/// VLAN changes since entries must be deleted and re-added. pub(crate) fn update_ipv4_entry( s: &Switch, ip: Ipv4Addr, - tgt: NatTarget, + new_tgt: NatTarget, + old_tgt: NatTarget, + old_vlan_id: Option, + new_vlan_id: Option, ) -> DpdResult<()> { - let match_key = Ipv4MatchKey::new(ip); - let action_key = Ipv4Action::Forward { - target: tgt.internal_ip, - inner_mac: tgt.inner_mac, - vni: tgt.vni.as_u32(), - }; - - debug!( - s.log, - "update ingress mcast entry {} -> {:?}", match_key, action_key - ); - - s.table_entry_update( - TableType::NatIngressIpv4Mcast, - &match_key, - &action_key, - ) + if old_vlan_id == new_vlan_id { + let action_key = Ipv4Action::Forward { + target: new_tgt.internal_ip, + inner_mac: new_tgt.inner_mac, + vni: new_tgt.vni.as_u32(), + }; + + let match_key_untagged = Ipv4VlanMatchKey::new(ip, None); + debug!( + s.log, + "update ingress mcast entry {} -> {:?}", + match_key_untagged, + action_key + ); + s.table_entry_update( + TableType::NatIngressIpv4Mcast, + &match_key_untagged, + &action_key, + )?; + + if let Some(vid) = old_vlan_id { + let match_key_tagged = Ipv4VlanMatchKey::new(ip, Some(vid)); + debug!( + s.log, + "update ingress mcast entry {} -> {:?}", + match_key_tagged, + action_key + ); + s.table_entry_update( + TableType::NatIngressIpv4Mcast, + &match_key_tagged, + &action_key, + )?; + } + return Ok(()); + } + + del_ipv4_entry_with_tgt(s, ip, old_tgt, old_vlan_id)?; + if let Err(e) = add_ipv4_entry(s, ip, new_tgt, new_vlan_id) { + // Restore deleted entries with old target + debug!(s.log, "add failed, restoring old NAT entries for {ip}"); + let _ = add_ipv4_entry(s, ip, old_tgt, old_vlan_id); + return Err(e); + } + Ok(()) } -/// Delete a NAT entry for IPv4 multicast traffic, keyed on `ip`. -pub(crate) fn del_ipv4_entry(s: &Switch, ip: Ipv4Addr) -> DpdResult<()> { - let match_key = Ipv4MatchKey::new(ip); - - debug!(s.log, "delete ingress mcast entry {}", match_key); +/// Delete NAT entries for IPv4 multicast traffic. +/// +/// Deletes both entries for VLAN groups (see `add_ipv4_entry` for details). +/// This version does not support rollback on partial failure. +pub(crate) fn del_ipv4_entry( + s: &Switch, + ip: Ipv4Addr, + vlan_id: Option, +) -> DpdResult<()> { + match vlan_id { + None => { + let match_key = Ipv4VlanMatchKey::new(ip, None); + debug!(s.log, "delete ingress mcast entry {}", match_key); + s.table_entry_del(TableType::NatIngressIpv4Mcast, &match_key) + } + Some(vid) => { + // Untagged + let match_key_untagged = Ipv4VlanMatchKey::new(ip, None); + debug!(s.log, "delete ingress mcast entry {}", match_key_untagged); + s.table_entry_del( + TableType::NatIngressIpv4Mcast, + &match_key_untagged, + )?; + + // Tagged + let match_key_tagged = Ipv4VlanMatchKey::new(ip, Some(vid)); + debug!(s.log, "delete ingress mcast entry {}", match_key_tagged); + if let Err(e) = s.table_entry_del( + TableType::NatIngressIpv4Mcast, + &match_key_tagged, + ) { + // Can't rollback without original action + debug!(s.log, "rollback not possible for untagged entry"); + return Err(e); + } + Ok(()) + } + } +} - s.table_entry_del(TableType::NatIngressIpv4Mcast, &match_key) +/// Delete NAT entries for IPv4 multicast traffic with rollback support. +/// +/// Deletes both entries for VLAN groups. If the tagged entry deletion fails +/// after the untagged entry was deleted, attempts to restore the untagged +/// entry using the provided NAT target. +pub(crate) fn del_ipv4_entry_with_tgt( + s: &Switch, + ip: Ipv4Addr, + tgt: NatTarget, + vlan_id: Option, +) -> DpdResult<()> { + match vlan_id { + None => { + let match_key = Ipv4VlanMatchKey::new(ip, None); + debug!(s.log, "delete ingress mcast entry {}", match_key); + s.table_entry_del(TableType::NatIngressIpv4Mcast, &match_key) + } + Some(vid) => { + // Delete untagged first + let match_key_untagged = Ipv4VlanMatchKey::new(ip, None); + debug!(s.log, "delete ingress mcast entry {}", match_key_untagged); + s.table_entry_del( + TableType::NatIngressIpv4Mcast, + &match_key_untagged, + )?; + + // Delete tagged + let match_key_tagged = Ipv4VlanMatchKey::new(ip, Some(vid)); + debug!(s.log, "delete ingress mcast entry {}", match_key_tagged); + if let Err(e) = s.table_entry_del( + TableType::NatIngressIpv4Mcast, + &match_key_tagged, + ) { + // Rollback: restore the untagged entry + debug!( + s.log, + "tagged delete failed, restoring untagged entry for {ip}" + ); + let action_key = Ipv4Action::Forward { + target: tgt.internal_ip, + inner_mac: tgt.inner_mac, + vni: tgt.vni.as_u32(), + }; + let _ = s.table_entry_add( + TableType::NatIngressIpv4Mcast, + &match_key_untagged, + &action_key, + ); + return Err(e); + } + Ok(()) + } + } } /// Dump the IPv4 NAT table's contents. pub(crate) fn ipv4_table_dump(s: &Switch) -> DpdResult { - s.table_dump::(TableType::NatIngressIpv4Mcast) + s.table_dump::(TableType::NatIngressIpv4Mcast) } /// Fetch the IPv4 NAT table's counters. @@ -109,7 +290,10 @@ pub(crate) fn ipv4_counter_fetch( s: &Switch, force_sync: bool, ) -> DpdResult> { - s.counter_fetch::(force_sync, TableType::NatIngressIpv4Mcast) + s.counter_fetch::( + force_sync, + TableType::NatIngressIpv4Mcast, + ) } /// Reset the Ipv4 NAT table. @@ -117,64 +301,246 @@ pub(crate) fn reset_ipv4(s: &Switch) -> DpdResult<()> { s.table_clear(TableType::NatIngressIpv4Mcast) } -/// Add a NAT entry for IPv6 multicast traffic, keyed on `ip`. +/// Add NAT entries for IPv6 multicast traffic. +/// +/// For groups with a VLAN, two entries are added: +/// 1. Untagged ingress match -> forward (for decapsulated Geneve packets) +/// 2. Correctly tagged ingress match -> forward (for already-tagged packets) +/// +/// This allows both packet types to match, while packets with the wrong VLAN +/// will miss both entries and not be NAT encapsulated. pub(crate) fn add_ipv6_entry( s: &Switch, ip: Ipv6Addr, tgt: NatTarget, + vlan_id: Option, ) -> DpdResult<()> { - let match_key = Ipv6MatchKey::new(ip); let action_key = Ipv6Action::Forward { target: tgt.internal_ip, inner_mac: tgt.inner_mac, vni: tgt.vni.as_u32(), }; - debug!( - s.log, - "add ingress mcast entry {} -> {:?}", match_key, action_key - ); - - s.table_entry_add(TableType::NatIngressIpv6Mcast, &match_key, &action_key) + match vlan_id { + None => { + // Untagged only + let match_key = Ipv6VlanMatchKey::new(ip, None); + debug!( + s.log, + "add ingress mcast entry {} -> {:?}", match_key, action_key + ); + s.table_entry_add( + TableType::NatIngressIpv6Mcast, + &match_key, + &action_key, + ) + } + Some(vid) => { + common::network::validate_vlan(vid)?; + + // Untagged entry + let match_key_untagged = Ipv6VlanMatchKey::new(ip, None); + debug!( + s.log, + "add ingress mcast entry {} -> {:?}", + match_key_untagged, + action_key + ); + s.table_entry_add( + TableType::NatIngressIpv6Mcast, + &match_key_untagged, + &action_key, + )?; + + // Tagged entry + let match_key_tagged = Ipv6VlanMatchKey::new(ip, Some(vid)); + debug!( + s.log, + "add ingress mcast entry {} -> {:?}", + match_key_tagged, + action_key + ); + if let Err(e) = s.table_entry_add( + TableType::NatIngressIpv6Mcast, + &match_key_tagged, + &action_key, + ) { + // Rollback untagged entry + debug!(s.log, "rollback: removing untagged entry"); + let _ = s.table_entry_del( + TableType::NatIngressIpv6Mcast, + &match_key_untagged, + ); + return Err(e); + } + Ok(()) + } + } } -/// Update a NAT entry for IPv6 multicast traffic. +/// Update NAT entries for IPv6 multicast traffic. +/// +/// When VLAN changes, old entries are deleted and new ones added because +/// the VLAN is part of the match key and cannot be updated in place. +/// +/// # Arguments +/// +/// * `new_tgt` - NAT target for the new entries. +/// * `old_tgt` - NAT target for restoring entries on failure. Required when +/// VLAN changes since entries must be deleted and re-added. pub(crate) fn update_ipv6_entry( s: &Switch, ip: Ipv6Addr, - tgt: NatTarget, + new_tgt: NatTarget, + old_tgt: NatTarget, + old_vlan_id: Option, + new_vlan_id: Option, ) -> DpdResult<()> { - let match_key = Ipv6MatchKey::new(ip); - let action_key = Ipv6Action::Forward { - target: tgt.internal_ip, - inner_mac: tgt.inner_mac, - vni: tgt.vni.as_u32(), - }; - - debug!( - s.log, - "update ingress mcast entry {} -> {:?}", match_key, action_key - ); - - s.table_entry_update( - TableType::NatIngressIpv6Mcast, - &match_key, - &action_key, - ) + if old_vlan_id == new_vlan_id { + let action_key = Ipv6Action::Forward { + target: new_tgt.internal_ip, + inner_mac: new_tgt.inner_mac, + vni: new_tgt.vni.as_u32(), + }; + + let match_key_untagged = Ipv6VlanMatchKey::new(ip, None); + debug!( + s.log, + "update ingress mcast entry {} -> {:?}", + match_key_untagged, + action_key + ); + s.table_entry_update( + TableType::NatIngressIpv6Mcast, + &match_key_untagged, + &action_key, + )?; + + if let Some(vid) = old_vlan_id { + let match_key_tagged = Ipv6VlanMatchKey::new(ip, Some(vid)); + debug!( + s.log, + "update ingress mcast entry {} -> {:?}", + match_key_tagged, + action_key + ); + s.table_entry_update( + TableType::NatIngressIpv6Mcast, + &match_key_tagged, + &action_key, + )?; + } + return Ok(()); + } + + del_ipv6_entry_with_tgt(s, ip, old_tgt, old_vlan_id)?; + if let Err(e) = add_ipv6_entry(s, ip, new_tgt, new_vlan_id) { + // Restore deleted entries with old target + debug!(s.log, "add failed, restoring old NAT entries for {ip}"); + let _ = add_ipv6_entry(s, ip, old_tgt, old_vlan_id); + return Err(e); + } + Ok(()) } -/// Delete a NAT entry for IPv6 multicast traffic, keyed on `ip`. -pub(crate) fn del_ipv6_entry(s: &Switch, ip: Ipv6Addr) -> DpdResult<()> { - let match_key = Ipv6MatchKey::new(ip); - - debug!(s.log, "delete ingress mcast entry {}", match_key); +/// Delete NAT entries for IPv6 multicast traffic. +/// +/// Deletes both entries for VLAN groups (see `add_ipv6_entry` for details). +/// This version does not support rollback on partial failure. +pub(crate) fn del_ipv6_entry( + s: &Switch, + ip: Ipv6Addr, + vlan_id: Option, +) -> DpdResult<()> { + match vlan_id { + None => { + let match_key = Ipv6VlanMatchKey::new(ip, None); + debug!(s.log, "delete ingress mcast entry {}", match_key); + s.table_entry_del(TableType::NatIngressIpv6Mcast, &match_key) + } + Some(vid) => { + // Untagged + let match_key_untagged = Ipv6VlanMatchKey::new(ip, None); + debug!(s.log, "delete ingress mcast entry {}", match_key_untagged); + s.table_entry_del( + TableType::NatIngressIpv6Mcast, + &match_key_untagged, + )?; + + // Tagged + let match_key_tagged = Ipv6VlanMatchKey::new(ip, Some(vid)); + debug!(s.log, "delete ingress mcast entry {}", match_key_tagged); + if let Err(e) = s.table_entry_del( + TableType::NatIngressIpv6Mcast, + &match_key_tagged, + ) { + // Can't rollback without original action + debug!(s.log, "rollback not possible for untagged entry"); + return Err(e); + } + Ok(()) + } + } +} - s.table_entry_del(TableType::NatIngressIpv6Mcast, &match_key) +/// Delete NAT entries for IPv6 multicast traffic with rollback support. +/// +/// Deletes both entries for VLAN groups. If the tagged entry deletion fails +/// after the untagged entry was deleted, attempts to restore the untagged +/// entry using the provided NAT target. +pub(crate) fn del_ipv6_entry_with_tgt( + s: &Switch, + ip: Ipv6Addr, + tgt: NatTarget, + vlan_id: Option, +) -> DpdResult<()> { + match vlan_id { + None => { + let match_key = Ipv6VlanMatchKey::new(ip, None); + debug!(s.log, "delete ingress mcast entry {}", match_key); + s.table_entry_del(TableType::NatIngressIpv6Mcast, &match_key) + } + Some(vid) => { + // Delete untagged first + let match_key_untagged = Ipv6VlanMatchKey::new(ip, None); + debug!(s.log, "delete ingress mcast entry {}", match_key_untagged); + s.table_entry_del( + TableType::NatIngressIpv6Mcast, + &match_key_untagged, + )?; + + // Delete tagged + let match_key_tagged = Ipv6VlanMatchKey::new(ip, Some(vid)); + debug!(s.log, "delete ingress mcast entry {}", match_key_tagged); + if let Err(e) = s.table_entry_del( + TableType::NatIngressIpv6Mcast, + &match_key_tagged, + ) { + // Rollback: restore the untagged entry + debug!( + s.log, + "tagged delete failed, restoring untagged entry for {ip}" + ); + let action_key = Ipv6Action::Forward { + target: tgt.internal_ip, + inner_mac: tgt.inner_mac, + vni: tgt.vni.as_u32(), + }; + let _ = s.table_entry_add( + TableType::NatIngressIpv6Mcast, + &match_key_untagged, + &action_key, + ); + return Err(e); + } + Ok(()) + } + } } /// Dump the IPv6 NAT table's contents. pub(crate) fn ipv6_table_dump(s: &Switch) -> DpdResult { - s.table_dump::(TableType::NatIngressIpv6Mcast) + s.table_dump::(TableType::NatIngressIpv6Mcast) } /// Fetch the IPv6 NAT table's counters. @@ -182,7 +548,10 @@ pub(crate) fn ipv6_counter_fetch( s: &Switch, force_sync: bool, ) -> DpdResult> { - s.counter_fetch::(force_sync, TableType::NatIngressIpv6Mcast) + s.counter_fetch::( + force_sync, + TableType::NatIngressIpv6Mcast, + ) } /// Reset the Ipv6 NAT table. diff --git a/dpd/src/table/mcast/mcast_route.rs b/dpd/src/table/mcast/mcast_route.rs index bd097e7c..292b6120 100644 --- a/dpd/src/table/mcast/mcast_route.rs +++ b/dpd/src/table/mcast/mcast_route.rs @@ -5,18 +5,24 @@ // Copyright 2026 Oxide Computer Company //! Table operations for multicast routing entries (on Ingress to the switch). +//! +//! Route tables match only on destination address and select the egress action: +//! - `forward`: Forward without VLAN modification +//! - `forward_vlan(vid)`: Add VLAN tag on egress +//! +//! VLAN-based access control (preventing VLAN translation) is handled by NAT +//! ingress tables before encapsulation, not by route tables. use std::net::{Ipv4Addr, Ipv6Addr}; -use crate::{Switch, table::*}; - -use super::{Ipv4MatchKey, Ipv6MatchKey}; - use aal::ActionParse; use aal_macros::*; use omicron_common::address::UNDERLAY_MULTICAST_SUBNET; use slog::debug; +use super::{Ipv4MatchKey, Ipv6MatchKey}; +use crate::{Switch, table::*}; + /// IPv4 Table for multicast routing entries. pub(crate) const IPV4_TABLE_NAME: &str = "pipe.Ingress.l3_router.MulticastRouter4.tbl"; @@ -40,61 +46,63 @@ enum Ipv6Action { ForwardVLAN { vlan_id: u16 }, } -/// Add an IPv4 multicast route entry to the routing table, keyed on -/// `route`, with an optional `vlan_id`. +/// Add an IPv4 multicast route entry. +/// +/// The action is selected based on VLAN configuration: +/// - No VLAN: `forward` (no VLAN modification on egress) +/// - With VLAN: `forward_vlan(vid)` (add VLAN tag on egress) pub(crate) fn add_ipv4_entry( s: &Switch, route: Ipv4Addr, vlan_id: Option, ) -> DpdResult<()> { let match_key = Ipv4MatchKey::new(route); - - let action_data = match vlan_id { + let action = match vlan_id { None => Ipv4Action::Forward, - Some(vlan_id) => { - common::network::validate_vlan(vlan_id)?; - Ipv4Action::ForwardVLAN { vlan_id } + Some(vid) => { + common::network::validate_vlan(vid)?; + Ipv4Action::ForwardVLAN { vlan_id: vid } } }; - debug!( - s.log, - "add multicast route entry {} -> {:?}", route, action_data - ); - - s.table_entry_add(TableType::RouteIpv4Mcast, &match_key, &action_data) + debug!(s.log, "add multicast route entry {match_key} -> {action:?}"); + s.table_entry_add(TableType::RouteIpv4Mcast, &match_key, &action) } -/// Update an IPv4 multicast route entry in the routing table. +/// Update an IPv4 multicast route entry. +/// +/// Updates the action when VLAN configuration changes. Since the match key +/// is just the destination address, this can be done in place. pub(crate) fn update_ipv4_entry( s: &Switch, route: Ipv4Addr, - vlan_id: Option, + old_vlan_id: Option, + new_vlan_id: Option, ) -> DpdResult<()> { + if old_vlan_id == new_vlan_id { + return Ok(()); + } + let match_key = Ipv4MatchKey::new(route); - let action_data = match vlan_id { + let action = match new_vlan_id { None => Ipv4Action::Forward, - Some(vlan_id) => { - common::network::validate_vlan(vlan_id)?; - Ipv4Action::ForwardVLAN { vlan_id } + Some(vid) => { + common::network::validate_vlan(vid)?; + Ipv4Action::ForwardVLAN { vlan_id: vid } } }; debug!( s.log, - "update multicast route entry {} -> {:?}", route, action_data + "update multicast route entry {match_key} -> {action:?}" ); - - s.table_entry_update(TableType::RouteIpv4Mcast, &match_key, &action_data) + s.table_entry_update(TableType::RouteIpv4Mcast, &match_key, &action) } -/// Delete an IPv4 multicast route entry from table, keyed on -/// `route`. +/// Delete an IPv4 multicast route entry. pub(crate) fn del_ipv4_entry(s: &Switch, route: Ipv4Addr) -> DpdResult<()> { let match_key = Ipv4MatchKey::new(route); - - debug!(s.log, "delete multicast route entry {}", match_key); - + debug!(s.log, "delete multicast route entry {match_key}"); s.table_entry_del(TableType::RouteIpv4Mcast, &match_key) } @@ -116,8 +124,15 @@ pub(crate) fn reset_ipv4(s: &Switch) -> DpdResult<()> { s.table_clear(TableType::RouteIpv4Mcast) } -/// Add an IPv6 multicast route entry to the routing table, keyed on -/// `route`, with an optional `vlan_id`. +/// Add an IPv6 multicast route entry. +/// +/// The action is selected based on VLAN configuration: +/// - No VLAN: `forward` (no VLAN modification on egress) +/// - With VLAN: `forward_vlan(vid)` (add VLAN tag on egress) +/// +/// Reserved underlay multicast subnet (ff04::/64) is internal to the rack +/// and always uses Forward without VLAN tagging, regardless of the vlan_id +/// parameter. pub(crate) fn add_ipv6_entry( s: &Switch, route: Ipv6Addr, @@ -126,68 +141,64 @@ pub(crate) fn add_ipv6_entry( let match_key = Ipv6MatchKey::new(route); // 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) { + // and doesn't require VLAN tagging. + let action: Ipv6Action = if UNDERLAY_MULTICAST_SUBNET.contains(route) { Ipv6Action::Forward } else { match vlan_id { None => Ipv6Action::Forward, - Some(vlan_id) => { - common::network::validate_vlan(vlan_id)?; - Ipv6Action::ForwardVLAN { vlan_id } + Some(vid) => { + common::network::validate_vlan(vid)?; + Ipv6Action::ForwardVLAN { vlan_id: vid } } } }; - debug!( - s.log, - "add multicast route entry {} -> {:?}", route, action_data - ); - - s.table_entry_add(TableType::RouteIpv6Mcast, &match_key, &action_data) + debug!(s.log, "add multicast route entry {match_key} -> {action:?}"); + s.table_entry_add(TableType::RouteIpv6Mcast, &match_key, &action) } -/// Update an IPv6 multicast route entry in the routing table. +/// Update an IPv6 multicast route entry. +/// +/// Updates the action when VLAN configuration changes. Since the match key +/// is just the destination address, this can be done in place. pub(crate) fn update_ipv6_entry( s: &Switch, route: Ipv6Addr, - vlan_id: Option, + old_vlan_id: Option, + new_vlan_id: Option, ) -> DpdResult<()> { + if old_vlan_id == new_vlan_id { + return Ok(()); + } + let match_key = Ipv6MatchKey::new(route); // 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) { + // and doesn't require VLAN tagging. + let action: Ipv6Action = if UNDERLAY_MULTICAST_SUBNET.contains(route) { Ipv6Action::Forward } else { - match vlan_id { + match new_vlan_id { None => Ipv6Action::Forward, - Some(vlan_id) => { - common::network::validate_vlan(vlan_id)?; - Ipv6Action::ForwardVLAN { vlan_id } + Some(vid) => { + common::network::validate_vlan(vid)?; + Ipv6Action::ForwardVLAN { vlan_id: vid } } } }; debug!( s.log, - "update multicast route entry {} -> {:?}", route, action_data + "update multicast route entry {match_key} -> {action:?}" ); - - s.table_entry_update(TableType::RouteIpv6Mcast, &match_key, &action_data) + s.table_entry_update(TableType::RouteIpv6Mcast, &match_key, &action) } -/// Delete an IPv6 multicast entry from routing table, keyed on -/// `route`. +/// Delete an IPv6 multicast route entry. pub(crate) fn del_ipv6_entry(s: &Switch, route: Ipv6Addr) -> DpdResult<()> { let match_key = Ipv6MatchKey::new(route); - - debug!(s.log, "delete multicast route entry {}", match_key); - + debug!(s.log, "delete multicast route entry {match_key}"); s.table_entry_del(TableType::RouteIpv6Mcast, &match_key) } diff --git a/dpd/src/table/mcast/mod.rs b/dpd/src/table/mcast/mod.rs index ee3ff5b5..70677e3e 100644 --- a/dpd/src/table/mcast/mod.rs +++ b/dpd/src/table/mcast/mod.rs @@ -55,3 +55,85 @@ impl fmt::Display for Ipv6MatchKey { write!(f, "{}", self.dst_addr) } } + +/// VLAN-aware match key for NAT ingress tables. +/// +/// Matches on destination address, VLAN header validity, and VLAN ID to prevent +/// VLAN translation. For groups with a VLAN, two entries are installed (untagged +/// and tagged), packets with a mismatched VLAN miss both and are dropped rather +/// than being translated to another customer's VLAN. +#[derive(MatchParse, Hash)] +pub(super) struct Ipv4VlanMatchKey { + dst_addr: Ipv4Addr, + #[match_xlate(name = "$valid")] + vlan_valid: bool, + vlan_id: u16, +} + +impl Ipv4VlanMatchKey { + pub(super) fn new(dst_addr: Ipv4Addr, vlan_id: Option) -> Self { + match vlan_id { + Some(id) => Self { + dst_addr, + vlan_valid: true, + vlan_id: id, + }, + None => Self { + dst_addr, + vlan_valid: false, + vlan_id: 0, + }, + } + } +} + +impl fmt::Display for Ipv4VlanMatchKey { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if self.vlan_valid { + write!(f, "{} vlan={}", self.dst_addr, self.vlan_id) + } else { + write!(f, "{} untagged", self.dst_addr) + } + } +} + +/// VLAN-aware match key for NAT ingress tables. +/// +/// Matches on destination address, VLAN header validity, and VLAN ID to prevent +/// VLAN translation. For groups with a VLAN, two entries are installed (untagged +/// and tagged), packets with a mismatched VLAN miss both and are dropped rather +/// than being translated to another customer's VLAN. +#[derive(MatchParse, Hash)] +pub(super) struct Ipv6VlanMatchKey { + dst_addr: Ipv6Addr, + #[match_xlate(name = "$valid")] + vlan_valid: bool, + vlan_id: u16, +} + +impl Ipv6VlanMatchKey { + pub(super) fn new(dst_addr: Ipv6Addr, vlan_id: Option) -> Self { + match vlan_id { + Some(id) => Self { + dst_addr, + vlan_valid: true, + vlan_id: id, + }, + None => Self { + dst_addr, + vlan_valid: false, + vlan_id: 0, + }, + } + } +} + +impl fmt::Display for Ipv6VlanMatchKey { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if self.vlan_valid { + write!(f, "{} vlan={}", self.dst_addr, self.vlan_id) + } else { + write!(f, "{} untagged", self.dst_addr) + } + } +} From 602dcc583d6f3d01165b2c15e2da514f1131be31 Mon Sep 17 00:00:00 2001 From: Zeeshan Lakhani Date: Thu, 15 Jan 2026 06:39:34 -0500 Subject: [PATCH 16/22] [validation,api] Move validation to types via FromStr, update delegation via dropshot Path try_map Includes: - delegate a versioned endpoint in lib.rs from api-server.rs - Cargo.lock updated for new dropshot/omicron --- Cargo.lock | 87 +++++++++++++++++++-------------------- Cargo.toml | 2 +- dpd-api/src/lib.rs | 65 ++++++++++++++++++++++++++++- dpd-api/src/v3.rs | 11 +++++ dpd-types/src/link.rs | 10 ++++- dpd-types/src/mcast.rs | 14 +++++++ dpd/src/api_server.rs | 22 ---------- dpd/src/mcast/validate.rs | 39 ++++-------------- 8 files changed, 149 insertions(+), 101 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3166ec41..d5e2212c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -125,7 +125,7 @@ checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "api_identity" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#75fe22d9f9c7815e3ece509b4f7409eb5252e0d1" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#c197cca8225b09d7115f1ac24d9b35b078a3ac70" dependencies = [ "omicron-workspace-hack", "proc-macro2", @@ -538,7 +538,7 @@ dependencies = [ [[package]] name = "bootstore" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#75fe22d9f9c7815e3ece509b4f7409eb5252e0d1" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#c197cca8225b09d7115f1ac24d9b35b078a3ac70" dependencies = [ "bytes", "camino", @@ -832,7 +832,7 @@ checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" [[package]] name = "clickhouse-admin-types" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#75fe22d9f9c7815e3ece509b4f7409eb5252e0d1" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#c197cca8225b09d7115f1ac24d9b35b078a3ac70" dependencies = [ "clickhouse-admin-types-versions", "omicron-workspace-hack", @@ -841,7 +841,7 @@ dependencies = [ [[package]] name = "clickhouse-admin-types-versions" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#75fe22d9f9c7815e3ece509b4f7409eb5252e0d1" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#c197cca8225b09d7115f1ac24d9b35b078a3ac70" dependencies = [ "anyhow", "atomicwrites", @@ -897,7 +897,7 @@ dependencies = [ [[package]] name = "cockroach-admin-types" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#75fe22d9f9c7815e3ece509b4f7409eb5252e0d1" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#c197cca8225b09d7115f1ac24d9b35b078a3ac70" dependencies = [ "cockroach-admin-types-versions", "omicron-workspace-hack", @@ -907,7 +907,7 @@ dependencies = [ [[package]] name = "cockroach-admin-types-versions" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#75fe22d9f9c7815e3ece509b4f7409eb5252e0d1" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#c197cca8225b09d7115f1ac24d9b35b078a3ac70" dependencies = [ "chrono", "csv", @@ -1979,7 +1979,7 @@ dependencies = [ [[package]] name = "ereport-types" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#75fe22d9f9c7815e3ece509b4f7409eb5252e0d1" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#c197cca8225b09d7115f1ac24d9b35b078a3ac70" dependencies = [ "dropshot", "omicron-uuid-kinds", @@ -2268,7 +2268,7 @@ dependencies = [ [[package]] name = "gateway-client" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#75fe22d9f9c7815e3ece509b4f7409eb5252e0d1" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#c197cca8225b09d7115f1ac24d9b35b078a3ac70" dependencies = [ "base64 0.22.1", "chrono", @@ -2310,7 +2310,7 @@ dependencies = [ [[package]] name = "gateway-types" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#75fe22d9f9c7815e3ece509b4f7409eb5252e0d1" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#c197cca8225b09d7115f1ac24d9b35b078a3ac70" dependencies = [ "gateway-types-versions", "omicron-workspace-hack", @@ -2319,7 +2319,7 @@ dependencies = [ [[package]] name = "gateway-types-versions" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#75fe22d9f9c7815e3ece509b4f7409eb5252e0d1" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#c197cca8225b09d7115f1ac24d9b35b078a3ac70" dependencies = [ "daft", "dropshot", @@ -2385,7 +2385,7 @@ dependencies = [ [[package]] name = "gfss" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#75fe22d9f9c7815e3ece509b4f7409eb5252e0d1" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#c197cca8225b09d7115f1ac24d9b35b078a3ac70" dependencies = [ "digest", "omicron-workspace-hack", @@ -2574,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" @@ -3065,7 +3062,7 @@ dependencies = [ [[package]] name = "illumos-utils" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#75fe22d9f9c7815e3ece509b4f7409eb5252e0d1" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#c197cca8225b09d7115f1ac24d9b35b078a3ac70" dependencies = [ "anyhow", "async-trait", @@ -3196,7 +3193,7 @@ dependencies = [ [[package]] name = "internal-dns-resolver" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#75fe22d9f9c7815e3ece509b4f7409eb5252e0d1" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#c197cca8225b09d7115f1ac24d9b35b078a3ac70" dependencies = [ "futures", "hickory-proto 0.25.2", @@ -3214,7 +3211,7 @@ dependencies = [ [[package]] name = "internal-dns-types" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#75fe22d9f9c7815e3ece509b4f7409eb5252e0d1" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#c197cca8225b09d7115f1ac24d9b35b078a3ac70" dependencies = [ "anyhow", "chrono", @@ -3230,7 +3227,7 @@ dependencies = [ [[package]] name = "internal-dns-types-versions" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#75fe22d9f9c7815e3ece509b4f7409eb5252e0d1" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#c197cca8225b09d7115f1ac24d9b35b078a3ac70" dependencies = [ "anyhow", "chrono", @@ -3926,7 +3923,7 @@ dependencies = [ [[package]] name = "nexus-client" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#75fe22d9f9c7815e3ece509b4f7409eb5252e0d1" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#c197cca8225b09d7115f1ac24d9b35b078a3ac70" dependencies = [ "chrono", "futures", @@ -3949,7 +3946,7 @@ dependencies = [ [[package]] name = "nexus-types" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#75fe22d9f9c7815e3ece509b4f7409eb5252e0d1" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#c197cca8225b09d7115f1ac24d9b35b078a3ac70" dependencies = [ "anyhow", "api_identity", @@ -4243,7 +4240,7 @@ dependencies = [ [[package]] name = "omicron-common" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#75fe22d9f9c7815e3ece509b4f7409eb5252e0d1" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#c197cca8225b09d7115f1ac24d9b35b078a3ac70" dependencies = [ "anyhow", "api_identity", @@ -4288,7 +4285,7 @@ dependencies = [ [[package]] name = "omicron-passwords" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#75fe22d9f9c7815e3ece509b4f7409eb5252e0d1" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#c197cca8225b09d7115f1ac24d9b35b078a3ac70" dependencies = [ "argon2", "omicron-workspace-hack", @@ -4303,7 +4300,7 @@ dependencies = [ [[package]] name = "omicron-rpaths" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#75fe22d9f9c7815e3ece509b4f7409eb5252e0d1" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#c197cca8225b09d7115f1ac24d9b35b078a3ac70" dependencies = [ "omicron-workspace-hack", ] @@ -4311,7 +4308,7 @@ dependencies = [ [[package]] name = "omicron-uuid-kinds" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#75fe22d9f9c7815e3ece509b4f7409eb5252e0d1" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#c197cca8225b09d7115f1ac24d9b35b078a3ac70" dependencies = [ "daft", "newtype-uuid", @@ -4515,7 +4512,7 @@ dependencies = [ [[package]] name = "oximeter" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#75fe22d9f9c7815e3ece509b4f7409eb5252e0d1" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#c197cca8225b09d7115f1ac24d9b35b078a3ac70" dependencies = [ "anyhow", "chrono", @@ -4534,7 +4531,7 @@ dependencies = [ [[package]] name = "oximeter-db" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#75fe22d9f9c7815e3ece509b4f7409eb5252e0d1" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#c197cca8225b09d7115f1ac24d9b35b078a3ac70" dependencies = [ "anyhow", "async-recursion", @@ -4587,7 +4584,7 @@ dependencies = [ [[package]] name = "oximeter-instruments" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#75fe22d9f9c7815e3ece509b4f7409eb5252e0d1" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#c197cca8225b09d7115f1ac24d9b35b078a3ac70" dependencies = [ "cfg-if", "chrono", @@ -4605,7 +4602,7 @@ dependencies = [ [[package]] name = "oximeter-macro-impl" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#75fe22d9f9c7815e3ece509b4f7409eb5252e0d1" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#c197cca8225b09d7115f1ac24d9b35b078a3ac70" dependencies = [ "omicron-workspace-hack", "proc-macro2", @@ -4616,7 +4613,7 @@ dependencies = [ [[package]] name = "oximeter-producer" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#75fe22d9f9c7815e3ece509b4f7409eb5252e0d1" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#c197cca8225b09d7115f1ac24d9b35b078a3ac70" dependencies = [ "chrono", "dropshot", @@ -4638,7 +4635,7 @@ dependencies = [ [[package]] name = "oximeter-schema" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#75fe22d9f9c7815e3ece509b4f7409eb5252e0d1" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#c197cca8225b09d7115f1ac24d9b35b078a3ac70" dependencies = [ "anyhow", "chrono", @@ -4659,7 +4656,7 @@ dependencies = [ [[package]] name = "oximeter-timeseries-macro" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#75fe22d9f9c7815e3ece509b4f7409eb5252e0d1" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#c197cca8225b09d7115f1ac24d9b35b078a3ac70" dependencies = [ "omicron-workspace-hack", "oximeter-schema", @@ -4672,7 +4669,7 @@ dependencies = [ [[package]] name = "oximeter-types" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#75fe22d9f9c7815e3ece509b4f7409eb5252e0d1" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#c197cca8225b09d7115f1ac24d9b35b078a3ac70" dependencies = [ "bytes", "chrono", @@ -4693,7 +4690,7 @@ dependencies = [ [[package]] name = "oximeter-types-versions" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#75fe22d9f9c7815e3ece509b4f7409eb5252e0d1" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#c197cca8225b09d7115f1ac24d9b35b078a3ac70" dependencies = [ "chrono", "omicron-common", @@ -4706,7 +4703,7 @@ dependencies = [ [[package]] name = "oxlog" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#75fe22d9f9c7815e3ece509b4f7409eb5252e0d1" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#c197cca8225b09d7115f1ac24d9b35b078a3ac70" dependencies = [ "anyhow", "camino", @@ -4735,7 +4732,7 @@ dependencies = [ [[package]] name = "oxql-types" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#75fe22d9f9c7815e3ece509b4f7409eb5252e0d1" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#c197cca8225b09d7115f1ac24d9b35b078a3ac70" dependencies = [ "anyhow", "chrono", @@ -6240,10 +6237,11 @@ dependencies = [ [[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]] @@ -6489,7 +6487,7 @@ checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" [[package]] name = "sled-agent-types" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#75fe22d9f9c7815e3ece509b4f7409eb5252e0d1" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#c197cca8225b09d7115f1ac24d9b35b078a3ac70" dependencies = [ "anyhow", "async-trait", @@ -6521,7 +6519,7 @@ dependencies = [ [[package]] name = "sled-agent-types-versions" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#75fe22d9f9c7815e3ece509b4f7409eb5252e0d1" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#c197cca8225b09d7115f1ac24d9b35b078a3ac70" dependencies = [ "async-trait", "bootstore", @@ -6539,6 +6537,7 @@ dependencies = [ "propolis_api_types", "schemars 0.8.22", "serde", + "serde_human_bytes", "serde_json", "serde_with", "sha3", @@ -6554,7 +6553,7 @@ dependencies = [ [[package]] name = "sled-hardware-types" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#75fe22d9f9c7815e3ece509b4f7409eb5252e0d1" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#c197cca8225b09d7115f1ac24d9b35b078a3ac70" dependencies = [ "daft", "illumos-utils", @@ -7787,7 +7786,7 @@ dependencies = [ [[package]] name = "trust-quorum-protocol" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#75fe22d9f9c7815e3ece509b4f7409eb5252e0d1" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#c197cca8225b09d7115f1ac24d9b35b078a3ac70" dependencies = [ "bootstore", "bytes", @@ -7822,7 +7821,7 @@ dependencies = [ [[package]] name = "trust-quorum-types" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#75fe22d9f9c7815e3ece509b4f7409eb5252e0d1" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#c197cca8225b09d7115f1ac24d9b35b078a3ac70" dependencies = [ "omicron-workspace-hack", "trust-quorum-types-versions", @@ -7831,7 +7830,7 @@ dependencies = [ [[package]] name = "trust-quorum-types-versions" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#75fe22d9f9c7815e3ece509b4f7409eb5252e0d1" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#c197cca8225b09d7115f1ac24d9b35b078a3ac70" dependencies = [ "daft", "derive_more", @@ -8026,7 +8025,7 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "update-engine" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#75fe22d9f9c7815e3ece509b4f7409eb5252e0d1" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#c197cca8225b09d7115f1ac24d9b35b078a3ac70" dependencies = [ "anyhow", "cancel-safe-futures", diff --git a/Cargo.toml b/Cargo.toml index 4bdb4601..f9912c26 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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/dpd-api/src/lib.rs b/dpd-api/src/lib.rs index c5c8209a..4fa4a5e6 100644 --- a/dpd-api/src/lib.rs +++ b/dpd-api/src/lib.rs @@ -11,7 +11,9 @@ pub mod v3; use std::{ collections::{BTreeMap, HashMap, HashSet}, + fmt, net::{IpAddr, Ipv4Addr, Ipv6Addr}, + str::FromStr, }; use common::{ @@ -1678,7 +1680,7 @@ pub trait DpdApi { /// Get an underlay (internal) multicast group configuration (API v1-v3). /// /// Uses the broader ff04::/16 (admin-local) address validation for backward - /// compatibility. + /// compatibility. Delegates to v4 endpoint with path param conversion. #[endpoint { method = GET, path = "/multicast/underlay-groups/{group_ip}", @@ -1687,7 +1689,24 @@ pub trait DpdApi { async fn multicast_group_get_underlay_v3( rqctx: RequestContext, path: Path, - ) -> Result, HttpError>; + ) -> 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. @@ -2687,6 +2706,48 @@ impl From for MulticastTag { } } +/// 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 { diff --git a/dpd-api/src/v3.rs b/dpd-api/src/v3.rs index 90f98b6c..bff9f0ee 100644 --- a/dpd-api/src/v3.rs +++ b/dpd-api/src/v3.rs @@ -16,6 +16,7 @@ use std::{ fmt, net::{IpAddr, Ipv6Addr}, + str::FromStr, }; use oxnet::Ipv6Net; @@ -102,6 +103,16 @@ impl fmt::Display for AdminScopedIpv6 { } } +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)] diff --git a/dpd-types/src/link.rs b/dpd-types/src/link.rs index 3e13d7b4..20d78a4d 100644 --- a/dpd-types/src/link.rs +++ b/dpd-types/src/link.rs @@ -4,7 +4,7 @@ // // Copyright 2026 Oxide Computer Company -use std::fmt; +use std::{fmt, str::FromStr}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -56,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 cdc59256..c4acef3f 100644 --- a/dpd-types/src/mcast.rs +++ b/dpd-types/src/mcast.rs @@ -9,6 +9,7 @@ use std::{ fmt, net::{IpAddr, Ipv6Addr}, + str::FromStr, }; use common::{nat::NatTarget, ports::PortId}; @@ -79,6 +80,17 @@ impl fmt::Display for UnderlayMulticastIpv6 { } } +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. @@ -240,4 +252,6 @@ pub enum Error { "Address {0} is not in underlay multicast subnet (must be ff04::/64)" )] InvalidUnderlayMulticastIp(Ipv6Addr), + #[error("Invalid IPv6 address '{0}': {1}")] + InvalidIpv6Address(String, std::net::AddrParseError), } diff --git a/dpd/src/api_server.rs b/dpd/src/api_server.rs index 94e0aa99..302bfac1 100644 --- a/dpd/src/api_server.rs +++ b/dpd/src/api_server.rs @@ -1991,28 +1991,6 @@ impl DpdApi for DpdApiImpl { .map_err(HttpError::from) } - async fn multicast_group_get_underlay_v3( - rqctx: RequestContext>, - path: Path, - ) -> 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}"), - ) - })?; - - mcast::get_group_internal(switch, underlay) - .map(|resp| HttpResponseOk(resp.into())) - .map_err(HttpError::from) - } - async fn multicast_group_get_underlay( rqctx: RequestContext>, path: Path, diff --git a/dpd/src/mcast/validate.rs b/dpd/src/mcast/validate.rs index 0eadfe26..44f9199b 100644 --- a/dpd/src/mcast/validate.rs +++ b/dpd/src/mcast/validate.rs @@ -14,6 +14,7 @@ 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, @@ -229,42 +230,17 @@ fn validate_ipv6_source_address(ipv6: Ipv6Addr) -> DpdResult<()> { Ok(()) } -/// Maximum length for multicast group tags. -/// -/// Keep in sync with Omicron's database schema column type for multicast group -/// tags. This is sized to accommodate the auto-generated format -/// `{uuid}:{group_ip}` for both IPv4 and IPv6 group IPs. -const MAX_TAG_LENGTH: usize = 80; - /// Validates tag format for group creation. /// -/// Tags must be 1-80 ASCII bytes containing only alphanumeric characters, -/// hyphens, underscores, colons, or periods. -/// -/// This character set is compatible with URL path segments, though colons are -/// RFC 3986 reserved characters and may require percent-encoding in some HTTP -/// client contexts. +/// 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<()> { - if tag.is_empty() { - return Err(DpdError::Invalid("tag cannot be empty".to_string())); - } - if tag.len() > MAX_TAG_LENGTH { - return Err(DpdError::Invalid(format!( - "tag cannot exceed {MAX_TAG_LENGTH} bytes" - ))); - } - if !tag.bytes().all(|b| { - b.is_ascii_alphanumeric() || matches!(b, b'-' | b'_' | b':' | b'.') - }) { - return Err(DpdError::Invalid( - "tag must contain only ASCII alphanumeric characters, hyphens, \ - underscores, colons, or periods" - .to_string(), - )); - } - Ok(()) + tag.parse::() + .map(|_| ()) + .map_err(|e| DpdError::Invalid(e.to_string())) } /// Validates that the request tag matches the existing group's tag. @@ -288,6 +264,7 @@ pub(crate) fn validate_tag( mod tests { use super::*; use common::{nat::Vni, network::MacAddr}; + use dpd_api::MAX_TAG_LENGTH; /// Admin-local IPv6 multicast prefix (ff04::/16, scope 4). const ADMIN_LOCAL_PREFIX: u16 = 0xff04; From 9e0f2d78cc4620bf77e3c67d6d93cfa635248d3a Mon Sep 17 00:00:00 2001 From: Zeeshan Lakhani Date: Thu, 15 Jan 2026 12:23:08 +0000 Subject: [PATCH 17/22] [fmt,2026,api] --- dpd-client/tests/integration_tests/common.rs | 2 +- dpd/p4/constants.p4 | 2 +- openapi/dpd/{dpd-3.0.0-e0be17.json => dpd-3.0.0-27dfd0.json} | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename openapi/dpd/{dpd-3.0.0-e0be17.json => dpd-3.0.0-27dfd0.json} (99%) diff --git a/dpd-client/tests/integration_tests/common.rs b/dpd-client/tests/integration_tests/common.rs index 5b315cbd..56db9fd5 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/p4/constants.p4 b/dpd/p4/constants.p4 index 343187ec..8ebe0b72 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; diff --git a/openapi/dpd/dpd-3.0.0-e0be17.json b/openapi/dpd/dpd-3.0.0-27dfd0.json similarity index 99% rename from openapi/dpd/dpd-3.0.0-e0be17.json rename to openapi/dpd/dpd-3.0.0-27dfd0.json index 1c8b8603..16d18479 100644 --- a/openapi/dpd/dpd-3.0.0-e0be17.json +++ b/openapi/dpd/dpd-3.0.0-27dfd0.json @@ -1409,7 +1409,7 @@ "/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.", + "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": [ { From 502dd757a03030a773a3c6a5136b42b885035c95 Mon Sep 17 00:00:00 2001 From: Zeeshan Lakhani Date: Thu, 15 Jan 2026 12:34:43 +0000 Subject: [PATCH 18/22] [post-merge] copyright fixes --- dpd/src/counters.rs | 2 +- dpd/src/table/mcast/mcast_nat.rs | 2 +- dpd/src/table/mcast/mod.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/dpd/src/counters.rs b/dpd/src/counters.rs index eb61c25c..d10f34ab 100644 --- a/dpd/src/counters.rs +++ b/dpd/src/counters.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 /// This module contains the support for reading the indirect counters defined /// by the p4 program. While direct counters are attached to an existing table, diff --git a/dpd/src/table/mcast/mcast_nat.rs b/dpd/src/table/mcast/mcast_nat.rs index 75517073..33cdfcaf 100644 --- a/dpd/src/table/mcast/mcast_nat.rs +++ b/dpd/src/table/mcast/mcast_nat.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 NAT entries. diff --git a/dpd/src/table/mcast/mod.rs b/dpd/src/table/mcast/mod.rs index 70677e3e..a3b3f249 100644 --- a/dpd/src/table/mcast/mod.rs +++ b/dpd/src/table/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 table operations. From 7402987eb972c4d4f97e168247cefbfbfd0ba7c3 Mon Sep 17 00:00:00 2001 From: Zeeshan Lakhani Date: Sat, 17 Jan 2026 08:51:50 +0000 Subject: [PATCH 19/22] [api,multicast] Use v3 type for create underlay endpoint, proper delegation We follow API guidelines recently posted in https://github.com/oxidecomputer/dropshot-api-manager/blob/main/guides/new-version.md --- dpd-api/src/lib.rs | 58 ++++++++----------- dpd-api/src/v2.rs | 44 +++++++++----- dpd-api/src/v3.rs | 20 +++++-- ....0.0-27dfd0.json => dpd-3.0.0-034efd.json} | 19 +++--- ....0.0-7b2800.json => dpd-4.0.0-04ac56.json} | 4 +- openapi/dpd/dpd-latest.json | 2 +- 6 files changed, 80 insertions(+), 67 deletions(-) rename openapi/dpd/{dpd-3.0.0-27dfd0.json => dpd-3.0.0-034efd.json} (99%) rename openapi/dpd/{dpd-4.0.0-7b2800.json => dpd-4.0.0-04ac56.json} (99%) diff --git a/dpd-api/src/lib.rs b/dpd-api/src/lib.rs index 4fa4a5e6..d5d1755e 100644 --- a/dpd-api/src/lib.rs +++ b/dpd-api/src/lib.rs @@ -1533,14 +1533,7 @@ pub trait DpdApi { } } - /** - * Create an underlay (internal) multicast group configuration. - * - * 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. - */ + /// Create an underlay (internal) multicast group configuration. #[endpoint { method = POST, path = "/multicast/underlay-groups", @@ -1555,6 +1548,8 @@ pub trait DpdApi { >; /// Create an underlay (internal) multicast group configuration (API v1-v3). + /// + /// Accepts the broader ff04::/16 (admin-local) address range. #[endpoint { method = POST, path = "/multicast/underlay-groups", @@ -1562,12 +1557,23 @@ pub trait DpdApi { }] async fn multicast_group_create_underlay_v3( rqctx: RequestContext, - group: TypedBody, + group: TypedBody, ) -> Result< HttpResponseCreated, HttpError, > { - match Self::multicast_group_create_underlay(rqctx, group).await { + let v4_body = group + .try_map(|entry| { + let group_ip = + mcast::UnderlayMulticastIpv6::try_from(entry.group_ip)?; + Ok(mcast::MulticastGroupCreateUnderlayEntry { + group_ip, + tag: entry.tag, + members: entry.members, + }) + }) + .map_err(|e: String| HttpError::for_bad_request(None, e))?; + match Self::multicast_group_create_underlay(rqctx, v4_body).await { Ok(HttpResponseCreated(resp)) => { Ok(HttpResponseCreated(resp.into())) } @@ -1661,12 +1667,7 @@ pub trait DpdApi { } } - /** - * Get an underlay (internal) multicast group configuration. - * - * Underlay groups handle admin-local IPv6 multicast traffic (ff04::/16) with - * replication infrastructure for external and underlay members. - */ + /// Get an underlay (internal) multicast group configuration. #[endpoint { method = GET, path = "/multicast/underlay-groups/{group_ip}", @@ -1679,8 +1680,7 @@ pub trait DpdApi { /// 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. + /// Accepts the broader ff04::/16 (admin-local) address range. #[endpoint { method = GET, path = "/multicast/underlay-groups/{group_ip}", @@ -1708,14 +1708,9 @@ pub trait DpdApi { } } - /** - * Update an underlay (internal) multicast group configuration. - * - * 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. - */ + /// Update an underlay (internal) multicast group configuration. + /// + /// The `tag` query parameter must match the group's existing tag. #[endpoint { method = PUT, path = "/multicast/underlay-groups/{group_ip}", @@ -1730,8 +1725,8 @@ pub trait DpdApi { /// 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. + /// Accepts the broader ff04::/16 (admin-local) address range. Tags are + /// optional. If omitted, the existing tag is used. #[endpoint { method = PUT, path = "/multicast/underlay-groups/{group_ip}", @@ -2772,11 +2767,8 @@ pub struct MulticastGroupTagQuery { 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 +/// Used to identify an underlay multicast group by IPv6 address within +/// the underlay multicast subnet (ff04::/64). #[derive(Deserialize, Serialize, JsonSchema)] pub struct MulticastUnderlayGroupIpParam { pub group_ip: mcast::UnderlayMulticastIpv6, diff --git a/dpd-api/src/v2.rs b/dpd-api/src/v2.rs index 4d6e37f1..03d3b1a1 100644 --- a/dpd-api/src/v2.rs +++ b/dpd-api/src/v2.rs @@ -4,9 +4,11 @@ // // Copyright 2026 Oxide Computer Company -//! Types from API version 2 that changed in version 3. +//! Types from API version 2 (DUAL_STACK_NAT_WORKFLOW) that changed in +//! version 3 (MCAST_SOURCE_FILTER_ANY). //! -//! The `IpSrc` enum changed from `{Exact, Subnet}` to `{Exact, Any}`. +//! Changes in v3: +//! - The `IpSrc` enum changed from `{Exact, Subnet}` to `{Exact, Any}`. use std::{fmt, net::IpAddr}; @@ -76,7 +78,7 @@ pub struct MulticastGroupCreateExternalEntry { /// 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 for validating update requests. Optional in v2. If not provided, /// tag validation is skipped. pub tag: Option, pub internal_forwarding: InternalForwarding, @@ -95,15 +97,15 @@ pub struct MulticastGroupExternalResponse { pub sources: Option>, } -/// Convert from API v4 response to v2 response. -impl From +/// Convert from v3 response to v2 response. +impl From for MulticastGroupExternalResponse { - fn from(resp: dpd_types::mcast::MulticastGroupExternalResponse) -> Self { + fn from(resp: crate::v3::MulticastGroupExternalResponse) -> Self { Self { group_ip: resp.group_ip, external_group_id: resp.external_group_id, - tag: Some(resp.tag), + tag: resp.tag, internal_forwarding: resp.internal_forwarding, external_forwarding: resp.external_forwarding, sources: resp @@ -113,6 +115,15 @@ impl From } } +/// Convert from v4 response to v2 response (chains through v3). +impl From + for MulticastGroupExternalResponse +{ + fn from(resp: dpd_types::mcast::MulticastGroupExternalResponse) -> Self { + crate::v3::MulticastGroupExternalResponse::from(resp).into() + } +} + /// Unified response type for operations that return mixed group types /// (API version 2). #[derive(Debug, Deserialize, Serialize, JsonSchema)] @@ -132,20 +143,25 @@ impl MulticastGroupResponse { } } -/// Convert from API v4 response to v2 response. -impl From for MulticastGroupResponse { - fn from(resp: dpd_types::mcast::MulticastGroupResponse) -> Self { +/// Convert from v3 response to v2 response. +impl From for MulticastGroupResponse { + fn from(resp: crate::v3::MulticastGroupResponse) -> Self { match resp { - dpd_types::mcast::MulticastGroupResponse::Underlay(u) => { - Self::Underlay(u.into()) - } - dpd_types::mcast::MulticastGroupResponse::External(e) => { + crate::v3::MulticastGroupResponse::Underlay(u) => Self::Underlay(u), + crate::v3::MulticastGroupResponse::External(e) => { Self::External(e.into()) } } } } +/// Convert from v4 response to v2 response (chains through v3). +impl From for MulticastGroupResponse { + fn from(resp: dpd_types::mcast::MulticastGroupResponse) -> Self { + crate::v3::MulticastGroupResponse::from(resp).into() + } +} + // ============================================================================ // v2 → v3 conversions (for request types) // ============================================================================ diff --git a/dpd-api/src/v3.rs b/dpd-api/src/v3.rs index bff9f0ee..d169af3d 100644 --- a/dpd-api/src/v3.rs +++ b/dpd-api/src/v3.rs @@ -4,9 +4,10 @@ // // Copyright 2026 Oxide Computer Company -//! Types from API version 3 that changed in version 4. +//! Types from API version 3 (MCAST_SOURCE_FILTER_ANY) that changed in +//! version 4 (MCAST_STRICT_UNDERLAY). //! -//! Changes in v4 (MCAST_STRICT_UNDERLAY): +//! Changes in v4: //! - 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. @@ -206,7 +207,7 @@ impl From for MulticastGroupResponse { /// 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 for validating update requests. Optional in v3. If not provided, /// tag validation is skipped. pub tag: Option, pub members: Vec, @@ -228,7 +229,7 @@ impl From /// 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 for validating update requests. Optional in v3. If not provided, /// tag validation is skipped. pub tag: Option, pub internal_forwarding: InternalForwarding, @@ -255,3 +256,14 @@ impl From pub struct MulticastUnderlayGroupIpParam { pub group_ip: AdminScopedIpv6, } + +/// Request body for creating underlay multicast groups (API version 3). +/// +/// Uses `AdminScopedIpv6` which accepts the broader ff04::/16 range. In v4+, +/// this was tightened to `UnderlayMulticastIpv6` (ff04::/64). +#[derive(Debug, Deserialize, Serialize, JsonSchema)] +pub struct MulticastGroupCreateUnderlayEntry { + pub group_ip: AdminScopedIpv6, + pub tag: Option, + pub members: Vec, +} diff --git a/openapi/dpd/dpd-3.0.0-27dfd0.json b/openapi/dpd/dpd-3.0.0-034efd.json similarity index 99% rename from openapi/dpd/dpd-3.0.0-27dfd0.json rename to openapi/dpd/dpd-3.0.0-034efd.json index 16d18479..c071d511 100644 --- a/openapi/dpd/dpd-3.0.0-27dfd0.json +++ b/openapi/dpd/dpd-3.0.0-034efd.json @@ -1375,6 +1375,7 @@ "/multicast/underlay-groups": { "post": { "summary": "Create an underlay (internal) multicast group configuration (API v1-v3).", + "description": "Accepts the broader ff04::/16 (admin-local) address range.", "operationId": "multicast_group_create_underlay_v3", "requestBody": { "content": { @@ -1409,7 +1410,7 @@ "/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.", + "description": "Accepts the broader ff04::/16 (admin-local) address range.", "operationId": "multicast_group_get_underlay_v3", "parameters": [ { @@ -1442,7 +1443,7 @@ }, "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.", + "description": "Accepts the broader ff04::/16 (admin-local) address range. Tags are optional. If omitted, the existing tag is used.", "operationId": "multicast_group_update_underlay_v3", "parameters": [ { @@ -7736,11 +7737,11 @@ ] }, "MulticastGroupCreateUnderlayEntry": { - "description": "A multicast group configuration for POST requests for internal (to the rack) groups.", + "description": "Request body for creating underlay multicast groups (API version 3).\n\nUses `AdminScopedIpv6` which accepts the broader ff04::/16 range. In v4+, this was tightened to `UnderlayMulticastIpv6` (ff04::/64).", "type": "object", "properties": { "group_ip": { - "$ref": "#/components/schemas/UnderlayMulticastIpv6" + "$ref": "#/components/schemas/AdminScopedIpv6" }, "members": { "type": "array", @@ -7750,7 +7751,6 @@ }, "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" } }, @@ -7984,7 +7984,7 @@ }, "tag": { "nullable": true, - "description": "Tag for validating update requests. Optional in v3; if not provided, tag validation is skipped.", + "description": "Tag for validating update requests. Optional in v3. If not provided, tag validation is skipped.", "type": "string" } }, @@ -8005,7 +8005,7 @@ }, "tag": { "nullable": true, - "description": "Tag for validating update requests. Optional in v3; if not provided, tag validation is skipped.", + "description": "Tag for validating update requests. Optional in v3. If not provided, tag validation is skipped.", "type": "string" } }, @@ -9522,11 +9522,6 @@ "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", diff --git a/openapi/dpd/dpd-4.0.0-7b2800.json b/openapi/dpd/dpd-4.0.0-04ac56.json similarity index 99% rename from openapi/dpd/dpd-4.0.0-7b2800.json rename to openapi/dpd/dpd-4.0.0-04ac56.json index f4270606..c090c68a 100644 --- a/openapi/dpd/dpd-4.0.0-7b2800.json +++ b/openapi/dpd/dpd-4.0.0-04ac56.json @@ -1396,7 +1396,6 @@ "/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": { @@ -1431,7 +1430,6 @@ "/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": [ { @@ -1464,7 +1462,7 @@ }, "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.", + "description": "The `tag` query parameter must match the group's existing tag.", "operationId": "multicast_group_update_underlay", "parameters": [ { diff --git a/openapi/dpd/dpd-latest.json b/openapi/dpd/dpd-latest.json index 872b94b0..c540aac7 120000 --- a/openapi/dpd/dpd-latest.json +++ b/openapi/dpd/dpd-latest.json @@ -1 +1 @@ -dpd-4.0.0-7b2800.json \ No newline at end of file +dpd-4.0.0-04ac56.json \ No newline at end of file From 85a98762b427749c8352ccb795010670bc4fa9a0 Mon Sep 17 00:00:00 2001 From: Zeeshan Lakhani Date: Sat, 17 Jan 2026 12:40:05 +0000 Subject: [PATCH 20/22] [openapi] update --- ....0.0-39189e.json => dpd-3.0.0-b02a88.json} | 84 +++++++++---------- 1 file changed, 41 insertions(+), 43 deletions(-) rename openapi/dpd/{dpd-3.0.0-39189e.json => dpd-3.0.0-b02a88.json} (98%) diff --git a/openapi/dpd/dpd-3.0.0-39189e.json b/openapi/dpd/dpd-3.0.0-b02a88.json similarity index 98% rename from openapi/dpd/dpd-3.0.0-39189e.json rename to openapi/dpd/dpd-3.0.0-b02a88.json index 9064f1ef..df5e1417 100644 --- a/openapi/dpd/dpd-3.0.0-39189e.json +++ b/openapi/dpd/dpd-3.0.0-b02a88.json @@ -13,7 +13,7 @@ "/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.", + "description": "This removes all data entirely.", "operationId": "reset_all", "responses": { "204": { @@ -31,7 +31,7 @@ "/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.", + "description": "This removes:\n\n- All ARP or NDP table entries. - All routes - All links on all switch ports", "operationId": "reset_all_tagged", "parameters": [ { @@ -459,7 +459,7 @@ "/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.", + "description": "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 switch port.", "operationId": "channels_list", "responses": { "200": { @@ -1248,8 +1248,9 @@ }, "/multicast/external-groups": { "post": { - "summary": "Create an external-only multicast group configuration (API v1-v2).", - "operationId": "multicast_group_create_external_v2", + "summary": "Create an external-only multicast group configuration.", + "description": "External-only groups are used for IPv4 and non-admin-scoped 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": { @@ -1282,9 +1283,9 @@ }, "/multicast/external-groups/{group_ip}": { "put": { - "summary": "Update an external-only multicast group configuration (API v1/v2).", - "description": "Tags are optional for backward compatibility.", - "operationId": "multicast_group_update_external_v2", + "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-scoped IPv6 multicast traffic that doesn't require replication infrastructure.", + "operationId": "multicast_group_update_external", "parameters": [ { "in": "path", @@ -1328,8 +1329,8 @@ }, "/multicast/groups": { "get": { - "summary": "List all multicast groups (API v1/v2).", - "operationId": "multicast_groups_list_v2", + "summary": "List all multicast groups.", + "operationId": "multicast_groups_list", "parameters": [ { "in": "query", @@ -1392,8 +1393,8 @@ }, "/multicast/groups/{group_ip}": { "get": { - "summary": "Get the multicast group configuration for a given group IP address (API v1-v2).", - "operationId": "multicast_group_get_v2", + "summary": "Get the multicast group configuration for a given group IP address.", + "operationId": "multicast_group_get", "parameters": [ { "in": "path", @@ -1425,8 +1426,8 @@ } }, "delete": { - "summary": "Delete a multicast group configuration by IP address (API versions 1-3).", - "operationId": "multicast_group_delete_v3", + "summary": "Delete a multicast group configuration by IP address.", + "operationId": "multicast_group_delete", "parameters": [ { "in": "path", @@ -1453,8 +1454,8 @@ }, "/multicast/tags/{tag}": { "get": { - "summary": "List all multicast groups with a given tag (API v1/v2).", - "operationId": "multicast_groups_list_by_tag_v2", + "summary": "List all multicast groups with a given tag.", + "operationId": "multicast_groups_list_by_tag", "parameters": [ { "in": "path", @@ -1508,9 +1509,8 @@ } }, "delete": { - "summary": "Delete all multicast groups (and associated routes) with a given tag", - "description": "(API versions 1-3).", - "operationId": "multicast_reset_by_tag_v3", + "summary": "Delete all multicast groups (and associated routes) with a given tag.", + "operationId": "multicast_reset_by_tag", "parameters": [ { "in": "path", @@ -1536,9 +1536,9 @@ }, "/multicast/underlay-groups": { "post": { - "summary": "Create an underlay (internal) multicast group configuration (API v1-v3).", - "description": "Accepts the broader ff04::/16 (admin-local) address range.", - "operationId": "multicast_group_create_underlay_v3", + "summary": "Create an underlay (internal) multicast group configuration.", + "description": "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.", + "operationId": "multicast_group_create_underlay", "requestBody": { "content": { "application/json": { @@ -1571,9 +1571,9 @@ }, "/multicast/underlay-groups/{group_ip}": { "get": { - "summary": "Get an underlay (internal) multicast group configuration (API v1-v3).", - "description": "Accepts the broader ff04::/16 (admin-local) address range.", - "operationId": "multicast_group_get_underlay_v3", + "summary": "Get an underlay (internal) multicast group configuration by admin-scoped", + "description": "IPv6 address.\n\nUnderlay groups handle admin-scoped IPv6 multicast traffic with replication infrastructure for external and underlay members.", + "operationId": "multicast_group_get_underlay", "parameters": [ { "in": "path", @@ -1604,9 +1604,9 @@ } }, "put": { - "summary": "Update an underlay (internal) multicast group configuration (API v1-v3).", - "description": "Accepts the broader ff04::/16 (admin-local) address range. Tags are optional. If omitted, the existing tag is used.", - "operationId": "multicast_group_update_underlay_v3", + "summary": "Update an underlay (internal) multicast group configuration for a given", + "description": "group IP address.\n\nUnderlay groups are used for admin-scoped IPv6 multicast traffic that requires replication infrastructure with external and underlay members.", + "operationId": "multicast_group_update_underlay", "parameters": [ { "in": "path", @@ -1650,7 +1650,7 @@ "/multicast/untagged": { "delete": { "summary": "Delete all multicast groups (and associated routes) without a tag.", - "operationId": "multicast_reset_untagged_v3", + "operationId": "multicast_reset_untagged", "responses": { "204": { "description": "successful deletion" @@ -5405,7 +5405,7 @@ "components": { "schemas": { "AdminScopedIpv6": { - "description": "A validated admin-local IPv6 multicast address (API version 4).\n\nIn v4, admin-local addresses are validated against ff04::/16 (scope 4). In v5+, this was renamed to `UnderlayMulticastIpv6` and tightened to ff04::/64 to match Omicron's underlay multicast subnet allocation.", + "description": "A validated admin-scoped IPv6 multicast address.\n\nAdmin-scoped addresses are ff04::/16, ff05::/16, or ff08::/16. These are used for internal/underlay multicast groups.", "type": "string", "format": "ipv6" }, @@ -6507,7 +6507,7 @@ ] }, "IpSrc": { - "description": "Source filter match key for multicast traffic (API versions 1, 2, and 3).\n\nThis is the original `IpSrc` enum that used a single `Subnet` variant (IPv4 only) rather than the `Any` variant added in version 4.", + "description": "Source filter match key for multicast traffic.", "oneOf": [ { "description": "Exact match for the source IP address.", @@ -7474,7 +7474,7 @@ ] }, "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", + "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.", "type": "integer", "format": "uint8", "minimum": 0 @@ -7955,7 +7955,7 @@ } }, "MulticastGroupCreateExternalEntry": { - "description": "A multicast group configuration for POST requests for external (to the rack) groups (API version 3).", + "description": "A multicast group configuration for POST requests for external (to the rack) groups.", "type": "object", "properties": { "external_forwarding": { @@ -7987,7 +7987,7 @@ ] }, "MulticastGroupCreateUnderlayEntry": { - "description": "Request body for creating underlay multicast groups (API version 4).\n\nUses `AdminScopedIpv6` which accepts the broader ff04::/16 range. In v4+, this was tightened to `UnderlayMulticastIpv6` (ff04::/64).", + "description": "A multicast group configuration for POST requests for internal (to the rack) groups.", "type": "object", "properties": { "group_ip": { @@ -8010,7 +8010,7 @@ ] }, "MulticastGroupExternalResponse": { - "description": "Response structure for external multicast group operations (API version 3).", + "description": "Response structure for external multicast group operations. These groups handle IPv4 and non-admin IPv6 multicast via NAT targets.", "type": "object", "properties": { "external_forwarding": { @@ -8068,10 +8068,10 @@ ] }, "MulticastGroupResponse": { - "description": "Unified response type for operations that return mixed group types (API version 3).", + "description": "Unified response type for operations that return mixed group types.", "oneOf": [ { - "description": "Response structure for underlay/internal multicast group operations (API version 4).", + "description": "Response structure for underlay/internal multicast group operations. These groups handle admin-scoped IPv6 multicast with full replication.", "type": "object", "properties": { "external_group_id": { @@ -8113,7 +8113,7 @@ ] }, { - "description": "Response structure for external multicast group operations (API version 3).", + "description": "Response structure for external multicast group operations. These groups handle IPv4 and non-admin IPv6 multicast via NAT targets.", "type": "object", "properties": { "external_forwarding": { @@ -8181,7 +8181,7 @@ ] }, "MulticastGroupUnderlayResponse": { - "description": "Response structure for underlay/internal multicast group operations (API version 4).", + "description": "Response structure for underlay/internal multicast group operations. These groups handle admin-scoped IPv6 multicast with full replication.", "type": "object", "properties": { "external_group_id": { @@ -8216,7 +8216,7 @@ ] }, "MulticastGroupUpdateExternalEntry": { - "description": "A multicast group update entry for PUT requests for external (to the rack) groups (API version 3).\n\nTag validation is optional in v3 for backward compatibility.", + "description": "A multicast group update entry for PUT requests for external (to the rack) groups.", "type": "object", "properties": { "external_forwarding": { @@ -8234,7 +8234,6 @@ }, "tag": { "nullable": true, - "description": "Tag for validating update requests. Optional in v3. If not provided, tag validation is skipped.", "type": "string" } }, @@ -8244,7 +8243,7 @@ ] }, "MulticastGroupUpdateUnderlayEntry": { - "description": "A multicast group update entry for PUT requests for internal groups (API version 4).\n\nTags are optional in v4 for backward compatibility. If not provided, the existing tag is preserved.", + "description": "Represents a multicast replication entry for PUT requests for internal (to the rack) groups.", "type": "object", "properties": { "members": { @@ -8255,7 +8254,6 @@ }, "tag": { "nullable": true, - "description": "Tag for validating update requests. Optional in v4. If not provided, tag validation is skipped.", "type": "string" } }, From c7d2f4c7bb30031215560f7a4051798ad5943046 Mon Sep 17 00:00:00 2001 From: Zeeshan Lakhani Date: Sat, 17 Jan 2026 13:19:01 +0000 Subject: [PATCH 21/22] [test] update size --- dpd-client/tests/integration_tests/table_tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dpd-client/tests/integration_tests/table_tests.rs b/dpd-client/tests/integration_tests/table_tests.rs index 86c9af8f..a756623f 100644 --- a/dpd-client/tests/integration_tests/table_tests.rs +++ b/dpd-client/tests/integration_tests/table_tests.rs @@ -44,7 +44,7 @@ const ADMIN_LOCAL_PREFIX: u16 = 0xFF04; // investigating. If it only changes by an entry or two, it's fine to just // adjust the constant below to match the observed result. // -const IPV4_LPM_SIZE: usize = 8191; // ipv4 forwarding table +const IPV4_LPM_SIZE: usize = 8190; // 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 From c3c1aa1bfd67f44e3d9dfa7fb68cc8e32f7637a1 Mon Sep 17 00:00:00 2001 From: Zeeshan Lakhani Date: Sat, 17 Jan 2026 14:12:10 +0000 Subject: [PATCH 22/22] [api] conversoin cleanup / conciseness --- dpd-api/src/lib.rs | 109 +++++++++++++++++---------------------------- 1 file changed, 42 insertions(+), 67 deletions(-) diff --git a/dpd-api/src/lib.rs b/dpd-api/src/lib.rs index 73ccfd56..0f9e4bf6 100644 --- a/dpd-api/src/lib.rs +++ b/dpd-api/src/lib.rs @@ -1570,12 +1570,9 @@ pub trait DpdApi { HttpResponseCreated, HttpError, > { - match Self::multicast_group_create_external(rqctx, group).await { - Ok(HttpResponseCreated(resp)) => { - Ok(HttpResponseCreated(resp.into())) - } - Err(e) => Err(e), - } + Self::multicast_group_create_external(rqctx, group) + .await + .map(|resp| resp.map(Into::into)) } /// Create an external-only multicast group configuration (API v1-v2). @@ -1591,17 +1588,9 @@ pub trait DpdApi { 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), - } + Self::multicast_group_create_external(rqctx, group.map(Into::into)) + .await + .map(|resp| resp.map(Into::into)) } /// Create an underlay (internal) multicast group configuration. @@ -1644,12 +1633,9 @@ pub trait DpdApi { }) }) .map_err(|e: String| HttpError::for_bad_request(None, e))?; - match Self::multicast_group_create_underlay(rqctx, v4_body).await { - Ok(HttpResponseCreated(resp)) => { - Ok(HttpResponseCreated(resp.into())) - } - Err(e) => Err(e), - } + Self::multicast_group_create_underlay(rqctx, v4_body) + .await + .map(|resp| resp.map(Into::into)) } /** @@ -1716,10 +1702,9 @@ pub trait DpdApi { 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), - } + Self::multicast_group_get(rqctx, path) + .await + .map(|resp| resp.map(Into::into)) } /// Get the multicast group configuration for a given group IP address (API v1-v2). @@ -1732,10 +1717,9 @@ pub trait DpdApi { 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), - } + Self::multicast_group_get(rqctx, path) + .await + .map(|resp| resp.map(Into::into)) } /// Get an underlay (internal) multicast group configuration. @@ -1773,10 +1757,9 @@ pub trait DpdApi { }) })?; - match Self::multicast_group_get_underlay(rqctx, v4_path).await { - Ok(HttpResponseOk(resp)) => Ok(HttpResponseOk(resp.into())), - Err(e) => Err(e), - } + Self::multicast_group_get_underlay(rqctx, v4_path) + .await + .map(|resp| resp.map(Into::into)) } /// Update an underlay (internal) multicast group configuration. @@ -1900,13 +1883,12 @@ pub trait DpdApi { 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), - } + let HttpResponseOk(page) = + Self::multicast_groups_list(rqctx, query_params).await?; + Ok(HttpResponseOk(ResultsPage { + items: page.items.into_iter().map(Into::into).collect(), + next_page: page.next_page, + })) } /// List all multicast groups (API v1/v2). @@ -1924,13 +1906,12 @@ pub trait DpdApi { 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), - } + let HttpResponseOk(page) = + Self::multicast_groups_list(rqctx, query_params).await?; + Ok(HttpResponseOk(ResultsPage { + items: page.items.into_iter().map(Into::into).collect(), + next_page: page.next_page, + })) } /** @@ -1972,19 +1953,16 @@ pub trait DpdApi { HttpResponseOk>, HttpError, > { - match Self::multicast_groups_list_by_tag( + let HttpResponseOk(page) = 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), - } + .await?; + Ok(HttpResponseOk(ResultsPage { + items: page.items.into_iter().map(Into::into).collect(), + next_page: page.next_page, + })) } /// List all multicast groups with a given tag (API v1/v2). @@ -2003,19 +1981,16 @@ pub trait DpdApi { HttpResponseOk>, HttpError, > { - match Self::multicast_groups_list_by_tag( + let HttpResponseOk(page) = 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), - } + .await?; + Ok(HttpResponseOk(ResultsPage { + items: page.items.into_iter().map(Into::into).collect(), + next_page: page.next_page, + })) } /**