From 57c3c755bfdab632a535bc13c774e07ecf03c510 Mon Sep 17 00:00:00 2001 From: "Andrew J. Stone" Date: Wed, 28 Aug 2024 16:12:51 -0400 Subject: [PATCH] Decouple inventory and blueprint zone types (#6466) There should be no behavioral changes in this PR and it is solely a refactoring for future ease of development. When converting between DB types and omicron internal types for zones, we previously had some common types shared between the inventory and blueprint zones to help with the conversions because the commonality is so great. However, blueprint zone types can contain extra, auxiliary information to help with planning, and the conversion to the common types required passing in extra values to the common constructor for the blueprint types and then passing `None` for the inventory types. Currently the only extra type contained in the blueprint types is an `ExternalIpUuid`. However, as we expand the reconfigurator we anticipate the blueprint gaining more auxiliary information and diverging further from the inventory types. This is already starting to happen with the replicated clickhouse zone types. Therefore, this PR removes the common types altogether, except for the `OmicronZoneNic` type, and adds some helper functions that can be used for both blueprint and inventory conversions. The logic of these functions is largely copied over from what was previously in the shared type generation code. --- nexus/db-model/src/deployment.rs | 525 ++++++++++++++--- nexus/db-model/src/inventory.rs | 403 +++++++++++-- nexus/db-model/src/omicron_zone_config.rs | 664 +++------------------- 3 files changed, 868 insertions(+), 724 deletions(-) diff --git a/nexus/db-model/src/deployment.rs b/nexus/db-model/src/deployment.rs index 6bef893a5b..b4c60e12ef 100644 --- a/nexus/db-model/src/deployment.rs +++ b/nexus/db-model/src/deployment.rs @@ -6,7 +6,7 @@ //! database use crate::inventory::ZoneType; -use crate::omicron_zone_config::{OmicronZone, OmicronZoneNic}; +use crate::omicron_zone_config::{self, OmicronZoneNic}; use crate::schema::{ blueprint, bp_omicron_physical_disk, bp_omicron_zone, bp_omicron_zone_nic, bp_sled_omicron_physical_disks, bp_sled_omicron_zones, bp_sled_state, @@ -17,21 +17,31 @@ use crate::{ impl_enum_type, ipv6, Generation, MacAddr, Name, SledState, SqlU16, SqlU32, SqlU8, }; +use anyhow::{anyhow, bail, Context, Result}; use chrono::{DateTime, Utc}; use ipnetwork::IpNetwork; -use nexus_types::deployment::BlueprintPhysicalDiskConfig; -use nexus_types::deployment::BlueprintPhysicalDisksConfig; +use nexus_sled_agent_shared::inventory::OmicronZoneDataset; use nexus_types::deployment::BlueprintTarget; use nexus_types::deployment::BlueprintZoneConfig; use nexus_types::deployment::BlueprintZoneDisposition; use nexus_types::deployment::BlueprintZonesConfig; use nexus_types::deployment::CockroachDbPreserveDowngrade; +use nexus_types::deployment::{ + blueprint_zone_type, BlueprintPhysicalDisksConfig, +}; +use nexus_types::deployment::{BlueprintPhysicalDiskConfig, BlueprintZoneType}; +use nexus_types::deployment::{ + OmicronZoneExternalFloatingAddr, OmicronZoneExternalFloatingIp, + OmicronZoneExternalSnatIp, +}; use omicron_common::api::internal::shared::NetworkInterface; use omicron_common::disk::DiskIdentity; -use omicron_uuid_kinds::GenericUuid; +use omicron_common::zpool_name::ZpoolName; use omicron_uuid_kinds::SledUuid; use omicron_uuid_kinds::ZpoolUuid; use omicron_uuid_kinds::{ExternalIpKind, SledKind, ZpoolKind}; +use omicron_uuid_kinds::{ExternalIpUuid, GenericUuid, OmicronZoneUuid}; +use std::net::{IpAddr, SocketAddrV6}; use uuid::Uuid; /// See [`nexus_types::deployment::Blueprint`]. @@ -256,82 +266,435 @@ impl BpOmicronZone { blueprint_id: Uuid, sled_id: SledUuid, blueprint_zone: &BlueprintZoneConfig, - ) -> Result { + ) -> anyhow::Result { let external_ip_id = blueprint_zone .zone_type .external_networking() - .map(|(ip, _)| ip.id()); - let zone = OmicronZone::new( - sled_id, - blueprint_zone.id.into_untyped_uuid(), - blueprint_zone.underlay_address, - blueprint_zone.filesystem_pool.as_ref().map(|pool| pool.id()), - &blueprint_zone.zone_type.clone().into(), - external_ip_id, - )?; - Ok(Self { + .map(|(ip, _)| ip.id().into()); + + // Create a dummy record to start, then fill in the rest + let mut bp_omicron_zone = BpOmicronZone { + // Fill in the known fields that don't require inspecting + // `blueprint_zone.zone_type` blueprint_id, - sled_id: zone.sled_id.into(), - id: zone.id, - underlay_address: zone.underlay_address, - zone_type: zone.zone_type, - primary_service_ip: zone.primary_service_ip, - primary_service_port: zone.primary_service_port, - second_service_ip: zone.second_service_ip, - second_service_port: zone.second_service_port, - dataset_zpool_name: zone.dataset_zpool_name, - bp_nic_id: zone.nic_id, - dns_gz_address: zone.dns_gz_address, - dns_gz_address_index: zone.dns_gz_address_index, - ntp_ntp_servers: zone.ntp_ntp_servers, - ntp_dns_servers: zone.ntp_dns_servers, - ntp_domain: zone.ntp_domain, - nexus_external_tls: zone.nexus_external_tls, - nexus_external_dns_servers: zone.nexus_external_dns_servers, - snat_ip: zone.snat_ip, - snat_first_port: zone.snat_first_port, - snat_last_port: zone.snat_last_port, - disposition: to_db_bp_zone_disposition(blueprint_zone.disposition), - external_ip_id: zone.external_ip_id.map(From::from), + sled_id: sled_id.into(), + id: blueprint_zone.id.into_untyped_uuid(), + underlay_address: blueprint_zone.underlay_address.into(), + external_ip_id, filesystem_pool: blueprint_zone .filesystem_pool .as_ref() .map(|pool| pool.id().into()), - }) + disposition: to_db_bp_zone_disposition(blueprint_zone.disposition), + zone_type: blueprint_zone.zone_type.kind().into(), + + // Set the remainder of the fields to a default + primary_service_ip: "::1" + .parse::() + .unwrap() + .into(), + primary_service_port: 0.into(), + second_service_ip: None, + second_service_port: None, + dataset_zpool_name: None, + bp_nic_id: None, + dns_gz_address: None, + dns_gz_address_index: None, + ntp_ntp_servers: None, + ntp_dns_servers: None, + ntp_domain: None, + nexus_external_tls: None, + nexus_external_dns_servers: None, + snat_ip: None, + snat_first_port: None, + snat_last_port: None, + }; + + match &blueprint_zone.zone_type { + BlueprintZoneType::BoundaryNtp( + blueprint_zone_type::BoundaryNtp { + address, + ntp_servers, + dns_servers, + domain, + nic, + external_ip, + }, + ) => { + // Set the common fields + bp_omicron_zone.set_primary_service_ip_and_port(address); + + // Set the zone specific fields + let snat_cfg = external_ip.snat_cfg; + let (first_port, last_port) = snat_cfg.port_range_raw(); + bp_omicron_zone.ntp_ntp_servers = Some(ntp_servers.clone()); + bp_omicron_zone.ntp_dns_servers = Some( + dns_servers + .into_iter() + .cloned() + .map(IpNetwork::from) + .collect(), + ); + bp_omicron_zone.ntp_domain.clone_from(domain); + bp_omicron_zone.snat_ip = Some(IpNetwork::from(snat_cfg.ip)); + bp_omicron_zone.snat_first_port = + Some(SqlU16::from(first_port)); + bp_omicron_zone.snat_last_port = Some(SqlU16::from(last_port)); + bp_omicron_zone.bp_nic_id = Some(nic.id); + } + BlueprintZoneType::Clickhouse( + blueprint_zone_type::Clickhouse { address, dataset }, + ) => { + // Set the common fields + bp_omicron_zone.set_primary_service_ip_and_port(address); + bp_omicron_zone.set_zpool_name(dataset); + } + BlueprintZoneType::ClickhouseKeeper( + blueprint_zone_type::ClickhouseKeeper { address, dataset }, + ) => { + // Set the common fields + bp_omicron_zone.set_primary_service_ip_and_port(address); + bp_omicron_zone.set_zpool_name(dataset); + } + BlueprintZoneType::ClickhouseServer( + blueprint_zone_type::ClickhouseServer { address, dataset }, + ) => { + // Set the common fields + bp_omicron_zone.set_primary_service_ip_and_port(address); + bp_omicron_zone.set_zpool_name(dataset); + } + BlueprintZoneType::CockroachDb( + blueprint_zone_type::CockroachDb { address, dataset }, + ) => { + // Set the common fields + bp_omicron_zone.set_primary_service_ip_and_port(address); + bp_omicron_zone.set_zpool_name(dataset); + } + BlueprintZoneType::Crucible(blueprint_zone_type::Crucible { + address, + dataset, + }) => { + // Set the common fields + bp_omicron_zone.set_primary_service_ip_and_port(address); + bp_omicron_zone.set_zpool_name(dataset); + } + BlueprintZoneType::CruciblePantry( + blueprint_zone_type::CruciblePantry { address }, + ) => { + // Set the common fields + bp_omicron_zone.set_primary_service_ip_and_port(address); + } + BlueprintZoneType::ExternalDns( + blueprint_zone_type::ExternalDns { + dataset, + http_address, + dns_address, + nic, + }, + ) => { + // Set the common fields + bp_omicron_zone.set_primary_service_ip_and_port(http_address); + bp_omicron_zone.set_zpool_name(dataset); + + // Set the zone specific fields + bp_omicron_zone.bp_nic_id = Some(nic.id); + bp_omicron_zone.second_service_ip = + Some(IpNetwork::from(dns_address.addr.ip())); + bp_omicron_zone.second_service_port = + Some(SqlU16::from(dns_address.addr.port())); + } + BlueprintZoneType::InternalDns( + blueprint_zone_type::InternalDns { + dataset, + http_address, + dns_address, + gz_address, + gz_address_index, + }, + ) => { + // Set the common fields + bp_omicron_zone.set_primary_service_ip_and_port(http_address); + bp_omicron_zone.set_zpool_name(dataset); + + // Set the zone specific fields + bp_omicron_zone.second_service_ip = + Some(IpNetwork::from(IpAddr::V6(*dns_address.ip()))); + bp_omicron_zone.second_service_port = + Some(SqlU16::from(dns_address.port())); + + bp_omicron_zone.dns_gz_address = + Some(ipv6::Ipv6Addr::from(gz_address)); + bp_omicron_zone.dns_gz_address_index = + Some(SqlU32::from(*gz_address_index)); + } + BlueprintZoneType::InternalNtp( + blueprint_zone_type::InternalNtp { + address, + ntp_servers, + dns_servers, + domain, + }, + ) => { + // Set the common fields + bp_omicron_zone.set_primary_service_ip_and_port(address); + + // Set the zone specific fields + bp_omicron_zone.ntp_ntp_servers = Some(ntp_servers.clone()); + bp_omicron_zone.ntp_dns_servers = Some( + dns_servers.iter().cloned().map(IpNetwork::from).collect(), + ); + bp_omicron_zone.ntp_domain.clone_from(domain); + } + BlueprintZoneType::Nexus(blueprint_zone_type::Nexus { + internal_address, + external_ip, + nic, + external_tls, + external_dns_servers, + }) => { + // Set the common fields + bp_omicron_zone + .set_primary_service_ip_and_port(internal_address); + + // Set the zone specific fields + bp_omicron_zone.bp_nic_id = Some(nic.id); + bp_omicron_zone.second_service_ip = + Some(IpNetwork::from(external_ip.ip)); + bp_omicron_zone.nexus_external_tls = Some(*external_tls); + bp_omicron_zone.nexus_external_dns_servers = Some( + external_dns_servers + .iter() + .cloned() + .map(IpNetwork::from) + .collect(), + ); + } + BlueprintZoneType::Oximeter(blueprint_zone_type::Oximeter { + address, + }) => { + // Set the common fields + bp_omicron_zone.set_primary_service_ip_and_port(address); + } + } + + Ok(bp_omicron_zone) + } + + fn set_primary_service_ip_and_port(&mut self, address: &SocketAddrV6) { + let (primary_service_ip, primary_service_port) = + (ipv6::Ipv6Addr::from(*address.ip()), SqlU16::from(address.port())); + self.primary_service_ip = primary_service_ip; + self.primary_service_port = primary_service_port; + } + + fn set_zpool_name(&mut self, dataset: &OmicronZoneDataset) { + self.dataset_zpool_name = Some(dataset.pool_name.to_string()); + } + /// Convert an external ip from a `BpOmicronZone` to a `BlueprintZoneType` + /// representation. + fn external_ip_to_blueprint_zone_type( + external_ip: Option>, + ) -> anyhow::Result { + external_ip + .map(Into::into) + .ok_or_else(|| anyhow!("expected an external IP ID")) } pub fn into_blueprint_zone_config( self, nic_row: Option, - ) -> Result { - let zone = OmicronZone { - sled_id: self.sled_id.into(), - id: self.id, - underlay_address: self.underlay_address, - filesystem_pool: self.filesystem_pool.map(|id| id.into()), - zone_type: self.zone_type, - primary_service_ip: self.primary_service_ip, - primary_service_port: self.primary_service_port, - second_service_ip: self.second_service_ip, - second_service_port: self.second_service_port, - dataset_zpool_name: self.dataset_zpool_name, - nic_id: self.bp_nic_id, - dns_gz_address: self.dns_gz_address, - dns_gz_address_index: self.dns_gz_address_index, - ntp_ntp_servers: self.ntp_ntp_servers, - ntp_dns_servers: self.ntp_dns_servers, - ntp_domain: self.ntp_domain, - nexus_external_tls: self.nexus_external_tls, - nexus_external_dns_servers: self.nexus_external_dns_servers, - snat_ip: self.snat_ip, - snat_first_port: self.snat_first_port, - snat_last_port: self.snat_last_port, - external_ip_id: self.external_ip_id.map(From::from), + ) -> anyhow::Result { + // Build up a set of common fields for our `BlueprintZoneType`s + // + // Some of these are results that we only evaluate when used, because + // not all zone types use all common fields. + let primary_address = SocketAddrV6::new( + self.primary_service_ip.into(), + *self.primary_service_port, + 0, + 0, + ); + let dataset = + omicron_zone_config::dataset_zpool_name_to_omicron_zone_dataset( + self.dataset_zpool_name, + ); + + // There is a nested result here. If there is a caller error (the outer + // Result) we immediately return. We check the inner result later, but + // only if some code path tries to use `nic` and it's not present. + let nic = omicron_zone_config::nic_row_to_network_interface( + self.id, + self.bp_nic_id, + nic_row.map(Into::into), + )?; + + let external_ip_id = + Self::external_ip_to_blueprint_zone_type(self.external_ip_id); + + let dns_address = + omicron_zone_config::secondary_ip_and_port_to_dns_address( + self.second_service_ip, + self.second_service_port, + ); + + let ntp_dns_servers = + omicron_zone_config::ntp_dns_servers_to_omicron_internal( + self.ntp_dns_servers, + ); + + let ntp_servers = omicron_zone_config::ntp_servers_to_omicron_internal( + self.ntp_ntp_servers, + ); + + let zone_type = match self.zone_type { + ZoneType::BoundaryNtp => { + let snat_cfg = match ( + self.snat_ip, + self.snat_first_port, + self.snat_last_port, + ) { + (Some(ip), Some(first_port), Some(last_port)) => { + nexus_types::inventory::SourceNatConfig::new( + ip.ip(), + *first_port, + *last_port, + ) + .context("bad SNAT config for boundary NTP")? + } + _ => bail!( + "expected non-NULL snat properties, \ + found at least one NULL" + ), + }; + BlueprintZoneType::BoundaryNtp( + blueprint_zone_type::BoundaryNtp { + address: primary_address, + ntp_servers: ntp_servers?, + dns_servers: ntp_dns_servers?, + domain: self.ntp_domain, + nic: nic?, + external_ip: OmicronZoneExternalSnatIp { + id: external_ip_id?, + snat_cfg, + }, + }, + ) + } + ZoneType::Clickhouse => { + BlueprintZoneType::Clickhouse(blueprint_zone_type::Clickhouse { + address: primary_address, + dataset: dataset?, + }) + } + ZoneType::ClickhouseKeeper => BlueprintZoneType::ClickhouseKeeper( + blueprint_zone_type::ClickhouseKeeper { + address: primary_address, + dataset: dataset?, + }, + ), + ZoneType::ClickhouseServer => BlueprintZoneType::ClickhouseServer( + blueprint_zone_type::ClickhouseServer { + address: primary_address, + dataset: dataset?, + }, + ), + + ZoneType::CockroachDb => BlueprintZoneType::CockroachDb( + blueprint_zone_type::CockroachDb { + address: primary_address, + dataset: dataset?, + }, + ), + ZoneType::Crucible => { + BlueprintZoneType::Crucible(blueprint_zone_type::Crucible { + address: primary_address, + dataset: dataset?, + }) + } + ZoneType::CruciblePantry => BlueprintZoneType::CruciblePantry( + blueprint_zone_type::CruciblePantry { + address: primary_address, + }, + ), + ZoneType::ExternalDns => BlueprintZoneType::ExternalDns( + blueprint_zone_type::ExternalDns { + dataset: dataset?, + http_address: primary_address, + dns_address: OmicronZoneExternalFloatingAddr { + id: external_ip_id?, + addr: dns_address?, + }, + nic: nic?, + }, + ), + ZoneType::InternalDns => BlueprintZoneType::InternalDns( + blueprint_zone_type::InternalDns { + dataset: dataset?, + http_address: primary_address, + dns_address: omicron_zone_config::to_internal_dns_address( + dns_address?, + )?, + gz_address: self + .dns_gz_address + .map(Into::into) + .ok_or_else(|| { + anyhow!("expected dns_gz_address, found none") + })?, + gz_address_index: *self.dns_gz_address_index.ok_or_else( + || anyhow!("expected dns_gz_address_index, found none"), + )?, + }, + ), + ZoneType::InternalNtp => BlueprintZoneType::InternalNtp( + blueprint_zone_type::InternalNtp { + address: primary_address, + ntp_servers: ntp_servers?, + dns_servers: ntp_dns_servers?, + domain: self.ntp_domain, + }, + ), + ZoneType::Nexus => { + BlueprintZoneType::Nexus(blueprint_zone_type::Nexus { + internal_address: primary_address, + external_ip: OmicronZoneExternalFloatingIp { + id: external_ip_id?, + ip: self + .second_service_ip + .ok_or_else(|| { + anyhow!("expected second service IP") + })? + .ip(), + }, + nic: nic?, + external_tls: self + .nexus_external_tls + .ok_or_else(|| anyhow!("expected 'external_tls'"))?, + external_dns_servers: self + .nexus_external_dns_servers + .ok_or_else(|| { + anyhow!("expected 'external_dns_servers'") + })? + .into_iter() + .map(|i| i.ip()) + .collect(), + }) + } + ZoneType::Oximeter => { + BlueprintZoneType::Oximeter(blueprint_zone_type::Oximeter { + address: primary_address, + }) + } }; - zone.into_blueprint_zone_config( - self.disposition.into(), - nic_row.map(OmicronZoneNic::from), - ) + + Ok(BlueprintZoneConfig { + disposition: self.disposition.into(), + id: OmicronZoneUuid::from_untyped_uuid(self.id), + underlay_address: self.underlay_address.into(), + filesystem_pool: self + .filesystem_pool + .map(|id| ZpoolName::new_external(id.into())), + zone_type, + }) } } @@ -394,21 +757,6 @@ pub struct BpOmicronZoneNic { slot: SqlU8, } -impl From for OmicronZoneNic { - fn from(value: BpOmicronZoneNic) -> Self { - OmicronZoneNic { - id: value.id, - name: value.name, - ip: value.ip, - mac: value.mac, - subnet: value.subnet, - vni: value.vni, - is_primary: value.is_primary, - slot: value.slot, - } - } -} - impl BpOmicronZoneNic { pub fn new( blueprint_id: Uuid, @@ -440,6 +788,21 @@ impl BpOmicronZoneNic { } } +impl From for OmicronZoneNic { + fn from(value: BpOmicronZoneNic) -> Self { + OmicronZoneNic { + id: value.id, + name: value.name, + ip: value.ip, + mac: value.mac, + subnet: value.subnet, + vni: value.vni, + is_primary: value.is_primary, + slot: value.slot, + } + } +} + mod diesel_util { use crate::{ schema::bp_omicron_zone::disposition, to_db_bp_zone_disposition, diff --git a/nexus/db-model/src/inventory.rs b/nexus/db-model/src/inventory.rs index 87986c4f54..71e44b4d82 100644 --- a/nexus/db-model/src/inventory.rs +++ b/nexus/db-model/src/inventory.rs @@ -4,7 +4,7 @@ //! Types for representing the hardware/software inventory in the database -use crate::omicron_zone_config::{OmicronZone, OmicronZoneNic}; +use crate::omicron_zone_config::{self, OmicronZoneNic}; use crate::schema::{ hw_baseboard_id, inv_caboose, inv_collection, inv_collection_error, inv_omicron_zone, inv_omicron_zone_nic, inv_physical_disk, @@ -18,7 +18,7 @@ use crate::{ impl_enum_type, ipv6, ByteCount, Generation, MacAddr, Name, ServiceKind, SqlU16, SqlU32, SqlU8, }; -use anyhow::anyhow; +use anyhow::{anyhow, bail, Context, Result}; use chrono::DateTime; use chrono::Utc; use diesel::backend::Backend; @@ -28,13 +28,15 @@ use diesel::pg::Pg; use diesel::serialize::ToSql; use diesel::{serialize, sql_types}; use ipnetwork::IpNetwork; +use nexus_sled_agent_shared::inventory::OmicronZoneDataset; use nexus_sled_agent_shared::inventory::{ - OmicronZoneConfig, OmicronZonesConfig, + OmicronZoneConfig, OmicronZoneType, OmicronZonesConfig, }; use nexus_types::inventory::{ BaseboardId, Caboose, Collection, PowerState, RotPage, RotSlot, }; use omicron_common::api::internal::shared::NetworkInterface; +use omicron_common::zpool_name::ZpoolName; use omicron_uuid_kinds::CollectionKind; use omicron_uuid_kinds::CollectionUuid; use omicron_uuid_kinds::GenericUuid; @@ -42,6 +44,7 @@ use omicron_uuid_kinds::SledKind; use omicron_uuid_kinds::SledUuid; use omicron_uuid_kinds::ZpoolKind; use omicron_uuid_kinds::ZpoolUuid; +use std::net::{IpAddr, SocketAddrV6}; use uuid::Uuid; // See [`nexus_types::inventory::PowerState`]. @@ -1090,73 +1093,351 @@ impl InvOmicronZone { sled_id: SledUuid, zone: &OmicronZoneConfig, ) -> Result { - // Inventory zones do not know the external IP ID. - let external_ip_id = None; - let zone = OmicronZone::new( - sled_id, - zone.id, - zone.underlay_address, - zone.filesystem_pool.as_ref().map(|pool| pool.id()), - &zone.zone_type, - external_ip_id, - )?; - Ok(Self { + // Create a dummy record to start, then fill in the rest + // according to the zone type + let mut inv_omicron_zone = InvOmicronZone { + // Fill in the known fields that don't require inspecting + // `zone.zone_type` inv_collection_id: inv_collection_id.into(), - sled_id: zone.sled_id.into(), + sled_id: sled_id.into(), id: zone.id, - underlay_address: zone.underlay_address, - zone_type: zone.zone_type, - primary_service_ip: zone.primary_service_ip, - primary_service_port: zone.primary_service_port, - second_service_ip: zone.second_service_ip, - second_service_port: zone.second_service_port, - dataset_zpool_name: zone.dataset_zpool_name, - nic_id: zone.nic_id, - dns_gz_address: zone.dns_gz_address, - dns_gz_address_index: zone.dns_gz_address_index, - ntp_ntp_servers: zone.ntp_ntp_servers, - ntp_dns_servers: zone.ntp_dns_servers, - ntp_domain: zone.ntp_domain, - nexus_external_tls: zone.nexus_external_tls, - nexus_external_dns_servers: zone.nexus_external_dns_servers, - snat_ip: zone.snat_ip, - snat_first_port: zone.snat_first_port, - snat_last_port: zone.snat_last_port, - filesystem_pool: zone.filesystem_pool.map(|id| id.into()), - }) + underlay_address: zone.underlay_address.into(), + filesystem_pool: zone + .filesystem_pool + .as_ref() + .map(|pool| pool.id().into()), + zone_type: zone.zone_type.kind().into(), + + // Set the remainder of the fields to a default + primary_service_ip: "::1" + .parse::() + .unwrap() + .into(), + primary_service_port: 0.into(), + second_service_ip: None, + second_service_port: None, + dataset_zpool_name: None, + nic_id: None, + dns_gz_address: None, + dns_gz_address_index: None, + ntp_ntp_servers: None, + ntp_dns_servers: None, + ntp_domain: None, + nexus_external_tls: None, + nexus_external_dns_servers: None, + snat_ip: None, + snat_first_port: None, + snat_last_port: None, + }; + + match &zone.zone_type { + OmicronZoneType::BoundaryNtp { + address, + ntp_servers, + dns_servers, + domain, + nic, + snat_cfg, + } => { + // Set the common fields + inv_omicron_zone.set_primary_service_ip_and_port(address); + + // Set the zone specific fields + let (first_port, last_port) = snat_cfg.port_range_raw(); + inv_omicron_zone.ntp_ntp_servers = Some(ntp_servers.clone()); + inv_omicron_zone.ntp_dns_servers = Some( + dns_servers + .into_iter() + .cloned() + .map(IpNetwork::from) + .collect(), + ); + inv_omicron_zone.ntp_domain.clone_from(domain); + inv_omicron_zone.snat_ip = Some(IpNetwork::from(snat_cfg.ip)); + inv_omicron_zone.snat_first_port = + Some(SqlU16::from(first_port)); + inv_omicron_zone.snat_last_port = Some(SqlU16::from(last_port)); + inv_omicron_zone.nic_id = Some(nic.id); + } + OmicronZoneType::Clickhouse { address, dataset } => { + // Set the common fields + inv_omicron_zone.set_primary_service_ip_and_port(address); + inv_omicron_zone.set_zpool_name(dataset); + } + OmicronZoneType::ClickhouseKeeper { address, dataset } => { + // Set the common fields + inv_omicron_zone.set_primary_service_ip_and_port(address); + inv_omicron_zone.set_zpool_name(dataset); + } + OmicronZoneType::ClickhouseServer { address, dataset } => { + // Set the common fields + inv_omicron_zone.set_primary_service_ip_and_port(address); + inv_omicron_zone.set_zpool_name(dataset); + } + OmicronZoneType::CockroachDb { address, dataset } => { + // Set the common fields + inv_omicron_zone.set_primary_service_ip_and_port(address); + inv_omicron_zone.set_zpool_name(dataset); + } + OmicronZoneType::Crucible { address, dataset } => { + // Set the common fields + inv_omicron_zone.set_primary_service_ip_and_port(address); + inv_omicron_zone.set_zpool_name(dataset); + } + OmicronZoneType::CruciblePantry { address } => { + // Set the common fields + inv_omicron_zone.set_primary_service_ip_and_port(address); + } + OmicronZoneType::ExternalDns { + dataset, + http_address, + dns_address, + nic, + } => { + // Set the common fields + inv_omicron_zone.set_primary_service_ip_and_port(http_address); + inv_omicron_zone.set_zpool_name(dataset); + + // Set the zone specific fields + inv_omicron_zone.nic_id = Some(nic.id); + inv_omicron_zone.second_service_ip = + Some(IpNetwork::from(dns_address.ip())); + inv_omicron_zone.second_service_port = + Some(SqlU16::from(dns_address.port())); + } + OmicronZoneType::InternalDns { + dataset, + http_address, + dns_address, + gz_address, + gz_address_index, + } => { + // Set the common fields + inv_omicron_zone.set_primary_service_ip_and_port(http_address); + inv_omicron_zone.set_zpool_name(dataset); + + // Set the zone specific fields + inv_omicron_zone.second_service_ip = + Some(IpNetwork::from(IpAddr::V6(*dns_address.ip()))); + inv_omicron_zone.second_service_port = + Some(SqlU16::from(dns_address.port())); + + inv_omicron_zone.dns_gz_address = + Some(ipv6::Ipv6Addr::from(gz_address)); + inv_omicron_zone.dns_gz_address_index = + Some(SqlU32::from(*gz_address_index)); + } + OmicronZoneType::InternalNtp { + address, + ntp_servers, + dns_servers, + domain, + } => { + // Set the common fields + inv_omicron_zone.set_primary_service_ip_and_port(address); + + // Set the zone specific fields + inv_omicron_zone.ntp_ntp_servers = Some(ntp_servers.clone()); + inv_omicron_zone.ntp_dns_servers = Some( + dns_servers.iter().cloned().map(IpNetwork::from).collect(), + ); + inv_omicron_zone.ntp_domain.clone_from(domain); + } + OmicronZoneType::Nexus { + internal_address, + external_ip, + nic, + external_tls, + external_dns_servers, + } => { + // Set the common fields + inv_omicron_zone + .set_primary_service_ip_and_port(internal_address); + + // Set the zone specific fields + inv_omicron_zone.nic_id = Some(nic.id); + inv_omicron_zone.second_service_ip = + Some(IpNetwork::from(*external_ip)); + inv_omicron_zone.nexus_external_tls = Some(*external_tls); + inv_omicron_zone.nexus_external_dns_servers = Some( + external_dns_servers + .iter() + .cloned() + .map(IpNetwork::from) + .collect(), + ); + } + OmicronZoneType::Oximeter { address } => { + // Set the common fields + inv_omicron_zone.set_primary_service_ip_and_port(address); + } + } + + Ok(inv_omicron_zone) + } + + fn set_primary_service_ip_and_port(&mut self, address: &SocketAddrV6) { + let (primary_service_ip, primary_service_port) = + (ipv6::Ipv6Addr::from(*address.ip()), SqlU16::from(address.port())); + self.primary_service_ip = primary_service_ip; + self.primary_service_port = primary_service_port; + } + + fn set_zpool_name(&mut self, dataset: &OmicronZoneDataset) { + self.dataset_zpool_name = Some(dataset.pool_name.to_string()); } pub fn into_omicron_zone_config( self, nic_row: Option, ) -> Result { - let zone = OmicronZone { - sled_id: self.sled_id.into(), - id: self.id, - underlay_address: self.underlay_address, - filesystem_pool: self.filesystem_pool.map(|id| id.into()), - zone_type: self.zone_type, - primary_service_ip: self.primary_service_ip, - primary_service_port: self.primary_service_port, - second_service_ip: self.second_service_ip, - second_service_port: self.second_service_port, - dataset_zpool_name: self.dataset_zpool_name, - nic_id: self.nic_id, - dns_gz_address: self.dns_gz_address, - dns_gz_address_index: self.dns_gz_address_index, - ntp_ntp_servers: self.ntp_ntp_servers, - ntp_dns_servers: self.ntp_dns_servers, - ntp_domain: self.ntp_domain, - nexus_external_tls: self.nexus_external_tls, - nexus_external_dns_servers: self.nexus_external_dns_servers, - snat_ip: self.snat_ip, - snat_first_port: self.snat_first_port, - snat_last_port: self.snat_last_port, - // Inventory zones don't know an external IP ID, and Omicron zone - // configs don't need it. - external_ip_id: None, + // Build up a set of common fields for our `OmicronZoneType`s + // + // Some of these are results that we only evaluate when used, because + // not all zone types use all common fields. + let primary_address = SocketAddrV6::new( + self.primary_service_ip.into(), + *self.primary_service_port, + 0, + 0, + ); + + let dataset = + omicron_zone_config::dataset_zpool_name_to_omicron_zone_dataset( + self.dataset_zpool_name, + ); + + // There is a nested result here. If there is a caller error (the outer + // Result) we immediately return. We check the inner result later, but + // only if some code path tries to use `nic` and it's not present. + let nic = omicron_zone_config::nic_row_to_network_interface( + self.id, + self.nic_id, + nic_row.map(Into::into), + )?; + + let dns_address = + omicron_zone_config::secondary_ip_and_port_to_dns_address( + self.second_service_ip, + self.second_service_port, + ); + + let ntp_dns_servers = + omicron_zone_config::ntp_dns_servers_to_omicron_internal( + self.ntp_dns_servers, + ); + + let ntp_servers = omicron_zone_config::ntp_servers_to_omicron_internal( + self.ntp_ntp_servers, + ); + + let zone_type = match self.zone_type { + ZoneType::BoundaryNtp => { + let snat_cfg = match ( + self.snat_ip, + self.snat_first_port, + self.snat_last_port, + ) { + (Some(ip), Some(first_port), Some(last_port)) => { + nexus_types::inventory::SourceNatConfig::new( + ip.ip(), + *first_port, + *last_port, + ) + .context("bad SNAT config for boundary NTP")? + } + _ => bail!( + "expected non-NULL snat properties, \ + found at least one NULL" + ), + }; + OmicronZoneType::BoundaryNtp { + address: primary_address, + ntp_servers: ntp_servers?, + dns_servers: ntp_dns_servers?, + domain: self.ntp_domain, + nic: nic?, + snat_cfg, + } + } + ZoneType::Clickhouse => OmicronZoneType::Clickhouse { + address: primary_address, + dataset: dataset?, + }, + ZoneType::ClickhouseKeeper => OmicronZoneType::ClickhouseKeeper { + address: primary_address, + dataset: dataset?, + }, + ZoneType::ClickhouseServer => OmicronZoneType::ClickhouseServer { + address: primary_address, + dataset: dataset?, + }, + ZoneType::CockroachDb => OmicronZoneType::CockroachDb { + address: primary_address, + dataset: dataset?, + }, + ZoneType::Crucible => OmicronZoneType::Crucible { + address: primary_address, + dataset: dataset?, + }, + ZoneType::CruciblePantry => { + OmicronZoneType::CruciblePantry { address: primary_address } + } + ZoneType::ExternalDns => OmicronZoneType::ExternalDns { + dataset: dataset?, + http_address: primary_address, + dns_address: dns_address?, + nic: nic?, + }, + ZoneType::InternalDns => OmicronZoneType::InternalDns { + dataset: dataset?, + http_address: primary_address, + dns_address: omicron_zone_config::to_internal_dns_address( + dns_address?, + )?, + gz_address: self.dns_gz_address.map(Into::into).ok_or_else( + || anyhow!("expected dns_gz_address, found none"), + )?, + gz_address_index: *self.dns_gz_address_index.ok_or_else( + || anyhow!("expected dns_gz_address_index, found none"), + )?, + }, + ZoneType::InternalNtp => OmicronZoneType::InternalNtp { + address: primary_address, + ntp_servers: ntp_servers?, + dns_servers: ntp_dns_servers?, + domain: self.ntp_domain, + }, + ZoneType::Nexus => OmicronZoneType::Nexus { + internal_address: primary_address, + external_ip: self + .second_service_ip + .ok_or_else(|| anyhow!("expected second service IP"))? + .ip(), + nic: nic?, + external_tls: self + .nexus_external_tls + .ok_or_else(|| anyhow!("expected 'external_tls'"))?, + external_dns_servers: self + .nexus_external_dns_servers + .ok_or_else(|| anyhow!("expected 'external_dns_servers'"))? + .into_iter() + .map(|i| i.ip()) + .collect(), + }, + ZoneType::Oximeter => { + OmicronZoneType::Oximeter { address: primary_address } + } }; - zone.into_omicron_zone_config(nic_row.map(OmicronZoneNic::from)) + + Ok(OmicronZoneConfig { + id: self.id, + underlay_address: self.underlay_address.into(), + filesystem_pool: self + .filesystem_pool + .map(|id| ZpoolName::new_external(id.into())), + zone_type, + }) } } diff --git a/nexus/db-model/src/omicron_zone_config.rs b/nexus/db-model/src/omicron_zone_config.rs index 23e1ef2dd9..0abc2bb4ec 100644 --- a/nexus/db-model/src/omicron_zone_config.rs +++ b/nexus/db-model/src/omicron_zone_config.rs @@ -2,613 +2,113 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. -//! Types for sharing nontrivial conversions between various `OmicronZoneConfig` -//! database serializations and the corresponding Nexus/sled-agent type +//! Helper types and methods for sharing nontrivial conversions between various +//! `OmicronZoneConfig` database serializations and the corresponding Nexus/ +//! sled-agent type //! //! Both inventory and deployment have nearly-identical tables to serialize -//! `OmicronZoneConfigs` that are collected or generated, respectively. We -//! expect those tables to diverge over time (e.g., inventory may start +//! `OmicronZoneConfigs` that are collected or generated, respectively. +//! We expect those tables to diverge over time (e.g., inventory may start //! collecting extra metadata like uptime). This module provides conversion //! helpers for the parts of those tables that are common between the two. -use crate::inventory::ZoneType; -use crate::{ipv6, MacAddr, Name, SqlU16, SqlU32, SqlU8}; +use crate::{MacAddr, Name, SqlU16, SqlU32, SqlU8}; use anyhow::{anyhow, bail, ensure, Context}; use ipnetwork::IpNetwork; -use nexus_sled_agent_shared::inventory::{ - OmicronZoneConfig, OmicronZoneDataset, OmicronZoneType, -}; -use nexus_types::deployment::{ - blueprint_zone_type, BlueprintZoneDisposition, BlueprintZoneType, - OmicronZoneExternalFloatingAddr, OmicronZoneExternalFloatingIp, - OmicronZoneExternalSnatIp, -}; +use nexus_sled_agent_shared::inventory::OmicronZoneDataset; use nexus_types::inventory::NetworkInterface; use omicron_common::api::internal::shared::NetworkInterfaceKind; -use omicron_common::zpool_name::ZpoolName; -use omicron_uuid_kinds::{ - ExternalIpUuid, GenericUuid, OmicronZoneUuid, SledUuid, ZpoolUuid, -}; -use std::net::{IpAddr, Ipv6Addr, SocketAddr, SocketAddrV6}; +use std::net::{IpAddr, SocketAddr, SocketAddrV6}; use uuid::Uuid; -#[derive(Debug)] -pub(crate) struct OmicronZone { - pub(crate) sled_id: SledUuid, - pub(crate) id: Uuid, - pub(crate) underlay_address: ipv6::Ipv6Addr, - pub(crate) filesystem_pool: Option, - pub(crate) zone_type: ZoneType, - pub(crate) primary_service_ip: ipv6::Ipv6Addr, - pub(crate) primary_service_port: SqlU16, - pub(crate) second_service_ip: Option, - pub(crate) second_service_port: Option, - pub(crate) dataset_zpool_name: Option, - pub(crate) nic_id: Option, - pub(crate) dns_gz_address: Option, - pub(crate) dns_gz_address_index: Option, - pub(crate) ntp_ntp_servers: Option>, - pub(crate) ntp_dns_servers: Option>, - pub(crate) ntp_domain: Option, - pub(crate) nexus_external_tls: Option, - pub(crate) nexus_external_dns_servers: Option>, - pub(crate) snat_ip: Option, - pub(crate) snat_first_port: Option, - pub(crate) snat_last_port: Option, - // Only present for BlueprintZoneConfig; always `None` for OmicronZoneConfig - pub(crate) external_ip_id: Option, +/// Convert ntp server config from the DB representation to the +/// omicron internal representation +pub fn ntp_servers_to_omicron_internal( + ntp_ntp_servers: Option>, +) -> anyhow::Result> { + ntp_ntp_servers.ok_or_else(|| anyhow!("expected ntp servers")) } -impl OmicronZone { - pub(crate) fn new( - sled_id: SledUuid, - zone_id: Uuid, - zone_underlay_address: Ipv6Addr, - filesystem_pool: Option, - zone_type: &OmicronZoneType, - external_ip_id: Option, - ) -> anyhow::Result { - let id = zone_id; - let underlay_address = ipv6::Ipv6Addr::from(zone_underlay_address); - let mut nic_id = None; - let mut dns_gz_address = None; - let mut dns_gz_address_index = None; - let mut ntp_ntp_servers = None; - let mut ntp_dns_servers = None; - let mut ntp_ntp_domain = None; - let mut nexus_external_tls = None; - let mut nexus_external_dns_servers = None; - let mut snat_ip = None; - let mut snat_first_port = None; - let mut snat_last_port = None; - let mut second_service_ip = None; - let mut second_service_port = None; - - let (zone_type, primary_service_sockaddr, dataset) = match zone_type { - OmicronZoneType::BoundaryNtp { - address, - ntp_servers, - dns_servers, - domain, - nic, - snat_cfg, - } => { - let (first_port, last_port) = snat_cfg.port_range_raw(); - ntp_ntp_servers = Some(ntp_servers.clone()); - ntp_dns_servers = Some(dns_servers.clone()); - ntp_ntp_domain.clone_from(domain); - snat_ip = Some(IpNetwork::from(snat_cfg.ip)); - snat_first_port = Some(SqlU16::from(first_port)); - snat_last_port = Some(SqlU16::from(last_port)); - nic_id = Some(nic.id); - (ZoneType::BoundaryNtp, address, None) - } - OmicronZoneType::Clickhouse { address, dataset } => { - (ZoneType::Clickhouse, address, Some(dataset)) - } - OmicronZoneType::ClickhouseKeeper { address, dataset } => { - (ZoneType::ClickhouseKeeper, address, Some(dataset)) - } - OmicronZoneType::ClickhouseServer { address, dataset } => { - (ZoneType::ClickhouseServer, address, Some(dataset)) - } - OmicronZoneType::CockroachDb { address, dataset } => { - (ZoneType::CockroachDb, address, Some(dataset)) - } - OmicronZoneType::Crucible { address, dataset } => { - (ZoneType::Crucible, address, Some(dataset)) - } - OmicronZoneType::CruciblePantry { address } => { - (ZoneType::CruciblePantry, address, None) - } - OmicronZoneType::ExternalDns { - dataset, - http_address, - dns_address, - nic, - } => { - nic_id = Some(nic.id); - second_service_ip = Some(dns_address.ip()); - second_service_port = Some(SqlU16::from(dns_address.port())); - (ZoneType::ExternalDns, http_address, Some(dataset)) - } - OmicronZoneType::InternalDns { - dataset, - http_address, - dns_address, - gz_address, - gz_address_index, - } => { - dns_gz_address = Some(ipv6::Ipv6Addr::from(gz_address)); - dns_gz_address_index = Some(SqlU32::from(*gz_address_index)); - second_service_ip = Some(IpAddr::V6(*dns_address.ip())); - second_service_port = Some(SqlU16::from(dns_address.port())); - (ZoneType::InternalDns, http_address, Some(dataset)) - } - OmicronZoneType::InternalNtp { - address, - ntp_servers, - dns_servers, - domain, - } => { - ntp_ntp_servers = Some(ntp_servers.clone()); - ntp_dns_servers = Some(dns_servers.clone()); - ntp_ntp_domain.clone_from(domain); - (ZoneType::InternalNtp, address, None) - } - OmicronZoneType::Nexus { - internal_address, - external_ip, - nic, - external_tls, - external_dns_servers, - } => { - nic_id = Some(nic.id); - nexus_external_tls = Some(*external_tls); - nexus_external_dns_servers = Some(external_dns_servers.clone()); - second_service_ip = Some(*external_ip); - (ZoneType::Nexus, internal_address, None) - } - OmicronZoneType::Oximeter { address } => { - (ZoneType::Oximeter, address, None) - } - }; - - let dataset_zpool_name = dataset.map(|d| d.pool_name.to_string()); - let (primary_service_ip, primary_service_port) = ( - ipv6::Ipv6Addr::from(*primary_service_sockaddr.ip()), - SqlU16::from(primary_service_sockaddr.port()), - ); - - Ok(Self { - sled_id, - id, - underlay_address, - filesystem_pool, - zone_type, - primary_service_ip, - primary_service_port, - second_service_ip: second_service_ip.map(IpNetwork::from), - second_service_port, - dataset_zpool_name, - nic_id, - dns_gz_address, - dns_gz_address_index, - ntp_ntp_servers, - ntp_dns_servers: ntp_dns_servers - .map(|list| list.into_iter().map(IpNetwork::from).collect()), - ntp_domain: ntp_ntp_domain, - nexus_external_tls, - nexus_external_dns_servers: nexus_external_dns_servers - .map(|list| list.into_iter().map(IpNetwork::from).collect()), - snat_ip, - snat_first_port, - snat_last_port, - external_ip_id, - }) - } +/// Convert ntp dns server config from the DB representation to +/// the omicron internal representation. +pub fn ntp_dns_servers_to_omicron_internal( + ntp_dns_servers: Option>, +) -> anyhow::Result> { + ntp_dns_servers + .ok_or_else(|| anyhow!("expected list of DNS servers, found null")) + .map(|list| list.into_iter().map(|ipnetwork| ipnetwork.ip()).collect()) +} - pub(crate) fn into_blueprint_zone_config( - self, - disposition: BlueprintZoneDisposition, - nic_row: Option, - ) -> anyhow::Result { - let common = self.into_zone_config_common(nic_row)?; - let address = common.primary_service_address; - let zone_type = match common.zone_type { - ZoneType::BoundaryNtp => { - let snat_cfg = match ( - common.snat_ip, - common.snat_first_port, - common.snat_last_port, - ) { - (Some(ip), Some(first_port), Some(last_port)) => { - nexus_types::inventory::SourceNatConfig::new( - ip.ip(), - *first_port, - *last_port, - ) - .context("bad SNAT config for boundary NTP")? - } - _ => bail!( - "expected non-NULL snat properties, \ - found at least one NULL" - ), - }; - BlueprintZoneType::BoundaryNtp( - blueprint_zone_type::BoundaryNtp { - address, - dns_servers: common.ntp_dns_servers?, - domain: common.ntp_domain, - nic: common.nic?, - ntp_servers: common.ntp_ntp_servers?, - external_ip: OmicronZoneExternalSnatIp { - id: common.external_ip_id?, - snat_cfg, - }, - }, - ) - } - ZoneType::Clickhouse => { - BlueprintZoneType::Clickhouse(blueprint_zone_type::Clickhouse { - address, - dataset: common.dataset?, - }) - } - ZoneType::ClickhouseKeeper => BlueprintZoneType::ClickhouseKeeper( - blueprint_zone_type::ClickhouseKeeper { - address, - dataset: common.dataset?, - }, - ), - ZoneType::ClickhouseServer => BlueprintZoneType::ClickhouseServer( - blueprint_zone_type::ClickhouseServer { - address, - dataset: common.dataset?, - }, - ), - ZoneType::CockroachDb => BlueprintZoneType::CockroachDb( - blueprint_zone_type::CockroachDb { - address, - dataset: common.dataset?, - }, - ), - ZoneType::Crucible => { - BlueprintZoneType::Crucible(blueprint_zone_type::Crucible { - address, - dataset: common.dataset?, - }) - } - ZoneType::CruciblePantry => BlueprintZoneType::CruciblePantry( - blueprint_zone_type::CruciblePantry { address }, - ), - ZoneType::ExternalDns => BlueprintZoneType::ExternalDns( - blueprint_zone_type::ExternalDns { - dataset: common.dataset?, - dns_address: OmicronZoneExternalFloatingAddr { - id: common.external_ip_id?, - addr: common.dns_address?, - }, - http_address: address, - nic: common.nic?, - }, - ), - ZoneType::InternalDns => BlueprintZoneType::InternalDns( - blueprint_zone_type::InternalDns { - dataset: common.dataset?, - dns_address: to_internal_dns_address(common.dns_address?)?, - http_address: address, - gz_address: *common.dns_gz_address.ok_or_else(|| { - anyhow!("expected dns_gz_address, found none") - })?, - gz_address_index: *common.dns_gz_address_index.ok_or_else( - || anyhow!("expected dns_gz_address_index, found none"), - )?, - }, - ), - ZoneType::InternalNtp => BlueprintZoneType::InternalNtp( - blueprint_zone_type::InternalNtp { - address, - dns_servers: common.ntp_dns_servers?, - domain: common.ntp_domain, - ntp_servers: common.ntp_ntp_servers?, - }, - ), - ZoneType::Nexus => { - BlueprintZoneType::Nexus(blueprint_zone_type::Nexus { - internal_address: address, - nic: common.nic?, - external_tls: common - .nexus_external_tls - .ok_or_else(|| anyhow!("expected 'external_tls'"))?, - external_ip: OmicronZoneExternalFloatingIp { - id: common.external_ip_id?, - ip: common - .second_service_ip - .ok_or_else(|| { - anyhow!("expected second service IP") - })? - .ip(), - }, - external_dns_servers: common - .nexus_external_dns_servers - .ok_or_else(|| { - anyhow!("expected 'external_dns_servers'") - })? - .into_iter() - .map(|i| i.ip()) - .collect(), - }) - } - ZoneType::Oximeter => { - BlueprintZoneType::Oximeter(blueprint_zone_type::Oximeter { - address, - }) - } - }; - Ok(nexus_types::deployment::BlueprintZoneConfig { - disposition, - id: OmicronZoneUuid::from_untyped_uuid(common.id), - underlay_address: std::net::Ipv6Addr::from(common.underlay_address), - filesystem_pool: common - .filesystem_pool - .map(|id| ZpoolName::new_external(id)), - zone_type, - }) +/// Assemble a value that we can use to extract the NIC _if necessary_ +/// and report an error if it was needed but not found. +/// +/// Any error here should be impossible. By the time we get here, the +/// caller should have provided `nic_row` iff there's a corresponding +/// `nic_id` in this row, and the ids should match up. And whoever +/// created this row ought to have provided a nic_id iff this type of +/// zone needs a NIC. This last issue is not under our control, though, +/// so we definitely want to handle that as an operational error. The +/// others could arguably be programmer errors (i.e., we could `assert`), +/// but it seems excessive to crash here. +/// +/// The outer result represents a programmer error and should be unwrapped +/// immediately. The inner result represents an operational error and should +/// only be unwrapped when the nic is used. +pub fn nic_row_to_network_interface( + zone_id: Uuid, + nic_id: Option, + nic_row: Option, +) -> anyhow::Result> { + match (nic_id, nic_row) { + (Some(expected_id), Some(nic_row)) => { + ensure!(expected_id == nic_row.id, "caller provided wrong NIC"); + Ok(nic_row.into_network_interface_for_zone(zone_id)) + } + (None, None) => Ok(Err(anyhow!( + "expected zone to have an associated NIC, but it doesn't" + ))), + (Some(_), None) => bail!("caller provided no NIC"), + (None, Some(_)) => bail!("caller unexpectedly provided a NIC"), } +} - pub(crate) fn into_omicron_zone_config( - self, - nic_row: Option, - ) -> anyhow::Result { - let common = self.into_zone_config_common(nic_row)?; - let address = common.primary_service_address; - - let zone_type = match common.zone_type { - ZoneType::BoundaryNtp => { - let snat_cfg = match ( - common.snat_ip, - common.snat_first_port, - common.snat_last_port, - ) { - (Some(ip), Some(first_port), Some(last_port)) => { - nexus_types::inventory::SourceNatConfig::new( - ip.ip(), - *first_port, - *last_port, - ) - .context("bad SNAT config for boundary NTP")? - } - _ => bail!( - "expected non-NULL snat properties, \ - found at least one NULL" - ), - }; - OmicronZoneType::BoundaryNtp { - address, - dns_servers: common.ntp_dns_servers?, - domain: common.ntp_domain, - nic: common.nic?, - ntp_servers: common.ntp_ntp_servers?, - snat_cfg, - } - } - ZoneType::Clickhouse => OmicronZoneType::Clickhouse { - address, - dataset: common.dataset?, - }, - ZoneType::ClickhouseKeeper => OmicronZoneType::ClickhouseKeeper { - address, - dataset: common.dataset?, - }, - ZoneType::ClickhouseServer => OmicronZoneType::ClickhouseServer { - address, - dataset: common.dataset?, - }, - ZoneType::CockroachDb => OmicronZoneType::CockroachDb { - address, - dataset: common.dataset?, - }, - ZoneType::Crucible => { - OmicronZoneType::Crucible { address, dataset: common.dataset? } - } - ZoneType::CruciblePantry => { - OmicronZoneType::CruciblePantry { address } - } - ZoneType::ExternalDns => OmicronZoneType::ExternalDns { - dataset: common.dataset?, - dns_address: common.dns_address?, - http_address: address, - nic: common.nic?, - }, - ZoneType::InternalDns => OmicronZoneType::InternalDns { - dataset: common.dataset?, - dns_address: to_internal_dns_address(common.dns_address?)?, - http_address: address, - gz_address: *common.dns_gz_address.ok_or_else(|| { - anyhow!("expected dns_gz_address, found none") +/// Convert a dataset from a DB representation to a an Omicron internal +/// representation +pub fn dataset_zpool_name_to_omicron_zone_dataset( + dataset_zpool_name: Option, +) -> anyhow::Result { + dataset_zpool_name + .map(|zpool_name| -> Result<_, anyhow::Error> { + Ok(OmicronZoneDataset { + pool_name: zpool_name.parse().map_err(|e| { + anyhow!("parsing zpool name {:?}: {}", zpool_name, e) })?, - gz_address_index: *common.dns_gz_address_index.ok_or_else( - || anyhow!("expected dns_gz_address_index, found none"), - )?, - }, - ZoneType::InternalNtp => OmicronZoneType::InternalNtp { - address, - dns_servers: common.ntp_dns_servers?, - domain: common.ntp_domain, - ntp_servers: common.ntp_ntp_servers?, - }, - ZoneType::Nexus => OmicronZoneType::Nexus { - internal_address: address, - nic: common.nic?, - external_tls: common - .nexus_external_tls - .ok_or_else(|| anyhow!("expected 'external_tls'"))?, - external_ip: common - .second_service_ip - .ok_or_else(|| anyhow!("expected second service IP"))? - .ip(), - external_dns_servers: common - .nexus_external_dns_servers - .ok_or_else(|| anyhow!("expected 'external_dns_servers'"))? - .into_iter() - .map(|i| i.ip()) - .collect(), - }, - ZoneType::Oximeter => OmicronZoneType::Oximeter { address }, - }; - Ok(OmicronZoneConfig { - id: common.id, - underlay_address: std::net::Ipv6Addr::from(common.underlay_address), - filesystem_pool: common - .filesystem_pool - .map(|id| ZpoolName::new_external(id)), - zone_type, - }) - } - - fn into_zone_config_common( - self, - nic_row: Option, - ) -> anyhow::Result { - let primary_service_address = SocketAddrV6::new( - std::net::Ipv6Addr::from(self.primary_service_ip), - *self.primary_service_port, - 0, - 0, - ); - - // Assemble a value that we can use to extract the NIC _if necessary_ - // and report an error if it was needed but not found. - // - // Any error here should be impossible. By the time we get here, the - // caller should have provided `nic_row` iff there's a corresponding - // `nic_id` in this row, and the ids should match up. And whoever - // created this row ought to have provided a nic_id iff this type of - // zone needs a NIC. This last issue is not under our control, though, - // so we definitely want to handle that as an operational error. The - // others could arguably be programmer errors (i.e., we could `assert`), - // but it seems excessive to crash here. - // - // Note that we immediately return for any of the caller errors here. - // For the other error, we will return only later, if some code path - // below tries to use `nic` when it's not present. - let nic = match (self.nic_id, nic_row) { - (Some(expected_id), Some(nic_row)) => { - ensure!(expected_id == nic_row.id, "caller provided wrong NIC"); - Ok(nic_row.into_network_interface_for_zone(self.id)?) - } - // We don't expect and don't have a NIC. This is reasonable, so we - // don't `bail!` like we do in the next two cases, but we also - // _don't have a NIC_. Put an error into `nic`, and then if we land - // in a zone below that expects one, we'll fail then. - (None, None) => Err(anyhow!( - "expected zone to have an associated NIC, but it doesn't" - )), - (Some(_), None) => bail!("caller provided no NIC"), - (None, Some(_)) => bail!("caller unexpectedly provided a NIC"), - }; - - // Similarly, assemble a value that we can use to extract the dataset, - // if necessary. We only return this error if code below tries to use - // this value. - let dataset = self - .dataset_zpool_name - .map(|zpool_name| -> Result<_, anyhow::Error> { - Ok(OmicronZoneDataset { - pool_name: zpool_name.parse().map_err(|e| { - anyhow!("parsing zpool name {:?}: {}", zpool_name, e) - })?, - }) }) - .transpose()? - .ok_or_else(|| anyhow!("expected dataset zpool name, found none")); - - // Do the same for the DNS server address. - let dns_address = - match (self.second_service_ip, self.second_service_port) { - (Some(dns_ip), Some(dns_port)) => { - Ok(std::net::SocketAddr::new(dns_ip.ip(), *dns_port)) - } - _ => Err(anyhow!( - "expected second service IP and port, \ - found one missing" - )), - }; - - // Do the same for NTP zone properties. - let ntp_dns_servers = self - .ntp_dns_servers - .ok_or_else(|| anyhow!("expected list of DNS servers, found null")) - .map(|list| { - list.into_iter().map(|ipnetwork| ipnetwork.ip()).collect() - }); - let ntp_ntp_servers = - self.ntp_ntp_servers.ok_or_else(|| anyhow!("expected ntp_servers")); - - // Do the same for the external IP ID. - let external_ip_id = - self.external_ip_id.context("expected an external IP ID"); - - Ok(ZoneConfigCommon { - id: self.id, - underlay_address: self.underlay_address, - filesystem_pool: self.filesystem_pool, - zone_type: self.zone_type, - primary_service_address, - snat_ip: self.snat_ip, - snat_first_port: self.snat_first_port, - snat_last_port: self.snat_last_port, - ntp_domain: self.ntp_domain, - dns_gz_address: self.dns_gz_address, - dns_gz_address_index: self.dns_gz_address_index, - nexus_external_tls: self.nexus_external_tls, - nexus_external_dns_servers: self.nexus_external_dns_servers, - second_service_ip: self.second_service_ip, - nic, - dataset, - dns_address, - ntp_dns_servers, - ntp_ntp_servers, - external_ip_id, }) - } + .transpose()? + .ok_or_else(|| anyhow!("expected dataset zpool name, found none")) } -struct ZoneConfigCommon { - id: Uuid, - underlay_address: ipv6::Ipv6Addr, - filesystem_pool: Option, - zone_type: ZoneType, - primary_service_address: SocketAddrV6, - snat_ip: Option, - snat_first_port: Option, - snat_last_port: Option, - ntp_domain: Option, - dns_gz_address: Option, - dns_gz_address_index: Option, - nexus_external_tls: Option, - nexus_external_dns_servers: Option>, +/// Convert the secondary ip and port to a dns address +pub fn secondary_ip_and_port_to_dns_address( second_service_ip: Option, - // These properties may or may not be needed, depending on the zone type. We - // store results here that can be unpacked once we determine our zone type. - nic: anyhow::Result, - dataset: anyhow::Result, - // Note that external DNS is SocketAddr (also supports v4) while internal - // DNS is always v6. - dns_address: anyhow::Result, - ntp_dns_servers: anyhow::Result>, - ntp_ntp_servers: anyhow::Result>, - external_ip_id: anyhow::Result, + second_service_port: Option, +) -> anyhow::Result { + match (second_service_ip, second_service_port) { + (Some(dns_ip), Some(dns_port)) => { + Ok(std::net::SocketAddr::new(dns_ip.ip(), *dns_port)) + } + _ => Err(anyhow!( + "expected second service IP and port, found one missing" + )), + } } -// Ideally this would be a method on `ZoneConfigCommon`, but that's more -// annoying to deal with because often, at the time this function is called, -// part of `ZoneConfigCommon` has already been moved out. -fn to_internal_dns_address( - external_address: SocketAddr, +/// Extract a SocketAddrV6 from a SocketAddr for a given dns address +pub fn to_internal_dns_address( + address: SocketAddr, ) -> anyhow::Result { - match external_address { + match address { SocketAddr::V4(address) => { bail!( "expected internal DNS address to be v6, found v4: {:?}",