diff --git a/.github/workflows/hakari.yml b/.github/workflows/hakari.yml index 4d9812a44e..0627ea1563 100644 --- a/.github/workflows/hakari.yml +++ b/.github/workflows/hakari.yml @@ -24,7 +24,7 @@ jobs: with: toolchain: stable - name: Install cargo-hakari - uses: taiki-e/install-action@56ab7930c591507f833cbaed864d201386d518a8 # v2 + uses: taiki-e/install-action@115b656342518960cf3dbf5c01f62b684985ca11 # v2 with: tool: cargo-hakari - name: Check workspace-hack Cargo.toml is up-to-date diff --git a/Cargo.lock b/Cargo.lock index 85e42458d4..3e8ad7495b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -168,7 +168,7 @@ dependencies = [ "omicron-workspace-hack", "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.46", ] [[package]] @@ -275,7 +275,7 @@ checksum = "5fd55a5ba1179988837d24ab4c7cc8ed6efdeff578ede0416b4225a5fca35bd0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.46", ] [[package]] @@ -297,18 +297,18 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.46", ] [[package]] name = "async-trait" -version = "0.1.74" +version = "0.1.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9" +checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.46", ] [[package]] @@ -328,13 +328,13 @@ checksum = "1181e1e0d1fce796a03db1ae795d67167da795f9cf4a39c37589e85ef57f26d3" [[package]] name = "atomicwrites" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4d45f362125ed144544e57b0ec6de8fd6a296d41a6252fc4a20c0cf12e9ed3a" +checksum = "fc7b2dbe9169059af0f821e811180fddc971fc210c776c133c7819ccd6e478db" dependencies = [ "rustix 0.38.25", "tempfile", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -359,7 +359,7 @@ dependencies = [ "quote", "serde", "serde_tokenstream 0.2.0", - "syn 2.0.32", + "syn 2.0.46", ] [[package]] @@ -496,7 +496,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.32", + "syn 2.0.46", "which", ] @@ -1001,7 +1001,7 @@ dependencies = [ "heck 0.4.1", "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.46", ] [[package]] @@ -1443,7 +1443,7 @@ checksum = "83fdaf97f4804dcebfa5862639bc9ce4121e82140bec2a987ac5140294865b5b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.46", ] [[package]] @@ -1491,7 +1491,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.10.0", - "syn 2.0.32", + "syn 2.0.46", ] [[package]] @@ -1513,7 +1513,7 @@ checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5" dependencies = [ "darling_core 0.20.3", "quote", - "syn 2.0.32", + "syn 2.0.46", ] [[package]] @@ -1545,7 +1545,7 @@ dependencies = [ "quote", "serde", "serde_tokenstream 0.2.0", - "syn 2.0.32", + "syn 2.0.46", ] [[package]] @@ -1597,7 +1597,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.46", ] [[package]] @@ -1630,7 +1630,7 @@ checksum = "5fe87ce4529967e0ba1dcf8450bab64d97dfd5010a6256187ffe2e43e6f0e049" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.46", ] [[package]] @@ -1644,13 +1644,13 @@ dependencies = [ [[package]] name = "derive-where" -version = "1.2.6" +version = "1.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48d9b1fc2a6d7e19c89e706a3769e31ee862ac7a4c810c7c0ff3910e1a42a4ce" +checksum = "62d671cc41a825ebabc75757b62d3d168c577f9149b2d49ece1dad1f72119d25" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.46", ] [[package]] @@ -1737,7 +1737,7 @@ dependencies = [ "diesel_table_macro_syntax", "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.46", ] [[package]] @@ -1746,7 +1746,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc5557efc453706fed5e4fa85006fe9817c224c3f480a34c7e5959fd700921c5" dependencies = [ - "syn 2.0.32", + "syn 2.0.46", ] [[package]] @@ -1989,7 +1989,7 @@ dependencies = [ "quote", "serde", "serde_tokenstream 0.2.0", - "syn 2.0.32", + "syn 2.0.46", ] [[package]] @@ -2355,7 +2355,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.46", ] [[package]] @@ -2418,9 +2418,9 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" [[package]] name = "futures" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0290714b38af9b4a7b094b8a37086d1b4e61f2df9122c3cad2577669145335" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" dependencies = [ "futures-channel", "futures-core", @@ -2433,9 +2433,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff4dd66668b557604244583e3e1e1eada8c5c2e96a6d0d6653ede395b78bbacb" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" dependencies = [ "futures-core", "futures-sink", @@ -2443,15 +2443,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" [[package]] name = "futures-executor" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f4fb8693db0cf099eadcca0efe2a5a22e4550f98ed16aba6c48700da29597bc" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" dependencies = [ "futures-core", "futures-task", @@ -2460,32 +2460,32 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bf34a163b5c4c52d0478a4d757da8fb65cabef42ba90515efee0f6f9fa45aaa" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" [[package]] name = "futures-macro" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.46", ] [[package]] name = "futures-sink" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" [[package]] name = "futures-task" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" [[package]] name = "futures-timer" @@ -2495,9 +2495,9 @@ checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c" [[package]] name = "futures-util" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ "futures-channel", "futures-core", @@ -2892,9 +2892,9 @@ checksum = "b4ba82c000837f4e74df01a5520f0dc48735d4aed955a99eae4428bab7cf3acd" [[package]] name = "hkdf" -version = "0.12.3" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "791a029f6b9fc27657f6f188ec6e5e43f6911f6f878e0dc5501396e09809d437" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" dependencies = [ "hmac", ] @@ -3600,7 +3600,7 @@ version = "0.1.0" source = "git+https://github.com/oxidecomputer/opte?rev=4e6e6ab6379fa4bc40f5d0c7340b9f35c45ad6e4#4e6e6ab6379fa4bc40f5d0c7340b9f35c45ad6e4" dependencies = [ "quote", - "syn 2.0.32", + "syn 2.0.46", ] [[package]] @@ -3721,9 +3721,9 @@ dependencies = [ [[package]] name = "libsw" -version = "3.3.0" +version = "3.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "610cd929d24f634af855498b575263c44d541a0e28c21d595968a6e25fe190f9" +checksum = "0673364c1ef7a1674241dbad9ba2415354103d6126451f01eeb7aaa25d6b4fce" dependencies = [ "tokio", ] @@ -4003,7 +4003,7 @@ dependencies = [ "cfg-if", "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.46", ] [[package]] @@ -4186,6 +4186,7 @@ dependencies = [ "pem", "petgraph", "pq-sys", + "pretty_assertions", "rand 0.8.5", "rcgen", "ref-cast", @@ -4229,12 +4230,18 @@ dependencies = [ "base64", "chrono", "expectorate", + "futures", "gateway-client", "gateway-messages", "gateway-test-utils", "nexus-types", + "omicron-common", + "omicron-sled-agent", "omicron-workspace-hack", "regex", + "reqwest", + "serde_json", + "sled-agent-client", "slog", "strum", "thiserror", @@ -4298,7 +4305,7 @@ version = "0.1.0" dependencies = [ "omicron-workspace-hack", "quote", - "syn 2.0.32", + "syn 2.0.46", ] [[package]] @@ -4320,6 +4327,7 @@ dependencies = [ "schemars", "serde", "serde_json", + "sled-agent-client", "steno", "strum", "uuid", @@ -4449,7 +4457,7 @@ checksum = "9e6a0fd4f737c707bd9086cc16c925f294943eb62eb71499e9fd4cf71f8b9f4e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.46", ] [[package]] @@ -5162,7 +5170,7 @@ dependencies = [ "string_cache", "subtle", "syn 1.0.109", - "syn 2.0.32", + "syn 2.0.46", "time", "time-macros", "tokio", @@ -5275,7 +5283,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.46", ] [[package]] @@ -5558,7 +5566,7 @@ dependencies = [ "omicron-workspace-hack", "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.46", ] [[package]] @@ -5698,7 +5706,7 @@ dependencies = [ "regex", "regex-syntax 0.7.5", "structmeta", - "syn 2.0.32", + "syn 2.0.46", ] [[package]] @@ -5838,7 +5846,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.46", ] [[package]] @@ -5908,7 +5916,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.46", ] [[package]] @@ -6148,12 +6156,12 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae005bd773ab59b4725093fd7df83fd7892f7d8eafb48dbd7de6e024e4215f9d" +checksum = "a41cf62165e97c7f814d2221421dbb9afcbcdb0a88068e5ea206e19951c2cbb5" dependencies = [ "proc-macro2", - "syn 2.0.32", + "syn 2.0.46", ] [[package]] @@ -6201,9 +6209,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.69" +version = "1.0.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" +checksum = "2de98502f212cfcea8d0bb305bd0f49d7ebdd75b64ba0a68f937d888f4e0d6db" dependencies = [ "unicode-ident", ] @@ -6249,7 +6257,7 @@ dependencies = [ "schemars", "serde", "serde_json", - "syn 2.0.32", + "syn 2.0.46", "thiserror", "typify", "unicode-ident", @@ -6269,7 +6277,7 @@ dependencies = [ "serde_json", "serde_tokenstream 0.2.0", "serde_yaml", - "syn 2.0.32", + "syn 2.0.46", ] [[package]] @@ -6370,9 +6378,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.33" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ "proc-macro2", ] @@ -6645,7 +6653,7 @@ checksum = "7f7473c2cfcf90008193dd0e3e16599455cb601a9fce322b5bb55de799664925" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.46", ] [[package]] @@ -6892,7 +6900,7 @@ dependencies = [ "regex", "relative-path", "rustc_version 0.4.0", - "syn 2.0.32", + "syn 2.0.46", "unicode-ident", ] @@ -7411,7 +7419,7 @@ checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.46", ] [[package]] @@ -7472,7 +7480,7 @@ checksum = "8725e1dfadb3a50f7e5ce0b1a540466f6ed3fe7a0fca2ac2b8b831d31316bd00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.46", ] [[package]] @@ -7504,7 +7512,7 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.32", + "syn 2.0.46", ] [[package]] @@ -7545,7 +7553,7 @@ dependencies = [ "darling 0.20.3", "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.46", ] [[package]] @@ -7860,7 +7868,7 @@ source = "git+https://github.com/oxidecomputer/slog-error-chain?branch=main#15f6 dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.46", ] [[package]] @@ -8212,7 +8220,7 @@ dependencies = [ "proc-macro2", "quote", "structmeta-derive", - "syn 2.0.32", + "syn 2.0.46", ] [[package]] @@ -8223,7 +8231,7 @@ checksum = "a60bcaff7397072dca0017d1db428e30d5002e00b6847703e2e42005c95fbe00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.46", ] [[package]] @@ -8282,7 +8290,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.32", + "syn 2.0.46", ] [[package]] @@ -8330,9 +8338,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.32" +version = "2.0.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "239814284fd6f1a4ffe4ca893952cdd93c224b6a1571c9a9eadd670295c0c9e2" +checksum = "89456b690ff72fddcecf231caedbe615c59480c93358a93dfae7fc29e3ebbf0e" dependencies = [ "proc-macro2", "quote", @@ -8514,7 +8522,7 @@ dependencies = [ "proc-macro2", "quote", "structmeta", - "syn 2.0.32", + "syn 2.0.46", ] [[package]] @@ -8554,7 +8562,7 @@ checksum = "10712f02019e9288794769fba95cd6847df9874d49d871d062172f9dd41bc4cc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.46", ] [[package]] @@ -8741,7 +8749,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.46", ] [[package]] @@ -8975,7 +8983,7 @@ checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.46", ] [[package]] @@ -9220,7 +9228,7 @@ dependencies = [ "regress", "schemars", "serde_json", - "syn 2.0.32", + "syn 2.0.46", "thiserror", "unicode-ident", ] @@ -9236,7 +9244,7 @@ dependencies = [ "serde", "serde_json", "serde_tokenstream 0.2.0", - "syn 2.0.32", + "syn 2.0.46", "typify-impl", ] @@ -9324,9 +9332,9 @@ dependencies = [ [[package]] name = "unsafe-libyaml" -version = "0.2.9" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f28467d3e1d3c6586d8f25fa243f544f5800fec42d97032474e17222c2b75cfa" +checksum = "ab4c90930b95a82d00dc9e9ac071b4991924390d46cbd0dfe566148667605e4b" [[package]] name = "untrusted" @@ -9340,6 +9348,35 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +[[package]] +name = "update-common" +version = "0.1.0" +dependencies = [ + "anyhow", + "bytes", + "camino", + "camino-tempfile", + "clap 4.4.3", + "debug-ignore", + "display-error-chain", + "dropshot", + "futures", + "hex", + "hubtools", + "omicron-common", + "omicron-test-utils", + "omicron-workspace-hack", + "rand 0.8.5", + "sha2", + "slog", + "thiserror", + "tokio", + "tokio-util", + "tough", + "tufaceous", + "tufaceous-lib", +] + [[package]] name = "update-engine" version = "0.1.0" @@ -9597,7 +9634,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.46", "wasm-bindgen-shared", ] @@ -9631,7 +9668,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.46", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -9843,6 +9880,7 @@ dependencies = [ "trust-dns-resolver", "tufaceous", "tufaceous-lib", + "update-common", "update-engine", "uuid", "wicket", @@ -10239,7 +10277,7 @@ checksum = "56097d5b91d711293a42be9289403896b68654625021732067eac7a4ca388a1f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.46", ] [[package]] @@ -10250,7 +10288,7 @@ checksum = "b3c129550b3e6de3fd0ba67ba5c81818f9805e58b8d7fee80a3a59d2c9fc601a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.46", ] [[package]] @@ -10270,7 +10308,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.46", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index f7256ce8b4..bc5ba0bc45 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -62,6 +62,7 @@ members = [ "test-utils", "tufaceous-lib", "tufaceous", + "update-common", "update-engine", "wicket-common", "wicket-dbg", @@ -130,6 +131,7 @@ default-members = [ "test-utils", "tufaceous-lib", "tufaceous", + "update-common", "update-engine", "wicket-common", "wicket-dbg", @@ -145,8 +147,8 @@ approx = "0.5.1" assert_matches = "1.5.0" assert_cmd = "2.0.12" async-bb8-diesel = { git = "https://github.com/oxidecomputer/async-bb8-diesel", rev = "ed7ab5ef0513ba303d33efd41d3e9e381169d59b" } -async-trait = "0.1.74" -atomicwrites = "0.4.2" +async-trait = "0.1.77" +atomicwrites = "0.4.3" authz-macros = { path = "nexus/authz-macros" } backoff = { version = "0.4.0", features = [ "tokio" ] } base64 = "0.21.5" @@ -181,7 +183,7 @@ ddm-admin-client = { path = "clients/ddm-admin-client" } db-macros = { path = "nexus/db-macros" } debug-ignore = "1.0.5" derive_more = "0.99.17" -derive-where = "1.2.6" +derive-where = "1.2.7" diesel = { version = "2.1.4", features = ["postgres", "r2d2", "chrono", "serde_json", "network-address", "uuid"] } diesel-dtrace = { git = "https://github.com/oxidecomputer/diesel-dtrace", branch = "main" } dns-server = { path = "dns-server" } @@ -196,7 +198,7 @@ flate2 = "1.0.28" flume = "0.11.0" foreign-types = "0.3.2" fs-err = "2.11.0" -futures = "0.3.29" +futures = "0.3.30" gateway-client = { path = "clients/gateway-client" } gateway-messages = { git = "https://github.com/oxidecomputer/management-gateway-service", rev = "2739c18e80697aa6bc235c935176d14b4d757ee9", default-features = false, features = ["std"] } gateway-sp-comms = { git = "https://github.com/oxidecomputer/management-gateway-service", rev = "2739c18e80697aa6bc235c935176d14b4d757ee9" } @@ -208,7 +210,7 @@ heck = "0.4" hex = "0.4.3" hex-literal = "0.4.1" highway = "1.1.0" -hkdf = "0.12.3" +hkdf = "0.12.4" http = "0.2.11" httptest = "0.15.5" hubtools = { git = "https://github.com/oxidecomputer/hubtools.git", branch = "main" } @@ -289,7 +291,7 @@ postgres-protocol = "0.6.6" predicates = "3.0.4" pretty_assertions = "1.4.0" pretty-hex = "0.4.0" -prettyplease = "0.2.15" +prettyplease = "0.2.16" proc-macro2 = "1.0" progenitor = { git = "https://github.com/oxidecomputer/progenitor", branch = "main" } progenitor-client = { git = "https://github.com/oxidecomputer/progenitor", branch = "main" } @@ -357,7 +359,7 @@ strum = { version = "0.25", features = [ "derive" ] } subprocess = "0.2.9" supports-color = "2.1.0" swrite = "0.1.0" -libsw = { version = "3.3.0", features = ["tokio"] } +libsw = { version = "3.3.1", features = ["tokio"] } syn = { version = "2.0" } tabled = "0.14" tar = "0.4" @@ -386,6 +388,7 @@ trybuild = "1.0.85" tufaceous = { path = "tufaceous" } tufaceous-lib = { path = "tufaceous-lib" } unicode-width = "0.1.11" +update-common = { path = "update-common" } update-engine = { path = "update-engine" } usdt = "0.3" uuid = { version = "1.6.1", features = ["serde", "v4"] } diff --git a/clients/sled-agent-client/src/lib.rs b/clients/sled-agent-client/src/lib.rs index 0bbd27cf3e..ee2214c3c2 100644 --- a/clients/sled-agent-client/src/lib.rs +++ b/clients/sled-agent-client/src/lib.rs @@ -10,7 +10,7 @@ use uuid::Uuid; progenitor::generate_api!( spec = "../../openapi/sled-agent.json", - derives = [ schemars::JsonSchema ], + derives = [ schemars::JsonSchema, PartialEq ], inner_type = slog::Logger, pre_hook = (|log: &slog::Logger, request: &reqwest::Request| { slog::debug!(log, "client request"; @@ -25,7 +25,9 @@ progenitor::generate_api!( //TODO trade the manual transformations later in this file for the // replace directives below? replace = { - //Ipv4Network = ipnetwork::Ipv4Network, + ByteCount = omicron_common::api::external::ByteCount, + Generation = omicron_common::api::external::Generation, + Name = omicron_common::api::external::Name, SwitchLocation = omicron_common::api::external::SwitchLocation, Ipv6Network = ipnetwork::Ipv6Network, IpNetwork = ipnetwork::IpNetwork, @@ -34,6 +36,37 @@ progenitor::generate_api!( } ); +// We cannot easily configure progenitor to derive `Eq` on all the client- +// generated types because some have floats and other types that can't impl +// `Eq`. We impl it explicitly for a few types on which we need it. +impl Eq for types::OmicronZonesConfig {} +impl Eq for types::OmicronZoneConfig {} +impl Eq for types::OmicronZoneType {} +impl Eq for types::OmicronZoneDataset {} + +impl types::OmicronZoneType { + /// Human-readable label describing what kind of zone this is + /// + /// This is just use for testing and reporting. + pub fn label(&self) -> impl std::fmt::Display { + match self { + types::OmicronZoneType::BoundaryNtp { .. } => "boundary_ntp", + types::OmicronZoneType::Clickhouse { .. } => "clickhouse", + types::OmicronZoneType::ClickhouseKeeper { .. } => { + "clickhouse_keeper" + } + types::OmicronZoneType::CockroachDb { .. } => "cockroach_db", + types::OmicronZoneType::Crucible { .. } => "crucible", + types::OmicronZoneType::CruciblePantry { .. } => "crucible_pantry", + types::OmicronZoneType::ExternalDns { .. } => "external_dns", + types::OmicronZoneType::InternalDns { .. } => "internal_dns", + types::OmicronZoneType::InternalNtp { .. } => "internal_ntp", + types::OmicronZoneType::Nexus { .. } => "nexus", + types::OmicronZoneType::Oximeter { .. } => "oximeter", + } + } +} + impl omicron_common::api::external::ClientError for types::Error { fn message(&self) -> String { self.message.clone() @@ -50,7 +83,7 @@ impl From propolis_id: s.propolis_id, dst_propolis_id: s.dst_propolis_id, migration_id: s.migration_id, - gen: s.gen.into(), + gen: s.gen, time_updated: s.time_updated, } } @@ -84,18 +117,6 @@ impl From } } -impl From for types::ByteCount { - fn from(s: omicron_common::api::external::ByteCount) -> Self { - Self(s.to_bytes()) - } -} - -impl From for types::Generation { - fn from(s: omicron_common::api::external::Generation) -> Self { - Self(i64::from(&s) as u64) - } -} - impl From for omicron_common::api::internal::nexus::InstanceRuntimeState { @@ -104,7 +125,7 @@ impl From propolis_id: s.propolis_id, dst_propolis_id: s.dst_propolis_id, migration_id: s.migration_id, - gen: s.gen.into(), + gen: s.gen, time_updated: s.time_updated, } } @@ -114,11 +135,7 @@ impl From for omicron_common::api::internal::nexus::VmmRuntimeState { fn from(s: types::VmmRuntimeState) -> Self { - Self { - state: s.state.into(), - gen: s.gen.into(), - time_updated: s.time_updated, - } + Self { state: s.state.into(), gen: s.gen, time_updated: s.time_updated } } } @@ -162,25 +179,13 @@ impl From } } -impl From for omicron_common::api::external::ByteCount { - fn from(s: types::ByteCount) -> Self { - Self::try_from(s.0).unwrap_or_else(|e| panic!("{}: {}", s.0, e)) - } -} - -impl From for omicron_common::api::external::Generation { - fn from(s: types::Generation) -> Self { - Self::try_from(s.0 as i64).unwrap() - } -} - impl From for types::DiskRuntimeState { fn from(s: omicron_common::api::internal::nexus::DiskRuntimeState) -> Self { Self { disk_state: s.disk_state.into(), - gen: s.gen.into(), + gen: s.gen, time_updated: s.time_updated, } } @@ -212,7 +217,7 @@ impl From fn from(s: types::DiskRuntimeState) -> Self { Self { disk_state: s.disk_state.into(), - gen: s.gen.into(), + gen: s.gen, time_updated: s.time_updated, } } @@ -238,13 +243,6 @@ impl From for omicron_common::api::external::DiskState { } } -impl From<&omicron_common::api::external::Name> for types::Name { - fn from(s: &omicron_common::api::external::Name) -> Self { - Self::try_from(<&str>::from(s)) - .unwrap_or_else(|e| panic!("{}: {}", s, e)) - } -} - impl From for types::Vni { fn from(v: omicron_common::api::external::Vni) -> Self { Self(u32::from(v)) @@ -264,6 +262,12 @@ impl From for types::MacAddr { } } +impl From for omicron_common::api::external::MacAddr { + fn from(s: types::MacAddr) -> Self { + s.parse().unwrap() + } +} + impl From for types::Ipv4Net { fn from(n: omicron_common::api::external::Ipv4Net) -> Self { Self::try_from(n.to_string()).unwrap_or_else(|e| panic!("{}: {}", n, e)) @@ -292,6 +296,12 @@ impl From for types::Ipv4Net { } } +impl From for ipnetwork::Ipv4Network { + fn from(n: types::Ipv4Net) -> Self { + n.parse().unwrap() + } +} + impl From for types::Ipv4Network { fn from(n: ipnetwork::Ipv4Network) -> Self { Self::try_from(n.to_string()).unwrap_or_else(|e| panic!("{}: {}", n, e)) @@ -304,6 +314,12 @@ impl From for types::Ipv6Net { } } +impl From for ipnetwork::Ipv6Network { + fn from(n: types::Ipv6Net) -> Self { + n.parse().unwrap() + } +} + impl From for types::IpNet { fn from(n: ipnetwork::IpNetwork) -> Self { use ipnetwork::IpNetwork; @@ -314,6 +330,15 @@ impl From for types::IpNet { } } +impl From for ipnetwork::IpNetwork { + fn from(n: types::IpNet) -> Self { + match n { + types::IpNet::V4(v4) => ipnetwork::IpNetwork::V4(v4.into()), + types::IpNet::V6(v6) => ipnetwork::IpNetwork::V6(v6.into()), + } + } +} + impl From for types::Ipv4Net { fn from(n: std::net::Ipv4Addr) -> Self { Self::try_from(format!("{n}/32")) @@ -478,7 +503,7 @@ impl From Self { id: s.id, kind: s.kind.into(), - name: (&s.name).into(), + name: s.name, ip: s.ip, mac: s.mac.into(), subnet: s.subnet.into(), diff --git a/common/src/api/external/mod.rs b/common/src/api/external/mod.rs index 446152137a..3b05c58df3 100644 --- a/common/src/api/external/mod.rs +++ b/common/src/api/external/mod.rs @@ -525,7 +525,9 @@ impl JsonSchema for RoleName { // to serialize the value. // // TODO: custom JsonSchema and Deserialize impls to enforce i64::MAX limit -#[derive(Copy, Clone, Debug, Deserialize, Serialize, JsonSchema, PartialEq)] +#[derive( + Copy, Clone, Debug, Deserialize, Serialize, JsonSchema, PartialEq, Eq, +)] pub struct ByteCount(u64); #[allow(non_upper_case_globals)] diff --git a/dev-tools/omdb/src/bin/omdb/db.rs b/dev-tools/omdb/src/bin/omdb/db.rs index 15b16d515e..201f4fef54 100644 --- a/dev-tools/omdb/src/bin/omdb/db.rs +++ b/dev-tools/omdb/src/bin/omdb/db.rs @@ -2475,6 +2475,7 @@ async fn cmd_db_inventory_collections_show( inv_collection_print(&collection).await?; let nerrors = inv_collection_print_errors(&collection).await?; inv_collection_print_devices(&collection, &long_string_formatter).await?; + inv_collection_print_sleds(&collection); if nerrors > 0 { eprintln!( @@ -2706,6 +2707,58 @@ async fn inv_collection_print_devices( Ok(()) } +fn inv_collection_print_sleds(collection: &Collection) { + println!("SLED AGENTS"); + for sled in collection.sled_agents.values() { + println!( + "\nsled {} (role = {:?}, serial {})", + sled.sled_id, + sled.sled_role, + match &sled.baseboard_id { + Some(baseboard_id) => &baseboard_id.serial_number, + None => "unknown", + }, + ); + println!( + " found at: {} from {}", + sled.time_collected, sled.source + ); + println!(" address: {}", sled.sled_agent_address); + println!(" usable hw threads: {}", sled.usable_hardware_threads); + println!( + " usable memory (GiB): {}", + sled.usable_physical_ram.to_whole_gibibytes() + ); + println!( + " reservoir (GiB): {}", + sled.reservoir_size.to_whole_gibibytes() + ); + + if let Some(zones) = collection.omicron_zones.get(&sled.sled_id) { + println!( + " zones collected from {} at {}", + zones.source, zones.time_collected, + ); + println!( + " zones generation: {} (count: {})", + zones.zones.generation, + zones.zones.zones.len() + ); + + if zones.zones.zones.is_empty() { + continue; + } + + println!(" ZONES FOUND"); + for z in &zones.zones.zones { + println!(" zone {} (type {})", z.id, z.zone_type.label()); + } + } else { + println!(" warning: no zone information found"); + } + } +} + #[derive(Debug)] struct LongStringFormatter { show_long_strings: bool, diff --git a/nexus/db-model/src/address_lot.rs b/nexus/db-model/src/address_lot.rs index de5a4654c5..4fef2466e6 100644 --- a/nexus/db-model/src/address_lot.rs +++ b/nexus/db-model/src/address_lot.rs @@ -13,7 +13,7 @@ use uuid::Uuid; impl_enum_type!( #[derive(SqlType, Debug, Clone, Copy)] - #[diesel(postgres_type(name = "address_lot_kind"))] + #[diesel(postgres_type(name = "address_lot_kind", schema = "public"))] pub struct AddressLotKindEnum; #[derive( diff --git a/nexus/db-model/src/block_size.rs b/nexus/db-model/src/block_size.rs index 1a090f1e44..c947f85388 100644 --- a/nexus/db-model/src/block_size.rs +++ b/nexus/db-model/src/block_size.rs @@ -9,7 +9,7 @@ use serde::{Deserialize, Serialize}; impl_enum_type!( #[derive(SqlType, Debug, QueryId)] - #[diesel(postgres_type(name = "block_size"))] + #[diesel(postgres_type(name = "block_size", schema = "public"))] pub struct BlockSizeEnum; #[derive(Copy, Clone, Debug, AsExpression, FromSqlRow, Serialize, Deserialize, PartialEq)] diff --git a/nexus/db-model/src/bytecount.rs b/nexus/db-model/src/bytecount.rs index 9ea13956b7..92a01db43f 100644 --- a/nexus/db-model/src/bytecount.rs +++ b/nexus/db-model/src/bytecount.rs @@ -53,12 +53,6 @@ where } } -impl From for sled_agent_client::types::ByteCount { - fn from(b: ByteCount) -> Self { - Self(b.to_bytes()) - } -} - impl From for ByteCount { fn from(bs: BlockSize) -> Self { Self(bs.to_bytes().into()) diff --git a/nexus/db-model/src/dataset_kind.rs b/nexus/db-model/src/dataset_kind.rs index d068f48fd3..00317592e8 100644 --- a/nexus/db-model/src/dataset_kind.rs +++ b/nexus/db-model/src/dataset_kind.rs @@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize}; impl_enum_type!( #[derive(SqlType, Debug, QueryId)] - #[diesel(postgres_type(name = "dataset_kind"))] + #[diesel(postgres_type(name = "dataset_kind", schema = "public"))] pub struct DatasetKindEnum; #[derive(Clone, Debug, AsExpression, FromSqlRow, Serialize, Deserialize, PartialEq)] diff --git a/nexus/db-model/src/dns.rs b/nexus/db-model/src/dns.rs index 6b37362c42..56dd1e0547 100644 --- a/nexus/db-model/src/dns.rs +++ b/nexus/db-model/src/dns.rs @@ -16,7 +16,7 @@ use uuid::Uuid; impl_enum_type!( #[derive(SqlType, Debug)] - #[diesel(postgres_type(name = "dns_group"))] + #[diesel(postgres_type(name = "dns_group", schema = "public"))] pub struct DnsGroupEnum; #[derive(Clone, Copy, Debug, AsExpression, FromSqlRow, PartialEq)] diff --git a/nexus/db-model/src/external_ip.rs b/nexus/db-model/src/external_ip.rs index 62fa6393da..715df30f03 100644 --- a/nexus/db-model/src/external_ip.rs +++ b/nexus/db-model/src/external_ip.rs @@ -30,7 +30,7 @@ use uuid::Uuid; impl_enum_type!( #[derive(SqlType, Debug, Clone, Copy, QueryId)] - #[diesel(postgres_type(name = "ip_kind"))] + #[diesel(postgres_type(name = "ip_kind", schema = "public"))] pub struct IpKindEnum; #[derive(Clone, Copy, Debug, AsExpression, FromSqlRow, PartialEq, Deserialize, Serialize)] diff --git a/nexus/db-model/src/generation.rs b/nexus/db-model/src/generation.rs index b7e3a2b954..751cb98f3c 100644 --- a/nexus/db-model/src/generation.rs +++ b/nexus/db-model/src/generation.rs @@ -60,9 +60,3 @@ where .map_err(|e| e.into()) } } - -impl From for sled_agent_client::types::Generation { - fn from(g: Generation) -> Self { - Self(i64::from(&g.0) as u64) - } -} diff --git a/nexus/db-model/src/identity_provider.rs b/nexus/db-model/src/identity_provider.rs index 6bc55b3220..869d64bc7e 100644 --- a/nexus/db-model/src/identity_provider.rs +++ b/nexus/db-model/src/identity_provider.rs @@ -13,7 +13,7 @@ use uuid::Uuid; impl_enum_type!( #[derive(SqlType, Debug, QueryId)] - #[diesel(postgres_type(name = "provider_type"))] + #[diesel(postgres_type(name = "provider_type", schema = "public"))] pub struct IdentityProviderTypeEnum; #[derive(Copy, Clone, Debug, AsExpression, FromSqlRow, Serialize, Deserialize, PartialEq)] diff --git a/nexus/db-model/src/instance_state.rs b/nexus/db-model/src/instance_state.rs index 644474257a..dca809758f 100644 --- a/nexus/db-model/src/instance_state.rs +++ b/nexus/db-model/src/instance_state.rs @@ -11,7 +11,7 @@ use std::io::Write; impl_enum_wrapper!( #[derive(SqlType, Debug)] - #[diesel(postgres_type(name = "instance_state"))] + #[diesel(postgres_type(name = "instance_state", schema = "public"))] pub struct InstanceStateEnum; #[derive(Clone, Debug, PartialEq, AsExpression, FromSqlRow, Serialize, Deserialize)] diff --git a/nexus/db-model/src/inventory.rs b/nexus/db-model/src/inventory.rs index d94334787d..4e3e5fad56 100644 --- a/nexus/db-model/src/inventory.rs +++ b/nexus/db-model/src/inventory.rs @@ -6,10 +6,16 @@ use crate::schema::{ hw_baseboard_id, inv_caboose, inv_collection, inv_collection_error, - inv_root_of_trust, inv_root_of_trust_page, inv_service_processor, - sw_caboose, sw_root_of_trust_page, + inv_omicron_zone, inv_omicron_zone_nic, inv_root_of_trust, + inv_root_of_trust_page, inv_service_processor, inv_sled_agent, + inv_sled_omicron_zones, sw_caboose, sw_root_of_trust_page, }; -use crate::{impl_enum_type, SqlU16, SqlU32}; +use crate::{ + impl_enum_type, ipv6, ByteCount, Generation, MacAddr, Name, SqlU16, SqlU32, + SqlU8, +}; +use anyhow::{anyhow, ensure}; +use anyhow::{bail, Context}; use chrono::DateTime; use chrono::Utc; use diesel::backend::Backend; @@ -18,15 +24,18 @@ use diesel::expression::AsExpression; use diesel::pg::Pg; use diesel::serialize::ToSql; use diesel::{serialize, sql_types}; +use ipnetwork::IpNetwork; use nexus_types::inventory::{ - BaseboardId, Caboose, Collection, PowerState, RotPage, RotSlot, + BaseboardId, Caboose, Collection, OmicronZoneType, PowerState, RotPage, + RotSlot, }; +use std::net::SocketAddrV6; use uuid::Uuid; // See [`nexus_types::inventory::PowerState`]. impl_enum_type!( #[derive(SqlType, Debug, QueryId)] - #[diesel(postgres_type(name = "hw_power_state"))] + #[diesel(postgres_type(name = "hw_power_state", schema = "public"))] pub struct HwPowerStateEnum; #[derive(Copy, Clone, Debug, AsExpression, FromSqlRow, PartialEq)] @@ -62,7 +71,7 @@ impl From for PowerState { // See [`nexus_types::inventory::RotSlot`]. impl_enum_type!( #[derive(SqlType, Debug, QueryId)] - #[diesel(postgres_type(name = "hw_rot_slot"))] + #[diesel(postgres_type(name = "hw_rot_slot", schema = "public"))] pub struct HwRotSlotEnum; #[derive(Copy, Clone, Debug, AsExpression, FromSqlRow, PartialEq)] @@ -95,7 +104,7 @@ impl From for RotSlot { // See [`nexus_types::inventory::CabooseWhich`]. impl_enum_type!( #[derive(SqlType, Debug, QueryId)] - #[diesel(postgres_type(name = "caboose_which"))] + #[diesel(postgres_type(name = "caboose_which", schema = "public"))] pub struct CabooseWhichEnum; #[derive(Copy, Clone, Debug, AsExpression, FromSqlRow, PartialEq)] @@ -136,7 +145,7 @@ impl From for nexus_types::inventory::CabooseWhich { // See [`nexus_types::inventory::RotPageWhich`]. impl_enum_type!( #[derive(SqlType, Debug, QueryId)] - #[diesel(postgres_type(name = "root_of_trust_page_which"))] + #[diesel(postgres_type(name = "root_of_trust_page_which", schema = "public"))] pub struct RotPageWhichEnum; #[derive(Copy, Clone, Debug, AsExpression, FromSqlRow, PartialEq)] @@ -189,7 +198,7 @@ impl From for nexus_types::inventory::RotPageWhich { // See [`nexus_types::inventory::SpType`]. impl_enum_type!( #[derive(SqlType, Debug, QueryId)] - #[diesel(postgres_type(name = "sp_type"))] + #[diesel(postgres_type(name = "sp_type", schema = "public"))] pub struct SpTypeEnum; #[derive( @@ -538,3 +547,615 @@ pub struct InvRotPage { pub which: RotPageWhich, pub sw_root_of_trust_page_id: Uuid, } + +// See [`nexus_types::inventory::SledRole`]. +impl_enum_type!( + #[derive(SqlType, Debug, QueryId)] + #[diesel(postgres_type(name = "sled_role"))] + pub struct SledRoleEnum; + + #[derive( + Copy, + Clone, + Debug, + AsExpression, + FromSqlRow, + PartialOrd, + Ord, + PartialEq, + Eq + )] + #[diesel(sql_type = SledRoleEnum)] + pub enum SledRole; + + // Enum values + Gimlet => b"gimlet" + Scrimlet => b"scrimlet" +); + +impl From for SledRole { + fn from(value: nexus_types::inventory::SledRole) -> Self { + match value { + nexus_types::inventory::SledRole::Gimlet => SledRole::Gimlet, + nexus_types::inventory::SledRole::Scrimlet => SledRole::Scrimlet, + } + } +} + +impl From for nexus_types::inventory::SledRole { + fn from(value: SledRole) -> Self { + match value { + SledRole::Gimlet => nexus_types::inventory::SledRole::Gimlet, + SledRole::Scrimlet => nexus_types::inventory::SledRole::Scrimlet, + } + } +} + +/// See [`nexus_types::inventory::SledAgent`]. +#[derive(Queryable, Clone, Debug, Selectable, Insertable)] +#[diesel(table_name = inv_sled_agent)] +pub struct InvSledAgent { + pub inv_collection_id: Uuid, + pub time_collected: DateTime, + pub source: String, + pub sled_id: Uuid, + pub hw_baseboard_id: Option, + pub sled_agent_ip: ipv6::Ipv6Addr, + pub sled_agent_port: SqlU16, + pub sled_role: SledRole, + pub usable_hardware_threads: SqlU32, + pub usable_physical_ram: ByteCount, + pub reservoir_size: ByteCount, +} + +impl InvSledAgent { + pub fn new_without_baseboard( + collection_id: Uuid, + sled_agent: &nexus_types::inventory::SledAgent, + ) -> Result { + // It's irritating to have to check this case at runtime. The challenge + // is that if this sled agent does have a baseboard id, we don't know + // what its (SQL) id is. The only way to get it is to query it from + // the database. As a result, the caller takes a wholly different code + // path for that case that doesn't even involve constructing one of + // these objects. (In fact, we never see the id in Rust.) + // + // To check this at compile time, we'd have to bifurcate + // `nexus_types::inventory::SledAgent` into an enum with two variants: + // one with a baseboard id and one without. This would muck up all the + // other consumers of this type, just for a highly database-specific + // concern. + if sled_agent.baseboard_id.is_some() { + Err(anyhow!( + "attempted to directly insert InvSledAgent with \ + non-null baseboard id" + )) + } else { + Ok(InvSledAgent { + inv_collection_id: collection_id, + time_collected: sled_agent.time_collected, + source: sled_agent.source.clone(), + sled_id: sled_agent.sled_id, + hw_baseboard_id: None, + sled_agent_ip: ipv6::Ipv6Addr::from( + *sled_agent.sled_agent_address.ip(), + ), + sled_agent_port: SqlU16(sled_agent.sled_agent_address.port()), + sled_role: SledRole::from(sled_agent.sled_role), + usable_hardware_threads: SqlU32( + sled_agent.usable_hardware_threads, + ), + usable_physical_ram: ByteCount::from( + sled_agent.usable_physical_ram, + ), + reservoir_size: ByteCount::from(sled_agent.reservoir_size), + }) + } + } +} + +/// See [`nexus_types::inventory::OmicronZonesFound`]. +#[derive(Queryable, Clone, Debug, Selectable, Insertable)] +#[diesel(table_name = inv_sled_omicron_zones)] +pub struct InvSledOmicronZones { + pub inv_collection_id: Uuid, + pub time_collected: DateTime, + pub source: String, + pub sled_id: Uuid, + pub generation: Generation, +} + +impl InvSledOmicronZones { + pub fn new( + inv_collection_id: Uuid, + zones_found: &nexus_types::inventory::OmicronZonesFound, + ) -> InvSledOmicronZones { + InvSledOmicronZones { + inv_collection_id, + time_collected: zones_found.time_collected, + source: zones_found.source.clone(), + sled_id: zones_found.sled_id, + generation: Generation(zones_found.zones.generation), + } + } + + pub fn into_uninit_zones_found( + self, + ) -> nexus_types::inventory::OmicronZonesFound { + nexus_types::inventory::OmicronZonesFound { + time_collected: self.time_collected, + source: self.source, + sled_id: self.sled_id, + zones: nexus_types::inventory::OmicronZonesConfig { + generation: *self.generation, + zones: Vec::new(), + }, + } + } +} + +impl_enum_type!( + #[derive(Clone, SqlType, Debug, QueryId)] + #[diesel(postgres_type(name = "zone_type"))] + pub struct ZoneTypeEnum; + + #[derive(Clone, Copy, Debug, Eq, AsExpression, FromSqlRow, PartialEq)] + #[diesel(sql_type = ZoneTypeEnum)] + pub enum ZoneType; + + // Enum values + BoundaryNtp => b"boundary_ntp" + Clickhouse => b"clickhouse" + ClickhouseKeeper => b"clickhouse_keeper" + CockroachDb => b"cockroach_db" + Crucible => b"crucible" + CruciblePantry => b"crucible_pantry" + ExternalDns => b"external_dns" + InternalDns => b"internal_dns" + InternalNtp => b"internal_ntp" + Nexus => b"nexus" + Oximeter => b"oximeter" +); + +/// See [`nexus_types::inventory::OmicronZoneConfig`]. +#[derive(Queryable, Clone, Debug, Selectable, Insertable)] +#[diesel(table_name = inv_omicron_zone)] +pub struct InvOmicronZone { + pub inv_collection_id: Uuid, + pub sled_id: Uuid, + pub id: Uuid, + pub underlay_address: ipv6::Ipv6Addr, + pub zone_type: ZoneType, + pub primary_service_ip: ipv6::Ipv6Addr, + pub primary_service_port: SqlU16, + pub second_service_ip: Option, + pub second_service_port: Option, + pub dataset_zpool_name: Option, + pub nic_id: Option, + pub dns_gz_address: Option, + pub dns_gz_address_index: Option, + pub ntp_ntp_servers: Option>, + pub ntp_dns_servers: Option>, + pub ntp_domain: Option, + pub nexus_external_tls: Option, + pub nexus_external_dns_servers: Option>, + pub snat_ip: Option, + pub snat_first_port: Option, + pub snat_last_port: Option, +} + +impl InvOmicronZone { + pub fn new( + inv_collection_id: Uuid, + sled_id: Uuid, + zone: &nexus_types::inventory::OmicronZoneConfig, + ) -> Result { + let id = zone.id; + let underlay_address = ipv6::Ipv6Addr::from(zone.underlay_address); + let mut nic_id = None; + let mut dns_gz_address = None; + let mut dns_gz_address_index = None; + let mut ntp_ntp_servers = None; + let mut ntp_dns_servers = None; + let mut ntp_ntp_domain = None; + let mut nexus_external_tls = None; + let mut nexus_external_dns_servers = None; + let mut snat_ip = None; + let mut snat_first_port = None; + let mut snat_last_port = None; + let mut second_service_ip = None; + let mut second_service_port = None; + + let (zone_type, primary_service_sockaddr_str, dataset) = match &zone + .zone_type + { + OmicronZoneType::BoundaryNtp { + address, + ntp_servers, + dns_servers, + domain, + nic, + snat_cfg, + } => { + ntp_ntp_servers = Some(ntp_servers.clone()); + ntp_dns_servers = Some(dns_servers.clone()); + ntp_ntp_domain = domain.clone(); + snat_ip = Some(IpNetwork::from(snat_cfg.ip)); + snat_first_port = Some(SqlU16::from(snat_cfg.first_port)); + snat_last_port = Some(SqlU16::from(snat_cfg.last_port)); + nic_id = Some(nic.id); + (ZoneType::BoundaryNtp, address, None) + } + OmicronZoneType::Clickhouse { address, dataset } => { + (ZoneType::Clickhouse, address, Some(dataset)) + } + OmicronZoneType::ClickhouseKeeper { address, dataset } => { + (ZoneType::ClickhouseKeeper, address, Some(dataset)) + } + OmicronZoneType::CockroachDb { address, dataset } => { + (ZoneType::CockroachDb, address, Some(dataset)) + } + OmicronZoneType::Crucible { address, dataset } => { + (ZoneType::Crucible, address, Some(dataset)) + } + OmicronZoneType::CruciblePantry { address } => { + (ZoneType::CruciblePantry, address, None) + } + OmicronZoneType::ExternalDns { + dataset, + http_address, + dns_address, + nic, + } => { + nic_id = Some(nic.id); + let sockaddr = dns_address + .parse::() + .with_context(|| { + format!( + "parsing address for external DNS server {:?}", + dns_address + ) + })?; + second_service_ip = Some(sockaddr.ip()); + second_service_port = Some(SqlU16::from(sockaddr.port())); + (ZoneType::ExternalDns, http_address, Some(dataset)) + } + OmicronZoneType::InternalDns { + dataset, + http_address, + dns_address, + gz_address, + gz_address_index, + } => { + dns_gz_address = Some(ipv6::Ipv6Addr::from(gz_address)); + dns_gz_address_index = Some(SqlU32::from(*gz_address_index)); + let sockaddr = dns_address + .parse::() + .with_context(|| { + format!( + "parsing address for internal DNS server {:?}", + dns_address + ) + })?; + second_service_ip = Some(sockaddr.ip()); + second_service_port = Some(SqlU16::from(sockaddr.port())); + (ZoneType::InternalDns, http_address, Some(dataset)) + } + OmicronZoneType::InternalNtp { + address, + ntp_servers, + dns_servers, + domain, + } => { + ntp_ntp_servers = Some(ntp_servers.clone()); + ntp_dns_servers = Some(dns_servers.clone()); + ntp_ntp_domain = domain.clone(); + (ZoneType::InternalNtp, address, None) + } + OmicronZoneType::Nexus { + internal_address, + external_ip, + nic, + external_tls, + external_dns_servers, + } => { + nic_id = Some(nic.id); + nexus_external_tls = Some(*external_tls); + nexus_external_dns_servers = Some(external_dns_servers.clone()); + second_service_ip = Some(*external_ip); + (ZoneType::Nexus, internal_address, None) + } + OmicronZoneType::Oximeter { address } => { + (ZoneType::Oximeter, address, None) + } + }; + + let dataset_zpool_name = + dataset.map(|d| d.pool_name.as_str().to_string()); + let primary_service_sockaddr = primary_service_sockaddr_str + .parse::() + .with_context(|| { + format!( + "parsing socket address for primary IP {:?}", + primary_service_sockaddr_str + ) + })?; + let (primary_service_ip, primary_service_port) = ( + ipv6::Ipv6Addr::from(*primary_service_sockaddr.ip()), + SqlU16::from(primary_service_sockaddr.port()), + ); + + Ok(InvOmicronZone { + inv_collection_id, + sled_id, + id, + underlay_address, + zone_type, + primary_service_ip, + primary_service_port, + second_service_ip: second_service_ip.map(IpNetwork::from), + second_service_port, + dataset_zpool_name, + nic_id, + dns_gz_address, + dns_gz_address_index, + ntp_ntp_servers, + ntp_dns_servers: ntp_dns_servers + .map(|list| list.into_iter().map(IpNetwork::from).collect()), + ntp_domain: ntp_ntp_domain, + nexus_external_tls, + nexus_external_dns_servers: nexus_external_dns_servers + .map(|list| list.into_iter().map(IpNetwork::from).collect()), + snat_ip, + snat_first_port, + snat_last_port, + }) + } + + pub fn into_omicron_zone_config( + self, + nic_row: Option, + ) -> Result { + let address = SocketAddrV6::new( + std::net::Ipv6Addr::from(self.primary_service_ip), + *self.primary_service_port, + 0, + 0, + ) + .to_string(); + + // Assemble a value that we can use to extract the NIC _if necessary_ + // and report an error if it was needed but not found. + // + // Any error here should be impossible. By the time we get here, the + // caller should have provided `nic_row` iff there's a corresponding + // `nic_id` in this row, and the ids should match up. And whoever + // created this row ought to have provided a nic_id iff this type of + // zone needs a NIC. This last issue is not under our control, though, + // so we definitely want to handle that as an operational error. The + // others could arguably be programmer errors (i.e., we could `assert`), + // but it seems excessive to crash here. + // + // Note that we immediately return for any of the caller errors here. + // For the other error, we will return only later, if some code path + // below tries to use `nic` when it's not present. + let nic = match (self.nic_id, nic_row) { + (Some(expected_id), Some(nic_row)) => { + ensure!(expected_id == nic_row.id, "caller provided wrong NIC"); + Ok(nic_row.into_network_interface_for_zone(self.id)) + } + (None, None) => Err(anyhow!( + "expected zone to have an associated NIC, but it doesn't" + )), + (Some(_), None) => bail!("caller provided no NIC"), + (None, Some(_)) => bail!("caller unexpectedly provided a NIC"), + }; + + // Similarly, assemble a value that we can use to extract the dataset, + // if necessary. We only return this error if code below tries to use + // this value. + let dataset = self + .dataset_zpool_name + .map(|zpool_name| -> Result<_, anyhow::Error> { + Ok(nexus_types::inventory::OmicronZoneDataset { + pool_name: zpool_name.parse().map_err(|e| { + anyhow!("parsing zpool name {:?}: {}", zpool_name, e) + })?, + }) + }) + .transpose()? + .ok_or_else(|| anyhow!("expected dataset zpool name, found none")); + + // Do the same for the DNS server address. + let dns_address = + match (self.second_service_ip, self.second_service_port) { + (Some(dns_ip), Some(dns_port)) => { + Ok(std::net::SocketAddr::new(dns_ip.ip(), *dns_port) + .to_string()) + } + _ => Err(anyhow!( + "expected second service IP and port, \ + found one missing" + )), + }; + + // Do the same for NTP zone properties. + let ntp_dns_servers = self + .ntp_dns_servers + .ok_or_else(|| anyhow!("expected list of DNS servers, found null")) + .map(|list| { + list.into_iter().map(|ipnetwork| ipnetwork.ip()).collect() + }); + let ntp_ntp_servers = + self.ntp_ntp_servers.ok_or_else(|| anyhow!("expected ntp_servers")); + + let zone_type = match self.zone_type { + ZoneType::BoundaryNtp => { + let snat_cfg = match ( + self.snat_ip, + self.snat_first_port, + self.snat_last_port, + ) { + (Some(ip), Some(first_port), Some(last_port)) => { + nexus_types::inventory::SourceNatConfig { + ip: ip.ip(), + first_port: *first_port, + last_port: *last_port, + } + } + _ => bail!( + "expected non-NULL snat properties, \ + found at least one NULL" + ), + }; + OmicronZoneType::BoundaryNtp { + address, + dns_servers: ntp_dns_servers?, + domain: self.ntp_domain, + nic: nic?, + ntp_servers: ntp_ntp_servers?, + snat_cfg, + } + } + ZoneType::Clickhouse => { + OmicronZoneType::Clickhouse { address, dataset: dataset? } + } + ZoneType::ClickhouseKeeper => { + OmicronZoneType::ClickhouseKeeper { address, dataset: dataset? } + } + ZoneType::CockroachDb => { + OmicronZoneType::CockroachDb { address, dataset: dataset? } + } + ZoneType::Crucible => { + OmicronZoneType::Crucible { address, dataset: dataset? } + } + ZoneType::CruciblePantry => { + OmicronZoneType::CruciblePantry { address } + } + ZoneType::ExternalDns => OmicronZoneType::ExternalDns { + dataset: dataset?, + dns_address: dns_address?, + http_address: address, + nic: nic?, + }, + ZoneType::InternalDns => OmicronZoneType::InternalDns { + dataset: dataset?, + dns_address: dns_address?, + http_address: address, + gz_address: *self.dns_gz_address.ok_or_else(|| { + anyhow!("expected dns_gz_address, found none") + })?, + gz_address_index: *self.dns_gz_address_index.ok_or_else( + || anyhow!("expected dns_gz_address_index, found none"), + )?, + }, + ZoneType::InternalNtp => OmicronZoneType::InternalNtp { + address, + dns_servers: ntp_dns_servers?, + domain: self.ntp_domain, + ntp_servers: ntp_ntp_servers?, + }, + ZoneType::Nexus => OmicronZoneType::Nexus { + internal_address: address, + nic: nic?, + external_tls: self + .nexus_external_tls + .ok_or_else(|| anyhow!("expected 'external_tls'"))?, + external_ip: self + .second_service_ip + .ok_or_else(|| anyhow!("expected second service IP"))? + .ip(), + external_dns_servers: self + .nexus_external_dns_servers + .ok_or_else(|| anyhow!("expected 'external_dns_servers'"))? + .into_iter() + .map(|i| i.ip()) + .collect(), + }, + ZoneType::Oximeter => OmicronZoneType::Oximeter { address }, + }; + Ok(nexus_types::inventory::OmicronZoneConfig { + id: self.id, + underlay_address: std::net::Ipv6Addr::from(self.underlay_address), + zone_type, + }) + } +} + +#[derive(Queryable, Clone, Debug, Selectable, Insertable)] +#[diesel(table_name = inv_omicron_zone_nic)] +pub struct InvOmicronZoneNic { + inv_collection_id: Uuid, + pub id: Uuid, + name: Name, + ip: IpNetwork, + mac: MacAddr, + subnet: IpNetwork, + vni: SqlU32, + is_primary: bool, + slot: SqlU8, +} + +impl InvOmicronZoneNic { + pub fn new( + inv_collection_id: Uuid, + zone: &nexus_types::inventory::OmicronZoneConfig, + ) -> Result, anyhow::Error> { + match &zone.zone_type { + OmicronZoneType::ExternalDns { nic, .. } + | OmicronZoneType::BoundaryNtp { nic, .. } + | OmicronZoneType::Nexus { nic, .. } => { + // We do not bother storing the NIC's kind and associated id + // because it should be inferrable from the other information + // that we have. Verify that here. + ensure!( + matches!( + nic.kind, + nexus_types::inventory::NetworkInterfaceKind::Service( + id + ) if id == zone.id + ), + "expected zone's NIC kind to be \"service\" and the \ + id to match the zone's id ({})", + zone.id + ); + + Ok(Some(InvOmicronZoneNic { + inv_collection_id, + id: nic.id, + name: Name::from(nic.name.clone()), + ip: IpNetwork::from(nic.ip), + mac: MacAddr::from( + omicron_common::api::external::MacAddr::from( + nic.mac.clone(), + ), + ), + subnet: IpNetwork::from(nic.subnet.clone()), + vni: SqlU32::from(nic.vni.0), + is_primary: nic.primary, + slot: SqlU8::from(nic.slot), + })) + } + _ => Ok(None), + } + } + + pub fn into_network_interface_for_zone( + self, + zone_id: Uuid, + ) -> nexus_types::inventory::NetworkInterface { + nexus_types::inventory::NetworkInterface { + id: self.id, + ip: self.ip.ip(), + kind: nexus_types::inventory::NetworkInterfaceKind::Service( + zone_id, + ), + mac: (*self.mac).into(), + name: self.name.into(), + primary: self.is_primary, + slot: *self.slot, + vni: nexus_types::inventory::Vni::from(*self.vni), + subnet: self.subnet.into(), + } + } +} diff --git a/nexus/db-model/src/lib.rs b/nexus/db-model/src/lib.rs index 2c3433b2d3..6b89e5a270 100644 --- a/nexus/db-model/src/lib.rs +++ b/nexus/db-model/src/lib.rs @@ -35,7 +35,7 @@ mod instance_state; mod inventory; mod ip_pool; mod ipv4net; -mod ipv6; +pub mod ipv6; mod ipv6net; mod l4_port_range; mod macaddr; diff --git a/nexus/db-model/src/network_interface.rs b/nexus/db-model/src/network_interface.rs index ada2148516..3d3fabbe66 100644 --- a/nexus/db-model/src/network_interface.rs +++ b/nexus/db-model/src/network_interface.rs @@ -19,7 +19,7 @@ use uuid::Uuid; impl_enum_type! { #[derive(SqlType, QueryId, Debug, Clone, Copy)] - #[diesel(postgres_type(name = "network_interface_kind"))] + #[diesel(postgres_type(name = "network_interface_kind", schema = "public"))] pub struct NetworkInterfaceKindEnum; #[derive(Clone, Copy, Debug, AsExpression, FromSqlRow, PartialEq)] diff --git a/nexus/db-model/src/physical_disk_kind.rs b/nexus/db-model/src/physical_disk_kind.rs index a55d42beef..fe86c801d0 100644 --- a/nexus/db-model/src/physical_disk_kind.rs +++ b/nexus/db-model/src/physical_disk_kind.rs @@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize}; impl_enum_type!( #[derive(Clone, SqlType, Debug, QueryId)] - #[diesel(postgres_type(name = "physical_disk_kind"))] + #[diesel(postgres_type(name = "physical_disk_kind", schema = "public"))] pub struct PhysicalDiskKindEnum; #[derive(Clone, Copy, Debug, AsExpression, FromSqlRow, Serialize, Deserialize, PartialEq)] diff --git a/nexus/db-model/src/producer_endpoint.rs b/nexus/db-model/src/producer_endpoint.rs index f282f6f08f..55533690f1 100644 --- a/nexus/db-model/src/producer_endpoint.rs +++ b/nexus/db-model/src/producer_endpoint.rs @@ -12,7 +12,7 @@ use uuid::Uuid; impl_enum_type!( #[derive(SqlType, Copy, Clone, Debug, QueryId)] - #[diesel(postgres_type(name = "producer_kind"))] + #[diesel(postgres_type(name = "producer_kind", schema = "public"))] pub struct ProducerKindEnum; #[derive(AsExpression, Copy, Clone, Debug, FromSqlRow, PartialEq)] diff --git a/nexus/db-model/src/role_assignment.rs b/nexus/db-model/src/role_assignment.rs index 45b0c65e37..fbbe18579e 100644 --- a/nexus/db-model/src/role_assignment.rs +++ b/nexus/db-model/src/role_assignment.rs @@ -12,7 +12,7 @@ use uuid::Uuid; impl_enum_type!( #[derive(SqlType, Debug, QueryId)] - #[diesel(postgres_type(name = "identity_type"))] + #[diesel(postgres_type(name = "identity_type", schema = "public"))] pub struct IdentityTypeEnum; #[derive( diff --git a/nexus/db-model/src/saga_types.rs b/nexus/db-model/src/saga_types.rs index f2a8b57659..bb21e803bc 100644 --- a/nexus/db-model/src/saga_types.rs +++ b/nexus/db-model/src/saga_types.rs @@ -140,7 +140,7 @@ where } #[derive(Clone, Copy, Debug, PartialEq, SqlType)] -#[diesel(postgres_type(name = "saga_state"))] +#[diesel(postgres_type(name = "saga_state", schema = "public"))] pub struct SagaCachedStateEnum; /// Newtype wrapper around [`steno::SagaCachedState`] which implements diff --git a/nexus/db-model/src/schema.rs b/nexus/db-model/src/schema.rs index 7af74036b2..7a5a3428bc 100644 --- a/nexus/db-model/src/schema.rs +++ b/nexus/db-model/src/schema.rs @@ -13,7 +13,7 @@ use omicron_common::api::external::SemverVersion; /// /// This should be updated whenever the schema is changed. For more details, /// refer to: schema/crdb/README.adoc -pub const SCHEMA_VERSION: SemverVersion = SemverVersion::new(22, 0, 0); +pub const SCHEMA_VERSION: SemverVersion = SemverVersion::new(23, 0, 0); table! { disk (id) { @@ -1332,6 +1332,77 @@ table! { } } +table! { + inv_sled_agent (inv_collection_id, sled_id) { + inv_collection_id -> Uuid, + time_collected -> Timestamptz, + source -> Text, + sled_id -> Uuid, + + hw_baseboard_id -> Nullable, + + sled_agent_ip -> Inet, + sled_agent_port -> Int4, + sled_role -> crate::SledRoleEnum, + usable_hardware_threads -> Int8, + usable_physical_ram -> Int8, + reservoir_size -> Int8, + } +} + +table! { + inv_sled_omicron_zones (inv_collection_id, sled_id) { + inv_collection_id -> Uuid, + time_collected -> Timestamptz, + source -> Text, + sled_id -> Uuid, + + generation -> Int8, + } +} + +table! { + inv_omicron_zone (inv_collection_id, id) { + inv_collection_id -> Uuid, + sled_id -> Uuid, + + id -> Uuid, + underlay_address -> Inet, + zone_type -> crate::ZoneTypeEnum, + + primary_service_ip -> Inet, + primary_service_port -> Int4, + second_service_ip -> Nullable, + second_service_port -> Nullable, + dataset_zpool_name -> Nullable, + nic_id -> Nullable, + dns_gz_address -> Nullable, + dns_gz_address_index -> Nullable, + ntp_ntp_servers -> Nullable>, + ntp_dns_servers -> Nullable>, + ntp_domain -> Nullable, + nexus_external_tls -> Nullable, + nexus_external_dns_servers -> Nullable>, + snat_ip -> Nullable, + snat_first_port -> Nullable, + snat_last_port -> Nullable, + } +} + +table! { + inv_omicron_zone_nic (inv_collection_id, id) { + inv_collection_id -> Uuid, + id -> Uuid, + name -> Text, + ip -> Inet, + mac -> Int8, + subnet -> Inet, + vni -> Int8, + is_primary -> Bool, + slot -> Int2, + } +} + table! { bootstore_keys (key, generation) { key -> Text, @@ -1367,6 +1438,7 @@ allow_tables_to_appear_in_same_query!( sw_root_of_trust_page, inv_root_of_trust_page ); +allow_tables_to_appear_in_same_query!(hw_baseboard_id, inv_sled_agent,); allow_tables_to_appear_in_same_query!( dataset, diff --git a/nexus/db-model/src/service_kind.rs b/nexus/db-model/src/service_kind.rs index 4210c3ee20..016de9c44e 100644 --- a/nexus/db-model/src/service_kind.rs +++ b/nexus/db-model/src/service_kind.rs @@ -10,7 +10,7 @@ use strum::EnumIter; impl_enum_type!( #[derive(Clone, SqlType, Debug, QueryId)] - #[diesel(postgres_type(name = "service_kind"))] + #[diesel(postgres_type(name = "service_kind", schema = "public"))] pub struct ServiceKindEnum; #[derive(Clone, Copy, Debug, Eq, AsExpression, FromSqlRow, Serialize, Deserialize, PartialEq, EnumIter)] diff --git a/nexus/db-model/src/silo.rs b/nexus/db-model/src/silo.rs index 21d12cd7f1..66520fccb1 100644 --- a/nexus/db-model/src/silo.rs +++ b/nexus/db-model/src/silo.rs @@ -20,7 +20,7 @@ use uuid::Uuid; impl_enum_type!( #[derive(SqlType, Debug, QueryId)] - #[diesel(postgres_type(name = "authentication_mode"))] + #[diesel(postgres_type(name = "authentication_mode", schema = "public"))] pub struct AuthenticationModeEnum; #[derive(Copy, Clone, Debug, AsExpression, FromSqlRow, PartialEq, Eq)] @@ -52,7 +52,7 @@ impl From for shared::AuthenticationMode { impl_enum_type!( #[derive(SqlType, Debug, QueryId)] - #[diesel(postgres_type(name = "user_provision_type"))] + #[diesel(postgres_type(name = "user_provision_type", schema = "public"))] pub struct UserProvisionTypeEnum; #[derive(Copy, Clone, Debug, AsExpression, FromSqlRow, PartialEq, Eq)] diff --git a/nexus/db-model/src/sled_provision_state.rs b/nexus/db-model/src/sled_provision_state.rs index b2b1ee39dc..ada842a32f 100644 --- a/nexus/db-model/src/sled_provision_state.rs +++ b/nexus/db-model/src/sled_provision_state.rs @@ -9,7 +9,7 @@ use thiserror::Error; impl_enum_type!( #[derive(Clone, SqlType, Debug, QueryId)] - #[diesel(postgres_type(name = "sled_provision_state"))] + #[diesel(postgres_type(name = "sled_provision_state", schema = "public"))] pub struct SledProvisionStateEnum; #[derive(Clone, Copy, Debug, AsExpression, FromSqlRow, Serialize, Deserialize, PartialEq)] diff --git a/nexus/db-model/src/sled_resource_kind.rs b/nexus/db-model/src/sled_resource_kind.rs index 1c92431cfa..c17eb2e106 100644 --- a/nexus/db-model/src/sled_resource_kind.rs +++ b/nexus/db-model/src/sled_resource_kind.rs @@ -7,7 +7,7 @@ use serde::{Deserialize, Serialize}; impl_enum_type!( #[derive(Clone, SqlType, Debug, QueryId)] - #[diesel(postgres_type(name = "sled_resource_kind"))] + #[diesel(postgres_type(name = "sled_resource_kind", schema = "public"))] pub struct SledResourceKindEnum; #[derive(Clone, Copy, Debug, AsExpression, FromSqlRow, Serialize, Deserialize, PartialEq)] diff --git a/nexus/db-model/src/snapshot.rs b/nexus/db-model/src/snapshot.rs index 2a93f03f69..6c160e5c6b 100644 --- a/nexus/db-model/src/snapshot.rs +++ b/nexus/db-model/src/snapshot.rs @@ -14,7 +14,7 @@ use uuid::Uuid; impl_enum_type!( #[derive(SqlType, Debug, QueryId)] - #[diesel(postgres_type(name = "snapshot_state"))] + #[diesel(postgres_type(name = "snapshot_state", schema = "public"))] pub struct SnapshotStateEnum; #[derive(Clone, Debug, AsExpression, FromSqlRow, Serialize, Deserialize, PartialEq)] diff --git a/nexus/db-model/src/switch_interface.rs b/nexus/db-model/src/switch_interface.rs index f0c4b91de6..71673354ea 100644 --- a/nexus/db-model/src/switch_interface.rs +++ b/nexus/db-model/src/switch_interface.rs @@ -14,7 +14,7 @@ use uuid::Uuid; impl_enum_type!( #[derive(SqlType, Debug, Clone, Copy)] - #[diesel(postgres_type(name = "switch_interface_kind"))] + #[diesel(postgres_type(name = "switch_interface_kind", schema = "public"))] pub struct DbSwitchInterfaceKindEnum; #[derive( diff --git a/nexus/db-model/src/switch_port.rs b/nexus/db-model/src/switch_port.rs index 6ff8612d2f..6ed918dae5 100644 --- a/nexus/db-model/src/switch_port.rs +++ b/nexus/db-model/src/switch_port.rs @@ -23,7 +23,7 @@ use uuid::Uuid; impl_enum_type!( #[derive(SqlType, Debug, Clone, Copy)] - #[diesel(postgres_type(name = "switch_port_geometry"))] + #[diesel(postgres_type(name = "switch_port_geometry", schema = "public"))] pub struct SwitchPortGeometryEnum; #[derive( @@ -46,7 +46,7 @@ impl_enum_type!( impl_enum_type!( #[derive(SqlType, Debug, Clone, Copy)] - #[diesel(postgres_type(name = "switch_link_fec"))] + #[diesel(postgres_type(name = "switch_link_fec", schema = "public"))] pub struct SwitchLinkFecEnum; #[derive( @@ -69,7 +69,7 @@ impl_enum_type!( impl_enum_type!( #[derive(SqlType, Debug, Clone, Copy)] - #[diesel(postgres_type(name = "switch_link_speed"))] + #[diesel(postgres_type(name = "switch_link_speed", schema = "public"))] pub struct SwitchLinkSpeedEnum; #[derive( diff --git a/nexus/db-model/src/system_update.rs b/nexus/db-model/src/system_update.rs index c8ae66648e..17421936b1 100644 --- a/nexus/db-model/src/system_update.rs +++ b/nexus/db-model/src/system_update.rs @@ -59,7 +59,7 @@ impl From for views::SystemUpdate { impl_enum_type!( #[derive(SqlType, Debug, QueryId)] - #[diesel(postgres_type(name = "update_status"))] + #[diesel(postgres_type(name = "update_status", schema = "public"))] pub struct UpdateStatusEnum; #[derive(Copy, Clone, Debug, AsExpression, FromSqlRow, Serialize, Deserialize, PartialEq)] @@ -81,7 +81,7 @@ impl From for views::UpdateStatus { impl_enum_type!( #[derive(SqlType, Debug, QueryId)] - #[diesel(postgres_type(name = "updateable_component_type"))] + #[diesel(postgres_type(name = "updateable_component_type", schema = "public"))] pub struct UpdateableComponentTypeEnum; #[derive(Copy, Clone, Debug, AsExpression, FromSqlRow, Serialize, Deserialize, PartialEq)] diff --git a/nexus/db-model/src/update_artifact.rs b/nexus/db-model/src/update_artifact.rs index 196dd6db4d..97c57b44cc 100644 --- a/nexus/db-model/src/update_artifact.rs +++ b/nexus/db-model/src/update_artifact.rs @@ -14,7 +14,7 @@ use std::io::Write; impl_enum_wrapper!( #[derive(SqlType, Debug, QueryId)] - #[diesel(postgres_type(name = "update_artifact_kind"))] + #[diesel(postgres_type(name = "update_artifact_kind", schema = "public"))] pub struct KnownArtifactKindEnum; #[derive(Clone, Copy, Debug, Display, AsExpression, FromSqlRow, PartialEq, Eq, Serialize, Deserialize)] diff --git a/nexus/db-model/src/vpc_firewall_rule.rs b/nexus/db-model/src/vpc_firewall_rule.rs index 6208d589ff..2d19796524 100644 --- a/nexus/db-model/src/vpc_firewall_rule.rs +++ b/nexus/db-model/src/vpc_firewall_rule.rs @@ -19,7 +19,7 @@ use uuid::Uuid; impl_enum_wrapper!( #[derive(SqlType, Debug)] - #[diesel(postgres_type(name = "vpc_firewall_rule_status"))] + #[diesel(postgres_type(name = "vpc_firewall_rule_status", schema = "public"))] pub struct VpcFirewallRuleStatusEnum; #[derive(Clone, Debug, AsExpression, FromSqlRow, Serialize, Deserialize)] @@ -34,7 +34,7 @@ NewtypeDeref! { () pub struct VpcFirewallRuleStatus(external::VpcFirewallRuleSta impl_enum_wrapper!( #[derive(SqlType, Debug)] - #[diesel(postgres_type(name = "vpc_firewall_rule_direction"))] + #[diesel(postgres_type(name = "vpc_firewall_rule_direction", schema = "public"))] pub struct VpcFirewallRuleDirectionEnum; #[derive(Clone, Debug, AsExpression, FromSqlRow, Serialize, Deserialize)] @@ -49,7 +49,7 @@ NewtypeDeref! { () pub struct VpcFirewallRuleDirection(external::VpcFirewallRule impl_enum_wrapper!( #[derive(SqlType, Debug)] - #[diesel(postgres_type(name = "vpc_firewall_rule_action"))] + #[diesel(postgres_type(name = "vpc_firewall_rule_action", schema = "public"))] pub struct VpcFirewallRuleActionEnum; #[derive(Clone, Debug, AsExpression, FromSqlRow, Serialize, Deserialize)] @@ -64,7 +64,7 @@ NewtypeDeref! { () pub struct VpcFirewallRuleAction(external::VpcFirewallRuleAct impl_enum_wrapper!( #[derive(SqlType, Debug)] - #[diesel(postgres_type(name = "vpc_firewall_rule_protocol"))] + #[diesel(postgres_type(name = "vpc_firewall_rule_protocol", schema = "public"))] pub struct VpcFirewallRuleProtocolEnum; #[derive(Clone, Debug, AsExpression, FromSqlRow, Serialize, Deserialize)] diff --git a/nexus/db-model/src/vpc_route.rs b/nexus/db-model/src/vpc_route.rs index 7f68f81254..168ed41cef 100644 --- a/nexus/db-model/src/vpc_route.rs +++ b/nexus/db-model/src/vpc_route.rs @@ -19,7 +19,7 @@ use uuid::Uuid; impl_enum_wrapper!( #[derive(SqlType, Debug)] - #[diesel(postgres_type(name = "router_route_kind"))] + #[diesel(postgres_type(name = "router_route_kind", schema = "public"))] pub struct RouterRouteKindEnum; #[derive(Clone, Debug, AsExpression, FromSqlRow)] diff --git a/nexus/db-model/src/vpc_router.rs b/nexus/db-model/src/vpc_router.rs index 676bc17ec4..71c753e6aa 100644 --- a/nexus/db-model/src/vpc_router.rs +++ b/nexus/db-model/src/vpc_router.rs @@ -14,7 +14,7 @@ use uuid::Uuid; impl_enum_type!( #[derive(SqlType, Debug)] - #[diesel(postgres_type(name = "vpc_router_kind"))] + #[diesel(postgres_type(name = "vpc_router_kind", schema = "public"))] pub struct VpcRouterKindEnum; #[derive(Clone, Copy, Debug, AsExpression, FromSqlRow, PartialEq)] diff --git a/nexus/db-queries/Cargo.toml b/nexus/db-queries/Cargo.toml index d5320be733..cae42a0944 100644 --- a/nexus/db-queries/Cargo.toml +++ b/nexus/db-queries/Cargo.toml @@ -72,6 +72,7 @@ omicron-test-utils.workspace = true openapiv3.workspace = true pem.workspace = true petgraph.workspace = true +pretty_assertions.workspace = true rcgen.workspace = true regex.workspace = true rustls.workspace = true diff --git a/nexus/db-queries/src/db/datastore/inventory.rs b/nexus/db-queries/src/db/datastore/inventory.rs index 7d880b4ec0..b7ff058234 100644 --- a/nexus/db-queries/src/db/datastore/inventory.rs +++ b/nexus/db-queries/src/db/datastore/inventory.rs @@ -36,12 +36,20 @@ use nexus_db_model::HwRotSlotEnum; use nexus_db_model::InvCaboose; use nexus_db_model::InvCollection; use nexus_db_model::InvCollectionError; +use nexus_db_model::InvOmicronZone; +use nexus_db_model::InvOmicronZoneNic; use nexus_db_model::InvRootOfTrust; use nexus_db_model::InvRotPage; use nexus_db_model::InvServiceProcessor; +use nexus_db_model::InvSledAgent; +use nexus_db_model::InvSledOmicronZones; use nexus_db_model::RotPageWhichEnum; +use nexus_db_model::SledRole; +use nexus_db_model::SledRoleEnum; use nexus_db_model::SpType; use nexus_db_model::SpTypeEnum; +use nexus_db_model::SqlU16; +use nexus_db_model::SqlU32; use nexus_db_model::SwCaboose; use nexus_db_model::SwRotPage; use nexus_types::inventory::BaseboardId; @@ -108,6 +116,55 @@ impl DataStore { )) }) .collect::, Error>>()?; + // Partition the sled agents into those with an associated baseboard id + // and those without one. We handle these pretty differently. + let (sled_agents_baseboards, sled_agents_no_baseboards): ( + Vec<_>, + Vec<_>, + ) = collection + .sled_agents + .values() + .partition(|sled_agent| sled_agent.baseboard_id.is_some()); + let sled_agents_no_baseboards = sled_agents_no_baseboards + .into_iter() + .map(|sled_agent| { + assert!(sled_agent.baseboard_id.is_none()); + InvSledAgent::new_without_baseboard(collection_id, sled_agent) + .map_err(|e| Error::internal_error(&e.to_string())) + }) + .collect::, Error>>()?; + + let sled_omicron_zones = collection + .omicron_zones + .values() + .map(|found| InvSledOmicronZones::new(collection_id, found)) + .collect::>(); + let omicron_zones = collection + .omicron_zones + .values() + .flat_map(|found| { + found.zones.zones.iter().map(|found_zone| { + InvOmicronZone::new( + collection_id, + found.sled_id, + found_zone, + ) + .map_err(|e| Error::internal_error(&e.to_string())) + }) + }) + .collect::, Error>>()?; + let omicron_zone_nics = collection + .omicron_zones + .values() + .flat_map(|found| { + found.zones.zones.iter().filter_map(|found_zone| { + InvOmicronZoneNic::new(collection_id, found_zone) + .with_context(|| format!("zone {:?}", found_zone.id)) + .map_err(|e| Error::internal_error(&format!("{:#}", e))) + .transpose() + }) + }) + .collect::, _>>()?; // This implementation inserts all records associated with the // collection in one transaction. This is primarily for simplicity. It @@ -573,6 +630,137 @@ impl DataStore { } } + // Insert rows for the sled agents that we found. In practice, we'd + // expect these to all have baseboards (if using Oxide hardware) or + // none have baseboards (if not). + { + use db::schema::hw_baseboard_id::dsl as baseboard_dsl; + use db::schema::inv_sled_agent::dsl as sa_dsl; + + // For sleds with a real baseboard id, we have to use the + // `INSERT INTO ... SELECT` pattern that we used for other types + // of rows above to pull in the baseboard id's uuid. + for sled_agent in &sled_agents_baseboards { + let baseboard_id = sled_agent.baseboard_id.as_ref().expect( + "already selected only sled agents with baseboards", + ); + let selection = db::schema::hw_baseboard_id::table + .select(( + collection_id.into_sql::(), + sled_agent + .time_collected + .into_sql::(), + sled_agent + .source + .clone() + .into_sql::(), + sled_agent + .sled_id + .into_sql::(), + baseboard_dsl::id.nullable(), + nexus_db_model::ipv6::Ipv6Addr::from( + sled_agent.sled_agent_address.ip(), + ) + .into_sql::(), + SqlU16(sled_agent.sled_agent_address.port()) + .into_sql::(), + SledRole::from(sled_agent.sled_role) + .into_sql::(), + SqlU32(sled_agent.usable_hardware_threads) + .into_sql::(), + nexus_db_model::ByteCount::from( + sled_agent.usable_physical_ram, + ) + .into_sql::(), + nexus_db_model::ByteCount::from( + sled_agent.reservoir_size, + ) + .into_sql::(), + )) + .filter( + baseboard_dsl::part_number + .eq(baseboard_id.part_number.clone()), + ) + .filter( + baseboard_dsl::serial_number + .eq(baseboard_id.serial_number.clone()), + ); + + let _ = + diesel::insert_into(db::schema::inv_sled_agent::table) + .values(selection) + .into_columns(( + sa_dsl::inv_collection_id, + sa_dsl::time_collected, + sa_dsl::source, + sa_dsl::sled_id, + sa_dsl::hw_baseboard_id, + sa_dsl::sled_agent_ip, + sa_dsl::sled_agent_port, + sa_dsl::sled_role, + sa_dsl::usable_hardware_threads, + sa_dsl::usable_physical_ram, + sa_dsl::reservoir_size, + )) + .execute_async(&conn) + .await?; + + // See the comment in the earlier block (where we use + // `inv_service_processor::all_columns()`). The same + // applies here. + let ( + _inv_collection_id, + _time_collected, + _source, + _sled_id, + _hw_baseboard_id, + _sled_agent_ip, + _sled_agent_port, + _sled_role, + _usable_hardware_threads, + _usable_physical_ram, + _reservoir_size, + ) = sa_dsl::inv_sled_agent::all_columns(); + } + + // For sleds with no baseboard information, we can't use + // the same INSERT INTO ... SELECT pattern because we + // won't find anything in the hw_baseboard_id table. It + // sucks that these are bifurcated code paths, but on + // the plus side, this is a much simpler INSERT, and we + // can insert all of them in one statement. + let _ = diesel::insert_into(db::schema::inv_sled_agent::table) + .values(sled_agents_no_baseboards) + .execute_async(&conn) + .await?; + } + + // Insert all the Omicron zones that we found. + { + use db::schema::inv_sled_omicron_zones::dsl as sled_zones; + let _ = diesel::insert_into(sled_zones::inv_sled_omicron_zones) + .values(sled_omicron_zones) + .execute_async(&conn) + .await?; + } + + { + use db::schema::inv_omicron_zone::dsl as omicron_zone; + let _ = diesel::insert_into(omicron_zone::inv_omicron_zone) + .values(omicron_zones) + .execute_async(&conn) + .await?; + } + + { + use db::schema::inv_omicron_zone_nic::dsl as omicron_zone_nic; + let _ = + diesel::insert_into(omicron_zone_nic::inv_omicron_zone_nic) + .values(omicron_zone_nics) + .execute_async(&conn) + .await?; + } + // Finally, insert the list of errors. { use db::schema::inv_collection_error::dsl as errors_dsl; @@ -825,7 +1013,18 @@ impl DataStore { // start removing it and we'd also need to make sure we didn't leak a // collection if we crash while deleting it. let conn = self.pool_connection_authorized(opctx).await?; - let (ncollections, nsps, nrots, ncabooses, nrot_pages, nerrors) = conn + let ( + ncollections, + nsps, + nrots, + ncabooses, + nrot_pages, + nsled_agents, + nsled_agent_zones, + nzones, + nnics, + nerrors, + ) = conn .transaction_async(|conn| async move { // Remove the record describing the collection itself. let ncollections = { @@ -881,6 +1080,48 @@ impl DataStore { .await? }; + // Remove rows for sled agents found. + let nsled_agents = { + use db::schema::inv_sled_agent::dsl; + diesel::delete( + dsl::inv_sled_agent + .filter(dsl::inv_collection_id.eq(collection_id)), + ) + .execute_async(&conn) + .await? + }; + + // Remove rows associated with Omicron zones + let nsled_agent_zones = { + use db::schema::inv_sled_omicron_zones::dsl; + diesel::delete( + dsl::inv_sled_omicron_zones + .filter(dsl::inv_collection_id.eq(collection_id)), + ) + .execute_async(&conn) + .await? + }; + + let nzones = { + use db::schema::inv_omicron_zone::dsl; + diesel::delete( + dsl::inv_omicron_zone + .filter(dsl::inv_collection_id.eq(collection_id)), + ) + .execute_async(&conn) + .await? + }; + + let nnics = { + use db::schema::inv_omicron_zone_nic::dsl; + diesel::delete( + dsl::inv_omicron_zone_nic + .filter(dsl::inv_collection_id.eq(collection_id)), + ) + .execute_async(&conn) + .await? + }; + // Remove rows for errors encountered. let nerrors = { use db::schema::inv_collection_error::dsl; @@ -892,7 +1133,18 @@ impl DataStore { .await? }; - Ok((ncollections, nsps, nrots, ncabooses, nrot_pages, nerrors)) + Ok(( + ncollections, + nsps, + nrots, + ncabooses, + nrot_pages, + nsled_agents, + nsled_agent_zones, + nzones, + nnics, + nerrors, + )) }) .await .map_err(|error| match error { @@ -909,6 +1161,10 @@ impl DataStore { "nrots" => nrots, "ncabooses" => ncabooses, "nrot_pages" => nrot_pages, + "nsled_agents" => nsled_agents, + "nsled_agent_zones" => nsled_agent_zones, + "nzones" => nzones, + "nnics" => nnics, "nerrors" => nerrors, ); @@ -1085,9 +1341,27 @@ impl DataStore { }; limit_reached = limit_reached || rots.len() == usize_limit; - // Collect the unique baseboard ids referenced by SPs and RoTs. - let baseboard_id_ids: BTreeSet<_> = - sps.keys().chain(rots.keys()).cloned().collect(); + let sled_agent_rows: Vec<_> = { + use db::schema::inv_sled_agent::dsl; + dsl::inv_sled_agent + .filter(dsl::inv_collection_id.eq(id)) + .limit(sql_limit) + .select(InvSledAgent::as_select()) + .load_async(&*conn) + .await + .map_err(|e| { + public_error_from_diesel(e, ErrorHandler::Server) + })? + }; + + // Collect the unique baseboard ids referenced by SPs, RoTs, and Sled + // Agents. + let baseboard_id_ids: BTreeSet<_> = sps + .keys() + .chain(rots.keys()) + .cloned() + .chain(sled_agent_rows.iter().filter_map(|s| s.hw_baseboard_id)) + .collect(); // Fetch the corresponding baseboard records. let baseboards_by_id: BTreeMap<_, _> = { use db::schema::hw_baseboard_id::dsl; @@ -1136,6 +1410,49 @@ impl DataStore { }) }) .collect::, _>>()?; + let sled_agents: BTreeMap<_, _> = + sled_agent_rows + .into_iter() + .map(|s: InvSledAgent| { + let sled_id = s.sled_id; + let baseboard_id = s + .hw_baseboard_id + .map(|id| { + baseboards_by_id.get(&id).cloned().ok_or_else( + || { + Error::internal_error( + "missing baseboard that we should have fetched", + ) + }, + ) + }) + .transpose()?; + let sled_agent = nexus_types::inventory::SledAgent { + time_collected: s.time_collected, + source: s.source, + sled_id, + baseboard_id, + sled_agent_address: std::net::SocketAddrV6::new( + std::net::Ipv6Addr::from(s.sled_agent_ip), + u16::from(s.sled_agent_port), + 0, + 0, + ), + sled_role: nexus_types::inventory::SledRole::from( + s.sled_role, + ), + usable_hardware_threads: u32::from( + s.usable_hardware_threads, + ), + usable_physical_ram: s.usable_physical_ram.into(), + reservoir_size: s.reservoir_size.into(), + }; + Ok((sled_id, sled_agent)) + }) + .collect::, + Error, + >>()?; // Fetch records of cabooses found. let inv_caboose_rows = { @@ -1237,7 +1554,7 @@ impl DataStore { .iter() .map(|inv_rot_page| inv_rot_page.sw_root_of_trust_page_id) .collect(); - // Fetch the corresponing records. + // Fetch the corresponding records. let rot_pages_by_id: BTreeMap<_, _> = { use db::schema::sw_root_of_trust_page::dsl; dsl::sw_root_of_trust_page @@ -1299,6 +1616,117 @@ impl DataStore { ); } + // Now read the Omicron zones. + // + // In the first pass, we'll load the "inv_sled_omicron_zones" records. + // There's one of these per sled. It does not contain the actual list + // of zones -- basically just collection metadata and the generation + // number. We'll assemble these directly into the data structure we're + // trying to build, which maps sled ids to objects describing the zones + // found on each sled. + let mut omicron_zones: BTreeMap<_, _> = { + use db::schema::inv_sled_omicron_zones::dsl; + dsl::inv_sled_omicron_zones + .filter(dsl::inv_collection_id.eq(id)) + .limit(sql_limit) + .select(InvSledOmicronZones::as_select()) + .load_async(&*conn) + .await + .map_err(|e| public_error_from_diesel(e, ErrorHandler::Server))? + .into_iter() + .map(|sled_zones_config| { + ( + sled_zones_config.sled_id, + sled_zones_config.into_uninit_zones_found(), + ) + }) + .collect() + }; + limit_reached = limit_reached || omicron_zones.len() == usize_limit; + + // Assemble a mutable map of all the NICs found, by NIC id. As we + // match these up with the corresponding zone below, we'll remove items + // from this set. That way we can tell if the same NIC was used twice + // or not used at all. + let mut omicron_zone_nics: BTreeMap<_, _> = { + use db::schema::inv_omicron_zone_nic::dsl; + dsl::inv_omicron_zone_nic + .filter(dsl::inv_collection_id.eq(id)) + .limit(sql_limit) + .select(InvOmicronZoneNic::as_select()) + .load_async(&*conn) + .await + .map_err(|e| public_error_from_diesel(e, ErrorHandler::Server))? + .into_iter() + .map(|found_zone_nic| (found_zone_nic.id, found_zone_nic)) + .collect() + }; + limit_reached = limit_reached || omicron_zone_nics.len() == usize_limit; + + // Now load the actual list of zones from all sleds. + let omicron_zones_list = { + use db::schema::inv_omicron_zone::dsl; + dsl::inv_omicron_zone + .filter(dsl::inv_collection_id.eq(id)) + // It's not strictly necessary to order these by id. Doing so + // ensures a consistent representation for `Collection`, which + // makes testing easier. It's already indexed to do this, too. + .order_by(dsl::id) + .limit(sql_limit) + .select(InvOmicronZone::as_select()) + .load_async(&*conn) + .await + .map_err(|e| { + public_error_from_diesel(e, ErrorHandler::Server) + })? + }; + limit_reached = + limit_reached || omicron_zones_list.len() == usize_limit; + for z in omicron_zones_list { + let nic_row = z + .nic_id + .map(|id| { + // This error means that we found a row in inv_omicron_zone + // that references a NIC by id but there's no corresponding + // row in inv_omicron_zone_nic with that id. This should be + // impossible and reflects either a bug or database + // corruption. + omicron_zone_nics.remove(&id).ok_or_else(|| { + Error::internal_error(&format!( + "zone {:?}: expected to find NIC {:?}, but didn't", + z.id, z.nic_id + )) + }) + }) + .transpose()?; + let map = omicron_zones.get_mut(&z.sled_id).ok_or_else(|| { + // This error means that we found a row in inv_omicron_zone with + // no associated record in inv_sled_omicron_zones. This should + // be impossible and reflects either a bug or database + // corruption. + Error::internal_error(&format!( + "zone {:?}: unknown sled: {:?}", + z.id, z.sled_id + )) + })?; + let zone_id = z.id; + let zone = z + .into_omicron_zone_config(nic_row) + .with_context(|| { + format!("zone {:?}: parse from database", zone_id) + }) + .map_err(|e| { + Error::internal_error(&format!("{:#}", e.to_string())) + })?; + map.zones.zones.push(zone); + } + + bail_unless!( + omicron_zone_nics.is_empty(), + "found extra Omicron zone NICs: {:?}", + omicron_zone_nics.keys() + ); + Ok(( Collection { id, @@ -1313,6 +1741,8 @@ impl DataStore { rots, cabooses_found, rot_pages_found, + sled_agents, + omicron_zones, }, limit_reached, )) @@ -1476,7 +1906,7 @@ mod test { assert_eq!(collection1, collection_read); // There ought to be no baseboards, cabooses, or RoT pages in the - // databases from that collection. + // database from that collection. assert_eq!(collection1.baseboards.len(), 0); assert_eq!(collection1.cabooses.len(), 0); assert_eq!(collection1.rot_pages.len(), 0); @@ -1815,6 +2245,39 @@ mod test { .await .unwrap(); assert_eq!(0, count); + let count = + schema::inv_root_of_trust_page::dsl::inv_root_of_trust_page + .select(diesel::dsl::count_star()) + .first_async::(&conn) + .await + .unwrap(); + assert_eq!(0, count); + let count = schema::inv_sled_agent::dsl::inv_sled_agent + .select(diesel::dsl::count_star()) + .first_async::(&conn) + .await + .unwrap(); + assert_eq!(0, count); + let count = + schema::inv_sled_omicron_zones::dsl::inv_sled_omicron_zones + .select(diesel::dsl::count_star()) + .first_async::(&conn) + .await + .unwrap(); + assert_eq!(0, count); + let count = schema::inv_omicron_zone::dsl::inv_omicron_zone + .select(diesel::dsl::count_star()) + .first_async::(&conn) + .await + .unwrap(); + assert_eq!(0, count); + let count = schema::inv_omicron_zone_nic::dsl::inv_omicron_zone_nic + .select(diesel::dsl::count_star()) + .first_async::(&conn) + .await + .unwrap(); + assert_eq!(0, count); + Ok::<(), anyhow::Error>(()) }) .await diff --git a/nexus/db-queries/src/db/datastore/network_interface.rs b/nexus/db-queries/src/db/datastore/network_interface.rs index 4d4e43c9a7..be12ea5231 100644 --- a/nexus/db-queries/src/db/datastore/network_interface.rs +++ b/nexus/db-queries/src/db/datastore/network_interface.rs @@ -76,7 +76,7 @@ impl From for sled_client_types::NetworkInterface { sled_client_types::NetworkInterface { id: nic.id, kind, - name: sled_client_types::Name::from(&nic.name.0), + name: nic.name.into(), ip: nic.ip.ip(), mac: sled_client_types::MacAddr::from(nic.mac.0), subnet: sled_client_types::IpNet::from(ip_subnet), diff --git a/nexus/db-queries/src/db/mod.rs b/nexus/db-queries/src/db/mod.rs index 924eab363f..e21ba2e3a8 100644 --- a/nexus/db-queries/src/db/mod.rs +++ b/nexus/db-queries/src/db/mod.rs @@ -24,6 +24,7 @@ pub mod lookup; // Public for doctests. pub mod pagination; mod pool; +mod pool_connection; // This is marked public because the error types are used elsewhere, e.g., in // sagas. pub mod queries; diff --git a/nexus/db-queries/src/db/pool.rs b/nexus/db-queries/src/db/pool.rs index 249852d832..497c8d97c5 100644 --- a/nexus/db-queries/src/db/pool.rs +++ b/nexus/db-queries/src/db/pool.rs @@ -25,16 +25,10 @@ // TODO-design Need TLS support (the types below hardcode NoTls). use super::Config as DbConfig; -use async_bb8_diesel::AsyncSimpleConnection; -use async_bb8_diesel::Connection; use async_bb8_diesel::ConnectionError; use async_bb8_diesel::ConnectionManager; -use async_trait::async_trait; -use bb8::CustomizeConnection; -use diesel::PgConnection; -use diesel_dtrace::DTraceConnection; -pub type DbConnection = DTraceConnection; +pub use super::pool_connection::DbConnection; /// Wrapper around a database connection pool. /// @@ -76,7 +70,9 @@ impl Pool { let error_sink = LoggingErrorSink::new(log); let manager = ConnectionManager::::new(url); let pool = builder - .connection_customizer(Box::new(DisallowFullTableScans {})) + .connection_customizer(Box::new( + super::pool_connection::ConnectionCustomizer::new(), + )) .error_sink(Box::new(error_sink)) .build_unchecked(manager); Pool { pool } @@ -88,25 +84,6 @@ impl Pool { } } -const DISALLOW_FULL_TABLE_SCAN_SQL: &str = - "set disallow_full_table_scans = on; set large_full_scan_rows = 0;"; - -#[derive(Debug)] -struct DisallowFullTableScans {} -#[async_trait] -impl CustomizeConnection, ConnectionError> - for DisallowFullTableScans -{ - async fn on_acquire( - &self, - conn: &mut Connection, - ) -> Result<(), ConnectionError> { - conn.batch_execute_async(DISALLOW_FULL_TABLE_SCAN_SQL) - .await - .map_err(|e| e.into()) - } -} - #[derive(Clone, Debug)] struct LoggingErrorSink { log: slog::Logger, diff --git a/nexus/db-queries/src/db/pool_connection.rs b/nexus/db-queries/src/db/pool_connection.rs new file mode 100644 index 0000000000..6fb951de84 --- /dev/null +++ b/nexus/db-queries/src/db/pool_connection.rs @@ -0,0 +1,305 @@ +// 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/. + +//! Customization that happens on each connection as they're acquired. + +use async_bb8_diesel::AsyncConnection; +use async_bb8_diesel::AsyncRunQueryDsl; +use async_bb8_diesel::AsyncSimpleConnection; +use async_bb8_diesel::Connection; +use async_bb8_diesel::ConnectionError; +use async_trait::async_trait; +use bb8::CustomizeConnection; +use diesel::pg::GetPgMetadataCache; +use diesel::pg::PgMetadataCacheKey; +use diesel::prelude::*; +use diesel::PgConnection; +use diesel_dtrace::DTraceConnection; +use std::collections::HashMap; +use tokio::sync::Mutex; + +pub type DbConnection = DTraceConnection; + +// This is a list of all user-defined types (ENUMS) in the current DB schema. +// +// Diesel looks up user-defined types as they are encountered, and loads +// them into a metadata cache. Although this cost is amortized over the lifetime +// of a connection, this can be slower than desired: +// - Diesel issues a round-trip database call on each user-defined type +// - The cache of OIDs for user-defined types is "per-connection", so when +// using a connection pool, we redo all these calls for new connections. +// +// To mitigate: We look up a list of user-defined types here on first access +// to the connection, and pre-populate the cache. Furthermore, we save this +// information and use it to populate other connections too, without incurring +// another database lookup. +// +// See https://github.com/oxidecomputer/omicron/issues/4733 for more context. +static CUSTOM_TYPE_KEYS: &'static [&'static str] = &[ + "address_lot_kind", + "authentication_mode", + "block_size", + "caboose_which", + "dataset_kind", + "dns_group", + "hw_power_state", + "hw_rot_slot", + "identity_type", + "instance_state", + "ip_kind", + "network_interface_kind", + "physical_disk_kind", + "producer_kind", + "provider_type", + "root_of_trust_page_which", + "router_route_kind", + "saga_state", + "service_kind", + "sled_provision_state", + "sled_resource_kind", + "sled_role", + "snapshot_state", + "sp_type", + "switch_interface_kind", + "switch_link_fec", + "switch_link_speed", + "switch_port_geometry", + "update_artifact_kind", + "update_status", + "updateable_component_type", + "user_provision_type", + "vpc_firewall_rule_action", + "vpc_firewall_rule_direction", + "vpc_firewall_rule_protocol", + "vpc_firewall_rule_status", + "vpc_router_kind", + "zone_type", +]; +const CUSTOM_TYPE_SCHEMA: &'static str = "public"; + +const DISALLOW_FULL_TABLE_SCAN_SQL: &str = + "set disallow_full_table_scans = on; set large_full_scan_rows = 0;"; + +#[derive(Debug)] +struct OIDCache(HashMap, (u32, u32)>); + +impl OIDCache { + // Populate a new OID cache by pre-filling values + async fn new( + conn: &mut Connection, + ) -> Result { + // Lookup all the OIDs for custom types. + // + // As a reminder, this is an optimization: + // - If we supply a value in CUSTOM_TYPE_KEYS that does not + // exist in the schema, the corresponding row won't be + // found, so the value will be ignored. + // - If we don't supply a value in CUSTOM_TYPE_KEYS, even + // though it DOES exist in the schema, it'll likewise not + // get pre-populated into the cache. Diesel would observe + // the cache miss, and perform the lookup later. + let results: Vec = pg_type::table + .select((pg_type::typname, pg_type::oid, pg_type::typarray)) + .inner_join( + pg_namespace::table + .on(pg_type::typnamespace.eq(pg_namespace::oid)), + ) + .filter(pg_type::typname.eq_any(CUSTOM_TYPE_KEYS)) + .filter(pg_namespace::nspname.eq(CUSTOM_TYPE_SCHEMA)) + .load_async(&*conn) + .await?; + + // Convert the OIDs into a ("Cache Key", "OID Tuple") pair, + // and store the result in a HashMap. + // + // We'll iterate over this HashMap to pre-populate the connection-local cache for all + // future connections, including this one. + Ok::<_, ConnectionError>(Self(HashMap::from_iter( + results.into_iter().map( + |PgTypeMetadata { typname, oid, array_oid }| { + ( + PgMetadataCacheKey::new( + Some(CUSTOM_TYPE_SCHEMA.into()), + std::borrow::Cow::Owned(typname), + ), + (oid, array_oid), + ) + }, + ), + ))) + } +} + +// String-based representation of the CockroachDB version. +// +// We currently do minimal parsing of this value, but it should +// be distinct between different revisions of CockroachDB. +// This version includes the semver version of the DB, but also +// build and target information. +#[derive(Debug, Eq, PartialEq, Hash)] +struct CockroachVersion(String); + +impl CockroachVersion { + async fn new( + conn: &Connection, + ) -> Result { + diesel::sql_function!(fn version() -> Text); + + let version = + diesel::select(version()).get_result_async::(conn).await?; + Ok(Self(version)) + } +} + +/// A customizer for all new connections made to CockroachDB, from Diesel. +#[derive(Debug)] +pub(crate) struct ConnectionCustomizer { + oid_caches: Mutex>, +} + +impl ConnectionCustomizer { + pub(crate) fn new() -> Self { + Self { oid_caches: Mutex::new(HashMap::new()) } + } + + async fn populate_metadata_cache( + &self, + conn: &mut Connection, + ) -> Result<(), ConnectionError> { + // Look up the CockroachDB version for new connections, to ensure + // that OID caches are distinct between different CRDB versions. + // + // This step is performed out of an abundance of caution: OIDs are not + // necessarily stable across major releases of CRDB, and this ensures + // that the OID lookups on custom types do not cross this version + // boundary. + let version = CockroachVersion::new(conn).await?; + + // Lookup the OID cache, or populate it if we haven't previously + // established a connection to this database version. + let mut oid_caches = self.oid_caches.lock().await; + let entry = oid_caches.entry(version); + use std::collections::hash_map::Entry::*; + let oid_cache = match entry { + Occupied(ref entry) => entry.get(), + Vacant(entry) => entry.insert(OIDCache::new(conn).await?), + }; + + // Copy the OID cache into this specific connection. + // + // NOTE: I don't love that this is blocking (due to "as_sync_conn"), but the + // "get_metadata_cache" method does not seem implemented for types that could have a + // non-Postgres backend. + let mut sync_conn = conn.as_sync_conn(); + let cache = sync_conn.get_metadata_cache(); + for (k, v) in &oid_cache.0 { + cache.store_type(k.clone(), *v); + } + Ok(()) + } + + async fn disallow_full_table_scans( + &self, + conn: &mut Connection, + ) -> Result<(), ConnectionError> { + conn.batch_execute_async(DISALLOW_FULL_TABLE_SCAN_SQL).await?; + Ok(()) + } +} + +#[async_trait] +impl CustomizeConnection, ConnectionError> + for ConnectionCustomizer +{ + async fn on_acquire( + &self, + conn: &mut Connection, + ) -> Result<(), ConnectionError> { + self.populate_metadata_cache(conn).await?; + self.disallow_full_table_scans(conn).await?; + Ok(()) + } +} + +#[derive(Debug, Clone, Hash, PartialEq, Eq, Queryable)] +pub struct PgTypeMetadata { + typname: String, + oid: u32, + array_oid: u32, +} + +table! { + pg_type (oid) { + oid -> Oid, + typname -> Text, + typarray -> Oid, + typnamespace -> Oid, + } +} + +table! { + pg_namespace (oid) { + oid -> Oid, + nspname -> Text, + } +} + +allow_tables_to_appear_in_same_query!(pg_type, pg_namespace); + +#[cfg(test)] +mod test { + use super::*; + use nexus_test_utils::db::test_setup_database; + use omicron_test_utils::dev; + + // Ensure that the "CUSTOM_TYPE_KEYS" values match the enums + // we find within the database. + // + // If the two are out-of-sync, identify the values causing problems. + #[tokio::test] + async fn all_enums_in_prepopulate_list() { + let logctx = dev::test_setup_log("test_project_creation"); + let mut crdb = test_setup_database(&logctx.log).await; + let client = crdb.connect().await.expect("Failed to connect to CRDB"); + + // https://www.cockroachlabs.com/docs/stable/show-enums + let rows = client + .query("SHOW ENUMS FROM omicron.public;", &[]) + .await + .unwrap_or_else(|_| panic!("failed to list enums")); + client.cleanup().await.expect("cleaning up after listing enums"); + + let mut observed_public_enums = rows + .into_iter() + .map(|row| -> String { + for i in 0..row.len() { + if row.columns()[i].name() == "name" { + return row.get(i); + } + } + panic!("Missing 'name' in row: {row:?}"); + }) + .collect::>(); + observed_public_enums.sort(); + + let mut expected_enums: Vec = + CUSTOM_TYPE_KEYS.into_iter().map(|s| s.to_string()).collect(); + expected_enums.sort(); + + pretty_assertions::assert_eq!( + observed_public_enums, + expected_enums, + "Enums did not match.\n\ + If the type is present on the left, but not the right:\n\ + \tThe enum is in the DB, but not in CUSTOM_TYPE_KEYS.\n\ + \tConsider adding it, so we can pre-populate the OID cache.\n\ + If the type is present on the right, but not the left:\n\ + \tThe enum is not the DB, but it is in CUSTOM_TYPE_KEYS.\n\ + \tConsider removing it, because the type no longer exists" + ); + + crdb.cleanup().await.unwrap(); + logctx.cleanup_successful(); + } +} diff --git a/nexus/inventory/Cargo.toml b/nexus/inventory/Cargo.toml index 22b48ebcec..1c20e8f8b6 100644 --- a/nexus/inventory/Cargo.toml +++ b/nexus/inventory/Cargo.toml @@ -8,9 +8,14 @@ license = "MPL-2.0" anyhow.workspace = true base64.workspace = true chrono.workspace = true +futures.workspace = true gateway-client.workspace = true gateway-messages.workspace = true nexus-types.workspace = true +omicron-common.workspace = true +reqwest.workspace = true +serde_json.workspace = true +sled-agent-client.workspace = true slog.workspace = true strum.workspace = true thiserror.workspace = true @@ -20,5 +25,6 @@ omicron-workspace-hack.workspace = true [dev-dependencies] expectorate.workspace = true gateway-test-utils.workspace = true +omicron-sled-agent.workspace = true regex.workspace = true tokio.workspace = true diff --git a/nexus/inventory/example-data/madrid-sled14.json b/nexus/inventory/example-data/madrid-sled14.json new file mode 100644 index 0000000000..f91c12d3f0 --- /dev/null +++ b/nexus/inventory/example-data/madrid-sled14.json @@ -0,0 +1,214 @@ +{ + "generation": 5, + "zones": [ + { + "id": "0a5f085b-dfb9-4eed-bd24-678bd97e453c", + "underlay_address": "fd00:1122:3344:104::c", + "zone_type": { + "type": "crucible", + "address": "[fd00:1122:3344:104::c]:32345", + "dataset": { + "pool_name": "oxp_e1bf20e5-603c-4d14-94c4-47dc1eb58c45" + } + } + }, + { + "id": "175eb50f-c54c-41ed-b30e-bb710868b362", + "underlay_address": "fd00:1122:3344:104::a", + "zone_type": { + "type": "crucible", + "address": "[fd00:1122:3344:104::a]:32345", + "dataset": { + "pool_name": "oxp_3bcdbecd-827a-426e-96b6-30c355b78301" + } + } + }, + { + "id": "844a964a-831c-4cb9-82b5-3883c9b404db", + "underlay_address": "fd00:1122:3344:104::e", + "zone_type": { + "type": "crucible", + "address": "[fd00:1122:3344:104::e]:32345", + "dataset": { + "pool_name": "oxp_d721dbb5-6a10-4fe8-9d70-fae69ab84676" + } + } + }, + { + "id": "cd8a5031-44a3-4090-86d7-2bfcc3de7942", + "underlay_address": "fd00:1122:3344:104::d", + "zone_type": { + "type": "crucible", + "address": "[fd00:1122:3344:104::d]:32345", + "dataset": { + "pool_name": "oxp_a90de3a7-b760-45b7-ad72-70cd3570a940" + } + } + }, + { + "id": "f7f78c86-f572-49bf-b6cd-24658ddee847", + "underlay_address": "fd00:1122:3344:104::7", + "zone_type": { + "type": "crucible", + "address": "[fd00:1122:3344:104::7]:32345", + "dataset": { + "pool_name": "oxp_5dd3aedf-c3c5-4258-8864-3ea8b5ae321b" + } + } + }, + { + "id": "543e32e4-7d8c-4888-a085-1c530555ee22", + "underlay_address": "fd00:1122:3344:104::6", + "zone_type": { + "type": "crucible", + "address": "[fd00:1122:3344:104::6]:32345", + "dataset": { + "pool_name": "oxp_46b4a891-addc-4690-b8ff-8625b9c5c3bc" + } + } + }, + { + "id": "28786d99-48d2-4491-a4ae-943e603f3dab", + "underlay_address": "fd00:1122:3344:104::9", + "zone_type": { + "type": "crucible", + "address": "[fd00:1122:3344:104::9]:32345", + "dataset": { + "pool_name": "oxp_8c96d804-3a6c-4c1b-be24-1e7fc18824de" + } + } + }, + { + "id": "e59b6fc3-3b0e-4e17-acfa-0351e2924771", + "underlay_address": "fd00:1122:3344:104::b", + "zone_type": { + "type": "crucible", + "address": "[fd00:1122:3344:104::b]:32345", + "dataset": { + "pool_name": "oxp_49154338-0e01-4394-9dd2-cb4c53cbb90f" + } + } + }, + { + "id": "ab67e1fa-337f-45e6-83f0-6e94a9d50fc0", + "underlay_address": "fd00:1122:3344:104::4", + "zone_type": { + "type": "nexus", + "internal_address": "[fd00:1122:3344:104::4]:12221", + "external_ip": "172.20.28.2", + "nic": { + "id": "5d4a7e78-d1e1-41cd-881c-02d808fb90be", + "kind": { + "type": "service", + "id": "ab67e1fa-337f-45e6-83f0-6e94a9d50fc0" + }, + "name": "nexus-ab67e1fa-337f-45e6-83f0-6e94a9d50fc0", + "ip": "172.30.2.5", + "mac": "A8:40:25:FF:B7:E2", + "subnet": "172.30.2.0/24", + "vni": 100, + "primary": true, + "slot": 0 + }, + "external_tls": true, + "external_dns_servers": [ + "1.1.1.1", + "9.9.9.9" + ] + } + }, + { + "id": "5a6d10a6-ce94-444b-82ce-be25ebe58b9a", + "underlay_address": "fd00:1122:3344:104::f", + "zone_type": { + "type": "crucible", + "address": "[fd00:1122:3344:104::f]:32345", + "dataset": { + "pool_name": "oxp_13a6ef76-5904-4794-8083-dfeb6806e5f1" + } + } + }, + { + "id": "442c669b-14d4-48b5-8f05-741b3c67a558", + "underlay_address": "fd00:1122:3344:104::8", + "zone_type": { + "type": "crucible", + "address": "[fd00:1122:3344:104::8]:32345", + "dataset": { + "pool_name": "oxp_0010be1f-4223-4f3e-844c-3e823488a852" + } + } + }, + { + "id": "5db69c8f-4565-4cae-8372-f20ada0f67e9", + "underlay_address": "fd00:1122:3344:104::5", + "zone_type": { + "type": "clickhouse", + "address": "[fd00:1122:3344:104::5]:8123", + "dataset": { + "pool_name": "oxp_46b4a891-addc-4690-b8ff-8625b9c5c3bc" + } + } + }, + { + "id": "5d840664-3eb1-45da-8876-d44e1cfb1142", + "underlay_address": "fd00:1122:3344:104::3", + "zone_type": { + "type": "cockroach_db", + "address": "[fd00:1122:3344:104::3]:32221", + "dataset": { + "pool_name": "oxp_46b4a891-addc-4690-b8ff-8625b9c5c3bc" + } + } + }, + { + "id": "d38984ac-a366-4936-b64f-d98ae3dc2035", + "underlay_address": "fd00:1122:3344:104::10", + "zone_type": { + "type": "boundary_ntp", + "address": "[fd00:1122:3344:104::10]:123", + "ntp_servers": [ + "ntp.eng.oxide.computer" + ], + "dns_servers": [ + "1.1.1.1", + "9.9.9.9" + ], + "domain": null, + "nic": { + "id": "2e4943f4-0477-4b5b-afd7-70c1f4aaf928", + "kind": { + "type": "service", + "id": "d38984ac-a366-4936-b64f-d98ae3dc2035" + }, + "name": "ntp-d38984ac-a366-4936-b64f-d98ae3dc2035", + "ip": "172.30.3.6", + "mac": "A8:40:25:FF:C0:38", + "subnet": "172.30.3.0/24", + "vni": 100, + "primary": true, + "slot": 0 + }, + "snat_cfg": { + "ip": "172.20.28.6", + "first_port": 16384, + "last_port": 32767 + } + } + }, + { + "id": "23856e18-8736-49a6-b487-bc5bf850fee0", + "underlay_address": "fd00:1122:3344:2::1", + "zone_type": { + "type": "internal_dns", + "dataset": { + "pool_name": "oxp_46b4a891-addc-4690-b8ff-8625b9c5c3bc" + }, + "http_address": "[fd00:1122:3344:2::1]:5353", + "dns_address": "[fd00:1122:3344:2::1]:53", + "gz_address": "fd00:1122:3344:2::2", + "gz_address_index": 1 + } + } + ] +} diff --git a/nexus/inventory/example-data/madrid-sled16.json b/nexus/inventory/example-data/madrid-sled16.json new file mode 100644 index 0000000000..edf3c71571 --- /dev/null +++ b/nexus/inventory/example-data/madrid-sled16.json @@ -0,0 +1,206 @@ +{ + "generation": 5, + "zones": [ + { + "id": "b2629475-65b2-4e8a-9e70-d4e8c034d8ad", + "underlay_address": "fd00:1122:3344:102::e", + "zone_type": { + "type": "crucible", + "address": "[fd00:1122:3344:102::e]:32345", + "dataset": { + "pool_name": "oxp_1cd1c449-b5e1-4e8b-bb2f-2e2bd5a8f301" + } + } + }, + { + "id": "1aa5fd71-d766-4f20-b3c7-9cf4fe9e4f2e", + "underlay_address": "fd00:1122:3344:102::9", + "zone_type": { + "type": "crucible", + "address": "[fd00:1122:3344:102::9]:32345", + "dataset": { + "pool_name": "oxp_6d799846-deac-4809-93bd-5dad30127938" + } + } + }, + { + "id": "271ee61b-9e97-4e45-a407-0083f8bf15a7", + "underlay_address": "fd00:1122:3344:102::7", + "zone_type": { + "type": "crucible", + "address": "[fd00:1122:3344:102::7]:32345", + "dataset": { + "pool_name": "oxp_65de425b-1487-4d46-85b5-f5fa7c9e776a" + } + } + }, + { + "id": "750b40ef-8e83-4c7a-be96-33964b2244f3", + "underlay_address": "fd00:1122:3344:102::a", + "zone_type": { + "type": "crucible", + "address": "[fd00:1122:3344:102::a]:32345", + "dataset": { + "pool_name": "oxp_901a85dd-8214-407a-a358-ef4aebfa810d" + } + } + }, + { + "id": "0322760d-a1e2-4911-8745-569f6bad8251", + "underlay_address": "fd00:1122:3344:102::4", + "zone_type": { + "type": "external_dns", + "dataset": { + "pool_name": "oxp_65de425b-1487-4d46-85b5-f5fa7c9e776a" + }, + "http_address": "[fd00:1122:3344:102::4]:5353", + "dns_address": "172.20.28.1:53", + "nic": { + "id": "8b99b41f-976d-4cb5-bad6-492cde39575a", + "kind": { + "type": "service", + "id": "0322760d-a1e2-4911-8745-569f6bad8251" + }, + "name": "external-dns-0322760d-a1e2-4911-8745-569f6bad8251", + "ip": "172.30.1.5", + "mac": "A8:40:25:FF:F7:4A", + "subnet": "172.30.1.0/24", + "vni": 100, + "primary": true, + "slot": 0 + } + } + }, + { + "id": "f350b534-e9bb-4e47-a2ae-4029efe48e1a", + "underlay_address": "fd00:1122:3344:102::6", + "zone_type": { + "type": "crucible_pantry", + "address": "[fd00:1122:3344:102::6]:17000" + } + }, + { + "id": "e9d7d6ba-59e3-44ff-9081-f43e61c9968a", + "underlay_address": "fd00:1122:3344:102::d", + "zone_type": { + "type": "crucible", + "address": "[fd00:1122:3344:102::d]:32345", + "dataset": { + "pool_name": "oxp_51abdeb3-6673-4af3-aa91-7e8748e4dda2" + } + } + }, + { + "id": "d02206f1-7567-4753-9221-6b2b70407925", + "underlay_address": "fd00:1122:3344:102::b", + "zone_type": { + "type": "crucible", + "address": "[fd00:1122:3344:102::b]:32345", + "dataset": { + "pool_name": "oxp_0fa59017-d1e7-47c1-9ed6-b66851e544ee" + } + } + }, + { + "id": "c489b9a3-33e5-487c-8a60-77853584dca1", + "underlay_address": "fd00:1122:3344:102::f", + "zone_type": { + "type": "crucible", + "address": "[fd00:1122:3344:102::f]:32345", + "dataset": { + "pool_name": "oxp_17d7dbce-b430-4c71-a27e-a5e66d175347" + } + } + }, + { + "id": "996f3011-5aaa-4732-a47d-e6514b1131d8", + "underlay_address": "fd00:1122:3344:102::c", + "zone_type": { + "type": "crucible", + "address": "[fd00:1122:3344:102::c]:32345", + "dataset": { + "pool_name": "oxp_87714aed-4573-438c-8c9d-3ed64688bdc4" + } + } + }, + { + "id": "cef138ff-87a4-4509-ba30-2395e01ac5f7", + "underlay_address": "fd00:1122:3344:102::5", + "zone_type": { + "type": "oximeter", + "address": "[fd00:1122:3344:102::5]:12223" + } + }, + { + "id": "263584b3-2f53-4f87-a9c0-60a4c78af6c4", + "underlay_address": "fd00:1122:3344:102::8", + "zone_type": { + "type": "crucible", + "address": "[fd00:1122:3344:102::8]:32345", + "dataset": { + "pool_name": "oxp_2acbc210-8b83-490a-b7a7-e458d742c269" + } + } + }, + { + "id": "2f336547-e4b0-422c-af54-deae20b4580c", + "underlay_address": "fd00:1122:3344:102::3", + "zone_type": { + "type": "cockroach_db", + "address": "[fd00:1122:3344:102::3]:32221", + "dataset": { + "pool_name": "oxp_65de425b-1487-4d46-85b5-f5fa7c9e776a" + } + } + }, + { + "id": "412bfd7b-4bf8-471d-ae4d-90bf0bdd05ff", + "underlay_address": "fd00:1122:3344:102::10", + "zone_type": { + "type": "boundary_ntp", + "address": "[fd00:1122:3344:102::10]:123", + "ntp_servers": [ + "ntp.eng.oxide.computer" + ], + "dns_servers": [ + "1.1.1.1", + "9.9.9.9" + ], + "domain": null, + "nic": { + "id": "6c2aa1c5-0e42-4b80-9b31-26d0e8599d0d", + "kind": { + "type": "service", + "id": "412bfd7b-4bf8-471d-ae4d-90bf0bdd05ff" + }, + "name": "ntp-412bfd7b-4bf8-471d-ae4d-90bf0bdd05ff", + "ip": "172.30.3.5", + "mac": "A8:40:25:FF:F2:45", + "subnet": "172.30.3.0/24", + "vni": 100, + "primary": true, + "slot": 0 + }, + "snat_cfg": { + "ip": "172.20.28.5", + "first_port": 0, + "last_port": 16383 + } + } + }, + { + "id": "7de28140-8cdc-4478-9204-63763ecc10ff", + "underlay_address": "fd00:1122:3344:1::1", + "zone_type": { + "type": "internal_dns", + "dataset": { + "pool_name": "oxp_65de425b-1487-4d46-85b5-f5fa7c9e776a" + }, + "http_address": "[fd00:1122:3344:1::1]:5353", + "dns_address": "[fd00:1122:3344:1::1]:53", + "gz_address": "fd00:1122:3344:1::2", + "gz_address_index": 0 + } + } + ] +} diff --git a/nexus/inventory/example-data/madrid-sled17.json b/nexus/inventory/example-data/madrid-sled17.json new file mode 100644 index 0000000000..8ac5dff840 --- /dev/null +++ b/nexus/inventory/example-data/madrid-sled17.json @@ -0,0 +1,172 @@ +{ + "generation": 5, + "zones": [ + { + "id": "e58917eb-98cc-4b85-b851-4b46833060dc", + "underlay_address": "fd00:1122:3344:103::8", + "zone_type": { + "type": "crucible", + "address": "[fd00:1122:3344:103::8]:32345", + "dataset": { + "pool_name": "oxp_c6911096-09b3-4f64-bcd6-21701ca2d6ae" + } + } + }, + { + "id": "ae07bfa3-09a9-4b19-9721-c89f39e153fe", + "underlay_address": "fd00:1122:3344:103::7", + "zone_type": { + "type": "crucible", + "address": "[fd00:1122:3344:103::7]:32345", + "dataset": { + "pool_name": "oxp_4c667609-0876-4c8c-ae60-1b30aaf236dc" + } + } + }, + { + "id": "6e305032-a926-4c2b-a89a-0165799b9810", + "underlay_address": "fd00:1122:3344:103::9", + "zone_type": { + "type": "crucible", + "address": "[fd00:1122:3344:103::9]:32345", + "dataset": { + "pool_name": "oxp_d9974711-1064-441d-ba75-0ffabfc86d27" + } + } + }, + { + "id": "2deb45cb-7160-47fc-9180-ab14c1731427", + "underlay_address": "fd00:1122:3344:103::c", + "zone_type": { + "type": "crucible", + "address": "[fd00:1122:3344:103::c]:32345", + "dataset": { + "pool_name": "oxp_e675c45a-5e1b-4d24-99af-806523ed17d5" + } + } + }, + { + "id": "804f9ff7-0d45-465f-8820-ee0fc7c25286", + "underlay_address": "fd00:1122:3344:103::4", + "zone_type": { + "type": "nexus", + "internal_address": "[fd00:1122:3344:103::4]:12221", + "external_ip": "172.20.28.3", + "nic": { + "id": "3e1324f0-cad4-484e-a101-e26da2706e92", + "kind": { + "type": "service", + "id": "804f9ff7-0d45-465f-8820-ee0fc7c25286" + }, + "name": "nexus-804f9ff7-0d45-465f-8820-ee0fc7c25286", + "ip": "172.30.2.6", + "mac": "A8:40:25:FF:A5:50", + "subnet": "172.30.2.0/24", + "vni": 100, + "primary": true, + "slot": 0 + }, + "external_tls": true, + "external_dns_servers": [ + "1.1.1.1", + "9.9.9.9" + ] + } + }, + { + "id": "c28abc48-2fb2-487b-b89a-96317c4e2df2", + "underlay_address": "fd00:1122:3344:103::b", + "zone_type": { + "type": "crucible", + "address": "[fd00:1122:3344:103::b]:32345", + "dataset": { + "pool_name": "oxp_93017061-5910-4bf5-a366-4f1b2871b5c3" + } + } + }, + { + "id": "4da2814a-7d31-4311-97cf-7648e7b64911", + "underlay_address": "fd00:1122:3344:103::d", + "zone_type": { + "type": "crucible", + "address": "[fd00:1122:3344:103::d]:32345", + "dataset": { + "pool_name": "oxp_33a4881a-2b3f-4840-b252-f370024eee64" + } + } + }, + { + "id": "a3a6216e-fdc8-47b6-8ba8-eb666629f5c2", + "underlay_address": "fd00:1122:3344:103::a", + "zone_type": { + "type": "crucible", + "address": "[fd00:1122:3344:103::a]:32345", + "dataset": { + "pool_name": "oxp_844fd687-fd26-4616-91aa-441cf136c62d" + } + } + }, + { + "id": "bd646149-3e59-4aac-b2a0-b79910b8d6a8", + "underlay_address": "fd00:1122:3344:103::6", + "zone_type": { + "type": "crucible", + "address": "[fd00:1122:3344:103::6]:32345", + "dataset": { + "pool_name": "oxp_d130514f-6532-4c02-ac9f-c4958052c669" + } + } + }, + { + "id": "8c9735b2-9097-4ba5-b783-dfea16c5e0ab", + "underlay_address": "fd00:1122:3344:103::5", + "zone_type": { + "type": "crucible_pantry", + "address": "[fd00:1122:3344:103::5]:17000" + } + }, + { + "id": "8fe2fb59-5a89-4d40-b47a-6d5fcfb66ddd", + "underlay_address": "fd00:1122:3344:103::3", + "zone_type": { + "type": "cockroach_db", + "address": "[fd00:1122:3344:103::3]:32221", + "dataset": { + "pool_name": "oxp_d130514f-6532-4c02-ac9f-c4958052c669" + } + } + }, + { + "id": "9ee036cf-88c5-4a0e-aae7-eb2849379aad", + "underlay_address": "fd00:1122:3344:103::e", + "zone_type": { + "type": "internal_ntp", + "address": "[fd00:1122:3344:103::e]:123", + "ntp_servers": [ + "412bfd7b-4bf8-471d-ae4d-90bf0bdd05ff.host.control-plane.oxide.internal", + "d38984ac-a366-4936-b64f-d98ae3dc2035.host.control-plane.oxide.internal" + ], + "dns_servers": [ + "fd00:1122:3344:1::1", + "fd00:1122:3344:2::1", + "fd00:1122:3344:3::1" + ], + "domain": null + } + }, + { + "id": "7b006fad-d693-441b-bdd0-84cb323530e9", + "underlay_address": "fd00:1122:3344:3::1", + "zone_type": { + "type": "internal_dns", + "dataset": { + "pool_name": "oxp_d130514f-6532-4c02-ac9f-c4958052c669" + }, + "http_address": "[fd00:1122:3344:3::1]:5353", + "dns_address": "[fd00:1122:3344:3::1]:53", + "gz_address": "fd00:1122:3344:3::2", + "gz_address_index": 2 + } + } + ] +} diff --git a/nexus/inventory/src/builder.rs b/nexus/inventory/src/builder.rs index 2d8ba0d1f9..62d338c1ee 100644 --- a/nexus/inventory/src/builder.rs +++ b/nexus/inventory/src/builder.rs @@ -19,11 +19,13 @@ use nexus_types::inventory::Caboose; use nexus_types::inventory::CabooseFound; use nexus_types::inventory::CabooseWhich; use nexus_types::inventory::Collection; +use nexus_types::inventory::OmicronZonesFound; use nexus_types::inventory::RotPage; use nexus_types::inventory::RotPageFound; use nexus_types::inventory::RotPageWhich; use nexus_types::inventory::RotState; use nexus_types::inventory::ServiceProcessor; +use nexus_types::inventory::SledAgent; use std::collections::BTreeMap; use std::collections::BTreeSet; use std::sync::Arc; @@ -81,6 +83,8 @@ pub struct CollectionBuilder { BTreeMap, CabooseFound>>, rot_pages_found: BTreeMap, RotPageFound>>, + sleds: BTreeMap, + omicron_zones: BTreeMap, } impl CollectionBuilder { @@ -101,11 +105,19 @@ impl CollectionBuilder { rots: BTreeMap::new(), cabooses_found: BTreeMap::new(), rot_pages_found: BTreeMap::new(), + sleds: BTreeMap::new(), + omicron_zones: BTreeMap::new(), } } /// Assemble a complete `Collection` representation - pub fn build(self) -> Collection { + pub fn build(mut self) -> Collection { + // This is not strictly necessary. But for testing, it's helpful for + // things to be in sorted order. + for v in self.omicron_zones.values_mut() { + v.zones.zones.sort_by(|a, b| a.id.cmp(&b.id)); + } + Collection { id: Uuid::new_v4(), errors: self.errors.into_iter().map(|e| e.to_string()).collect(), @@ -119,6 +131,8 @@ impl CollectionBuilder { rots: self.rots, cabooses_found: self.cabooses_found, rot_pages_found: self.rot_pages_found, + sled_agents: self.sleds, + omicron_zones: self.omicron_zones, } } @@ -387,6 +401,105 @@ impl CollectionBuilder { pub fn found_error(&mut self, error: InventoryError) { self.errors.push(error); } + + /// Record information about a sled that's part of the control plane + pub fn found_sled_inventory( + &mut self, + source: &str, + inventory: sled_agent_client::types::Inventory, + ) -> Result<(), anyhow::Error> { + let sled_id = inventory.sled_id; + + // Normalize the baseboard id, if any. + use sled_agent_client::types::Baseboard; + let baseboard_id = match inventory.baseboard { + Baseboard::Pc { .. } => None, + Baseboard::Gimlet { identifier, model, revision: _ } => { + Some(Self::normalize_item( + &mut self.baseboards, + BaseboardId { + serial_number: identifier, + part_number: model, + }, + )) + } + Baseboard::Unknown => { + self.found_error(InventoryError::from(anyhow!( + "sled {:?}: reported unknown baseboard", + sled_id + ))); + None + } + }; + + // Socket addresses come through the OpenAPI spec as strings, which + // means they don't get validated when everything else does. This + // error is an operational error in collecting the data, not a collector + // bug. + let sled_agent_address = match inventory.sled_agent_address.parse() { + Ok(addr) => addr, + Err(error) => { + self.found_error(InventoryError::from(anyhow!( + "sled {:?}: bad sled agent address: {:?}: {:#}", + sled_id, + inventory.sled_agent_address, + error, + ))); + return Ok(()); + } + }; + let sled = SledAgent { + source: source.to_string(), + sled_agent_address, + sled_role: inventory.sled_role, + baseboard_id, + usable_hardware_threads: inventory.usable_hardware_threads, + usable_physical_ram: inventory.usable_physical_ram, + reservoir_size: inventory.reservoir_size, + time_collected: now(), + sled_id, + }; + + if let Some(previous) = self.sleds.get(&sled_id) { + Err(anyhow!( + "sled {:?}: reported sled multiple times \ + (previously {:?}, now {:?})", + sled_id, + previous, + sled, + )) + } else { + self.sleds.insert(sled_id, sled); + Ok(()) + } + } + + /// Record information about Omicron zones found on a sled + pub fn found_sled_omicron_zones( + &mut self, + source: &str, + sled_id: Uuid, + zones: sled_agent_client::types::OmicronZonesConfig, + ) -> Result<(), anyhow::Error> { + if let Some(previous) = self.omicron_zones.get(&sled_id) { + Err(anyhow!( + "sled {:?} omicron zones: reported previously: {:?}", + sled_id, + previous + )) + } else { + self.omicron_zones.insert( + sled_id, + OmicronZonesFound { + time_collected: now(), + source: source.to_string(), + sled_id, + zones, + }, + ); + Ok(()) + } + } } /// Returns the current time, truncated to the previous microsecond. @@ -422,6 +535,8 @@ mod test { use nexus_types::inventory::CabooseWhich; use nexus_types::inventory::RotPage; use nexus_types::inventory::RotPageWhich; + use nexus_types::inventory::SledRole; + use omicron_common::api::external::ByteCount; // Verify the contents of an empty collection. #[test] @@ -455,6 +570,8 @@ mod test { // - some missing cabooses // - some cabooses common to multiple baseboards; others not // - serial number reused across different model numbers + // - sled agent inventory + // - omicron zone inventory // // This test is admittedly pretty tedious and maybe not worthwhile but it's // a useful quick check. @@ -463,9 +580,11 @@ mod test { let time_before = now(); let Representative { builder, - sleds: [sled1_bb, sled2_bb, sled3_bb], + sleds: [sled1_bb, sled2_bb, sled3_bb, sled4_bb], switch, psc, + sled_agents: + [sled_agent_id_basic, sled_agent_id_extra, sled_agent_id_pc, sled_agent_id_unknown], } = representative(); let collection = builder.build(); let time_after = now(); @@ -479,21 +598,27 @@ mod test { // no RoT information. assert_eq!( collection.errors.iter().map(|e| e.to_string()).collect::>(), - ["MGS \"fake MGS 1\": reading RoT state for BaseboardId \ + [ + "MGS \"fake MGS 1\": reading RoT state for BaseboardId \ { part_number: \"model1\", serial_number: \"s2\" }: test suite \ - injected error"] + injected error", + "sled 5c5b4cf9-3e13-45fd-871c-f177d6537510: reported unknown \ + baseboard" + ] ); // Verify the baseboard ids found. let expected_baseboards = - &[&sled1_bb, &sled2_bb, &sled3_bb, &switch, &psc]; + &[&sled1_bb, &sled2_bb, &sled3_bb, &sled4_bb, &switch, &psc]; for bb in expected_baseboards { assert!(collection.baseboards.contains(*bb)); } assert_eq!(collection.baseboards.len(), expected_baseboards.len()); // Verify the stuff that's easy to verify for all SPs: timestamps. - assert_eq!(collection.sps.len(), collection.baseboards.len()); + // There will be one more baseboard than SP because of the one added for + // the extra sled agent. + assert_eq!(collection.sps.len() + 1, collection.baseboards.len()); for (bb, sp) in collection.sps.iter() { assert!(collection.time_started <= sp.time_collected); assert!(sp.time_collected <= collection.time_done); @@ -755,6 +880,42 @@ mod test { // plus the common one; same for RoT pages. assert_eq!(collection.cabooses.len(), 5); assert_eq!(collection.rot_pages.len(), 5); + + // Verify that we found the sled agents. + assert_eq!(collection.sled_agents.len(), 4); + for (sled_id, sled_agent) in &collection.sled_agents { + assert_eq!(*sled_id, sled_agent.sled_id); + if *sled_id == sled_agent_id_extra { + assert_eq!(sled_agent.sled_role, SledRole::Scrimlet); + } else { + assert_eq!(sled_agent.sled_role, SledRole::Gimlet); + } + + assert_eq!( + sled_agent.sled_agent_address, + "[::1]:56792".parse().unwrap() + ); + assert_eq!(sled_agent.usable_hardware_threads, 10); + assert_eq!( + sled_agent.usable_physical_ram, + ByteCount::from(1024 * 1024) + ); + assert_eq!(sled_agent.reservoir_size, ByteCount::from(1024)); + } + + let sled1_agent = &collection.sled_agents[&sled_agent_id_basic]; + let sled1_bb = sled1_agent.baseboard_id.as_ref().unwrap(); + assert_eq!(sled1_bb.part_number, "model1"); + assert_eq!(sled1_bb.serial_number, "s1"); + let sled4_agent = &collection.sled_agents[&sled_agent_id_extra]; + let sled4_bb = sled4_agent.baseboard_id.as_ref().unwrap(); + assert_eq!(sled4_bb.serial_number, "s4"); + assert!(collection.sled_agents[&sled_agent_id_pc] + .baseboard_id + .is_none()); + assert!(collection.sled_agents[&sled_agent_id_unknown] + .baseboard_id + .is_none()); } // Exercises all the failure cases that shouldn't happen in real systems. diff --git a/nexus/inventory/src/collector.rs b/nexus/inventory/src/collector.rs index aeca6e43a1..ab9af3f9e0 100644 --- a/nexus/inventory/src/collector.rs +++ b/nexus/inventory/src/collector.rs @@ -6,6 +6,7 @@ use crate::builder::CollectionBuilder; use crate::builder::InventoryError; +use crate::SledAgentEnumerator; use anyhow::Context; use gateway_client::types::GetCfpaParams; use gateway_client::types::RotCfpaSlot; @@ -14,25 +15,34 @@ use nexus_types::inventory::CabooseWhich; use nexus_types::inventory::Collection; use nexus_types::inventory::RotPage; use nexus_types::inventory::RotPageWhich; +use slog::o; use slog::{debug, error}; use std::sync::Arc; +use std::time::Duration; use strum::IntoEnumIterator; -pub struct Collector { +/// connection and request timeout used for Sled Agent HTTP client +const SLED_AGENT_TIMEOUT: Duration = Duration::from_secs(60); + +/// Collect all inventory data from an Oxide system +pub struct Collector<'a> { log: slog::Logger, mgs_clients: Vec>, + sled_agent_lister: &'a (dyn SledAgentEnumerator + Send + Sync), in_progress: CollectionBuilder, } -impl Collector { +impl<'a> Collector<'a> { pub fn new( creator: &str, mgs_clients: &[Arc], + sled_agent_lister: &'a (dyn SledAgentEnumerator + Send + Sync), log: slog::Logger, ) -> Self { Collector { log, mgs_clients: mgs_clients.to_vec(), + sled_agent_lister, in_progress: CollectionBuilder::new(creator), } } @@ -54,9 +64,8 @@ impl Collector { debug!(&self.log, "begin collection"); - // When we add stages to collect from other components (e.g., sled - // agents), those will go here. self.collect_all_mgs().await; + self.collect_all_sled_agents().await; debug!(&self.log, "finished collection"); @@ -283,15 +292,95 @@ impl Collector { } } } + + /// Collect inventory from all sled agent instances + async fn collect_all_sled_agents(&mut self) { + let urls = match self.sled_agent_lister.list_sled_agents().await { + Err(error) => { + self.in_progress.found_error(error); + return; + } + Ok(clients) => clients, + }; + + for url in urls { + let log = self.log.new(o!("SledAgent" => url.clone())); + let reqwest_client = reqwest::ClientBuilder::new() + .connect_timeout(SLED_AGENT_TIMEOUT) + .timeout(SLED_AGENT_TIMEOUT) + .build() + .unwrap(); + let client = Arc::new(sled_agent_client::Client::new_with_client( + &url, + reqwest_client, + log, + )); + + if let Err(error) = self.collect_one_sled_agent(&client).await { + error!( + &self.log, + "sled agent {:?}: {:#}", + client.baseurl(), + error + ); + } + } + } + + async fn collect_one_sled_agent( + &mut self, + client: &sled_agent_client::Client, + ) -> Result<(), anyhow::Error> { + let sled_agent_url = client.baseurl(); + debug!(&self.log, "begin collection from Sled Agent"; + "sled_agent_url" => client.baseurl() + ); + + let maybe_ident = client.inventory().await.with_context(|| { + format!("Sled Agent {:?}: inventory", &sled_agent_url) + }); + let inventory = match maybe_ident { + Ok(inventory) => inventory.into_inner(), + Err(error) => { + self.in_progress.found_error(InventoryError::from(error)); + return Ok(()); + } + }; + + let sled_id = inventory.sled_id; + self.in_progress.found_sled_inventory(&sled_agent_url, inventory)?; + + let maybe_config = + client.omicron_zones_get().await.with_context(|| { + format!("Sled Agent {:?}: omicron zones", &sled_agent_url) + }); + match maybe_config { + Err(error) => { + self.in_progress.found_error(InventoryError::from(error)); + Ok(()) + } + Ok(zones) => self.in_progress.found_sled_omicron_zones( + &sled_agent_url, + sled_id, + zones.into_inner(), + ), + } + } } #[cfg(test)] mod test { use super::Collector; + use crate::StaticSledAgentEnumerator; use gateway_messages::SpPort; use nexus_types::inventory::Collection; + use omicron_common::api::external::Generation; + use omicron_sled_agent::sim; use std::fmt::Write; + use std::net::Ipv6Addr; + use std::net::SocketAddrV6; use std::sync::Arc; + use uuid::Uuid; fn dump_collection(collection: &Collection) -> String { // Construct a stable, human-readable summary of the Collection @@ -379,6 +468,35 @@ mod test { } } + write!(&mut s, "\nsled agents found:\n").unwrap(); + for (sled_id, sled_info) in &collection.sled_agents { + assert_eq!(*sled_id, sled_info.sled_id); + write!(&mut s, " sled {} ({:?})\n", sled_id, sled_info.sled_role) + .unwrap(); + write!(&mut s, " baseboard {:?}\n", sled_info.baseboard_id) + .unwrap(); + + if let Some(found_zones) = collection.omicron_zones.get(sled_id) { + assert_eq!(*sled_id, found_zones.sled_id); + write!( + &mut s, + " zone generation: {:?}\n", + found_zones.zones.generation + ) + .unwrap(); + write!(&mut s, " zones found:\n").unwrap(); + for zone in &found_zones.zones.zones { + write!( + &mut s, + " zone {} type {}\n", + zone.id, + zone.zone_type.label(), + ) + .unwrap(); + } + } + } + write!(&mut s, "\nerrors:\n").unwrap(); for e in &collection.errors { // Some error strings have OS error numbers in them. We want to @@ -402,19 +520,75 @@ mod test { s } + async fn sim_sled_agent( + log: slog::Logger, + sled_id: Uuid, + zone_id: Uuid, + ) -> sim::Server { + // Start a simulated sled agent. + let config = + sim::Config::for_testing(sled_id, sim::SimMode::Auto, None, None); + let agent = sim::Server::start(&config, &log, false).await.unwrap(); + + // Pretend to put some zones onto this sled. We don't need to test this + // exhaustively here because there are builder tests that exercise a + // variety of different data. We just want to make sure that if the + // sled agent reports something specific (some non-degenerate case), + // then it shows up in the resulting collection. + let sled_url = format!("http://{}/", agent.http_server.local_addr()); + let client = sled_agent_client::Client::new(&sled_url, log); + + let zone_address = SocketAddrV6::new(Ipv6Addr::LOCALHOST, 123, 0, 0); + client + .omicron_zones_put(&sled_agent_client::types::OmicronZonesConfig { + generation: Generation::from(3), + zones: vec![sled_agent_client::types::OmicronZoneConfig { + id: zone_id, + underlay_address: *zone_address.ip(), + zone_type: + sled_agent_client::types::OmicronZoneType::Oximeter { + address: zone_address.to_string(), + }, + }], + }) + .await + .expect("failed to write initial zone version to fake sled agent"); + + agent + } + #[tokio::test] async fn test_basic() { - // Set up the stock MGS test setup which includes a couple of fake SPs. - // Then run a collection against it. + // Set up the stock MGS test setup (which includes a couple of fake SPs) + // and a simulated sled agent. Then run a collection against these. let gwtestctx = gateway_test_utils::setup::test_setup("test_basic", SpPort::One) .await; let log = &gwtestctx.logctx.log; + let sled1 = sim_sled_agent( + log.clone(), + "9cb9b78f-5614-440c-b66d-e8e81fab69b0".parse().unwrap(), + "5125277f-0988-490b-ac01-3bba20cc8f07".parse().unwrap(), + ) + .await; + let sled2 = sim_sled_agent( + log.clone(), + "03265caf-da7d-46c7-b1c2-39fa90ce5c65".parse().unwrap(), + "8b88a56f-3eb6-4d80-ba42-75d867bc427d".parse().unwrap(), + ) + .await; + let sled1_url = format!("http://{}/", sled1.http_server.local_addr()); + let sled2_url = format!("http://{}/", sled2.http_server.local_addr()); let mgs_url = format!("http://{}/", gwtestctx.client.bind_address); let mgs_client = Arc::new(gateway_client::Client::new(&mgs_url, log.clone())); - let collector = - Collector::new("test-suite", &[mgs_client], log.clone()); + let sled_enum = StaticSledAgentEnumerator::new([sled1_url, sled2_url]); + let collector = Collector::new( + "test-suite", + &[mgs_client], + &sled_enum, + log.clone(), + ); let collection = collector .collect_all() .await @@ -425,6 +599,7 @@ mod test { let s = dump_collection(&collection); expectorate::assert_contents("tests/output/collector_basic.txt", &s); + sled1.http_server.close().await.unwrap(); gwtestctx.teardown().await; } @@ -444,6 +619,20 @@ mod test { ) .await; let log = &gwtestctx1.logctx.log; + let sled1 = sim_sled_agent( + log.clone(), + "9cb9b78f-5614-440c-b66d-e8e81fab69b0".parse().unwrap(), + "5125277f-0988-490b-ac01-3bba20cc8f07".parse().unwrap(), + ) + .await; + let sled2 = sim_sled_agent( + log.clone(), + "03265caf-da7d-46c7-b1c2-39fa90ce5c65".parse().unwrap(), + "8b88a56f-3eb6-4d80-ba42-75d867bc427d".parse().unwrap(), + ) + .await; + let sled1_url = format!("http://{}/", sled1.http_server.local_addr()); + let sled2_url = format!("http://{}/", sled2.http_server.local_addr()); let mgs_clients = [&gwtestctx1, &gwtestctx2] .into_iter() .map(|g| { @@ -452,7 +641,9 @@ mod test { Arc::new(client) }) .collect::>(); - let collector = Collector::new("test-suite", &mgs_clients, log.clone()); + let sled_enum = StaticSledAgentEnumerator::new([sled1_url, sled2_url]); + let collector = + Collector::new("test-suite", &mgs_clients, &sled_enum, log.clone()); let collection = collector .collect_all() .await @@ -463,6 +654,7 @@ mod test { let s = dump_collection(&collection); expectorate::assert_contents("tests/output/collector_basic.txt", &s); + sled1.http_server.close().await.unwrap(); gwtestctx1.teardown().await; gwtestctx2.teardown().await; } @@ -490,7 +682,9 @@ mod test { Arc::new(client) }; let mgs_clients = &[bad_client, real_client]; - let collector = Collector::new("test-suite", mgs_clients, log.clone()); + let sled_enum = StaticSledAgentEnumerator::empty(); + let collector = + Collector::new("test-suite", mgs_clients, &sled_enum, log.clone()); let collection = collector .collect_all() .await @@ -502,4 +696,50 @@ mod test { gwtestctx.teardown().await; } + + #[tokio::test] + async fn test_sled_agent_failure() { + // Similar to the basic test, but use multiple sled agents, one of which + // is non-functional. + let gwtestctx = gateway_test_utils::setup::test_setup( + "test_sled_agent_failure", + SpPort::One, + ) + .await; + let log = &gwtestctx.logctx.log; + let sled1 = sim_sled_agent( + log.clone(), + "9cb9b78f-5614-440c-b66d-e8e81fab69b0".parse().unwrap(), + "5125277f-0988-490b-ac01-3bba20cc8f07".parse().unwrap(), + ) + .await; + let sled1_url = format!("http://{}/", sled1.http_server.local_addr()); + let sledbogus_url = String::from("http://[100::1]:45678"); + let mgs_url = format!("http://{}/", gwtestctx.client.bind_address); + let mgs_client = + Arc::new(gateway_client::Client::new(&mgs_url, log.clone())); + let sled_enum = + StaticSledAgentEnumerator::new([sled1_url, sledbogus_url]); + let collector = Collector::new( + "test-suite", + &[mgs_client], + &sled_enum, + log.clone(), + ); + let collection = collector + .collect_all() + .await + .expect("failed to carry out collection"); + assert!(!collection.errors.is_empty()); + assert_eq!(collection.collector, "test-suite"); + + let s = dump_collection(&collection); + expectorate::assert_contents( + "tests/output/collector_sled_agent_errors.txt", + &s, + ); + + sled1.http_server.close().await.unwrap(); + gwtestctx.teardown().await; + } } diff --git a/nexus/inventory/src/examples.rs b/nexus/inventory/src/examples.rs index 0ce3712942..93ba139c85 100644 --- a/nexus/inventory/src/examples.rs +++ b/nexus/inventory/src/examples.rs @@ -13,10 +13,13 @@ use gateway_client::types::SpState; use gateway_client::types::SpType; use nexus_types::inventory::BaseboardId; use nexus_types::inventory::CabooseWhich; +use nexus_types::inventory::OmicronZonesConfig; use nexus_types::inventory::RotPage; use nexus_types::inventory::RotPageWhich; +use omicron_common::api::external::ByteCount; use std::sync::Arc; use strum::IntoEnumIterator; +use uuid::Uuid; /// Returns an example Collection used for testing /// @@ -264,19 +267,136 @@ pub fn representative() -> Representative { // We deliberately provide no RoT pages for sled2. + // Report some sled agents. + // + // This first one will match "sled1_bb"'s baseboard information. + let sled_agent_id_basic = + "c5aec1df-b897-49e4-8085-ccd975f9b529".parse().unwrap(); + builder + .found_sled_inventory( + "fake sled agent 1", + sled_agent( + sled_agent_id_basic, + sled_agent_client::types::Baseboard::Gimlet { + identifier: String::from("s1"), + model: String::from("model1"), + revision: 0, + }, + sled_agent_client::types::SledRole::Gimlet, + ), + ) + .unwrap(); + + // Here, we report a different sled *with* baseboard information that + // doesn't match one of the baseboards we found. This is unlikely but could + // happen. Make this one a Scrimlet. + let sled4_bb = Arc::new(BaseboardId { + part_number: String::from("model1"), + serial_number: String::from("s4"), + }); + let sled_agent_id_extra = + "d7efa9c4-833d-4354-a9a2-94ba9715c154".parse().unwrap(); + builder + .found_sled_inventory( + "fake sled agent 4", + sled_agent( + sled_agent_id_extra, + sled_agent_client::types::Baseboard::Gimlet { + identifier: sled4_bb.serial_number.clone(), + model: sled4_bb.part_number.clone(), + revision: 0, + }, + sled_agent_client::types::SledRole::Scrimlet, + ), + ) + .unwrap(); + + // Now report a different sled as though it were a PC. It'd be unlikely to + // see a mix of real Oxide hardware and PCs in the same deployment, but this + // exercises different code paths. + let sled_agent_id_pc = + "c4a5325b-e852-4747-b28a-8aaa7eded8a0".parse().unwrap(); + builder + .found_sled_inventory( + "fake sled agent 5", + sled_agent( + sled_agent_id_pc, + sled_agent_client::types::Baseboard::Pc { + identifier: String::from("fellofftruck1"), + model: String::from("fellofftruck"), + }, + sled_agent_client::types::SledRole::Gimlet, + ), + ) + .unwrap(); + + // Finally, report a sled with unknown baseboard information. This should + // look the same as the PC as far as inventory is concerned but let's verify + // it. + let sled_agent_id_unknown = + "5c5b4cf9-3e13-45fd-871c-f177d6537510".parse().unwrap(); + + builder + .found_sled_inventory( + "fake sled agent 6", + sled_agent( + sled_agent_id_unknown, + sled_agent_client::types::Baseboard::Unknown, + sled_agent_client::types::SledRole::Gimlet, + ), + ) + .unwrap(); + + // Report a representative set of Omicron zones. + // + // We've hand-selected a minimal set of files to cover each type of zone. + // These files were constructed by: + // + // (1) copying the "omicron zones" ledgers from the sleds in a working + // Omicron deployment + // (2) pretty-printing each one with `json --in-place --file FILENAME` + // (3) adjusting the format slightly with + // `jq '{ generation: .omicron_generation, zones: .zones }'` + let sled14_data = include_str!("../example-data/madrid-sled14.json"); + let sled16_data = include_str!("../example-data/madrid-sled16.json"); + let sled17_data = include_str!("../example-data/madrid-sled17.json"); + let sled14: OmicronZonesConfig = serde_json::from_str(sled14_data).unwrap(); + let sled16: OmicronZonesConfig = serde_json::from_str(sled16_data).unwrap(); + let sled17: OmicronZonesConfig = serde_json::from_str(sled17_data).unwrap(); + + let sled14_id = "7612d745-d978-41c8-8ee0-84564debe1d2".parse().unwrap(); + builder + .found_sled_omicron_zones("fake sled 14 agent", sled14_id, sled14) + .unwrap(); + let sled16_id = "af56cb43-3422-4f76-85bf-3f229db5f39c".parse().unwrap(); + builder + .found_sled_omicron_zones("fake sled 15 agent", sled16_id, sled16) + .unwrap(); + let sled17_id = "6eb2a0d9-285d-4e03-afa1-090e4656314b".parse().unwrap(); + builder + .found_sled_omicron_zones("fake sled 15 agent", sled17_id, sled17) + .unwrap(); + Representative { builder, - sleds: [sled1_bb, sled2_bb, sled3_bb], + sleds: [sled1_bb, sled2_bb, sled3_bb, sled4_bb], switch: switch1_bb, psc: psc_bb, + sled_agents: [ + sled_agent_id_basic, + sled_agent_id_extra, + sled_agent_id_pc, + sled_agent_id_unknown, + ], } } pub struct Representative { pub builder: CollectionBuilder, - pub sleds: [Arc; 3], + pub sleds: [Arc; 4], pub switch: Arc, pub psc: Arc, + pub sled_agents: [Uuid; 4], } /// Returns an SP state that can be used to populate a collection for testing @@ -314,3 +434,19 @@ pub fn rot_page(unique: &str) -> RotPage { data_base64: base64::engine::general_purpose::STANDARD.encode(unique), } } + +pub fn sled_agent( + sled_id: Uuid, + baseboard: sled_agent_client::types::Baseboard, + sled_role: sled_agent_client::types::SledRole, +) -> sled_agent_client::types::Inventory { + sled_agent_client::types::Inventory { + baseboard, + reservoir_size: ByteCount::from(1024), + sled_role, + sled_agent_address: "[::1]:56792".parse().unwrap(), + sled_id, + usable_hardware_threads: 10, + usable_physical_ram: ByteCount::from(1024 * 1024), + } +} diff --git a/nexus/inventory/src/lib.rs b/nexus/inventory/src/lib.rs index e92c46916d..f11af8fede 100644 --- a/nexus/inventory/src/lib.rs +++ b/nexus/inventory/src/lib.rs @@ -20,6 +20,7 @@ mod builder; mod collector; pub mod examples; +mod sled_agent_enumerator; // only exposed for test code to construct collections pub use builder::CollectionBuilder; @@ -27,3 +28,6 @@ pub use builder::CollectorBug; pub use builder::InventoryError; pub use collector::Collector; + +pub use sled_agent_enumerator::SledAgentEnumerator; +pub use sled_agent_enumerator::StaticSledAgentEnumerator; diff --git a/nexus/inventory/src/sled_agent_enumerator.rs b/nexus/inventory/src/sled_agent_enumerator.rs new file mode 100644 index 0000000000..8a1b480e3f --- /dev/null +++ b/nexus/inventory/src/sled_agent_enumerator.rs @@ -0,0 +1,44 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use crate::InventoryError; +use futures::future::BoxFuture; +use futures::FutureExt; + +/// Describes how to find the list of sled agents to collect from +/// +/// In a real system, this queries the database to list all sleds. But for +/// testing the `StaticSledAgentEnumerator` below can be used to avoid a +/// database dependency. +pub trait SledAgentEnumerator { + /// Returns a list of URLs for Sled Agent HTTP endpoints + fn list_sled_agents( + &self, + ) -> BoxFuture<'_, Result, InventoryError>>; +} + +/// Used to provide an explicit list of sled agents to a `Collector` +/// +/// This is mainly used for testing. +pub struct StaticSledAgentEnumerator { + agents: Vec, +} + +impl StaticSledAgentEnumerator { + pub fn new(iter: impl IntoIterator) -> Self { + StaticSledAgentEnumerator { agents: iter.into_iter().collect() } + } + + pub fn empty() -> Self { + Self::new(std::iter::empty()) + } +} + +impl SledAgentEnumerator for StaticSledAgentEnumerator { + fn list_sled_agents( + &self, + ) -> BoxFuture<'_, Result, InventoryError>> { + futures::future::ready(Ok(self.agents.clone())).boxed() + } +} diff --git a/nexus/inventory/tests/output/collector_basic.txt b/nexus/inventory/tests/output/collector_basic.txt index b9894ff184..e59e19967a 100644 --- a/nexus/inventory/tests/output/collector_basic.txt +++ b/nexus/inventory/tests/output/collector_basic.txt @@ -3,6 +3,8 @@ baseboards: part "FAKE_SIM_GIMLET" serial "SimGimlet01" part "FAKE_SIM_SIDECAR" serial "SimSidecar0" part "FAKE_SIM_SIDECAR" serial "SimSidecar1" + part "sim-gimlet" serial "sim-03265caf-da7d-46c7-b1c2-39fa90ce5c65" + part "sim-gimlet" serial "sim-9cb9b78f-5614-440c-b66d-e8e81fab69b0" cabooses: board "SimGimletSp" name "SimGimlet" version "0.0.1" git_commit "ffffffff" @@ -68,4 +70,16 @@ rot pages found: CfpaScratch baseboard part "FAKE_SIM_SIDECAR" serial "SimSidecar0": data_base64 "c2lkZWNhci1jZnBhLXNjcmF0Y2gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" CfpaScratch baseboard part "FAKE_SIM_SIDECAR" serial "SimSidecar1": data_base64 "c2lkZWNhci1jZnBhLXNjcmF0Y2gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" +sled agents found: + sled 03265caf-da7d-46c7-b1c2-39fa90ce5c65 (Gimlet) + baseboard Some(BaseboardId { part_number: "sim-gimlet", serial_number: "sim-03265caf-da7d-46c7-b1c2-39fa90ce5c65" }) + zone generation: Generation(3) + zones found: + zone 8b88a56f-3eb6-4d80-ba42-75d867bc427d type oximeter + sled 9cb9b78f-5614-440c-b66d-e8e81fab69b0 (Gimlet) + baseboard Some(BaseboardId { part_number: "sim-gimlet", serial_number: "sim-9cb9b78f-5614-440c-b66d-e8e81fab69b0" }) + zone generation: Generation(3) + zones found: + zone 5125277f-0988-490b-ac01-3bba20cc8f07 type oximeter + errors: diff --git a/nexus/inventory/tests/output/collector_errors.txt b/nexus/inventory/tests/output/collector_errors.txt index a50e24ca30..c39d6b249a 100644 --- a/nexus/inventory/tests/output/collector_errors.txt +++ b/nexus/inventory/tests/output/collector_errors.txt @@ -68,5 +68,7 @@ rot pages found: CfpaScratch baseboard part "FAKE_SIM_SIDECAR" serial "SimSidecar0": data_base64 "c2lkZWNhci1jZnBhLXNjcmF0Y2gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" CfpaScratch baseboard part "FAKE_SIM_SIDECAR" serial "SimSidecar1": data_base64 "c2lkZWNhci1jZnBhLXNjcmF0Y2gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" +sled agents found: + errors: error: MGS "http://[100::1]:12345": listing ignition targets: Communication Error <> diff --git a/nexus/inventory/tests/output/collector_sled_agent_errors.txt b/nexus/inventory/tests/output/collector_sled_agent_errors.txt new file mode 100644 index 0000000000..9ebf2cece9 --- /dev/null +++ b/nexus/inventory/tests/output/collector_sled_agent_errors.txt @@ -0,0 +1,80 @@ +baseboards: + part "FAKE_SIM_GIMLET" serial "SimGimlet00" + part "FAKE_SIM_GIMLET" serial "SimGimlet01" + part "FAKE_SIM_SIDECAR" serial "SimSidecar0" + part "FAKE_SIM_SIDECAR" serial "SimSidecar1" + part "sim-gimlet" serial "sim-9cb9b78f-5614-440c-b66d-e8e81fab69b0" + +cabooses: + board "SimGimletSp" name "SimGimlet" version "0.0.1" git_commit "ffffffff" + board "SimRot" name "SimGimlet" version "0.0.1" git_commit "eeeeeeee" + board "SimRot" name "SimSidecar" version "0.0.1" git_commit "eeeeeeee" + board "SimSidecarSp" name "SimSidecar" version "0.0.1" git_commit "ffffffff" + +rot pages: + data_base64 "Z2ltbGV0LWNmcGEtYWN0aXZlAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + data_base64 "Z2ltbGV0LWNmcGEtaW5hY3RpdmUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + data_base64 "Z2ltbGV0LWNmcGEtc2NyYXRjaAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + data_base64 "Z2ltbGV0LWNtcGEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + data_base64 "c2lkZWNhci1jZnBhLWFjdGl2ZQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + data_base64 "c2lkZWNhci1jZnBhLWluYWN0aXZlAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + data_base64 "c2lkZWNhci1jZnBhLXNjcmF0Y2gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + data_base64 "c2lkZWNhci1jbXBhAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + +SPs: + baseboard part "FAKE_SIM_GIMLET" serial "SimGimlet00" + baseboard part "FAKE_SIM_GIMLET" serial "SimGimlet01" + baseboard part "FAKE_SIM_SIDECAR" serial "SimSidecar0" + baseboard part "FAKE_SIM_SIDECAR" serial "SimSidecar1" + +RoTs: + baseboard part "FAKE_SIM_GIMLET" serial "SimGimlet00" + baseboard part "FAKE_SIM_GIMLET" serial "SimGimlet01" + baseboard part "FAKE_SIM_SIDECAR" serial "SimSidecar0" + baseboard part "FAKE_SIM_SIDECAR" serial "SimSidecar1" + +cabooses found: + SpSlot0 baseboard part "FAKE_SIM_GIMLET" serial "SimGimlet00": board "SimGimletSp" + SpSlot0 baseboard part "FAKE_SIM_GIMLET" serial "SimGimlet01": board "SimGimletSp" + SpSlot0 baseboard part "FAKE_SIM_SIDECAR" serial "SimSidecar0": board "SimSidecarSp" + SpSlot0 baseboard part "FAKE_SIM_SIDECAR" serial "SimSidecar1": board "SimSidecarSp" + SpSlot1 baseboard part "FAKE_SIM_GIMLET" serial "SimGimlet00": board "SimGimletSp" + SpSlot1 baseboard part "FAKE_SIM_GIMLET" serial "SimGimlet01": board "SimGimletSp" + SpSlot1 baseboard part "FAKE_SIM_SIDECAR" serial "SimSidecar0": board "SimSidecarSp" + SpSlot1 baseboard part "FAKE_SIM_SIDECAR" serial "SimSidecar1": board "SimSidecarSp" + RotSlotA baseboard part "FAKE_SIM_GIMLET" serial "SimGimlet00": board "SimRot" + RotSlotA baseboard part "FAKE_SIM_GIMLET" serial "SimGimlet01": board "SimRot" + RotSlotA baseboard part "FAKE_SIM_SIDECAR" serial "SimSidecar0": board "SimRot" + RotSlotA baseboard part "FAKE_SIM_SIDECAR" serial "SimSidecar1": board "SimRot" + RotSlotB baseboard part "FAKE_SIM_GIMLET" serial "SimGimlet00": board "SimRot" + RotSlotB baseboard part "FAKE_SIM_GIMLET" serial "SimGimlet01": board "SimRot" + RotSlotB baseboard part "FAKE_SIM_SIDECAR" serial "SimSidecar0": board "SimRot" + RotSlotB baseboard part "FAKE_SIM_SIDECAR" serial "SimSidecar1": board "SimRot" + +rot pages found: + Cmpa baseboard part "FAKE_SIM_GIMLET" serial "SimGimlet00": data_base64 "Z2ltbGV0LWNtcGEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + Cmpa baseboard part "FAKE_SIM_GIMLET" serial "SimGimlet01": data_base64 "Z2ltbGV0LWNtcGEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + Cmpa baseboard part "FAKE_SIM_SIDECAR" serial "SimSidecar0": data_base64 "c2lkZWNhci1jbXBhAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + Cmpa baseboard part "FAKE_SIM_SIDECAR" serial "SimSidecar1": data_base64 "c2lkZWNhci1jbXBhAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + CfpaActive baseboard part "FAKE_SIM_GIMLET" serial "SimGimlet00": data_base64 "Z2ltbGV0LWNmcGEtYWN0aXZlAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + CfpaActive baseboard part "FAKE_SIM_GIMLET" serial "SimGimlet01": data_base64 "Z2ltbGV0LWNmcGEtYWN0aXZlAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + CfpaActive baseboard part "FAKE_SIM_SIDECAR" serial "SimSidecar0": data_base64 "c2lkZWNhci1jZnBhLWFjdGl2ZQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + CfpaActive baseboard part "FAKE_SIM_SIDECAR" serial "SimSidecar1": data_base64 "c2lkZWNhci1jZnBhLWFjdGl2ZQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + CfpaInactive baseboard part "FAKE_SIM_GIMLET" serial "SimGimlet00": data_base64 "Z2ltbGV0LWNmcGEtaW5hY3RpdmUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + CfpaInactive baseboard part "FAKE_SIM_GIMLET" serial "SimGimlet01": data_base64 "Z2ltbGV0LWNmcGEtaW5hY3RpdmUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + CfpaInactive baseboard part "FAKE_SIM_SIDECAR" serial "SimSidecar0": data_base64 "c2lkZWNhci1jZnBhLWluYWN0aXZlAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + CfpaInactive baseboard part "FAKE_SIM_SIDECAR" serial "SimSidecar1": data_base64 "c2lkZWNhci1jZnBhLWluYWN0aXZlAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + CfpaScratch baseboard part "FAKE_SIM_GIMLET" serial "SimGimlet00": data_base64 "Z2ltbGV0LWNmcGEtc2NyYXRjaAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + CfpaScratch baseboard part "FAKE_SIM_GIMLET" serial "SimGimlet01": data_base64 "Z2ltbGV0LWNmcGEtc2NyYXRjaAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + CfpaScratch baseboard part "FAKE_SIM_SIDECAR" serial "SimSidecar0": data_base64 "c2lkZWNhci1jZnBhLXNjcmF0Y2gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + CfpaScratch baseboard part "FAKE_SIM_SIDECAR" serial "SimSidecar1": data_base64 "c2lkZWNhci1jZnBhLXNjcmF0Y2gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + +sled agents found: + sled 9cb9b78f-5614-440c-b66d-e8e81fab69b0 (Gimlet) + baseboard Some(BaseboardId { part_number: "sim-gimlet", serial_number: "sim-9cb9b78f-5614-440c-b66d-e8e81fab69b0" }) + zone generation: Generation(3) + zones found: + zone 5125277f-0988-490b-ac01-3bba20cc8f07 type oximeter + +errors: +error: Sled Agent "http://[100::1]:45678": inventory: Communication Error <> diff --git a/nexus/src/app/background/inventory_collection.rs b/nexus/src/app/background/inventory_collection.rs index f095b094db..5c52fa519b 100644 --- a/nexus/src/app/background/inventory_collection.rs +++ b/nexus/src/app/background/inventory_collection.rs @@ -11,11 +11,18 @@ use futures::future::BoxFuture; use futures::FutureExt; use internal_dns::ServiceName; use nexus_db_queries::context::OpContext; +use nexus_db_queries::db::pagination::Paginator; use nexus_db_queries::db::DataStore; +use nexus_inventory::InventoryError; +use nexus_types::identity::Asset; use nexus_types::inventory::Collection; use serde_json::json; +use std::num::NonZeroU32; use std::sync::Arc; +/// How many rows to request in each paginated database query +const DB_PAGE_SIZE: u32 = 1024; + /// Background task that reads inventory for the rack pub struct InventoryCollector { datastore: Arc, @@ -123,10 +130,15 @@ async fn inventory_activate( }) .collect::>(); + // Create an enumerator to find sled agents. + let page_size = NonZeroU32::new(DB_PAGE_SIZE).unwrap(); + let sled_enum = DbSledAgentEnumerator { opctx, datastore, page_size }; + // Run a collection. let inventory = nexus_inventory::Collector::new( creator, &mgs_clients, + &sled_enum, opctx.log.clone(), ); let collection = @@ -141,14 +153,64 @@ async fn inventory_activate( Ok(collection) } +/// Determine which sleds to inventory based on what's in the database +/// +/// We only want to inventory what's actually part of the control plane (i.e., +/// has a "sled" record). +struct DbSledAgentEnumerator<'a> { + opctx: &'a OpContext, + datastore: &'a DataStore, + page_size: NonZeroU32, +} + +impl<'a> nexus_inventory::SledAgentEnumerator for DbSledAgentEnumerator<'a> { + fn list_sled_agents( + &self, + ) -> BoxFuture<'_, Result, InventoryError>> { + async { + let mut all_sleds = Vec::new(); + let mut paginator = Paginator::new(self.page_size); + while let Some(p) = paginator.next() { + let records_batch = self + .datastore + .sled_list(&self.opctx, &p.current_pagparams()) + .await + .context("listing sleds")?; + paginator = p.found_batch( + &records_batch, + &|s: &nexus_db_model::Sled| s.id(), + ); + all_sleds.extend( + records_batch + .into_iter() + .map(|sled| format!("http://{}", sled.address())), + ); + } + + Ok(all_sleds) + } + .boxed() + } +} + #[cfg(test)] mod test { use crate::app::background::common::BackgroundTask; + use crate::app::background::inventory_collection::DbSledAgentEnumerator; use crate::app::background::inventory_collection::InventoryCollector; + use nexus_db_model::SledBaseboard; + use nexus_db_model::SledSystemHardware; + use nexus_db_model::SledUpdate; use nexus_db_queries::context::OpContext; use nexus_db_queries::db::datastore::DataStoreInventoryTest; + use nexus_inventory::SledAgentEnumerator; use nexus_test_utils_macros::nexus_test; + use omicron_common::api::external::ByteCount; use omicron_test_utils::dev::poll; + use std::net::Ipv6Addr; + use std::net::SocketAddrV6; + use std::num::NonZeroU32; + use uuid::Uuid; type ControlPlaneTestContext = nexus_test_utils::ControlPlaneTestContext; @@ -240,4 +302,80 @@ mod test { let latest = datastore.inventory_collections().await.unwrap(); assert_eq!(previous, latest); } + + #[nexus_test(server = crate::Server)] + async fn test_db_sled_enumerator(cptestctx: &ControlPlaneTestContext) { + let nexus = &cptestctx.server.apictx().nexus; + let datastore = nexus.datastore(); + let opctx = OpContext::for_tests( + cptestctx.logctx.log.clone(), + datastore.clone(), + ); + let db_enum = DbSledAgentEnumerator { + opctx: &opctx, + datastore: &datastore, + page_size: NonZeroU32::new(3).unwrap(), + }; + + // There will be one sled agent set up as part of the test context. + let found_urls = db_enum.list_sled_agents().await.unwrap(); + assert_eq!(found_urls.len(), 1); + + // Insert some sleds. + let rack_id = Uuid::new_v4(); + let mut sleds = Vec::new(); + for i in 0..64 { + let sled = SledUpdate::new( + Uuid::new_v4(), + SocketAddrV6::new(Ipv6Addr::LOCALHOST, 1200 + i, 0, 0), + SledBaseboard { + serial_number: format!("serial-{}", i), + part_number: String::from("fake-sled"), + revision: 3, + }, + SledSystemHardware { + is_scrimlet: false, + usable_hardware_threads: 12, + usable_physical_ram: ByteCount::from_gibibytes_u32(16) + .into(), + reservoir_size: ByteCount::from_gibibytes_u32(8).into(), + }, + rack_id, + ); + sleds.push(datastore.sled_upsert(sled).await.unwrap()); + } + + // The same enumerator should immediately find all the new sleds. + let mut expected_urls: Vec<_> = found_urls + .into_iter() + .chain(sleds.into_iter().map(|s| format!("http://{}", s.address()))) + .collect(); + expected_urls.sort(); + println!("expected_urls: {:?}", expected_urls); + + let mut found_urls = db_enum.list_sled_agents().await.unwrap(); + found_urls.sort(); + assert_eq!(expected_urls, found_urls); + + // We should get the same result even with a page size of 1. + let db_enum = DbSledAgentEnumerator { + opctx: &opctx, + datastore: &datastore, + page_size: NonZeroU32::new(1).unwrap(), + }; + let mut found_urls = db_enum.list_sled_agents().await.unwrap(); + found_urls.sort(); + assert_eq!(expected_urls, found_urls); + + // We should get the same result even with a page size much larger than + // we need. + let db_enum = DbSledAgentEnumerator { + opctx: &opctx, + datastore: &datastore, + page_size: NonZeroU32::new(1024).unwrap(), + }; + let mut found_urls = db_enum.list_sled_agents().await.unwrap(); + found_urls.sort(); + assert_eq!(expected_urls, found_urls); + } } diff --git a/nexus/src/app/sled.rs b/nexus/src/app/sled.rs index 44efc2934e..943490ac04 100644 --- a/nexus/src/app/sled.rs +++ b/nexus/src/app/sled.rs @@ -96,9 +96,9 @@ impl super::Nexus { // but for now, connections to sled agents are constructed // on an "as requested" basis. // - // Franky, returning an "Arc" here without a connection pool is a little - // silly; it's not actually used if each client connection exists as a - // one-shot. + // Frankly, returning an "Arc" here without a connection pool is a + // little silly; it's not actually used if each client connection exists + // as a one-shot. let (.., sled) = self.sled_lookup(&self.opctx_alloc, id)?.fetch().await?; diff --git a/nexus/test-utils/src/lib.rs b/nexus/test-utils/src/lib.rs index 52ff8910f9..d2ac0405fc 100644 --- a/nexus/test-utils/src/lib.rs +++ b/nexus/test-utils/src/lib.rs @@ -51,6 +51,9 @@ use trust_dns_resolver::config::ResolverOpts; use trust_dns_resolver::TokioAsyncResolver; use uuid::Uuid; +pub use sim::TEST_HARDWARE_THREADS; +pub use sim::TEST_RESERVOIR_RAM; + pub mod db; pub mod http_testing; pub mod resource_helpers; @@ -62,13 +65,6 @@ pub const OXIMETER_UUID: &str = "39e6175b-4df2-4730-b11d-cbc1e60a2e78"; pub const PRODUCER_UUID: &str = "a6458b7d-87c3-4483-be96-854d814c20de"; pub const RACK_SUBNET: &str = "fd00:1122:3344:01::/56"; -/// The reported amount of hardware threads for an emulated sled agent. -pub const TEST_HARDWARE_THREADS: u32 = 16; -/// The reported amount of physical RAM for an emulated sled agent. -pub const TEST_PHYSICAL_RAM: u64 = 32 * (1 << 30); -/// The reported amount of VMM reservoir RAM for an emulated sled agent. -pub const TEST_RESERVOIR_RAM: u64 = 16 * (1 << 30); - /// Password for the user created by the test suite /// /// This is only used by the test suite and `omicron-dev run-all` (the latter of @@ -994,32 +990,15 @@ pub async fn start_sled_agent( update_directory: &Utf8Path, sim_mode: sim::SimMode, ) -> Result { - let config = sim::Config { + let config = sim::Config::for_testing( id, sim_mode, - nexus_address, - dropshot: ConfigDropshot { - bind_address: SocketAddr::new(Ipv6Addr::LOCALHOST.into(), 0), - request_body_max_bytes: 1024 * 1024, - default_handler_task_mode: HandlerTaskMode::Detached, - }, - // TODO-cleanup this is unused - log: ConfigLogging::StderrTerminal { level: ConfigLoggingLevel::Debug }, - storage: sim::ConfigStorage { - zpools: vec![], - ip: IpAddr::from(Ipv6Addr::LOCALHOST), - }, - updates: sim::ConfigUpdates { - zone_artifact_path: update_directory.to_path_buf(), - }, - hardware: sim::ConfigHardware { - hardware_threads: TEST_HARDWARE_THREADS, - physical_ram: TEST_PHYSICAL_RAM, - reservoir_ram: TEST_RESERVOIR_RAM, - }, - }; - let server = - sim::Server::start(&config, &log).await.map_err(|e| e.to_string())?; + Some(nexus_address), + Some(update_directory), + ); + let server = sim::Server::start(&config, &log, true) + .await + .map_err(|e| e.to_string())?; Ok(server) } diff --git a/nexus/types/Cargo.toml b/nexus/types/Cargo.toml index 9cb94a8484..90ec67c0e6 100644 --- a/nexus/types/Cargo.toml +++ b/nexus/types/Cargo.toml @@ -24,3 +24,4 @@ gateway-client.workspace = true omicron-common.workspace = true omicron-passwords.workspace = true omicron-workspace-hack.workspace = true +sled-agent-client.workspace = true diff --git a/nexus/types/src/inventory.rs b/nexus/types/src/inventory.rs index 77bc73306d..b27d7277ba 100644 --- a/nexus/types/src/inventory.rs +++ b/nexus/types/src/inventory.rs @@ -9,20 +9,31 @@ //! nexus/inventory does not currently know about nexus/db-model and it's //! convenient to separate these concerns.) +use crate::external_api::params::UninitializedSledId; +use crate::external_api::shared::Baseboard; use chrono::DateTime; use chrono::Utc; pub use gateway_client::types::PowerState; pub use gateway_client::types::RotSlot; pub use gateway_client::types::SpType; +use omicron_common::api::external::ByteCount; +pub use sled_agent_client::types::NetworkInterface; +pub use sled_agent_client::types::NetworkInterfaceKind; +pub use sled_agent_client::types::OmicronZoneConfig; +pub use sled_agent_client::types::OmicronZoneDataset; +pub use sled_agent_client::types::OmicronZoneType; +pub use sled_agent_client::types::OmicronZonesConfig; +pub use sled_agent_client::types::SledRole; +pub use sled_agent_client::types::SourceNatConfig; +pub use sled_agent_client::types::Vni; +pub use sled_agent_client::types::ZpoolName; use std::collections::BTreeMap; use std::collections::BTreeSet; +use std::net::SocketAddrV6; use std::sync::Arc; use strum::EnumIter; use uuid::Uuid; -use crate::external_api::params::UninitializedSledId; -use crate::external_api::shared::Baseboard; - /// Results of collecting hardware/software inventory from various Omicron /// components /// @@ -89,6 +100,12 @@ pub struct Collection { /// table. pub rot_pages_found: BTreeMap, RotPageFound>>, + + /// Sled Agent information, by *sled* id + pub sled_agents: BTreeMap, + + /// Omicron zones found, by *sled* id + pub omicron_zones: BTreeMap, } impl Collection { @@ -269,3 +286,30 @@ impl IntoRotPage for gateway_client::types::RotCfpa { (which, RotPage { data_base64: self.base64_data }) } } + +/// Inventory reported by sled agent +/// +/// This is a software notion of a sled, distinct from an underlying baseboard. +/// A sled may be on a PC (in dev/test environments) and have no associated +/// baseboard. There might also be baseboards with no associated sled (if +/// they have not been formally added to the control plane). +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SledAgent { + pub time_collected: DateTime, + pub source: String, + pub sled_id: Uuid, + pub baseboard_id: Option>, + pub sled_agent_address: SocketAddrV6, + pub sled_role: SledRole, + pub usable_hardware_threads: u32, + pub usable_physical_ram: ByteCount, + pub reservoir_size: ByteCount, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct OmicronZonesFound { + pub time_collected: DateTime, + pub source: String, + pub sled_id: Uuid, + pub zones: OmicronZonesConfig, +} diff --git a/openapi/sled-agent.json b/openapi/sled-agent.json index 90fc53cb0b..3e3f6abec6 100644 --- a/openapi/sled-agent.json +++ b/openapi/sled-agent.json @@ -487,6 +487,30 @@ } } }, + "/inventory": { + "get": { + "summary": "Fetch basic information about this sled", + "operationId": "inventory", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Inventory" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, "/metrics/collect/{producer_id}": { "get": { "summary": "Collect oximeter samples from the sled agent.", @@ -5031,6 +5055,45 @@ } } }, + "Inventory": { + "description": "Identity and basic status information about this sled agent", + "type": "object", + "properties": { + "baseboard": { + "$ref": "#/components/schemas/Baseboard" + }, + "reservoir_size": { + "$ref": "#/components/schemas/ByteCount" + }, + "sled_agent_address": { + "type": "string" + }, + "sled_id": { + "type": "string", + "format": "uuid" + }, + "sled_role": { + "$ref": "#/components/schemas/SledRole" + }, + "usable_hardware_threads": { + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "usable_physical_ram": { + "$ref": "#/components/schemas/ByteCount" + } + }, + "required": [ + "baseboard", + "reservoir_size", + "sled_agent_address", + "sled_id", + "sled_role", + "usable_hardware_threads", + "usable_physical_ram" + ] + }, "IpNet": { "oneOf": [ { @@ -6269,6 +6332,7 @@ ] }, "SledRole": { + "description": "Describes the role of the sled within the rack.\n\nNote that this may change if the sled is physically moved within the rack.", "oneOf": [ { "description": "The sled is a general compute sled.", diff --git a/oximeter/db/Cargo.toml b/oximeter/db/Cargo.toml index 99985a3b80..c4ee44acb6 100644 --- a/oximeter/db/Cargo.toml +++ b/oximeter/db/Cargo.toml @@ -28,7 +28,7 @@ slog.workspace = true slog-async.workspace = true slog-term.workspace = true sqlparser.workspace = true -sqlformat = "0.2.2" +sqlformat = "0.2.3" tabled.workspace = true thiserror.workspace = true usdt.workspace = true diff --git a/schema/crdb/22.0.0/up01.sql b/schema/crdb/22.0.0/up01.sql index 0cb511fb91..2e7699d24b 100644 --- a/schema/crdb/22.0.0/up01.sql +++ b/schema/crdb/22.0.0/up01.sql @@ -1,6 +1,4 @@ -CREATE TYPE IF NOT EXISTS omicron.public.ip_attach_state AS ENUM ( - 'detached', - 'attached', - 'detaching', - 'attaching' +CREATE TYPE IF NOT EXISTS omicron.public.sled_role AS ENUM ( + 'scrimlet', + 'gimlet' ); diff --git a/schema/crdb/22.0.0/up02.sql b/schema/crdb/22.0.0/up02.sql index 324a907dd4..8f8ddea015 100644 --- a/schema/crdb/22.0.0/up02.sql +++ b/schema/crdb/22.0.0/up02.sql @@ -1,4 +1,19 @@ --- Intentionally nullable for now as we need to backfill using the current --- value of parent_id. -ALTER TABLE omicron.public.external_ip -ADD COLUMN IF NOT EXISTS state omicron.public.ip_attach_state; +CREATE TABLE IF NOT EXISTS omicron.public.inv_sled_agent ( + inv_collection_id UUID NOT NULL, + time_collected TIMESTAMPTZ NOT NULL, + source TEXT NOT NULL, + + sled_id UUID NOT NULL, + + hw_baseboard_id UUID, + + sled_agent_ip INET NOT NULL, + sled_agent_port INT4 NOT NULL, + sled_role omicron.public.sled_role NOT NULL, + usable_hardware_threads INT8 + CHECK (usable_hardware_threads BETWEEN 0 AND 4294967295) NOT NULL, + usable_physical_ram INT8 NOT NULL, + reservoir_size INT8 CHECK (reservoir_size < usable_physical_ram) NOT NULL, + + PRIMARY KEY (inv_collection_id, sled_id) +); diff --git a/schema/crdb/22.0.0/up03.sql b/schema/crdb/22.0.0/up03.sql index ea1d461250..b741141b2b 100644 --- a/schema/crdb/22.0.0/up03.sql +++ b/schema/crdb/22.0.0/up03.sql @@ -1,7 +1,11 @@ --- initialise external ip state for detached IPs. -set - local disallow_full_table_scans = off; +CREATE TABLE IF NOT EXISTS omicron.public.inv_sled_omicron_zones ( + inv_collection_id UUID NOT NULL, + time_collected TIMESTAMPTZ NOT NULL, + source TEXT NOT NULL, -UPDATE omicron.public.external_ip -SET state = 'detached' -WHERE parent_id IS NULL; + sled_id UUID NOT NULL, + + generation INT8 NOT NULL, + + PRIMARY KEY (inv_collection_id, sled_id) +); diff --git a/schema/crdb/22.0.0/up04.sql b/schema/crdb/22.0.0/up04.sql index 7bf89d6626..74620e9685 100644 --- a/schema/crdb/22.0.0/up04.sql +++ b/schema/crdb/22.0.0/up04.sql @@ -1,7 +1,13 @@ --- initialise external ip state for attached IPs. -set - local disallow_full_table_scans = off; - -UPDATE omicron.public.external_ip -SET state = 'attached' -WHERE parent_id IS NOT NULL; +CREATE TYPE IF NOT EXISTS omicron.public.zone_type AS ENUM ( + 'boundary_ntp', + 'clickhouse', + 'clickhouse_keeper', + 'cockroach_db', + 'crucible', + 'crucible_pantry', + 'external_dns', + 'internal_dns', + 'internal_ntp', + 'nexus', + 'oximeter' +); diff --git a/schema/crdb/22.0.0/up05.sql b/schema/crdb/22.0.0/up05.sql index 894806a3dc..11d8684854 100644 --- a/schema/crdb/22.0.0/up05.sql +++ b/schema/crdb/22.0.0/up05.sql @@ -1,2 +1,41 @@ --- Now move the new column to its intended state of non-nullable. -ALTER TABLE omicron.public.external_ip ALTER COLUMN state SET NOT NULL; +CREATE TABLE IF NOT EXISTS omicron.public.inv_omicron_zone ( + inv_collection_id UUID NOT NULL, + + sled_id UUID NOT NULL, + + id UUID NOT NULL, + underlay_address INET NOT NULL, + zone_type omicron.public.zone_type NOT NULL, + + primary_service_ip INET NOT NULL, + primary_service_port INT4 + CHECK (primary_service_port BETWEEN 0 AND 65535) + NOT NULL, + + second_service_ip INET, + second_service_port INT4 + CHECK (second_service_port IS NULL + OR second_service_port BETWEEN 0 AND 65535), + + dataset_zpool_name TEXT, + + nic_id UUID, + + dns_gz_address INET, + dns_gz_address_index INT8, + + ntp_ntp_servers TEXT[], + ntp_dns_servers INET[], + ntp_domain TEXT, + + nexus_external_tls BOOLEAN, + nexus_external_dns_servers INET ARRAY, + + snat_ip INET, + snat_first_port INT4 + CHECK (snat_first_port IS NULL OR snat_first_port BETWEEN 0 AND 65535), + snat_last_port INT4 + CHECK (snat_last_port IS NULL OR snat_last_port BETWEEN 0 AND 65535), + + PRIMARY KEY (inv_collection_id, id) +); diff --git a/schema/crdb/22.0.0/up06.sql b/schema/crdb/22.0.0/up06.sql index ca19081e37..3d50bcfefd 100644 --- a/schema/crdb/22.0.0/up06.sql +++ b/schema/crdb/22.0.0/up06.sql @@ -1,4 +1,13 @@ -ALTER TABLE omicron.public.external_ip -ADD CONSTRAINT IF NOT EXISTS detached_null_parent_id CHECK ( - (state = 'detached') OR (parent_id IS NOT NULL) +CREATE TABLE IF NOT EXISTS omicron.public.inv_omicron_zone_nic ( + inv_collection_id UUID NOT NULL, + id UUID NOT NULL, + name TEXT NOT NULL, + ip INET NOT NULL, + mac INT8 NOT NULL, + subnet INET NOT NULL, + vni INT8 NOT NULL, + is_primary BOOLEAN NOT NULL, + slot INT2 NOT NULL, + + PRIMARY KEY (inv_collection_id, id) ); diff --git a/schema/crdb/23.0.0/up01.sql b/schema/crdb/23.0.0/up01.sql new file mode 100644 index 0000000000..0cb511fb91 --- /dev/null +++ b/schema/crdb/23.0.0/up01.sql @@ -0,0 +1,6 @@ +CREATE TYPE IF NOT EXISTS omicron.public.ip_attach_state AS ENUM ( + 'detached', + 'attached', + 'detaching', + 'attaching' +); diff --git a/schema/crdb/23.0.0/up02.sql b/schema/crdb/23.0.0/up02.sql new file mode 100644 index 0000000000..324a907dd4 --- /dev/null +++ b/schema/crdb/23.0.0/up02.sql @@ -0,0 +1,4 @@ +-- Intentionally nullable for now as we need to backfill using the current +-- value of parent_id. +ALTER TABLE omicron.public.external_ip +ADD COLUMN IF NOT EXISTS state omicron.public.ip_attach_state; diff --git a/schema/crdb/23.0.0/up03.sql b/schema/crdb/23.0.0/up03.sql new file mode 100644 index 0000000000..ea1d461250 --- /dev/null +++ b/schema/crdb/23.0.0/up03.sql @@ -0,0 +1,7 @@ +-- initialise external ip state for detached IPs. +set + local disallow_full_table_scans = off; + +UPDATE omicron.public.external_ip +SET state = 'detached' +WHERE parent_id IS NULL; diff --git a/schema/crdb/23.0.0/up04.sql b/schema/crdb/23.0.0/up04.sql new file mode 100644 index 0000000000..7bf89d6626 --- /dev/null +++ b/schema/crdb/23.0.0/up04.sql @@ -0,0 +1,7 @@ +-- initialise external ip state for attached IPs. +set + local disallow_full_table_scans = off; + +UPDATE omicron.public.external_ip +SET state = 'attached' +WHERE parent_id IS NOT NULL; diff --git a/schema/crdb/23.0.0/up05.sql b/schema/crdb/23.0.0/up05.sql new file mode 100644 index 0000000000..894806a3dc --- /dev/null +++ b/schema/crdb/23.0.0/up05.sql @@ -0,0 +1,2 @@ +-- Now move the new column to its intended state of non-nullable. +ALTER TABLE omicron.public.external_ip ALTER COLUMN state SET NOT NULL; diff --git a/schema/crdb/23.0.0/up06.sql b/schema/crdb/23.0.0/up06.sql new file mode 100644 index 0000000000..ca19081e37 --- /dev/null +++ b/schema/crdb/23.0.0/up06.sql @@ -0,0 +1,4 @@ +ALTER TABLE omicron.public.external_ip +ADD CONSTRAINT IF NOT EXISTS detached_null_parent_id CHECK ( + (state = 'detached') OR (parent_id IS NOT NULL) +); diff --git a/schema/crdb/22.0.0/up07.sql b/schema/crdb/23.0.0/up07.sql similarity index 100% rename from schema/crdb/22.0.0/up07.sql rename to schema/crdb/23.0.0/up07.sql diff --git a/schema/crdb/22.0.0/up08.sql b/schema/crdb/23.0.0/up08.sql similarity index 100% rename from schema/crdb/22.0.0/up08.sql rename to schema/crdb/23.0.0/up08.sql diff --git a/schema/crdb/22.0.0/up09.sql b/schema/crdb/23.0.0/up09.sql similarity index 100% rename from schema/crdb/22.0.0/up09.sql rename to schema/crdb/23.0.0/up09.sql diff --git a/schema/crdb/dbinit.sql b/schema/crdb/dbinit.sql index 0e38859251..88ee585624 100644 --- a/schema/crdb/dbinit.sql +++ b/schema/crdb/dbinit.sql @@ -2946,6 +2946,161 @@ CREATE TABLE IF NOT EXISTS omicron.public.inv_root_of_trust_page ( PRIMARY KEY (inv_collection_id, hw_baseboard_id, which) ); +CREATE TYPE IF NOT EXISTS omicron.public.sled_role AS ENUM ( + -- this sled is directly attached to a Sidecar + 'scrimlet', + -- everything else + 'gimlet' +); + +-- observations from and about sled agents +CREATE TABLE IF NOT EXISTS omicron.public.inv_sled_agent ( + -- where this observation came from + -- (foreign key into `inv_collection` table) + inv_collection_id UUID NOT NULL, + -- when this observation was made + time_collected TIMESTAMPTZ NOT NULL, + -- URL of the sled agent that reported this data + source TEXT NOT NULL, + + -- unique id for this sled (should be foreign keys into `sled` table, though + -- it's conceivable a sled will report an id that we don't know about) + sled_id UUID NOT NULL, + + -- which system this sled agent reports it's running on + -- (foreign key into `hw_baseboard_id` table) + -- This is optional because dev/test systems support running on non-Oxide + -- hardware. + hw_baseboard_id UUID, + + -- Many of the following properties are duplicated from the `sled` table, + -- which predates the current inventory system. + sled_agent_ip INET NOT NULL, + sled_agent_port INT4 NOT NULL, + sled_role omicron.public.sled_role NOT NULL, + usable_hardware_threads INT8 + CHECK (usable_hardware_threads BETWEEN 0 AND 4294967295) NOT NULL, + usable_physical_ram INT8 NOT NULL, + reservoir_size INT8 CHECK (reservoir_size < usable_physical_ram) NOT NULL, + + PRIMARY KEY (inv_collection_id, sled_id) +); + +CREATE TABLE IF NOT EXISTS omicron.public.inv_sled_omicron_zones ( + -- where this observation came from + -- (foreign key into `inv_collection` table) + inv_collection_id UUID NOT NULL, + -- when this observation was made + time_collected TIMESTAMPTZ NOT NULL, + -- URL of the sled agent that reported this data + source TEXT NOT NULL, + + -- unique id for this sled (should be foreign keys into `sled` table, though + -- it's conceivable a sled will report an id that we don't know about) + sled_id UUID NOT NULL, + + -- OmicronZonesConfig generation reporting these zones + generation INT8 NOT NULL, + + PRIMARY KEY (inv_collection_id, sled_id) +); + +CREATE TYPE IF NOT EXISTS omicron.public.zone_type AS ENUM ( + 'boundary_ntp', + 'clickhouse', + 'clickhouse_keeper', + 'cockroach_db', + 'crucible', + 'crucible_pantry', + 'external_dns', + 'internal_dns', + 'internal_ntp', + 'nexus', + 'oximeter' +); + +-- observations from sled agents about Omicron-managed zones +CREATE TABLE IF NOT EXISTS omicron.public.inv_omicron_zone ( + -- where this observation came from + -- (foreign key into `inv_collection` table) + inv_collection_id UUID NOT NULL, + + -- unique id for this sled (should be foreign keys into `sled` table, though + -- it's conceivable a sled will report an id that we don't know about) + sled_id UUID NOT NULL, + + -- unique id for this zone + id UUID NOT NULL, + underlay_address INET NOT NULL, + zone_type omicron.public.zone_type NOT NULL, + + -- SocketAddr of the "primary" service for this zone + -- (what this describes varies by zone type, but all zones have at least one + -- service in them) + primary_service_ip INET NOT NULL, + primary_service_port INT4 + CHECK (primary_service_port BETWEEN 0 AND 65535) + NOT NULL, + + -- The remaining properties may be NULL for different kinds of zones. The + -- specific constraints are not enforced at the database layer, basically + -- because it's really complicated to do that and it's not obvious that it's + -- worthwhile. + + -- Some zones have a second service. Like the primary one, the meaning of + -- this is zone-type-dependent. + second_service_ip INET, + second_service_port INT4 + CHECK (second_service_port IS NULL + OR second_service_port BETWEEN 0 AND 65535), + + -- Zones may have an associated dataset. They're currently always on a U.2. + -- The only thing we need to identify it here is the name of the zpool that + -- it's on. + dataset_zpool_name TEXT, + + -- Zones with external IPs have an associated NIC and sockaddr for listening + -- (first is a foreign key into `inv_omicron_zone_nic`) + nic_id UUID, + + -- Properties for internal DNS servers + -- address attached to this zone from outside the sled's subnet + dns_gz_address INET, + dns_gz_address_index INT8, + + -- Properties common to both kinds of NTP zones + ntp_ntp_servers TEXT[], + ntp_dns_servers INET[], + ntp_domain TEXT, + + -- Properties specific to Nexus zones + nexus_external_tls BOOLEAN, + nexus_external_dns_servers INET ARRAY, + + -- Source NAT configuration (currently used for boundary NTP only) + snat_ip INET, + snat_first_port INT4 + CHECK (snat_first_port IS NULL OR snat_first_port BETWEEN 0 AND 65535), + snat_last_port INT4 + CHECK (snat_last_port IS NULL OR snat_last_port BETWEEN 0 AND 65535), + + PRIMARY KEY (inv_collection_id, id) +); + +CREATE TABLE IF NOT EXISTS omicron.public.inv_omicron_zone_nic ( + inv_collection_id UUID NOT NULL, + id UUID NOT NULL, + name TEXT NOT NULL, + ip INET NOT NULL, + mac INT8 NOT NULL, + subnet INET NOT NULL, + vni INT8 NOT NULL, + is_primary BOOLEAN NOT NULL, + slot INT2 NOT NULL, + + PRIMARY KEY (inv_collection_id, id) +); + /*******************************************************************/ /* @@ -3126,7 +3281,7 @@ INSERT INTO omicron.public.db_metadata ( version, target_version ) VALUES - ( TRUE, NOW(), NOW(), '22.0.0', NULL) + ( TRUE, NOW(), NOW(), '23.0.0', NULL) ON CONFLICT DO NOTHING; COMMIT; diff --git a/sled-agent/src/bin/sled-agent-sim.rs b/sled-agent/src/bin/sled-agent-sim.rs index ee0ebda71e..4b3bc9e432 100644 --- a/sled-agent/src/bin/sled-agent-sim.rs +++ b/sled-agent/src/bin/sled-agent-sim.rs @@ -12,15 +12,15 @@ use clap::Parser; use dropshot::ConfigDropshot; use dropshot::ConfigLogging; use dropshot::ConfigLoggingLevel; -use dropshot::HandlerTaskMode; use nexus_client::types as NexusTypes; use omicron_common::cmd::fatal; use omicron_common::cmd::CmdError; use omicron_sled_agent::sim::RssArgs; use omicron_sled_agent::sim::{ - run_standalone_server, Config, ConfigHardware, ConfigStorage, - ConfigUpdates, ConfigZpool, SimMode, + run_standalone_server, Config, ConfigHardware, ConfigStorage, ConfigZpool, + SimMode, }; +use sled_hardware::Baseboard; use std::net::SocketAddr; use std::net::SocketAddrV6; use uuid::Uuid; @@ -98,26 +98,31 @@ async fn do_run() -> Result<(), CmdError> { let tmp = camino_tempfile::tempdir() .map_err(|e| CmdError::Failure(anyhow!(e)))?; let config = Config { - id: args.uuid, - sim_mode: args.sim_mode, - nexus_address: args.nexus_addr, dropshot: ConfigDropshot { bind_address: args.sled_agent_addr.into(), - request_body_max_bytes: 1024 * 1024, - default_handler_task_mode: HandlerTaskMode::Detached, + ..Default::default() }, - log: ConfigLogging::StderrTerminal { level: ConfigLoggingLevel::Info }, storage: ConfigStorage { // Create 10 "virtual" U.2s, with 1 TB of storage. zpools: vec![ConfigZpool { size: 1 << 40 }; 10], ip: (*args.sled_agent_addr.ip()).into(), }, - updates: ConfigUpdates { zone_artifact_path: tmp.path().to_path_buf() }, hardware: ConfigHardware { hardware_threads: 32, physical_ram: 64 * (1 << 30), reservoir_ram: 32 * (1 << 30), + baseboard: Baseboard::Gimlet { + identifier: format!("sim-{}", args.uuid), + model: String::from("sim-gimlet"), + revision: 3, + }, }, + ..Config::for_testing( + args.uuid, + args.sim_mode, + Some(args.nexus_addr), + Some(tmp.path()), + ) }; let tls_certificate = match (args.rss_tls_cert, args.rss_tls_key) { @@ -145,5 +150,9 @@ async fn do_run() -> Result<(), CmdError> { tls_certificate, }; - run_standalone_server(&config, &rss_args).await.map_err(CmdError::Failure) + let config_logging = + ConfigLogging::StderrTerminal { level: ConfigLoggingLevel::Info }; + run_standalone_server(&config, &config_logging, &rss_args) + .await + .map_err(CmdError::Failure) } diff --git a/sled-agent/src/http_entrypoints.rs b/sled-agent/src/http_entrypoints.rs index a444a09604..0798aed664 100644 --- a/sled-agent/src/http_entrypoints.rs +++ b/sled-agent/src/http_entrypoints.rs @@ -10,9 +10,9 @@ use crate::bootstrap::params::AddSledRequest; use crate::params::{ CleanupContextUpdate, DiskEnsureBody, InstanceEnsureBody, InstanceExternalIpBody, InstancePutMigrationIdsBody, InstancePutStateBody, - InstancePutStateResponse, InstanceUnregisterResponse, OmicronZonesConfig, - SledRole, TimeSync, VpcFirewallRulesEnsureBody, ZoneBundleId, - ZoneBundleMetadata, Zpool, + InstancePutStateResponse, InstanceUnregisterResponse, Inventory, + OmicronZonesConfig, SledRole, TimeSync, VpcFirewallRulesEnsureBody, + ZoneBundleId, ZoneBundleMetadata, Zpool, }; use crate::sled_agent::Error as SledAgentError; use crate::zone_bundle; @@ -84,6 +84,7 @@ pub fn api() -> SledApiDescription { api.register(host_os_write_start)?; api.register(host_os_write_status_get)?; api.register(host_os_write_status_delete)?; + api.register(inventory)?; Ok(()) } @@ -959,3 +960,15 @@ async fn host_os_write_status_delete( .map_err(|err| HttpError::from(&err))?; Ok(HttpResponseUpdatedNoContent()) } + +/// Fetch basic information about this sled +#[endpoint { + method = GET, + path = "/inventory", +}] +async fn inventory( + request_context: RequestContext, +) -> Result, HttpError> { + let sa = request_context.context(); + Ok(HttpResponseOk(sa.inventory()?)) +} diff --git a/sled-agent/src/params.rs b/sled-agent/src/params.rs index 2263aa725d..f59bbc40d5 100644 --- a/sled-agent/src/params.rs +++ b/sled-agent/src/params.rs @@ -10,6 +10,7 @@ pub use illumos_utils::opte::params::DhcpConfig; pub use illumos_utils::opte::params::VpcFirewallRule; pub use illumos_utils::opte::params::VpcFirewallRulesEnsureBody; use illumos_utils::zpool::ZpoolName; +use omicron_common::api::external::ByteCount; use omicron_common::api::external::Generation; use omicron_common::api::internal::nexus::{ DiskRuntimeState, InstanceProperties, InstanceRuntimeState, @@ -20,6 +21,7 @@ use omicron_common::api::internal::shared::{ }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use sled_hardware::Baseboard; pub use sled_hardware::DendriteAsic; use sled_storage::dataset::DatasetKind; use sled_storage::dataset::DatasetName; @@ -296,7 +298,7 @@ pub struct OmicronZonesConfig { impl From for sled_agent_client::types::OmicronZonesConfig { fn from(local: OmicronZonesConfig) -> Self { Self { - generation: local.generation.into(), + generation: local.generation, zones: local.zones.into_iter().map(|s| s.into()).collect(), } } @@ -805,16 +807,6 @@ pub struct TimeSync { pub correction: f64, } -#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, PartialEq)] -#[serde(rename_all = "snake_case")] -pub enum SledRole { - /// The sled is a general compute sled. - Gimlet, - /// The sled is attached to the network switch, and has additional - /// responsibilities. - Scrimlet, -} - /// Parameters used to update the zone bundle cleanup context. #[derive(Clone, Debug, Deserialize, JsonSchema, Serialize)] pub struct CleanupContextUpdate { @@ -835,3 +827,20 @@ pub enum InstanceExternalIpBody { Ephemeral(IpAddr), Floating(IpAddr), } + +// Our SledRole and Baseboard types do not have to be identical to the Nexus +// ones, but they generally should be, and this avoids duplication. If it +// becomes easier to maintain a separate copy, we should do that. +pub type SledRole = nexus_client::types::SledRole; + +/// Identity and basic status information about this sled agent +#[derive(Clone, Debug, Deserialize, JsonSchema, Serialize)] +pub struct Inventory { + pub sled_id: Uuid, + pub sled_agent_address: SocketAddrV6, + pub sled_role: SledRole, + pub baseboard: Baseboard, + pub usable_hardware_threads: u32, + pub usable_physical_ram: ByteCount, + pub reservoir_size: ByteCount, +} diff --git a/sled-agent/src/sim/config.rs b/sled-agent/src/sim/config.rs index 62012a7109..81e11dc1c2 100644 --- a/sled-agent/src/sim/config.rs +++ b/sled-agent/src/sim/config.rs @@ -5,13 +5,22 @@ //! Interfaces for working with sled agent configuration use crate::updates::ConfigUpdates; +use camino::Utf8Path; use dropshot::ConfigDropshot; -use dropshot::ConfigLogging; use serde::Deserialize; use serde::Serialize; +pub use sled_hardware::Baseboard; +use std::net::Ipv6Addr; use std::net::{IpAddr, SocketAddr}; use uuid::Uuid; +/// The reported amount of hardware threads for an emulated sled agent. +pub const TEST_HARDWARE_THREADS: u32 = 16; +/// The reported amount of physical RAM for an emulated sled agent. +pub const TEST_PHYSICAL_RAM: u64 = 32 * (1 << 30); +/// The reported amount of VMM reservoir RAM for an emulated sled agent. +pub const TEST_RESERVOIR_RAM: u64 = 16 * (1 << 30); + /// How a [`SledAgent`](`super::sled_agent::SledAgent`) simulates object states and /// transitions #[derive(Copy, Clone, Debug, Deserialize, PartialEq, Serialize)] @@ -47,6 +56,7 @@ pub struct ConfigHardware { pub hardware_threads: u32, pub physical_ram: u64, pub reservoir_ram: u64, + pub baseboard: Baseboard, } /// Configuration for a sled agent @@ -60,8 +70,6 @@ pub struct Config { pub nexus_address: SocketAddr, /// configuration for the sled agent dropshot server pub dropshot: ConfigDropshot, - /// configuration for the sled agent debug log - pub log: ConfigLogging, /// configuration for the sled agent's storage pub storage: ConfigStorage, /// configuration for the sled agent's updates @@ -69,3 +77,49 @@ pub struct Config { /// configuration to emulate the sled agent's hardware pub hardware: ConfigHardware, } + +impl Config { + pub fn for_testing( + id: Uuid, + sim_mode: SimMode, + nexus_address: Option, + update_directory: Option<&Utf8Path>, + ) -> Config { + // This IP range is guaranteed by RFC 6666 to discard traffic. + // For tests that don't use a Nexus, we use this address to simulate a + // non-functioning Nexus. + let nexus_address = + nexus_address.unwrap_or_else(|| "[100::1]:12345".parse().unwrap()); + // If the caller doesn't care to provide a directory in which to put + // updates, make up a path that doesn't exist. + let update_directory = + update_directory.unwrap_or_else(|| "/nonexistent".into()); + Config { + id, + sim_mode, + nexus_address, + dropshot: ConfigDropshot { + bind_address: SocketAddr::new(Ipv6Addr::LOCALHOST.into(), 0), + request_body_max_bytes: 1024 * 1024, + ..Default::default() + }, + storage: ConfigStorage { + zpools: vec![], + ip: IpAddr::from(Ipv6Addr::LOCALHOST), + }, + updates: ConfigUpdates { + zone_artifact_path: update_directory.to_path_buf(), + }, + hardware: ConfigHardware { + hardware_threads: TEST_HARDWARE_THREADS, + physical_ram: TEST_PHYSICAL_RAM, + reservoir_ram: TEST_RESERVOIR_RAM, + baseboard: Baseboard::Gimlet { + identifier: format!("sim-{}", id), + model: String::from("sim-gimlet"), + revision: 3, + }, + }, + } + } +} diff --git a/sled-agent/src/sim/http_entrypoints.rs b/sled-agent/src/sim/http_entrypoints.rs index d533db3252..09ffdf5dc4 100644 --- a/sled-agent/src/sim/http_entrypoints.rs +++ b/sled-agent/src/sim/http_entrypoints.rs @@ -10,8 +10,8 @@ use crate::bootstrap::early_networking::{ use crate::params::{ DiskEnsureBody, InstanceEnsureBody, InstanceExternalIpBody, InstancePutMigrationIdsBody, InstancePutStateBody, - InstancePutStateResponse, InstanceUnregisterResponse, - VpcFirewallRulesEnsureBody, + InstancePutStateResponse, InstanceUnregisterResponse, Inventory, + OmicronZonesConfig, VpcFirewallRulesEnsureBody, }; use dropshot::endpoint; use dropshot::ApiDescription; @@ -59,6 +59,9 @@ pub fn api() -> SledApiDescription { api.register(uplink_ensure)?; api.register(read_network_bootstore_config)?; api.register(write_network_bootstore_config)?; + api.register(inventory)?; + api.register(omicron_zones_get)?; + api.register(omicron_zones_put)?; Ok(()) } @@ -419,3 +422,43 @@ async fn write_network_bootstore_config( ) -> Result { Ok(HttpResponseUpdatedNoContent()) } + +/// Fetch basic information about this sled +#[endpoint { + method = GET, + path = "/inventory", +}] +async fn inventory( + rqctx: RequestContext>, +) -> Result, HttpError> { + let sa = rqctx.context(); + Ok(HttpResponseOk( + sa.inventory(rqctx.server.local_addr) + .map_err(|e| HttpError::for_internal_error(format!("{:#}", e)))?, + )) +} + +#[endpoint { + method = GET, + path = "/omicron-zones", +}] +async fn omicron_zones_get( + rqctx: RequestContext>, +) -> Result, HttpError> { + let sa = rqctx.context(); + Ok(HttpResponseOk(sa.omicron_zones_list().await)) +} + +#[endpoint { + method = PUT, + path = "/omicron-zones", +}] +async fn omicron_zones_put( + rqctx: RequestContext>, + body: TypedBody, +) -> Result { + let sa = rqctx.context(); + let body_args = body.into_inner(); + sa.omicron_zones_ensure(body_args).await; + Ok(HttpResponseUpdatedNoContent()) +} diff --git a/sled-agent/src/sim/mod.rs b/sled-agent/src/sim/mod.rs index 8a730d5988..14d980cf79 100644 --- a/sled-agent/src/sim/mod.rs +++ b/sled-agent/src/sim/mod.rs @@ -17,6 +17,9 @@ mod sled_agent; mod storage; pub use crate::updates::ConfigUpdates; -pub use config::{Config, ConfigHardware, ConfigStorage, ConfigZpool, SimMode}; +pub use config::{ + Baseboard, Config, ConfigHardware, ConfigStorage, ConfigZpool, SimMode, + TEST_HARDWARE_THREADS, TEST_RESERVOIR_RAM, +}; pub use server::{run_standalone_server, RssArgs, Server}; pub use sled_agent::SledAgent; diff --git a/sled-agent/src/sim/server.rs b/sled-agent/src/sim/server.rs index 1f2fe8e1d8..b214667631 100644 --- a/sled-agent/src/sim/server.rs +++ b/sled-agent/src/sim/server.rs @@ -50,6 +50,7 @@ impl Server { pub async fn start( config: &Config, log: &Logger, + wait_for_nexus: bool, ) -> Result { info!(log, "setting up sled agent server"); @@ -87,49 +88,61 @@ impl Server { // TODO-robustness if this returns a 400 error, we probably want to // return a permanent error from the `notify_nexus` closure. let sa_address = http_server.local_addr(); - let notify_nexus = || async { - debug!(log, "contacting server nexus"); - nexus_client - .sled_agent_put( - &config.id, - &NexusTypes::SledAgentStartupInfo { - sa_address: sa_address.to_string(), - role: NexusTypes::SledRole::Scrimlet, - baseboard: NexusTypes::Baseboard { - serial_number: format!( - "sim-{}", - &config.id.to_string()[0..8] - ), - part_number: String::from("Unknown"), - revision: 0, + let config_clone = config.clone(); + let log_clone = log.clone(); + let task = tokio::spawn(async move { + let config = config_clone; + let log = log_clone; + let nexus_client = nexus_client.clone(); + let notify_nexus = || async { + debug!(log, "contacting server nexus"); + nexus_client + .sled_agent_put( + &config.id, + &NexusTypes::SledAgentStartupInfo { + sa_address: sa_address.to_string(), + role: NexusTypes::SledRole::Scrimlet, + baseboard: NexusTypes::Baseboard { + serial_number: format!( + "sim-{}", + &config.id.to_string()[0..8] + ), + part_number: String::from("Unknown"), + revision: 0, + }, + usable_hardware_threads: config + .hardware + .hardware_threads, + usable_physical_ram: + NexusTypes::ByteCount::try_from( + config.hardware.physical_ram, + ) + .unwrap(), + reservoir_size: NexusTypes::ByteCount::try_from( + config.hardware.reservoir_ram, + ) + .unwrap(), }, - usable_hardware_threads: config - .hardware - .hardware_threads, - usable_physical_ram: NexusTypes::ByteCount::try_from( - config.hardware.physical_ram, - ) - .unwrap(), - reservoir_size: NexusTypes::ByteCount::try_from( - config.hardware.reservoir_ram, - ) - .unwrap(), - }, - ) - .await - .map_err(BackoffError::transient) - }; - let log_notification_failure = |error, delay| { - warn!(log, "failed to contact nexus, will retry in {:?}", delay; - "error" => ?error); - }; - retry_notify( - retry_policy_internal_service_aggressive(), - notify_nexus, - log_notification_failure, - ) - .await - .expect("Expected an infinite retry loop contacting Nexus"); + ) + .await + .map_err(BackoffError::transient) + }; + let log_notification_failure = |error, delay| { + warn!(log, "failed to contact nexus, will retry in {:?}", delay; + "error" => ?error); + }; + retry_notify( + retry_policy_internal_service_aggressive(), + notify_nexus, + log_notification_failure, + ) + .await + .expect("Expected an infinite retry loop contacting Nexus"); + }); + + if wait_for_nexus { + task.await.unwrap(); + } let mut datasets = vec![]; // Create all the Zpools requested by the config, and allocate a single @@ -262,11 +275,11 @@ pub struct RssArgs { /// - Performs handoff to Nexus pub async fn run_standalone_server( config: &Config, + logging: &dropshot::ConfigLogging, rss_args: &RssArgs, ) -> Result<(), anyhow::Error> { let (drain, registration) = slog_dtrace::with_drain( - config - .log + logging .to_logger("sled-agent") .map_err(|message| anyhow!("initializing logger: {}", message))?, ); @@ -280,7 +293,7 @@ pub async fn run_standalone_server( } // Start the sled agent - let mut server = Server::start(config, &log).await?; + let mut server = Server::start(config, &log, true).await?; info!(log, "sled agent started successfully"); // Start the Internal DNS server diff --git a/sled-agent/src/sim/sled_agent.rs b/sled-agent/src/sim/sled_agent.rs index d58fec384b..56cfaf57c8 100644 --- a/sled-agent/src/sim/sled_agent.rs +++ b/sled-agent/src/sim/sled_agent.rs @@ -10,41 +10,43 @@ use super::disk::SimDisk; use super::instance::SimInstance; use super::storage::CrucibleData; use super::storage::Storage; - use crate::nexus::NexusClient; use crate::params::{ DiskStateRequested, InstanceExternalIpBody, InstanceHardware, InstanceMigrationSourceParams, InstancePutStateResponse, - InstanceStateRequested, InstanceUnregisterResponse, + InstanceStateRequested, InstanceUnregisterResponse, Inventory, + OmicronZonesConfig, SledRole, }; use crate::sim::simulatable::Simulatable; use crate::updates::UpdateManager; +use anyhow::bail; +use anyhow::Context; +use dropshot::HttpServer; use futures::lock::Mutex; -use omicron_common::api::external::{DiskState, Error, ResourceType}; +use illumos_utils::opte::params::{ + DeleteVirtualNetworkInterfaceHost, SetVirtualNetworkInterfaceHost, +}; +use nexus_client::types::PhysicalDiskKind; +use omicron_common::address::PROPOLIS_PORT; +use omicron_common::api::external::{ + ByteCount, DiskState, Error, Generation, ResourceType, +}; use omicron_common::api::internal::nexus::{ DiskRuntimeState, SledInstanceState, }; use omicron_common::api::internal::nexus::{ InstanceRuntimeState, VmmRuntimeState, }; -use slog::Logger; -use std::net::{IpAddr, Ipv6Addr, SocketAddr}; -use std::sync::Arc; -use uuid::Uuid; - -use std::collections::{HashMap, HashSet}; -use std::str::FromStr; - -use dropshot::HttpServer; -use illumos_utils::opte::params::{ - DeleteVirtualNetworkInterfaceHost, SetVirtualNetworkInterfaceHost, -}; -use nexus_client::types::PhysicalDiskKind; -use omicron_common::address::PROPOLIS_PORT; use propolis_client::{ types::VolumeConstructionRequest, Client as PropolisClient, }; use propolis_mock_server::Context as PropolisContext; +use slog::Logger; +use std::collections::{HashMap, HashSet}; +use std::net::{IpAddr, Ipv6Addr, SocketAddr}; +use std::str::FromStr; +use std::sync::Arc; +use uuid::Uuid; /// Simulates management of the control plane on a sled /// @@ -70,7 +72,8 @@ pub struct SledAgent { Mutex>, PropolisClient)>>, /// lists of external IPs assigned to instances pub external_ips: Mutex>>, - + config: Config, + fake_zones: Mutex, instance_ensure_state_error: Mutex>, } @@ -164,6 +167,11 @@ impl SledAgent { v2p_mappings: Mutex::new(HashMap::new()), external_ips: Mutex::new(HashMap::new()), mock_propolis: Mutex::new(None), + config: config.clone(), + fake_zones: Mutex::new(OmicronZonesConfig { + generation: Generation::new(), + zones: vec![], + }), instance_ensure_state_error: Mutex::new(None), }) } @@ -720,4 +728,39 @@ impl SledAgent { *mock_lock = Some((srv, client)); Ok(()) } + + pub fn inventory(&self, addr: SocketAddr) -> anyhow::Result { + let sled_agent_address = match addr { + SocketAddr::V4(_) => { + bail!("sled_agent_ip must be v6 for inventory") + } + SocketAddr::V6(v6) => v6, + }; + Ok(Inventory { + sled_id: self.id, + sled_agent_address, + sled_role: SledRole::Gimlet, + baseboard: self.config.hardware.baseboard.clone(), + usable_hardware_threads: self.config.hardware.hardware_threads, + usable_physical_ram: ByteCount::try_from( + self.config.hardware.physical_ram, + ) + .context("usable_physical_ram")?, + reservoir_size: ByteCount::try_from( + self.config.hardware.reservoir_ram, + ) + .context("reservoir_size")?, + }) + } + + pub async fn omicron_zones_list(&self) -> OmicronZonesConfig { + self.fake_zones.lock().await.clone() + } + + pub async fn omicron_zones_ensure( + &self, + requested_zones: OmicronZonesConfig, + ) { + *self.fake_zones.lock().await = requested_zones; + } } diff --git a/sled-agent/src/sled_agent.rs b/sled-agent/src/sled_agent.rs index 43d985f0a9..2373ae0270 100644 --- a/sled-agent/src/sled_agent.rs +++ b/sled-agent/src/sled_agent.rs @@ -18,8 +18,9 @@ use crate::nexus::{ConvertInto, NexusClientWithResolver, NexusRequestQueue}; use crate::params::{ DiskStateRequested, InstanceExternalIpBody, InstanceHardware, InstanceMigrationSourceParams, InstancePutStateResponse, - InstanceStateRequested, InstanceUnregisterResponse, OmicronZonesConfig, - SledRole, TimeSync, VpcFirewallRule, ZoneBundleMetadata, Zpool, + InstanceStateRequested, InstanceUnregisterResponse, Inventory, + OmicronZonesConfig, SledRole, TimeSync, VpcFirewallRule, + ZoneBundleMetadata, Zpool, }; use crate::services::{self, ServiceManager}; use crate::storage_monitor::UnderlayAccess; @@ -42,7 +43,7 @@ use illumos_utils::zone::ZONE_PREFIX; use omicron_common::address::{ get_sled_address, get_switch_zone_address, Ipv6Subnet, SLED_PREFIX, }; -use omicron_common::api::external::Vni; +use omicron_common::api::external::{ByteCount, ByteCountRangeError, Vni}; use omicron_common::api::internal::nexus::ProducerEndpoint; use omicron_common::api::internal::nexus::ProducerKind; use omicron_common::api::internal::nexus::{ @@ -214,6 +215,35 @@ impl From for dropshot::HttpError { } } +/// Error returned by `SledAgent::inventory()` +#[derive(thiserror::Error, Debug)] +pub enum InventoryError { + // This error should be impossible because ByteCount supports values from + // [0, i64::MAX] and we don't have anything with that many bytes in the + // system. + #[error(transparent)] + BadByteCount(#[from] ByteCountRangeError), +} + +impl From for omicron_common::api::external::Error { + fn from(inventory_error: InventoryError) -> Self { + match inventory_error { + e @ InventoryError::BadByteCount(..) => { + omicron_common::api::external::Error::internal_error(&format!( + "{:#}", + e + )) + } + } + } +} + +impl From for dropshot::HttpError { + fn from(error: InventoryError) -> Self { + Self::from(omicron_common::api::external::Error::from(error)) + } +} + /// Describes an executing Sled Agent object. /// /// Contains both a connection to the Nexus, as well as managed instances. @@ -1080,6 +1110,37 @@ impl SledAgent { pub(crate) fn boot_disk_os_writer(&self) -> &BootDiskOsWriter { &self.inner.boot_disk_os_writer } + + /// Return basic information about ourselves: identity and status + /// + /// This is basically a GET version of the information we push to Nexus on + /// startup. + pub(crate) fn inventory(&self) -> Result { + let sled_id = self.inner.id; + let sled_agent_address = self.inner.sled_address(); + let is_scrimlet = self.inner.hardware.is_scrimlet(); + let baseboard = self.inner.hardware.baseboard(); + let usable_hardware_threads = + self.inner.hardware.online_processor_count(); + let usable_physical_ram = + self.inner.hardware.usable_physical_ram_bytes(); + let reservoir_size = self.inner.instances.reservoir_size(); + let sled_role = if is_scrimlet { + crate::params::SledRole::Scrimlet + } else { + crate::params::SledRole::Gimlet + }; + + Ok(Inventory { + sled_id, + sled_agent_address, + sled_role, + baseboard, + usable_hardware_threads, + usable_physical_ram: ByteCount::try_from(usable_physical_ram)?, + reservoir_size, + }) + } } async fn register_metric_producer_with_nexus( diff --git a/update-common/Cargo.toml b/update-common/Cargo.toml new file mode 100644 index 0000000000..cc2ee86232 --- /dev/null +++ b/update-common/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "update-common" +version = "0.1.0" +edition = "2021" +license = "MPL-2.0" + +[dependencies] +anyhow.workspace = true +bytes.workspace = true +camino.workspace = true +camino-tempfile.workspace = true +debug-ignore.workspace = true +display-error-chain.workspace = true +dropshot.workspace = true +futures.workspace = true +hex.workspace = true +hubtools.workspace = true +omicron-common.workspace = true +sha2.workspace = true +slog.workspace = true +thiserror.workspace = true +tokio.workspace = true +tokio-util.workspace = true +tough.workspace = true +tufaceous-lib.workspace = true +omicron-workspace-hack.workspace = true + +[dev-dependencies] +clap.workspace = true +omicron-test-utils.workspace = true +rand.workspace = true +tufaceous.workspace = true diff --git a/update-common/src/artifacts/artifact_types.rs b/update-common/src/artifacts/artifact_types.rs new file mode 100644 index 0000000000..e70970993a --- /dev/null +++ b/update-common/src/artifacts/artifact_types.rs @@ -0,0 +1,31 @@ +// 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/. + +//! General types for artifacts that don't quite fit into the other modules. + +use std::borrow::Borrow; + +use omicron_common::update::ArtifactId; + +use super::ExtractedArtifactDataHandle; + +/// A pair containing both the ID of an artifact and a handle to its data. +/// +/// Note that cloning an `ArtifactIdData` will clone the handle, which has +/// implications on temporary directory cleanup. See +/// [`ExtractedArtifactDataHandle`] for details. +#[derive(Debug, Clone)] +pub struct ArtifactIdData { + pub id: ArtifactId, + pub data: ExtractedArtifactDataHandle, +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct Board(pub String); + +impl Borrow for Board { + fn borrow(&self) -> &String { + &self.0 + } +} diff --git a/wicketd/src/artifacts/artifacts_with_plan.rs b/update-common/src/artifacts/artifacts_with_plan.rs similarity index 96% rename from wicketd/src/artifacts/artifacts_with_plan.rs rename to update-common/src/artifacts/artifacts_with_plan.rs index d3319d7f6b..94c7294d48 100644 --- a/wicketd/src/artifacts/artifacts_with_plan.rs +++ b/update-common/src/artifacts/artifacts_with_plan.rs @@ -2,10 +2,10 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. -use super::error::RepositoryError; -use super::update_plan::UpdatePlanBuilder; use super::ExtractedArtifactDataHandle; use super::UpdatePlan; +use super::UpdatePlanBuilder; +use crate::errors::RepositoryError; use camino_tempfile::Utf8TempDir; use debug_ignore::DebugIgnore; use omicron_common::update::ArtifactHash; @@ -22,7 +22,7 @@ use tufaceous_lib::OmicronRepo; /// A collection of artifacts along with an update plan using those artifacts. #[derive(Debug)] -pub(super) struct ArtifactsWithPlan { +pub struct ArtifactsWithPlan { // Map of top-level artifact IDs (present in the TUF repo) to the actual // artifacts we're serving (e.g., a top-level RoT artifact will map to two // artifact hashes: one for each of the A and B images). @@ -50,7 +50,7 @@ pub(super) struct ArtifactsWithPlan { } impl ArtifactsWithPlan { - pub(super) async fn from_zip( + pub async fn from_zip( zip_data: T, log: &Logger, ) -> Result @@ -81,7 +81,7 @@ impl ArtifactsWithPlan { // these are just direct copies of artifacts we just unpacked into // `dir`, but we'll also unpack nested artifacts like the RoT dual A/B // archives. - let mut plan_builder = + let mut builder = UpdatePlanBuilder::new(artifacts.system_version, log)?; // Make a pass through each artifact in the repo. For each artifact, we @@ -146,7 +146,7 @@ impl ArtifactsWithPlan { RepositoryError::MissingTarget(artifact.target.clone()) })?; - plan_builder + builder .add_artifact( artifact.into_id(), artifact_hash, @@ -159,12 +159,12 @@ impl ArtifactsWithPlan { // Ensure we know how to apply updates from this set of artifacts; we'll // remember the plan we create. - let plan = plan_builder.build()?; + let artifacts = builder.build()?; - Ok(Self { by_id, by_hash: by_hash.into(), plan }) + Ok(Self { by_id, by_hash: by_hash.into(), plan: artifacts }) } - pub(super) fn by_id(&self) -> &BTreeMap> { + pub fn by_id(&self) -> &BTreeMap> { &self.by_id } @@ -175,11 +175,11 @@ impl ArtifactsWithPlan { &self.by_hash } - pub(super) fn plan(&self) -> &UpdatePlan { + pub fn plan(&self) -> &UpdatePlan { &self.plan } - pub(super) fn get_by_hash( + pub fn get_by_hash( &self, id: &ArtifactHashId, ) -> Option { diff --git a/wicketd/src/artifacts/extracted_artifacts.rs b/update-common/src/artifacts/extracted_artifacts.rs similarity index 95% rename from wicketd/src/artifacts/extracted_artifacts.rs rename to update-common/src/artifacts/extracted_artifacts.rs index 5683cd1c13..06e0e5ec65 100644 --- a/wicketd/src/artifacts/extracted_artifacts.rs +++ b/update-common/src/artifacts/extracted_artifacts.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/. -use super::error::RepositoryError; +use crate::errors::RepositoryError; use anyhow::Context; use camino::Utf8PathBuf; use camino_tempfile::NamedUtf8TempFile; @@ -39,7 +39,7 @@ use tokio_util::io::ReaderStream; /// contexts where you need the data and need the temporary directory containing /// it to stick around. #[derive(Debug, Clone)] -pub(crate) struct ExtractedArtifactDataHandle { +pub struct ExtractedArtifactDataHandle { tempdir: Arc, file_size: usize, hash_id: ArtifactHashId, @@ -61,11 +61,11 @@ impl Eq for ExtractedArtifactDataHandle {} impl ExtractedArtifactDataHandle { /// File size of this artifact in bytes. - pub(crate) fn file_size(&self) -> usize { + pub fn file_size(&self) -> usize { self.file_size } - pub(crate) fn hash(&self) -> ArtifactHash { + pub fn hash(&self) -> ArtifactHash { self.hash_id.hash } @@ -73,7 +73,7 @@ impl ExtractedArtifactDataHandle { /// /// This can fail due to I/O errors outside our control (e.g., something /// removed the contents of our temporary directory). - pub(crate) async fn reader_stream( + pub async fn reader_stream( &self, ) -> anyhow::Result> { let path = path_for_artifact(&self.tempdir, &self.hash_id); @@ -96,7 +96,7 @@ impl ExtractedArtifactDataHandle { /// (e.g., when a new TUF repository is uploaded). The handles can be used to /// on-demand read files that were copied into the temp dir during ingest. #[derive(Debug)] -pub(crate) struct ExtractedArtifacts { +pub struct ExtractedArtifacts { // Directory in which we store extracted artifacts. This is currently a // single flat directory with files named by artifact hash; we don't expect // more than a few dozen files total, so no need to nest directories. @@ -104,7 +104,7 @@ pub(crate) struct ExtractedArtifacts { } impl ExtractedArtifacts { - pub(super) fn new(log: &Logger) -> Result { + pub fn new(log: &Logger) -> Result { let tempdir = camino_tempfile::Builder::new() .prefix("wicketd-update-artifacts.") .tempdir() @@ -125,7 +125,7 @@ impl ExtractedArtifacts { /// Copy from `stream` into our temp directory, returning a handle to the /// extracted artifact on success. - pub(super) async fn store( + pub async fn store( &mut self, artifact_hash_id: ArtifactHashId, stream: impl Stream>, @@ -185,7 +185,7 @@ impl ExtractedArtifacts { /// As the returned file is written to, the data will be hashed; once /// writing is complete, call [`ExtractedArtifacts::store_tempfile()`] to /// persist the temporary file into an [`ExtractedArtifactDataHandle`]. - pub(super) fn new_tempfile( + pub fn new_tempfile( &self, ) -> Result { let file = NamedUtf8TempFile::new_in(self.tempdir.path()).map_err( @@ -203,7 +203,7 @@ impl ExtractedArtifacts { /// Persist a temporary file that was returned by /// [`ExtractedArtifacts::new_tempfile()`] as an extracted artifact. - pub(super) fn store_tempfile( + pub fn store_tempfile( &self, kind: ArtifactKind, file: HashingNamedUtf8TempFile, @@ -249,7 +249,7 @@ fn path_for_artifact( } // Wrapper around a `NamedUtf8TempFile` that hashes contents as they're written. -pub(super) struct HashingNamedUtf8TempFile { +pub struct HashingNamedUtf8TempFile { file: io::BufWriter, hasher: Sha256, bytes_written: usize, diff --git a/update-common/src/artifacts/mod.rs b/update-common/src/artifacts/mod.rs new file mode 100644 index 0000000000..d68c488599 --- /dev/null +++ b/update-common/src/artifacts/mod.rs @@ -0,0 +1,15 @@ +// 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/. + +//! Types to represent update artifacts. + +mod artifact_types; +mod artifacts_with_plan; +mod extracted_artifacts; +mod update_plan; + +pub use artifact_types::*; +pub use artifacts_with_plan::*; +pub use extracted_artifacts::*; +pub use update_plan::*; diff --git a/wicketd/src/artifacts/update_plan.rs b/update-common/src/artifacts/update_plan.rs similarity index 97% rename from wicketd/src/artifacts/update_plan.rs rename to update-common/src/artifacts/update_plan.rs index c6db7c1b65..e30389f646 100644 --- a/wicketd/src/artifacts/update_plan.rs +++ b/update-common/src/artifacts/update_plan.rs @@ -8,12 +8,12 @@ //! apply to which components; the ordering and application of the plan lives //! elsewhere. -use super::error::RepositoryError; -use super::extracted_artifacts::ExtractedArtifacts; -use super::extracted_artifacts::HashingNamedUtf8TempFile; use super::ArtifactIdData; use super::Board; use super::ExtractedArtifactDataHandle; +use super::ExtractedArtifacts; +use super::HashingNamedUtf8TempFile; +use crate::errors::RepositoryError; use bytes::Bytes; use futures::Stream; use futures::StreamExt; @@ -34,21 +34,20 @@ use std::io; use tufaceous_lib::HostPhaseImages; use tufaceous_lib::RotArchives; -/// The update plan currently in effect. -/// -/// Exposed for testing. +/// Artifacts with their hashes and sources, as obtained from an uploaded +/// repository. #[derive(Debug, Clone)] pub struct UpdatePlan { - pub(crate) system_version: SemverVersion, - pub(crate) gimlet_sp: BTreeMap, - pub(crate) gimlet_rot_a: Vec, - pub(crate) gimlet_rot_b: Vec, - pub(crate) psc_sp: BTreeMap, - pub(crate) psc_rot_a: Vec, - pub(crate) psc_rot_b: Vec, - pub(crate) sidecar_sp: BTreeMap, - pub(crate) sidecar_rot_a: Vec, - pub(crate) sidecar_rot_b: Vec, + pub system_version: SemverVersion, + pub gimlet_sp: BTreeMap, + pub gimlet_rot_a: Vec, + pub gimlet_rot_b: Vec, + pub psc_sp: BTreeMap, + pub psc_rot_a: Vec, + pub psc_rot_b: Vec, + pub sidecar_sp: BTreeMap, + pub sidecar_rot_a: Vec, + pub sidecar_rot_b: Vec, // Note: The Trampoline image is broken into phase1/phase2 as part of our // update plan (because they go to different destinations), but the two @@ -58,21 +57,17 @@ pub struct UpdatePlan { // The same would apply to the host phase1/phase2, but we don't actually // need the `host_phase_2` data as part of this plan (we serve it from the // artifact server instead). - pub(crate) host_phase_1: ArtifactIdData, - pub(crate) trampoline_phase_1: ArtifactIdData, - pub(crate) trampoline_phase_2: ArtifactIdData, + pub host_phase_1: ArtifactIdData, + pub trampoline_phase_1: ArtifactIdData, + pub trampoline_phase_2: ArtifactIdData, // We need to send installinator the hash of the host_phase_2 data it should // fetch from us; we compute it while generating the plan. - // - // This is exposed for testing. pub host_phase_2_hash: ArtifactHash, // We also need to send installinator the hash of the control_plane image it // should fetch from us. This is already present in the TUF repository, but // we record it here for use by the update process. - // - // This is exposed for testing. pub control_plane_hash: ArtifactHash, } @@ -81,7 +76,7 @@ pub struct UpdatePlan { /// [`UpdatePlanBuilder::build()`] will (fallibly) convert from the builder to /// the final plan. #[derive(Debug)] -pub(super) struct UpdatePlanBuilder<'a> { +pub struct UpdatePlanBuilder<'a> { // fields that mirror `UpdatePlan` system_version: SemverVersion, gimlet_sp: BTreeMap, @@ -118,7 +113,7 @@ pub(super) struct UpdatePlanBuilder<'a> { } impl<'a> UpdatePlanBuilder<'a> { - pub(super) fn new( + pub fn new( system_version: SemverVersion, log: &'a Logger, ) -> Result { @@ -145,7 +140,7 @@ impl<'a> UpdatePlanBuilder<'a> { }) } - pub(super) async fn add_artifact( + pub async fn add_artifact( &mut self, artifact_id: ArtifactId, artifact_hash: ArtifactHash, @@ -665,7 +660,7 @@ impl<'a> UpdatePlanBuilder<'a> { Ok((image1, image2)) } - pub(super) fn build(self) -> Result { + pub fn build(self) -> Result { // Ensure our multi-board-supporting kinds have at least one board // present. for (kind, no_artifacts) in [ diff --git a/wicketd/src/artifacts/error.rs b/update-common/src/errors.rs similarity index 98% rename from wicketd/src/artifacts/error.rs rename to update-common/src/errors.rs index ada8fbe011..5fba43b944 100644 --- a/wicketd/src/artifacts/error.rs +++ b/update-common/src/errors.rs @@ -2,6 +2,8 @@ // 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/. +//! Error types for this crate. + use camino::Utf8PathBuf; use display_error_chain::DisplayErrorChain; use dropshot::HttpError; @@ -12,7 +14,7 @@ use slog::error; use thiserror::Error; #[derive(Debug, Error)] -pub(super) enum RepositoryError { +pub enum RepositoryError { #[error("error opening archive")] OpenArchive(#[source] anyhow::Error), @@ -129,7 +131,7 @@ pub(super) enum RepositoryError { } impl RepositoryError { - pub(super) fn to_http_error(&self) -> HttpError { + pub fn to_http_error(&self) -> HttpError { let message = DisplayErrorChain::new(self).to_string(); match self { diff --git a/update-common/src/lib.rs b/update-common/src/lib.rs new file mode 100644 index 0000000000..b1f0d88484 --- /dev/null +++ b/update-common/src/lib.rs @@ -0,0 +1,8 @@ +// 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/. + +//! Common update types and code shared between wicketd and Nexus. + +pub mod artifacts; +pub mod errors; diff --git a/wicketd/Cargo.toml b/wicketd/Cargo.toml index 97550342d0..83e7bf33ca 100644 --- a/wicketd/Cargo.toml +++ b/wicketd/Cargo.toml @@ -56,6 +56,7 @@ omicron-common.workspace = true omicron-passwords.workspace = true sled-hardware.workspace = true tufaceous-lib.workspace = true +update-common.workspace = true update-engine.workspace = true wicket-common.workspace = true wicketd-client.workspace = true diff --git a/wicketd/src/artifacts.rs b/wicketd/src/artifacts.rs index 7b55d73dcb..3e5854d17e 100644 --- a/wicketd/src/artifacts.rs +++ b/wicketd/src/artifacts.rs @@ -2,37 +2,8 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. -use omicron_common::update::ArtifactId; -use std::borrow::Borrow; - -mod artifacts_with_plan; -mod error; -mod extracted_artifacts; mod server; mod store; -mod update_plan; -pub(crate) use self::extracted_artifacts::ExtractedArtifactDataHandle; pub(crate) use self::server::WicketdArtifactServer; pub(crate) use self::store::WicketdArtifactStore; -pub use self::update_plan::UpdatePlan; - -/// A pair containing both the ID of an artifact and a handle to its data. -/// -/// Note that cloning an `ArtifactIdData` will clone the handle, which has -/// implications on temporary directory cleanup. See -/// [`ExtractedArtifactDataHandle`] for details. -#[derive(Debug, Clone)] -pub(crate) struct ArtifactIdData { - pub(crate) id: ArtifactId, - pub(crate) data: ExtractedArtifactDataHandle, -} - -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] -pub(crate) struct Board(pub(crate) String); - -impl Borrow for Board { - fn borrow(&self) -> &String { - &self.0 - } -} diff --git a/wicketd/src/artifacts/store.rs b/wicketd/src/artifacts/store.rs index 2a7b4a646b..a5f24993a8 100644 --- a/wicketd/src/artifacts/store.rs +++ b/wicketd/src/artifacts/store.rs @@ -2,9 +2,6 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. -use super::artifacts_with_plan::ArtifactsWithPlan; -use super::ExtractedArtifactDataHandle; -use super::UpdatePlan; use crate::http_entrypoints::InstallableArtifacts; use dropshot::HttpError; use omicron_common::api::external::SemverVersion; @@ -13,6 +10,9 @@ use slog::Logger; use std::io; use std::sync::Arc; use std::sync::Mutex; +use update_common::artifacts::ArtifactsWithPlan; +use update_common::artifacts::ExtractedArtifactDataHandle; +use update_common::artifacts::UpdatePlan; /// The artifact store for wicketd. /// diff --git a/wicketd/src/update_tracker.rs b/wicketd/src/update_tracker.rs index 336333f899..823a7964de 100644 --- a/wicketd/src/update_tracker.rs +++ b/wicketd/src/update_tracker.rs @@ -4,8 +4,6 @@ // Copyright 2023 Oxide Computer Company -use crate::artifacts::ArtifactIdData; -use crate::artifacts::UpdatePlan; use crate::artifacts::WicketdArtifactStore; use crate::helpers::sps_to_string; use crate::http_entrypoints::ClearUpdateStateResponse; @@ -65,6 +63,8 @@ use tokio::sync::watch; use tokio::sync::Mutex; use tokio::task::JoinHandle; use tokio_util::io::StreamReader; +use update_common::artifacts::ArtifactIdData; +use update_common::artifacts::UpdatePlan; use update_engine::events::ProgressUnits; use update_engine::AbortHandle; use update_engine::StepSpec; diff --git a/workspace-hack/Cargo.toml b/workspace-hack/Cargo.toml index 653e8b370a..e42a95a824 100644 --- a/workspace-hack/Cargo.toml +++ b/workspace-hack/Cargo.toml @@ -41,13 +41,13 @@ either = { version = "1.9.0" } elliptic-curve = { version = "0.13.8", features = ["ecdh", "hazmat", "pem", "std"] } ff = { version = "0.13.0", default-features = false, features = ["alloc"] } flate2 = { version = "1.0.28" } -futures = { version = "0.3.29" } -futures-channel = { version = "0.3.29", features = ["sink"] } -futures-core = { version = "0.3.29" } -futures-io = { version = "0.3.29", default-features = false, features = ["std"] } -futures-sink = { version = "0.3.29" } -futures-task = { version = "0.3.29", default-features = false, features = ["std"] } -futures-util = { version = "0.3.29", features = ["channel", "io", "sink"] } +futures = { version = "0.3.30" } +futures-channel = { version = "0.3.30", features = ["sink"] } +futures-core = { version = "0.3.30" } +futures-io = { version = "0.3.30", default-features = false, features = ["std"] } +futures-sink = { version = "0.3.30" } +futures-task = { version = "0.3.30", default-features = false, features = ["std"] } +futures-util = { version = "0.3.30", features = ["channel", "io", "sink"] } gateway-messages = { git = "https://github.com/oxidecomputer/management-gateway-service", rev = "2739c18e80697aa6bc235c935176d14b4d757ee9", features = ["std"] } generic-array = { version = "0.14.7", default-features = false, features = ["more_lengths", "zeroize"] } getrandom = { version = "0.2.10", default-features = false, features = ["js", "rdrand", "std"] } @@ -77,7 +77,7 @@ petgraph = { version = "0.6.4", features = ["serde-1"] } postgres-types = { version = "0.2.6", default-features = false, features = ["with-chrono-0_4", "with-serde_json-1", "with-uuid-1"] } ppv-lite86 = { version = "0.2.17", default-features = false, features = ["simd", "std"] } predicates = { version = "3.0.4" } -proc-macro2 = { version = "1.0.69" } +proc-macro2 = { version = "1.0.74" } rand = { version = "0.8.5" } rand_chacha = { version = "0.3.1", default-features = false, features = ["std"] } regex = { version = "1.10.2" } @@ -97,7 +97,7 @@ spin = { version = "0.9.8" } string_cache = { version = "0.8.7" } subtle = { version = "2.5.0" } syn-dff4ba8e3ae991db = { package = "syn", version = "1.0.109", features = ["extra-traits", "fold", "full", "visit"] } -syn-f595c2ba2a3f28df = { package = "syn", version = "2.0.32", features = ["extra-traits", "fold", "full", "visit", "visit-mut"] } +syn-f595c2ba2a3f28df = { package = "syn", version = "2.0.46", features = ["extra-traits", "fold", "full", "visit", "visit-mut"] } time = { version = "0.3.27", features = ["formatting", "local-offset", "macros", "parsing"] } tokio = { version = "1.35.0", features = ["full", "test-util"] } tokio-postgres = { version = "0.7.10", features = ["with-chrono-0_4", "with-serde_json-1", "with-uuid-1"] } @@ -144,13 +144,13 @@ either = { version = "1.9.0" } elliptic-curve = { version = "0.13.8", features = ["ecdh", "hazmat", "pem", "std"] } ff = { version = "0.13.0", default-features = false, features = ["alloc"] } flate2 = { version = "1.0.28" } -futures = { version = "0.3.29" } -futures-channel = { version = "0.3.29", features = ["sink"] } -futures-core = { version = "0.3.29" } -futures-io = { version = "0.3.29", default-features = false, features = ["std"] } -futures-sink = { version = "0.3.29" } -futures-task = { version = "0.3.29", default-features = false, features = ["std"] } -futures-util = { version = "0.3.29", features = ["channel", "io", "sink"] } +futures = { version = "0.3.30" } +futures-channel = { version = "0.3.30", features = ["sink"] } +futures-core = { version = "0.3.30" } +futures-io = { version = "0.3.30", default-features = false, features = ["std"] } +futures-sink = { version = "0.3.30" } +futures-task = { version = "0.3.30", default-features = false, features = ["std"] } +futures-util = { version = "0.3.30", features = ["channel", "io", "sink"] } gateway-messages = { git = "https://github.com/oxidecomputer/management-gateway-service", rev = "2739c18e80697aa6bc235c935176d14b4d757ee9", features = ["std"] } generic-array = { version = "0.14.7", default-features = false, features = ["more_lengths", "zeroize"] } getrandom = { version = "0.2.10", default-features = false, features = ["js", "rdrand", "std"] } @@ -180,7 +180,7 @@ petgraph = { version = "0.6.4", features = ["serde-1"] } postgres-types = { version = "0.2.6", default-features = false, features = ["with-chrono-0_4", "with-serde_json-1", "with-uuid-1"] } ppv-lite86 = { version = "0.2.17", default-features = false, features = ["simd", "std"] } predicates = { version = "3.0.4" } -proc-macro2 = { version = "1.0.69" } +proc-macro2 = { version = "1.0.74" } rand = { version = "0.8.5" } rand_chacha = { version = "0.3.1", default-features = false, features = ["std"] } regex = { version = "1.10.2" } @@ -200,7 +200,7 @@ spin = { version = "0.9.8" } string_cache = { version = "0.8.7" } subtle = { version = "2.5.0" } syn-dff4ba8e3ae991db = { package = "syn", version = "1.0.109", features = ["extra-traits", "fold", "full", "visit"] } -syn-f595c2ba2a3f28df = { package = "syn", version = "2.0.32", features = ["extra-traits", "fold", "full", "visit", "visit-mut"] } +syn-f595c2ba2a3f28df = { package = "syn", version = "2.0.46", features = ["extra-traits", "fold", "full", "visit", "visit-mut"] } time = { version = "0.3.27", features = ["formatting", "local-offset", "macros", "parsing"] } time-macros = { version = "0.2.13", default-features = false, features = ["formatting", "parsing"] } tokio = { version = "1.35.0", features = ["full", "test-util"] }