From b60b80f87e24945111a61f3b8993a8019250cff5 Mon Sep 17 00:00:00 2001 From: Laura Abbott Date: Mon, 2 Dec 2024 11:27:08 -0500 Subject: [PATCH 1/4] Remove mfg (#7178) --- tools/permslip_staging | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tools/permslip_staging b/tools/permslip_staging index be7647da24..9c413ddc6e 100644 --- a/tools/permslip_staging +++ b/tools/permslip_staging @@ -1,5 +1,5 @@ -1dd01ff0ce69bc1d0dc379924d8fd66945e693bef933a33da7dbdd1bbfb1f47a manifest-gimlet-v1.0.32.toml -949ec9036a538eb2ffa2e701c6bd6cdae1df4a3e7bd97e70a58f8ae2362620ce manifest-oxide-rot-1-v1.0.30.toml +c33a381e716127e05da928c39b3a4d5f5278e43f526ff8c5c817708c378a5c87 manifest-gimlet-v1.0.32.toml +2cda350adba506b3ab67813db932d07c7a7836b5731d5351e57d49302f41dbf4 manifest-oxide-rot-1-v1.0.30.toml 70de21757b47e3e6c15d4c8701efe80e8cc90125afdd2883ff160045aed20956 manifest-psc-v1.0.31.toml -da2a87df5f3bbeede159adac75d395e430a7879ab7a618267ca66ce6e5b6075c manifest-sidecar-v1.0.31.toml -7225234108ec390ab6ab754a481c570ac2fc0b8daa608457418ccf51a24c36f4 manifest-bootleby-v1.3.1.toml +499ee08eb77ed3600564239f3f3efdcf79f122ffc4b93b168790c24358ae1e3c manifest-sidecar-v1.0.31.toml +6f8459afe22c27d5920356878e4d8d639464f39a15ce7b5b040c2d908d52a570 manifest-bootleby-v1.3.1.toml From faadf3d9cfb049bdedf29c9139a7faadfb8a4986 Mon Sep 17 00:00:00 2001 From: Sean Klein Date: Mon, 2 Dec 2024 10:01:13 -0800 Subject: [PATCH 2/4] [test][omdb] Wait for producer registration before listing them (#7183) Refactors "wait_for_producer" into nexus-test-utils so it can be used by omdb, outside the nexus integration test suite. Fixes https://github.com/oxidecomputer/omicron/issues/7182 --- dev-tools/omdb/tests/test_all_output.rs | 12 +++++++ nexus/test-utils/src/lib.rs | 38 ++++++++++++++++++++++ nexus/tests/integration_tests/disks.rs | 3 +- nexus/tests/integration_tests/instances.rs | 3 +- nexus/tests/integration_tests/metrics.rs | 38 +--------------------- nexus/tests/integration_tests/oximeter.rs | 2 +- 6 files changed, 54 insertions(+), 42 deletions(-) diff --git a/dev-tools/omdb/tests/test_all_output.rs b/dev-tools/omdb/tests/test_all_output.rs index 57594fcca5..66205530f2 100644 --- a/dev-tools/omdb/tests/test_all_output.rs +++ b/dev-tools/omdb/tests/test_all_output.rs @@ -10,6 +10,7 @@ use dropshot::Method; use expectorate::assert_contents; use http::StatusCode; +use nexus_test_utils::wait_for_producer; use nexus_test_utils::{OXIMETER_UUID, PRODUCER_UUID}; use nexus_test_utils_macros::nexus_test; use nexus_types::deployment::Blueprint; @@ -23,6 +24,7 @@ use std::fmt::Write; use std::net::IpAddr; use std::path::Path; use subprocess::Exec; +use uuid::Uuid; /// name of the "omdb" executable const CMD_OMDB: &str = env!("CARGO_BIN_EXE_omdb"); @@ -274,6 +276,11 @@ async fn test_omdb_success_cases(cptestctx: &ControlPlaneTestContext) { let mut ox_output = String::new(); let ox = ox_url.clone(); + wait_for_producer( + &cptestctx.oximeter, + PRODUCER_UUID.parse::().unwrap(), + ) + .await; do_run_no_redactions( &mut ox_output, move |exec| exec.env("OMDB_OXIMETER_URL", &ox), @@ -423,6 +430,11 @@ async fn test_omdb_env_settings(cptestctx: &ControlPlaneTestContext) { // Case 2: is covered by the success tests above. let ox_args1 = &["oximeter", "--oximeter-url", &ox_url, "list-producers"]; let mut ox_output1 = String::new(); + wait_for_producer( + &cptestctx.oximeter, + PRODUCER_UUID.parse::().unwrap(), + ) + .await; do_run_no_redactions( &mut ox_output1, move |exec| exec, diff --git a/nexus/test-utils/src/lib.rs b/nexus/test-utils/src/lib.rs index 92b4b14ffe..e78fb45ce6 100644 --- a/nexus/test-utils/src/lib.rs +++ b/nexus/test-utils/src/lib.rs @@ -73,6 +73,7 @@ use omicron_common::disk::CompressionAlgorithm; use omicron_common::zpool_name::ZpoolName; use omicron_sled_agent::sim; use omicron_test_utils::dev; +use omicron_test_utils::dev::poll::{wait_for_condition, CondCheckError}; use omicron_uuid_kinds::DatasetUuid; use omicron_uuid_kinds::ExternalIpUuid; use omicron_uuid_kinds::GenericUuid; @@ -1716,3 +1717,40 @@ pub async fn start_dns_server( Ok((dns_server, http_server, resolver)) } + +/// Wait until a producer is registered with Oximeter. +/// +/// This blocks until the producer is registered, for up to 60s. It panics if +/// the retry loop hits a permanent error. +pub async fn wait_for_producer( + oximeter: &oximeter_collector::Oximeter, + producer_id: G, +) { + wait_for_producer_impl(oximeter, producer_id.into_untyped_uuid()).await; +} + +// This function is outlined from wait_for_producer to avoid unnecessary +// monomorphization. +async fn wait_for_producer_impl( + oximeter: &oximeter_collector::Oximeter, + producer_id: Uuid, +) { + wait_for_condition( + || async { + if oximeter + .list_producers(None, usize::MAX) + .await + .iter() + .any(|p| p.id == producer_id) + { + Ok(()) + } else { + Err(CondCheckError::<()>::NotYet) + } + }, + &Duration::from_secs(1), + &Duration::from_secs(60), + ) + .await + .expect("Failed to find producer within time limit"); +} diff --git a/nexus/tests/integration_tests/disks.rs b/nexus/tests/integration_tests/disks.rs index 54cf9036af..d9888f9ccd 100644 --- a/nexus/tests/integration_tests/disks.rs +++ b/nexus/tests/integration_tests/disks.rs @@ -4,8 +4,6 @@ //! Tests basic disk support in the API -use crate::integration_tests::metrics::wait_for_producer; - use super::instances::instance_wait_for_state; use super::metrics::{get_latest_silo_metric, query_for_metrics}; use chrono::Utc; @@ -32,6 +30,7 @@ use nexus_test_utils::resource_helpers::create_instance; use nexus_test_utils::resource_helpers::create_instance_with; use nexus_test_utils::resource_helpers::create_project; use nexus_test_utils::resource_helpers::objects_list_page_authz; +use nexus_test_utils::wait_for_producer; use nexus_test_utils::SLED_AGENT_UUID; use nexus_test_utils_macros::nexus_test; use nexus_types::external_api::params; diff --git a/nexus/tests/integration_tests/instances.rs b/nexus/tests/integration_tests/instances.rs index 2949ea4560..163869896f 100644 --- a/nexus/tests/integration_tests/instances.rs +++ b/nexus/tests/integration_tests/instances.rs @@ -4,8 +4,6 @@ //! Tests basic instance support in the API -use crate::integration_tests::metrics::wait_for_producer; - use super::external_ips::floating_ip_get; use super::external_ips::get_floating_ip_by_id_url; use super::metrics::{get_latest_silo_metric, get_latest_system_metric}; @@ -39,6 +37,7 @@ use nexus_test_utils::resource_helpers::object_put; use nexus_test_utils::resource_helpers::objects_list_page_authz; use nexus_test_utils::resource_helpers::DiskTest; use nexus_test_utils::start_sled_agent; +use nexus_test_utils::wait_for_producer; use nexus_types::external_api::params::SshKeyCreate; use nexus_types::external_api::shared::IpKind; use nexus_types::external_api::shared::IpRange; diff --git a/nexus/tests/integration_tests/metrics.rs b/nexus/tests/integration_tests/metrics.rs index 4bac62f9d8..a468fa23d5 100644 --- a/nexus/tests/integration_tests/metrics.rs +++ b/nexus/tests/integration_tests/metrics.rs @@ -16,6 +16,7 @@ use nexus_test_utils::resource_helpers::{ create_default_ip_pool, create_disk, create_instance, create_project, objects_list_page_authz, DiskTest, }; +use nexus_test_utils::wait_for_producer; use nexus_test_utils::ControlPlaneTestContext; use nexus_test_utils_macros::nexus_test; use nexus_types::external_api::views::OxqlQueryResult; @@ -790,40 +791,3 @@ async fn test_mgs_metrics( // this test, we are responsible for removing its logs. mgs.logctx.cleanup_successful(); } - -/// Wait until a producer is registered with Oximeter. -/// -/// This blocks until the producer is registered, for up to 60s. It panics if -/// the retry loop hits a permanent error. -pub async fn wait_for_producer( - oximeter: &oximeter_collector::Oximeter, - producer_id: G, -) { - wait_for_producer_impl(oximeter, producer_id.into_untyped_uuid()).await; -} - -// This function is outlined from wait_for_producer to avoid unnecessary -// monomorphization. -async fn wait_for_producer_impl( - oximeter: &oximeter_collector::Oximeter, - producer_id: Uuid, -) { - wait_for_condition( - || async { - if oximeter - .list_producers(None, usize::MAX) - .await - .iter() - .any(|p| p.id == producer_id) - { - Ok(()) - } else { - Err(CondCheckError::<()>::NotYet) - } - }, - &Duration::from_secs(1), - &Duration::from_secs(60), - ) - .await - .expect("Failed to find producer within time limit"); -} diff --git a/nexus/tests/integration_tests/oximeter.rs b/nexus/tests/integration_tests/oximeter.rs index 2674e952e8..bc02e4c356 100644 --- a/nexus/tests/integration_tests/oximeter.rs +++ b/nexus/tests/integration_tests/oximeter.rs @@ -4,8 +4,8 @@ //! Integration tests for oximeter collectors and producers. -use crate::integration_tests::metrics::wait_for_producer; use nexus_test_interface::NexusServer; +use nexus_test_utils::wait_for_producer; use nexus_test_utils_macros::nexus_test; use omicron_test_utils::dev::poll::{wait_for_condition, CondCheckError}; use oximeter_db::DbWrite; From a9df1f8df83c42404871ef142fffa4604c2eeca8 Mon Sep 17 00:00:00 2001 From: John Gallagher Date: Mon, 2 Dec 2024 13:26:03 -0500 Subject: [PATCH 3/4] [oximeter] Don't stop refreshing producers forever if one attempt fails (#7191) #7126 introduced a `return;` inside `refresh_producer_list` to avoid clobbering our producer list on an error talking to Nexus, but `refresh_producer_list` had _two_ loops: it was both "do one refresh" (from which `return`ing is correct) and also "periodically refresh" (from which `return`ing is incorrect: it causes us to never refresh again). This PR splits `refresh_producer_list` into `refresh_producer_list_{task,once}`; strongly recommend looking at the diff with whitespace ignored, as it's basically a no-op other than this split (which makes the `return` correct, as it only terminates a single refresh and not all future ones). I also added some `InlineErrorChain` bits to try to get more info from logged errors. Fixes #7190. --- Cargo.lock | 1 + oximeter/collector/Cargo.toml | 1 + oximeter/collector/src/agent.rs | 155 +++++++++++++++++--------------- 3 files changed, 86 insertions(+), 71 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d8a1d33a6b..71ead293b1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7839,6 +7839,7 @@ dependencies = [ "slog", "slog-async", "slog-dtrace", + "slog-error-chain", "slog-term", "strum", "subprocess", diff --git a/oximeter/collector/Cargo.toml b/oximeter/collector/Cargo.toml index 87c82d1183..10ea4e6a9d 100644 --- a/oximeter/collector/Cargo.toml +++ b/oximeter/collector/Cargo.toml @@ -32,6 +32,7 @@ semver.workspace = true serde.workspace = true slog.workspace = true slog-async.workspace = true +slog-error-chain.workspace = true slog-dtrace.workspace = true slog-term.workspace = true strum.workspace = true diff --git a/oximeter/collector/src/agent.rs b/oximeter/collector/src/agent.rs index 8572f7f508..6fa8c01c56 100644 --- a/oximeter/collector/src/agent.rs +++ b/oximeter/collector/src/agent.rs @@ -32,6 +32,7 @@ use slog::o; use slog::trace; use slog::warn; use slog::Logger; +use slog_error_chain::InlineErrorChain; use std::collections::btree_map::Entry; use std::collections::BTreeMap; use std::net::SocketAddrV6; @@ -126,7 +127,7 @@ async fn perform_collection( warn!( log, "failed to collect results from producer"; - "error" => ?e, + InlineErrorChain::new(&e), ); Err(self_stats::FailureReason::Deserialization) } @@ -144,7 +145,7 @@ async fn perform_collection( error!( log, "failed to send collection request to producer"; - "error" => ?e + InlineErrorChain::new(&e), ); Err(self_stats::FailureReason::Unreachable) } @@ -238,7 +239,7 @@ async fn inner_collection_loop( log, "failed to receive on producer update \ watch channel, exiting"; - "error" => ?e, + InlineErrorChain::new(&e), ); return; } @@ -548,7 +549,7 @@ async fn results_printer( error!( log, "received error from a producer"; - "err" => ?e, + InlineErrorChain::new(&e), ); } } @@ -760,8 +761,10 @@ impl OximeterAgent { ) { let mut task = self.refresh_task.lock().unwrap(); if task.is_none() { - let refresh_task = - tokio::spawn(refresh_producer_list(self.clone(), nexus_pool)); + let refresh_task = tokio::spawn(refresh_producer_list_task( + self.clone(), + nexus_pool, + )); *task = Some(refresh_task); } } @@ -1026,7 +1029,7 @@ impl OximeterAgent { self.log, "failed to shut down collection task"; "producer_id" => %id, - "error" => ?e, + InlineErrorChain::new(&e), ), } Ok(()) @@ -1074,7 +1077,7 @@ impl OximeterAgent { } // A task which periodically updates our list of producers from Nexus. -async fn refresh_producer_list( +async fn refresh_producer_list_task( agent: OximeterAgent, nexus_pool: Pool, ) { @@ -1089,77 +1092,86 @@ async fn refresh_producer_list( loop { interval.tick().await; info!(agent.log, "refreshing list of producers from Nexus"); + refresh_producer_list_once(&agent, &nexus_pool).await; + } +} - let client = claim_nexus_with_backoff(&agent.log, &nexus_pool).await; - let mut stream = client.cpapi_assigned_producers_list_stream( - &agent.id, - // This is a _total_ limit, not a page size, so `None` means "get - // all entries". - None, - Some(IdSortMode::IdAscending), - ); - let mut expected_producers = BTreeMap::new(); - loop { - match stream.try_next().await { - Err(e) => { - // TODO-robustness: Some errors here may not be "fatal", in - // the sense that we can continue to process the list we - // receive from Nexus. It's not clear which ones though, - // since most of these are either impossible (pre-hook - // errors) or indicate we've made a serious programming - // error or updated to incompatible versions of Nexus / - // Oximeter. One that we might be able to handle is a - // communication error, say failing to fetch the last page - // when we've already fetched the first few. But for now, - // we'll simply keep the list we have and try again on the - // next refresh. - // - // For now, let's just avoid doing anything at all here, on - // the theory that if we hit this, we should just continue - // collecting from the last known-good set of producers. - error!( - agent.log, - "error fetching next assigned producer, \ - abandoning this refresh attempt"; - "err" => ?e, - ); - return; - } - Ok(Some(p)) => { - let endpoint = match ProducerEndpoint::try_from(p) { - Ok(ep) => ep, - Err(e) => { - error!( - agent.log, - "failed to convert producer description \ - from Nexus, skipping producer"; - "err" => e - ); - continue; - } - }; - let old = expected_producers.insert(endpoint.id, endpoint); - if let Some(ProducerEndpoint { id, .. }) = old { +// Run a single "producer refresh from Nexus" operation (which may require +// multiple requests to Nexus to fetch multiple pages of producers). +async fn refresh_producer_list_once( + agent: &OximeterAgent, + nexus_pool: &Pool, +) { + let client = claim_nexus_with_backoff(&agent.log, &nexus_pool).await; + let mut stream = client.cpapi_assigned_producers_list_stream( + &agent.id, + // This is a _total_ limit, not a page size, so `None` means "get + // all entries". + None, + Some(IdSortMode::IdAscending), + ); + let mut expected_producers = BTreeMap::new(); + loop { + match stream.try_next().await { + Err(e) => { + // TODO-robustness: Some errors here may not be "fatal", in + // the sense that we can continue to process the list we + // receive from Nexus. It's not clear which ones though, + // since most of these are either impossible (pre-hook + // errors) or indicate we've made a serious programming + // error or updated to incompatible versions of Nexus / + // Oximeter. One that we might be able to handle is a + // communication error, say failing to fetch the last page + // when we've already fetched the first few. But for now, + // we'll simply keep the list we have and try again on the + // next refresh. + // + // For now, let's just avoid doing anything at all here, on + // the theory that if we hit this, we should just continue + // collecting from the last known-good set of producers. + error!( + agent.log, + "error fetching next assigned producer, \ + abandoning this refresh attempt"; + InlineErrorChain::new(&e), + ); + return; + } + Ok(Some(p)) => { + let endpoint = match ProducerEndpoint::try_from(p) { + Ok(ep) => ep, + Err(e) => { error!( agent.log, - "Nexus appears to have sent duplicate producer info"; - "producer_id" => %id, + "failed to convert producer description \ + from Nexus, skipping producer"; + // No `InlineErrorChain` here: `e` is a string + "error" => e, ); + continue; } + }; + let old = expected_producers.insert(endpoint.id, endpoint); + if let Some(ProducerEndpoint { id, .. }) = old { + error!( + agent.log, + "Nexus appears to have sent duplicate producer info"; + "producer_id" => %id, + ); } - Ok(None) => break, } + Ok(None) => break, } - let n_current_tasks = expected_producers.len(); - let n_pruned_tasks = agent.ensure_producers(expected_producers).await; - *agent.last_refresh_time.lock().unwrap() = Some(Utc::now()); - info!( - agent.log, - "refreshed list of producers from Nexus"; - "n_pruned_tasks" => n_pruned_tasks, - "n_current_tasks" => n_current_tasks, - ); } + let n_current_tasks = expected_producers.len(); + let n_pruned_tasks = agent.ensure_producers(expected_producers).await; + *agent.last_refresh_time.lock().unwrap() = Some(Utc::now()); + info!( + agent.log, + "refreshed list of producers from Nexus"; + "n_pruned_tasks" => n_pruned_tasks, + "n_current_tasks" => n_current_tasks, + ); } async fn claim_nexus_with_backoff( @@ -1171,7 +1183,8 @@ async fn claim_nexus_with_backoff( log, "failed to lookup Nexus IP, will retry"; "delay" => ?delay, - "error" => ?error, + // No `InlineErrorChain` here: `error` is a string + "error" => error, ); }; let do_lookup = || async { From 24b59fcc3aadb1bdbf981ad8b42aa12129951186 Mon Sep 17 00:00:00 2001 From: "Andrew J. Stone" Date: Mon, 2 Dec 2024 20:54:16 -0500 Subject: [PATCH 4/4] Remove persistence of RSS plans (#7185) Implements #7174 Obviates the need for #724 This still needs some manual testing. --- schema/rss-service-plan-v2.json | 769 ----------- schema/rss-service-plan-v3.json | 909 ------------ schema/rss-service-plan-v4.json | 968 ------------- schema/rss-service-plan-v5.json | 1220 ----------------- schema/rss-sled-plan.json | 1187 ---------------- sled-agent/src/rack_setup/plan/service.rs | 207 --- sled-agent/src/rack_setup/plan/sled.rs | 144 +- sled-agent/src/rack_setup/service.rs | 270 ++-- .../madrid-rss-sled-plan.json | 1 - .../madrid-rss-sled-plan.json | 182 --- 10 files changed, 128 insertions(+), 5729 deletions(-) delete mode 100644 schema/rss-service-plan-v2.json delete mode 100644 schema/rss-service-plan-v3.json delete mode 100644 schema/rss-service-plan-v4.json delete mode 100644 schema/rss-service-plan-v5.json delete mode 100644 schema/rss-sled-plan.json delete mode 100644 sled-agent/tests/old-rss-sled-plans/madrid-rss-sled-plan.json delete mode 100644 sled-agent/tests/output/new-rss-sled-plans/madrid-rss-sled-plan.json diff --git a/schema/rss-service-plan-v2.json b/schema/rss-service-plan-v2.json deleted file mode 100644 index e5aba43040..0000000000 --- a/schema/rss-service-plan-v2.json +++ /dev/null @@ -1,769 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Plan", - "type": "object", - "required": [ - "dns_config", - "services" - ], - "properties": { - "dns_config": { - "$ref": "#/definitions/DnsConfigParams" - }, - "services": { - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/SledConfig" - } - } - }, - "definitions": { - "DnsConfigParams": { - "description": "DnsConfigParams\n\n
JSON schema\n\n```json { \"type\": \"object\", \"required\": [ \"generation\", \"time_created\", \"zones\" ], \"properties\": { \"generation\": { \"type\": \"integer\", \"format\": \"uint64\", \"minimum\": 0.0 }, \"time_created\": { \"type\": \"string\", \"format\": \"date-time\" }, \"zones\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/components/schemas/DnsConfigZone\" } } } } ```
", - "type": "object", - "required": [ - "generation", - "time_created", - "zones" - ], - "properties": { - "generation": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "time_created": { - "type": "string", - "format": "date-time" - }, - "zones": { - "type": "array", - "items": { - "$ref": "#/definitions/DnsConfigZone" - } - } - } - }, - "DnsConfigZone": { - "description": "DnsConfigZone\n\n
JSON schema\n\n```json { \"type\": \"object\", \"required\": [ \"records\", \"zone_name\" ], \"properties\": { \"records\": { \"type\": \"object\", \"additionalProperties\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/components/schemas/DnsRecord\" } } }, \"zone_name\": { \"type\": \"string\" } } } ```
", - "type": "object", - "required": [ - "records", - "zone_name" - ], - "properties": { - "records": { - "type": "object", - "additionalProperties": { - "type": "array", - "items": { - "$ref": "#/definitions/DnsRecord" - } - } - }, - "zone_name": { - "type": "string" - } - } - }, - "DnsRecord": { - "description": "DnsRecord\n\n
JSON schema\n\n```json { \"oneOf\": [ { \"type\": \"object\", \"required\": [ \"data\", \"type\" ], \"properties\": { \"data\": { \"type\": \"string\", \"format\": \"ipv4\" }, \"type\": { \"type\": \"string\", \"enum\": [ \"A\" ] } } }, { \"type\": \"object\", \"required\": [ \"data\", \"type\" ], \"properties\": { \"data\": { \"type\": \"string\", \"format\": \"ipv6\" }, \"type\": { \"type\": \"string\", \"enum\": [ \"AAAA\" ] } } }, { \"type\": \"object\", \"required\": [ \"data\", \"type\" ], \"properties\": { \"data\": { \"$ref\": \"#/components/schemas/Srv\" }, \"type\": { \"type\": \"string\", \"enum\": [ \"SRV\" ] } } } ] } ```
", - "oneOf": [ - { - "type": "object", - "required": [ - "data", - "type" - ], - "properties": { - "data": { - "type": "string", - "format": "ipv4" - }, - "type": { - "type": "string", - "enum": [ - "A" - ] - } - } - }, - { - "type": "object", - "required": [ - "data", - "type" - ], - "properties": { - "data": { - "type": "string", - "format": "ipv6" - }, - "type": { - "type": "string", - "enum": [ - "AAAA" - ] - } - } - }, - { - "type": "object", - "required": [ - "data", - "type" - ], - "properties": { - "data": { - "$ref": "#/definitions/Srv" - }, - "type": { - "type": "string", - "enum": [ - "SRV" - ] - } - } - } - ] - }, - "IpNet": { - "oneOf": [ - { - "title": "v4", - "allOf": [ - { - "$ref": "#/definitions/Ipv4Net" - } - ] - }, - { - "title": "v6", - "allOf": [ - { - "$ref": "#/definitions/Ipv6Net" - } - ] - } - ] - }, - "Ipv4Net": { - "title": "An IPv4 subnet", - "description": "An IPv4 subnet, including prefix and subnet mask", - "examples": [ - "192.168.1.0/24" - ], - "type": "string", - "pattern": "^(([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])/([0-9]|1[0-9]|2[0-9]|3[0-2])$" - }, - "Ipv6Net": { - "title": "An IPv6 subnet", - "description": "An IPv6 subnet, including prefix and subnet mask", - "examples": [ - "fd12:3456::/64" - ], - "type": "string", - "pattern": "^([fF][dD])[0-9a-fA-F]{2}:(([0-9a-fA-F]{1,4}:){6}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,6}:)([0-9a-fA-F]{1,4})?\\/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8])$" - }, - "MacAddr": { - "title": "A MAC address", - "description": "A Media Access Control address, in EUI-48 format", - "examples": [ - "ff:ff:ff:ff:ff:ff" - ], - "type": "string", - "maxLength": 17, - "minLength": 5, - "pattern": "^([0-9a-fA-F]{0,2}:){5}[0-9a-fA-F]{0,2}$" - }, - "Name": { - "title": "A name unique within the parent collection", - "description": "Names must begin with a lower case ASCII letter, be composed exclusively of lowercase ASCII, uppercase ASCII, numbers, and '-', and may not end with a '-'. Names cannot be a UUID, but they may contain a UUID. They can be at most 63 characters long.", - "type": "string", - "maxLength": 63, - "minLength": 1, - "pattern": "^(?![0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$)^[a-z]([a-zA-Z0-9-]*[a-zA-Z0-9]+)?$" - }, - "NetworkInterface": { - "description": "Information required to construct a virtual network interface", - "type": "object", - "required": [ - "id", - "ip", - "kind", - "mac", - "name", - "primary", - "slot", - "subnet", - "vni" - ], - "properties": { - "id": { - "type": "string", - "format": "uuid" - }, - "ip": { - "type": "string", - "format": "ip" - }, - "kind": { - "$ref": "#/definitions/NetworkInterfaceKind" - }, - "mac": { - "$ref": "#/definitions/MacAddr" - }, - "name": { - "$ref": "#/definitions/Name" - }, - "primary": { - "type": "boolean" - }, - "slot": { - "type": "integer", - "format": "uint8", - "minimum": 0.0 - }, - "subnet": { - "$ref": "#/definitions/IpNet" - }, - "vni": { - "$ref": "#/definitions/Vni" - } - } - }, - "NetworkInterfaceKind": { - "description": "The type of network interface", - "oneOf": [ - { - "description": "A vNIC attached to a guest instance", - "type": "object", - "required": [ - "id", - "type" - ], - "properties": { - "id": { - "type": "string", - "format": "uuid" - }, - "type": { - "type": "string", - "enum": [ - "instance" - ] - } - } - }, - { - "description": "A vNIC associated with an internal service", - "type": "object", - "required": [ - "id", - "type" - ], - "properties": { - "id": { - "type": "string", - "format": "uuid" - }, - "type": { - "type": "string", - "enum": [ - "service" - ] - } - } - }, - { - "description": "A vNIC associated with a probe", - "type": "object", - "required": [ - "id", - "type" - ], - "properties": { - "id": { - "type": "string", - "format": "uuid" - }, - "type": { - "type": "string", - "enum": [ - "probe" - ] - } - } - } - ] - }, - "OmicronZoneConfig": { - "description": "Describes one Omicron-managed zone running on a sled", - "type": "object", - "required": [ - "id", - "underlay_address", - "zone_type" - ], - "properties": { - "id": { - "type": "string", - "format": "uuid" - }, - "underlay_address": { - "type": "string", - "format": "ipv6" - }, - "zone_type": { - "$ref": "#/definitions/OmicronZoneType" - } - } - }, - "OmicronZoneDataset": { - "description": "Describes a persistent ZFS dataset associated with an Omicron zone", - "type": "object", - "required": [ - "pool_name" - ], - "properties": { - "pool_name": { - "$ref": "#/definitions/ZpoolName" - } - } - }, - "OmicronZoneType": { - "description": "Describes what kind of zone this is (i.e., what component is running in it) as well as any type-specific configuration", - "oneOf": [ - { - "type": "object", - "required": [ - "address", - "dns_servers", - "nic", - "ntp_servers", - "snat_cfg", - "type" - ], - "properties": { - "address": { - "type": "string" - }, - "dns_servers": { - "type": "array", - "items": { - "type": "string", - "format": "ip" - } - }, - "domain": { - "type": [ - "string", - "null" - ] - }, - "nic": { - "description": "The service vNIC providing outbound connectivity using OPTE.", - "allOf": [ - { - "$ref": "#/definitions/NetworkInterface" - } - ] - }, - "ntp_servers": { - "type": "array", - "items": { - "type": "string" - } - }, - "snat_cfg": { - "description": "The SNAT configuration for outbound connections.", - "allOf": [ - { - "$ref": "#/definitions/SourceNatConfig" - } - ] - }, - "type": { - "type": "string", - "enum": [ - "boundary_ntp" - ] - } - } - }, - { - "type": "object", - "required": [ - "address", - "dataset", - "type" - ], - "properties": { - "address": { - "type": "string" - }, - "dataset": { - "$ref": "#/definitions/OmicronZoneDataset" - }, - "type": { - "type": "string", - "enum": [ - "clickhouse" - ] - } - } - }, - { - "type": "object", - "required": [ - "address", - "dataset", - "type" - ], - "properties": { - "address": { - "type": "string" - }, - "dataset": { - "$ref": "#/definitions/OmicronZoneDataset" - }, - "type": { - "type": "string", - "enum": [ - "clickhouse_keeper" - ] - } - } - }, - { - "type": "object", - "required": [ - "address", - "dataset", - "type" - ], - "properties": { - "address": { - "type": "string" - }, - "dataset": { - "$ref": "#/definitions/OmicronZoneDataset" - }, - "type": { - "type": "string", - "enum": [ - "cockroach_db" - ] - } - } - }, - { - "type": "object", - "required": [ - "address", - "dataset", - "type" - ], - "properties": { - "address": { - "type": "string" - }, - "dataset": { - "$ref": "#/definitions/OmicronZoneDataset" - }, - "type": { - "type": "string", - "enum": [ - "crucible" - ] - } - } - }, - { - "type": "object", - "required": [ - "address", - "type" - ], - "properties": { - "address": { - "type": "string" - }, - "type": { - "type": "string", - "enum": [ - "crucible_pantry" - ] - } - } - }, - { - "type": "object", - "required": [ - "dataset", - "dns_address", - "http_address", - "nic", - "type" - ], - "properties": { - "dataset": { - "$ref": "#/definitions/OmicronZoneDataset" - }, - "dns_address": { - "description": "The address at which the external DNS server is reachable.", - "type": "string" - }, - "http_address": { - "description": "The address at which the external DNS server API is reachable.", - "type": "string" - }, - "nic": { - "description": "The service vNIC providing external connectivity using OPTE.", - "allOf": [ - { - "$ref": "#/definitions/NetworkInterface" - } - ] - }, - "type": { - "type": "string", - "enum": [ - "external_dns" - ] - } - } - }, - { - "type": "object", - "required": [ - "dataset", - "dns_address", - "gz_address", - "gz_address_index", - "http_address", - "type" - ], - "properties": { - "dataset": { - "$ref": "#/definitions/OmicronZoneDataset" - }, - "dns_address": { - "type": "string" - }, - "gz_address": { - "description": "The addresses in the global zone which should be created\n\nFor the DNS service, which exists outside the sleds's typical subnet - adding an address in the GZ is necessary to allow inter-zone traffic routing.", - "type": "string", - "format": "ipv6" - }, - "gz_address_index": { - "description": "The address is also identified with an auxiliary bit of information to ensure that the created global zone address can have a unique name.", - "type": "integer", - "format": "uint32", - "minimum": 0.0 - }, - "http_address": { - "type": "string" - }, - "type": { - "type": "string", - "enum": [ - "internal_dns" - ] - } - } - }, - { - "type": "object", - "required": [ - "address", - "dns_servers", - "ntp_servers", - "type" - ], - "properties": { - "address": { - "type": "string" - }, - "dns_servers": { - "type": "array", - "items": { - "type": "string", - "format": "ip" - } - }, - "domain": { - "type": [ - "string", - "null" - ] - }, - "ntp_servers": { - "type": "array", - "items": { - "type": "string" - } - }, - "type": { - "type": "string", - "enum": [ - "internal_ntp" - ] - } - } - }, - { - "type": "object", - "required": [ - "external_dns_servers", - "external_ip", - "external_tls", - "internal_address", - "nic", - "type" - ], - "properties": { - "external_dns_servers": { - "description": "External DNS servers Nexus can use to resolve external hosts.", - "type": "array", - "items": { - "type": "string", - "format": "ip" - } - }, - "external_ip": { - "description": "The address at which the external nexus server is reachable.", - "type": "string", - "format": "ip" - }, - "external_tls": { - "description": "Whether Nexus's external endpoint should use TLS", - "type": "boolean" - }, - "internal_address": { - "description": "The address at which the internal nexus server is reachable.", - "type": "string" - }, - "nic": { - "description": "The service vNIC providing external connectivity using OPTE.", - "allOf": [ - { - "$ref": "#/definitions/NetworkInterface" - } - ] - }, - "type": { - "type": "string", - "enum": [ - "nexus" - ] - } - } - }, - { - "type": "object", - "required": [ - "address", - "type" - ], - "properties": { - "address": { - "type": "string" - }, - "type": { - "type": "string", - "enum": [ - "oximeter" - ] - } - } - } - ] - }, - "SledConfig": { - "type": "object", - "required": [ - "zones" - ], - "properties": { - "zones": { - "description": "zones configured for this sled", - "type": "array", - "items": { - "$ref": "#/definitions/OmicronZoneConfig" - } - } - } - }, - "SourceNatConfig": { - "description": "An IP address and port range used for source NAT, i.e., making outbound network connections from guests or services.", - "type": "object", - "required": [ - "first_port", - "ip", - "last_port" - ], - "properties": { - "first_port": { - "description": "The first port used for source NAT, inclusive.", - "type": "integer", - "format": "uint16", - "minimum": 0.0 - }, - "ip": { - "description": "The external address provided to the instance or service.", - "type": "string", - "format": "ip" - }, - "last_port": { - "description": "The last port used for source NAT, also inclusive.", - "type": "integer", - "format": "uint16", - "minimum": 0.0 - } - } - }, - "Srv": { - "description": "Srv\n\n
JSON schema\n\n```json { \"type\": \"object\", \"required\": [ \"port\", \"prio\", \"target\", \"weight\" ], \"properties\": { \"port\": { \"type\": \"integer\", \"format\": \"uint16\", \"minimum\": 0.0 }, \"prio\": { \"type\": \"integer\", \"format\": \"uint16\", \"minimum\": 0.0 }, \"target\": { \"type\": \"string\" }, \"weight\": { \"type\": \"integer\", \"format\": \"uint16\", \"minimum\": 0.0 } } } ```
", - "type": "object", - "required": [ - "port", - "prio", - "target", - "weight" - ], - "properties": { - "port": { - "type": "integer", - "format": "uint16", - "minimum": 0.0 - }, - "prio": { - "type": "integer", - "format": "uint16", - "minimum": 0.0 - }, - "target": { - "type": "string" - }, - "weight": { - "type": "integer", - "format": "uint16", - "minimum": 0.0 - } - } - }, - "Vni": { - "description": "A Geneve Virtual Network Identifier", - "type": "integer", - "format": "uint32", - "minimum": 0.0 - }, - "ZpoolName": { - "title": "The name of a Zpool", - "description": "Zpool names are of the format ox{i,p}_. They are either Internal or External, and should be unique", - "type": "string", - "pattern": "^ox[ip]_[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" - } - } -} \ No newline at end of file diff --git a/schema/rss-service-plan-v3.json b/schema/rss-service-plan-v3.json deleted file mode 100644 index a003cde6f0..0000000000 --- a/schema/rss-service-plan-v3.json +++ /dev/null @@ -1,909 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Plan", - "type": "object", - "required": [ - "dns_config", - "services" - ], - "properties": { - "dns_config": { - "$ref": "#/definitions/DnsConfigParams" - }, - "services": { - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/SledConfig" - } - } - }, - "definitions": { - "DiskIdentity": { - "description": "Uniquely identifies a disk.", - "type": "object", - "required": [ - "model", - "serial", - "vendor" - ], - "properties": { - "model": { - "type": "string" - }, - "serial": { - "type": "string" - }, - "vendor": { - "type": "string" - } - } - }, - "DnsConfigParams": { - "description": "DnsConfigParams\n\n
JSON schema\n\n```json { \"type\": \"object\", \"required\": [ \"generation\", \"time_created\", \"zones\" ], \"properties\": { \"generation\": { \"type\": \"integer\", \"format\": \"uint64\", \"minimum\": 0.0 }, \"time_created\": { \"type\": \"string\", \"format\": \"date-time\" }, \"zones\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/components/schemas/DnsConfigZone\" } } } } ```
", - "type": "object", - "required": [ - "generation", - "time_created", - "zones" - ], - "properties": { - "generation": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "time_created": { - "type": "string", - "format": "date-time" - }, - "zones": { - "type": "array", - "items": { - "$ref": "#/definitions/DnsConfigZone" - } - } - } - }, - "DnsConfigZone": { - "description": "DnsConfigZone\n\n
JSON schema\n\n```json { \"type\": \"object\", \"required\": [ \"records\", \"zone_name\" ], \"properties\": { \"records\": { \"type\": \"object\", \"additionalProperties\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/components/schemas/DnsRecord\" } } }, \"zone_name\": { \"type\": \"string\" } } } ```
", - "type": "object", - "required": [ - "records", - "zone_name" - ], - "properties": { - "records": { - "type": "object", - "additionalProperties": { - "type": "array", - "items": { - "$ref": "#/definitions/DnsRecord" - } - } - }, - "zone_name": { - "type": "string" - } - } - }, - "DnsRecord": { - "description": "DnsRecord\n\n
JSON schema\n\n```json { \"oneOf\": [ { \"type\": \"object\", \"required\": [ \"data\", \"type\" ], \"properties\": { \"data\": { \"type\": \"string\", \"format\": \"ipv4\" }, \"type\": { \"type\": \"string\", \"enum\": [ \"A\" ] } } }, { \"type\": \"object\", \"required\": [ \"data\", \"type\" ], \"properties\": { \"data\": { \"type\": \"string\", \"format\": \"ipv6\" }, \"type\": { \"type\": \"string\", \"enum\": [ \"AAAA\" ] } } }, { \"type\": \"object\", \"required\": [ \"data\", \"type\" ], \"properties\": { \"data\": { \"$ref\": \"#/components/schemas/Srv\" }, \"type\": { \"type\": \"string\", \"enum\": [ \"SRV\" ] } } } ] } ```
", - "oneOf": [ - { - "type": "object", - "required": [ - "data", - "type" - ], - "properties": { - "data": { - "type": "string", - "format": "ipv4" - }, - "type": { - "type": "string", - "enum": [ - "A" - ] - } - } - }, - { - "type": "object", - "required": [ - "data", - "type" - ], - "properties": { - "data": { - "type": "string", - "format": "ipv6" - }, - "type": { - "type": "string", - "enum": [ - "AAAA" - ] - } - } - }, - { - "type": "object", - "required": [ - "data", - "type" - ], - "properties": { - "data": { - "$ref": "#/definitions/Srv" - }, - "type": { - "type": "string", - "enum": [ - "SRV" - ] - } - } - } - ] - }, - "Generation": { - "description": "Generation numbers stored in the database, used for optimistic concurrency control", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "IpNet": { - "oneOf": [ - { - "title": "v4", - "allOf": [ - { - "$ref": "#/definitions/Ipv4Net" - } - ] - }, - { - "title": "v6", - "allOf": [ - { - "$ref": "#/definitions/Ipv6Net" - } - ] - } - ], - "x-rust-type": { - "crate": "oxnet", - "path": "oxnet::IpNet", - "version": "0.1.0" - } - }, - "Ipv4Net": { - "title": "An IPv4 subnet", - "description": "An IPv4 subnet, including prefix and prefix length", - "examples": [ - "192.168.1.0/24" - ], - "type": "string", - "pattern": "^(([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])/([0-9]|1[0-9]|2[0-9]|3[0-2])$", - "x-rust-type": { - "crate": "oxnet", - "path": "oxnet::Ipv4Net", - "version": "0.1.0" - } - }, - "Ipv6Net": { - "title": "An IPv6 subnet", - "description": "An IPv6 subnet, including prefix and subnet mask", - "examples": [ - "fd12:3456::/64" - ], - "type": "string", - "pattern": "^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))\\/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8])$", - "x-rust-type": { - "crate": "oxnet", - "path": "oxnet::Ipv6Net", - "version": "0.1.0" - } - }, - "MacAddr": { - "title": "A MAC address", - "description": "A Media Access Control address, in EUI-48 format", - "examples": [ - "ff:ff:ff:ff:ff:ff" - ], - "type": "string", - "maxLength": 17, - "minLength": 5, - "pattern": "^([0-9a-fA-F]{0,2}:){5}[0-9a-fA-F]{0,2}$" - }, - "Name": { - "title": "A name unique within the parent collection", - "description": "Names must begin with a lower case ASCII letter, be composed exclusively of lowercase ASCII, uppercase ASCII, numbers, and '-', and may not end with a '-'. Names cannot be a UUID, but they may contain a UUID. They can be at most 63 characters long.", - "type": "string", - "maxLength": 63, - "minLength": 1, - "pattern": "^(?![0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$)^[a-z]([a-zA-Z0-9-]*[a-zA-Z0-9]+)?$" - }, - "NetworkInterface": { - "description": "Information required to construct a virtual network interface", - "type": "object", - "required": [ - "id", - "ip", - "kind", - "mac", - "name", - "primary", - "slot", - "subnet", - "vni" - ], - "properties": { - "id": { - "type": "string", - "format": "uuid" - }, - "ip": { - "type": "string", - "format": "ip" - }, - "kind": { - "$ref": "#/definitions/NetworkInterfaceKind" - }, - "mac": { - "$ref": "#/definitions/MacAddr" - }, - "name": { - "$ref": "#/definitions/Name" - }, - "primary": { - "type": "boolean" - }, - "slot": { - "type": "integer", - "format": "uint8", - "minimum": 0.0 - }, - "subnet": { - "$ref": "#/definitions/IpNet" - }, - "transit_ips": { - "default": [], - "type": "array", - "items": { - "$ref": "#/definitions/IpNet" - } - }, - "vni": { - "$ref": "#/definitions/Vni" - } - } - }, - "NetworkInterfaceKind": { - "description": "The type of network interface", - "oneOf": [ - { - "description": "A vNIC attached to a guest instance", - "type": "object", - "required": [ - "id", - "type" - ], - "properties": { - "id": { - "type": "string", - "format": "uuid" - }, - "type": { - "type": "string", - "enum": [ - "instance" - ] - } - } - }, - { - "description": "A vNIC associated with an internal service", - "type": "object", - "required": [ - "id", - "type" - ], - "properties": { - "id": { - "type": "string", - "format": "uuid" - }, - "type": { - "type": "string", - "enum": [ - "service" - ] - } - } - }, - { - "description": "A vNIC associated with a probe", - "type": "object", - "required": [ - "id", - "type" - ], - "properties": { - "id": { - "type": "string", - "format": "uuid" - }, - "type": { - "type": "string", - "enum": [ - "probe" - ] - } - } - } - ] - }, - "OmicronPhysicalDiskConfig": { - "type": "object", - "required": [ - "id", - "identity", - "pool_id" - ], - "properties": { - "id": { - "type": "string", - "format": "uuid" - }, - "identity": { - "$ref": "#/definitions/DiskIdentity" - }, - "pool_id": { - "$ref": "#/definitions/TypedUuidForZpoolKind" - } - } - }, - "OmicronPhysicalDisksConfig": { - "type": "object", - "required": [ - "disks", - "generation" - ], - "properties": { - "disks": { - "type": "array", - "items": { - "$ref": "#/definitions/OmicronPhysicalDiskConfig" - } - }, - "generation": { - "description": "generation number of this configuration\n\nThis generation number is owned by the control plane (i.e., RSS or Nexus, depending on whether RSS-to-Nexus handoff has happened). It should not be bumped within Sled Agent.\n\nSled Agent rejects attempts to set the configuration to a generation older than the one it's currently running.", - "allOf": [ - { - "$ref": "#/definitions/Generation" - } - ] - } - } - }, - "OmicronZoneConfig": { - "description": "Describes one Omicron-managed zone running on a sled", - "type": "object", - "required": [ - "id", - "underlay_address", - "zone_type" - ], - "properties": { - "filesystem_pool": { - "description": "The pool on which we'll place this zone's filesystem.\n\nNote that this is transient -- the sled agent is permitted to destroy the zone's dataset on this pool each time the zone is initialized.", - "anyOf": [ - { - "$ref": "#/definitions/ZpoolName" - }, - { - "type": "null" - } - ] - }, - "id": { - "type": "string", - "format": "uuid" - }, - "underlay_address": { - "type": "string", - "format": "ipv6" - }, - "zone_type": { - "$ref": "#/definitions/OmicronZoneType" - } - } - }, - "OmicronZoneDataset": { - "description": "Describes a persistent ZFS dataset associated with an Omicron zone", - "type": "object", - "required": [ - "pool_name" - ], - "properties": { - "pool_name": { - "$ref": "#/definitions/ZpoolName" - } - } - }, - "OmicronZoneType": { - "description": "Describes what kind of zone this is (i.e., what component is running in it) as well as any type-specific configuration", - "oneOf": [ - { - "type": "object", - "required": [ - "address", - "dns_servers", - "nic", - "ntp_servers", - "snat_cfg", - "type" - ], - "properties": { - "address": { - "type": "string" - }, - "dns_servers": { - "type": "array", - "items": { - "type": "string", - "format": "ip" - } - }, - "domain": { - "type": [ - "string", - "null" - ] - }, - "nic": { - "description": "The service vNIC providing outbound connectivity using OPTE.", - "allOf": [ - { - "$ref": "#/definitions/NetworkInterface" - } - ] - }, - "ntp_servers": { - "type": "array", - "items": { - "type": "string" - } - }, - "snat_cfg": { - "description": "The SNAT configuration for outbound connections.", - "allOf": [ - { - "$ref": "#/definitions/SourceNatConfig" - } - ] - }, - "type": { - "type": "string", - "enum": [ - "boundary_ntp" - ] - } - } - }, - { - "description": "Type of clickhouse zone used for a single node clickhouse deployment", - "type": "object", - "required": [ - "address", - "dataset", - "type" - ], - "properties": { - "address": { - "type": "string" - }, - "dataset": { - "$ref": "#/definitions/OmicronZoneDataset" - }, - "type": { - "type": "string", - "enum": [ - "clickhouse" - ] - } - } - }, - { - "description": "A zone used to run a Clickhouse Keeper node\n\nKeepers are only used in replicated clickhouse setups", - "type": "object", - "required": [ - "address", - "dataset", - "type" - ], - "properties": { - "address": { - "type": "string" - }, - "dataset": { - "$ref": "#/definitions/OmicronZoneDataset" - }, - "type": { - "type": "string", - "enum": [ - "clickhouse_keeper" - ] - } - } - }, - { - "description": "A zone used to run a Clickhouse Server in a replicated deployment", - "type": "object", - "required": [ - "address", - "dataset", - "type" - ], - "properties": { - "address": { - "type": "string" - }, - "dataset": { - "$ref": "#/definitions/OmicronZoneDataset" - }, - "type": { - "type": "string", - "enum": [ - "clickhouse_server" - ] - } - } - }, - { - "type": "object", - "required": [ - "address", - "dataset", - "type" - ], - "properties": { - "address": { - "type": "string" - }, - "dataset": { - "$ref": "#/definitions/OmicronZoneDataset" - }, - "type": { - "type": "string", - "enum": [ - "cockroach_db" - ] - } - } - }, - { - "type": "object", - "required": [ - "address", - "dataset", - "type" - ], - "properties": { - "address": { - "type": "string" - }, - "dataset": { - "$ref": "#/definitions/OmicronZoneDataset" - }, - "type": { - "type": "string", - "enum": [ - "crucible" - ] - } - } - }, - { - "type": "object", - "required": [ - "address", - "type" - ], - "properties": { - "address": { - "type": "string" - }, - "type": { - "type": "string", - "enum": [ - "crucible_pantry" - ] - } - } - }, - { - "type": "object", - "required": [ - "dataset", - "dns_address", - "http_address", - "nic", - "type" - ], - "properties": { - "dataset": { - "$ref": "#/definitions/OmicronZoneDataset" - }, - "dns_address": { - "description": "The address at which the external DNS server is reachable.", - "type": "string" - }, - "http_address": { - "description": "The address at which the external DNS server API is reachable.", - "type": "string" - }, - "nic": { - "description": "The service vNIC providing external connectivity using OPTE.", - "allOf": [ - { - "$ref": "#/definitions/NetworkInterface" - } - ] - }, - "type": { - "type": "string", - "enum": [ - "external_dns" - ] - } - } - }, - { - "type": "object", - "required": [ - "dataset", - "dns_address", - "gz_address", - "gz_address_index", - "http_address", - "type" - ], - "properties": { - "dataset": { - "$ref": "#/definitions/OmicronZoneDataset" - }, - "dns_address": { - "type": "string" - }, - "gz_address": { - "description": "The addresses in the global zone which should be created\n\nFor the DNS service, which exists outside the sleds's typical subnet - adding an address in the GZ is necessary to allow inter-zone traffic routing.", - "type": "string", - "format": "ipv6" - }, - "gz_address_index": { - "description": "The address is also identified with an auxiliary bit of information to ensure that the created global zone address can have a unique name.", - "type": "integer", - "format": "uint32", - "minimum": 0.0 - }, - "http_address": { - "type": "string" - }, - "type": { - "type": "string", - "enum": [ - "internal_dns" - ] - } - } - }, - { - "type": "object", - "required": [ - "address", - "dns_servers", - "ntp_servers", - "type" - ], - "properties": { - "address": { - "type": "string" - }, - "dns_servers": { - "type": "array", - "items": { - "type": "string", - "format": "ip" - } - }, - "domain": { - "type": [ - "string", - "null" - ] - }, - "ntp_servers": { - "type": "array", - "items": { - "type": "string" - } - }, - "type": { - "type": "string", - "enum": [ - "internal_ntp" - ] - } - } - }, - { - "type": "object", - "required": [ - "external_dns_servers", - "external_ip", - "external_tls", - "internal_address", - "nic", - "type" - ], - "properties": { - "external_dns_servers": { - "description": "External DNS servers Nexus can use to resolve external hosts.", - "type": "array", - "items": { - "type": "string", - "format": "ip" - } - }, - "external_ip": { - "description": "The address at which the external nexus server is reachable.", - "type": "string", - "format": "ip" - }, - "external_tls": { - "description": "Whether Nexus's external endpoint should use TLS", - "type": "boolean" - }, - "internal_address": { - "description": "The address at which the internal nexus server is reachable.", - "type": "string" - }, - "nic": { - "description": "The service vNIC providing external connectivity using OPTE.", - "allOf": [ - { - "$ref": "#/definitions/NetworkInterface" - } - ] - }, - "type": { - "type": "string", - "enum": [ - "nexus" - ] - } - } - }, - { - "type": "object", - "required": [ - "address", - "type" - ], - "properties": { - "address": { - "type": "string" - }, - "type": { - "type": "string", - "enum": [ - "oximeter" - ] - } - } - } - ] - }, - "SledConfig": { - "type": "object", - "required": [ - "disks", - "zones" - ], - "properties": { - "disks": { - "description": "Control plane disks configured for this sled", - "allOf": [ - { - "$ref": "#/definitions/OmicronPhysicalDisksConfig" - } - ] - }, - "zones": { - "description": "zones configured for this sled", - "type": "array", - "items": { - "$ref": "#/definitions/OmicronZoneConfig" - } - } - } - }, - "SourceNatConfig": { - "description": "An IP address and port range used for source NAT, i.e., making outbound network connections from guests or services.", - "type": "object", - "required": [ - "first_port", - "ip", - "last_port" - ], - "properties": { - "first_port": { - "description": "The first port used for source NAT, inclusive.", - "type": "integer", - "format": "uint16", - "minimum": 0.0 - }, - "ip": { - "description": "The external address provided to the instance or service.", - "type": "string", - "format": "ip" - }, - "last_port": { - "description": "The last port used for source NAT, also inclusive.", - "type": "integer", - "format": "uint16", - "minimum": 0.0 - } - } - }, - "Srv": { - "description": "Srv\n\n
JSON schema\n\n```json { \"type\": \"object\", \"required\": [ \"port\", \"prio\", \"target\", \"weight\" ], \"properties\": { \"port\": { \"type\": \"integer\", \"format\": \"uint16\", \"minimum\": 0.0 }, \"prio\": { \"type\": \"integer\", \"format\": \"uint16\", \"minimum\": 0.0 }, \"target\": { \"type\": \"string\" }, \"weight\": { \"type\": \"integer\", \"format\": \"uint16\", \"minimum\": 0.0 } } } ```
", - "type": "object", - "required": [ - "port", - "prio", - "target", - "weight" - ], - "properties": { - "port": { - "type": "integer", - "format": "uint16", - "minimum": 0.0 - }, - "prio": { - "type": "integer", - "format": "uint16", - "minimum": 0.0 - }, - "target": { - "type": "string" - }, - "weight": { - "type": "integer", - "format": "uint16", - "minimum": 0.0 - } - } - }, - "TypedUuidForZpoolKind": { - "type": "string", - "format": "uuid" - }, - "Vni": { - "description": "A Geneve Virtual Network Identifier", - "type": "integer", - "format": "uint32", - "minimum": 0.0 - }, - "ZpoolName": { - "title": "The name of a Zpool", - "description": "Zpool names are of the format ox{i,p}_. They are either Internal or External, and should be unique", - "type": "string", - "pattern": "^ox[ip]_[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" - } - } -} \ No newline at end of file diff --git a/schema/rss-service-plan-v4.json b/schema/rss-service-plan-v4.json deleted file mode 100644 index 7b2276cabb..0000000000 --- a/schema/rss-service-plan-v4.json +++ /dev/null @@ -1,968 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Plan", - "type": "object", - "required": [ - "dns_config", - "services" - ], - "properties": { - "dns_config": { - "$ref": "#/definitions/DnsConfigParams" - }, - "services": { - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/SledConfig" - } - } - }, - "definitions": { - "BlueprintZoneConfig": { - "description": "Describes one Omicron-managed zone in a blueprint.\n\nPart of [`BlueprintZonesConfig`].", - "type": "object", - "required": [ - "disposition", - "id", - "zone_type" - ], - "properties": { - "disposition": { - "description": "The disposition (desired state) of this zone recorded in the blueprint.", - "allOf": [ - { - "$ref": "#/definitions/BlueprintZoneDisposition" - } - ] - }, - "filesystem_pool": { - "description": "zpool used for the zone's (transient) root filesystem", - "anyOf": [ - { - "$ref": "#/definitions/ZpoolName" - }, - { - "type": "null" - } - ] - }, - "id": { - "$ref": "#/definitions/TypedUuidForOmicronZoneKind" - }, - "zone_type": { - "$ref": "#/definitions/BlueprintZoneType" - } - } - }, - "BlueprintZoneDisposition": { - "description": "The desired state of an Omicron-managed zone in a blueprint.\n\nPart of [`BlueprintZoneConfig`].", - "oneOf": [ - { - "description": "The zone is in-service.", - "type": "string", - "enum": [ - "in_service" - ] - }, - { - "description": "The zone is not in service.", - "type": "string", - "enum": [ - "quiesced" - ] - }, - { - "description": "The zone is permanently gone.", - "type": "string", - "enum": [ - "expunged" - ] - } - ] - }, - "BlueprintZoneType": { - "oneOf": [ - { - "type": "object", - "required": [ - "address", - "dns_servers", - "external_ip", - "nic", - "ntp_servers", - "type" - ], - "properties": { - "address": { - "type": "string" - }, - "dns_servers": { - "type": "array", - "items": { - "type": "string", - "format": "ip" - } - }, - "domain": { - "type": [ - "string", - "null" - ] - }, - "external_ip": { - "$ref": "#/definitions/OmicronZoneExternalSnatIp" - }, - "nic": { - "description": "The service vNIC providing outbound connectivity using OPTE.", - "allOf": [ - { - "$ref": "#/definitions/NetworkInterface" - } - ] - }, - "ntp_servers": { - "type": "array", - "items": { - "type": "string" - } - }, - "type": { - "type": "string", - "enum": [ - "boundary_ntp" - ] - } - } - }, - { - "description": "Used in single-node clickhouse setups", - "type": "object", - "required": [ - "address", - "dataset", - "type" - ], - "properties": { - "address": { - "type": "string" - }, - "dataset": { - "$ref": "#/definitions/OmicronZoneDataset" - }, - "type": { - "type": "string", - "enum": [ - "clickhouse" - ] - } - } - }, - { - "type": "object", - "required": [ - "address", - "dataset", - "type" - ], - "properties": { - "address": { - "type": "string" - }, - "dataset": { - "$ref": "#/definitions/OmicronZoneDataset" - }, - "type": { - "type": "string", - "enum": [ - "clickhouse_keeper" - ] - } - } - }, - { - "description": "Used in replicated clickhouse setups", - "type": "object", - "required": [ - "address", - "dataset", - "type" - ], - "properties": { - "address": { - "type": "string" - }, - "dataset": { - "$ref": "#/definitions/OmicronZoneDataset" - }, - "type": { - "type": "string", - "enum": [ - "clickhouse_server" - ] - } - } - }, - { - "type": "object", - "required": [ - "address", - "dataset", - "type" - ], - "properties": { - "address": { - "type": "string" - }, - "dataset": { - "$ref": "#/definitions/OmicronZoneDataset" - }, - "type": { - "type": "string", - "enum": [ - "cockroach_db" - ] - } - } - }, - { - "type": "object", - "required": [ - "address", - "dataset", - "type" - ], - "properties": { - "address": { - "type": "string" - }, - "dataset": { - "$ref": "#/definitions/OmicronZoneDataset" - }, - "type": { - "type": "string", - "enum": [ - "crucible" - ] - } - } - }, - { - "type": "object", - "required": [ - "address", - "type" - ], - "properties": { - "address": { - "type": "string" - }, - "type": { - "type": "string", - "enum": [ - "crucible_pantry" - ] - } - } - }, - { - "type": "object", - "required": [ - "dataset", - "dns_address", - "http_address", - "nic", - "type" - ], - "properties": { - "dataset": { - "$ref": "#/definitions/OmicronZoneDataset" - }, - "dns_address": { - "description": "The address at which the external DNS server is reachable.", - "allOf": [ - { - "$ref": "#/definitions/OmicronZoneExternalFloatingAddr" - } - ] - }, - "http_address": { - "description": "The address at which the external DNS server API is reachable.", - "type": "string" - }, - "nic": { - "description": "The service vNIC providing external connectivity using OPTE.", - "allOf": [ - { - "$ref": "#/definitions/NetworkInterface" - } - ] - }, - "type": { - "type": "string", - "enum": [ - "external_dns" - ] - } - } - }, - { - "type": "object", - "required": [ - "dataset", - "dns_address", - "gz_address", - "gz_address_index", - "http_address", - "type" - ], - "properties": { - "dataset": { - "$ref": "#/definitions/OmicronZoneDataset" - }, - "dns_address": { - "type": "string" - }, - "gz_address": { - "description": "The addresses in the global zone which should be created\n\nFor the DNS service, which exists outside the sleds's typical subnet - adding an address in the GZ is necessary to allow inter-zone traffic routing.", - "type": "string", - "format": "ipv6" - }, - "gz_address_index": { - "description": "The address is also identified with an auxiliary bit of information to ensure that the created global zone address can have a unique name.", - "type": "integer", - "format": "uint32", - "minimum": 0.0 - }, - "http_address": { - "type": "string" - }, - "type": { - "type": "string", - "enum": [ - "internal_dns" - ] - } - } - }, - { - "type": "object", - "required": [ - "address", - "type" - ], - "properties": { - "address": { - "type": "string" - }, - "type": { - "type": "string", - "enum": [ - "internal_ntp" - ] - } - } - }, - { - "type": "object", - "required": [ - "external_dns_servers", - "external_ip", - "external_tls", - "internal_address", - "nic", - "type" - ], - "properties": { - "external_dns_servers": { - "description": "External DNS servers Nexus can use to resolve external hosts.", - "type": "array", - "items": { - "type": "string", - "format": "ip" - } - }, - "external_ip": { - "description": "The address at which the external nexus server is reachable.", - "allOf": [ - { - "$ref": "#/definitions/OmicronZoneExternalFloatingIp" - } - ] - }, - "external_tls": { - "description": "Whether Nexus's external endpoint should use TLS", - "type": "boolean" - }, - "internal_address": { - "description": "The address at which the internal nexus server is reachable.", - "type": "string" - }, - "nic": { - "description": "The service vNIC providing external connectivity using OPTE.", - "allOf": [ - { - "$ref": "#/definitions/NetworkInterface" - } - ] - }, - "type": { - "type": "string", - "enum": [ - "nexus" - ] - } - } - }, - { - "type": "object", - "required": [ - "address", - "type" - ], - "properties": { - "address": { - "type": "string" - }, - "type": { - "type": "string", - "enum": [ - "oximeter" - ] - } - } - } - ] - }, - "DiskIdentity": { - "description": "Uniquely identifies a disk.", - "type": "object", - "required": [ - "model", - "serial", - "vendor" - ], - "properties": { - "model": { - "type": "string" - }, - "serial": { - "type": "string" - }, - "vendor": { - "type": "string" - } - } - }, - "DnsConfigParams": { - "type": "object", - "required": [ - "generation", - "time_created", - "zones" - ], - "properties": { - "generation": { - "$ref": "#/definitions/Generation" - }, - "time_created": { - "type": "string", - "format": "date-time" - }, - "zones": { - "type": "array", - "items": { - "$ref": "#/definitions/DnsConfigZone" - } - } - } - }, - "DnsConfigZone": { - "type": "object", - "required": [ - "records", - "zone_name" - ], - "properties": { - "records": { - "type": "object", - "additionalProperties": { - "type": "array", - "items": { - "$ref": "#/definitions/DnsRecord" - } - } - }, - "zone_name": { - "type": "string" - } - } - }, - "DnsRecord": { - "oneOf": [ - { - "type": "object", - "required": [ - "data", - "type" - ], - "properties": { - "data": { - "type": "string", - "format": "ipv4" - }, - "type": { - "type": "string", - "enum": [ - "A" - ] - } - } - }, - { - "type": "object", - "required": [ - "data", - "type" - ], - "properties": { - "data": { - "type": "string", - "format": "ipv6" - }, - "type": { - "type": "string", - "enum": [ - "AAAA" - ] - } - } - }, - { - "type": "object", - "required": [ - "data", - "type" - ], - "properties": { - "data": { - "$ref": "#/definitions/Srv" - }, - "type": { - "type": "string", - "enum": [ - "SRV" - ] - } - } - } - ] - }, - "Generation": { - "description": "Generation numbers stored in the database, used for optimistic concurrency control", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "IpNet": { - "oneOf": [ - { - "title": "v4", - "allOf": [ - { - "$ref": "#/definitions/Ipv4Net" - } - ] - }, - { - "title": "v6", - "allOf": [ - { - "$ref": "#/definitions/Ipv6Net" - } - ] - } - ], - "x-rust-type": { - "crate": "oxnet", - "path": "oxnet::IpNet", - "version": "0.1.0" - } - }, - "Ipv4Net": { - "title": "An IPv4 subnet", - "description": "An IPv4 subnet, including prefix and prefix length", - "examples": [ - "192.168.1.0/24" - ], - "type": "string", - "pattern": "^(([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])/([0-9]|1[0-9]|2[0-9]|3[0-2])$", - "x-rust-type": { - "crate": "oxnet", - "path": "oxnet::Ipv4Net", - "version": "0.1.0" - } - }, - "Ipv6Net": { - "title": "An IPv6 subnet", - "description": "An IPv6 subnet, including prefix and subnet mask", - "examples": [ - "fd12:3456::/64" - ], - "type": "string", - "pattern": "^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))\\/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8])$", - "x-rust-type": { - "crate": "oxnet", - "path": "oxnet::Ipv6Net", - "version": "0.1.0" - } - }, - "MacAddr": { - "title": "A MAC address", - "description": "A Media Access Control address, in EUI-48 format", - "examples": [ - "ff:ff:ff:ff:ff:ff" - ], - "type": "string", - "maxLength": 17, - "minLength": 5, - "pattern": "^([0-9a-fA-F]{0,2}:){5}[0-9a-fA-F]{0,2}$" - }, - "Name": { - "title": "A name unique within the parent collection", - "description": "Names must begin with a lower case ASCII letter, be composed exclusively of lowercase ASCII, uppercase ASCII, numbers, and '-', and may not end with a '-'. Names cannot be a UUID, but they may contain a UUID. They can be at most 63 characters long.", - "type": "string", - "maxLength": 63, - "minLength": 1, - "pattern": "^(?![0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$)^[a-z]([a-zA-Z0-9-]*[a-zA-Z0-9]+)?$" - }, - "NetworkInterface": { - "description": "Information required to construct a virtual network interface", - "type": "object", - "required": [ - "id", - "ip", - "kind", - "mac", - "name", - "primary", - "slot", - "subnet", - "vni" - ], - "properties": { - "id": { - "type": "string", - "format": "uuid" - }, - "ip": { - "type": "string", - "format": "ip" - }, - "kind": { - "$ref": "#/definitions/NetworkInterfaceKind" - }, - "mac": { - "$ref": "#/definitions/MacAddr" - }, - "name": { - "$ref": "#/definitions/Name" - }, - "primary": { - "type": "boolean" - }, - "slot": { - "type": "integer", - "format": "uint8", - "minimum": 0.0 - }, - "subnet": { - "$ref": "#/definitions/IpNet" - }, - "transit_ips": { - "default": [], - "type": "array", - "items": { - "$ref": "#/definitions/IpNet" - } - }, - "vni": { - "$ref": "#/definitions/Vni" - } - } - }, - "NetworkInterfaceKind": { - "description": "The type of network interface", - "oneOf": [ - { - "description": "A vNIC attached to a guest instance", - "type": "object", - "required": [ - "id", - "type" - ], - "properties": { - "id": { - "type": "string", - "format": "uuid" - }, - "type": { - "type": "string", - "enum": [ - "instance" - ] - } - } - }, - { - "description": "A vNIC associated with an internal service", - "type": "object", - "required": [ - "id", - "type" - ], - "properties": { - "id": { - "type": "string", - "format": "uuid" - }, - "type": { - "type": "string", - "enum": [ - "service" - ] - } - } - }, - { - "description": "A vNIC associated with a probe", - "type": "object", - "required": [ - "id", - "type" - ], - "properties": { - "id": { - "type": "string", - "format": "uuid" - }, - "type": { - "type": "string", - "enum": [ - "probe" - ] - } - } - } - ] - }, - "OmicronPhysicalDiskConfig": { - "type": "object", - "required": [ - "id", - "identity", - "pool_id" - ], - "properties": { - "id": { - "type": "string", - "format": "uuid" - }, - "identity": { - "$ref": "#/definitions/DiskIdentity" - }, - "pool_id": { - "$ref": "#/definitions/TypedUuidForZpoolKind" - } - } - }, - "OmicronPhysicalDisksConfig": { - "type": "object", - "required": [ - "disks", - "generation" - ], - "properties": { - "disks": { - "type": "array", - "items": { - "$ref": "#/definitions/OmicronPhysicalDiskConfig" - } - }, - "generation": { - "description": "generation number of this configuration\n\nThis generation number is owned by the control plane (i.e., RSS or Nexus, depending on whether RSS-to-Nexus handoff has happened). It should not be bumped within Sled Agent.\n\nSled Agent rejects attempts to set the configuration to a generation older than the one it's currently running.", - "allOf": [ - { - "$ref": "#/definitions/Generation" - } - ] - } - } - }, - "OmicronZoneDataset": { - "description": "Describes a persistent ZFS dataset associated with an Omicron zone", - "type": "object", - "required": [ - "pool_name" - ], - "properties": { - "pool_name": { - "$ref": "#/definitions/ZpoolName" - } - } - }, - "OmicronZoneExternalFloatingAddr": { - "description": "Floating external address with port allocated to an Omicron-managed zone.", - "type": "object", - "required": [ - "addr", - "id" - ], - "properties": { - "addr": { - "type": "string" - }, - "id": { - "$ref": "#/definitions/TypedUuidForExternalIpKind" - } - } - }, - "OmicronZoneExternalFloatingIp": { - "description": "Floating external IP allocated to an Omicron-managed zone.\n\nThis is a slimmer `nexus_db_model::ExternalIp` that only stores the fields necessary for blueprint planning, and requires that the zone have a single IP.", - "type": "object", - "required": [ - "id", - "ip" - ], - "properties": { - "id": { - "$ref": "#/definitions/TypedUuidForExternalIpKind" - }, - "ip": { - "type": "string", - "format": "ip" - } - } - }, - "OmicronZoneExternalSnatIp": { - "description": "SNAT (outbound) external IP allocated to an Omicron-managed zone.\n\nThis is a slimmer `nexus_db_model::ExternalIp` that only stores the fields necessary for blueprint planning, and requires that the zone have a single IP.", - "type": "object", - "required": [ - "id", - "snat_cfg" - ], - "properties": { - "id": { - "$ref": "#/definitions/TypedUuidForExternalIpKind" - }, - "snat_cfg": { - "$ref": "#/definitions/SourceNatConfig" - } - } - }, - "SledConfig": { - "type": "object", - "required": [ - "disks", - "zones" - ], - "properties": { - "disks": { - "description": "Control plane disks configured for this sled", - "allOf": [ - { - "$ref": "#/definitions/OmicronPhysicalDisksConfig" - } - ] - }, - "zones": { - "description": "zones configured for this sled", - "type": "array", - "items": { - "$ref": "#/definitions/BlueprintZoneConfig" - } - } - } - }, - "SourceNatConfig": { - "description": "An IP address and port range used for source NAT, i.e., making outbound network connections from guests or services.", - "type": "object", - "required": [ - "first_port", - "ip", - "last_port" - ], - "properties": { - "first_port": { - "description": "The first port used for source NAT, inclusive.", - "type": "integer", - "format": "uint16", - "minimum": 0.0 - }, - "ip": { - "description": "The external address provided to the instance or service.", - "type": "string", - "format": "ip" - }, - "last_port": { - "description": "The last port used for source NAT, also inclusive.", - "type": "integer", - "format": "uint16", - "minimum": 0.0 - } - } - }, - "Srv": { - "type": "object", - "required": [ - "port", - "prio", - "target", - "weight" - ], - "properties": { - "port": { - "type": "integer", - "format": "uint16", - "minimum": 0.0 - }, - "prio": { - "type": "integer", - "format": "uint16", - "minimum": 0.0 - }, - "target": { - "type": "string" - }, - "weight": { - "type": "integer", - "format": "uint16", - "minimum": 0.0 - } - } - }, - "TypedUuidForExternalIpKind": { - "type": "string", - "format": "uuid" - }, - "TypedUuidForOmicronZoneKind": { - "type": "string", - "format": "uuid" - }, - "TypedUuidForZpoolKind": { - "type": "string", - "format": "uuid" - }, - "Vni": { - "description": "A Geneve Virtual Network Identifier", - "type": "integer", - "format": "uint32", - "minimum": 0.0 - }, - "ZpoolName": { - "title": "The name of a Zpool", - "description": "Zpool names are of the format ox{i,p}_. They are either Internal or External, and should be unique", - "type": "string", - "pattern": "^ox[ip]_[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" - } - } -} \ No newline at end of file diff --git a/schema/rss-service-plan-v5.json b/schema/rss-service-plan-v5.json deleted file mode 100644 index f4418eb8e4..0000000000 --- a/schema/rss-service-plan-v5.json +++ /dev/null @@ -1,1220 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Plan", - "type": "object", - "required": [ - "dns_config", - "services" - ], - "properties": { - "dns_config": { - "$ref": "#/definitions/DnsConfigParams" - }, - "services": { - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/SledConfig" - } - } - }, - "definitions": { - "BlueprintPhysicalDiskConfig": { - "description": "Information about an Omicron physical disk as recorded in a bluerprint.", - "type": "object", - "required": [ - "disposition", - "id", - "identity", - "pool_id" - ], - "properties": { - "disposition": { - "$ref": "#/definitions/BlueprintPhysicalDiskDisposition" - }, - "id": { - "$ref": "#/definitions/TypedUuidForPhysicalDiskKind" - }, - "identity": { - "$ref": "#/definitions/DiskIdentity" - }, - "pool_id": { - "$ref": "#/definitions/TypedUuidForZpoolKind" - } - } - }, - "BlueprintPhysicalDiskDisposition": { - "description": "The desired state of an Omicron-managed physical disk in a blueprint.", - "oneOf": [ - { - "description": "The physical disk is in-service.", - "type": "string", - "enum": [ - "in_service" - ] - }, - { - "description": "The physical disk is permanently gone.", - "type": "string", - "enum": [ - "expunged" - ] - } - ] - }, - "BlueprintPhysicalDisksConfig": { - "description": "Information about Omicron physical disks as recorded in a blueprint.\n\nPart of [`Blueprint`].", - "type": "object", - "required": [ - "disks", - "generation" - ], - "properties": { - "disks": { - "type": "array", - "items": { - "$ref": "#/definitions/BlueprintPhysicalDiskConfig" - } - }, - "generation": { - "$ref": "#/definitions/Generation" - } - } - }, - "BlueprintZoneConfig": { - "description": "Describes one Omicron-managed zone in a blueprint.\n\nPart of [`BlueprintZonesConfig`].", - "type": "object", - "required": [ - "disposition", - "id", - "zone_type" - ], - "properties": { - "disposition": { - "description": "The disposition (desired state) of this zone recorded in the blueprint.", - "allOf": [ - { - "$ref": "#/definitions/BlueprintZoneDisposition" - } - ] - }, - "filesystem_pool": { - "description": "zpool used for the zone's (transient) root filesystem", - "anyOf": [ - { - "$ref": "#/definitions/ZpoolName" - }, - { - "type": "null" - } - ] - }, - "id": { - "$ref": "#/definitions/TypedUuidForOmicronZoneKind" - }, - "zone_type": { - "$ref": "#/definitions/BlueprintZoneType" - } - } - }, - "BlueprintZoneDisposition": { - "description": "The desired state of an Omicron-managed zone in a blueprint.\n\nPart of [`BlueprintZoneConfig`].", - "oneOf": [ - { - "description": "The zone is in-service.", - "type": "string", - "enum": [ - "in_service" - ] - }, - { - "description": "The zone is not in service.", - "type": "string", - "enum": [ - "quiesced" - ] - }, - { - "description": "The zone is permanently gone.", - "type": "string", - "enum": [ - "expunged" - ] - } - ] - }, - "BlueprintZoneType": { - "oneOf": [ - { - "type": "object", - "required": [ - "address", - "dns_servers", - "external_ip", - "nic", - "ntp_servers", - "type" - ], - "properties": { - "address": { - "type": "string" - }, - "dns_servers": { - "type": "array", - "items": { - "type": "string", - "format": "ip" - } - }, - "domain": { - "type": [ - "string", - "null" - ] - }, - "external_ip": { - "$ref": "#/definitions/OmicronZoneExternalSnatIp" - }, - "nic": { - "description": "The service vNIC providing outbound connectivity using OPTE.", - "allOf": [ - { - "$ref": "#/definitions/NetworkInterface" - } - ] - }, - "ntp_servers": { - "type": "array", - "items": { - "type": "string" - } - }, - "type": { - "type": "string", - "enum": [ - "boundary_ntp" - ] - } - } - }, - { - "description": "Used in single-node clickhouse setups", - "type": "object", - "required": [ - "address", - "dataset", - "type" - ], - "properties": { - "address": { - "type": "string" - }, - "dataset": { - "$ref": "#/definitions/OmicronZoneDataset" - }, - "type": { - "type": "string", - "enum": [ - "clickhouse" - ] - } - } - }, - { - "type": "object", - "required": [ - "address", - "dataset", - "type" - ], - "properties": { - "address": { - "type": "string" - }, - "dataset": { - "$ref": "#/definitions/OmicronZoneDataset" - }, - "type": { - "type": "string", - "enum": [ - "clickhouse_keeper" - ] - } - } - }, - { - "description": "Used in replicated clickhouse setups", - "type": "object", - "required": [ - "address", - "dataset", - "type" - ], - "properties": { - "address": { - "type": "string" - }, - "dataset": { - "$ref": "#/definitions/OmicronZoneDataset" - }, - "type": { - "type": "string", - "enum": [ - "clickhouse_server" - ] - } - } - }, - { - "type": "object", - "required": [ - "address", - "dataset", - "type" - ], - "properties": { - "address": { - "type": "string" - }, - "dataset": { - "$ref": "#/definitions/OmicronZoneDataset" - }, - "type": { - "type": "string", - "enum": [ - "cockroach_db" - ] - } - } - }, - { - "type": "object", - "required": [ - "address", - "dataset", - "type" - ], - "properties": { - "address": { - "type": "string" - }, - "dataset": { - "$ref": "#/definitions/OmicronZoneDataset" - }, - "type": { - "type": "string", - "enum": [ - "crucible" - ] - } - } - }, - { - "type": "object", - "required": [ - "address", - "type" - ], - "properties": { - "address": { - "type": "string" - }, - "type": { - "type": "string", - "enum": [ - "crucible_pantry" - ] - } - } - }, - { - "type": "object", - "required": [ - "dataset", - "dns_address", - "http_address", - "nic", - "type" - ], - "properties": { - "dataset": { - "$ref": "#/definitions/OmicronZoneDataset" - }, - "dns_address": { - "description": "The address at which the external DNS server is reachable.", - "allOf": [ - { - "$ref": "#/definitions/OmicronZoneExternalFloatingAddr" - } - ] - }, - "http_address": { - "description": "The address at which the external DNS server API is reachable.", - "type": "string" - }, - "nic": { - "description": "The service vNIC providing external connectivity using OPTE.", - "allOf": [ - { - "$ref": "#/definitions/NetworkInterface" - } - ] - }, - "type": { - "type": "string", - "enum": [ - "external_dns" - ] - } - } - }, - { - "type": "object", - "required": [ - "dataset", - "dns_address", - "gz_address", - "gz_address_index", - "http_address", - "type" - ], - "properties": { - "dataset": { - "$ref": "#/definitions/OmicronZoneDataset" - }, - "dns_address": { - "type": "string" - }, - "gz_address": { - "description": "The addresses in the global zone which should be created\n\nFor the DNS service, which exists outside the sleds's typical subnet - adding an address in the GZ is necessary to allow inter-zone traffic routing.", - "type": "string", - "format": "ipv6" - }, - "gz_address_index": { - "description": "The address is also identified with an auxiliary bit of information to ensure that the created global zone address can have a unique name.", - "type": "integer", - "format": "uint32", - "minimum": 0.0 - }, - "http_address": { - "type": "string" - }, - "type": { - "type": "string", - "enum": [ - "internal_dns" - ] - } - } - }, - { - "type": "object", - "required": [ - "address", - "type" - ], - "properties": { - "address": { - "type": "string" - }, - "type": { - "type": "string", - "enum": [ - "internal_ntp" - ] - } - } - }, - { - "type": "object", - "required": [ - "external_dns_servers", - "external_ip", - "external_tls", - "internal_address", - "nic", - "type" - ], - "properties": { - "external_dns_servers": { - "description": "External DNS servers Nexus can use to resolve external hosts.", - "type": "array", - "items": { - "type": "string", - "format": "ip" - } - }, - "external_ip": { - "description": "The address at which the external nexus server is reachable.", - "allOf": [ - { - "$ref": "#/definitions/OmicronZoneExternalFloatingIp" - } - ] - }, - "external_tls": { - "description": "Whether Nexus's external endpoint should use TLS", - "type": "boolean" - }, - "internal_address": { - "description": "The address at which the internal nexus server is reachable.", - "type": "string" - }, - "nic": { - "description": "The service vNIC providing external connectivity using OPTE.", - "allOf": [ - { - "$ref": "#/definitions/NetworkInterface" - } - ] - }, - "type": { - "type": "string", - "enum": [ - "nexus" - ] - } - } - }, - { - "type": "object", - "required": [ - "address", - "type" - ], - "properties": { - "address": { - "type": "string" - }, - "type": { - "type": "string", - "enum": [ - "oximeter" - ] - } - } - } - ] - }, - "ByteCount": { - "description": "Byte count to express memory or storage capacity.", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "CompressionAlgorithm": { - "oneOf": [ - { - "type": "object", - "required": [ - "type" - ], - "properties": { - "type": { - "type": "string", - "enum": [ - "on" - ] - } - } - }, - { - "type": "object", - "required": [ - "type" - ], - "properties": { - "type": { - "type": "string", - "enum": [ - "off" - ] - } - } - }, - { - "type": "object", - "required": [ - "type" - ], - "properties": { - "type": { - "type": "string", - "enum": [ - "gzip" - ] - } - } - }, - { - "type": "object", - "required": [ - "level", - "type" - ], - "properties": { - "level": { - "$ref": "#/definitions/GzipLevel" - }, - "type": { - "type": "string", - "enum": [ - "gzip_n" - ] - } - } - }, - { - "type": "object", - "required": [ - "type" - ], - "properties": { - "type": { - "type": "string", - "enum": [ - "lz4" - ] - } - } - }, - { - "type": "object", - "required": [ - "type" - ], - "properties": { - "type": { - "type": "string", - "enum": [ - "lzjb" - ] - } - } - }, - { - "type": "object", - "required": [ - "type" - ], - "properties": { - "type": { - "type": "string", - "enum": [ - "zle" - ] - } - } - } - ] - }, - "DatasetConfig": { - "description": "Configuration information necessary to request a single dataset.\n\nThese datasets are tracked directly by Nexus.", - "type": "object", - "required": [ - "compression", - "id", - "name" - ], - "properties": { - "compression": { - "description": "The compression mode to be used by the dataset", - "allOf": [ - { - "$ref": "#/definitions/CompressionAlgorithm" - } - ] - }, - "id": { - "description": "The UUID of the dataset being requested", - "allOf": [ - { - "$ref": "#/definitions/TypedUuidForDatasetKind" - } - ] - }, - "name": { - "description": "The dataset's name", - "allOf": [ - { - "$ref": "#/definitions/DatasetName" - } - ] - }, - "quota": { - "description": "The upper bound on the amount of storage used by this dataset", - "anyOf": [ - { - "$ref": "#/definitions/ByteCount" - }, - { - "type": "null" - } - ] - }, - "reservation": { - "description": "The lower bound on the amount of storage usable by this dataset", - "anyOf": [ - { - "$ref": "#/definitions/ByteCount" - }, - { - "type": "null" - } - ] - } - } - }, - "DatasetKind": { - "description": "The kind of dataset. See the `DatasetKind` enum in omicron-common for possible values.", - "type": "string" - }, - "DatasetName": { - "type": "object", - "required": [ - "kind", - "pool_name" - ], - "properties": { - "kind": { - "$ref": "#/definitions/DatasetKind" - }, - "pool_name": { - "$ref": "#/definitions/ZpoolName" - } - } - }, - "DatasetsConfig": { - "type": "object", - "required": [ - "datasets", - "generation" - ], - "properties": { - "datasets": { - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/DatasetConfig" - } - }, - "generation": { - "description": "generation number of this configuration\n\nThis generation number is owned by the control plane (i.e., RSS or Nexus, depending on whether RSS-to-Nexus handoff has happened). It should not be bumped within Sled Agent.\n\nSled Agent rejects attempts to set the configuration to a generation older than the one it's currently running.\n\nNote that \"Generation::new()\", AKA, the first generation number, is reserved for \"no datasets\". This is the default configuration for a sled before any requests have been made.", - "allOf": [ - { - "$ref": "#/definitions/Generation" - } - ] - } - } - }, - "DiskIdentity": { - "description": "Uniquely identifies a disk.", - "type": "object", - "required": [ - "model", - "serial", - "vendor" - ], - "properties": { - "model": { - "type": "string" - }, - "serial": { - "type": "string" - }, - "vendor": { - "type": "string" - } - } - }, - "DnsConfigParams": { - "type": "object", - "required": [ - "generation", - "time_created", - "zones" - ], - "properties": { - "generation": { - "$ref": "#/definitions/Generation" - }, - "time_created": { - "type": "string", - "format": "date-time" - }, - "zones": { - "type": "array", - "items": { - "$ref": "#/definitions/DnsConfigZone" - } - } - } - }, - "DnsConfigZone": { - "type": "object", - "required": [ - "records", - "zone_name" - ], - "properties": { - "records": { - "type": "object", - "additionalProperties": { - "type": "array", - "items": { - "$ref": "#/definitions/DnsRecord" - } - } - }, - "zone_name": { - "type": "string" - } - } - }, - "DnsRecord": { - "oneOf": [ - { - "type": "object", - "required": [ - "data", - "type" - ], - "properties": { - "data": { - "type": "string", - "format": "ipv4" - }, - "type": { - "type": "string", - "enum": [ - "A" - ] - } - } - }, - { - "type": "object", - "required": [ - "data", - "type" - ], - "properties": { - "data": { - "type": "string", - "format": "ipv6" - }, - "type": { - "type": "string", - "enum": [ - "AAAA" - ] - } - } - }, - { - "type": "object", - "required": [ - "data", - "type" - ], - "properties": { - "data": { - "$ref": "#/definitions/Srv" - }, - "type": { - "type": "string", - "enum": [ - "SRV" - ] - } - } - } - ] - }, - "Generation": { - "description": "Generation numbers stored in the database, used for optimistic concurrency control", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "GzipLevel": { - "type": "integer", - "format": "uint8", - "minimum": 0.0 - }, - "IpNet": { - "oneOf": [ - { - "title": "v4", - "allOf": [ - { - "$ref": "#/definitions/Ipv4Net" - } - ] - }, - { - "title": "v6", - "allOf": [ - { - "$ref": "#/definitions/Ipv6Net" - } - ] - } - ], - "x-rust-type": { - "crate": "oxnet", - "path": "oxnet::IpNet", - "version": "0.1.0" - } - }, - "Ipv4Net": { - "title": "An IPv4 subnet", - "description": "An IPv4 subnet, including prefix and prefix length", - "examples": [ - "192.168.1.0/24" - ], - "type": "string", - "pattern": "^(([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])/([0-9]|1[0-9]|2[0-9]|3[0-2])$", - "x-rust-type": { - "crate": "oxnet", - "path": "oxnet::Ipv4Net", - "version": "0.1.0" - } - }, - "Ipv6Net": { - "title": "An IPv6 subnet", - "description": "An IPv6 subnet, including prefix and subnet mask", - "examples": [ - "fd12:3456::/64" - ], - "type": "string", - "pattern": "^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))\\/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8])$", - "x-rust-type": { - "crate": "oxnet", - "path": "oxnet::Ipv6Net", - "version": "0.1.0" - } - }, - "MacAddr": { - "title": "A MAC address", - "description": "A Media Access Control address, in EUI-48 format", - "examples": [ - "ff:ff:ff:ff:ff:ff" - ], - "type": "string", - "maxLength": 17, - "minLength": 5, - "pattern": "^([0-9a-fA-F]{0,2}:){5}[0-9a-fA-F]{0,2}$" - }, - "Name": { - "title": "A name unique within the parent collection", - "description": "Names must begin with a lower case ASCII letter, be composed exclusively of lowercase ASCII, uppercase ASCII, numbers, and '-', and may not end with a '-'. Names cannot be a UUID, but they may contain a UUID. They can be at most 63 characters long.", - "type": "string", - "maxLength": 63, - "minLength": 1, - "pattern": "^(?![0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$)^[a-z]([a-zA-Z0-9-]*[a-zA-Z0-9]+)?$" - }, - "NetworkInterface": { - "description": "Information required to construct a virtual network interface", - "type": "object", - "required": [ - "id", - "ip", - "kind", - "mac", - "name", - "primary", - "slot", - "subnet", - "vni" - ], - "properties": { - "id": { - "type": "string", - "format": "uuid" - }, - "ip": { - "type": "string", - "format": "ip" - }, - "kind": { - "$ref": "#/definitions/NetworkInterfaceKind" - }, - "mac": { - "$ref": "#/definitions/MacAddr" - }, - "name": { - "$ref": "#/definitions/Name" - }, - "primary": { - "type": "boolean" - }, - "slot": { - "type": "integer", - "format": "uint8", - "minimum": 0.0 - }, - "subnet": { - "$ref": "#/definitions/IpNet" - }, - "transit_ips": { - "default": [], - "type": "array", - "items": { - "$ref": "#/definitions/IpNet" - } - }, - "vni": { - "$ref": "#/definitions/Vni" - } - } - }, - "NetworkInterfaceKind": { - "description": "The type of network interface", - "oneOf": [ - { - "description": "A vNIC attached to a guest instance", - "type": "object", - "required": [ - "id", - "type" - ], - "properties": { - "id": { - "type": "string", - "format": "uuid" - }, - "type": { - "type": "string", - "enum": [ - "instance" - ] - } - } - }, - { - "description": "A vNIC associated with an internal service", - "type": "object", - "required": [ - "id", - "type" - ], - "properties": { - "id": { - "type": "string", - "format": "uuid" - }, - "type": { - "type": "string", - "enum": [ - "service" - ] - } - } - }, - { - "description": "A vNIC associated with a probe", - "type": "object", - "required": [ - "id", - "type" - ], - "properties": { - "id": { - "type": "string", - "format": "uuid" - }, - "type": { - "type": "string", - "enum": [ - "probe" - ] - } - } - } - ] - }, - "OmicronZoneDataset": { - "description": "Describes a persistent ZFS dataset associated with an Omicron zone", - "type": "object", - "required": [ - "pool_name" - ], - "properties": { - "pool_name": { - "$ref": "#/definitions/ZpoolName" - } - } - }, - "OmicronZoneExternalFloatingAddr": { - "description": "Floating external address with port allocated to an Omicron-managed zone.", - "type": "object", - "required": [ - "addr", - "id" - ], - "properties": { - "addr": { - "type": "string" - }, - "id": { - "$ref": "#/definitions/TypedUuidForExternalIpKind" - } - } - }, - "OmicronZoneExternalFloatingIp": { - "description": "Floating external IP allocated to an Omicron-managed zone.\n\nThis is a slimmer `nexus_db_model::ExternalIp` that only stores the fields necessary for blueprint planning, and requires that the zone have a single IP.", - "type": "object", - "required": [ - "id", - "ip" - ], - "properties": { - "id": { - "$ref": "#/definitions/TypedUuidForExternalIpKind" - }, - "ip": { - "type": "string", - "format": "ip" - } - } - }, - "OmicronZoneExternalSnatIp": { - "description": "SNAT (outbound) external IP allocated to an Omicron-managed zone.\n\nThis is a slimmer `nexus_db_model::ExternalIp` that only stores the fields necessary for blueprint planning, and requires that the zone have a single IP.", - "type": "object", - "required": [ - "id", - "snat_cfg" - ], - "properties": { - "id": { - "$ref": "#/definitions/TypedUuidForExternalIpKind" - }, - "snat_cfg": { - "$ref": "#/definitions/SourceNatConfig" - } - } - }, - "SledConfig": { - "type": "object", - "required": [ - "datasets", - "disks", - "zones" - ], - "properties": { - "datasets": { - "description": "Datasets configured for this sled", - "allOf": [ - { - "$ref": "#/definitions/DatasetsConfig" - } - ] - }, - "disks": { - "description": "Control plane disks configured for this sled", - "allOf": [ - { - "$ref": "#/definitions/BlueprintPhysicalDisksConfig" - } - ] - }, - "zones": { - "description": "zones configured for this sled", - "type": "array", - "items": { - "$ref": "#/definitions/BlueprintZoneConfig" - } - } - } - }, - "SourceNatConfig": { - "description": "An IP address and port range used for source NAT, i.e., making outbound network connections from guests or services.", - "type": "object", - "required": [ - "first_port", - "ip", - "last_port" - ], - "properties": { - "first_port": { - "description": "The first port used for source NAT, inclusive.", - "type": "integer", - "format": "uint16", - "minimum": 0.0 - }, - "ip": { - "description": "The external address provided to the instance or service.", - "type": "string", - "format": "ip" - }, - "last_port": { - "description": "The last port used for source NAT, also inclusive.", - "type": "integer", - "format": "uint16", - "minimum": 0.0 - } - } - }, - "Srv": { - "type": "object", - "required": [ - "port", - "prio", - "target", - "weight" - ], - "properties": { - "port": { - "type": "integer", - "format": "uint16", - "minimum": 0.0 - }, - "prio": { - "type": "integer", - "format": "uint16", - "minimum": 0.0 - }, - "target": { - "type": "string" - }, - "weight": { - "type": "integer", - "format": "uint16", - "minimum": 0.0 - } - } - }, - "TypedUuidForDatasetKind": { - "type": "string", - "format": "uuid" - }, - "TypedUuidForExternalIpKind": { - "type": "string", - "format": "uuid" - }, - "TypedUuidForOmicronZoneKind": { - "type": "string", - "format": "uuid" - }, - "TypedUuidForPhysicalDiskKind": { - "type": "string", - "format": "uuid" - }, - "TypedUuidForZpoolKind": { - "type": "string", - "format": "uuid" - }, - "Vni": { - "description": "A Geneve Virtual Network Identifier", - "type": "integer", - "format": "uint32", - "minimum": 0.0 - }, - "ZpoolName": { - "title": "The name of a Zpool", - "description": "Zpool names are of the format ox{i,p}_. They are either Internal or External, and should be unique", - "type": "string", - "pattern": "^ox[ip]_[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" - } - } -} \ No newline at end of file diff --git a/schema/rss-sled-plan.json b/schema/rss-sled-plan.json deleted file mode 100644 index fbfc92cb7e..0000000000 --- a/schema/rss-sled-plan.json +++ /dev/null @@ -1,1187 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Plan", - "type": "object", - "required": [ - "config", - "rack_id", - "sleds" - ], - "properties": { - "config": { - "$ref": "#/definitions/RackInitializeRequest" - }, - "rack_id": { - "type": "string", - "format": "uuid" - }, - "sleds": { - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/StartSledAgentRequest" - } - } - }, - "definitions": { - "AllowedSourceIps": { - "description": "Description of source IPs allowed to reach rack services.", - "oneOf": [ - { - "description": "Allow traffic from any external IP address.", - "type": "object", - "required": [ - "allow" - ], - "properties": { - "allow": { - "type": "string", - "enum": [ - "any" - ] - } - } - }, - { - "description": "Restrict access to a specific set of source IP addresses or subnets.\n\nAll others are prevented from reaching rack services.", - "type": "object", - "required": [ - "allow", - "ips" - ], - "properties": { - "allow": { - "type": "string", - "enum": [ - "list" - ] - }, - "ips": { - "type": "array", - "items": { - "$ref": "#/definitions/IpNet" - } - } - } - } - ] - }, - "Baseboard": { - "description": "Describes properties that should uniquely identify a Gimlet.", - "oneOf": [ - { - "type": "object", - "required": [ - "identifier", - "model", - "revision", - "type" - ], - "properties": { - "identifier": { - "type": "string" - }, - "model": { - "type": "string" - }, - "revision": { - "type": "integer", - "format": "uint32", - "minimum": 0.0 - }, - "type": { - "type": "string", - "enum": [ - "gimlet" - ] - } - } - }, - { - "type": "object", - "required": [ - "type" - ], - "properties": { - "type": { - "type": "string", - "enum": [ - "unknown" - ] - } - } - }, - { - "type": "object", - "required": [ - "identifier", - "model", - "type" - ], - "properties": { - "identifier": { - "type": "string" - }, - "model": { - "type": "string" - }, - "type": { - "type": "string", - "enum": [ - "pc" - ] - } - } - } - ] - }, - "BfdMode": { - "description": "BFD connection mode.", - "type": "string", - "enum": [ - "single_hop", - "multi_hop" - ] - }, - "BfdPeerConfig": { - "type": "object", - "required": [ - "detection_threshold", - "mode", - "remote", - "required_rx", - "switch" - ], - "properties": { - "detection_threshold": { - "type": "integer", - "format": "uint8", - "minimum": 0.0 - }, - "local": { - "type": [ - "string", - "null" - ], - "format": "ip" - }, - "mode": { - "$ref": "#/definitions/BfdMode" - }, - "remote": { - "type": "string", - "format": "ip" - }, - "required_rx": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "switch": { - "$ref": "#/definitions/SwitchLocation" - } - } - }, - "BgpConfig": { - "type": "object", - "required": [ - "asn", - "originate" - ], - "properties": { - "asn": { - "description": "The autonomous system number for the BGP configuration.", - "type": "integer", - "format": "uint32", - "minimum": 0.0 - }, - "checker": { - "description": "Checker to apply to incoming messages.", - "default": null, - "type": [ - "string", - "null" - ] - }, - "originate": { - "description": "The set of prefixes for the BGP router to originate.", - "type": "array", - "items": { - "$ref": "#/definitions/Ipv4Net" - } - }, - "shaper": { - "description": "Shaper to apply to outgoing messages.", - "default": null, - "type": [ - "string", - "null" - ] - } - } - }, - "BgpPeerConfig": { - "type": "object", - "required": [ - "addr", - "asn", - "port" - ], - "properties": { - "addr": { - "description": "Address of the peer.", - "type": "string", - "format": "ipv4" - }, - "allowed_export": { - "description": "Define export policy for a peer.", - "default": { - "type": "no_filtering" - }, - "allOf": [ - { - "$ref": "#/definitions/ImportExportPolicy" - } - ] - }, - "allowed_import": { - "description": "Define import policy for a peer.", - "default": { - "type": "no_filtering" - }, - "allOf": [ - { - "$ref": "#/definitions/ImportExportPolicy" - } - ] - }, - "asn": { - "description": "The autonomous system number of the router the peer belongs to.", - "type": "integer", - "format": "uint32", - "minimum": 0.0 - }, - "communities": { - "description": "Include the provided communities in updates sent to the peer.", - "default": [], - "type": "array", - "items": { - "type": "integer", - "format": "uint32", - "minimum": 0.0 - } - }, - "connect_retry": { - "description": "The interval in seconds between peer connection retry attempts.", - "type": [ - "integer", - "null" - ], - "format": "uint64", - "minimum": 0.0 - }, - "delay_open": { - "description": "How long to delay sending open messages to a peer. In seconds.", - "type": [ - "integer", - "null" - ], - "format": "uint64", - "minimum": 0.0 - }, - "enforce_first_as": { - "description": "Enforce that the first AS in paths received from this peer is the peer's AS.", - "default": false, - "type": "boolean" - }, - "hold_time": { - "description": "How long to keep a session alive without a keepalive in seconds. Defaults to 6.", - "type": [ - "integer", - "null" - ], - "format": "uint64", - "minimum": 0.0 - }, - "idle_hold_time": { - "description": "How long to keep a peer in idle after a state machine reset in seconds.", - "type": [ - "integer", - "null" - ], - "format": "uint64", - "minimum": 0.0 - }, - "keepalive": { - "description": "The interval to send keepalive messages at.", - "type": [ - "integer", - "null" - ], - "format": "uint64", - "minimum": 0.0 - }, - "local_pref": { - "description": "Apply a local preference to routes received from this peer.", - "default": null, - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 - }, - "md5_auth_key": { - "description": "Use the given key for TCP-MD5 authentication with the peer.", - "default": null, - "type": [ - "string", - "null" - ] - }, - "min_ttl": { - "description": "Require messages from a peer have a minimum IP time to live field.", - "default": null, - "type": [ - "integer", - "null" - ], - "format": "uint8", - "minimum": 0.0 - }, - "multi_exit_discriminator": { - "description": "Apply the provided multi-exit discriminator (MED) updates sent to the peer.", - "default": null, - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 - }, - "port": { - "description": "Switch port the peer is reachable on.", - "type": "string" - }, - "remote_asn": { - "description": "Require that a peer has a specified ASN.", - "default": null, - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 - }, - "vlan_id": { - "description": "Associate a VLAN ID with a BGP peer session.", - "default": null, - "type": [ - "integer", - "null" - ], - "format": "uint16", - "minimum": 0.0 - } - } - }, - "BootstrapAddressDiscovery": { - "oneOf": [ - { - "description": "Ignore all bootstrap addresses except our own.", - "type": "object", - "required": [ - "type" - ], - "properties": { - "type": { - "type": "string", - "enum": [ - "only_ours" - ] - } - } - }, - { - "description": "Ignore all bootstrap addresses except the following.", - "type": "object", - "required": [ - "addrs", - "type" - ], - "properties": { - "addrs": { - "type": "array", - "items": { - "type": "string", - "format": "ipv6" - }, - "uniqueItems": true - }, - "type": { - "type": "string", - "enum": [ - "only_these" - ] - } - } - } - ] - }, - "Certificate": { - "type": "object", - "required": [ - "cert", - "key" - ], - "properties": { - "cert": { - "type": "string" - }, - "key": { - "type": "string" - } - } - }, - "ImportExportPolicy": { - "description": "Define policy relating to the import and export of prefixes from a BGP peer.", - "oneOf": [ - { - "description": "Do not perform any filtering.", - "type": "object", - "required": [ - "type" - ], - "properties": { - "type": { - "type": "string", - "enum": [ - "no_filtering" - ] - } - } - }, - { - "type": "object", - "required": [ - "type", - "value" - ], - "properties": { - "type": { - "type": "string", - "enum": [ - "allow" - ] - }, - "value": { - "type": "array", - "items": { - "$ref": "#/definitions/IpNet" - } - } - } - } - ] - }, - "IpNet": { - "oneOf": [ - { - "title": "v4", - "allOf": [ - { - "$ref": "#/definitions/Ipv4Net" - } - ] - }, - { - "title": "v6", - "allOf": [ - { - "$ref": "#/definitions/Ipv6Net" - } - ] - } - ], - "x-rust-type": { - "crate": "oxnet", - "path": "oxnet::IpNet", - "version": "0.1.0" - } - }, - "IpRange": { - "oneOf": [ - { - "title": "v4", - "allOf": [ - { - "$ref": "#/definitions/Ipv4Range" - } - ] - }, - { - "title": "v6", - "allOf": [ - { - "$ref": "#/definitions/Ipv6Range" - } - ] - } - ] - }, - "Ipv4Net": { - "title": "An IPv4 subnet", - "description": "An IPv4 subnet, including prefix and prefix length", - "examples": [ - "192.168.1.0/24" - ], - "type": "string", - "pattern": "^(([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])/([0-9]|1[0-9]|2[0-9]|3[0-2])$", - "x-rust-type": { - "crate": "oxnet", - "path": "oxnet::Ipv4Net", - "version": "0.1.0" - } - }, - "Ipv4Range": { - "description": "A non-decreasing IPv4 address range, inclusive of both ends.\n\nThe first address must be less than or equal to the last address.", - "type": "object", - "required": [ - "first", - "last" - ], - "properties": { - "first": { - "type": "string", - "format": "ipv4" - }, - "last": { - "type": "string", - "format": "ipv4" - } - } - }, - "Ipv6Net": { - "title": "An IPv6 subnet", - "description": "An IPv6 subnet, including prefix and subnet mask", - "examples": [ - "fd12:3456::/64" - ], - "type": "string", - "pattern": "^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))\\/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8])$", - "x-rust-type": { - "crate": "oxnet", - "path": "oxnet::Ipv6Net", - "version": "0.1.0" - } - }, - "Ipv6Range": { - "description": "A non-decreasing IPv6 address range, inclusive of both ends.\n\nThe first address must be less than or equal to the last address.", - "type": "object", - "required": [ - "first", - "last" - ], - "properties": { - "first": { - "type": "string", - "format": "ipv6" - }, - "last": { - "type": "string", - "format": "ipv6" - } - } - }, - "Ipv6Subnet": { - "description": "Wraps an [`Ipv6Net`] with a compile-time prefix length.", - "type": "object", - "required": [ - "net" - ], - "properties": { - "net": { - "$ref": "#/definitions/Ipv6Net" - } - } - }, - "LldpAdminStatus": { - "description": "To what extent should this port participate in LLDP", - "type": "string", - "enum": [ - "enabled", - "disabled", - "rx_only", - "tx_only" - ] - }, - "LldpPortConfig": { - "description": "Per-port LLDP configuration settings. Only the \"status\" setting is mandatory. All other fields have natural defaults or may be inherited from the switch.", - "type": "object", - "required": [ - "status" - ], - "properties": { - "chassis_id": { - "description": "Chassis ID to advertise. If this is set, it will be advertised as a LocallyAssigned ID type. If this is not set, it will be inherited from the switch-level settings.", - "type": [ - "string", - "null" - ] - }, - "management_addrs": { - "description": "Management IP addresses to advertise. If this is not set, it will be inherited from the switch-level settings.", - "type": [ - "array", - "null" - ], - "items": { - "type": "string", - "format": "ip" - } - }, - "port_description": { - "description": "Port description to advertise. If this is not set, no description will be advertised.", - "type": [ - "string", - "null" - ] - }, - "port_id": { - "description": "Port ID to advertise. If this is set, it will be advertised as a LocallyAssigned ID type. If this is not set, it will be set to the port name. e.g., qsfp0/0.", - "type": [ - "string", - "null" - ] - }, - "status": { - "description": "To what extent should this port participate in LLDP", - "allOf": [ - { - "$ref": "#/definitions/LldpAdminStatus" - } - ] - }, - "system_description": { - "description": "System description to advertise. If this is not set, it will be inherited from the switch-level settings.", - "type": [ - "string", - "null" - ] - }, - "system_name": { - "description": "System name to advertise. If this is not set, it will be inherited from the switch-level settings.", - "type": [ - "string", - "null" - ] - } - } - }, - "Name": { - "title": "A name unique within the parent collection", - "description": "Names must begin with a lower case ASCII letter, be composed exclusively of lowercase ASCII, uppercase ASCII, numbers, and '-', and may not end with a '-'. Names cannot be a UUID, but they may contain a UUID. They can be at most 63 characters long.", - "type": "string", - "maxLength": 63, - "minLength": 1, - "pattern": "^(?![0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$)^[a-z]([a-zA-Z0-9-]*[a-zA-Z0-9]+)?$" - }, - "NewPasswordHash": { - "title": "A password hash in PHC string format", - "description": "Password hashes must be in PHC (Password Hashing Competition) string format. Passwords must be hashed with Argon2id. Password hashes may be rejected if the parameters appear not to be secure enough.", - "type": "string" - }, - "PortConfigV2": { - "type": "object", - "required": [ - "addresses", - "bgp_peers", - "port", - "routes", - "switch", - "uplink_port_speed" - ], - "properties": { - "addresses": { - "description": "This port's addresses and optional vlan IDs", - "type": "array", - "items": { - "$ref": "#/definitions/UplinkAddressConfig" - } - }, - "autoneg": { - "description": "Whether or not to set autonegotiation", - "default": false, - "type": "boolean" - }, - "bgp_peers": { - "description": "BGP peers on this port", - "type": "array", - "items": { - "$ref": "#/definitions/BgpPeerConfig" - } - }, - "lldp": { - "description": "LLDP configuration for this port", - "anyOf": [ - { - "$ref": "#/definitions/LldpPortConfig" - }, - { - "type": "null" - } - ] - }, - "port": { - "description": "Nmae of the port this config applies to.", - "type": "string" - }, - "routes": { - "description": "The set of routes associated with this port.", - "type": "array", - "items": { - "$ref": "#/definitions/RouteConfig" - } - }, - "switch": { - "description": "Switch the port belongs to.", - "allOf": [ - { - "$ref": "#/definitions/SwitchLocation" - } - ] - }, - "tx_eq": { - "description": "TX-EQ configuration for this port", - "anyOf": [ - { - "$ref": "#/definitions/TxEqConfig" - }, - { - "type": "null" - } - ] - }, - "uplink_port_fec": { - "description": "Port forward error correction type.", - "anyOf": [ - { - "$ref": "#/definitions/PortFec" - }, - { - "type": "null" - } - ] - }, - "uplink_port_speed": { - "description": "Port speed.", - "allOf": [ - { - "$ref": "#/definitions/PortSpeed" - } - ] - } - } - }, - "PortFec": { - "description": "Switchport FEC options", - "type": "string", - "enum": [ - "firecode", - "none", - "rs" - ] - }, - "PortSpeed": { - "description": "Switchport Speed options", - "type": "string", - "enum": [ - "speed0_g", - "speed1_g", - "speed10_g", - "speed25_g", - "speed40_g", - "speed50_g", - "speed100_g", - "speed200_g", - "speed400_g" - ] - }, - "RackInitializeRequest": { - "description": "Configuration for the \"rack setup service\".\n\nThe Rack Setup Service should be responsible for one-time setup actions, such as CockroachDB placement and initialization. Without operator intervention, however, these actions need a way to be automated in our deployment.", - "type": "object", - "required": [ - "bootstrap_discovery", - "dns_servers", - "external_certificates", - "external_dns_ips", - "external_dns_zone_name", - "internal_services_ip_pool_ranges", - "ntp_servers", - "rack_network_config", - "recovery_silo" - ], - "properties": { - "allowed_source_ips": { - "description": "IPs or subnets allowed to make requests to user-facing services", - "default": { - "allow": "any" - }, - "allOf": [ - { - "$ref": "#/definitions/AllowedSourceIps" - } - ] - }, - "bootstrap_discovery": { - "description": "Describes how bootstrap addresses should be collected during RSS.", - "allOf": [ - { - "$ref": "#/definitions/BootstrapAddressDiscovery" - } - ] - }, - "dns_servers": { - "description": "The external DNS server addresses.", - "type": "array", - "items": { - "type": "string", - "format": "ip" - } - }, - "external_certificates": { - "description": "initial TLS certificates for the external API", - "type": "array", - "items": { - "$ref": "#/definitions/Certificate" - } - }, - "external_dns_ips": { - "description": "Service IP addresses on which we run external DNS servers.\n\nEach address must be present in `internal_services_ip_pool_ranges`.", - "type": "array", - "items": { - "type": "string", - "format": "ip" - } - }, - "external_dns_zone_name": { - "description": "DNS name for the DNS zone delegated to the rack for external DNS", - "type": "string" - }, - "internal_services_ip_pool_ranges": { - "description": "Ranges of the service IP pool which may be used for internal services.", - "type": "array", - "items": { - "$ref": "#/definitions/IpRange" - } - }, - "ntp_servers": { - "description": "The external NTP server addresses.", - "type": "array", - "items": { - "type": "string" - } - }, - "rack_network_config": { - "description": "Initial rack network configuration", - "allOf": [ - { - "$ref": "#/definitions/RackNetworkConfigV2" - } - ] - }, - "recovery_silo": { - "description": "Configuration of the Recovery Silo (the initial Silo)", - "allOf": [ - { - "$ref": "#/definitions/RecoverySiloConfig" - } - ] - }, - "trust_quorum_peers": { - "description": "The set of peer_ids required to initialize trust quorum\n\nThe value is `None` if we are not using trust quorum", - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/Baseboard" - } - } - } - }, - "RackNetworkConfigV2": { - "description": "Initial network configuration", - "type": "object", - "required": [ - "bgp", - "infra_ip_first", - "infra_ip_last", - "ports", - "rack_subnet" - ], - "properties": { - "bfd": { - "description": "BFD configuration for connecting the rack to external networks", - "default": [], - "type": "array", - "items": { - "$ref": "#/definitions/BfdPeerConfig" - } - }, - "bgp": { - "description": "BGP configurations for connecting the rack to external networks", - "type": "array", - "items": { - "$ref": "#/definitions/BgpConfig" - } - }, - "infra_ip_first": { - "description": "First ip address to be used for configuring network infrastructure", - "type": "string", - "format": "ipv4" - }, - "infra_ip_last": { - "description": "Last ip address to be used for configuring network infrastructure", - "type": "string", - "format": "ipv4" - }, - "ports": { - "description": "Uplinks for connecting the rack to external networks", - "type": "array", - "items": { - "$ref": "#/definitions/PortConfigV2" - } - }, - "rack_subnet": { - "$ref": "#/definitions/Ipv6Net" - } - } - }, - "RecoverySiloConfig": { - "type": "object", - "required": [ - "silo_name", - "user_name", - "user_password_hash" - ], - "properties": { - "silo_name": { - "$ref": "#/definitions/Name" - }, - "user_name": { - "$ref": "#/definitions/UserId" - }, - "user_password_hash": { - "$ref": "#/definitions/NewPasswordHash" - } - } - }, - "RouteConfig": { - "type": "object", - "required": [ - "destination", - "nexthop" - ], - "properties": { - "destination": { - "description": "The destination of the route.", - "allOf": [ - { - "$ref": "#/definitions/IpNet" - } - ] - }, - "nexthop": { - "description": "The nexthop/gateway address.", - "type": "string", - "format": "ip" - }, - "rib_priority": { - "description": "The RIB priority (i.e. Admin Distance) associated with this route.", - "default": null, - "type": [ - "integer", - "null" - ], - "format": "uint8", - "minimum": 0.0 - }, - "vlan_id": { - "description": "The VLAN id associated with this route.", - "default": null, - "type": [ - "integer", - "null" - ], - "format": "uint16", - "minimum": 0.0 - } - } - }, - "StartSledAgentRequest": { - "description": "Configuration information for launching a Sled Agent.", - "type": "object", - "required": [ - "body", - "generation", - "schema_version" - ], - "properties": { - "body": { - "$ref": "#/definitions/StartSledAgentRequestBody" - }, - "generation": { - "description": "The current generation number of data as stored in CRDB.\n\nThe initial generation is set during RSS time and then only mutated by Nexus. For now, we don't actually anticipate mutating this data, but we leave open the possiblity.", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "schema_version": { - "type": "integer", - "format": "uint32", - "minimum": 0.0 - } - } - }, - "StartSledAgentRequestBody": { - "description": "This is the actual app level data of `StartSledAgentRequest`\n\nWe nest it below the \"header\" of `generation` and `schema_version` so that we can perform partial deserialization of `EarlyNetworkConfig` to only read the header and defer deserialization of the body once we know the schema version. This is possible via the use of [`serde_json::value::RawValue`] in future (post-v1) deserialization paths.", - "type": "object", - "required": [ - "id", - "is_lrtq_learner", - "rack_id", - "subnet", - "use_trust_quorum" - ], - "properties": { - "id": { - "description": "Uuid of the Sled Agent to be created.", - "allOf": [ - { - "$ref": "#/definitions/TypedUuidForSledKind" - } - ] - }, - "is_lrtq_learner": { - "description": "Is this node an LRTQ learner node?\n\nWe only put the node into learner mode if `use_trust_quorum` is also true.", - "type": "boolean" - }, - "rack_id": { - "description": "Uuid of the rack to which this sled agent belongs.", - "type": "string", - "format": "uuid" - }, - "subnet": { - "description": "Portion of the IP space to be managed by the Sled Agent.", - "allOf": [ - { - "$ref": "#/definitions/Ipv6Subnet" - } - ] - }, - "use_trust_quorum": { - "description": "Use trust quorum for key generation", - "type": "boolean" - } - } - }, - "SwitchLocation": { - "description": "Identifies switch physical location", - "oneOf": [ - { - "description": "Switch in upper slot", - "type": "string", - "enum": [ - "switch0" - ] - }, - { - "description": "Switch in lower slot", - "type": "string", - "enum": [ - "switch1" - ] - } - ] - }, - "TxEqConfig": { - "description": "Per-port tx-eq overrides. This can be used to fine-tune the transceiver equalization settings to improve signal integrity.", - "type": "object", - "properties": { - "main": { - "description": "Main tap", - "type": [ - "integer", - "null" - ], - "format": "int32" - }, - "post1": { - "description": "Post-cursor tap1", - "type": [ - "integer", - "null" - ], - "format": "int32" - }, - "post2": { - "description": "Post-cursor tap2", - "type": [ - "integer", - "null" - ], - "format": "int32" - }, - "pre1": { - "description": "Pre-cursor tap1", - "type": [ - "integer", - "null" - ], - "format": "int32" - }, - "pre2": { - "description": "Pre-cursor tap2", - "type": [ - "integer", - "null" - ], - "format": "int32" - } - } - }, - "TypedUuidForSledKind": { - "type": "string", - "format": "uuid" - }, - "UplinkAddressConfig": { - "type": "object", - "required": [ - "address" - ], - "properties": { - "address": { - "$ref": "#/definitions/IpNet" - }, - "vlan_id": { - "description": "The VLAN id (if any) associated with this address.", - "default": null, - "type": [ - "integer", - "null" - ], - "format": "uint16", - "minimum": 0.0 - } - } - }, - "UserId": { - "title": "A username for a local-only user", - "description": "Usernames must begin with a lower case ASCII letter, be composed exclusively of lowercase ASCII, uppercase ASCII, numbers, and '-', and may not end with a '-'. Usernames cannot be a UUID, but they may contain a UUID. They can be at most 63 characters long.", - "type": "string", - "maxLength": 63, - "minLength": 1, - "pattern": "^(?![0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$)^[a-z]([a-zA-Z0-9-]*[a-zA-Z0-9]+)?$" - } - } -} \ No newline at end of file diff --git a/sled-agent/src/rack_setup/plan/service.rs b/sled-agent/src/rack_setup/plan/service.rs index cb188fa442..8d5e3223f7 100644 --- a/sled-agent/src/rack_setup/plan/service.rs +++ b/sled-agent/src/rack_setup/plan/service.rs @@ -4,7 +4,6 @@ //! Plan generation for "where should services be initialized". -use camino::Utf8PathBuf; use illumos_utils::zpool::ZpoolName; use internal_dns_types::config::{ DnsConfigBuilder, DnsConfigParams, Host, Zone, @@ -38,7 +37,6 @@ use omicron_common::disk::{ CompressionAlgorithm, DatasetConfig, DatasetKind, DatasetName, DatasetsConfig, DiskVariant, SharedDatasetConfig, }; -use omicron_common::ledger::{self, Ledger, Ledgerable}; use omicron_common::policy::{ BOUNDARY_NTP_REDUNDANCY, COCKROACHDB_REDUNDANCY, CRUCIBLE_PANTRY_REDUNDANCY, INTERNAL_DNS_REDUNDANCY, NEXUS_REDUNDANCY, @@ -57,8 +55,6 @@ use sled_agent_client::{ }; use sled_agent_types::rack_init::RackInitializeRequest as Config; use sled_agent_types::sled::StartSledAgentRequest; -use sled_storage::dataset::CONFIG_DATASET; -use sled_storage::manager::StorageHandle; use slog::Logger; use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV6}; @@ -78,9 +74,6 @@ pub enum PlanError { err: std::io::Error, }, - #[error("Failed to access ledger: {0}")] - Ledger(#[from] ledger::Error), - #[error("Error making HTTP request to Sled Agent: {0}")] SledApi(#[from] SledAgentError), @@ -98,18 +91,6 @@ pub enum PlanError { #[error("Unexpected dataset kind: {0}")] UnexpectedDataset(String), - - #[error("Found only v1 service plan")] - FoundV1, - - #[error("Found only v2 service plan")] - FoundV2, - - #[error("Found only v3 service plan")] - FoundV3, - - #[error("Found only v4 service plan")] - FoundV4, } #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] @@ -182,18 +163,6 @@ pub struct Plan { pub dns_config: DnsConfigParams, } -impl Ledgerable for Plan { - fn is_newer_than(&self, _other: &Self) -> bool { - true - } - fn generation_bump(&mut self) {} -} -const RSS_SERVICE_PLAN_V1_FILENAME: &str = "rss-service-plan.json"; -const RSS_SERVICE_PLAN_V2_FILENAME: &str = "rss-service-plan-v2.json"; -const RSS_SERVICE_PLAN_V3_FILENAME: &str = "rss-service-plan-v3.json"; -const RSS_SERVICE_PLAN_V4_FILENAME: &str = "rss-service-plan-v4.json"; -const RSS_SERVICE_PLAN_FILENAME: &str = "rss-service-plan-v5.json"; - pub fn from_sockaddr_to_external_floating_addr( addr: SocketAddr, ) -> OmicronZoneExternalFloatingAddr { @@ -240,160 +209,6 @@ pub fn from_source_nat_config_to_external_snat_ip( } impl Plan { - pub async fn load( - log: &Logger, - storage_manager: &StorageHandle, - ) -> Result, PlanError> { - let paths: Vec = storage_manager - .get_latest_disks() - .await - .all_m2_mountpoints(CONFIG_DATASET) - .into_iter() - .map(|p| p.join(RSS_SERVICE_PLAN_FILENAME)) - .collect(); - - // If we already created a plan for this RSS to allocate - // services to sleds, re-use that existing plan. - let ledger = Ledger::::new(log, paths.clone()).await; - - if let Some(ledger) = ledger { - info!(log, "RSS plan already created, loading from file"); - Ok(Some(ledger.data().clone())) - } else if Self::has_v1(storage_manager).await.map_err(|err| { - PlanError::Io { - message: String::from("looking for v1 RSS plan"), - err, - } - })? { - // If we found no current-version service plan, but we _do_ find - // a v1 plan present, bail out. We do not expect to ever see this - // in practice because that would indicate that: - // - // - We ran RSS previously on this same system using an older - // version of the software that generates v1 service plans and it - // got far enough through RSS to have written the v1 service plan. - // - That means it must have finished initializing all sled agents, - // including itself, causing it to record a - // `StartSledAgentRequest`s in its ledger -- while still running - // the older RSS. - // - But we're currently running software that knows about v2 - // service plans. Thus, this process started some time after that - // ledger was written. - // - But the bootstrap agent refuses to execute RSS if it has a - // local `StartSledAgentRequest` ledgered. So we shouldn't get - // here if all of the above happened. - // - // This sounds like a complicated set of assumptions. If we got - // this wrong, we'll fail spuriously here and we'll have to figure - // out what happened. But the alternative is doing extra work to - // support a condition that we do not believe can ever happen in any - // system. - Err(PlanError::FoundV1) - } else if Self::has_v2(storage_manager).await.map_err(|err| { - // Same as the comment above, but for version 2. - PlanError::Io { - message: String::from("looking for v2 RSS plan"), - err, - } - })? { - Err(PlanError::FoundV2) - } else if Self::has_v3(storage_manager).await.map_err(|err| { - // Same as the comment above, but for version 3. - PlanError::Io { - message: String::from("looking for v3 RSS plan"), - err, - } - })? { - Err(PlanError::FoundV3) - } else if Self::has_v4(storage_manager).await.map_err(|err| { - // Same as the comment above, but for version 4. - PlanError::Io { - message: String::from("looking for v4 RSS plan"), - err, - } - })? { - Err(PlanError::FoundV4) - } else { - Ok(None) - } - } - - async fn has_v1( - storage_manager: &StorageHandle, - ) -> Result { - let paths = storage_manager - .get_latest_disks() - .await - .all_m2_mountpoints(CONFIG_DATASET) - .into_iter() - .map(|p| p.join(RSS_SERVICE_PLAN_V1_FILENAME)); - - for p in paths { - if p.try_exists()? { - return Ok(true); - } - } - - Ok(false) - } - - async fn has_v2( - storage_manager: &StorageHandle, - ) -> Result { - let paths = storage_manager - .get_latest_disks() - .await - .all_m2_mountpoints(CONFIG_DATASET) - .into_iter() - .map(|p| p.join(RSS_SERVICE_PLAN_V2_FILENAME)); - - for p in paths { - if p.try_exists()? { - return Ok(true); - } - } - - Ok(false) - } - - async fn has_v3( - storage_manager: &StorageHandle, - ) -> Result { - let paths = storage_manager - .get_latest_disks() - .await - .all_m2_mountpoints(CONFIG_DATASET) - .into_iter() - .map(|p| p.join(RSS_SERVICE_PLAN_V3_FILENAME)); - - for p in paths { - if p.try_exists()? { - return Ok(true); - } - } - - Ok(false) - } - - async fn has_v4( - storage_manager: &StorageHandle, - ) -> Result { - let paths = storage_manager - .get_latest_disks() - .await - .all_m2_mountpoints(CONFIG_DATASET) - .into_iter() - .map(|p| p.join(RSS_SERVICE_PLAN_V4_FILENAME)); - - for p in paths { - if p.try_exists()? { - return Ok(true); - } - } - - Ok(false) - } - async fn is_sled_scrimlet( log: &Logger, address: SocketAddrV6, @@ -939,7 +754,6 @@ impl Plan { pub async fn create( log: &Logger, config: &Config, - storage_manager: &StorageHandle, sleds: &BTreeMap, ) -> Result { // Load the information we need about each Sled to be able to allocate @@ -968,18 +782,6 @@ impl Plan { }; let plan = Self::create_transient(config, sled_info)?; - - // Once we've constructed a plan, write it down to durable storage. - let paths: Vec = storage_manager - .get_latest_disks() - .await - .all_m2_mountpoints(CONFIG_DATASET) - .into_iter() - .map(|p| p.join(RSS_SERVICE_PLAN_FILENAME)) - .collect(); - let mut ledger = Ledger::::new_with(log, paths, plan.clone()); - ledger.commit().await?; - info!(log, "Service plan written to storage"); Ok(plan) } } @@ -1483,15 +1285,6 @@ mod tests { assert_eq!(internal_service_ips, expected_internal_service_ips); } - #[test] - fn test_rss_service_plan_v5_schema() { - let schema = schemars::schema_for!(Plan); - expectorate::assert_contents( - "../schema/rss-service-plan-v5.json", - &serde_json::to_string_pretty(&schema).unwrap(), - ); - } - #[test] fn test_dataset_and_zone_count() { // We still need these values to provision external services diff --git a/sled-agent/src/rack_setup/plan/sled.rs b/sled-agent/src/rack_setup/plan/sled.rs index 9ac4b850c1..3d06a91e27 100644 --- a/sled-agent/src/rack_setup/plan/sled.rs +++ b/sled-agent/src/rack_setup/plan/sled.rs @@ -5,123 +5,33 @@ //! Plan generation for "how should sleds be initialized". use crate::bootstrap::config::BOOTSTRAP_AGENT_RACK_INIT_PORT; -use camino::Utf8PathBuf; -use omicron_common::ledger::{self, Ledger, Ledgerable}; use omicron_uuid_kinds::SledUuid; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use sled_agent_types::rack_init::back_compat::RackInitializeRequestV1 as ConfigV1; use sled_agent_types::rack_init::RackInitializeRequest as Config; use sled_agent_types::sled::StartSledAgentRequest; use sled_agent_types::sled::StartSledAgentRequestBody; -use sled_storage::dataset::CONFIG_DATASET; -use sled_storage::manager::StorageHandle; use slog::Logger; use std::collections::{BTreeMap, BTreeSet}; use std::net::{Ipv6Addr, SocketAddrV6}; -use std::str::FromStr; -use thiserror::Error; use uuid::Uuid; -/// Describes errors which may occur while generating a plan for sleds. -#[derive(Error, Debug)] -pub enum PlanError { - #[error("I/O error while {message}: {err}")] - Io { - message: String, - #[source] - err: std::io::Error, - }, - - #[error("Failed to access ledger: {0}")] - Ledger(#[from] ledger::Error), -} - -impl Ledgerable for Plan { - fn is_newer_than(&self, _other: &Self) -> bool { - true - } - fn generation_bump(&mut self) {} -} -const RSS_SLED_PLAN_FILENAME: &str = "rss-sled-plan.json"; - #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] pub struct Plan { pub rack_id: Uuid, pub sleds: BTreeMap, - // Store the provided RSS configuration as part of the sled plan; if it - // changes after reboot, we need to know. + // Store the provided RSS configuration as part of the sled plan. pub config: Config, } -impl FromStr for Plan { - type Err = String; - - fn from_str(value: &str) -> Result { - #[derive(Deserialize)] - struct ShadowPlan { - pub rack_id: Uuid, - pub sleds: BTreeMap, - pub config: Config, - } - #[derive(Deserialize)] - struct ShadowPlanV1 { - pub rack_id: Uuid, - pub sleds: BTreeMap, - pub config: ConfigV1, - } - let v2_err = match serde_json::from_str::(&value) { - Ok(plan) => { - return Ok(Plan { - rack_id: plan.rack_id, - sleds: plan.sleds, - config: plan.config, - }) - } - Err(e) => format!("unable to parse Plan: {e:?}"), - }; - serde_json::from_str::(&value) - .map(|v1| Plan { - rack_id: v1.rack_id, - sleds: v1.sleds, - config: v1.config.into(), - }) - .map_err(|_| v2_err) - } -} - impl Plan { - pub async fn load( - log: &Logger, - storage: &StorageHandle, - ) -> Result, PlanError> { - let paths: Vec = storage - .get_latest_disks() - .await - .all_m2_mountpoints(CONFIG_DATASET) - .into_iter() - .map(|p| p.join(RSS_SLED_PLAN_FILENAME)) - .collect(); - - // If we already created a plan for this RSS to allocate - // subnets/requests to sleds, re-use that existing plan. - let ledger = Ledger::::new(log, paths.clone()).await; - if let Some(ledger) = ledger { - info!(log, "RSS plan already created, loading from file"); - Ok(Some(ledger.data().clone())) - } else { - Ok(None) - } - } - pub async fn create( log: &Logger, config: &Config, - storage_manager: &StorageHandle, bootstrap_addrs: BTreeSet, use_trust_quorum: bool, - ) -> Result { + ) -> Self { let rack_id = Uuid::new_v4(); let bootstrap_addrs = bootstrap_addrs.into_iter().enumerate(); @@ -160,54 +70,6 @@ impl Plan { sleds.insert(addr, allocation); } - let plan = Self { rack_id, sleds, config: config.clone() }; - - // Once we've constructed a plan, write it down to durable storage. - let paths: Vec = storage_manager - .get_latest_disks() - .await - .all_m2_mountpoints(CONFIG_DATASET) - .into_iter() - .map(|p| p.join(RSS_SLED_PLAN_FILENAME)) - .collect(); - - let mut ledger = Ledger::::new_with(log, paths, plan.clone()); - ledger.commit().await?; - info!(log, "Sled plan written to storage: {plan:#?}"); - Ok(plan) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_rss_sled_plan_schema() { - let schema = schemars::schema_for!(Plan); - expectorate::assert_contents( - "../schema/rss-sled-plan.json", - &serde_json::to_string_pretty(&schema).unwrap(), - ); - } - - #[test] - fn test_read_known_rss_sled_plans() { - let known_rss_sled_plans = &["madrid-rss-sled-plan.json"]; - - let path = Utf8PathBuf::from("tests/old-rss-sled-plans"); - let out_path = Utf8PathBuf::from("tests/output/new-rss-sled-plans"); - for sled_plan_basename in known_rss_sled_plans { - println!("checking {:?}", sled_plan_basename); - let contents = - std::fs::read_to_string(path.join(sled_plan_basename)) - .expect("failed to read file"); - let parsed = - Plan::from_str(&contents).expect("failed to parse file"); - expectorate::assert_contents( - out_path.join(sled_plan_basename), - &serde_json::to_string_pretty(&parsed).unwrap(), - ); - } + Self { rack_id, sleds, config: config.clone() } } } diff --git a/sled-agent/src/rack_setup/service.rs b/sled-agent/src/rack_setup/service.rs index 245e8fa12c..7bb6bffe02 100644 --- a/sled-agent/src/rack_setup/service.rs +++ b/sled-agent/src/rack_setup/service.rs @@ -16,19 +16,22 @@ //! Rack setup occurs in distinct phases that are denoted by the presence of //! state files that get generated as RSS executes: //! -//! - /pool/int/UUID/config/rss-sled-plan.json (Sled Plan) -//! - /pool/int/UUID/config/rss-service-plan-v5.json (Service Plan) -//! - /pool/int/UUID/config/rss-plan-completed.marker (Plan Execution Complete) +//! - /pool/int/UUID/config/rss-started.marker (RSS has started) +//! - /pool/int/UUID/config/rss-plan-completed.marker (RSS Complete) //! -//! These phases are described below. As each phase completes, a corresponding -//! state file is written. This mechanism is designed so that if RSS restarts -//! (e.g., after a crash) then it will resume execution using the same plans. +//! In its current incarnation, RSS is not capable of restarting without a +//! clean-slate of the rack. Rather than persisting plan files which may let +//! us pick up and restart at certain given points we instead only record two +//! marker files. These marker files indicate one of two things: //! -//! The service plan file has "-v2" in the filename because its structure -//! changed in omicron#4466. It is possible that on startup, RSS finds an -//! older-form service plan. In that case, it fails altogether. We do not -//! expect this condition to happen in practice. See the implementation for -//! details. +//! * RSS has started. We must clean-slate the rack if we try to start RSS and +//! see this file, as it indicates the original RSS attempt failed. +//! * RSS has completed and handed off to nexus. The system is up and running +//! and any clean-slate would reset the rack to factory default state losing +//! any existing data. +//! +//! Between these two marker states we perform RSS which performs the following +//! operations. //! //! ## Sled Plan //! @@ -36,11 +39,7 @@ //! (Scrimlet). It must communicate with other sleds on the bootstrap network to //! discover neighbors. RSS uses the bootstrap network to identify peers, assign //! them subnets and UUIDs, and initialize a trust quorum. Once RSS decides -//! these values it commits them to a local file as the "Sled Plan", before -//! sending requests. -//! -//! As a result, restarting RSS should result in retransmission of the same -//! values, as long as the same configuration file is used. +//! these values it constructs a `SledPlan`. //! //! ## Service Plan //! @@ -53,16 +52,20 @@ //! - Nexus itself //! //! Once the distribution of these services is decided (which sled should run -//! what service? On what zpools should CockroachDB be provisioned?) it is -//! committed to the Service Plan, and executed. +//! what service? On what zpools should CockroachDB be provisioned?), the +//! `ServicePlan` is created and executed. //! -//! ## Execution Complete +//! ## Handoff to Nexus //! -//! Once the both the Sled and Service plans have finished execution, handoff of -//! control to Nexus can occur. -//! covers this in more detail, but in short, RSS creates a "marker" file after -//! completing execution, and unconditionally calls the "handoff to Nexus" API -//! thereafter. +//! Once the both the Sled and Service plans have finished execution, handoff +//! of control to Nexus can occur. +//! covers this in more detail, but in short, RSS creates a "marker" file +//! after completing execution and calling the "handoff to Nexus" API. In prior +//! versions of RSS this marker file was written prior to handoff. However, in +//! order to simplify state management we decided to eliminate plan persistence +//! and force RSS to run to completion through nexus handoff or be restarted +//! after a clean slate upon failure. +//! See for details. use super::plan::service::SledConfig; use crate::bootstrap::config::BOOTSTRAP_AGENT_HTTP_PORT; @@ -73,9 +76,7 @@ use crate::bootstrap::rss_handle::BootstrapAgentHandle; use crate::rack_setup::plan::service::{ Plan as ServicePlan, PlanError as ServicePlanError, }; -use crate::rack_setup::plan::sled::{ - Plan as SledPlan, PlanError as SledPlanError, -}; +use crate::rack_setup::plan::sled::Plan as SledPlan; use anyhow::{bail, Context}; use bootstore::schemes::v0 as bootstore; use camino::Utf8PathBuf; @@ -130,7 +131,7 @@ use slog::Logger; use std::collections::{btree_map, BTreeMap, BTreeSet}; use std::collections::{HashMap, HashSet}; use std::iter; -use std::net::{Ipv6Addr, SocketAddrV6}; +use std::net::SocketAddrV6; use std::time::Duration; use thiserror::Error; use tokio::sync::watch; @@ -168,9 +169,6 @@ pub enum SetupServiceError { #[error("Cannot create plan for sled services: {0}")] ServicePlan(#[from] ServicePlanError), - #[error("Cannot create plan for sled setup: {0}")] - SledPlan(#[from] SledPlanError), - #[error("Bad configuration for setting up rack: {0}")] BadConfig(String), @@ -214,6 +212,12 @@ pub enum SetupServiceError { // of error variants already in this type #[error(transparent)] EarlyNetworkSetup(#[from] EarlyNetworkSetupError), + + #[error("Rack already initialized")] + RackAlreadyInitialized, + + #[error("Rack initialization was interrupted. Clean-slate required")] + RackInitInterrupted, } // The workload / information allocated to a single sled. @@ -291,6 +295,18 @@ impl RackSetupService { } } +#[derive(Clone, Serialize, Deserialize, Default)] +struct RssStartedMarker {} + +impl Ledgerable for RssStartedMarker { + fn is_newer_than(&self, _other: &Self) -> bool { + true + } + fn generation_bump(&mut self) {} +} + +const RSS_STARTED_FILENAME: &str = "rss-started.marker"; + #[derive(Clone, Serialize, Deserialize, Default)] struct RssCompleteMarker {} @@ -1192,7 +1208,15 @@ impl ServiceInner { config.az_subnet(), )?; - let marker_paths: Vec = storage_manager + let started_marker_paths: Vec = storage_manager + .get_latest_disks() + .await + .all_m2_mountpoints(CONFIG_DATASET) + .into_iter() + .map(|p| p.join(RSS_STARTED_FILENAME)) + .collect(); + + let completed_marker_paths: Vec = storage_manager .get_latest_disks() .await .all_m2_mountpoints(CONFIG_DATASET) @@ -1200,106 +1224,72 @@ impl ServiceInner { .map(|p| p.join(RSS_COMPLETED_FILENAME)) .collect(); - let ledger = - Ledger::::new(&self.log, marker_paths.clone()) - .await; + let started_ledger = Ledger::::new( + &self.log, + started_marker_paths.clone(), + ) + .await; + let completed_ledger = Ledger::::new( + &self.log, + completed_marker_paths.clone(), + ) + .await; // Check if a previous RSS plan has completed successfully. // - // If it has, the system should be up-and-running. - if ledger.is_some() { - // TODO(https://github.com/oxidecomputer/omicron/issues/724): If the - // running configuration doesn't match Config, we could try to - // update things. - info!( - self.log, - "RSS configuration looks like it has already been applied", - ); - - rss_step.update(RssStep::LoadExistingPlan); - let sled_plan = SledPlan::load(&self.log, storage_manager) - .await? - .expect("Sled plan should exist if completed marker exists"); - if &sled_plan.config != config { - return Err(SetupServiceError::BadConfig( - "Configuration changed".to_string(), - )); - } - let service_plan = ServicePlan::load(&self.log, storage_manager) - .await? - .expect("Service plan should exist if completed marker exists"); - - let switch_mgmt_addrs = EarlyNetworkSetup::new(&self.log) - .lookup_switch_zone_underlay_addrs(&resolver) - .await; - - let nexus_address = - resolver.lookup_socket_v6(ServiceName::Nexus).await?; - - rss_step.update(RssStep::NexusHandoff); - self.handoff_to_nexus( - &config, - &sled_plan, - &service_plan, - ExternalPortDiscovery::Auto(switch_mgmt_addrs), - nexus_address, - ) - .await?; - return Ok(()); - } else { - info!(self.log, "RSS configuration has not been fully applied yet"); + // If we see the completion marker in the `completed_ledger` then the + // system should be up-and-running. If we see the started marker in + // the `started_ledger`, then RSS did not complete and the rack should + // be clean-slated before RSS is run again. + if completed_ledger.is_some() { + info!(self.log, "RSS configuration has already been applied",); + return Err(SetupServiceError::RackAlreadyInitialized); + } else if started_ledger.is_some() { + error!(self.log, "RSS failed to complete rack initialization"); + return Err(SetupServiceError::RackInitInterrupted); } - rss_step.update(RssStep::CreateSledPlan); - // Wait for either: - // - All the peers to re-load an old plan (if one exists) - // - Enough peers to create a new plan (if one does not exist) + info!(self.log, "RSS not previously run. Creating plans."); + + // Wait for enough peers to create a new plan let bootstrap_addrs = match &config.bootstrap_discovery { BootstrapAddressDiscovery::OnlyOurs => { BTreeSet::from([local_bootstrap_agent.our_address()]) } BootstrapAddressDiscovery::OnlyThese { addrs } => addrs.clone(), }; - let maybe_sled_plan = - SledPlan::load(&self.log, storage_manager).await?; - if let Some(plan) = &maybe_sled_plan { - let stored_peers: BTreeSet = - plan.sleds.keys().map(|a| *a.ip()).collect(); - if stored_peers != bootstrap_addrs { - let e = concat!( - "Set of sleds requested does not match those in", - " existing sled plan" - ); - return Err(SetupServiceError::BadConfig(e.to_string())); - } - } + if bootstrap_addrs.is_empty() { return Err(SetupServiceError::BadConfig( "Must request at least one peer".to_string(), )); } - // If we created a plan, reuse it. Otherwise, create a new plan. + // Create a new plan. // // NOTE: This is a "point-of-no-return" -- before sending any requests - // to neighboring sleds, the plan must be recorded to durable storage. - // This way, if the RSS power-cycles, it can idempotently provide the - // same subnets to the same sleds. - let plan = if let Some(plan) = maybe_sled_plan { - info!(self.log, "Re-using existing allocation plan"); - plan - } else { - info!(self.log, "Creating new allocation plan"); - SledPlan::create( - &self.log, - config, - &storage_manager, - bootstrap_addrs, - config.trust_quorum_peers.is_some(), - ) - .await? - }; - let config = &plan.config; + // to neighboring sleds, we record that RSS has started. + // This way, if the RSS power-cycles, it can be detected and we can + // clean-slate and try again. + + // Record that we have started RSS + let mut ledger = Ledger::::new_with( + &self.log, + started_marker_paths.clone(), + RssStartedMarker::default(), + ); + ledger.commit().await?; + + rss_step.update(RssStep::CreateSledPlan); + info!(self.log, "Creating new allocation plan"); + let sled_plan = SledPlan::create( + &self.log, + config, + bootstrap_addrs, + config.trust_quorum_peers.is_some(), + ) + .await; + let config = &sled_plan.config; rss_step.update(RssStep::InitTrustQuorum); // Initialize the trust quorum if there are peers configured. @@ -1307,7 +1297,7 @@ impl ServiceInner { let initial_membership: BTreeSet<_> = peers.iter().cloned().collect(); bootstore - .init_rack(plan.rack_id.into(), initial_membership) + .init_rack(sled_plan.rack_id.into(), initial_membership) .await?; } @@ -1331,7 +1321,8 @@ impl ServiceInner { // Forward the sled initialization requests to our sled-agent. local_bootstrap_agent .initialize_sleds( - plan.sleds + sled_plan + .sleds .iter() .map(move |(bootstrap_addr, initialization_request)| { (*bootstrap_addr, initialization_request.clone()) @@ -1343,26 +1334,8 @@ impl ServiceInner { // Now that sled agents have been initialized, we can create // a service allocation plan. - let sled_addresses: Vec<_> = plan - .sleds - .values() - .map(|initialization_request| { - get_sled_address(initialization_request.body.subnet) - }) - .collect(); - let service_plan = if let Some(plan) = - ServicePlan::load(&self.log, storage_manager).await? - { - plan - } else { - ServicePlan::create( - &self.log, - &config, - &storage_manager, - &plan.sleds, - ) - .await? - }; + let service_plan = + ServicePlan::create(&self.log, &config, &sled_plan.sleds).await?; rss_step.update(RssStep::EnsureStorage); // Before we can ask for any services, we need to ensure that storage is @@ -1405,8 +1378,15 @@ impl ServiceInner { ); self.ensure_zone_config_at_least(v3generator.sled_configs()).await?; - rss_step.update(RssStep::WaitForTimeSync); // Wait until time is synchronized on all sleds before proceeding. + rss_step.update(RssStep::WaitForTimeSync); + let sled_addresses: Vec<_> = sled_plan + .sleds + .values() + .map(|initialization_request| { + get_sled_address(initialization_request.body.subnet) + }) + .collect(); self.wait_for_timesync(&sled_addresses).await?; info!(self.log, "Finished setting up Internal DNS and NTP"); @@ -1434,14 +1414,6 @@ impl ServiceInner { info!(self.log, "Finished setting up services"); - // Finally, mark that we've completed executing the plans. - let mut ledger = Ledger::::new_with( - &self.log, - marker_paths.clone(), - RssCompleteMarker::default(), - ); - ledger.commit().await?; - let nexus_address = resolver.lookup_socket_v6(ServiceName::Nexus).await?; @@ -1450,13 +1422,21 @@ impl ServiceInner { // services, or DNS records. self.handoff_to_nexus( &config, - &plan, + &sled_plan, &service_plan, ExternalPortDiscovery::Auto(switch_mgmt_addrs), nexus_address, ) .await?; + // Finally, mark that we've completed executing the plans and handed off to nexus. + let mut ledger = Ledger::::new_with( + &self.log, + completed_marker_paths.clone(), + RssCompleteMarker::default(), + ); + ledger.commit().await?; + Ok(()) } } diff --git a/sled-agent/tests/old-rss-sled-plans/madrid-rss-sled-plan.json b/sled-agent/tests/old-rss-sled-plans/madrid-rss-sled-plan.json deleted file mode 100644 index 5512247ee8..0000000000 --- a/sled-agent/tests/old-rss-sled-plans/madrid-rss-sled-plan.json +++ /dev/null @@ -1 +0,0 @@ -{"rack_id":"ed6bcf59-9620-491d-8ebd-4a4eebf2e136","sleds":{"[fdb0:a840:2504:396::1]:12346":{"generation":0,"schema_version":1,"body":{"id":"b3e78a88-0f2e-476e-a8a9-2d8c90a169d6","rack_id":"ed6bcf59-9620-491d-8ebd-4a4eebf2e136","use_trust_quorum":true,"is_lrtq_learner":false,"subnet":{"net":"fd00:1122:3344:103::/64"}}},"[fdb0:a840:2504:157::1]:12346":{"generation":0,"schema_version":1,"body":{"id":"168e1ad6-1e4b-4f7a-b894-157974bd8bb8","rack_id":"ed6bcf59-9620-491d-8ebd-4a4eebf2e136","use_trust_quorum":true,"is_lrtq_learner":false,"subnet":{"net":"fd00:1122:3344:104::/64"}}},"[fdb0:a840:2504:355::1]:12346":{"generation":0,"schema_version":1,"body":{"id":"b9877212-212b-4588-b818-9c7b53c5b143","rack_id":"ed6bcf59-9620-491d-8ebd-4a4eebf2e136","use_trust_quorum":true,"is_lrtq_learner":false,"subnet":{"net":"fd00:1122:3344:102::/64"}}},"[fdb0:a840:2504:3d2::1]:12346":{"generation":0,"schema_version":1,"body":{"id":"c3a0f8be-5b05-4ee8-8c4e-2514de6501b6","rack_id":"ed6bcf59-9620-491d-8ebd-4a4eebf2e136","use_trust_quorum":true,"is_lrtq_learner":false,"subnet":{"net":"fd00:1122:3344:101::/64"}}}},"config":{"rack_subnet":"fd00:1122:3344:100::","trust_quorum_peers":[{"type":"gimlet","identifier":"BRM42220081","model":"913-0000019","revision":6},{"type":"gimlet","identifier":"BRM42220046","model":"913-0000019","revision":6},{"type":"gimlet","identifier":"BRM44220001","model":"913-0000019","revision":6},{"type":"gimlet","identifier":"BRM42220004","model":"913-0000019","revision":6}],"bootstrap_discovery":{"type":"only_these","addrs":["fdb0:a840:2504:3d2::1","fdb0:a840:2504:355::1","fdb0:a840:2504:396::1","fdb0:a840:2504:157::1"]},"ntp_servers":["ntp.eng.oxide.computer"],"dns_servers":["1.1.1.1","9.9.9.9"],"internal_services_ip_pool_ranges":[{"first":"172.20.28.1","last":"172.20.28.10"}],"external_dns_ips":["172.20.28.1"],"external_dns_zone_name":"madrid.eng.oxide.computer","external_certificates":[{"cert":"","key":""}],"recovery_silo":{"silo_name":"recovery","user_name":"recovery","user_password_hash":"$argon2id$v=19$m=98304,t=13,p=1$RUlWc0ZxaHo0WFdrN0N6ZQ$S8p52j85GPvMhR/ek3GL0el/oProgTwWpHJZ8lsQQoY"},"rack_network_config":{"rack_subnet":"fd00:1122:3344:1::/56","infra_ip_first":"172.20.15.37","infra_ip_last":"172.20.15.38","ports":[{"routes":[{"destination":"0.0.0.0/0","nexthop":"172.20.15.33"}],"addresses":["172.20.15.38/29"],"switch":"switch0","port":"qsfp0","uplink_port_speed":"speed40_g","uplink_port_fec":"none","bgp_peers":[],"autoneg":false},{"routes":[{"destination":"0.0.0.0/0","nexthop":"172.20.15.33"}],"addresses":["172.20.15.37/29"],"switch":"switch1","port":"qsfp0","uplink_port_speed":"speed40_g","uplink_port_fec":"none","bgp_peers":[],"autoneg":false}],"bgp":[]}}} diff --git a/sled-agent/tests/output/new-rss-sled-plans/madrid-rss-sled-plan.json b/sled-agent/tests/output/new-rss-sled-plans/madrid-rss-sled-plan.json deleted file mode 100644 index 25e470e389..0000000000 --- a/sled-agent/tests/output/new-rss-sled-plans/madrid-rss-sled-plan.json +++ /dev/null @@ -1,182 +0,0 @@ -{ - "rack_id": "ed6bcf59-9620-491d-8ebd-4a4eebf2e136", - "sleds": { - "[fdb0:a840:2504:157::1]:12346": { - "generation": 0, - "schema_version": 1, - "body": { - "id": "168e1ad6-1e4b-4f7a-b894-157974bd8bb8", - "rack_id": "ed6bcf59-9620-491d-8ebd-4a4eebf2e136", - "use_trust_quorum": true, - "is_lrtq_learner": false, - "subnet": { - "net": "fd00:1122:3344:104::/64" - } - } - }, - "[fdb0:a840:2504:355::1]:12346": { - "generation": 0, - "schema_version": 1, - "body": { - "id": "b9877212-212b-4588-b818-9c7b53c5b143", - "rack_id": "ed6bcf59-9620-491d-8ebd-4a4eebf2e136", - "use_trust_quorum": true, - "is_lrtq_learner": false, - "subnet": { - "net": "fd00:1122:3344:102::/64" - } - } - }, - "[fdb0:a840:2504:396::1]:12346": { - "generation": 0, - "schema_version": 1, - "body": { - "id": "b3e78a88-0f2e-476e-a8a9-2d8c90a169d6", - "rack_id": "ed6bcf59-9620-491d-8ebd-4a4eebf2e136", - "use_trust_quorum": true, - "is_lrtq_learner": false, - "subnet": { - "net": "fd00:1122:3344:103::/64" - } - } - }, - "[fdb0:a840:2504:3d2::1]:12346": { - "generation": 0, - "schema_version": 1, - "body": { - "id": "c3a0f8be-5b05-4ee8-8c4e-2514de6501b6", - "rack_id": "ed6bcf59-9620-491d-8ebd-4a4eebf2e136", - "use_trust_quorum": true, - "is_lrtq_learner": false, - "subnet": { - "net": "fd00:1122:3344:101::/64" - } - } - } - }, - "config": { - "trust_quorum_peers": [ - { - "type": "gimlet", - "identifier": "BRM42220081", - "model": "913-0000019", - "revision": 6 - }, - { - "type": "gimlet", - "identifier": "BRM42220046", - "model": "913-0000019", - "revision": 6 - }, - { - "type": "gimlet", - "identifier": "BRM44220001", - "model": "913-0000019", - "revision": 6 - }, - { - "type": "gimlet", - "identifier": "BRM42220004", - "model": "913-0000019", - "revision": 6 - } - ], - "bootstrap_discovery": { - "type": "only_these", - "addrs": [ - "fdb0:a840:2504:157::1", - "fdb0:a840:2504:355::1", - "fdb0:a840:2504:396::1", - "fdb0:a840:2504:3d2::1" - ] - }, - "ntp_servers": [ - "ntp.eng.oxide.computer" - ], - "dns_servers": [ - "1.1.1.1", - "9.9.9.9" - ], - "internal_services_ip_pool_ranges": [ - { - "first": "172.20.28.1", - "last": "172.20.28.10" - } - ], - "external_dns_ips": [ - "172.20.28.1" - ], - "external_dns_zone_name": "madrid.eng.oxide.computer", - "external_certificates": [ - { - "cert": "", - "key": "" - } - ], - "recovery_silo": { - "silo_name": "recovery", - "user_name": "recovery", - "user_password_hash": "$argon2id$v=19$m=98304,t=13,p=1$RUlWc0ZxaHo0WFdrN0N6ZQ$S8p52j85GPvMhR/ek3GL0el/oProgTwWpHJZ8lsQQoY" - }, - "rack_network_config": { - "rack_subnet": "fd00:1122:3344:1::/56", - "infra_ip_first": "172.20.15.37", - "infra_ip_last": "172.20.15.38", - "ports": [ - { - "routes": [ - { - "destination": "0.0.0.0/0", - "nexthop": "172.20.15.33", - "vlan_id": null, - "rib_priority": null - } - ], - "addresses": [ - { - "address": "172.20.15.38/29", - "vlan_id": null - } - ], - "switch": "switch0", - "port": "qsfp0", - "uplink_port_speed": "speed40_g", - "uplink_port_fec": "none", - "bgp_peers": [], - "autoneg": false, - "lldp": null, - "tx_eq": null - }, - { - "routes": [ - { - "destination": "0.0.0.0/0", - "nexthop": "172.20.15.33", - "vlan_id": null, - "rib_priority": null - } - ], - "addresses": [ - { - "address": "172.20.15.37/29", - "vlan_id": null - } - ], - "switch": "switch1", - "port": "qsfp0", - "uplink_port_speed": "speed40_g", - "uplink_port_fec": "none", - "bgp_peers": [], - "autoneg": false, - "lldp": null, - "tx_eq": null - } - ], - "bgp": [], - "bfd": [] - }, - "allowed_source_ips": { - "allow": "any" - } - } -} \ No newline at end of file