Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor InstalledZone::install to use a builder pattern, per TODO. #4325

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions illumos-utils/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ async-trait.workspace = true
bhyve_api.workspace = true
byteorder.workspace = true
camino.workspace = true
camino-tempfile.workspace = true
cfg-if.workspace = true
crucible-smf.workspace = true
futures.workspace = true
Expand Down
238 changes: 211 additions & 27 deletions illumos-utils/src/running_zone.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@ use crate::opte::{Port, PortTicket};
use crate::svc::wait_for_service;
use crate::zone::{AddressRequest, IPADM, ZONE_PREFIX};
use camino::{Utf8Path, Utf8PathBuf};
use camino_tempfile::Utf8TempDir;
use ipnetwork::IpNetwork;
use omicron_common::backoff;
use slog::{error, info, o, warn, Logger};
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use std::sync::Arc;
#[cfg(target_os = "illumos")]
use std::sync::OnceLock;
#[cfg(target_os = "illumos")]
Expand Down Expand Up @@ -1042,7 +1044,7 @@ pub struct ServiceProcess {
pub log_file: Utf8PathBuf,
}

/// Errors returned from [`InstalledZone::install`].
/// Errors returned from [`ZoneBuilder::install`].
#[derive(thiserror::Error, Debug)]
pub enum InstallZoneError {
#[error("Cannot create '{zone}': failed to create control VNIC: {err}")]
Expand All @@ -1062,6 +1064,9 @@ pub enum InstallZoneError {

#[error("Failed to find zone image '{image}' from {paths:?}")]
ImageNotFound { image: String, paths: Vec<Utf8PathBuf> },

#[error("Attempted to call install() on underspecified ZoneBuilder")]
IncompleteBuilder,
}

pub struct InstalledZone {
Expand Down Expand Up @@ -1118,24 +1123,208 @@ impl InstalledZone {
&self.zonepath
}

// TODO: This would benefit from a "builder-pattern" interface.
#[allow(clippy::too_many_arguments)]
pub async fn install(
log: &Logger,
underlay_vnic_allocator: &VnicAllocator<Etherstub>,
zone_root_path: &Utf8Path,
zone_image_paths: &[Utf8PathBuf],
zone_type: &str,
unique_name: Option<Uuid>,
datasets: &[zone::Dataset],
filesystems: &[zone::Fs],
data_links: &[String],
devices: &[zone::Device],
opte_ports: Vec<(Port, PortTicket)>,
bootstrap_vnic: Option<Link>,
links: Vec<Link>,
limit_priv: Vec<String>,
) -> Result<InstalledZone, InstallZoneError> {
pub fn site_profile_xml_path(&self) -> Utf8PathBuf {
let mut path: Utf8PathBuf = self.zonepath().into();
path.push("root/var/svc/profile/site.xml");
path
}
}

#[derive(Clone)]
pub struct FakeZoneBuilderConfig {
temp_dir: Arc<Utf8TempDir>,
}

#[derive(Clone, Default)]
pub struct ZoneBuilderFactory {
// Why this is part of this builder/factory and not some separate builder
// type: At time of writing, to the best of my knowledge:
// - If we want builder pattern, we need to return some type of `Self`.
// - If we have a trait that returns `Self` type, we can't turn it into a
// trait object (i.e. Box<dyn ZoneBuilderFactoryInterface>).
// - Plumbing concrete types as generics through every other type that
// needs to construct zones (and anything else with a lot of parameters)
// seems like a worse idea.
fake_cfg: Option<FakeZoneBuilderConfig>,
}

impl ZoneBuilderFactory {
/// For use in unit tests that don't require actual zone creation to occur.
pub fn fake() -> Self {
Self {
fake_cfg: Some(FakeZoneBuilderConfig {
temp_dir: Arc::new(Utf8TempDir::new().unwrap()),
}),
}
}

/// Create a [ZoneBuilder] that inherits this factory's fakeness.
pub fn builder<'a>(&self) -> ZoneBuilder<'a> {
ZoneBuilder { fake_cfg: self.fake_cfg.clone(), ..Default::default() }
}
}

/// Builder-pattern construct for creating an [InstalledZone].
/// Created by [ZoneBuilderFactory].
#[derive(Default)]
pub struct ZoneBuilder<'a> {
log: Option<Logger>,
underlay_vnic_allocator: Option<&'a VnicAllocator<Etherstub>>,
zone_root_path: Option<&'a Utf8Path>,
zone_image_paths: Option<&'a [Utf8PathBuf]>,
zone_type: Option<&'a str>,
unique_name: Option<Uuid>, // actually optional
datasets: Option<&'a [zone::Dataset]>,
filesystems: Option<&'a [zone::Fs]>,
data_links: Option<&'a [String]>,
devices: Option<&'a [zone::Device]>,
opte_ports: Option<Vec<(Port, PortTicket)>>,
bootstrap_vnic: Option<Link>, // actually optional
links: Option<Vec<Link>>,
limit_priv: Option<Vec<String>>,
fake_cfg: Option<FakeZoneBuilderConfig>,
}

impl<'a> ZoneBuilder<'a> {
pub fn with_log(mut self, log: Logger) -> Self {
self.log = Some(log);
self
}

pub fn with_underlay_vnic_allocator(
mut self,
vnic_allocator: &'a VnicAllocator<Etherstub>,
) -> Self {
self.underlay_vnic_allocator = Some(vnic_allocator);
self
}

pub fn with_zone_root_path(mut self, root_path: &'a Utf8Path) -> Self {
self.zone_root_path = Some(root_path);
self
}

pub fn with_zone_image_paths(
mut self,
image_paths: &'a [Utf8PathBuf],
) -> Self {
self.zone_image_paths = Some(image_paths);
self
}

pub fn with_zone_type(mut self, zone_type: &'a str) -> Self {
self.zone_type = Some(zone_type);
self
}

pub fn with_unique_name(mut self, uuid: Uuid) -> Self {
self.unique_name = Some(uuid);
self
}

pub fn with_datasets(mut self, datasets: &'a [zone::Dataset]) -> Self {
self.datasets = Some(datasets);
self
}

pub fn with_filesystems(mut self, filesystems: &'a [zone::Fs]) -> Self {
self.filesystems = Some(filesystems);
self
}

pub fn with_data_links(mut self, links: &'a [String]) -> Self {
self.data_links = Some(links);
self
}

pub fn with_devices(mut self, devices: &'a [zone::Device]) -> Self {
self.devices = Some(devices);
self
}

pub fn with_opte_ports(mut self, ports: Vec<(Port, PortTicket)>) -> Self {
self.opte_ports = Some(ports);
self
}

pub fn with_bootstrap_vnic(mut self, vnic: Link) -> Self {
self.bootstrap_vnic = Some(vnic);
self
}

pub fn with_links(mut self, links: Vec<Link>) -> Self {
self.links = Some(links);
self
}

pub fn with_limit_priv(mut self, limit_priv: Vec<String>) -> Self {
self.limit_priv = Some(limit_priv);
self
}

fn fake_install(self) -> Result<InstalledZone, InstallZoneError> {
let zone = self
.zone_type
.ok_or(InstallZoneError::IncompleteBuilder)?
.to_string();
let control_vnic = self
.underlay_vnic_allocator
.ok_or(InstallZoneError::IncompleteBuilder)?
.new_control(None)
.map_err(move |err| InstallZoneError::CreateVnic { zone, err })?;
let fake_cfg = self.fake_cfg.unwrap();
let temp_dir = fake_cfg.temp_dir.path().to_path_buf();
(|| {
let full_zone_name = InstalledZone::get_zone_name(
self.zone_type?,
self.unique_name,
);
let zonepath = temp_dir
.join(self.zone_root_path?.strip_prefix("/").unwrap())
.join(&full_zone_name);
let iz = InstalledZone {
log: self.log?,
zonepath,
name: full_zone_name,
control_vnic,
bootstrap_vnic: self.bootstrap_vnic,
opte_ports: self.opte_ports?,
links: self.links?,
};
let xml_path = iz.site_profile_xml_path().parent()?.to_path_buf();
std::fs::create_dir_all(&xml_path)
.unwrap_or_else(|_| panic!("ZoneBuilder::fake_install couldn't create site profile xml path {:?}", xml_path));
Some(iz)
})()
.ok_or(InstallZoneError::IncompleteBuilder)
}

pub async fn install(self) -> Result<InstalledZone, InstallZoneError> {
if self.fake_cfg.is_some() {
return self.fake_install();
}

let Self {
log: Some(log),
underlay_vnic_allocator: Some(underlay_vnic_allocator),
zone_root_path: Some(zone_root_path),
zone_image_paths: Some(zone_image_paths),
zone_type: Some(zone_type),
unique_name,
datasets: Some(datasets),
filesystems: Some(filesystems),
data_links: Some(data_links),
devices: Some(devices),
opte_ports: Some(opte_ports),
bootstrap_vnic,
links: Some(links),
limit_priv: Some(limit_priv),
..
} = self
else {
return Err(InstallZoneError::IncompleteBuilder);
};

let control_vnic =
underlay_vnic_allocator.new_control(None).map_err(|err| {
InstallZoneError::CreateVnic {
Expand All @@ -1144,7 +1333,8 @@ impl InstalledZone {
}
})?;

let full_zone_name = Self::get_zone_name(zone_type, unique_name);
let full_zone_name =
InstalledZone::get_zone_name(zone_type, unique_name);

// Looks for the image within `zone_image_path`, in order.
let image = format!("{}.tar.gz", zone_type);
Expand Down Expand Up @@ -1182,7 +1372,7 @@ impl InstalledZone {
net_device_names.dedup();

Zones::install_omicron_zone(
log,
&log,
&zone_root_path,
&full_zone_name,
&zone_image_path,
Expand All @@ -1209,12 +1399,6 @@ impl InstalledZone {
links,
})
}

pub fn site_profile_xml_path(&self) -> Utf8PathBuf {
let mut path: Utf8PathBuf = self.zonepath().into();
path.push("root/var/svc/profile/site.xml");
path
}
}

/// Return true if the service with the given FMRI appears to be an
Expand Down
48 changes: 25 additions & 23 deletions sled-agent/src/instance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ use futures::lock::{Mutex, MutexGuard};
use illumos_utils::dladm::Etherstub;
use illumos_utils::link::VnicAllocator;
use illumos_utils::opte::{DhcpCfg, PortManager};
use illumos_utils::running_zone::{InstalledZone, RunningZone};
use illumos_utils::running_zone::{RunningZone, ZoneBuilderFactory};
use illumos_utils::svc::wait_for_service;
use illumos_utils::zone::Zones;
use illumos_utils::zone::PROPOLIS_ZONE_PREFIX;
Expand Down Expand Up @@ -226,6 +226,9 @@ struct InstanceInner {
// Storage resources
storage: StorageHandle,

// Used to create propolis zones
zone_builder_factory: ZoneBuilderFactory,

// Object used to collect zone bundles from this instance when terminated.
zone_bundler: ZoneBundler,

Expand Down Expand Up @@ -611,6 +614,7 @@ impl Instance {
port_manager,
storage,
zone_bundler,
zone_builder_factory,
} = services;

let mut dhcp_config = DhcpCfg {
Expand Down Expand Up @@ -678,6 +682,7 @@ impl Instance {
running_state: None,
nexus_client,
storage,
zone_builder_factory,
zone_bundler,
instance_ticket: ticket,
};
Expand Down Expand Up @@ -904,31 +909,28 @@ impl Instance {
.choose(&mut rng)
.ok_or_else(|| Error::U2NotFound)?
.clone();
let installed_zone = InstalledZone::install(
&inner.log,
&inner.vnic_allocator,
&root,
&["/opt/oxide".into()],
"propolis-server",
Some(*inner.propolis_id()),
// dataset=
&[],
// filesystems=
&[],
// data_links=
&[],
&[
let installed_zone = inner
.zone_builder_factory
.builder()
.with_log(inner.log.clone())
.with_underlay_vnic_allocator(&inner.vnic_allocator)
.with_zone_root_path(&root)
.with_zone_image_paths(&["/opt/oxide".into()])
.with_zone_type("propolis-server")
.with_unique_name(*inner.propolis_id())
.with_datasets(&[])
.with_filesystems(&[])
.with_data_links(&[])
.with_devices(&[
zone::Device { name: "/dev/vmm/*".to_string() },
zone::Device { name: "/dev/vmmctl".to_string() },
zone::Device { name: "/dev/viona".to_string() },
],
opte_ports,
// physical_nic=
None,
vec![],
vec![],
)
.await?;
])
.with_opte_ports(opte_ports)
.with_links(vec![])
.with_limit_priv(vec![])
.install()
.await?;

let gateway = inner.port_manager.underlay_ip();

Expand Down
Loading
Loading