diff --git a/Cargo.lock b/Cargo.lock index 0eebdaf752f..1865029811e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -335,6 +335,15 @@ dependencies = [ "syn", ] +[[package]] +name = "db-macros" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "derivative" version = "2.2.0" @@ -1280,6 +1289,7 @@ dependencies = [ "bb8", "bytes", "chrono", + "db-macros", "diesel", "dropshot", "expectorate", diff --git a/Cargo.toml b/Cargo.toml index 18bb89cd4e2..5686667b9c5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ members = [ "common", "nexus", + "nexus/src/db/db-macros", "rpaths", "sled-agent", "oximeter/oximeter", @@ -15,6 +16,7 @@ members = [ default-members = [ "common", "nexus", + "nexus/src/db/db-macros", "rpaths", "sled-agent", "oximeter/oximeter", diff --git a/common/src/sql/dbinit.sql b/common/src/sql/dbinit.sql index b8a9bf1b748..4a376449ee8 100644 --- a/common/src/sql/dbinit.sql +++ b/common/src/sql/dbinit.sql @@ -39,17 +39,25 @@ CREATE DATABASE omicron; CREATE USER omicron; GRANT INSERT, SELECT, UPDATE, DELETE ON DATABASE omicron to omicron; +/* + * Racks + */ +CREATE TABLE omicron.public.Rack ( + /* Identity metadata (asset) */ + id UUID PRIMARY KEY, + time_created TIMESTAMPTZ NOT NULL, + time_modified TIMESTAMPTZ NOT NULL +); + /* * Sleds */ CREATE TABLE omicron.public.Sled ( - /* Identity metadata */ + /* Identity metadata (asset) */ id UUID PRIMARY KEY, time_created TIMESTAMPTZ NOT NULL, time_modified TIMESTAMPTZ NOT NULL, - /* Indicates that the object has been deleted */ - time_deleted TIMESTAMPTZ, ip INET NOT NULL, port INT4 NOT NULL @@ -83,7 +91,7 @@ CREATE UNIQUE INDEX ON omicron.public.Organization ( */ CREATE TABLE omicron.public.Project ( - /* Identity metadata */ + /* Identity metadata (resource) */ id UUID PRIMARY KEY, name STRING(63) NOT NULL, description STRING(512) NOT NULL, @@ -129,7 +137,7 @@ CREATE UNIQUE INDEX ON omicron.public.Project ( * like. Or changing # of CPUs or memory size. */ CREATE TABLE omicron.public.Instance ( - /* Identity metadata */ + /* Identity metadata (resource) */ id UUID PRIMARY KEY, name STRING(63) NOT NULL, description STRING(512) NOT NULL, @@ -191,7 +199,7 @@ CREATE UNIQUE INDEX ON omicron.public.Instance ( -- ); CREATE TABLE omicron.public.Disk ( - /* Identity metadata */ + /* Identity metadata (resource) */ id UUID PRIMARY KEY, name STRING(63) NOT NULL, description STRING(512) NOT NULL, @@ -274,7 +282,7 @@ CREATE INDEX ON omicron.public.MetricProducer ( CREATE TABLE omicron.public.Vpc ( - /* Identity metadata */ + /* Identity metadata (resource) */ id UUID PRIMARY KEY, name STRING(63) NOT NULL, description STRING(512) NOT NULL, @@ -293,7 +301,7 @@ CREATE UNIQUE INDEX ON omicron.public.Vpc ( time_deleted IS NULL; CREATE TABLE omicron.public.VpcSubnet ( - /* Identity metadata */ + /* Identity metadata (resource) */ id UUID PRIMARY KEY, name STRING(63) NOT NULL, description STRING(512) NOT NULL, @@ -314,7 +322,7 @@ CREATE UNIQUE INDEX ON omicron.public.VpcSubnet ( time_deleted IS NULL; CREATE TABLE omicron.public.NetworkInterface ( - /* Identity metadata */ + /* Identity metadata (resource) */ id UUID PRIMARY KEY, name STRING(63) NOT NULL, description STRING(512) NOT NULL, diff --git a/nexus/Cargo.toml b/nexus/Cargo.toml index 958ca8a2c07..371e3d3481a 100644 --- a/nexus/Cargo.toml +++ b/nexus/Cargo.toml @@ -16,6 +16,7 @@ diesel = { git = "https://github.com/diesel-rs/diesel", rev = "a39dd2e", feature futures = "0.3.15" http = "0.2.5" hyper = "0.14" +db-macros = { path = "src/db/db-macros" } ipnetwork = "0.18" lazy_static = "1.4.0" libc = "0.2.103" diff --git a/nexus/src/db/datastore.rs b/nexus/src/db/datastore.rs index 537895a286d..c9cfc0e47d3 100644 --- a/nexus/src/db/datastore.rs +++ b/nexus/src/db/datastore.rs @@ -18,6 +18,7 @@ * complicated to do safely and generally compared to what we have now. */ +use super::identity::{Asset, Resource}; use super::Pool; use async_bb8_diesel::{AsyncRunQueryDsl, ConnectionManager}; use chrono::Utc; @@ -44,8 +45,9 @@ use crate::db::{ }, model::{ Disk, DiskAttachment, DiskRuntimeState, Generation, Instance, - InstanceRuntimeState, Name, OximeterInfo, ProducerEndpoint, Project, - ProjectUpdate, Sled, Vpc, VpcSubnet, VpcSubnetUpdate, VpcUpdate, + InstanceRuntimeState, Name, Organization, OrganizationUpdate, + OximeterInfo, ProducerEndpoint, Project, ProjectUpdate, Sled, Vpc, + VpcSubnet, VpcSubnetUpdate, VpcUpdate, }, pagination::paginated, update_and_check::{UpdateAndCheck, UpdateStatus}, @@ -76,13 +78,14 @@ impl DataStore { dsl::ip.eq(sled.ip), dsl::port.eq(sled.port), )) + .returning(Sled::as_returning()) .get_result_async(self.pool()) .await .map_err(|e| { public_error_from_diesel_pool_create( e, ResourceType::Sled, - &sled.id.to_string(), + &sled.id().to_string(), ) }) } @@ -93,7 +96,7 @@ impl DataStore { ) -> ListResultVec { use db::schema::sled::dsl; paginated(dsl::sled, dsl::id, pagparams) - .filter(dsl::time_deleted.is_null()) + .select(Sled::as_select()) .load_async(self.pool()) .await .map_err(|e| { @@ -108,8 +111,8 @@ impl DataStore { pub async fn sled_fetch(&self, id: Uuid) -> LookupResult { use db::schema::sled::dsl; dsl::sled - .filter(dsl::time_deleted.is_null()) .filter(dsl::id.eq(id)) + .select(Sled::as_select()) .first_async(self.pool()) .await .map_err(|e| { @@ -124,13 +127,14 @@ impl DataStore { /// Create a organization pub async fn organization_create( &self, - organization: db::model::Organization, - ) -> CreateResult { + organization: Organization, + ) -> CreateResult { use db::schema::organization::dsl; - let name = organization.name().to_string(); + let name = organization.name().as_str().to_string(); diesel::insert_into(dsl::organization) .values(organization) + .returning(Organization::as_returning()) .get_result_async(self.pool()) .await .map_err(|e| { @@ -146,12 +150,13 @@ impl DataStore { pub async fn organization_fetch( &self, name: &Name, - ) -> LookupResult { + ) -> LookupResult { use db::schema::organization::dsl; dsl::organization .filter(dsl::time_deleted.is_null()) .filter(dsl::name.eq(name.clone())) - .first_async::(self.pool()) + .select(Organization::as_select()) + .first_async::(self.pool()) .await .map_err(|e| { public_error_from_diesel_pool( @@ -232,11 +237,12 @@ impl DataStore { pub async fn organizations_list_by_id( &self, pagparams: &DataPageParams<'_, Uuid>, - ) -> ListResultVec { + ) -> ListResultVec { use db::schema::organization::dsl; paginated(dsl::organization, dsl::id, pagparams) .filter(dsl::time_deleted.is_null()) - .load_async::(self.pool()) + .select(Organization::as_select()) + .load_async::(self.pool()) .await .map_err(|e| { public_error_from_diesel_pool( @@ -250,11 +256,12 @@ impl DataStore { pub async fn organizations_list_by_name( &self, pagparams: &DataPageParams<'_, Name>, - ) -> ListResultVec { + ) -> ListResultVec { use db::schema::organization::dsl; paginated(dsl::organization, dsl::name, pagparams) .filter(dsl::time_deleted.is_null()) - .load_async::(self.pool()) + .select(Organization::as_select()) + .load_async::(self.pool()) .await .map_err(|e| { public_error_from_diesel_pool( @@ -270,15 +277,15 @@ impl DataStore { &self, name: &Name, update_params: &api::external::OrganizationUpdateParams, - ) -> UpdateResult { + ) -> UpdateResult { use db::schema::organization::dsl; - let updates: db::model::OrganizationUpdate = - update_params.clone().into(); + let updates: OrganizationUpdate = update_params.clone().into(); diesel::update(dsl::organization) .filter(dsl::time_deleted.is_null()) .filter(dsl::name.eq(name.clone())) .set(updates) + .returning(Organization::as_returning()) .get_result_async(self.pool()) .await .map_err(|e| { @@ -297,9 +304,10 @@ impl DataStore { ) -> CreateResult { use db::schema::project::dsl; - let name = project.name().to_string(); + let name = project.name().as_str().to_string(); diesel::insert_into(dsl::project) .values(project) + .returning(Project::as_returning()) .get_result_async(self.pool()) .await .map_err(|e| { @@ -317,6 +325,7 @@ impl DataStore { dsl::project .filter(dsl::time_deleted.is_null()) .filter(dsl::name.eq(name.clone())) + .select(Project::as_select()) .first_async(self.pool()) .await .map_err(|e| { @@ -341,7 +350,8 @@ impl DataStore { .filter(dsl::time_deleted.is_null()) .filter(dsl::name.eq(name.clone())) .set(dsl::time_deleted.eq(now)) - .get_result_async::(self.pool()) + .returning(Project::as_returning()) + .get_result_async(self.pool()) .await .map_err(|e| { public_error_from_diesel_pool( @@ -381,7 +391,8 @@ impl DataStore { use db::schema::project::dsl; paginated(dsl::project, dsl::id, pagparams) .filter(dsl::time_deleted.is_null()) - .load_async::(self.pool()) + .select(Project::as_select()) + .load_async(self.pool()) .await .map_err(|e| { public_error_from_diesel_pool( @@ -400,7 +411,8 @@ impl DataStore { paginated(dsl::project, dsl::name, &pagparams) .filter(dsl::time_deleted.is_null()) - .load_async::(self.pool()) + .select(Project::as_select()) + .load_async(self.pool()) .await .map_err(|e| { public_error_from_diesel_pool( @@ -424,6 +436,7 @@ impl DataStore { .filter(dsl::time_deleted.is_null()) .filter(dsl::name.eq(name.clone())) .set(updates) + .returning(Project::as_returning()) .get_result_async(self.pool()) .await .map_err(|e| { @@ -477,7 +490,7 @@ impl DataStore { params, runtime_initial.clone(), ); - let name = instance.name.clone(); + let name = instance.name().clone(); let instance: Instance = diesel::insert_into(dsl::instance) .values(instance) .on_conflict(dsl::id) @@ -704,7 +717,7 @@ impl DataStore { params.clone(), runtime_initial.clone(), ); - let name = disk.name.clone(); + let name = disk.name().clone(); let disk: Disk = diesel::insert_into(dsl::disk) .values(disk) .on_conflict(dsl::id) @@ -962,7 +975,8 @@ impl DataStore { paginated(dsl::metricproducer, dsl::id, &pagparams) .filter(dsl::oximeter_id.eq(oximeter_id)) .order_by((dsl::oximeter_id, dsl::id)) - .load_async::(self.pool()) + .select(ProducerEndpoint::as_select()) + .load_async(self.pool()) .await .map_err(|e| { public_error_from_diesel_pool_create( @@ -1119,7 +1133,8 @@ impl DataStore { paginated(dsl::vpc, dsl::name, &pagparams) .filter(dsl::time_deleted.is_null()) .filter(dsl::project_id.eq(*project_id)) - .load_async::(self.pool()) + .select(Vpc::as_select()) + .load_async(self.pool()) .await .map_err(|e| { public_error_from_diesel_pool( @@ -1139,11 +1154,12 @@ impl DataStore { use db::schema::vpc::dsl; let vpc = Vpc::new(*vpc_id, *project_id, params.clone()); - let name = vpc.name.clone(); + let name = vpc.name().clone(); let vpc = diesel::insert_into(dsl::vpc) .values(vpc) .on_conflict(dsl::id) .do_nothing() + .returning(Vpc::as_returning()) .get_result_async(self.pool()) .await .map_err(|e| { @@ -1191,6 +1207,7 @@ impl DataStore { .filter(dsl::time_deleted.is_null()) .filter(dsl::project_id.eq(*project_id)) .filter(dsl::name.eq(vpc_name.clone())) + .select(Vpc::as_select()) .get_result_async(self.pool()) .await .map_err(|e| { @@ -1210,7 +1227,8 @@ impl DataStore { .filter(dsl::time_deleted.is_null()) .filter(dsl::id.eq(*vpc_id)) .set(dsl::time_deleted.eq(now)) - .get_result_async::(self.pool()) + .returning(Vpc::as_returning()) + .get_result_async(self.pool()) .await .map_err(|e| { public_error_from_diesel_pool( @@ -1232,7 +1250,8 @@ impl DataStore { paginated(dsl::vpcsubnet, dsl::name, &pagparams) .filter(dsl::time_deleted.is_null()) .filter(dsl::vpc_id.eq(*vpc_id)) - .load_async::(self.pool()) + .select(VpcSubnet::as_select()) + .load_async(self.pool()) .await .map_err(|e| { public_error_from_diesel_pool( @@ -1253,6 +1272,7 @@ impl DataStore { .filter(dsl::time_deleted.is_null()) .filter(dsl::vpc_id.eq(*vpc_id)) .filter(dsl::name.eq(subnet_name.clone())) + .select(VpcSubnet::as_select()) .get_result_async(self.pool()) .await .map_err(|e| { @@ -1273,11 +1293,12 @@ impl DataStore { use db::schema::vpcsubnet::dsl; let subnet = VpcSubnet::new(*subnet_id, *vpc_id, params.clone()); - let name = subnet.name.clone(); + let name = subnet.name().clone(); let subnet = diesel::insert_into(dsl::vpcsubnet) .values(subnet) .on_conflict(dsl::id) .do_nothing() + .returning(VpcSubnet::as_returning()) .get_result_async(self.pool()) .await .map_err(|e| { @@ -1298,7 +1319,8 @@ impl DataStore { .filter(dsl::time_deleted.is_null()) .filter(dsl::id.eq(*subnet_id)) .set(dsl::time_deleted.eq(now)) - .get_result_async::(self.pool()) + .returning(VpcSubnet::as_returning()) + .get_result_async(self.pool()) .await .map_err(|e| { public_error_from_diesel_pool( diff --git a/nexus/src/db/db-macros/Cargo.toml b/nexus/src/db/db-macros/Cargo.toml new file mode 100644 index 00000000000..29d04f87e3b --- /dev/null +++ b/nexus/src/db/db-macros/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "db-macros" +version = "0.1.0" +authors = ["Sean Klein "] +edition = "2018" + +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = { version = "1.0" } +quote = { version = "1.0" } +syn = { version = "1.0", features = [ "full", "derive", "extra-traits" ] } diff --git a/nexus/src/db/db-macros/src/lib.rs b/nexus/src/db/db-macros/src/lib.rs new file mode 100644 index 00000000000..bf9eecaaf6f --- /dev/null +++ b/nexus/src/db/db-macros/src/lib.rs @@ -0,0 +1,438 @@ +//! Database-related macro implementations. +//! +//! Diesel provides facilities for mapping structures to SQL tables, but these +//! often require additional layers of structures to be usable. This crate +//! provides support for auto-generating structures that are common among many +//! tables. + +// Copyright 2021 Oxide Computer Company + +extern crate proc_macro; + +use proc_macro2::TokenStream; +use quote::{format_ident, quote}; +use syn::spanned::Spanned; +use syn::{Data, DataStruct, DeriveInput, Error, Fields, Ident, Lit, Meta}; + +/// Looks for a Meta-style attribute with a particular identifier. +/// +/// As an example, for an attribute like `#[foo = "bar"]`, we can find this +/// attribute by calling `get_meta_attr(&item.attrs, "foo")`. +fn get_meta_attr(attrs: &[syn::Attribute], name: &str) -> Option { + attrs + .iter() + .filter_map(|attr| attr.parse_meta().ok()) + .find(|meta| meta.path().is_ident(name)) +} + +/// Accesses the "value" part of a name-value Meta attribute. +fn get_attribute_value(meta: &Meta) -> Option<&Lit> { + if let Meta::NameValue(ref nv) = meta { + Some(&nv.lit) + } else { + None + } +} + +/// Looks up a named field within a struct. +fn get_field_with_name<'a>( + data: &'a DataStruct, + name: &str, +) -> Option<&'a syn::Field> { + if let Fields::Named(ref data_fields) = data.fields { + data_fields.named.iter().find(|field| { + if let Some(ident) = &field.ident { + ident == name + } else { + false + } + }) + } else { + None + } +} + +// Describes which derive macro is being used; allows sharing common code. +enum IdentityVariant { + Asset, + Resource, +} + +/// Implements the "Resource" trait, and generates a bespoke Identity struct. +/// +/// Many tables within our database make use of common fields, +/// including: +/// - ID +/// - Name +/// - Description +/// - Time Created +/// - Time Modified +/// - Time Deleted. +/// +/// Although these fields can be refactored into a common structure (to be used +/// within the context of Diesel) they must be uniquely identified for a single +/// table. +#[proc_macro_derive(Resource)] +pub fn resource_target( + input: proc_macro::TokenStream, +) -> proc_macro::TokenStream { + derive_impl(input.into(), IdentityVariant::Resource) + .unwrap_or_else(|e| e.to_compile_error()) + .into() +} + +/// Identical to [`macro@Resource`], but generates fewer fields. +/// +/// Contains: +/// - ID +/// - Time Created +/// - Time Modified +#[proc_macro_derive(Asset)] +pub fn asset_target(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + derive_impl(input.into(), IdentityVariant::Asset) + .unwrap_or_else(|e| e.to_compile_error()) + .into() +} + +// Implementation of `#[derive(Resource)]` and `#[derive(Asset)]`. +fn derive_impl( + tokens: TokenStream, + flavor: IdentityVariant, +) -> syn::Result { + let item = syn::parse2::(tokens)?; + let name = &item.ident; + + // Ensure that the "table_name" attribute exists, and get it. + let table_meta = + get_meta_attr(&item.attrs, "table_name").ok_or_else(|| { + Error::new( + item.span(), + format!( + "Resource needs 'table_name' attribute.\n\ + Try adding #[table_name = \"your_table_name\"] to {}.", + name + ), + ) + })?; + let table_name = get_attribute_value(&table_meta) + .ok_or_else(|| { + Error::new( + item.span(), + "'table_name' needs to be a name-value pair, like #[table_name = foo]" + ) + })?; + + // Ensure that a field named "identity" exists within this struct. + if let Data::Struct(ref data) = item.data { + // We extract type of "identity" and enforce it is the expected type + // using injected traits. + let field = get_field_with_name(data, "identity") + .ok_or_else(|| { + Error::new( + item.span(), + format!( + "{name}Identity must be embedded within {name} as a field named `identity`.\n\ + This proc macro will try to add accessor methods to {name}; this can only be\n\ + accomplished if we know where to access them.", + name=name, + ) + ) + })?; + + return Ok(build(name, table_name, &field.ty, flavor)); + } + + Err(Error::new(item.span(), "Resource can only be derived for structs")) +} + +// Emits generated structures, depending on the requested flavor of identity. +fn build( + struct_name: &Ident, + table_name: &Lit, + observed_identity_ty: &syn::Type, + flavor: IdentityVariant, +) -> TokenStream { + let (identity_struct, resource_impl) = { + match flavor { + IdentityVariant::Resource => ( + build_resource_identity(struct_name, table_name), + build_resource_impl(struct_name, observed_identity_ty), + ), + IdentityVariant::Asset => ( + build_asset_identity(struct_name, table_name), + build_asset_impl(struct_name, observed_identity_ty), + ), + } + }; + quote! { + #identity_struct + #resource_impl + } +} + +// Builds an "Identity" structure for a resource. +fn build_resource_identity( + struct_name: &Ident, + table_name: &Lit, +) -> TokenStream { + let identity_doc = format!( + "Auto-generated identity for [`{}`] from deriving [`macro@Resource`].", + struct_name, + ); + let identity_name = format_ident!("{}Identity", struct_name); + quote! { + #[doc = #identity_doc] + #[derive(Clone, Debug, Selectable, Queryable, Insertable)] + #[table_name = #table_name ] + pub struct #identity_name { + pub id: ::uuid::Uuid, + pub name: crate::db::model::Name, + pub description: ::std::string::String, + pub time_created: ::chrono::DateTime<::chrono::Utc>, + pub time_modified: ::chrono::DateTime<::chrono::Utc>, + pub time_deleted: ::std::option::Option>, + } + + impl #identity_name { + pub fn new( + id: ::uuid::Uuid, + params: ::omicron_common::api::external::IdentityMetadataCreateParams + ) -> Self { + let now = ::chrono::Utc::now(); + Self { + id, + name: params.name.into(), + description: params.description, + time_created: now, + time_modified: now, + time_deleted: None, + } + } + } + } +} + +// Builds an "Identity" structure for an asset. +fn build_asset_identity(struct_name: &Ident, table_name: &Lit) -> TokenStream { + let identity_doc = format!( + "Auto-generated identity for [`{}`] from deriving [`macro@Asset`].", + struct_name, + ); + let identity_name = format_ident!("{}Identity", struct_name); + quote! { + #[doc = #identity_doc] + #[derive(Clone, Debug, Selectable, Queryable, Insertable)] + #[table_name = #table_name ] + pub struct #identity_name { + pub id: ::uuid::Uuid, + pub time_created: ::chrono::DateTime<::chrono::Utc>, + pub time_modified: ::chrono::DateTime<::chrono::Utc>, + } + + impl #identity_name { + pub fn new( + id: ::uuid::Uuid, + ) -> Self { + let now = ::chrono::Utc::now(); + Self { + id, + time_created: now, + time_modified: now, + } + } + } + } +} + +// Implements "Resource" for the requested structure. +fn build_resource_impl( + struct_name: &Ident, + observed_identity_type: &syn::Type, +) -> TokenStream { + let identity_trait = format_ident!("__{}IdentityMarker", struct_name); + let identity_name = format_ident!("{}Identity", struct_name); + quote! { + // Verify that the field named "identity" is actually the generated + // type within the struct deriving Resource. + trait #identity_trait {} + impl #identity_trait for #identity_name {} + const _: () = { + fn assert_identity() {} + fn assert_all() { + assert_identity::<#observed_identity_type>(); + } + }; + + impl crate::db::identity::Resource for #struct_name { + fn id(&self) -> ::uuid::Uuid { + self.identity.id + } + + fn name(&self) -> &crate::db::model::Name { + &self.identity.name + } + + fn description(&self) -> &str { + &self.identity.description + } + + fn time_created(&self) -> ::chrono::DateTime<::chrono::Utc> { + self.identity.time_created + } + + fn time_modified(&self) -> ::chrono::DateTime<::chrono::Utc> { + self.identity.time_modified + } + + fn time_deleted(&self) -> ::std::option::Option<::chrono::DateTime<::chrono::Utc>> { + self.identity.time_deleted + } + } + } +} + +// Implements "Asset" for the requested structure. +fn build_asset_impl( + struct_name: &Ident, + observed_identity_type: &syn::Type, +) -> TokenStream { + let identity_trait = format_ident!("__{}IdentityMarker", struct_name); + let identity_name = format_ident!("{}Identity", struct_name); + quote! { + // Verify that the field named "identity" is actually the generated + // type within the struct deriving Asset. + trait #identity_trait {} + impl #identity_trait for #identity_name {} + const _: () = { + fn assert_identity() {} + fn assert_all() { + assert_identity::<#observed_identity_type>(); + } + }; + + impl crate::db::identity::Asset for #struct_name { + fn id(&self) -> ::uuid::Uuid { + self.identity.id + } + + fn time_created(&self) -> ::chrono::DateTime<::chrono::Utc> { + self.identity.time_created + } + + fn time_modified(&self) -> ::chrono::DateTime<::chrono::Utc> { + self.identity.time_modified + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_derive_metadata_identity_fails_without_table_name() { + let out = derive_impl( + quote! { + #[derive(Resource)] + struct MyTarget { + identity: MyTargetIdentity, + name: String, + is_cool: bool, + } + } + .into(), + IdentityVariant::Resource, + ); + assert!(out.is_err()); + assert_eq!( + "Resource needs 'table_name' attribute.\n\ + Try adding #[table_name = \"your_table_name\"] to MyTarget.", + out.unwrap_err().to_string() + ); + } + + #[test] + fn test_derive_metadata_identity_fails_with_wrong_table_name_type() { + let out = derive_impl( + quote! { + #[derive(Resource)] + #[table_name] + struct MyTarget { + identity: MyTargetIdentity, + name: String, + is_cool: bool, + } + } + .into(), + IdentityVariant::Resource, + ); + assert!(out.is_err()); + assert_eq!( + "'table_name' needs to be a name-value pair, like #[table_name = foo]", + out.unwrap_err().to_string() + ); + } + + #[test] + fn test_derive_metadata_identity_fails_for_enums() { + let out = derive_impl( + quote! { + #[derive(Resource)] + #[table_name = "foo"] + enum MyTarget { + Foo, + Bar, + } + } + .into(), + IdentityVariant::Resource, + ); + assert!(out.is_err()); + assert_eq!( + "Resource can only be derived for structs", + out.unwrap_err().to_string() + ); + } + + #[test] + fn test_derive_metadata_identity_fails_without_embedded_identity() { + let out = derive_impl( + quote! { + #[derive(Resource)] + #[table_name = "my_target"] + struct MyTarget { + name: String, + is_cool: bool, + } + } + .into(), + IdentityVariant::Resource, + ); + assert!(out.is_err()); + assert_eq!( + "MyTargetIdentity must be embedded within MyTarget as a field named `identity`.\n\ + This proc macro will try to add accessor methods to MyTarget; this can only be\n\ + accomplished if we know where to access them.", + out.unwrap_err().to_string() + ); + } + + #[test] + fn test_derive_metadata_identity_minimal_example_compiles() { + let out = derive_impl( + quote! { + #[derive(Resource)] + #[table_name = "my_target"] + struct MyTarget { + identity: MyTargetIdentity, + name: String, + is_cool: bool, + } + } + .into(), + IdentityVariant::Resource, + ); + assert!(out.is_ok()); + } +} diff --git a/nexus/src/db/identity.rs b/nexus/src/db/identity.rs new file mode 100644 index 00000000000..58695f017ac --- /dev/null +++ b/nexus/src/db/identity.rs @@ -0,0 +1,59 @@ +//! Identity-related traits which may be derived for DB structures. + +// Copyright 2021 Oxide Computer Company + +use super::model::Name; +use chrono::{DateTime, Utc}; +use omicron_common::api::external; +use std::convert::TryFrom; +use uuid::Uuid; + +/// Identity-related accessors for resources. +/// +/// These are end-user-visible objects with names, descriptions, +/// and which may be soft-deleted. +/// +/// For durable objects which do not require soft-deletion or descriptions, +/// consider the [`Asset`] trait instead. +/// +/// May be derived from [`macro@db_macros::Resource`]. +pub trait Resource { + fn id(&self) -> Uuid; + fn name(&self) -> &Name; + fn description(&self) -> &str; + fn time_created(&self) -> DateTime; + fn time_modified(&self) -> DateTime; + fn time_deleted(&self) -> Option>; + + fn identity(&self) -> external::IdentityMetadata { + external::IdentityMetadata { + id: self.id(), + name: self.name().clone().into(), + description: self.description().to_string(), + time_created: self.time_created(), + time_modified: self.time_modified(), + } + } +} + +/// Identity-related accessors for assets. +/// +/// These are objects similar to [`Resource`], but without +/// names, descriptions, or soft deletions. +/// +/// May be derived from [`macro@db_macros::Asset`]. +pub trait Asset { + fn id(&self) -> Uuid; + fn time_created(&self) -> DateTime; + fn time_modified(&self) -> DateTime; + + fn identity(&self) -> external::IdentityMetadata { + external::IdentityMetadata { + id: self.id(), + name: external::Name::try_from("no-name").unwrap(), + description: "no description".to_string(), + time_created: self.time_created(), + time_modified: self.time_modified(), + } + } +} diff --git a/nexus/src/db/mod.rs b/nexus/src/db/mod.rs index b849c5c2a67..283c4f00dbb 100644 --- a/nexus/src/db/mod.rs +++ b/nexus/src/db/mod.rs @@ -15,6 +15,7 @@ mod update_and_check; #[cfg(test)] mod test_utils; +pub mod identity; pub mod model; pub mod schema; diff --git a/nexus/src/db/model.rs b/nexus/src/db/model.rs index d82ac67e5c7..fb0b5edc1ff 100644 --- a/nexus/src/db/model.rs +++ b/nexus/src/db/model.rs @@ -1,10 +1,12 @@ //! Structures stored to the database. -use super::schema::{ +use crate::db::identity::{Asset, Resource}; +use crate::db::schema::{ disk, instance, metricproducer, networkinterface, organization, oximeter, - project, sled, vpc, vpcsubnet, + project, rack, sled, vpc, vpcsubnet, }; use chrono::{DateTime, Utc}; +use db_macros::{Asset, Resource}; use diesel::backend::{Backend, RawValue}; use diesel::deserialize::{self, FromSql}; use diesel::serialize::{self, ToSql}; @@ -278,25 +280,25 @@ where // // However, it likely will be in the future - for the single-rack // case, however, it is synthesized. +#[derive(Queryable, Insertable, Debug, Clone, Selectable, Asset)] +#[table_name = "rack"] pub struct Rack { - pub identity: IdentityMetadata, + #[diesel(embed)] + pub identity: RackIdentity, } impl Into for Rack { fn into(self) -> external::Rack { - external::Rack { identity: self.identity.into() } + external::Rack { identity: self.identity() } } } /// Database representation of a Sled. -#[derive(Queryable, Identifiable, Insertable, Debug, Clone)] +#[derive(Queryable, Insertable, Debug, Clone, Selectable, Asset)] #[table_name = "sled"] pub struct Sled { - // IdentityMetadata - pub id: Uuid, - pub time_created: DateTime, - pub time_modified: DateTime, - pub time_deleted: Option>, + #[diesel(embed)] + identity: SledIdentity, // ServiceAddress (Sled Agent). pub ip: ipnetwork::IpNetwork, @@ -304,26 +306,14 @@ pub struct Sled { } impl Sled { - pub fn new( - id: Uuid, - addr: SocketAddr, - params: external::IdentityMetadataCreateParams, - ) -> Self { - let identity = IdentityMetadata::new(id, params); + pub fn new(id: Uuid, addr: SocketAddr) -> Self { Self { - id, - time_created: identity.time_created, - time_modified: identity.time_modified, - time_deleted: identity.time_deleted, + identity: SledIdentity::new(id), ip: addr.ip().into(), port: addr.port().into(), } } - pub fn id(&self) -> &Uuid { - &self.id - } - pub fn address(&self) -> SocketAddr { // TODO: avoid this unwrap SocketAddr::new(self.ip.ip(), u16::try_from(self.port).unwrap()) @@ -333,80 +323,16 @@ impl Sled { impl Into for Sled { fn into(self) -> external::Sled { let service_address = self.address(); - external::Sled { - identity: external::IdentityMetadata { - id: self.id, - name: external::Name::try_from("sled").unwrap(), - description: "sled description".to_string(), - time_created: self.time_created, - time_modified: self.time_modified, - }, - service_address, - } - } -} - -// TODO: As Diesel needs to flatten things out, this structure -// may become unused. -#[derive(Clone, Debug)] -pub struct IdentityMetadata { - pub id: Uuid, - pub name: Name, - pub description: String, - pub time_created: DateTime, - pub time_modified: DateTime, - pub time_deleted: Option>, -} - -impl IdentityMetadata { - fn new(id: Uuid, params: external::IdentityMetadataCreateParams) -> Self { - let now = Utc::now(); - Self { - id, - name: Name(params.name), - description: params.description, - time_created: now, - time_modified: now, - time_deleted: None, - } - } -} - -impl Into for IdentityMetadata { - fn into(self) -> external::IdentityMetadata { - external::IdentityMetadata { - id: self.id, - name: self.name.0, - description: self.description, - time_created: self.time_created, - time_modified: self.time_modified, - } - } -} - -impl From for IdentityMetadata { - fn from(metadata: external::IdentityMetadata) -> Self { - Self { - id: metadata.id, - name: Name(metadata.name), - description: metadata.description, - time_created: metadata.time_created, - time_modified: metadata.time_modified, - time_deleted: None, - } + external::Sled { identity: self.identity(), service_address } } } /// Describes an organization within the database. -#[derive(Queryable, Identifiable, Insertable, Debug)] +#[derive(Queryable, Insertable, Debug, Resource, Selectable)] #[table_name = "organization"] pub struct Organization { - pub id: Uuid, - pub name: Name, - pub description: String, - pub time_created: DateTime, - pub time_modified: DateTime, - pub time_deleted: Option>, + #[diesel(embed)] + identity: OrganizationIdentity, /// child resource generation number, per RFD 192 pub rcgen: Generation, @@ -416,53 +342,16 @@ impl Organization { /// Creates a new database Organization object. pub fn new(params: external::OrganizationCreateParams) -> Self { let id = Uuid::new_v4(); - let identity = IdentityMetadata::new(id, params.identity); Self { - id: identity.id, - name: identity.name, - description: identity.description, - time_created: identity.time_created, - time_modified: identity.time_modified, - time_deleted: identity.time_deleted, + identity: OrganizationIdentity::new(id, params.identity), rcgen: Generation::new(), } } - - pub fn name(&self) -> &str { - self.name.as_str() - } - - pub fn id(&self) -> &Uuid { - &self.id - } } impl Into for Organization { fn into(self) -> external::Organization { - external::Organization { - identity: external::IdentityMetadata { - id: self.id, - name: self.name.into(), - description: self.description, - time_created: self.time_created, - time_modified: self.time_modified, - }, - } - } -} - -/// Conversion from the external API type. -impl From for Organization { - fn from(organization: external::Organization) -> Self { - Self { - id: organization.identity.id, - name: organization.identity.name.into(), - description: organization.identity.description, - time_created: organization.identity.time_created, - time_modified: organization.identity.time_modified, - time_deleted: None, - rcgen: Generation::new(), - } + external::Organization { identity: self.identity() } } } @@ -486,66 +375,23 @@ impl From for OrganizationUpdate { } /// Describes a project within the database. -#[derive(Queryable, Identifiable, Insertable, Debug)] +#[derive(Selectable, Queryable, Insertable, Debug, Resource)] #[table_name = "project"] pub struct Project { - pub id: Uuid, - pub name: Name, - pub description: String, - pub time_created: DateTime, - pub time_modified: DateTime, - pub time_deleted: Option>, + #[diesel(embed)] + identity: ProjectIdentity, } impl Project { /// Creates a new database Project object. pub fn new(params: external::ProjectCreateParams) -> Self { - let id = Uuid::new_v4(); - let identity = IdentityMetadata::new(id, params.identity); - Self { - id: identity.id, - name: identity.name, - description: identity.description, - time_created: identity.time_created, - time_modified: identity.time_modified, - time_deleted: identity.time_deleted, - } - } - - pub fn name(&self) -> &str { - self.name.as_str() - } - - pub fn id(&self) -> &Uuid { - &self.id + Self { identity: ProjectIdentity::new(Uuid::new_v4(), params.identity) } } } impl Into for Project { fn into(self) -> external::Project { - external::Project { - identity: external::IdentityMetadata { - id: self.id, - name: self.name.0, - description: self.description, - time_created: self.time_created, - time_modified: self.time_modified, - }, - } - } -} - -/// Conversion from the internal API type. -impl From for Project { - fn from(project: external::Project) -> Self { - Self { - id: project.identity.id, - name: Name(project.identity.name), - description: project.identity.description, - time_created: project.identity.time_created, - time_modified: project.identity.time_modified, - time_deleted: None, - } + external::Project { identity: self.identity() } } } @@ -569,15 +415,11 @@ impl From for ProjectUpdate { } /// An Instance (VM). -#[derive(Queryable, Identifiable, Insertable, Debug, Selectable)] +#[derive(Queryable, Insertable, Debug, Selectable, Resource)] #[table_name = "instance"] pub struct Instance { - pub id: Uuid, - pub name: Name, - pub description: String, - pub time_created: DateTime, - pub time_modified: DateTime, - pub time_deleted: Option>, + #[diesel(embed)] + identity: InstanceIdentity, /// id for the project containing this Instance pub project_id: Uuid, @@ -595,33 +437,8 @@ impl Instance { runtime: InstanceRuntimeState, ) -> Self { let identity = - IdentityMetadata::new(instance_id, params.identity.clone()); - Self { - id: identity.id, - name: identity.name, - description: identity.description, - time_created: identity.time_created, - time_modified: identity.time_modified, - time_deleted: identity.time_deleted, - - project_id, - - runtime_state: runtime, - } - } - - // TODO: We could definitely derive this. - // We could actually derive any "into subset" struct with - // identically named fields. - pub fn identity(&self) -> IdentityMetadata { - IdentityMetadata { - id: self.id, - name: self.name.clone(), - description: self.description.clone(), - time_created: self.time_created, - time_modified: self.time_modified, - time_deleted: self.time_deleted, - } + InstanceIdentity::new(instance_id, params.identity.clone()); + Self { identity, project_id, runtime_state: runtime } } pub fn runtime(&self) -> &InstanceRuntimeState { @@ -633,7 +450,7 @@ impl Instance { impl Into for Instance { fn into(self) -> external::Instance { external::Instance { - identity: self.identity().into(), + identity: self.identity(), project_id: self.project_id, ncpus: self.runtime().ncpus.into(), memory: self.runtime().memory.into(), @@ -755,16 +572,11 @@ where } /// A Disk (network block device). -#[derive(Queryable, Identifiable, Insertable, Clone, Debug, Selectable)] +#[derive(Queryable, Insertable, Clone, Debug, Selectable, Resource)] #[table_name = "disk"] pub struct Disk { - // IdentityMetadata - pub id: Uuid, - pub name: Name, - pub description: String, - pub time_created: DateTime, - pub time_modified: DateTime, - pub time_deleted: Option>, + #[diesel(embed)] + identity: DiskIdentity, /// id for the project containing this Disk pub project_id: Uuid, @@ -789,40 +601,16 @@ impl Disk { params: external::DiskCreateParams, runtime_initial: DiskRuntimeState, ) -> Self { - let identity = IdentityMetadata::new(disk_id, params.identity); + let identity = DiskIdentity::new(disk_id, params.identity); Self { - id: identity.id, - name: identity.name, - description: identity.description, - time_created: identity.time_created, - time_modified: identity.time_modified, - time_deleted: identity.time_deleted, - + identity, project_id, - - runtime_state: DiskRuntimeState { - disk_state: runtime_initial.disk_state, - attach_instance_id: runtime_initial.attach_instance_id, - gen: runtime_initial.gen, - time_updated: runtime_initial.time_updated, - }, - + runtime_state: runtime_initial, size: params.size.into(), create_snapshot_id: params.snapshot_id, } } - pub fn identity(&self) -> IdentityMetadata { - IdentityMetadata { - id: self.id, - name: self.name.clone(), - description: self.description.clone(), - time_created: self.time_created, - time_modified: self.time_modified, - time_deleted: self.time_deleted, - } - } - pub fn state(&self) -> DiskState { self.runtime_state.state() } @@ -835,8 +623,8 @@ impl Disk { if let Some(instance_id) = self.runtime_state.attach_instance_id { Some(DiskAttachment { instance_id, - disk_id: self.id, - disk_name: self.name.clone(), + disk_id: self.id(), + disk_name: self.name().clone(), disk_state: self.state(), }) } else { @@ -848,9 +636,9 @@ impl Disk { /// Conversion to the external API type. impl Into for Disk { fn into(self) -> external::Disk { - let device_path = format!("/mnt/{}", self.name.as_str()); + let device_path = format!("/mnt/{}", self.name().as_str()); external::Disk { - identity: self.identity().into(), + identity: self.identity(), project_id: self.project_id, snapshot_id: self.create_snapshot_id, size: self.size.into(), @@ -991,12 +779,12 @@ impl Into for DiskAttachment { /// Information announced by a metric server, used so that clients can contact it and collect /// available metric data from it. -#[derive(Queryable, Identifiable, Insertable, Debug, Clone, Selectable)] +#[derive(Queryable, Insertable, Debug, Clone, Selectable, Asset)] #[table_name = "metricproducer"] pub struct ProducerEndpoint { - pub id: Uuid, - pub time_created: DateTime, - pub time_modified: DateTime, + #[diesel(embed)] + identity: ProducerEndpointIdentity, + pub ip: ipnetwork::IpNetwork, pub port: i32, pub interval: f64, @@ -1011,11 +799,8 @@ impl ProducerEndpoint { endpoint: &internal::nexus::ProducerEndpoint, oximeter_id: Uuid, ) -> Self { - let now = Utc::now(); Self { - id: endpoint.id, - time_created: now, - time_modified: now, + identity: ProducerEndpointIdentity::new(endpoint.id), ip: endpoint.address.ip().into(), port: endpoint.address.port().into(), base_route: endpoint.base_route.clone(), @@ -1026,12 +811,12 @@ impl ProducerEndpoint { /// Return the route that can be used to request metric data. pub fn collection_route(&self) -> String { - format!("{}/{}", &self.base_route, &self.id) + format!("{}/{}", &self.base_route, self.id()) } } /// Message used to notify Nexus that this oximeter instance is up and running. -#[derive(Queryable, Identifiable, Insertable, Debug, Clone, Copy)] +#[derive(Queryable, Insertable, Debug, Clone, Copy)] #[table_name = "oximeter"] pub struct OximeterInfo { /// The ID for this oximeter instance. @@ -1058,15 +843,11 @@ impl OximeterInfo { } } -#[derive(Queryable, Identifiable, Insertable, Clone, Debug)] +#[derive(Queryable, Insertable, Clone, Debug, Selectable, Resource)] #[table_name = "vpc"] pub struct Vpc { - pub id: Uuid, - pub name: Name, - pub description: String, - pub time_created: DateTime, - pub time_modified: DateTime, - pub time_deleted: Option>, + #[diesel(embed)] + identity: VpcIdentity, pub project_id: Uuid, pub dns_name: Name, @@ -1078,36 +859,15 @@ impl Vpc { project_id: Uuid, params: external::VpcCreateParams, ) -> Self { - let identity = IdentityMetadata::new(vpc_id, params.identity); - Self { - id: identity.id, - name: identity.name, - description: identity.description, - time_created: identity.time_created, - time_modified: identity.time_modified, - time_deleted: identity.time_deleted, - - project_id, - dns_name: Name(params.dns_name), - } - } - - pub fn identity(&self) -> IdentityMetadata { - IdentityMetadata { - id: self.id, - name: self.name.clone(), - description: self.description.clone(), - time_created: self.time_created, - time_modified: self.time_modified, - time_deleted: self.time_deleted, - } + let identity = VpcIdentity::new(vpc_id, params.identity); + Self { identity, project_id, dns_name: params.dns_name.into() } } } impl Into for Vpc { fn into(self) -> external::Vpc { external::Vpc { - identity: self.identity().into(), + identity: self.identity(), project_id: self.project_id, dns_name: self.dns_name.0, } @@ -1134,15 +894,11 @@ impl From for VpcUpdate { } } -#[derive(Queryable, Identifiable, Insertable, Clone, Debug)] +#[derive(Queryable, Insertable, Clone, Debug, Selectable, Resource)] #[table_name = "vpcsubnet"] pub struct VpcSubnet { - pub id: Uuid, - pub name: Name, - pub description: String, - pub time_created: DateTime, - pub time_modified: DateTime, - pub time_deleted: Option>, + #[diesel(embed)] + identity: VpcSubnetIdentity, pub vpc_id: Uuid, pub ipv4_block: Option, @@ -1155,38 +911,20 @@ impl VpcSubnet { vpc_id: Uuid, params: external::VpcSubnetCreateParams, ) -> Self { - let identity = IdentityMetadata::new(subnet_id, params.identity); + let identity = VpcSubnetIdentity::new(subnet_id, params.identity); Self { - id: identity.id, - name: identity.name, - description: identity.description, - time_created: identity.time_created, - time_modified: identity.time_modified, - time_deleted: identity.time_deleted, - + identity, vpc_id, - ipv4_block: params.ipv4_block.map(Ipv4Net), ipv6_block: params.ipv6_block.map(Ipv6Net), } } - - pub fn identity(&self) -> IdentityMetadata { - IdentityMetadata { - id: self.id, - name: self.name.clone(), - description: self.description.clone(), - time_created: self.time_created, - time_modified: self.time_modified, - time_deleted: self.time_deleted, - } - } } impl Into for VpcSubnet { fn into(self) -> external::VpcSubnet { external::VpcSubnet { - identity: self.identity().into(), + identity: self.identity(), vpc_id: self.vpc_id, ipv4_block: self.ipv4_block.map(|ip| ip.into()), ipv6_block: self.ipv6_block.map(|ip| ip.into()), @@ -1216,31 +954,14 @@ impl From for VpcSubnetUpdate { } } -#[derive(Queryable, Identifiable, Insertable, Clone, Debug)] +#[derive(Queryable, Insertable, Clone, Debug, Resource)] #[table_name = "networkinterface"] pub struct NetworkInterface { - pub id: Uuid, - pub name: Name, - pub description: String, - pub time_created: DateTime, - pub time_modified: DateTime, - pub time_deleted: Option>, + #[diesel(embed)] + pub identity: NetworkInterfaceIdentity, pub vpc_id: Uuid, pub subnet_id: Uuid, pub mac: MacAddr, pub ip: ipnetwork::IpNetwork, } - -impl NetworkInterface { - pub fn identity(&self) -> IdentityMetadata { - IdentityMetadata { - id: self.id, - name: self.name.clone(), - description: self.description.clone(), - time_created: self.time_created, - time_modified: self.time_modified, - time_deleted: self.time_deleted, - } - } -} diff --git a/nexus/src/db/schema.rs b/nexus/src/db/schema.rs index 6d0dd289020..664a7b0cbe8 100644 --- a/nexus/src/db/schema.rs +++ b/nexus/src/db/schema.rs @@ -125,12 +125,19 @@ table! { } } +table! { + rack (id) { + id -> Uuid, + time_created -> Timestamptz, + time_modified -> Timestamptz, + } +} + table! { sled (id) { id -> Uuid, time_created -> Timestamptz, time_modified -> Timestamptz, - time_deleted -> Nullable, ip -> Inet, port -> Int4, diff --git a/nexus/src/nexus.rs b/nexus/src/nexus.rs index 3a7224bd77e..35a02e5073e 100644 --- a/nexus/src/nexus.rs +++ b/nexus/src/nexus.rs @@ -3,12 +3,12 @@ */ use crate::db; +use crate::db::identity::{Asset, Resource}; use crate::db::model::Name; use crate::saga_interface::SagaContext; use crate::sagas; use anyhow::Context; use async_trait::async_trait; -use chrono::Utc; use futures::future::ready; use futures::StreamExt; use omicron_common::api::external; @@ -19,7 +19,6 @@ use omicron_common::api::external::DiskAttachment; use omicron_common::api::external::DiskCreateParams; use omicron_common::api::external::DiskState; use omicron_common::api::external::Error; -use omicron_common::api::external::IdentityMetadata; use omicron_common::api::external::IdentityMetadataCreateParams; use omicron_common::api::external::InstanceCreateParams; use omicron_common::api::external::InstanceState; @@ -99,7 +98,7 @@ pub struct Nexus { log: Logger, /** cached rack identity metadata */ - api_rack_identity: IdentityMetadata, + api_rack_identity: db::model::RackIdentity, /** persistent storage for resources in the control plane */ db_datastore: Arc, @@ -148,14 +147,7 @@ impl Nexus { let nexus = Nexus { rack_id: *rack_id, log: log.new(o!()), - api_rack_identity: IdentityMetadata { - id: *rack_id, - name: external::Name::try_from(format!("rack-{}", *rack_id)) - .unwrap(), - description: String::from(""), - time_created: Utc::now(), - time_modified: Utc::now(), - }, + api_rack_identity: db::model::RackIdentity::new(*rack_id), db_datastore: Arc::clone(&db_datastore), sec_client: Arc::clone(&sec_client), recovery_task: std::sync::Mutex::new(None), @@ -188,11 +180,7 @@ impl Nexus { info!(self.log, "registered sled agent"; "sled_uuid" => id.to_string()); // Insert the sled into the database. - let create_params = IdentityMetadataCreateParams { - name: external::Name::try_from("sled").unwrap(), - description: "Self-Identified Sled".to_string(), - }; - let sled = db::model::Sled::new(id, address, create_params); + let sled = db::model::Sled::new(id, address); self.db_datastore.sled_upsert(sled).await?; Ok(()) @@ -239,7 +227,7 @@ impl Nexus { ); for producer in producers.into_iter() { let producer_info = ProducerEndpoint { - id: producer.id, + id: producer.id(), address: SocketAddr::new( producer.ip.ip(), producer.port.try_into().unwrap(), @@ -424,7 +412,7 @@ impl Nexus { .db_datastore .project_create_vpc( &id, - db_project.id(), + &db_project.id(), &VpcCreateParams { identity: IdentityMetadataCreateParams { name: external::Name::try_from("default").unwrap(), @@ -510,7 +498,7 @@ impl Nexus { .db_datastore .project_create_disk( &disk_id, - project.id(), + &project.id(), params, &db::model::DiskRuntimeState::new(), ) @@ -577,7 +565,7 @@ impl Nexus { * before actually beginning the attach process. Sagas can maybe * address that. */ - self.db_datastore.project_delete_disk(&disk.id).await + self.db_datastore.project_delete_disk(&disk.id()).await } /* @@ -604,7 +592,7 @@ impl Nexus { .ok_or_else(|| Error::ServiceUnavailable { message: String::from("no sleds available for new Instance"), }) - .map(|s| *s.id()) + .map(|s| s.id()) } pub async fn project_list_instances( @@ -708,7 +696,7 @@ impl Nexus { .db_datastore .instance_fetch_by_name(&project_id, instance_name) .await?; - self.db_datastore.project_delete_instance(&instance.id).await + self.db_datastore.project_delete_instance(&instance.id()).await } pub async fn project_lookup_instance( @@ -820,7 +808,7 @@ impl Nexus { }, ) .await?; - self.db_datastore.instance_fetch(&instance.id).await + self.db_datastore.instance_fetch(&instance.id()).await } /** @@ -843,7 +831,7 @@ impl Nexus { }, ) .await?; - self.db_datastore.instance_fetch(&instance.id).await + self.db_datastore.instance_fetch(&instance.id()).await } /** @@ -866,7 +854,7 @@ impl Nexus { }, ) .await?; - self.db_datastore.instance_fetch(&instance.id).await + self.db_datastore.instance_fetch(&instance.id()).await } /** @@ -895,11 +883,11 @@ impl Nexus { }; let new_runtime = sa - .instance_ensure(instance.id, instance_hardware, requested) + .instance_ensure(instance.id(), instance_hardware, requested) .await?; self.db_datastore - .instance_update_runtime(&instance.id, &new_runtime.into()) + .instance_update_runtime(&instance.id(), &new_runtime.into()) .await .map(|_| ()) } @@ -915,7 +903,7 @@ impl Nexus { ) -> ListResultVec { let instance = self.project_lookup_instance(project_name, instance_name).await?; - self.db_datastore.instance_list_disks(&instance.id, pagparams).await + self.db_datastore.instance_list_disks(&instance.id(), pagparams).await } /** @@ -931,11 +919,11 @@ impl Nexus { self.project_lookup_instance(project_name, instance_name).await?; let disk = self.project_lookup_disk(project_name, disk_name).await?; if let Some(instance_id) = disk.runtime_state.attach_instance_id { - if instance_id == instance.id { + if instance_id == instance.id() { return Ok(DiskAttachment { - instance_id: instance.id, - disk_name: disk.name.clone().into(), - disk_id: disk.id, + instance_id: instance.id(), + disk_name: disk.name().clone().into(), + disk_id: disk.id(), disk_state: disk.state().into(), }); } @@ -963,20 +951,20 @@ impl Nexus { let instance = self.project_lookup_instance(project_name, instance_name).await?; let disk = self.project_lookup_disk(project_name, disk_name).await?; - let instance_id = &instance.id; + let instance_id = &instance.id(); fn disk_attachment_for( instance: &db::model::Instance, disk: &db::model::Disk, ) -> CreateResult { assert_eq!( - instance.id, + instance.id(), disk.runtime_state.attach_instance_id.unwrap() ); Ok(DiskAttachment { - instance_id: instance.id, - disk_id: disk.id, - disk_name: disk.name.clone().into(), + instance_id: instance.id(), + disk_id: disk.id(), + disk_name: disk.name().clone().into(), disk_state: disk.runtime().state().into(), }) } @@ -1008,7 +996,7 @@ impl Nexus { }; let message = format!( "cannot attach disk \"{}\": {}", - disk.name.as_str(), + disk.name().as_str(), disk_status ); Err(Error::InvalidRequest { message }) @@ -1062,7 +1050,7 @@ impl Nexus { DiskStateRequested::Attached(*instance_id), ) .await?; - let disk = self.db_datastore.disk_fetch(&disk.id).await?; + let disk = self.db_datastore.disk_fetch(&disk.id()).await?; disk_attachment_for(&instance, &disk) } @@ -1078,7 +1066,7 @@ impl Nexus { let instance = self.project_lookup_instance(project_name, instance_name).await?; let disk = self.project_lookup_disk(project_name, disk_name).await?; - let instance_id = &instance.id; + let instance_id = &instance.id(); match &disk.state().into() { /* @@ -1140,9 +1128,9 @@ impl Nexus { * reflect the new intermediate state. */ let new_runtime = - sa.disk_ensure(disk.id, disk.runtime().into(), requested).await?; + sa.disk_ensure(disk.id(), disk.runtime().into(), requested).await?; self.db_datastore - .disk_update_runtime(&disk.id, &new_runtime.into()) + .disk_update_runtime(&disk.id(), &new_runtime.into()) .await .map(|_| ()) } @@ -1203,7 +1191,7 @@ impl Nexus { self.db_datastore.project_lookup_id_by_name(project_name).await?; let vpc = self.db_datastore.vpc_fetch_by_name(&project_id, vpc_name).await?; - Ok(self.db_datastore.project_update_vpc(&vpc.id, params).await?) + Ok(self.db_datastore.project_update_vpc(&vpc.id(), params).await?) } pub async fn project_delete_vpc( @@ -1293,7 +1281,7 @@ impl Nexus { */ fn as_rack(&self) -> db::model::Rack { - db::model::Rack { identity: self.api_rack_identity.clone().into() } + db::model::Rack { identity: self.api_rack_identity.clone() } } pub async fn racks_list(