Skip to content

Commit 0c41f95

Browse files
author
lif
committed
Refactor InstalledZone::install to use a builder pattern, per TODO.
Additionally, make a builder-factory with an option to create fake builders, in service of refactoring some things to enable some unit tests being written.
1 parent 85ac741 commit 0c41f95

File tree

7 files changed

+272
-68
lines changed

7 files changed

+272
-68
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

illumos-utils/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ async-trait.workspace = true
1111
bhyve_api.workspace = true
1212
byteorder.workspace = true
1313
camino.workspace = true
14+
camino-tempfile.workspace = true
1415
cfg-if.workspace = true
1516
crucible-smf.workspace = true
1617
futures.workspace = true

illumos-utils/src/running_zone.rs

Lines changed: 211 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@ use crate::opte::{Port, PortTicket};
1111
use crate::svc::wait_for_service;
1212
use crate::zone::{AddressRequest, IPADM, ZONE_PREFIX};
1313
use camino::{Utf8Path, Utf8PathBuf};
14+
use camino_tempfile::Utf8TempDir;
1415
use ipnetwork::IpNetwork;
1516
use omicron_common::backoff;
1617
use slog::{error, info, o, warn, Logger};
1718
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
19+
use std::sync::Arc;
1820
#[cfg(target_os = "illumos")]
1921
use std::sync::OnceLock;
2022
#[cfg(target_os = "illumos")]
@@ -1042,7 +1044,7 @@ pub struct ServiceProcess {
10421044
pub log_file: Utf8PathBuf,
10431045
}
10441046

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

10631065
#[error("Failed to find zone image '{image}' from {paths:?}")]
10641066
ImageNotFound { image: String, paths: Vec<Utf8PathBuf> },
1067+
1068+
#[error("Attempted to call install() on underspecified ZoneBuilder")]
1069+
IncompleteBuilder,
10651070
}
10661071

10671072
pub struct InstalledZone {
@@ -1118,24 +1123,208 @@ impl InstalledZone {
11181123
&self.zonepath
11191124
}
11201125

1121-
// TODO: This would benefit from a "builder-pattern" interface.
1122-
#[allow(clippy::too_many_arguments)]
1123-
pub async fn install(
1124-
log: &Logger,
1125-
underlay_vnic_allocator: &VnicAllocator<Etherstub>,
1126-
zone_root_path: &Utf8Path,
1127-
zone_image_paths: &[Utf8PathBuf],
1128-
zone_type: &str,
1129-
unique_name: Option<Uuid>,
1130-
datasets: &[zone::Dataset],
1131-
filesystems: &[zone::Fs],
1132-
data_links: &[String],
1133-
devices: &[zone::Device],
1134-
opte_ports: Vec<(Port, PortTicket)>,
1135-
bootstrap_vnic: Option<Link>,
1136-
links: Vec<Link>,
1137-
limit_priv: Vec<String>,
1138-
) -> Result<InstalledZone, InstallZoneError> {
1126+
pub fn site_profile_xml_path(&self) -> Utf8PathBuf {
1127+
let mut path: Utf8PathBuf = self.zonepath().into();
1128+
path.push("root/var/svc/profile/site.xml");
1129+
path
1130+
}
1131+
}
1132+
1133+
#[derive(Clone)]
1134+
pub struct FakeZoneBuilderConfig {
1135+
temp_dir: Arc<Utf8TempDir>,
1136+
}
1137+
1138+
#[derive(Clone, Default)]
1139+
pub struct ZoneBuilderFactory {
1140+
// Why this is part of this builder/factory and not some separate builder
1141+
// type: At time of writing, to the best of my knowledge:
1142+
// - If we want builder pattern, we need to return some type of `Self`.
1143+
// - If we have a trait that returns `Self` type, we can't turn it into a
1144+
// trait object (i.e. Box<dyn ZoneBuilderFactoryInterface>).
1145+
// - Plumbing concrete types as generics through every other type that
1146+
// needs to construct zones (and anything else with a lot of parameters)
1147+
// seems like a worse idea.
1148+
fake_cfg: Option<FakeZoneBuilderConfig>,
1149+
}
1150+
1151+
impl ZoneBuilderFactory {
1152+
/// For use in unit tests that don't require actual zone creation to occur.
1153+
pub fn fake() -> Self {
1154+
Self {
1155+
fake_cfg: Some(FakeZoneBuilderConfig {
1156+
temp_dir: Arc::new(Utf8TempDir::new().unwrap()),
1157+
}),
1158+
}
1159+
}
1160+
1161+
/// Create a [ZoneBuilder] that inherits this factory's fakeness.
1162+
pub fn builder<'a>(&self) -> ZoneBuilder<'a> {
1163+
ZoneBuilder { fake_cfg: self.fake_cfg.clone(), ..Default::default() }
1164+
}
1165+
}
1166+
1167+
/// Builder-pattern construct for creating an [InstalledZone].
1168+
/// Created by [ZoneBuilderFactory].
1169+
#[derive(Default)]
1170+
pub struct ZoneBuilder<'a> {
1171+
log: Option<Logger>,
1172+
underlay_vnic_allocator: Option<&'a VnicAllocator<Etherstub>>,
1173+
zone_root_path: Option<&'a Utf8Path>,
1174+
zone_image_paths: Option<&'a [Utf8PathBuf]>,
1175+
zone_type: Option<&'a str>,
1176+
unique_name: Option<Uuid>, // actually optional
1177+
datasets: Option<&'a [zone::Dataset]>,
1178+
filesystems: Option<&'a [zone::Fs]>,
1179+
data_links: Option<&'a [String]>,
1180+
devices: Option<&'a [zone::Device]>,
1181+
opte_ports: Option<Vec<(Port, PortTicket)>>,
1182+
bootstrap_vnic: Option<Link>, // actually optional
1183+
links: Option<Vec<Link>>,
1184+
limit_priv: Option<Vec<String>>,
1185+
fake_cfg: Option<FakeZoneBuilderConfig>,
1186+
}
1187+
1188+
impl<'a> ZoneBuilder<'a> {
1189+
pub fn with_log(mut self, log: Logger) -> Self {
1190+
self.log = Some(log);
1191+
self
1192+
}
1193+
1194+
pub fn with_underlay_vnic_allocator(
1195+
mut self,
1196+
vnic_allocator: &'a VnicAllocator<Etherstub>,
1197+
) -> Self {
1198+
self.underlay_vnic_allocator = Some(vnic_allocator);
1199+
self
1200+
}
1201+
1202+
pub fn with_zone_root_path(mut self, root_path: &'a Utf8Path) -> Self {
1203+
self.zone_root_path = Some(root_path);
1204+
self
1205+
}
1206+
1207+
pub fn with_zone_image_paths(
1208+
mut self,
1209+
image_paths: &'a [Utf8PathBuf],
1210+
) -> Self {
1211+
self.zone_image_paths = Some(image_paths);
1212+
self
1213+
}
1214+
1215+
pub fn with_zone_type(mut self, zone_type: &'a str) -> Self {
1216+
self.zone_type = Some(zone_type);
1217+
self
1218+
}
1219+
1220+
pub fn with_unique_name(mut self, uuid: Uuid) -> Self {
1221+
self.unique_name = Some(uuid);
1222+
self
1223+
}
1224+
1225+
pub fn with_datasets(mut self, datasets: &'a [zone::Dataset]) -> Self {
1226+
self.datasets = Some(datasets);
1227+
self
1228+
}
1229+
1230+
pub fn with_filesystems(mut self, filesystems: &'a [zone::Fs]) -> Self {
1231+
self.filesystems = Some(filesystems);
1232+
self
1233+
}
1234+
1235+
pub fn with_data_links(mut self, links: &'a [String]) -> Self {
1236+
self.data_links = Some(links);
1237+
self
1238+
}
1239+
1240+
pub fn with_devices(mut self, devices: &'a [zone::Device]) -> Self {
1241+
self.devices = Some(devices);
1242+
self
1243+
}
1244+
1245+
pub fn with_opte_ports(mut self, ports: Vec<(Port, PortTicket)>) -> Self {
1246+
self.opte_ports = Some(ports);
1247+
self
1248+
}
1249+
1250+
pub fn with_bootstrap_vnic(mut self, vnic: Link) -> Self {
1251+
self.bootstrap_vnic = Some(vnic);
1252+
self
1253+
}
1254+
1255+
pub fn with_links(mut self, links: Vec<Link>) -> Self {
1256+
self.links = Some(links);
1257+
self
1258+
}
1259+
1260+
pub fn with_limit_priv(mut self, limit_priv: Vec<String>) -> Self {
1261+
self.limit_priv = Some(limit_priv);
1262+
self
1263+
}
1264+
1265+
fn fake_install(self) -> Result<InstalledZone, InstallZoneError> {
1266+
let zone = self
1267+
.zone_type
1268+
.ok_or(InstallZoneError::IncompleteBuilder)?
1269+
.to_string();
1270+
let control_vnic = self
1271+
.underlay_vnic_allocator
1272+
.ok_or(InstallZoneError::IncompleteBuilder)?
1273+
.new_control(None)
1274+
.map_err(move |err| InstallZoneError::CreateVnic { zone, err })?;
1275+
let fake_cfg = self.fake_cfg.unwrap();
1276+
let temp_dir = fake_cfg.temp_dir.path().to_path_buf();
1277+
(|| {
1278+
let full_zone_name = InstalledZone::get_zone_name(
1279+
self.zone_type?,
1280+
self.unique_name,
1281+
);
1282+
let zonepath = temp_dir
1283+
.join(self.zone_root_path?.strip_prefix("/").unwrap())
1284+
.join(&full_zone_name);
1285+
let iz = InstalledZone {
1286+
log: self.log?,
1287+
zonepath,
1288+
name: full_zone_name,
1289+
control_vnic,
1290+
bootstrap_vnic: self.bootstrap_vnic,
1291+
opte_ports: self.opte_ports?,
1292+
links: self.links?,
1293+
};
1294+
let xml_path = iz.site_profile_xml_path().parent()?.to_path_buf();
1295+
std::fs::create_dir_all(&xml_path)
1296+
.unwrap_or_else(|_| panic!("ZoneBuilder::fake_install couldn't create site profile xml path {:?}", xml_path));
1297+
Some(iz)
1298+
})()
1299+
.ok_or(InstallZoneError::IncompleteBuilder)
1300+
}
1301+
1302+
pub async fn install(self) -> Result<InstalledZone, InstallZoneError> {
1303+
if self.fake_cfg.is_some() {
1304+
return self.fake_install();
1305+
}
1306+
1307+
let Self {
1308+
log: Some(log),
1309+
underlay_vnic_allocator: Some(underlay_vnic_allocator),
1310+
zone_root_path: Some(zone_root_path),
1311+
zone_image_paths: Some(zone_image_paths),
1312+
zone_type: Some(zone_type),
1313+
unique_name,
1314+
datasets: Some(datasets),
1315+
filesystems: Some(filesystems),
1316+
data_links: Some(data_links),
1317+
devices: Some(devices),
1318+
opte_ports: Some(opte_ports),
1319+
bootstrap_vnic,
1320+
links: Some(links),
1321+
limit_priv: Some(limit_priv),
1322+
..
1323+
} = self
1324+
else {
1325+
return Err(InstallZoneError::IncompleteBuilder);
1326+
};
1327+
11391328
let control_vnic =
11401329
underlay_vnic_allocator.new_control(None).map_err(|err| {
11411330
InstallZoneError::CreateVnic {
@@ -1144,7 +1333,8 @@ impl InstalledZone {
11441333
}
11451334
})?;
11461335

1147-
let full_zone_name = Self::get_zone_name(zone_type, unique_name);
1336+
let full_zone_name =
1337+
InstalledZone::get_zone_name(zone_type, unique_name);
11481338

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

11841374
Zones::install_omicron_zone(
1185-
log,
1375+
&log,
11861376
&zone_root_path,
11871377
&full_zone_name,
11881378
&zone_image_path,
@@ -1209,12 +1399,6 @@ impl InstalledZone {
12091399
links,
12101400
})
12111401
}
1212-
1213-
pub fn site_profile_xml_path(&self) -> Utf8PathBuf {
1214-
let mut path: Utf8PathBuf = self.zonepath().into();
1215-
path.push("root/var/svc/profile/site.xml");
1216-
path
1217-
}
12181402
}
12191403

12201404
/// Return true if the service with the given FMRI appears to be an

sled-agent/src/instance.rs

Lines changed: 25 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ use futures::lock::{Mutex, MutexGuard};
2727
use illumos_utils::dladm::Etherstub;
2828
use illumos_utils::link::VnicAllocator;
2929
use illumos_utils::opte::{DhcpCfg, PortManager};
30-
use illumos_utils::running_zone::{InstalledZone, RunningZone};
30+
use illumos_utils::running_zone::{RunningZone, ZoneBuilderFactory};
3131
use illumos_utils::svc::wait_for_service;
3232
use illumos_utils::zone::Zones;
3333
use illumos_utils::zone::PROPOLIS_ZONE_PREFIX;
@@ -227,6 +227,9 @@ struct InstanceInner {
227227
// Storage resources
228228
storage: StorageResources,
229229

230+
// Used to create propolis zones
231+
zone_builder_factory: ZoneBuilderFactory,
232+
230233
// Object used to collect zone bundles from this instance when terminated.
231234
zone_bundler: ZoneBundler,
232235

@@ -612,6 +615,7 @@ impl Instance {
612615
port_manager,
613616
storage,
614617
zone_bundler,
618+
zone_builder_factory,
615619
} = services;
616620

617621
let mut dhcp_config = DhcpCfg {
@@ -679,6 +683,7 @@ impl Instance {
679683
running_state: None,
680684
nexus_client,
681685
storage,
686+
zone_builder_factory,
682687
zone_bundler,
683688
instance_ticket: ticket,
684689
};
@@ -904,31 +909,28 @@ impl Instance {
904909
.choose(&mut rng)
905910
.ok_or_else(|| Error::U2NotFound)?
906911
.clone();
907-
let installed_zone = InstalledZone::install(
908-
&inner.log,
909-
&inner.vnic_allocator,
910-
&root,
911-
&["/opt/oxide".into()],
912-
"propolis-server",
913-
Some(*inner.propolis_id()),
914-
// dataset=
915-
&[],
916-
// filesystems=
917-
&[],
918-
// data_links=
919-
&[],
920-
&[
912+
let installed_zone = inner
913+
.zone_builder_factory
914+
.builder()
915+
.with_log(inner.log.clone())
916+
.with_underlay_vnic_allocator(&inner.vnic_allocator)
917+
.with_zone_root_path(&root)
918+
.with_zone_image_paths(&["/opt/oxide".into()])
919+
.with_zone_type("propolis-server")
920+
.with_unique_name(*inner.propolis_id())
921+
.with_datasets(&[])
922+
.with_filesystems(&[])
923+
.with_data_links(&[])
924+
.with_devices(&[
921925
zone::Device { name: "/dev/vmm/*".to_string() },
922926
zone::Device { name: "/dev/vmmctl".to_string() },
923927
zone::Device { name: "/dev/viona".to_string() },
924-
],
925-
opte_ports,
926-
// physical_nic=
927-
None,
928-
vec![],
929-
vec![],
930-
)
931-
.await?;
928+
])
929+
.with_opte_ports(opte_ports)
930+
.with_links(vec![])
931+
.with_limit_priv(vec![])
932+
.install()
933+
.await?;
932934

933935
let gateway = inner.port_manager.underlay_ip();
934936

0 commit comments

Comments
 (0)