diff --git a/Cargo.lock b/Cargo.lock index a14b71ecddc..907e40bbc3d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -691,7 +691,7 @@ dependencies = [ [[package]] name = "bhyve_api" version = "0.0.0" -source = "git+https://github.com/oxidecomputer/propolis?rev=e5c85d84b0a51803caffb335a1063612edb02f6d#e5c85d84b0a51803caffb335a1063612edb02f6d" +source = "git+https://github.com/oxidecomputer/propolis?rev=060a204d91e401a368c700a09d24510b7cd2b0e4#060a204d91e401a368c700a09d24510b7cd2b0e4" dependencies = [ "bhyve_api_sys", "libc", @@ -701,7 +701,7 @@ dependencies = [ [[package]] name = "bhyve_api_sys" version = "0.0.0" -source = "git+https://github.com/oxidecomputer/propolis?rev=e5c85d84b0a51803caffb335a1063612edb02f6d#e5c85d84b0a51803caffb335a1063612edb02f6d" +source = "git+https://github.com/oxidecomputer/propolis?rev=060a204d91e401a368c700a09d24510b7cd2b0e4#060a204d91e401a368c700a09d24510b7cd2b0e4" dependencies = [ "libc", "strum", @@ -939,7 +939,7 @@ dependencies = [ name = "bootstrap-agent-api" version = "0.1.0" dependencies = [ - "dropshot 0.16.0", + "dropshot", "omicron-common", "omicron-uuid-kinds", "omicron-workspace-hack", @@ -1226,7 +1226,7 @@ version = "0.1.0" dependencies = [ "anyhow", "clap", - "dropshot 0.16.0", + "dropshot", "futures", "libc", "omicron-common", @@ -1398,7 +1398,7 @@ dependencies = [ "clap", "clickhouse-admin-server-client", "clickhouse-admin-types", - "dropshot 0.16.0", + "dropshot", "futures", "omicron-common", "omicron-workspace-hack", @@ -1419,7 +1419,7 @@ name = "clickhouse-admin-api" version = "0.1.0" dependencies = [ "clickhouse-admin-types", - "dropshot 0.16.0", + "dropshot", "omicron-common", "omicron-uuid-kinds", "omicron-workspace-hack", @@ -1479,7 +1479,7 @@ dependencies = [ "camino", "clickhouse-admin-types", "clickward", - "dropshot 0.16.0", + "dropshot", "omicron-workspace-hack", ] @@ -1566,7 +1566,7 @@ name = "cockroach-admin-api" version = "0.1.0" dependencies = [ "cockroach-admin-types", - "dropshot 0.16.0", + "dropshot", "omicron-common", "omicron-uuid-kinds", "omicron-workspace-hack", @@ -1837,7 +1837,7 @@ name = "crdb-seed" version = "0.1.0" dependencies = [ "anyhow", - "dropshot 0.16.0", + "dropshot", "omicron-test-utils", "omicron-workspace-hack", "slog", @@ -1952,7 +1952,7 @@ dependencies = [ [[package]] name = "crucible-agent-client" version = "0.0.1" -source = "git+https://github.com/oxidecomputer/crucible?rev=da3cf198a0e000bb89efc3a1c77d7ba09340a600#da3cf198a0e000bb89efc3a1c77d7ba09340a600" +source = "git+https://github.com/oxidecomputer/crucible?rev=45801597f410685015ac2704d044919a41e3ff75#45801597f410685015ac2704d044919a41e3ff75" dependencies = [ "anyhow", "chrono", @@ -1968,7 +1968,7 @@ dependencies = [ [[package]] name = "crucible-client-types" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/crucible?rev=da3cf198a0e000bb89efc3a1c77d7ba09340a600#da3cf198a0e000bb89efc3a1c77d7ba09340a600" +source = "git+https://github.com/oxidecomputer/crucible?rev=45801597f410685015ac2704d044919a41e3ff75#45801597f410685015ac2704d044919a41e3ff75" dependencies = [ "base64 0.22.1", "crucible-workspace-hack", @@ -1981,12 +1981,12 @@ dependencies = [ [[package]] name = "crucible-common" version = "0.0.1" -source = "git+https://github.com/oxidecomputer/crucible?rev=da3cf198a0e000bb89efc3a1c77d7ba09340a600#da3cf198a0e000bb89efc3a1c77d7ba09340a600" +source = "git+https://github.com/oxidecomputer/crucible?rev=45801597f410685015ac2704d044919a41e3ff75#45801597f410685015ac2704d044919a41e3ff75" dependencies = [ "anyhow", "atty", "crucible-workspace-hack", - "dropshot 0.16.0", + "dropshot", "nix 0.29.0", "rustls-pemfile 1.0.4", "schemars", @@ -2010,7 +2010,7 @@ dependencies = [ [[package]] name = "crucible-pantry-client" version = "0.0.1" -source = "git+https://github.com/oxidecomputer/crucible?rev=da3cf198a0e000bb89efc3a1c77d7ba09340a600#da3cf198a0e000bb89efc3a1c77d7ba09340a600" +source = "git+https://github.com/oxidecomputer/crucible?rev=45801597f410685015ac2704d044919a41e3ff75#45801597f410685015ac2704d044919a41e3ff75" dependencies = [ "anyhow", "chrono", @@ -2027,7 +2027,7 @@ dependencies = [ [[package]] name = "crucible-smf" version = "0.0.0" -source = "git+https://github.com/oxidecomputer/crucible?rev=da3cf198a0e000bb89efc3a1c77d7ba09340a600#da3cf198a0e000bb89efc3a1c77d7ba09340a600" +source = "git+https://github.com/oxidecomputer/crucible?rev=45801597f410685015ac2704d044919a41e3ff75#45801597f410685015ac2704d044919a41e3ff75" dependencies = [ "crucible-workspace-hack", "libc", @@ -2586,7 +2586,7 @@ dependencies = [ "clap", "dns-server-api", "dns-service-client", - "dropshot 0.16.0", + "dropshot", "expectorate", "hickory-client", "hickory-proto", @@ -2621,7 +2621,7 @@ name = "dns-server-api" version = "0.1.0" dependencies = [ "chrono", - "dropshot 0.16.0", + "dropshot", "internal-dns-types", "omicron-workspace-hack", "openapi-manager-types", @@ -2699,54 +2699,6 @@ dependencies = [ "uuid", ] -[[package]] -name = "dropshot" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab804b8d4ab58d96e1e19c8ef87e1747a70d2819e92b659d6fe8d5ac5ac44d50" -dependencies = [ - "async-stream", - "async-trait", - "base64 0.22.1", - "bytes", - "camino", - "chrono", - "debug-ignore", - "dropshot_endpoint 0.12.0", - "form_urlencoded", - "futures", - "hostname 0.4.0", - "http", - "http-body-util", - "hyper", - "hyper-util", - "indexmap 2.7.1", - "multer", - "openapiv3", - "paste", - "percent-encoding", - "rustls 0.22.4", - "rustls-pemfile 2.2.0", - "schemars", - "scopeguard", - "serde", - "serde_json", - "serde_path_to_error", - "serde_urlencoded", - "sha1", - "slog", - "slog-async", - "slog-bunyan", - "slog-json", - "slog-term", - "tokio", - "tokio-rustls 0.25.0", - "toml 0.8.20", - "uuid", - "version_check", - "waitgroup", -] - [[package]] name = "dropshot" version = "0.16.0" @@ -2760,7 +2712,7 @@ dependencies = [ "camino", "chrono", "debug-ignore", - "dropshot_endpoint 0.16.0", + "dropshot_endpoint", "form_urlencoded", "futures", "hostname 0.4.0", @@ -2798,20 +2750,6 @@ dependencies = [ "waitgroup", ] -[[package]] -name = "dropshot_endpoint" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "796be76b11b79de0decd7be2105add01220f8bbe04cf1f83214c0b801414a722" -dependencies = [ - "heck 0.5.0", - "proc-macro2", - "quote", - "serde", - "serde_tokenstream", - "syn 2.0.98", -] - [[package]] name = "dropshot_endpoint" version = "0.16.0" @@ -3069,7 +3007,7 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" name = "ereport-api" version = "0.1.0" dependencies = [ - "dropshot 0.16.0", + "dropshot", "ereport-types", "omicron-workspace-hack", "openapi-manager-types", @@ -3509,7 +3447,7 @@ dependencies = [ name = "gateway-api" version = "0.1.0" dependencies = [ - "dropshot 0.16.0", + "dropshot", "gateway-types", "omicron-common", "omicron-uuid-kinds", @@ -3617,7 +3555,7 @@ name = "gateway-test-utils" version = "0.1.0" dependencies = [ "camino", - "dropshot 0.16.0", + "dropshot", "gateway-messages", "gateway-types", "omicron-gateway", @@ -4624,7 +4562,7 @@ dependencies = [ "cfg-if", "crucible-smf", "debug-ignore", - "dropshot 0.16.0", + "dropshot", "futures", "http", "ipnetwork", @@ -4831,7 +4769,7 @@ name = "installinator-api" version = "0.1.0" dependencies = [ "anyhow", - "dropshot 0.16.0", + "dropshot", "hyper", "installinator-common", "omicron-common", @@ -4897,7 +4835,7 @@ version = "0.1.0" dependencies = [ "anyhow", "clap", - "dropshot 0.16.0", + "dropshot", "hickory-resolver", "internal-dns-resolver", "internal-dns-types", @@ -4915,7 +4853,7 @@ dependencies = [ "assert_matches", "dns-server", "dns-service-client", - "dropshot 0.16.0", + "dropshot", "expectorate", "futures", "hickory-resolver", @@ -5873,7 +5811,7 @@ dependencies = [ "base64 0.22.1", "chrono", "cookie", - "dropshot 0.16.0", + "dropshot", "futures", "headers", "http", @@ -5938,7 +5876,7 @@ version = "0.1.0" dependencies = [ "anyhow", "camino", - "dropshot 0.16.0", + "dropshot", "expectorate", "libc", "omicron-common", @@ -6073,7 +6011,7 @@ dependencies = [ "db-macros", "diesel", "diesel-dtrace", - "dropshot 0.16.0", + "dropshot", "expectorate", "futures", "gateway-client", @@ -6167,7 +6105,7 @@ name = "nexus-external-api" version = "0.1.0" dependencies = [ "anyhow", - "dropshot 0.16.0", + "dropshot", "http", "hyper", "ipnetwork", @@ -6184,7 +6122,7 @@ dependencies = [ name = "nexus-internal-api" version = "0.1.0" dependencies = [ - "dropshot 0.16.0", + "dropshot", "http", "nexus-types", "omicron-common", @@ -6590,7 +6528,7 @@ dependencies = [ "crucible-agent-client", "dns-server", "dns-service-client", - "dropshot 0.16.0", + "dropshot", "futures", "gateway-messages", "gateway-test-utils", @@ -6655,7 +6593,7 @@ dependencies = [ "daft", "derive-where", "derive_more", - "dropshot 0.16.0", + "dropshot", "futures", "gateway-client", "http", @@ -7005,7 +6943,7 @@ dependencies = [ "clickhouse-admin-test-utils", "clickhouse-admin-types", "clickward", - "dropshot 0.16.0", + "dropshot", "expectorate", "flume", "http", @@ -7046,7 +6984,7 @@ dependencies = [ "cockroach-admin-api", "cockroach-admin-types", "csv", - "dropshot 0.16.0", + "dropshot", "expectorate", "http", "illumos-utils", @@ -7088,7 +7026,7 @@ dependencies = [ "camino-tempfile", "chrono", "daft", - "dropshot 0.16.0", + "dropshot", "expectorate", "futures", "hex", @@ -7150,7 +7088,7 @@ version = "0.1.0" dependencies = [ "anyhow", "clap", - "dropshot 0.16.0", + "dropshot", "expectorate", "futures", "libc", @@ -7190,7 +7128,7 @@ dependencies = [ "camino", "chrono", "clap", - "dropshot 0.16.0", + "dropshot", "ereport-api", "expectorate", "futures", @@ -7236,7 +7174,7 @@ version = "0.1.0" dependencies = [ "anyhow", "assert_matches", - "dropshot 0.16.0", + "dropshot", "futures", "internal-dns-resolver", "internal-dns-types", @@ -7313,7 +7251,7 @@ dependencies = [ "dns-server", "dns-service-client", "dpd-client", - "dropshot 0.16.0", + "dropshot", "ereport-client", "expectorate", "fatfs", @@ -7395,7 +7333,7 @@ dependencies = [ "pq-sys", "pretty_assertions", "progenitor-client 0.9.1", - "propolis-client 0.1.0 (git+https://github.com/oxidecomputer/propolis?rev=e5c85d84b0a51803caffb335a1063612edb02f6d)", + "propolis-client 0.1.0 (git+https://github.com/oxidecomputer/propolis?rev=060a204d91e401a368c700a09d24510b7cd2b0e4)", "qorb", "rand 0.8.5", "range-requests", @@ -7456,7 +7394,7 @@ dependencies = [ "crucible-agent-client", "csv", "diesel", - "dropshot 0.16.0", + "dropshot", "dyn-clone", "expectorate", "futures", @@ -7591,7 +7529,7 @@ dependencies = [ "anyhow", "camino", "clap", - "dropshot 0.16.0", + "dropshot", "internal-dns-resolver", "internal-dns-types", "nexus-db-model", @@ -7665,7 +7603,7 @@ dependencies = [ "bytes", "camino", "clap", - "dropshot 0.16.0", + "dropshot", "futures", "omicron-workspace-hack", "repo-depot-api", @@ -7712,7 +7650,7 @@ dependencies = [ "dns-server", "dns-service-client", "dpd-client", - "dropshot 0.16.0", + "dropshot", "ereport-api", "expectorate", "flate2", @@ -7755,7 +7693,7 @@ dependencies = [ "oximeter-producer", "oxnet", "pretty_assertions", - "propolis-client 0.1.0 (git+https://github.com/oxidecomputer/propolis?rev=e5c85d84b0a51803caffb335a1063612edb02f6d)", + "propolis-client 0.1.0 (git+https://github.com/oxidecomputer/propolis?rev=060a204d91e401a368c700a09d24510b7cd2b0e4)", "propolis-mock-server", "propolis_api_types", "rand 0.8.5", @@ -7814,7 +7752,7 @@ dependencies = [ "camino", "camino-tempfile", "chrono", - "dropshot 0.16.0", + "dropshot", "expectorate", "filetime", "futures", @@ -8076,7 +8014,7 @@ dependencies = [ "cockroach-admin-api", "debug-ignore", "dns-server-api", - "dropshot 0.16.0", + "dropshot", "ereport-api", "fs-err 3.1.0", "gateway-api", @@ -8331,7 +8269,7 @@ name = "oximeter-api" version = "0.1.0" dependencies = [ "chrono", - "dropshot 0.16.0", + "dropshot", "omicron-common", "omicron-workspace-hack", "schemars", @@ -8363,7 +8301,7 @@ dependencies = [ "camino", "chrono", "clap", - "dropshot 0.16.0", + "dropshot", "expectorate", "futures", "httpmock", @@ -8421,7 +8359,7 @@ dependencies = [ "crossterm", "debug-ignore", "display-error-chain", - "dropshot 0.16.0", + "dropshot", "expectorate", "futures", "gethostname", @@ -8473,7 +8411,7 @@ version = "0.1.0" dependencies = [ "cfg-if", "chrono", - "dropshot 0.16.0", + "dropshot", "futures", "http", "hyper", @@ -8520,7 +8458,7 @@ dependencies = [ "anyhow", "chrono", "clap", - "dropshot 0.16.0", + "dropshot", "internal-dns-resolver", "internal-dns-types", "nexus-client", @@ -9689,7 +9627,7 @@ dependencies = [ [[package]] name = "propolis-client" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/propolis?rev=e5c85d84b0a51803caffb335a1063612edb02f6d#e5c85d84b0a51803caffb335a1063612edb02f6d" +source = "git+https://github.com/oxidecomputer/propolis?rev=060a204d91e401a368c700a09d24510b7cd2b0e4#060a204d91e401a368c700a09d24510b7cd2b0e4" dependencies = [ "async-trait", "base64 0.21.7", @@ -9734,20 +9672,22 @@ dependencies = [ [[package]] name = "propolis-mock-server" version = "0.0.0" -source = "git+https://github.com/oxidecomputer/propolis?rev=e5c85d84b0a51803caffb335a1063612edb02f6d#e5c85d84b0a51803caffb335a1063612edb02f6d" +source = "git+https://github.com/oxidecomputer/propolis?rev=060a204d91e401a368c700a09d24510b7cd2b0e4#060a204d91e401a368c700a09d24510b7cd2b0e4" dependencies = [ "anyhow", "atty", "base64 0.21.7", "clap", - "dropshot 0.12.0", + "dropshot", "futures", "hyper", "progenitor 0.9.1", + "propolis_api_types", "propolis_types", "rand 0.8.5", "reqwest", "schemars", + "semver 1.0.25", "serde", "serde_json", "slog", @@ -9776,13 +9716,12 @@ dependencies = [ [[package]] name = "propolis_api_types" version = "0.0.0" -source = "git+https://github.com/oxidecomputer/propolis?rev=e5c85d84b0a51803caffb335a1063612edb02f6d#e5c85d84b0a51803caffb335a1063612edb02f6d" +source = "git+https://github.com/oxidecomputer/propolis?rev=060a204d91e401a368c700a09d24510b7cd2b0e4#060a204d91e401a368c700a09d24510b7cd2b0e4" dependencies = [ "crucible-client-types", "propolis_types", "schemars", "serde", - "serde_with", "thiserror 1.0.69", "uuid", ] @@ -9790,7 +9729,7 @@ dependencies = [ [[package]] name = "propolis_types" version = "0.0.0" -source = "git+https://github.com/oxidecomputer/propolis?rev=e5c85d84b0a51803caffb335a1063612edb02f6d#e5c85d84b0a51803caffb335a1063612edb02f6d" +source = "git+https://github.com/oxidecomputer/propolis?rev=060a204d91e401a368c700a09d24510b7cd2b0e4#060a204d91e401a368c700a09d24510b7cd2b0e4" dependencies = [ "schemars", "serde", @@ -10058,7 +9997,7 @@ name = "range-requests" version = "0.1.0" dependencies = [ "bytes", - "dropshot 0.16.0", + "dropshot", "futures", "http", "http-body", @@ -10175,7 +10114,7 @@ dependencies = [ "anyhow", "chrono", "clap", - "dropshot 0.16.0", + "dropshot", "futures", "gateway-client", "humantime", @@ -10316,7 +10255,7 @@ checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" name = "repo-depot-api" version = "0.1.0" dependencies = [ - "dropshot 0.16.0", + "dropshot", "omicron-workspace-hack", "schemars", "serde", @@ -11435,7 +11374,7 @@ name = "sled-agent-api" version = "0.1.0" dependencies = [ "camino", - "dropshot 0.16.0", + "dropshot", "http", "nexus-sled-agent-shared", "omicron-common", @@ -11463,12 +11402,13 @@ dependencies = [ "omicron-workspace-hack", "oxnet", "progenitor 0.9.1", - "propolis-client 0.1.0 (git+https://github.com/oxidecomputer/propolis?rev=e5c85d84b0a51803caffb335a1063612edb02f6d)", + "propolis-client 0.1.0 (git+https://github.com/oxidecomputer/propolis?rev=060a204d91e401a368c700a09d24510b7cd2b0e4)", "regress", "reqwest", "schemars", "serde", "serde_json", + "sled-agent-types", "slog", "uuid", ] @@ -11489,7 +11429,7 @@ dependencies = [ "omicron-uuid-kinds", "omicron-workspace-hack", "oxnet", - "propolis-client 0.1.0 (git+https://github.com/oxidecomputer/propolis?rev=e5c85d84b0a51803caffb335a1063612edb02f6d)", + "propolis-client 0.1.0 (git+https://github.com/oxidecomputer/propolis?rev=060a204d91e401a368c700a09d24510b7cd2b0e4)", "rcgen", "schemars", "serde", @@ -11841,7 +11781,7 @@ dependencies = [ "anyhow", "async-trait", "clap", - "dropshot 0.16.0", + "dropshot", "futures", "gateway-messages", "gateway-types", @@ -13516,7 +13456,7 @@ dependencies = [ "clap", "debug-ignore", "display-error-chain", - "dropshot 0.16.0", + "dropshot", "flate2", "fs-err 3.1.0", "futures", @@ -14029,7 +13969,7 @@ version = "0.1.0" dependencies = [ "anyhow", "dpd-client", - "dropshot 0.16.0", + "dropshot", "gateway-client", "maplit", "omicron-common", @@ -14087,7 +14027,7 @@ dependencies = [ "debug-ignore", "display-error-chain", "dpd-client", - "dropshot 0.16.0", + "dropshot", "either", "expectorate", "flate2", @@ -14158,7 +14098,7 @@ name = "wicketd-api" version = "0.1.0" dependencies = [ "bootstrap-agent-client", - "dropshot 0.16.0", + "dropshot", "gateway-client", "omicron-common", "omicron-passwords", @@ -14914,7 +14854,7 @@ dependencies = [ "anyhow", "camino", "clap", - "dropshot 0.16.0", + "dropshot", "illumos-utils", "omicron-common", "omicron-workspace-hack", diff --git a/Cargo.toml b/Cargo.toml index 5e4db73fbda..c0789cd0c52 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -387,10 +387,10 @@ crossterm = { version = "0.28.1", features = ["event-stream"] } # NOTE: if you change the pinned revision of the `crucible` dependencies, you # must also update the references in package-manifest.toml to match the new # revision. -crucible-agent-client = { git = "https://github.com/oxidecomputer/crucible", rev = "da3cf198a0e000bb89efc3a1c77d7ba09340a600" } -crucible-pantry-client = { git = "https://github.com/oxidecomputer/crucible", rev = "da3cf198a0e000bb89efc3a1c77d7ba09340a600" } -crucible-smf = { git = "https://github.com/oxidecomputer/crucible", rev = "da3cf198a0e000bb89efc3a1c77d7ba09340a600" } -crucible-common = { git = "https://github.com/oxidecomputer/crucible", rev = "da3cf198a0e000bb89efc3a1c77d7ba09340a600" } +crucible-agent-client = { git = "https://github.com/oxidecomputer/crucible", rev = "45801597f410685015ac2704d044919a41e3ff75" } +crucible-pantry-client = { git = "https://github.com/oxidecomputer/crucible", rev = "45801597f410685015ac2704d044919a41e3ff75" } +crucible-smf = { git = "https://github.com/oxidecomputer/crucible", rev = "45801597f410685015ac2704d044919a41e3ff75" } +crucible-common = { git = "https://github.com/oxidecomputer/crucible", rev = "45801597f410685015ac2704d044919a41e3ff75" } # NOTE: See above! csv = "1.3.1" curve25519-dalek = "4" @@ -604,10 +604,10 @@ progenitor-client = "0.9.1" # NOTE: if you change the pinned revision of the `bhyve_api` and propolis # dependencies, you must also update the references in package-manifest.toml to # match the new revision. -bhyve_api = { git = "https://github.com/oxidecomputer/propolis", rev = "e5c85d84b0a51803caffb335a1063612edb02f6d" } -propolis_api_types = { git = "https://github.com/oxidecomputer/propolis", rev = "e5c85d84b0a51803caffb335a1063612edb02f6d" } -propolis-client = { git = "https://github.com/oxidecomputer/propolis", rev = "e5c85d84b0a51803caffb335a1063612edb02f6d" } -propolis-mock-server = { git = "https://github.com/oxidecomputer/propolis", rev = "e5c85d84b0a51803caffb335a1063612edb02f6d" } +bhyve_api = { git = "https://github.com/oxidecomputer/propolis", rev = "060a204d91e401a368c700a09d24510b7cd2b0e4" } +propolis_api_types = { git = "https://github.com/oxidecomputer/propolis", rev = "060a204d91e401a368c700a09d24510b7cd2b0e4" } +propolis-client = { git = "https://github.com/oxidecomputer/propolis", rev = "060a204d91e401a368c700a09d24510b7cd2b0e4" } +propolis-mock-server = { git = "https://github.com/oxidecomputer/propolis", rev = "060a204d91e401a368c700a09d24510b7cd2b0e4" } # NOTE: see above! proptest = "1.6.0" qorb = "0.3.1" diff --git a/clients/sled-agent-client/Cargo.toml b/clients/sled-agent-client/Cargo.toml index e6c77fe24ae..4a0394b585d 100644 --- a/clients/sled-agent-client/Cargo.toml +++ b/clients/sled-agent-client/Cargo.toml @@ -23,5 +23,6 @@ reqwest = { workspace = true, features = [ "json", "rustls-tls", "stream" ] } schemars.workspace = true serde.workspace = true serde_json.workspace = true +sled-agent-types.workspace = true slog.workspace = true uuid.workspace = true diff --git a/clients/sled-agent-client/src/lib.rs b/clients/sled-agent-client/src/lib.rs index 280a3925d07..b27f3048439 100644 --- a/clients/sled-agent-client/src/lib.rs +++ b/clients/sled-agent-client/src/lib.rs @@ -56,6 +56,7 @@ progenitor::generate_api!( DiskVariant = omicron_common::disk::DiskVariant, ExternalIpGatewayMap = omicron_common::api::internal::shared::ExternalIpGatewayMap, Generation = omicron_common::api::external::Generation, + Hostname = omicron_common::api::external::Hostname, ImportExportPolicy = omicron_common::api::external::ImportExportPolicy, Inventory = nexus_sled_agent_shared::inventory::Inventory, InventoryDisk = nexus_sled_agent_shared::inventory::InventoryDisk, @@ -117,14 +118,6 @@ impl From for types::VmmState { } } -impl From - for types::InstanceCpuCount -{ - fn from(s: omicron_common::api::external::InstanceCpuCount) -> Self { - Self(s.0) - } -} - impl From for omicron_common::api::internal::nexus::VmmState { fn from(s: types::VmmState) -> Self { use omicron_common::api::internal::nexus::VmmState as Output; @@ -188,14 +181,6 @@ impl From } } -impl From - for omicron_common::api::external::InstanceCpuCount -{ - fn from(s: types::InstanceCpuCount) -> Self { - Self(s.0) - } -} - impl From for types::DiskRuntimeState { diff --git a/nexus/Cargo.toml b/nexus/Cargo.toml index 9f69a3e79cc..e2b4a748769 100644 --- a/nexus/Cargo.toml +++ b/nexus/Cargo.toml @@ -92,6 +92,7 @@ serde_urlencoded.workspace = true serde_with.workspace = true sha2.workspace = true sled-agent-client.workspace = true +sled-agent-types.workspace = true slog.workspace = true slog-async.workspace = true slog-dtrace.workspace = true @@ -162,7 +163,6 @@ pretty_assertions.workspace = true rcgen.workspace = true regex.workspace = true similar-asserts.workspace = true -sled-agent-types.workspace = true sp-sim.workspace = true rustls.workspace = true subprocess.workspace = true diff --git a/nexus/db-model/src/instance_cpu_count.rs b/nexus/db-model/src/instance_cpu_count.rs index b03dc5028d1..f739db2336d 100644 --- a/nexus/db-model/src/instance_cpu_count.rs +++ b/nexus/db-model/src/instance_cpu_count.rs @@ -52,9 +52,3 @@ where .map_err(|e| e.into()) } } - -impl From for sled_agent_client::types::InstanceCpuCount { - fn from(i: InstanceCpuCount) -> Self { - Self(i.0.0) - } -} diff --git a/nexus/src/app/instance.rs b/nexus/src/app/instance.rs index bcaa81d899e..7bff48d2877 100644 --- a/nexus/src/app/instance.rs +++ b/nexus/src/app/instance.rs @@ -14,7 +14,6 @@ use super::MAX_VCPU_PER_INSTANCE; use super::MIN_MEMORY_BYTES_PER_INSTANCE; use crate::app::sagas; use crate::app::sagas::NexusSaga; -use crate::cidata::InstanceCiData; use crate::external_api::params; use cancel_safe_futures::prelude::*; use futures::future::Fuse; @@ -41,6 +40,7 @@ use omicron_common::api::external::CreateResult; use omicron_common::api::external::DataPageParams; use omicron_common::api::external::DeleteResult; use omicron_common::api::external::Error; +use omicron_common::api::external::Hostname; use omicron_common::api::external::InstanceCpuCount; use omicron_common::api::external::InstanceState; use omicron_common::api::external::InternalContext; @@ -64,9 +64,7 @@ use propolis_client::support::tungstenite::protocol::frame::coding::CloseCode; use sagas::instance_common::ExternalIpAttach; use sagas::instance_start; use sagas::instance_update; -use sled_agent_client::types::InstanceBootSettings; use sled_agent_client::types::InstanceMigrationTargetParams; -use sled_agent_client::types::InstanceProperties; use sled_agent_client::types::VmmPutStateBody; use std::matches; use std::net::SocketAddr; @@ -1080,7 +1078,7 @@ impl super::Nexus { // TODO-cleanup: This can be removed when we are confident that no // instances exist prior to the addition of strict hostname validation // in the API. - let Ok(hostname) = db_instance.hostname.parse() else { + let Ok(hostname) = db_instance.hostname.parse::() else { let msg = format!( "The instance hostname '{}' is no longer valid. \ To access the data on its disks, this instance \ @@ -1107,57 +1105,6 @@ impl super::Nexus { ) .await?; - let mut disk_reqs = vec![]; - for disk in &disks { - // Disks that are attached to an instance should always have a slot - // assignment, but if for some reason this one doesn't, return an - // error instead of taking down the whole process. - let slot = match disk.slot { - Some(s) => s, - None => { - error!(self.log, "attached disk has no PCI slot assignment"; - "disk_id" => %disk.id(), - "disk_name" => disk.name().to_string(), - "instance_id" => ?disk.runtime_state.attach_instance_id); - - return Err(Error::internal_error(&format!( - "disk {} is attached but has no PCI slot assignment", - disk.id() - )) - .into()); - } - }; - - let volume = self - .db_datastore - .volume_checkout( - disk.volume_id(), - match operation { - InstanceRegisterReason::Start { vmm_id } => - db::datastore::VolumeCheckoutReason::InstanceStart { vmm_id }, - InstanceRegisterReason::Migrate { vmm_id, target_vmm_id } => - db::datastore::VolumeCheckoutReason::InstanceMigrate { vmm_id, target_vmm_id }, - } - ) - .await?; - - disk_reqs.push(sled_agent_client::types::InstanceDisk { - disk_id: disk.id(), - name: disk.name().to_string(), - slot: slot.0, - read_only: false, - vcr_json: volume.data().to_owned(), - }); - } - - // The routines that maintain an instance's boot options are supposed to - // guarantee that the boot disk ID, if present, is the ID of an attached - // disk. If this invariant isn't upheld, Propolis will catch the failure - // when it processes its received VM configuration. - let boot_settings = db_instance - .boot_disk_id - .map(|id| InstanceBootSettings { order: vec![id] }); - let nics = self .db_datastore .derive_guest_network_interface_info(&opctx, &authz_instance) @@ -1271,28 +1218,25 @@ impl super::Nexus { .unwrap(), }), ) - .await? - .into_iter(); + .await?; - let ssh_keys: Vec = - ssh_keys.map(|ssh_key| ssh_key.public_key).collect(); + let vmm_spec = self + .generate_vmm_spec( + &operation, + db_instance, + &disks, + &nics, + &ssh_keys, + ) + .await?; let metadata = sled_agent_client::types::InstanceMetadata { silo_id: authz_silo.id(), project_id: authz_project.id(), }; - // Ask the sled agent to begin the state change. Then update the - // database to reflect the new intermediate state. If this update is - // not the newest one, that's fine. That might just mean the sled agent - // beat us to it. - - let instance_hardware = sled_agent_client::types::InstanceHardware { - properties: InstanceProperties { - ncpus: db_instance.ncpus.into(), - memory: db_instance.memory.into(), - hostname, - }, + let local_config = sled_agent_client::types::InstanceSledLocalConfig { + hostname, nics, source_nat, ephemeral_ip, @@ -1304,12 +1248,6 @@ impl super::Nexus { host_domain: None, search_domains: Vec::new(), }, - disks: disk_reqs, - boot_settings, - cloud_init_bytes: Some(base64::Engine::encode( - &base64::engine::general_purpose::STANDARD, - db_instance.generate_cidata(&ssh_keys)?, - )), }; let instance_id = InstanceUuid::from_untyped_uuid(db_instance.id()); @@ -1338,7 +1276,8 @@ impl super::Nexus { .vmm_register( propolis_id, &sled_agent_client::types::InstanceEnsureBody { - hardware: instance_hardware, + vmm_spec, + local_config, migration_id: db_instance.runtime().migration_id, vmm_runtime, instance_id, diff --git a/nexus/src/app/instance_platform.rs b/nexus/src/app/instance_platform.rs new file mode 100644 index 00000000000..aed3d1b2f69 --- /dev/null +++ b/nexus/src/app/instance_platform.rs @@ -0,0 +1,494 @@ +// 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/. + +//! Logic for computing the virtual hardware platform to expose to an instance +//! given Nexus's records of its configuration. +//! +//! A VM's "virtual hardware platform" describes a set of virtual hardware +//! components to expose to a virtual guest and how those components are +//! configured. The code in this module is responsible for taking database +//! records describing an instance, its properties, and its attached peripherals +//! (e.g. disks and NICs) and constructing an "instance specification" that +//! sled-agent can use to initialize a Propolis VM that exposes the necessary +//! components. +//! +//! For more background on virtual platforms, see [RFD +//! 505](https://505.rfd.oxide.computer/). +//! +//! # Component identification +//! +//! Propolis VM specifications contain a "mainboard" that describes a VM's CPUs, +//! memory, and chipset and a "components" map that describes all of the VM's +//! attached virtual peripherals. Some peripherals are split into "device" and +//! "backend" components: the device tells Propolis what hardware to emulate for +//! the guest, and the backend describes what host resources (e.g. host VNICs) +//! and Oxide services (e.g. Crucible servers) the device should use to provide +//! its abstractions. +//! +//! Each component in a VM spec has a name, which is also its key in the +//! component map. Generally speaking, Propolis does not care how its clients +//! name components, but it does require that callers identify those components +//! by name in API calls that refer to a specific component. To make this as +//! easy as possible for the rest of Nexus, this module names components as +//! follows: +//! +//! - If a component corresponds to a specific control plane object (i.e. +//! something like a disk or a NIC that has its own database record and a +//! corresponding unique identifier): +//! - If the component requires both a device and a backend, the *backend* +//! uses the object's ID as a name, and the device uses a module-generated +//! name. +//! - If the component is unitary (i.e. it only has one component entry in the +//! instance spec), this module uses the object ID as its name. +//! - "Default" components that don't correspond to control plane objects, such +//! as a VM's serial ports, are named using the constants in the +//! [`component_names`] module. +//! +//! Using component IDs as backend names makes it easy for other parts of Nexus +//! to know what ID to use to refer to a particular Propolis component: changes +//! to Crucible configuration use the disk ID; changes to a host VNIC name use +//! the relevant network interface ID; and so on. Device frontend configuration +//! changes are much less common and so are less important to optimize for. +//! +//! ## Live migration +//! +//! When a VM migrates, the source Propolis passes the VM's specification +//! directly to the target Propolis. For the most part, this allows Nexus to +//! avoid having to re-create a running VM's configuration in order to migrate +//! it. The exceptions are, once again, component backends: Nexus and sled-agent +//! may need to change how backends are configured when they migrate from one +//! sled to another. To facilitate this, Propolis's migration-request API allows +//! the caller to pass a list of "replacement" components that the target should +//! substitute into the specification it receives from the source. Substitutions +//! are done by component name, so this module must make sure to use a +//! consistent naming scheme for any components that might be replaced during a +//! migration. Since (at this writing) the only substitutable components are +//! backends, this is easily done by using component IDs as backend names, as +//! described above. + +use std::collections::{BTreeMap, HashMap}; + +use crate::app::instance::InstanceRegisterReason; +use crate::cidata::InstanceCiData; + +use nexus_db_queries::db; +use nexus_types::identity::Resource; +use omicron_common::api::external::Error; +use omicron_common::api::internal::shared::NetworkInterface; +use sled_agent_client::types::{ + BlobStorageBackend, Board, BootOrderEntry, BootSettings, Chipset, + ComponentV0, CrucibleStorageBackend, I440Fx, InstanceSpecV0, NvmeDisk, + PciPath, QemuPvpanic, SerialPort, SerialPortNumber, SpecKey, VirtioDisk, + VirtioNetworkBackend, VirtioNic, VmmSpec, +}; +use uuid::Uuid; + +/// Constants and functions used to assign names to assorted VM components. +mod component_names { + pub(super) const COM1: &'static str = "com1"; + pub(super) const COM2: &'static str = "com2"; + pub(super) const COM3: &'static str = "com3"; + pub(super) const COM4: &'static str = "com4"; + pub(super) const PVPANIC: &'static str = "pvpanic"; + pub(super) const BOOT_SETTINGS: &'static str = "boot-settings"; + pub(super) const CLOUD_INIT_DEVICE: &'static str = "cloud-init-dev"; + pub(super) const CLOUD_INIT_BACKEND: &'static str = "cloud-init-backend"; + + /// Given an object ID, derives a name for the "device" half of the + /// device/backend component pair that describes that object. + pub(super) fn device_name_from_id(id: &uuid::Uuid) -> String { + format!("{id}:device") + } +} + +enum PciDeviceKind { + Disk, + Nic, + CloudInitDisk, +} + +impl std::fmt::Display for PciDeviceKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + Self::Disk => "disk", + Self::Nic => "network interface", + Self::CloudInitDisk => "cloud-init data disk", + } + ) + } +} + +/// Computes the PCI attachment point for a device of the given `kind` which +/// carries the supplied `logical_slot` in its database record. +/// +/// WARNING: Guests may write their virtual devices' PCI addresses to persistent +/// storage and expect that those devices will always appear in the same places +/// when the system is stopped and restarted. Changing these mappings for +/// existing instances may break them! +fn slot_to_pci_bdf( + logical_slot: u8, + kind: PciDeviceKind, +) -> Result { + // Use the mappings Propolis used when it was responsible for converting + // slot numbers to device numbers: NICs get device numbers 8 through 15, + // disks get 16 through 23, and the cloud-init disk is device 24. + let device = match kind { + PciDeviceKind::Disk if logical_slot < 8 => logical_slot + 0x10, + PciDeviceKind::Nic if logical_slot < 8 => logical_slot + 0x8, + PciDeviceKind::CloudInitDisk if logical_slot == 0 => 0x18, + _ => { + return Err(Error::invalid_value( + format!("{kind} with slot {logical_slot}"), + "slot number out of range for device", + )); + } + }; + + Ok(PciPath { bus: 0, device, function: 0 }) +} + +/// Generates a 20-byte NVMe device serial number from the bytes in a string +/// slice and pads the remaining bytes with zeroes. If the supplied slice is +/// more than 20 bytes, it is truncated. +/// +/// NOTE: Zero-padding serial numbers is non-compliant behavior per version 1.2 +/// of the NVMe spec (June 5, 2016), section 1.5. It is provided here for +/// compatibility with prior versions of Propolis that produced zero-padded +/// serial numbers from control plane-supplied disk names. Preserving +/// compatibility here is important because some guests may read an NVMe disk's +/// serial number to produce a logical "disk ID" that those guests (and their +/// users) may assume will remain stable for the lifetime of the disk. +pub fn zero_padded_nvme_serial_from_str(s: &str) -> [u8; 20] { + let mut sn = [0u8; 20]; + + let bytes_from_slice = sn.len().min(s.len()); + sn[..bytes_from_slice].copy_from_slice(&s.as_bytes()[..bytes_from_slice]); + sn +} + +/// Describes a Crucible-backed disk that should be added to an instance +/// specification. +#[derive(Debug)] +struct CrucibleDisk { + device_name: String, + device: ComponentV0, + backend: CrucibleStorageBackend, +} + +/// Stores a mapping from Nexus disk IDs to Crucible disk descriptors. This +/// allows the platform construction process to quickly determine the *device +/// name* for a disk with a given ID so that that name can be inserted into the +/// instance spec's boot settings. +struct DisksById(BTreeMap); + +impl DisksById { + /// Creates a disk list from an iterator over a set of disk and volume + /// records. + /// + /// The caller must ensure that the supplied `Volume`s have been checked out + /// (i.e., that their Crucible generation numbers are up-to-date) before + /// calling this function. + fn from_disks<'i>( + disks: impl Iterator, + ) -> Result { + let mut map = BTreeMap::new(); + for (disk, volume) in disks { + let slot = match disk.slot { + Some(s) => s.0, + None => { + return Err(Error::internal_error(&format!( + "disk {} is attached but has no PCI slot assignment", + disk.id() + ))); + } + }; + + let pci_path = slot_to_pci_bdf(slot, PciDeviceKind::Disk)?; + let device = ComponentV0::NvmeDisk(NvmeDisk { + backend_id: SpecKey::Uuid(disk.id()), + pci_path, + serial_number: zero_padded_nvme_serial_from_str( + disk.name().as_str(), + ), + }); + + let backend = CrucibleStorageBackend { + readonly: false, + request_json: volume.data().to_owned(), + }; + + let device_name = component_names::device_name_from_id(&disk.id()); + if map + .insert( + disk.id(), + CrucibleDisk { device_name, device, backend }, + ) + .is_some() + { + return Err(Error::internal_error(&format!( + "instance has multiple attached disks with ID {}", + disk.id() + ))); + } + } + + Ok(Self(map)) + } +} + +/// A list of named components to add to an instance's spec. +// +// This is a HashMap so that it can be moved directly into a sled-agent instance +// spec (Progenitor generates HashMaps when it needs a map type). +struct Components(HashMap); + +impl Default for Components { + /// Adds the default set of VM components that are expected to exist for all + /// Oxide VMs (serial ports attached to COM1-COM4 and a paravirtualized + /// panic device). + fn default() -> Self { + let map = [ + ( + component_names::COM1, + ComponentV0::SerialPort(SerialPort { + num: SerialPortNumber::Com1, + }), + ), + ( + component_names::COM2, + ComponentV0::SerialPort(SerialPort { + num: SerialPortNumber::Com2, + }), + ), + ( + component_names::COM3, + ComponentV0::SerialPort(SerialPort { + num: SerialPortNumber::Com3, + }), + ), + ( + component_names::COM4, + ComponentV0::SerialPort(SerialPort { + num: SerialPortNumber::Com4, + }), + ), + ( + component_names::PVPANIC, + ComponentV0::QemuPvpanic(QemuPvpanic { enable_isa: true }), + ), + ] + .into_iter() + .map(|(k, v)| (k.to_owned(), v)) + .collect(); + + Self(map) + } +} + +impl Components { + /// Adds a named component to this component list. Returns a 500 error if + /// the component name was already present in the list. + fn add( + &mut self, + key: String, + component: ComponentV0, + ) -> Result<(), Error> { + use std::collections::hash_map::Entry; + + // Component names are an implementation detail internal to this module, + // so they should always be unique. + match self.0.entry(key) { + Entry::Occupied(occupied_entry) => { + Err(Error::internal_error(&format!( + "duplicate instance spec component key: {}", + occupied_entry.key() + ))) + } + Entry::Vacant(vacant_entry) => { + vacant_entry.insert(component); + Ok(()) + } + } + } + + /// Adds the set of disks in the supplied disk list to this component + /// list. + fn add_disks(&mut self, disks: DisksById) -> Result<(), Error> { + // This operation will add a device and a backend for every disk in the + // input set. + self.0.reserve(disks.0.len() * 2); + for (id, CrucibleDisk { device_name, device, backend }) in + disks.0.into_iter() + { + self.add(device_name, device)?; + self.add( + id.to_string(), + ComponentV0::CrucibleStorageBackend(backend), + )?; + } + + Ok(()) + } + + /// Adds the supplied set of NICs to this component manifest. + fn add_nics(&mut self, nics: &[NetworkInterface]) -> Result<(), Error> { + // This operation will add a device and a backend for every NIC in the + // input slice. + self.0.reserve(nics.len() * 2); + for nic in nics.iter() { + let device_name = component_names::device_name_from_id(&nic.id); + let device = ComponentV0::VirtioNic(VirtioNic { + backend_id: SpecKey::Uuid(nic.id), + interface_id: nic.id, + pci_path: slot_to_pci_bdf(nic.slot, PciDeviceKind::Nic)?, + }); + + // Sled-agent creates OPTE ports during instance startup using its + // per-sled port allocator, so it needs to (and will) fill in the + // correct port name once one is assigned. + let backend = + ComponentV0::VirtioNetworkBackend(VirtioNetworkBackend { + vnic_name: "".to_string(), + }); + + // N.B. It's crucial to use the NIC ID as the backend name here so + // that sled-agent can correlate this component entry with the OPTE + // port it created for the guest network interface with this ID. + self.add(nic.id.to_string(), backend)?; + self.add(device_name, device)?; + } + + Ok(()) + } + + /// Adds a cloud-init disk to this component manifest. The disk will expose + /// the supplied `ssh_keys` to the guest. + fn add_cloud_init( + &mut self, + instance: &db::model::Instance, + ssh_keys: &[db::model::SshKey], + ) -> Result<(), Error> { + let keys: Vec = + ssh_keys.iter().map(|k| k.public_key.clone()).collect(); + let base64 = base64::Engine::encode( + &base64::engine::general_purpose::STANDARD, + instance.generate_cidata(&keys)?, + ); + + let device = ComponentV0::VirtioDisk(VirtioDisk { + backend_id: SpecKey::Name( + component_names::CLOUD_INIT_BACKEND.to_string(), + ), + pci_path: slot_to_pci_bdf(0, PciDeviceKind::CloudInitDisk) + .expect("slot 0 is always valid for cloud-init disks"), + }); + + let backend = ComponentV0::BlobStorageBackend(BlobStorageBackend { + base64, + readonly: true, + }); + + self.add(component_names::CLOUD_INIT_DEVICE.to_string(), device)?; + self.add(component_names::CLOUD_INIT_BACKEND.to_string(), backend)?; + + Ok(()) + } +} + +impl super::Nexus { + /// Generates a Propolis VM specificaation from the supplied database + /// records. + pub(crate) async fn generate_vmm_spec( + &self, + reason: &InstanceRegisterReason, + instance: &db::model::Instance, + disks: &[db::model::Disk], + nics: &[NetworkInterface], + ssh_keys: &[db::model::SshKey], + ) -> Result { + let cpus = u8::try_from(instance.ncpus.0.0).map_err(|c| { + Error::invalid_value( + c.to_string(), + "failed to convert instance CPU count to a u8", + ) + })?; + + let mut components = Components::default(); + + // Get the volume information needed to fill in the disks' backends' + // volume construction requests. Calling `volume_checkout` bumps + // the volumes' generation numbers. + let mut volumes = Vec::with_capacity(disks.len()); + for disk in disks { + use db::datastore::VolumeCheckoutReason; + let volume = self + .db_datastore + .volume_checkout( + disk.volume_id(), + match reason { + InstanceRegisterReason::Start { vmm_id } => { + VolumeCheckoutReason::InstanceStart { + vmm_id: *vmm_id, + } + } + InstanceRegisterReason::Migrate { + vmm_id, + target_vmm_id, + } => VolumeCheckoutReason::InstanceMigrate { + vmm_id: *vmm_id, + target_vmm_id: *target_vmm_id, + }, + }, + ) + .await?; + + volumes.push(volume); + } + + let disks = DisksById::from_disks(disks.iter().zip(volumes.iter()))?; + + // Add the instance's boot settings. Propolis expects boot order entries + // that specify disks to refer to the *device* components of those + // disks, not the backend components, so use the disk map to get this + // module's selected device name for the appropriate disk. + if let Some(boot_disk_id) = instance.boot_disk_id { + if let Some(disk) = disks.0.get(&boot_disk_id) { + let entry = BootOrderEntry { + id: SpecKey::Name(disk.device_name.clone()), + }; + + components.add( + component_names::BOOT_SETTINGS.to_owned(), + ComponentV0::BootSettings(BootSettings { + order: vec![entry], + }), + )?; + } else { + return Err(Error::internal_error(&format!( + "instance's boot disk {boot_disk_id} is not attached" + ))); + } + } + + components.add_disks(disks)?; + components.add_nics(nics)?; + components.add_cloud_init(instance, ssh_keys)?; + + let spec = InstanceSpecV0 { + board: Board { + chipset: Chipset::I440Fx(I440Fx { enable_pcie: false }), + cpuid: None, + cpus, + guest_hv_interface: None, + memory_mb: instance.memory.to_whole_mebibytes(), + }, + components: components.0, + }; + + Ok(VmmSpec(spec)) + } +} diff --git a/nexus/src/app/mod.rs b/nexus/src/app/mod.rs index c8bad4a2098..1291f369c73 100644 --- a/nexus/src/app/mod.rs +++ b/nexus/src/app/mod.rs @@ -69,6 +69,7 @@ mod iam; mod image; mod instance; mod instance_network; +mod instance_platform; mod internet_gateway; mod ip_pool; mod lldp; diff --git a/openapi/sled-agent.json b/openapi/sled-agent.json index d49fa77e879..e63c9ae81d4 100644 --- a/openapi/sled-agent.json +++ b/openapi/sled-agent.json @@ -2585,6 +2585,74 @@ "port" ] }, + "BlobStorageBackend": { + "description": "A storage backend for a disk whose initial contents are given explicitly by the specification.", + "type": "object", + "properties": { + "base64": { + "description": "The disk's initial contents, encoded as a base64 string.", + "type": "string" + }, + "readonly": { + "description": "Indicates whether the storage is read-only.", + "type": "boolean" + } + }, + "required": [ + "base64", + "readonly" + ], + "additionalProperties": false + }, + "Board": { + "description": "A VM's mainboard.", + "type": "object", + "properties": { + "chipset": { + "description": "The chipset to expose to guest software.", + "allOf": [ + { + "$ref": "#/components/schemas/Chipset" + } + ] + }, + "cpuid": { + "nullable": true, + "description": "The CPUID values to expose to the guest. If `None`, bhyve will derive default values from the host's CPUID values.", + "allOf": [ + { + "$ref": "#/components/schemas/Cpuid" + } + ] + }, + "cpus": { + "description": "The number of virtual logical processors attached to this VM.", + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "guest_hv_interface": { + "description": "The hypervisor platform to expose to the guest. The default is a bhyve-compatible interface with no additional features.\n\nFor compatibility with older versions of Propolis, this field is only serialized if it specifies a non-default interface.", + "allOf": [ + { + "$ref": "#/components/schemas/GuestHypervisorInterface" + } + ] + }, + "memory_mb": { + "description": "The amount of guest RAM attached to this VM.", + "type": "integer", + "format": "uint64", + "minimum": 0 + } + }, + "required": [ + "chipset", + "cpus", + "memory_mb" + ], + "additionalProperties": false + }, "BootDiskOsWriteProgress": { "description": "Current progress of an OS image being written to disk.", "oneOf": [ @@ -2741,6 +2809,40 @@ } ] }, + "BootOrderEntry": { + "description": "An entry in the boot order stored in a [`BootSettings`] component.", + "type": "object", + "properties": { + "id": { + "description": "The ID of another component in the spec that Propolis should try to boot from.\n\nCurrently, only disk device components are supported.", + "allOf": [ + { + "$ref": "#/components/schemas/SpecKey" + } + ] + } + }, + "required": [ + "id" + ] + }, + "BootSettings": { + "description": "Settings supplied to the guest's firmware image that specify the order in which it should consider its options when selecting a device to try to boot from.", + "type": "object", + "properties": { + "order": { + "description": "An ordered list of components to attempt to boot from.", + "type": "array", + "items": { + "$ref": "#/components/schemas/BootOrderEntry" + } + } + }, + "required": [ + "order" + ], + "additionalProperties": false + }, "BootstoreStatus": { "type": "object", "properties": { @@ -2830,6 +2932,31 @@ "format": "uint64", "minimum": 0 }, + "Chipset": { + "description": "A kind of virtual chipset.", + "oneOf": [ + { + "description": "An Intel 440FX-compatible chipset.", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "i440_fx" + ] + }, + "value": { + "$ref": "#/components/schemas/I440Fx" + } + }, + "required": [ + "type", + "value" + ], + "additionalProperties": false + } + ] + }, "CleanupContext": { "description": "Context provided for the zone bundle cleanup task.", "type": "object", @@ -2926,6 +3053,333 @@ } ] }, + "ComponentV0": { + "oneOf": [ + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/VirtioDisk" + }, + "type": { + "type": "string", + "enum": [ + "virtio_disk" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/NvmeDisk" + }, + "type": { + "type": "string", + "enum": [ + "nvme_disk" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/VirtioNic" + }, + "type": { + "type": "string", + "enum": [ + "virtio_nic" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/SerialPort" + }, + "type": { + "type": "string", + "enum": [ + "serial_port" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/PciPciBridge" + }, + "type": { + "type": "string", + "enum": [ + "pci_pci_bridge" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/QemuPvpanic" + }, + "type": { + "type": "string", + "enum": [ + "qemu_pvpanic" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/BootSettings" + }, + "type": { + "type": "string", + "enum": [ + "boot_settings" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/SoftNpuPciPort" + }, + "type": { + "type": "string", + "enum": [ + "soft_npu_pci_port" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/SoftNpuPort" + }, + "type": { + "type": "string", + "enum": [ + "soft_npu_port" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/SoftNpuP9" + }, + "type": { + "type": "string", + "enum": [ + "soft_npu_p9" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/P9fs" + }, + "type": { + "type": "string", + "enum": [ + "p9fs" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/MigrationFailureInjector" + }, + "type": { + "type": "string", + "enum": [ + "migration_failure_injector" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/CrucibleStorageBackend" + }, + "type": { + "type": "string", + "enum": [ + "crucible_storage_backend" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/FileStorageBackend" + }, + "type": { + "type": "string", + "enum": [ + "file_storage_backend" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/BlobStorageBackend" + }, + "type": { + "type": "string", + "enum": [ + "blob_storage_backend" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/VirtioNetworkBackend" + }, + "type": { + "type": "string", + "enum": [ + "virtio_network_backend" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/DlpiNetworkBackend" + }, + "type": { + "type": "string", + "enum": [ + "dlpi_network_backend" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + } + ] + }, "CompressionAlgorithm": { "oneOf": [ { @@ -3032,6 +3486,110 @@ } ] }, + "Cpuid": { + "description": "A set of CPUID values to expose to a guest.", + "type": "object", + "properties": { + "entries": { + "description": "A list of CPUID leaves/subleaves and their associated values.\n\nPropolis servers require that each entry's `leaf` be unique and that it falls in either the \"standard\" (0 to 0xFFFF) or \"extended\" (0x8000_0000 to 0x8000_FFFF) function ranges, since these are the only valid input ranges currently defined by Intel and AMD. See the Intel 64 and IA-32 Architectures Software Developer's Manual (June 2024) Table 3-17 and the AMD64 Architecture Programmer's Manual (March 2024) Volume 3's documentation of the CPUID instruction.", + "type": "array", + "items": { + "$ref": "#/components/schemas/CpuidEntry" + } + }, + "vendor": { + "description": "The CPU vendor to emulate.\n\nCPUID leaves in the extended range (0x8000_0000 to 0x8000_FFFF) have vendor-defined semantics. Propolis uses this value to determine these semantics when deciding whether it needs to specialize the supplied template values for these leaves.", + "allOf": [ + { + "$ref": "#/components/schemas/CpuidVendor" + } + ] + } + }, + "required": [ + "entries", + "vendor" + ], + "additionalProperties": false + }, + "CpuidEntry": { + "description": "A full description of a CPUID leaf/subleaf and the values it produces.", + "type": "object", + "properties": { + "eax": { + "description": "The value to return in eax.", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "ebx": { + "description": "The value to return in ebx.", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "ecx": { + "description": "The value to return in ecx.", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "edx": { + "description": "The value to return in edx.", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "leaf": { + "description": "The leaf (function) number for this entry.", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "subleaf": { + "nullable": true, + "description": "The subleaf (index) number for this entry, if it uses subleaves.", + "type": "integer", + "format": "uint32", + "minimum": 0 + } + }, + "required": [ + "eax", + "ebx", + "ecx", + "edx", + "leaf" + ], + "additionalProperties": false + }, + "CpuidVendor": { + "description": "A CPU vendor to use when interpreting the meanings of CPUID leaves in the extended ID range (0x80000000 to 0x8000FFFF).", + "type": "string", + "enum": [ + "amd", + "intel" + ] + }, + "CrucibleStorageBackend": { + "description": "A Crucible storage backend.", + "type": "object", + "properties": { + "readonly": { + "description": "Indicates whether the storage is read-only.", + "type": "boolean" + }, + "request_json": { + "description": "A serialized `[crucible_client_types::VolumeConstructionRequest]`. This is stored in serialized form so that breaking changes to the definition of a `VolumeConstructionRequest` do not inadvertently break instance spec deserialization.\n\nWhen using a spec to initialize a new instance, the spec author must ensure this request is well-formed and can be deserialized by the version of `crucible_client_types` used by the target Propolis.", + "type": "string" + } + }, + "required": [ + "readonly", + "request_json" + ], + "additionalProperties": false + }, "DatasetConfig": { "description": "Configuration information necessary to request a single dataset.\n\nThese datasets are tracked directly by Nexus.", "type": "object", @@ -3635,6 +4193,20 @@ "M2" ] }, + "DlpiNetworkBackend": { + "description": "A network backend associated with a DLPI VNIC on the host.", + "type": "object", + "properties": { + "vnic_name": { + "description": "The name of the VNIC to use as a backend.", + "type": "string" + } + }, + "required": [ + "vnic_name" + ], + "additionalProperties": false + }, "Duration": { "type": "object", "properties": { @@ -3759,12 +4331,85 @@ "mappings" ] }, + "FileStorageBackend": { + "description": "A storage backend backed by a file in the host system's file system.", + "type": "object", + "properties": { + "path": { + "description": "A path to a file that backs a disk.", + "type": "string" + }, + "readonly": { + "description": "Indicates whether the storage is read-only.", + "type": "boolean" + } + }, + "required": [ + "path", + "readonly" + ], + "additionalProperties": false + }, "Generation": { "description": "Generation numbers stored in the database, used for optimistic concurrency control", "type": "integer", "format": "uint64", "minimum": 0 }, + "GuestHypervisorInterface": { + "description": "A hypervisor interface to expose to the guest.", + "oneOf": [ + { + "description": "Expose a bhyve-like interface (\"bhyve bhyve \" as the hypervisor ID in leaf 0x4000_0000 and no additional leaves or features).", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "bhyve" + ] + } + }, + "required": [ + "type" + ], + "additionalProperties": false + }, + { + "description": "Expose a Hyper-V-compatible hypervisor interface with the supplied features enabled.", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "hyper_v" + ] + }, + "value": { + "type": "object", + "properties": { + "features": { + "type": "array", + "items": { + "$ref": "#/components/schemas/HyperVFeatureFlag" + }, + "uniqueItems": true + } + }, + "required": [ + "features" + ], + "additionalProperties": false + } + }, + "required": [ + "type", + "value" + ], + "additionalProperties": false + } + ] + }, "GzipLevel": { "type": "integer", "format": "uint8", @@ -3855,6 +4500,27 @@ "minLength": 1, "maxLength": 253 }, + "HyperVFeatureFlag": { + "description": "Flags that enable \"simple\" Hyper-V enlightenments that require no feature-specific configuration.", + "type": "string", + "enum": [ + "reference_tsc" + ] + }, + "I440Fx": { + "description": "An Intel 440FX-compatible chipset.", + "type": "object", + "properties": { + "enable_pcie": { + "description": "Specifies whether the chipset should allow PCI configuration space to be accessed through the PCIe extended configuration mechanism.", + "type": "boolean" + } + }, + "required": [ + "enable_pcie" + ], + "additionalProperties": false + }, "ImportExportPolicy": { "description": "Define policy relating to the import and export of prefixes from a BGP peer.", "oneOf": [ @@ -3896,82 +4562,23 @@ } ] }, - "InstanceBootSettings": { - "description": "Configures how an instance is told to try to boot.", - "type": "object", - "properties": { - "order": { - "description": "Propolis should tell guest firmware to try to boot from devices in this order.", - "type": "array", - "items": { - "type": "string", - "format": "uuid" - } - } - }, - "required": [ - "order" - ] - }, - "InstanceCpuCount": { - "description": "The number of CPUs in an Instance", - "type": "integer", - "format": "uint16", - "minimum": 0 - }, - "InstanceDisk": { - "description": "A request to attach a disk to an instance.", - "type": "object", - "properties": { - "disk_id": { - "description": "The disk's UUID.", - "type": "string", - "format": "uuid" - }, - "name": { - "description": "The disk's name, used to generate the serial number for the virtual disk exposed to the guest.", - "type": "string" - }, - "read_only": { - "description": "True if the disk is read-only.", - "type": "boolean" - }, - "slot": { - "description": "The logical slot number assigned to the disk in its database record.", - "type": "integer", - "format": "uint8", - "minimum": 0 - }, - "vcr_json": { - "description": "A JSON representation of the Crucible volume construction request for this attachment.", - "type": "string" - } - }, - "required": [ - "disk_id", - "name", - "read_only", - "slot", - "vcr_json" - ] - }, "InstanceEnsureBody": { "description": "The body of a request to ensure that a instance and VMM are known to a sled agent.", "type": "object", "properties": { - "hardware": { - "description": "A description of the instance's virtual hardware and the initial runtime state this sled agent should store for this incarnation of the instance.", + "instance_id": { + "description": "The ID of the instance for which this VMM is being created.", "allOf": [ { - "$ref": "#/components/schemas/InstanceHardware" + "$ref": "#/components/schemas/TypedUuidForInstanceKind" } ] }, - "instance_id": { - "description": "The ID of the instance for which this VMM is being created.", + "local_config": { + "description": "Information about the sled-local configuration that needs to be established to make the VM's virtual hardware fully functional.", "allOf": [ { - "$ref": "#/components/schemas/TypedUuidForInstanceKind" + "$ref": "#/components/schemas/InstanceSledLocalConfig" } ] }, @@ -4000,14 +4607,23 @@ "$ref": "#/components/schemas/VmmRuntimeState" } ] + }, + "vmm_spec": { + "description": "The virtual hardware configuration this virtual machine should have when it is started.", + "allOf": [ + { + "$ref": "#/components/schemas/VmmSpec" + } + ] } }, "required": [ - "hardware", "instance_id", + "local_config", "metadata", "propolis_addr", - "vmm_runtime" + "vmm_runtime", + "vmm_spec" ] }, "InstanceExternalIpBody": { @@ -4053,31 +4669,44 @@ } ] }, - "InstanceHardware": { - "description": "Describes the instance hardware.", + "InstanceMetadata": { + "description": "Metadata used to track statistics about an instance.", "type": "object", "properties": { - "boot_settings": { - "nullable": true, - "allOf": [ - { - "$ref": "#/components/schemas/InstanceBootSettings" - } - ] + "project_id": { + "type": "string", + "format": "uuid" }, - "cloud_init_bytes": { - "nullable": true, + "silo_id": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "project_id", + "silo_id" + ] + }, + "InstanceMigrationTargetParams": { + "description": "Parameters used when directing Propolis to initialize itself via live migration.", + "type": "object", + "properties": { + "src_propolis_addr": { + "description": "The address of the Propolis server that will serve as the migration source.", "type": "string" - }, + } + }, + "required": [ + "src_propolis_addr" + ] + }, + "InstanceSledLocalConfig": { + "description": "Describes sled-local configuration that a sled-agent must establish to make the instance's virtual hardware fully functional.", + "type": "object", + "properties": { "dhcp_config": { "$ref": "#/components/schemas/DhcpConfig" }, - "disks": { - "type": "array", - "items": { - "$ref": "#/components/schemas/InstanceDisk" - } - }, "ephemeral_ip": { "nullable": true, "description": "Zero or more external IP addresses (either floating or ephemeral), provided to an instance to allow inbound connectivity.", @@ -4097,84 +4726,46 @@ "format": "ip" } }, + "hostname": { + "$ref": "#/components/schemas/Hostname" + }, "nics": { "type": "array", "items": { "$ref": "#/components/schemas/NetworkInterface" - } - }, - "properties": { - "$ref": "#/components/schemas/InstanceProperties" - }, - "source_nat": { - "$ref": "#/components/schemas/SourceNatConfig" - } - }, - "required": [ - "dhcp_config", - "disks", - "firewall_rules", - "floating_ips", - "nics", - "properties", - "source_nat" - ] - }, - "InstanceMetadata": { - "description": "Metadata used to track statistics about an instance.", - "type": "object", - "properties": { - "project_id": { - "type": "string", - "format": "uuid" + } }, - "silo_id": { - "type": "string", - "format": "uuid" - } - }, - "required": [ - "project_id", - "silo_id" - ] - }, - "InstanceMigrationTargetParams": { - "description": "Parameters used when directing Propolis to initialize itself via live migration.", - "type": "object", - "properties": { - "src_propolis_addr": { - "description": "The address of the Propolis server that will serve as the migration source.", - "type": "string" + "source_nat": { + "$ref": "#/components/schemas/SourceNatConfig" } }, "required": [ - "src_propolis_addr" + "dhcp_config", + "firewall_rules", + "floating_ips", + "hostname", + "nics", + "source_nat" ] }, - "InstanceProperties": { - "description": "The \"static\" properties of an instance: information about the instance that doesn't change while the instance is running.", + "InstanceSpecV0": { "type": "object", "properties": { - "hostname": { - "description": "RFC1035-compliant hostname for the instance.", - "allOf": [ - { - "$ref": "#/components/schemas/Hostname" - } - ] + "board": { + "$ref": "#/components/schemas/Board" }, - "memory": { - "$ref": "#/components/schemas/ByteCount" - }, - "ncpus": { - "$ref": "#/components/schemas/InstanceCpuCount" + "components": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/ComponentV0" + } } }, "required": [ - "hostname", - "memory", - "ncpus" - ] + "board", + "components" + ], + "additionalProperties": false }, "InternetGatewayRouterTarget": { "description": "An Internet Gateway router target.", @@ -4553,6 +5144,29 @@ "minLength": 5, "maxLength": 17 }, + "MigrationFailureInjector": { + "description": "Describes a synthetic device that registers for VM lifecycle notifications and returns errors during attempts to migrate.\n\nThis is only supported by Propolis servers compiled with the `failure-injection` feature.", + "type": "object", + "properties": { + "fail_exports": { + "description": "The number of times this device should fail requests to export state.", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "fail_imports": { + "description": "The number of times this device should fail requests to import state.", + "type": "integer", + "format": "uint32", + "minimum": 0 + } + }, + "required": [ + "fail_exports", + "fail_imports" + ], + "additionalProperties": false + }, "MigrationRuntimeState": { "description": "An update from a sled regarding the state of a migration, indicating the role of the VMM whose migration state was updated.", "type": "object", @@ -4741,6 +5355,45 @@ } ] }, + "NvmeDisk": { + "description": "A disk that presents an NVMe interface to the guest.", + "type": "object", + "properties": { + "backend_id": { + "description": "The name of the disk's backend component.", + "allOf": [ + { + "$ref": "#/components/schemas/SpecKey" + } + ] + }, + "pci_path": { + "description": "The PCI bus/device/function at which this disk should be attached.", + "allOf": [ + { + "$ref": "#/components/schemas/PciPath" + } + ] + }, + "serial_number": { + "description": "The serial number to return in response to an NVMe Identify Controller command.", + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "minItems": 20, + "maxItems": 20 + } + }, + "required": [ + "backend_id", + "pci_path", + "serial_number" + ], + "additionalProperties": false + }, "OmicronPhysicalDiskConfig": { "type": "object", "properties": { @@ -5288,6 +5941,92 @@ "zones" ] }, + "P9fs": { + "description": "Describes a filesystem to expose through a P9 device.\n\nThis is only supported by Propolis servers compiled with the `falcon` feature.", + "type": "object", + "properties": { + "chunk_size": { + "description": "The chunk size to use in the 9P protocol. Vanilla Helios images should use 8192. Falcon Helios base images and Linux can use up to 65536.", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "pci_path": { + "description": "The PCI path at which to attach the guest to this P9 filesystem.", + "allOf": [ + { + "$ref": "#/components/schemas/PciPath" + } + ] + }, + "source": { + "description": "The host source path to mount into the guest.", + "type": "string" + }, + "target": { + "description": "The 9P target filesystem tag.", + "type": "string" + } + }, + "required": [ + "chunk_size", + "pci_path", + "source", + "target" + ], + "additionalProperties": false + }, + "PciPath": { + "description": "A PCI bus/device/function tuple.", + "type": "object", + "properties": { + "bus": { + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "device": { + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "function": { + "type": "integer", + "format": "uint8", + "minimum": 0 + } + }, + "required": [ + "bus", + "device", + "function" + ] + }, + "PciPciBridge": { + "description": "A PCI-PCI bridge.", + "type": "object", + "properties": { + "downstream_bus": { + "description": "The logical bus number of this bridge's downstream bus. Other devices may use this bus number in their PCI paths to indicate they should be attached to this bridge's bus.", + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "pci_path": { + "description": "The PCI path at which to attach this bridge.", + "allOf": [ + { + "$ref": "#/components/schemas/PciPath" + } + ] + } + }, + "required": [ + "downstream_bus", + "pci_path" + ], + "additionalProperties": false + }, "PortConfigV2": { "type": "object", "properties": { @@ -5426,6 +6165,19 @@ "minItems": 2, "maxItems": 2 }, + "QemuPvpanic": { + "type": "object", + "properties": { + "enable_isa": { + "description": "Enable the QEMU PVPANIC ISA bus device (I/O port 0x505).", + "type": "boolean" + } + }, + "required": [ + "enable_isa" + ], + "additionalProperties": false + }, "RackNetworkConfigV2": { "description": "Initial network configuration", "type": "object", @@ -5776,6 +6528,34 @@ "version" ] }, + "SerialPort": { + "description": "A serial port device.", + "type": "object", + "properties": { + "num": { + "description": "The serial port number for this port.", + "allOf": [ + { + "$ref": "#/components/schemas/SerialPortNumber" + } + ] + } + }, + "required": [ + "num" + ], + "additionalProperties": false + }, + "SerialPortNumber": { + "description": "A serial port identifier, which determines what I/O ports a guest can use to access a port.", + "type": "string", + "enum": [ + "com1", + "com2", + "com3", + "com4" + ] + }, "SledDiagnosticsQueryOutput": { "oneOf": [ { @@ -5929,6 +6709,65 @@ "vmm_state" ] }, + "SoftNpuP9": { + "description": "Describes a PCI device that shares host files with the guest using the P9 protocol.\n\nThis is only supported by Propolis servers compiled with the `falcon` feature.", + "type": "object", + "properties": { + "pci_path": { + "description": "The PCI path at which to attach the guest to this port.", + "allOf": [ + { + "$ref": "#/components/schemas/PciPath" + } + ] + } + }, + "required": [ + "pci_path" + ], + "additionalProperties": false + }, + "SoftNpuPciPort": { + "description": "Describes a SoftNPU PCI device.\n\nThis is only supported by Propolis servers compiled with the `falcon` feature.", + "type": "object", + "properties": { + "pci_path": { + "description": "The PCI path at which to attach the guest to this port.", + "allOf": [ + { + "$ref": "#/components/schemas/PciPath" + } + ] + } + }, + "required": [ + "pci_path" + ], + "additionalProperties": false + }, + "SoftNpuPort": { + "description": "Describes a port in a SoftNPU emulated ASIC.\n\nThis is only supported by Propolis servers compiled with the `falcon` feature.", + "type": "object", + "properties": { + "backend_id": { + "description": "The name of the port's associated DLPI backend.", + "allOf": [ + { + "$ref": "#/components/schemas/SpecKey" + } + ] + }, + "link_name": { + "description": "The data link name for this port.", + "type": "string" + } + }, + "required": [ + "backend_id", + "link_name" + ], + "additionalProperties": false + }, "SourceNatConfig": { "description": "An IP address and port range used for source NAT, i.e., making outbound network connections from guests or services.", "type": "object", @@ -5957,6 +6796,28 @@ "last_port" ] }, + "SpecKey": { + "description": "A key identifying a component in an instance spec.", + "oneOf": [ + { + "title": "uuid", + "allOf": [ + { + "type": "string", + "format": "uuid" + } + ] + }, + { + "title": "name", + "allOf": [ + { + "type": "string" + } + ] + } + ] + }, "StartSledAgentRequest": { "description": "Configuration information for launching a Sled Agent.", "type": "object", @@ -6214,6 +7075,80 @@ "address" ] }, + "VirtioDisk": { + "description": "A disk that presents a virtio-block interface to the guest.", + "type": "object", + "properties": { + "backend_id": { + "description": "The name of the disk's backend component.", + "allOf": [ + { + "$ref": "#/components/schemas/SpecKey" + } + ] + }, + "pci_path": { + "description": "The PCI bus/device/function at which this disk should be attached.", + "allOf": [ + { + "$ref": "#/components/schemas/PciPath" + } + ] + } + }, + "required": [ + "backend_id", + "pci_path" + ], + "additionalProperties": false + }, + "VirtioNetworkBackend": { + "description": "A network backend associated with a virtio-net (viona) VNIC on the host.", + "type": "object", + "properties": { + "vnic_name": { + "description": "The name of the viona VNIC to use as a backend.", + "type": "string" + } + }, + "required": [ + "vnic_name" + ], + "additionalProperties": false + }, + "VirtioNic": { + "description": "A network card that presents a virtio-net interface to the guest.", + "type": "object", + "properties": { + "backend_id": { + "description": "The name of the device's backend.", + "allOf": [ + { + "$ref": "#/components/schemas/SpecKey" + } + ] + }, + "interface_id": { + "description": "A caller-defined correlation identifier for this interface. If Propolis is configured to collect network interface kstats in its Oximeter metrics, the metric series for this interface will be associated with this identifier.", + "type": "string", + "format": "uuid" + }, + "pci_path": { + "description": "The PCI path at which to attach this device.", + "allOf": [ + { + "$ref": "#/components/schemas/PciPath" + } + ] + } + }, + "required": [ + "backend_id", + "interface_id", + "pci_path" + ], + "additionalProperties": false + }, "VirtualNetworkInterfaceHost": { "description": "A mapping from a virtual NIC to a physical host", "type": "object", @@ -6328,6 +7263,14 @@ "time_updated" ] }, + "VmmSpec": { + "description": "Specifies the virtual hardware configuration of a new Propolis VMM in the form of a Propolis instance specification.\n\nSled-agent expects that when an instance spec is provided alongside an `InstanceSledLocalConfig` to initialize a new instance, the NIC IDs in that config's network interface list will match the IDs of the virtio network backends in the instance spec.", + "allOf": [ + { + "$ref": "#/components/schemas/InstanceSpecV0" + } + ] + }, "VmmState": { "description": "One of the states that a VMM can be in.", "oneOf": [ diff --git a/package-manifest.toml b/package-manifest.toml index 9ef26bf7bf6..1f058a32b83 100644 --- a/package-manifest.toml +++ b/package-manifest.toml @@ -579,10 +579,10 @@ only_for_targets.image = "standard" # 3. Use source.type = "manual" instead of "prebuilt" source.type = "prebuilt" source.repo = "crucible" -source.commit = "da3cf198a0e000bb89efc3a1c77d7ba09340a600" +source.commit = "45801597f410685015ac2704d044919a41e3ff75" # The SHA256 digest is automatically posted to: # https://buildomat.eng.oxide.computer/public/file/oxidecomputer/crucible/image//crucible.sha256.txt -source.sha256 = "9beaa26906b6259a32a5270e45052c2242526d24896048e9c9599f6baead7ba6" +source.sha256 = "bbb971dd03a7ca90e44ee039e9863c36f52e7fb40a9c67eb3ba0df1062371fec" output.type = "zone" output.intermediate_only = true @@ -591,10 +591,10 @@ service_name = "crucible_pantry_prebuilt" only_for_targets.image = "standard" source.type = "prebuilt" source.repo = "crucible" -source.commit = "da3cf198a0e000bb89efc3a1c77d7ba09340a600" +source.commit = "45801597f410685015ac2704d044919a41e3ff75" # The SHA256 digest is automatically posted to: # https://buildomat.eng.oxide.computer/public/file/oxidecomputer/crucible/image//crucible-pantry.sha256.txt -source.sha256 = "dcef0d2c87a518b41a82458f296452c40f3f030359b36b769c7f505980b1442d" +source.sha256 = "564980b6879aa9cc6bf175083ed62cc1fdab9c4b871b55c336ac30c660f14dd8" output.type = "zone" output.intermediate_only = true @@ -608,10 +608,10 @@ service_name = "crucible_dtrace" only_for_targets.image = "standard" source.type = "prebuilt" source.repo = "crucible" -source.commit = "da3cf198a0e000bb89efc3a1c77d7ba09340a600" +source.commit = "45801597f410685015ac2704d044919a41e3ff75" # The SHA256 digest is automatically posted to: # https://buildomat.eng.oxide.computer/public/file/oxidecomputer/crucible/image//crucible-dtrace.sha256.txt -source.sha256 = "5c1de2b9db4df3165f58848d73556cc83a1a8dfa9c819ad632bda20d1a88c1cd" +source.sha256 = "15349c177d50776fa16817d585582f7dcca8627d5306d72a7798db04b5de5839" output.type = "tarball" # Refer to @@ -622,10 +622,10 @@ service_name = "propolis-server" only_for_targets.image = "standard" source.type = "prebuilt" source.repo = "propolis" -source.commit = "e5c85d84b0a51803caffb335a1063612edb02f6d" +source.commit = "060a204d91e401a368c700a09d24510b7cd2b0e4" # The SHA256 digest is automatically posted to: # https://buildomat.eng.oxide.computer/public/file/oxidecomputer/propolis/image//propolis-server.sha256.txt -source.sha256 = "4b9edf5ce8c5bd0d622305fd5a267ab976f2a7e14f11231ba4bd2682735c2f38" +source.sha256 = "1a86f30bf2078d24326cbc7d999519f9669c123e072464468007fb72f8fe07d9" output.type = "zone" [package.mg-ddm-gz] diff --git a/sled-agent/src/instance.rs b/sled-agent/src/instance.rs index 62a1e29e69a..161e477498a 100644 --- a/sled-agent/src/instance.rs +++ b/sled-agent/src/instance.rs @@ -21,7 +21,6 @@ use illumos_utils::opte::{DhcpCfg, PortCreateParams, PortManager}; use illumos_utils::running_zone::{RunningZone, ZoneBuilderFactory}; use illumos_utils::zone::PROPOLIS_ZONE_PREFIX; use illumos_utils::zpool::ZpoolOrRamdisk; -use omicron_common::NoDebug; use omicron_common::api::internal::nexus::{SledVmmState, VmmRuntimeState}; use omicron_common::api::internal::shared::{ NetworkInterface, ResolvedVpcFirewallRule, SledIdentifiers, SourceNatConfig, @@ -34,6 +33,7 @@ use omicron_uuid_kinds::{ }; use propolis_api_types::ErrorCode as PropolisErrorCode; use propolis_client::Client as PropolisClient; +use propolis_client::instance_spec::{ComponentV0, SpecKey}; use rand::SeedableRng; use rand::prelude::IteratorRandom; use sled_agent_types::instance::*; @@ -76,6 +76,9 @@ pub enum Error { #[error("Failure during migration: {0}")] Migration(anyhow::Error), + #[error("requested NIC {0} has no virtio network backend in Propolis spec")] + NicNotInPropolisSpec(Uuid), + #[error(transparent)] ZoneCommand(#[from] illumos_utils::running_zone::RunCommandError), @@ -496,8 +499,6 @@ struct InstanceRunner { // Properties visible to Propolis properties: propolis_client::types::InstanceProperties, - vcpus: u8, - memory_mib: u64, // The ID of the Propolis server (and zone) running this instance propolis_id: PropolisUuid, @@ -505,6 +506,8 @@ struct InstanceRunner { // The socket address of the Propolis server running this instance propolis_addr: SocketAddr, + propolis_spec: VmmSpec, + // NIC-related properties vnic_allocator: VnicAllocator, @@ -520,11 +523,6 @@ struct InstanceRunner { firewall_rules: Vec, dhcp_config: DhcpCfg, - // Disk related properties - requested_disks: Vec, - boot_settings: Option, - cloud_init_bytes: Option>, - // Internal State management state: InstanceStates, running_state: Option, @@ -1031,161 +1029,21 @@ impl InstanceRunner { res.map(|_| ()) } - /// Sends an instance ensure request to this instance's Propolis, - /// constructing its configuration from the fields in `self` that describe - /// the instance's virtual hardware configuration. + /// Sends an instance ensure request to this instance's Propolis using the + /// information contained in this instance's Propolis VM spec. async fn send_propolis_instance_ensure( &self, client: &PropolisClient, running_zone: &RunningZone, migrate: Option, ) -> Result<(), Error> { - // A bit of history helps to explain the workings of the rest of this - // function. - // - // In the past, the Propolis API accepted an InstanceEnsureRequest - // struct that described a VM's hardware configuration at more or less - // the level of specificity used in Nexus's database. Callers passed - // this struct irrespective of whether they were starting a brand new VM - // migrating from an existing VM in some other Propolis. It was - // Propolis's job to convert any insufficiently-specific parameters - // passed in its API into the concrete details needed to set up VM - // components (e.g., converting device slot numbers into concrete PCI - // bus/device/function numbers). - // - // The Propolis VM creation API used below differs from this scheme in - // two important ways: - // - // 1. Callers are responsible for filling in all the details of a VM's - // configuration (e.g., choosing PCI BDFs for PCI devices). - // 2. During a live migration, the migration target inherits most of its - // configuration directly from the migration source. Propolis only - // allows clients to specify new configurations for the specific set - // of components that it expects to be reconfigured over a migration. - // These are described further below. - // - // This scheme aims to - // - // 1. prevent bugs where an instance can't migrate because the control - // plane has specified conflicting configurations to the source and - // target, and - // 2. maximize the updateability of VM configurations by allowing the - // control plane, which (at least in Nexus) is relatively easy to - // update, to make the rules about how Nexus instance configurations - // are realized in Propolis VMs. - // - // See propolis#804 for even more context on this API's design. - // - // A "virtual platform" is a set of rules that describe how to realize a - // Propolis VM configuration from a control plane instance description. - // The logic below encodes the "Oxide MVP" virtual platform that - // Propolis implicitly implemented in its legacy instance-ensure API. In - // the future there will be additional virtual platforms that may encode - // different rules and configuration options. - // - // TODO(#615): Eventually this logic should move to a robust virtual - // platform library in Nexus. - use propolis_client::{ - PciPath, SpecKey, - types::{ - BlobStorageBackend, Board, BootOrderEntry, BootSettings, - Chipset, ComponentV0, CrucibleStorageBackend, - InstanceInitializationMethod, NvmeDisk, QemuPvpanic, - ReplacementComponent, SerialPort, SerialPortNumber, VirtioDisk, - VirtioNetworkBackend, VirtioNic, - }, + instance_spec::ReplacementComponent, + types::InstanceInitializationMethod, }; - // Define some local helper structs for unpacking hardware descriptions - // into the types Propolis wants to see in its specifications. - struct DiskComponents { - id: Uuid, - device: NvmeDisk, - backend: CrucibleStorageBackend, - } - - struct NicComponents { - id: Uuid, - device: VirtioNic, - backend: VirtioNetworkBackend, - } - - // Assemble the list of NVMe disks associated with this instance. - let disks: Vec = self - .requested_disks - .iter() - .map(|disk| -> Result { - // One of the details Propolis asks clients to fill in is the - // serial number for each NVMe disk. It's important that this - // number remain stable because it can be written to an - // instance's nonvolatile EFI variables, specifically its boot - // order variables, which can be undercut if a serial number - // changes. - // - // The old version of the Propolis API generated serial numbers - // by taking the control plane disk name and padding it with - // zero bytes. Match this behavior here. - // - // Note that this scheme violates version 1.2.1 of the NVMe - // specification: section 1.5 says that string fields like this - // one should be left-justified and padded with spaces, not - // zeroes. Future versions of this logic may switch to this - // behavior. - let serial_number = - propolis_client::support::nvme_serial_from_str( - &disk.name, 0, - ); - - Ok(DiskComponents { - id: disk.disk_id, - device: NvmeDisk { - backend_id: SpecKey::Uuid(disk.disk_id), - // The old Propolis API added 16 to disk slot numbers to - // get their PCI device numbers. - pci_path: PciPath::new(0, disk.slot + 0x10, 0)?, - serial_number, - }, - backend: CrucibleStorageBackend { - readonly: disk.read_only, - request_json: disk.vcr_json.0.clone(), - }, - }) - }) - .collect::, _>>()?; - - // Next, assemble the list of guest NICs. - let nics: Vec = running_zone - .opte_ports() - .map(|port| -> Result { - let nic = self - .requested_nics - .iter() - // We expect to match NIC slots to OPTE port slots. - // Error out if we can't find a NIC for a port. - .find(|nic| nic.slot == port.slot()) - .ok_or(Error::Opte( - illumos_utils::opte::Error::NoNicforPort( - port.name().into(), - port.slot().into(), - ), - ))?; - - Ok(NicComponents { - id: nic.id, - device: VirtioNic { - backend_id: SpecKey::Uuid(nic.id), - interface_id: nic.id, - // The old Propolis API added 8 to NIC slot numbers to - // get their PCI device numbers. - pci_path: PciPath::new(0, nic.slot + 8, 0)?, - }, - backend: VirtioNetworkBackend { - vnic_name: port.name().to_string(), - }, - }) - }) - .collect::, _>>()?; + let mut spec = self.propolis_spec.clone(); + self.amend_propolis_network_backends(&mut spec, running_zone)?; // When a VM migrates, the migration target inherits most of its // configuration directly from the migration source. The exceptions are @@ -1226,30 +1084,29 @@ impl InstanceRunner { // Add the new Crucible backends to the list of source instance spec // elements that should be replaced in the target's spec. - let mut elements_to_replace: std::collections::HashMap<_, _> = - disks - .into_iter() - .map(|disk| { - ( - // N.B. This ID must match the one supplied when the - // instance was started. - disk.id.to_string(), - ReplacementComponent::CrucibleStorageBackend( - disk.backend, - ), - ) - }) - .collect(); + let mut elements_to_replace: std::collections::HashMap<_, _> = spec + .crucible_backends() + .map(|(id, backend)| { + ( + id.to_string(), + ReplacementComponent::CrucibleStorageBackend( + backend.clone(), + ), + ) + }) + .collect(); // Add new OPTE backend configuration to the replacement list. - elements_to_replace.extend(nics.into_iter().map(|nic| { - ( - // N.B. This ID must match the one supplied when the - // instance was started. - nic.id.to_string(), - ReplacementComponent::VirtioNetworkBackend(nic.backend), - ) - })); + elements_to_replace.extend(spec.viona_backends().map( + |(id, backend)| { + ( + id.to_string(), + ReplacementComponent::VirtioNetworkBackend( + backend.clone(), + ), + ) + }, + )); propolis_client::types::InstanceEnsureRequest { properties: self.properties.clone(), @@ -1260,136 +1117,9 @@ impl InstanceRunner { }, } } else { - // This is not a request to migrate, so construct a brand new spec - // to send to Propolis. - // - // Spec components must all have unique names. This routine ensures - // that names are unique as follows: - // - // 1. Backend components corresponding to specific control plane - // objects (e.g. Crucible disks, network interfaces) are - // identified by their control plane record IDs, which are UUIDs. - // (If these UUIDs collide, Nexus has many other problems.) - // 2. Devices attached to those backends are identified by a string - // that includes the backend UUID; see `id_to_device_name` below. - // 3. Other components that are implicitly added to all VMs are - // assigned unique component names within this function. - // - // Because *Nexus object names* (which *can* alias) are never used - // directly to name spec components, there should never be a - // conflict, so this helper asserts that all keys in the component - // map are unique. - fn add_component( - spec: &mut propolis_client::types::InstanceSpecV0, - key: String, - component: ComponentV0, - ) { - assert!(spec.components.insert(key, component).is_none()); - } - - fn id_to_device_name(id: &Uuid) -> String { - format!("{id}:device") - } - - let mut spec = propolis_client::types::InstanceSpecV0 { - board: Board { - chipset: Chipset::default(), - cpus: self.vcpus, - memory_mb: self.memory_mib, - cpuid: None, - guest_hv_interface: None, - }, - components: Default::default(), - }; - - for (name, num) in [ - ("com1", SerialPortNumber::Com1), - ("com2", SerialPortNumber::Com2), - ("com3", SerialPortNumber::Com3), - ("com4", SerialPortNumber::Com4), - ] { - add_component( - &mut spec, - name.to_string(), - ComponentV0::SerialPort(SerialPort { num }), - ); - } - - for DiskComponents { id, device, backend } in disks.into_iter() { - add_component( - &mut spec, - id_to_device_name(&id), - ComponentV0::NvmeDisk(device), - ); - - add_component( - &mut spec, - id.to_string(), - ComponentV0::CrucibleStorageBackend(backend), - ); - } - - for NicComponents { id, device, backend } in nics.into_iter() { - add_component( - &mut spec, - id_to_device_name(&id), - ComponentV0::VirtioNic(device), - ); - add_component( - &mut spec, - id.to_string(), - ComponentV0::VirtioNetworkBackend(backend), - ); - } - - add_component( - &mut spec, - "pvpanic".to_owned(), - ComponentV0::QemuPvpanic(QemuPvpanic { enable_isa: true }), - ); - - if let Some(settings) = &self.boot_settings { - // The boot order contains a list of disk IDs. Propolis matches - // boot order entries based on device component names, so pass - // the ID through `id_to_device_name` when generating the - // Propolis boot order. - let settings = ComponentV0::BootSettings(BootSettings { - order: settings - .order - .iter() - .map(|id| BootOrderEntry { - id: SpecKey::Name(id_to_device_name(&id)), - }) - .collect(), - }); - - add_component(&mut spec, "boot_settings".to_string(), settings); - } - - if let Some(cloud_init) = &self.cloud_init_bytes { - let device_name = "cloud-init-dev"; - let backend_name = "cloud-init-backend"; - - // The old Propolis API (and sled-agent's arguments to it) - // always attached cloud-init drives at BDF 0.24.0. - let device = ComponentV0::VirtioDisk(VirtioDisk { - backend_id: SpecKey::Name(backend_name.to_string()), - pci_path: PciPath::new(0, 0x18, 0).unwrap(), - }); - - let backend = - ComponentV0::BlobStorageBackend(BlobStorageBackend { - base64: cloud_init.0.clone(), - readonly: true, - }); - - add_component(&mut spec, device_name.to_string(), device); - add_component(&mut spec, backend_name.to_string(), backend); - } - propolis_client::types::InstanceEnsureRequest { properties: self.properties.clone(), - init: InstanceInitializationMethod::Spec { spec }, + init: InstanceInitializationMethod::Spec { spec: spec.0 }, } }; @@ -1400,6 +1130,51 @@ impl InstanceRunner { Ok(()) } + /// Amends the network backend entries in the supplied Propolis VM `spec` so + /// that they map to the correct OPTE ports in the supplied `running_zone`. + fn amend_propolis_network_backends( + &self, + spec: &mut VmmSpec, + running_zone: &RunningZone, + ) -> Result<(), Error> { + for port in running_zone.opte_ports() { + let nic = self + .requested_nics + .iter() + .find(|nic| nic.slot == port.slot()) + .ok_or(Error::Opte( + illumos_utils::opte::Error::NoNicforPort( + port.name().into(), + port.slot().into(), + ), + ))?; + + // The caller is presumed to have arranged things so that NICs in + // the requested NIC list appear with the same IDs in the instance + // spec. Bail if this isn't the case. + let Some(backend) = + spec.0.components.get_mut(&SpecKey::Uuid(nic.id)) + else { + return Err(Error::NicNotInPropolisSpec(nic.id)); + }; + + let ComponentV0::VirtioNetworkBackend(be) = backend else { + return Err(Error::NicNotInPropolisSpec(nic.id)); + }; + + be.vnic_name = port.name().to_string(); + } + + Ok(()) + } + + /// Given a freshly-created Propolis process, sends an ensure request to + /// that Propolis and launches all of the tasks needed to monitor the + /// resulting Propolis VM. + /// + /// # Panics + /// + /// Panics if this routine is called more than once for a given Instance. async fn install_running_state( &mut self, PropolisSetup { client, running_zone }: PropolisSetup, @@ -1729,7 +1504,8 @@ pub struct Instance { #[derive(Debug)] pub(crate) struct InstanceInitialState { - pub hardware: InstanceHardware, + pub vmm_spec: VmmSpec, + pub local_config: InstanceSledLocalConfig, pub vmm_runtime: VmmRuntimeState, pub propolis_addr: SocketAddr, /// UUID of the migration in to this VMM, if the VMM is being created as the @@ -1769,7 +1545,8 @@ impl Instance { "state" => ?state); let InstanceInitialState { - hardware, + vmm_spec, + local_config, vmm_runtime, propolis_addr, migration_id, @@ -1787,20 +1564,19 @@ impl Instance { let mut dhcp_config = DhcpCfg { hostname: Some( - hardware - .properties + local_config .hostname .as_str() .parse() .map_err(Error::InvalidHostname)?, ), - host_domain: hardware + host_domain: local_config .dhcp_config .host_domain .map(|domain| domain.parse()) .transpose() .map_err(Error::InvalidHostname)?, - domain_search_list: hardware + domain_search_list: local_config .dhcp_config .search_domains .into_iter() @@ -1810,7 +1586,7 @@ impl Instance { dns4_servers: Vec::new(), dns6_servers: Vec::new(), }; - for ip in hardware.dhcp_config.dns_servers { + for ip in local_config.dhcp_config.dns_servers { match ip { IpAddr::V4(ip) => dhcp_config.dns4_servers.push(ip.into()), IpAddr::V6(ip) => dhcp_config.dns6_servers.push(ip.into()), @@ -1849,31 +1625,23 @@ impl Instance { tx_monitor, rx_monitor, monitor_handle: None, - // NOTE: Mostly lies. properties: propolis_client::types::InstanceProperties { id: id.into_untyped_uuid(), - name: hardware.properties.hostname.to_string(), - description: "Test description".to_string(), + name: local_config.hostname.to_string(), + description: "Omicron-managed VM".to_string(), metadata, }, - // TODO: we should probably make propolis aligned with - // InstanceCpuCount here, to avoid any casting... - vcpus: hardware.properties.ncpus.0 as u8, - // TODO: Align the byte type w/propolis. - memory_mib: hardware.properties.memory.to_whole_mebibytes(), + propolis_spec: vmm_spec, propolis_id, propolis_addr, vnic_allocator, port_manager, - requested_nics: hardware.nics, - source_nat: hardware.source_nat, - ephemeral_ip: hardware.ephemeral_ip, - floating_ips: hardware.floating_ips, - firewall_rules: hardware.firewall_rules, + requested_nics: local_config.nics, + source_nat: local_config.source_nat, + ephemeral_ip: local_config.ephemeral_ip, + floating_ips: local_config.floating_ips, + firewall_rules: local_config.firewall_rules, dhcp_config, - requested_disks: hardware.disks, - cloud_init_bytes: hardware.cloud_init_bytes, - boot_settings: hardware.boot_settings, state: InstanceStates::new(vmm_runtime, migration_id), running_state: None, nexus_client, @@ -2504,10 +2272,8 @@ mod tests { use dropshot::HttpServer; use internal_dns_resolver::Resolver; use omicron_common::FileKv; - use omicron_common::api::external::{ - ByteCount, Generation, Hostname, InstanceCpuCount, - }; - use omicron_common::api::internal::nexus::{InstanceProperties, VmmState}; + use omicron_common::api::external::{Generation, Hostname}; + use omicron_common::api::internal::nexus::VmmState; use omicron_common::api::internal::shared::{DhcpConfig, SledIdentifiers}; use propolis_client::types::{ InstanceMigrateStatusResponse, InstanceStateMonitorResponse, @@ -2707,12 +2473,20 @@ mod tests { fn fake_instance_initial_state( propolis_addr: SocketAddr, ) -> InstanceInitialState { - let hardware = InstanceHardware { - properties: InstanceProperties { - ncpus: InstanceCpuCount(1), - memory: ByteCount::from_gibibytes_u32(1), - hostname: Hostname::from_str("bert").unwrap(), + use propolis_client::instance_spec::{Board, InstanceSpecV0}; + let spec = VmmSpec(InstanceSpecV0 { + board: Board { + cpus: 1, + memory_mb: 1024, + chipset: Default::default(), + guest_hv_interface: Default::default(), + cpuid: None, }, + components: Default::default(), + }); + + let local_config = InstanceSledLocalConfig { + hostname: Hostname::from_str("bert").unwrap(), nics: vec![], source_nat: SourceNatConfig::new( IpAddr::V6(Ipv6Addr::UNSPECIFIED), @@ -2728,13 +2502,11 @@ mod tests { host_domain: None, search_domains: vec![], }, - disks: vec![], - boot_settings: None, - cloud_init_bytes: None, }; InstanceInitialState { - hardware, + vmm_spec: spec, + local_config, vmm_runtime: VmmRuntimeState { state: VmmState::Starting, gen: Generation::new(), @@ -3022,7 +2794,8 @@ mod tests { let instance_id = InstanceUuid::new_v4(); let propolis_id = PropolisUuid::from_untyped_uuid(PROPOLIS_ID); let InstanceInitialState { - hardware, + vmm_spec, + local_config, vmm_runtime, propolis_addr, migration_id: _, @@ -3045,9 +2818,10 @@ mod tests { .ensure_registered( propolis_id, InstanceEnsureBody { + vmm_spec, + local_config, instance_id, migration_id: None, - hardware, vmm_runtime, propolis_addr, metadata, @@ -3124,7 +2898,8 @@ mod tests { let instance_id = InstanceUuid::new_v4(); let propolis_id = PropolisUuid::from_untyped_uuid(PROPOLIS_ID); let InstanceInitialState { - hardware, + vmm_spec, + local_config, vmm_runtime, propolis_addr, migration_id: _, @@ -3147,9 +2922,10 @@ mod tests { .ensure_registered( propolis_id, InstanceEnsureBody { + vmm_spec, + local_config, instance_id, migration_id: None, - hardware, vmm_runtime, propolis_addr, metadata, @@ -3279,7 +3055,8 @@ mod tests { }; let InstanceInitialState { - hardware, + vmm_spec, + local_config, vmm_runtime, propolis_addr, migration_id, @@ -3310,21 +3087,17 @@ mod tests { description: "test instance".to_string(), metadata, }, - vcpus: 1, - memory_mib: 1024, + propolis_spec: vmm_spec, propolis_id, propolis_addr, vnic_allocator, port_manager, - requested_nics: hardware.nics, - source_nat: hardware.source_nat, - ephemeral_ip: hardware.ephemeral_ip, - floating_ips: hardware.floating_ips, - firewall_rules: hardware.firewall_rules, + requested_nics: local_config.nics, + source_nat: local_config.source_nat, + ephemeral_ip: local_config.ephemeral_ip, + floating_ips: local_config.floating_ips, + firewall_rules: local_config.firewall_rules, dhcp_config, - requested_disks: hardware.disks, - cloud_init_bytes: hardware.cloud_init_bytes, - boot_settings: hardware.boot_settings, state: InstanceStates::new(vmm_runtime, migration_id), running_state: None, nexus_client, diff --git a/sled-agent/src/instance_manager.rs b/sled-agent/src/instance_manager.rs index 0812a26ea51..9bd0ad76497 100644 --- a/sled-agent/src/instance_manager.rs +++ b/sled-agent/src/instance_manager.rs @@ -547,20 +547,22 @@ impl InstanceManagerRunner { sled_identifiers: SledIdentifiers, ) -> Result { let InstanceEnsureBody { + vmm_spec, + local_config, instance_id, migration_id, propolis_addr, - hardware, vmm_runtime, metadata, } = instance; info!( &self.log, "ensuring instance is registered"; + "propolis_spec" => ?vmm_spec, "instance_id" => %instance_id, "propolis_id" => %propolis_id, "migration_id" => ?migration_id, - "hardware" => ?hardware, + "local_config" => ?local_config, "vmm_runtime" => ?vmm_runtime, "propolis_addr" => ?propolis_addr, "metadata" => ?metadata, @@ -612,7 +614,8 @@ impl InstanceManagerRunner { }; let state = crate::instance::InstanceInitialState { - hardware, + vmm_spec, + local_config, vmm_runtime, propolis_addr, migration_id, diff --git a/sled-agent/src/sim/sled_agent.rs b/sled-agent/src/sim/sled_agent.rs index 98174dbdd61..ff9efaeb470 100644 --- a/sled-agent/src/sim/sled_agent.rs +++ b/sled-agent/src/sim/sled_agent.rs @@ -47,12 +47,9 @@ use omicron_uuid_kinds::{ SupportBundleUuid, ZpoolUuid, }; use oxnet::Ipv6Net; +use propolis_client::instance_spec::SpecKey; use propolis_client::{ - Client as PropolisClient, - types::{ - Board, Chipset, ComponentV0, InstanceInitializationMethod, - InstanceSpecV0, SerialPort, SerialPortNumber, - }, + Client as PropolisClient, types::InstanceInitializationMethod, }; use range_requests::PotentialRange; use sled_agent_api::SupportBundleMetadata; @@ -203,23 +200,31 @@ impl SledAgent { instance: InstanceEnsureBody, ) -> Result { let InstanceEnsureBody { + vmm_spec, + local_config, instance_id, migration_id, - hardware, vmm_runtime, metadata, .. } = instance; // respond with a fake 500 level failure if asked to ensure an instance // with more than 16 CPUs. - let ncpus: i64 = (&hardware.properties.ncpus).into(); + let ncpus = vmm_spec.0.board.cpus; if ncpus > 16 { return Err(Error::internal_error( &"could not allocate an instance: ran out of CPUs!", )); }; - for disk in &hardware.disks { + for (id, _disk) in vmm_spec.crucible_backends() { + let SpecKey::Uuid(id) = id else { + return Err(Error::invalid_value( + id.to_string(), + "Crucible disks in a Propolis spec must have UUID keys", + )); + }; + let initial_state = DiskRuntimeState { disk_state: DiskState::Attached( instance_id.into_untyped_uuid(), @@ -230,7 +235,6 @@ impl SledAgent { // Ensure that any disks that are in this request are attached to // this instance. - let id = disk.disk_id; self.disks .sim_ensure( &id, @@ -241,7 +245,7 @@ impl SledAgent { ) .await?; self.disks - .sim_ensure_producer(&id, (self.nexus_address, id)) + .sim_ensure_producer(id, (self.nexus_address, *id)) .await?; } @@ -275,7 +279,7 @@ impl SledAgent { }; let properties = propolis_client::types::InstanceProperties { id: propolis_id.into_untyped_uuid(), - name: hardware.properties.hostname.to_string(), + name: local_config.hostname.to_string(), description: "sled-agent-sim created instance".to_string(), metadata, }; @@ -283,28 +287,10 @@ impl SledAgent { let body = propolis_client::types::InstanceEnsureRequest { properties, init: InstanceInitializationMethod::Spec { - spec: InstanceSpecV0 { - board: Board { - cpus: hardware.properties.ncpus.0 as u8, - chipset: Chipset::default(), - memory_mb: hardware - .properties - .memory - .to_whole_mebibytes(), - cpuid: None, - guest_hv_interface: None, - }, - components: [( - "com1".to_string(), - ComponentV0::SerialPort(SerialPort { - num: SerialPortNumber::Com1, - }), - )] - .into_iter() - .collect(), - }, + spec: vmm_spec.0.clone(), }, }; + // Try to create the instance client.instance_ensure().body(body).send().await.map_err( |e| { @@ -338,13 +324,17 @@ impl SledAgent { ) .await?; - for disk_request in &hardware.disks { - let vcr = serde_json::from_str(&disk_request.vcr_json.0)?; - self.simulated_upstairs.map_id_to_vcr(disk_request.disk_id, &vcr); + for (id, disk_request) in vmm_spec.crucible_backends() { + let SpecKey::Uuid(id) = id else { + unreachable!("already verified Crucible disks have UUID keys"); + }; + + let vcr = serde_json::from_str(&disk_request.request_json)?; + self.simulated_upstairs.map_id_to_vcr(*id, &vcr); } let mut routes = self.vpc_routes.lock().unwrap(); - for nic in &hardware.nics { + for nic in &local_config.nics { let my_routers = [ RouterId { vni: nic.vni, kind: RouterKind::System }, RouterId { vni: nic.vni, kind: RouterKind::Custom(nic.subnet) }, @@ -711,6 +701,7 @@ impl SledAgent { SocketAddr::new(Ipv6Addr::LOCALHOST.into(), 0); let dropshot_config = propolis_mock_server::Config { bind_address: propolis_bind_address, + default_request_body_max_bytes: 1024 * 1024, ..Default::default() }; info!(log, "Starting mock propolis-server..."); diff --git a/sled-agent/types/src/instance.rs b/sled-agent/types/src/instance.rs index f4dd1f9bc93..b2de8311dbb 100644 --- a/sled-agent/types/src/instance.rs +++ b/sled-agent/types/src/instance.rs @@ -9,14 +9,20 @@ use std::{ net::{IpAddr, SocketAddr}, }; -use omicron_common::NoDebug; -use omicron_common::api::internal::{ - nexus::{InstanceProperties, SledVmmState, VmmRuntimeState}, - shared::{ - DhcpConfig, NetworkInterface, ResolvedVpcFirewallRule, SourceNatConfig, +use omicron_common::api::{ + external::Hostname, + internal::{ + nexus::{SledVmmState, VmmRuntimeState}, + shared::{ + DhcpConfig, NetworkInterface, ResolvedVpcFirewallRule, + SourceNatConfig, + }, }, }; use omicron_uuid_kinds::InstanceUuid; +use propolis_client::instance_spec::{ + ComponentV0, CrucibleStorageBackend, SpecKey, VirtioNetworkBackend, +}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use uuid::Uuid; @@ -25,9 +31,13 @@ use uuid::Uuid; /// agent. #[derive(Serialize, Deserialize, JsonSchema)] pub struct InstanceEnsureBody { - /// A description of the instance's virtual hardware and the initial runtime - /// state this sled agent should store for this incarnation of the instance. - pub hardware: InstanceHardware, + /// The virtual hardware configuration this virtual machine should have when + /// it is started. + pub vmm_spec: VmmSpec, + + /// Information about the sled-local configuration that needs to be + /// established to make the VM's virtual hardware fully functional. + pub local_config: InstanceSledLocalConfig, /// The initial VMM runtime state for the VMM being registered. pub vmm_runtime: VmmRuntimeState, @@ -47,43 +57,11 @@ pub struct InstanceEnsureBody { pub metadata: InstanceMetadata, } -/// A request to attach a disk to an instance. -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] -pub struct InstanceDisk { - /// The disk's UUID. - pub disk_id: Uuid, - /// The logical slot number assigned to the disk in its database record. - pub slot: u8, - /// True if the disk is read-only. - pub read_only: bool, - /// A JSON representation of the Crucible volume construction request for - /// this attachment. - // - // This is marked as `NoDebug` because the VCR contains the volume's - // encryption keys. - pub vcr_json: NoDebug, - - /// The disk's name, used to generate the serial number for the virtual disk - /// exposed to the guest. - // - // TODO(#7153): Making this depend on the disk name means that a disk's ID - // may change if it is renamed or if a snapshot of it is used to create a - // new disk. - pub name: String, -} - -/// Configures how an instance is told to try to boot. -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] -pub struct InstanceBootSettings { - /// Propolis should tell guest firmware to try to boot from devices in this - /// order. - pub order: Vec, -} - -/// Describes the instance hardware. +/// Describes sled-local configuration that a sled-agent must establish to make +/// the instance's virtual hardware fully functional. #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] -pub struct InstanceHardware { - pub properties: InstanceProperties, +pub struct InstanceSledLocalConfig { + pub hostname: Hostname, pub nics: Vec, pub source_nat: SourceNatConfig, /// Zero or more external IP addresses (either floating or ephemeral), @@ -92,9 +70,6 @@ pub struct InstanceHardware { pub floating_ips: Vec, pub firewall_rules: Vec, pub dhcp_config: DhcpConfig, - pub disks: Vec, - pub boot_settings: Option, - pub cloud_init_bytes: Option>, } /// Metadata used to track statistics about an instance. @@ -198,3 +173,37 @@ pub enum InstanceExternalIpBody { Ephemeral(IpAddr), Floating(IpAddr), } + +/// Specifies the virtual hardware configuration of a new Propolis VMM in the +/// form of a Propolis instance specification. +/// +/// Sled-agent expects that when an instance spec is provided alongside an +/// `InstanceSledLocalConfig` to initialize a new instance, the NIC IDs in that +/// config's network interface list will match the IDs of the virtio network +/// backends in the instance spec. +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] +pub struct VmmSpec(pub propolis_client::instance_spec::InstanceSpecV0); + +impl VmmSpec { + pub fn crucible_backends( + &self, + ) -> impl Iterator { + self.0.components.iter().filter_map( + |(key, component)| match component { + ComponentV0::CrucibleStorageBackend(be) => Some((key, be)), + _ => None, + }, + ) + } + + pub fn viona_backends( + &self, + ) -> impl Iterator { + self.0.components.iter().filter_map( + |(key, component)| match component { + ComponentV0::VirtioNetworkBackend(be) => Some((key, be)), + _ => None, + }, + ) + } +}