diff --git a/Cargo.lock b/Cargo.lock index b7296ea1844..27bb636eb5c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -40,9 +40,9 @@ dependencies = [ [[package]] name = "aes-gcm" -version = "0.10.2" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "209b47e8954a928e1d72e86eca7000ebb6655fe1436d33eefc2201cad027e237" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" dependencies = [ "aead", "aes", @@ -80,9 +80,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.0.4" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6748e8def348ed4d14996fa801f4122cd763fff530258cdc03f64b25f89d3a5a" +checksum = "ea5d730647d4fadd988536d06fecce94b7b4f2a7efdae548f1cf4b63205518ab" dependencies = [ "memchr", ] @@ -125,9 +125,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.5.0" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f58811cfac344940f1a400b6e6231ce35171f614f26439e80f8c1465c5cc0c" +checksum = "2ab91ebe16eb252986481c5b62f6098f3b698a45e34b5b98200cf20dd2484a44" dependencies = [ "anstyle", "anstyle-parse", @@ -139,15 +139,15 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.2" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15c4c2c83f81532e5845a733998b6971faca23490340a418e9b72a3ec9de12ea" +checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" [[package]] name = "anstyle-parse" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333" +checksum = "317b9a89c1868f5ea6ff1d9539a69f45dffc21ce321ac1fd1160dfa48c8e2140" dependencies = [ "utf8parse", ] @@ -163,9 +163,9 @@ dependencies = [ [[package]] name = "anstyle-wincon" -version = "2.1.0" +version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58f54d10c6dfa51283a066ceab3ec1ab78d13fae00aa49243a45e4571fb79dfd" +checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628" dependencies = [ "anstyle", "windows-sys 0.48.0", @@ -184,20 +184,21 @@ dependencies = [ name = "api_identity" version = "0.1.0" dependencies = [ - "omicron-workspace-hack", + "omicron-workspace-hack 0.1.0", "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.37", ] [[package]] name = "api_identity" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#3dcc8d2eb648c87b42454882a2ce024b409cbb8c" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#8a11c709b49eed5bbe980d2f3e69ddf115278914" dependencies = [ + "omicron-workspace-hack 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.37", ] [[package]] @@ -270,9 +271,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88903cb14723e4d4003335bb7f8a14f27691649105346a0f0957466c096adfe6" dependencies = [ "anstyle", - "bstr 1.6.0", + "bstr 1.6.2", "doc-comment", - "predicates 3.0.3", + "predicates", "predicates-core", "predicates-tree", "wait-timeout", @@ -304,7 +305,7 @@ checksum = "5fd55a5ba1179988837d24ab4c7cc8ed6efdeff578ede0416b4225a5fca35bd0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.37", ] [[package]] @@ -326,7 +327,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.37", ] [[package]] @@ -337,7 +338,7 @@ checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.37", ] [[package]] @@ -351,9 +352,9 @@ dependencies = [ [[package]] name = "atomic-waker" -version = "1.1.1" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1181e1e0d1fce796a03db1ae795d67167da795f9cf4a39c37589e85ef57f26d3" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "atty" @@ -371,12 +372,12 @@ name = "authz-macros" version = "0.1.0" dependencies = [ "heck 0.4.1", - "omicron-workspace-hack", + "omicron-workspace-hack 0.1.0", "proc-macro2", "quote", "serde", "serde_tokenstream 0.2.0", - "syn 2.0.32", + "syn 2.0.37", ] [[package]] @@ -518,7 +519,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.32", + "syn 2.0.37", "which", ] @@ -607,13 +608,13 @@ dependencies = [ [[package]] name = "blake2b_simd" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c2f0dc9a68c6317d884f97cc36cf5a3d20ba14ce404227df55e1af708ab04bc" +checksum = "23285ad32269793932e830392f2fe2f83e26488fd3ec778883a93c8323735780" dependencies = [ "arrayref", "arrayvec", - "constant_time_eq 0.2.6", + "constant_time_eq", ] [[package]] @@ -660,7 +661,7 @@ dependencies = [ "omicron-common 0.1.0", "omicron-rpaths", "omicron-test-utils", - "omicron-workspace-hack", + "omicron-workspace-hack 0.1.0", "pq-sys", "proptest", "rand 0.8.5", @@ -687,7 +688,7 @@ dependencies = [ "chrono", "ipnetwork", "omicron-common 0.1.0", - "omicron-workspace-hack", + "omicron-workspace-hack 0.1.0", "progenitor", "regress", "reqwest", @@ -711,9 +712,9 @@ dependencies = [ [[package]] name = "bstr" -version = "1.6.0" +version = "1.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6798148dccfbff0fae41c7574d2fa8f1ef3492fba0face179de5d8d447d67b05" +checksum = "4c2f7349907b712260e64b0afe2f84692af14a454be26187d9df565c7f69266a" dependencies = [ "memchr", "regex-automata 0.3.8", @@ -732,9 +733,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.13.0" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" [[package]] name = "bytecount" @@ -793,7 +794,7 @@ version = "0.1.0" dependencies = [ "anyhow", "hubtools", - "omicron-workspace-hack", + "omicron-workspace-hack 0.1.0", ] [[package]] @@ -845,7 +846,7 @@ checksum = "fb9ac64500cc83ce4b9f8dafa78186aa008c8dea77a09b94cd307fd0cd5022a8" dependencies = [ "camino", "cargo-platform", - "semver 1.0.18", + "semver 1.0.19", "serde", "serde_json", "thiserror", @@ -858,7 +859,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3f9629bc6c4388ea699781dc988c2b99766d7679b151c81990b4fa1208fafd3" dependencies = [ "serde", - "toml 0.8.0", + "toml 0.8.1", ] [[package]] @@ -1035,9 +1036,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.4.3" +version = "4.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84ed82781cea27b43c9b106a979fe450a13a31aab0500595fb3fc06616de08e6" +checksum = "d04704f56c2cde07f43e8e2c154b43f216dc5c92fc98ada720177362f953b956" dependencies = [ "clap_builder", "clap_derive 4.4.2", @@ -1045,9 +1046,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.4.2" +version = "4.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bb9faaa7c2ef94b2743a21f5a29e6f0010dff4caa69ac8e9d6cf8b6fa74da08" +checksum = "0e231faeaca65ebd1ea3c737966bf858971cd38c3849107aa3ea7de90a804e45" dependencies = [ "anstream", "anstyle", @@ -1078,7 +1079,7 @@ dependencies = [ "heck 0.4.1", "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.37", ] [[package]] @@ -1152,12 +1153,6 @@ dependencies = [ "unicode-xid", ] -[[package]] -name = "constant_time_eq" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21a53c0a4d288377e7415b53dcfc3c04da5cdc2cc95c8d5ac178b58f0b861ad6" - [[package]] name = "constant_time_eq" version = "0.3.0" @@ -1250,7 +1245,7 @@ dependencies = [ "dropshot", "hex", "omicron-test-utils", - "omicron-workspace-hack", + "omicron-workspace-hack 0.1.0", "ring", "slog", "tokio", @@ -1265,7 +1260,7 @@ dependencies = [ "anes", "cast", "ciborium", - "clap 4.4.3", + "clap 4.4.6", "criterion-plot", "futures", "is-terminal", @@ -1488,7 +1483,7 @@ source = "git+https://github.com/oxidecomputer/crucible?rev=aeb69dda26c7e1a8b6ea dependencies = [ "anyhow", "atty", - "nix 0.26.2 (registry+https://github.com/rust-lang/crates.io-index)", + "nix 0.26.4", "rusqlite", "rustls-pemfile", "schemars", @@ -1558,9 +1553,9 @@ checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" [[package]] name = "crypto-bigint" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf4c2f4e1afd912bc40bfd6fed5d9dc1f288e0ba01bfcc835cc5bc3eb13efe15" +checksum = "740fe28e594155f10cfc383984cbefd529d7396050557148f79cb0f621204124" dependencies = [ "generic-array", "rand_core 0.6.4", @@ -1600,9 +1595,9 @@ dependencies = [ [[package]] name = "curve25519-dalek" -version = "4.1.0" +version = "4.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "622178105f911d937a42cdb140730ba4a3ed2becd8ae6ce39c7d28b5d75d4588" +checksum = "e89b8c6a2e4b1f45971ad09761aafb85514a84744b67a95e32c3cc1352d1f65c" dependencies = [ "cfg-if 1.0.0", "cpufeatures", @@ -1624,7 +1619,7 @@ checksum = "83fdaf97f4804dcebfa5862639bc9ce4121e82140bec2a987ac5140294865b5b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.37", ] [[package]] @@ -1672,7 +1667,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.10.0", - "syn 2.0.32", + "syn 2.0.37", ] [[package]] @@ -1694,20 +1689,7 @@ checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5" dependencies = [ "darling_core 0.20.3", "quote", - "syn 2.0.32", -] - -[[package]] -name = "dashmap" -version = "5.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edd72493923899c6f10c641bdbdeddc7183d6396641d99c1a0d1597f37f92e28" -dependencies = [ - "cfg-if 1.0.0", - "hashbrown 0.14.0", - "lock_api", - "once_cell", - "parking_lot_core 0.9.8", + "syn 2.0.37", ] [[package]] @@ -1732,13 +1714,13 @@ name = "db-macros" version = "0.1.0" dependencies = [ "heck 0.4.1", - "omicron-workspace-hack", + "omicron-workspace-hack 0.1.0", "proc-macro2", "quote", "rustfmt-wrapper", "serde", "serde_tokenstream 0.2.0", - "syn 2.0.32", + "syn 2.0.37", ] [[package]] @@ -1748,7 +1730,7 @@ dependencies = [ "anyhow", "either", "omicron-common 0.1.0", - "omicron-workspace-hack", + "omicron-workspace-hack 0.1.0", "omicron-zone-package", "progenitor", "progenitor-client", @@ -1791,7 +1773,7 @@ checksum = "5fe87ce4529967e0ba1dcf8450bab64d97dfd5010a6256187ffe2e43e6f0e049" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.37", ] [[package]] @@ -1811,7 +1793,7 @@ checksum = "146398d62142a0f35248a608f17edf0dde57338354966d6e41d0eb2d16980ccb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.37", ] [[package]] @@ -1860,9 +1842,9 @@ dependencies = [ [[package]] name = "diesel" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d98235fdc2f355d330a8244184ab6b4b33c28679c0b4158f63138e51d6cf7e88" +checksum = "53c8a2cb22327206568569e5a45bb5a2c946455efdd76e24d15b7e82171af95e" dependencies = [ "bitflags 2.4.0", "byteorder", @@ -1891,14 +1873,14 @@ dependencies = [ [[package]] name = "diesel_derives" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e054665eaf6d97d1e7125512bb2d35d07c73ac86cc6920174cb42d1ab697a554" +checksum = "ef8337737574f55a468005a83499da720f20c65586241ffea339db9ecdfd2b44" dependencies = [ "diesel_table_macro_syntax", "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.37", ] [[package]] @@ -1907,7 +1889,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc5557efc453706fed5e4fa85006fe9817c224c3f480a34c7e5959fd700921c5" dependencies = [ - "syn 2.0.32", + "syn 2.0.37", ] [[package]] @@ -2011,13 +1993,13 @@ dependencies = [ "anyhow", "camino", "chrono", - "clap 4.4.3", + "clap 4.4.6", "dns-service-client 0.1.0", "dropshot", "expectorate", "http", "omicron-test-utils", - "omicron-workspace-hack", + "omicron-workspace-hack 0.1.0", "openapi-lint", "openapiv3", "pretty-hex 0.3.0", @@ -2048,7 +2030,7 @@ version = "0.1.0" dependencies = [ "chrono", "http", - "omicron-workspace-hack", + "omicron-workspace-hack 0.1.0", "progenitor", "reqwest", "schemars", @@ -2061,10 +2043,11 @@ dependencies = [ [[package]] name = "dns-service-client" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#3dcc8d2eb648c87b42454882a2ce024b409cbb8c" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#8a11c709b49eed5bbe980d2f3e69ddf115278914" dependencies = [ "chrono", "http", + "omicron-workspace-hack 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", "progenitor", "reqwest", "schemars", @@ -2090,12 +2073,6 @@ dependencies = [ "zerocopy 0.3.0", ] -[[package]] -name = "downcast" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" - [[package]] name = "dpd-client" version = "0.1.0" @@ -2105,7 +2082,7 @@ dependencies = [ "futures", "http", "ipnetwork", - "omicron-workspace-hack", + "omicron-workspace-hack 0.1.0", "omicron-zone-package", "progenitor", "progenitor-client", @@ -2125,7 +2102,7 @@ dependencies = [ [[package]] name = "dropshot" version = "0.9.1-dev" -source = "git+https://github.com/oxidecomputer/dropshot?branch=main#fa728d07970824fd5f3bd57a3d4dc0fdbea09bfd" +source = "git+https://github.com/oxidecomputer/dropshot?branch=main#a5ec1cb0ad562c93759d311db2430d01cf83286e" dependencies = [ "async-stream", "async-trait", @@ -2140,7 +2117,7 @@ dependencies = [ "hostname", "http", "hyper", - "indexmap 2.0.0", + "indexmap 2.0.2", "multer", "openapiv3", "paste", @@ -2161,7 +2138,7 @@ dependencies = [ "slog-term", "tokio", "tokio-rustls", - "toml 0.7.8", + "toml 0.8.1", "usdt", "uuid", "version_check", @@ -2171,13 +2148,13 @@ dependencies = [ [[package]] name = "dropshot_endpoint" version = "0.9.1-dev" -source = "git+https://github.com/oxidecomputer/dropshot?branch=main#fa728d07970824fd5f3bd57a3d4dc0fdbea09bfd" +source = "git+https://github.com/oxidecomputer/dropshot?branch=main#a5ec1cb0ad562c93759d311db2430d01cf83286e" dependencies = [ "proc-macro2", "quote", "serde", "serde_tokenstream 0.2.0", - "syn 2.0.32", + "syn 2.0.37", ] [[package]] @@ -2193,9 +2170,9 @@ dependencies = [ [[package]] name = "dyn-clone" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbfc4744c1b8f2a09adc0e55242f60b1af195d88596bd8700be74418c056c555" +checksum = "23d2f3407d9a573d666de4b5bdf10569d73ca9478087346697dcbae6244bfbcd" [[package]] name = "ed25519" @@ -2291,7 +2268,7 @@ dependencies = [ "http", "omicron-sled-agent", "omicron-test-utils", - "omicron-workspace-hack", + "omicron-workspace-hack 0.1.0", "oxide-client", "rand 0.8.5", "reqwest", @@ -2339,7 +2316,7 @@ checksum = "eecf8589574ce9b895052fa12d69af7a233f99e6107f5cb8dd1044f2a17bfdcb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.37", ] [[package]] @@ -2374,18 +2351,18 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "erased-serde" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "837c0466252947ada828b975e12daf82e18bb5444e4df87be6038d4469e2a3d2" +checksum = "6c138974f9d5e7fe373eb04df7cae98833802ae4b11c24ac7039a21d5af4b26c" dependencies = [ "serde", ] [[package]] name = "errno" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b30f669a7961ef1631673d2766cc92f52d64f7ef354d4fe0ddfd30ed52f0f4f" +checksum = "136526188508e25c6fef639d7927dfb3e0e3084488bf202267829cf7fc23dbdd" dependencies = [ "errno-dragonfly", "libc", @@ -2404,9 +2381,9 @@ dependencies = [ [[package]] name = "expectorate" -version = "1.0.7" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "710ab6a2d57038a835d66f78d5af3fa5d27c1ec4682f823b9203c48826cb0591" +checksum = "de6f19b25bdfa2747ae775f37cd109c31f1272d4e4c83095be0727840aa1d75f" dependencies = [ "console", "newline-converter", @@ -2427,9 +2404,9 @@ checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" [[package]] name = "fastrand" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764" +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" [[package]] name = "fatfs" @@ -2450,7 +2427,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef033ed5e9bad94e55838ca0ca906db0e043f517adda0c8b79c7a8c66c93c1b5" dependencies = [ "cfg-if 1.0.0", - "rustix 0.38.9", + "rustix", "windows-sys 0.48.0", ] @@ -2482,6 +2459,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "finl_unicode" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fcfdc7a0362c9f4444381a9e697c79d435fe65b52a37466fc2c1184cee9edc6" + [[package]] name = "fixedbitset" version = "0.4.2" @@ -2490,9 +2473,9 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "flagset" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cda653ca797810c02f7ca4b804b40b8b95ae046eb989d356bce17919a8c25499" +checksum = "d52a7e408202050813e6f1d9addadcaafef3dca7530c7ddfb005d4081cce6779" [[package]] name = "flate2" @@ -2558,7 +2541,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.37", ] [[package]] @@ -2582,12 +2565,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "fragile" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c2141d6d6c8512188a7891b4b01590a45f6dac67afb4f255c4124dbb86d4eaa" - [[package]] name = "fs-err" version = "2.9.0" @@ -2672,7 +2649,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.37", ] [[package]] @@ -2725,14 +2702,14 @@ name = "gateway-cli" version = "0.1.0" dependencies = [ "anyhow", - "clap 4.4.3", + "clap 4.4.6", "futures", "gateway-client", "gateway-messages", "hex", "libc", "omicron-common 0.1.0", - "omicron-workspace-hack", + "omicron-workspace-hack 0.1.0", "reqwest", "serde", "serde_json", @@ -2751,7 +2728,7 @@ version = "0.1.0" dependencies = [ "base64 0.21.4", "chrono", - "omicron-workspace-hack", + "omicron-workspace-hack 0.1.0", "progenitor", "rand 0.8.5", "reqwest", @@ -2774,7 +2751,7 @@ dependencies = [ "smoltcp 0.9.1", "static_assertions", "uuid", - "zerocopy 0.6.3", + "zerocopy 0.6.4", ] [[package]] @@ -2791,12 +2768,12 @@ dependencies = [ "hubpack 0.1.2", "hubtools", "lru-cache", - "nix 0.26.2 (git+https://github.com/jgallagher/nix?branch=r0.26-illumos)", + "nix 0.26.2", "once_cell", "serde", "serde-big-array 0.5.1", "slog", - "socket2 0.5.3", + "socket2 0.5.4", "string_cache", "thiserror", "tlvc 0.3.1 (git+https://github.com/oxidecomputer/tlvc.git?branch=main)", @@ -2816,7 +2793,7 @@ dependencies = [ "gateway-messages", "omicron-gateway", "omicron-test-utils", - "omicron-workspace-hack", + "omicron-workspace-hack 0.1.0", "slog", "sp-sim", "tokio", @@ -2885,11 +2862,11 @@ checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" [[package]] name = "git2" -version = "0.17.2" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b989d6a7ca95a362cf2cfc5ad688b3a467be1f87e480b8dad07fee8c79b0044" +checksum = "fbf97ba92db08df386e10c8ede66a2a0369bd277090afd8710e19e38de9ec0cd" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.4.0", "libc", "libgit2-sys", "log", @@ -2909,7 +2886,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "759c97c1e17c55525b57192c06a267cda0ac5210b222d6b82189a2338fa1c13d" dependencies = [ "aho-corasick", - "bstr 1.6.0", + "bstr 1.6.2", "fnv", "log", "regex", @@ -2977,9 +2954,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.14.0" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" +checksum = "7dfda62a12f55daeae5015f81b0baea145391cb4520f86c248fc615d72640d12" dependencies = [ "ahash", "allocator-api2", @@ -2987,11 +2964,11 @@ dependencies = [ [[package]] name = "hashlink" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "312f66718a2d7789ffef4f4b7b213138ed9f1eb3aa1d0d82fc99f88fb3ffd26f" +checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" dependencies = [ - "hashbrown 0.14.0", + "hashbrown 0.14.1", ] [[package]] @@ -3046,6 +3023,71 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +[[package]] +name = "helios-fusion" +version = "0.1.0" +dependencies = [ + "async-trait", + "itertools 0.11.0", + "omicron-workspace-hack 0.1.0", + "shlex", + "slog", + "thiserror", + "tokio", +] + +[[package]] +name = "helios-protostar" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "camino", + "cfg-if 1.0.0", + "futures", + "helios-fusion", + "itertools 0.11.0", + "libc", + "omicron-common 0.1.0", + "omicron-test-utils", + "omicron-workspace-hack 0.1.0", + "schemars", + "serde", + "shlex", + "slog", + "smf", + "thiserror", + "tokio", + "uuid", + "zone", +] + +[[package]] +name = "helios-tokamak" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "camino", + "cfg-if 1.0.0", + "futures", + "helios-fusion", + "itertools 0.11.0", + "libc", + "omicron-common 0.1.0", + "omicron-test-utils", + "omicron-workspace-hack 0.1.0", + "schemars", + "serde", + "shlex", + "slog", + "smf", + "thiserror", + "tokio", + "uuid", + "zone", +] + [[package]] name = "hermit-abi" version = "0.1.19" @@ -3057,9 +3099,9 @@ dependencies = [ [[package]] name = "hermit-abi" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" +checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" [[package]] name = "hex" @@ -3179,7 +3221,7 @@ dependencies = [ [[package]] name = "hubpack" version = "0.1.0" -source = "git+https://github.com/cbiffle/hubpack?rev=df08cc3a6e1f97381cd0472ae348e310f0119e25#df08cc3a6e1f97381cd0472ae348e310f0119e25" +source = "git+https://github.com/cbiffle/hubpack.git?rev=df08cc3a6e1f97381cd0472ae348e310f0119e25#df08cc3a6e1f97381cd0472ae348e310f0119e25" dependencies = [ "hubpack_derive 0.1.0", "serde", @@ -3198,7 +3240,7 @@ dependencies = [ [[package]] name = "hubpack_derive" version = "0.1.0" -source = "git+https://github.com/cbiffle/hubpack?rev=df08cc3a6e1f97381cd0472ae348e310f0119e25#df08cc3a6e1f97381cd0472ae348e310f0119e25" +source = "git+https://github.com/cbiffle/hubpack.git?rev=df08cc3a6e1f97381cd0472ae348e310f0119e25#df08cc3a6e1f97381cd0472ae348e310f0119e25" dependencies = [ "proc-macro2", "quote", @@ -3219,7 +3261,7 @@ dependencies = [ [[package]] name = "hubtools" version = "0.4.1" -source = "git+https://github.com/oxidecomputer/hubtools.git?branch=main#0c642f6e1f83b74725c7119a546bc26ac7452a48" +source = "git+https://github.com/oxidecomputer/hubtools.git?branch=main#2481445b80f8476041f62a1c8b6301e4918c63ed" dependencies = [ "lpc55_areas", "lpc55_sign", @@ -3227,11 +3269,11 @@ dependencies = [ "path-slash", "rsa", "thiserror", - "tlvc 0.3.1 (git+https://github.com/oxidecomputer/tlvc.git)", + "tlvc 0.3.1 (git+https://github.com/oxidecomputer/tlvc)", "tlvc-text", "toml 0.7.8", "x509-cert", - "zerocopy 0.6.3", + "zerocopy 0.6.4", "zip", ] @@ -3388,19 +3430,24 @@ dependencies = [ "byteorder", "camino", "cfg-if 1.0.0", + "debug-ignore", "futures", + "helios-fusion", + "helios-tokamak", "ipnetwork", + "itertools 0.11.0", "libc", "macaddr", - "mockall", "omicron-common 0.1.0", - "omicron-workspace-hack", + "omicron-test-utils", + "omicron-workspace-hack 0.1.0", "opte-ioctl", "oxide-vpc", "regress", "schemars", "serde", "serde_json", + "shlex", "slog", "smf", "thiserror", @@ -3434,20 +3481,20 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.0.0" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" +checksum = "8adf3ddd720272c6ea8bf59463c04e0f93d0bbf7c5439b691bca2987e0270897" dependencies = [ "equivalent", - "hashbrown 0.14.0", + "hashbrown 0.14.1", "serde", ] [[package]] name = "indicatif" -version = "0.17.6" +version = "0.17.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b297dc40733f23a0e52728a58fa9489a5b7638a324932de16b41adc3ef80730" +checksum = "fb28741c9db9a713d93deb3bb9515c20788cef5815265bee4980e87bde7e0f25" dependencies = [ "console", "instant", @@ -3465,9 +3512,9 @@ checksum = "bfa799dd5ed20a7e349f3b4639aa80d74549c81716d9ec4f994c9b5815598306" [[package]] name = "indoc" -version = "2.0.3" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c785eefb63ebd0e33416dfcb8d6da0bf27ce752843a45632a67bf10d4d4b5c4" +checksum = "1e186cfbae8084e513daff4240b4797e342f988cecda4fb6c939150f96315fd8" [[package]] name = "inout" @@ -3489,10 +3536,13 @@ dependencies = [ "bytes", "camino", "cancel-safe-futures", - "clap 4.4.3", + "clap 4.4.6", "ddm-admin-client", "display-error-chain", "futures", + "helios-fusion", + "helios-protostar", + "helios-tokamak", "hex", "hex-literal", "http", @@ -3504,7 +3554,7 @@ dependencies = [ "libc", "omicron-common 0.1.0", "omicron-test-utils", - "omicron-workspace-hack", + "omicron-workspace-hack 0.1.0", "once_cell", "partial-io", "progenitor-client", @@ -3534,7 +3584,7 @@ name = "installinator-artifact-client" version = "0.1.0" dependencies = [ "installinator-common", - "omicron-workspace-hack", + "omicron-workspace-hack 0.1.0", "progenitor", "regress", "reqwest", @@ -3553,14 +3603,14 @@ dependencies = [ "anyhow", "async-trait", "camino", - "clap 4.4.3", + "clap 4.4.6", "dropshot", "expectorate", "hyper", "installinator-common", "omicron-common 0.1.0", "omicron-test-utils", - "omicron-workspace-hack", + "omicron-workspace-hack 0.1.0", "openapi-lint", "openapiv3", "schemars", @@ -3580,7 +3630,7 @@ dependencies = [ "camino", "illumos-utils", "omicron-common 0.1.0", - "omicron-workspace-hack", + "omicron-workspace-hack 0.1.0", "schemars", "serde", "serde_json", @@ -3613,7 +3663,7 @@ dependencies = [ "hyper", "omicron-common 0.1.0", "omicron-test-utils", - "omicron-workspace-hack", + "omicron-workspace-hack 0.1.0", "progenitor", "reqwest", "serde", @@ -3631,7 +3681,7 @@ dependencies = [ [[package]] name = "internal-dns" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#3dcc8d2eb648c87b42454882a2ce024b409cbb8c" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#8a11c709b49eed5bbe980d2f3e69ddf115278914" dependencies = [ "anyhow", "chrono", @@ -3639,6 +3689,7 @@ dependencies = [ "futures", "hyper", "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-workspace-hack 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", "reqwest", "slog", "thiserror", @@ -3652,27 +3703,16 @@ name = "internal-dns-cli" version = "0.1.0" dependencies = [ "anyhow", - "clap 4.4.3", + "clap 4.4.6", "dropshot", "internal-dns 0.1.0", "omicron-common 0.1.0", - "omicron-workspace-hack", + "omicron-workspace-hack 0.1.0", "slog", "tokio", "trust-dns-resolver", ] -[[package]] -name = "io-lifetimes" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" -dependencies = [ - "hermit-abi 0.3.2", - "libc", - "windows-sys 0.48.0", -] - [[package]] name = "ipcc-key-value" version = "0.1.0" @@ -3680,7 +3720,7 @@ dependencies = [ "ciborium", "libc", "omicron-common 0.1.0", - "omicron-workspace-hack", + "omicron-workspace-hack 0.1.0", "proptest", "serde", "test-strategy", @@ -3694,7 +3734,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" dependencies = [ - "socket2 0.5.3", + "socket2 0.5.4", "widestring", "windows-sys 0.48.0", "winreg", @@ -3722,8 +3762,8 @@ version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" dependencies = [ - "hermit-abi 0.3.2", - "rustix 0.38.9", + "hermit-abi 0.3.3", + "rustix", "windows-sys 0.48.0", ] @@ -3785,7 +3825,7 @@ dependencies = [ "async-trait", "hkdf", "omicron-common 0.1.0", - "omicron-workspace-hack", + "omicron-workspace-hack 0.1.0", "secrecy", "sha3", "slog", @@ -3876,9 +3916,9 @@ checksum = "b024e211b1b371da58cd69e4fb8fa4ed16915edcc0e2e1fb04ac4bad61959f25" [[package]] name = "libgit2-sys" -version = "0.15.2+1.6.4" +version = "0.16.1+1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a80df2e11fb4a61f4ba2ab42dbe7f74468da143f1a75c74e11dee7c813f694fa" +checksum = "f2a2bb3680b094add03bb3732ec520ece34da31a8cd2d633d1389d0f0fb60d0c" dependencies = [ "cc", "libc", @@ -3905,7 +3945,7 @@ checksum = "f7012b1bbb0719e1097c47611d3898568c546d597c2e74d66f6087edd5233ff4" [[package]] name = "libnet" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/netadm-sys#f114bd0d543d886cd453932e9f0967de57289bc2" +source = "git+https://github.com/oxidecomputer/netadm-sys#59e69ef8fb17be233e336cf4943e31ae398aa4d1" dependencies = [ "anyhow", "cfg-if 1.0.0", @@ -3979,15 +4019,9 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linux-raw-sys" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" - -[[package]] -name = "linux-raw-sys" -version = "0.4.5" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57bcfdad1b858c2db7c38303a6d2ad4dfaf5eb53dfeb0910128b2c26d6158503" +checksum = "1a9bad9f94746442c783ca431b22403b519cd7fbeed0533fdd6328b2f2212128" [[package]] name = "lock_api" @@ -4008,18 +4042,18 @@ checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" [[package]] name = "lpc55_areas" version = "0.2.4" -source = "git+https://github.com/oxidecomputer/lpc55_support#4051a3b9421573dc36ed6098b292a7609a3cf98b" +source = "git+https://github.com/oxidecomputer/lpc55_support#96f064eaae5e95930efaab6c29fd1b2e22225dac" dependencies = [ "bitfield", - "clap 4.4.3", + "clap 4.4.6", "packed_struct", "serde", ] [[package]] name = "lpc55_sign" -version = "0.3.2" -source = "git+https://github.com/oxidecomputer/lpc55_support#4051a3b9421573dc36ed6098b292a7609a3cf98b" +version = "0.3.3" +source = "git+https://github.com/oxidecomputer/lpc55_support#96f064eaae5e95930efaab6c29fd1b2e22225dac" dependencies = [ "byteorder", "const-oid", @@ -4038,7 +4072,7 @@ dependencies = [ "sha2", "thiserror", "x509-cert", - "zerocopy 0.6.3", + "zerocopy 0.6.4", ] [[package]] @@ -4091,10 +4125,11 @@ checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" [[package]] name = "md-5" -version = "0.10.5" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6365506850d44bff6e2fbcb5176cf63650e48bd45ef2fe2665ae1570e0f4b9ca" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" dependencies = [ + "cfg-if 1.0.0", "digest", ] @@ -4171,33 +4206,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "mockall" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c84490118f2ee2d74570d114f3d0493cbf02790df303d2707606c3e14e07c96" -dependencies = [ - "cfg-if 1.0.0", - "downcast", - "fragile", - "lazy_static", - "mockall_derive", - "predicates 2.1.5", - "predicates-tree", -] - -[[package]] -name = "mockall_derive" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22ce75669015c4f47b289fd4d4f56e894e4c96003ffdf3ac51313126f94c6cbb" -dependencies = [ - "cfg-if 1.0.0", - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "multer" version = "2.1.0" @@ -4276,7 +4284,7 @@ dependencies = [ "ipnetwork", "omicron-common 0.1.0", "omicron-passwords 0.1.0", - "omicron-workspace-hack", + "omicron-workspace-hack 0.1.0", "progenitor", "regress", "reqwest", @@ -4290,13 +4298,14 @@ dependencies = [ [[package]] name = "nexus-client" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#3dcc8d2eb648c87b42454882a2ce024b409cbb8c" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#8a11c709b49eed5bbe980d2f3e69ddf115278914" dependencies = [ "chrono", "futures", "ipnetwork", "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", "omicron-passwords 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-workspace-hack 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", "progenitor", "regress", "reqwest", @@ -4326,13 +4335,13 @@ dependencies = [ "omicron-common 0.1.0", "omicron-passwords 0.1.0", "omicron-rpaths", - "omicron-workspace-hack", + "omicron-workspace-hack 0.1.0", "parse-display", "pq-sys", "rand 0.8.5", "ref-cast", "schemars", - "semver 1.0.18", + "semver 1.0.19", "serde", "serde_json", "sled-agent-client", @@ -4382,7 +4391,7 @@ dependencies = [ "omicron-rpaths", "omicron-sled-agent", "omicron-test-utils", - "omicron-workspace-hack", + "omicron-workspace-hack 0.1.0", "once_cell", "openapiv3", "openssl", @@ -4428,7 +4437,7 @@ dependencies = [ "ipnetwork", "lazy_static", "omicron-common 0.1.0", - "omicron-workspace-hack", + "omicron-workspace-hack 0.1.0", "rand 0.8.5", "serde_json", ] @@ -4442,7 +4451,7 @@ dependencies = [ "internal-dns 0.1.0", "nexus-types", "omicron-common 0.1.0", - "omicron-workspace-hack", + "omicron-workspace-hack 0.1.0", "slog", "uuid", ] @@ -4471,7 +4480,7 @@ dependencies = [ "omicron-passwords 0.1.0", "omicron-sled-agent", "omicron-test-utils", - "omicron-workspace-hack", + "omicron-workspace-hack 0.1.0", "oximeter 0.1.0", "oximeter-client", "oximeter-collector", @@ -4491,10 +4500,10 @@ dependencies = [ name = "nexus-test-utils-macros" version = "0.1.0" dependencies = [ - "omicron-workspace-hack", + "omicron-workspace-hack 0.1.0", "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.37", ] [[package]] @@ -4510,7 +4519,7 @@ dependencies = [ "newtype_derive", "omicron-common 0.1.0", "omicron-passwords 0.1.0", - "omicron-workspace-hack", + "omicron-workspace-hack 0.1.0", "openssl", "openssl-probe", "openssl-sys", @@ -4529,14 +4538,13 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" dependencies = [ - "smallvec 1.11.0", + "smallvec 1.11.1", ] [[package]] name = "nix" version = "0.26.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a" +source = "git+https://github.com/jgallagher/nix?branch=r0.26-illumos#c1a3636db0524f194b714cfd117cd9b637b8b10e" dependencies = [ "bitflags 1.3.2", "cfg-if 1.0.0", @@ -4548,15 +4556,15 @@ dependencies = [ [[package]] name = "nix" -version = "0.26.2" -source = "git+https://github.com/jgallagher/nix?branch=r0.26-illumos#c1a3636db0524f194b714cfd117cd9b637b8b10e" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" dependencies = [ "bitflags 1.3.2", "cfg-if 1.0.0", "libc", "memoffset 0.7.1", "pin-utils", - "static_assertions", ] [[package]] @@ -4629,7 +4637,7 @@ dependencies = [ "num-traits", "rand 0.8.5", "serde", - "smallvec 1.11.0", + "smallvec 1.11.1", "zeroize", ] @@ -4650,7 +4658,7 @@ checksum = "9e6a0fd4f737c707bd9086cc16c925f294943eb62eb71499e9fd4cf71f8b9f4e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.37", ] [[package]] @@ -4701,7 +4709,7 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ - "hermit-abi 0.3.2", + "hermit-abi 0.3.3", "libc", ] @@ -4744,7 +4752,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.37", ] [[package]] @@ -4817,7 +4825,7 @@ dependencies = [ "foreign-types 0.3.2", "omicron-common 0.1.0", "omicron-test-utils", - "omicron-workspace-hack", + "omicron-workspace-hack 0.1.0", "openssl", "openssl-sys", "rcgen", @@ -4845,7 +4853,7 @@ dependencies = [ "lazy_static", "libc", "macaddr", - "omicron-workspace-hack", + "omicron-workspace-hack 0.1.0", "parse-display", "progenitor", "proptest", @@ -4854,7 +4862,7 @@ dependencies = [ "reqwest", "ring", "schemars", - "semver 1.0.18", + "semver 1.0.19", "serde", "serde_derive", "serde_human_bytes", @@ -4874,7 +4882,7 @@ dependencies = [ [[package]] name = "omicron-common" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#3dcc8d2eb648c87b42454882a2ce024b409cbb8c" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#8a11c709b49eed5bbe980d2f3e69ddf115278914" dependencies = [ "anyhow", "api_identity 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", @@ -4890,13 +4898,14 @@ dependencies = [ "ipnetwork", "lazy_static", "macaddr", + "omicron-workspace-hack 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", "parse-display", "progenitor", "rand 0.8.5", "reqwest", "ring", "schemars", - "semver 1.0.18", + "semver 1.0.19", "serde", "serde_derive", "serde_human_bytes", @@ -4916,10 +4925,10 @@ name = "omicron-deploy" version = "0.1.0" dependencies = [ "anyhow", - "clap 4.4.3", + "clap 4.4.6", "crossbeam", "omicron-package", - "omicron-workspace-hack", + "omicron-workspace-hack 0.1.0", "serde", "serde_derive", "thiserror", @@ -4933,7 +4942,7 @@ dependencies = [ "anyhow", "camino", "camino-tempfile", - "clap 4.4.3", + "clap 4.4.6", "dropshot", "expectorate", "futures", @@ -4945,7 +4954,7 @@ dependencies = [ "omicron-rpaths", "omicron-sled-agent", "omicron-test-utils", - "omicron-workspace-hack", + "omicron-workspace-hack 0.1.0", "openssl", "oxide-client", "pq-sys", @@ -4965,7 +4974,7 @@ dependencies = [ "anyhow", "async-trait", "ciborium", - "clap 4.4.3", + "clap 4.4.6", "crucible-smf", "dropshot", "expectorate", @@ -4979,7 +4988,7 @@ dependencies = [ "ipcc-key-value", "omicron-common 0.1.0", "omicron-test-utils", - "omicron-workspace-hack", + "omicron-workspace-hack 0.1.0", "once_cell", "openapi-lint", "openapiv3", @@ -5015,7 +5024,7 @@ dependencies = [ "camino", "cancel-safe-futures", "chrono", - "clap 4.4.3", + "clap 4.4.6", "cookie", "criterion", "crucible-agent-client", @@ -5057,7 +5066,7 @@ dependencies = [ "omicron-rpaths", "omicron-sled-agent", "omicron-test-utils", - "omicron-workspace-hack", + "omicron-workspace-hack 0.1.0", "once_cell", "openapi-lint", "openapiv3", @@ -5088,7 +5097,7 @@ dependencies = [ "rustls", "samael", "schemars", - "semver 1.0.18", + "semver 1.0.19", "serde", "serde_json", "serde_urlencoded", @@ -5121,7 +5130,7 @@ dependencies = [ "anyhow", "async-bb8-diesel", "chrono", - "clap 4.4.3", + "clap 4.4.6", "diesel", "dropshot", "expectorate", @@ -5137,7 +5146,7 @@ dependencies = [ "omicron-nexus", "omicron-rpaths", "omicron-test-utils", - "omicron-workspace-hack", + "omicron-workspace-hack 0.1.0", "pq-sys", "regex", "serde", @@ -5157,20 +5166,22 @@ name = "omicron-package" version = "0.1.0" dependencies = [ "anyhow", - "clap 4.4.3", + "clap 4.4.6", "expectorate", "futures", + "helios-fusion", + "helios-protostar", "hex", "illumos-utils", "indicatif", "omicron-common 0.1.0", - "omicron-workspace-hack", + "omicron-workspace-hack 0.1.0", "omicron-zone-package", "petgraph", "rayon", "reqwest", "ring", - "semver 1.0.18", + "semver 1.0.19", "serde", "serde_derive", "sled-hardware", @@ -5194,7 +5205,7 @@ version = "0.1.0" dependencies = [ "argon2", "criterion", - "omicron-workspace-hack", + "omicron-workspace-hack 0.1.0", "rand 0.8.5", "rust-argon2", "schemars", @@ -5206,9 +5217,10 @@ dependencies = [ [[package]] name = "omicron-passwords" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#3dcc8d2eb648c87b42454882a2ce024b409cbb8c" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#8a11c709b49eed5bbe980d2f3e69ddf115278914" dependencies = [ "argon2", + "omicron-workspace-hack 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", "rand 0.8.5", "schemars", "serde", @@ -5220,7 +5232,7 @@ dependencies = [ name = "omicron-rpaths" version = "0.1.0" dependencies = [ - "omicron-workspace-hack", + "omicron-workspace-hack 0.1.0", ] [[package]] @@ -5240,7 +5252,7 @@ dependencies = [ "cancel-safe-futures", "cfg-if 1.0.0", "chrono", - "clap 4.4.3", + "clap 4.4.6", "crucible-agent-client", "crucible-client-types", "ddm-admin-client", @@ -5254,6 +5266,9 @@ dependencies = [ "futures", "gateway-client", "glob", + "helios-fusion", + "helios-protostar", + "helios-tokamak", "http", "hyper", "hyper-staticfile", @@ -5267,7 +5282,7 @@ dependencies = [ "nexus-client 0.1.0", "omicron-common 0.1.0", "omicron-test-utils", - "omicron-workspace-hack", + "omicron-workspace-hack 0.1.0", "once_cell", "openapi-lint", "openapiv3", @@ -5284,10 +5299,9 @@ dependencies = [ "rcgen", "reqwest", "schemars", - "semver 1.0.18", + "semver 1.0.19", "serde", "serde_json", - "serial_test", "sha3", "sled-agent-client", "sled-hardware", @@ -5323,7 +5337,7 @@ dependencies = [ "http", "libc", "omicron-common 0.1.0", - "omicron-workspace-hack", + "omicron-workspace-hack 0.1.0", "pem", "rcgen", "regex", @@ -5349,12 +5363,12 @@ dependencies = [ "bitflags 2.4.0", "bitvec", "bstr 0.2.17", - "bstr 1.6.0", + "bstr 1.6.2", "bytes", "cc", "chrono", "cipher", - "clap 4.4.3", + "clap 4.4.6", "clap_builder", "console", "const-oid", @@ -5376,11 +5390,11 @@ dependencies = [ "generic-array", "getrandom 0.2.10", "hashbrown 0.13.2", - "hashbrown 0.14.0", + "hashbrown 0.14.1", "hex", "hyper", "hyper-rustls", - "indexmap 2.0.0", + "indexmap 2.0.2", "inout", "ipnetwork", "itertools 0.10.5", @@ -5400,7 +5414,7 @@ dependencies = [ "petgraph", "postgres-types", "ppv-lite86", - "predicates 3.0.3", + "predicates", "rand 0.8.5", "rand_chacha 0.3.1", "regex", @@ -5408,9 +5422,113 @@ dependencies = [ "regex-syntax 0.7.5", "reqwest", "ring", - "rustix 0.38.9", + "rustix", "schemars", - "semver 1.0.18", + "semver 1.0.19", + "serde", + "sha2", + "similar", + "slog", + "spin 0.9.8", + "string_cache", + "subtle", + "syn 1.0.109", + "syn 2.0.37", + "textwrap 0.16.0", + "time", + "time-macros", + "tokio", + "tokio-postgres", + "tokio-stream", + "toml 0.7.8", + "toml_datetime", + "toml_edit 0.19.15", + "tracing", + "trust-dns-proto", + "unicode-bidi", + "unicode-normalization", + "unicode-xid", + "usdt", + "uuid", + "yasna", + "zeroize", + "zip", +] + +[[package]] +name = "omicron-workspace-hack" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#8a11c709b49eed5bbe980d2f3e69ddf115278914" +dependencies = [ + "anyhow", + "bit-set", + "bit-vec", + "bitflags 1.3.2", + "bitflags 2.4.0", + "bitvec", + "bstr 0.2.17", + "bstr 1.6.2", + "bytes", + "cc", + "chrono", + "cipher", + "clap 4.4.6", + "clap_builder", + "console", + "const-oid", + "crossbeam-epoch", + "crossbeam-utils", + "crypto-common", + "diesel", + "digest", + "either", + "flate2", + "futures", + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", + "gateway-messages", + "generic-array", + "getrandom 0.2.10", + "hashbrown 0.13.2", + "hashbrown 0.14.1", + "hex", + "hyper", + "hyper-rustls", + "indexmap 2.0.2", + "inout", + "ipnetwork", + "itertools 0.10.5", + "lalrpop-util", + "lazy_static", + "libc", + "log", + "managed", + "memchr", + "mio", + "num-bigint", + "num-integer", + "num-iter", + "num-traits", + "once_cell", + "openapiv3", + "petgraph", + "postgres-types", + "ppv-lite86", + "predicates", + "rand 0.8.5", + "rand_chacha 0.3.1", + "regex", + "regex-automata 0.3.8", + "regex-syntax 0.7.5", + "reqwest", + "ring", + "rustix", + "schemars", + "semver 1.0.19", "serde", "sha2", "signature 2.1.0", @@ -5420,7 +5538,7 @@ dependencies = [ "string_cache", "subtle", "syn 1.0.109", - "syn 2.0.32", + "syn 2.0.37", "textwrap 0.16.0", "time", "time-macros", @@ -5455,7 +5573,7 @@ dependencies = [ "flate2", "futures-util", "reqwest", - "semver 1.0.18", + "semver 1.0.19", "serde", "serde_derive", "tar", @@ -5490,7 +5608,7 @@ version = "0.4.0" source = "git+https://github.com/oxidecomputer/openapi-lint?branch=main#bb69a3a4a184d966bac2a0df2be5c9038d9867d0" dependencies = [ "heck 0.4.1", - "indexmap 2.0.0", + "indexmap 2.0.2", "lazy_static", "openapiv3", "regex", @@ -5502,7 +5620,7 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75e56d5c441965b6425165b7e3223cc933ca469834f4a8b4786817a1f9dc4f13" dependencies = [ - "indexmap 2.0.0", + "indexmap 2.0.2", "serde", "serde_json", ] @@ -5530,7 +5648,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.37", ] [[package]] @@ -5565,7 +5683,7 @@ dependencies = [ "serde", "smoltcp 0.8.2", "version_check", - "zerocopy 0.6.3", + "zerocopy 0.6.4", ] [[package]] @@ -5648,7 +5766,7 @@ dependencies = [ "futures", "http", "hyper", - "omicron-workspace-hack", + "omicron-workspace-hack 0.1.0", "progenitor", "rand 0.8.5", "regress", @@ -5671,7 +5789,7 @@ dependencies = [ "opte", "serde", "smoltcp 0.8.2", - "zerocopy 0.6.3", + "zerocopy 0.6.4", ] [[package]] @@ -5683,7 +5801,7 @@ dependencies = [ "chrono", "num", "omicron-common 0.1.0", - "omicron-workspace-hack", + "omicron-workspace-hack 0.1.0", "oximeter-macro-impl 0.1.0", "rstest", "schemars", @@ -5696,12 +5814,13 @@ dependencies = [ [[package]] name = "oximeter" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#3dcc8d2eb648c87b42454882a2ce024b409cbb8c" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#8a11c709b49eed5bbe980d2f3e69ddf115278914" dependencies = [ "bytes", "chrono", - "num-traits", + "num", "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-workspace-hack 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", "oximeter-macro-impl 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", "schemars", "serde", @@ -5715,7 +5834,7 @@ version = "0.1.0" dependencies = [ "chrono", "omicron-common 0.1.0", - "omicron-workspace-hack", + "omicron-workspace-hack 0.1.0", "progenitor", "reqwest", "serde", @@ -5727,7 +5846,7 @@ dependencies = [ name = "oximeter-collector" version = "0.1.0" dependencies = [ - "clap 4.4.3", + "clap 4.4.6", "dropshot", "expectorate", "futures", @@ -5735,7 +5854,7 @@ dependencies = [ "nexus-client 0.1.0", "omicron-common 0.1.0", "omicron-test-utils", - "omicron-workspace-hack", + "omicron-workspace-hack 0.1.0", "openapi-lint", "openapiv3", "oximeter 0.1.0", @@ -5760,11 +5879,11 @@ dependencies = [ "async-trait", "bytes", "chrono", - "clap 4.4.3", + "clap 4.4.6", "dropshot", "itertools 0.11.0", "omicron-test-utils", - "omicron-workspace-hack", + "omicron-workspace-hack 0.1.0", "oximeter 0.1.0", "regex", "reqwest", @@ -5789,7 +5908,7 @@ dependencies = [ "dropshot", "futures", "http", - "omicron-workspace-hack", + "omicron-workspace-hack 0.1.0", "oximeter 0.1.0", "tokio", "uuid", @@ -5799,20 +5918,21 @@ dependencies = [ name = "oximeter-macro-impl" version = "0.1.0" dependencies = [ - "omicron-workspace-hack", + "omicron-workspace-hack 0.1.0", "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.37", ] [[package]] name = "oximeter-macro-impl" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#3dcc8d2eb648c87b42454882a2ce024b409cbb8c" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#8a11c709b49eed5bbe980d2f3e69ddf115278914" dependencies = [ + "omicron-workspace-hack 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.37", ] [[package]] @@ -5823,7 +5943,7 @@ dependencies = [ "dropshot", "nexus-client 0.1.0", "omicron-common 0.1.0", - "omicron-workspace-hack", + "omicron-workspace-hack 0.1.0", "oximeter 0.1.0", "reqwest", "schemars", @@ -5838,12 +5958,13 @@ dependencies = [ [[package]] name = "oximeter-producer" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/omicron?branch=main#3dcc8d2eb648c87b42454882a2ce024b409cbb8c" +source = "git+https://github.com/oxidecomputer/omicron?branch=main#8a11c709b49eed5bbe980d2f3e69ddf115278914" dependencies = [ "chrono", "dropshot", "nexus-client 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", "omicron-common 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", + "omicron-workspace-hack 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", "oximeter 0.1.0 (git+https://github.com/oxidecomputer/omicron?branch=main)", "reqwest", "schemars", @@ -5919,7 +6040,7 @@ dependencies = [ "instant", "libc", "redox_syscall 0.2.16", - "smallvec 1.11.0", + "smallvec 1.11.1", "winapi", ] @@ -5932,7 +6053,7 @@ dependencies = [ "cfg-if 1.0.0", "libc", "redox_syscall 0.3.5", - "smallvec 1.11.0", + "smallvec 1.11.1", "windows-targets 0.48.5", ] @@ -6004,18 +6125,18 @@ checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" [[package]] name = "path-absolutize" -version = "3.1.0" +version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43eb3595c63a214e1b37b44f44b0a84900ef7ae0b4c5efce59e123d246d7a0de" +checksum = "e4af381fe79fa195b4909485d99f73a80792331df0625188e707854f0b3383f5" dependencies = [ "path-dedot", ] [[package]] name = "path-dedot" -version = "3.1.0" +version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d55e486337acb9973cdea3ec5638c1b3bcb22e573b2b7b41969e0c744d5a15e" +checksum = "07ba0ad7e047712414213ff67533e6dd477af0a4e1d14fb52343e53d30ea9397" dependencies = [ "once_cell", ] @@ -6079,19 +6200,20 @@ checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" [[package]] name = "pest" -version = "2.7.2" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1acb4a4365a13f749a93f1a094a7805e5cfa0955373a9de860d962eaa3a5fe5a" +checksum = "c022f1e7b65d6a24c0dbbd5fb344c66881bc01f3e5ae74a1c8100f2f985d98a4" dependencies = [ + "memchr", "thiserror", "ucd-trie", ] [[package]] name = "pest_derive" -version = "2.7.2" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "666d00490d4ac815001da55838c500eafb0320019bbaa44444137c48b443a853" +checksum = "35513f630d46400a977c4cb58f78e1bfbe01434316e60c37d27b9ad6139c66d8" dependencies = [ "pest", "pest_generator", @@ -6099,22 +6221,22 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.7.2" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ca01446f50dbda87c1786af8770d535423fa8a53aec03b8f4e3d7eb10e0929" +checksum = "bc9fc1b9e7057baba189b5c626e2d6f40681ae5b6eb064dc7c7834101ec8123a" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.37", ] [[package]] name = "pest_meta" -version = "2.7.2" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56af0a30af74d0445c0bf6d9d051c979b516a1a5af790d251daee76005420a48" +checksum = "1df74e9e7ec4053ceb980e7c0c8bd3594e977fde1af91daba9c928e8e8c6708d" dependencies = [ "once_cell", "pest", @@ -6128,7 +6250,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9" dependencies = [ "fixedbitset", - "indexmap 2.0.0", + "indexmap 2.0.2", "serde", "serde_derive", ] @@ -6177,7 +6299,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.37", ] [[package]] @@ -6221,9 +6343,9 @@ checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" [[package]] name = "platforms" -version = "3.0.2" +version = "3.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3d7ddaed09e0eb771a79ab0fd64609ba0afb0a8366421957936ad14cbd13630" +checksum = "4503fa043bf02cee09a9582e9554b4c6403b2ef55e4612e96561d294419429f8" [[package]] name = "plotters" @@ -6369,28 +6491,14 @@ checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" [[package]] name = "predicates" -version = "2.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59230a63c37f3e18569bdb90e4a89cbf5bf8b06fea0b84e65ea10cc4df47addd" -dependencies = [ - "difflib", - "float-cmp", - "itertools 0.10.5", - "normalize-line-endings", - "predicates-core", - "regex", -] - -[[package]] -name = "predicates" -version = "3.0.3" +version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09963355b9f467184c04017ced4a2ba2d75cbcb4e7462690d388233253d4b1a9" +checksum = "6dfc28575c2e3f19cb3c73b93af36460ae898d426eba6fc15b9bd2a5220758a0" dependencies = [ "anstyle", "difflib", "float-cmp", - "itertools 0.10.5", + "itertools 0.11.0", "normalize-line-endings", "predicates-core", "regex", @@ -6436,12 +6544,12 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.12" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c64d9ba0963cdcea2e1b2230fbae2bab30eb25a174be395c41e764bfb65dd62" +checksum = "ae005bd773ab59b4725093fd7df83fd7892f7d8eafb48dbd7de6e024e4215f9d" dependencies = [ "proc-macro2", - "syn 2.0.32", + "syn 2.0.37", ] [[package]] @@ -6489,8 +6597,8 @@ dependencies = [ [[package]] name = "progenitor" -version = "0.3.0" -source = "git+https://github.com/oxidecomputer/progenitor?branch=main#5c941c0b41b0235031f3ade33a9c119945f1fd51" +version = "0.4.0" +source = "git+https://github.com/oxidecomputer/progenitor?branch=main#850867b33245b0a565aacf62a84657abb274bf1d" dependencies = [ "progenitor-client", "progenitor-impl", @@ -6500,8 +6608,8 @@ dependencies = [ [[package]] name = "progenitor-client" -version = "0.3.0" -source = "git+https://github.com/oxidecomputer/progenitor?branch=main#5c941c0b41b0235031f3ade33a9c119945f1fd51" +version = "0.4.0" +source = "git+https://github.com/oxidecomputer/progenitor?branch=main#850867b33245b0a565aacf62a84657abb274bf1d" dependencies = [ "bytes", "futures-core", @@ -6514,13 +6622,13 @@ dependencies = [ [[package]] name = "progenitor-impl" -version = "0.3.0" -source = "git+https://github.com/oxidecomputer/progenitor?branch=main#5c941c0b41b0235031f3ade33a9c119945f1fd51" +version = "0.4.0" +source = "git+https://github.com/oxidecomputer/progenitor?branch=main#850867b33245b0a565aacf62a84657abb274bf1d" dependencies = [ "getopts", "heck 0.4.1", "http", - "indexmap 2.0.0", + "indexmap 2.0.2", "openapiv3", "proc-macro2", "quote", @@ -6528,7 +6636,7 @@ dependencies = [ "schemars", "serde", "serde_json", - "syn 2.0.32", + "syn 2.0.37", "thiserror", "typify", "unicode-ident", @@ -6536,8 +6644,8 @@ dependencies = [ [[package]] name = "progenitor-macro" -version = "0.3.0" -source = "git+https://github.com/oxidecomputer/progenitor?branch=main#5c941c0b41b0235031f3ade33a9c119945f1fd51" +version = "0.4.0" +source = "git+https://github.com/oxidecomputer/progenitor?branch=main#850867b33245b0a565aacf62a84657abb274bf1d" dependencies = [ "openapiv3", "proc-macro2", @@ -6548,7 +6656,7 @@ dependencies = [ "serde_json", "serde_tokenstream 0.2.0", "serde_yaml", - "syn 2.0.32", + "syn 2.0.37", ] [[package]] @@ -6622,7 +6730,7 @@ dependencies = [ "bytes", "cfg-if 1.0.0", "chrono", - "clap 4.4.3", + "clap 4.4.6", "const_format", "crucible-client-types", "dropshot", @@ -6709,9 +6817,9 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] name = "quick-xml" -version = "0.23.1" +version = "0.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11bafc859c6815fbaffbbbf4229ecb767ac913fecb27f9ad4343662e9ef099ea" +checksum = "0ce5e73202a820a31f8a0ee32ada5e21029c81fd9e3ebf668a40832e4219d9d1" dependencies = [ "memchr", "serde", @@ -6870,7 +6978,7 @@ dependencies = [ "bitflags 2.4.0", "cassowary", "crossterm 0.27.0", - "indoc 2.0.3", + "indoc 2.0.4", "itertools 0.11.0", "paste", "strum", @@ -6880,9 +6988,9 @@ dependencies = [ [[package]] name = "rayon" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b" +checksum = "9c27db03db7734835b3f53954b534c91069375ce6ccaa2e065441e07d9b6cdb1" dependencies = [ "either", "rayon-core", @@ -6890,14 +6998,12 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d" +checksum = "5ce3fb6ad83f861aac485e76e1985cd109d9a3713802152be56c3b1f0e0658ed" dependencies = [ - "crossbeam-channel", "crossbeam-deque", "crossbeam-utils", - "num_cpus", ] [[package]] @@ -6987,7 +7093,7 @@ checksum = "7f7473c2cfcf90008193dd0e3e16599455cb601a9fce322b5bb55de799664925" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.37", ] [[package]] @@ -7229,7 +7335,7 @@ dependencies = [ "regex", "relative-path", "rustc_version 0.4.0", - "syn 2.0.32", + "syn 2.0.37", "unicode-ident", ] @@ -7254,7 +7360,7 @@ dependencies = [ "fallible-streaming-iterator", "hashlink", "libsqlite3-sys", - "smallvec 1.11.0", + "smallvec 1.11.1", ] [[package]] @@ -7346,7 +7452,7 @@ checksum = "a5885493fdf0be6cdff808d1533ce878d21cfa49c7086fa00c66355cd9141bfc" dependencies = [ "base64 0.21.4", "blake2b_simd", - "constant_time_eq 0.3.0", + "constant_time_eq", "crossbeam-utils", ] @@ -7377,7 +7483,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" dependencies = [ - "semver 1.0.18", + "semver 1.0.19", ] [[package]] @@ -7395,28 +7501,14 @@ dependencies = [ [[package]] name = "rustix" -version = "0.37.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d69718bf81c6127a49dc64e44a742e8bb9213c0ff8869a22c308f84c1d4ab06" -dependencies = [ - "bitflags 1.3.2", - "errno", - "io-lifetimes", - "libc", - "linux-raw-sys 0.3.8", - "windows-sys 0.48.0", -] - -[[package]] -name = "rustix" -version = "0.38.9" +version = "0.38.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bfe0f2582b4931a45d1fa608f8a8722e8b3c7ac54dd6d5f3b3212791fedef49" +checksum = "d2f9da0cbd88f9f09e7814e388301c8414c51c62aa6ce1e4b5c551d49d96e531" dependencies = [ "bitflags 2.4.0", "errno", "libc", - "linux-raw-sys 0.4.5", + "linux-raw-sys", "windows-sys 0.48.0", ] @@ -7455,9 +7547,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.101.4" +version = "0.101.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d93931baf2d282fff8d3a532bbfd7653f734643161b87e3e01e59a04439bf0d" +checksum = "3c7d5dece342910d9ba34d259310cae3e0154b873b35408b787b59bce53d34fe" dependencies = [ "ring", "untrusted", @@ -7518,8 +7610,8 @@ dependencies = [ [[package]] name = "samael" -version = "0.0.10" -source = "git+https://github.com/njaremko/samael?branch=master#52028e45d11ceb7114bf0c730a9971207e965602" +version = "0.0.12" +source = "git+https://github.com/njaremko/samael?branch=master#4974e40ac036462baaf4eff1c0c33b4a6802b6d0" dependencies = [ "base64 0.21.4", "bindgen", @@ -7537,7 +7629,7 @@ dependencies = [ "quick-xml", "rand 0.8.5", "serde", - "snafu", + "thiserror", "url", "uuid", ] @@ -7571,9 +7663,9 @@ dependencies = [ [[package]] name = "schemars" -version = "0.8.13" +version = "0.8.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "763f8cd0d4c71ed8389c90cb8100cba87e763bd01a8e614d4f0af97bcd50a161" +checksum = "1f7b0ce13155372a76ee2e1c5ffba1fe61ede73fbea5630d61eee6fac4929c0c" dependencies = [ "bytes", "chrono", @@ -7586,9 +7678,9 @@ dependencies = [ [[package]] name = "schemars_derive" -version = "0.8.13" +version = "0.8.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0f696e21e10fa546b7ffb1c9672c6de8fbc7a81acf59524386d8639bf12737" +checksum = "e85e2a16b12bdb763244c69ab79363d71db2b4b918a2def53f80b02e0574b13c" dependencies = [ "proc-macro2", "quote", @@ -7661,9 +7753,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.18" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918" +checksum = "ad977052201c6de01a8ef2aa3378c4bd23217a056337d1d6da40468d267a4fb0" dependencies = [ "serde", ] @@ -7732,7 +7824,7 @@ checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.37", ] [[package]] @@ -7793,7 +7885,7 @@ checksum = "8725e1dfadb3a50f7e5ce0b1a540466f6ed3fe7a0fca2ac2b8b831d31316bd00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.37", ] [[package]] @@ -7825,7 +7917,7 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.32", + "syn 2.0.37", ] [[package]] @@ -7865,7 +7957,7 @@ dependencies = [ "darling 0.20.3", "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.37", ] [[package]] @@ -7874,38 +7966,13 @@ version = "0.9.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a49e178e4452f45cb61d0cd8cebc1b0fafd3e41929e996cef79aa3aca91f574" dependencies = [ - "indexmap 2.0.0", + "indexmap 2.0.2", "itoa", "ryu", "serde", "unsafe-libyaml", ] -[[package]] -name = "serial_test" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c789ec87f4687d022a2405cf46e0cd6284889f1839de292cadeb6c6019506f2" -dependencies = [ - "dashmap", - "futures", - "lazy_static", - "log", - "parking_lot 0.12.1", - "serial_test_derive", -] - -[[package]] -name = "serial_test_derive" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b64f9e531ce97c88b4778aad0ceee079216071cffec6ac9b904277f8f92e7fe3" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "sha-1" version = "0.10.1" @@ -7919,9 +7986,9 @@ dependencies = [ [[package]] name = "sha1" -version = "0.10.5" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if 1.0.0", "cpufeatures", @@ -7930,9 +7997,9 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.7" +version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ "cfg-if 1.0.0", "cpufeatures", @@ -7957,9 +8024,9 @@ checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" [[package]] name = "shlex" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" +checksum = "a7cee0529a6d40f580e7a5e6c495c8fbfe21b7b52795ed4bb5e62cdf92bc6380" [[package]] name = "signal-hook" @@ -8078,7 +8145,7 @@ dependencies = [ "chrono", "ipnetwork", "omicron-common 0.1.0", - "omicron-workspace-hack", + "omicron-workspace-hack 0.1.0", "progenitor", "regress", "reqwest", @@ -8095,6 +8162,8 @@ dependencies = [ "camino", "cfg-if 1.0.0", "futures", + "helios-fusion", + "helios-tokamak", "illumos-devinfo", "illumos-utils", "key-manager", @@ -8104,11 +8173,10 @@ dependencies = [ "nexus-client 0.1.0", "omicron-common 0.1.0", "omicron-test-utils", - "omicron-workspace-hack", + "omicron-workspace-hack 0.1.0", "rand 0.8.5", "schemars", "serde", - "serial_test", "slog", "thiserror", "tofino", @@ -8233,21 +8301,21 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" +checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" [[package]] name = "smawk" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043" +checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" [[package]] name = "smf" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6015a9bbf269b84c928dc68e11680bbdfa6f065f1c6d5383ec134f55bab188b" +checksum = "4a491bfc47dffa70a3c267bc379e9de9f4b0a7195e474a94498189b177f8d18c" dependencies = [ "thiserror", ] @@ -8310,9 +8378,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2538b18701741680e0322a2302176d3253a35388e2e62f172f64f4f16605f877" +checksum = "4031e820eb552adee9295814c0ced9e5cf38ddf1e8b7d566d6de8e2538ea989e" dependencies = [ "libc", "windows-sys 0.48.0", @@ -8324,14 +8392,14 @@ version = "0.1.0" dependencies = [ "anyhow", "async-trait", - "clap 4.4.3", + "clap 4.4.6", "dropshot", "futures", "gateway-messages", "hex", "omicron-common 0.1.0", "omicron-gateway", - "omicron-workspace-hack", + "omicron-workspace-hack 0.1.0", "serde", "slog", "slog-dtrace", @@ -8443,10 +8511,11 @@ dependencies = [ [[package]] name = "stringprep" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db3737bde7edce97102e0e2b15365bf7a20bfdb5f60f4f9e8d7004258a51a8da" +checksum = "bb41d74e231a107a1b4ee36bd1214b11285b77768d2e3824aedafa988fd36ee6" dependencies = [ + "finl_unicode", "unicode-bidi", "unicode-normalization", ] @@ -8551,7 +8620,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.32", + "syn 2.0.37", ] [[package]] @@ -8583,9 +8652,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.32" +version = "2.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "239814284fd6f1a4ffe4ca893952cdd93c224b6a1571c9a9eadd670295c0c9e2" +checksum = "7303ef2c05cd654186cb250d29049a24840ca25d2747c25c0381c8d9e2f582e8" dependencies = [ "proc-macro2", "quote", @@ -8670,7 +8739,7 @@ dependencies = [ "cfg-if 1.0.0", "fastrand", "redox_syscall 0.3.5", - "rustix 0.38.9", + "rustix", "windows-sys 0.48.0", ] @@ -8687,20 +8756,20 @@ dependencies = [ [[package]] name = "termcolor" -version = "1.2.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" +checksum = "6093bad37da69aab9d123a8091e4be0aa4a03e4d601ec641c327398315f62b64" dependencies = [ "winapi-util", ] [[package]] name = "terminal_size" -version = "0.2.6" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e6bf6f19e9f8ed8d4048dc22981458ebcf406d67e94cd422e5ecd73d63b3237" +checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7" dependencies = [ - "rustix 0.37.23", + "rustix", "windows-sys 0.48.0", ] @@ -8753,22 +8822,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.48" +version = "1.0.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d6d7a740b8a666a7e828dd00da9c0dc290dff53154ea77ac109281de90589b7" +checksum = "1177e8c6d7ede7afde3585fd2513e611227efd6481bd78d2e82ba1ce16557ed4" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.48" +version = "1.0.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49922ecae66cc8a249b77e68d1d0623c1b2c514f0060c27cdc68bd62a1219d35" +checksum = "10712f02019e9288794769fba95cd6847df9874d49d871d062172f9dd41bc4cc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.37", ] [[package]] @@ -8823,9 +8892,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.27" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bb39ee79a6d8de55f48f2293a830e040392f1c5f16e336bdd1788cd0aadce07" +checksum = "426f806f4089c493dcac0d24c29c01e2c38baf8e30f1b716ee37e83d200b18fe" dependencies = [ "deranged", "itoa", @@ -8838,15 +8907,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.13" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "733d258752e9303d392b94b75230d07b0b9c489350c69b851fc6c065fde3e8f9" +checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20" dependencies = [ "time-core", ] @@ -8892,28 +8961,28 @@ source = "git+https://github.com/oxidecomputer/tlvc.git?branch=main#e644a21a7ca9 dependencies = [ "byteorder", "crc", - "zerocopy 0.6.3", + "zerocopy 0.6.4", ] [[package]] name = "tlvc" version = "0.3.1" -source = "git+https://github.com/oxidecomputer/tlvc.git#e644a21a7ca973ed31499106ea926bd63ebccc6f" +source = "git+https://github.com/oxidecomputer/tlvc#e644a21a7ca973ed31499106ea926bd63ebccc6f" dependencies = [ "byteorder", "crc", - "zerocopy 0.6.3", + "zerocopy 0.6.4", ] [[package]] name = "tlvc-text" version = "0.3.0" -source = "git+https://github.com/oxidecomputer/tlvc.git#e644a21a7ca973ed31499106ea926bd63ebccc6f" +source = "git+https://github.com/oxidecomputer/tlvc#e644a21a7ca973ed31499106ea926bd63ebccc6f" dependencies = [ "ron 0.8.1", "serde", - "tlvc 0.3.1 (git+https://github.com/oxidecomputer/tlvc.git)", - "zerocopy 0.6.3", + "tlvc 0.3.1 (git+https://github.com/oxidecomputer/tlvc)", + "zerocopy 0.6.4", ] [[package]] @@ -8942,7 +9011,7 @@ dependencies = [ "parking_lot 0.12.1", "pin-project-lite", "signal-hook-registry", - "socket2 0.5.3", + "socket2 0.5.4", "tokio-macros", "windows-sys 0.48.0", ] @@ -8955,7 +9024,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.37", ] [[package]] @@ -8988,7 +9057,7 @@ dependencies = [ "postgres-protocol", "postgres-types", "rand 0.8.5", - "socket2 0.5.3", + "socket2 0.5.4", "tokio", "tokio-util", "whoami", @@ -9041,9 +9110,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.8" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" +checksum = "1d68074620f57a0b21594d9735eb2e98ab38b17f80d3fcb189fca266771ca60d" dependencies = [ "bytes", "futures-core", @@ -9076,14 +9145,14 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c226a7bba6d859b63c92c4b4fe69c5b6b72d0cb897dbc8e6012298e6154cb56e" +checksum = "1bc1433177506450fe920e46a4f9812d0c211f5dd556da10e731a0a3dfa151f0" dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit 0.20.0", + "toml_edit 0.20.1", ] [[package]] @@ -9101,7 +9170,7 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.0.0", + "indexmap 2.0.2", "serde", "serde_spanned", "toml_datetime", @@ -9110,11 +9179,11 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.20.0" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ff63e60a958cefbb518ae1fd6566af80d9d4be430a33f3723dfc47d1d411d95" +checksum = "ca676d9ba1a322c1b64eb8045a5ec5c0cfb0c9d08e15e9ff622589ad5221c8fe" dependencies = [ - "indexmap 2.0.0", + "indexmap 2.0.2", "serde", "serde_spanned", "toml_datetime", @@ -9194,7 +9263,7 @@ checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.37", ] [[package]] @@ -9243,7 +9312,7 @@ dependencies = [ "ipnet", "lazy_static", "rand 0.8.5", - "smallvec 1.11.0", + "smallvec 1.11.1", "thiserror", "tinyvec", "tokio", @@ -9264,7 +9333,7 @@ dependencies = [ "lru-cache", "parking_lot 0.12.1", "resolv-conf", - "smallvec 1.11.0", + "smallvec 1.11.1", "thiserror", "tokio", "tracing", @@ -9322,15 +9391,15 @@ dependencies = [ "assert_cmd", "camino", "chrono", - "clap 4.4.3", + "clap 4.4.6", "console", "datatest-stable", "fs-err", "humantime", "omicron-common 0.1.0", "omicron-test-utils", - "omicron-workspace-hack", - "predicates 3.0.3", + "omicron-workspace-hack 0.1.0", + "predicates", "slog", "slog-async", "slog-envlogger", @@ -9358,7 +9427,7 @@ dependencies = [ "itertools 0.11.0", "omicron-common 0.1.0", "omicron-test-utils", - "omicron-workspace-hack", + "omicron-workspace-hack 0.1.0", "rand 0.8.5", "ring", "serde", @@ -9434,14 +9503,14 @@ dependencies = [ [[package]] name = "typenum" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "typify" -version = "0.0.13" -source = "git+https://github.com/oxidecomputer/typify#de16c4238a2b34400d0fece086a6469951c3236b" +version = "0.0.14" +source = "git+https://github.com/oxidecomputer/typify#eea570e2333b3dd9ee5bfbe5d786e52af5b7b595" dependencies = [ "typify-impl", "typify-macro", @@ -9449,8 +9518,8 @@ dependencies = [ [[package]] name = "typify-impl" -version = "0.0.13" -source = "git+https://github.com/oxidecomputer/typify#de16c4238a2b34400d0fece086a6469951c3236b" +version = "0.0.14" +source = "git+https://github.com/oxidecomputer/typify#eea570e2333b3dd9ee5bfbe5d786e52af5b7b595" dependencies = [ "heck 0.4.1", "log", @@ -9459,15 +9528,15 @@ dependencies = [ "regress", "schemars", "serde_json", - "syn 2.0.32", + "syn 2.0.37", "thiserror", "unicode-ident", ] [[package]] name = "typify-macro" -version = "0.0.13" -source = "git+https://github.com/oxidecomputer/typify#de16c4238a2b34400d0fece086a6469951c3236b" +version = "0.0.14" +source = "git+https://github.com/oxidecomputer/typify#eea570e2333b3dd9ee5bfbe5d786e52af5b7b595" dependencies = [ "proc-macro2", "quote", @@ -9475,7 +9544,7 @@ dependencies = [ "serde", "serde_json", "serde_tokenstream 0.2.0", - "syn 2.0.32", + "syn 2.0.37", "typify-impl", ] @@ -9535,9 +9604,9 @@ checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" [[package]] name = "unicode-width" -version = "0.1.10" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" +checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" [[package]] name = "unicode-xid" @@ -9581,11 +9650,11 @@ dependencies = [ "derive-where", "either", "futures", - "indexmap 2.0.0", + "indexmap 2.0.2", "indicatif", "linear-map", "omicron-test-utils", - "omicron-workspace-hack", + "omicron-workspace-hack 0.1.0", "owo-colors", "petgraph", "schemars", @@ -9706,9 +9775,9 @@ checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" [[package]] name = "vergen" -version = "8.2.4" +version = "8.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbc5ad0d9d26b2c49a5ab7da76c3e79d3ee37e7821799f8223fcb8f2f391a2e7" +checksum = "85e7dc29b3c54a2ea67ef4f953d5ec0c4085035c0ae2d325be1c0d2144bd9f16" dependencies = [ "anyhow", "git2", @@ -9849,7 +9918,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.37", "wasm-bindgen-shared", ] @@ -9883,7 +9952,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.37", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -9925,13 +9994,14 @@ checksum = "14247bb57be4f377dfb94c72830b8ce8fc6beac03cf4bf7b9732eadd414123fc" [[package]] name = "which" -version = "4.4.0" +version = "4.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2441c784c52b289a054b7201fc93253e288f094e2f4be9058343127c4226a269" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" dependencies = [ "either", - "libc", + "home", "once_cell", + "rustix", ] [[package]] @@ -9953,24 +10023,24 @@ dependencies = [ "buf-list", "camino", "ciborium", - "clap 4.4.3", + "clap 4.4.6", "crossterm 0.27.0", "futures", "hex", "humantime", - "indexmap 2.0.0", + "indexmap 2.0.2", "indicatif", "itertools 0.11.0", "omicron-common 0.1.0", "omicron-passwords 0.1.0", - "omicron-workspace-hack", + "omicron-workspace-hack 0.1.0", "once_cell", "owo-colors", "proptest", "ratatui", "reqwest", "rpassword", - "semver 1.0.18", + "semver 1.0.19", "serde", "serde_json", "shell-words", @@ -9999,7 +10069,7 @@ dependencies = [ "anyhow", "gateway-client", "omicron-common 0.1.0", - "omicron-workspace-hack", + "omicron-workspace-hack 0.1.0", "schemars", "serde", "serde_json", @@ -10015,9 +10085,9 @@ dependencies = [ "bytes", "camino", "ciborium", - "clap 4.4.3", + "clap 4.4.6", "crossterm 0.27.0", - "omicron-workspace-hack", + "omicron-workspace-hack 0.1.0", "ratatui", "reedline", "serde", @@ -10039,7 +10109,7 @@ dependencies = [ "bytes", "camino", "camino-tempfile", - "clap 4.4.3", + "clap 4.4.6", "ddm-admin-client", "debug-ignore", "display-error-chain", @@ -10054,6 +10124,8 @@ dependencies = [ "gateway-client", "gateway-messages", "gateway-test-utils", + "helios-fusion", + "helios-tokamak", "hex", "http", "hubtools", @@ -10068,7 +10140,7 @@ dependencies = [ "omicron-common 0.1.0", "omicron-passwords 0.1.0", "omicron-test-utils", - "omicron-workspace-hack", + "omicron-workspace-hack 0.1.0", "openapi-lint", "openapiv3", "rand 0.8.5", @@ -10105,7 +10177,7 @@ dependencies = [ "chrono", "installinator-common", "ipnetwork", - "omicron-workspace-hack", + "omicron-workspace-hack 0.1.0", "progenitor", "regress", "reqwest", @@ -10142,9 +10214,9 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" dependencies = [ "winapi", ] @@ -10352,7 +10424,7 @@ dependencies = [ "camino", "cargo_metadata", "cargo_toml", - "clap 4.4.3", + "clap 4.4.6", ] [[package]] @@ -10384,12 +10456,12 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3b9c234616391070b0b173963ebc65a9195068e7ed3731c6edac2ec45ebe106" +checksum = "20707b61725734c595e840fb3704378a0cd2b9c74cc9e6e20724838fc6a1e2f9" dependencies = [ "byteorder", - "zerocopy-derive 0.6.3", + "zerocopy-derive 0.6.4", ] [[package]] @@ -10405,13 +10477,13 @@ dependencies = [ [[package]] name = "zerocopy-derive" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f7f3a471f98d0a61c34322fbbfd10c384b07687f680d4119813713f72308d91" +checksum = "56097d5b91d711293a42be9289403896b68654625021732067eac7a4ca388a1f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.37", ] [[package]] @@ -10431,7 +10503,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.37", ] [[package]] @@ -10449,9 +10521,9 @@ dependencies = [ [[package]] name = "zone" -version = "0.2.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0545a42fbd7a81245726d54a0146cb4fd93882ebb6da50d60acf2e37394f198" +checksum = "a62a428a79ea2224ce8ab05d6d8a21bdd7b4b68a8dbc1230511677a56e72ef22" dependencies = [ "itertools 0.10.5", "thiserror", @@ -10461,9 +10533,9 @@ dependencies = [ [[package]] name = "zone_cfg_derive" -version = "0.2.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef224b009d070d3b1adb9e375fcf8ec2f1948a412c3bbf8755c0ef4e3f91ef94" +checksum = "d5c4f01d3785e222d5aca11c9813e9c46b69abfe258756c99c9b628683626cc8" dependencies = [ "heck 0.4.1", "proc-macro-error", diff --git a/Cargo.toml b/Cargo.toml index 0e194394f9e..8d8baf60ae2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -56,6 +56,9 @@ members = [ "sled-hardware", "sp-sim", "test-utils", + "helios/fusion", + "helios/protostar", + "helios/tokamak", "tufaceous-lib", "tufaceous", "update-engine", @@ -118,6 +121,9 @@ default-members = [ "sled-hardware", "sp-sim", "test-utils", + "helios/fusion", + "helios/protostar", + "helios/tokamak", "tufaceous", "tufaceous-lib", "update-engine", @@ -194,6 +200,9 @@ gateway-test-utils = { path = "gateway-test-utils" } glob = "0.3.1" headers = "0.3.9" heck = "0.4" +helios-fusion = { path = "helios/fusion" } +helios-protostar = { path = "helios/protostar" } +helios-tokamak = { path = "helios/tokamak" } hex = "0.4.3" hex-literal = "0.4.1" hkdf = "0.12.3" @@ -221,7 +230,6 @@ libc = "0.2.148" linear-map = "1.2.0" macaddr = { version = "1.0.1", features = ["serde_std"] } mime_guess = "2.0.4" -mockall = "0.11" newtype_derive = "0.1.6" nexus-client = { path = "nexus-client" } nexus-db-model = { path = "nexus/db-model" } @@ -308,10 +316,10 @@ serde_path_to_error = "0.1.14" serde_tokenstream = "0.2" serde_urlencoded = "0.7.1" serde_with = "2.3.3" -serial_test = "0.10" sha2 = "0.10.7" sha3 = "0.10.8" shell-words = "1.1.0" +shlex = "1.1.0" signal-hook = "0.3" signal-hook-tokio = { version = "0.3", features = [ "futures-v0_3" ] } similar-asserts = "1.5.0" @@ -323,7 +331,7 @@ slog-async = "2.8" slog-dtrace = "0.2" slog-envlogger = "2.2" slog-term = "2.9" -smf = "0.2" +smf = "0.2.2" snafu = "0.7" sp-sim = { path = "sp-sim" } sprockets-common = { git = "http://github.com/oxidecomputer/sprockets", rev = "77df31efa5619d0767ffc837ef7468101608aee9" } @@ -374,7 +382,7 @@ wicket-common = { path = "wicket-common" } wicketd-client = { path = "wicketd-client" } zeroize = { version = "1.6.0", features = ["zeroize_derive", "std"] } zip = { version = "0.6.6", default-features = false, features = ["deflate","bzip2"] } -zone = { version = "0.2", default-features = false, features = ["async"] } +zone = { version = "0.3", default-features = false, features = ["async"] } [profile.dev] panic = "abort" diff --git a/helios/README.adoc b/helios/README.adoc new file mode 100644 index 00000000000..3531f737054 --- /dev/null +++ b/helios/README.adoc @@ -0,0 +1,13 @@ +:showtitle: +:toc: left +:icons: font + += helios + +This directory describes various interfaces for acting upon a Helios system. + +* `fusion` describes an interface for interacting with the underlying OS. +* `tokamak` provides a fake implementation of `fusion` - not enough +to be virtualized, but enough to test code depending on `fusion` under test. +* `protostar` provides a real implementation of `fusion`, which should actually +make calls to the underlying OS. diff --git a/helios/fusion/Cargo.toml b/helios/fusion/Cargo.toml new file mode 100644 index 00000000000..d189f9fc0ab --- /dev/null +++ b/helios/fusion/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "helios-fusion" +description = "Interface to access an underlying Helios system" +version = "0.1.0" +edition = "2021" +license = "MPL-2.0" + +[dependencies] +async-trait.workspace = true +itertools.workspace = true +shlex.workspace = true +slog.workspace = true +thiserror.workspace = true +tokio.workspace = true +omicron-workspace-hack = { version = "0.1", path = "../../workspace-hack" } diff --git a/helios/fusion/src/error.rs b/helios/fusion/src/error.rs new file mode 100644 index 00000000000..6b131d35381 --- /dev/null +++ b/helios/fusion/src/error.rs @@ -0,0 +1,75 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use slog::error; + +#[derive(Debug)] +pub struct FailureInfo { + pub command: String, + pub status: std::process::ExitStatus, + pub stdout: String, + pub stderr: String, +} + +impl std::fmt::Display for FailureInfo { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!( + f, + "Command [{}] executed and failed with status: {}", + self.command, self.status + )?; + write!(f, " stdout: {}", self.stdout)?; + write!(f, " stderr: {}", self.stderr) + } +} + +#[derive(thiserror::Error, Debug)] +pub enum ExecutionError { + #[error("Failed to start execution of [{command}]: {err}")] + ExecutionStart { command: String, err: std::io::Error }, + + #[error("{0}")] + CommandFailure(Box), + + #[error("Failed to manipulate process contract: {err}")] + ContractFailure { err: std::io::Error }, + + #[error("Zone not running")] + NotRunning, +} + +/// Convenience trait for turning [std::process::Command] into a String. +pub trait AsCommandStr { + fn into_str(&self) -> String; +} + +impl AsCommandStr for String { + fn into_str(&self) -> String { + self.into() + } +} + +impl AsCommandStr for &std::process::Command { + fn into_str(&self) -> String { + shlex::join( + std::iter::once(self.get_program()) + .chain(self.get_args()) + .map(|s| s.to_str().expect("Invalid UTF-8")), + ) + } +} + +impl ExecutionError { + pub fn from_output( + command: S, + output: &std::process::Output, + ) -> Self { + Self::CommandFailure(Box::new(FailureInfo { + command: command.into_str(), + status: output.status, + stdout: String::from_utf8_lossy(&output.stdout).to_string(), + stderr: String::from_utf8_lossy(&output.stderr).to_string(), + })) + } +} diff --git a/helios/fusion/src/executor.rs b/helios/fusion/src/executor.rs new file mode 100644 index 00000000000..8fd000f964b --- /dev/null +++ b/helios/fusion/src/executor.rs @@ -0,0 +1,111 @@ +// 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 used to interact with the underlying host system. + +use crate::{error::ExecutionError, input::Input, output::Output}; + +use async_trait::async_trait; +use itertools::Itertools; +use slog::{debug, info, Logger}; +use std::io::{Read, Write}; +use std::process::Command; +use std::str::from_utf8; +use std::sync::Arc; + +fn to_space_separated_string(iter: T) -> String +where + T: IntoIterator, + I: std::fmt::Debug, +{ + Itertools::intersperse( + iter.into_iter().map(|arg| format!("{arg:?}")), + " ".into(), + ) + .collect::() +} + +pub fn log_input(log: &Logger, id: u64, command: &Command) { + info!( + log, + "running command via executor"; "id" => id, "command" => %Input::from(command) + ); + debug!( + log, + "running command via executor"; "id" => id, "envs" => %to_space_separated_string(command.get_envs()) + ); +} + +pub fn log_output(log: &Logger, id: u64, output: &Output) { + info!( + log, + "finished running command via executor"; + "id" => id, + "succeeded" => output.status.success(), + "status" => output.status.code() + ); + if !output.stdout.is_empty() { + debug!( + log, + "finished command stdout"; + "id" => id, + "stdout" => from_utf8(&output.stdout).unwrap_or(""), + ); + } + if !output.stderr.is_empty() { + debug!( + log, + "finished command stderr"; + "id" => id, + "stderr" => from_utf8(&output.stderr).unwrap_or(""), + ); + } +} + +/// Describes the commonly-used "safe-to-reference" type describing the +/// Executor as a trait object. +pub type BoxedExecutor = Arc; + +/// Describes an "executor", which can run [Command]s and return a response. +/// +/// - In production, this is usually `helios_protostar`'s executor. +/// - Under test, this can be customized, and `helios_tokamak`'s executor may be used. +#[async_trait] +pub trait Executor: Send + Sync { + /// Executes a task, waiting for it to complete, and returning output. + async fn execute_async( + &self, + command: &mut tokio::process::Command, + ) -> Result; + + /// Executes a task, waiting for it to complete, and returning output. + fn execute(&self, command: &mut Command) -> Result; + + /// Spawns a task, without waiting for it to complete. + fn spawn( + &self, + command: &mut Command, + ) -> Result; +} + +/// A wrapper around a spawned [Child] process. +pub type BoxedChild = Box; + +/// A child process spawned by the executor. +pub trait Child: Send { + /// Accesses the stdin of the spawned child, as a Writer. + fn take_stdin(&mut self) -> Option>; + + /// Accesses the stdout of the spawned child, as a Reader. + fn take_stdout(&mut self) -> Option>; + + /// Accesses the stderr of the spawned child, as a Reader. + fn take_stderr(&mut self) -> Option>; + + /// OS-assigned PID identifier for the child + fn id(&self) -> u32; + + /// Waits for the child to complete, and returns the output. + fn wait(self: Box) -> Result; +} diff --git a/helios/fusion/src/input.rs b/helios/fusion/src/input.rs new file mode 100644 index 00000000000..1450ebd42c0 --- /dev/null +++ b/helios/fusion/src/input.rs @@ -0,0 +1,67 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use std::process::Command; + +/// Wrapper around the input of a [std::process::Command] as strings. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Input { + pub program: String, + pub args: Vec, + pub envs: Vec<(String, String)>, +} + +impl Input { + pub fn new>(program: S, args: Vec) -> Self { + Self { + program: program.into(), + args: args.into_iter().map(|s| s.into()).collect(), + envs: vec![], + } + } + + /// Short-hand for a whitespace-separated string, which can be provided + /// "like a shell command". + pub fn shell>(input: S) -> Self { + let mut args = shlex::split(input.as_ref()).expect("Invalid input"); + + if args.is_empty() { + panic!("Empty input is invalid"); + } + + Self::new(args.remove(0), args) + } +} + +impl std::fmt::Display for Input { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", shlex::quote(&self.program))?; + for arg in &self.args { + write!(f, " {}", shlex::quote(arg))?; + } + Ok(()) + } +} + +fn os_str_to_string(s: &std::ffi::OsStr) -> String { + s.to_string_lossy().to_string() +} + +impl From<&Command> for Input { + fn from(command: &Command) -> Self { + Self { + program: os_str_to_string(command.get_program()), + args: command.get_args().map(os_str_to_string).collect(), + envs: command + .get_envs() + .map(|(k, v)| { + ( + os_str_to_string(k), + os_str_to_string(v.unwrap_or_default()), + ) + }) + .collect(), + } + } +} diff --git a/helios/fusion/src/lib.rs b/helios/fusion/src/lib.rs new file mode 100644 index 00000000000..3fc9d5a1727 --- /dev/null +++ b/helios/fusion/src/lib.rs @@ -0,0 +1,17 @@ +// 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 used to interact with the underlying host system. + +mod error; +mod executor; +mod input; +mod output; + +pub use error::*; +pub use executor::*; +pub use input::*; +pub use output::*; + +pub const PFEXEC: &str = "/usr/bin/pfexec"; diff --git a/helios/fusion/src/output.rs b/helios/fusion/src/output.rs new file mode 100644 index 00000000000..b7c5bd401ea --- /dev/null +++ b/helios/fusion/src/output.rs @@ -0,0 +1,45 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use std::os::unix::process::ExitStatusExt; +use std::process::ExitStatus; + +pub type Output = std::process::Output; + +/// Convenience functions for usage in tests, to perform common operations +/// with minimal boilerplate. +pub trait OutputExt: Sized { + fn success() -> Self; + fn failure() -> Self; + fn set_stdout>(self, stdout: S) -> Self; + fn set_stderr>(self, stderr: S) -> Self; +} + +impl OutputExt for Output { + fn success() -> Self { + Output { + status: ExitStatus::from_raw(0), + stdout: vec![], + stderr: vec![], + } + } + + fn failure() -> Self { + Output { + status: ExitStatus::from_raw(-1), + stdout: vec![], + stderr: vec![], + } + } + + fn set_stdout>(mut self, stdout: S) -> Self { + self.stdout = stdout.as_ref().as_bytes().to_vec(); + self + } + + fn set_stderr>(mut self, stderr: S) -> Self { + self.stderr = stderr.as_ref().as_bytes().to_vec(); + self + } +} diff --git a/helios/protostar/Cargo.toml b/helios/protostar/Cargo.toml new file mode 100644 index 00000000000..f20cf7177c6 --- /dev/null +++ b/helios/protostar/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "helios-protostar" +description = "Utilities to interact with a real Helios system" +version = "0.1.0" +edition = "2021" +license = "MPL-2.0" + +[dependencies] +anyhow.workspace = true +async-trait.workspace = true +camino.workspace = true +cfg-if.workspace = true +futures.workspace = true +helios-fusion.workspace = true +itertools.workspace = true +libc.workspace = true +omicron-common.workspace = true +schemars.workspace = true +serde.workspace = true +shlex.workspace = true +slog.workspace = true +smf.workspace = true +thiserror.workspace = true +tokio.workspace = true +uuid.workspace = true +zone.workspace = true +omicron-workspace-hack = { version = "0.1", path = "../../workspace-hack" } + +[dev-dependencies] +omicron-test-utils.workspace = true diff --git a/helios/protostar/README.adoc b/helios/protostar/README.adoc new file mode 100644 index 00000000000..591447e05cf --- /dev/null +++ b/helios/protostar/README.adoc @@ -0,0 +1,22 @@ +:showtitle: +:toc: left +:icons: font + += protostar + +[TIP] +A protostar is one of the earliest stages in a star's lifecycle, which gathers +mass before eventually igniting and becoming a main-sequence star. When this +happens, a star generates energy via fusion. + +`protostar` is an implementation of the `fusion` interface which is designed to +execute on a real Helios system. This interface, while necessary, is notoriously +difficult to test. Although we encourage the addition of arbitrary host-interfacing +calls here, we recommend the following: + +1. Keeping them as simple as possible, with the understanding that code within +this crate can only sufficiently be tested via end-to-end tests. +2. When adding new interfaces to the host, access them via the `fusion` interface, +and provide a "fake" implementation in the `tokamak` crate. This will allow callers +integrating with host interfaces to write tests, and simulate the underlying system +behaviors. diff --git a/helios/protostar/src/lib.rs b/helios/protostar/src/lib.rs new file mode 100644 index 00000000000..5ad75e4f01c --- /dev/null +++ b/helios/protostar/src/lib.rs @@ -0,0 +1,154 @@ +// 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/. + +//! A "real" [Executor] implementation, which sends commands to the host. + +use helios_fusion::{ + log_input, log_output, AsCommandStr, BoxedChild, BoxedExecutor, Child, + ExecutionError, Executor, Input, Output, +}; + +use async_trait::async_trait; +use slog::{error, Logger}; +use std::io::{Read, Write}; +use std::process::{Command, Stdio}; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::Arc; + +/// Implements [Executor] by running commands against the host system. +pub struct HostExecutor { + log: slog::Logger, + counter: std::sync::atomic::AtomicU64, +} + +impl HostExecutor { + pub fn new(log: Logger) -> Arc { + Arc::new(Self { log, counter: AtomicU64::new(0) }) + } + + pub fn as_executor(self: Arc) -> BoxedExecutor { + self + } + + fn prepare(&self, command: &Command) -> u64 { + let id = self.counter.fetch_add(1, Ordering::SeqCst); + log_input(&self.log, id, command); + id + } + + fn finalize( + &self, + command: &Command, + id: u64, + output: Output, + ) -> Result { + log_output(&self.log, id, &output); + if !output.status.success() { + return Err(ExecutionError::from_output(command, &output)); + } + Ok(output) + } +} + +#[async_trait] +impl Executor for HostExecutor { + async fn execute_async( + &self, + command: &mut tokio::process::Command, + ) -> Result { + let id = self.prepare(command.as_std()); + let output = command.output().await.map_err(|err| { + error!(self.log, "Could not start program asynchronously!"; "id" => id); + ExecutionError::ExecutionStart { + command: Input::from(command.as_std()).to_string(), + err, + } + })?; + self.finalize(command.as_std(), id, output) + } + + fn execute(&self, command: &mut Command) -> Result { + let id = self.prepare(command); + let output = command.output().map_err(|err| { + error!(self.log, "Could not start program!"; "id" => id); + ExecutionError::ExecutionStart { + command: Input::from(&*command).to_string(), + err, + } + })?; + self.finalize(command, id, output) + } + + fn spawn( + &self, + command: &mut Command, + ) -> Result { + let command_str = (&*command).into_str(); + Ok(Box::new(SpawnedChild { + child: Some( + command + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .map_err(|err| ExecutionError::ExecutionStart { + command: command_str.clone(), + err, + })?, + ), + command_str, + })) + } +} + +/// A real, host-controlled child process +pub struct SpawnedChild { + command_str: String, + child: Option, +} + +impl Child for SpawnedChild { + fn take_stdin(&mut self) -> Option> { + self.child + .as_mut()? + .stdin + .take() + .map(|s| Box::new(s) as Box) + } + + fn take_stdout(&mut self) -> Option> { + self.child + .as_mut()? + .stdout + .take() + .map(|s| Box::new(s) as Box) + } + + fn take_stderr(&mut self) -> Option> { + self.child + .as_mut()? + .stderr + .take() + .map(|s| Box::new(s) as Box) + } + + fn id(&self) -> u32 { + self.child.as_ref().expect("No child").id() + } + + fn wait(mut self: Box) -> Result { + let output = + self.child.take().unwrap().wait_with_output().map_err(|err| { + ExecutionError::ExecutionStart { + command: self.command_str.clone(), + err, + } + })?; + + if !output.status.success() { + return Err(ExecutionError::from_output(self.command_str, &output)); + } + + Ok(output) + } +} diff --git a/helios/tokamak/Cargo.toml b/helios/tokamak/Cargo.toml new file mode 100644 index 00000000000..0ab10049647 --- /dev/null +++ b/helios/tokamak/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "helios-tokamak" +description = "Utilities to create a fake Helios system" +version = "0.1.0" +edition = "2021" +license = "MPL-2.0" + +[dependencies] +anyhow.workspace = true +async-trait.workspace = true +camino.workspace = true +cfg-if.workspace = true +futures.workspace = true +helios-fusion.workspace = true +itertools.workspace = true +libc.workspace = true +omicron-common.workspace = true +schemars.workspace = true +serde.workspace = true +shlex.workspace = true +slog.workspace = true +smf.workspace = true +thiserror.workspace = true +tokio.workspace = true +uuid.workspace = true +zone.workspace = true +omicron-workspace-hack = { version = "0.1", path = "../../workspace-hack" } + +[dev-dependencies] +omicron-test-utils.workspace = true diff --git a/helios/tokamak/README.adoc b/helios/tokamak/README.adoc new file mode 100644 index 00000000000..54cde184c48 --- /dev/null +++ b/helios/tokamak/README.adoc @@ -0,0 +1,16 @@ +:showtitle: +:toc: left +:icons: font + += tokamak + +[TIP] +A tokamak is a magnetic confinement fusion device which can be used to +produce nuclear fusion reactions. In other words, they're one of our tools for +creating a "artificial sun". + +`tokamak` is a toolkit implementing the `fusion` interface which allows callers +to run an emulated Helios system. It does not intend to operate as a virtual +machine, nor run any real workloads: the purpose of this crate is to give +callers an opportunity to test very specific interfaces against the underlying +host, without actually requiring a host that has undergone that behavior. diff --git a/helios/tokamak/src/executor.rs b/helios/tokamak/src/executor.rs new file mode 100644 index 00000000000..888d900a220 --- /dev/null +++ b/helios/tokamak/src/executor.rs @@ -0,0 +1,309 @@ +// 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/. + +//! A "fake" [Executor] implementation, which can respond to host requests. + +use crate::shared_byte_queue::SharedByteQueue; + +use async_trait::async_trait; +use helios_fusion::{ + log_input, log_output, BoxedChild, BoxedExecutor, Child, ExecutionError, + Executor, Input, Output, OutputExt, +}; +use slog::Logger; +use std::io::{Read, Write}; +use std::process::Command; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::{Arc, Mutex}; + +/// Handler called when spawning a fake child process +type SpawnFn = dyn FnMut(&mut FakeChild) + Send + Sync; +type BoxedSpawnFn = Box; + +/// Handler called when awaiting a fake child process +type WaitFn = dyn FnMut(&mut FakeChild) -> Output + Send + Sync; +type BoxedWaitFn = Box; + +pub struct FakeExecutorBuilder { + log: Logger, + spawn_handler: Option, + wait_handler: Option, +} + +impl FakeExecutorBuilder { + pub fn new(log: Logger) -> Self { + Self { log, spawn_handler: None, wait_handler: None } + } + + pub fn spawn_handler(mut self, f: BoxedSpawnFn) -> Self { + self.spawn_handler = Some(f); + self + } + + pub fn wait_handler(mut self, f: BoxedWaitFn) -> Self { + self.wait_handler = Some(f); + self + } + + /// Convenience function to register the sequence with a [FakeExecutor]. + pub fn with_sequence(mut self, mut sequence: CommandSequence) -> Self { + self.wait_handler = + Some(Box::new(move |child: &mut FakeChild| -> Output { + sequence.execute(child.command()) + })); + self + } + + pub fn build(self) -> Arc { + FakeExecutor::new( + self.log, + self.spawn_handler.unwrap_or_else(|| Box::new(|_cmd| ())), + self.wait_handler + .unwrap_or_else(|| Box::new(|_cmd| Output::success())), + ) + } +} + +pub(crate) struct FakeExecutorInner { + log: Logger, + counter: AtomicU64, + spawn_handler: Mutex, + wait_handler: Mutex, +} + +/// An executor which can expect certain inputs, and respond with specific outputs. +pub struct FakeExecutor { + inner: Arc, +} + +impl FakeExecutor { + pub fn new( + log: Logger, + s: BoxedSpawnFn, + w: BoxedWaitFn, + ) -> Arc { + Arc::new(Self { + inner: Arc::new(FakeExecutorInner { + log, + counter: AtomicU64::new(0), + spawn_handler: Mutex::new(s), + wait_handler: Mutex::new(w), + }), + }) + } + + /// Perform some type coercion to access a commonly-used trait object. + pub fn as_executor(self: Arc) -> BoxedExecutor { + self + } + + fn execute_internal( + &self, + command: &Command, + ) -> Result { + let id = self.inner.counter.fetch_add(1, Ordering::SeqCst); + log_input(&self.inner.log, id, command); + + let mut child = FakeChild::new(id, command, self.inner.clone()); + + // Call our handler function with the caller-provided functions. + // + // This performs both the "spawn" and "wait" actions back-to-back. + self.inner.spawn_handler.lock().unwrap()(&mut child); + let output = self.inner.wait_handler.lock().unwrap()(&mut child); + log_output(&self.inner.log, id, &output); + + if !output.status.success() { + return Err(ExecutionError::from_output(command, &output)); + } + Ok(output) + } +} + +#[async_trait] +impl Executor for FakeExecutor { + // NOTE: We aren't actually performing any async operations -- it's up to + // the caller to control the (synchronous) handlers. + // + // However, this still provides testability, while letting the "real + // executor" make truly async calls while launching processes. + async fn execute_async( + &self, + command: &mut tokio::process::Command, + ) -> Result { + self.execute_internal(command.as_std()) + } + + fn execute(&self, command: &mut Command) -> Result { + self.execute_internal(command) + } + + fn spawn( + &self, + command: &mut Command, + ) -> Result { + let id = self.inner.counter.fetch_add(1, Ordering::SeqCst); + log_input(&self.inner.log, id, command); + + Ok(FakeChild::new(id, command, self.inner.clone())) + } +} + +/// A child spawned by a [FakeExecutor]. +pub struct FakeChild { + id: u64, + command: Command, + executor: Arc, + stdin: SharedByteQueue, + stdout: SharedByteQueue, + stderr: SharedByteQueue, +} + +impl FakeChild { + fn new( + id: u64, + command: &Command, + executor: Arc, + ) -> Box { + // std::process::Command -- somewhat reasonably - doesn't implement Copy + // or Clone. However, we'd like to be able to reference it in the + // FakeChild, independently of where it was spawned. + // + // Manually copy the relevant pieces of the incoming command. + let mut copy_command = Command::new(command.get_program()); + copy_command.args(command.get_args()); + copy_command.envs(command.get_envs().filter_map(|(k, v)| { + if let Some(v) = v { + Some((k, v)) + } else { + None + } + })); + + Box::new(FakeChild { + id, + command: copy_command, + executor, + stdin: SharedByteQueue::new(), + stdout: SharedByteQueue::new(), + stderr: SharedByteQueue::new(), + }) + } + + pub fn command(&self) -> &Command { + &self.command + } +} + +impl Child for FakeChild { + fn take_stdin(&mut self) -> Option> { + Some(Box::new(self.stdin.clone())) + } + + fn take_stdout(&mut self) -> Option> { + Some(Box::new(self.stdout.clone())) + } + + fn take_stderr(&mut self) -> Option> { + Some(Box::new(self.stderr.clone())) + } + + fn id(&self) -> u32 { + self.id.try_into().expect("u32 overflow") + } + + fn wait(mut self: Box) -> Result { + let executor = self.executor.clone(); + let output = executor.wait_handler.lock().unwrap()(&mut self); + log_output(&self.executor.log, self.id, &output); + if !output.status.success() { + return Err(ExecutionError::from_output(&self.command, &output)); + } + Ok(output) + } +} + +type DynamicHandler = Box Output + Send + Sync>; + +enum HandledCommand { + Static { input: Input, output: Output }, + Dynamic { handler: DynamicHandler }, +} + +/// A handler that may be used for setting inputs/outputs to the executor +/// when these commands are known ahead-of-time. +/// +/// See: [FakeExecutorBuilder::with_sequence] for integration with a [FakeExecutor]. +pub struct CommandSequence { + expected: Vec, + index: usize, +} + +impl CommandSequence { + pub fn new() -> Self { + Self { expected: Vec::new(), index: 0 } + } + + /// Expects a static "input" to exactly produce some "output". + pub fn expect(&mut self, input: Input, output: Output) { + self.expected.push(HandledCommand::Static { input, output }); + } + + /// A helper for [Self::expect] which quietly succeeds. + pub fn expect_ok>(&mut self, input: S) { + self.expect(Input::shell(input), Output::success()) + } + + /// A helper for [Self::expect] which quietly fails. + pub fn expect_fail>(&mut self, input: S) { + self.expect(Input::shell(input), Output::failure()) + } + + /// Expects a dynamic handler to be invoked to dynamically + /// determine the output of this call. + pub fn expect_dynamic(&mut self, handler: DynamicHandler) { + self.expected.push(HandledCommand::Dynamic { handler }); + } + + fn execute(&mut self, command: &Command) -> Output { + let observed_input = Input::from(command); + let expected = &mut self + .expected + .get_mut(self.index) + .unwrap_or_else(|| panic!("Unexpected command: {observed_input}")); + self.index += 1; + + match expected { + HandledCommand::Static { input, output } => { + assert_eq!(&observed_input, input, "Unexpected input command"); + output.clone() + } + HandledCommand::Dynamic { ref mut handler } => { + handler(observed_input) + } + } + } +} + +impl Drop for CommandSequence { + fn drop(&mut self) { + let expected = self.expected.len(); + let actual = self.index; + if actual < expected { + let next = &self.expected[actual]; + let tip = match next { + HandledCommand::Static { input, .. } => input.to_string(), + HandledCommand::Dynamic { .. } => { + "".to_string() + } + }; + let errmsg = format!("Only saw {actual} calls, expected {expected}\nNext would have been: {tip}"); + if !std::thread::panicking() { + assert!(false, "{errmsg}"); + } else { + eprintln!("{errmsg}"); + } + } + } +} diff --git a/helios/tokamak/src/lib.rs b/helios/tokamak/src/lib.rs new file mode 100644 index 00000000000..352d2763c73 --- /dev/null +++ b/helios/tokamak/src/lib.rs @@ -0,0 +1,8 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +mod executor; +mod shared_byte_queue; + +pub use executor::*; diff --git a/helios/tokamak/src/shared_byte_queue.rs b/helios/tokamak/src/shared_byte_queue.rs new file mode 100644 index 00000000000..52e91c266bc --- /dev/null +++ b/helios/tokamak/src/shared_byte_queue.rs @@ -0,0 +1,37 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use std::collections::VecDeque; +use std::sync::{Arc, Mutex}; + +/// A queue of bytes that can selectively act as a reader or writer, +/// which can also be cloned. +/// +/// This is primarily used to emulate stdin / stdout / stderr. +#[derive(Clone)] +pub struct SharedByteQueue { + buf: Arc>>, +} + +impl SharedByteQueue { + pub fn new() -> Self { + Self { buf: Arc::new(Mutex::new(VecDeque::new())) } + } +} + +impl std::io::Write for SharedByteQueue { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + self.buf.lock().unwrap().write(buf) + } + + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } +} + +impl std::io::Read for SharedByteQueue { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + self.buf.lock().unwrap().read(buf) + } +} diff --git a/illumos-utils/Cargo.toml b/illumos-utils/Cargo.toml index e292097bc59..e4333eee83c 100644 --- a/illumos-utils/Cargo.toml +++ b/illumos-utils/Cargo.toml @@ -12,14 +12,18 @@ bhyve_api.workspace = true byteorder.workspace = true camino.workspace = true cfg-if.workspace = true +debug-ignore.workspace = true futures.workspace = true +helios-fusion.workspace = true ipnetwork.workspace = true +itertools.workspace = true libc.workspace = true macaddr.workspace = true omicron-common.workspace = true oxide-vpc.workspace = true schemars.workspace = true serde.workspace = true +shlex.workspace = true slog.workspace = true smf.workspace = true thiserror.workspace = true @@ -27,19 +31,14 @@ tokio.workspace = true uuid.workspace = true zone.workspace = true -# only enabled via the `testing` feature -mockall = { workspace = true, optional = true } omicron-workspace-hack = { version = "0.1", path = "../workspace-hack" } [target.'cfg(target_os = "illumos")'.dependencies] opte-ioctl.workspace = true [dev-dependencies] -mockall.workspace = true +helios-tokamak.workspace = true +omicron-test-utils.workspace = true regress.workspace = true serde_json.workspace = true toml.workspace = true - -[features] -# Enable to generate MockZones -testing = ["mockall"] diff --git a/illumos-utils/src/dladm.rs b/illumos-utils/src/dladm.rs index 1e8a7e70bf4..e42add426a3 100644 --- a/illumos-utils/src/dladm.rs +++ b/illumos-utils/src/dladm.rs @@ -6,7 +6,7 @@ use crate::link::{Link, LinkKind}; use crate::zone::IPADM; -use crate::{execute, ExecutionError, PFEXEC}; +use helios_fusion::{BoxedExecutor, ExecutionError, PFEXEC}; use omicron_common::api::external::MacAddr; use omicron_common::vlan::VlanID; use serde::{Deserialize, Serialize}; @@ -176,24 +176,29 @@ impl VnicSource for PhysicalLink { /// Wraps commands for interacting with data links. pub struct Dladm {} -#[cfg_attr(any(test, feature = "testing"), mockall::automock, allow(dead_code))] impl Dladm { /// Creates an etherstub, or returns one which already exists. - pub fn ensure_etherstub(name: &str) -> Result { - if let Ok(stub) = Self::get_etherstub(name) { + pub fn ensure_etherstub( + executor: &BoxedExecutor, + name: &str, + ) -> Result { + if let Ok(stub) = Self::get_etherstub(executor, name) { return Ok(stub); } let mut command = std::process::Command::new(PFEXEC); let cmd = command.args(&[DLADM, "create-etherstub", "-t", name]); - execute(cmd)?; + executor.execute(cmd)?; Ok(Etherstub(name.to_string())) } /// Finds an etherstub. - fn get_etherstub(name: &str) -> Result { + fn get_etherstub( + executor: &BoxedExecutor, + name: &str, + ) -> Result { let mut command = std::process::Command::new(PFEXEC); let cmd = command.args(&[DLADM, "show-etherstub", name]); - execute(cmd)?; + executor.execute(cmd)?; Ok(Etherstub(name.to_string())) } @@ -202,6 +207,7 @@ impl Dladm { /// This VNIC is not tracked like [`crate::link::Link`], because /// it is expected to exist for the lifetime of the sled. pub fn ensure_etherstub_vnic( + executor: &BoxedExecutor, source: &Etherstub, ) -> Result { let (vnic_name, mtu) = match source.0.as_str() { @@ -209,80 +215,95 @@ impl Dladm { BOOTSTRAP_ETHERSTUB_NAME => (BOOTSTRAP_ETHERSTUB_VNIC_NAME, 1500), _ => unreachable!(), }; - if let Ok(vnic) = Self::get_etherstub_vnic(vnic_name) { + if let Ok(vnic) = Self::get_etherstub_vnic(executor, vnic_name) { return Ok(vnic); } - Self::create_vnic(source, vnic_name, None, None, mtu)?; + Self::create_vnic(executor, source, vnic_name, None, None, mtu)?; Ok(EtherstubVnic(vnic_name.to_string())) } - fn get_etherstub_vnic(name: &str) -> Result { + fn get_etherstub_vnic( + executor: &BoxedExecutor, + name: &str, + ) -> Result { let mut command = std::process::Command::new(PFEXEC); let cmd = command.args(&[DLADM, "show-vnic", name]); - execute(cmd)?; + executor.execute(cmd)?; Ok(EtherstubVnic(name.to_string())) } // Return the name of the IP interface over the etherstub VNIC, if it // exists. fn get_etherstub_vnic_interface( + executor: &BoxedExecutor, name: &str, ) -> Result { let mut cmd = std::process::Command::new(PFEXEC); let cmd = cmd.args(&[IPADM, "show-if", "-p", "-o", "IFNAME", name]); - execute(cmd)?; + executor.execute(cmd)?; Ok(name.to_string()) } /// Delete the VNIC over the inter-zone comms etherstub. - pub fn delete_etherstub_vnic(name: &str) -> Result<(), ExecutionError> { + pub fn delete_etherstub_vnic( + executor: &BoxedExecutor, + name: &str, + ) -> Result<(), ExecutionError> { // It's not clear why, but this requires deleting the _interface_ that's // over the VNIC first. Other VNICs don't require this for some reason. - if Self::get_etherstub_vnic_interface(name).is_ok() { + if Self::get_etherstub_vnic_interface(executor, name).is_ok() { let mut cmd = std::process::Command::new(PFEXEC); let cmd = cmd.args(&[IPADM, "delete-if", name]); - execute(cmd)?; + executor.execute(cmd)?; } - if Self::get_etherstub_vnic(name).is_ok() { + if Self::get_etherstub_vnic(executor, name).is_ok() { let mut cmd = std::process::Command::new(PFEXEC); let cmd = cmd.args(&[DLADM, "delete-vnic", name]); - execute(cmd)?; + executor.execute(cmd)?; } Ok(()) } /// Delete the inter-zone comms etherstub. - pub fn delete_etherstub(name: &str) -> Result<(), ExecutionError> { - if Self::get_etherstub(name).is_ok() { + pub fn delete_etherstub( + executor: &BoxedExecutor, + name: &str, + ) -> Result<(), ExecutionError> { + if Self::get_etherstub(executor, name).is_ok() { let mut cmd = std::process::Command::new(PFEXEC); let cmd = cmd.args(&[DLADM, "delete-etherstub", name]); - execute(cmd)?; + executor.execute(cmd)?; } Ok(()) } /// Verify that the given link exists - pub fn verify_link(link: &str) -> Result { + pub fn verify_link( + executor: &BoxedExecutor, + link: &str, + ) -> Result { let mut command = std::process::Command::new(PFEXEC); let cmd = command.args(&[DLADM, "show-link", "-p", "-o", "LINK", link]); - let output = execute(cmd)?; + let output = executor.execute(cmd)?; match String::from_utf8_lossy(&output.stdout) .lines() .next() .map(|s| s.trim()) { - Some(x) if x == link => Ok(Link::wrap_physical(link)), + Some(x) if x == link => Ok(Link::wrap_physical(executor, link)), _ => Err(FindPhysicalLinkError::NoPhysicalLinkFound), } } /// Returns the name of the first observed physical data link. - pub fn find_physical() -> Result { + pub fn find_physical( + executor: &BoxedExecutor, + ) -> Result { // TODO: This is arbitrary, but we're currently grabbing the first // physical device. Should we have a more sophisticated method for // selection? - Self::list_physical()? + Self::list_physical(executor)? .into_iter() .next() .ok_or_else(|| FindPhysicalLinkError::NoPhysicalLinkFound) @@ -291,10 +312,12 @@ impl Dladm { /// List the extant physical data links on the system. /// /// Note that this returns _all_ links. - pub fn list_physical() -> Result, FindPhysicalLinkError> { + pub fn list_physical( + executor: &BoxedExecutor, + ) -> Result, FindPhysicalLinkError> { let mut command = std::process::Command::new(PFEXEC); let cmd = command.args(&[DLADM, "show-phys", "-p", "-o", "LINK"]); - let output = execute(cmd)?; + let output = executor.execute(cmd)?; std::str::from_utf8(&output.stdout) .map_err(FindPhysicalLinkError::NonUtf8Output) .map(|stdout| { @@ -306,7 +329,10 @@ impl Dladm { } /// Returns the MAC address of a physical link. - pub fn get_mac(link: &PhysicalLink) -> Result { + pub fn get_mac( + executor: &BoxedExecutor, + link: &PhysicalLink, + ) -> Result { let mut command = std::process::Command::new(PFEXEC); let cmd = command.args(&[ DLADM, @@ -317,7 +343,7 @@ impl Dladm { "ADDRESS", &link.0, ]); - let output = execute(cmd)?; + let output = executor.execute(cmd)?; let name = String::from_utf8_lossy(&output.stdout) .lines() .next() @@ -344,6 +370,7 @@ impl Dladm { /// * `mac`: An optional unicast MAC address for the newly created NIC. /// * `vlan`: An optional VLAN ID for VLAN tagging. pub fn create_vnic( + executor: &BoxedExecutor, source: &T, vnic_name: &str, mac: Option, @@ -375,7 +402,7 @@ impl Dladm { args.push(vnic_name.to_string()); let cmd = command.args(&args); - execute(cmd).map_err(|err| CreateVnicError { + executor.execute(cmd).map_err(|err| CreateVnicError { name: vnic_name.to_string(), link: source.name().to_string(), err, @@ -395,7 +422,7 @@ impl Dladm { &prop, vnic_name, ]); - execute(cmd).map_err(|err| CreateVnicError { + executor.execute(cmd).map_err(|err| CreateVnicError { name: vnic_name.to_string(), link: source.name().to_string(), err, @@ -405,10 +432,13 @@ impl Dladm { } /// Returns VNICs that may be managed by the Sled Agent. - pub fn get_vnics() -> Result, GetVnicError> { + pub fn get_vnics( + executor: &BoxedExecutor, + ) -> Result, GetVnicError> { let mut command = std::process::Command::new(PFEXEC); let cmd = command.args(&[DLADM, "show-vnic", "-p", "-o", "LINK"]); - let output = execute(cmd).map_err(|err| GetVnicError { err })?; + let output = + executor.execute(cmd).map_err(|err| GetVnicError { err })?; let vnics = String::from_utf8_lossy(&output.stdout) .lines() @@ -425,10 +455,13 @@ impl Dladm { } /// Returns simnet links masquerading as tfport devices - pub fn get_simulated_tfports() -> Result, GetSimnetError> { + pub fn get_simulated_tfports( + executor: &BoxedExecutor, + ) -> Result, GetSimnetError> { let mut command = std::process::Command::new(PFEXEC); let cmd = command.args(&[DLADM, "show-simnet", "-p", "-o", "LINK"]); - let output = execute(cmd).map_err(|err| GetSimnetError { err })?; + let output = + executor.execute(cmd).map_err(|err| GetSimnetError { err })?; let tfports = String::from_utf8_lossy(&output.stdout) .lines() @@ -444,16 +477,21 @@ impl Dladm { } /// Remove a vnic from the sled. - pub fn delete_vnic(name: &str) -> Result<(), DeleteVnicError> { + pub fn delete_vnic( + executor: &BoxedExecutor, + name: &str, + ) -> Result<(), DeleteVnicError> { let mut command = std::process::Command::new(PFEXEC); let cmd = command.args(&[DLADM, "delete-vnic", name]); - execute(cmd) + executor + .execute(cmd) .map_err(|err| DeleteVnicError { name: name.to_string(), err })?; Ok(()) } /// Get a link property value on a VNIC pub fn get_linkprop( + executor: &BoxedExecutor, vnic: &str, prop_name: &str, ) -> Result { @@ -468,7 +506,7 @@ impl Dladm { prop_name, vnic, ]); - let result = execute(cmd).map_err(|err| GetLinkpropError { + let result = executor.execute(cmd).map_err(|err| GetLinkpropError { link_name: vnic.to_string(), prop_name: prop_name.to_string(), err, @@ -477,6 +515,7 @@ impl Dladm { } /// Set a link property on a VNIC pub fn set_linkprop( + executor: &BoxedExecutor, vnic: &str, prop_name: &str, prop_value: &str, @@ -485,7 +524,7 @@ impl Dladm { let prop = format!("{}={}", prop_name, prop_value); let cmd = command.args(&[DLADM, "set-linkprop", "-t", "-p", &prop, vnic]); - execute(cmd).map_err(|err| SetLinkpropError { + executor.execute(cmd).map_err(|err| SetLinkpropError { link_name: vnic.to_string(), prop_name: prop_name.to_string(), prop_value: prop_value.to_string(), @@ -496,6 +535,7 @@ impl Dladm { /// Reset a link property on a VNIC pub fn reset_linkprop( + executor: &BoxedExecutor, vnic: &str, prop_name: &str, ) -> Result<(), ResetLinkpropError> { @@ -508,7 +548,7 @@ impl Dladm { prop_name, vnic, ]); - execute(cmd).map_err(|err| ResetLinkpropError { + executor.execute(cmd).map_err(|err| ResetLinkpropError { link_name: vnic.to_string(), prop_name: prop_name.to_string(), err, @@ -516,3 +556,135 @@ impl Dladm { Ok(()) } } + +#[cfg(test)] +mod test { + use super::*; + use helios_fusion::{Input, OutputExt}; + use helios_tokamak::{CommandSequence, FakeExecutorBuilder}; + use omicron_test_utils::dev; + use std::process::Output; + + #[test] + fn ensure_new_etherstub() { + let logctx = dev::test_setup_log("ensure_new_etherstub"); + + let mut handler = CommandSequence::new(); + handler.expect_fail(format!("{PFEXEC} {DLADM} show-etherstub mystub1")); + handler + .expect_ok(format!("{PFEXEC} {DLADM} create-etherstub -t mystub1")); + + let executor = FakeExecutorBuilder::new(logctx.log.clone()) + .with_sequence(handler) + .build(); + + let etherstub = + Dladm::ensure_etherstub(&executor.as_executor(), "mystub1") + .expect("Failed to ensure etherstub"); + assert_eq!(etherstub.0, "mystub1"); + + logctx.cleanup_successful(); + } + + #[test] + fn ensure_existing_etherstub() { + let logctx = dev::test_setup_log("ensure_existing_etherstub"); + + let mut handler = CommandSequence::new(); + handler.expect_ok(format!("{PFEXEC} {DLADM} show-etherstub mystub1")); + let executor = FakeExecutorBuilder::new(logctx.log.clone()) + .with_sequence(handler) + .build(); + + let etherstub = + Dladm::ensure_etherstub(&executor.as_executor(), "mystub1") + .expect("Failed to ensure etherstub"); + assert_eq!(etherstub.0, "mystub1"); + + logctx.cleanup_successful(); + } + + #[test] + fn ensure_existing_etherstub_vnic() { + let logctx = dev::test_setup_log("ensure_existing_etherstub_vnic"); + + let mut handler = CommandSequence::new(); + handler.expect_ok(format!( + "{PFEXEC} {DLADM} show-etherstub {UNDERLAY_ETHERSTUB_NAME}" + )); + handler.expect_ok(format!( + "{PFEXEC} {DLADM} show-vnic {UNDERLAY_ETHERSTUB_VNIC_NAME}" + )); + let executor = FakeExecutorBuilder::new(logctx.log.clone()) + .with_sequence(handler) + .build(); + + let executor = &executor.as_executor(); + let etherstub = + Dladm::ensure_etherstub(executor, UNDERLAY_ETHERSTUB_NAME) + .expect("Failed to ensure etherstub"); + let _vnic = Dladm::ensure_etherstub_vnic(executor, ðerstub) + .expect("Failed to ensure etherstub VNIC"); + + logctx.cleanup_successful(); + } + + #[test] + fn ensure_new_etherstub_vnic() { + let logctx = dev::test_setup_log("ensure_new_etherstub_vnic"); + + let mut handler = CommandSequence::new(); + handler.expect_ok(format!( + "{PFEXEC} {DLADM} show-etherstub {UNDERLAY_ETHERSTUB_NAME}" + )); + handler.expect_fail(format!( + "{PFEXEC} {DLADM} show-vnic {UNDERLAY_ETHERSTUB_VNIC_NAME}" + )); + handler.expect_ok(format!( + "{PFEXEC} {DLADM} create-vnic -t -l {UNDERLAY_ETHERSTUB_NAME} \ + -p mtu=9000 {UNDERLAY_ETHERSTUB_VNIC_NAME}" + )); + handler.expect_ok(format!( + "{PFEXEC} {DLADM} set-linkprop -t -p mtu=9000 \ + {UNDERLAY_ETHERSTUB_VNIC_NAME}" + )); + let executor = FakeExecutorBuilder::new(logctx.log.clone()) + .with_sequence(handler) + .build(); + + let executor = &executor.as_executor(); + let etherstub = + Dladm::ensure_etherstub(executor, UNDERLAY_ETHERSTUB_NAME) + .expect("Failed to ensure etherstub"); + let _vnic = Dladm::ensure_etherstub_vnic(executor, ðerstub) + .expect("Failed to ensure etherstub VNIC"); + + logctx.cleanup_successful(); + } + + #[test] + fn only_parse_oxide_vnics() { + let logctx = dev::test_setup_log("only_parse_oxide_vnics"); + + let mut handler = CommandSequence::new(); + handler.expect( + Input::shell(format!("{PFEXEC} {DLADM} show-vnic -p -o LINK")), + Output::success().set_stdout( + "oxVnic\nvopteVnic\nInvalid\noxBootstrapVnic\nInvalid", + ), + ); + let executor = FakeExecutorBuilder::new(logctx.log.clone()) + .with_sequence(handler) + .build(); + + let executor = &executor.as_executor(); + let vnics = Dladm::get_vnics(executor).expect("Failed to get VNICs"); + + assert_eq!(vnics[0], "oxVnic"); + assert_eq!(vnics[1], "vopteVnic"); + assert_eq!(vnics[2], "oxBootstrapVnic"); + assert_eq!(vnics.len(), 3); + + logctx.cleanup_successful(); + } +} diff --git a/illumos-utils/src/dumpadm.rs b/illumos-utils/src/dumpadm.rs index feb470e4941..fb882f72e1b 100644 --- a/illumos-utils/src/dumpadm.rs +++ b/illumos-utils/src/dumpadm.rs @@ -1,3 +1,9 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use helios_fusion::{BoxedExecutor, ExecutionError}; + use byteorder::{LittleEndian, ReadBytesExt}; use camino::Utf8PathBuf; use std::ffi::OsString; @@ -101,10 +107,10 @@ pub enum DumpAdmError { SavecoreFailure(OsString), #[error("Failed to execute dumpadm process: {0}")] - ExecDumpadm(std::io::Error), + ExecDumpadm(ExecutionError), #[error("Failed to execute savecore process: {0}")] - ExecSavecore(std::io::Error), + ExecSavecore(ExecutionError), } /// Invokes `dumpadm(8)` to configure the kernel to dump core into the given @@ -114,6 +120,7 @@ pub enum DumpAdmError { /// On success, returns Ok(Some(stdout)) if `savecore(8)` was invoked, or /// Ok(None) if it wasn't. pub fn dumpadm( + executor: &BoxedExecutor, dump_slice: &Utf8PathBuf, savecore_dir: Option<&Utf8PathBuf>, ) -> Result, DumpAdmError> { @@ -150,7 +157,7 @@ pub fn dumpadm( cmd.arg("-s").arg(tmp_crash); } - let out = cmd.output().map_err(DumpAdmError::ExecDumpadm)?; + let out = executor.execute(&mut cmd).map_err(DumpAdmError::ExecDumpadm)?; match out.status.code() { Some(0) => { @@ -158,7 +165,7 @@ pub fn dumpadm( if savecore_dir.is_some() { // and does the dump slice have one to save off if let Ok(true) = dump_flag_is_valid(dump_slice) { - return savecore(); + return savecore(executor); } } Ok(None) @@ -191,11 +198,13 @@ pub fn dumpadm( // in the event that savecore(8) terminates before it finishes copying the // dump, the incomplete dump will remain in the target directory, but the next // invocation will overwrite it, because `bounds` wasn't created/incremented. -fn savecore() -> Result, DumpAdmError> { +fn savecore( + executor: &BoxedExecutor, +) -> Result, DumpAdmError> { let mut cmd = Command::new(SAVECORE); cmd.env_clear(); cmd.arg("-v"); - let out = cmd.output().map_err(DumpAdmError::ExecSavecore)?; + let out = executor.execute(&mut cmd).map_err(DumpAdmError::ExecSavecore)?; if out.status.success() { if out.stdout.is_empty() || out.stdout == vec![b'\n'] { Ok(None) diff --git a/illumos-utils/src/fstyp.rs b/illumos-utils/src/fstyp.rs index dbbe3442dcc..41d0317f1dc 100644 --- a/illumos-utils/src/fstyp.rs +++ b/illumos-utils/src/fstyp.rs @@ -5,8 +5,8 @@ //! Helper for calling fstyp. use crate::zpool::ZpoolName; -use crate::{execute, PFEXEC}; use camino::Utf8Path; +use helios_fusion::{BoxedExecutor, ExecutionError, PFEXEC}; use std::str::FromStr; const FSTYP: &str = "/usr/sbin/fstyp"; @@ -17,7 +17,7 @@ pub enum Error { NotValidUtf8(#[from] std::string::FromUtf8Error), #[error("fstyp execution error: {0}")] - Execution(#[from] crate::ExecutionError), + Execution(#[from] ExecutionError), #[error("Failed to find zpool name from fstyp")] NotFound, @@ -29,18 +29,20 @@ pub enum Error { /// Wraps 'fstyp' command. pub struct Fstyp {} -#[cfg_attr(test, mockall::automock)] impl Fstyp { /// Executes the 'fstyp' command and parses the name of a zpool from it, if /// one exists. - pub fn get_zpool(path: &Utf8Path) -> Result { + pub fn get_zpool( + executor: &BoxedExecutor, + path: &Utf8Path, + ) -> Result { let mut cmd = std::process::Command::new(PFEXEC); cmd.env_clear(); cmd.env("LC_ALL", "C.UTF-8"); let cmd = cmd.arg(FSTYP).arg("-a").arg(path); - let output = execute(cmd).map_err(Error::from)?; + let output = executor.execute(cmd).map_err(Error::from)?; let stdout = String::from_utf8(output.stdout)?; let mut seen_zfs_marker = false; diff --git a/illumos-utils/src/lib.rs b/illumos-utils/src/lib.rs index 1d585ee7864..d1375224ccc 100644 --- a/illumos-utils/src/lib.rs +++ b/illumos-utils/src/lib.rs @@ -4,8 +4,6 @@ //! Wrappers around illumos-specific commands. -use cfg_if::cfg_if; - pub mod addrobj; pub mod coreadm; pub mod destructor; @@ -22,95 +20,3 @@ pub mod vmm_reservoir; pub mod zfs; pub mod zone; pub mod zpool; - -pub const PFEXEC: &str = "/usr/bin/pfexec"; - -#[derive(Debug)] -pub struct CommandFailureInfo { - command: String, - status: std::process::ExitStatus, - stdout: String, - stderr: String, -} - -impl std::fmt::Display for CommandFailureInfo { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!( - f, - "Command [{}] executed and failed with status: {}", - self.command, self.status - )?; - write!(f, " stdout: {}", self.stdout)?; - write!(f, " stderr: {}", self.stderr) - } -} - -#[derive(thiserror::Error, Debug)] -pub enum ExecutionError { - #[error("Failed to start execution of [{command}]: {err}")] - ExecutionStart { command: String, err: std::io::Error }, - - #[error("{0}")] - CommandFailure(Box), - - #[error("Failed to manipulate process contract: {err}")] - ContractFailure { err: std::io::Error }, - - #[error("Zone is not running")] - NotRunning, -} - -// We wrap this method in an inner module to make it possible to mock -// these free functions. -#[cfg_attr(any(test, feature = "testing"), mockall::automock, allow(dead_code))] -mod inner { - use super::*; - - fn to_string(command: &mut std::process::Command) -> String { - command - .get_args() - .map(|s| s.to_string_lossy().into()) - .collect::>() - .join(" ") - } - - pub fn output_to_exec_error( - command: &std::process::Command, - output: &std::process::Output, - ) -> ExecutionError { - ExecutionError::CommandFailure(Box::new(CommandFailureInfo { - command: command - .get_args() - .map(|s| s.to_string_lossy().into()) - .collect::>() - .join(" "), - status: output.status, - stdout: String::from_utf8_lossy(&output.stdout).to_string(), - stderr: String::from_utf8_lossy(&output.stderr).to_string(), - })) - } - - // Helper function for starting the process and checking the - // exit code result. - pub fn execute( - command: &mut std::process::Command, - ) -> Result { - let output = command.output().map_err(|err| { - ExecutionError::ExecutionStart { command: to_string(command), err } - })?; - - if !output.status.success() { - return Err(output_to_exec_error(command, &output)); - } - - Ok(output) - } -} - -cfg_if! { - if #[cfg(any(test, feature = "testing"))] { - pub use mock_inner::*; - } else { - pub use inner::*; - } -} diff --git a/illumos-utils/src/link.rs b/illumos-utils/src/link.rs index 871ba55e753..04d13e36ce5 100644 --- a/illumos-utils/src/link.rs +++ b/illumos-utils/src/link.rs @@ -6,25 +6,22 @@ use crate::destructor::{Deletable, Destructor}; use crate::dladm::{ - CreateVnicError, DeleteVnicError, VnicSource, VNIC_PREFIX, + CreateVnicError, DeleteVnicError, Dladm, VnicSource, VNIC_PREFIX, VNIC_PREFIX_BOOTSTRAP, VNIC_PREFIX_CONTROL, VNIC_PREFIX_GUEST, }; +use helios_fusion::BoxedExecutor; use omicron_common::api::external::MacAddr; use std::sync::{ atomic::{AtomicU64, Ordering}, Arc, }; -#[cfg(not(any(test, feature = "testing")))] -use crate::dladm::Dladm; -#[cfg(any(test, feature = "testing"))] -use crate::dladm::MockDladm as Dladm; - /// A shareable wrapper around an atomic counter. /// May be used to allocate runtime-unique IDs for objects /// which have naming constraints - such as VNICs. #[derive(Clone)] pub struct VnicAllocator { + executor: BoxedExecutor, value: Arc, scope: String, data_link: DL, @@ -44,8 +41,13 @@ impl VnicAllocator
{ /// /// VnicAllocator::new("Storage") produces /// - oxControlStorage0 - pub fn new>(scope: S, data_link: DL) -> Self { + pub fn new>( + executor: &BoxedExecutor, + scope: S, + data_link: DL, + ) -> Self { Self { + executor: executor.clone(), value: Arc::new(AtomicU64::new(0)), scope: scope.as_ref().to_string(), data_link, @@ -63,8 +65,16 @@ impl VnicAllocator
{ let name = allocator.next(); debug_assert!(name.starts_with(VNIC_PREFIX)); debug_assert!(name.starts_with(VNIC_PREFIX_CONTROL)); - Dladm::create_vnic(&self.data_link, &name, mac, None, 9000)?; + Dladm::create_vnic( + &self.executor, + &self.data_link, + &name, + mac, + None, + 9000, + )?; Ok(Link { + executor: self.executor.clone(), name, deleted: false, kind: LinkKind::OxideControlVnic, @@ -79,6 +89,7 @@ impl VnicAllocator
{ ) -> Result { match LinkKind::from_name(name.as_ref()) { Some(kind) => Ok(Link { + executor: self.executor.clone(), name: name.as_ref().to_owned(), deleted: false, kind, @@ -90,6 +101,7 @@ impl VnicAllocator
{ fn new_superscope>(&self, scope: S) -> Self { Self { + executor: self.executor.clone(), value: self.value.clone(), scope: format!("{}{}", scope.as_ref(), self.scope), data_link: self.data_link.clone(), @@ -99,8 +111,16 @@ impl VnicAllocator
{ pub fn new_bootstrap(&self) -> Result { let name = self.next(); - Dladm::create_vnic(&self.data_link, &name, None, None, 1500)?; + Dladm::create_vnic( + &self.executor, + &self.data_link, + &name, + None, + None, + 1500, + )?; Ok(Link { + executor: self.executor.clone(), name, deleted: false, kind: LinkKind::OxideBootstrapVnic, @@ -156,6 +176,7 @@ pub struct InvalidLinkKind(String); /// another process in the global zone could also modify / destroy /// the VNIC while this object is alive. pub struct Link { + executor: BoxedExecutor, name: String, deleted: bool, kind: LinkKind, @@ -176,8 +197,12 @@ impl Link { /// Wraps a physical nic in a Link structure. /// /// It is the caller's responsibility to ensure this is a physical link. - pub fn wrap_physical>(name: S) -> Self { + pub fn wrap_physical>( + executor: &BoxedExecutor, + name: S, + ) -> Self { Link { + executor: executor.clone(), name: name.as_ref().to_owned(), deleted: false, kind: LinkKind::Physical, @@ -190,7 +215,7 @@ impl Link { if self.deleted || self.kind == LinkKind::Physical { Ok(()) } else { - Dladm::delete_vnic(&self.name)?; + Dladm::delete_vnic(&self.executor, &self.name)?; self.deleted = true; Ok(()) } @@ -208,8 +233,10 @@ impl Link { impl Drop for Link { fn drop(&mut self) { if let Some(destructor) = self.destructor.take() { - destructor - .enqueue_destroy(VnicDestruction { name: self.name.clone() }); + destructor.enqueue_destroy(VnicDestruction { + executor: self.executor.clone(), + name: self.name.clone(), + }); } } } @@ -217,12 +244,13 @@ impl Drop for Link { // Represents the request to destroy a VNIC struct VnicDestruction { name: String, + executor: BoxedExecutor, } #[async_trait::async_trait] impl Deletable for VnicDestruction { async fn delete(&self) -> Result<(), anyhow::Error> { - Dladm::delete_vnic(&self.name)?; + Dladm::delete_vnic(&self.executor, &self.name)?; Ok(()) } } @@ -231,22 +259,37 @@ impl Deletable for VnicDestruction { mod test { use super::*; use crate::dladm::Etherstub; + use helios_tokamak::FakeExecutorBuilder; + + use omicron_test_utils::dev; #[tokio::test] async fn test_allocate() { - let allocator = - VnicAllocator::new("Foo", Etherstub("mystub".to_string())); + let logctx = dev::test_setup_log("test_allocate"); + let executor = FakeExecutorBuilder::new(logctx.log.clone()).build(); + let allocator = VnicAllocator::new( + &executor.as_executor(), + "Foo", + Etherstub("mystub".to_string()), + ); assert_eq!("oxFoo0", allocator.next()); assert_eq!("oxFoo1", allocator.next()); assert_eq!("oxFoo2", allocator.next()); + logctx.cleanup_successful(); } #[tokio::test] async fn test_allocate_within_scopes() { - let allocator = - VnicAllocator::new("Foo", Etherstub("mystub".to_string())); + let logctx = dev::test_setup_log("test_allocate_within_scopes"); + let executor = FakeExecutorBuilder::new(logctx.log.clone()).build(); + let allocator = VnicAllocator::new( + &executor.as_executor(), + "Foo", + Etherstub("mystub".to_string()), + ); assert_eq!("oxFoo0", allocator.next()); let allocator = allocator.new_superscope("Baz"); assert_eq!("oxBazFoo1", allocator.next()); + logctx.cleanup_successful(); } } diff --git a/illumos-utils/src/opte/non_illumos.rs b/illumos-utils/src/opte/non_illumos.rs index ccd4990d5f7..1893c7f1aaa 100644 --- a/illumos-utils/src/opte/non_illumos.rs +++ b/illumos-utils/src/opte/non_illumos.rs @@ -2,7 +2,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. -//! Mock / dummy versions of the OPTE module, for non-illumos platforms +//! Stub versions of the OPTE module, for non-illumos platforms use slog::Logger; diff --git a/illumos-utils/src/opte/port.rs b/illumos-utils/src/opte/port.rs index d06cdfe1ec2..b1c10d448bf 100644 --- a/illumos-utils/src/opte/port.rs +++ b/illumos-utils/src/opte/port.rs @@ -6,12 +6,16 @@ use crate::opte::Gateway; use crate::opte::Vni; +use debug_ignore::DebugIgnore; +use helios_fusion::BoxedExecutor; use macaddr::MacAddr6; use std::net::IpAddr; use std::sync::Arc; #[derive(Debug)] struct PortInner { + #[allow(dead_code)] + executor: DebugIgnore, // Name of the port as identified by OPTE name: String, // IP address within the VPC Subnet @@ -39,7 +43,9 @@ struct PortInner { #[cfg(target_os = "illumos")] impl Drop for PortInner { fn drop(&mut self) { - if let Err(e) = crate::dladm::Dladm::delete_vnic(&self.vnic) { + if let Err(e) = + crate::dladm::Dladm::delete_vnic(&self.executor, &self.vnic) + { eprintln!( "WARNING: Failed to delete OPTE port overlay VNIC \ while dropping port. The VNIC will not be cleaned up \ @@ -83,7 +89,9 @@ pub struct Port { } impl Port { + #[allow(clippy::too_many_arguments)] pub fn new( + executor: &BoxedExecutor, name: String, ip: IpAddr, mac: MacAddr6, @@ -94,6 +102,7 @@ impl Port { ) -> Self { Self { inner: Arc::new(PortInner { + executor: DebugIgnore(executor.clone()), name, _ip: ip, mac, diff --git a/illumos-utils/src/opte/port_manager.rs b/illumos-utils/src/opte/port_manager.rs index 893db9a6eda..a4c9d133c4a 100644 --- a/illumos-utils/src/opte/port_manager.rs +++ b/illumos-utils/src/opte/port_manager.rs @@ -13,6 +13,8 @@ use crate::opte::Error; use crate::opte::Gateway; use crate::opte::Port; use crate::opte::Vni; +use debug_ignore::DebugIgnore; +use helios_fusion::BoxedExecutor; use ipnetwork::IpNetwork; use omicron_common::api::external; use omicron_common::api::internal::shared::NetworkInterface; @@ -48,6 +50,8 @@ const XDE_LINK_PREFIX: &str = "opte"; struct PortManagerInner { log: Logger, + executor: DebugIgnore, + // Sequential identifier for each port on the system. next_port_id: AtomicU64, @@ -77,9 +81,14 @@ pub struct PortManager { impl PortManager { /// Create a new manager, for creating OPTE ports - pub fn new(log: Logger, underlay_ip: Ipv6Addr) -> Self { + pub fn new( + log: Logger, + executor: &BoxedExecutor, + underlay_ip: Ipv6Addr, + ) -> Self { let inner = Arc::new(PortManagerInner { log, + executor: DebugIgnore(executor.clone()), next_port_id: AtomicU64::new(0), underlay_ip, ports: Mutex::new(BTreeMap::new()), @@ -267,6 +276,7 @@ impl PortManager { let vnic_name = format!("v{}", port_name); #[cfg(target_os = "illumos")] if let Err(e) = crate::dladm::Dladm::create_vnic( + &self.inner.executor, &crate::dladm::PhysicalLink(port_name.clone()), &vnic_name, Some(nic.mac), @@ -304,6 +314,7 @@ impl PortManager { let mut ports = self.inner.ports.lock().unwrap(); let ticket = PortTicket::new(nic.id, nic.kind, self.inner.clone()); let port = Port::new( + &self.inner.executor, port_name.clone(), nic.ip, mac, diff --git a/illumos-utils/src/running_zone.rs b/illumos-utils/src/running_zone.rs index 4d3481b6c32..c8f6a46cc7b 100644 --- a/illumos-utils/src/running_zone.rs +++ b/illumos-utils/src/running_zone.rs @@ -9,8 +9,9 @@ use crate::dladm::Etherstub; use crate::link::{Link, VnicAllocator}; use crate::opte::{Port, PortTicket}; use crate::svc::wait_for_service; -use crate::zone::{AddressRequest, IPADM, ZONE_PREFIX}; +use crate::zone::{AddressRequest, Zones, IPADM, ZONE_PREFIX}; use camino::{Utf8Path, Utf8PathBuf}; +use helios_fusion::{BoxedExecutor, ExecutionError}; use ipnetwork::IpNetwork; use omicron_common::backoff; use slog::{error, info, o, warn, Logger}; @@ -21,11 +22,6 @@ use std::sync::OnceLock; use std::thread; use uuid::Uuid; -#[cfg(any(test, feature = "testing"))] -use crate::zone::MockZones as Zones; -#[cfg(not(any(test, feature = "testing")))] -use crate::zone::Zones; - /// Errors returned from methods for fetching SMF services and log files #[derive(thiserror::Error, Debug)] pub enum ServiceError { @@ -42,7 +38,7 @@ pub enum ServiceError { pub struct RunCommandError { zone: String, #[source] - err: crate::ExecutionError, + err: ExecutionError, } /// Errors returned from [`RunningZone::boot`]. @@ -78,7 +74,7 @@ pub enum EnsureAddressError { GetAddressesError(#[from] crate::zone::GetAddressesError), #[error("Failed ensuring link-local address in {zone}: {err}")] - LinkLocal { zone: String, err: crate::ExecutionError }, + LinkLocal { zone: String, err: ExecutionError }, #[error("Failed to find non-link-local address in {zone}")] NoDhcpV6Addr { zone: String }, @@ -173,6 +169,7 @@ pub fn ensure_contract_reaper(log: &Logger) { // inside a non-global zone. #[cfg(target_os = "illumos")] mod zenter { + use super::*; use libc::ctid_t; use libc::zoneid_t; use slog::{debug, error, Logger}; @@ -342,12 +339,12 @@ mod zenter { // Automatically detach inherited contracts. const CT_PR_REGENT: c_uint = 0x08; - pub fn new() -> Result { + pub fn new() -> Result { let path = CStr::from_bytes_with_nul(Self::TEMPLATE_PATH).unwrap(); let fd = unsafe { libc::open(path.as_ptr(), libc::O_RDWR) }; if fd < 0 { let err = std::io::Error::last_os_error(); - return Err(crate::ExecutionError::ContractFailure { err }); + return Err(ExecutionError::ContractFailure { err }); } // Initialize the contract template. @@ -372,7 +369,7 @@ mod zenter { || unsafe { ct_tmpl_activate(fd) } != 0 { let err = std::io::Error::last_os_error(); - return Err(crate::ExecutionError::ContractFailure { err }); + return Err(ExecutionError::ContractFailure { err }); } Ok(Self { fd }) } @@ -431,7 +428,7 @@ impl RunningZone { let Some(id) = self.id else { return Err(RunCommandError { zone: self.name().to_string(), - err: crate::ExecutionError::NotRunning, + err: ExecutionError::NotRunning, }); }; let template = @@ -439,7 +436,7 @@ impl RunningZone { RunCommandError { zone: self.name().to_string(), err } })?); let tmpl = std::sync::Arc::clone(&template); - let mut command = std::process::Command::new(crate::PFEXEC); + let mut command = std::process::Command::new(helios_fusion::PFEXEC); let logger = self.inner.log.clone(); let zone = self.name().to_string(); command.env_clear(); @@ -468,9 +465,8 @@ impl RunningZone { // Capture the result, and be sure to clear the template for this // process itself before returning. - let res = crate::execute(command).map_err(|err| RunCommandError { - zone: self.name().to_string(), - err, + let res = self.inner.executor.execute(command).map_err(|err| { + RunCommandError { zone: self.name().to_string(), err } }); template.clear(); @@ -485,12 +481,13 @@ impl RunningZone { S: AsRef, { // NOTE: This implementation is useless, and will never work. However, - // it must actually call `crate::execute()` for the testing purposes. - // That's mocked by `mockall` to return known data, and so the command - // that's actually run is irrelevant. - let mut command = std::process::Command::new("echo"); - let command = command.args(args); - crate::execute(command) + // it must actually call `execute()` for the testing purposes. + let mut command = std::process::Command::new(helios_fusion::PFEXEC); + let command = + command.arg(crate::zone::ZLOGIN).arg(self.name()).args(args); + self.inner + .executor + .execute(command) .map_err(|err| RunCommandError { zone: self.name().to_string(), err, @@ -505,7 +502,7 @@ impl RunningZone { // Boot the zone. info!(zone.log, "Zone booting"); - Zones::boot(&zone.name).await?; + Zones::boot(&zone.executor, &zone.name).await?; // Wait until the zone reaches the 'single-user' SMF milestone. // At this point, we know that the dependent @@ -514,12 +511,12 @@ impl RunningZone { // services are up, so future requests to create network addresses // or manipulate services will work. let fmri = "svc:/milestone/single-user:default"; - wait_for_service(Some(&zone.name), fmri).await.map_err(|_| { - BootError::Timeout { + wait_for_service(&zone.executor, Some(&zone.name), fmri) + .await + .map_err(|_| BootError::Timeout { service: fmri.to_string(), zone: zone.name.to_string(), - } - })?; + })?; // If the zone is self-assembling, then SMF service(s) inside the zone // will be creating the listen address for the zone's service(s), @@ -529,7 +526,7 @@ impl RunningZone { // Use the zone ID in order to check if /var/svc/profile/site.xml // exists. - let id = Zones::id(&zone.name) + let id = Zones::id(&zone.executor, &zone.name) .await? .ok_or_else(|| BootError::NoZoneId { zone: zone.name.clone() })?; let site_profile_xml_exists = @@ -606,8 +603,12 @@ impl RunningZone { zone: self.inner.name.clone(), err, })?; - let network = - Zones::ensure_address(Some(&self.inner.name), &addrobj, addrtype)?; + let network = Zones::ensure_address( + &self.inner.executor, + Some(&self.inner.name), + &addrobj, + addrtype, + )?; Ok(network) } @@ -633,8 +634,12 @@ impl RunningZone { err, } })?; - let _ = - Zones::ensure_address(Some(&self.inner.name), &addrobj, addrtype)?; + let _ = Zones::ensure_address( + &self.inner.executor, + Some(&self.inner.name), + &addrobj, + addrtype, + )?; Ok(()) } @@ -661,8 +666,12 @@ impl RunningZone { })?; let zone = Some(self.inner.name.as_ref()); if let IpAddr::V4(gateway) = port.gateway().ip() { - let addr = - Zones::ensure_address(zone, &addrobj, AddressRequest::Dhcp)?; + let addr = Zones::ensure_address( + &self.inner.executor, + zone, + &addrobj, + AddressRequest::Dhcp, + )?; // TODO-remove(#2931): OPTE's DHCP "server" returns the list of routes // to add via option 121 (Classless Static Route). The illumos DHCP // client currently does not support this option, so we add the routes @@ -690,12 +699,15 @@ impl RunningZone { } else { // If the port is using IPv6 addressing we still want it to use // DHCP(v6) which requires first creating a link-local address. - Zones::ensure_has_link_local_v6_address(zone, &addrobj).map_err( - |err| EnsureAddressError::LinkLocal { - zone: self.inner.name.clone(), - err, - }, - )?; + Zones::ensure_has_link_local_v6_address( + &self.inner.executor, + zone, + &addrobj, + ) + .map_err(|err| EnsureAddressError::LinkLocal { + zone: self.inner.name.clone(), + err, + })?; // Unlike DHCPv4, there's no blocking `ipadm` call we can // make as it just happens in the background. So we just poll @@ -705,12 +717,16 @@ impl RunningZone { || async { // Grab all the address on the addrobj. There should // always be at least one (the link-local we added) - let addrs = Zones::get_all_addresses(zone, &addrobj) - .map_err(|e| { - backoff::BackoffError::permanent( - EnsureAddressError::from(e), - ) - })?; + let addrs = Zones::get_all_addresses( + &self.inner.executor, + zone, + &addrobj, + ) + .map_err(|e| { + backoff::BackoffError::permanent( + EnsureAddressError::from(e), + ) + })?; // Ipv6Addr::is_unicast_link_local is sadly not stable let is_ll = @@ -801,11 +817,12 @@ impl RunningZone { /// address on the zone. pub async fn get( log: &Logger, + executor: &BoxedExecutor, vnic_allocator: &VnicAllocator, zone_prefix: &str, addrtype: AddressRequest, ) -> Result { - let zone_info = Zones::get() + let zone_info = Zones::get(executor) .await .map_err(|err| GetZoneError::GetZones { prefix: zone_prefix.to_string(), @@ -825,22 +842,19 @@ impl RunningZone { } let zone_name = zone_info.name(); - let vnic_name = - Zones::get_control_interface(zone_name).map_err(|err| { - GetZoneError::ControlInterface { - name: zone_name.to_string(), - err, - } + let vnic_name = Zones::get_control_interface(executor, zone_name) + .map_err(|err| GetZoneError::ControlInterface { + name: zone_name.to_string(), + err, })?; let addrobj = AddrObject::new_control(&vnic_name).map_err(|err| { GetZoneError::AddrObject { name: zone_name.to_string(), err } })?; - Zones::ensure_address(Some(zone_name), &addrobj, addrtype).map_err( - |err| GetZoneError::EnsureAddress { + Zones::ensure_address(executor, Some(zone_name), &addrobj, addrtype) + .map_err(|err| GetZoneError::EnsureAddress { name: zone_name.to_string(), err, - }, - )?; + })?; let control_vnic = vnic_allocator .wrap_existing(vnic_name) @@ -849,16 +863,17 @@ impl RunningZone { // The bootstrap address for a running zone never changes, // so there's no need to call `Zones::ensure_address`. // Currently, only the switch zone has a bootstrap interface. - let bootstrap_vnic = Zones::get_bootstrap_interface(zone_name) - .map_err(|err| GetZoneError::BootstrapInterface { - name: zone_name.to_string(), - err, - })? - .map(|name| { - vnic_allocator - .wrap_existing(name) - .expect("Failed to wrap valid bootstrap VNIC") - }); + let bootstrap_vnic = + Zones::get_bootstrap_interface(executor, zone_name) + .map_err(|err| GetZoneError::BootstrapInterface { + name: zone_name.to_string(), + err, + })? + .map(|name| { + vnic_allocator + .wrap_existing(name) + .expect("Failed to wrap valid bootstrap VNIC") + }); Ok(Self { id: zone_info.id().map(|x| { @@ -866,6 +881,7 @@ impl RunningZone { }), inner: InstalledZone { log: log.new(o!("zone" => zone_name.to_string())), + executor: executor.clone(), zonepath: zone_info.path().to_path_buf().try_into()?, name: zone_name.to_string(), control_vnic, @@ -898,7 +914,7 @@ impl RunningZone { if let Some(_) = self.id.take() { let log = self.inner.log.clone(); let name = self.name().to_string(); - Zones::halt_and_remove_logged(&log, &name) + Zones::halt_and_remove_logged(&log, &self.inner.executor, &name) .await .map_err(|err| err.to_string())?; } @@ -1063,8 +1079,11 @@ impl Drop for RunningZone { if let Some(_) = self.id.take() { let log = self.inner.log.clone(); let name = self.name().to_string(); + let executor = self.inner.executor.clone(); tokio::task::spawn(async move { - match Zones::halt_and_remove_logged(&log, &name).await { + match Zones::halt_and_remove_logged(&log, &executor, &name) + .await + { Ok(()) => { info!(log, "Stopped and uninstalled zone") } @@ -1102,21 +1121,27 @@ pub enum InstallZoneError { err: crate::dladm::CreateVnicError, }, - #[error("Failed to install zone '{zone}' from '{image_path}': {err}")] - InstallZone { - zone: String, - image_path: Utf8PathBuf, - #[source] - err: crate::zone::AdmError, - }, + #[error(transparent)] + InstallZone(Box), #[error("Failed to find zone image '{image}' from {paths:?}")] ImageNotFound { image: String, paths: Vec }, } +#[derive(thiserror::Error, Debug)] +#[error("Failed to install zone '{zone}' from '{image_path}': {err}")] +pub struct InstallFailure { + zone: String, + image_path: Utf8PathBuf, + #[source] + err: crate::zone::AdmError, +} + pub struct InstalledZone { log: Logger, + executor: BoxedExecutor, + // Filesystem path of the zone zonepath: Utf8PathBuf, @@ -1172,6 +1197,7 @@ impl InstalledZone { #[allow(clippy::too_many_arguments)] pub async fn install( log: &Logger, + executor: &BoxedExecutor, underlay_vnic_allocator: &VnicAllocator, zone_root_path: &Utf8Path, zone_image_paths: &[Utf8PathBuf], @@ -1232,6 +1258,7 @@ impl InstalledZone { net_device_names.dedup(); Zones::install_omicron_zone( + executor, log, &zone_root_path, &full_zone_name, @@ -1243,14 +1270,17 @@ impl InstalledZone { limit_priv, ) .await - .map_err(|err| InstallZoneError::InstallZone { - zone: full_zone_name.to_string(), - image_path: zone_image_path.clone(), - err, + .map_err(|err| { + InstallZoneError::InstallZone(Box::new(InstallFailure { + zone: full_zone_name.to_string(), + image_path: zone_image_path.clone(), + err, + })) })?; Ok(InstalledZone { log: log.new(o!("zone" => full_zone_name.clone())), + executor: executor.clone(), zonepath: zone_root_path.join(&full_zone_name), name: full_zone_name, control_vnic, diff --git a/illumos-utils/src/svc.rs b/illumos-utils/src/svc.rs index b08679b7201..234b7050446 100644 --- a/illumos-utils/src/svc.rs +++ b/illumos-utils/src/svc.rs @@ -4,68 +4,61 @@ //! Utilities for accessing services. -use cfg_if::cfg_if; - +use helios_fusion::BoxedExecutor; use omicron_common::api::external::Error; use omicron_common::backoff; -#[cfg_attr(any(test, feature = "testing"), mockall::automock, allow(dead_code))] -mod inner { - use super::*; - - // TODO(https://www.illumos.org/issues/13837): This is a hack; - // remove me when when fixed. Ideally, the ".synchronous()" argument - // to "svcadm enable" would wait for the service to be online, which - // would simplify all this stuff. - // - // Ideally, when "svccfg add" returns, these properties would be set, - // but unfortunately, they are not. This means that when we invoke - // "svcadm enable -s", it's possible for critical restarter - // properties to not exist when the command returns. - // - // We workaround this by querying for these properties in a loop. - pub async fn wait_for_service<'a, 'b>( - zone: Option<&'a str>, - fmri: &'b str, - ) -> Result<(), Error> { - let name = smf::PropertyName::new("restarter", "state").unwrap(); +// TODO(https://www.illumos.org/issues/13837): This is a hack; +// remove me when when fixed. Ideally, the ".synchronous()" argument +// to "svcadm enable" would wait for the service to be online, which +// would simplify all this stuff. +// +// Ideally, when "svccfg add" returns, these properties would be set, +// but unfortunately, they are not. This means that when we invoke +// "svcadm enable -s", it's possible for critical restarter +// properties to not exist when the command returns. +// +// We workaround this by querying for these properties in a loop. +pub async fn wait_for_service<'a, 'b>( + executor: &BoxedExecutor, + zone: Option<&'a str>, + fmri: &'b str, +) -> Result<(), Error> { + let name = smf::PropertyName::new("restarter", "state").unwrap(); - let log_notification_failure = |_error, _delay| {}; - backoff::retry_notify( - backoff::retry_policy_local(), - || async { - let mut p = smf::Properties::new(); - let properties = { - if let Some(zone) = zone { - p.zone(zone) - } else { - &mut p - } - }; - if let Ok(value) = properties.lookup().run(&name, &fmri) { - if value.value() - == &smf::PropertyValue::Astring("online".to_string()) - { - return Ok(()); - } + let log_notification_failure = |_error, _delay| {}; + backoff::retry_notify( + backoff::retry_policy_local(), + || async { + let mut p = smf::Properties::new(); + let properties = { + if let Some(zone) = zone { + p.zone(zone) + } else { + &mut p } + }; + let mut cmd = properties.lookup().as_command(&name, &fmri); + + let Ok(output) = executor.execute(&mut cmd) else { return Err(backoff::BackoffError::transient( - "Property not found", + "Failed to execute command", )); - }, - log_notification_failure, - ) - .await - .map_err(|e| Error::InternalError { - internal_message: format!("Failed to wait for service: {}", e), - }) - } -} + }; -cfg_if! { - if #[cfg(any(test, feature = "testing"))] { - pub use mock_inner::*; - } else { - pub use inner::*; - } + if let Ok(value) = smf::PropertyLookup::parse_output(&output) { + if value.value() + == &smf::PropertyValue::Astring("online".to_string()) + { + return Ok(()); + } + } + return Err(backoff::BackoffError::transient("Property not found")); + }, + log_notification_failure, + ) + .await + .map_err(|e| Error::InternalError { + internal_message: format!("Failed to wait for service: {}", e), + }) } diff --git a/illumos-utils/src/zfs.rs b/illumos-utils/src/zfs.rs index ba8cd8c84a8..980709597e7 100644 --- a/illumos-utils/src/zfs.rs +++ b/illumos-utils/src/zfs.rs @@ -4,8 +4,8 @@ //! Utilities for poking at ZFS. -use crate::{execute, PFEXEC}; use camino::Utf8PathBuf; +use helios_fusion::{BoxedExecutor, ExecutionError, PFEXEC}; use omicron_common::disk::DiskIdentity; use std::fmt; @@ -28,7 +28,7 @@ pub const KEYPATH_ROOT: &str = "/var/run/oxide/"; pub struct ListDatasetsError { name: String, #[source] - err: crate::ExecutionError, + err: ExecutionError, } #[derive(thiserror::Error, Debug)] @@ -36,7 +36,7 @@ pub enum DestroyDatasetErrorVariant { #[error("Dataset not found")] NotFound, #[error(transparent)] - Other(crate::ExecutionError), + Other(ExecutionError), } /// Error returned by [`Zfs::destroy_dataset`]. @@ -51,7 +51,7 @@ pub struct DestroyDatasetError { #[derive(thiserror::Error, Debug)] enum EnsureFilesystemErrorRaw { #[error("ZFS execution error: {0}")] - Execution(#[from] crate::ExecutionError), + Execution(#[from] ExecutionError), #[error("Filesystem does not exist, and formatting was not requested")] NotFoundNotFormatted, @@ -60,7 +60,7 @@ enum EnsureFilesystemErrorRaw { Output(String), #[error("Failed to mount encrypted filesystem: {0}")] - MountEncryptedFsFailed(crate::ExecutionError), + MountEncryptedFsFailed(ExecutionError), } /// Error returned by [`Zfs::ensure_filesystem`]. @@ -84,13 +84,13 @@ pub struct SetValueError { filesystem: String, name: String, value: String, - err: crate::ExecutionError, + err: ExecutionError, } #[derive(thiserror::Error, Debug)] enum GetValueErrorRaw { #[error(transparent)] - Execution(#[from] crate::ExecutionError), + Execution(#[from] ExecutionError), #[error("No value found with that name")] MissingValue, @@ -160,14 +160,17 @@ pub struct SizeDetails { pub compression: Option<&'static str>, } -#[cfg_attr(any(test, feature = "testing"), mockall::automock, allow(dead_code))] impl Zfs { /// Lists all datasets within a pool or existing dataset. - pub fn list_datasets(name: &str) -> Result, ListDatasetsError> { + pub fn list_datasets( + executor: &BoxedExecutor, + name: &str, + ) -> Result, ListDatasetsError> { let mut command = std::process::Command::new(ZFS); let cmd = command.args(&["list", "-d", "1", "-rHpo", "name", name]); - let output = execute(cmd) + let output = executor + .execute(cmd) .map_err(|err| ListDatasetsError { name: name.to_string(), err })?; let stdout = String::from_utf8_lossy(&output.stdout); let filesystems: Vec = stdout @@ -182,12 +185,15 @@ impl Zfs { } /// Destroys a dataset. - pub fn destroy_dataset(name: &str) -> Result<(), DestroyDatasetError> { + pub fn destroy_dataset( + executor: &BoxedExecutor, + name: &str, + ) -> Result<(), DestroyDatasetError> { let mut command = std::process::Command::new(PFEXEC); let cmd = command.args(&[ZFS, "destroy", "-r", name]); - execute(cmd).map_err(|err| { + executor.execute(cmd).map_err(|err| { let variant = match err { - crate::ExecutionError::CommandFailure(info) + ExecutionError::CommandFailure(info) if info.stderr.contains("does not exist") => { DestroyDatasetErrorVariant::NotFound @@ -203,6 +209,7 @@ impl Zfs { /// /// Applies an optional quota, provided _in bytes_. pub fn ensure_filesystem( + executor: &BoxedExecutor, name: &str, mountpoint: Mountpoint, zoned: bool, @@ -210,12 +217,19 @@ impl Zfs { encryption_details: Option, size_details: Option, ) -> Result<(), EnsureFilesystemError> { - let (exists, mounted) = Self::dataset_exists(name, &mountpoint)?; + let (exists, mounted) = + Self::dataset_exists(executor, name, &mountpoint)?; if exists { if let Some(SizeDetails { quota, compression }) = size_details { // apply quota and compression mode (in case they've changed across // sled-agent versions since creation) - Self::apply_properties(name, &mountpoint, quota, compression)?; + Self::apply_properties( + executor, + name, + &mountpoint, + quota, + compression, + )?; } if encryption_details.is_none() { @@ -228,7 +242,11 @@ impl Zfs { return Ok(()); } // We need to load the encryption key and mount the filesystem - return Self::mount_encrypted_dataset(name, &mountpoint); + return Self::mount_encrypted_dataset( + executor, + name, + &mountpoint, + ); } } @@ -262,7 +280,7 @@ impl Zfs { } cmd.args(&["-o", &format!("mountpoint={}", mountpoint), name]); - execute(cmd).map_err(|err| EnsureFilesystemError { + executor.execute(cmd).map_err(|err| EnsureFilesystemError { name: name.to_string(), mountpoint: mountpoint.clone(), err: err.into(), @@ -270,13 +288,20 @@ impl Zfs { if let Some(SizeDetails { quota, compression }) = size_details { // Apply any quota and compression mode. - Self::apply_properties(name, &mountpoint, quota, compression)?; + Self::apply_properties( + executor, + name, + &mountpoint, + quota, + compression, + )?; } Ok(()) } fn apply_properties( + executor: &BoxedExecutor, name: &str, mountpoint: &Mountpoint, quota: Option, @@ -284,7 +309,7 @@ impl Zfs { ) -> Result<(), EnsureFilesystemError> { if let Some(quota) = quota { if let Err(err) = - Self::set_value(name, "quota", &format!("{quota}")) + Self::set_value(executor, name, "quota", &format!("{quota}")) { return Err(EnsureFilesystemError { name: name.to_string(), @@ -295,7 +320,8 @@ impl Zfs { } } if let Some(compression) = compression { - if let Err(err) = Self::set_value(name, "compression", compression) + if let Err(err) = + Self::set_value(executor, name, "compression", compression) { return Err(EnsureFilesystemError { name: name.to_string(), @@ -309,12 +335,13 @@ impl Zfs { } fn mount_encrypted_dataset( + executor: &BoxedExecutor, name: &str, mountpoint: &Mountpoint, ) -> Result<(), EnsureFilesystemError> { let mut command = std::process::Command::new(PFEXEC); let cmd = command.args(&[ZFS, "mount", "-l", name]); - execute(cmd).map_err(|err| EnsureFilesystemError { + executor.execute(cmd).map_err(|err| EnsureFilesystemError { name: name.to_string(), mountpoint: mountpoint.clone(), err: EnsureFilesystemErrorRaw::MountEncryptedFsFailed(err), @@ -325,6 +352,7 @@ impl Zfs { // Return (true, mounted) if the dataset exists, (false, false) otherwise, // where mounted is if the dataset is mounted. fn dataset_exists( + executor: &BoxedExecutor, name: &str, mountpoint: &Mountpoint, ) -> Result<(bool, bool), EnsureFilesystemError> { @@ -336,7 +364,7 @@ impl Zfs { name, ]); // If the list command returns any valid output, validate it. - if let Ok(output) = execute(cmd) { + if let Ok(output) = executor.execute(cmd) { let stdout = String::from_utf8_lossy(&output.stdout); let values: Vec<&str> = stdout.trim().split('\t').collect(); if &values[..3] != &[name, "filesystem", &mountpoint.to_string()] { @@ -354,14 +382,21 @@ impl Zfs { } pub fn set_oxide_value( + executor: &BoxedExecutor, filesystem_name: &str, name: &str, value: &str, ) -> Result<(), SetValueError> { - Zfs::set_value(filesystem_name, &format!("oxide:{}", name), value) + Zfs::set_value( + executor, + filesystem_name, + &format!("oxide:{}", name), + value, + ) } fn set_value( + executor: &BoxedExecutor, filesystem_name: &str, name: &str, value: &str, @@ -369,7 +404,7 @@ impl Zfs { let mut command = std::process::Command::new(PFEXEC); let value_arg = format!("{}={}", name, value); let cmd = command.args(&[ZFS, "set", &value_arg, filesystem_name]); - execute(cmd).map_err(|err| SetValueError { + executor.execute(cmd).map_err(|err| SetValueError { filesystem: filesystem_name.to_string(), name: name.to_string(), value: value.to_string(), @@ -379,20 +414,22 @@ impl Zfs { } pub fn get_oxide_value( + executor: &BoxedExecutor, filesystem_name: &str, name: &str, ) -> Result { - Zfs::get_value(filesystem_name, &format!("oxide:{}", name)) + Zfs::get_value(executor, filesystem_name, &format!("oxide:{}", name)) } fn get_value( + executor: &BoxedExecutor, filesystem_name: &str, name: &str, ) -> Result { let mut command = std::process::Command::new(PFEXEC); let cmd = command.args(&[ZFS, "get", "-Ho", "value", &name, filesystem_name]); - let output = execute(cmd).map_err(|err| GetValueError { + let output = executor.execute(cmd).map_err(|err| GetValueError { filesystem: filesystem_name.to_string(), name: name.to_string(), err: err.into(), @@ -411,17 +448,19 @@ impl Zfs { } /// Returns all datasets managed by Omicron -pub fn get_all_omicron_datasets_for_delete() -> anyhow::Result> { +pub fn get_all_omicron_datasets_for_delete( + executor: &BoxedExecutor, +) -> anyhow::Result> { let mut datasets = vec![]; // Collect all datasets within Oxide zpools. // // This includes cockroachdb, clickhouse, and crucible datasets. - let zpools = crate::zpool::Zpool::list()?; + let zpools = crate::zpool::Zpool::list(executor)?; for pool in &zpools { let internal = pool.kind() == crate::zpool::ZpoolKind::Internal; let pool = pool.to_string(); - for dataset in &Zfs::list_datasets(&pool)? { + for dataset in &Zfs::list_datasets(executor, &pool)? { // Avoid erasing crashdump datasets on internal pools if dataset == "crash" && internal { continue; @@ -437,7 +476,8 @@ pub fn get_all_omicron_datasets_for_delete() -> anyhow::Result> { } // Collect all datasets for ramdisk-based Oxide zones, if any exist. - if let Ok(ramdisk_datasets) = Zfs::list_datasets(&ZONE_ZFS_RAMDISK_DATASET) + if let Ok(ramdisk_datasets) = + Zfs::list_datasets(executor, &ZONE_ZFS_RAMDISK_DATASET) { for dataset in &ramdisk_datasets { datasets.push(format!("{}/{dataset}", ZONE_ZFS_RAMDISK_DATASET)); diff --git a/illumos-utils/src/zone.rs b/illumos-utils/src/zone.rs index a3f73b39545..209c403fd2d 100644 --- a/illumos-utils/src/zone.rs +++ b/illumos-utils/src/zone.rs @@ -14,7 +14,7 @@ use std::net::{IpAddr, Ipv6Addr}; use crate::addrobj::AddrObject; use crate::dladm::{EtherstubVnic, VNIC_PREFIX_BOOTSTRAP, VNIC_PREFIX_CONTROL}; -use crate::{execute, PFEXEC}; +use helios_fusion::{BoxedExecutor, ExecutionError, PFEXEC}; use omicron_common::address::SLED_PREFIX; const DLADM: &str = "/usr/sbin/dladm"; @@ -22,6 +22,8 @@ pub const IPADM: &str = "/usr/sbin/ipadm"; pub const SVCADM: &str = "/usr/sbin/svcadm"; pub const SVCCFG: &str = "/usr/sbin/svccfg"; pub const ZLOGIN: &str = "/usr/sbin/zlogin"; +pub const ZONEADM: &str = "/usr/sbin/zoneadm"; +pub const ZONECFG: &str = "/usr/sbin/zonecfg"; // TODO: These could become enums pub const ZONE_PREFIX: &str = "oxz_"; @@ -30,7 +32,7 @@ pub const PROPOLIS_ZONE_PREFIX: &str = "oxz_propolis-server_"; #[derive(thiserror::Error, Debug)] enum Error { #[error("Zone execution error: {0}")] - Execution(#[from] crate::ExecutionError), + Execution(#[from] ExecutionError), #[error(transparent)] AddrObject(#[from] crate::addrobj::ParseError), @@ -51,6 +53,13 @@ pub enum Operation { Uninstall, } +#[derive(thiserror::Error, Debug)] +#[error(transparent)] +enum AdmErrorVariant { + Execution(#[from] ExecutionError), + Adm(#[from] zone::ZoneError), +} + /// Errors from issuing [`zone::Adm`] commands. #[derive(thiserror::Error, Debug)] #[error("Failed to execute zoneadm command '{op:?}' for zone '{zone}': {err}")] @@ -58,7 +67,7 @@ pub struct AdmError { op: Operation, zone: String, #[source] - err: zone::ZoneError, + err: AdmErrorVariant, } /// Errors which may be encountered when deleting addresses. @@ -68,7 +77,7 @@ pub struct DeleteAddressError { zone: String, addrobj: AddrObject, #[source] - err: crate::ExecutionError, + err: ExecutionError, } /// Errors from [`Zones::get_control_interface`]. @@ -79,7 +88,7 @@ pub enum GetControlInterfaceError { Execution { zone: String, #[source] - err: crate::ExecutionError, + err: ExecutionError, }, #[error("VNIC starting with 'oxControl' not found in {zone}")] @@ -94,7 +103,7 @@ pub enum GetBootstrapInterfaceError { Execution { zone: String, #[source] - err: crate::ExecutionError, + err: ExecutionError, }, #[error("VNIC starting with 'oxBootstrap' not found in {zone}")] @@ -203,16 +212,16 @@ fn parse_ip_network(s: &str) -> Result { } } -#[cfg_attr(any(test, feature = "testing"), mockall::automock, allow(dead_code))] impl Zones { /// Ensures a zone is halted before both uninstalling and deleting it. /// /// Returns the state the zone was in before it was removed, or None if the /// zone did not exist. pub async fn halt_and_remove( + executor: &BoxedExecutor, name: &str, ) -> Result, AdmError> { - match Self::find(name).await? { + match Self::find(executor, name).await? { None => Ok(None), Some(zone) => { let state = zone.state(); @@ -231,7 +240,7 @@ impl Zones { AdmError { op: Operation::Halt, zone: name.to_string(), - err, + err: err.into(), } })?; } @@ -242,7 +251,7 @@ impl Zones { .map_err(|err| AdmError { op: Operation::Uninstall, zone: name.to_string(), - err, + err: err.into(), })?; } zone::Config::new(name) @@ -252,7 +261,7 @@ impl Zones { .map_err(|err| AdmError { op: Operation::Delete, zone: name.to_string(), - err, + err: err.into(), })?; Ok(Some(state)) } @@ -262,9 +271,10 @@ impl Zones { /// Halt and remove the zone, logging the state in which the zone was found. pub async fn halt_and_remove_logged( log: &Logger, + executor: &BoxedExecutor, name: &str, ) -> Result<(), AdmError> { - if let Some(state) = Self::halt_and_remove(name).await? { + if let Some(state) = Self::halt_and_remove(executor, name).await? { info!( log, "halt_and_remove_logged: Previous zone state: {:?}", state @@ -280,6 +290,7 @@ impl Zones { /// - Otherwise, the zone is deleted. #[allow(clippy::too_many_arguments)] pub async fn install_omicron_zone( + executor: &BoxedExecutor, log: &Logger, zone_root_path: &Utf8Path, zone_name: &str, @@ -290,7 +301,7 @@ impl Zones { links: Vec, limit_priv: Vec, ) -> Result<(), AdmError> { - if let Some(zone) = Self::find(zone_name).await? { + if let Some(zone) = Self::find(executor, zone_name).await? { info!( log, "install_omicron_zone: Found zone: {} in state {:?}", @@ -307,7 +318,8 @@ impl Zones { "Invalid state; uninstalling and deleting zone {}", zone_name ); - Zones::halt_and_remove_logged(log, zone.name()).await?; + Zones::halt_and_remove_logged(log, executor, zone.name()) + .await?; } } @@ -344,34 +356,41 @@ impl Zones { ..Default::default() }); } - cfg.run().await.map_err(|err| AdmError { + let mut cmd = tokio::process::Command::from(cfg.as_command()); + executor.execute_async(&mut cmd).await.map_err(|err| AdmError { op: Operation::Configure, zone: zone_name.to_string(), - err, + err: err.into(), })?; info!(log, "Installing Omicron zone: {}", zone_name); - zone::Adm::new(zone_name) - .install(&[ + let mut cmd = tokio::process::Command::from( + zone::Adm::new(zone_name).install_command(&[ zone_image.as_ref(), "/opt/oxide/overlay.tar.gz".as_ref(), - ]) - .await - .map_err(|err| AdmError { - op: Operation::Install, - zone: zone_name.to_string(), - err, - })?; + ]), + ); + executor.execute_async(&mut cmd).await.map_err(|err| AdmError { + op: Operation::Install, + zone: zone_name.to_string(), + err: err.into(), + })?; Ok(()) } /// Boots a zone (named `name`). - pub async fn boot(name: &str) -> Result<(), AdmError> { - zone::Adm::new(name).boot().await.map_err(|err| AdmError { + pub async fn boot( + executor: &BoxedExecutor, + name: &str, + ) -> Result<(), AdmError> { + let mut cmd = + tokio::process::Command::from(zone::Adm::new(name).boot_command()); + + executor.execute_async(&mut cmd).await.map_err(|err| AdmError { op: Operation::Boot, zone: name.to_string(), - err, + err: err.into(), })?; Ok(()) } @@ -379,14 +398,25 @@ impl Zones { /// Returns all zones that may be managed by the Sled Agent. /// /// These zones must have names starting with [`ZONE_PREFIX`]. - pub async fn get() -> Result, AdmError> { - Ok(zone::Adm::list() + pub async fn get( + executor: &BoxedExecutor, + ) -> Result, AdmError> { + let handle_err = |err| AdmError { + op: Operation::List, + zone: "".to_string(), + err, + }; + + let mut cmd = tokio::process::Command::from(zone::Adm::list_command()); + let output = executor + .execute_async(&mut cmd) .await - .map_err(|err| AdmError { - op: Operation::List, - zone: "".to_string(), - err, - })? + .map_err(|err| handle_err(err.into()))?; + + let zones = zone::Adm::parse_list_output(&output) + .map_err(|err| handle_err(err.into()))?; + + Ok(zones .into_iter() .filter(|z| z.name().starts_with(ZONE_PREFIX)) .collect()) @@ -396,8 +426,14 @@ impl Zones { /// /// Can only return zones that start with [`ZONE_PREFIX`], as they /// are managed by the Sled Agent. - pub async fn find(name: &str) -> Result, AdmError> { - Ok(Self::get().await?.into_iter().find(|zone| zone.name() == name)) + pub async fn find( + executor: &BoxedExecutor, + name: &str, + ) -> Result, AdmError> { + Ok(Self::get(executor) + .await? + .into_iter() + .find(|zone| zone.name() == name)) } /// Return the ID for a _running_ zone with the specified name. @@ -407,10 +443,13 @@ impl Zones { // object. But that can't easily be done, because we need to supply // `mockall` with a value to return, and `zone::Zone` objects can't be // constructed since they have private fields. - pub async fn id(name: &str) -> Result, AdmError> { + pub async fn id( + executor: &BoxedExecutor, + name: &str, + ) -> Result, AdmError> { // Safety: illumos defines `zoneid_t` as a typedef for an integer, i.e., // an `i32`, so this unwrap should always be safe. - match Self::find(name).await?.map(|zn| zn.id()) { + match Self::find(executor, name).await?.map(|zn| zn.id()) { Some(Some(id)) => Ok(Some(id.try_into().unwrap())), Some(None) | None => Ok(None), } @@ -418,6 +457,7 @@ impl Zones { /// Returns the name of the VNIC used to communicate with the control plane. pub fn get_control_interface( + executor: &BoxedExecutor, zone: &str, ) -> Result { let mut command = std::process::Command::new(PFEXEC); @@ -430,7 +470,7 @@ impl Zones { "-o", "LINK", ]); - let output = execute(cmd).map_err(|err| { + let output = executor.execute(cmd).map_err(|err| { GetControlInterfaceError::Execution { zone: zone.to_string(), err } })?; String::from_utf8_lossy(&output.stdout) @@ -449,6 +489,7 @@ impl Zones { /// Returns the name of the VNIC used to communicate with the bootstrap network. pub fn get_bootstrap_interface( + executor: &BoxedExecutor, zone: &str, ) -> Result, GetBootstrapInterfaceError> { let mut command = std::process::Command::new(PFEXEC); @@ -461,7 +502,7 @@ impl Zones { "-o", "LINK", ]); - let output = execute(cmd).map_err(|err| { + let output = executor.execute(cmd).map_err(|err| { GetBootstrapInterfaceError::Execution { zone: zone.to_string(), err, @@ -496,12 +537,13 @@ impl Zones { /// If `None` is supplied, the address is queried from the Global Zone. #[allow(clippy::needless_lifetimes)] pub fn ensure_address<'a>( + executor: &BoxedExecutor, zone: Option<&'a str>, addrobj: &AddrObject, addrtype: AddressRequest, ) -> Result { |zone, addrobj, addrtype| -> Result { - match Self::get_address_impl(zone, addrobj) { + match Self::get_address_impl(executor, zone, addrobj) { Ok(addr) => { if let AddressRequest::Static(expected_addr) = addrtype { // If the address is static, we need to validate that it @@ -509,18 +551,20 @@ impl Zones { if addr != expected_addr { // If the address doesn't match, try removing the old // value before using the new one. - Self::delete_address(zone, addrobj) + Self::delete_address(executor, zone, addrobj) .map_err(|e| anyhow!(e))?; return Self::create_address( - zone, addrobj, addrtype, + executor, zone, addrobj, addrtype, ) .map_err(|e| anyhow!(e)); } } Ok(addr) } - Err(_) => Self::create_address(zone, addrobj, addrtype) - .map_err(|e| anyhow!(e)), + Err(_) => { + Self::create_address(executor, zone, addrobj, addrtype) + .map_err(|e| anyhow!(e)) + } } }(zone, addrobj, addrtype) .map_err(|err| EnsureAddressError { @@ -537,13 +581,16 @@ impl Zones { /// If `None` is supplied, the address is queried from the Global Zone. #[allow(clippy::needless_lifetimes)] pub fn get_address<'a>( + executor: &BoxedExecutor, zone: Option<&'a str>, addrobj: &AddrObject, ) -> Result { - Self::get_address_impl(zone, addrobj).map_err(|err| GetAddressError { - zone: zone.unwrap_or("global").to_string(), - name: addrobj.clone(), - err: anyhow!(err), + Self::get_address_impl(executor, zone, addrobj).map_err(|err| { + GetAddressError { + zone: zone.unwrap_or("global").to_string(), + name: addrobj.clone(), + err: anyhow!(err), + } }) } @@ -553,6 +600,7 @@ impl Zones { /// If `None` is supplied, the address is queried from the Global Zone. #[allow(clippy::needless_lifetimes)] fn get_address_impl<'a>( + executor: &BoxedExecutor, zone: Option<&'a str>, addrobj: &AddrObject, ) -> Result { @@ -567,7 +615,7 @@ impl Zones { args.extend(&[IPADM, "show-addr", "-p", "-o", "ADDR", &addrobj_str]); let cmd = command.args(args); - let output = execute(cmd)?; + let output = executor.execute(cmd)?; String::from_utf8_lossy(&output.stdout) .lines() .find_map(|s| parse_ip_network(s).ok()) @@ -580,6 +628,7 @@ impl Zones { /// If `None` is supplied, the address is queried from the Global Zone. #[allow(clippy::needless_lifetimes)] pub fn get_all_addresses<'a>( + executor: &BoxedExecutor, zone: Option<&'a str>, addrobj: &AddrObject, ) -> Result, GetAddressesError> { @@ -594,11 +643,12 @@ impl Zones { args.extend(&[IPADM, "show-addr", "-p", "-o", "ADDR", &addrobj_str]); let cmd = command.args(args); - let output = execute(cmd).map_err(|err| GetAddressesError { - zone: zone.unwrap_or("global").to_string(), - name: addrobj.clone(), - err: err.into(), - })?; + let output = + executor.execute(cmd).map_err(|err| GetAddressesError { + zone: zone.unwrap_or("global").to_string(), + name: addrobj.clone(), + err: err.into(), + })?; Ok(String::from_utf8_lossy(&output.stdout) .lines() .filter_map(|s| s.parse().ok()) @@ -611,6 +661,7 @@ impl Zones { /// run the command in the Global zone. #[allow(clippy::needless_lifetimes)] fn has_link_local_v6_address<'a>( + executor: &BoxedExecutor, zone: Option<&'a str>, addrobj: &AddrObject, ) -> Result<(), Error> { @@ -625,7 +676,7 @@ impl Zones { let args = prefix.iter().chain(show_addr_args); let cmd = command.args(args); - let output = execute(cmd)?; + let output = executor.execute(cmd)?; if let Some(_) = String::from_utf8_lossy(&output.stdout) .lines() .find(|s| s.trim() == "addrconf") @@ -640,10 +691,11 @@ impl Zones { // Does NOT check if the address already exists. #[allow(clippy::needless_lifetimes)] fn create_address_internal<'a>( + executor: &BoxedExecutor, zone: Option<&'a str>, addrobj: &AddrObject, addrtype: AddressRequest, - ) -> Result<(), crate::ExecutionError> { + ) -> Result<(), ExecutionError> { let mut command = std::process::Command::new(PFEXEC); let mut args = vec![]; if let Some(zone) = zone { @@ -670,13 +722,14 @@ impl Zones { args.push(addrobj.to_string()); let cmd = command.args(args); - execute(cmd)?; + executor.execute(cmd)?; Ok(()) } #[allow(clippy::needless_lifetimes)] pub fn delete_address<'a>( + executor: &BoxedExecutor, zone: Option<&'a str>, addrobj: &AddrObject, ) -> Result<(), DeleteAddressError> { @@ -692,7 +745,7 @@ impl Zones { args.push(addrobj.to_string()); let cmd = command.args(args); - execute(cmd).map_err(|err| DeleteAddressError { + executor.execute(cmd).map_err(|err| DeleteAddressError { zone: zone.unwrap_or("global").to_string(), addrobj: addrobj.clone(), err, @@ -709,10 +762,13 @@ impl Zones { /// #[allow(clippy::needless_lifetimes)] pub fn ensure_has_link_local_v6_address<'a>( + executor: &BoxedExecutor, zone: Option<&'a str>, addrobj: &AddrObject, - ) -> Result<(), crate::ExecutionError> { - if let Ok(()) = Self::has_link_local_v6_address(zone, &addrobj) { + ) -> Result<(), ExecutionError> { + if let Ok(()) = + Self::has_link_local_v6_address(executor, zone, &addrobj) + { return Ok(()); } @@ -733,7 +789,7 @@ impl Zones { let args = prefix.iter().chain(create_addr_args); let cmd = command.args(args); - execute(cmd)?; + executor.execute(cmd)?; Ok(()) } @@ -743,6 +799,7 @@ impl Zones { // (which exists pre-RSS), but we should remove all uses of it other than // the bootstrap agent. pub fn ensure_has_global_zone_v6_address( + executor: &BoxedExecutor, link: EtherstubVnic, address: Ipv6Addr, name: &str, @@ -753,6 +810,7 @@ impl Zones { let gz_link_local_addrobj = AddrObject::link_local(&link.0) .map_err(|err| anyhow!(err))?; Self::ensure_has_link_local_v6_address( + executor, None, &gz_link_local_addrobj, ) @@ -765,6 +823,7 @@ impl Zones { // this sled itself are within the underlay or bootstrap prefix. // Anything else must be routed through Sidecar. Self::ensure_address( + executor, None, &gz_link_local_addrobj .on_same_interface(name) @@ -805,6 +864,7 @@ impl Zones { // Creates an IP address within a Zone. #[allow(clippy::needless_lifetimes)] fn create_address<'a>( + executor: &BoxedExecutor, zone: Option<&'a str>, addrobj: &AddrObject, addrtype: AddressRequest, @@ -824,6 +884,7 @@ impl Zones { let link_local_addrobj = addrobj.link_local_on_same_interface()?; Self::ensure_has_link_local_v6_address( + executor, Some(zone), &link_local_addrobj, )?; @@ -833,15 +894,130 @@ impl Zones { }; // Actually perform address allocation. - Self::create_address_internal(zone, addrobj, addrtype)?; - - Self::get_address_impl(zone, addrobj) + Self::create_address_internal(executor, zone, addrobj, addrtype)?; + Self::get_address_impl(executor, zone, addrobj) } } #[cfg(test)] mod tests { use super::*; + use helios_fusion::{Input, OutputExt}; + use helios_tokamak::{CommandSequence, FakeExecutorBuilder}; + use omicron_test_utils::dev; + use std::process::Output; + + #[tokio::test] + async fn install_new_zone_calls_config_then_install() { + let logctx = + dev::test_setup_log("install_new_zone_calls_config_then_install"); + + let zone_root_path = Utf8Path::new("/root"); + let zone_name = "oxz_myzone"; + let zone_image = Utf8Path::new("/image.tar.gz"); + + // When installing a new zone, we expect to see: + // - A request for the list of existing zones + // - A command to configure the zone + // - A command to install the zone + let mut handler = CommandSequence::new(); + handler.expect( + Input::shell(format!("{PFEXEC} {ZONEADM} list -cip")), + Output::success().set_stdout("0:global:running:/::ipkg:shared"), + ); + + handler.expect( + Input::shell(format!( + "{PFEXEC} {ZONECFG} -z {zone_name} \ + create -F -b ; \ + set brand=omicron1 ; \ + set zonepath={zone_root_path}/{zone_name} ; \ + set autoboot=false ; \ + set ip-type=exclusive" + )), + Output::success(), + ); + + handler.expect( + Input::shell(format!( + "{PFEXEC} {ZONEADM} -z {zone_name} \ + install {zone_image} /opt/oxide/overlay.tar.gz" + )), + Output::success(), + ); + + let executor = FakeExecutorBuilder::new(logctx.log.clone()) + .with_sequence(handler) + .build(); + + let datasets = []; + let filesystems = []; + let devices = []; + let links = vec![]; + let limit_priv = vec![]; + + Zones::install_omicron_zone( + &executor.as_executor(), + &logctx.log, + &zone_root_path, + zone_name, + &zone_image, + &datasets, + &filesystems, + &devices, + links, + limit_priv, + ) + .await + .expect("Failed to install zone"); + + logctx.cleanup_successful(); + } + + #[tokio::test] + async fn install_existing_zone_queries_for_it() { + let logctx = + dev::test_setup_log("install_existing_zone_queries_for_it"); + + let zone_root_path = Utf8Path::new("/root"); + let zone_name = "oxz_myzone"; + let zone_image = Utf8Path::new("/image.tar.gz"); + + let mut handler = CommandSequence::new(); + handler.expect( + Input::shell(format!("{PFEXEC} {ZONEADM} list -cip")), + Output::success().set_stdout( + "0:global:running:/::ipkg:shared\n1:oxz_myzone:running:/root/oxz_myzone::omicron1:excl" + ) + ); + + let executor = FakeExecutorBuilder::new(logctx.log.clone()) + .with_sequence(handler) + .build(); + + let datasets = []; + let filesystems = []; + let devices = []; + let links = vec![]; + let limit_priv = vec![]; + + Zones::install_omicron_zone( + &executor.as_executor(), + &logctx.log, + &zone_root_path, + zone_name, + &zone_image, + &datasets, + &filesystems, + &devices, + links, + limit_priv, + ) + .await + .expect("Failed to install zone"); + + logctx.cleanup_successful(); + } #[test] fn test_parse_ip_network() { diff --git a/illumos-utils/src/zpool.rs b/illumos-utils/src/zpool.rs index 81ded2655ec..d96efc60d70 100644 --- a/illumos-utils/src/zpool.rs +++ b/illumos-utils/src/zpool.rs @@ -4,8 +4,8 @@ //! Utilities for managing Zpools. -use crate::{execute, ExecutionError, PFEXEC}; use camino::{Utf8Path, Utf8PathBuf}; +use helios_fusion::{BoxedExecutor, ExecutionError, PFEXEC}; use schemars::JsonSchema; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use std::fmt; @@ -14,7 +14,7 @@ use uuid::Uuid; const ZPOOL_EXTERNAL_PREFIX: &str = "oxp_"; const ZPOOL_INTERNAL_PREFIX: &str = "oxi_"; -const ZPOOL: &str = "/usr/sbin/zpool"; +pub const ZPOOL: &str = "/usr/sbin/zpool"; #[derive(thiserror::Error, Debug, PartialEq, Eq)] #[error("Failed to parse output: {0}")] @@ -23,7 +23,7 @@ pub struct ParseError(String); #[derive(thiserror::Error, Debug)] pub enum Error { #[error("Zpool execution error: {0}")] - Execution(#[from] crate::ExecutionError), + Execution(#[from] ExecutionError), #[error(transparent)] Parse(#[from] ParseError), @@ -165,16 +165,19 @@ impl FromStr for ZpoolInfo { /// Wraps commands for interacting with ZFS pools. pub struct Zpool {} -#[cfg_attr(any(test, feature = "testing"), mockall::automock, allow(dead_code))] impl Zpool { - pub fn create(name: ZpoolName, vdev: &Utf8Path) -> Result<(), CreateError> { + pub fn create( + executor: &BoxedExecutor, + name: ZpoolName, + vdev: &Utf8Path, + ) -> Result<(), CreateError> { let mut cmd = std::process::Command::new(PFEXEC); cmd.env_clear(); cmd.env("LC_ALL", "C.UTF-8"); cmd.arg(ZPOOL).arg("create"); cmd.arg(&name.to_string()); cmd.arg(vdev); - execute(&mut cmd).map_err(Error::from)?; + executor.execute(&mut cmd).map_err(Error::from)?; // Ensure that this zpool has the encryption feature enabled let mut cmd = std::process::Command::new(PFEXEC); @@ -184,18 +187,21 @@ impl Zpool { .arg("set") .arg("feature@encryption=enabled") .arg(&name.to_string()); - execute(&mut cmd).map_err(Error::from)?; + executor.execute(&mut cmd).map_err(Error::from)?; Ok(()) } - pub fn import(name: ZpoolName) -> Result<(), Error> { + pub fn import( + executor: &BoxedExecutor, + name: ZpoolName, + ) -> Result<(), Error> { let mut cmd = std::process::Command::new(PFEXEC); cmd.env_clear(); cmd.env("LC_ALL", "C.UTF-8"); cmd.arg(ZPOOL).arg("import").arg("-f"); cmd.arg(&name.to_string()); - match execute(&mut cmd) { + match executor.execute(&mut cmd) { Ok(_) => Ok(()), Err(ExecutionError::CommandFailure(err_info)) => { // I'd really prefer to match on a specific error code, but the @@ -213,18 +219,24 @@ impl Zpool { } } - pub fn export(name: &ZpoolName) -> Result<(), Error> { + pub fn export( + executor: &BoxedExecutor, + name: &ZpoolName, + ) -> Result<(), Error> { let mut cmd = std::process::Command::new(PFEXEC); cmd.env_clear(); cmd.env("LC_ALL", "C.UTF-8"); cmd.arg(ZPOOL).arg("export").arg(&name.to_string()); - execute(&mut cmd)?; + executor.execute(&mut cmd)?; Ok(()) } /// `zpool set failmode=continue ` - pub fn set_failmode_continue(name: &ZpoolName) -> Result<(), Error> { + pub fn set_failmode_continue( + executor: &BoxedExecutor, + name: &ZpoolName, + ) -> Result<(), Error> { let mut cmd = std::process::Command::new(PFEXEC); cmd.env_clear(); cmd.env("LC_ALL", "C.UTF-8"); @@ -232,15 +244,15 @@ impl Zpool { .arg("set") .arg("failmode=continue") .arg(&name.to_string()); - execute(&mut cmd)?; + executor.execute(&mut cmd)?; Ok(()) } - pub fn list() -> Result, ListError> { + pub fn list(executor: &BoxedExecutor) -> Result, ListError> { let mut command = std::process::Command::new(ZPOOL); let cmd = command.args(&["list", "-Hpo", "name"]); - let output = execute(cmd).map_err(Error::from)?; + let output = executor.execute(cmd).map_err(Error::from)?; let stdout = String::from_utf8_lossy(&output.stdout); let zpool = stdout .lines() @@ -250,7 +262,10 @@ impl Zpool { } #[cfg_attr(test, allow(dead_code))] - pub fn get_info(name: &str) -> Result { + pub fn get_info( + executor: &BoxedExecutor, + name: &str, + ) -> Result { let mut command = std::process::Command::new(ZPOOL); let cmd = command.args(&[ "list", @@ -259,7 +274,7 @@ impl Zpool { name, ]); - let output = execute(cmd).map_err(|err| GetInfoError { + let output = executor.execute(cmd).map_err(|err| GetInfoError { name: name.to_string(), err: err.into(), })?; diff --git a/installinator/Cargo.toml b/installinator/Cargo.toml index 3b2f04c38f8..6022daee780 100644 --- a/installinator/Cargo.toml +++ b/installinator/Cargo.toml @@ -15,6 +15,8 @@ clap.workspace = true ddm-admin-client.workspace = true display-error-chain.workspace = true futures.workspace = true +helios-fusion.workspace = true +helios-protostar.workspace = true hex.workspace = true http.workspace = true illumos-utils.workspace = true @@ -46,6 +48,7 @@ omicron-workspace-hack = { version = "0.1", path = "../workspace-hack" } [dev-dependencies] omicron-test-utils.workspace = true +helios-tokamak.workspace = true hex-literal.workspace = true partial-io.workspace = true proptest.workspace = true diff --git a/installinator/src/bootstrap.rs b/installinator/src/bootstrap.rs index 2854293d8a4..017e5d0b507 100644 --- a/installinator/src/bootstrap.rs +++ b/installinator/src/bootstrap.rs @@ -10,6 +10,7 @@ use anyhow::ensure; use anyhow::Context; use anyhow::Result; use ddm_admin_client::Client as DdmAdminClient; +use helios_fusion::BoxedExecutor; use illumos_utils::addrobj::AddrObject; use illumos_utils::dladm; use illumos_utils::dladm::Dladm; @@ -29,9 +30,10 @@ const MG_DDM_MANIFEST_PATH: &str = "/opt/oxide/mg-ddm/pkg/ddm/manifest.xml"; pub(crate) async fn bootstrap_sled( data_links: &[String; 2], log: Logger, + executor: &BoxedExecutor, ) -> Result<()> { // Find address objects to pass to maghemite. - let links = underlay::find_chelsio_links(data_links) + let links = underlay::find_chelsio_links(executor, data_links) .context("failed to find chelsio links")?; ensure!( !links.is_empty(), @@ -39,8 +41,10 @@ pub(crate) async fn bootstrap_sled( ); let mg_addr_objs = - underlay::ensure_links_have_global_zone_link_local_v6_addresses(&links) - .context("failed to create address objects for maghemite")?; + underlay::ensure_links_have_global_zone_link_local_v6_addresses( + executor, &links, + ) + .context("failed to create address objects for maghemite")?; info!(log, "Starting mg-ddm service"); { @@ -54,20 +58,22 @@ pub(crate) async fn bootstrap_sled( // Set up an interface for our bootstrap network. let bootstrap_etherstub = - Dladm::ensure_etherstub(dladm::BOOTSTRAP_ETHERSTUB_NAME) + Dladm::ensure_etherstub(executor, dladm::BOOTSTRAP_ETHERSTUB_NAME) .context("failed to ensure bootstrap etherstub existence")?; let bootstrap_etherstub_vnic = - Dladm::ensure_etherstub_vnic(&bootstrap_etherstub) + Dladm::ensure_etherstub_vnic(executor, &bootstrap_etherstub) .context("failed to ensure bootstrap etherstub vnic existence")?; // Use the mac address of the first link to derive our bootstrap address. - let ip = - BootstrapInterface::GlobalZone.ip(&links[0]).with_context(|| { + let ip = BootstrapInterface::GlobalZone + .ip(executor, &links[0]) + .with_context(|| { format!("failed to derive a bootstrap prefix from {:?}", links[0]) })?; Zones::ensure_has_global_zone_v6_address( + executor, bootstrap_etherstub_vnic, ip, "bootstrap6", diff --git a/installinator/src/dispatch.rs b/installinator/src/dispatch.rs index 9c06aeac77e..435ae2ebb88 100644 --- a/installinator/src/dispatch.rs +++ b/installinator/src/dispatch.rs @@ -8,6 +8,7 @@ use anyhow::{bail, Context, Result}; use buf_list::{BufList, Cursor}; use camino::{Utf8Path, Utf8PathBuf}; use clap::{Args, Parser, Subcommand}; +use helios_fusion::BoxedExecutor; use installinator_common::{ InstallinatorCompletionMetadata, InstallinatorComponent, InstallinatorSpec, InstallinatorStepId, StepContext, StepHandle, StepProgress, StepSuccess, @@ -40,13 +41,19 @@ pub struct InstallinatorApp { impl InstallinatorApp { /// Executes the app. - pub async fn exec(self, log: &slog::Logger) -> Result<()> { + pub async fn exec( + self, + log: &slog::Logger, + executor: &BoxedExecutor, + ) -> Result<()> { match self.subcommand { InstallinatorCommand::DebugDiscover(opts) => opts.exec(log).await, InstallinatorCommand::DebugHardwareScan(opts) => { - opts.exec(log).await + opts.exec(log, executor).await + } + InstallinatorCommand::Install(opts) => { + opts.exec(log, executor).await } - InstallinatorCommand::Install(opts) => opts.exec(log).await, } } @@ -115,11 +122,15 @@ struct DiscoverOpts { struct DebugHardwareScan {} impl DebugHardwareScan { - async fn exec(self, log: &slog::Logger) -> Result<()> { + async fn exec( + self, + log: &slog::Logger, + executor: &BoxedExecutor, + ) -> Result<()> { // Finding the write destination from the gimlet hardware logs details // about what it's doing sufficiently for this subcommand; just create a // write destination and then discard it. - _ = WriteDestination::from_hardware(log).await?; + _ = WriteDestination::from_hardware(log, executor).await?; Ok(()) } } @@ -174,10 +185,19 @@ struct InstallOpts { } impl InstallOpts { - async fn exec(self, log: &slog::Logger) -> Result<()> { + async fn exec( + self, + log: &slog::Logger, + executor: &BoxedExecutor, + ) -> Result<()> { if self.bootstrap_sled { let data_links = [self.data_link0.clone(), self.data_link1.clone()]; - crate::bootstrap::bootstrap_sled(&data_links, log.clone()).await?; + crate::bootstrap::bootstrap_sled( + &data_links, + log.clone(), + executor, + ) + .await?; } let image_id = self.artifact_ids.resolve()?; @@ -295,7 +315,7 @@ impl InstallOpts { InstallinatorStepId::Scan, "Scanning hardware to find M.2 disks", move |cx| async move { - scan_hardware_with_retries(&cx, &log).await + scan_hardware_with_retries(&cx, &log, executor).await }, ) .register() @@ -352,7 +372,7 @@ impl InstallOpts { // TODO: verify artifact was correctly written out to disk. - let write_output = writer.write(&cx, log).await; + let write_output = writer.write(&cx, log, executor).await; let slots_not_written = write_output.slots_not_written(); let metadata = InstallinatorCompletionMetadata::Write { @@ -431,6 +451,7 @@ async fn check_downloaded_artifact_hash( async fn scan_hardware_with_retries( cx: &StepContext, log: &slog::Logger, + executor: &BoxedExecutor, ) -> Result> { // Scanning for our disks is inherently racy: we have to wait for the disks // to attach. This should take milliseconds in general; we'll set a hard cap @@ -443,7 +464,7 @@ async fn scan_hardware_with_retries( let mut retry = 0; let result = loop { let log = log.clone(); - let result = WriteDestination::from_hardware(&log).await; + let result = WriteDestination::from_hardware(&log, executor).await; match result { Ok(destination) => break Ok(destination), diff --git a/installinator/src/hardware.rs b/installinator/src/hardware.rs index ffa0b74739e..ec4260e490d 100644 --- a/installinator/src/hardware.rs +++ b/installinator/src/hardware.rs @@ -6,6 +6,7 @@ use anyhow::anyhow; use anyhow::ensure; use anyhow::Context; use anyhow::Result; +use helios_fusion::BoxedExecutor; use sled_hardware::Disk; use sled_hardware::DiskVariant; use sled_hardware::HardwareManager; @@ -18,7 +19,7 @@ pub struct Hardware { } impl Hardware { - pub async fn scan(log: &Logger) -> Result { + pub async fn scan(log: &Logger, executor: &BoxedExecutor) -> Result { let is_gimlet = sled_hardware::is_gimlet() .context("failed to detect whether host is a gimlet")?; ensure!(is_gimlet, "hardware scan only supported on gimlets"); @@ -47,7 +48,7 @@ impl Hardware { ); } DiskVariant::M2 => { - let disk = Disk::new(log, disk, None) + let disk = Disk::new(log, executor, disk, None) .await .context("failed to instantiate Disk handle for M.2")?; m2_disks.push(disk); diff --git a/installinator/src/main.rs b/installinator/src/main.rs index 1fb3d3f6786..006cf7457dd 100644 --- a/installinator/src/main.rs +++ b/installinator/src/main.rs @@ -5,12 +5,14 @@ use std::error::Error; use clap::Parser; +use helios_protostar::HostExecutor; use installinator::InstallinatorApp; #[tokio::main] async fn main() -> Result<(), Box> { let app = InstallinatorApp::parse(); let log = InstallinatorApp::setup_log("/tmp/installinator.log")?; - app.exec(&log).await?; + let executor = HostExecutor::new(log.clone()).as_executor(); + app.exec(&log, &executor).await?; Ok(()) } diff --git a/installinator/src/write.rs b/installinator/src/write.rs index 6c0c1f63c7b..a222d507fea 100644 --- a/installinator/src/write.rs +++ b/installinator/src/write.rs @@ -15,6 +15,7 @@ use async_trait::async_trait; use buf_list::BufList; use bytes::Buf; use camino::{Utf8Path, Utf8PathBuf}; +use helios_fusion::BoxedExecutor; use illumos_utils::{ dkio::{self, MediaInfoExtended}, zpool::{Zpool, ZpoolName}, @@ -92,8 +93,11 @@ impl WriteDestination { Ok(Self { drives, is_host_phase_2_block_device: false }) } - pub(crate) async fn from_hardware(log: &Logger) -> Result { - let hardware = Hardware::scan(log).await?; + pub(crate) async fn from_hardware( + log: &Logger, + executor: &BoxedExecutor, + ) -> Result { + let hardware = Hardware::scan(log, executor).await?; // We want the `,raw`-suffixed path to the boot image partition, as that // allows us file-like access via the character device. @@ -221,6 +225,7 @@ impl<'a> ArtifactWriter<'a> { &mut self, cx: &StepContext, log: &Logger, + executor: &BoxedExecutor, ) -> WriteOutput { let mut control_plane_transport = FileTransport; if self.is_host_phase_2_block_device { @@ -228,6 +233,7 @@ impl<'a> ArtifactWriter<'a> { self.write_with_transport( cx, log, + executor, &mut host_transport, &mut control_plane_transport, ) @@ -237,6 +243,7 @@ impl<'a> ArtifactWriter<'a> { self.write_with_transport( cx, log, + executor, &mut host_transport, &mut control_plane_transport, ) @@ -248,6 +255,7 @@ impl<'a> ArtifactWriter<'a> { &mut self, cx: &StepContext, log: &Logger, + executor: &BoxedExecutor, host_phase_2_transport: &mut impl WriteTransport, control_plane_transport: &mut impl WriteTransport, ) -> WriteOutput { @@ -266,6 +274,7 @@ impl<'a> ArtifactWriter<'a> { // want each drive to track success and failure independently. let write_cx = SlotWriteContext { log: log.clone(), + executor, artifacts: self.artifacts, slot: *drive, destinations, @@ -348,6 +357,7 @@ impl<'a> ArtifactWriter<'a> { struct SlotWriteContext<'a> { log: Logger, + executor: &'a BoxedExecutor, artifacts: ArtifactsToWrite<'a>, slot: M2Slot, destinations: &'a ArtifactDestination, @@ -498,6 +508,7 @@ impl<'a> SlotWriteContext<'a> { self.artifacts .write_control_plane( &self.log, + &self.executor, self.slot, self.destinations, transport, @@ -549,6 +560,7 @@ impl ArtifactsToWrite<'_> { async fn write_control_plane( &self, log: &Logger, + executor: &BoxedExecutor, slot: M2Slot, destinations: &ArtifactDestination, transport: &mut impl WriteTransport, @@ -558,6 +570,7 @@ impl ArtifactsToWrite<'_> { // own step. let inner_cx = &ControlPlaneZoneWriteContext { slot, + executor, clean_output_directory: destinations.clean_control_plane_dir, output_directory: &destinations.control_plane_dir, zones: self.control_plane_zones, @@ -591,6 +604,7 @@ impl ArtifactsToWrite<'_> { struct ControlPlaneZoneWriteContext<'a> { slot: M2Slot, + executor: &'a BoxedExecutor, clean_output_directory: bool, output_directory: &'a Utf8Path, zones: &'a ControlPlaneZoneImages, @@ -689,7 +703,7 @@ impl ControlPlaneZoneWriteContext<'_> { std::mem::drop(output_directory); if let Some(zpool) = zpool { - Zpool::export(zpool)?; + Zpool::export(&self.executor, zpool)?; } StepSuccess::new(()).into() @@ -1148,6 +1162,9 @@ mod tests { let engine = UpdateEngine::new(&logctx.log, event_sender); let log = logctx.log.clone(); + let executor = helios_tokamak::FakeExecutorBuilder::new(log.clone()) + .build() + .as_executor(); engine .new_step( InstallinatorComponent::Both, @@ -1158,6 +1175,7 @@ mod tests { .write_with_transport( &cx, &log, + &executor, &mut host_transport, &mut control_plane_transport, ) diff --git a/package/Cargo.toml b/package/Cargo.toml index 9fc46100200..a77a292ca59 100644 --- a/package/Cargo.toml +++ b/package/Cargo.toml @@ -9,6 +9,8 @@ license = "MPL-2.0" anyhow.workspace = true clap.workspace = true futures.workspace = true +helios-fusion.workspace = true +helios-protostar.workspace = true hex.workspace = true illumos-utils.workspace = true indicatif.workspace = true diff --git a/package/src/bin/omicron-package.rs b/package/src/bin/omicron-package.rs index a0146eee50b..3c0e62584af 100644 --- a/package/src/bin/omicron-package.rs +++ b/package/src/bin/omicron-package.rs @@ -7,6 +7,8 @@ use anyhow::{anyhow, bail, Context, Result}; use clap::{Parser, Subcommand}; use futures::stream::{self, StreamExt, TryStreamExt}; +use helios_fusion::BoxedExecutor; +use helios_protostar::HostExecutor; use illumos_utils::{zfs, zone}; use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; use omicron_package::target::KnownTarget; @@ -598,20 +600,23 @@ async fn do_install( do_activate(config, install_dir) } -async fn uninstall_all_omicron_zones() -> Result<()> { +async fn uninstall_all_omicron_zones(executor: &BoxedExecutor) -> Result<()> { const CONCURRENCY_CAP: usize = 32; - futures::stream::iter(zone::Zones::get().await?) + futures::stream::iter(zone::Zones::get(executor).await?) .map(Ok::<_, anyhow::Error>) .try_for_each_concurrent(CONCURRENCY_CAP, |zone| async move { - zone::Zones::halt_and_remove(zone.name()).await?; + zone::Zones::halt_and_remove(executor, zone.name()).await?; Ok(()) }) .await?; Ok(()) } -fn uninstall_all_omicron_datasets(config: &Config) -> Result<()> { - let datasets = match zfs::get_all_omicron_datasets_for_delete() { +fn uninstall_all_omicron_datasets( + executor: &BoxedExecutor, + config: &Config, +) -> Result<()> { + let datasets = match zfs::get_all_omicron_datasets_for_delete(executor) { Err(e) => { warn!(config.log, "Failed to get omicron datasets: {}", e); return Err(e); @@ -629,7 +634,7 @@ fn uninstall_all_omicron_datasets(config: &Config) -> Result<()> { ))?; for dataset in &datasets { info!(config.log, "Deleting dataset: {dataset}"); - zfs::Zfs::destroy_dataset(dataset)?; + zfs::Zfs::destroy_dataset(executor, dataset)?; } Ok(()) @@ -698,19 +703,21 @@ fn remove_all_except>( } async fn do_deactivate(config: &Config) -> Result<()> { + let executor = HostExecutor::new(config.log.clone()).as_executor(); info!(&config.log, "Removing all Omicron zones"); - uninstall_all_omicron_zones().await?; + uninstall_all_omicron_zones(&executor).await?; info!(config.log, "Uninstalling all packages"); uninstall_all_packages(config); info!(config.log, "Removing networking resources"); - cleanup_networking_resources(&config.log).await?; + cleanup_networking_resources(&config.log, &executor).await?; Ok(()) } async fn do_uninstall(config: &Config) -> Result<()> { + let executor = HostExecutor::new(config.log.clone()).as_executor(); do_deactivate(config).await?; info!(config.log, "Removing datasets"); - uninstall_all_omicron_datasets(config)?; + uninstall_all_omicron_datasets(&executor, config)?; Ok(()) } diff --git a/sled-agent/Cargo.toml b/sled-agent/Cargo.toml index b1316983954..d704940506b 100644 --- a/sled-agent/Cargo.toml +++ b/sled-agent/Cargo.toml @@ -31,6 +31,8 @@ dropshot.workspace = true flate2.workspace = true futures.workspace = true glob.workspace = true +helios-fusion.workspace = true +helios-protostar.workspace = true http.workspace = true hyper-staticfile.workspace = true gateway-client.workspace = true @@ -84,6 +86,7 @@ opte-ioctl.workspace = true [dev-dependencies] assert_matches.workspace = true expectorate.workspace = true +helios-tokamak.workspace = true http.workspace = true hyper.workspace = true omicron-test-utils.workspace = true @@ -91,13 +94,10 @@ openapi-lint.workspace = true openapiv3.workspace = true pretty_assertions.workspace = true rcgen.workspace = true -serial_test.workspace = true subprocess.workspace = true slog-async.workspace = true slog-term.workspace = true -illumos-utils = { workspace = true, features = ["testing"] } - # # Disable doc builds by default for our binaries to work around issue # rust-lang/cargo#8373. These docs would not be very useful anyway. diff --git a/sled-agent/src/bootstrap/pre_server.rs b/sled-agent/src/bootstrap/pre_server.rs index 0899bdd82fe..91a482c8553 100644 --- a/sled-agent/src/bootstrap/pre_server.rs +++ b/sled-agent/src/bootstrap/pre_server.rs @@ -22,6 +22,8 @@ use cancel_safe_futures::TryStreamExt; use ddm_admin_client::Client as DdmAdminClient; use futures::stream; use futures::StreamExt; +use helios_fusion::BoxedExecutor; +use helios_protostar::HostExecutor; use illumos_utils::addrobj::AddrObject; use illumos_utils::dladm; use illumos_utils::dladm::Dladm; @@ -141,6 +143,7 @@ impl BootstrapManagers { pub(super) struct BootstrapAgentStartup { pub(super) config: Config, + pub(super) executor: BoxedExecutor, pub(super) global_zone_bootstrap_ip: Ipv6Addr, pub(super) ddm_admin_localhost_client: DdmAdminClient, pub(super) base_log: Logger, @@ -154,15 +157,18 @@ impl BootstrapAgentStartup { let base_log = build_logger(&config)?; let log = base_log.new(o!("component" => "BootstrapAgentStartup")); + let executor = HostExecutor::new(log.clone()).as_executor(); // Perform several blocking startup tasks first; we move `config` and // `log` into this task, and on success, it gives them back to us. + let executor_clone = executor.clone(); let (config, log, ddm_admin_localhost_client, startup_networking) = tokio::task::spawn_blocking(move || { - enable_mg_ddm(&config, &log)?; + enable_mg_ddm(&config, &log, &executor_clone)?; ensure_zfs_key_directory_exists(&log)?; - let startup_networking = BootstrapNetworking::setup(&config)?; + let startup_networking = + BootstrapNetworking::setup(&config, &executor_clone)?; // Start trying to notify ddmd of our bootstrap address so it can // advertise it to other sleds. @@ -175,7 +181,7 @@ impl BootstrapAgentStartup { // Before we create the switch zone, we need to ensure that the // necessary ZFS and Zone resources are ready. All other zones // are created on U.2 drives. - ensure_zfs_ramdisk_dataset()?; + ensure_zfs_ramdisk_dataset(&executor_clone)?; Ok::<_, StartError>(( config, @@ -191,14 +197,14 @@ impl BootstrapAgentStartup { // predictable state. // // This means all VNICs, zones, etc. - cleanup_all_old_global_state(&log).await?; + cleanup_all_old_global_state(&log, &executor).await?; // Ipv6 forwarding must be enabled to route traffic between zones, // including the switch zone which we may launch below if we find we're // actually running on a scrimlet. // // This should be a no-op if already enabled. - BootstrapNetworking::enable_ipv6_forwarding().await?; + BootstrapNetworking::enable_ipv6_forwarding(&executor).await?; // Spawn the `KeyManager` which is needed by the the StorageManager to // retrieve encryption keys. @@ -227,7 +233,8 @@ impl BootstrapAgentStartup { // Create a `StorageManager` and (possibly) synthetic disks. let storage_manager = - StorageManager::new(&base_log, storage_key_requester).await; + StorageManager::new(&base_log, &executor, storage_key_requester) + .await; upsert_synthetic_zpools_if_needed(&log, &storage_manager, &config) .await; @@ -236,6 +243,7 @@ impl BootstrapAgentStartup { let service_manager = ServiceManager::new( &base_log, + &executor, ddm_admin_localhost_client.clone(), startup_networking, sled_mode, @@ -248,6 +256,7 @@ impl BootstrapAgentStartup { Ok(Self { config, + executor, global_zone_bootstrap_ip, ddm_admin_localhost_client, base_log, @@ -285,7 +294,10 @@ fn build_logger(config: &Config) -> Result { // This may re-establish contact in the future, and re-construct a picture of // the expected state of each service. However, at the moment, "starting from a // known clean slate" is easier to work with. -async fn cleanup_all_old_global_state(log: &Logger) -> Result<(), StartError> { +async fn cleanup_all_old_global_state( + log: &Logger, + executor: &BoxedExecutor, +) -> Result<(), StartError> { // Identify all existing zones which should be managed by the Sled // Agent. // @@ -293,7 +305,7 @@ async fn cleanup_all_old_global_state(log: &Logger) -> Result<(), StartError> { // Currently, we're removing these zones. In the future, we should // re-establish contact (i.e., if the Sled Agent crashed, but we wanted // to leave the running Zones intact). - let zones = Zones::get().await.map_err(StartError::ListZones)?; + let zones = Zones::get(&executor).await.map_err(StartError::ListZones)?; stream::iter(zones) .zip(stream::iter(std::iter::repeat(log.clone()))) @@ -303,7 +315,7 @@ async fn cleanup_all_old_global_state(log: &Logger) -> Result<(), StartError> { // the caller that this failed. .for_each_concurrent_then_try(None, |(zone, log)| async move { warn!(log, "Deleting existing zone"; "zone_name" => zone.name()); - Zones::halt_and_remove_logged(&log, zone.name()).await + Zones::halt_and_remove_logged(&log, &executor, zone.name()).await }) .await .map_err(StartError::DeleteZone)?; @@ -321,7 +333,7 @@ async fn cleanup_all_old_global_state(log: &Logger) -> Result<(), StartError> { // Note that we don't currently delete the VNICs in any particular // order. That should be OK, since we're definitely deleting the guest // VNICs before the xde devices, which is the main constraint. - sled_hardware::cleanup::delete_omicron_vnics(&log) + sled_hardware::cleanup::delete_omicron_vnics(&log, &executor) .await .map_err(StartError::DeleteOmicronVnics)?; @@ -338,8 +350,12 @@ async fn cleanup_all_old_global_state(log: &Logger) -> Result<(), StartError> { Ok(()) } -fn enable_mg_ddm(config: &Config, log: &Logger) -> Result<(), StartError> { - let mg_addr_objs = underlay::find_nics(&config.data_links) +fn enable_mg_ddm( + config: &Config, + log: &Logger, + executor: &BoxedExecutor, +) -> Result<(), StartError> { + let mg_addr_objs = underlay::find_nics(executor, &config.data_links) .map_err(StartError::FindMaghemiteAddrObjs)?; if mg_addr_objs.is_empty() { return Err(StartError::NoUnderlayAddrObjs); @@ -367,12 +383,15 @@ fn ensure_zfs_key_directory_exists(log: &Logger) -> Result<(), StartError> { }) } -fn ensure_zfs_ramdisk_dataset() -> Result<(), StartError> { +fn ensure_zfs_ramdisk_dataset( + executor: &BoxedExecutor, +) -> Result<(), StartError> { let zoned = true; let do_format = true; let encryption_details = None; let quota = None; Zfs::ensure_filesystem( + executor, zfs::ZONE_ZFS_RAMDISK_DATASET, zfs::Mountpoint::Path(Utf8PathBuf::from( zfs::ZONE_ZFS_RAMDISK_DATASET_MOUNTPOINT, @@ -459,29 +478,34 @@ pub(crate) struct BootstrapNetworking { } impl BootstrapNetworking { - fn setup(config: &Config) -> Result { - let link_for_mac = config.get_link().map_err(StartError::ConfigLink)?; + fn setup( + config: &Config, + executor: &BoxedExecutor, + ) -> Result { + let link_for_mac = + config.get_link(executor).map_err(StartError::ConfigLink)?; let global_zone_bootstrap_ip = underlay::BootstrapInterface::GlobalZone - .ip(&link_for_mac) + .ip(executor, &link_for_mac) .map_err(StartError::BootstrapLinkMac)?; - let bootstrap_etherstub = Dladm::ensure_etherstub( - dladm::BOOTSTRAP_ETHERSTUB_NAME, - ) - .map_err(|err| StartError::EnsureEtherstubError { - name: dladm::BOOTSTRAP_ETHERSTUB_NAME, - err, - })?; + let bootstrap_etherstub = + Dladm::ensure_etherstub(executor, dladm::BOOTSTRAP_ETHERSTUB_NAME) + .map_err(|err| StartError::EnsureEtherstubError { + name: dladm::BOOTSTRAP_ETHERSTUB_NAME, + err, + })?; let bootstrap_etherstub_vnic = - Dladm::ensure_etherstub_vnic(&bootstrap_etherstub)?; + Dladm::ensure_etherstub_vnic(executor, &bootstrap_etherstub)?; Zones::ensure_has_global_zone_v6_address( + executor, bootstrap_etherstub_vnic.clone(), global_zone_bootstrap_ip, "bootstrap6", )?; let global_zone_bootstrap_link_local_address = Zones::get_address( + executor, None, // AddrObject::link_local() can only fail if the interface name is // malformed, but we just got it from `Dladm`, so we know it's @@ -499,18 +523,17 @@ impl BootstrapNetworking { }; let switch_zone_bootstrap_ip = underlay::BootstrapInterface::SwitchZone - .ip(&link_for_mac) + .ip(executor, &link_for_mac) .map_err(StartError::BootstrapLinkMac)?; - let underlay_etherstub = Dladm::ensure_etherstub( - dladm::UNDERLAY_ETHERSTUB_NAME, - ) - .map_err(|err| StartError::EnsureEtherstubError { - name: dladm::UNDERLAY_ETHERSTUB_NAME, - err, - })?; + let underlay_etherstub = + Dladm::ensure_etherstub(executor, dladm::UNDERLAY_ETHERSTUB_NAME) + .map_err(|err| StartError::EnsureEtherstubError { + name: dladm::UNDERLAY_ETHERSTUB_NAME, + err, + })?; let underlay_etherstub_vnic = - Dladm::ensure_etherstub_vnic(&underlay_etherstub)?; + Dladm::ensure_etherstub_vnic(executor, &underlay_etherstub)?; Ok(Self { bootstrap_etherstub, @@ -522,21 +545,21 @@ impl BootstrapNetworking { }) } - async fn enable_ipv6_forwarding() -> Result<(), StartError> { - tokio::task::spawn_blocking(|| { - let mut command = std::process::Command::new(illumos_utils::PFEXEC); - let cmd = command.args(&[ - "/usr/sbin/routeadm", - // Needed to access all zones, which are on the underlay. - "-e", - "ipv6-forwarding", - "-u", - ]); - illumos_utils::execute(cmd) - .map(|_output| ()) - .map_err(StartError::EnableIpv6Forwarding) - }) - .await - .unwrap() + async fn enable_ipv6_forwarding( + executor: &BoxedExecutor, + ) -> Result<(), StartError> { + let mut command = std::process::Command::new(helios_fusion::PFEXEC); + command.args(&[ + "/usr/sbin/routeadm", + // Needed to access all zones, which are on the underlay. + "-e", + "ipv6-forwarding", + "-u", + ]); + executor + .execute_async(&mut tokio::process::Command::from(command)) + .await + .map(|_output| ()) + .map_err(StartError::EnableIpv6Forwarding) } } diff --git a/sled-agent/src/bootstrap/server.rs b/sled-agent/src/bootstrap/server.rs index 0cbbf0678b2..51d56044bd2 100644 --- a/sled-agent/src/bootstrap/server.rs +++ b/sled-agent/src/bootstrap/server.rs @@ -35,6 +35,7 @@ use ddm_admin_client::DdmError; use dropshot::HttpServer; use futures::Future; use futures::StreamExt; +use helios_fusion::BoxedExecutor; use illumos_utils::dladm; use illumos_utils::zfs; use illumos_utils::zone; @@ -100,7 +101,7 @@ pub enum StartError { EnsureEtherstubError { name: &'static str, #[source] - err: illumos_utils::ExecutionError, + err: helios_fusion::ExecutionError, }, #[error(transparent)] @@ -131,7 +132,7 @@ pub enum StartError { DeleteXdeDevices(#[source] illumos_utils::opte::Error), #[error("Failed to enable ipv6-forwarding")] - EnableIpv6Forwarding(#[from] illumos_utils::ExecutionError), + EnableIpv6Forwarding(#[from] helios_fusion::ExecutionError), #[error("Incorrect binary packaging: {0}")] IncorrectBuildPackaging(&'static str), @@ -173,6 +174,7 @@ impl Server { // fail to start. let BootstrapAgentStartup { config, + executor, global_zone_bootstrap_ip, ddm_admin_localhost_client, base_log, @@ -292,6 +294,7 @@ impl Server { let sled_agent_server = wait_while_handling_hardware_updates( start_sled_agent( &config, + &executor, &sled_request.request, &bootstore_handles.node_handle, &managers, @@ -345,6 +348,7 @@ impl Server { // agent state. let inner = Inner { config, + executor, hardware_monitor, state, sled_init_rx, @@ -424,8 +428,10 @@ impl From for StartError { } } +#[allow(clippy::too_many_arguments)] async fn start_sled_agent( config: &SledConfig, + executor: &BoxedExecutor, request: &StartSledAgentRequest, bootstore: &bootstore::NodeHandle, managers: &BootstrapManagers, @@ -469,6 +475,7 @@ async fn start_sled_agent( let server = SledAgentServer::start( config, base_log.clone(), + &executor, request.clone(), managers.service.clone(), managers.storage.clone(), @@ -636,6 +643,7 @@ pub fn run_openapi() -> Result<(), String> { struct Inner { config: SledConfig, + executor: BoxedExecutor, hardware_monitor: broadcast::Receiver, state: SledAgentState, sled_init_rx: mpsc::Receiver<( @@ -721,6 +729,7 @@ impl Inner { SledAgentState::Bootstrapping => { let response = match start_sled_agent( &self.config, + &self.executor, &request, &self.bootstore_handles.node_handle, &self.managers, @@ -778,14 +787,14 @@ impl Inner { // Uninstall all oxide zones (except the switch zone) async fn uninstall_zones(&self) -> Result<(), BootstrapError> { const CONCURRENCY_CAP: usize = 32; - futures::stream::iter(Zones::get().await?) + futures::stream::iter(Zones::get(&self.executor).await?) .map(Ok::<_, anyhow::Error>) // Use for_each_concurrent_then_try to delete as much as possible. // We only return one error though -- hopefully that's enough to // signal to the caller that this failed. .for_each_concurrent_then_try(CONCURRENCY_CAP, |zone| async move { if zone.name() != "oxz_switch" { - Zones::halt_and_remove(zone.name()).await?; + Zones::halt_and_remove(&self.executor, zone.name()).await?; } Ok(()) }) @@ -847,9 +856,9 @@ impl Inner { // these addresses would delete "cxgbe0/ll", and could render // the sled inaccessible via a local interface. - sled_hardware::cleanup::delete_underlay_addresses(&log) + sled_hardware::cleanup::delete_underlay_addresses(&log, &self.executor) .map_err(BootstrapError::Cleanup)?; - sled_hardware::cleanup::delete_omicron_vnics(&log) + sled_hardware::cleanup::delete_omicron_vnics(&log, &self.executor) .await .map_err(BootstrapError::Cleanup)?; illumos_utils::opte::delete_all_xde_devices(&log)?; @@ -860,11 +869,11 @@ impl Inner { &self, log: &Logger, ) -> Result<(), BootstrapError> { - let datasets = zfs::get_all_omicron_datasets_for_delete() + let datasets = zfs::get_all_omicron_datasets_for_delete(&self.executor) .map_err(BootstrapError::ZfsDatasetsList)?; for dataset in &datasets { info!(log, "Removing dataset: {dataset}"); - zfs::Zfs::destroy_dataset(dataset)?; + zfs::Zfs::destroy_dataset(&self.executor, dataset)?; } Ok(()) diff --git a/sled-agent/src/config.rs b/sled-agent/src/config.rs index 2473c14566a..100ea33a99f 100644 --- a/sled-agent/src/config.rs +++ b/sled-agent/src/config.rs @@ -7,6 +7,7 @@ use crate::updates::ConfigUpdates; use camino::{Utf8Path, Utf8PathBuf}; use dropshot::ConfigLogging; +use helios_fusion::BoxedExecutor; use illumos_utils::dladm::Dladm; use illumos_utils::dladm::FindPhysicalLinkError; use illumos_utils::dladm::PhysicalLink; @@ -121,12 +122,15 @@ impl Config { Ok(config) } - pub fn get_link(&self) -> Result { + pub fn get_link( + &self, + executor: &BoxedExecutor, + ) -> Result { if let Some(link) = self.data_link.as_ref() { Ok(link.clone()) } else { if is_gimlet().map_err(ConfigError::SystemDetection)? { - Dladm::list_physical() + Dladm::list_physical(executor) .map_err(ConfigError::FindLinks)? .into_iter() .find(|link| link.0.starts_with(CHELSIO_LINK_PREFIX)) @@ -136,7 +140,7 @@ impl Config { ) }) } else { - Dladm::find_physical().map_err(ConfigError::FindLinks) + Dladm::find_physical(executor).map_err(ConfigError::FindLinks) } } } diff --git a/sled-agent/src/instance.rs b/sled-agent/src/instance.rs index baf92af28aa..6b91c707dac 100644 --- a/sled-agent/src/instance.rs +++ b/sled-agent/src/instance.rs @@ -23,6 +23,7 @@ use crate::zone_bundle::ZoneBundler; use anyhow::anyhow; use backoff::BackoffError; use futures::lock::{Mutex, MutexGuard}; +use helios_fusion::BoxedExecutor; use illumos_utils::dladm::Etherstub; use illumos_utils::link::VnicAllocator; use illumos_utils::opte::PortManager; @@ -208,6 +209,8 @@ struct PropolisSetup { struct InstanceInner { log: Logger, + executor: BoxedExecutor, + // Properties visible to Propolis properties: propolis_client::api::InstanceProperties, @@ -573,7 +576,9 @@ impl InstanceInner { // `RunningZone::stop` in case we're called between creating the // zone and assigning `running_state`. warn!(self.log, "Halting and removing zone: {}", zname); - Zones::halt_and_remove_logged(&self.log, &zname).await.unwrap(); + Zones::halt_and_remove_logged(&self.log, &self.executor, &zname) + .await + .unwrap(); // Remove ourselves from the instance manager's map of instances. self.instance_ticket.terminate(); @@ -616,6 +621,7 @@ impl Instance { #[allow(clippy::too_many_arguments)] pub fn new( log: Logger, + executor: &BoxedExecutor, id: Uuid, ticket: InstanceTicket, initial: InstanceHardware, @@ -628,6 +634,7 @@ impl Instance { info!(log, "Instance::new w/initial HW: {:?}", initial); let instance = InstanceInner { log: log.new(o!("instance_id" => id.to_string())), + executor: executor.clone(), // NOTE: Mostly lies. properties: propolis_client::api::InstanceProperties { id, @@ -896,6 +903,7 @@ impl Instance { .clone(); let installed_zone = InstalledZone::install( &inner.log, + &inner.executor, &inner.vnic_allocator, &root, &["/opt/oxide".into()], @@ -973,7 +981,7 @@ impl Instance { // but it helps distinguish "online in SMF" from "responding to HTTP // requests". let fmri = fmri_name(); - wait_for_service(Some(&zname), &fmri) + wait_for_service(&inner.executor, Some(&zname), &fmri) .await .map_err(|_| Error::Timeout(fmri.to_string()))?; info!(inner.log, "Propolis SMF service is online"); diff --git a/sled-agent/src/instance_manager.rs b/sled-agent/src/instance_manager.rs index bdd29e4d1f5..9dce18bff90 100644 --- a/sled-agent/src/instance_manager.rs +++ b/sled-agent/src/instance_manager.rs @@ -15,6 +15,7 @@ use crate::params::{ use crate::storage_manager::StorageResources; use crate::zone_bundle::BundleError; use crate::zone_bundle::ZoneBundler; +use helios_fusion::BoxedExecutor; use illumos_utils::dladm::Etherstub; use illumos_utils::link::VnicAllocator; use illumos_utils::opte::PortManager; @@ -49,6 +50,7 @@ pub enum Error { struct InstanceManagerInternal { log: Logger, + executor: BoxedExecutor, nexus_client: NexusClientWithResolver, /// Last set size of the VMM reservoir (in bytes) @@ -75,6 +77,7 @@ impl InstanceManager { /// Initializes a new [`InstanceManager`] object. pub fn new( log: Logger, + executor: &BoxedExecutor, nexus_client: NexusClientWithResolver, etherstub: Etherstub, port_manager: PortManager, @@ -84,12 +87,15 @@ impl InstanceManager { Ok(InstanceManager { inner: Arc::new(InstanceManagerInternal { log: log.new(o!("component" => "InstanceManager")), + executor: executor.clone(), nexus_client, // no reservoir size set on startup reservoir_size: Mutex::new(ByteCount::from_kibibytes_u32(0)), instances: Mutex::new(BTreeMap::new()), - vnic_allocator: VnicAllocator::new("Instance", etherstub), + vnic_allocator: VnicAllocator::new( + executor, "Instance", etherstub, + ), port_manager, storage, zone_bundler, @@ -209,6 +215,7 @@ impl InstanceManager { InstanceTicket::new(instance_id, self.inner.clone()); let instance = Instance::new( instance_log, + &self.inner.executor, instance_id, ticket, initial_hardware, diff --git a/sled-agent/src/server.rs b/sled-agent/src/server.rs index 156547627c2..a0fc0279006 100644 --- a/sled-agent/src/server.rs +++ b/sled-agent/src/server.rs @@ -12,6 +12,7 @@ use crate::nexus::NexusClientWithResolver; use crate::services::ServiceManager; use crate::storage_manager::StorageManager; use bootstore::schemes::v0 as bootstore; +use helios_fusion::BoxedExecutor; use internal_dns::resolver::Resolver; use slog::Logger; use std::net::SocketAddr; @@ -38,6 +39,7 @@ impl Server { pub async fn start( config: &Config, log: Logger, + executor: &BoxedExecutor, request: StartSledAgentRequest, services: ServiceManager, storage: StorageManager, @@ -60,6 +62,7 @@ impl Server { let sled_agent = SledAgent::new( &config, log.clone(), + executor, nexus_client, request, services, diff --git a/sled-agent/src/services.rs b/sled-agent/src/services.rs index 96cdf8222b1..30825b979db 100644 --- a/sled-agent/src/services.rs +++ b/sled-agent/src/services.rs @@ -46,6 +46,7 @@ use camino::{Utf8Path, Utf8PathBuf}; use ddm_admin_client::{Client as DdmAdminClient, DdmError}; use dpd_client::{types as DpdTypes, Client as DpdClient, Error as DpdError}; use dropshot::HandlerTaskMode; +use helios_fusion::{BoxedExecutor, PFEXEC}; use illumos_utils::addrobj::AddrObject; use illumos_utils::addrobj::IPV6_LINK_LOCAL_NAME; use illumos_utils::dladm::{ @@ -59,7 +60,6 @@ use illumos_utils::running_zone::{ use illumos_utils::zfs::ZONE_ZFS_RAMDISK_DATASET_MOUNTPOINT; use illumos_utils::zone::AddressRequest; use illumos_utils::zone::Zones; -use illumos_utils::{execute, PFEXEC}; use internal_dns::resolver::Resolver; use itertools::Itertools; use omicron_common::address::Ipv6Subnet; @@ -205,7 +205,7 @@ pub enum Error { NtpZoneNotReady, #[error("Execution error: {0}")] - ExecutionError(#[from] illumos_utils::ExecutionError), + ExecutionError(#[from] helios_fusion::ExecutionError), #[error("Error resolving DNS name: {0}")] ResolveError(#[from] internal_dns::resolver::ResolveError), @@ -354,6 +354,7 @@ enum SledLocalZone { /// Manages miscellaneous Sled-local services. pub struct ServiceManagerInner { log: Logger, + executor: BoxedExecutor, global_zone_bootstrap_link_local_address: Ipv6Addr, switch_zone: Mutex, sled_mode: SledMode, @@ -409,6 +410,7 @@ impl ServiceManager { #[allow(clippy::too_many_arguments)] pub(crate) fn new( log: &Logger, + executor: &BoxedExecutor, ddmd_client: DdmAdminClient, bootstrap_networking: BootstrapNetworking, sled_mode: SledMode, @@ -422,6 +424,7 @@ impl ServiceManager { Self { inner: Arc::new(ServiceManagerInner { log: log.clone(), + executor: executor.clone(), global_zone_bootstrap_link_local_address: bootstrap_networking .global_zone_bootstrap_link_local_ip, // TODO(https://github.com/oxidecomputer/omicron/issues/725): @@ -434,11 +437,13 @@ impl ServiceManager { switch_zone_maghemite_links, zones: Mutex::new(BTreeMap::new()), underlay_vnic_allocator: VnicAllocator::new( + executor, "Service", bootstrap_networking.underlay_etherstub, ), underlay_vnic: bootstrap_networking.underlay_etherstub_vnic, bootstrap_vnic_allocator: VnicAllocator::new( + executor, "Bootstrap", bootstrap_networking.bootstrap_etherstub, ), @@ -742,7 +747,7 @@ impl ServiceManager { // The tfport service requires a MAC device to/from which sidecar // packets may be multiplexed. If the link isn't present, don't // bother trying to start the zone. - match Dladm::verify_link(pkt_source) { + match Dladm::verify_link(&self.inner.executor, pkt_source) { Ok(link) => { // It's important that tfpkt does **not** receive a // link local address! See: https://github.com/oxidecomputer/stlouis/issues/391 @@ -761,7 +766,10 @@ impl ServiceManager { // If on a non-gimlet, sled-agent can be configured to map // links into the switch zone. Validate those links here. for link in &self.inner.switch_zone_maghemite_links { - match Dladm::verify_link(&link.to_string()) { + match Dladm::verify_link( + &self.inner.executor, + &link.to_string(), + ) { Ok(link) => { // Link local addresses should be created in the // zone so that maghemite can listen on them. @@ -1075,13 +1083,15 @@ impl ServiceManager { .map(|d| zone::Device { name: d.to_string() }) .collect(); - // Look for the image in the ramdisk first - let mut zone_image_paths = vec![Utf8PathBuf::from("/opt/oxide")]; + let mut zone_image_paths = vec![]; // Inject an image path if requested by a test. if let Some(path) = self.inner.image_directory_override.get() { zone_image_paths.push(path.clone()); }; + // Look for the image in the ramdisk next. + zone_image_paths.push(Utf8PathBuf::from("/opt/oxide")); + // If the boot disk exists, look for the image in the "install" dataset // there too. if let Some((_, boot_zpool)) = self.inner.storage.boot_disk().await { @@ -1093,6 +1103,7 @@ impl ServiceManager { let installed_zone = InstalledZone::install( &self.inner.log, + &self.inner.executor, &self.inner.underlay_vnic_allocator, &request.root, zone_image_paths.as_slice(), @@ -1316,6 +1327,7 @@ impl ServiceManager { IPV6_LINK_LOCAL_NAME ); Zones::ensure_has_link_local_v6_address( + &self.inner.executor, Some(running_zone.name()), &AddrObject::new(link.name(), IPV6_LINK_LOCAL_NAME) .unwrap(), @@ -1574,6 +1586,7 @@ impl ServiceManager { // to be given distinct names. let addr_name = format!("internaldns{gz_address_index}"); Zones::ensure_has_global_zone_v6_address( + &self.inner.executor, self.inner.underlay_vnic.clone(), *gz_address, &addr_name, @@ -2202,7 +2215,7 @@ impl ServiceManager { let name = zone.zone_name(); if existing_zones.contains_key(&name) { // Make sure the zone actually exists in the right state too - match Zones::find(&name).await { + match Zones::find(&self.inner.executor, &name).await { Ok(Some(zone)) if zone.state() == zone::State::Running => { info!(log, "skipping running zone"; "zone" => &name); continue; @@ -2267,6 +2280,7 @@ impl ServiceManager { // safer. if zone.name().contains(&ZoneType::CockroachDb.to_string()) { let address = Zones::get_address( + &self.inner.executor, Some(zone.name()), &zone.control_interface(), )? @@ -2356,7 +2370,7 @@ impl ServiceManager { &format!("{}", now.as_secs()), &file.as_str(), ]); - match execute(cmd) { + match self.inner.executor.execute(cmd) { Err(e) => { warn!(self.inner.log, "Updating {} failed: {}", &file, e); } @@ -2493,7 +2507,8 @@ impl ServiceManager { ..Default::default() }; filesystems.push(softnpu_filesystem); - data_links = Dladm::get_simulated_tfports()?; + data_links = + Dladm::get_simulated_tfports(&self.inner.executor)?; } vec![ ServiceType::Dendrite { asic }, @@ -2930,20 +2945,20 @@ mod test { use super::*; use crate::params::{ServiceZoneService, ZoneType}; use async_trait::async_trait; + use helios_fusion::{Input, Output, OutputExt}; + use helios_tokamak::{CommandSequence, FakeExecutorBuilder}; use illumos_utils::{ dladm::{ - Etherstub, MockDladm, BOOTSTRAP_ETHERSTUB_NAME, - UNDERLAY_ETHERSTUB_NAME, UNDERLAY_ETHERSTUB_VNIC_NAME, + Etherstub, BOOTSTRAP_ETHERSTUB_NAME, UNDERLAY_ETHERSTUB_NAME, + UNDERLAY_ETHERSTUB_VNIC_NAME, }, - svc, - zone::MockZones, + zone::{ZLOGIN, ZONEADM, ZONECFG}, }; use key_manager::{ SecretRetriever, SecretRetrieverError, SecretState, VersionedIkm, }; use omicron_common::address::OXIMETER_PORT; use std::net::{Ipv6Addr, SocketAddrV6}; - use std::os::unix::process::ExitStatusExt; use uuid::Uuid; // Just placeholders. Not used. @@ -2967,75 +2982,137 @@ mod test { } } - // Returns the expectations for a new service to be created. - fn expect_new_service() -> Vec> { - // Create a VNIC - let create_vnic_ctx = MockDladm::create_vnic_context(); - create_vnic_ctx.expect().return_once( - |physical_link: &Etherstub, _, _, _, _| { - assert_eq!(&physical_link.0, &UNDERLAY_ETHERSTUB_NAME); - Ok(()) - }, + // Generate a static executor handler with the expected invocations (and + // responses) when generating a new service. + fn expect_new_service( + handler: &mut CommandSequence, + config: &TestConfig, + zone_id: Uuid, + u2_mountpoint: &Utf8Path, + ) { + handler.expect( + Input::shell(format!("{PFEXEC} /usr/sbin/dladm create-vnic -t -l underlay_stub0 -p mtu=9000 oxControlService0")), + Output::success() + ); + handler.expect( + Input::shell(format!("{PFEXEC} /usr/sbin/dladm set-linkprop -t -p mtu=9000 oxControlService0")), + Output::success() ); - // Install the Omicron Zone - let install_ctx = MockZones::install_omicron_zone_context(); - install_ctx.expect().return_once(|_, _, name, _, _, _, _, _, _| { - assert!(name.starts_with(EXPECTED_ZONE_NAME_PREFIX)); - Ok(()) - }); - // Boot the zone. - let boot_ctx = MockZones::boot_context(); - boot_ctx.expect().return_once(|name| { - assert!(name.starts_with(EXPECTED_ZONE_NAME_PREFIX)); - Ok(()) - }); + handler.expect( + Input::shell(format!("{PFEXEC} {ZONEADM} list -cip")), + Output::success().set_stdout("0:global:running:/::ipkg:shared"), + ); - // After calling `MockZones::boot`, `RunningZone::boot` will then look - // up the zone ID for the booted zone. This goes through - // `MockZone::id` to find the zone and get its ID. - let id_ctx = MockZones::id_context(); - id_ctx.expect().return_once(|name| { - assert!(name.starts_with(EXPECTED_ZONE_NAME_PREFIX)); - Ok(Some(1)) - }); + let zone_name = format!("{EXPECTED_ZONE_NAME_PREFIX}_{zone_id}"); + + let zonepath = format!("{u2_mountpoint}/{zone_name}"); + handler.expect( + Input::shell(format!( + "{PFEXEC} {ZONECFG} -z {zone_name} \ + create -F -b ; \ + set brand=omicron1 ; \ + set zonepath={zonepath} ; \ + set autoboot=false ; \ + set ip-type=exclusive ; \ + add net ; \ + set physical=oxControlService0 ; \ + end" + )), + Output::success(), + ); - // Ensure the address exists - let ensure_address_ctx = MockZones::ensure_address_context(); - ensure_address_ctx.expect().return_once(|_, _, _| { - Ok(ipnetwork::IpNetwork::new(IpAddr::V6(Ipv6Addr::LOCALHOST), 64) - .unwrap()) - }); + let zone_image = + format!("{}/oximeter.tar.gz", config.config_dir.path()); + handler.expect( + Input::shell(format!( + "{PFEXEC} {ZONEADM} -z {zone_name} \ + install {zone_image} /opt/oxide/overlay.tar.gz" + )), + Output::success(), + ); - // Wait for the networking service. - let wait_ctx = svc::wait_for_service_context(); - wait_ctx.expect().return_once(|_, _| Ok(())); - - // Import the manifest, enable the service - let execute_ctx = illumos_utils::execute_context(); - execute_ctx.expect().times(..).returning(|_| { - Ok(std::process::Output { - status: std::process::ExitStatus::from_raw(0), - stdout: vec![], - stderr: vec![], - }) - }); + handler.expect( + Input::shell(format!("{PFEXEC} {ZONEADM} -z {zone_name} boot")), + Output::success(), + ); + + handler.expect( + Input::shell( + format!("{PFEXEC} /usr/bin/svcprop -t -z {zone_name} -p restarter/state svc:/milestone/single-user:default") + ), + Output::success().set_stdout("restarter/state astring online"), + ); - vec![ - Box::new(create_vnic_ctx), - Box::new(install_ctx), - Box::new(boot_ctx), - Box::new(id_ctx), - Box::new(ensure_address_ctx), - Box::new(wait_ctx), - Box::new(execute_ctx), - ] + handler.expect( + Input::shell(format!("{PFEXEC} {ZONEADM} list -cip")), + Output::success().set_stdout( + format!("0:global:running:/::ipkg:shared\n1:{zone_name}:running:{zonepath}::omicron1:excl") + ) + ); + + // Refer to illumos-utils/src/running_zone.rs for the difference here. + // + // On illumos, we tend to avoid using zlogin, and instead use + // thread-level contracts with zenter::zone_enter to run commands within + // the context of zones. + // + // On non-illumos systems, we just pretend to zlogin, since the + // interface for doing so is simpler than the host API to access + // zenter. + let login = if cfg!(target_os = "illumos") { + format!("{PFEXEC} ") + } else { + format!("{PFEXEC} {ZLOGIN} {zone_name} ") + }; + + handler.expect( + Input::shell(format!( + "{login} /usr/sbin/ipadm create-if -t oxControlService0" + )), + Output::success(), + ); + handler.expect( + Input::shell(format!("{login} /usr/sbin/ipadm set-ifprop -t -p mtu=9000 -m ipv4 oxControlService0")), + Output::success(), + ); + handler.expect( + Input::shell(format!("{login} /usr/sbin/ipadm set-ifprop -t -p mtu=9000 -m ipv6 oxControlService0")), + Output::success(), + ); + handler.expect( + Input::shell(format!( + "{login} /usr/sbin/route add -inet6 default -inet6 ::1" + )), + Output::success(), + ); + handler.expect( + Input::shell(format!("{login} /usr/sbin/svccfg import /var/svc/manifest/site/oximeter/manifest.xml")), + Output::success(), + ); + handler.expect( + Input::shell(format!("{login} /usr/sbin/svccfg -s svc:/oxide/oximeter setprop config/id={zone_id}")), + Output::success(), + ); + handler.expect( + Input::shell(format!("{login} /usr/sbin/svccfg -s svc:/oxide/oximeter setprop config/address=[::1]:12223")), + Output::success(), + ); + handler.expect( + Input::shell( + format!("{login} /usr/sbin/svccfg -s svc:/oxide/oximeter:default refresh"), + ), + Output::success(), + ); + handler.expect( + Input::shell( + format!("{login} /usr/sbin/svcadm enable -t svc:/oxide/oximeter:default"), + ), + Output::success(), + ); } - // Prepare to call "ensure" for a new service, then actually call "ensure". async fn ensure_new_service(mgr: &ServiceManager, id: Uuid) { - let _expectations = expect_new_service(); - mgr.ensure_all_services_persistent(ServiceEnsureBody { services: vec![ServiceZoneRequest { id, @@ -3059,8 +3136,6 @@ mod test { .unwrap(); } - // Prepare to call "ensure" for a service which already exists. We should - // return the service without actually installing a new zone. async fn ensure_existing_service(mgr: &ServiceManager, id: Uuid) { mgr.ensure_all_services_persistent(ServiceEnsureBody { services: vec![ServiceZoneRequest { @@ -3085,23 +3160,6 @@ mod test { .unwrap(); } - // Prepare to drop the service manager. - // - // This will shut down all allocated zones, and delete their - // associated VNICs. - fn drop_service_manager(mgr: ServiceManager) { - let halt_ctx = MockZones::halt_and_remove_logged_context(); - halt_ctx.expect().returning(|_, name| { - assert!(name.starts_with(EXPECTED_ZONE_NAME_PREFIX)); - Ok(()) - }); - let delete_vnic_ctx = MockDladm::delete_vnic_context(); - delete_vnic_ctx.expect().returning(|_| Ok(())); - - // Explicitly drop the service manager - drop(mgr); - } - struct TestConfig { config_dir: camino_tempfile::Utf8TempDir, } @@ -3160,34 +3218,44 @@ mod test { } #[tokio::test] - #[serial_test::serial] async fn test_ensure_service() { let logctx = omicron_test_utils::dev::test_setup_log("test_ensure_service"); let log = logctx.log.clone(); let test_config = TestConfig::new().await; - let resources = StorageResources::new_for_test(); - let zone_bundler = ZoneBundler::new( - log.clone(), - resources.clone(), - Default::default(), - ); + let storage = StorageResources::new_for_test(); + let u2_mountpoints = storage.all_u2_mountpoints(ZONE_DATASET).await; + assert_eq!(u2_mountpoints.len(), 1); + let u2_mountpoint = &u2_mountpoints[0]; + + let id = Uuid::new_v4(); + let mut handler = CommandSequence::new(); + expect_new_service(&mut handler, &test_config, id, &u2_mountpoint); + let executor = FakeExecutorBuilder::new(log.clone()) + .with_sequence(handler) + .build() + .as_executor(); + + let zone_bundler = + ZoneBundler::new(log.clone(), storage.clone(), Default::default()); let mgr = ServiceManager::new( &log, + &executor, DdmAdminClient::localhost(&log).unwrap(), make_bootstrap_networking_config(), SledMode::Auto, Some(true), SidecarRevision::Physical("rev-test".to_string()), vec![], - resources, + storage, zone_bundler, ); test_config.override_paths(&mgr); let port_manager = PortManager::new( logctx.log.new(o!("component" => "PortManager")), + &executor, Ipv6Addr::new(0xfd00, 0x1de, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01), ); mgr.sled_agent_started( @@ -3199,15 +3267,13 @@ mod test { ) .unwrap(); - let id = Uuid::new_v4(); ensure_new_service(&mgr, id).await; - drop_service_manager(mgr); + drop(mgr); logctx.cleanup_successful(); } #[tokio::test] - #[serial_test::serial] async fn test_ensure_service_which_already_exists() { let logctx = omicron_test_utils::dev::test_setup_log( "test_ensure_service_which_already_exists", @@ -3215,27 +3281,38 @@ mod test { let log = logctx.log.clone(); let test_config = TestConfig::new().await; - let resources = StorageResources::new_for_test(); - let zone_bundler = ZoneBundler::new( - log.clone(), - resources.clone(), - Default::default(), - ); + let storage = StorageResources::new_for_test(); + let u2_mountpoints = storage.all_u2_mountpoints(ZONE_DATASET).await; + assert_eq!(u2_mountpoints.len(), 1); + let u2_mountpoint = &u2_mountpoints[0]; + + let id = Uuid::new_v4(); + let mut handler = CommandSequence::new(); + expect_new_service(&mut handler, &test_config, id, &u2_mountpoint); + let executor = FakeExecutorBuilder::new(log.clone()) + .with_sequence(handler) + .build() + .as_executor(); + + let zone_bundler = + ZoneBundler::new(log.clone(), storage.clone(), Default::default()); let mgr = ServiceManager::new( &log, + &executor, DdmAdminClient::localhost(&log).unwrap(), make_bootstrap_networking_config(), SledMode::Auto, Some(true), SidecarRevision::Physical("rev-test".to_string()), vec![], - resources, + storage, zone_bundler, ); test_config.override_paths(&mgr); let port_manager = PortManager::new( logctx.log.new(o!("component" => "PortManager")), + &executor, Ipv6Addr::new(0xfd00, 0x1de, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01), ); mgr.sled_agent_started( @@ -3247,16 +3324,14 @@ mod test { ) .unwrap(); - let id = Uuid::new_v4(); ensure_new_service(&mgr, id).await; ensure_existing_service(&mgr, id).await; - drop_service_manager(mgr); + drop(mgr); logctx.cleanup_successful(); } #[tokio::test] - #[serial_test::serial] async fn test_services_are_recreated_on_reboot() { let logctx = omicron_test_utils::dev::test_setup_log( "test_services_are_recreated_on_reboot", @@ -3266,29 +3341,40 @@ mod test { let ddmd_client = DdmAdminClient::localhost(&log).unwrap(); let bootstrap_networking = make_bootstrap_networking_config(); + let storage = StorageResources::new_for_test(); + let u2_mountpoints = storage.all_u2_mountpoints(ZONE_DATASET).await; + assert_eq!(u2_mountpoints.len(), 1); + let u2_mountpoint = &u2_mountpoints[0]; + + let id = Uuid::new_v4(); + let mut handler = CommandSequence::new(); + expect_new_service(&mut handler, &test_config, id, &u2_mountpoint); + let executor = FakeExecutorBuilder::new(log.clone()) + .with_sequence(handler) + .build() + .as_executor(); + // First, spin up a ServiceManager, create a new service, and tear it // down. - let resources = StorageResources::new_for_test(); - let zone_bundler = ZoneBundler::new( - log.clone(), - resources.clone(), - Default::default(), - ); + let zone_bundler = + ZoneBundler::new(log.clone(), storage.clone(), Default::default()); let mgr = ServiceManager::new( &log, + &executor, ddmd_client.clone(), bootstrap_networking.clone(), SledMode::Auto, Some(true), SidecarRevision::Physical("rev-test".to_string()), vec![], - resources.clone(), + storage.clone(), zone_bundler.clone(), ); test_config.override_paths(&mgr); let port_manager = PortManager::new( log.new(o!("component" => "PortManager")), + &executor, Ipv6Addr::new(0xfd00, 0x1de, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01), ); mgr.sled_agent_started( @@ -3300,28 +3386,50 @@ mod test { ) .unwrap(); - let id = Uuid::new_v4(); ensure_new_service(&mgr, id).await; - drop_service_manager(mgr); + drop(mgr); // Before we re-create the service manager - notably, using the same // config file! - expect that a service gets initialized. - let _expectations = expect_new_service(); + let mut handler = CommandSequence::new(); + + handler.expect_dynamic(Box::new(|input| -> Output { + assert_eq!(input.program, PFEXEC); + assert_eq!(input.args[0], "/usr/platform/oxide/bin/tmpx"); + // input.args[1] is the current time. + assert_eq!(input.args[2], "/var/adm/utmpx"); + Output::success() + })); + handler.expect_dynamic(Box::new(|input| -> Output { + assert_eq!(input.program, PFEXEC); + assert_eq!(input.args[0], "/usr/platform/oxide/bin/tmpx"); + // input.args[1] is the current time. + assert_eq!(input.args[2], "/var/adm/wtmpx"); + Output::success() + })); + expect_new_service(&mut handler, &test_config, id, &u2_mountpoint); + let executor = FakeExecutorBuilder::new(log.clone()) + .with_sequence(handler) + .build() + .as_executor(); + let mgr = ServiceManager::new( &log, + &executor, ddmd_client, bootstrap_networking, SledMode::Auto, Some(true), SidecarRevision::Physical("rev-test".to_string()), vec![], - resources.clone(), + storage.clone(), zone_bundler.clone(), ); test_config.override_paths(&mgr); let port_manager = PortManager::new( log.new(o!("component" => "PortManager")), + &executor, Ipv6Addr::new(0xfd00, 0x1de, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01), ); mgr.sled_agent_started( @@ -3333,13 +3441,13 @@ mod test { ) .unwrap(); - drop_service_manager(mgr); + mgr.load_services().await.expect("Failed to load services"); + drop(mgr); logctx.cleanup_successful(); } #[tokio::test] - #[serial_test::serial] async fn test_services_do_not_persist_without_config() { let logctx = omicron_test_utils::dev::test_setup_log( "test_services_do_not_persist_without_config", @@ -3349,29 +3457,40 @@ mod test { let ddmd_client = DdmAdminClient::localhost(&log).unwrap(); let bootstrap_networking = make_bootstrap_networking_config(); + let storage = StorageResources::new_for_test(); + let u2_mountpoints = storage.all_u2_mountpoints(ZONE_DATASET).await; + assert_eq!(u2_mountpoints.len(), 1); + let u2_mountpoint = &u2_mountpoints[0]; + + let id = Uuid::new_v4(); + let mut handler = CommandSequence::new(); + expect_new_service(&mut handler, &test_config, id, &u2_mountpoint); + let executor = FakeExecutorBuilder::new(log.clone()) + .with_sequence(handler) + .build() + .as_executor(); + // First, spin up a ServiceManager, create a new service, and tear it // down. - let resources = StorageResources::new_for_test(); - let zone_bundler = ZoneBundler::new( - log.clone(), - resources.clone(), - Default::default(), - ); + let zone_bundler = + ZoneBundler::new(log.clone(), storage.clone(), Default::default()); let mgr = ServiceManager::new( &log, + &executor, ddmd_client.clone(), bootstrap_networking.clone(), SledMode::Auto, Some(true), SidecarRevision::Physical("rev-test".to_string()), vec![], - resources.clone(), + storage.clone(), zone_bundler.clone(), ); test_config.override_paths(&mgr); let port_manager = PortManager::new( log.new(o!("component" => "PortManager")), + &executor, Ipv6Addr::new(0xfd00, 0x1de, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01), ); mgr.sled_agent_started( @@ -3383,9 +3502,8 @@ mod test { ) .unwrap(); - let id = Uuid::new_v4(); ensure_new_service(&mgr, id).await; - drop_service_manager(mgr); + drop(mgr); // Next, delete the ledger. This means the service we just created will // not be remembered on the next initialization. @@ -3397,19 +3515,21 @@ mod test { // Observe that the old service is not re-initialized. let mgr = ServiceManager::new( &log, + &executor, ddmd_client, bootstrap_networking, SledMode::Auto, Some(true), SidecarRevision::Physical("rev-test".to_string()), vec![], - resources.clone(), + storage, zone_bundler.clone(), ); test_config.override_paths(&mgr); let port_manager = PortManager::new( log.new(o!("component" => "PortManager")), + &executor, Ipv6Addr::new(0xfd00, 0x1de, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01), ); mgr.sled_agent_started( @@ -3421,7 +3541,7 @@ mod test { ) .unwrap(); - drop_service_manager(mgr); + drop(mgr); logctx.cleanup_successful(); } diff --git a/sled-agent/src/sled_agent.rs b/sled-agent/src/sled_agent.rs index 7e62f6a8a70..726c69a69f3 100644 --- a/sled-agent/src/sled_agent.rs +++ b/sled-agent/src/sled_agent.rs @@ -25,10 +25,13 @@ use crate::zone_bundle::BundleError; use bootstore::schemes::v0 as bootstore; use camino::Utf8PathBuf; use dropshot::HttpError; +use helios_fusion::BoxedExecutor; +use illumos_utils::dladm::Dladm; use illumos_utils::opte::params::{ DeleteVirtualNetworkInterfaceHost, SetVirtualNetworkInterfaceHost, }; use illumos_utils::opte::PortManager; +use illumos_utils::zone::Zones; use illumos_utils::zone::PROPOLIS_ZONE_PREFIX; use illumos_utils::zone::ZONE_PREFIX; use omicron_common::address::{ @@ -52,11 +55,6 @@ use std::net::{Ipv6Addr, SocketAddrV6}; use std::sync::Arc; use uuid::Uuid; -#[cfg(not(test))] -use illumos_utils::{dladm::Dladm, zone::Zones}; -#[cfg(test)] -use illumos_utils::{dladm::MockDladm as Dladm, zone::MockZones as Zones}; - #[derive(thiserror::Error, Debug)] pub enum Error { #[error("Configuration error: {0}")] @@ -66,7 +64,7 @@ pub enum Error { SwapDevice(#[from] crate::swap_device::SwapDeviceError), #[error("Failed to acquire etherstub: {0}")] - Etherstub(illumos_utils::ExecutionError), + Etherstub(helios_fusion::ExecutionError), #[error("Failed to acquire etherstub VNIC: {0}")] EtherstubVnic(illumos_utils::dladm::CreateVnicError), @@ -75,7 +73,7 @@ pub enum Error { Bootstrap(#[from] crate::bootstrap::BootstrapError), #[error("Failed to remove Omicron address: {0}")] - DeleteAddress(#[from] illumos_utils::ExecutionError), + DeleteAddress(#[from] helios_fusion::ExecutionError), #[error("Failed to operate on underlay device: {0}")] Underlay(#[from] underlay::Error), @@ -194,6 +192,9 @@ struct SledAgentInner { // ID of the Sled id: Uuid, + // Sled Agent's interaction with the host system + executor: BoxedExecutor, + // Subnet of the Sled's underlay. // // The Sled Agent's address can be derived from this value. @@ -248,9 +249,11 @@ pub struct SledAgent { impl SledAgent { /// Initializes a new [`SledAgent`] object. + #[allow(clippy::too_many_arguments)] pub async fn new( config: &Config, log: Logger, + executor: &BoxedExecutor, nexus_client: NexusClientWithResolver, request: StartSledAgentRequest, services: ServiceManager, @@ -278,6 +281,7 @@ impl SledAgent { })?; crate::swap_device::ensure_swap_device( &parent_log, + executor, &boot_disk.1, sz, )?; @@ -299,15 +303,17 @@ impl SledAgent { // etherstub and etherstub VNIC exist on startup - could it pass them // through to us? let etherstub = Dladm::ensure_etherstub( + executor, illumos_utils::dladm::UNDERLAY_ETHERSTUB_NAME, ) .map_err(|e| Error::Etherstub(e))?; - let etherstub_vnic = Dladm::ensure_etherstub_vnic(ðerstub) + let etherstub_vnic = Dladm::ensure_etherstub_vnic(executor, ðerstub) .map_err(|e| Error::EtherstubVnic(e))?; // Ensure the global zone has a functioning IPv6 address. let sled_address = request.sled_address(); Zones::ensure_has_global_zone_v6_address( + executor, etherstub_vnic.clone(), *sled_address.ip(), "sled6", @@ -315,12 +321,13 @@ impl SledAgent { .map_err(|err| Error::SledSubnet { err })?; // Initialize the xde kernel driver with the underlay devices. - let underlay_nics = underlay::find_nics(&config.data_links)?; + let underlay_nics = underlay::find_nics(executor, &config.data_links)?; illumos_utils::opte::initialize_xde_driver(&log, &underlay_nics)?; // Create the PortManager to manage all the OPTE ports on the sled. let port_manager = PortManager::new( parent_log.new(o!("component" => "PortManager")), + executor, *sled_address.ip(), ); @@ -341,6 +348,7 @@ impl SledAgent { let instances = InstanceManager::new( parent_log.clone(), + executor, nexus_client.clone(), etherstub.clone(), port_manager.clone(), @@ -425,6 +433,7 @@ impl SledAgent { let sled_agent = SledAgent { inner: Arc::new(SledAgentInner { id: request.id, + executor: executor.clone(), subnet: request.subnet, storage, instances, @@ -643,7 +652,7 @@ impl SledAgent { /// List the zones that the sled agent is currently managing. pub async fn zones_list(&self) -> Result, Error> { - Zones::get() + Zones::get(&self.inner.executor) .await .map(|zones| { let mut zn: Vec<_> = zones diff --git a/sled-agent/src/storage/dump_setup.rs b/sled-agent/src/storage/dump_setup.rs index 9b5edc0a7e5..278f3931251 100644 --- a/sled-agent/src/storage/dump_setup.rs +++ b/sled-agent/src/storage/dump_setup.rs @@ -1,6 +1,7 @@ use crate::storage_manager::DiskWrapper; use camino::Utf8PathBuf; use derive_more::{AsRef, Deref, From}; +use helios_fusion::{BoxedExecutor, ExecutionError}; use illumos_utils::dumpadm::DumpAdmError; use illumos_utils::zone::{AdmError, Zones}; use illumos_utils::zpool::{ZpoolHealth, ZpoolName}; @@ -15,15 +16,18 @@ use std::time::{Duration, SystemTime, SystemTimeError, UNIX_EPOCH}; use tokio::sync::MutexGuard; pub struct DumpSetup { + executor: BoxedExecutor, worker: Arc>, _poller: std::thread::JoinHandle<()>, log: Logger, } impl DumpSetup { - pub fn new(log: &Logger) -> Self { + pub fn new(log: &Logger, executor: BoxedExecutor) -> Self { + let exec = executor.clone(); let worker = Arc::new(std::sync::Mutex::new(DumpSetupWorker::new( log.new(o!("component" => "DumpSetup-worker")), + exec, ))); let worker_weak = Arc::downgrade(&worker); let log_poll = log.new(o!("component" => "DumpSetup-archival")); @@ -31,7 +35,7 @@ impl DumpSetup { Self::poll_file_archival(worker_weak, log_poll) }); let log = log.new(o!("component" => "DumpSetup")); - Self { worker, _poller, log } + Self { executor, worker, _poller, log } } } @@ -58,8 +62,11 @@ struct DebugZpool(ZpoolName); trait GetMountpoint: std::ops::Deref { type NewType: From; const MOUNTPOINT: &'static str; - fn mountpoint(&self) -> Result, ZfsGetError> { - if zfs_get_prop(self.to_string(), "mounted")? == "yes" { + fn mountpoint( + &self, + executor: &BoxedExecutor, + ) -> Result, ZfsGetError> { + if zfs_get_prop(executor, self.to_string(), "mounted")? == "yes" { Ok(Some(Self::NewType::from( self.dataset_mountpoint(Self::MOUNTPOINT), ))) @@ -92,6 +99,7 @@ struct DumpSetupWorker { savecored_slices: HashSet, log: Logger, + executor: BoxedExecutor, } const ARCHIVAL_INTERVAL: Duration = Duration::from_secs(300); @@ -119,6 +127,7 @@ impl DumpSetup { } let name = disk.zpool_name(); if let Ok(info) = illumos_utils::zpool::Zpool::get_info( + &self.executor, &name.to_string(), ) { if info.health() == ZpoolHealth::Online { @@ -131,6 +140,7 @@ impl DumpSetup { DiskVariant::U2 => { let name = disk.zpool_name(); if let Ok(info) = illumos_utils::zpool::Zpool::get_info( + &self.executor, &name.to_string(), ) { if info.health() == ZpoolHealth::Online { @@ -201,6 +211,8 @@ impl DumpSetup { #[derive(Debug, thiserror::Error)] enum ZfsGetError { + #[error("Failed to execute command: {0}")] + Execution(#[from] ExecutionError), #[error("Error executing 'zfs get' command: {0}")] IoError(#[from] std::io::Error), #[error("Output of 'zfs get' was not only not an integer string, it wasn't even UTF-8: {0}")] @@ -213,13 +225,17 @@ const ZFS_PROP_USED: &str = "used"; const ZFS_PROP_AVAILABLE: &str = "available"; fn zfs_get_integer( + executor: &BoxedExecutor, mountpoint_or_name: impl AsRef, property: &str, ) -> Result { - zfs_get_prop(mountpoint_or_name, property)?.parse().map_err(Into::into) + zfs_get_prop(executor, mountpoint_or_name, property)? + .parse() + .map_err(Into::into) } fn zfs_get_prop( + executor: &BoxedExecutor, mountpoint_or_name: impl AsRef + Sized, property: &str, ) -> Result { @@ -228,7 +244,7 @@ fn zfs_get_prop( cmd.arg("get").arg("-Hpo").arg("value"); cmd.arg(property); cmd.arg(mountpoint); - let output = cmd.output()?; + let output = executor.execute(&mut cmd)?; Ok(String::from_utf8(output.stdout)?.trim().to_string()) } @@ -236,18 +252,19 @@ const DATASET_USAGE_PERCENT_CHOICE: u64 = 70; const DATASET_USAGE_PERCENT_CLEANUP: u64 = 80; fn below_thresh( + executor: &BoxedExecutor, mountpoint: &Utf8PathBuf, percent: u64, ) -> Result<(bool, u64), ZfsGetError> { - let used = zfs_get_integer(mountpoint, ZFS_PROP_USED)?; - let available = zfs_get_integer(mountpoint, ZFS_PROP_AVAILABLE)?; + let used = zfs_get_integer(executor, mountpoint, ZFS_PROP_USED)?; + let available = zfs_get_integer(executor, mountpoint, ZFS_PROP_AVAILABLE)?; let capacity = used + available; let below = (used * 100) / capacity < percent; Ok((below, used)) } impl DumpSetupWorker { - fn new(log: Logger) -> Self { + fn new(log: Logger, executor: BoxedExecutor) -> Self { Self { core_dataset_names: vec![], debug_dataset_names: vec![], @@ -259,6 +276,7 @@ impl DumpSetupWorker { known_core_dirs: vec![], savecored_slices: Default::default(), log, + executor, } } @@ -282,13 +300,13 @@ impl DumpSetupWorker { self.known_debug_dirs = self .debug_dataset_names .iter() - .flat_map(|ds| ds.mountpoint()) + .flat_map(|ds| ds.mountpoint(&self.executor)) .flatten() .collect(); self.known_core_dirs = self .core_dataset_names .iter() - .flat_map(|ds| ds.mountpoint()) + .flat_map(|ds| ds.mountpoint(&self.executor)) .flatten() .collect(); } @@ -302,7 +320,7 @@ impl DumpSetupWorker { // below a certain usage threshold. self.known_debug_dirs.sort_by_cached_key( |mountpoint: &DebugDataset| { - match below_thresh(mountpoint.as_ref(), DATASET_USAGE_PERCENT_CHOICE) { + match below_thresh(&self.executor, mountpoint.as_ref(), DATASET_USAGE_PERCENT_CHOICE) { Ok((below, used)) => { let priority = if below { 0 } else { 1 }; (priority, used, mountpoint.clone()) @@ -317,7 +335,9 @@ impl DumpSetupWorker { ); self.known_core_dirs.sort_by_cached_key(|mnt| { // these get archived periodically anyway, pick one with room - let available = zfs_get_integer(&**mnt, "available").unwrap_or(0); + let available = + zfs_get_integer(&self.executor, &**mnt, "available") + .unwrap_or(0); (u64::MAX - available, mnt.clone()) }); @@ -326,11 +346,16 @@ impl DumpSetupWorker { warn!(self.log, "Previously-chosen debug/dump dir {x:?} no longer exists in our view of reality"); self.chosen_debug_dir = None; } else { - match below_thresh(x.as_ref(), DATASET_USAGE_PERCENT_CLEANUP) { + match below_thresh( + &self.executor, + x.as_ref(), + DATASET_USAGE_PERCENT_CLEANUP, + ) { Ok((true, _)) => {} Ok((false, _)) => { if self.known_debug_dirs.iter().any(|x| { below_thresh( + &self.executor, x.as_ref(), DATASET_USAGE_PERCENT_CHOICE, ) @@ -427,7 +452,9 @@ impl DumpSetupWorker { // Have dumpadm write the config for crash dumps to be // on this slice, at least, until a U.2 comes along. match illumos_utils::dumpadm::dumpadm( - dump_slice, None, + &self.executor, + dump_slice, + None, ) { Ok(_) => { info!(self.log, "Using dump device {dump_slice:?} with no savecore destination (no U.2 debug zvol yet)"); @@ -486,9 +513,11 @@ impl DumpSetupWorker { // in the event of a kernel crash if changed_slice { if let Some(dump_slice) = &self.chosen_dump_slice { - if let Err(err) = - illumos_utils::dumpadm::dumpadm(dump_slice, None) - { + if let Err(err) = illumos_utils::dumpadm::dumpadm( + &self.executor, + dump_slice, + None, + ) { error!(self.log, "Could not restore dump slice to {dump_slice:?}: {err:?}"); } } @@ -574,7 +603,7 @@ impl DumpSetupWorker { // its 'sync' and 'async' features simultaneously :( let rt = tokio::runtime::Runtime::new().map_err(ArchiveLogsError::Tokio)?; - let oxz_zones = rt.block_on(Zones::get())?; + let oxz_zones = rt.block_on(Zones::get(&self.executor))?; self.archive_logs_inner( debug_dir, PathBuf::from("/var/svc/log"), @@ -662,8 +691,11 @@ impl DumpSetupWorker { let savecore_dir = self.chosen_debug_dir.clone().unwrap().0; - match illumos_utils::dumpadm::dumpadm(&dump_slice, Some(&savecore_dir)) - { + match illumos_utils::dumpadm::dumpadm( + &self.executor, + &dump_slice, + Some(&savecore_dir), + ) { Ok(saved) => { self.savecored_slices.insert(dump_slice.clone()); Ok(saved) @@ -675,7 +707,7 @@ impl DumpSetupWorker { fn cleanup(&self) -> Result<(), CleanupError> { let mut dir_info = Vec::new(); for dir in &self.known_debug_dirs { - match Self::scope_dir_for_cleanup(dir) { + match Self::scope_dir_for_cleanup(&self.executor, dir) { Ok(info) => { dir_info.push((info, dir)); } @@ -713,10 +745,12 @@ impl DumpSetupWorker { } fn scope_dir_for_cleanup( + executor: &BoxedExecutor, debug_dir: &DebugDataset, ) -> Result { - let used = zfs_get_integer(&**debug_dir, ZFS_PROP_USED)?; - let available = zfs_get_integer(&**debug_dir, ZFS_PROP_AVAILABLE)?; + let used = zfs_get_integer(executor, &**debug_dir, ZFS_PROP_USED)?; + let available = + zfs_get_integer(executor, &**debug_dir, ZFS_PROP_AVAILABLE)?; let capacity = used + available; let target_used = capacity * DATASET_USAGE_PERCENT_CHOICE / 100; diff --git a/sled-agent/src/storage_manager.rs b/sled-agent/src/storage_manager.rs index bd713713963..869333b8549 100644 --- a/sled-agent/src/storage_manager.rs +++ b/sled-agent/src/storage_manager.rs @@ -13,8 +13,10 @@ use derive_more::From; use futures::stream::FuturesOrdered; use futures::FutureExt; use futures::StreamExt; -use illumos_utils::zpool::{ZpoolKind, ZpoolName}; -use illumos_utils::{zfs::Mountpoint, zpool::ZpoolInfo}; +use helios_fusion::BoxedExecutor; +use illumos_utils::dumpadm::DumpHdrError; +use illumos_utils::zfs::{Mountpoint, Zfs}; +use illumos_utils::zpool::{Zpool, ZpoolInfo, ZpoolKind, ZpoolName}; use key_manager::StorageKeyRequester; use nexus_client::types::PhysicalDiskDeleteRequest; use nexus_client::types::PhysicalDiskKind; @@ -38,12 +40,6 @@ use tokio::task::JoinHandle; use tokio::time::{interval, MissedTickBehavior}; use uuid::Uuid; -use illumos_utils::dumpadm::DumpHdrError; -#[cfg(test)] -use illumos_utils::{zfs::MockZfs as Zfs, zpool::MockZpool as Zpool}; -#[cfg(not(test))] -use illumos_utils::{zfs::Zfs, zpool::Zpool}; - // A key manager can only become ready once. This occurs during RSS or cold // boot when the bootstore has detected it has a key share. static KEY_MANAGER_READY: OnceLock<()> = OnceLock::new(); @@ -153,8 +149,12 @@ impl Pool { /// Queries for an existing Zpool by name. /// /// Returns Ok if the pool exists. - fn new(name: ZpoolName, parent: DiskIdentity) -> Result { - let info = Zpool::get_info(&name.to_string())?; + fn new( + executor: &BoxedExecutor, + name: ZpoolName, + parent: DiskIdentity, + ) -> Result { + let info = Zpool::get_info(executor, &name.to_string())?; Ok(Pool { name, info, parent }) } @@ -361,6 +361,7 @@ pub struct UnderlayAccess { // A worker that starts zones for pools as they are received. struct StorageWorker { log: Logger, + executor: BoxedExecutor, nexus_notifications: FuturesOrdered, rx: mpsc::Receiver, underlay: Arc>>, @@ -411,6 +412,7 @@ impl StorageWorker { let encryption_details = None; let size_details = None; Zfs::ensure_filesystem( + &self.executor, &dataset_name.full(), Mountpoint::Path(Utf8PathBuf::from("/data")), zoned, @@ -419,7 +421,9 @@ impl StorageWorker { size_details, )?; // Ensure the dataset has a usable UUID. - if let Ok(id_str) = Zfs::get_oxide_value(&fs_name, "uuid") { + if let Ok(id_str) = + Zfs::get_oxide_value(&self.executor, &fs_name, "uuid") + { if let Ok(id) = id_str.parse::() { if id != dataset_id { return Err(Error::UuidMismatch { @@ -431,7 +435,12 @@ impl StorageWorker { return Ok(()); } } - Zfs::set_oxide_value(&fs_name, "uuid", &dataset_id.to_string())?; + Zfs::set_oxide_value( + &self.executor, + &fs_name, + "uuid", + &dataset_id.to_string(), + )?; Ok(()) } @@ -638,6 +647,7 @@ impl StorageWorker { ) -> Result { match sled_hardware::Disk::new( &self.log, + &self.executor, unparsed_disk.clone(), Some(&self.key_requester), ) @@ -684,6 +694,7 @@ impl StorageWorker { }; match sled_hardware::Disk::ensure_zpool_ready( &self.log, + &self.executor, &zpool_name, &synthetic_id, Some(&self.key_requester), @@ -975,7 +986,7 @@ impl StorageWorker { pool_name: &ZpoolName, ) -> Result<(), Error> { let mut pools = resources.pools.lock().await; - let zpool = Pool::new(pool_name.clone(), parent)?; + let zpool = Pool::new(&self.executor, pool_name.clone(), parent)?; let pool = match pools.entry(pool_name.id()) { hash_map::Entry::Occupied(mut entry) => { @@ -1237,7 +1248,11 @@ pub struct StorageManager { impl StorageManager { /// Creates a new [`StorageManager`] which should manage local storage. - pub async fn new(log: &Logger, key_requester: StorageKeyRequester) -> Self { + pub async fn new( + log: &Logger, + executor: &BoxedExecutor, + key_requester: StorageKeyRequester, + ) -> Self { let log = log.new(o!("component" => "StorageManager")); let resources = StorageResources { disks: Arc::new(Mutex::new(HashMap::new())), @@ -1245,6 +1260,7 @@ impl StorageManager { }; let (tx, rx) = mpsc::channel(30); + let executor = executor.clone(); let zb_log = log.new(o!("component" => "ZoneBundler")); let zone_bundler = ZoneBundler::new(zb_log, resources.clone(), Default::default()); @@ -1255,9 +1271,11 @@ impl StorageManager { resources: resources.clone(), tx, task: tokio::task::spawn(async move { - let dump_setup = Arc::new(DumpSetup::new(&log)); + let dump_setup = + Arc::new(DumpSetup::new(&log, executor.clone())); let mut worker = StorageWorker { log, + executor, nexus_notifications: FuturesOrdered::new(), rx, underlay: Arc::new(Mutex::new(None)), diff --git a/sled-agent/src/swap_device.rs b/sled-agent/src/swap_device.rs index 5a8f40adbd1..ac56422d404 100644 --- a/sled-agent/src/swap_device.rs +++ b/sled-agent/src/swap_device.rs @@ -4,6 +4,7 @@ //! Operations for creating a system swap device. +use helios_fusion::{BoxedExecutor, ExecutionError}; use std::io::Read; use zeroize::Zeroize; @@ -13,7 +14,7 @@ pub enum SwapDeviceError { BootDiskNotFound, #[error("Error running ZFS command: {0}")] - Zfs(illumos_utils::ExecutionError), + Zfs(ExecutionError), #[error("Error listing swap devices: {0}")] ListDevices(String), @@ -59,6 +60,7 @@ pub enum SwapDeviceError { /// configuration. pub(crate) fn ensure_swap_device( log: &slog::Logger, + executor: &BoxedExecutor, boot_zpool_name: &illumos_utils::zpool::ZpoolName, size_gb: u32, ) -> Result<(), SwapDeviceError> { @@ -85,13 +87,13 @@ pub(crate) fn ensure_swap_device( } let swap_zvol = format!("{}/{}", boot_zpool_name, "swap"); - if zvol_exists(&swap_zvol)? { + if zvol_exists(executor, &swap_zvol)? { info!(log, "swap zvol \"{}\" aleady exists; destroying", swap_zvol); - zvol_destroy(&swap_zvol)?; + zvol_destroy(executor, &swap_zvol)?; info!(log, "swap zvol \"{}\" destroyed", swap_zvol); } - create_encrypted_swap_zvol(log, &swap_zvol, size_gb)?; + create_encrypted_swap_zvol(log, executor, &swap_zvol, size_gb)?; // The process of paging out uses block I/O, so use the "dsk" version of // the zvol path (as opposed to "rdsk", which is for character/raw access.) @@ -105,12 +107,14 @@ pub(crate) fn ensure_swap_device( } /// Check whether the given zvol exists. -fn zvol_exists(name: &str) -> Result { +fn zvol_exists( + executor: &BoxedExecutor, + name: &str, +) -> Result { let mut command = std::process::Command::new(illumos_utils::zfs::ZFS); let cmd = command.args(&["list", "-Hpo", "name,type"]); - let output = - illumos_utils::execute(cmd).map_err(|e| SwapDeviceError::Zfs(e))?; + let output = executor.execute(cmd).map_err(|e| SwapDeviceError::Zfs(e))?; let stdout = String::from_utf8_lossy(&output.stdout); for line in stdout.lines() { @@ -136,10 +140,13 @@ fn zvol_exists(name: &str) -> Result { } /// Destroys a zvol at the given path. -fn zvol_destroy(name: &str) -> Result<(), SwapDeviceError> { +fn zvol_destroy( + executor: &BoxedExecutor, + name: &str, +) -> Result<(), SwapDeviceError> { let mut command = std::process::Command::new(illumos_utils::zfs::ZFS); let cmd = command.args(&["destroy", name]); - illumos_utils::execute(cmd).map_err(|e| SwapDeviceError::Zfs(e))?; + executor.execute(cmd).map_err(|e| SwapDeviceError::Zfs(e))?; Ok(()) } @@ -150,6 +157,7 @@ fn zvol_destroy(name: &str) -> Result<(), SwapDeviceError> { /// and zeroize the buffer after the zvol is created. fn create_encrypted_swap_zvol( log: &slog::Logger, + executor: &BoxedExecutor, name: &str, size_gb: u32, ) -> Result<(), SwapDeviceError> { @@ -171,7 +179,7 @@ fn create_encrypted_swap_zvol( })?; let mut command = std::process::Command::new(illumos_utils::zfs::ZFS); #[rustfmt::skip] - let cmd = command + let mut cmd = command // create sparse volume of a given size with no reservation, exported as // a block device at: /dev/zvol/{dsk,rdsk}/ .arg("create") @@ -218,11 +226,13 @@ fn create_encrypted_swap_zvol( })?; // Spawn the process, writing the key in through stdin. - let mut child = cmd.spawn().map_err(|e| SwapDeviceError::Misc { - msg: format!("failed to spawn `zfs create` for zvol \"{}\"", name), - error: e.to_string(), - })?; - let mut stdin = child.stdin.take().unwrap(); + let mut spawn = + executor.spawn(&mut cmd).map_err(|e| SwapDeviceError::Misc { + msg: format!("failed to spawn `zfs create` for zvol \"{}\"", name), + error: e.to_string(), + })?; + + let mut stdin = spawn.take_stdin().take().unwrap(); let child_log = log.clone(); let hdl = std::thread::spawn(move || { use std::io::Write; @@ -236,15 +246,14 @@ fn create_encrypted_swap_zvol( }); // Wait for the process, and the thread writing the bytes to it, to complete. - let output = - child.wait_with_output().map_err(|e| SwapDeviceError::Misc { - msg: "failed to read stdout".to_string(), - error: e.to_string(), - })?; + let output = spawn.wait().map_err(|e| SwapDeviceError::Misc { + msg: "failed to read stdout".to_string(), + error: e.to_string(), + })?; hdl.join().unwrap(); if !output.status.success() { - return Err(SwapDeviceError::Zfs(illumos_utils::output_to_exec_error( + return Err(SwapDeviceError::Zfs(ExecutionError::from_output( &command, &output, ))); } diff --git a/sled-hardware/Cargo.toml b/sled-hardware/Cargo.toml index 880f93441c9..9f46ed808f8 100644 --- a/sled-hardware/Cargo.toml +++ b/sled-hardware/Cargo.toml @@ -10,6 +10,7 @@ anyhow.workspace = true camino.workspace = true cfg-if.workspace = true futures.workspace = true +helios-fusion.workspace = true illumos-utils.workspace = true key-manager.workspace = true libc.workspace = true @@ -31,6 +32,5 @@ illumos-devinfo = { git = "https://github.com/oxidecomputer/illumos-devinfo", br libefi-illumos = { git = "https://github.com/oxidecomputer/libefi-illumos", branch = "master" } [dev-dependencies] -illumos-utils = { workspace = true, features = ["testing"] } +helios-tokamak.workspace = true omicron-test-utils.workspace = true -serial_test.workspace = true diff --git a/sled-hardware/src/cleanup.rs b/sled-hardware/src/cleanup.rs index 1a7f8be2f70..401d7c69726 100644 --- a/sled-hardware/src/cleanup.rs +++ b/sled-hardware/src/cleanup.rs @@ -6,6 +6,7 @@ use anyhow::Error; use futures::stream::{self, StreamExt, TryStreamExt}; +use helios_fusion::{BoxedExecutor, ExecutionError, PFEXEC}; use illumos_utils::dladm::Dladm; use illumos_utils::dladm::BOOTSTRAP_ETHERSTUB_NAME; use illumos_utils::dladm::BOOTSTRAP_ETHERSTUB_VNIC_NAME; @@ -14,30 +15,35 @@ use illumos_utils::dladm::UNDERLAY_ETHERSTUB_VNIC_NAME; use illumos_utils::link::LinkKind; use illumos_utils::opte; use illumos_utils::zone::IPADM; -use illumos_utils::ExecutionError; -use illumos_utils::{execute, PFEXEC}; use slog::warn; use slog::Logger; use std::process::Command; -pub fn delete_underlay_addresses(log: &Logger) -> Result<(), Error> { +pub fn delete_underlay_addresses( + log: &Logger, + executor: &BoxedExecutor, +) -> Result<(), Error> { let underlay_prefix = format!("{}/", UNDERLAY_ETHERSTUB_VNIC_NAME); - delete_addresses_matching_prefixes(log, &[underlay_prefix]) + delete_addresses_matching_prefixes(log, executor, &[underlay_prefix]) } -pub fn delete_bootstrap_addresses(log: &Logger) -> Result<(), Error> { +pub fn delete_bootstrap_addresses( + log: &Logger, + executor: &BoxedExecutor, +) -> Result<(), Error> { let bootstrap_prefix = format!("{}/", BOOTSTRAP_ETHERSTUB_VNIC_NAME); - delete_addresses_matching_prefixes(log, &[bootstrap_prefix]) + delete_addresses_matching_prefixes(log, executor, &[bootstrap_prefix]) } fn delete_addresses_matching_prefixes( log: &Logger, + executor: &BoxedExecutor, prefixes: &[String], ) -> Result<(), Error> { use std::io::BufRead; let mut cmd = Command::new(PFEXEC); let cmd = cmd.args(&[IPADM, "show-addr", "-p", "-o", "ADDROBJ"]); - let output = execute(cmd)?; + let output = executor.execute(cmd)?; // `ipadm show-addr` can return multiple addresses with the same name, but // multiple values. Collecting to a set ensures that only a single name is @@ -57,34 +63,41 @@ fn delete_addresses_matching_prefixes( ); let mut cmd = Command::new(PFEXEC); let cmd = cmd.args(&[IPADM, "delete-addr", addrobj.as_str()]); - execute(cmd)?; + executor.execute(cmd)?; } } Ok(()) } /// Delete the etherstub and underlay VNIC used for interzone communication -pub fn delete_etherstub(log: &Logger) -> Result<(), ExecutionError> { +pub fn delete_etherstub( + log: &Logger, + executor: &BoxedExecutor, +) -> Result<(), ExecutionError> { warn!(log, "Deleting Omicron underlay VNIC"; "vnic_name" => UNDERLAY_ETHERSTUB_VNIC_NAME); - Dladm::delete_etherstub_vnic(UNDERLAY_ETHERSTUB_VNIC_NAME)?; + Dladm::delete_etherstub_vnic(executor, UNDERLAY_ETHERSTUB_VNIC_NAME)?; warn!(log, "Deleting Omicron underlay etherstub"; "stub_name" => UNDERLAY_ETHERSTUB_NAME); - Dladm::delete_etherstub(UNDERLAY_ETHERSTUB_NAME)?; + Dladm::delete_etherstub(executor, UNDERLAY_ETHERSTUB_NAME)?; warn!(log, "Deleting Omicron bootstrap VNIC"; "vnic_name" => BOOTSTRAP_ETHERSTUB_VNIC_NAME); - Dladm::delete_etherstub_vnic(BOOTSTRAP_ETHERSTUB_VNIC_NAME)?; + Dladm::delete_etherstub_vnic(executor, BOOTSTRAP_ETHERSTUB_VNIC_NAME)?; warn!(log, "Deleting Omicron bootstrap etherstub"; "stub_name" => BOOTSTRAP_ETHERSTUB_NAME); - Dladm::delete_etherstub(BOOTSTRAP_ETHERSTUB_NAME)?; + Dladm::delete_etherstub(executor, BOOTSTRAP_ETHERSTUB_NAME)?; Ok(()) } /// Delete all VNICs that can be managed by the control plane. /// /// These are currently those that match the prefix `ox` or `vopte`. -pub async fn delete_omicron_vnics(log: &Logger) -> Result<(), Error> { - let vnics = Dladm::get_vnics()?; +pub async fn delete_omicron_vnics( + log: &Logger, + executor: &BoxedExecutor, +) -> Result<(), Error> { + let vnics = Dladm::get_vnics(executor)?; stream::iter(vnics) .zip(stream::iter(std::iter::repeat(log.clone()))) .map(Ok::<_, illumos_utils::dladm::DeleteVnicError>) .try_for_each_concurrent(None, |(vnic, log)| async { + let executor = executor.clone(); tokio::task::spawn_blocking(move || { warn!( log, @@ -92,7 +105,7 @@ pub async fn delete_omicron_vnics(log: &Logger) -> Result<(), Error> { "vnic_name" => &vnic, "vnic_kind" => ?LinkKind::from_name(&vnic).unwrap(), ); - Dladm::delete_vnic(&vnic) + Dladm::delete_vnic(&executor, &vnic) }) .await .unwrap() @@ -101,11 +114,14 @@ pub async fn delete_omicron_vnics(log: &Logger) -> Result<(), Error> { Ok(()) } -pub async fn cleanup_networking_resources(log: &Logger) -> Result<(), Error> { - delete_underlay_addresses(log)?; - delete_bootstrap_addresses(log)?; - delete_omicron_vnics(log).await?; - delete_etherstub(log)?; +pub async fn cleanup_networking_resources( + log: &Logger, + executor: &BoxedExecutor, +) -> Result<(), Error> { + delete_underlay_addresses(log, executor)?; + delete_bootstrap_addresses(log, executor)?; + delete_omicron_vnics(log, executor).await?; + delete_etherstub(log, executor)?; opte::delete_all_xde_devices(log)?; Ok(()) diff --git a/sled-hardware/src/disk.rs b/sled-hardware/src/disk.rs index aec99ae3f84..b10dfc97e08 100644 --- a/sled-hardware/src/disk.rs +++ b/sled-hardware/src/disk.rs @@ -3,6 +3,7 @@ // file, You can obtain one at https://mozilla.org/MPL/2.0/. use camino::{Utf8Path, Utf8PathBuf}; +use helios_fusion::BoxedExecutor; use illumos_utils::fstyp::Fstyp; use illumos_utils::zfs; use illumos_utils::zfs::DestroyDatasetErrorVariant; @@ -315,6 +316,7 @@ impl Disk { /// `None` is for the M.2s touched by the Installinator. pub async fn new( log: &Logger, + executor: &BoxedExecutor, unparsed_disk: UnparsedDisk, key_requester: Option<&StorageKeyRequester>, ) -> Result { @@ -322,7 +324,8 @@ impl Disk { let variant = unparsed_disk.variant; // Ensure the GPT has the right format. This does not necessarily // mean that the partitions are populated with the data we need. - let partitions = ensure_partition_layout(&log, &paths, variant)?; + let partitions = + ensure_partition_layout(&log, &executor, &paths, variant)?; // Find the path to the zpool which exists on this disk. // @@ -334,9 +337,11 @@ impl Disk { false, )?; - let zpool_name = Self::ensure_zpool_exists(log, variant, &zpool_path)?; + let zpool_name = + Self::ensure_zpool_exists(log, executor, variant, &zpool_path)?; Self::ensure_zpool_ready( log, + executor, &zpool_name, &unparsed_disk.identity, key_requester, @@ -356,14 +361,16 @@ impl Disk { pub async fn ensure_zpool_ready( log: &Logger, + executor: &BoxedExecutor, zpool_name: &ZpoolName, disk_identity: &DiskIdentity, key_requester: Option<&StorageKeyRequester>, ) -> Result<(), DiskError> { - Self::ensure_zpool_imported(log, &zpool_name)?; - Self::ensure_zpool_failmode_is_continue(log, &zpool_name)?; + Self::ensure_zpool_imported(log, executor, &zpool_name)?; + Self::ensure_zpool_failmode_is_continue(log, executor, &zpool_name)?; Self::ensure_zpool_has_datasets( log, + executor, &zpool_name, disk_identity, key_requester, @@ -374,10 +381,11 @@ impl Disk { fn ensure_zpool_exists( log: &Logger, + executor: &BoxedExecutor, variant: DiskVariant, zpool_path: &Utf8Path, ) -> Result { - let zpool_name = match Fstyp::get_zpool(&zpool_path) { + let zpool_name = match Fstyp::get_zpool(executor, &zpool_path) { Ok(zpool_name) => zpool_name, Err(_) => { // What happened here? @@ -401,11 +409,11 @@ impl Disk { DiskVariant::M2 => ZpoolName::new_internal(Uuid::new_v4()), DiskVariant::U2 => ZpoolName::new_external(Uuid::new_v4()), }; - Zpool::create(zpool_name.clone(), &zpool_path)?; + Zpool::create(executor, zpool_name.clone(), &zpool_path)?; zpool_name } }; - Zpool::import(zpool_name.clone()).map_err(|e| { + Zpool::import(executor, zpool_name.clone()).map_err(|e| { warn!(log, "Failed to import zpool {zpool_name}: {e}"); DiskError::ZpoolImport(e) })?; @@ -415,9 +423,10 @@ impl Disk { fn ensure_zpool_imported( log: &Logger, + executor: &BoxedExecutor, zpool_name: &ZpoolName, ) -> Result<(), DiskError> { - Zpool::import(zpool_name.clone()).map_err(|e| { + Zpool::import(executor, zpool_name.clone()).map_err(|e| { warn!(log, "Failed to import zpool {zpool_name}: {e}"); DiskError::ZpoolImport(e) })?; @@ -426,6 +435,7 @@ impl Disk { fn ensure_zpool_failmode_is_continue( log: &Logger, + executor: &BoxedExecutor, zpool_name: &ZpoolName, ) -> Result<(), DiskError> { // Ensure failmode is set to `continue`. See @@ -435,7 +445,7 @@ impl Disk { // actively harmful to try to wait for it to come back; we'll be waiting // forever and get stuck. We'd rather get the errors so we can deal with // them ourselves. - Zpool::set_failmode_continue(&zpool_name).map_err(|e| { + Zpool::set_failmode_continue(executor, &zpool_name).map_err(|e| { warn!( log, "Failed to set failmode=continue on zpool {zpool_name}: {e}" @@ -449,6 +459,7 @@ impl Disk { // contain. async fn ensure_zpool_has_datasets( log: &Logger, + executor: &BoxedExecutor, zpool_name: &ZpoolName, disk_identity: &DiskIdentity, key_requester: Option<&StorageKeyRequester>, @@ -472,31 +483,32 @@ impl Disk { let mountpoint = zpool_name.dataset_mountpoint(dataset); let keypath: Keypath = disk_identity.into(); - let epoch = - if let Ok(epoch_str) = Zfs::get_oxide_value(dataset, "epoch") { - if let Ok(epoch) = epoch_str.parse::() { - epoch - } else { - return Err(DiskError::CannotParseEpochProperty( - dataset.to_string(), - )); - } + let epoch = if let Ok(epoch_str) = + Zfs::get_oxide_value(executor, dataset, "epoch") + { + if let Ok(epoch) = epoch_str.parse::() { + epoch } else { - // We got an error trying to call `Zfs::get_oxide_value` - // which indicates that the dataset doesn't exist or there - // was a problem running the command. - // - // Note that `Zfs::get_oxide_value` will succeed even if - // the epoch is missing. `epoch_str` will show up as a dash - // (`-`) and will not parse into a `u64`. So we don't have - // to worry about that case here as it is handled above. - // - // If the error indicated that the command failed for some - // other reason, but the dataset actually existed, we will - // try to create the dataset below and that will fail. So - // there is no harm in just loading the latest secret here. - key_requester.load_latest_secret().await? - }; + return Err(DiskError::CannotParseEpochProperty( + dataset.to_string(), + )); + } + } else { + // We got an error trying to call `Zfs::get_oxide_value` + // which indicates that the dataset doesn't exist or there + // was a problem running the command. + // + // Note that `Zfs::get_oxide_value` will succeed even if + // the epoch is missing. `epoch_str` will show up as a dash + // (`-`) and will not parse into a `u64`. So we don't have + // to worry about that case here as it is handled above. + // + // If the error indicated that the command failed for some + // other reason, but the dataset actually existed, we will + // try to create the dataset below and that will fail. So + // there is no harm in just loading the latest secret here. + key_requester.load_latest_secret().await? + }; let key = key_requester.get_key(epoch, disk_identity.clone()).await?; @@ -518,6 +530,7 @@ impl Disk { epoch ); let result = Zfs::ensure_filesystem( + executor, &format!("{}/{}", zpool_name, dataset), Mountpoint::Path(mountpoint), zoned, @@ -549,7 +562,7 @@ impl Disk { }); if dataset.wipe { - match Zfs::get_oxide_value(name, "agent") { + match Zfs::get_oxide_value(executor, name, "agent") { Ok(v) if &v == agent_local_value => { info!( log, @@ -561,17 +574,19 @@ impl Disk { log, "Automatically destroying dataset: {}", name ); - Zfs::destroy_dataset(name).or_else(|err| { - // If we can't find the dataset, that's fine -- it might - // not have been formatted yet. - if let DestroyDatasetErrorVariant::NotFound = - err.err - { - Ok(()) - } else { - Err(err) - } - })?; + Zfs::destroy_dataset(executor, name).or_else( + |err| { + // If we can't find the dataset, that's fine -- it might + // not have been formatted yet. + if let DestroyDatasetErrorVariant::NotFound = + err.err + { + Ok(()) + } else { + Err(err) + } + }, + )?; } } } @@ -582,6 +597,7 @@ impl Disk { compression: dataset.compression, }); Zfs::ensure_filesystem( + executor, name, Mountpoint::Path(mountpoint), zoned, @@ -591,11 +607,18 @@ impl Disk { )?; if dataset.wipe { - Zfs::set_oxide_value(name, "agent", agent_local_value) - .map_err(|err| DiskError::CannotSetAgentProperty { + Zfs::set_oxide_value( + executor, + name, + "agent", + agent_local_value, + ) + .map_err(|err| { + DiskError::CannotSetAgentProperty { dataset: name.clone(), err: Box::new(err), - })?; + } + })?; } } Ok(()) diff --git a/sled-hardware/src/illumos/partitions.rs b/sled-hardware/src/illumos/partitions.rs index 950074bd3ad..3b248ff0509 100644 --- a/sled-hardware/src/illumos/partitions.rs +++ b/sled-hardware/src/illumos/partitions.rs @@ -7,16 +7,12 @@ use crate::illumos::gpt; use crate::{DiskError, DiskPaths, DiskVariant, Partition}; use camino::Utf8Path; -use illumos_utils::zpool::ZpoolName; +use helios_fusion::BoxedExecutor; +use illumos_utils::zpool::{Zpool, ZpoolName}; use slog::info; use slog::Logger; use uuid::Uuid; -#[cfg(test)] -use illumos_utils::zpool::MockZpool as Zpool; -#[cfg(not(test))] -use illumos_utils::zpool::Zpool; - // The expected layout of an M.2 device within the Oxide rack. // // Partitions beyond this "expected partition" array are ignored. @@ -78,16 +74,20 @@ fn parse_partition_types( /// to also be the index of the partition. pub fn ensure_partition_layout( log: &Logger, + executor: &BoxedExecutor, paths: &DiskPaths, variant: DiskVariant, ) -> Result, DiskError> { - internal_ensure_partition_layout::(log, paths, variant) + internal_ensure_partition_layout::( + log, executor, paths, variant, + ) } // Same as the [ensure_partition_layout], but with generic parameters // for access to external resources. fn internal_ensure_partition_layout( log: &Logger, + executor: &BoxedExecutor, paths: &DiskPaths, variant: DiskVariant, ) -> Result, DiskError> { @@ -121,7 +121,7 @@ fn internal_ensure_partition_layout( info!(log, "Formatting zpool on disk {}", paths.devfs_path); // If a zpool does not already exist, create one. let zpool_name = ZpoolName::new_external(Uuid::new_v4()); - Zpool::create(zpool_name, dev_path)?; + Zpool::create(&executor, zpool_name, dev_path)?; return Ok(vec![Partition::ZfsPool]); } DiskVariant::M2 => { @@ -158,9 +158,13 @@ mod test { use super::*; use crate::DiskPaths; use camino::Utf8PathBuf; - use illumos_utils::zpool::MockZpool; + use helios_fusion::{Input, OutputExt, PFEXEC}; + use helios_tokamak::{FakeChild, FakeExecutorBuilder}; + use illumos_utils::zpool::{ZpoolKind, ZPOOL}; use omicron_test_utils::dev::test_setup_log; use std::path::Path; + use std::process::Output; + use std::str::FromStr; struct FakePartition { index: usize, @@ -189,10 +193,12 @@ mod test { "ensure_partition_layout_u2_no_format_without_dev_path", ); let log = &logctx.log; + let executor = FakeExecutorBuilder::new(log.clone()).build(); let devfs_path = Utf8PathBuf::from("/devfs/path"); let result = internal_ensure_partition_layout::( &log, + &executor.as_executor(), &DiskPaths { devfs_path, dev_path: None }, DiskVariant::U2, ); @@ -205,25 +211,55 @@ mod test { } #[test] - #[serial_test::serial] fn ensure_partition_layout_u2_format_with_dev_path() { let logctx = test_setup_log("ensure_partition_layout_u2_format_with_dev_path"); let log = &logctx.log; - let devfs_path = Utf8PathBuf::from("/devfs/path"); const DEV_PATH: &'static str = "/dev/path"; - // We expect that formatting a zpool will involve calling - // "Zpool::create" with the provided "dev_path". - let create_ctx = MockZpool::create_context(); - create_ctx.expect().return_once(|_, observed_dev_path| { - assert_eq!(&Utf8PathBuf::from(DEV_PATH), observed_dev_path); - Ok(()) + let mut calls = 0; + let mut zpool_name = None; + let wait_handler = Box::new(move |child: &mut FakeChild| -> Output { + let input = Input::from(child.command()); + assert_eq!(input.program, PFEXEC); + + match calls { + 0 => { + assert_eq!(input.args.len(), 4); + assert_eq!(input.args[0], ZPOOL); + assert_eq!(input.args[1], "create"); + let name = ZpoolName::from_str(&input.args[2]) + .expect("Cannot parse Zpool Name"); + assert_eq!(name.kind(), ZpoolKind::External); + assert_eq!(input.args[3], DEV_PATH); + zpool_name = Some(name); + } + 1 => { + assert_eq!(input.args.len(), 4); + assert_eq!(input.args[0], ZPOOL); + assert_eq!(input.args[1], "set"); + assert_eq!(input.args[2], "feature@encryption=enabled"); + assert_eq!( + &ZpoolName::from_str(&input.args[3]) + .expect("Cannot parse zpool name"), + zpool_name.as_ref().expect( + "Should have grabbed zpool name from prior call" + ) + ); + } + _ => panic!("Unexpected call: {}", input), + }; + calls += 1; + Output::success() }); + let executor = FakeExecutorBuilder::new(log.clone()) + .wait_handler(wait_handler) + .build(); let partitions = internal_ensure_partition_layout::( &log, + &executor.as_executor(), &DiskPaths { devfs_path, dev_path: Some(Utf8PathBuf::from(DEV_PATH)), @@ -242,12 +278,14 @@ mod test { fn ensure_partition_layout_m2_cannot_format() { let logctx = test_setup_log("ensure_partition_layout_m2_cannot_format"); let log = &logctx.log.clone(); + let executor = FakeExecutorBuilder::new(log.clone()).build(); let devfs_path = Utf8PathBuf::from("/devfs/path"); const DEV_PATH: &'static str = "/dev/path"; assert!(internal_ensure_partition_layout::( &log, + &executor.as_executor(), &DiskPaths { devfs_path, dev_path: Some(Utf8PathBuf::from(DEV_PATH)) @@ -279,12 +317,14 @@ mod test { let logctx = test_setup_log("ensure_partition_layout_u2_with_expected_format"); let log = &logctx.log; + let executor = FakeExecutorBuilder::new(log.clone()).build(); let devfs_path = Utf8PathBuf::from("/devfs/path"); const DEV_PATH: &'static str = "/dev/path"; let partitions = internal_ensure_partition_layout::( &log, + &executor.as_executor(), &DiskPaths { devfs_path, dev_path: Some(Utf8PathBuf::from(DEV_PATH)), @@ -321,12 +361,14 @@ mod test { let logctx = test_setup_log("ensure_partition_layout_m2_with_expected_format"); let log = &logctx.log; + let executor = FakeExecutorBuilder::new(log.clone()).build(); let devfs_path = Utf8PathBuf::from("/devfs/path"); const DEV_PATH: &'static str = "/dev/path"; let partitions = internal_ensure_partition_layout::( &log, + &executor.as_executor(), &DiskPaths { devfs_path, dev_path: Some(Utf8PathBuf::from(DEV_PATH)), @@ -359,6 +401,7 @@ mod test { let logctx = test_setup_log("ensure_partition_layout_m2_fails_with_empty_gpt"); let log = &logctx.log; + let executor = FakeExecutorBuilder::new(log.clone()).build(); let devfs_path = Utf8PathBuf::from("/devfs/path"); const DEV_PATH: &'static str = "/dev/path"; @@ -366,6 +409,7 @@ mod test { assert!(matches!( internal_ensure_partition_layout::( &log, + &executor.as_executor(), &DiskPaths { devfs_path, dev_path: Some(Utf8PathBuf::from(DEV_PATH)), @@ -384,6 +428,7 @@ mod test { let logctx = test_setup_log("ensure_partition_layout_u2_fails_with_empty_gpt"); let log = &logctx.log; + let executor = FakeExecutorBuilder::new(log.clone()).build(); let devfs_path = Utf8PathBuf::from("/devfs/path"); const DEV_PATH: &'static str = "/dev/path"; @@ -391,6 +436,7 @@ mod test { assert!(matches!( internal_ensure_partition_layout::( &log, + &executor.as_executor(), &DiskPaths { devfs_path, dev_path: Some(Utf8PathBuf::from(DEV_PATH)), diff --git a/sled-hardware/src/non_illumos/mod.rs b/sled-hardware/src/non_illumos/mod.rs index 6e36330df08..6f0f8d4c666 100644 --- a/sled-hardware/src/non_illumos/mod.rs +++ b/sled-hardware/src/non_illumos/mod.rs @@ -4,6 +4,7 @@ use crate::disk::{DiskError, DiskPaths, DiskVariant, Partition, UnparsedDisk}; use crate::{Baseboard, SledMode}; +use helios_fusion::BoxedExecutor; use slog::Logger; use std::collections::HashSet; use tokio::sync::broadcast; @@ -54,6 +55,7 @@ impl HardwareManager { pub fn ensure_partition_layout( _log: &Logger, + _executor: &BoxedExecutor, _paths: &DiskPaths, _variant: DiskVariant, ) -> Result, DiskError> { diff --git a/sled-hardware/src/underlay.rs b/sled-hardware/src/underlay.rs index 48d1dd2d643..ccaf277adb7 100644 --- a/sled-hardware/src/underlay.rs +++ b/sled-hardware/src/underlay.rs @@ -5,6 +5,7 @@ //! Finding the underlay network physical links and address objects. use crate::is_gimlet; +use helios_fusion::BoxedExecutor; use illumos_utils::addrobj; use illumos_utils::addrobj::AddrObject; use illumos_utils::dladm; @@ -29,7 +30,7 @@ pub enum Error { #[error( "Failed to create an IPv6 link-local address for underlay devices: {0}" )] - UnderlayDeviceAddress(#[from] illumos_utils::ExecutionError), + UnderlayDeviceAddress(#[from] helios_fusion::ExecutionError), #[error(transparent)] BadAddrObj(#[from] addrobj::ParseError), @@ -51,22 +52,27 @@ pub enum Error { /// `ensure_links_have_global_zone_link_local_v6_addresses()` with the links /// returned by `find_chelsio_links()`. pub fn find_nics( + executor: &BoxedExecutor, config_data_links: &[String; 2], ) -> Result, Error> { - let underlay_nics = find_chelsio_links(config_data_links)?; + let underlay_nics = find_chelsio_links(executor, config_data_links)?; // Before these links have any consumers (eg. IP interfaces), set the MTU. // If we have previously set the MTU, do not attempt to re-set. const MTU: &str = "9000"; for link in &underlay_nics { - let existing_mtu = Dladm::get_linkprop(&link.to_string(), "mtu")?; + let existing_mtu = + Dladm::get_linkprop(executor, &link.to_string(), "mtu")?; if existing_mtu != MTU { - Dladm::set_linkprop(&link.to_string(), "mtu", MTU)?; + Dladm::set_linkprop(executor, &link.to_string(), "mtu", MTU)?; } } - ensure_links_have_global_zone_link_local_v6_addresses(&underlay_nics) + ensure_links_have_global_zone_link_local_v6_addresses( + executor, + &underlay_nics, + ) } /// Return the Chelsio links on the system. @@ -75,10 +81,11 @@ pub fn find_nics( /// developer machine, or generally a non-Gimlet, this will return the /// VNICs we use to emulate those Chelsio links. pub fn find_chelsio_links( + executor: &BoxedExecutor, config_data_links: &[String; 2], ) -> Result, Error> { if is_gimlet().map_err(Error::SystemDetection)? { - Dladm::list_physical().map_err(Error::FindLinks).map(|links| { + Dladm::list_physical(executor).map_err(Error::FindLinks).map(|links| { links .into_iter() .filter(|link| link.0.starts_with(CHELSIO_LINK_PREFIX)) @@ -95,13 +102,14 @@ pub fn find_chelsio_links( /// Ensure each of the `PhysicalLink`s has a link local IPv6 address in the /// global zone. pub fn ensure_links_have_global_zone_link_local_v6_addresses( + executor: &BoxedExecutor, links: &[PhysicalLink], ) -> Result, Error> { let mut addr_objs = Vec::with_capacity(links.len()); for link in links { let addrobj = AddrObject::link_local(&link.0)?; - Zones::ensure_has_link_local_v6_address(None, &addrobj)?; + Zones::ensure_has_link_local_v6_address(executor, None, &addrobj)?; addr_objs.push(addrobj); } @@ -126,9 +134,10 @@ impl BootstrapInterface { // could be randomly generated when it no longer needs to be durable. pub fn ip( self, + executor: &BoxedExecutor, link: &PhysicalLink, ) -> Result { - let mac = Dladm::get_mac(link)?; + let mac = Dladm::get_mac(executor, link)?; Ok(mac_to_bootstrap_ip(mac, self.interface_id())) } } diff --git a/wicketd/Cargo.toml b/wicketd/Cargo.toml index 6df5e0e4e55..9bfeaefa48f 100644 --- a/wicketd/Cargo.toml +++ b/wicketd/Cargo.toml @@ -19,6 +19,7 @@ either.workspace = true flume.workspace = true futures.workspace = true gateway-messages.workspace = true +helios-fusion.workspace = true hex.workspace = true hubtools.workspace = true http.workspace = true @@ -65,6 +66,7 @@ expectorate.workspace = true flate2.workspace = true fs-err.workspace = true gateway-test-utils.workspace = true +helios-tokamak.workspace = true http.workspace = true installinator.workspace = true installinator-artifact-client.workspace = true diff --git a/wicketd/src/preflight_check/uplink.rs b/wicketd/src/preflight_check/uplink.rs index c0f5d0c6bb9..a9a7e8b0a25 100644 --- a/wicketd/src/preflight_check/uplink.rs +++ b/wicketd/src/preflight_check/uplink.rs @@ -15,8 +15,8 @@ use dpd_client::types::RouteSettingsV4; use dpd_client::Client as DpdClient; use dpd_client::ClientState as DpdClientState; use either::Either; +use helios_fusion::PFEXEC; use illumos_utils::zone::SVCCFG; -use illumos_utils::PFEXEC; use omicron_common::address::DENDRITE_PORT; use omicron_common::api::internal::shared::PortFec as OmicronPortFec; use omicron_common::api::internal::shared::PortSpeed as OmicronPortSpeed; diff --git a/wicketd/tests/integration_tests/updates.rs b/wicketd/tests/integration_tests/updates.rs index a198068ef3a..ab7c3e82c6b 100644 --- a/wicketd/tests/integration_tests/updates.rs +++ b/wicketd/tests/integration_tests/updates.rs @@ -11,6 +11,7 @@ use camino_tempfile::Utf8TempDir; use clap::Parser; use gateway_messages::SpPort; use gateway_test_utils::setup as gateway_setup; +use helios_tokamak::FakeExecutorBuilder; use installinator::HOST_PHASE_2_FILE_NAME; use omicron_common::{ api::internal::nexus::KnownArtifactKind, @@ -285,7 +286,8 @@ async fn test_installinator_fetch() { ]) .expect("installinator args parsed successfully"); - args.exec(&log.new(slog::o!("crate" => "installinator"))) + let executor = FakeExecutorBuilder::new(log.clone()).build().as_executor(); + args.exec(&log.new(slog::o!("crate" => "installinator")), &executor) .await .expect("installinator succeeded"); diff --git a/workspace-hack/Cargo.toml b/workspace-hack/Cargo.toml index d3e00b18310..a6a0d8e7d2a 100644 --- a/workspace-hack/Cargo.toml +++ b/workspace-hack/Cargo.toml @@ -18,7 +18,7 @@ anyhow = { version = "1", features = ["backtrace"] } bit-set = { version = "0.5" } bit-vec = { version = "0.6" } bitflags-dff4ba8e3ae991db = { package = "bitflags", version = "1" } -bitflags-f595c2ba2a3f28df = { package = "bitflags", version = "2", default-features = false, features = ["serde"] } +bitflags-f595c2ba2a3f28df = { package = "bitflags", version = "2", default-features = false, features = ["serde", "std"] } bitvec = { version = "1" } bstr-6f8ce4dd05d13bba = { package = "bstr", version = "0.2" } bstr-dff4ba8e3ae991db = { package = "bstr", version = "1" } @@ -80,7 +80,6 @@ schemars = { version = "0.8", features = ["bytes", "chrono", "uuid1"] } semver = { version = "1", features = ["serde"] } serde = { version = "1", features = ["alloc", "derive", "rc"] } sha2 = { version = "0.10", features = ["oid"] } -signature = { version = "2", default-features = false, features = ["digest", "rand_core", "std"] } similar = { version = "2", features = ["inline", "unicode"] } slog = { version = "2", features = ["dynamic-keys", "max_level_trace", "release_max_level_debug", "release_max_level_trace"] } spin = { version = "0.9" } @@ -111,7 +110,7 @@ anyhow = { version = "1", features = ["backtrace"] } bit-set = { version = "0.5" } bit-vec = { version = "0.6" } bitflags-dff4ba8e3ae991db = { package = "bitflags", version = "1" } -bitflags-f595c2ba2a3f28df = { package = "bitflags", version = "2", default-features = false, features = ["serde"] } +bitflags-f595c2ba2a3f28df = { package = "bitflags", version = "2", default-features = false, features = ["serde", "std"] } bitvec = { version = "1" } bstr-6f8ce4dd05d13bba = { package = "bstr", version = "0.2" } bstr-dff4ba8e3ae991db = { package = "bstr", version = "1" } @@ -174,7 +173,6 @@ schemars = { version = "0.8", features = ["bytes", "chrono", "uuid1"] } semver = { version = "1", features = ["serde"] } serde = { version = "1", features = ["alloc", "derive", "rc"] } sha2 = { version = "0.10", features = ["oid"] } -signature = { version = "2", default-features = false, features = ["digest", "rand_core", "std"] } similar = { version = "2", features = ["inline", "unicode"] } slog = { version = "2", features = ["dynamic-keys", "max_level_trace", "release_max_level_debug", "release_max_level_trace"] } spin = { version = "0.9" } @@ -203,56 +201,48 @@ zeroize = { version = "1", features = ["std", "zeroize_derive"] } zip = { version = "0.6", default-features = false, features = ["bzip2", "deflate"] } [target.x86_64-unknown-linux-gnu.dependencies] -bitflags-f595c2ba2a3f28df = { package = "bitflags", version = "2", default-features = false, features = ["std"] } hyper-rustls = { version = "0.24" } mio = { version = "0.8", features = ["net", "os-ext"] } once_cell = { version = "1", features = ["unstable"] } rustix = { version = "0.38", features = ["fs", "termios"] } [target.x86_64-unknown-linux-gnu.build-dependencies] -bitflags-f595c2ba2a3f28df = { package = "bitflags", version = "2", default-features = false, features = ["std"] } hyper-rustls = { version = "0.24" } mio = { version = "0.8", features = ["net", "os-ext"] } once_cell = { version = "1", features = ["unstable"] } rustix = { version = "0.38", features = ["fs", "termios"] } [target.x86_64-apple-darwin.dependencies] -bitflags-f595c2ba2a3f28df = { package = "bitflags", version = "2", default-features = false, features = ["std"] } hyper-rustls = { version = "0.24" } mio = { version = "0.8", features = ["net", "os-ext"] } once_cell = { version = "1", features = ["unstable"] } rustix = { version = "0.38", features = ["fs", "termios"] } [target.x86_64-apple-darwin.build-dependencies] -bitflags-f595c2ba2a3f28df = { package = "bitflags", version = "2", default-features = false, features = ["std"] } hyper-rustls = { version = "0.24" } mio = { version = "0.8", features = ["net", "os-ext"] } once_cell = { version = "1", features = ["unstable"] } rustix = { version = "0.38", features = ["fs", "termios"] } [target.aarch64-apple-darwin.dependencies] -bitflags-f595c2ba2a3f28df = { package = "bitflags", version = "2", default-features = false, features = ["std"] } hyper-rustls = { version = "0.24" } mio = { version = "0.8", features = ["net", "os-ext"] } once_cell = { version = "1", features = ["unstable"] } rustix = { version = "0.38", features = ["fs", "termios"] } [target.aarch64-apple-darwin.build-dependencies] -bitflags-f595c2ba2a3f28df = { package = "bitflags", version = "2", default-features = false, features = ["std"] } hyper-rustls = { version = "0.24" } mio = { version = "0.8", features = ["net", "os-ext"] } once_cell = { version = "1", features = ["unstable"] } rustix = { version = "0.38", features = ["fs", "termios"] } [target.x86_64-unknown-illumos.dependencies] -bitflags-f595c2ba2a3f28df = { package = "bitflags", version = "2", default-features = false, features = ["std"] } hyper-rustls = { version = "0.24" } mio = { version = "0.8", features = ["net", "os-ext"] } once_cell = { version = "1", features = ["unstable"] } rustix = { version = "0.38", features = ["fs", "termios"] } [target.x86_64-unknown-illumos.build-dependencies] -bitflags-f595c2ba2a3f28df = { package = "bitflags", version = "2", default-features = false, features = ["std"] } hyper-rustls = { version = "0.24" } mio = { version = "0.8", features = ["net", "os-ext"] } once_cell = { version = "1", features = ["unstable"] }