diff --git a/Cargo.lock b/Cargo.lock index 6580e1de55c..5a85a35a805 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6038,9 +6038,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.69" +version = "1.0.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" +checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b" dependencies = [ "unicode-ident", ] diff --git a/common/src/api/external/mod.rs b/common/src/api/external/mod.rs index 3e58d1d4d4d..4208b94e963 100644 --- a/common/src/api/external/mod.rs +++ b/common/src/api/external/mod.rs @@ -2604,6 +2604,15 @@ pub struct BgpImportedRouteIpv4 { pub switch: SwitchLocation, } +#[derive( + Clone, Debug, Deserialize, JsonSchema, Serialize, PartialEq, ObjectIdentity, +)] +pub struct Probe { + #[serde(flatten)] + pub identity: IdentityMetadata, + pub sled: Uuid, +} + #[cfg(test)] mod test { use serde::Deserialize; diff --git a/nexus/db-model/src/lib.rs b/nexus/db-model/src/lib.rs index 43bf83fd349..d5d2961b01d 100644 --- a/nexus/db-model/src/lib.rs +++ b/nexus/db-model/src/lib.rs @@ -44,6 +44,7 @@ mod network_interface; mod oximeter_info; mod physical_disk; mod physical_disk_kind; +mod probe; mod producer_endpoint; mod project; mod semver_version; @@ -137,6 +138,7 @@ pub use network_interface::*; pub use oximeter_info::*; pub use physical_disk::*; pub use physical_disk_kind::*; +pub use probe::*; pub use producer_endpoint::*; pub use project::*; pub use rack::*; diff --git a/nexus/db-model/src/probe.rs b/nexus/db-model/src/probe.rs new file mode 100644 index 00000000000..d904729101d --- /dev/null +++ b/nexus/db-model/src/probe.rs @@ -0,0 +1,48 @@ +use crate::schema::probe; +use db_macros::Resource; +use nexus_types::external_api::params; +use nexus_types::identity::Resource; +use omicron_common::api::external; +use omicron_common::api::external::IdentityMetadataCreateParams; +use serde::Deserialize; +use serde::Serialize; +use uuid::Uuid; + +#[derive( + Queryable, + Insertable, + Selectable, + Clone, + Debug, + Resource, + Serialize, + Deserialize, +)] +#[diesel(table_name = probe)] +pub struct Probe { + #[diesel(embed)] + pub identity: ProbeIdentity, + + pub sled: Uuid, +} + +impl From<¶ms::ProbeCreate> for Probe { + fn from(p: ¶ms::ProbeCreate) -> Self { + Self { + identity: ProbeIdentity::new( + Uuid::new_v4(), + IdentityMetadataCreateParams { + name: p.identity.name.clone(), + description: p.identity.description.clone(), + }, + ), + sled: p.sled, + } + } +} + +impl Into for Probe { + fn into(self) -> external::Probe { + external::Probe { identity: self.identity().clone(), sled: self.sled } + } +} diff --git a/nexus/db-model/src/schema.rs b/nexus/db-model/src/schema.rs index be345032acd..9866b57f5be 100644 --- a/nexus/db-model/src/schema.rs +++ b/nexus/db-model/src/schema.rs @@ -1296,12 +1296,24 @@ table! { } } +table! { + probe (id) { + id -> Uuid, + name -> Text, + description -> Text, + time_created -> Timestamptz, + time_modified -> Timestamptz, + time_deleted -> Nullable, + sled -> Uuid, + } +} + /// The version of the database schema this particular version of Nexus was /// built against. /// /// This should be updated whenever the schema is changed. For more details, /// refer to: schema/crdb/README.adoc -pub const SCHEMA_VERSION: SemverVersion = SemverVersion::new(17, 0, 0); +pub const SCHEMA_VERSION: SemverVersion = SemverVersion::new(18, 0, 0); allow_tables_to_appear_in_same_query!( system_update, diff --git a/nexus/db-queries/src/db/datastore/mod.rs b/nexus/db-queries/src/db/datastore/mod.rs index 44cd7a95b7a..a1aa8a7c45e 100644 --- a/nexus/db-queries/src/db/datastore/mod.rs +++ b/nexus/db-queries/src/db/datastore/mod.rs @@ -67,6 +67,7 @@ mod ipv4_nat_entry; mod network_interface; mod oximeter; mod physical_disk; +mod probe; mod project; mod rack; mod region; diff --git a/nexus/db-queries/src/db/datastore/probe.rs b/nexus/db-queries/src/db/datastore/probe.rs new file mode 100644 index 00000000000..e1927cb51d7 --- /dev/null +++ b/nexus/db-queries/src/db/datastore/probe.rs @@ -0,0 +1,135 @@ +use crate::context::OpContext; +use crate::db; +use crate::db::error::public_error_from_diesel; +use crate::db::error::ErrorHandler; +use crate::db::model::Name; +use crate::db::pagination::paginated; +use async_bb8_diesel::AsyncRunQueryDsl; +use chrono::Utc; +use diesel::{ExpressionMethods, QueryDsl, SelectableHelper}; +use nexus_db_model::Probe; +use nexus_types::external_api::params; +use omicron_common::api::external::http_pagination::PaginatedBy; +use omicron_common::api::external::CreateResult; +use omicron_common::api::external::DeleteResult; +use omicron_common::api::external::ListResultVec; +use omicron_common::api::external::LookupResult; +use omicron_common::api::external::NameOrId; +use ref_cast::RefCast; +use uuid::Uuid; + +impl super::DataStore { + pub async fn probe_list( + &self, + opctx: &OpContext, + pagparams: &PaginatedBy<'_>, + ) -> ListResultVec { + use db::schema::probe::dsl; + + let pool = self.pool_connection_authorized(opctx).await?; + + match pagparams { + PaginatedBy::Id(pagparams) => { + paginated(dsl::probe, dsl::id, &pagparams) + } + PaginatedBy::Name(pagparams) => paginated( + dsl::probe, + dsl::name, + &pagparams.map_name(|n| Name::ref_cast(n)), + ), + } + .filter(dsl::time_deleted.is_null()) + .select(Probe::as_select()) + .load_async(&*pool) + .await + .map_err(|e| public_error_from_diesel(e, ErrorHandler::Server)) + } + + pub async fn probe_get( + &self, + opctx: &OpContext, + name_or_id: &NameOrId, + ) -> LookupResult { + use db::schema::probe; + use db::schema::probe::dsl; + let pool = self.pool_connection_authorized(opctx).await?; + + let name_or_id = name_or_id.clone(); + + let probe = match name_or_id { + NameOrId::Name(name) => dsl::probe + .filter(probe::name.eq(name.to_string())) + .filter(probe::time_deleted.is_null()) + .select(Probe::as_select()) + .limit(1) + .first_async::(&*pool) + .await + .map_err(|e| public_error_from_diesel(e, ErrorHandler::Server)), + NameOrId::Id(id) => dsl::probe + .filter(probe::id.eq(id)) + .select(Probe::as_select()) + .limit(1) + .first_async::(&*pool) + .await + .map_err(|e| public_error_from_diesel(e, ErrorHandler::Server)), + }?; + + Ok(probe) + } + + pub async fn probe_create( + &self, + opctx: &OpContext, + new_probe: ¶ms::ProbeCreate, + ) -> CreateResult { + use db::schema::probe::dsl; + let pool = self.pool_connection_authorized(opctx).await?; + + let probe = Probe::from(new_probe); + + let result = diesel::insert_into(dsl::probe) + .values(probe.clone()) + .returning(Probe::as_returning()) + .get_result_async(&*pool) + .await + .map_err(|e| public_error_from_diesel(e, ErrorHandler::Server))?; + + Ok(result) + } + + pub async fn probe_delete( + &self, + opctx: &OpContext, + name_or_id: &NameOrId, + ) -> DeleteResult { + use db::schema::probe; + use db::schema::probe::dsl; + let pool = self.pool_connection_authorized(opctx).await?; + + let name_or_id = name_or_id.clone(); + + //TODO in transaction + let id = match name_or_id { + NameOrId::Name(name) => dsl::probe + .filter(probe::name.eq(name.to_string())) + .filter(probe::time_deleted.is_null()) + .select(probe::id) + .limit(1) + .first_async::(&*pool) + .await + .map_err(|e| { + public_error_from_diesel(e, ErrorHandler::Server) + })?, + NameOrId::Id(id) => id, + }; + + diesel::update(dsl::probe) + .filter(dsl::id.eq(id)) + .set(dsl::time_deleted.eq(Utc::now())) + .execute_async(&*pool) + .await + .map_err(|e| public_error_from_diesel(e, ErrorHandler::Server))?; + + Ok(()) + } +} diff --git a/nexus/src/app/mod.rs b/nexus/src/app/mod.rs index 18c9dae8412..3d9583a4005 100644 --- a/nexus/src/app/mod.rs +++ b/nexus/src/app/mod.rs @@ -50,6 +50,7 @@ mod ip_pool; mod metrics; mod network_interface; mod oximeter; +mod probe; mod project; mod rack; pub(crate) mod saga; diff --git a/nexus/src/app/probe.rs b/nexus/src/app/probe.rs new file mode 100644 index 00000000000..88e765fb5de --- /dev/null +++ b/nexus/src/app/probe.rs @@ -0,0 +1,41 @@ +use nexus_db_model::Probe; +use nexus_db_queries::context::OpContext; +use nexus_types::external_api::params; +use omicron_common::api::external::{ + http_pagination::PaginatedBy, CreateResult, DeleteResult, ListResultVec, + LookupResult, NameOrId, +}; + +impl super::Nexus { + pub(crate) async fn probe_list( + &self, + opctx: &OpContext, + pagparams: &PaginatedBy<'_>, + ) -> ListResultVec { + self.db_datastore.probe_list(opctx, pagparams).await + } + + pub(crate) async fn probe_get( + &self, + opctx: &OpContext, + name_or_id: NameOrId, + ) -> LookupResult { + self.db_datastore.probe_get(opctx, &name_or_id).await + } + + pub(crate) async fn probe_create( + &self, + opctx: &OpContext, + new_probe_params: ¶ms::ProbeCreate, + ) -> CreateResult { + self.db_datastore.probe_create(opctx, new_probe_params).await + } + + pub(crate) async fn probe_delete( + &self, + opctx: &OpContext, + name_or_id: NameOrId, + ) -> DeleteResult { + self.db_datastore.probe_delete(opctx, &name_or_id).await + } +} diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index f1302f4a73b..3ef74ec9f07 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -77,6 +77,7 @@ use omicron_common::api::external::InstanceNetworkInterface; use omicron_common::api::external::InternalContext; use omicron_common::api::external::LoopbackAddress; use omicron_common::api::external::NameOrId; +use omicron_common::api::external::Probe; use omicron_common::api::external::RouterRoute; use omicron_common::api::external::RouterRouteKind; use omicron_common::api::external::SwitchPort; @@ -335,6 +336,11 @@ pub(crate) fn external_api() -> NexusApiDescription { api.register(device_auth::device_auth_confirm)?; api.register(device_auth::device_access_token)?; + api.register(probe_list)?; + api.register(probe_view)?; + api.register(probe_create)?; + api.register(probe_delete)?; + Ok(()) } @@ -5553,6 +5559,103 @@ async fn current_user_ssh_key_delete( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } +/// List instrumentation probes. +#[endpoint { + method = GET, + path = "/v1/probes", + tags = ["probes"], +}] +async fn probe_list( + rqctx: RequestContext>, + query_params: Query>, +) -> Result>, HttpError> { + let apictx = rqctx.context(); + let handler = async { + let nexus = &apictx.nexus; + let query = query_params.into_inner(); + let pag_params = data_page_params_for(&rqctx, &query)?; + let scan_params = ScanByNameOrId::from_query(&query)?; + let paginated_by = name_or_id_pagination(&pag_params, scan_params)?; + let opctx = crate::context::op_context_for_external_api(&rqctx).await?; + let probes = nexus + .probe_list(&opctx, &paginated_by) + .await? + .into_iter() + .map(|i| i.into()) + .collect(); + + Ok(HttpResponseOk(ScanByNameOrId::results_page( + &query, + probes, + &marker_for_name_or_id, + )?)) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + +/// View an instrumentation probe. +#[endpoint { + method = GET, + path = "/v1/probes/{probe}", + tags = ["probes"], +}] +async fn probe_view( + rqctx: RequestContext>, + path_params: Path, +) -> Result, HttpError> { + let apictx = rqctx.context(); + let handler = async { + let nexus = &apictx.nexus; + let path = path_params.into_inner(); + let opctx = crate::context::op_context_for_external_api(&rqctx).await?; + let probe = nexus.probe_get(&opctx, path.probe).await?; + Ok(HttpResponseOk(probe.into())) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + +/// Create an instrumentation probe. +#[endpoint { + method = POST, + path = "/v1/probes", + tags = ["probes"], +}] +async fn probe_create( + rqctx: RequestContext>, + new_probe: TypedBody, +) -> Result, HttpError> { + let apictx = rqctx.context(); + let handler = async { + let nexus = &apictx.nexus; + let opctx = crate::context::op_context_for_external_api(&rqctx).await?; + let new_probe_params = &new_probe.into_inner(); + let probe = nexus.probe_create(&opctx, &new_probe_params).await?; + Ok(HttpResponseCreated(probe.into())) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + +/// Delete an instrumentation probe. +#[endpoint { + method = DELETE, + path = "/v1/probes/{probe}", + tags = ["probes"], +}] +async fn probe_delete( + rqctx: RequestContext>, + path_params: Path, +) -> Result { + let apictx = rqctx.context(); + let handler = async { + let nexus = &apictx.nexus; + let path = path_params.into_inner(); + let opctx = crate::context::op_context_for_external_api(&rqctx).await?; + nexus.probe_delete(&opctx, path.probe).await?; + Ok(HttpResponseDeleted()) + }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + #[cfg(test)] mod test { use super::external_api; diff --git a/nexus/src/external_api/tag-config.json b/nexus/src/external_api/tag-config.json index 07eb198016f..c5862a6c016 100644 --- a/nexus/src/external_api/tag-config.json +++ b/nexus/src/external_api/tag-config.json @@ -80,6 +80,12 @@ "url": "http://docs.oxide.computer/api/vpcs" } }, + "probes": { + "description": "Probes for testing network connectivity", + "external_docs": { + "url": "http://docs.oxide.computer/api/probes" + } + }, "system/status": { "description": "Endpoints related to system health", "external_docs": { diff --git a/nexus/tests/integration_tests/mod.rs b/nexus/tests/integration_tests/mod.rs index 87c5c74f0fa..c4ab05d387c 100644 --- a/nexus/tests/integration_tests/mod.rs +++ b/nexus/tests/integration_tests/mod.rs @@ -21,6 +21,7 @@ mod metrics; mod oximeter; mod pantry; mod password_login; +mod probe; mod projects; mod rack; mod role_assignments; diff --git a/nexus/tests/integration_tests/probe.rs b/nexus/tests/integration_tests/probe.rs new file mode 100644 index 00000000000..1e0e60e0985 --- /dev/null +++ b/nexus/tests/integration_tests/probe.rs @@ -0,0 +1,83 @@ +use nexus_test_utils::http_testing::{AuthnMode, NexusRequest}; +use nexus_test_utils_macros::nexus_test; +use nexus_types::external_api::params::ProbeCreate; +use omicron_common::api::external::{IdentityMetadataCreateParams, Probe}; +use uuid::Uuid; + +type ControlPlaneTestContext = + nexus_test_utils::ControlPlaneTestContext; + +#[nexus_test] +async fn test_probe_basic_crud(ctx: &ControlPlaneTestContext) { + let client = &ctx.external_client; + + let probes = NexusRequest::iter_collection_authn::( + client, + "/v1/probes", + "", + None, + ) + .await + .expect("Failed to list probes") + .all_items; + + assert_eq!(probes.len(), 0, "Expected zero probes"); + + let params = ProbeCreate { + identity: IdentityMetadataCreateParams { + name: "class1".parse().unwrap(), + description: "subspace relay probe".to_owned(), + }, + sled: Uuid::new_v4(), + }; + + let created: Probe = + NexusRequest::objects_post(client, "/v1/probes", ¶ms) + .authn_as(AuthnMode::PrivilegedUser) + .execute() + .await + .unwrap() + .parsed_body() + .unwrap(); + + let probes = NexusRequest::iter_collection_authn::( + client, + "/v1/probes", + "", + None, + ) + .await + .expect("Failed to list probes") + .all_items; + + assert_eq!(probes.len(), 1, "Expected one probe"); + assert_eq!(probes[0], created); + + let fetched: Probe = NexusRequest::object_get(client, "/v1/probes/class1") + .authn_as(AuthnMode::PrivilegedUser) + .execute() + .await + .unwrap() + .parsed_body() + .unwrap(); + + assert_eq!(fetched, created); + + NexusRequest::object_delete(client, "/v1/probes/class1") + .authn_as(AuthnMode::PrivilegedUser) + .execute() + .await + .unwrap(); + + let probes = NexusRequest::iter_collection_authn::( + client, + "/v1/probes", + "", + None, + ) + .await + .expect("Failed to list probes") + .all_items; + + assert_eq!(probes.len(), 0, "Expected zero probes"); +} diff --git a/nexus/types/src/external_api/params.rs b/nexus/types/src/external_api/params.rs index 3303d383679..d848c30cb4d 100644 --- a/nexus/types/src/external_api/params.rs +++ b/nexus/types/src/external_api/params.rs @@ -62,6 +62,7 @@ path_param!(ProviderPath, provider, "SAML identity provider"); path_param!(IpPoolPath, pool, "IP pool"); path_param!(SshKeyPath, ssh_key, "SSH key"); path_param!(AddressLotPath, address_lot, "address lot"); +path_param!(ProbePath, probe, "probe"); id_path_param!(GroupPath, group_id, "group"); @@ -1845,3 +1846,19 @@ pub struct UpdateableComponentCreate { pub component_type: shared::UpdateableComponentType, pub device_id: String, } + +// Probes + +#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] +pub struct ProbeCreate { + #[serde(flatten)] + pub identity: IdentityMetadataCreateParams, + pub sled: Uuid, +} + +/// List BGP configs with an optional name or id. +#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, PartialEq)] +pub struct ProbeListSelector { + /// A name or id to use when selecting a probe. + pub name_or_id: Option, +} diff --git a/openapi/nexus.json b/openapi/nexus.json index 15e75f93ff6..8445d10cc4d 100644 --- a/openapi/nexus.json +++ b/openapi/nexus.json @@ -2906,6 +2906,175 @@ } } }, + "/v1/probes": { + "get": { + "tags": [ + "probes" + ], + "summary": "List instrumentation probes.", + "operationId": "probe_list", + "parameters": [ + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "name_or_id", + "description": "A name or id to use when selecting a probe.", + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + }, + { + "in": "query", + "name": "sort_by", + "schema": { + "$ref": "#/components/schemas/NameOrIdSortMode" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProbeResultsPage" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + }, + "x-dropshot-pagination": { + "required": [] + } + }, + "post": { + "tags": [ + "probes" + ], + "summary": "Create an instrumentation probe.", + "operationId": "probe_create", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProbeCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Probe" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v1/probes/{probe}": { + "get": { + "tags": [ + "probes" + ], + "summary": "View an instrumentation probe.", + "operationId": "probe_view", + "parameters": [ + { + "in": "path", + "name": "probe", + "description": "Name or ID of the probe", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Probe" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "tags": [ + "probes" + ], + "summary": "Delete an instrumentation probe.", + "operationId": "probe_delete", + "parameters": [ + { + "in": "path", + "name": "probe", + "description": "Name or ID of the probe", + "required": true, + "schema": { + "$ref": "#/components/schemas/NameOrId" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, "/v1/projects": { "get": { "tags": [ @@ -12403,6 +12572,93 @@ "ok" ] }, + "Probe": { + "description": "Identity-related metadata that's included in nearly all public API objects", + "type": "object", + "properties": { + "description": { + "description": "human-readable free-form text about a resource", + "type": "string" + }, + "id": { + "description": "unique, immutable, system-controlled identifier for each resource", + "type": "string", + "format": "uuid" + }, + "name": { + "description": "unique, mutable, user-controlled identifier for each resource", + "allOf": [ + { + "$ref": "#/components/schemas/Name" + } + ] + }, + "sled": { + "type": "string", + "format": "uuid" + }, + "time_created": { + "description": "timestamp when this resource was created", + "type": "string", + "format": "date-time" + }, + "time_modified": { + "description": "timestamp when this resource was last modified", + "type": "string", + "format": "date-time" + } + }, + "required": [ + "description", + "id", + "name", + "sled", + "time_created", + "time_modified" + ] + }, + "ProbeCreate": { + "description": "Create-time identity-related parameters", + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "name": { + "$ref": "#/components/schemas/Name" + }, + "sled": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "description", + "name", + "sled" + ] + }, + "ProbeResultsPage": { + "description": "A single page of results", + "type": "object", + "properties": { + "items": { + "description": "list of items on this page of results", + "type": "array", + "items": { + "$ref": "#/components/schemas/Probe" + } + }, + "next_page": { + "nullable": true, + "description": "token used to fetch the next page of results (if any)", + "type": "string" + } + }, + "required": [ + "items" + ] + }, "Project": { "description": "View of a Project", "type": "object", @@ -15248,6 +15504,13 @@ "url": "http://docs.oxide.computer/api/policy" } }, + { + "name": "probes", + "description": "Probes for testing network connectivity", + "externalDocs": { + "url": "http://docs.oxide.computer/api/probes" + } + }, { "name": "projects", "description": "Projects are a grouping of associated resources such as instances and disks within a silo for purposes of billing and access control.", diff --git a/schema/crdb/18.0.0/up1.sh b/schema/crdb/18.0.0/up1.sh new file mode 100644 index 00000000000..4f37b0fc00d --- /dev/null +++ b/schema/crdb/18.0.0/up1.sh @@ -0,0 +1,14 @@ +CREATE TABLE IF NOT EXISTS omicron.public.probe ( + id UUID NOT NULL PRIMARY KEY, + name STRING(63) NOT NULL PRIMARY KEY, + description STRING(512) NOT NULL, + time_created TIMESTAMPTZ NOT NULL, + time_modified TIMESTAMPTZ NOT NULL, + time_deleted TIMESTAMPTZ, + sled UUID NOT NULL +); + +CREATE UNIQUE INDEX IF NOT EXISTS lookup_probe_by_name ON omicron.public.probe ( + name +) WHERE + time_deleted IS NULL; diff --git a/schema/crdb/dbinit.sql b/schema/crdb/dbinit.sql index f4caa2a4e69..a23b010d5d2 100644 --- a/schema/crdb/dbinit.sql +++ b/schema/crdb/dbinit.sql @@ -2977,6 +2977,21 @@ STORING ( time_deleted ); +CREATE TABLE IF NOT EXISTS omicron.public.probe ( + id UUID NOT NULL PRIMARY KEY, + name STRING(63) NOT NULL, + description STRING(512) NOT NULL, + time_created TIMESTAMPTZ NOT NULL, + time_modified TIMESTAMPTZ NOT NULL, + time_deleted TIMESTAMPTZ, + sled UUID NOT NULL +); + +CREATE UNIQUE INDEX IF NOT EXISTS lookup_probe_by_name ON omicron.public.probe ( + name +) WHERE + time_deleted IS NULL; + /* * Metadata for the schema itself. This version number isn't great, as there's * nothing to ensure it gets bumped when it should be, but it's a start. @@ -3009,7 +3024,7 @@ INSERT INTO omicron.public.db_metadata ( version, target_version ) VALUES - ( TRUE, NOW(), NOW(), '17.0.0', NULL) + ( TRUE, NOW(), NOW(), '18.0.0', NULL) ON CONFLICT DO NOTHING; COMMIT;