diff --git a/Cargo.lock b/Cargo.lock index e4b9b27b698..1e2d195657a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -215,7 +215,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b62ddb9cb1ec0a098ad4bbf9344d0713fa193ae1a80af55febcff2627b6a00c1" dependencies = [ "futures-core", - "getrandom 0.2.7", + "getrandom 0.2.8", "instant", "pin-project-lite", "rand 0.8.5", @@ -260,9 +260,9 @@ checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "base64ct" -version = "1.5.2" +version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea2b2456fd614d856680dcd9fcc660a51a820fa09daef2e49772b56a193c8474" +checksum = "b645a089122eccb6111b4f81cbc1a49f5900ac4666bb93ac027feaecf15607bf" [[package]] name = "bb8" @@ -318,7 +318,7 @@ dependencies = [ "bitflags", "cexpr", "clang-sys", - "clap 3.2.22", + "clap 3.2.23", "env_logger", "lazy_static", "lazycell", @@ -481,9 +481,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.11.0" +version = "3.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1ad822118d20d2c234f427000d5acc36eabe1e29a348c89b63dd60b13f28e5d" +checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba" [[package]] name = "byteorder" @@ -535,9 +535,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.0.73" +version = "1.0.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" +checksum = "581f5dba903aac52ea3feb5ec4810848460ee833876f1f9b0fdeab1f19091574" [[package]] name = "cexpr" @@ -650,9 +650,9 @@ dependencies = [ [[package]] name = "clap" -version = "3.2.22" +version = "3.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86447ad904c7fb335a790c9d7fe3d0d971dc523b8ccd1561a520de9a85302750" +checksum = "71655c45cb9845d3270c9d6df84ebe72b4dad3c2ba3f7023ad47c144e4e473a5" dependencies = [ "atty", "bitflags", @@ -783,7 +783,7 @@ version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "344adc371239ef32293cb1c4fe519592fcf21206c79c02854320afcdf3ab4917" dependencies = [ - "time 0.3.15", + "time 0.3.16", "version_check", ] @@ -864,7 +864,7 @@ dependencies = [ "atty", "cast", "ciborium", - "clap 3.2.22", + "clap 3.2.23", "criterion-plot", "futures", "itertools", @@ -1106,9 +1106,9 @@ dependencies = [ [[package]] name = "cxx" -version = "1.0.79" +version = "1.0.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f83d0ebf42c6eafb8d7c52f7e5f2d3003b89c7aa4fd2b79229209459a849af8" +checksum = "6b7d4e43b25d3c994662706a1d4fcfc32aaa6afd287502c111b237093bb23f3a" dependencies = [ "cc", "cxxbridge-flags", @@ -1118,9 +1118,9 @@ dependencies = [ [[package]] name = "cxx-build" -version = "1.0.79" +version = "1.0.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07d050484b55975889284352b0ffc2ecbda25c0c55978017c132b29ba0818a86" +checksum = "84f8829ddc213e2c1368e51a2564c552b65a8cb6a28f31e576270ac81d5e5827" dependencies = [ "cc", "codespan-reporting", @@ -1133,15 +1133,15 @@ dependencies = [ [[package]] name = "cxxbridge-flags" -version = "1.0.79" +version = "1.0.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d2199b00553eda8012dfec8d3b1c75fce747cf27c169a270b3b99e3448ab78" +checksum = "e72537424b474af1460806647c41d4b6d35d09ef7fe031c5c2fa5766047cc56a" [[package]] name = "cxxbridge-macro" -version = "1.0.79" +version = "1.0.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcb67a6de1f602736dd7eaead0080cf3435df806c61b24b13328db128c58868f" +checksum = "309e4fb93eed90e1e14bea0da16b209f81813ba9fc7830c20ed151dd7bc0a4d7" dependencies = [ "proc-macro2", "quote", @@ -1150,9 +1150,9 @@ dependencies = [ [[package]] name = "darling" -version = "0.14.1" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4529658bdda7fd6769b8614be250cdcfc3aeb0ee72fe66f9e41e5e5eb73eac02" +checksum = "b0dd3cd20dc6b5a876612a6e5accfe7f3dd883db6d07acfbf14c128f61550dfa" dependencies = [ "darling_core", "darling_macro", @@ -1160,9 +1160,9 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.14.1" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "649c91bc01e8b1eac09fb91e8dbc7d517684ca6be8ebc75bb9cafc894f9fdb6f" +checksum = "a784d2ccaf7c98501746bf0be29b2022ba41fd62a2e622af997a03e9f972859f" dependencies = [ "fnv", "ident_case", @@ -1174,9 +1174,9 @@ dependencies = [ [[package]] name = "darling_macro" -version = "0.14.1" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddfc69c5bfcbd2fc09a0f38451d2daf0e372e367986a83906d1b0dbc88134fb5" +checksum = "7618812407e9402654622dd402b0a89dff9ba93badd6540781526117b92aab7e" dependencies = [ "darling_core", "quote", @@ -1192,7 +1192,7 @@ dependencies = [ "cfg-if 1.0.0", "hashbrown", "lock_api", - "parking_lot_core 0.9.3", + "parking_lot_core 0.9.4", ] [[package]] @@ -1437,7 +1437,7 @@ checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" [[package]] name = "dropshot" version = "0.8.1-dev" -source = "git+https://github.com/oxidecomputer/dropshot?branch=main#961a6715e832c490a6040ec01ebf2c3416fc95de" +source = "git+https://github.com/oxidecomputer/dropshot?branch=main#ed0b9c85e085f999013d030fada9a5d7f5ced69d" dependencies = [ "async-stream", "async-trait", @@ -1477,7 +1477,7 @@ dependencies = [ [[package]] name = "dropshot_endpoint" version = "0.8.1-dev" -source = "git+https://github.com/oxidecomputer/dropshot?branch=main#961a6715e832c490a6040ec01ebf2c3416fc95de" +source = "git+https://github.com/oxidecomputer/dropshot?branch=main#ed0b9c85e085f999013d030fada9a5d7f5ced69d" dependencies = [ "proc-macro2", "quote", @@ -1692,14 +1692,14 @@ dependencies = [ [[package]] name = "filetime" -version = "0.2.17" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e94a7bbaa59354bc20dd75b67f23e2797b4490e9d6928203fb105c79e448c86c" +checksum = "4b9663d381d07ae25dc88dbdf27df458faa83a9b25336bcac83d5e452b5fc9d3" dependencies = [ "cfg-if 1.0.0", "libc", "redox_syscall", - "windows-sys", + "windows-sys 0.42.0", ] [[package]] @@ -2012,9 +2012,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.7" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" +checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" dependencies = [ "cfg-if 1.0.0", "libc", @@ -2060,9 +2060,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.14" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca32592cf21ac7ccab1825cd87f6c9b3d9022c44d086172ed0966bec8af30be" +checksum = "5f9f29bc9dda355256b2916cf526ab02ce0aeaaaf2bad60d65ef3f12f11dd0f4" dependencies = [ "bytes", "fnv", @@ -2301,9 +2301,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.20" +version = "0.14.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02c929dc5c39e335a03c405292728118860721b10190d98c2a0f0efd5baafbac" +checksum = "abfba89e19b959ca163c7752ba59d737c1ceea53a5d31a149c805446fc958064" dependencies = [ "bytes", "futures-channel", @@ -2351,9 +2351,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.51" +version = "0.1.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5a6ef98976b22b3b7f2f3a806f858cb862044cfa66805aa3ad84cb3d3b785ed" +checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -2661,9 +2661,9 @@ dependencies = [ [[package]] name = "libsqlite3-sys" -version = "0.25.1" +version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f0455f2c1bc9a7caa792907026e469c1d91761fb0ea37cbb16427c77280cf35" +checksum = "29f835d03d717946d28b1d1ed632eb6f0e24a299388ee623d0c23118d3e8a7fa" dependencies = [ "pkg-config", "vcpkg", @@ -2819,14 +2819,14 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57ee1c23c7c63b0c9250c339ffdc69255f110b298b901b9f6c82547b7b87caaf" +checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de" dependencies = [ "libc", "log", "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys", + "windows-sys 0.42.0", ] [[package]] @@ -2897,9 +2897,9 @@ checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" [[package]] name = "newline-converter" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6f81c2b19eebbc4249b3ca6aff70ae05bf18d6a99b7cc63cf0248774e640565" +checksum = "f05d47d2bcf073a0a8864360195ac45e76acd18cadfe632ef01757a179070b32" [[package]] name = "newtype_derive" @@ -2975,6 +2975,16 @@ dependencies = [ "thiserror", ] +[[package]] +name = "nexus-test-interface" +version = "0.1.0" +dependencies = [ + "async-trait", + "dropshot", + "omicron-common", + "slog", +] + [[package]] name = "nexus-test-utils" version = "0.1.0" @@ -2986,6 +2996,7 @@ dependencies = [ "headers", "http", "hyper", + "nexus-test-interface", "omicron-common", "omicron-nexus", "omicron-sled-agent", @@ -3007,6 +3018,7 @@ dependencies = [ name = "nexus-test-utils-macros" version = "0.1.0" dependencies = [ + "proc-macro2", "quote", "syn", ] @@ -3180,6 +3192,7 @@ dependencies = [ "http", "hyper", "ipnetwork", + "libc", "macaddr", "parse-display", "progenitor 0.2.1-dev (git+https://github.com/oxidecomputer/progenitor)", @@ -3281,19 +3294,20 @@ dependencies = [ "ipnetwork", "itertools", "lazy_static", - "libc", "macaddr", "mime_guess", "newtype_derive", "nexus-db-model", "nexus-defaults", "nexus-passwords", + "nexus-test-interface", "nexus-test-utils", "nexus-test-utils-macros", "nexus-types", "num-integer", "omicron-common", "omicron-rpaths", + "omicron-sled-agent", "omicron-test-utils", "openapi-lint", "openapiv3", @@ -3485,9 +3499,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1" +checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860" [[package]] name = "oorandom" @@ -3612,9 +3626,9 @@ dependencies = [ [[package]] name = "os_str_bytes" -version = "6.3.0" +version = "6.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ff7415e9ae3fff1225851df9e0d9e4e5479f947619774677a63572e55e80eff" +checksum = "3baf96e39c5359d2eb0dd6ccb42c62b91d9678aa68160d261b9e0ccbf9e9dea9" [[package]] name = "oso" @@ -3823,7 +3837,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" dependencies = [ "lock_api", - "parking_lot_core 0.9.3", + "parking_lot_core 0.9.4", ] [[package]] @@ -3842,15 +3856,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929" +checksum = "4dc9e0dc2adc1c69d09143aff38d3d30c5c3f0df0dad82e6d25547af174ebec0" dependencies = [ "cfg-if 1.0.0", "libc", "redox_syscall", "smallvec", - "windows-sys", + "windows-sys 0.42.0", ] [[package]] @@ -4086,9 +4100,9 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.25" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" +checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" [[package]] name = "plotters" @@ -4319,7 +4333,7 @@ dependencies = [ [[package]] name = "progenitor" version = "0.2.1-dev" -source = "git+https://github.com/oxidecomputer/progenitor#54382d83d0dec348a56849f4cf8cc55479ffcb48" +source = "git+https://github.com/oxidecomputer/progenitor#f9708ef56c3a0b88dc88fc0a0fbf0d8885fdd3e8" dependencies = [ "anyhow", "clap 4.0.18", @@ -4348,7 +4362,7 @@ dependencies = [ [[package]] name = "progenitor-client" version = "0.2.1-dev" -source = "git+https://github.com/oxidecomputer/progenitor#54382d83d0dec348a56849f4cf8cc55479ffcb48" +source = "git+https://github.com/oxidecomputer/progenitor#f9708ef56c3a0b88dc88fc0a0fbf0d8885fdd3e8" dependencies = [ "bytes", "futures-core", @@ -4384,7 +4398,7 @@ dependencies = [ [[package]] name = "progenitor-impl" version = "0.2.1-dev" -source = "git+https://github.com/oxidecomputer/progenitor#54382d83d0dec348a56849f4cf8cc55479ffcb48" +source = "git+https://github.com/oxidecomputer/progenitor#f9708ef56c3a0b88dc88fc0a0fbf0d8885fdd3e8" dependencies = [ "getopts", "heck 0.4.0", @@ -4421,7 +4435,7 @@ dependencies = [ [[package]] name = "progenitor-macro" version = "0.2.1-dev" -source = "git+https://github.com/oxidecomputer/progenitor#54382d83d0dec348a56849f4cf8cc55479ffcb48" +source = "git+https://github.com/oxidecomputer/progenitor#f9708ef56c3a0b88dc88fc0a0fbf0d8885fdd3e8" dependencies = [ "openapiv3", "proc-macro2", @@ -4627,7 +4641,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.7", + "getrandom 0.2.8", ] [[package]] @@ -4696,25 +4710,25 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" dependencies = [ - "getrandom 0.2.7", + "getrandom 0.2.8", "redox_syscall", "thiserror", ] [[package]] name = "ref-cast" -version = "1.0.12" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12a733f1746c929b4913fe48f8697fcf9c55e3304ba251a79ffb41adfeaf49c2" +checksum = "53b15debb4f9d60d767cd8ca9ef7abb2452922f3214671ff052defc7f3502c44" dependencies = [ "ref-cast-impl", ] [[package]] name = "ref-cast-impl" -version = "1.0.12" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5887de4a01acafd221861463be6113e6e87275e79804e56779f4cdc131c60368" +checksum = "abfa8511e9e94fd3de6585a3d3cd00e01ed556dc9814829280af0e8dc72a8f36" dependencies = [ "proc-macro2", "quote", @@ -4917,9 +4931,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.20.6" +version = "0.20.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aab8ee6c7097ed6057f43c187a62418d0c05a4bd5f18b3571db50ee0f9ce033" +checksum = "539a2bfe908f471bfa933876bd1eb6a19cf2176d375f82ef7f99530a40e48c2c" dependencies = [ "log", "ring", @@ -5031,7 +5045,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88d6731146462ea25d9244b2ed5fd1d716d25c52e4d54aa4fb0f3c4e9854dbe2" dependencies = [ "lazy_static", - "windows-sys", + "windows-sys 0.36.1", ] [[package]] @@ -5235,9 +5249,9 @@ dependencies = [ [[package]] name = "serde_plain" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95455e7e29fada2052e72170af226fbe368a4ca33dee847875325d9fdb133858" +checksum = "d6018081315db179d0ce57b1fe4b62a12a0028c9cf9bbef868c9cf477b3c34ae" dependencies = [ "serde", ] @@ -5289,7 +5303,7 @@ dependencies = [ "serde", "serde_json", "serde_with_macros", - "time 0.3.15", + "time 0.3.16", ] [[package]] @@ -5524,7 +5538,7 @@ dependencies = [ "hostname", "slog", "slog-json", - "time 0.3.15", + "time 0.3.16", ] [[package]] @@ -5565,7 +5579,7 @@ dependencies = [ "serde", "serde_json", "slog", - "time 0.3.15", + "time 0.3.16", ] [[package]] @@ -5600,7 +5614,7 @@ dependencies = [ "slog", "term", "thread_local", - "time 0.3.15", + "time 0.3.16", ] [[package]] @@ -5736,7 +5750,7 @@ version = "0.1.0" source = "git+http://github.com/oxidecomputer/sprockets?rev=77df31efa5619d0767ffc837ef7468101608aee9#77df31efa5619d0767ffc837ef7468101608aee9" dependencies = [ "anyhow", - "clap 3.2.22", + "clap 3.2.23", "derive_more", "futures", "pin-project", @@ -5798,9 +5812,8 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "steno" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f695d04f2d9e08f3ed0c72acfa21cddc20eb76698a2ff0961a6758d3566454c2" +version = "0.2.1-dev" +source = "git+https://github.com/oxidecomputer/steno?rev=4cebb996217453b79896b53c9c2026007f2e69e8#4cebb996217453b79896b53c9c2026007f2e69e8" dependencies = [ "anyhow", "async-trait", @@ -6029,9 +6042,9 @@ checksum = "507e9898683b6c43a9aa55b64259b721b52ba226e0f3779137e50ad114a4c90b" [[package]] name = "textwrap" -version = "0.15.1" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "949517c0cf1bf4ee812e2e07e08ab448e3ae0d23472aee8a06c985f0c8815b16" +checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" [[package]] name = "thiserror" @@ -6153,22 +6166,32 @@ dependencies = [ [[package]] name = "time" -version = "0.3.15" +version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d634a985c4d4238ec39cacaed2e7ae552fbd3c476b552c1deac3021b7d7eaf0c" +checksum = "0fab5c8b9980850e06d92ddbe3ab839c062c801f3927c0fb8abd6fc8e918fbca" dependencies = [ "itoa", "libc", "num_threads", "serde", + "time-core", "time-macros", ] +[[package]] +name = "time-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" + [[package]] name = "time-macros" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42657b1a6f4d817cda8e7a0ace261fe0cc946cf3a80314390b22cc61ae080792" +checksum = "65bb801831d812c562ae7d2bfb531f26e66e4e1f6b17307ba4149c5064710e5b" +dependencies = [ + "time-core", +] [[package]] name = "tiny-keccak" @@ -6429,7 +6452,7 @@ dependencies = [ "radix_trie", "rand 0.8.5", "thiserror", - "time 0.3.15", + "time 0.3.16", "tokio", "tracing", "trust-dns-proto", @@ -6494,7 +6517,7 @@ dependencies = [ "futures-util", "serde", "thiserror", - "time 0.3.15", + "time 0.3.16", "tokio", "toml", "tracing", @@ -6761,7 +6784,7 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "feb41e78f93363bb2df8b0e86a2ca30eed7806ea16ea0c790d757cf93f79be83" dependencies = [ - "getrandom 0.2.7", + "getrandom 0.2.8", "serde", ] @@ -7037,43 +7060,100 @@ version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" dependencies = [ - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_msvc", + "windows_aarch64_msvc 0.36.1", + "windows_i686_gnu 0.36.1", + "windows_i686_msvc 0.36.1", + "windows_x86_64_gnu 0.36.1", + "windows_x86_64_msvc 0.36.1", ] +[[package]] +name = "windows-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc 0.42.0", + "windows_i686_gnu 0.42.0", + "windows_i686_msvc 0.42.0", + "windows_x86_64_gnu 0.42.0", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc 0.42.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e" + [[package]] name = "windows_aarch64_msvc" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4" + [[package]] name = "windows_i686_gnu" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" +[[package]] +name = "windows_i686_gnu" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7" + [[package]] name = "windows_i686_msvc" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" +[[package]] +name = "windows_i686_msvc" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246" + [[package]] name = "windows_x86_64_gnu" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028" + [[package]] name = "windows_x86_64_msvc" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" + [[package]] name = "winreg" version = "0.7.0" diff --git a/common/Cargo.toml b/common/Cargo.toml index f023020be08..13a42579ca4 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -25,7 +25,7 @@ serde_json = "1.0" serde_with = "2.0.1" slog = { version = "2.5", features = [ "max_level_trace", "release_max_level_debug" ] } smf = "0.2" -steno = "0.2" +steno = { git = "https://github.com/oxidecomputer/steno", rev = "4cebb996217453b79896b53c9c2026007f2e69e8" } thiserror = "1.0" tokio = { version = "1.21", features = [ "full" ] } tokio-postgres = { version = "0.7", features = [ "with-chrono-0_4", "with-uuid-1" ] } @@ -36,5 +36,6 @@ progenitor = { git = "https://github.com/oxidecomputer/progenitor" } [dev-dependencies] expectorate = "1.0.5" +libc = "0.2.135" serde_urlencoded = "0.7.1" tokio = { version = "1.21", features = [ "test-util" ] } diff --git a/common/src/nexus_config.rs b/common/src/nexus_config.rs index 13a6a7b057b..90ed8e0d0fb 100644 --- a/common/src/nexus_config.rs +++ b/common/src/nexus_config.rs @@ -7,11 +7,16 @@ use super::address::{Ipv6Subnet, RACK_PREFIX}; use super::postgres_config::PostgresConfigWithUrl; +use anyhow::anyhow; use dropshot::ConfigDropshot; +use dropshot::ConfigLogging; use serde::{Deserialize, Serialize}; use serde_with::serde_as; +use serde_with::DeserializeFromStr; use serde_with::DisplayFromStr; +use serde_with::SerializeDisplay; use std::fmt; +use std::net::SocketAddr; use std::path::{Path, PathBuf}; use uuid::Uuid; @@ -131,3 +136,578 @@ impl DeploymentConfig { Ok(config_parsed) } } + +// By design, we require that all config properties be specified (i.e., we don't +// use `serde(default)`). + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub struct AuthnConfig { + /// allowed authentication schemes for external HTTP server + pub schemes_external: Vec, +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub struct ConsoleConfig { + pub static_dir: PathBuf, + /// how long the browser can cache static assets + pub cache_control_max_age_minutes: u32, + /// how long a session can be idle before expiring + pub session_idle_timeout_minutes: u32, + /// how long a session can exist before expiring + pub session_absolute_timeout_minutes: u32, +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub struct UpdatesConfig { + /// Trusted root.json role for the TUF updates repository. + pub trusted_root: PathBuf, + /// Default base URL for the TUF repository. + pub default_base_url: String, +} + +/// Optional configuration for the timeseries database. +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +pub struct TimeseriesDbConfig { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub address: Option, +} + +// A deserializable type that does no validation on the tunable parameters. +#[derive(Clone, Debug, Deserialize, PartialEq)] +struct UnvalidatedTunables { + max_vpc_ipv4_subnet_prefix: u8, +} + +/// Tunable configuration parameters, intended for use in test environments or +/// other situations in which experimentation / tuning is valuable. +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[serde(try_from = "UnvalidatedTunables")] +pub struct Tunables { + /// The maximum prefix size supported for VPC Subnet IPv4 subnetworks. + /// + /// Note that this is the maximum _prefix_ size, which sets the minimum size + /// of the subnet. + pub max_vpc_ipv4_subnet_prefix: u8, +} + +// Convert from the unvalidated tunables, verifying each parameter as needed. +impl TryFrom for Tunables { + type Error = InvalidTunable; + + fn try_from(unvalidated: UnvalidatedTunables) -> Result { + Tunables::validate_ipv4_prefix(unvalidated.max_vpc_ipv4_subnet_prefix)?; + Ok(Tunables { + max_vpc_ipv4_subnet_prefix: unvalidated.max_vpc_ipv4_subnet_prefix, + }) + } +} + +/// Minimum prefix size supported in IPv4 VPC Subnets. +/// +/// NOTE: This is the minimum _prefix_, which sets the maximum subnet size. +pub const MIN_VPC_IPV4_SUBNET_PREFIX: u8 = 8; + +/// The number of reserved addresses at the beginning of a subnet range. +pub const NUM_INITIAL_RESERVED_IP_ADDRESSES: usize = 5; + +impl Tunables { + fn validate_ipv4_prefix(prefix: u8) -> Result<(), InvalidTunable> { + let absolute_max: u8 = 32_u8 + .checked_sub( + // Always need space for the reserved Oxide addresses, including the + // broadcast address at the end of the subnet. + ((NUM_INITIAL_RESERVED_IP_ADDRESSES + 1) as f32) + .log2() // Subnet size to bit prefix. + .ceil() // Round up to a whole number of bits. + as u8, + ) + .expect("Invalid absolute maximum IPv4 subnet prefix"); + if prefix >= MIN_VPC_IPV4_SUBNET_PREFIX && prefix <= absolute_max { + Ok(()) + } else { + Err(InvalidTunable { + tunable: String::from("max_vpc_ipv4_subnet_prefix"), + message: format!( + "IPv4 subnet prefix must be in the range [0, {}], found: {}", + absolute_max, + prefix, + ), + }) + } + } +} + +/// The maximum prefix size by default. +/// +/// There are 6 Oxide reserved IP addresses, 5 at the beginning for DNS and the +/// like, and the broadcast address at the end of the subnet. This size provides +/// room for 2 ** 6 - 6 = 58 IP addresses, which seems like a reasonable size +/// for the smallest subnet that's still useful in many contexts. +pub const MAX_VPC_IPV4_SUBNET_PREFIX: u8 = 26; + +impl Default for Tunables { + fn default() -> Self { + Tunables { max_vpc_ipv4_subnet_prefix: MAX_VPC_IPV4_SUBNET_PREFIX } + } +} + +/// Configuration for a nexus server +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub struct PackageConfig { + /// Console-related tunables + pub console: ConsoleConfig, + /// Server-wide logging configuration. + pub log: ConfigLogging, + /// Authentication-related configuration + pub authn: AuthnConfig, + /// Timeseries database configuration. + #[serde(default)] + pub timeseries_db: TimeseriesDbConfig, + /// Updates-related configuration. Updates APIs return 400 Bad Request when this is + /// unconfigured. + #[serde(default)] + pub updates: Option, + /// Tunable configuration for testing and experimentation + #[serde(default)] + pub tunables: Tunables, +} + +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +pub struct Config { + /// Configuration parameters known at compile-time. + #[serde(flatten)] + pub pkg: PackageConfig, + + /// A variety of configuration parameters only known at deployment time. + pub deployment: DeploymentConfig, +} + +impl Config { + /// Load a `Config` from the given TOML file + /// + /// This config object can then be used to create a new `Nexus`. + /// The format is described in the README. + pub fn from_file>(path: P) -> Result { + let path = path.as_ref(); + let file_contents = std::fs::read_to_string(path) + .map_err(|e| (path.to_path_buf(), e))?; + let config_parsed: Self = toml::from_str(&file_contents) + .map_err(|e| (path.to_path_buf(), e))?; + Ok(config_parsed) + } +} + +/// List of supported external authn schemes +/// +/// Note that the authn subsystem doesn't know about this type. It allows +/// schemes to be called whatever they want. This is just to provide a set of +/// allowed values for configuration. +#[derive( + Clone, Copy, Debug, DeserializeFromStr, Eq, PartialEq, SerializeDisplay, +)] +pub enum SchemeName { + Spoof, + SessionCookie, + AccessToken, +} + +impl std::str::FromStr for SchemeName { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + match s { + "spoof" => Ok(SchemeName::Spoof), + "session_cookie" => Ok(SchemeName::SessionCookie), + "access_token" => Ok(SchemeName::AccessToken), + _ => Err(anyhow!("unsupported authn scheme: {:?}", s)), + } + } +} + +impl std::fmt::Display for SchemeName { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + SchemeName::Spoof => "spoof", + SchemeName::SessionCookie => "session_cookie", + SchemeName::AccessToken => "access_token", + }) + } +} + +#[cfg(test)] +mod test { + use super::Tunables; + use super::{ + AuthnConfig, Config, ConsoleConfig, LoadError, PackageConfig, + SchemeName, TimeseriesDbConfig, UpdatesConfig, + }; + use crate::address::{Ipv6Subnet, RACK_PREFIX}; + use crate::nexus_config::{Database, DeploymentConfig, LoadErrorKind}; + use dropshot::ConfigDropshot; + use dropshot::ConfigLogging; + use dropshot::ConfigLoggingIfExists; + use dropshot::ConfigLoggingLevel; + use libc; + use std::fs; + use std::net::{Ipv6Addr, SocketAddr}; + use std::path::Path; + use std::path::PathBuf; + + /// Generates a temporary filesystem path unique for the given label. + fn temp_path(label: &str) -> PathBuf { + let arg0str = std::env::args().next().expect("expected process arg0"); + let arg0 = Path::new(&arg0str) + .file_name() + .expect("expected arg0 filename") + .to_str() + .expect("expected arg0 filename to be valid Unicode"); + let pid = std::process::id(); + let mut pathbuf = std::env::temp_dir(); + pathbuf.push(format!("{}.{}.{}", arg0, pid, label)); + pathbuf + } + + /// Load a Config with the given string `contents`. To exercise + /// the full path, this function writes the contents to a file first, then + /// loads the config from that file, then removes the file. `label` is used + /// as a unique string for the filename and error messages. It should be + /// unique for each test. + fn read_config(label: &str, contents: &str) -> Result { + let pathbuf = temp_path(label); + let path = pathbuf.as_path(); + eprintln!("writing test config {}", path.display()); + fs::write(path, contents).expect("write to tempfile failed"); + + let result = Config::from_file(path); + fs::remove_file(path).expect("failed to remove temporary file"); + eprintln!("{:?}", result); + result + } + + // Totally bogus config files (nonexistent, bad TOML syntax) + + #[test] + fn test_config_nonexistent() { + let error = Config::from_file(Path::new("/nonexistent")) + .expect_err("expected config to fail from /nonexistent"); + let expected = std::io::Error::from_raw_os_error(libc::ENOENT); + assert_eq!(error, expected); + } + + #[test] + fn test_config_bad_toml() { + let error = + read_config("bad_toml", "foo =").expect_err("expected failure"); + if let LoadErrorKind::Parse(error) = &error.kind { + assert_eq!(error.line_col(), Some((0, 5))); + assert_eq!( + error.to_string(), + "unexpected eof encountered at line 1 column 6" + ); + } else { + panic!( + "Got an unexpected error, expected Parse but got {:?}", + error + ); + } + } + + // Empty config (special case of a missing required field, but worth calling + // out explicitly) + + #[test] + fn test_config_empty() { + let error = read_config("empty", "").expect_err("expected failure"); + if let LoadErrorKind::Parse(error) = &error.kind { + assert_eq!(error.line_col(), None); + assert_eq!(error.to_string(), "missing field `deployment`"); + } else { + panic!( + "Got an unexpected error, expected Parse but got {:?}", + error + ); + } + } + + // Success case. We don't need to retest semantics for either ConfigLogging + // or ConfigDropshot because those are both tested within Dropshot. If we + // add new configuration sections of our own, we will want to test those + // here (both syntax and semantics). + #[test] + fn test_valid() { + let config = read_config( + "valid", + r##" + [console] + static_dir = "tests/static" + cache_control_max_age_minutes = 10 + session_idle_timeout_minutes = 60 + session_absolute_timeout_minutes = 480 + [authn] + schemes_external = [] + [log] + mode = "file" + level = "debug" + path = "/nonexistent/path" + if_exists = "fail" + [timeseries_db] + address = "[::1]:8123" + [updates] + trusted_root = "/path/to/root.json" + default_base_url = "http://example.invalid/" + [tunables] + max_vpc_ipv4_subnet_prefix = 27 + [deployment] + id = "28b90dc4-c22a-65ba-f49a-f051fe01208f" + rack_id = "38b90dc4-c22a-65ba-f49a-f051fe01208f" + [[deployment.dropshot_external]] + bind_address = "10.1.2.3:4567" + request_body_max_bytes = 1024 + [deployment.dropshot_internal] + bind_address = "10.1.2.3:4568" + request_body_max_bytes = 1024 + [deployment.subnet] + net = "::/56" + [deployment.database] + type = "from_dns" + "##, + ) + .unwrap(); + + assert_eq!( + config, + Config { + deployment: DeploymentConfig { + id: "28b90dc4-c22a-65ba-f49a-f051fe01208f".parse().unwrap(), + rack_id: "38b90dc4-c22a-65ba-f49a-f051fe01208f" + .parse() + .unwrap(), + dropshot_external: vec![ConfigDropshot { + bind_address: "10.1.2.3:4567" + .parse::() + .unwrap(), + ..Default::default() + },], + dropshot_internal: ConfigDropshot { + bind_address: "10.1.2.3:4568" + .parse::() + .unwrap(), + ..Default::default() + }, + subnet: Ipv6Subnet::::new(Ipv6Addr::LOCALHOST), + database: Database::FromDns, + }, + pkg: PackageConfig { + console: ConsoleConfig { + static_dir: "tests/static".parse().unwrap(), + cache_control_max_age_minutes: 10, + session_idle_timeout_minutes: 60, + session_absolute_timeout_minutes: 480 + }, + authn: AuthnConfig { schemes_external: Vec::new() }, + log: ConfigLogging::File { + level: ConfigLoggingLevel::Debug, + if_exists: ConfigLoggingIfExists::Fail, + path: "/nonexistent/path".to_string() + }, + timeseries_db: TimeseriesDbConfig { + address: Some("[::1]:8123".parse().unwrap()) + }, + updates: Some(UpdatesConfig { + trusted_root: PathBuf::from("/path/to/root.json"), + default_base_url: "http://example.invalid/".into(), + }), + tunables: Tunables { max_vpc_ipv4_subnet_prefix: 27 }, + }, + } + ); + + let config = read_config( + "valid", + r##" + [console] + static_dir = "tests/static" + cache_control_max_age_minutes = 10 + session_idle_timeout_minutes = 60 + session_absolute_timeout_minutes = 480 + [authn] + schemes_external = [ "spoof", "session_cookie" ] + [log] + mode = "file" + level = "debug" + path = "/nonexistent/path" + if_exists = "fail" + [timeseries_db] + address = "[::1]:8123" + [deployment] + id = "28b90dc4-c22a-65ba-f49a-f051fe01208f" + rack_id = "38b90dc4-c22a-65ba-f49a-f051fe01208f" + [[deployment.dropshot_external]] + bind_address = "10.1.2.3:4567" + request_body_max_bytes = 1024 + [deployment.dropshot_internal] + bind_address = "10.1.2.3:4568" + request_body_max_bytes = 1024 + [deployment.subnet] + net = "::/56" + [deployment.database] + type = "from_dns" + "##, + ) + .unwrap(); + + assert_eq!( + config.pkg.authn.schemes_external, + vec![SchemeName::Spoof, SchemeName::SessionCookie], + ); + } + + #[test] + fn test_bad_authn_schemes() { + let error = read_config( + "bad authn.schemes_external", + r##" + [console] + static_dir = "tests/static" + cache_control_max_age_minutes = 10 + session_idle_timeout_minutes = 60 + session_absolute_timeout_minutes = 480 + [authn] + schemes_external = ["trust-me"] + [log] + mode = "file" + level = "debug" + path = "/nonexistent/path" + if_exists = "fail" + [timeseries_db] + address = "[::1]:8123" + [deployment] + id = "28b90dc4-c22a-65ba-f49a-f051fe01208f" + rack_id = "38b90dc4-c22a-65ba-f49a-f051fe01208f" + [[deployment.dropshot_external]] + bind_address = "10.1.2.3:4567" + request_body_max_bytes = 1024 + [deployment.dropshot_internal] + bind_address = "10.1.2.3:4568" + request_body_max_bytes = 1024 + [deployment.subnet] + net = "::/56" + [deployment.database] + type = "from_dns" + "##, + ) + .expect_err("expected failure"); + if let LoadErrorKind::Parse(error) = &error.kind { + assert!( + error + .to_string() + .starts_with("unsupported authn scheme: \"trust-me\""), + "error = {}", + error.to_string() + ); + } else { + panic!( + "Got an unexpected error, expected Parse but got {:?}", + error + ); + } + } + + #[test] + fn test_invalid_ipv4_prefix_tunable() { + let error = read_config( + "invalid_ipv4_prefix_tunable", + r##" + [console] + static_dir = "tests/static" + cache_control_max_age_minutes = 10 + session_idle_timeout_minutes = 60 + session_absolute_timeout_minutes = 480 + [authn] + schemes_external = [] + [log] + mode = "file" + level = "debug" + path = "/nonexistent/path" + if_exists = "fail" + [timeseries_db] + address = "[::1]:8123" + [updates] + trusted_root = "/path/to/root.json" + default_base_url = "http://example.invalid/" + [tunables] + max_vpc_ipv4_subnet_prefix = 100 + [deployment] + id = "28b90dc4-c22a-65ba-f49a-f051fe01208f" + rack_id = "38b90dc4-c22a-65ba-f49a-f051fe01208f" + [[deployment.dropshot_external]] + bind_address = "10.1.2.3:4567" + request_body_max_bytes = 1024 + [deployment.dropshot_internal] + bind_address = "10.1.2.3:4568" + request_body_max_bytes = 1024 + [deployment.subnet] + net = "::/56" + [deployment.database] + type = "from_dns" + "##, + ) + .expect_err("Expected failure"); + if let LoadErrorKind::Parse(error) = &error.kind { + assert!(error.to_string().starts_with( + r#"invalid "max_vpc_ipv4_subnet_prefix": "IPv4 subnet prefix must"#, + )); + } else { + panic!( + "Got an unexpected error, expected Parse but got {:?}", + error + ); + } + } + + #[test] + fn test_repo_configs_are_valid() { + // The example config file should be valid. + let config_path = "../nexus/examples/config.toml"; + println!("checking {:?}", config_path); + let example_config = Config::from_file(config_path) + .expect("example config file is not valid"); + + // The config file used for the tests should also be valid. The tests + // won't clear the runway anyway if this file isn't valid. But it's + // helpful to verify this here explicitly as well. + let config_path = "../nexus/examples/config.toml"; + println!("checking {:?}", config_path); + let _ = Config::from_file(config_path) + .expect("test config file is not valid"); + + // The partial config file that's used to deploy Nexus must also be + // valid. However, it's missing the "deployment" section because that's + // generated at deployment time. We'll serialize this section from the + // example config file (loaded above), append it to the contents of this + // file, and verify the whole thing. + #[derive(serde::Serialize)] + struct DummyConfig { + deployment: DeploymentConfig, + } + let config_path = "../smf/nexus/config-partial.toml"; + println!( + "checking {:?} with example deployment section added", + config_path + ); + let mut contents = std::fs::read_to_string(config_path) + .expect("failed to read Nexus SMF config file"); + contents.push_str( + "\n\n\n \ + # !! content below added by test_repo_configs_are_valid()\n\ + \n\n\n", + ); + let example_deployment = toml::to_string_pretty(&DummyConfig { + deployment: example_config.deployment, + }) + .unwrap(); + contents.push_str(&example_deployment); + let _: Config = toml::from_str(&contents) + .expect("Nexus SMF config file is not valid"); + } +} diff --git a/nexus/Cargo.toml b/nexus/Cargo.toml index bcbc1827d68..ef426c09120 100644 --- a/nexus/Cargo.toml +++ b/nexus/Cargo.toml @@ -30,10 +30,12 @@ hyper = "0.14" internal-dns-client = { path = "../internal-dns-client" } ipnetwork = "0.20" lazy_static = "1.4.0" -libc = "0.2.137" macaddr = { version = "1.0.1", features = [ "serde_std" ]} mime_guess = "2.0.4" newtype_derive = "0.1.6" +# Not under "dev-dependencies"; these also need to be implemented for +# integration tests. +nexus-test-interface = { path = "test-interface" } num-integer = "0.1.45" # must match samael's crate! openssl = "0.10" @@ -56,7 +58,7 @@ serde_urlencoded = "0.7.1" serde_with = "2.0.1" sled-agent-client = { path = "../sled-agent-client" } slog-dtrace = "0.2" -steno = "0.2" +steno = { git = "https://github.com/oxidecomputer/steno", rev = "4cebb996217453b79896b53c9c2026007f2e69e8" } tempfile = "3.3" thiserror = "1.0" tokio-tungstenite = "0.17.2" @@ -127,6 +129,7 @@ itertools = "0.10.5" nexus-test-utils-macros = { path = "test-utils-macros" } nexus-test-utils = { path = "test-utils" } omicron-test-utils = { path = "../test-utils" } +omicron-sled-agent = { path = "../sled-agent" } openapiv3 = "1.0" regex = "1.6.0" subprocess = "0.2.9" diff --git a/nexus/benches/setup_benchmark.rs b/nexus/benches/setup_benchmark.rs index 24584670ce5..d9e9577a1f0 100644 --- a/nexus/benches/setup_benchmark.rs +++ b/nexus/benches/setup_benchmark.rs @@ -12,7 +12,9 @@ use omicron_test_utils::dev; // This is the default wrapper around most Nexus integration tests. // Benchmark how long an "empty test" would take. async fn do_full_setup() { - let ctx = nexus_test_utils::test_setup("full_setup").await; + let ctx = + nexus_test_utils::test_setup::("full_setup") + .await; ctx.teardown().await; } diff --git a/nexus/db-model/Cargo.toml b/nexus/db-model/Cargo.toml index 6e648657c8a..37353bc6e5f 100644 --- a/nexus/db-model/Cargo.toml +++ b/nexus/db-model/Cargo.toml @@ -25,7 +25,7 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" uuid = { version = "1.2.1", features = ["serde", "v4"] } -steno = "0.2" +steno = { git = "https://github.com/oxidecomputer/steno", rev = "4cebb996217453b79896b53c9c2026007f2e69e8" } db-macros = { path = "../db-macros" } omicron-common = { path = "../../common" } diff --git a/nexus/db-model/src/ipv4net.rs b/nexus/db-model/src/ipv4net.rs index 664dda5c80c..c5b45e4064f 100644 --- a/nexus/db-model/src/ipv4net.rs +++ b/nexus/db-model/src/ipv4net.rs @@ -8,8 +8,8 @@ use diesel::pg::Pg; use diesel::serialize::{self, ToSql}; use diesel::sql_types; use ipnetwork::IpNetwork; -use nexus_defaults as defaults; use omicron_common::api::external; +use omicron_common::nexus_config::NUM_INITIAL_RESERVED_IP_ADDRESSES; use std::net::Ipv4Addr; #[derive(Clone, Copy, Debug, PartialEq, AsExpression, FromSqlRow)] @@ -26,7 +26,7 @@ impl Ipv4Net { && ( // First N addresses are reserved self.iter() - .take(defaults::NUM_INITIAL_RESERVED_IP_ADDRESSES) + .take(NUM_INITIAL_RESERVED_IP_ADDRESSES) .all(|this| this != addr) ) && ( diff --git a/nexus/db-model/src/ipv6net.rs b/nexus/db-model/src/ipv6net.rs index a91c3efcd4d..2bbcb08a4bb 100644 --- a/nexus/db-model/src/ipv6net.rs +++ b/nexus/db-model/src/ipv6net.rs @@ -8,8 +8,8 @@ use diesel::pg::Pg; use diesel::serialize::{self, ToSql}; use diesel::sql_types; use ipnetwork::IpNetwork; -use nexus_defaults as defaults; use omicron_common::api::external; +use omicron_common::nexus_config::NUM_INITIAL_RESERVED_IP_ADDRESSES; use rand::{rngs::StdRng, SeedableRng}; use std::net::Ipv6Addr; @@ -77,7 +77,7 @@ impl Ipv6Net { self.contains(addr) && self .iter() - .take(defaults::NUM_INITIAL_RESERVED_IP_ADDRESSES) + .take(NUM_INITIAL_RESERVED_IP_ADDRESSES) .all(|this| this != addr) } } diff --git a/nexus/defaults/src/lib.rs b/nexus/defaults/src/lib.rs index c7af06cca01..3474804cfa0 100644 --- a/nexus/defaults/src/lib.rs +++ b/nexus/defaults/src/lib.rs @@ -13,14 +13,6 @@ use omicron_common::api::external::Ipv6Net; use std::net::Ipv4Addr; use std::net::Ipv6Addr; -/// Minimum prefix size supported in IPv4 VPC Subnets. -/// -/// NOTE: This is the minimum _prefix_, which sets the maximum subnet size. -pub const MIN_VPC_IPV4_SUBNET_PREFIX: u8 = 8; - -/// The number of reserved addresses at the beginning of a subnet range. -pub const NUM_INITIAL_RESERVED_IP_ADDRESSES: usize = 5; - /// The name provided for a default primary network interface for a guest /// instance. pub const DEFAULT_PRIMARY_NIC_NAME: &str = "net0"; diff --git a/nexus/src/app/mod.rs b/nexus/src/app/mod.rs index 172d83b9123..ce54bc347b3 100644 --- a/nexus/src/app/mod.rs +++ b/nexus/src/app/mod.rs @@ -33,7 +33,7 @@ mod organization; mod oximeter; mod project; mod rack; -mod saga; +pub mod saga; mod session; mod silo; mod sled; @@ -46,7 +46,7 @@ mod vpc_subnet; // Sagas are not part of the "Nexus" implementation, but they are // application logic. -mod sagas; +pub mod sagas; // TODO: When referring to API types, we should try to include // the prefix unless it is unambiguous. diff --git a/nexus/src/app/saga.rs b/nexus/src/app/saga.rs index 983349d58b1..256c3a73290 100644 --- a/nexus/src/app/saga.rs +++ b/nexus/src/app/saga.rs @@ -11,6 +11,7 @@ use crate::authz; use crate::context::OpContext; use crate::saga_interface::SagaContext; use anyhow::Context; +use futures::future::BoxFuture; use futures::StreamExt; use omicron_common::api::external; use omicron_common::api::external::DataPageParams; @@ -24,9 +25,37 @@ use steno::DagBuilder; use steno::SagaDag; use steno::SagaId; use steno::SagaName; +use steno::SagaResult; use steno::SagaResultOk; use uuid::Uuid; +/// Encapsulates a saga to be run before we actually start running it +/// +/// At this point, we've built the DAG, loaded it into the SEC, etc. but haven't +/// started it running. This is a useful point to inject errors, inspect the +/// DAG, etc. +pub struct RunnableSaga { + id: SagaId, + fut: BoxFuture<'static, SagaResult>, +} + +impl RunnableSaga { + pub fn id(&self) -> SagaId { + self.id + } +} + +pub fn create_saga_dag( + params: N::Params, +) -> Result { + let builder = DagBuilder::new(SagaName::new(N::NAME)); + let dag = N::make_saga_dag(¶ms, builder)?; + let params = serde_json::to_value(¶ms).map_err(|e| { + SagaInitError::SerializeError(String::from("saga params"), e) + })?; + Ok(SagaDag::new(dag, params)) +} + impl super::Nexus { pub async fn sagas_list( &self, @@ -66,27 +95,18 @@ impl super::Nexus { })? } - /// Given a saga type and parameters, create a new saga and execute it. - pub(crate) async fn execute_saga( + pub async fn create_runnable_saga( self: &Arc, - params: N::Params, - ) -> Result { - let saga = { - let builder = DagBuilder::new(SagaName::new(N::NAME)); - let dag = N::make_saga_dag(¶ms, builder)?; - let params = serde_json::to_value(¶ms).map_err(|e| { - SagaInitError::SerializeError(String::from("saga params"), e) - })?; - SagaDag::new(dag, params) - }; - + dag: SagaDag, + ) -> Result { + // Construct the context necessary to execute this saga. let saga_id = SagaId(Uuid::new_v4()); let saga_logger = self.log.new(o!( - "saga_name" => saga.saga_name().to_string(), + "saga_name" => dag.saga_name().to_string(), "saga_id" => saga_id.to_string() )); let saga_context = Arc::new(Arc::new(SagaContext::new( - Arc::clone(self), + self.clone(), saga_logger, Arc::clone(&self.authz), ))); @@ -95,7 +115,7 @@ impl super::Nexus { .saga_create( saga_id, saga_context, - Arc::new(saga), + Arc::new(dag), ACTION_REGISTRY.clone(), ) .await @@ -106,14 +126,20 @@ impl super::Nexus { // Steno. Error::internal_error(&format!("{:#}", error)) })?; + Ok(RunnableSaga { id: saga_id, fut: future }) + } + pub async fn run_saga( + &self, + runnable_saga: RunnableSaga, + ) -> Result { self.sec_client - .saga_start(saga_id) + .saga_start(runnable_saga.id) .await .context("starting saga") .map_err(|error| Error::internal_error(&format!("{:#}", error)))?; - let result = future.await; + let result = runnable_saga.fut.await; result.kind.map_err(|saga_error| { saga_error .error_source @@ -125,4 +151,23 @@ impl super::Nexus { )) }) } + + pub fn sec(&self) -> &steno::SecClient { + &self.sec_client + } + + /// Given a saga type and parameters, create a new saga and execute it. + pub(crate) async fn execute_saga( + self: &Arc, + params: N::Params, + ) -> Result { + // Construct the DAG specific to this saga. + let dag = create_saga_dag::(params)?; + + // Register the saga with the saga executor. + let runnable_saga = self.create_runnable_saga(dag).await?; + + // Actually run the saga to completion. + self.run_saga(runnable_saga).await + } } diff --git a/nexus/src/app/sagas/disk_create.rs b/nexus/src/app/sagas/disk_create.rs index 9bbbb658277..f72d01d6cae 100644 --- a/nexus/src/app/sagas/disk_create.rs +++ b/nexus/src/app/sagas/disk_create.rs @@ -3,8 +3,11 @@ // file, You can obtain one at https://mozilla.org/MPL/2.0/. use super::{ - common_storage::ensure_all_datasets_and_regions, ActionRegistry, - NexusActionContext, NexusSaga, SagaInitError, ACTION_GENERATE_ID, + common_storage::{ + delete_crucible_regions, ensure_all_datasets_and_regions, + }, + ActionRegistry, NexusActionContext, NexusSaga, SagaInitError, + ACTION_GENERATE_ID, }; use crate::app::sagas::NexusAction; use crate::context::OpContext; @@ -42,10 +45,16 @@ lazy_static! { sdc_create_disk_record, sdc_create_disk_record_undo ); - static ref REGIONS_ALLOC: NexusAction = - new_action_noop_undo("disk-create.regions-alloc", sdc_alloc_regions,); - static ref REGIONS_ENSURE: NexusAction = - new_action_noop_undo("disk-create.regions-ensure", sdc_regions_ensure,); + static ref REGIONS_ALLOC: NexusAction = ActionFunc::new_action( + "disk-create.regions-alloc", + sdc_alloc_regions, + sdc_alloc_regions_undo, + ); + static ref REGIONS_ENSURE: NexusAction = ActionFunc::new_action( + "disk-create.regions-ensure", + sdc_regions_ensure, + sdc_regions_ensure_undo, + ); static ref CREATE_VOLUME_RECORD: NexusAction = ActionFunc::new_action( "disk-create.create-volume-record", sdc_create_volume_record, @@ -242,6 +251,23 @@ async fn sdc_alloc_regions( Ok(datasets_and_regions) } +async fn sdc_alloc_regions_undo( + sagactx: NexusActionContext, +) -> Result<(), anyhow::Error> { + let osagactx = sagactx.user_data(); + + let region_ids = sagactx + .lookup::>( + "datasets_and_regions", + )? + .into_iter() + .map(|(_, region)| region.id()) + .collect::>(); + + osagactx.datastore().regions_hard_delete(region_ids).await?; + Ok(()) +} + /// Call out to Crucible agent and perform region creation. async fn sdc_regions_ensure( sagactx: NexusActionContext, @@ -262,7 +288,6 @@ async fn sdc_regions_ensure( // If a disk source was requested, set the read-only parent of this disk. let osagactx = sagactx.user_data(); let params = sagactx.saga_params::()?; - let log = osagactx.log(); let opctx = OpContext::for_saga_action(&sagactx, ¶ms.serialized_authn); let mut read_only_parent: Option> = @@ -432,6 +457,21 @@ async fn sdc_regions_ensure( Ok(volume_data) } +async fn sdc_regions_ensure_undo( + sagactx: NexusActionContext, +) -> Result<(), anyhow::Error> { + let log = sagactx.user_data().log(); + warn!(log, "sdc_regions_ensure_undo: Deleting crucible regions"); + delete_crucible_regions( + sagactx.lookup::>( + "datasets_and_regions", + )?, + ) + .await?; + info!(log, "sdc_regions_ensure_undo: Deleted crucible regions"); + Ok(()) +} + async fn sdc_create_volume_record( sagactx: NexusActionContext, ) -> Result { @@ -557,3 +597,194 @@ fn randomize_volume_construction_request_ids( } } } + +#[cfg(test)] +mod test { + use crate::{ + app::saga::create_saga_dag, app::sagas::disk_create::Params, + app::sagas::disk_create::SagaDiskCreate, authn::saga::Serialized, + context::OpContext, db::datastore::DataStore, external_api::params, + }; + use async_bb8_diesel::{AsyncRunQueryDsl, OptionalExtension}; + use diesel::{ExpressionMethods, QueryDsl, SelectableHelper}; + use dropshot::test_util::ClientTestContext; + use nexus_test_utils::resource_helpers::create_ip_pool; + use nexus_test_utils::resource_helpers::create_organization; + use nexus_test_utils::resource_helpers::create_project; + use nexus_test_utils::resource_helpers::DiskTest; + use nexus_test_utils_macros::nexus_test; + use omicron_common::api::external::ByteCount; + use omicron_common::api::external::IdentityMetadataCreateParams; + use omicron_sled_agent::sim::SledAgent; + use uuid::Uuid; + + type ControlPlaneTestContext = + nexus_test_utils::ControlPlaneTestContext; + + const ORG_NAME: &str = "test-org"; + const PROJECT_NAME: &str = "springfield-squidport"; + + async fn create_org_and_project(client: &ClientTestContext) -> Uuid { + create_ip_pool(&client, "p0", None, None).await; + create_organization(&client, ORG_NAME).await; + let project = create_project(client, ORG_NAME, PROJECT_NAME).await; + project.identity.id + } + + // Helper for creating disk create parameters + fn new_test_params(opctx: &OpContext, project_id: Uuid) -> Params { + Params { + serialized_authn: Serialized::for_opctx(opctx), + project_id, + create_params: params::DiskCreate { + identity: IdentityMetadataCreateParams { + name: "my-disk".parse().expect("Invalid disk name"), + description: "My disk".to_string(), + }, + disk_source: params::DiskSource::Blank { + block_size: params::BlockSize(512), + }, + size: ByteCount::from_gibibytes_u32(1), + }, + } + } + + pub fn test_opctx(cptestctx: &ControlPlaneTestContext) -> OpContext { + OpContext::for_tests( + cptestctx.logctx.log.new(o!()), + cptestctx.server.apictx.nexus.datastore().clone(), + ) + } + + #[nexus_test(server = crate::Server)] + async fn test_saga_basic_usage_succeeds( + cptestctx: &ControlPlaneTestContext, + ) { + DiskTest::new(cptestctx).await; + + let client = &cptestctx.external_client; + let nexus = &cptestctx.server.apictx.nexus; + let project_id = create_org_and_project(&client).await; + + // Build the saga DAG with the provided test parameters + let opctx = test_opctx(cptestctx); + let params = new_test_params(&opctx, project_id); + let dag = create_saga_dag::(params).unwrap(); + let runnable_saga = nexus.create_runnable_saga(dag).await.unwrap(); + + // Actually run the saga + let output = nexus.run_saga(runnable_saga).await.unwrap(); + + let disk = output + .lookup_node_output::("created_disk") + .unwrap(); + assert_eq!(disk.project_id, project_id); + } + + async fn no_disk_records_exist(datastore: &DataStore) -> bool { + use crate::db::model::Disk; + use crate::db::schema::disk::dsl; + + dsl::disk + .filter(dsl::time_deleted.is_null()) + .select(Disk::as_select()) + .first_async::(datastore.pool_for_tests().await.unwrap()) + .await + .optional() + .unwrap() + .is_none() + } + + async fn no_region_allocations_exist( + datastore: &DataStore, + test: &DiskTest, + ) -> bool { + for zpool in &test.zpools { + for dataset in &zpool.datasets { + if datastore + .regions_total_occupied_size(dataset.id) + .await + .unwrap() + != 0 + { + return false; + } + } + } + true + } + + async fn no_regions_ensured( + sled_agent: &SledAgent, + test: &DiskTest, + ) -> bool { + for zpool in &test.zpools { + for dataset in &zpool.datasets { + let crucible_dataset = + sled_agent.get_crucible_dataset(zpool.id, dataset.id).await; + if !crucible_dataset.is_empty().await { + return false; + } + } + } + true + } + + #[nexus_test(server = crate::Server)] + async fn test_action_failure_can_unwind( + cptestctx: &ControlPlaneTestContext, + ) { + let test = DiskTest::new(cptestctx).await; + let log = &cptestctx.logctx.log; + + let client = &cptestctx.external_client; + let nexus = &cptestctx.server.apictx.nexus; + let project_id = create_org_and_project(&client).await; + + // Build the saga DAG with the provided test parameters + let opctx = test_opctx(cptestctx); + + let params = new_test_params(&opctx, project_id); + let dag = create_saga_dag::(params).unwrap(); + + for node in dag.get_nodes() { + // Create a new saga for this node. + info!( + log, + "Creating new saga which will fail at index {:?}", node.index(); + "node_name" => node.name().as_ref(), + "label" => node.label(), + ); + let runnable_saga = + nexus.create_runnable_saga(dag.clone()).await.unwrap(); + + // Inject an error instead of running the node. + // + // This should cause the saga to unwind. + nexus + .sec() + .saga_inject_error(runnable_saga.id(), node.index()) + .await + .unwrap(); + nexus + .run_saga(runnable_saga) + .await + .expect_err("Saga should have failed"); + + let datastore = nexus.datastore(); + + // Check that no partial artifacts of disk creation exist: + assert!(no_disk_records_exist(datastore).await); + assert!(no_region_allocations_exist(datastore, &test).await); + assert!( + no_regions_ensured(&cptestctx.sled_agent.sled_agent, &test) + .await + ); + } + } + + // TODO: We still need to test: + // - Can we repeat each action safely, without failing / leaving detritus? + // - Can we repeat each undo action safely? + // - Is each node atomic? (This seems harder to test) +} diff --git a/nexus/src/app/vpc_subnet.rs b/nexus/src/app/vpc_subnet.rs index a162377b260..144e8abf793 100644 --- a/nexus/src/app/vpc_subnet.rs +++ b/nexus/src/app/vpc_subnet.rs @@ -13,7 +13,6 @@ use crate::db::model::Name; use crate::db::model::VpcSubnet; use crate::db::queries::vpc_subnet::SubnetError; use crate::external_api::params; -use nexus_defaults as defaults; use omicron_common::api::external; use omicron_common::api::external::CreateResult; use omicron_common::api::external::DataPageParams; @@ -21,6 +20,7 @@ use omicron_common::api::external::DeleteResult; use omicron_common::api::external::ListResultVec; use omicron_common::api::external::LookupResult; use omicron_common::api::external::UpdateResult; +use omicron_common::nexus_config::MIN_VPC_IPV4_SUBNET_PREFIX; use uuid::Uuid; impl super::Nexus { @@ -48,7 +48,7 @@ impl super::Nexus { "VPC Subnet IPv4 address ranges must be from a private range", )); } - if params.ipv4_block.prefix() < defaults::MIN_VPC_IPV4_SUBNET_PREFIX + if params.ipv4_block.prefix() < MIN_VPC_IPV4_SUBNET_PREFIX || params.ipv4_block.prefix() > self.tunables.max_vpc_ipv4_subnet_prefix { @@ -57,7 +57,7 @@ impl super::Nexus { "VPC Subnet IPv4 address ranges must have prefix ", "length between {} and {}, inclusive" ), - defaults::MIN_VPC_IPV4_SUBNET_PREFIX, + MIN_VPC_IPV4_SUBNET_PREFIX, self.tunables.max_vpc_ipv4_subnet_prefix, ))); } diff --git a/nexus/src/config.rs b/nexus/src/config.rs index d622368ef1b..dc265d819c3 100644 --- a/nexus/src/config.rs +++ b/nexus/src/config.rs @@ -5,585 +5,10 @@ //! Interfaces for parsing configuration files and working with a nexus server //! configuration -use anyhow::anyhow; -use dropshot::ConfigLogging; -use omicron_common::nexus_config::{ - DeploymentConfig, InvalidTunable, LoadError, -}; -use serde::Deserialize; -use serde::Serialize; -use serde_with::DeserializeFromStr; -use serde_with::SerializeDisplay; -use std::net::SocketAddr; -use std::path::{Path, PathBuf}; +// TODO: Use them directly? No need for this file -// By design, we require that all config properties be specified (i.e., we don't -// use `serde(default)`). - -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] -pub struct AuthnConfig { - /// allowed authentication schemes for external HTTP server - pub schemes_external: Vec, -} - -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] -pub struct ConsoleConfig { - pub static_dir: PathBuf, - /// how long the browser can cache static assets - pub cache_control_max_age_minutes: u32, - /// how long a session can be idle before expiring - pub session_idle_timeout_minutes: u32, - /// how long a session can exist before expiring - pub session_absolute_timeout_minutes: u32, -} - -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] -pub struct UpdatesConfig { - /// Trusted root.json role for the TUF updates repository. - pub trusted_root: PathBuf, - /// Default base URL for the TUF repository. - pub default_base_url: String, -} - -/// Optional configuration for the timeseries database. -#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] -pub struct TimeseriesDbConfig { - #[serde(default, skip_serializing_if = "Option::is_none")] - pub address: Option, -} - -// A deserializable type that does no validation on the tunable parameters. -#[derive(Clone, Debug, Deserialize, PartialEq)] -struct UnvalidatedTunables { - max_vpc_ipv4_subnet_prefix: u8, -} - -/// Tunable configuration parameters, intended for use in test environments or -/// other situations in which experimentation / tuning is valuable. -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] -#[serde(try_from = "UnvalidatedTunables")] -pub struct Tunables { - /// The maximum prefix size supported for VPC Subnet IPv4 subnetworks. - /// - /// Note that this is the maximum _prefix_ size, which sets the minimum size - /// of the subnet. - pub max_vpc_ipv4_subnet_prefix: u8, -} - -// Convert from the unvalidated tunables, verifying each parameter as needed. -impl TryFrom for Tunables { - type Error = InvalidTunable; - - fn try_from(unvalidated: UnvalidatedTunables) -> Result { - Tunables::validate_ipv4_prefix(unvalidated.max_vpc_ipv4_subnet_prefix)?; - Ok(Tunables { - max_vpc_ipv4_subnet_prefix: unvalidated.max_vpc_ipv4_subnet_prefix, - }) - } -} - -impl Tunables { - fn validate_ipv4_prefix(prefix: u8) -> Result<(), InvalidTunable> { - let absolute_max: u8 = 32_u8 - .checked_sub( - // Always need space for the reserved Oxide addresses, including the - // broadcast address at the end of the subnet. - ((nexus_defaults::NUM_INITIAL_RESERVED_IP_ADDRESSES + 1) as f32) - .log2() // Subnet size to bit prefix. - .ceil() // Round up to a whole number of bits. - as u8, - ) - .expect("Invalid absolute maximum IPv4 subnet prefix"); - if prefix >= nexus_defaults::MIN_VPC_IPV4_SUBNET_PREFIX - && prefix <= absolute_max - { - Ok(()) - } else { - Err(InvalidTunable { - tunable: String::from("max_vpc_ipv4_subnet_prefix"), - message: format!( - "IPv4 subnet prefix must be in the range [0, {}], found: {}", - absolute_max, - prefix, - ), - }) - } - } -} - -/// The maximum prefix size by default. -/// -/// There are 6 Oxide reserved IP addresses, 5 at the beginning for DNS and the -/// like, and the broadcast address at the end of the subnet. This size provides -/// room for 2 ** 6 - 6 = 58 IP addresses, which seems like a reasonable size -/// for the smallest subnet that's still useful in many contexts. -pub const MAX_VPC_IPV4_SUBNET_PREFIX: u8 = 26; - -impl Default for Tunables { - fn default() -> Self { - Tunables { max_vpc_ipv4_subnet_prefix: MAX_VPC_IPV4_SUBNET_PREFIX } - } -} - -/// Configuration for a nexus server -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] -pub struct PackageConfig { - /// Console-related tunables - pub console: ConsoleConfig, - /// Server-wide logging configuration. - pub log: ConfigLogging, - /// Authentication-related configuration - pub authn: AuthnConfig, - /// Timeseries database configuration. - #[serde(default)] - pub timeseries_db: TimeseriesDbConfig, - /// Updates-related configuration. Updates APIs return 400 Bad Request when this is - /// unconfigured. - #[serde(default)] - pub updates: Option, - /// Tunable configuration for testing and experimentation - #[serde(default)] - pub tunables: Tunables, -} - -#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] -pub struct Config { - /// Configuration parameters known at compile-time. - #[serde(flatten)] - pub pkg: PackageConfig, - - /// A variety of configuration parameters only known at deployment time. - pub deployment: DeploymentConfig, -} - -impl Config { - /// Load a `Config` from the given TOML file - /// - /// This config object can then be used to create a new `Nexus`. - /// The format is described in the README. - pub fn from_file>(path: P) -> Result { - let path = path.as_ref(); - let file_contents = std::fs::read_to_string(path) - .map_err(|e| (path.to_path_buf(), e))?; - let config_parsed: Self = toml::from_str(&file_contents) - .map_err(|e| (path.to_path_buf(), e))?; - Ok(config_parsed) - } -} - -/// List of supported external authn schemes -/// -/// Note that the authn subsystem doesn't know about this type. It allows -/// schemes to be called whatever they want. This is just to provide a set of -/// allowed values for configuration. -#[derive( - Clone, Copy, Debug, DeserializeFromStr, Eq, PartialEq, SerializeDisplay, -)] -pub enum SchemeName { - Spoof, - SessionCookie, - AccessToken, -} - -impl std::str::FromStr for SchemeName { - type Err = anyhow::Error; - - fn from_str(s: &str) -> Result { - match s { - "spoof" => Ok(SchemeName::Spoof), - "session_cookie" => Ok(SchemeName::SessionCookie), - "access_token" => Ok(SchemeName::AccessToken), - _ => Err(anyhow!("unsupported authn scheme: {:?}", s)), - } - } -} - -impl std::fmt::Display for SchemeName { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(match self { - SchemeName::Spoof => "spoof", - SchemeName::SessionCookie => "session_cookie", - SchemeName::AccessToken => "access_token", - }) - } -} - -#[cfg(test)] -mod test { - use super::Tunables; - use super::{ - AuthnConfig, Config, ConsoleConfig, LoadError, PackageConfig, - SchemeName, TimeseriesDbConfig, UpdatesConfig, - }; - use dropshot::ConfigDropshot; - use dropshot::ConfigLogging; - use dropshot::ConfigLoggingIfExists; - use dropshot::ConfigLoggingLevel; - use libc; - use omicron_common::address::{Ipv6Subnet, RACK_PREFIX}; - use omicron_common::nexus_config::{ - Database, DeploymentConfig, LoadErrorKind, - }; - use std::fs; - use std::net::{Ipv6Addr, SocketAddr}; - use std::path::Path; - use std::path::PathBuf; - - /// Generates a temporary filesystem path unique for the given label. - fn temp_path(label: &str) -> PathBuf { - let arg0str = std::env::args().next().expect("expected process arg0"); - let arg0 = Path::new(&arg0str) - .file_name() - .expect("expected arg0 filename") - .to_str() - .expect("expected arg0 filename to be valid Unicode"); - let pid = std::process::id(); - let mut pathbuf = std::env::temp_dir(); - pathbuf.push(format!("{}.{}.{}", arg0, pid, label)); - pathbuf - } - - /// Load a Config with the given string `contents`. To exercise - /// the full path, this function writes the contents to a file first, then - /// loads the config from that file, then removes the file. `label` is used - /// as a unique string for the filename and error messages. It should be - /// unique for each test. - fn read_config(label: &str, contents: &str) -> Result { - let pathbuf = temp_path(label); - let path = pathbuf.as_path(); - eprintln!("writing test config {}", path.display()); - fs::write(path, contents).expect("write to tempfile failed"); - - let result = Config::from_file(path); - fs::remove_file(path).expect("failed to remove temporary file"); - eprintln!("{:?}", result); - result - } - - // Totally bogus config files (nonexistent, bad TOML syntax) - - #[test] - fn test_config_nonexistent() { - let error = Config::from_file(Path::new("/nonexistent")) - .expect_err("expected config to fail from /nonexistent"); - let expected = std::io::Error::from_raw_os_error(libc::ENOENT); - assert_eq!(error, expected); - } - - #[test] - fn test_config_bad_toml() { - let error = - read_config("bad_toml", "foo =").expect_err("expected failure"); - if let LoadErrorKind::Parse(error) = &error.kind { - assert_eq!(error.line_col(), Some((0, 5))); - assert_eq!( - error.to_string(), - "unexpected eof encountered at line 1 column 6" - ); - } else { - panic!( - "Got an unexpected error, expected Parse but got {:?}", - error - ); - } - } - - // Empty config (special case of a missing required field, but worth calling - // out explicitly) - - #[test] - fn test_config_empty() { - let error = read_config("empty", "").expect_err("expected failure"); - if let LoadErrorKind::Parse(error) = &error.kind { - assert_eq!(error.line_col(), None); - assert_eq!(error.to_string(), "missing field `deployment`"); - } else { - panic!( - "Got an unexpected error, expected Parse but got {:?}", - error - ); - } - } - - // Success case. We don't need to retest semantics for either ConfigLogging - // or ConfigDropshot because those are both tested within Dropshot. If we - // add new configuration sections of our own, we will want to test those - // here (both syntax and semantics). - #[test] - fn test_valid() { - let config = read_config( - "valid", - r##" - [console] - static_dir = "tests/static" - cache_control_max_age_minutes = 10 - session_idle_timeout_minutes = 60 - session_absolute_timeout_minutes = 480 - [authn] - schemes_external = [] - [log] - mode = "file" - level = "debug" - path = "/nonexistent/path" - if_exists = "fail" - [timeseries_db] - address = "[::1]:8123" - [updates] - trusted_root = "/path/to/root.json" - default_base_url = "http://example.invalid/" - [tunables] - max_vpc_ipv4_subnet_prefix = 27 - [deployment] - id = "28b90dc4-c22a-65ba-f49a-f051fe01208f" - rack_id = "38b90dc4-c22a-65ba-f49a-f051fe01208f" - [[deployment.dropshot_external]] - bind_address = "10.1.2.3:4567" - request_body_max_bytes = 1024 - [deployment.dropshot_internal] - bind_address = "10.1.2.3:4568" - request_body_max_bytes = 1024 - [deployment.subnet] - net = "::/56" - [deployment.database] - type = "from_dns" - "##, - ) - .unwrap(); - - assert_eq!( - config, - Config { - deployment: DeploymentConfig { - id: "28b90dc4-c22a-65ba-f49a-f051fe01208f".parse().unwrap(), - rack_id: "38b90dc4-c22a-65ba-f49a-f051fe01208f" - .parse() - .unwrap(), - dropshot_external: vec![ConfigDropshot { - bind_address: "10.1.2.3:4567" - .parse::() - .unwrap(), - ..Default::default() - },], - dropshot_internal: ConfigDropshot { - bind_address: "10.1.2.3:4568" - .parse::() - .unwrap(), - ..Default::default() - }, - subnet: Ipv6Subnet::::new(Ipv6Addr::LOCALHOST), - database: Database::FromDns, - }, - pkg: PackageConfig { - console: ConsoleConfig { - static_dir: "tests/static".parse().unwrap(), - cache_control_max_age_minutes: 10, - session_idle_timeout_minutes: 60, - session_absolute_timeout_minutes: 480 - }, - authn: AuthnConfig { schemes_external: Vec::new() }, - log: ConfigLogging::File { - level: ConfigLoggingLevel::Debug, - if_exists: ConfigLoggingIfExists::Fail, - path: "/nonexistent/path".to_string() - }, - timeseries_db: TimeseriesDbConfig { - address: Some("[::1]:8123".parse().unwrap()) - }, - updates: Some(UpdatesConfig { - trusted_root: PathBuf::from("/path/to/root.json"), - default_base_url: "http://example.invalid/".into(), - }), - tunables: Tunables { max_vpc_ipv4_subnet_prefix: 27 }, - }, - } - ); - - let config = read_config( - "valid", - r##" - [console] - static_dir = "tests/static" - cache_control_max_age_minutes = 10 - session_idle_timeout_minutes = 60 - session_absolute_timeout_minutes = 480 - [authn] - schemes_external = [ "spoof", "session_cookie" ] - [log] - mode = "file" - level = "debug" - path = "/nonexistent/path" - if_exists = "fail" - [timeseries_db] - address = "[::1]:8123" - [deployment] - id = "28b90dc4-c22a-65ba-f49a-f051fe01208f" - rack_id = "38b90dc4-c22a-65ba-f49a-f051fe01208f" - [[deployment.dropshot_external]] - bind_address = "10.1.2.3:4567" - request_body_max_bytes = 1024 - [deployment.dropshot_internal] - bind_address = "10.1.2.3:4568" - request_body_max_bytes = 1024 - [deployment.subnet] - net = "::/56" - [deployment.database] - type = "from_dns" - "##, - ) - .unwrap(); - - assert_eq!( - config.pkg.authn.schemes_external, - vec![SchemeName::Spoof, SchemeName::SessionCookie], - ); - } - - #[test] - fn test_bad_authn_schemes() { - let error = read_config( - "bad authn.schemes_external", - r##" - [console] - static_dir = "tests/static" - cache_control_max_age_minutes = 10 - session_idle_timeout_minutes = 60 - session_absolute_timeout_minutes = 480 - [authn] - schemes_external = ["trust-me"] - [log] - mode = "file" - level = "debug" - path = "/nonexistent/path" - if_exists = "fail" - [timeseries_db] - address = "[::1]:8123" - [deployment] - id = "28b90dc4-c22a-65ba-f49a-f051fe01208f" - rack_id = "38b90dc4-c22a-65ba-f49a-f051fe01208f" - [[deployment.dropshot_external]] - bind_address = "10.1.2.3:4567" - request_body_max_bytes = 1024 - [deployment.dropshot_internal] - bind_address = "10.1.2.3:4568" - request_body_max_bytes = 1024 - [deployment.subnet] - net = "::/56" - [deployment.database] - type = "from_dns" - "##, - ) - .expect_err("expected failure"); - if let LoadErrorKind::Parse(error) = &error.kind { - assert!( - error - .to_string() - .starts_with("unsupported authn scheme: \"trust-me\""), - "error = {}", - error.to_string() - ); - } else { - panic!( - "Got an unexpected error, expected Parse but got {:?}", - error - ); - } - } - - #[test] - fn test_invalid_ipv4_prefix_tunable() { - let error = read_config( - "invalid_ipv4_prefix_tunable", - r##" - [console] - static_dir = "tests/static" - cache_control_max_age_minutes = 10 - session_idle_timeout_minutes = 60 - session_absolute_timeout_minutes = 480 - [authn] - schemes_external = [] - [log] - mode = "file" - level = "debug" - path = "/nonexistent/path" - if_exists = "fail" - [timeseries_db] - address = "[::1]:8123" - [updates] - trusted_root = "/path/to/root.json" - default_base_url = "http://example.invalid/" - [tunables] - max_vpc_ipv4_subnet_prefix = 100 - [deployment] - id = "28b90dc4-c22a-65ba-f49a-f051fe01208f" - rack_id = "38b90dc4-c22a-65ba-f49a-f051fe01208f" - [[deployment.dropshot_external]] - bind_address = "10.1.2.3:4567" - request_body_max_bytes = 1024 - [deployment.dropshot_internal] - bind_address = "10.1.2.3:4568" - request_body_max_bytes = 1024 - [deployment.subnet] - net = "::/56" - [deployment.database] - type = "from_dns" - "##, - ) - .expect_err("Expected failure"); - if let LoadErrorKind::Parse(error) = &error.kind { - assert!(error.to_string().starts_with( - r#"invalid "max_vpc_ipv4_subnet_prefix": "IPv4 subnet prefix must"#, - )); - } else { - panic!( - "Got an unexpected error, expected Parse but got {:?}", - error - ); - } - } - - #[test] - fn test_repo_configs_are_valid() { - // The example config file should be valid. - let config_path = "examples/config.toml"; - println!("checking {:?}", config_path); - let example_config = Config::from_file(config_path) - .expect("example config file is not valid"); - - // The config file used for the tests should also be valid. The tests - // won't clear the runway anyway if this file isn't valid. But it's - // helpful to verify this here explicitly as well. - let config_path = "examples/config.toml"; - println!("checking {:?}", config_path); - let _ = Config::from_file(config_path) - .expect("test config file is not valid"); - - // The partial config file that's used to deploy Nexus must also be - // valid. However, it's missing the "deployment" section because that's - // generated at deployment time. We'll serialize this section from the - // example config file (loaded above), append it to the contents of this - // file, and verify the whole thing. - #[derive(serde::Serialize)] - struct DummyConfig { - deployment: DeploymentConfig, - } - let config_path = "../smf/nexus/config-partial.toml"; - println!( - "checking {:?} with example deployment section added", - config_path - ); - let mut contents = std::fs::read_to_string(config_path) - .expect("failed to read Nexus SMF config file"); - contents.push_str( - "\n\n\n \ - # !! content below added by test_repo_configs_are_valid()\n\ - \n\n\n", - ); - let example_deployment = toml::to_string_pretty(&DummyConfig { - deployment: example_config.deployment, - }) - .unwrap(); - contents.push_str(&example_deployment); - let _: Config = toml::from_str(&contents) - .expect("Nexus SMF config file is not valid"); - } -} +pub use omicron_common::nexus_config::Config; +pub use omicron_common::nexus_config::PackageConfig; +pub use omicron_common::nexus_config::SchemeName; +pub use omicron_common::nexus_config::Tunables; +pub use omicron_common::nexus_config::UpdatesConfig; diff --git a/nexus/src/db/datastore/mod.rs b/nexus/src/db/datastore/mod.rs index b096e9fd658..f636e510ddb 100644 --- a/nexus/src/db/datastore/mod.rs +++ b/nexus/src/db/datastore/mod.rs @@ -130,6 +130,13 @@ impl DataStore { Ok(self.pool.pool()) } + #[cfg(test)] + pub async fn pool_for_tests( + &self, + ) -> Result<&bb8::Pool>, Error> { + Ok(self.pool.pool()) + } + /// Return the next available IPv6 address for an Oxide service running on /// the provided sled. pub async fn next_ipv6_address( diff --git a/nexus/src/db/queries/network_interface.rs b/nexus/src/db/queries/network_interface.rs index fdae2f5e84f..825d3068494 100644 --- a/nexus/src/db/queries/network_interface.rs +++ b/nexus/src/db/queries/network_interface.rs @@ -25,8 +25,8 @@ use diesel::QueryResult; use diesel::RunQueryDsl; use ipnetwork::IpNetwork; use ipnetwork::Ipv4Network; -use nexus_defaults::NUM_INITIAL_RESERVED_IP_ADDRESSES; use omicron_common::api::external; +use omicron_common::nexus_config::NUM_INITIAL_RESERVED_IP_ADDRESSES; use std::net::IpAddr; use uuid::Uuid; diff --git a/nexus/src/lib.rs b/nexus/src/lib.rs index a29c02cfb14..ef29550f7da 100644 --- a/nexus/src/lib.rs +++ b/nexus/src/lib.rs @@ -27,12 +27,13 @@ pub mod updates; // public for testing pub use app::test_interfaces::TestInterfaces; pub use app::Nexus; -pub use config::{Config, PackageConfig}; +pub use config::Config; pub use context::ServerContext; pub use crucible_agent_client; use external_api::http_entrypoints::external_api; use internal_api::http_entrypoints::internal_api; use slog::Logger; +use std::net::SocketAddr; use std::sync::Arc; #[macro_use] @@ -161,6 +162,33 @@ impl Server { } } +#[async_trait::async_trait] +impl nexus_test_interface::NexusServer for Server { + async fn start_and_populate(config: &Config, log: &Logger) -> Self { + let server = Server::start(config, log).await.unwrap(); + server.apictx.nexus.wait_for_populate().await.unwrap(); + server + } + + fn get_http_servers_external(&self) -> Vec { + self.http_servers_external + .iter() + .map(|server| server.local_addr()) + .collect() + } + + fn get_http_server_internal(&self) -> SocketAddr { + self.http_server_internal.local_addr() + } + + async fn close(mut self) { + for server in self.http_servers_external { + server.close().await.unwrap(); + } + self.http_server_internal.close().await.unwrap(); + } +} + /// Run an instance of the [Server]. pub async fn run_server(config: &Config) -> Result<(), String> { use slog::Drain; diff --git a/nexus/test-interface/Cargo.toml b/nexus/test-interface/Cargo.toml new file mode 100644 index 00000000000..1df5091c5ec --- /dev/null +++ b/nexus/test-interface/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "nexus-test-interface" +version = "0.1.0" +edition = "2021" +license = "MPL-2.0" + +[dependencies] +async-trait = "0.1.56" +dropshot = { git = "https://github.com/oxidecomputer/dropshot", branch = "main" , features = [ "usdt-probes" ] } +omicron-common = { path = "../../common" } +slog = { version = "2.7" } diff --git a/nexus/test-interface/src/lib.rs b/nexus/test-interface/src/lib.rs new file mode 100644 index 00000000000..c130dde0802 --- /dev/null +++ b/nexus/test-interface/src/lib.rs @@ -0,0 +1,46 @@ +// 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/. + +//! Interfaces for Nexus under test. +//! +//! By splitting these interfaces into a new crate, we can avoid a circular +//! dependency on Nexus during testing. +//! +//! Both Nexus unit tests and Integration tests want to able to share +//! utilities for launching Nexus, a multi-service setup process that exists in +//! `nexus-test-utils`. +//! +//! Without a separate test interface crate, this dependency looks like the +//! following (note: "->" means "depends on") +//! +//! - nexus -> nexus-test-utils +//! - nexus-test-utils -> nexus +//! - integration tests -> nexus +//! - integration tests -> nexus-test-utils +//! +//! As we can see, this introduces a circular dependency between +//! `nexus-test-utils` and `nexus`. +//! +//! However, by separating out the portion of `nexus` used by `nexus-test-utils` +//! into a separate trait, we can break the circular dependency: +//! +//! - nexus -> nexus-test-interface +//! - nexus -> nexus-test-utils +//! - nexus-test-utils -> nexus-test-interface +//! - integration tests -> nexus +//! - integration tests -> nexus-test-utils + +use async_trait::async_trait; +use omicron_common::nexus_config::Config; +use slog::Logger; +use std::net::SocketAddr; + +#[async_trait] +pub trait NexusServer { + async fn start_and_populate(config: &Config, log: &Logger) -> Self; + + fn get_http_servers_external(&self) -> Vec; + fn get_http_server_internal(&self) -> SocketAddr; + async fn close(self); +} diff --git a/nexus/test-utils-macros/Cargo.toml b/nexus/test-utils-macros/Cargo.toml index 3fceb1b8339..08609f68db2 100644 --- a/nexus/test-utils-macros/Cargo.toml +++ b/nexus/test-utils-macros/Cargo.toml @@ -7,5 +7,6 @@ edition = "2021" proc-macro = true [dependencies] +proc-macro2 = { version = "1.0" } quote = "1.0" syn = { version="1.0", features=["full", "fold", "parsing"] } diff --git a/nexus/test-utils-macros/src/lib.rs b/nexus/test-utils-macros/src/lib.rs index 451e65867a9..ac217686414 100644 --- a/nexus/test-utils-macros/src/lib.rs +++ b/nexus/test-utils-macros/src/lib.rs @@ -1,6 +1,37 @@ use proc_macro::TokenStream; use quote::quote; -use syn::{parse_macro_input, ItemFn}; +use std::collections::HashSet as Set; +use syn::parse::{Parse, ParseStream, Result}; +use syn::punctuated::Punctuated; +use syn::{parse_macro_input, ItemFn, Token}; + +#[derive(Debug, PartialEq, Eq, Hash)] +pub(crate) struct NameValue { + name: syn::Path, + _eq_token: syn::token::Eq, + value: syn::Path, +} + +impl syn::parse::Parse for NameValue { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + Ok(Self { + name: input.parse()?, + _eq_token: input.parse()?, + value: input.parse()?, + }) + } +} + +struct Args { + vars: Set, +} + +impl Parse for Args { + fn parse(input: ParseStream) -> Result { + let vars = Punctuated::::parse_terminated(input)?; + Ok(Args { vars: vars.into_iter().collect() }) + } +} /// Attribute for wrapping a test function to handle automatically /// creating and destroying a ControlPlaneTestContext. If the wrapped test @@ -10,6 +41,8 @@ use syn::{parse_macro_input, ItemFn}; /// Example usage: /// /// ```ignore +/// use ControlPlaneTestContext = +/// nexus_test_utils::ControlPlaneTestContext; /// #[nexus_test] /// async fn test_my_test_case(cptestctx: &ControlPlaneTestContext) { /// assert!(true); @@ -20,7 +53,7 @@ use syn::{parse_macro_input, ItemFn}; /// we want the teardown to only happen when the test doesn't fail (which causes /// a panic and unwind). #[proc_macro_attribute] -pub fn nexus_test(_metadata: TokenStream, input: TokenStream) -> TokenStream { +pub fn nexus_test(attrs: TokenStream, input: TokenStream) -> TokenStream { let input_func = parse_macro_input!(input as ItemFn); let mut correct_signature = true; @@ -31,6 +64,25 @@ pub fn nexus_test(_metadata: TokenStream, input: TokenStream) -> TokenStream { correct_signature = false; } + // By default, import "omicron_nexus::Server" as the server under test. + // + // However, a caller can supply their own implementation of the server + // using: + // + // #[nexus_test(server = )] + // + // This mechanism allows Nexus unit test to be tested using the `nexus_test` + // macro without a circular dependency on nexus-test-utils. + let attrs = parse_macro_input!(attrs as Args); + let which_nexus = attrs + .vars + .iter() + .find(|nv| nv.name.is_ident("server")) + .map(|nv| nv.value.clone()) + .unwrap_or_else(|| { + syn::parse_str::("::omicron_nexus::Server").unwrap() + }); + // Verify we're returning an empty tuple correct_signature &= match input_func.sig.output { syn::ReturnType::Default => true, @@ -52,7 +104,7 @@ pub fn nexus_test(_metadata: TokenStream, input: TokenStream) -> TokenStream { { #input_func - let ctx = ::nexus_test_utils::test_setup(#func_ident_string).await; + let ctx = ::nexus_test_utils::test_setup::<#which_nexus>(#func_ident_string).await; #func_ident(&ctx).await; ctx.teardown().await; } diff --git a/nexus/test-utils/Cargo.toml b/nexus/test-utils/Cargo.toml index 584cc4d7f43..e9ccaca801c 100644 --- a/nexus/test-utils/Cargo.toml +++ b/nexus/test-utils/Cargo.toml @@ -12,6 +12,7 @@ dropshot = { git = "https://github.com/oxidecomputer/dropshot", branch = "main", headers = "0.3.8" http = "0.2.7" hyper = "0.14" +nexus-test-interface = { path = "../test-interface" } omicron-common = { path = "../../common" } omicron-nexus = { path = ".." } omicron-sled-agent = { path = "../../sled-agent" } diff --git a/nexus/test-utils/src/lib.rs b/nexus/test-utils/src/lib.rs index 82b01c3a0ff..92227506bd8 100644 --- a/nexus/test-utils/src/lib.rs +++ b/nexus/test-utils/src/lib.rs @@ -9,6 +9,7 @@ use dropshot::test_util::LogContext; use dropshot::ConfigDropshot; use dropshot::ConfigLogging; use dropshot::ConfigLoggingLevel; +use nexus_test_interface::NexusServer; use omicron_common::api::external::IdentityMetadata; use omicron_common::api::internal::nexus::ProducerEndpoint; use omicron_common::nexus_config; @@ -32,10 +33,10 @@ pub const RACK_UUID: &str = "c19a698f-c6f9-4a17-ae30-20d711b8f7dc"; pub const OXIMETER_UUID: &str = "39e6175b-4df2-4730-b11d-cbc1e60a2e78"; pub const PRODUCER_UUID: &str = "a6458b7d-87c3-4483-be96-854d814c20de"; -pub struct ControlPlaneTestContext { +pub struct ControlPlaneTestContext { pub external_client: ClientTestContext, pub internal_client: ClientTestContext, - pub server: omicron_nexus::Server, + pub server: N, pub database: dev::db::CockroachInstance, pub clickhouse: dev::clickhouse::ClickHouseInstance, pub logctx: LogContext, @@ -44,12 +45,9 @@ pub struct ControlPlaneTestContext { pub producer: ProducerServer, } -impl ControlPlaneTestContext { +impl ControlPlaneTestContext { pub async fn teardown(mut self) { - for server in self.server.http_servers_external { - server.close().await.unwrap(); - } - self.server.http_server_internal.close().await.unwrap(); + self.server.close().await; self.database.cleanup().await.unwrap(); self.clickhouse.cleanup().await.unwrap(); self.sled_agent.http_server.close().await.unwrap(); @@ -59,7 +57,7 @@ impl ControlPlaneTestContext { } } -pub fn load_test_config() -> omicron_nexus::Config { +pub fn load_test_config() -> omicron_common::nexus_config::Config { // We load as much configuration as we can from the test suite configuration // file. In practice, TestContext requires that: // @@ -76,21 +74,24 @@ pub fn load_test_config() -> omicron_nexus::Config { // configuration options, we expect many of those can be usefully configured // (and reconfigured) for the test suite. let config_file_path = Path::new("tests/config.test.toml"); - let mut config = omicron_nexus::Config::from_file(config_file_path) - .expect("failed to load config.test.toml"); + let mut config = + omicron_common::nexus_config::Config::from_file(config_file_path) + .expect("failed to load config.test.toml"); config.deployment.id = Uuid::new_v4(); config } -pub async fn test_setup(test_name: &str) -> ControlPlaneTestContext { +pub async fn test_setup( + test_name: &str, +) -> ControlPlaneTestContext { let mut config = load_test_config(); - test_setup_with_config(test_name, &mut config).await + test_setup_with_config::(test_name, &mut config).await } -pub async fn test_setup_with_config( +pub async fn test_setup_with_config( test_name: &str, - config: &mut omicron_nexus::Config, -) -> ControlPlaneTestContext { + config: &mut omicron_common::nexus_config::Config, +) -> ControlPlaneTestContext { let logctx = LogContext::new(test_name, &config.pkg.log); let log = &logctx.log; @@ -111,21 +112,14 @@ pub async fn test_setup_with_config( .expect("Tests expect to set a port of Clickhouse") .set_port(clickhouse.port()); - let server = - omicron_nexus::Server::start(&config, &logctx.log).await.unwrap(); - server - .apictx - .nexus - .wait_for_populate() - .await - .expect("Nexus never loaded users"); + let server = N::start_and_populate(&config, &logctx.log).await; let testctx_external = ClientTestContext::new( - server.http_servers_external[0].local_addr(), + server.get_http_servers_external()[0], logctx.log.new(o!("component" => "external client test context")), ); let testctx_internal = ClientTestContext::new( - server.http_server_internal.local_addr(), + server.get_http_server_internal(), logctx.log.new(o!("component" => "internal client test context")), ); @@ -136,7 +130,7 @@ pub async fn test_setup_with_config( "component" => "omicron_sled_agent::sim::Server", "sled_id" => sa_id.to_string(), )), - server.http_server_internal.local_addr(), + server.get_http_server_internal(), sa_id, ) .await @@ -146,7 +140,7 @@ pub async fn test_setup_with_config( let collector_id = Uuid::parse_str(OXIMETER_UUID).unwrap(); let oximeter = start_oximeter( log.new(o!("component" => "oximeter")), - server.http_server_internal.local_addr(), + server.get_http_server_internal(), clickhouse.port(), collector_id, ) @@ -155,12 +149,10 @@ pub async fn test_setup_with_config( // Set up a test metric producer server let producer_id = Uuid::parse_str(PRODUCER_UUID).unwrap(); - let producer = start_producer_server( - server.http_server_internal.local_addr(), - producer_id, - ) - .await - .unwrap(); + let producer = + start_producer_server(server.get_http_server_internal(), producer_id) + .await + .unwrap(); register_test_producer(&producer).unwrap(); ControlPlaneTestContext { diff --git a/nexus/test-utils/src/resource_helpers.rs b/nexus/test-utils/src/resource_helpers.rs index f847dd7ae3f..ecccbe03fc0 100644 --- a/nexus/test-utils/src/resource_helpers.rs +++ b/nexus/test-utils/src/resource_helpers.rs @@ -434,7 +434,7 @@ impl DiskTest { pub const DEFAULT_ZPOOL_SIZE_GIB: u32 = 10; // Creates fake physical storage, an organization, and a project. - pub async fn new(cptestctx: &ControlPlaneTestContext) -> Self { + pub async fn new(cptestctx: &ControlPlaneTestContext) -> Self { let sled_agent = cptestctx.sled_agent.sled_agent.clone(); let mut disk_test = Self { sled_agent, zpools: vec![] }; diff --git a/nexus/tests/integration_tests/authz.rs b/nexus/tests/integration_tests/authz.rs index 4621da9551c..bdd4ef16c4d 100644 --- a/nexus/tests/integration_tests/authz.rs +++ b/nexus/tests/integration_tests/authz.rs @@ -5,7 +5,6 @@ //! Tests for authz policy not covered in the set of unauthorized tests use nexus_test_utils::http_testing::{AuthnMode, NexusRequest, RequestBuilder}; -use nexus_test_utils::ControlPlaneTestContext; use nexus_test_utils_macros::nexus_test; use omicron_common::api::external::IdentityMetadataCreateParams; @@ -20,6 +19,9 @@ use httptest::{matchers::*, responders::*, Expectation, ServerBuilder}; use uuid::Uuid; +type ControlPlaneTestContext = + nexus_test_utils::ControlPlaneTestContext; + // Test that a user cannot read other user's SSH keys #[nexus_test] async fn test_cannot_read_others_ssh_keys(cptestctx: &ControlPlaneTestContext) { diff --git a/nexus/tests/integration_tests/basic.rs b/nexus/tests/integration_tests/basic.rs index 0f136dc42e4..5a3ca2c0024 100644 --- a/nexus/tests/integration_tests/basic.rs +++ b/nexus/tests/integration_tests/basic.rs @@ -29,9 +29,11 @@ use nexus_test_utils::http_testing::RequestBuilder; use nexus_test_utils::resource_helpers::create_organization; use nexus_test_utils::resource_helpers::create_project; use nexus_test_utils::start_sled_agent; -use nexus_test_utils::ControlPlaneTestContext; use nexus_test_utils_macros::nexus_test; +type ControlPlaneTestContext = + nexus_test_utils::ControlPlaneTestContext; + #[nexus_test] async fn test_basic_failures(cptestctx: &ControlPlaneTestContext) { let client = &cptestctx.external_client; diff --git a/nexus/tests/integration_tests/console_api.rs b/nexus/tests/integration_tests/console_api.rs index e220d8551ed..d6f074e3e08 100644 --- a/nexus/tests/integration_tests/console_api.rs +++ b/nexus/tests/integration_tests/console_api.rs @@ -12,9 +12,7 @@ use nexus_test_utils::http_testing::{ AuthnMode, NexusRequest, RequestBuilder, TestResponse, }; use nexus_test_utils::resource_helpers::grant_iam; -use nexus_test_utils::{ - load_test_config, test_setup_with_config, ControlPlaneTestContext, -}; +use nexus_test_utils::{load_test_config, test_setup_with_config}; use nexus_test_utils_macros::nexus_test; use omicron_common::api::external::IdentityMetadataCreateParams; use omicron_nexus::authn::{USER_TEST_PRIVILEGED, USER_TEST_UNPRIVILEGED}; @@ -25,6 +23,9 @@ use omicron_nexus::external_api::console_api::SpoofLoginBody; use omicron_nexus::external_api::params::OrganizationCreate; use omicron_nexus::external_api::{shared, views}; +type ControlPlaneTestContext = + nexus_test_utils::ControlPlaneTestContext; + #[nexus_test] async fn test_sessions(cptestctx: &ControlPlaneTestContext) { let testctx = &cptestctx.external_client; @@ -304,8 +305,11 @@ async fn test_assets(cptestctx: &ControlPlaneTestContext) { async fn test_absolute_static_dir() { let mut config = load_test_config(); config.pkg.console.static_dir = current_dir().unwrap().join("tests/static"); - let cptestctx = - test_setup_with_config("test_absolute_static_dir", &mut config).await; + let cptestctx = test_setup_with_config::( + "test_absolute_static_dir", + &mut config, + ) + .await; let testctx = &cptestctx.external_client; // existing file is returned diff --git a/nexus/tests/integration_tests/datasets.rs b/nexus/tests/integration_tests/datasets.rs index 42b0d48a847..a5dcc13e06c 100644 --- a/nexus/tests/integration_tests/datasets.rs +++ b/nexus/tests/integration_tests/datasets.rs @@ -11,9 +11,12 @@ use omicron_nexus::internal_api::params::{ use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use uuid::Uuid; -use nexus_test_utils::{ControlPlaneTestContext, SLED_AGENT_UUID}; +use nexus_test_utils::SLED_AGENT_UUID; use nexus_test_utils_macros::nexus_test; +type ControlPlaneTestContext = + nexus_test_utils::ControlPlaneTestContext; + // Tests the "normal" case of dataset_put: inserting a dataset within a known // zpool. // diff --git a/nexus/tests/integration_tests/device_auth.rs b/nexus/tests/integration_tests/device_auth.rs index ca0e9da9ef2..a0de75f2e9d 100644 --- a/nexus/tests/integration_tests/device_auth.rs +++ b/nexus/tests/integration_tests/device_auth.rs @@ -3,7 +3,6 @@ // file, You can obtain one at https://mozilla.org/MPL/2.0/. use nexus_test_utils::http_testing::{AuthnMode, NexusRequest, RequestBuilder}; -use nexus_test_utils::ControlPlaneTestContext; use nexus_test_utils_macros::nexus_test; use omicron_nexus::external_api::device_auth::{ DeviceAccessTokenRequest, DeviceAuthRequest, DeviceAuthVerify, @@ -16,6 +15,9 @@ use http::{header, method::Method, StatusCode}; use serde::Deserialize; use uuid::Uuid; +type ControlPlaneTestContext = + nexus_test_utils::ControlPlaneTestContext; + #[derive(Deserialize)] struct OAuthError { error: String, diff --git a/nexus/tests/integration_tests/disks.rs b/nexus/tests/integration_tests/disks.rs index 954cd028a3c..22a96cde435 100644 --- a/nexus/tests/integration_tests/disks.rs +++ b/nexus/tests/integration_tests/disks.rs @@ -24,7 +24,6 @@ use nexus_test_utils::resource_helpers::create_organization; use nexus_test_utils::resource_helpers::create_project; use nexus_test_utils::resource_helpers::objects_list_page_authz; use nexus_test_utils::resource_helpers::DiskTest; -use nexus_test_utils::ControlPlaneTestContext; use nexus_test_utils_macros::nexus_test; use omicron_common::api::external::ByteCount; use omicron_common::api::external::Disk; @@ -41,6 +40,9 @@ use sled_agent_client::TestInterfaces as _; use std::sync::Arc; use uuid::Uuid; +type ControlPlaneTestContext = + nexus_test_utils::ControlPlaneTestContext; + const ORG_NAME: &str = "test-org"; const PROJECT_NAME: &str = "springfield-squidport-disks"; const DISK_NAME: &str = "just-rainsticks"; diff --git a/nexus/tests/integration_tests/images.rs b/nexus/tests/integration_tests/images.rs index 22b2fd9ad44..9925731a435 100644 --- a/nexus/tests/integration_tests/images.rs +++ b/nexus/tests/integration_tests/images.rs @@ -12,7 +12,6 @@ use nexus_test_utils::http_testing::RequestBuilder; use nexus_test_utils::resource_helpers::create_organization; use nexus_test_utils::resource_helpers::create_project; use nexus_test_utils::resource_helpers::DiskTest; -use nexus_test_utils::ControlPlaneTestContext; use nexus_test_utils_macros::nexus_test; use omicron_common::api::external::{ByteCount, IdentityMetadataCreateParams}; @@ -21,6 +20,9 @@ use omicron_nexus::external_api::views::GlobalImage; use httptest::{matchers::*, responders::*, Expectation, ServerBuilder}; +type ControlPlaneTestContext = + nexus_test_utils::ControlPlaneTestContext; + #[nexus_test] async fn test_global_image_create(cptestctx: &ControlPlaneTestContext) { let client = &cptestctx.external_client; diff --git a/nexus/tests/integration_tests/instances.rs b/nexus/tests/integration_tests/instances.rs index fa803e7ab26..29bc92416d8 100644 --- a/nexus/tests/integration_tests/instances.rs +++ b/nexus/tests/integration_tests/instances.rs @@ -43,9 +43,11 @@ use nexus_test_utils::identity_eq; use nexus_test_utils::resource_helpers::{ create_instance, create_organization, create_project, }; -use nexus_test_utils::ControlPlaneTestContext; use nexus_test_utils_macros::nexus_test; +type ControlPlaneTestContext = + nexus_test_utils::ControlPlaneTestContext; + static POOL_NAME: &str = "p0"; static ORGANIZATION_NAME: &str = "test-org"; static PROJECT_NAME: &str = "springfield-squidport"; diff --git a/nexus/tests/integration_tests/ip_pools.rs b/nexus/tests/integration_tests/ip_pools.rs index 0dfe0d4a65b..9bcd288d079 100644 --- a/nexus/tests/integration_tests/ip_pools.rs +++ b/nexus/tests/integration_tests/ip_pools.rs @@ -15,7 +15,6 @@ use nexus_test_utils::resource_helpers::create_instance; use nexus_test_utils::resource_helpers::create_organization; use nexus_test_utils::resource_helpers::create_project; use nexus_test_utils::resource_helpers::objects_list_page_authz; -use nexus_test_utils::ControlPlaneTestContext; use nexus_test_utils_macros::nexus_test; use omicron_common::api::external::IdentityMetadataCreateParams; use omicron_common::api::external::IdentityMetadataUpdateParams; @@ -29,6 +28,9 @@ use omicron_nexus::external_api::views::IpPoolRange; use omicron_nexus::TestInterfaces; use sled_agent_client::TestInterfaces as SledTestInterfaces; +type ControlPlaneTestContext = + nexus_test_utils::ControlPlaneTestContext; + // Basic test verifying CRUD behavior on the IP Pool itself. #[nexus_test] async fn test_ip_pool_basic_crud(cptestctx: &ControlPlaneTestContext) { diff --git a/nexus/tests/integration_tests/organizations.rs b/nexus/tests/integration_tests/organizations.rs index bd5624736e8..8cc40e3f532 100644 --- a/nexus/tests/integration_tests/organizations.rs +++ b/nexus/tests/integration_tests/organizations.rs @@ -10,9 +10,11 @@ use http::StatusCode; use nexus_test_utils::resource_helpers::{ create_organization, create_project, objects_list_page_authz, }; -use nexus_test_utils::ControlPlaneTestContext; use nexus_test_utils_macros::nexus_test; +type ControlPlaneTestContext = + nexus_test_utils::ControlPlaneTestContext; + #[nexus_test] async fn test_organizations(cptestctx: &ControlPlaneTestContext) { let client = &cptestctx.external_client; diff --git a/nexus/tests/integration_tests/oximeter.rs b/nexus/tests/integration_tests/oximeter.rs index 8455277b834..ad1844f59bb 100644 --- a/nexus/tests/integration_tests/oximeter.rs +++ b/nexus/tests/integration_tests/oximeter.rs @@ -4,7 +4,6 @@ //! Integration tests for oximeter collectors and producers. -use nexus_test_utils::ControlPlaneTestContext; use nexus_test_utils_macros::nexus_test; use omicron_test_utils::dev::poll::{wait_for_condition, CondCheckError}; use oximeter_db::DbWrite; @@ -12,6 +11,9 @@ use std::net; use std::time::Duration; use uuid::Uuid; +type ControlPlaneTestContext = + nexus_test_utils::ControlPlaneTestContext; + #[nexus_test] async fn test_oximeter_database_records(context: &ControlPlaneTestContext) { let db = &context.database; @@ -62,8 +64,10 @@ async fn test_oximeter_database_records(context: &ControlPlaneTestContext) { #[tokio::test] async fn test_oximeter_reregistration() { - let mut context = - nexus_test_utils::test_setup("test_oximeter_reregistration").await; + let mut context = nexus_test_utils::test_setup::( + "test_oximeter_reregistration", + ) + .await; let db = &context.database; let producer_id = nexus_test_utils::PRODUCER_UUID.parse().unwrap(); let oximeter_id = nexus_test_utils::OXIMETER_UUID.parse().unwrap(); diff --git a/nexus/tests/integration_tests/password_login.rs b/nexus/tests/integration_tests/password_login.rs index 82ede653afc..73f6a154610 100644 --- a/nexus/tests/integration_tests/password_login.rs +++ b/nexus/tests/integration_tests/password_login.rs @@ -8,7 +8,6 @@ use nexus_passwords::MIN_EXPECTED_PASSWORD_VERIFY_TIME; use nexus_test_utils::http_testing::{AuthnMode, NexusRequest, RequestBuilder}; use nexus_test_utils::resource_helpers::grant_iam; use nexus_test_utils::resource_helpers::{create_local_user, create_silo}; -use nexus_test_utils::ControlPlaneTestContext; use nexus_test_utils_macros::nexus_test; use omicron_common::api::external::Name; use omicron_nexus::authz::SiloRole; @@ -17,6 +16,9 @@ use omicron_nexus::external_api::shared; use omicron_nexus::external_api::views; use std::str::FromStr; +type ControlPlaneTestContext = + nexus_test_utils::ControlPlaneTestContext; + // TODO-coverage verify that deleting a Silo deletes all the users and their // password hashes diff --git a/nexus/tests/integration_tests/projects.rs b/nexus/tests/integration_tests/projects.rs index 7b230b1f73b..400ddf6751a 100644 --- a/nexus/tests/integration_tests/projects.rs +++ b/nexus/tests/integration_tests/projects.rs @@ -7,9 +7,11 @@ use nexus_test_utils::resource_helpers::project_get; use omicron_nexus::external_api::views::Project; use nexus_test_utils::resource_helpers::{create_organization, create_project}; -use nexus_test_utils::ControlPlaneTestContext; use nexus_test_utils_macros::nexus_test; +type ControlPlaneTestContext = + nexus_test_utils::ControlPlaneTestContext; + #[nexus_test] async fn test_projects(cptestctx: &ControlPlaneTestContext) { let client = &cptestctx.external_client; diff --git a/nexus/tests/integration_tests/rack.rs b/nexus/tests/integration_tests/rack.rs index 10961edc221..438ea2baa88 100644 --- a/nexus/tests/integration_tests/rack.rs +++ b/nexus/tests/integration_tests/rack.rs @@ -4,11 +4,13 @@ use nexus_test_utils::http_testing::AuthnMode; use nexus_test_utils::http_testing::NexusRequest; -use nexus_test_utils::ControlPlaneTestContext; use nexus_test_utils_macros::nexus_test; use omicron_nexus::external_api::views::Rack; use omicron_nexus::TestInterfaces; +type ControlPlaneTestContext = + nexus_test_utils::ControlPlaneTestContext; + #[nexus_test] async fn test_list_own_rack(cptestctx: &ControlPlaneTestContext) { let client = &cptestctx.external_client; diff --git a/nexus/tests/integration_tests/role_assignments.rs b/nexus/tests/integration_tests/role_assignments.rs index 44959f225fa..c739326726e 100644 --- a/nexus/tests/integration_tests/role_assignments.rs +++ b/nexus/tests/integration_tests/role_assignments.rs @@ -14,7 +14,6 @@ use nexus_test_utils::http_testing::AuthnMode; use nexus_test_utils::http_testing::NexusRequest; use nexus_test_utils::resource_helpers::create_organization; use nexus_test_utils::resource_helpers::create_project; -use nexus_test_utils::ControlPlaneTestContext; use nexus_test_utils_macros::nexus_test; use omicron_common::api::external::ObjectIdentity; use omicron_nexus::authn::USER_TEST_UNPRIVILEGED; @@ -26,6 +25,9 @@ use omicron_nexus::db::model::DatabaseString; use omicron_nexus::external_api::shared; use omicron_nexus::external_api::views; +type ControlPlaneTestContext = + nexus_test_utils::ControlPlaneTestContext; + /// Describes the role assignment test for a particular kind of resource /// /// This trait essentially describes a test case that will be fed into diff --git a/nexus/tests/integration_tests/roles_builtin.rs b/nexus/tests/integration_tests/roles_builtin.rs index 2f6ce82827c..b8070f8ac41 100644 --- a/nexus/tests/integration_tests/roles_builtin.rs +++ b/nexus/tests/integration_tests/roles_builtin.rs @@ -9,10 +9,12 @@ use http::StatusCode; use nexus_test_utils::http_testing::AuthnMode; use nexus_test_utils::http_testing::NexusRequest; use nexus_test_utils::http_testing::RequestBuilder; -use nexus_test_utils::ControlPlaneTestContext; use nexus_test_utils_macros::nexus_test; use omicron_nexus::external_api::views::Role; +type ControlPlaneTestContext = + nexus_test_utils::ControlPlaneTestContext; + #[nexus_test] async fn test_roles_builtin(cptestctx: &ControlPlaneTestContext) { let testctx = &cptestctx.external_client; diff --git a/nexus/tests/integration_tests/router_routes.rs b/nexus/tests/integration_tests/router_routes.rs index 8ae05ff9cdd..5ef1a5af71a 100644 --- a/nexus/tests/integration_tests/router_routes.rs +++ b/nexus/tests/integration_tests/router_routes.rs @@ -8,7 +8,6 @@ use nexus_test_utils::http_testing::AuthnMode; use nexus_test_utils::http_testing::NexusRequest; use nexus_test_utils::identity_eq; use nexus_test_utils::resource_helpers::objects_list_page_authz; -use nexus_test_utils::ControlPlaneTestContext; use nexus_test_utils_macros::nexus_test; use omicron_common::api::external::{ IdentityMetadataCreateParams, IdentityMetadataUpdateParams, @@ -22,6 +21,9 @@ use nexus_test_utils::resource_helpers::{ create_organization, create_project, create_router, create_vpc, }; +type ControlPlaneTestContext = + nexus_test_utils::ControlPlaneTestContext; + #[nexus_test] async fn test_router_routes(cptestctx: &ControlPlaneTestContext) { let client = &cptestctx.external_client; diff --git a/nexus/tests/integration_tests/saml.rs b/nexus/tests/integration_tests/saml.rs index eb2827e9070..2e85c615081 100644 --- a/nexus/tests/integration_tests/saml.rs +++ b/nexus/tests/integration_tests/saml.rs @@ -18,13 +18,15 @@ use http::method::Method; use http::StatusCode; use nexus_test_utils::resource_helpers::{create_silo, object_create}; -use nexus_test_utils::ControlPlaneTestContext; use nexus_test_utils_macros::nexus_test; use dropshot::ResultsPage; use httptest::{matchers::*, responders::*, Expectation, Server}; use uuid::Uuid; +type ControlPlaneTestContext = + nexus_test_utils::ControlPlaneTestContext; + // Valid SAML IdP entity descriptor from https://en.wikipedia.org/wiki/SAML_metadata#Identity_provider_metadata // note: no signing keys pub const SAML_IDP_DESCRIPTOR: &str = diff --git a/nexus/tests/integration_tests/silos.rs b/nexus/tests/integration_tests/silos.rs index 8ef380711f9..17a815b8b72 100644 --- a/nexus/tests/integration_tests/silos.rs +++ b/nexus/tests/integration_tests/silos.rs @@ -26,7 +26,6 @@ use nexus_test_utils::resource_helpers::{ }; use crate::integration_tests::saml::SAML_IDP_DESCRIPTOR; -use nexus_test_utils::ControlPlaneTestContext; use nexus_test_utils_macros::nexus_test; use omicron_nexus::authz::{self, SiloRole}; use uuid::Uuid; @@ -37,6 +36,9 @@ use omicron_nexus::authn::{USER_TEST_PRIVILEGED, USER_TEST_UNPRIVILEGED}; use omicron_nexus::db::fixed_data::silo::{DEFAULT_SILO, SILO_ID}; use omicron_nexus::db::identity::Asset; +type ControlPlaneTestContext = + nexus_test_utils::ControlPlaneTestContext; + #[nexus_test] async fn test_silos(cptestctx: &ControlPlaneTestContext) { let client = &cptestctx.external_client; diff --git a/nexus/tests/integration_tests/snapshots.rs b/nexus/tests/integration_tests/snapshots.rs index 7231d1a1c92..e8c2da55907 100644 --- a/nexus/tests/integration_tests/snapshots.rs +++ b/nexus/tests/integration_tests/snapshots.rs @@ -16,7 +16,6 @@ use nexus_test_utils::resource_helpers::create_organization; use nexus_test_utils::resource_helpers::create_project; use nexus_test_utils::resource_helpers::object_create; use nexus_test_utils::resource_helpers::DiskTest; -use nexus_test_utils::ControlPlaneTestContext; use nexus_test_utils_macros::nexus_test; use omicron_common::api::external; use omicron_common::api::external::ByteCount; @@ -36,6 +35,9 @@ use uuid::Uuid; use httptest::{matchers::*, responders::*, Expectation, ServerBuilder}; +type ControlPlaneTestContext = + nexus_test_utils::ControlPlaneTestContext; + const ORG_NAME: &str = "test-org"; const PROJECT_NAME: &str = "springfield-squidport-disks"; diff --git a/nexus/tests/integration_tests/ssh_keys.rs b/nexus/tests/integration_tests/ssh_keys.rs index 5e03637a4d0..0f23fda4ec2 100644 --- a/nexus/tests/integration_tests/ssh_keys.rs +++ b/nexus/tests/integration_tests/ssh_keys.rs @@ -4,13 +4,15 @@ use http::{method::Method, StatusCode}; use nexus_test_utils::http_testing::{AuthnMode, NexusRequest}; use nexus_test_utils::resource_helpers::objects_list_page_authz; -use nexus_test_utils::ControlPlaneTestContext; use nexus_test_utils_macros::nexus_test; use omicron_common::api::external::IdentityMetadataCreateParams; use omicron_nexus::external_api::params::SshKeyCreate; use omicron_nexus::external_api::views::SshKey; +type ControlPlaneTestContext = + nexus_test_utils::ControlPlaneTestContext; + // Note: we use UnprivilegedUser in this test because unlike most tests, all the // endpoints here _can_ be accessed by that user and we want to explicitly // verify that behavior. diff --git a/nexus/tests/integration_tests/subnet_allocation.rs b/nexus/tests/integration_tests/subnet_allocation.rs index 9d8643d7c67..636157262ec 100644 --- a/nexus/tests/integration_tests/subnet_allocation.rs +++ b/nexus/tests/integration_tests/subnet_allocation.rs @@ -10,7 +10,6 @@ use dropshot::HttpErrorResponseBody; use http::method::Method; use http::StatusCode; use ipnetwork::Ipv4Network; -use nexus_defaults::NUM_INITIAL_RESERVED_IP_ADDRESSES; use nexus_test_utils::http_testing::AuthnMode; use nexus_test_utils::http_testing::NexusRequest; use nexus_test_utils::http_testing::RequestBuilder; @@ -18,15 +17,18 @@ use nexus_test_utils::resource_helpers::create_instance_with; use nexus_test_utils::resource_helpers::create_ip_pool; use nexus_test_utils::resource_helpers::objects_list_page_authz; use nexus_test_utils::resource_helpers::{create_organization, create_project}; -use nexus_test_utils::ControlPlaneTestContext; use nexus_test_utils_macros::nexus_test; use omicron_common::api::external::{ ByteCount, IdentityMetadataCreateParams, InstanceCpuCount, Ipv4Net, NetworkInterface, }; +use omicron_common::nexus_config::NUM_INITIAL_RESERVED_IP_ADDRESSES; use omicron_nexus::external_api::params; use std::net::Ipv4Addr; +type ControlPlaneTestContext = + nexus_test_utils::ControlPlaneTestContext; + async fn create_instance_expect_failure( client: &ClientTestContext, url_instances: &String, diff --git a/nexus/tests/integration_tests/timeseries.rs b/nexus/tests/integration_tests/timeseries.rs index 6e02d5f7a69..0f2bd84b28b 100644 --- a/nexus/tests/integration_tests/timeseries.rs +++ b/nexus/tests/integration_tests/timeseries.rs @@ -3,13 +3,15 @@ // file, You can obtain one at https://mozilla.org/MPL/2.0/. use nexus_test_utils::resource_helpers::objects_list_page_authz; -use nexus_test_utils::ControlPlaneTestContext; use nexus_test_utils_macros::nexus_test; use omicron_test_utils::dev::poll::{wait_for_condition, CondCheckError}; use oximeter_db::TimeseriesSchema; use std::convert::Infallible; use std::time::Duration; +type ControlPlaneTestContext = + nexus_test_utils::ControlPlaneTestContext; + #[nexus_test] async fn test_timeseries_schema(context: &ControlPlaneTestContext) { let client = &context.external_client; diff --git a/nexus/tests/integration_tests/unauthorized.rs b/nexus/tests/integration_tests/unauthorized.rs index dd5f6cb06dc..134256a6ce9 100644 --- a/nexus/tests/integration_tests/unauthorized.rs +++ b/nexus/tests/integration_tests/unauthorized.rs @@ -19,10 +19,12 @@ use nexus_test_utils::http_testing::NexusRequest; use nexus_test_utils::http_testing::RequestBuilder; use nexus_test_utils::http_testing::TestResponse; use nexus_test_utils::resource_helpers::DiskTest; -use nexus_test_utils::ControlPlaneTestContext; use nexus_test_utils_macros::nexus_test; use omicron_nexus::authn::external::spoof; +type ControlPlaneTestContext = + nexus_test_utils::ControlPlaneTestContext; + // This test hits a list Nexus API endpoints using both unauthenticated and // unauthorized requests to make sure we get the expected behavior (generally: // 401, 403, or 404). This is trickier than it sounds because the appropriate diff --git a/nexus/tests/integration_tests/updates.rs b/nexus/tests/integration_tests/updates.rs index c249f7adbfe..5206831d8db 100644 --- a/nexus/tests/integration_tests/updates.rs +++ b/nexus/tests/integration_tests/updates.rs @@ -74,8 +74,11 @@ async fn test_update_end_to_end() { trusted_root: tuf_repo.path().join("metadata").join("1.root.json"), default_base_url: format!("http://{}/", local_addr), }); - let cptestctx = - test_setup_with_config("test_update_end_to_end", &mut config).await; + let cptestctx = test_setup_with_config::( + "test_update_end_to_end", + &mut config, + ) + .await; let client = &cptestctx.external_client; // call /system/updates/refresh on nexus @@ -283,7 +286,9 @@ impl KeySource for KeyKeySource { // Tests that ".." paths are disallowed by dropshot. #[tokio::test] async fn test_download_with_dots_fails() { - let cptestctx = test_setup("test_download_with_dots_fails").await; + let cptestctx = + test_setup::("test_download_with_dots_fails") + .await; let client = &cptestctx.internal_client; let filename = "hey/can/you/look/../../../../up/the/directory/tree"; diff --git a/nexus/tests/integration_tests/users_builtin.rs b/nexus/tests/integration_tests/users_builtin.rs index baa1f6fcf67..654d14c0c56 100644 --- a/nexus/tests/integration_tests/users_builtin.rs +++ b/nexus/tests/integration_tests/users_builtin.rs @@ -3,12 +3,14 @@ use dropshot::ResultsPage; use nexus_test_utils::http_testing::AuthnMode; use nexus_test_utils::http_testing::NexusRequest; -use nexus_test_utils::ControlPlaneTestContext; use nexus_test_utils_macros::nexus_test; use omicron_nexus::authn; use omicron_nexus::external_api::views::UserBuiltin; use std::collections::BTreeMap; +type ControlPlaneTestContext = + nexus_test_utils::ControlPlaneTestContext; + #[nexus_test] async fn test_users_builtin(cptestctx: &ControlPlaneTestContext) { let testctx = &cptestctx.external_client; diff --git a/nexus/tests/integration_tests/volume_management.rs b/nexus/tests/integration_tests/volume_management.rs index 469d35ffb2a..1e028bd1721 100644 --- a/nexus/tests/integration_tests/volume_management.rs +++ b/nexus/tests/integration_tests/volume_management.rs @@ -16,7 +16,6 @@ use nexus_test_utils::resource_helpers::create_organization; use nexus_test_utils::resource_helpers::create_project; use nexus_test_utils::resource_helpers::object_create; use nexus_test_utils::resource_helpers::DiskTest; -use nexus_test_utils::ControlPlaneTestContext; use nexus_test_utils_macros::nexus_test; use omicron_common::api::external::ByteCount; use omicron_common::api::external::Disk; @@ -33,6 +32,9 @@ use uuid::Uuid; use httptest::{matchers::*, responders::*, Expectation, ServerBuilder}; +type ControlPlaneTestContext = + nexus_test_utils::ControlPlaneTestContext; + const ORG_NAME: &str = "test-org"; const PROJECT_NAME: &str = "springfield-squidport-disks"; diff --git a/nexus/tests/integration_tests/vpc_firewall.rs b/nexus/tests/integration_tests/vpc_firewall.rs index d8eade9c5c9..ebd2a0b98ce 100644 --- a/nexus/tests/integration_tests/vpc_firewall.rs +++ b/nexus/tests/integration_tests/vpc_firewall.rs @@ -8,7 +8,6 @@ use nexus_test_utils::http_testing::{AuthnMode, NexusRequest}; use nexus_test_utils::resource_helpers::{ create_organization, create_project, create_vpc, }; -use nexus_test_utils::ControlPlaneTestContext; use nexus_test_utils_macros::nexus_test; use omicron_common::api::external::{ IdentityMetadata, L4Port, L4PortRange, VpcFirewallRule, @@ -21,6 +20,9 @@ use omicron_nexus::external_api::views::Vpc; use std::convert::TryFrom; use uuid::Uuid; +type ControlPlaneTestContext = + nexus_test_utils::ControlPlaneTestContext; + #[nexus_test] async fn test_vpc_firewall(cptestctx: &ControlPlaneTestContext) { let client = &cptestctx.external_client; diff --git a/nexus/tests/integration_tests/vpc_routers.rs b/nexus/tests/integration_tests/vpc_routers.rs index 547aa2d8c5e..7e2014a0ef0 100644 --- a/nexus/tests/integration_tests/vpc_routers.rs +++ b/nexus/tests/integration_tests/vpc_routers.rs @@ -13,7 +13,6 @@ use nexus_test_utils::resource_helpers::objects_list_page_authz; use nexus_test_utils::resource_helpers::{ create_organization, create_project, create_vpc, }; -use nexus_test_utils::ControlPlaneTestContext; use nexus_test_utils_macros::nexus_test; use omicron_common::api::external::IdentityMetadataCreateParams; use omicron_common::api::external::IdentityMetadataUpdateParams; @@ -21,6 +20,9 @@ use omicron_nexus::external_api::params; use omicron_nexus::external_api::views::VpcRouter; use omicron_nexus::external_api::views::VpcRouterKind; +type ControlPlaneTestContext = + nexus_test_utils::ControlPlaneTestContext; + #[nexus_test] async fn test_vpc_routers(cptestctx: &ControlPlaneTestContext) { let client = &cptestctx.external_client; diff --git a/nexus/tests/integration_tests/vpc_subnets.rs b/nexus/tests/integration_tests/vpc_subnets.rs index 35f427a4b76..0bf8eab37e4 100644 --- a/nexus/tests/integration_tests/vpc_subnets.rs +++ b/nexus/tests/integration_tests/vpc_subnets.rs @@ -17,7 +17,6 @@ use nexus_test_utils::resource_helpers::{ create_instance, create_ip_pool, create_organization, create_project, create_vpc, }; -use nexus_test_utils::ControlPlaneTestContext; use nexus_test_utils_macros::nexus_test; use omicron_common::api::external::IdentityMetadataCreateParams; use omicron_common::api::external::IdentityMetadataUpdateParams; @@ -25,6 +24,9 @@ use omicron_common::api::external::Ipv4Net; use omicron_common::api::external::Ipv6Net; use omicron_nexus::external_api::{params, views::VpcSubnet}; +type ControlPlaneTestContext = + nexus_test_utils::ControlPlaneTestContext; + #[nexus_test] async fn test_delete_vpc_subnet_with_interfaces_fails( cptestctx: &ControlPlaneTestContext, diff --git a/nexus/tests/integration_tests/vpcs.rs b/nexus/tests/integration_tests/vpcs.rs index de75a7cb0e1..5e6ffaca928 100644 --- a/nexus/tests/integration_tests/vpcs.rs +++ b/nexus/tests/integration_tests/vpcs.rs @@ -14,13 +14,15 @@ use nexus_test_utils::resource_helpers::objects_list_page_authz; use nexus_test_utils::resource_helpers::{ create_organization, create_project, create_vpc, create_vpc_with_error, }; -use nexus_test_utils::ControlPlaneTestContext; use nexus_test_utils_macros::nexus_test; use omicron_common::api::external::IdentityMetadataCreateParams; use omicron_common::api::external::IdentityMetadataUpdateParams; use omicron_common::api::external::Ipv6Net; use omicron_nexus::external_api::{params, views::Vpc}; +type ControlPlaneTestContext = + nexus_test_utils::ControlPlaneTestContext; + #[nexus_test] async fn test_vpcs(cptestctx: &ControlPlaneTestContext) { let client = &cptestctx.external_client; diff --git a/nexus/tests/integration_tests/zpools.rs b/nexus/tests/integration_tests/zpools.rs index 6dc760a3df0..89c8ffada85 100644 --- a/nexus/tests/integration_tests/zpools.rs +++ b/nexus/tests/integration_tests/zpools.rs @@ -8,9 +8,12 @@ use omicron_common::api::external::ByteCount; use omicron_nexus::internal_api::params::ZpoolPutRequest; use uuid::Uuid; -use nexus_test_utils::{ControlPlaneTestContext, SLED_AGENT_UUID}; +use nexus_test_utils::SLED_AGENT_UUID; use nexus_test_utils_macros::nexus_test; +type ControlPlaneTestContext = + nexus_test_utils::ControlPlaneTestContext; + // Tests the "normal" case of zpool_put: inserting a known Zpool. // // This will typically be invoked by the Sled Agent, after performing inventory.