From 2ce06f3ffc2c08c1a0697228e22584402b6386a8 Mon Sep 17 00:00:00 2001 From: Sean Klein Date: Wed, 16 Aug 2023 12:11:38 -0700 Subject: [PATCH 01/18] Starting to make a fake host --- common/src/vlan.rs | 2 +- illumos-utils/src/host/host.rs | 760 ++++++++++++++++++++++++++++++ illumos-utils/src/host/input.rs | 3 +- illumos-utils/src/host/mod.rs | 1 + illumos-utils/src/lib.rs | 2 + illumos-utils/src/running_zone.rs | 20 +- 6 files changed, 771 insertions(+), 17 deletions(-) create mode 100644 illumos-utils/src/host/host.rs diff --git a/common/src/vlan.rs b/common/src/vlan.rs index 45776e09ac9..c9ed8b11e4e 100644 --- a/common/src/vlan.rs +++ b/common/src/vlan.rs @@ -13,7 +13,7 @@ use std::str::FromStr; pub const VLAN_MAX: u16 = 4094; /// Wrapper around a VLAN ID, ensuring it is valid. -#[derive(Debug, Deserialize, Clone, Copy)] +#[derive(Debug, Deserialize, Clone, Copy, Eq, PartialEq)] pub struct VlanID(u16); impl VlanID { diff --git a/illumos-utils/src/host/host.rs b/illumos-utils/src/host/host.rs new file mode 100644 index 00000000000..cf8e753f066 --- /dev/null +++ b/illumos-utils/src/host/host.rs @@ -0,0 +1,760 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// 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/. + +//! Emulates an illumos system + +// TODO TODO TODO REMOVE ME +#![allow(dead_code)] + +use crate::dladm::DLADM; +use crate::host::input::Input; +use crate::host::PFEXEC; +use crate::zone::IPADM; +use crate::zone::SVCCFG; +use crate::zone::ZLOGIN; +use crate::zone::ZONEADM; +use crate::zone::ZONECFG; +use crate::ROUTE; +use camino::Utf8PathBuf; +use ipnetwork::IpNetwork; +use omicron_common::vlan::VlanID; +use std::collections::HashMap; +use std::str::FromStr; + +enum LinkType { + Etherstub, + Vnic, +} + +#[derive(Debug, PartialEq, Eq)] +struct LinkName(String); +struct Link { + ty: LinkType, + parent: Option, + properties: HashMap, +} + +struct IpInterfaceName(String); +struct IpInterface {} + +enum RouteDestination { + Default, + Literal(IpNetwork), +} + +struct Route { + destination: RouteDestination, + gateway: IpNetwork, +} + +struct ServiceName(String); + +struct Service { + state: smf::SmfState, + properties: HashMap, +} + +struct ZoneEnvironment { + id: u64, + links: HashMap, + ip_interfaces: HashMap, + routes: Vec, + services: HashMap, +} + +impl ZoneEnvironment { + fn new(id: u64) -> Self { + Self { + id, + links: HashMap::new(), + ip_interfaces: HashMap::new(), + routes: vec![], + services: HashMap::new(), + } + } +} + +// TODO: How much is it worth doing this vs just making things self-assembling? +// XXX E.g. is it actually worth emulating svccfg? + +struct ZoneName(String); +struct ZoneConfig { + state: zone::State, + brand: String, + // zonepath + path: Utf8PathBuf, + nets: Vec, + fs: Vec, + // E.g. zone image, overlays, etc. + layers: Vec, +} + +struct Zone { + config: ZoneConfig, + environment: ZoneEnvironment, +} + +struct Host { + global: ZoneEnvironment, + zones: HashMap, +} + +impl Host { + pub fn new() -> Self { + Self { global: ZoneEnvironment::new(0), zones: HashMap::new() } + } +} + +#[derive(Debug)] +enum DladmCommand { + CreateVnic { + link: LinkName, + temporary: bool, + mac: Option, + vlan: Option, + name: LinkName, + properties: HashMap, + }, + CreateEtherstub { + temporary: bool, + name: LinkName, + }, + DeleteEtherstub { + temporary: bool, + name: LinkName, + }, + DeleteVnic { + temporary: bool, + name: LinkName, + }, + ShowEtherstub { + name: Option, + }, + ShowLink { + name: LinkName, + fields: Vec, + }, + ShowPhys { + mac: bool, + fields: Vec, + name: Option, + }, + ShowVnic { + fields: Option>, + name: Option, + }, + SetLinkprop { + temporary: bool, + properties: HashMap, + name: LinkName, + }, +} + +impl TryFrom for DladmCommand { + type Error = String; + + fn try_from(mut input: Input) -> Result { + if input.program != DLADM { + return Err(format!("Not dladm command: {}", input.program)); + } + + match input.args.pop_front().ok_or_else(|| "Missing command")?.as_str() + { + "create-vnic" => { + let mut link = None; + let mut temporary = false; + let mut mac = None; + let mut vlan = None; + let mut properties = HashMap::new(); + let name = LinkName( + input.args.pop_back().ok_or_else(|| "Missing name")?, + ); + + while !input.args.is_empty() { + if shift_if_eq(&mut input, "-t")? { + temporary = true; + } else if shift_if_eq(&mut input, "-p")? { + let props = shift_arg(&mut input)?; + let props = props.split(','); + for prop in props { + let (k, v) = prop + .split_once('=') + .ok_or_else(|| "Bad property")?; + properties.insert(k.to_string(), v.to_string()); + } + } else if shift_if_eq(&mut input, "-m")? { + // NOTE: Not yet supporting the keyword-based MACs. + mac = Some(shift_arg(&mut input)?); + } else if shift_if_eq(&mut input, "-l")? { + link = Some(LinkName(shift_arg(&mut input)?)); + } else if shift_if_eq(&mut input, "-v")? { + vlan = Some( + VlanID::from_str(&shift_arg(&mut input)?) + .map_err(|e| e.to_string())?, + ); + } else { + return Err(format!("Invalid arguments {}", input)); + } + } + + Ok(Self::CreateVnic { + link: link.ok_or_else(|| "Missing link")?, + temporary, + mac, + vlan, + name, + properties, + }) + } + "create-etherstub" => { + let mut temporary = false; + let name = LinkName( + input.args.pop_back().ok_or_else(|| "Missing name")?, + ); + while !input.args.is_empty() { + if shift_if_eq(&mut input, "-t")? { + temporary = true; + } else { + return Err(format!("Invalid arguments {}", input)); + } + } + Ok(Self::CreateEtherstub { temporary, name }) + } + "delete-etherstub" => { + let mut temporary = false; + let name = LinkName( + input.args.pop_back().ok_or_else(|| "Missing name")?, + ); + while !input.args.is_empty() { + if shift_if_eq(&mut input, "-t")? { + temporary = true; + } else { + return Err(format!("Invalid arguments {}", input)); + } + } + Ok(Self::DeleteEtherstub { temporary, name }) + } + "delete-vnic" => { + let mut temporary = false; + let name = LinkName( + input.args.pop_back().ok_or_else(|| "Missing name")?, + ); + while !input.args.is_empty() { + if shift_if_eq(&mut input, "-t")? { + temporary = true; + } else { + return Err(format!("Invalid arguments {}", input)); + } + } + Ok(Self::DeleteVnic { temporary, name }) + } + "show-etherstub" => { + let name = input.args.pop_back().map(|s| LinkName(s)); + no_args_remaining(&input)?; + Ok(Self::ShowEtherstub { name }) + } + "show-link" => { + let name = LinkName( + input.args.pop_back().ok_or_else(|| "Missing name")?, + ); + if !shift_if_eq(&mut input, "-p")? { + return Err( + "You should ask for parseable output ('-p')".into() + ); + } + if !shift_if_eq(&mut input, "-o")? { + return Err( + "You should ask for specific outputs ('-o')".into() + ); + } + // TODO: Could parse an enum of known properties... + let fields = shift_arg(&mut input)? + .split(',') + .map(|s| s.to_string()) + .collect(); + no_args_remaining(&input)?; + + Ok(Self::ShowLink { name, fields }) + } + "show-phys" => { + let mut mac = false; + if shift_if_eq(&mut input, "-m")? { + mac = true; + } + if !shift_if_eq(&mut input, "-p")? { + return Err( + "You should ask for parseable output ('-p')".into() + ); + } + if !shift_if_eq(&mut input, "-o")? { + return Err( + "You should ask for specific outputs ('-o')".into() + ); + } + // TODO: Could parse an enum of known properties... + let fields = shift_arg(&mut input)? + .split(',') + .map(|s| s.to_string()) + .collect(); + let name = input.args.pop_front().map(|s| LinkName(s)); + no_args_remaining(&input)?; + + Ok(Self::ShowPhys { mac, fields, name }) + } + "show-vnic" => { + let mut fields = None; + if shift_if_eq(&mut input, "-p")? { + if !shift_if_eq(&mut input, "-o")? { + return Err( + "You should ask for specific outputs ('-o')".into(), + ); + } + fields = Some( + shift_arg(&mut input)? + .split(',') + .map(|s| s.to_string()) + .collect(), + ); + } + + let name = input.args.pop_front().map(|s| LinkName(s)); + no_args_remaining(&input)?; + Ok(Self::ShowVnic { fields, name }) + } + "set-linkprop" => { + let mut temporary = false; + let mut properties = HashMap::new(); + let name = LinkName( + input.args.pop_back().ok_or_else(|| "Missing name")?, + ); + + while !input.args.is_empty() { + if shift_if_eq(&mut input, "-t")? { + temporary = true; + } else if shift_if_eq(&mut input, "-p")? { + let props = shift_arg(&mut input)?; + let props = props.split(','); + for prop in props { + let (k, v) = prop + .split_once('=') + .ok_or_else(|| "Bad property")?; + properties.insert(k.to_string(), v.to_string()); + } + } else { + return Err(format!("Invalid arguments {}", input)); + } + } + + if properties.is_empty() { + return Err("Missing properties".into()); + } + + Ok(Self::SetLinkprop { temporary, properties, name }) + } + _ => Err(format!("Unsupported command: {}", input.program)), + } + } +} + +enum IpadmCommand { + CreateIf, + DeleteIf, + ShowIf, + SetIfprop, +} + +enum RouteCommand { + Add, +} + +enum SvccfgCommand { + Import, + Refresh, + Setprop, +} + +enum ZoneadmCommand { + List, + Install, + Boot, +} + +enum ZonecfgCommand { + Create, +} + +enum KnownCommand { + Dladm(DladmCommand), + Ipadm(IpadmCommand), + RouteAdm, + Route(RouteCommand), + Svccfg(SvccfgCommand), + Zoneadm(ZoneadmCommand), + Zonecfg(ZonecfgCommand), +} + +struct Command { + with_pfexec: bool, + in_zone: Option, + cmd: KnownCommand, +} + +impl TryFrom for Command { + type Error = String; + + fn try_from(mut input: Input) -> Result { + let mut with_pfexec = false; + let mut in_zone = None; + + while input.program == PFEXEC { + with_pfexec = true; + shift_program(&mut input)?; + } + if input.program == ZLOGIN { + shift_program(&mut input)?; + in_zone = Some(ZoneName(shift_program(&mut input)?)); + } + + let cmd = match input.program.as_str() { + DLADM => KnownCommand::Dladm(DladmCommand::try_from(input)?), + IPADM => todo!(), + ROUTE => todo!(), + SVCCFG => todo!(), + ZONEADM => todo!(), + ZONECFG => todo!(), + _ => return Err(format!("Unknown command: {}", input.program)), + }; + + Ok(Command { with_pfexec, in_zone, cmd }) + } +} + +// Shifts out the program, putting the subsequent argument in its place. +// +// Returns the prior program value. +fn shift_program(input: &mut Input) -> Result { + let new = input + .args + .pop_front() + .ok_or_else(|| format!("Failed to parse {input}"))?; + + let old = std::mem::replace(&mut input.program, new); + + Ok(old) +} + +fn no_args_remaining(input: &Input) -> Result<(), String> { + if !input.args.is_empty() { + return Err(format!("Unexpected extra arguments: {input}")); + } + Ok(()) +} + +// Removes the next argument unconditionally. +fn shift_arg(input: &mut Input) -> Result { + Ok(input.args.pop_front().ok_or_else(|| "Missing argument")?) +} + +// Removes the next argument if it equals value. +// +// Returns if it was equal. +fn shift_if_eq(input: &mut Input, value: &str) -> Result { + let eq = input.args.front().ok_or_else(|| "Not enough args")? == value; + if eq { + input.args.pop_front(); + } + Ok(eq) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn empty_state() { + let host = Host::new(); + + assert_eq!(0, host.global.id); + assert!(host.global.links.is_empty()); + assert!(host.global.ip_interfaces.is_empty()); + assert!(host.global.routes.is_empty()); + assert!(host.global.services.is_empty()); + assert!(host.zones.is_empty()); + } + + #[test] + fn dladm_create_vnic() { + // Valid usage + let DladmCommand::CreateVnic { link, temporary, mac, vlan, name, properties } = DladmCommand::try_from( + Input::shell(format!("{DLADM} create-vnic -t -l mylink newlink")) + ).unwrap() else { + panic!("Wrong command"); + }; + assert_eq!(link.0, "mylink"); + assert!(temporary); + assert!(mac.is_none()); + assert!(vlan.is_none()); + assert_eq!(name.0, "newlink"); + assert!(properties.is_empty()); + + // Valid usage + let DladmCommand::CreateVnic { link, temporary, mac, vlan, name, properties } = DladmCommand::try_from( + Input::shell(format!("{DLADM} create-vnic -l mylink -v 3 -m foobar -p mtu=123 newlink")) + ).unwrap() else { + panic!("Wrong command"); + }; + assert_eq!(link.0, "mylink"); + assert!(!temporary); + assert_eq!(mac.unwrap(), "foobar"); + assert_eq!(vlan.unwrap(), VlanID::new(3).unwrap()); + assert_eq!(name.0, "newlink"); + assert_eq!( + properties, + HashMap::from([("mtu".to_string(), "123".to_string())]) + ); + + // Missing link + DladmCommand::try_from(Input::shell(format!( + "{DLADM} create-vnic newlink" + ))) + .unwrap_err(); + + // Missing name + DladmCommand::try_from(Input::shell(format!( + "{DLADM} create-vnic -l mylink" + ))) + .unwrap_err(); + + // Bad properties + DladmCommand::try_from(Input::shell(format!( + "{DLADM} create-vnic -l mylink -p foo=bar,baz mylink" + ))) + .unwrap_err(); + + // Unknown argument + DladmCommand::try_from(Input::shell(format!( + "{DLADM} create-vnic -l mylink --splorch mylink" + ))) + .unwrap_err(); + + // Missing command + DladmCommand::try_from(Input::shell(DLADM)).unwrap_err(); + + // Not dladm + DladmCommand::try_from(Input::shell("hello!")).unwrap_err(); + } + + #[test] + fn dladm_create_etherstub() { + // Valid usage + let DladmCommand::CreateEtherstub { temporary, name } = DladmCommand::try_from( + Input::shell(format!("{DLADM} create-etherstub -t newlink")) + ).unwrap() else { + panic!("Wrong command"); + }; + + assert!(temporary); + assert_eq!(name.0, "newlink"); + + // Missing link + DladmCommand::try_from(Input::shell(format!( + "{DLADM} create-etherstub" + ))) + .unwrap_err(); + + // Invalid argument + DladmCommand::try_from(Input::shell(format!( + "{DLADM} create-etherstub --splorch mylink" + ))) + .unwrap_err(); + } + + #[test] + fn dladm_delete_etherstub() { + // Valid usage + let DladmCommand::DeleteEtherstub { temporary, name } = DladmCommand::try_from( + Input::shell(format!("{DLADM} delete-etherstub -t newlink")) + ).unwrap() else { + panic!("Wrong command"); + }; + + assert!(temporary); + assert_eq!(name.0, "newlink"); + + // Missing link + DladmCommand::try_from(Input::shell(format!( + "{DLADM} delete-etherstub" + ))) + .unwrap_err(); + + // Invalid argument + DladmCommand::try_from(Input::shell(format!( + "{DLADM} delete-etherstub --splorch mylink" + ))) + .unwrap_err(); + } + + #[test] + fn dladm_delete_vnic() { + // Valid usage + let DladmCommand::DeleteVnic { temporary, name } = DladmCommand::try_from( + Input::shell(format!("{DLADM} delete-vnic -t newlink")) + ).unwrap() else { + panic!("Wrong command"); + }; + + assert!(temporary); + assert_eq!(name.0, "newlink"); + + // Missing link + DladmCommand::try_from(Input::shell(format!("{DLADM} delete-vnic"))) + .unwrap_err(); + + // Invalid argument + DladmCommand::try_from(Input::shell(format!( + "{DLADM} delete-vnic --splorch mylink" + ))) + .unwrap_err(); + } + + #[test] + fn dladm_show_etherstub() { + // Valid usage + let DladmCommand::ShowEtherstub { name } = DladmCommand::try_from( + Input::shell(format!("{DLADM} show-etherstub newlink")) + ).unwrap() else { + panic!("Wrong command"); + }; + assert_eq!(name.unwrap().0, "newlink"); + + // Valid usage + let DladmCommand::ShowEtherstub { name } = DladmCommand::try_from( + Input::shell(format!("{DLADM} show-etherstub")) + ).unwrap() else { + panic!("Wrong command"); + }; + assert!(name.is_none()); + + // Invalid argument + DladmCommand::try_from(Input::shell(format!( + "{DLADM} show-etherstub --splorch mylink" + ))) + .unwrap_err(); + } + + #[test] + fn dladm_show_link() { + // Valid usage + let DladmCommand::ShowLink { name, fields } = DladmCommand::try_from( + Input::shell(format!("{DLADM} show-link -p -o LINK,STATE newlink")) + ).unwrap() else { + panic!("Wrong command"); + }; + assert_eq!(name.0, "newlink"); + assert_eq!(fields[0], "LINK"); + assert_eq!(fields[1], "STATE"); + + // Missing link name + DladmCommand::try_from(Input::shell(format!("{DLADM} show-link"))) + .unwrap_err(); + + // Not asking for output + DladmCommand::try_from(Input::shell(format!( + "{DLADM} show-link mylink" + ))) + .unwrap_err(); + + // Not asking for parseable output + DladmCommand::try_from(Input::shell(format!( + "{DLADM} show-link -o LINK mylink" + ))) + .unwrap_err(); + } + + #[test] + fn dladm_show_phys() { + // Valid usage + let DladmCommand::ShowPhys{ mac, fields, name } = DladmCommand::try_from( + Input::shell(format!("{DLADM} show-phys -p -o LINK")) + ).unwrap() else { + panic!("Wrong command"); + }; + assert!(!mac); + assert_eq!(fields[0], "LINK"); + assert!(name.is_none()); + + // Not asking for output + DladmCommand::try_from(Input::shell(format!( + "{DLADM} show-phys mylink" + ))) + .unwrap_err(); + + // Not asking for parseable output + DladmCommand::try_from(Input::shell(format!( + "{DLADM} show-phys -o LINK mylink" + ))) + .unwrap_err(); + } + + #[test] + fn dladm_show_vnic() { + // Valid usage + let DladmCommand::ShowVnic{ fields, name } = DladmCommand::try_from( + Input::shell(format!("{DLADM} show-vnic -p -o LINK")) + ).unwrap() else { + panic!("Wrong command"); + }; + assert_eq!(fields.unwrap(), vec!["LINK"]); + assert!(name.is_none()); + + // Valid usage + let DladmCommand::ShowVnic{ fields, name } = DladmCommand::try_from( + Input::shell(format!("{DLADM} show-vnic mylink")) + ).unwrap() else { + panic!("Wrong command"); + }; + assert!(fields.is_none()); + assert_eq!(name.unwrap().0, "mylink"); + + // Not asking for parseable output + DladmCommand::try_from(Input::shell(format!( + "{DLADM} show-vnic -o LINK mylink" + ))) + .unwrap_err(); + } + + #[test] + fn dladm_set_linkprop() { + // Valid usage + let DladmCommand::SetLinkprop { temporary, properties, name } = DladmCommand::try_from( + Input::shell(format!("{DLADM} set-linkprop -t -p mtu=123 mylink")) + ).unwrap() else { + panic!("Wrong command"); + }; + assert!(temporary); + assert_eq!( + properties, + HashMap::from([("mtu".to_string(), "123".to_string())]) + ); + assert_eq!(name.0, "mylink"); + + // Missing properties + DladmCommand::try_from(Input::shell(format!( + "{DLADM} set-linkprop mylink" + ))) + .unwrap_err(); + + // Bad property + DladmCommand::try_from(Input::shell(format!( + "{DLADM} set-linkprop -p bar mylink" + ))) + .unwrap_err(); + + // Missing link + DladmCommand::try_from(Input::shell(format!( + "{DLADM} set-linkprop -p foo=bar" + ))) + .unwrap_err(); + } +} diff --git a/illumos-utils/src/host/input.rs b/illumos-utils/src/host/input.rs index 721dc1df23a..6fa24050ba9 100644 --- a/illumos-utils/src/host/input.rs +++ b/illumos-utils/src/host/input.rs @@ -2,13 +2,14 @@ // 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/. +use std::collections::VecDeque; use std::process::Command; /// Wrapper around the input of a [std::process::Command] as strings. #[derive(Clone, Debug, Eq, PartialEq)] pub struct Input { pub program: String, - pub args: Vec, + pub args: VecDeque, pub envs: Vec<(String, String)>, } diff --git a/illumos-utils/src/host/mod.rs b/illumos-utils/src/host/mod.rs index 87c0ae8e6c1..9e7fe07b2ee 100644 --- a/illumos-utils/src/host/mod.rs +++ b/illumos-utils/src/host/mod.rs @@ -7,6 +7,7 @@ mod byte_queue; mod error; mod executor; +mod host; mod input; mod output; diff --git a/illumos-utils/src/lib.rs b/illumos-utils/src/lib.rs index 23cc379fef6..45a62f12512 100644 --- a/illumos-utils/src/lib.rs +++ b/illumos-utils/src/lib.rs @@ -21,3 +21,5 @@ pub mod vmm_reservoir; pub mod zfs; pub mod zone; pub mod zpool; + +pub const ROUTE: &'static str = "/usr/sbin/route"; diff --git a/illumos-utils/src/running_zone.rs b/illumos-utils/src/running_zone.rs index 790797333bd..2e34a256456 100644 --- a/illumos-utils/src/running_zone.rs +++ b/illumos-utils/src/running_zone.rs @@ -11,6 +11,7 @@ use crate::link::{Link, VnicAllocator}; use crate::opte::{Port, PortTicket}; use crate::svc::wait_for_service; use crate::zone::{AddressRequest, Zones, IPADM, ZONE_PREFIX}; +use crate::ROUTE; use camino::{Utf8Path, Utf8PathBuf}; use ipnetwork::IpNetwork; use omicron_common::backoff; @@ -680,13 +681,7 @@ impl RunningZone { "-ifp", port.vnic_name(), ])?; - self.run_cmd(&[ - "/usr/sbin/route", - "add", - "-inet", - "default", - &gateway_ip, - ])?; + self.run_cmd(&[ROUTE, "add", "-inet", "default", &gateway_ip])?; Ok(addr) } else { // If the port is using IPv6 addressing we still want it to use @@ -757,7 +752,7 @@ impl RunningZone { gateway: Ipv6Addr, ) -> Result<(), RunCommandError> { self.run_cmd([ - "/usr/sbin/route", + ROUTE, "add", "-inet6", "default", @@ -771,12 +766,7 @@ impl RunningZone { &self, gateway: Ipv4Addr, ) -> Result<(), RunCommandError> { - self.run_cmd([ - "/usr/sbin/route", - "add", - "default", - &gateway.to_string(), - ])?; + self.run_cmd([ROUTE, "add", "default", &gateway.to_string()])?; Ok(()) } @@ -787,7 +777,7 @@ impl RunningZone { zone_vnic_name: &str, ) -> Result<(), RunCommandError> { self.run_cmd([ - "/usr/sbin/route", + ROUTE, "add", "-inet6", &format!("{bootstrap_prefix:x}::/16"), From 51e3d14eacd0caae325624486e06f2478492761f Mon Sep 17 00:00:00 2001 From: Sean Klein Date: Wed, 16 Aug 2023 16:13:02 -0700 Subject: [PATCH 02/18] zonecfg, ipadm --- illumos-utils/src/addrobj.rs | 15 + illumos-utils/src/host/host.rs | 688 +++++++++++++++++++++++++++++++-- 2 files changed, 663 insertions(+), 40 deletions(-) diff --git a/illumos-utils/src/addrobj.rs b/illumos-utils/src/addrobj.rs index d63aa42bdf5..655d8c93ad3 100644 --- a/illumos-utils/src/addrobj.rs +++ b/illumos-utils/src/addrobj.rs @@ -4,6 +4,8 @@ //! API for operating on addrobj objects. +use std::str::FromStr; + /// The name provided to all link-local IPv6 addresses. pub const IPV6_LINK_LOCAL_NAME: &str = "ll"; @@ -26,6 +28,7 @@ pub struct AddrObject { enum BadName { Interface(String), Object(String), + Other(String), } impl std::fmt::Display for BadName { @@ -36,6 +39,7 @@ impl std::fmt::Display for BadName { match self { BadName::Interface(s) => write!(f, "Bad interface name: {}", s), BadName::Object(s) => write!(f, "Bad object name: {}", s), + BadName::Other(s) => write!(f, "Bad name: {}", s), } } } @@ -84,6 +88,17 @@ impl AddrObject { } } +impl FromStr for AddrObject { + type Err = ParseError; + + fn from_str(s: &str) -> Result { + let (interface, name) = s.split_once('/').ok_or_else(|| { + ParseError { name: BadName::Other(s.to_string()) } + })?; + Self::new(interface, name) + } +} + impl std::fmt::Display for AddrObject { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!(f, "{}/{}", self.interface, self.name) diff --git a/illumos-utils/src/host/host.rs b/illumos-utils/src/host/host.rs index cf8e753f066..b7fd2c6b749 100644 --- a/illumos-utils/src/host/host.rs +++ b/illumos-utils/src/host/host.rs @@ -4,9 +4,10 @@ //! Emulates an illumos system -// TODO TODO TODO REMOVE ME +// TODO REMOVE ME #![allow(dead_code)] +use crate::addrobj::AddrObject; use crate::dladm::DLADM; use crate::host::input::Input; use crate::host::PFEXEC; @@ -75,15 +76,16 @@ impl ZoneEnvironment { } } -// TODO: How much is it worth doing this vs just making things self-assembling? -// XXX E.g. is it actually worth emulating svccfg? - +#[derive(Debug)] struct ZoneName(String); + struct ZoneConfig { state: zone::State, brand: String, // zonepath path: Utf8PathBuf, + datasets: Vec, + devices: Vec, nets: Vec, fs: Vec, // E.g. zone image, overlays, etc. @@ -159,8 +161,7 @@ impl TryFrom for DladmCommand { return Err(format!("Not dladm command: {}", input.program)); } - match input.args.pop_front().ok_or_else(|| "Missing command")?.as_str() - { + match shift_arg(&mut input)?.as_str() { "create-vnic" => { let mut link = None; let mut temporary = false; @@ -172,23 +173,24 @@ impl TryFrom for DladmCommand { ); while !input.args.is_empty() { - if shift_if_eq(&mut input, "-t")? { + if shift_arg_if(&mut input, "-t")? { temporary = true; - } else if shift_if_eq(&mut input, "-p")? { + } else if shift_arg_if(&mut input, "-p")? { let props = shift_arg(&mut input)?; let props = props.split(','); for prop in props { - let (k, v) = prop - .split_once('=') - .ok_or_else(|| "Bad property")?; + let (k, v) = + prop.split_once('=').ok_or_else(|| { + format!("Bad property: {prop}") + })?; properties.insert(k.to_string(), v.to_string()); } - } else if shift_if_eq(&mut input, "-m")? { + } else if shift_arg_if(&mut input, "-m")? { // NOTE: Not yet supporting the keyword-based MACs. mac = Some(shift_arg(&mut input)?); - } else if shift_if_eq(&mut input, "-l")? { + } else if shift_arg_if(&mut input, "-l")? { link = Some(LinkName(shift_arg(&mut input)?)); - } else if shift_if_eq(&mut input, "-v")? { + } else if shift_arg_if(&mut input, "-v")? { vlan = Some( VlanID::from_str(&shift_arg(&mut input)?) .map_err(|e| e.to_string())?, @@ -213,7 +215,7 @@ impl TryFrom for DladmCommand { input.args.pop_back().ok_or_else(|| "Missing name")?, ); while !input.args.is_empty() { - if shift_if_eq(&mut input, "-t")? { + if shift_arg_if(&mut input, "-t")? { temporary = true; } else { return Err(format!("Invalid arguments {}", input)); @@ -227,7 +229,7 @@ impl TryFrom for DladmCommand { input.args.pop_back().ok_or_else(|| "Missing name")?, ); while !input.args.is_empty() { - if shift_if_eq(&mut input, "-t")? { + if shift_arg_if(&mut input, "-t")? { temporary = true; } else { return Err(format!("Invalid arguments {}", input)); @@ -241,7 +243,7 @@ impl TryFrom for DladmCommand { input.args.pop_back().ok_or_else(|| "Missing name")?, ); while !input.args.is_empty() { - if shift_if_eq(&mut input, "-t")? { + if shift_arg_if(&mut input, "-t")? { temporary = true; } else { return Err(format!("Invalid arguments {}", input)); @@ -258,17 +260,16 @@ impl TryFrom for DladmCommand { let name = LinkName( input.args.pop_back().ok_or_else(|| "Missing name")?, ); - if !shift_if_eq(&mut input, "-p")? { + if !shift_arg_if(&mut input, "-p")? { return Err( "You should ask for parseable output ('-p')".into() ); } - if !shift_if_eq(&mut input, "-o")? { + if !shift_arg_if(&mut input, "-o")? { return Err( "You should ask for specific outputs ('-o')".into() ); } - // TODO: Could parse an enum of known properties... let fields = shift_arg(&mut input)? .split(',') .map(|s| s.to_string()) @@ -279,20 +280,19 @@ impl TryFrom for DladmCommand { } "show-phys" => { let mut mac = false; - if shift_if_eq(&mut input, "-m")? { + if shift_arg_if(&mut input, "-m")? { mac = true; } - if !shift_if_eq(&mut input, "-p")? { + if !shift_arg_if(&mut input, "-p")? { return Err( "You should ask for parseable output ('-p')".into() ); } - if !shift_if_eq(&mut input, "-o")? { + if !shift_arg_if(&mut input, "-o")? { return Err( "You should ask for specific outputs ('-o')".into() ); } - // TODO: Could parse an enum of known properties... let fields = shift_arg(&mut input)? .split(',') .map(|s| s.to_string()) @@ -304,8 +304,8 @@ impl TryFrom for DladmCommand { } "show-vnic" => { let mut fields = None; - if shift_if_eq(&mut input, "-p")? { - if !shift_if_eq(&mut input, "-o")? { + if shift_arg_if(&mut input, "-p")? { + if !shift_arg_if(&mut input, "-o")? { return Err( "You should ask for specific outputs ('-o')".into(), ); @@ -330,15 +330,16 @@ impl TryFrom for DladmCommand { ); while !input.args.is_empty() { - if shift_if_eq(&mut input, "-t")? { + if shift_arg_if(&mut input, "-t")? { temporary = true; - } else if shift_if_eq(&mut input, "-p")? { + } else if shift_arg_if(&mut input, "-p")? { let props = shift_arg(&mut input)?; let props = props.split(','); for prop in props { - let (k, v) = prop - .split_once('=') - .ok_or_else(|| "Bad property")?; + let (k, v) = + prop.split_once('=').ok_or_else(|| { + format!("Bad property: {prop}") + })?; properties.insert(k.to_string(), v.to_string()); } } else { @@ -357,17 +358,156 @@ impl TryFrom for DladmCommand { } } +#[derive(Debug, PartialEq)] +enum AddrType { + Dhcp, + Static(IpNetwork), + Addrconf, +} + enum IpadmCommand { - CreateIf, - DeleteIf, - ShowIf, - SetIfprop, + CreateAddr { + temporary: bool, + ty: AddrType, + addrobj: AddrObject, + }, + CreateIf { + temporary: bool, + name: IpInterfaceName, + }, + DeleteAddr { + addrobj: AddrObject, + }, + DeleteIf { + name: IpInterfaceName, + }, + ShowIf { + properties: Vec, + name: IpInterfaceName, + }, + SetIfprop { + temporary: bool, + properties: HashMap, + module: String, + name: IpInterfaceName, + }, +} + +impl TryFrom for IpadmCommand { + type Error = String; + + fn try_from(mut input: Input) -> Result { + if input.program != IPADM { + return Err(format!("Not ipadm command: {}", input.program)); + } + + match shift_arg(&mut input)?.as_str() { + "create-addr" => { + let temporary = shift_arg_if(&mut input, "-t")?; + shift_arg_expect(&mut input, "-T")?; + + let ty = match shift_arg(&mut input)?.as_str() { + "static" => { + shift_arg_expect(&mut input, "-a")?; + let addr = shift_arg(&mut input)?; + AddrType::Static( + IpNetwork::from_str(&addr) + .map_err(|e| e.to_string())?, + ) + } + "dhcp" => AddrType::Dhcp, + "addrconf" => AddrType::Addrconf, + ty => return Err(format!("Unknown address type {ty}")), + }; + let addrobj = AddrObject::from_str(&shift_arg(&mut input)?) + .map_err(|e| e.to_string())?; + no_args_remaining(&input)?; + Ok(IpadmCommand::CreateAddr { temporary, ty, addrobj }) + } + "create-ip" | "create-if" => { + let temporary = shift_arg_if(&mut input, "-t")?; + let name = IpInterfaceName(shift_arg(&mut input)?); + no_args_remaining(&input)?; + Ok(IpadmCommand::CreateIf { temporary, name }) + } + "delete-addr" => { + let addrobj = AddrObject::from_str(&shift_arg(&mut input)?) + .map_err(|e| e.to_string())?; + no_args_remaining(&input)?; + Ok(IpadmCommand::DeleteAddr { addrobj }) + } + "delete-ip" | "delete-if" => { + let name = IpInterfaceName(shift_arg(&mut input)?); + no_args_remaining(&input)?; + Ok(IpadmCommand::DeleteIf { name }) + } + "show-if" => { + let name = IpInterfaceName( + input.args.pop_back().ok_or_else(|| "Missing name")?, + ); + let mut properties = vec![]; + while !input.args.is_empty() { + if shift_arg_if(&mut input, "-p")? { + shift_arg_expect(&mut input, "-o")?; + properties = shift_arg(&mut input)? + .split(',') + .map(|s| s.to_string()) + .collect(); + } else { + return Err(format!("Unexpected input: {input}")); + } + } + + Ok(IpadmCommand::ShowIf { properties, name }) + } + "set-ifprop" => { + let name = IpInterfaceName( + input.args.pop_back().ok_or_else(|| "Missing name")?, + ); + + let mut temporary = false; + let mut properties = HashMap::new(); + let mut module = "ip".to_string(); + + while !input.args.is_empty() { + if shift_arg_if(&mut input, "-t")? { + temporary = true; + } else if shift_arg_if(&mut input, "-m")? { + module = shift_arg(&mut input)?; + } else if shift_arg_if(&mut input, "-p")? { + let props = shift_arg(&mut input)?; + let props = props.split(','); + for prop in props { + let (k, v) = + prop.split_once('=').ok_or_else(|| { + format!("Bad property: {prop}") + })?; + properties.insert(k.to_string(), v.to_string()); + } + } else { + return Err(format!("Unexpected input: {input}")); + } + } + + Ok(IpadmCommand::SetIfprop { + temporary, + properties, + module, + name, + }) + } + command => return Err(format!("Unexpected command: {command}")), + } + } } enum RouteCommand { Add, } +// TODO: How much is it worth doing this vs just making things self-assembling? +// XXX E.g. is it actually worth emulating svccfg? + enum SvccfgCommand { Import, Refresh, @@ -381,7 +521,200 @@ enum ZoneadmCommand { } enum ZonecfgCommand { - Create, + Create { name: ZoneName, config: ZoneConfig }, + Delete { name: ZoneName }, +} + +impl TryFrom for ZonecfgCommand { + type Error = String; + + fn try_from(mut input: Input) -> Result { + if input.program != ZONECFG { + return Err(format!("Not zonecfg command: {}", input.program)); + } + shift_arg_expect(&mut input, "-z")?; + let zone = ZoneName(shift_arg(&mut input)?); + match shift_arg(&mut input)?.as_str() { + "create" => { + shift_arg_expect(&mut input, "-F")?; + shift_arg_expect(&mut input, "-b")?; + + enum Scope { + Global, + Dataset(zone::Dataset), + Device(zone::Device), + Fs(zone::Fs), + Net(zone::Net), + } + let mut scope = Scope::Global; + + // Globally-scoped Resources + let mut brand = None; + let mut path = None; + + // Non-Global Resources + let mut datasets = vec![]; + let mut devices = vec![]; + let mut nets = vec![]; + let mut fs = vec![]; + + while !input.args.is_empty() { + shift_arg_expect(&mut input, ";")?; + match shift_arg(&mut input)?.as_str() { + "set" => { + let prop = shift_arg(&mut input)?; + let (k, v) = + prop.split_once('=').ok_or_else(|| { + format!("Bad property: {prop}") + })?; + + match &mut scope { + Scope::Global => { + match k { + "brand" => { + brand = Some(v.to_string()); + } + "zonepath" => { + path = Some(Utf8PathBuf::from(v)); + } + "autoboot" => { + if v != "false" { + return Err(format!("Unhandled autoboot value: {v}")); + } + } + "ip-type" => { + if v != "exclusive" { + return Err(format!("Unhandled ip-type value: {v}")); + } + } + k => { + return Err(format!( + "Unknown property name: {k}" + )) + } + } + } + Scope::Dataset(d) => match k { + "name" => d.name = v.to_string(), + k => { + return Err(format!( + "Unknown property name: {k}" + )) + } + }, + Scope::Device(d) => match k { + "match" => d.name = v.to_string(), + k => { + return Err(format!( + "Unknown property name: {k}" + )) + } + }, + Scope::Fs(f) => match k { + "type" => f.ty = v.to_string(), + "dir" => f.dir = v.to_string(), + "special" => f.special = v.to_string(), + "raw" => f.raw = Some(v.to_string()), + "options" => { + f.options = v + .split(',') + .map(|s| s.to_string()) + .collect() + } + k => { + return Err(format!( + "Unknown property name: {k}" + )) + } + }, + Scope::Net(n) => match k { + "physical" => n.physical = v.to_string(), + "address" => { + n.address = Some(v.to_string()) + } + "allowed-address" => { + n.allowed_address = Some(v.to_string()) + } + k => { + return Err(format!( + "Unknown property name: {k}" + )) + } + }, + } + } + "add" => { + if !matches!(scope, Scope::Global) { + return Err("Cannot add from non-global scope" + .to_string()); + } + match shift_arg(&mut input)?.as_str() { + "dataset" => { + scope = + Scope::Dataset(zone::Dataset::default()) + } + "device" => { + scope = + Scope::Device(zone::Device::default()) + } + "fs" => scope = Scope::Fs(zone::Fs::default()), + "net" => { + scope = Scope::Net(zone::Net::default()) + } + scope => { + return Err(format!( + "Unexpected scope: {scope}" + )) + } + } + } + "end" => { + match scope { + Scope::Global => { + return Err( + "Cannot end global scope".to_string() + ) + } + Scope::Dataset(d) => datasets.push(d), + Scope::Device(d) => devices.push(d), + Scope::Fs(f) => fs.push(f), + Scope::Net(n) => nets.push(n), + } + scope = Scope::Global; + } + sc => { + return Err(format!("Unexpected subcommand: {sc}")) + } + } + } + + if !matches!(scope, Scope::Global) { + return Err( + "Cannot end zonecfg outside global scope".to_string() + ); + } + + Ok(ZonecfgCommand::Create { + name: zone, + config: ZoneConfig { + state: zone::State::Configured, + brand: brand.ok_or_else(|| "Missing brand")?, + path: path.ok_or_else(|| "Missing zonepath")?, + datasets, + devices, + nets, + fs, + layers: vec![], + }, + }) + } + "delete" => { + shift_arg_expect(&mut input, "-F")?; + Ok(ZonecfgCommand::Delete { name: zone }) + } + command => return Err(format!("Unexpected command: {command}")), + } + } } enum KnownCommand { @@ -418,11 +751,11 @@ impl TryFrom for Command { let cmd = match input.program.as_str() { DLADM => KnownCommand::Dladm(DladmCommand::try_from(input)?), - IPADM => todo!(), + IPADM => KnownCommand::Ipadm(IpadmCommand::try_from(input)?), ROUTE => todo!(), SVCCFG => todo!(), ZONEADM => todo!(), - ZONECFG => todo!(), + ZONECFG => KnownCommand::Zonecfg(ZonecfgCommand::try_from(input)?), _ => return Err(format!("Unknown command: {}", input.program)), }; @@ -456,10 +789,19 @@ fn shift_arg(input: &mut Input) -> Result { Ok(input.args.pop_front().ok_or_else(|| "Missing argument")?) } -// Removes the next argument if it equals value. +// Removes the next argument, which must equal the provided value. +fn shift_arg_expect(input: &mut Input, value: &str) -> Result<(), String> { + let v = input.args.pop_front().ok_or_else(|| "Not enough args")?; + if value != v { + return Err(format!("Unexpected argument {v} (expected: {value}")); + } + Ok(()) +} + +// Removes the next argument if it equals `value`. // // Returns if it was equal. -fn shift_if_eq(input: &mut Input, value: &str) -> Result { +fn shift_arg_if(input: &mut Input, value: &str) -> Result { let eq = input.args.front().ok_or_else(|| "Not enough args")? == value; if eq { input.args.pop_front(); @@ -757,4 +1099,270 @@ mod test { ))) .unwrap_err(); } + + #[test] + fn zonecfg_create() { + let ZonecfgCommand::Create { name, config } = ZonecfgCommand::try_from( + Input::shell(format!( + "{ZONECFG} -z myzone \ + create -F -b ; \ + set brand=omicron1 ; \ + set zonepath=/zone/myzone ; \ + set autoboot=false ; \ + set ip-type=exclusive ; \ + add net ; \ + set physical=oxControlService0 ; \ + end" + )), + ).unwrap() else { + panic!("Wrong command"); + }; + + assert_eq!(name.0, "myzone"); + assert_eq!(config.state, zone::State::Configured); + assert_eq!(config.brand, "omicron1"); + assert_eq!(config.path, Utf8PathBuf::from("/zone/myzone")); + assert!(config.datasets.is_empty()); + assert_eq!(config.nets[0].physical, "oxControlService0"); + assert!(config.fs.is_empty()); + assert!(config.layers.is_empty()); + + // Missing brand + assert!(ZonecfgCommand::try_from(Input::shell(format!( + "{ZONECFG} -z myzone \ + create -F -b ; \ + set zonepath=/zone/myzone" + )),) + .err() + .unwrap() + .contains("Missing brand")); + + // Missing zonepath + assert!(ZonecfgCommand::try_from(Input::shell(format!( + "{ZONECFG} -z myzone \ + create -F -b ; \ + set brand=omicron1" + )),) + .err() + .unwrap() + .contains("Missing zonepath")); + + // Ending mid-scope + assert!(ZonecfgCommand::try_from(Input::shell(format!( + "{ZONECFG} -z myzone \ + create -F -b ; \ + set brand=omicron1 ; \ + set zonepath=/zone/myzone ; \ + add net ; \ + set physical=oxControlService0" + )),) + .err() + .unwrap() + .contains("Cannot end zonecfg outside global scope")); + } + + #[test] + fn zonecfg_delete() { + let ZonecfgCommand::Delete { name } = ZonecfgCommand::try_from( + Input::shell(format!("{ZONECFG} -z myzone delete -F")), + ).unwrap() else { + panic!("Wrong command"); + }; + assert_eq!(name.0, "myzone"); + } + + #[test] + fn ipadm_create_addr() { + // Valid command + let IpadmCommand::CreateAddr { temporary, ty, addrobj } = IpadmCommand::try_from( + Input::shell(format!("{IPADM} create-addr -t -T addrconf foo/bar")) + ).unwrap() else { + panic!("Wrong command") + }; + assert!(temporary); + assert!(matches!(ty, AddrType::Addrconf)); + assert_eq!("foo/bar", addrobj.to_string()); + + // Valid command + let IpadmCommand::CreateAddr { temporary, ty, addrobj } = IpadmCommand::try_from( + Input::shell(format!("{IPADM} create-addr -T static -a ::/32 foo/bar")) + ).unwrap() else { + panic!("Wrong command") + }; + assert!(!temporary); + assert_eq!(ty, AddrType::Static(IpNetwork::from_str("::/32").unwrap())); + assert_eq!("foo/bar", addrobj.to_string()); + + // Bad type + IpadmCommand::try_from(Input::shell(format!( + "{IPADM} create-addr -T quadratric foo/bar" + ))) + .err() + .unwrap() + .contains("Unknown address type"); + + // Missing name + IpadmCommand::try_from(Input::shell(format!( + "{IPADM} create-addr -T dhcp" + ))) + .err() + .unwrap() + .contains("Missing argument"); + + // Too many arguments + IpadmCommand::try_from(Input::shell(format!( + "{IPADM} create-addr -T dhcp foo/bar baz" + ))) + .err() + .unwrap() + .contains("Unexpected extra arguments"); + + // Not addrobject + IpadmCommand::try_from(Input::shell(format!( + "{IPADM} create-addr -T dhcp foobar" + ))) + .err() + .unwrap() + .contains("Failed to parse addrobj name"); + } + + #[test] + fn ipadm_create_if() { + // Valid command + let IpadmCommand::CreateIf { temporary, name } = IpadmCommand::try_from( + Input::shell(format!("{IPADM} create-if foobar")) + ).unwrap() else { + panic!("Wrong command") + }; + assert!(!temporary); + assert_eq!(name.0, "foobar"); + + // Too many arguments + IpadmCommand::try_from(Input::shell(format!( + "{IPADM} create-if foo bar" + ))) + .err() + .unwrap() + .contains("Unexpected extra arguments"); + } + + #[test] + fn ipadm_delete_addr() { + // Valid command + let IpadmCommand::DeleteAddr { addrobj } = IpadmCommand::try_from( + Input::shell(format!("{IPADM} delete-addr foo/bar")) + ).unwrap() else { + panic!("Wrong command") + }; + assert_eq!(addrobj.to_string(), "foo/bar"); + + // Not addrobject + IpadmCommand::try_from(Input::shell(format!( + "{IPADM} delete-addr foobar" + ))) + .err() + .unwrap() + .contains("Failed to parse addobj name"); + + // Too many arguments + IpadmCommand::try_from(Input::shell(format!( + "{IPADM} delete-addr foo/bar foo/bar" + ))) + .err() + .unwrap() + .contains("Unexpected extra arguments"); + } + + #[test] + fn ipadm_delete_if() { + // Valid command + let IpadmCommand::DeleteIf { name } = IpadmCommand::try_from( + Input::shell(format!("{IPADM} delete-if foobar")) + ).unwrap() else { + panic!("Wrong command") + }; + assert_eq!(name.0, "foobar"); + + // Too many arguments + IpadmCommand::try_from(Input::shell(format!( + "{IPADM} delete-if foo bar" + ))) + .err() + .unwrap() + .contains("Unexpected extra arguments"); + } + + #[test] + fn ipadm_show_if() { + // Valid command + let IpadmCommand::ShowIf { properties, name } = IpadmCommand::try_from( + Input::shell(format!("{IPADM} show-if foobar")) + ).unwrap() else { + panic!("Wrong command") + }; + assert!(properties.is_empty()); + assert_eq!(name.0, "foobar"); + + // Valid command + let IpadmCommand::ShowIf { properties, name } = IpadmCommand::try_from( + Input::shell(format!("{IPADM} show-if -p -o IFNAME foobar")) + ).unwrap() else { + panic!("Wrong command") + }; + assert_eq!(properties[0], "IFNAME"); + assert_eq!(name.0, "foobar"); + + // Non parseable output + IpadmCommand::try_from(Input::shell(format!( + "{IPADM} show-if -o IFNAME foobar" + ))) + .err() + .unwrap(); + + // Not asking for specific field + IpadmCommand::try_from(Input::shell(format!( + "{IPADM} show-if -p foobar" + ))) + .err() + .unwrap(); + + // Too many arguments + IpadmCommand::try_from(Input::shell(format!( + "{IPADM} show-if fizz buzz" + ))) + .err() + .unwrap() + .contains("Unexpected input"); + } + + #[test] + fn ipadm_set_ifprop() { + // Valid command + let IpadmCommand::SetIfprop { temporary, properties, module, name } = IpadmCommand::try_from( + Input::shell(format!("{IPADM} set-ifprop -t -m ipv4 -p mtu=123 foo")) + ).unwrap() else { + panic!("Wrong command") + }; + + assert!(temporary); + assert_eq!(properties["mtu"], "123"); + assert_eq!(module, "ipv4"); + assert_eq!(name.0, "foo"); + + // Bad property + IpadmCommand::try_from(Input::shell(format!( + "{IPADM} set-ifprop -p blarg foo" + ))) + .err() + .unwrap() + .contains("Bad property: blarg"); + + // Too many arguments + IpadmCommand::try_from(Input::shell(format!( + "{IPADM} set-ifprop -p mtu=123 foo bar" + ))) + .err() + .unwrap() + .contains("Unexpected input"); + } } From 6f48ec427d6cda35a00b5ad861abe989941e9d65 Mon Sep 17 00:00:00 2001 From: Sean Klein Date: Wed, 16 Aug 2023 16:59:01 -0700 Subject: [PATCH 03/18] Add route --- illumos-utils/src/host/host.rs | 118 ++++++++++++++++++++++++++++++++- 1 file changed, 115 insertions(+), 3 deletions(-) diff --git a/illumos-utils/src/host/host.rs b/illumos-utils/src/host/host.rs index b7fd2c6b749..91f40d03255 100644 --- a/illumos-utils/src/host/host.rs +++ b/illumos-utils/src/host/host.rs @@ -353,7 +353,7 @@ impl TryFrom for DladmCommand { Ok(Self::SetLinkprop { temporary, properties, name }) } - _ => Err(format!("Unsupported command: {}", input.program)), + command => Err(format!("Unsupported command: {}", command)), } } } @@ -501,8 +501,75 @@ impl TryFrom for IpadmCommand { } } +#[derive(Debug, PartialEq, Eq)] +enum RouteTarget { + Default, + DefaultV4, + DefaultV6, + ByAddress(IpNetwork), +} + +impl RouteTarget { + fn shift_target(input: &mut Input) -> Result { + let force_v4 = shift_arg_if(input, "-inet")?; + let force_v6 = shift_arg_if(input, "-inet6")?; + + let target = match (force_v4, force_v6, shift_arg(input)?.as_str()) { + (true, true, _) => { + return Err("Cannot force both v4 and v6".to_string()) + } + (true, false, "default") => RouteTarget::DefaultV4, + (false, true, "default") => RouteTarget::DefaultV6, + (false, false, "default") => RouteTarget::Default, + (_, _, other) => { + let net = + IpNetwork::from_str(other).map_err(|e| e.to_string())?; + if force_v4 && !net.is_ipv4() { + return Err(format!("{net} is not ipv4")); + } + if force_v6 && !net.is_ipv6() { + return Err(format!("{net} is not ipv6")); + } + RouteTarget::ByAddress(net) + } + }; + Ok(target) + } +} + enum RouteCommand { - Add, + Add { + destination: RouteTarget, + gateway: RouteTarget, + interface: Option, + }, +} + +impl TryFrom for RouteCommand { + type Error = String; + + fn try_from(mut input: Input) -> Result { + if input.program != ROUTE { + return Err(format!("Not route command: {}", input.program)); + } + + match shift_arg(&mut input)?.as_str() { + "add" => { + let destination = RouteTarget::shift_target(&mut input)?; + let gateway = RouteTarget::shift_target(&mut input)?; + + let interface = + if let Ok(true) = shift_arg_if(&mut input, "-ifp") { + Some(LinkName(shift_arg(&mut input)?)) + } else { + None + }; + no_args_remaining(&input)?; + Ok(RouteCommand::Add { destination, gateway, interface }) + } + command => return Err(format!("Unsupported command: {}", command)), + } + } } // TODO: How much is it worth doing this vs just making things self-assembling? @@ -752,7 +819,7 @@ impl TryFrom for Command { let cmd = match input.program.as_str() { DLADM => KnownCommand::Dladm(DladmCommand::try_from(input)?), IPADM => KnownCommand::Ipadm(IpadmCommand::try_from(input)?), - ROUTE => todo!(), + ROUTE => KnownCommand::Route(RouteCommand::try_from(input)?), SVCCFG => todo!(), ZONEADM => todo!(), ZONECFG => KnownCommand::Zonecfg(ZonecfgCommand::try_from(input)?), @@ -1171,6 +1238,51 @@ mod test { assert_eq!(name.0, "myzone"); } + #[test] + fn route_add() { + // Valid command + let RouteCommand::Add { destination, gateway, interface } = + RouteCommand::try_from(Input::shell(format!( + "{ROUTE} add -inet6 fd00::/16 default -ifp mylink" + ))) + .unwrap(); + assert_eq!( + destination, + RouteTarget::ByAddress(IpNetwork::from_str("fd00::/16").unwrap()) + ); + assert_eq!(gateway, RouteTarget::Default); + assert_eq!(interface.unwrap().0, "mylink"); + + // Valid command + let RouteCommand::Add { destination, gateway, interface } = + RouteCommand::try_from(Input::shell(format!( + "{ROUTE} add -inet default 127.0.0.1/8" + ))) + .unwrap(); + assert_eq!(destination, RouteTarget::DefaultV4); + assert_eq!( + gateway, + RouteTarget::ByAddress(IpNetwork::from_str("127.0.0.1/8").unwrap()) + ); + assert!(interface.is_none()); + + // Invalid address family + RouteCommand::try_from(Input::shell(format!( + "{ROUTE} add -inet -inet6 default 127.0.0.1/8" + ))) + .err() + .unwrap() + .contains("Cannot force both v4 and v6"); + + // Invalid address family + RouteCommand::try_from(Input::shell(format!( + "{ROUTE} add -ine6 default -inet6 127.0.0.1/8" + ))) + .err() + .unwrap() + .contains("127.0.0.1/8 is not ipv6"); + } + #[test] fn ipadm_create_addr() { // Valid command From 8a73de19b304fbc530acac9266c769bfbb4a3ef0 Mon Sep 17 00:00:00 2001 From: Sean Klein Date: Wed, 16 Aug 2023 17:45:33 -0700 Subject: [PATCH 04/18] Skeleton of more commands --- illumos-utils/src/host/host.rs | 172 +++++++++++++++++++++++++++++++-- 1 file changed, 162 insertions(+), 10 deletions(-) diff --git a/illumos-utils/src/host/host.rs b/illumos-utils/src/host/host.rs index 91f40d03255..6a7c4d1765e 100644 --- a/illumos-utils/src/host/host.rs +++ b/illumos-utils/src/host/host.rs @@ -11,16 +11,20 @@ use crate::addrobj::AddrObject; use crate::dladm::DLADM; use crate::host::input::Input; use crate::host::PFEXEC; +use crate::zfs::ZFS; use crate::zone::IPADM; +use crate::zone::SVCADM; use crate::zone::SVCCFG; use crate::zone::ZLOGIN; use crate::zone::ZONEADM; use crate::zone::ZONECFG; +use crate::zpool::ZPOOL; +use crate::zpool::ZpoolName; use crate::ROUTE; use camino::Utf8PathBuf; use ipnetwork::IpNetwork; use omicron_common::vlan::VlanID; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::str::FromStr; enum LinkType { @@ -100,11 +104,21 @@ struct Zone { struct Host { global: ZoneEnvironment, zones: HashMap, + + // TODO: Is this the right abstraction layer? + // How do you want to represent zpools & filesystems? + // + // TODO: Should filesystems be part of the "ZoneEnvironment" abstraction? + zpools: HashSet, } impl Host { pub fn new() -> Self { - Self { global: ZoneEnvironment::new(0), zones: HashMap::new() } + Self { + global: ZoneEnvironment::new(0), + zones: HashMap::new(), + zpools: HashSet::new(), + } } } @@ -572,13 +586,114 @@ impl TryFrom for RouteCommand { } } -// TODO: How much is it worth doing this vs just making things self-assembling? -// XXX E.g. is it actually worth emulating svccfg? - enum SvccfgCommand { - Import, - Refresh, - Setprop, + Addpropvalue { + zone: Option, + service: ServiceName, + key: smf::PropertyName, + value: smf::PropertyValue, + }, + Addpg { + zone: Option, + service: ServiceName, + group: smf::PropertyGroupName, + group_type: String, + }, + Delpg { + zone: Option, + service: ServiceName, + group: smf::PropertyName, + value: String, + }, + Delpropvalue { + zone: Option, + service: ServiceName, + group: smf::PropertyName, + value: String, + }, + Import { + zone: Option, + file: Utf8PathBuf, + }, + Refresh { + zone: Option, + service: ServiceName, + }, + Setprop { + zone: Option, + service: ServiceName, + key: smf::PropertyName, + value: smf::PropertyValue, + }, +} + +impl TryFrom for SvccfgCommand { + type Error = String; + + fn try_from(mut input: Input) -> Result { + if input.program != SVCCFG { + return Err(format!("Not svccfg command: {}", input.program)); + } + + let zone = if shift_arg_if(&mut input, "-z")? { + Some(ZoneName(shift_arg(&mut input)?)) + } else { + None + }; + + let fmri = if shift_arg_if(&mut input, "-s")? { + Some(ServiceName(shift_arg(&mut input)?)) + } else { + None + }; + + match shift_arg(&mut input)?.as_str() { + "addpropvalue" => todo!(), + "addpg" => todo!(), + "delpg" => todo!(), + "delpropvalue" => todo!(), + "import" => todo!(), + "refresh" => todo!(), + "setprop" => todo!(), + command => return Err(format!("Unexpected command: {command}")), + } + } +} + +enum SvcadmCommand { + Enable { zone: Option, service: ServiceName }, + Disable { zone: Option, service: ServiceName }, +} + +impl TryFrom for SvcadmCommand { + type Error = String; + + fn try_from(mut input: Input) -> Result { + if input.program != SVCADM { + return Err(format!("Not svcadm command: {}", input.program)); + } + todo!(); + } +} + +enum ZfsCommand { + Create, + Destroy, + Get, + List, + Mount, + Set, +} + +impl TryFrom for ZfsCommand { + type Error = String; + + fn try_from(mut input: Input) -> Result { + if input.program != ZFS { + return Err(format!("Not zfs command: {}", input.program)); + } + todo!(); + } } enum ZoneadmCommand { @@ -587,6 +702,17 @@ enum ZoneadmCommand { Boot, } +impl TryFrom for ZoneadmCommand { + type Error = String; + + fn try_from(mut input: Input) -> Result { + if input.program != ZONEADM { + return Err(format!("Not zoneadm command: {}", input.program)); + } + todo!(); + } +} + enum ZonecfgCommand { Create { name: ZoneName, config: ZoneConfig }, Delete { name: ZoneName }, @@ -784,14 +910,37 @@ impl TryFrom for ZonecfgCommand { } } +enum ZpoolCommand { + Create, + Export, + Import, + List, + Set, +} + +impl TryFrom for ZpoolCommand { + type Error = String; + + fn try_from(mut input: Input) -> Result { + if input.program != ZPOOL { + return Err(format!("Not zpool command: {}", input.program)); + } + + todo!(); + } +} + enum KnownCommand { Dladm(DladmCommand), Ipadm(IpadmCommand), RouteAdm, Route(RouteCommand), Svccfg(SvccfgCommand), + Svcadm(SvcadmCommand), + Zfs(ZfsCommand), Zoneadm(ZoneadmCommand), Zonecfg(ZonecfgCommand), + Zpool(ZpoolCommand), } struct Command { @@ -820,9 +969,12 @@ impl TryFrom for Command { DLADM => KnownCommand::Dladm(DladmCommand::try_from(input)?), IPADM => KnownCommand::Ipadm(IpadmCommand::try_from(input)?), ROUTE => KnownCommand::Route(RouteCommand::try_from(input)?), - SVCCFG => todo!(), - ZONEADM => todo!(), + SVCCFG => KnownCommand::Svccfg(SvccfgCommand::try_from(input)?), + SVCADM => KnownCommand::Svcadm(SvcadmCommand::try_from(input)?), + ZFS => KnownCommand::Zfs(ZfsCommand::try_from(input)?), + ZONEADM => KnownCommand::Zoneadm(ZoneadmCommand::try_from(input)?), ZONECFG => KnownCommand::Zonecfg(ZonecfgCommand::try_from(input)?), + ZPOOL => KnownCommand::Zpool(ZpoolCommand::try_from(input)?), _ => return Err(format!("Unknown command: {}", input.program)), }; From 57a353a3b80cc8847da8dd33b392417e91ab4e30 Mon Sep 17 00:00:00 2001 From: Sean Klein Date: Tue, 22 Aug 2023 09:25:28 -0700 Subject: [PATCH 05/18] Add a few more parsed commands --- helios/tokamak/src/host.rs | 984 +++++++++++++++++++++++++++--- illumos-utils/src/opte/illumos.rs | 4 +- illumos-utils/src/zfs.rs | 10 +- 3 files changed, 925 insertions(+), 73 deletions(-) diff --git a/helios/tokamak/src/host.rs b/helios/tokamak/src/host.rs index 4d7897cd04b..fc588bcf068 100644 --- a/helios/tokamak/src/host.rs +++ b/helios/tokamak/src/host.rs @@ -48,6 +48,7 @@ struct Route { gateway: IpNetwork, } +#[derive(Debug)] struct ServiceName(String); struct Service { @@ -271,7 +272,7 @@ impl TryFrom for DladmCommand { ); if !shift_arg_if(&mut input, "-p")? { return Err( - "You should ask for parseable output ('-p')".into() + "You should ask for parsable output ('-p')".into() ); } if !shift_arg_if(&mut input, "-o")? { @@ -294,7 +295,7 @@ impl TryFrom for DladmCommand { } if !shift_arg_if(&mut input, "-p")? { return Err( - "You should ask for parseable output ('-p')".into() + "You should ask for parsable output ('-p')".into() ); } if !shift_arg_if(&mut input, "-o")? { @@ -584,27 +585,27 @@ impl TryFrom for RouteCommand { enum SvccfgCommand { Addpropvalue { zone: Option, - service: ServiceName, + fmri: ServiceName, key: smf::PropertyName, - value: smf::PropertyValue, + ty: Option, + value: String, }, Addpg { zone: Option, - service: ServiceName, + fmri: ServiceName, group: smf::PropertyGroupName, group_type: String, }, Delpg { zone: Option, - service: ServiceName, - group: smf::PropertyName, - value: String, + fmri: ServiceName, + group: smf::PropertyGroupName, }, Delpropvalue { zone: Option, - service: ServiceName, - group: smf::PropertyName, - value: String, + fmri: ServiceName, + name: smf::PropertyName, + glob: String, }, Import { zone: Option, @@ -612,13 +613,13 @@ enum SvccfgCommand { }, Refresh { zone: Option, - service: ServiceName, + fmri: ServiceName, }, Setprop { zone: Option, - service: ServiceName, - key: smf::PropertyName, - value: smf::PropertyValue, + fmri: ServiceName, + name: smf::PropertyName, + value: String, }, } @@ -643,13 +644,118 @@ impl TryFrom for SvccfgCommand { }; match shift_arg(&mut input)?.as_str() { - "addpropvalue" => todo!(), - "addpg" => todo!(), - "delpg" => todo!(), - "delpropvalue" => todo!(), - "import" => todo!(), - "refresh" => todo!(), - "setprop" => todo!(), + "addpropvalue" => { + let name = shift_arg(&mut input)?; + let name = smf::PropertyName::from_str(&name) + .map_err(|e| e.to_string())?; + + let type_or_value = shift_arg(&mut input)?; + let (ty, value) = match input.args.pop_front() { + Some(value) => { + let ty = type_or_value + .strip_suffix(':') + .ok_or_else(|| { + format!("Bad property type: {type_or_value}") + })? + .to_string(); + (Some(ty), value) + } + None => (None, type_or_value), + }; + + let fmri = fmri.ok_or_else(|| { + format!("-s option required for addpropvalue") + })?; + + no_args_remaining(&input)?; + Ok(SvccfgCommand::Addpropvalue { + zone, + fmri, + key: name, + ty, + value, + }) + } + "addpg" => { + let name = shift_arg(&mut input)?; + let group = smf::PropertyGroupName::new(&name) + .map_err(|e| e.to_string())?; + + let group_type = shift_arg(&mut input)?; + if let Some(flags) = input.args.pop_front() { + return Err( + "Parsing of optional flags not implemented".to_string() + ); + } + let fmri = fmri + .ok_or_else(|| format!("-s option required for addpg"))?; + + no_args_remaining(&input)?; + Ok(SvccfgCommand::Addpg { zone, fmri, group, group_type }) + } + "delpg" => { + let name = shift_arg(&mut input)?; + let group = smf::PropertyGroupName::new(&name) + .map_err(|e| e.to_string())?; + let fmri = fmri + .ok_or_else(|| format!("-s option required for delpg"))?; + + no_args_remaining(&input)?; + Ok(SvccfgCommand::Delpg { zone, fmri, group }) + } + "delpropvalue" => { + let name = shift_arg(&mut input)?; + let name = smf::PropertyName::from_str(&name) + .map_err(|e| e.to_string())?; + let fmri = fmri.ok_or_else(|| { + format!("-s option required for delpropvalue") + })?; + let glob = shift_arg(&mut input)?; + + no_args_remaining(&input)?; + Ok(SvccfgCommand::Delpropvalue { zone, fmri, name, glob }) + } + "import" => { + let file = shift_arg(&mut input)?; + if let Some(_) = fmri { + return Err( + "Cannot use '-s' option with import".to_string() + ); + } + no_args_remaining(&input)?; + Ok(SvccfgCommand::Import { zone, file: file.into() }) + } + "refresh" => { + let fmri = fmri + .ok_or_else(|| format!("-s option required for refresh"))?; + no_args_remaining(&input)?; + Ok(SvccfgCommand::Refresh { zone, fmri }) + } + "setprop" => { + let fmri = fmri + .ok_or_else(|| format!("-s option required for setprop"))?; + + // Setprop seems fine accepting args of the form: + // - name=value + // - name = value + // - name = type: value (NOTE: not yet supported) + let first_arg = shift_arg(&mut input)?; + let (name, value) = + if let Some((name, value)) = first_arg.split_once('=') { + (name.to_string(), value.to_string()) + } else { + let name = first_arg; + shift_arg_expect(&mut input, "=")?; + let value = shift_arg(&mut input)?; + (name, value.to_string()) + }; + + let name = smf::PropertyName::from_str(&name) + .map_err(|e| e.to_string())?; + + no_args_remaining(&input)?; + Ok(SvccfgCommand::Setprop { zone, fmri, name, value }) + } command => return Err(format!("Unexpected command: {command}")), } } @@ -667,17 +773,75 @@ impl TryFrom for SvcadmCommand { if input.program != SVCADM { return Err(format!("Not svcadm command: {}", input.program)); } - todo!(); + + let zone = if shift_arg_if(&mut input, "-z")? { + Some(ZoneName(shift_arg(&mut input)?)) + } else { + None + }; + + match shift_arg(&mut input)?.as_str() { + "enable" => { + // Intentionally ignored + shift_arg_if(&mut input, "-t")?; + let service = ServiceName(shift_arg(&mut input)?); + no_args_remaining(&input)?; + Ok(SvcadmCommand::Enable { zone, service }) + } + "disable" => { + // Intentionally ignored + shift_arg_if(&mut input, "-t")?; + let service = ServiceName(shift_arg(&mut input)?); + no_args_remaining(&input)?; + Ok(SvcadmCommand::Disable { zone, service }) + } + command => return Err(format!("Unexpected command: {command}")), + } } } +struct FilesystemName(String); + enum ZfsCommand { - Create, - Destroy, - Get, - List, - Mount, - Set, + CreateFilesystem { + properties: Vec<(String, String)>, + name: FilesystemName, + }, + CreateVolume { + properties: Vec<(String, String)>, + sparse: bool, + blocksize: Option, + size: u64, + name: FilesystemName, + }, + Destroy { + recursive_dependents: bool, + recursive_children: bool, + force_unmount: bool, + name: FilesystemName, + }, + Get { + recursive: bool, + depth: Option, + // name, property, value, source + fields: Vec, + properties: Vec, + datasets: Option>, + }, + List { + recursive: bool, + depth: Option, + properties: Vec, + datasets: Option>, + }, + Mount { + load_keys: bool, + filesystem: FilesystemName, + }, + Set { + properties: Vec<(String, String)>, + name: FilesystemName, + }, } impl TryFrom for ZfsCommand { @@ -687,7 +851,271 @@ impl TryFrom for ZfsCommand { if input.program != ZFS { return Err(format!("Not zfs command: {}", input.program)); } - todo!(); + + match shift_arg(&mut input)?.as_str() { + "create" => { + let mut size = None; + let mut blocksize = None; + let mut sparse = None; + let mut properties = vec![]; + + while input.args.len() > 1 { + // Volume Size (volumes only, required) + if shift_arg_if(&mut input, "-V")? { + size = Some( + shift_arg(&mut input)? + .parse::() + .map_err(|e| e.to_string())?, + ); + // Sparse (volumes only, optional) + } else if shift_arg_if(&mut input, "-s")? { + sparse = Some(true); + // Block size (volumes only, optional) + } else if shift_arg_if(&mut input, "-b")? { + blocksize = Some( + shift_arg(&mut input)? + .parse::() + .map_err(|e| e.to_string())?, + ); + // Properties + } else if shift_arg_if(&mut input, "-o")? { + let prop = shift_arg(&mut input)?; + let (k, v) = prop + .split_once('=') + .ok_or_else(|| format!("Bad property: {prop}"))?; + properties.push((k.to_string(), v.to_string())); + } + } + let name = FilesystemName(shift_arg(&mut input)?); + no_args_remaining(&input)?; + + if let Some(size) = size { + // Volume + let sparse = sparse.unwrap_or(false); + Ok(ZfsCommand::CreateVolume { + properties, + sparse, + blocksize, + size, + name, + }) + } else { + // Filesystem + if sparse.is_some() || blocksize.is_some() { + return Err("Using volume arguments, but forgot to specify '-V size'?".to_string()); + } + Ok(ZfsCommand::CreateFilesystem { properties, name }) + } + } + "destroy" => { + let mut recursive_dependents = false; + let mut recursive_children = false; + let mut force_unmount = false; + let mut name = None; + + while !input.args.is_empty() { + let arg = shift_arg(&mut input)?; + let mut chars = arg.chars(); + if let Some('-') = chars.next() { + while let Some(c) = chars.next() { + match c { + 'R' => recursive_dependents = true, + 'r' => recursive_children = true, + 'f' => force_unmount = true, + c => { + return Err(format!( + "Unrecognized option '-{c}'" + )) + } + } + } + } else { + name = Some(FilesystemName(arg)); + no_args_remaining(&input)?; + } + } + let name = name.ok_or_else(|| "Missing name".to_string())?; + Ok(ZfsCommand::Destroy { + recursive_dependents, + recursive_children, + force_unmount, + name, + }) + } + "get" => { + let mut scripting = false; + let mut parsable = false; + let mut recursive = false; + let mut depth = None; + let mut fields = ["name", "property", "value", "source"] + .map(String::from) + .to_vec(); + let mut properties = vec![]; + + while !input.args.is_empty() { + let arg = shift_arg(&mut input)?; + let mut chars = arg.chars(); + // ZFS list lets callers pass in flags in groups, or + // separately. + if let Some('-') = chars.next() { + while let Some(c) = chars.next() { + match c { + 'r' => recursive = true, + 'H' => scripting = true, + 'p' => parsable = true, + 'd' => { + let depth_raw = + if chars.clone().next().is_some() { + chars.collect::() + } else { + shift_arg(&mut input)? + }; + depth = Some( + depth_raw + .parse::() + .map_err(|e| e.to_string())?, + ); + // Convince the compiler we won't use any + // more 'chars', because used them all + // parsing 'depth'. + break; + } + 'o' => { + if chars.next().is_some() { + return Err("-o should be immediately followed by fields".to_string()); + } + fields = shift_arg(&mut input)? + .split(',') + .map(|s| s.to_string()) + .collect(); + } + c => { + return Err(format!( + "Unrecognized option '-{c}'" + )) + } + } + } + } else { + properties = + arg.split(',').map(|s| s.to_string()).collect(); + break; + } + } + + let datasets = Some( + std::mem::take(&mut input.args) + .into_iter() + .collect::>(), + ); + if !scripting || !parsable { + return Err("You should run 'zfs get' commands with the '-Hp' flags enabled".to_string()); + } + + Ok(ZfsCommand::Get { + recursive, + depth, + fields, + properties, + datasets, + }) + } + "list" => { + let mut scripting = false; + let mut parsable = false; + let mut recursive = false; + let mut depth = None; + let mut properties = vec![]; + let mut datasets = None; + + while !input.args.is_empty() { + let arg = shift_arg(&mut input)?; + let mut chars = arg.chars(); + // ZFS list lets callers pass in flags in groups, or + // separately. + if let Some('-') = chars.next() { + while let Some(c) = chars.next() { + match c { + 'r' => recursive = true, + 'H' => scripting = true, + 'p' => parsable = true, + 'd' => { + let depth_raw = + if chars.clone().next().is_some() { + chars.collect::() + } else { + shift_arg(&mut input)? + }; + depth = Some( + depth_raw + .parse::() + .map_err(|e| e.to_string())?, + ); + // Convince the compiler we won't use any + // more 'chars', because used them all + // parsing 'depth'. + break; + } + 'o' => { + if chars.next().is_some() { + return Err("-o should be immediately followed by properties".to_string()); + } + properties = shift_arg(&mut input)? + .split(',') + .map(|s| s.to_string()) + .collect(); + } + c => { + return Err(format!( + "Unrecognized option '-{c}'" + )) + } + } + } + } else { + // As soon as non-flag arguments are passed, the rest of + // the arguments are treated as datasets. + datasets = Some(vec![arg]); + break; + } + } + + let remaining_datasets = std::mem::take(&mut input.args); + if !remaining_datasets.is_empty() { + datasets + .get_or_insert(vec![]) + .extend(remaining_datasets.into_iter()); + }; + + if !scripting || !parsable { + return Err("You should run 'zfs list' commands with the '-Hp' flags enabled".to_string()); + } + + Ok(ZfsCommand::List { recursive, depth, properties, datasets }) + } + "mount" => { + let load_keys = shift_arg_if(&mut input, "-l")?; + let filesystem = FilesystemName(shift_arg(&mut input)?); + no_args_remaining(&input)?; + Ok(ZfsCommand::Mount { load_keys, filesystem }) + } + "set" => { + let mut properties = vec![]; + + while input.args.len() > 1 { + let prop = shift_arg(&mut input)?; + let (k, v) = prop + .split_once('=') + .ok_or_else(|| format!("Bad property: {prop}"))?; + properties.push((k.to_string(), v.to_string())); + } + let name = FilesystemName(shift_arg(&mut input)?); + no_args_remaining(&input)?; + + Ok(ZfsCommand::Set { properties, name }) + } + command => return Err(format!("Unexpected command: {command}")), + } } } @@ -906,11 +1334,11 @@ impl TryFrom for ZonecfgCommand { } enum ZpoolCommand { - Create, - Export, - Import, - List, - Set, + Create { pool: String, vdev: String }, + Export { pool: String }, + Import { force: bool, pool: String }, + List { properties: Vec, pools: Option> }, + Set { property: String, value: String, pool: String }, } impl TryFrom for ZpoolCommand { @@ -921,13 +1349,93 @@ impl TryFrom for ZpoolCommand { return Err(format!("Not zpool command: {}", input.program)); } - todo!(); + match shift_arg(&mut input)?.as_str() { + "create" => { + let pool = shift_arg(&mut input)?; + let vdev = shift_arg(&mut input)?; + no_args_remaining(&input)?; + Ok(ZpoolCommand::Create { pool, vdev }) + } + "export" => { + let pool = shift_arg(&mut input)?; + no_args_remaining(&input)?; + Ok(ZpoolCommand::Export { pool }) + } + "import" => { + let force = shift_arg_if(&mut input, "-f")?; + let pool = shift_arg(&mut input)?; + Ok(ZpoolCommand::Import { force, pool }) + } + "list" => { + let mut scripting = false; + let mut parsable = false; + let mut properties = vec![]; + let mut pools = None; + + while !input.args.is_empty() { + let arg = shift_arg(&mut input)?; + let mut chars = arg.chars(); + // ZFS list lets callers pass in flags in groups, or + // separately. + if let Some('-') = chars.next() { + while let Some(c) = chars.next() { + match c { + 'H' => scripting = true, + 'p' => parsable = true, + 'o' => { + if chars.next().is_some() { + return Err("-o should be immediately followed by properties".to_string()); + } + properties = shift_arg(&mut input)? + .split(',') + .map(|s| s.to_string()) + .collect(); + } + c => { + return Err(format!( + "Unrecognized option '-{c}'" + )) + } + } + } + } else { + pools = Some(vec![arg]); + break; + } + } + + let remaining_pools = std::mem::take(&mut input.args); + if !remaining_pools.is_empty() { + pools + .get_or_insert(vec![]) + .extend(remaining_pools.into_iter()); + }; + if !scripting || !parsable { + return Err("You should run 'zpool list' commands with the '-Hp' flags enabled".to_string()); + } + Ok(ZpoolCommand::List { properties, pools }) + } + "set" => { + let prop = shift_arg(&mut input)?; + let (k, v) = prop + .split_once('=') + .ok_or_else(|| format!("Bad property: {prop}"))?; + let property = k.to_string(); + let value = v.to_string(); + + let pool = shift_arg(&mut input)?; + no_args_remaining(&input)?; + Ok(ZpoolCommand::Set { property, value, pool }) + } + command => return Err(format!("Unexpected command: {command}")), + } } } enum KnownCommand { Dladm(DladmCommand), Ipadm(IpadmCommand), + Fstyp, RouteAdm, Route(RouteCommand), Svccfg(SvccfgCommand), @@ -1005,7 +1513,7 @@ fn shift_arg(input: &mut Input) -> Result { // Removes the next argument, which must equal the provided value. fn shift_arg_expect(input: &mut Input, value: &str) -> Result<(), String> { - let v = input.args.pop_front().ok_or_else(|| "Not enough args")?; + let v = input.args.pop_front().ok_or_else(|| "Missing argument")?; if value != v { return Err(format!("Unexpected argument {v} (expected: {value}")); } @@ -1016,7 +1524,7 @@ fn shift_arg_expect(input: &mut Input, value: &str) -> Result<(), String> { // // Returns if it was equal. fn shift_arg_if(input: &mut Input, value: &str) -> Result { - let eq = input.args.front().ok_or_else(|| "Not enough args")? == value; + let eq = input.args.front().ok_or_else(|| "Missing argument")? == value; if eq { input.args.pop_front(); } @@ -1221,7 +1729,7 @@ mod test { ))) .unwrap_err(); - // Not asking for parseable output + // Not asking for parsable output DladmCommand::try_from(Input::shell(format!( "{DLADM} show-link -o LINK mylink" ))) @@ -1246,7 +1754,7 @@ mod test { ))) .unwrap_err(); - // Not asking for parseable output + // Not asking for parsable output DladmCommand::try_from(Input::shell(format!( "{DLADM} show-phys -o LINK mylink" ))) @@ -1273,7 +1781,7 @@ mod test { assert!(fields.is_none()); assert_eq!(name.unwrap().0, "mylink"); - // Not asking for parseable output + // Not asking for parsable output DladmCommand::try_from(Input::shell(format!( "{DLADM} show-vnic -o LINK mylink" ))) @@ -1314,6 +1822,228 @@ mod test { .unwrap_err(); } + #[test] + fn svccfg_addpropvalue() { + let SvccfgCommand::Addpropvalue { zone, fmri, key, ty, value } = SvccfgCommand::try_from( + Input::shell(format!( + "{SVCCFG} -z myzone -s svc:/myservice:default addpropvalue foo/bar astring: baz" + )) + ).unwrap() else { + panic!("Wrong command"); + }; + + assert_eq!(zone.unwrap().0, "myzone"); + assert_eq!(fmri.0, "svc:/myservice:default"); + assert_eq!(key.to_string(), "foo/bar"); + assert_eq!(ty, Some("astring".to_string())); + assert_eq!(value, "baz"); + + assert!(SvccfgCommand::try_from(Input::shell(format!( + "{SVCCFG} addpropvalue foo/bar baz" + ))) + .err() + .unwrap() + .contains("-s option required")); + + assert!(SvccfgCommand::try_from(Input::shell(format!( + "{SVCCFG} -s svc:/mysvc addpropvalue foo/bar astring baz" + ))) + .err() + .unwrap() + .contains("Bad property type")); + } + + #[test] + fn svccfg_addpg() { + let SvccfgCommand::Addpg { zone, fmri, group, group_type } = SvccfgCommand::try_from( + Input::shell(format!( + "{SVCCFG} -z myzone -s svc:/myservice:default addpg foo baz" + )) + ).unwrap() else { + panic!("Wrong command"); + }; + + assert_eq!(zone.unwrap().0, "myzone"); + assert_eq!(fmri.0, "svc:/myservice:default"); + assert_eq!(group.to_string(), "foo"); + assert_eq!(group_type, "baz"); + + assert!(SvccfgCommand::try_from(Input::shell(format!( + "{SVCCFG} addpg foo baz" + ))) + .err() + .unwrap() + .contains("-s option required")); + + assert!(SvccfgCommand::try_from(Input::shell(format!( + "{SVCCFG} addpg foo baz P" + ))) + .err() + .unwrap() + .contains("Parsing of optional flags not implemented")); + } + + #[test] + fn svccfg_delpg() { + let SvccfgCommand::Delpg { zone, fmri, group } = SvccfgCommand::try_from( + Input::shell(format!( + "{SVCCFG} -z myzone -s svc:/myservice:default delpg foo" + )) + ).unwrap() else { + panic!("Wrong command"); + }; + + assert_eq!(zone.unwrap().0, "myzone"); + assert_eq!(fmri.0, "svc:/myservice:default"); + assert_eq!(group.to_string(), "foo"); + + assert!(SvccfgCommand::try_from(Input::shell(format!( + "{SVCCFG} delpg foo" + ))) + .err() + .unwrap() + .contains("-s option required")); + + assert!(SvccfgCommand::try_from(Input::shell(format!( + "{SVCCFG} -s mysvc delpg foo baz" + ))) + .err() + .unwrap() + .contains("Unexpected extra arguments")); + } + + #[test] + fn svccfg_import() { + let SvccfgCommand::Import { zone, file } = SvccfgCommand::try_from( + Input::shell(format!( + "{SVCCFG} -z myzone import myfile" + )) + ).unwrap() else { + panic!("Wrong command"); + }; + + assert_eq!(zone.unwrap().0, "myzone"); + assert_eq!(file, "myfile"); + + assert!(SvccfgCommand::try_from(Input::shell(format!( + "{SVCCFG} import myfile myotherfile" + ))) + .err() + .unwrap() + .contains("Unexpected extra arguments")); + + assert!(SvccfgCommand::try_from(Input::shell(format!( + "{SVCCFG} -s myservice import myfile" + ))) + .err() + .unwrap() + .contains("Cannot use '-s' option with import")); + } + + #[test] + fn svccfg_refresh() { + let SvccfgCommand::Refresh { zone, fmri } = SvccfgCommand::try_from( + Input::shell(format!( + "{SVCCFG} -z myzone -s myservice refresh" + )) + ).unwrap() else { + panic!("Wrong command"); + }; + + assert_eq!(zone.unwrap().0, "myzone"); + assert_eq!(fmri.0, "myservice"); + } + + #[test] + fn svccfg_setprop() { + let SvccfgCommand::Setprop { zone, fmri, name, value } = SvccfgCommand::try_from( + Input::shell(format!( + "{SVCCFG} -z myzone -s myservice setprop foo/bar=baz" + )) + ).unwrap() else { + panic!("Wrong command"); + }; + + assert_eq!(zone.unwrap().0, "myzone"); + assert_eq!(fmri.0, "myservice"); + assert_eq!(name.to_string(), "foo/bar"); + assert_eq!(value, "baz"); + + // Try that command again, but with spaces + let SvccfgCommand::Setprop { zone, fmri, name, value } = SvccfgCommand::try_from( + Input::shell(format!( + "{SVCCFG} -z myzone -s myservice setprop foo/bar = baz" + )) + ).unwrap() else { + panic!("Wrong command"); + }; + assert_eq!(zone.unwrap().0, "myzone"); + assert_eq!(fmri.0, "myservice"); + assert_eq!(name.to_string(), "foo/bar"); + assert_eq!(value, "baz"); + + // Try that command again, but with quotes + let SvccfgCommand::Setprop { zone, fmri, name, value } = SvccfgCommand::try_from( + Input::shell(format!( + "{SVCCFG} -z myzone -s myservice setprop foo/bar = \"fizz buzz\"" + )) + ).unwrap() else { + panic!("Wrong command"); + }; + assert_eq!(zone.unwrap().0, "myzone"); + assert_eq!(fmri.0, "myservice"); + assert_eq!(name.to_string(), "foo/bar"); + assert_eq!(value, "fizz buzz"); + + assert!(SvccfgCommand::try_from( + Input::shell(format!( + "{SVCCFG} -z myzone -s myservice setprop foo/bar = \"fizz buzz\" blat" + )) + ).err().unwrap().contains("Unexpected extra arguments")); + } + + #[test] + fn svcadm_enable() { + let SvcadmCommand::Enable { zone, service } = SvcadmCommand::try_from( + Input::shell(format!( + "{SVCADM} -z myzone enable -t foobar" + )), + ).unwrap() else { + panic!("wrong command"); + }; + + assert_eq!(zone.unwrap().0, "myzone"); + assert_eq!(service.0, "foobar"); + + assert!(SvcadmCommand::try_from(Input::shell(format!( + "{SVCADM} enable" + ))) + .err() + .unwrap() + .contains("Missing argument")); + } + + #[test] + fn svcadm_disable() { + let SvcadmCommand::Disable { zone, service } = SvcadmCommand::try_from( + Input::shell(format!( + "{SVCADM} -z myzone disable -t foobar" + )), + ).unwrap() else { + panic!("wrong command"); + }; + + assert_eq!(zone.unwrap().0, "myzone"); + assert_eq!(service.0, "foobar"); + + assert!(SvcadmCommand::try_from(Input::shell(format!( + "{SVCADM} disable" + ))) + .err() + .unwrap() + .contains("Missing argument")); + } + #[test] fn zonecfg_create() { let ZonecfgCommand::Create { name, config } = ZonecfgCommand::try_from( @@ -1414,20 +2144,20 @@ mod test { assert!(interface.is_none()); // Invalid address family - RouteCommand::try_from(Input::shell(format!( + assert!(RouteCommand::try_from(Input::shell(format!( "{ROUTE} add -inet -inet6 default 127.0.0.1/8" ))) .err() .unwrap() - .contains("Cannot force both v4 and v6"); + .contains("Cannot force both v4 and v6")); // Invalid address family - RouteCommand::try_from(Input::shell(format!( - "{ROUTE} add -ine6 default -inet6 127.0.0.1/8" + assert!(RouteCommand::try_from(Input::shell(format!( + "{ROUTE} add -inet6 default -inet6 127.0.0.1/8" ))) .err() .unwrap() - .contains("127.0.0.1/8 is not ipv6"); + .contains("127.0.0.1/8 is not ipv6")); } #[test] @@ -1453,36 +2183,36 @@ mod test { assert_eq!("foo/bar", addrobj.to_string()); // Bad type - IpadmCommand::try_from(Input::shell(format!( + assert!(IpadmCommand::try_from(Input::shell(format!( "{IPADM} create-addr -T quadratric foo/bar" ))) .err() .unwrap() - .contains("Unknown address type"); + .contains("Unknown address type")); // Missing name - IpadmCommand::try_from(Input::shell(format!( + assert!(IpadmCommand::try_from(Input::shell(format!( "{IPADM} create-addr -T dhcp" ))) .err() .unwrap() - .contains("Missing argument"); + .contains("Missing argument")); // Too many arguments - IpadmCommand::try_from(Input::shell(format!( + assert!(IpadmCommand::try_from(Input::shell(format!( "{IPADM} create-addr -T dhcp foo/bar baz" ))) .err() .unwrap() - .contains("Unexpected extra arguments"); + .contains("Unexpected extra arguments")); // Not addrobject - IpadmCommand::try_from(Input::shell(format!( + assert!(IpadmCommand::try_from(Input::shell(format!( "{IPADM} create-addr -T dhcp foobar" ))) .err() .unwrap() - .contains("Failed to parse addrobj name"); + .contains("Failed to parse addrobj name")); } #[test] @@ -1497,12 +2227,12 @@ mod test { assert_eq!(name.0, "foobar"); // Too many arguments - IpadmCommand::try_from(Input::shell(format!( + assert!(IpadmCommand::try_from(Input::shell(format!( "{IPADM} create-if foo bar" ))) .err() .unwrap() - .contains("Unexpected extra arguments"); + .contains("Unexpected extra arguments")); } #[test] @@ -1516,20 +2246,20 @@ mod test { assert_eq!(addrobj.to_string(), "foo/bar"); // Not addrobject - IpadmCommand::try_from(Input::shell(format!( + assert!(IpadmCommand::try_from(Input::shell(format!( "{IPADM} delete-addr foobar" ))) .err() .unwrap() - .contains("Failed to parse addobj name"); + .contains("Failed to parse addrobj name")); // Too many arguments - IpadmCommand::try_from(Input::shell(format!( + assert!(IpadmCommand::try_from(Input::shell(format!( "{IPADM} delete-addr foo/bar foo/bar" ))) .err() .unwrap() - .contains("Unexpected extra arguments"); + .contains("Unexpected extra arguments")); } #[test] @@ -1543,12 +2273,12 @@ mod test { assert_eq!(name.0, "foobar"); // Too many arguments - IpadmCommand::try_from(Input::shell(format!( + assert!(IpadmCommand::try_from(Input::shell(format!( "{IPADM} delete-if foo bar" ))) .err() .unwrap() - .contains("Unexpected extra arguments"); + .contains("Unexpected extra arguments")); } #[test] @@ -1571,7 +2301,7 @@ mod test { assert_eq!(properties[0], "IFNAME"); assert_eq!(name.0, "foobar"); - // Non parseable output + // Non parsable output IpadmCommand::try_from(Input::shell(format!( "{IPADM} show-if -o IFNAME foobar" ))) @@ -1586,12 +2316,12 @@ mod test { .unwrap(); // Too many arguments - IpadmCommand::try_from(Input::shell(format!( + assert!(IpadmCommand::try_from(Input::shell(format!( "{IPADM} show-if fizz buzz" ))) .err() .unwrap() - .contains("Unexpected input"); + .contains("Unexpected input")); } #[test] @@ -1609,19 +2339,135 @@ mod test { assert_eq!(name.0, "foo"); // Bad property - IpadmCommand::try_from(Input::shell(format!( + assert!(IpadmCommand::try_from(Input::shell(format!( "{IPADM} set-ifprop -p blarg foo" ))) .err() .unwrap() - .contains("Bad property: blarg"); + .contains("Bad property: blarg")); // Too many arguments - IpadmCommand::try_from(Input::shell(format!( + assert!(IpadmCommand::try_from(Input::shell(format!( "{IPADM} set-ifprop -p mtu=123 foo bar" ))) .err() .unwrap() - .contains("Unexpected input"); + .contains("Unexpected input")); + } + + #[test] + fn zfs_create() { + let ZfsCommand::CreateFilesystem { properties, name } = ZfsCommand::try_from( + Input::shell(format!("{ZFS} create myfilesystem")) + ).unwrap() else { panic!("wrong command") }; + + assert_eq!(properties, vec![]); + assert_eq!(name.0, "myfilesystem"); + + let ZfsCommand::CreateVolume { properties, sparse, blocksize, size, name } = ZfsCommand::try_from( + Input::shell(format!("{ZFS} create -s -V 1024 -b 512 -o foo=bar myvolume")) + ).unwrap() else { panic!("wrong command") }; + + assert_eq!(properties, vec![("foo".to_string(), "bar".to_string())]); + assert_eq!(name.0, "myvolume"); + assert!(sparse); + assert_eq!(size, 1024); + assert_eq!(blocksize, Some(512)); + + assert!(ZfsCommand::try_from(Input::shell(format!( + "{ZFS} create -s -b 512 -o foo=bar myvolume" + ))) + .err() + .unwrap() + .contains("Using volume arguments, but forgot to specify '-V size'")); + } + + #[test] + fn zfs_destroy() { + let ZfsCommand::Destroy { recursive_dependents, recursive_children, force_unmount, name } = + ZfsCommand::try_from( + Input::shell(format!("{ZFS} destroy -rf foobar")) + ).unwrap() else { panic!("wrong command") }; + + assert!(!recursive_dependents); + assert!(recursive_children); + assert!(force_unmount); + assert_eq!(name.0, "foobar"); + + assert!(ZfsCommand::try_from(Input::shell(format!( + "{ZFS} destroy -x doit" + ))) + .err() + .unwrap() + .contains("Unrecognized option '-x'")); + } + + #[test] + fn zfs_get() { + let ZfsCommand::Get { recursive, depth, fields, properties, datasets } = ZfsCommand::try_from( + Input::shell(format!("{ZFS} get -Hrpd10 -o name,value mounted,available myvolume")) + ).unwrap() else { panic!("wrong command") }; + + assert!(recursive); + assert_eq!(depth, Some(10)); + assert_eq!(fields, vec!["name", "value"]); + assert_eq!(properties, vec!["mounted", "available"]); + assert_eq!(datasets.unwrap(), vec!["myvolume"]); + + assert!(ZfsCommand::try_from(Input::shell(format!( + "{ZFS} get -o name,value mounted,available myvolume" + ))) + .err() + .unwrap() + .contains( + "You should run 'zfs get' commands with the '-Hp' flags enabled" + )); + } + + #[test] + fn zfs_list() { + let ZfsCommand::List { recursive, depth, properties, datasets } = ZfsCommand::try_from( + Input::shell(format!("{ZFS} list -d 1 -rHpo name myfilesystem")) + ).unwrap() else { panic!("wrong command") }; + + assert!(recursive); + assert_eq!(depth.unwrap(), 1); + assert_eq!(properties, vec!["name"]); + assert_eq!(datasets.unwrap(), vec!["myfilesystem"]); + + assert!(ZfsCommand::try_from(Input::shell(format!( + "{ZFS} list name myfilesystem" + ))) + .err() + .unwrap() + .contains( + "You should run 'zfs list' commands with the '-Hp' flags enabled" + )); + } + + #[test] + fn zfs_mount() { + let ZfsCommand::Mount { load_keys, filesystem } = ZfsCommand::try_from( + Input::shell(format!("{ZFS} mount -l foobar")) + ).unwrap() else { panic!("wrong command") }; + + assert!(load_keys); + assert_eq!(filesystem.0, "foobar"); + } + + #[test] + fn zfs_set() { + let ZfsCommand::Set { properties, name } = ZfsCommand::try_from( + Input::shell(format!("{ZFS} set foo=bar baz=blat myfs")) + ).unwrap() else { panic!("wrong command") }; + + assert_eq!( + properties, + vec![ + ("foo".to_string(), "bar".to_string()), + ("baz".to_string(), "blat".to_string()) + ] + ); + assert_eq!(name.0, "myfs"); } } diff --git a/illumos-utils/src/opte/illumos.rs b/illumos-utils/src/opte/illumos.rs index 88e8d343b14..98fd2ae1999 100644 --- a/illumos-utils/src/opte/illumos.rs +++ b/illumos-utils/src/opte/illumos.rs @@ -4,9 +4,9 @@ //! Interactions with the Oxide Packet Transformation Engine (OPTE) -use crate::addrobj::AddrObject; use crate::dladm; use camino::Utf8Path; +use helios_fusion::addrobj::AddrObject; use omicron_common::api::internal::shared::NetworkInterfaceKind; use opte_ioctl::OpteHdl; use slog::info; @@ -33,7 +33,7 @@ pub enum Error { IncompatibleKernel, #[error(transparent)] - BadAddrObj(#[from] crate::addrobj::ParseError), + BadAddrObj(#[from] helios_fusion::addrobj::ParseError), #[error(transparent)] SetLinkpropError(#[from] crate::dladm::SetLinkpropError), diff --git a/illumos-utils/src/zfs.rs b/illumos-utils/src/zfs.rs index 56e0ed5af6d..6779ffab91b 100644 --- a/illumos-utils/src/zfs.rs +++ b/illumos-utils/src/zfs.rs @@ -428,8 +428,14 @@ impl Zfs { name: &str, ) -> Result { let mut command = std::process::Command::new(PFEXEC); - let cmd = - command.args(&[ZFS, "get", "-Ho", "value", &name, filesystem_name]); + let cmd = command.args(&[ + ZFS, + "get", + "-Hpo", + "value", + &name, + filesystem_name, + ]); let output = executor.execute(cmd).map_err(|err| GetValueError { filesystem: filesystem_name.to_string(), name: name.to_string(), From 9eef84bf8b344a5657221a285f74e01e6dad4a88 Mon Sep 17 00:00:00 2001 From: Sean Klein Date: Tue, 22 Aug 2023 12:41:27 -0700 Subject: [PATCH 06/18] Split into many files --- helios/tokamak/src/host.rs | 2473 ---------------------------- helios/tokamak/src/host/dladm.rs | 530 ++++++ helios/tokamak/src/host/ipadm.rs | 344 ++++ helios/tokamak/src/host/mod.rs | 298 ++++ helios/tokamak/src/host/route.rs | 96 ++ helios/tokamak/src/host/svcadm.rs | 91 + helios/tokamak/src/host/svccfg.rs | 369 +++++ helios/tokamak/src/host/zfs.rs | 447 +++++ helios/tokamak/src/host/zoneadm.rs | 118 ++ helios/tokamak/src/host/zonecfg.rs | 283 ++++ helios/tokamak/src/host/zpool.rs | 109 ++ 11 files changed, 2685 insertions(+), 2473 deletions(-) delete mode 100644 helios/tokamak/src/host.rs create mode 100644 helios/tokamak/src/host/dladm.rs create mode 100644 helios/tokamak/src/host/ipadm.rs create mode 100644 helios/tokamak/src/host/mod.rs create mode 100644 helios/tokamak/src/host/route.rs create mode 100644 helios/tokamak/src/host/svcadm.rs create mode 100644 helios/tokamak/src/host/svccfg.rs create mode 100644 helios/tokamak/src/host/zfs.rs create mode 100644 helios/tokamak/src/host/zoneadm.rs create mode 100644 helios/tokamak/src/host/zonecfg.rs create mode 100644 helios/tokamak/src/host/zpool.rs diff --git a/helios/tokamak/src/host.rs b/helios/tokamak/src/host.rs deleted file mode 100644 index fc588bcf068..00000000000 --- a/helios/tokamak/src/host.rs +++ /dev/null @@ -1,2473 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// 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/. - -//! Emulates an illumos system - -// TODO REMOVE ME -#![allow(dead_code)] -#![allow(unused_mut)] -#![allow(unused_variables)] - -use camino::Utf8PathBuf; -use helios_fusion::addrobj::AddrObject; -use helios_fusion::zpool::ZpoolName; -use helios_fusion::Input; -use helios_fusion::{ - DLADM, IPADM, PFEXEC, ROUTE, SVCADM, SVCCFG, ZFS, ZLOGIN, ZONEADM, ZONECFG, - ZPOOL, -}; -use ipnetwork::IpNetwork; -use omicron_common::vlan::VlanID; -use std::collections::{HashMap, HashSet}; -use std::str::FromStr; - -enum LinkType { - Etherstub, - Vnic, -} - -#[derive(Debug, PartialEq, Eq)] -struct LinkName(String); -struct Link { - ty: LinkType, - parent: Option, - properties: HashMap, -} - -struct IpInterfaceName(String); -struct IpInterface {} - -enum RouteDestination { - Default, - Literal(IpNetwork), -} - -struct Route { - destination: RouteDestination, - gateway: IpNetwork, -} - -#[derive(Debug)] -struct ServiceName(String); - -struct Service { - state: smf::SmfState, - properties: HashMap, -} - -struct ZoneEnvironment { - id: u64, - links: HashMap, - ip_interfaces: HashMap, - routes: Vec, - services: HashMap, -} - -impl ZoneEnvironment { - fn new(id: u64) -> Self { - Self { - id, - links: HashMap::new(), - ip_interfaces: HashMap::new(), - routes: vec![], - services: HashMap::new(), - } - } -} - -#[derive(Debug)] -struct ZoneName(String); - -struct ZoneConfig { - state: zone::State, - brand: String, - // zonepath - path: Utf8PathBuf, - datasets: Vec, - devices: Vec, - nets: Vec, - fs: Vec, - // E.g. zone image, overlays, etc. - layers: Vec, -} - -struct Zone { - config: ZoneConfig, - environment: ZoneEnvironment, -} - -struct Host { - global: ZoneEnvironment, - zones: HashMap, - - // TODO: Is this the right abstraction layer? - // How do you want to represent zpools & filesystems? - // - // TODO: Should filesystems be part of the "ZoneEnvironment" abstraction? - zpools: HashSet, -} - -impl Host { - pub fn new() -> Self { - Self { - global: ZoneEnvironment::new(0), - zones: HashMap::new(), - zpools: HashSet::new(), - } - } -} - -#[derive(Debug)] -enum DladmCommand { - CreateVnic { - link: LinkName, - temporary: bool, - mac: Option, - vlan: Option, - name: LinkName, - properties: HashMap, - }, - CreateEtherstub { - temporary: bool, - name: LinkName, - }, - DeleteEtherstub { - temporary: bool, - name: LinkName, - }, - DeleteVnic { - temporary: bool, - name: LinkName, - }, - ShowEtherstub { - name: Option, - }, - ShowLink { - name: LinkName, - fields: Vec, - }, - ShowPhys { - mac: bool, - fields: Vec, - name: Option, - }, - ShowVnic { - fields: Option>, - name: Option, - }, - SetLinkprop { - temporary: bool, - properties: HashMap, - name: LinkName, - }, -} - -impl TryFrom for DladmCommand { - type Error = String; - - fn try_from(mut input: Input) -> Result { - if input.program != DLADM { - return Err(format!("Not dladm command: {}", input.program)); - } - - match shift_arg(&mut input)?.as_str() { - "create-vnic" => { - let mut link = None; - let mut temporary = false; - let mut mac = None; - let mut vlan = None; - let mut properties = HashMap::new(); - let name = LinkName( - input.args.pop_back().ok_or_else(|| "Missing name")?, - ); - - while !input.args.is_empty() { - if shift_arg_if(&mut input, "-t")? { - temporary = true; - } else if shift_arg_if(&mut input, "-p")? { - let props = shift_arg(&mut input)?; - let props = props.split(','); - for prop in props { - let (k, v) = - prop.split_once('=').ok_or_else(|| { - format!("Bad property: {prop}") - })?; - properties.insert(k.to_string(), v.to_string()); - } - } else if shift_arg_if(&mut input, "-m")? { - // NOTE: Not yet supporting the keyword-based MACs. - mac = Some(shift_arg(&mut input)?); - } else if shift_arg_if(&mut input, "-l")? { - link = Some(LinkName(shift_arg(&mut input)?)); - } else if shift_arg_if(&mut input, "-v")? { - vlan = Some( - VlanID::from_str(&shift_arg(&mut input)?) - .map_err(|e| e.to_string())?, - ); - } else { - return Err(format!("Invalid arguments {}", input)); - } - } - - Ok(Self::CreateVnic { - link: link.ok_or_else(|| "Missing link")?, - temporary, - mac, - vlan, - name, - properties, - }) - } - "create-etherstub" => { - let mut temporary = false; - let name = LinkName( - input.args.pop_back().ok_or_else(|| "Missing name")?, - ); - while !input.args.is_empty() { - if shift_arg_if(&mut input, "-t")? { - temporary = true; - } else { - return Err(format!("Invalid arguments {}", input)); - } - } - Ok(Self::CreateEtherstub { temporary, name }) - } - "delete-etherstub" => { - let mut temporary = false; - let name = LinkName( - input.args.pop_back().ok_or_else(|| "Missing name")?, - ); - while !input.args.is_empty() { - if shift_arg_if(&mut input, "-t")? { - temporary = true; - } else { - return Err(format!("Invalid arguments {}", input)); - } - } - Ok(Self::DeleteEtherstub { temporary, name }) - } - "delete-vnic" => { - let mut temporary = false; - let name = LinkName( - input.args.pop_back().ok_or_else(|| "Missing name")?, - ); - while !input.args.is_empty() { - if shift_arg_if(&mut input, "-t")? { - temporary = true; - } else { - return Err(format!("Invalid arguments {}", input)); - } - } - Ok(Self::DeleteVnic { temporary, name }) - } - "show-etherstub" => { - let name = input.args.pop_back().map(|s| LinkName(s)); - no_args_remaining(&input)?; - Ok(Self::ShowEtherstub { name }) - } - "show-link" => { - let name = LinkName( - input.args.pop_back().ok_or_else(|| "Missing name")?, - ); - if !shift_arg_if(&mut input, "-p")? { - return Err( - "You should ask for parsable output ('-p')".into() - ); - } - if !shift_arg_if(&mut input, "-o")? { - return Err( - "You should ask for specific outputs ('-o')".into() - ); - } - let fields = shift_arg(&mut input)? - .split(',') - .map(|s| s.to_string()) - .collect(); - no_args_remaining(&input)?; - - Ok(Self::ShowLink { name, fields }) - } - "show-phys" => { - let mut mac = false; - if shift_arg_if(&mut input, "-m")? { - mac = true; - } - if !shift_arg_if(&mut input, "-p")? { - return Err( - "You should ask for parsable output ('-p')".into() - ); - } - if !shift_arg_if(&mut input, "-o")? { - return Err( - "You should ask for specific outputs ('-o')".into() - ); - } - let fields = shift_arg(&mut input)? - .split(',') - .map(|s| s.to_string()) - .collect(); - let name = input.args.pop_front().map(|s| LinkName(s)); - no_args_remaining(&input)?; - - Ok(Self::ShowPhys { mac, fields, name }) - } - "show-vnic" => { - let mut fields = None; - if shift_arg_if(&mut input, "-p")? { - if !shift_arg_if(&mut input, "-o")? { - return Err( - "You should ask for specific outputs ('-o')".into(), - ); - } - fields = Some( - shift_arg(&mut input)? - .split(',') - .map(|s| s.to_string()) - .collect(), - ); - } - - let name = input.args.pop_front().map(|s| LinkName(s)); - no_args_remaining(&input)?; - Ok(Self::ShowVnic { fields, name }) - } - "set-linkprop" => { - let mut temporary = false; - let mut properties = HashMap::new(); - let name = LinkName( - input.args.pop_back().ok_or_else(|| "Missing name")?, - ); - - while !input.args.is_empty() { - if shift_arg_if(&mut input, "-t")? { - temporary = true; - } else if shift_arg_if(&mut input, "-p")? { - let props = shift_arg(&mut input)?; - let props = props.split(','); - for prop in props { - let (k, v) = - prop.split_once('=').ok_or_else(|| { - format!("Bad property: {prop}") - })?; - properties.insert(k.to_string(), v.to_string()); - } - } else { - return Err(format!("Invalid arguments {}", input)); - } - } - - if properties.is_empty() { - return Err("Missing properties".into()); - } - - Ok(Self::SetLinkprop { temporary, properties, name }) - } - command => Err(format!("Unsupported command: {}", command)), - } - } -} - -#[derive(Debug, PartialEq)] -enum AddrType { - Dhcp, - Static(IpNetwork), - Addrconf, -} - -enum IpadmCommand { - CreateAddr { - temporary: bool, - ty: AddrType, - addrobj: AddrObject, - }, - CreateIf { - temporary: bool, - name: IpInterfaceName, - }, - DeleteAddr { - addrobj: AddrObject, - }, - DeleteIf { - name: IpInterfaceName, - }, - ShowIf { - properties: Vec, - name: IpInterfaceName, - }, - SetIfprop { - temporary: bool, - properties: HashMap, - module: String, - name: IpInterfaceName, - }, -} - -impl TryFrom for IpadmCommand { - type Error = String; - - fn try_from(mut input: Input) -> Result { - if input.program != IPADM { - return Err(format!("Not ipadm command: {}", input.program)); - } - - match shift_arg(&mut input)?.as_str() { - "create-addr" => { - let temporary = shift_arg_if(&mut input, "-t")?; - shift_arg_expect(&mut input, "-T")?; - - let ty = match shift_arg(&mut input)?.as_str() { - "static" => { - shift_arg_expect(&mut input, "-a")?; - let addr = shift_arg(&mut input)?; - AddrType::Static( - IpNetwork::from_str(&addr) - .map_err(|e| e.to_string())?, - ) - } - "dhcp" => AddrType::Dhcp, - "addrconf" => AddrType::Addrconf, - ty => return Err(format!("Unknown address type {ty}")), - }; - let addrobj = AddrObject::from_str(&shift_arg(&mut input)?) - .map_err(|e| e.to_string())?; - no_args_remaining(&input)?; - Ok(IpadmCommand::CreateAddr { temporary, ty, addrobj }) - } - "create-ip" | "create-if" => { - let temporary = shift_arg_if(&mut input, "-t")?; - let name = IpInterfaceName(shift_arg(&mut input)?); - no_args_remaining(&input)?; - Ok(IpadmCommand::CreateIf { temporary, name }) - } - "delete-addr" => { - let addrobj = AddrObject::from_str(&shift_arg(&mut input)?) - .map_err(|e| e.to_string())?; - no_args_remaining(&input)?; - Ok(IpadmCommand::DeleteAddr { addrobj }) - } - "delete-ip" | "delete-if" => { - let name = IpInterfaceName(shift_arg(&mut input)?); - no_args_remaining(&input)?; - Ok(IpadmCommand::DeleteIf { name }) - } - "show-if" => { - let name = IpInterfaceName( - input.args.pop_back().ok_or_else(|| "Missing name")?, - ); - let mut properties = vec![]; - while !input.args.is_empty() { - if shift_arg_if(&mut input, "-p")? { - shift_arg_expect(&mut input, "-o")?; - properties = shift_arg(&mut input)? - .split(',') - .map(|s| s.to_string()) - .collect(); - } else { - return Err(format!("Unexpected input: {input}")); - } - } - - Ok(IpadmCommand::ShowIf { properties, name }) - } - "set-ifprop" => { - let name = IpInterfaceName( - input.args.pop_back().ok_or_else(|| "Missing name")?, - ); - - let mut temporary = false; - let mut properties = HashMap::new(); - let mut module = "ip".to_string(); - - while !input.args.is_empty() { - if shift_arg_if(&mut input, "-t")? { - temporary = true; - } else if shift_arg_if(&mut input, "-m")? { - module = shift_arg(&mut input)?; - } else if shift_arg_if(&mut input, "-p")? { - let props = shift_arg(&mut input)?; - let props = props.split(','); - for prop in props { - let (k, v) = - prop.split_once('=').ok_or_else(|| { - format!("Bad property: {prop}") - })?; - properties.insert(k.to_string(), v.to_string()); - } - } else { - return Err(format!("Unexpected input: {input}")); - } - } - - Ok(IpadmCommand::SetIfprop { - temporary, - properties, - module, - name, - }) - } - command => return Err(format!("Unexpected command: {command}")), - } - } -} - -#[derive(Debug, PartialEq, Eq)] -enum RouteTarget { - Default, - DefaultV4, - DefaultV6, - ByAddress(IpNetwork), -} - -impl RouteTarget { - fn shift_target(input: &mut Input) -> Result { - let force_v4 = shift_arg_if(input, "-inet")?; - let force_v6 = shift_arg_if(input, "-inet6")?; - - let target = match (force_v4, force_v6, shift_arg(input)?.as_str()) { - (true, true, _) => { - return Err("Cannot force both v4 and v6".to_string()) - } - (true, false, "default") => RouteTarget::DefaultV4, - (false, true, "default") => RouteTarget::DefaultV6, - (false, false, "default") => RouteTarget::Default, - (_, _, other) => { - let net = - IpNetwork::from_str(other).map_err(|e| e.to_string())?; - if force_v4 && !net.is_ipv4() { - return Err(format!("{net} is not ipv4")); - } - if force_v6 && !net.is_ipv6() { - return Err(format!("{net} is not ipv6")); - } - RouteTarget::ByAddress(net) - } - }; - Ok(target) - } -} - -enum RouteCommand { - Add { - destination: RouteTarget, - gateway: RouteTarget, - interface: Option, - }, -} - -impl TryFrom for RouteCommand { - type Error = String; - - fn try_from(mut input: Input) -> Result { - if input.program != ROUTE { - return Err(format!("Not route command: {}", input.program)); - } - - match shift_arg(&mut input)?.as_str() { - "add" => { - let destination = RouteTarget::shift_target(&mut input)?; - let gateway = RouteTarget::shift_target(&mut input)?; - - let interface = - if let Ok(true) = shift_arg_if(&mut input, "-ifp") { - Some(LinkName(shift_arg(&mut input)?)) - } else { - None - }; - no_args_remaining(&input)?; - Ok(RouteCommand::Add { destination, gateway, interface }) - } - command => return Err(format!("Unsupported command: {}", command)), - } - } -} - -enum SvccfgCommand { - Addpropvalue { - zone: Option, - fmri: ServiceName, - key: smf::PropertyName, - ty: Option, - value: String, - }, - Addpg { - zone: Option, - fmri: ServiceName, - group: smf::PropertyGroupName, - group_type: String, - }, - Delpg { - zone: Option, - fmri: ServiceName, - group: smf::PropertyGroupName, - }, - Delpropvalue { - zone: Option, - fmri: ServiceName, - name: smf::PropertyName, - glob: String, - }, - Import { - zone: Option, - file: Utf8PathBuf, - }, - Refresh { - zone: Option, - fmri: ServiceName, - }, - Setprop { - zone: Option, - fmri: ServiceName, - name: smf::PropertyName, - value: String, - }, -} - -impl TryFrom for SvccfgCommand { - type Error = String; - - fn try_from(mut input: Input) -> Result { - if input.program != SVCCFG { - return Err(format!("Not svccfg command: {}", input.program)); - } - - let zone = if shift_arg_if(&mut input, "-z")? { - Some(ZoneName(shift_arg(&mut input)?)) - } else { - None - }; - - let fmri = if shift_arg_if(&mut input, "-s")? { - Some(ServiceName(shift_arg(&mut input)?)) - } else { - None - }; - - match shift_arg(&mut input)?.as_str() { - "addpropvalue" => { - let name = shift_arg(&mut input)?; - let name = smf::PropertyName::from_str(&name) - .map_err(|e| e.to_string())?; - - let type_or_value = shift_arg(&mut input)?; - let (ty, value) = match input.args.pop_front() { - Some(value) => { - let ty = type_or_value - .strip_suffix(':') - .ok_or_else(|| { - format!("Bad property type: {type_or_value}") - })? - .to_string(); - (Some(ty), value) - } - None => (None, type_or_value), - }; - - let fmri = fmri.ok_or_else(|| { - format!("-s option required for addpropvalue") - })?; - - no_args_remaining(&input)?; - Ok(SvccfgCommand::Addpropvalue { - zone, - fmri, - key: name, - ty, - value, - }) - } - "addpg" => { - let name = shift_arg(&mut input)?; - let group = smf::PropertyGroupName::new(&name) - .map_err(|e| e.to_string())?; - - let group_type = shift_arg(&mut input)?; - if let Some(flags) = input.args.pop_front() { - return Err( - "Parsing of optional flags not implemented".to_string() - ); - } - let fmri = fmri - .ok_or_else(|| format!("-s option required for addpg"))?; - - no_args_remaining(&input)?; - Ok(SvccfgCommand::Addpg { zone, fmri, group, group_type }) - } - "delpg" => { - let name = shift_arg(&mut input)?; - let group = smf::PropertyGroupName::new(&name) - .map_err(|e| e.to_string())?; - let fmri = fmri - .ok_or_else(|| format!("-s option required for delpg"))?; - - no_args_remaining(&input)?; - Ok(SvccfgCommand::Delpg { zone, fmri, group }) - } - "delpropvalue" => { - let name = shift_arg(&mut input)?; - let name = smf::PropertyName::from_str(&name) - .map_err(|e| e.to_string())?; - let fmri = fmri.ok_or_else(|| { - format!("-s option required for delpropvalue") - })?; - let glob = shift_arg(&mut input)?; - - no_args_remaining(&input)?; - Ok(SvccfgCommand::Delpropvalue { zone, fmri, name, glob }) - } - "import" => { - let file = shift_arg(&mut input)?; - if let Some(_) = fmri { - return Err( - "Cannot use '-s' option with import".to_string() - ); - } - no_args_remaining(&input)?; - Ok(SvccfgCommand::Import { zone, file: file.into() }) - } - "refresh" => { - let fmri = fmri - .ok_or_else(|| format!("-s option required for refresh"))?; - no_args_remaining(&input)?; - Ok(SvccfgCommand::Refresh { zone, fmri }) - } - "setprop" => { - let fmri = fmri - .ok_or_else(|| format!("-s option required for setprop"))?; - - // Setprop seems fine accepting args of the form: - // - name=value - // - name = value - // - name = type: value (NOTE: not yet supported) - let first_arg = shift_arg(&mut input)?; - let (name, value) = - if let Some((name, value)) = first_arg.split_once('=') { - (name.to_string(), value.to_string()) - } else { - let name = first_arg; - shift_arg_expect(&mut input, "=")?; - let value = shift_arg(&mut input)?; - (name, value.to_string()) - }; - - let name = smf::PropertyName::from_str(&name) - .map_err(|e| e.to_string())?; - - no_args_remaining(&input)?; - Ok(SvccfgCommand::Setprop { zone, fmri, name, value }) - } - command => return Err(format!("Unexpected command: {command}")), - } - } -} - -enum SvcadmCommand { - Enable { zone: Option, service: ServiceName }, - Disable { zone: Option, service: ServiceName }, -} - -impl TryFrom for SvcadmCommand { - type Error = String; - - fn try_from(mut input: Input) -> Result { - if input.program != SVCADM { - return Err(format!("Not svcadm command: {}", input.program)); - } - - let zone = if shift_arg_if(&mut input, "-z")? { - Some(ZoneName(shift_arg(&mut input)?)) - } else { - None - }; - - match shift_arg(&mut input)?.as_str() { - "enable" => { - // Intentionally ignored - shift_arg_if(&mut input, "-t")?; - let service = ServiceName(shift_arg(&mut input)?); - no_args_remaining(&input)?; - Ok(SvcadmCommand::Enable { zone, service }) - } - "disable" => { - // Intentionally ignored - shift_arg_if(&mut input, "-t")?; - let service = ServiceName(shift_arg(&mut input)?); - no_args_remaining(&input)?; - Ok(SvcadmCommand::Disable { zone, service }) - } - command => return Err(format!("Unexpected command: {command}")), - } - } -} - -struct FilesystemName(String); - -enum ZfsCommand { - CreateFilesystem { - properties: Vec<(String, String)>, - name: FilesystemName, - }, - CreateVolume { - properties: Vec<(String, String)>, - sparse: bool, - blocksize: Option, - size: u64, - name: FilesystemName, - }, - Destroy { - recursive_dependents: bool, - recursive_children: bool, - force_unmount: bool, - name: FilesystemName, - }, - Get { - recursive: bool, - depth: Option, - // name, property, value, source - fields: Vec, - properties: Vec, - datasets: Option>, - }, - List { - recursive: bool, - depth: Option, - properties: Vec, - datasets: Option>, - }, - Mount { - load_keys: bool, - filesystem: FilesystemName, - }, - Set { - properties: Vec<(String, String)>, - name: FilesystemName, - }, -} - -impl TryFrom for ZfsCommand { - type Error = String; - - fn try_from(mut input: Input) -> Result { - if input.program != ZFS { - return Err(format!("Not zfs command: {}", input.program)); - } - - match shift_arg(&mut input)?.as_str() { - "create" => { - let mut size = None; - let mut blocksize = None; - let mut sparse = None; - let mut properties = vec![]; - - while input.args.len() > 1 { - // Volume Size (volumes only, required) - if shift_arg_if(&mut input, "-V")? { - size = Some( - shift_arg(&mut input)? - .parse::() - .map_err(|e| e.to_string())?, - ); - // Sparse (volumes only, optional) - } else if shift_arg_if(&mut input, "-s")? { - sparse = Some(true); - // Block size (volumes only, optional) - } else if shift_arg_if(&mut input, "-b")? { - blocksize = Some( - shift_arg(&mut input)? - .parse::() - .map_err(|e| e.to_string())?, - ); - // Properties - } else if shift_arg_if(&mut input, "-o")? { - let prop = shift_arg(&mut input)?; - let (k, v) = prop - .split_once('=') - .ok_or_else(|| format!("Bad property: {prop}"))?; - properties.push((k.to_string(), v.to_string())); - } - } - let name = FilesystemName(shift_arg(&mut input)?); - no_args_remaining(&input)?; - - if let Some(size) = size { - // Volume - let sparse = sparse.unwrap_or(false); - Ok(ZfsCommand::CreateVolume { - properties, - sparse, - blocksize, - size, - name, - }) - } else { - // Filesystem - if sparse.is_some() || blocksize.is_some() { - return Err("Using volume arguments, but forgot to specify '-V size'?".to_string()); - } - Ok(ZfsCommand::CreateFilesystem { properties, name }) - } - } - "destroy" => { - let mut recursive_dependents = false; - let mut recursive_children = false; - let mut force_unmount = false; - let mut name = None; - - while !input.args.is_empty() { - let arg = shift_arg(&mut input)?; - let mut chars = arg.chars(); - if let Some('-') = chars.next() { - while let Some(c) = chars.next() { - match c { - 'R' => recursive_dependents = true, - 'r' => recursive_children = true, - 'f' => force_unmount = true, - c => { - return Err(format!( - "Unrecognized option '-{c}'" - )) - } - } - } - } else { - name = Some(FilesystemName(arg)); - no_args_remaining(&input)?; - } - } - let name = name.ok_or_else(|| "Missing name".to_string())?; - Ok(ZfsCommand::Destroy { - recursive_dependents, - recursive_children, - force_unmount, - name, - }) - } - "get" => { - let mut scripting = false; - let mut parsable = false; - let mut recursive = false; - let mut depth = None; - let mut fields = ["name", "property", "value", "source"] - .map(String::from) - .to_vec(); - let mut properties = vec![]; - - while !input.args.is_empty() { - let arg = shift_arg(&mut input)?; - let mut chars = arg.chars(); - // ZFS list lets callers pass in flags in groups, or - // separately. - if let Some('-') = chars.next() { - while let Some(c) = chars.next() { - match c { - 'r' => recursive = true, - 'H' => scripting = true, - 'p' => parsable = true, - 'd' => { - let depth_raw = - if chars.clone().next().is_some() { - chars.collect::() - } else { - shift_arg(&mut input)? - }; - depth = Some( - depth_raw - .parse::() - .map_err(|e| e.to_string())?, - ); - // Convince the compiler we won't use any - // more 'chars', because used them all - // parsing 'depth'. - break; - } - 'o' => { - if chars.next().is_some() { - return Err("-o should be immediately followed by fields".to_string()); - } - fields = shift_arg(&mut input)? - .split(',') - .map(|s| s.to_string()) - .collect(); - } - c => { - return Err(format!( - "Unrecognized option '-{c}'" - )) - } - } - } - } else { - properties = - arg.split(',').map(|s| s.to_string()).collect(); - break; - } - } - - let datasets = Some( - std::mem::take(&mut input.args) - .into_iter() - .collect::>(), - ); - if !scripting || !parsable { - return Err("You should run 'zfs get' commands with the '-Hp' flags enabled".to_string()); - } - - Ok(ZfsCommand::Get { - recursive, - depth, - fields, - properties, - datasets, - }) - } - "list" => { - let mut scripting = false; - let mut parsable = false; - let mut recursive = false; - let mut depth = None; - let mut properties = vec![]; - let mut datasets = None; - - while !input.args.is_empty() { - let arg = shift_arg(&mut input)?; - let mut chars = arg.chars(); - // ZFS list lets callers pass in flags in groups, or - // separately. - if let Some('-') = chars.next() { - while let Some(c) = chars.next() { - match c { - 'r' => recursive = true, - 'H' => scripting = true, - 'p' => parsable = true, - 'd' => { - let depth_raw = - if chars.clone().next().is_some() { - chars.collect::() - } else { - shift_arg(&mut input)? - }; - depth = Some( - depth_raw - .parse::() - .map_err(|e| e.to_string())?, - ); - // Convince the compiler we won't use any - // more 'chars', because used them all - // parsing 'depth'. - break; - } - 'o' => { - if chars.next().is_some() { - return Err("-o should be immediately followed by properties".to_string()); - } - properties = shift_arg(&mut input)? - .split(',') - .map(|s| s.to_string()) - .collect(); - } - c => { - return Err(format!( - "Unrecognized option '-{c}'" - )) - } - } - } - } else { - // As soon as non-flag arguments are passed, the rest of - // the arguments are treated as datasets. - datasets = Some(vec![arg]); - break; - } - } - - let remaining_datasets = std::mem::take(&mut input.args); - if !remaining_datasets.is_empty() { - datasets - .get_or_insert(vec![]) - .extend(remaining_datasets.into_iter()); - }; - - if !scripting || !parsable { - return Err("You should run 'zfs list' commands with the '-Hp' flags enabled".to_string()); - } - - Ok(ZfsCommand::List { recursive, depth, properties, datasets }) - } - "mount" => { - let load_keys = shift_arg_if(&mut input, "-l")?; - let filesystem = FilesystemName(shift_arg(&mut input)?); - no_args_remaining(&input)?; - Ok(ZfsCommand::Mount { load_keys, filesystem }) - } - "set" => { - let mut properties = vec![]; - - while input.args.len() > 1 { - let prop = shift_arg(&mut input)?; - let (k, v) = prop - .split_once('=') - .ok_or_else(|| format!("Bad property: {prop}"))?; - properties.push((k.to_string(), v.to_string())); - } - let name = FilesystemName(shift_arg(&mut input)?); - no_args_remaining(&input)?; - - Ok(ZfsCommand::Set { properties, name }) - } - command => return Err(format!("Unexpected command: {command}")), - } - } -} - -enum ZoneadmCommand { - List, - Install, - Boot, -} - -impl TryFrom for ZoneadmCommand { - type Error = String; - - fn try_from(mut input: Input) -> Result { - if input.program != ZONEADM { - return Err(format!("Not zoneadm command: {}", input.program)); - } - todo!(); - } -} - -enum ZonecfgCommand { - Create { name: ZoneName, config: ZoneConfig }, - Delete { name: ZoneName }, -} - -impl TryFrom for ZonecfgCommand { - type Error = String; - - fn try_from(mut input: Input) -> Result { - if input.program != ZONECFG { - return Err(format!("Not zonecfg command: {}", input.program)); - } - shift_arg_expect(&mut input, "-z")?; - let zone = ZoneName(shift_arg(&mut input)?); - match shift_arg(&mut input)?.as_str() { - "create" => { - shift_arg_expect(&mut input, "-F")?; - shift_arg_expect(&mut input, "-b")?; - - enum Scope { - Global, - Dataset(zone::Dataset), - Device(zone::Device), - Fs(zone::Fs), - Net(zone::Net), - } - let mut scope = Scope::Global; - - // Globally-scoped Resources - let mut brand = None; - let mut path = None; - - // Non-Global Resources - let mut datasets = vec![]; - let mut devices = vec![]; - let mut nets = vec![]; - let mut fs = vec![]; - - while !input.args.is_empty() { - shift_arg_expect(&mut input, ";")?; - match shift_arg(&mut input)?.as_str() { - "set" => { - let prop = shift_arg(&mut input)?; - let (k, v) = - prop.split_once('=').ok_or_else(|| { - format!("Bad property: {prop}") - })?; - - match &mut scope { - Scope::Global => { - match k { - "brand" => { - brand = Some(v.to_string()); - } - "zonepath" => { - path = Some(Utf8PathBuf::from(v)); - } - "autoboot" => { - if v != "false" { - return Err(format!("Unhandled autoboot value: {v}")); - } - } - "ip-type" => { - if v != "exclusive" { - return Err(format!("Unhandled ip-type value: {v}")); - } - } - k => { - return Err(format!( - "Unknown property name: {k}" - )) - } - } - } - Scope::Dataset(d) => match k { - "name" => d.name = v.to_string(), - k => { - return Err(format!( - "Unknown property name: {k}" - )) - } - }, - Scope::Device(d) => match k { - "match" => d.name = v.to_string(), - k => { - return Err(format!( - "Unknown property name: {k}" - )) - } - }, - Scope::Fs(f) => match k { - "type" => f.ty = v.to_string(), - "dir" => f.dir = v.to_string(), - "special" => f.special = v.to_string(), - "raw" => f.raw = Some(v.to_string()), - "options" => { - f.options = v - .split(',') - .map(|s| s.to_string()) - .collect() - } - k => { - return Err(format!( - "Unknown property name: {k}" - )) - } - }, - Scope::Net(n) => match k { - "physical" => n.physical = v.to_string(), - "address" => { - n.address = Some(v.to_string()) - } - "allowed-address" => { - n.allowed_address = Some(v.to_string()) - } - k => { - return Err(format!( - "Unknown property name: {k}" - )) - } - }, - } - } - "add" => { - if !matches!(scope, Scope::Global) { - return Err("Cannot add from non-global scope" - .to_string()); - } - match shift_arg(&mut input)?.as_str() { - "dataset" => { - scope = - Scope::Dataset(zone::Dataset::default()) - } - "device" => { - scope = - Scope::Device(zone::Device::default()) - } - "fs" => scope = Scope::Fs(zone::Fs::default()), - "net" => { - scope = Scope::Net(zone::Net::default()) - } - scope => { - return Err(format!( - "Unexpected scope: {scope}" - )) - } - } - } - "end" => { - match scope { - Scope::Global => { - return Err( - "Cannot end global scope".to_string() - ) - } - Scope::Dataset(d) => datasets.push(d), - Scope::Device(d) => devices.push(d), - Scope::Fs(f) => fs.push(f), - Scope::Net(n) => nets.push(n), - } - scope = Scope::Global; - } - sc => { - return Err(format!("Unexpected subcommand: {sc}")) - } - } - } - - if !matches!(scope, Scope::Global) { - return Err( - "Cannot end zonecfg outside global scope".to_string() - ); - } - - Ok(ZonecfgCommand::Create { - name: zone, - config: ZoneConfig { - state: zone::State::Configured, - brand: brand.ok_or_else(|| "Missing brand")?, - path: path.ok_or_else(|| "Missing zonepath")?, - datasets, - devices, - nets, - fs, - layers: vec![], - }, - }) - } - "delete" => { - shift_arg_expect(&mut input, "-F")?; - Ok(ZonecfgCommand::Delete { name: zone }) - } - command => return Err(format!("Unexpected command: {command}")), - } - } -} - -enum ZpoolCommand { - Create { pool: String, vdev: String }, - Export { pool: String }, - Import { force: bool, pool: String }, - List { properties: Vec, pools: Option> }, - Set { property: String, value: String, pool: String }, -} - -impl TryFrom for ZpoolCommand { - type Error = String; - - fn try_from(mut input: Input) -> Result { - if input.program != ZPOOL { - return Err(format!("Not zpool command: {}", input.program)); - } - - match shift_arg(&mut input)?.as_str() { - "create" => { - let pool = shift_arg(&mut input)?; - let vdev = shift_arg(&mut input)?; - no_args_remaining(&input)?; - Ok(ZpoolCommand::Create { pool, vdev }) - } - "export" => { - let pool = shift_arg(&mut input)?; - no_args_remaining(&input)?; - Ok(ZpoolCommand::Export { pool }) - } - "import" => { - let force = shift_arg_if(&mut input, "-f")?; - let pool = shift_arg(&mut input)?; - Ok(ZpoolCommand::Import { force, pool }) - } - "list" => { - let mut scripting = false; - let mut parsable = false; - let mut properties = vec![]; - let mut pools = None; - - while !input.args.is_empty() { - let arg = shift_arg(&mut input)?; - let mut chars = arg.chars(); - // ZFS list lets callers pass in flags in groups, or - // separately. - if let Some('-') = chars.next() { - while let Some(c) = chars.next() { - match c { - 'H' => scripting = true, - 'p' => parsable = true, - 'o' => { - if chars.next().is_some() { - return Err("-o should be immediately followed by properties".to_string()); - } - properties = shift_arg(&mut input)? - .split(',') - .map(|s| s.to_string()) - .collect(); - } - c => { - return Err(format!( - "Unrecognized option '-{c}'" - )) - } - } - } - } else { - pools = Some(vec![arg]); - break; - } - } - - let remaining_pools = std::mem::take(&mut input.args); - if !remaining_pools.is_empty() { - pools - .get_or_insert(vec![]) - .extend(remaining_pools.into_iter()); - }; - if !scripting || !parsable { - return Err("You should run 'zpool list' commands with the '-Hp' flags enabled".to_string()); - } - Ok(ZpoolCommand::List { properties, pools }) - } - "set" => { - let prop = shift_arg(&mut input)?; - let (k, v) = prop - .split_once('=') - .ok_or_else(|| format!("Bad property: {prop}"))?; - let property = k.to_string(); - let value = v.to_string(); - - let pool = shift_arg(&mut input)?; - no_args_remaining(&input)?; - Ok(ZpoolCommand::Set { property, value, pool }) - } - command => return Err(format!("Unexpected command: {command}")), - } - } -} - -enum KnownCommand { - Dladm(DladmCommand), - Ipadm(IpadmCommand), - Fstyp, - RouteAdm, - Route(RouteCommand), - Svccfg(SvccfgCommand), - Svcadm(SvcadmCommand), - Zfs(ZfsCommand), - Zoneadm(ZoneadmCommand), - Zonecfg(ZonecfgCommand), - Zpool(ZpoolCommand), -} - -struct Command { - with_pfexec: bool, - in_zone: Option, - cmd: KnownCommand, -} - -impl TryFrom for Command { - type Error = String; - - fn try_from(mut input: Input) -> Result { - let mut with_pfexec = false; - let mut in_zone = None; - - while input.program == PFEXEC { - with_pfexec = true; - shift_program(&mut input)?; - } - if input.program == ZLOGIN { - shift_program(&mut input)?; - in_zone = Some(ZoneName(shift_program(&mut input)?)); - } - - let cmd = match input.program.as_str() { - DLADM => KnownCommand::Dladm(DladmCommand::try_from(input)?), - IPADM => KnownCommand::Ipadm(IpadmCommand::try_from(input)?), - ROUTE => KnownCommand::Route(RouteCommand::try_from(input)?), - SVCCFG => KnownCommand::Svccfg(SvccfgCommand::try_from(input)?), - SVCADM => KnownCommand::Svcadm(SvcadmCommand::try_from(input)?), - ZFS => KnownCommand::Zfs(ZfsCommand::try_from(input)?), - ZONEADM => KnownCommand::Zoneadm(ZoneadmCommand::try_from(input)?), - ZONECFG => KnownCommand::Zonecfg(ZonecfgCommand::try_from(input)?), - ZPOOL => KnownCommand::Zpool(ZpoolCommand::try_from(input)?), - _ => return Err(format!("Unknown command: {}", input.program)), - }; - - Ok(Command { with_pfexec, in_zone, cmd }) - } -} - -// Shifts out the program, putting the subsequent argument in its place. -// -// Returns the prior program value. -fn shift_program(input: &mut Input) -> Result { - let new = input - .args - .pop_front() - .ok_or_else(|| format!("Failed to parse {input}"))?; - - let old = std::mem::replace(&mut input.program, new); - - Ok(old) -} - -fn no_args_remaining(input: &Input) -> Result<(), String> { - if !input.args.is_empty() { - return Err(format!("Unexpected extra arguments: {input}")); - } - Ok(()) -} - -// Removes the next argument unconditionally. -fn shift_arg(input: &mut Input) -> Result { - Ok(input.args.pop_front().ok_or_else(|| "Missing argument")?) -} - -// Removes the next argument, which must equal the provided value. -fn shift_arg_expect(input: &mut Input, value: &str) -> Result<(), String> { - let v = input.args.pop_front().ok_or_else(|| "Missing argument")?; - if value != v { - return Err(format!("Unexpected argument {v} (expected: {value}")); - } - Ok(()) -} - -// Removes the next argument if it equals `value`. -// -// Returns if it was equal. -fn shift_arg_if(input: &mut Input, value: &str) -> Result { - let eq = input.args.front().ok_or_else(|| "Missing argument")? == value; - if eq { - input.args.pop_front(); - } - Ok(eq) -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn empty_state() { - let host = Host::new(); - - assert_eq!(0, host.global.id); - assert!(host.global.links.is_empty()); - assert!(host.global.ip_interfaces.is_empty()); - assert!(host.global.routes.is_empty()); - assert!(host.global.services.is_empty()); - assert!(host.zones.is_empty()); - } - - #[test] - fn dladm_create_vnic() { - // Valid usage - let DladmCommand::CreateVnic { link, temporary, mac, vlan, name, properties } = DladmCommand::try_from( - Input::shell(format!("{DLADM} create-vnic -t -l mylink newlink")) - ).unwrap() else { - panic!("Wrong command"); - }; - assert_eq!(link.0, "mylink"); - assert!(temporary); - assert!(mac.is_none()); - assert!(vlan.is_none()); - assert_eq!(name.0, "newlink"); - assert!(properties.is_empty()); - - // Valid usage - let DladmCommand::CreateVnic { link, temporary, mac, vlan, name, properties } = DladmCommand::try_from( - Input::shell(format!("{DLADM} create-vnic -l mylink -v 3 -m foobar -p mtu=123 newlink")) - ).unwrap() else { - panic!("Wrong command"); - }; - assert_eq!(link.0, "mylink"); - assert!(!temporary); - assert_eq!(mac.unwrap(), "foobar"); - assert_eq!(vlan.unwrap(), VlanID::new(3).unwrap()); - assert_eq!(name.0, "newlink"); - assert_eq!( - properties, - HashMap::from([("mtu".to_string(), "123".to_string())]) - ); - - // Missing link - DladmCommand::try_from(Input::shell(format!( - "{DLADM} create-vnic newlink" - ))) - .unwrap_err(); - - // Missing name - DladmCommand::try_from(Input::shell(format!( - "{DLADM} create-vnic -l mylink" - ))) - .unwrap_err(); - - // Bad properties - DladmCommand::try_from(Input::shell(format!( - "{DLADM} create-vnic -l mylink -p foo=bar,baz mylink" - ))) - .unwrap_err(); - - // Unknown argument - DladmCommand::try_from(Input::shell(format!( - "{DLADM} create-vnic -l mylink --splorch mylink" - ))) - .unwrap_err(); - - // Missing command - DladmCommand::try_from(Input::shell(DLADM)).unwrap_err(); - - // Not dladm - DladmCommand::try_from(Input::shell("hello!")).unwrap_err(); - } - - #[test] - fn dladm_create_etherstub() { - // Valid usage - let DladmCommand::CreateEtherstub { temporary, name } = DladmCommand::try_from( - Input::shell(format!("{DLADM} create-etherstub -t newlink")) - ).unwrap() else { - panic!("Wrong command"); - }; - - assert!(temporary); - assert_eq!(name.0, "newlink"); - - // Missing link - DladmCommand::try_from(Input::shell(format!( - "{DLADM} create-etherstub" - ))) - .unwrap_err(); - - // Invalid argument - DladmCommand::try_from(Input::shell(format!( - "{DLADM} create-etherstub --splorch mylink" - ))) - .unwrap_err(); - } - - #[test] - fn dladm_delete_etherstub() { - // Valid usage - let DladmCommand::DeleteEtherstub { temporary, name } = DladmCommand::try_from( - Input::shell(format!("{DLADM} delete-etherstub -t newlink")) - ).unwrap() else { - panic!("Wrong command"); - }; - - assert!(temporary); - assert_eq!(name.0, "newlink"); - - // Missing link - DladmCommand::try_from(Input::shell(format!( - "{DLADM} delete-etherstub" - ))) - .unwrap_err(); - - // Invalid argument - DladmCommand::try_from(Input::shell(format!( - "{DLADM} delete-etherstub --splorch mylink" - ))) - .unwrap_err(); - } - - #[test] - fn dladm_delete_vnic() { - // Valid usage - let DladmCommand::DeleteVnic { temporary, name } = DladmCommand::try_from( - Input::shell(format!("{DLADM} delete-vnic -t newlink")) - ).unwrap() else { - panic!("Wrong command"); - }; - - assert!(temporary); - assert_eq!(name.0, "newlink"); - - // Missing link - DladmCommand::try_from(Input::shell(format!("{DLADM} delete-vnic"))) - .unwrap_err(); - - // Invalid argument - DladmCommand::try_from(Input::shell(format!( - "{DLADM} delete-vnic --splorch mylink" - ))) - .unwrap_err(); - } - - #[test] - fn dladm_show_etherstub() { - // Valid usage - let DladmCommand::ShowEtherstub { name } = DladmCommand::try_from( - Input::shell(format!("{DLADM} show-etherstub newlink")) - ).unwrap() else { - panic!("Wrong command"); - }; - assert_eq!(name.unwrap().0, "newlink"); - - // Valid usage - let DladmCommand::ShowEtherstub { name } = DladmCommand::try_from( - Input::shell(format!("{DLADM} show-etherstub")) - ).unwrap() else { - panic!("Wrong command"); - }; - assert!(name.is_none()); - - // Invalid argument - DladmCommand::try_from(Input::shell(format!( - "{DLADM} show-etherstub --splorch mylink" - ))) - .unwrap_err(); - } - - #[test] - fn dladm_show_link() { - // Valid usage - let DladmCommand::ShowLink { name, fields } = DladmCommand::try_from( - Input::shell(format!("{DLADM} show-link -p -o LINK,STATE newlink")) - ).unwrap() else { - panic!("Wrong command"); - }; - assert_eq!(name.0, "newlink"); - assert_eq!(fields[0], "LINK"); - assert_eq!(fields[1], "STATE"); - - // Missing link name - DladmCommand::try_from(Input::shell(format!("{DLADM} show-link"))) - .unwrap_err(); - - // Not asking for output - DladmCommand::try_from(Input::shell(format!( - "{DLADM} show-link mylink" - ))) - .unwrap_err(); - - // Not asking for parsable output - DladmCommand::try_from(Input::shell(format!( - "{DLADM} show-link -o LINK mylink" - ))) - .unwrap_err(); - } - - #[test] - fn dladm_show_phys() { - // Valid usage - let DladmCommand::ShowPhys{ mac, fields, name } = DladmCommand::try_from( - Input::shell(format!("{DLADM} show-phys -p -o LINK")) - ).unwrap() else { - panic!("Wrong command"); - }; - assert!(!mac); - assert_eq!(fields[0], "LINK"); - assert!(name.is_none()); - - // Not asking for output - DladmCommand::try_from(Input::shell(format!( - "{DLADM} show-phys mylink" - ))) - .unwrap_err(); - - // Not asking for parsable output - DladmCommand::try_from(Input::shell(format!( - "{DLADM} show-phys -o LINK mylink" - ))) - .unwrap_err(); - } - - #[test] - fn dladm_show_vnic() { - // Valid usage - let DladmCommand::ShowVnic{ fields, name } = DladmCommand::try_from( - Input::shell(format!("{DLADM} show-vnic -p -o LINK")) - ).unwrap() else { - panic!("Wrong command"); - }; - assert_eq!(fields.unwrap(), vec!["LINK"]); - assert!(name.is_none()); - - // Valid usage - let DladmCommand::ShowVnic{ fields, name } = DladmCommand::try_from( - Input::shell(format!("{DLADM} show-vnic mylink")) - ).unwrap() else { - panic!("Wrong command"); - }; - assert!(fields.is_none()); - assert_eq!(name.unwrap().0, "mylink"); - - // Not asking for parsable output - DladmCommand::try_from(Input::shell(format!( - "{DLADM} show-vnic -o LINK mylink" - ))) - .unwrap_err(); - } - - #[test] - fn dladm_set_linkprop() { - // Valid usage - let DladmCommand::SetLinkprop { temporary, properties, name } = DladmCommand::try_from( - Input::shell(format!("{DLADM} set-linkprop -t -p mtu=123 mylink")) - ).unwrap() else { - panic!("Wrong command"); - }; - assert!(temporary); - assert_eq!( - properties, - HashMap::from([("mtu".to_string(), "123".to_string())]) - ); - assert_eq!(name.0, "mylink"); - - // Missing properties - DladmCommand::try_from(Input::shell(format!( - "{DLADM} set-linkprop mylink" - ))) - .unwrap_err(); - - // Bad property - DladmCommand::try_from(Input::shell(format!( - "{DLADM} set-linkprop -p bar mylink" - ))) - .unwrap_err(); - - // Missing link - DladmCommand::try_from(Input::shell(format!( - "{DLADM} set-linkprop -p foo=bar" - ))) - .unwrap_err(); - } - - #[test] - fn svccfg_addpropvalue() { - let SvccfgCommand::Addpropvalue { zone, fmri, key, ty, value } = SvccfgCommand::try_from( - Input::shell(format!( - "{SVCCFG} -z myzone -s svc:/myservice:default addpropvalue foo/bar astring: baz" - )) - ).unwrap() else { - panic!("Wrong command"); - }; - - assert_eq!(zone.unwrap().0, "myzone"); - assert_eq!(fmri.0, "svc:/myservice:default"); - assert_eq!(key.to_string(), "foo/bar"); - assert_eq!(ty, Some("astring".to_string())); - assert_eq!(value, "baz"); - - assert!(SvccfgCommand::try_from(Input::shell(format!( - "{SVCCFG} addpropvalue foo/bar baz" - ))) - .err() - .unwrap() - .contains("-s option required")); - - assert!(SvccfgCommand::try_from(Input::shell(format!( - "{SVCCFG} -s svc:/mysvc addpropvalue foo/bar astring baz" - ))) - .err() - .unwrap() - .contains("Bad property type")); - } - - #[test] - fn svccfg_addpg() { - let SvccfgCommand::Addpg { zone, fmri, group, group_type } = SvccfgCommand::try_from( - Input::shell(format!( - "{SVCCFG} -z myzone -s svc:/myservice:default addpg foo baz" - )) - ).unwrap() else { - panic!("Wrong command"); - }; - - assert_eq!(zone.unwrap().0, "myzone"); - assert_eq!(fmri.0, "svc:/myservice:default"); - assert_eq!(group.to_string(), "foo"); - assert_eq!(group_type, "baz"); - - assert!(SvccfgCommand::try_from(Input::shell(format!( - "{SVCCFG} addpg foo baz" - ))) - .err() - .unwrap() - .contains("-s option required")); - - assert!(SvccfgCommand::try_from(Input::shell(format!( - "{SVCCFG} addpg foo baz P" - ))) - .err() - .unwrap() - .contains("Parsing of optional flags not implemented")); - } - - #[test] - fn svccfg_delpg() { - let SvccfgCommand::Delpg { zone, fmri, group } = SvccfgCommand::try_from( - Input::shell(format!( - "{SVCCFG} -z myzone -s svc:/myservice:default delpg foo" - )) - ).unwrap() else { - panic!("Wrong command"); - }; - - assert_eq!(zone.unwrap().0, "myzone"); - assert_eq!(fmri.0, "svc:/myservice:default"); - assert_eq!(group.to_string(), "foo"); - - assert!(SvccfgCommand::try_from(Input::shell(format!( - "{SVCCFG} delpg foo" - ))) - .err() - .unwrap() - .contains("-s option required")); - - assert!(SvccfgCommand::try_from(Input::shell(format!( - "{SVCCFG} -s mysvc delpg foo baz" - ))) - .err() - .unwrap() - .contains("Unexpected extra arguments")); - } - - #[test] - fn svccfg_import() { - let SvccfgCommand::Import { zone, file } = SvccfgCommand::try_from( - Input::shell(format!( - "{SVCCFG} -z myzone import myfile" - )) - ).unwrap() else { - panic!("Wrong command"); - }; - - assert_eq!(zone.unwrap().0, "myzone"); - assert_eq!(file, "myfile"); - - assert!(SvccfgCommand::try_from(Input::shell(format!( - "{SVCCFG} import myfile myotherfile" - ))) - .err() - .unwrap() - .contains("Unexpected extra arguments")); - - assert!(SvccfgCommand::try_from(Input::shell(format!( - "{SVCCFG} -s myservice import myfile" - ))) - .err() - .unwrap() - .contains("Cannot use '-s' option with import")); - } - - #[test] - fn svccfg_refresh() { - let SvccfgCommand::Refresh { zone, fmri } = SvccfgCommand::try_from( - Input::shell(format!( - "{SVCCFG} -z myzone -s myservice refresh" - )) - ).unwrap() else { - panic!("Wrong command"); - }; - - assert_eq!(zone.unwrap().0, "myzone"); - assert_eq!(fmri.0, "myservice"); - } - - #[test] - fn svccfg_setprop() { - let SvccfgCommand::Setprop { zone, fmri, name, value } = SvccfgCommand::try_from( - Input::shell(format!( - "{SVCCFG} -z myzone -s myservice setprop foo/bar=baz" - )) - ).unwrap() else { - panic!("Wrong command"); - }; - - assert_eq!(zone.unwrap().0, "myzone"); - assert_eq!(fmri.0, "myservice"); - assert_eq!(name.to_string(), "foo/bar"); - assert_eq!(value, "baz"); - - // Try that command again, but with spaces - let SvccfgCommand::Setprop { zone, fmri, name, value } = SvccfgCommand::try_from( - Input::shell(format!( - "{SVCCFG} -z myzone -s myservice setprop foo/bar = baz" - )) - ).unwrap() else { - panic!("Wrong command"); - }; - assert_eq!(zone.unwrap().0, "myzone"); - assert_eq!(fmri.0, "myservice"); - assert_eq!(name.to_string(), "foo/bar"); - assert_eq!(value, "baz"); - - // Try that command again, but with quotes - let SvccfgCommand::Setprop { zone, fmri, name, value } = SvccfgCommand::try_from( - Input::shell(format!( - "{SVCCFG} -z myzone -s myservice setprop foo/bar = \"fizz buzz\"" - )) - ).unwrap() else { - panic!("Wrong command"); - }; - assert_eq!(zone.unwrap().0, "myzone"); - assert_eq!(fmri.0, "myservice"); - assert_eq!(name.to_string(), "foo/bar"); - assert_eq!(value, "fizz buzz"); - - assert!(SvccfgCommand::try_from( - Input::shell(format!( - "{SVCCFG} -z myzone -s myservice setprop foo/bar = \"fizz buzz\" blat" - )) - ).err().unwrap().contains("Unexpected extra arguments")); - } - - #[test] - fn svcadm_enable() { - let SvcadmCommand::Enable { zone, service } = SvcadmCommand::try_from( - Input::shell(format!( - "{SVCADM} -z myzone enable -t foobar" - )), - ).unwrap() else { - panic!("wrong command"); - }; - - assert_eq!(zone.unwrap().0, "myzone"); - assert_eq!(service.0, "foobar"); - - assert!(SvcadmCommand::try_from(Input::shell(format!( - "{SVCADM} enable" - ))) - .err() - .unwrap() - .contains("Missing argument")); - } - - #[test] - fn svcadm_disable() { - let SvcadmCommand::Disable { zone, service } = SvcadmCommand::try_from( - Input::shell(format!( - "{SVCADM} -z myzone disable -t foobar" - )), - ).unwrap() else { - panic!("wrong command"); - }; - - assert_eq!(zone.unwrap().0, "myzone"); - assert_eq!(service.0, "foobar"); - - assert!(SvcadmCommand::try_from(Input::shell(format!( - "{SVCADM} disable" - ))) - .err() - .unwrap() - .contains("Missing argument")); - } - - #[test] - fn zonecfg_create() { - let ZonecfgCommand::Create { name, config } = ZonecfgCommand::try_from( - Input::shell(format!( - "{ZONECFG} -z myzone \ - create -F -b ; \ - set brand=omicron1 ; \ - set zonepath=/zone/myzone ; \ - set autoboot=false ; \ - set ip-type=exclusive ; \ - add net ; \ - set physical=oxControlService0 ; \ - end" - )), - ).unwrap() else { - panic!("Wrong command"); - }; - - assert_eq!(name.0, "myzone"); - assert_eq!(config.state, zone::State::Configured); - assert_eq!(config.brand, "omicron1"); - assert_eq!(config.path, Utf8PathBuf::from("/zone/myzone")); - assert!(config.datasets.is_empty()); - assert_eq!(config.nets[0].physical, "oxControlService0"); - assert!(config.fs.is_empty()); - assert!(config.layers.is_empty()); - - // Missing brand - assert!(ZonecfgCommand::try_from(Input::shell(format!( - "{ZONECFG} -z myzone \ - create -F -b ; \ - set zonepath=/zone/myzone" - )),) - .err() - .unwrap() - .contains("Missing brand")); - - // Missing zonepath - assert!(ZonecfgCommand::try_from(Input::shell(format!( - "{ZONECFG} -z myzone \ - create -F -b ; \ - set brand=omicron1" - )),) - .err() - .unwrap() - .contains("Missing zonepath")); - - // Ending mid-scope - assert!(ZonecfgCommand::try_from(Input::shell(format!( - "{ZONECFG} -z myzone \ - create -F -b ; \ - set brand=omicron1 ; \ - set zonepath=/zone/myzone ; \ - add net ; \ - set physical=oxControlService0" - )),) - .err() - .unwrap() - .contains("Cannot end zonecfg outside global scope")); - } - - #[test] - fn zonecfg_delete() { - let ZonecfgCommand::Delete { name } = ZonecfgCommand::try_from( - Input::shell(format!("{ZONECFG} -z myzone delete -F")), - ).unwrap() else { - panic!("Wrong command"); - }; - assert_eq!(name.0, "myzone"); - } - - #[test] - fn route_add() { - // Valid command - let RouteCommand::Add { destination, gateway, interface } = - RouteCommand::try_from(Input::shell(format!( - "{ROUTE} add -inet6 fd00::/16 default -ifp mylink" - ))) - .unwrap(); - assert_eq!( - destination, - RouteTarget::ByAddress(IpNetwork::from_str("fd00::/16").unwrap()) - ); - assert_eq!(gateway, RouteTarget::Default); - assert_eq!(interface.unwrap().0, "mylink"); - - // Valid command - let RouteCommand::Add { destination, gateway, interface } = - RouteCommand::try_from(Input::shell(format!( - "{ROUTE} add -inet default 127.0.0.1/8" - ))) - .unwrap(); - assert_eq!(destination, RouteTarget::DefaultV4); - assert_eq!( - gateway, - RouteTarget::ByAddress(IpNetwork::from_str("127.0.0.1/8").unwrap()) - ); - assert!(interface.is_none()); - - // Invalid address family - assert!(RouteCommand::try_from(Input::shell(format!( - "{ROUTE} add -inet -inet6 default 127.0.0.1/8" - ))) - .err() - .unwrap() - .contains("Cannot force both v4 and v6")); - - // Invalid address family - assert!(RouteCommand::try_from(Input::shell(format!( - "{ROUTE} add -inet6 default -inet6 127.0.0.1/8" - ))) - .err() - .unwrap() - .contains("127.0.0.1/8 is not ipv6")); - } - - #[test] - fn ipadm_create_addr() { - // Valid command - let IpadmCommand::CreateAddr { temporary, ty, addrobj } = IpadmCommand::try_from( - Input::shell(format!("{IPADM} create-addr -t -T addrconf foo/bar")) - ).unwrap() else { - panic!("Wrong command") - }; - assert!(temporary); - assert!(matches!(ty, AddrType::Addrconf)); - assert_eq!("foo/bar", addrobj.to_string()); - - // Valid command - let IpadmCommand::CreateAddr { temporary, ty, addrobj } = IpadmCommand::try_from( - Input::shell(format!("{IPADM} create-addr -T static -a ::/32 foo/bar")) - ).unwrap() else { - panic!("Wrong command") - }; - assert!(!temporary); - assert_eq!(ty, AddrType::Static(IpNetwork::from_str("::/32").unwrap())); - assert_eq!("foo/bar", addrobj.to_string()); - - // Bad type - assert!(IpadmCommand::try_from(Input::shell(format!( - "{IPADM} create-addr -T quadratric foo/bar" - ))) - .err() - .unwrap() - .contains("Unknown address type")); - - // Missing name - assert!(IpadmCommand::try_from(Input::shell(format!( - "{IPADM} create-addr -T dhcp" - ))) - .err() - .unwrap() - .contains("Missing argument")); - - // Too many arguments - assert!(IpadmCommand::try_from(Input::shell(format!( - "{IPADM} create-addr -T dhcp foo/bar baz" - ))) - .err() - .unwrap() - .contains("Unexpected extra arguments")); - - // Not addrobject - assert!(IpadmCommand::try_from(Input::shell(format!( - "{IPADM} create-addr -T dhcp foobar" - ))) - .err() - .unwrap() - .contains("Failed to parse addrobj name")); - } - - #[test] - fn ipadm_create_if() { - // Valid command - let IpadmCommand::CreateIf { temporary, name } = IpadmCommand::try_from( - Input::shell(format!("{IPADM} create-if foobar")) - ).unwrap() else { - panic!("Wrong command") - }; - assert!(!temporary); - assert_eq!(name.0, "foobar"); - - // Too many arguments - assert!(IpadmCommand::try_from(Input::shell(format!( - "{IPADM} create-if foo bar" - ))) - .err() - .unwrap() - .contains("Unexpected extra arguments")); - } - - #[test] - fn ipadm_delete_addr() { - // Valid command - let IpadmCommand::DeleteAddr { addrobj } = IpadmCommand::try_from( - Input::shell(format!("{IPADM} delete-addr foo/bar")) - ).unwrap() else { - panic!("Wrong command") - }; - assert_eq!(addrobj.to_string(), "foo/bar"); - - // Not addrobject - assert!(IpadmCommand::try_from(Input::shell(format!( - "{IPADM} delete-addr foobar" - ))) - .err() - .unwrap() - .contains("Failed to parse addrobj name")); - - // Too many arguments - assert!(IpadmCommand::try_from(Input::shell(format!( - "{IPADM} delete-addr foo/bar foo/bar" - ))) - .err() - .unwrap() - .contains("Unexpected extra arguments")); - } - - #[test] - fn ipadm_delete_if() { - // Valid command - let IpadmCommand::DeleteIf { name } = IpadmCommand::try_from( - Input::shell(format!("{IPADM} delete-if foobar")) - ).unwrap() else { - panic!("Wrong command") - }; - assert_eq!(name.0, "foobar"); - - // Too many arguments - assert!(IpadmCommand::try_from(Input::shell(format!( - "{IPADM} delete-if foo bar" - ))) - .err() - .unwrap() - .contains("Unexpected extra arguments")); - } - - #[test] - fn ipadm_show_if() { - // Valid command - let IpadmCommand::ShowIf { properties, name } = IpadmCommand::try_from( - Input::shell(format!("{IPADM} show-if foobar")) - ).unwrap() else { - panic!("Wrong command") - }; - assert!(properties.is_empty()); - assert_eq!(name.0, "foobar"); - - // Valid command - let IpadmCommand::ShowIf { properties, name } = IpadmCommand::try_from( - Input::shell(format!("{IPADM} show-if -p -o IFNAME foobar")) - ).unwrap() else { - panic!("Wrong command") - }; - assert_eq!(properties[0], "IFNAME"); - assert_eq!(name.0, "foobar"); - - // Non parsable output - IpadmCommand::try_from(Input::shell(format!( - "{IPADM} show-if -o IFNAME foobar" - ))) - .err() - .unwrap(); - - // Not asking for specific field - IpadmCommand::try_from(Input::shell(format!( - "{IPADM} show-if -p foobar" - ))) - .err() - .unwrap(); - - // Too many arguments - assert!(IpadmCommand::try_from(Input::shell(format!( - "{IPADM} show-if fizz buzz" - ))) - .err() - .unwrap() - .contains("Unexpected input")); - } - - #[test] - fn ipadm_set_ifprop() { - // Valid command - let IpadmCommand::SetIfprop { temporary, properties, module, name } = IpadmCommand::try_from( - Input::shell(format!("{IPADM} set-ifprop -t -m ipv4 -p mtu=123 foo")) - ).unwrap() else { - panic!("Wrong command") - }; - - assert!(temporary); - assert_eq!(properties["mtu"], "123"); - assert_eq!(module, "ipv4"); - assert_eq!(name.0, "foo"); - - // Bad property - assert!(IpadmCommand::try_from(Input::shell(format!( - "{IPADM} set-ifprop -p blarg foo" - ))) - .err() - .unwrap() - .contains("Bad property: blarg")); - - // Too many arguments - assert!(IpadmCommand::try_from(Input::shell(format!( - "{IPADM} set-ifprop -p mtu=123 foo bar" - ))) - .err() - .unwrap() - .contains("Unexpected input")); - } - - #[test] - fn zfs_create() { - let ZfsCommand::CreateFilesystem { properties, name } = ZfsCommand::try_from( - Input::shell(format!("{ZFS} create myfilesystem")) - ).unwrap() else { panic!("wrong command") }; - - assert_eq!(properties, vec![]); - assert_eq!(name.0, "myfilesystem"); - - let ZfsCommand::CreateVolume { properties, sparse, blocksize, size, name } = ZfsCommand::try_from( - Input::shell(format!("{ZFS} create -s -V 1024 -b 512 -o foo=bar myvolume")) - ).unwrap() else { panic!("wrong command") }; - - assert_eq!(properties, vec![("foo".to_string(), "bar".to_string())]); - assert_eq!(name.0, "myvolume"); - assert!(sparse); - assert_eq!(size, 1024); - assert_eq!(blocksize, Some(512)); - - assert!(ZfsCommand::try_from(Input::shell(format!( - "{ZFS} create -s -b 512 -o foo=bar myvolume" - ))) - .err() - .unwrap() - .contains("Using volume arguments, but forgot to specify '-V size'")); - } - - #[test] - fn zfs_destroy() { - let ZfsCommand::Destroy { recursive_dependents, recursive_children, force_unmount, name } = - ZfsCommand::try_from( - Input::shell(format!("{ZFS} destroy -rf foobar")) - ).unwrap() else { panic!("wrong command") }; - - assert!(!recursive_dependents); - assert!(recursive_children); - assert!(force_unmount); - assert_eq!(name.0, "foobar"); - - assert!(ZfsCommand::try_from(Input::shell(format!( - "{ZFS} destroy -x doit" - ))) - .err() - .unwrap() - .contains("Unrecognized option '-x'")); - } - - #[test] - fn zfs_get() { - let ZfsCommand::Get { recursive, depth, fields, properties, datasets } = ZfsCommand::try_from( - Input::shell(format!("{ZFS} get -Hrpd10 -o name,value mounted,available myvolume")) - ).unwrap() else { panic!("wrong command") }; - - assert!(recursive); - assert_eq!(depth, Some(10)); - assert_eq!(fields, vec!["name", "value"]); - assert_eq!(properties, vec!["mounted", "available"]); - assert_eq!(datasets.unwrap(), vec!["myvolume"]); - - assert!(ZfsCommand::try_from(Input::shell(format!( - "{ZFS} get -o name,value mounted,available myvolume" - ))) - .err() - .unwrap() - .contains( - "You should run 'zfs get' commands with the '-Hp' flags enabled" - )); - } - - #[test] - fn zfs_list() { - let ZfsCommand::List { recursive, depth, properties, datasets } = ZfsCommand::try_from( - Input::shell(format!("{ZFS} list -d 1 -rHpo name myfilesystem")) - ).unwrap() else { panic!("wrong command") }; - - assert!(recursive); - assert_eq!(depth.unwrap(), 1); - assert_eq!(properties, vec!["name"]); - assert_eq!(datasets.unwrap(), vec!["myfilesystem"]); - - assert!(ZfsCommand::try_from(Input::shell(format!( - "{ZFS} list name myfilesystem" - ))) - .err() - .unwrap() - .contains( - "You should run 'zfs list' commands with the '-Hp' flags enabled" - )); - } - - #[test] - fn zfs_mount() { - let ZfsCommand::Mount { load_keys, filesystem } = ZfsCommand::try_from( - Input::shell(format!("{ZFS} mount -l foobar")) - ).unwrap() else { panic!("wrong command") }; - - assert!(load_keys); - assert_eq!(filesystem.0, "foobar"); - } - - #[test] - fn zfs_set() { - let ZfsCommand::Set { properties, name } = ZfsCommand::try_from( - Input::shell(format!("{ZFS} set foo=bar baz=blat myfs")) - ).unwrap() else { panic!("wrong command") }; - - assert_eq!( - properties, - vec![ - ("foo".to_string(), "bar".to_string()), - ("baz".to_string(), "blat".to_string()) - ] - ); - assert_eq!(name.0, "myfs"); - } -} diff --git a/helios/tokamak/src/host/dladm.rs b/helios/tokamak/src/host/dladm.rs new file mode 100644 index 00000000000..83ce572cacf --- /dev/null +++ b/helios/tokamak/src/host/dladm.rs @@ -0,0 +1,530 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// 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/. + +use crate::host::LinkName; +use crate::host::{no_args_remaining, shift_arg, shift_arg_if}; + +use helios_fusion::Input; +use helios_fusion::DLADM; +use omicron_common::vlan::VlanID; +use std::collections::HashMap; +use std::str::FromStr; + +#[derive(Debug)] +pub(crate) enum Command { + CreateVnic { + link: LinkName, + temporary: bool, + mac: Option, + vlan: Option, + name: LinkName, + properties: HashMap, + }, + CreateEtherstub { + temporary: bool, + name: LinkName, + }, + DeleteEtherstub { + temporary: bool, + name: LinkName, + }, + DeleteVnic { + temporary: bool, + name: LinkName, + }, + ShowEtherstub { + name: Option, + }, + ShowLink { + name: LinkName, + fields: Vec, + }, + ShowPhys { + mac: bool, + fields: Vec, + name: Option, + }, + ShowVnic { + fields: Option>, + name: Option, + }, + SetLinkprop { + temporary: bool, + properties: HashMap, + name: LinkName, + }, +} + +impl TryFrom for Command { + type Error = String; + + fn try_from(mut input: Input) -> Result { + if input.program != DLADM { + return Err(format!("Not dladm command: {}", input.program)); + } + + match shift_arg(&mut input)?.as_str() { + "create-vnic" => { + let mut link = None; + let mut temporary = false; + let mut mac = None; + let mut vlan = None; + let mut properties = HashMap::new(); + let name = LinkName( + input.args.pop_back().ok_or_else(|| "Missing name")?, + ); + + while !input.args.is_empty() { + if shift_arg_if(&mut input, "-t")? { + temporary = true; + } else if shift_arg_if(&mut input, "-p")? { + let props = shift_arg(&mut input)?; + let props = props.split(','); + for prop in props { + let (k, v) = + prop.split_once('=').ok_or_else(|| { + format!("Bad property: {prop}") + })?; + properties.insert(k.to_string(), v.to_string()); + } + } else if shift_arg_if(&mut input, "-m")? { + // NOTE: Not yet supporting the keyword-based MACs. + mac = Some(shift_arg(&mut input)?); + } else if shift_arg_if(&mut input, "-l")? { + link = Some(LinkName(shift_arg(&mut input)?)); + } else if shift_arg_if(&mut input, "-v")? { + vlan = Some( + VlanID::from_str(&shift_arg(&mut input)?) + .map_err(|e| e.to_string())?, + ); + } else { + return Err(format!("Invalid arguments {}", input)); + } + } + + Ok(Self::CreateVnic { + link: link.ok_or_else(|| "Missing link")?, + temporary, + mac, + vlan, + name, + properties, + }) + } + "create-etherstub" => { + let mut temporary = false; + let name = LinkName( + input.args.pop_back().ok_or_else(|| "Missing name")?, + ); + while !input.args.is_empty() { + if shift_arg_if(&mut input, "-t")? { + temporary = true; + } else { + return Err(format!("Invalid arguments {}", input)); + } + } + Ok(Self::CreateEtherstub { temporary, name }) + } + "delete-etherstub" => { + let mut temporary = false; + let name = LinkName( + input.args.pop_back().ok_or_else(|| "Missing name")?, + ); + while !input.args.is_empty() { + if shift_arg_if(&mut input, "-t")? { + temporary = true; + } else { + return Err(format!("Invalid arguments {}", input)); + } + } + Ok(Self::DeleteEtherstub { temporary, name }) + } + "delete-vnic" => { + let mut temporary = false; + let name = LinkName( + input.args.pop_back().ok_or_else(|| "Missing name")?, + ); + while !input.args.is_empty() { + if shift_arg_if(&mut input, "-t")? { + temporary = true; + } else { + return Err(format!("Invalid arguments {}", input)); + } + } + Ok(Self::DeleteVnic { temporary, name }) + } + "show-etherstub" => { + let name = input.args.pop_back().map(|s| LinkName(s)); + no_args_remaining(&input)?; + Ok(Self::ShowEtherstub { name }) + } + "show-link" => { + let name = LinkName( + input.args.pop_back().ok_or_else(|| "Missing name")?, + ); + if !shift_arg_if(&mut input, "-p")? { + return Err( + "You should ask for parsable output ('-p')".into() + ); + } + if !shift_arg_if(&mut input, "-o")? { + return Err( + "You should ask for specific outputs ('-o')".into() + ); + } + let fields = shift_arg(&mut input)? + .split(',') + .map(|s| s.to_string()) + .collect(); + no_args_remaining(&input)?; + + Ok(Self::ShowLink { name, fields }) + } + "show-phys" => { + let mut mac = false; + if shift_arg_if(&mut input, "-m")? { + mac = true; + } + if !shift_arg_if(&mut input, "-p")? { + return Err( + "You should ask for parsable output ('-p')".into() + ); + } + if !shift_arg_if(&mut input, "-o")? { + return Err( + "You should ask for specific outputs ('-o')".into() + ); + } + let fields = shift_arg(&mut input)? + .split(',') + .map(|s| s.to_string()) + .collect(); + let name = input.args.pop_front().map(|s| LinkName(s)); + no_args_remaining(&input)?; + + Ok(Self::ShowPhys { mac, fields, name }) + } + "show-vnic" => { + let mut fields = None; + if shift_arg_if(&mut input, "-p")? { + if !shift_arg_if(&mut input, "-o")? { + return Err( + "You should ask for specific outputs ('-o')".into(), + ); + } + fields = Some( + shift_arg(&mut input)? + .split(',') + .map(|s| s.to_string()) + .collect(), + ); + } + + let name = input.args.pop_front().map(|s| LinkName(s)); + no_args_remaining(&input)?; + Ok(Self::ShowVnic { fields, name }) + } + "set-linkprop" => { + let mut temporary = false; + let mut properties = HashMap::new(); + let name = LinkName( + input.args.pop_back().ok_or_else(|| "Missing name")?, + ); + + while !input.args.is_empty() { + if shift_arg_if(&mut input, "-t")? { + temporary = true; + } else if shift_arg_if(&mut input, "-p")? { + let props = shift_arg(&mut input)?; + let props = props.split(','); + for prop in props { + let (k, v) = + prop.split_once('=').ok_or_else(|| { + format!("Bad property: {prop}") + })?; + properties.insert(k.to_string(), v.to_string()); + } + } else { + return Err(format!("Invalid arguments {}", input)); + } + } + + if properties.is_empty() { + return Err("Missing properties".into()); + } + + Ok(Self::SetLinkprop { temporary, properties, name }) + } + command => Err(format!("Unsupported command: {}", command)), + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn create_vnic() { + // Valid usage + let Command::CreateVnic { link, temporary, mac, vlan, name, properties } = Command::try_from( + Input::shell(format!("{DLADM} create-vnic -t -l mylink newlink")) + ).unwrap() else { + panic!("Wrong command"); + }; + assert_eq!(link.0, "mylink"); + assert!(temporary); + assert!(mac.is_none()); + assert!(vlan.is_none()); + assert_eq!(name.0, "newlink"); + assert!(properties.is_empty()); + + // Valid usage + let Command::CreateVnic { link, temporary, mac, vlan, name, properties } = Command::try_from( + Input::shell(format!("{DLADM} create-vnic -l mylink -v 3 -m foobar -p mtu=123 newlink")) + ).unwrap() else { + panic!("Wrong command"); + }; + assert_eq!(link.0, "mylink"); + assert!(!temporary); + assert_eq!(mac.unwrap(), "foobar"); + assert_eq!(vlan.unwrap(), VlanID::new(3).unwrap()); + assert_eq!(name.0, "newlink"); + assert_eq!( + properties, + HashMap::from([("mtu".to_string(), "123".to_string())]) + ); + + // Missing link + Command::try_from(Input::shell(format!("{DLADM} create-vnic newlink"))) + .unwrap_err(); + + // Missing name + Command::try_from(Input::shell(format!( + "{DLADM} create-vnic -l mylink" + ))) + .unwrap_err(); + + // Bad properties + Command::try_from(Input::shell(format!( + "{DLADM} create-vnic -l mylink -p foo=bar,baz mylink" + ))) + .unwrap_err(); + + // Unknown argument + Command::try_from(Input::shell(format!( + "{DLADM} create-vnic -l mylink --splorch mylink" + ))) + .unwrap_err(); + + // Missing command + Command::try_from(Input::shell(DLADM)).unwrap_err(); + + // Not dladm + Command::try_from(Input::shell("hello!")).unwrap_err(); + } + + #[test] + fn create_etherstub() { + // Valid usage + let Command::CreateEtherstub { temporary, name } = Command::try_from( + Input::shell(format!("{DLADM} create-etherstub -t newlink")) + ).unwrap() else { + panic!("Wrong command"); + }; + + assert!(temporary); + assert_eq!(name.0, "newlink"); + + // Missing link + Command::try_from(Input::shell(format!("{DLADM} create-etherstub"))) + .unwrap_err(); + + // Invalid argument + Command::try_from(Input::shell(format!( + "{DLADM} create-etherstub --splorch mylink" + ))) + .unwrap_err(); + } + + #[test] + fn delete_etherstub() { + // Valid usage + let Command::DeleteEtherstub { temporary, name } = Command::try_from( + Input::shell(format!("{DLADM} delete-etherstub -t newlink")) + ).unwrap() else { + panic!("Wrong command"); + }; + + assert!(temporary); + assert_eq!(name.0, "newlink"); + + // Missing link + Command::try_from(Input::shell(format!("{DLADM} delete-etherstub"))) + .unwrap_err(); + + // Invalid argument + Command::try_from(Input::shell(format!( + "{DLADM} delete-etherstub --splorch mylink" + ))) + .unwrap_err(); + } + + #[test] + fn delete_vnic() { + // Valid usage + let Command::DeleteVnic { temporary, name } = Command::try_from( + Input::shell(format!("{DLADM} delete-vnic -t newlink")) + ).unwrap() else { + panic!("Wrong command"); + }; + + assert!(temporary); + assert_eq!(name.0, "newlink"); + + // Missing link + Command::try_from(Input::shell(format!("{DLADM} delete-vnic"))) + .unwrap_err(); + + // Invalid argument + Command::try_from(Input::shell(format!( + "{DLADM} delete-vnic --splorch mylink" + ))) + .unwrap_err(); + } + + #[test] + fn show_etherstub() { + // Valid usage + let Command::ShowEtherstub { name } = Command::try_from( + Input::shell(format!("{DLADM} show-etherstub newlink")) + ).unwrap() else { + panic!("Wrong command"); + }; + assert_eq!(name.unwrap().0, "newlink"); + + // Valid usage + let Command::ShowEtherstub { name } = Command::try_from( + Input::shell(format!("{DLADM} show-etherstub")) + ).unwrap() else { + panic!("Wrong command"); + }; + assert!(name.is_none()); + + // Invalid argument + Command::try_from(Input::shell(format!( + "{DLADM} show-etherstub --splorch mylink" + ))) + .unwrap_err(); + } + + #[test] + fn show_link() { + // Valid usage + let Command::ShowLink { name, fields } = Command::try_from( + Input::shell(format!("{DLADM} show-link -p -o LINK,STATE newlink")) + ).unwrap() else { + panic!("Wrong command"); + }; + assert_eq!(name.0, "newlink"); + assert_eq!(fields[0], "LINK"); + assert_eq!(fields[1], "STATE"); + + // Missing link name + Command::try_from(Input::shell(format!("{DLADM} show-link"))) + .unwrap_err(); + + // Not asking for output + Command::try_from(Input::shell(format!("{DLADM} show-link mylink"))) + .unwrap_err(); + + // Not asking for parsable output + Command::try_from(Input::shell(format!( + "{DLADM} show-link -o LINK mylink" + ))) + .unwrap_err(); + } + + #[test] + fn show_phys() { + // Valid usage + let Command::ShowPhys{ mac, fields, name } = Command::try_from( + Input::shell(format!("{DLADM} show-phys -p -o LINK")) + ).unwrap() else { + panic!("Wrong command"); + }; + assert!(!mac); + assert_eq!(fields[0], "LINK"); + assert!(name.is_none()); + + // Not asking for output + Command::try_from(Input::shell(format!("{DLADM} show-phys mylink"))) + .unwrap_err(); + + // Not asking for parsable output + Command::try_from(Input::shell(format!( + "{DLADM} show-phys -o LINK mylink" + ))) + .unwrap_err(); + } + + #[test] + fn show_vnic() { + // Valid usage + let Command::ShowVnic{ fields, name } = Command::try_from( + Input::shell(format!("{DLADM} show-vnic -p -o LINK")) + ).unwrap() else { + panic!("Wrong command"); + }; + assert_eq!(fields.unwrap(), vec!["LINK"]); + assert!(name.is_none()); + + // Valid usage + let Command::ShowVnic{ fields, name } = Command::try_from( + Input::shell(format!("{DLADM} show-vnic mylink")) + ).unwrap() else { + panic!("Wrong command"); + }; + assert!(fields.is_none()); + assert_eq!(name.unwrap().0, "mylink"); + + // Not asking for parsable output + Command::try_from(Input::shell(format!( + "{DLADM} show-vnic -o LINK mylink" + ))) + .unwrap_err(); + } + + #[test] + fn set_linkprop() { + // Valid usage + let Command::SetLinkprop { temporary, properties, name } = Command::try_from( + Input::shell(format!("{DLADM} set-linkprop -t -p mtu=123 mylink")) + ).unwrap() else { + panic!("Wrong command"); + }; + assert!(temporary); + assert_eq!( + properties, + HashMap::from([("mtu".to_string(), "123".to_string())]) + ); + assert_eq!(name.0, "mylink"); + + // Missing properties + Command::try_from(Input::shell(format!("{DLADM} set-linkprop mylink"))) + .unwrap_err(); + + // Bad property + Command::try_from(Input::shell(format!( + "{DLADM} set-linkprop -p bar mylink" + ))) + .unwrap_err(); + + // Missing link + Command::try_from(Input::shell(format!( + "{DLADM} set-linkprop -p foo=bar" + ))) + .unwrap_err(); + } +} diff --git a/helios/tokamak/src/host/ipadm.rs b/helios/tokamak/src/host/ipadm.rs new file mode 100644 index 00000000000..3b77c713234 --- /dev/null +++ b/helios/tokamak/src/host/ipadm.rs @@ -0,0 +1,344 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// 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/. + +use crate::host::{ + no_args_remaining, shift_arg, shift_arg_expect, shift_arg_if, +}; +use crate::host::{AddrType, IpInterfaceName}; + +use helios_fusion::addrobj::AddrObject; +use helios_fusion::Input; +use helios_fusion::IPADM; +use ipnetwork::IpNetwork; +use std::collections::HashMap; +use std::str::FromStr; + +pub(crate) enum Command { + CreateAddr { + temporary: bool, + ty: AddrType, + addrobj: AddrObject, + }, + CreateIf { + temporary: bool, + name: IpInterfaceName, + }, + DeleteAddr { + addrobj: AddrObject, + }, + DeleteIf { + name: IpInterfaceName, + }, + ShowIf { + properties: Vec, + name: IpInterfaceName, + }, + SetIfprop { + temporary: bool, + properties: HashMap, + module: String, + name: IpInterfaceName, + }, +} + +impl TryFrom for Command { + type Error = String; + + fn try_from(mut input: Input) -> Result { + if input.program != IPADM { + return Err(format!("Not ipadm command: {}", input.program)); + } + + match shift_arg(&mut input)?.as_str() { + "create-addr" => { + let temporary = shift_arg_if(&mut input, "-t")?; + shift_arg_expect(&mut input, "-T")?; + + let ty = match shift_arg(&mut input)?.as_str() { + "static" => { + shift_arg_expect(&mut input, "-a")?; + let addr = shift_arg(&mut input)?; + AddrType::Static( + IpNetwork::from_str(&addr) + .map_err(|e| e.to_string())?, + ) + } + "dhcp" => AddrType::Dhcp, + "addrconf" => AddrType::Addrconf, + ty => return Err(format!("Unknown address type {ty}")), + }; + let addrobj = AddrObject::from_str(&shift_arg(&mut input)?) + .map_err(|e| e.to_string())?; + no_args_remaining(&input)?; + Ok(Command::CreateAddr { temporary, ty, addrobj }) + } + "create-ip" | "create-if" => { + let temporary = shift_arg_if(&mut input, "-t")?; + let name = IpInterfaceName(shift_arg(&mut input)?); + no_args_remaining(&input)?; + Ok(Command::CreateIf { temporary, name }) + } + "delete-addr" => { + let addrobj = AddrObject::from_str(&shift_arg(&mut input)?) + .map_err(|e| e.to_string())?; + no_args_remaining(&input)?; + Ok(Command::DeleteAddr { addrobj }) + } + "delete-ip" | "delete-if" => { + let name = IpInterfaceName(shift_arg(&mut input)?); + no_args_remaining(&input)?; + Ok(Command::DeleteIf { name }) + } + "show-if" => { + let name = IpInterfaceName( + input.args.pop_back().ok_or_else(|| "Missing name")?, + ); + let mut properties = vec![]; + while !input.args.is_empty() { + if shift_arg_if(&mut input, "-p")? { + shift_arg_expect(&mut input, "-o")?; + properties = shift_arg(&mut input)? + .split(',') + .map(|s| s.to_string()) + .collect(); + } else { + return Err(format!("Unexpected input: {input}")); + } + } + + Ok(Command::ShowIf { properties, name }) + } + "set-ifprop" => { + let name = IpInterfaceName( + input.args.pop_back().ok_or_else(|| "Missing name")?, + ); + + let mut temporary = false; + let mut properties = HashMap::new(); + let mut module = "ip".to_string(); + + while !input.args.is_empty() { + if shift_arg_if(&mut input, "-t")? { + temporary = true; + } else if shift_arg_if(&mut input, "-m")? { + module = shift_arg(&mut input)?; + } else if shift_arg_if(&mut input, "-p")? { + let props = shift_arg(&mut input)?; + let props = props.split(','); + for prop in props { + let (k, v) = + prop.split_once('=').ok_or_else(|| { + format!("Bad property: {prop}") + })?; + properties.insert(k.to_string(), v.to_string()); + } + } else { + return Err(format!("Unexpected input: {input}")); + } + } + + Ok(Command::SetIfprop { temporary, properties, module, name }) + } + command => return Err(format!("Unexpected command: {command}")), + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn create_addr() { + // Valid command + let Command::CreateAddr { temporary, ty, addrobj } = Command::try_from( + Input::shell(format!("{IPADM} create-addr -t -T addrconf foo/bar")) + ).unwrap() else { + panic!("Wrong command") + }; + assert!(temporary); + assert!(matches!(ty, AddrType::Addrconf)); + assert_eq!("foo/bar", addrobj.to_string()); + + // Valid command + let Command::CreateAddr { temporary, ty, addrobj } = Command::try_from( + Input::shell(format!("{IPADM} create-addr -T static -a ::/32 foo/bar")) + ).unwrap() else { + panic!("Wrong command") + }; + assert!(!temporary); + assert_eq!(ty, AddrType::Static(IpNetwork::from_str("::/32").unwrap())); + assert_eq!("foo/bar", addrobj.to_string()); + + // Bad type + assert!(Command::try_from(Input::shell(format!( + "{IPADM} create-addr -T quadratric foo/bar" + ))) + .err() + .unwrap() + .contains("Unknown address type")); + + // Missing name + assert!(Command::try_from(Input::shell(format!( + "{IPADM} create-addr -T dhcp" + ))) + .err() + .unwrap() + .contains("Missing argument")); + + // Too many arguments + assert!(Command::try_from(Input::shell(format!( + "{IPADM} create-addr -T dhcp foo/bar baz" + ))) + .err() + .unwrap() + .contains("Unexpected extra arguments")); + + // Not addrobject + assert!(Command::try_from(Input::shell(format!( + "{IPADM} create-addr -T dhcp foobar" + ))) + .err() + .unwrap() + .contains("Failed to parse addrobj name")); + } + + #[test] + fn create_if() { + // Valid command + let Command::CreateIf { temporary, name } = Command::try_from( + Input::shell(format!("{IPADM} create-if foobar")) + ).unwrap() else { + panic!("Wrong command") + }; + assert!(!temporary); + assert_eq!(name.0, "foobar"); + + // Too many arguments + assert!(Command::try_from(Input::shell(format!( + "{IPADM} create-if foo bar" + ))) + .err() + .unwrap() + .contains("Unexpected extra arguments")); + } + + #[test] + fn delete_addr() { + // Valid command + let Command::DeleteAddr { addrobj } = Command::try_from( + Input::shell(format!("{IPADM} delete-addr foo/bar")) + ).unwrap() else { + panic!("Wrong command") + }; + assert_eq!(addrobj.to_string(), "foo/bar"); + + // Not addrobject + assert!(Command::try_from(Input::shell(format!( + "{IPADM} delete-addr foobar" + ))) + .err() + .unwrap() + .contains("Failed to parse addrobj name")); + + // Too many arguments + assert!(Command::try_from(Input::shell(format!( + "{IPADM} delete-addr foo/bar foo/bar" + ))) + .err() + .unwrap() + .contains("Unexpected extra arguments")); + } + + #[test] + fn delete_if() { + // Valid command + let Command::DeleteIf { name } = Command::try_from( + Input::shell(format!("{IPADM} delete-if foobar")) + ).unwrap() else { + panic!("Wrong command") + }; + assert_eq!(name.0, "foobar"); + + // Too many arguments + assert!(Command::try_from(Input::shell(format!( + "{IPADM} delete-if foo bar" + ))) + .err() + .unwrap() + .contains("Unexpected extra arguments")); + } + + #[test] + fn show_if() { + // Valid command + let Command::ShowIf { properties, name } = Command::try_from( + Input::shell(format!("{IPADM} show-if foobar")) + ).unwrap() else { + panic!("Wrong command") + }; + assert!(properties.is_empty()); + assert_eq!(name.0, "foobar"); + + // Valid command + let Command::ShowIf { properties, name } = Command::try_from( + Input::shell(format!("{IPADM} show-if -p -o IFNAME foobar")) + ).unwrap() else { + panic!("Wrong command") + }; + assert_eq!(properties[0], "IFNAME"); + assert_eq!(name.0, "foobar"); + + // Non parsable output + Command::try_from(Input::shell(format!( + "{IPADM} show-if -o IFNAME foobar" + ))) + .err() + .unwrap(); + + // Not asking for specific field + Command::try_from(Input::shell(format!("{IPADM} show-if -p foobar"))) + .err() + .unwrap(); + + // Too many arguments + assert!(Command::try_from(Input::shell(format!( + "{IPADM} show-if fizz buzz" + ))) + .err() + .unwrap() + .contains("Unexpected input")); + } + + #[test] + fn set_ifprop() { + // Valid command + let Command::SetIfprop { temporary, properties, module, name } = Command::try_from( + Input::shell(format!("{IPADM} set-ifprop -t -m ipv4 -p mtu=123 foo")) + ).unwrap() else { + panic!("Wrong command") + }; + + assert!(temporary); + assert_eq!(properties["mtu"], "123"); + assert_eq!(module, "ipv4"); + assert_eq!(name.0, "foo"); + + // Bad property + assert!(Command::try_from(Input::shell(format!( + "{IPADM} set-ifprop -p blarg foo" + ))) + .err() + .unwrap() + .contains("Bad property: blarg")); + + // Too many arguments + assert!(Command::try_from(Input::shell(format!( + "{IPADM} set-ifprop -p mtu=123 foo bar" + ))) + .err() + .unwrap() + .contains("Unexpected input")); + } +} diff --git a/helios/tokamak/src/host/mod.rs b/helios/tokamak/src/host/mod.rs new file mode 100644 index 00000000000..db4ea4d0b21 --- /dev/null +++ b/helios/tokamak/src/host/mod.rs @@ -0,0 +1,298 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// 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/. + +//! Emulates an illumos system + +// TODO REMOVE ME +#![allow(dead_code)] +#![allow(unused_mut)] +#![allow(unused_variables)] + +use camino::Utf8PathBuf; +use helios_fusion::zpool::ZpoolName; +use helios_fusion::Input; +use helios_fusion::{ + DLADM, IPADM, PFEXEC, ROUTE, SVCADM, SVCCFG, ZFS, ZLOGIN, ZONEADM, ZONECFG, + ZPOOL, +}; +use ipnetwork::IpNetwork; +use std::collections::{HashMap, HashSet}; +use std::str::FromStr; + +mod dladm; +mod ipadm; +mod route; +mod svcadm; +mod svccfg; +mod zfs; +mod zoneadm; +mod zonecfg; +mod zpool; + +enum LinkType { + Etherstub, + Vnic, +} + +#[derive(Debug, PartialEq, Eq)] +pub struct LinkName(String); +struct Link { + ty: LinkType, + parent: Option, + properties: HashMap, +} + +pub struct IpInterfaceName(String); +struct IpInterface {} + +enum RouteDestination { + Default, + Literal(IpNetwork), +} + +struct Route { + destination: RouteDestination, + gateway: IpNetwork, +} + +#[derive(Debug)] +pub struct ServiceName(String); + +struct Service { + state: smf::SmfState, + properties: HashMap, +} + +struct ZoneEnvironment { + id: u64, + links: HashMap, + ip_interfaces: HashMap, + routes: Vec, + services: HashMap, +} + +impl ZoneEnvironment { + fn new(id: u64) -> Self { + Self { + id, + links: HashMap::new(), + ip_interfaces: HashMap::new(), + routes: vec![], + services: HashMap::new(), + } + } +} + +#[derive(Debug)] +pub struct ZoneName(String); + +pub struct ZoneConfig { + state: zone::State, + brand: String, + // zonepath + path: Utf8PathBuf, + datasets: Vec, + devices: Vec, + nets: Vec, + fs: Vec, + // E.g. zone image, overlays, etc. + layers: Vec, +} + +struct Zone { + config: ZoneConfig, + environment: ZoneEnvironment, +} + +struct Host { + global: ZoneEnvironment, + zones: HashMap, + + // TODO: Is this the right abstraction layer? + // How do you want to represent zpools & filesystems? + // + // TODO: Should filesystems be part of the "ZoneEnvironment" abstraction? + zpools: HashSet, +} + +impl Host { + pub fn new() -> Self { + Self { + global: ZoneEnvironment::new(0), + zones: HashMap::new(), + zpools: HashSet::new(), + } + } +} + +#[derive(Debug, PartialEq)] +pub enum AddrType { + Dhcp, + Static(IpNetwork), + Addrconf, +} + +#[derive(Debug, PartialEq, Eq)] +pub enum RouteTarget { + Default, + DefaultV4, + DefaultV6, + ByAddress(IpNetwork), +} + +impl RouteTarget { + fn shift_target(input: &mut Input) -> Result { + let force_v4 = shift_arg_if(input, "-inet")?; + let force_v6 = shift_arg_if(input, "-inet6")?; + + let target = match (force_v4, force_v6, shift_arg(input)?.as_str()) { + (true, true, _) => { + return Err("Cannot force both v4 and v6".to_string()) + } + (true, false, "default") => RouteTarget::DefaultV4, + (false, true, "default") => RouteTarget::DefaultV6, + (false, false, "default") => RouteTarget::Default, + (_, _, other) => { + let net = + IpNetwork::from_str(other).map_err(|e| e.to_string())?; + if force_v4 && !net.is_ipv4() { + return Err(format!("{net} is not ipv4")); + } + if force_v6 && !net.is_ipv6() { + return Err(format!("{net} is not ipv6")); + } + RouteTarget::ByAddress(net) + } + }; + Ok(target) + } +} + +pub struct FilesystemName(String); + +enum KnownCommand { + Dladm(dladm::Command), + Ipadm(ipadm::Command), + Fstyp, + RouteAdm, + Route(route::Command), + Svccfg(svccfg::Command), + Svcadm(svcadm::Command), + Zfs(zfs::Command), + Zoneadm(zoneadm::Command), + Zonecfg(zonecfg::Command), + Zpool(zpool::Command), +} + +struct Command { + with_pfexec: bool, + in_zone: Option, + cmd: KnownCommand, +} + +impl TryFrom for Command { + type Error = String; + + fn try_from(mut input: Input) -> Result { + let mut with_pfexec = false; + let mut in_zone = None; + + while input.program == PFEXEC { + with_pfexec = true; + shift_program(&mut input)?; + } + if input.program == ZLOGIN { + shift_program(&mut input)?; + in_zone = Some(ZoneName(shift_program(&mut input)?)); + } + + let cmd = match input.program.as_str() { + DLADM => KnownCommand::Dladm(dladm::Command::try_from(input)?), + IPADM => KnownCommand::Ipadm(ipadm::Command::try_from(input)?), + ROUTE => KnownCommand::Route(route::Command::try_from(input)?), + SVCCFG => KnownCommand::Svccfg(svccfg::Command::try_from(input)?), + SVCADM => KnownCommand::Svcadm(svcadm::Command::try_from(input)?), + ZFS => KnownCommand::Zfs(zfs::Command::try_from(input)?), + ZONEADM => { + KnownCommand::Zoneadm(zoneadm::Command::try_from(input)?) + } + ZONECFG => { + KnownCommand::Zonecfg(zonecfg::Command::try_from(input)?) + } + ZPOOL => KnownCommand::Zpool(zpool::Command::try_from(input)?), + _ => return Err(format!("Unknown command: {}", input.program)), + }; + + Ok(Command { with_pfexec, in_zone, cmd }) + } +} + +// Shifts out the program, putting the subsequent argument in its place. +// +// Returns the prior program value. +pub(crate) fn shift_program(input: &mut Input) -> Result { + let new = input + .args + .pop_front() + .ok_or_else(|| format!("Failed to parse {input}"))?; + + let old = std::mem::replace(&mut input.program, new); + + Ok(old) +} + +pub(crate) fn no_args_remaining(input: &Input) -> Result<(), String> { + if !input.args.is_empty() { + return Err(format!("Unexpected extra arguments: {input}")); + } + Ok(()) +} + +// Removes the next argument unconditionally. +pub(crate) fn shift_arg(input: &mut Input) -> Result { + Ok(input.args.pop_front().ok_or_else(|| "Missing argument")?) +} + +// Removes the next argument, which must equal the provided value. +pub(crate) fn shift_arg_expect( + input: &mut Input, + value: &str, +) -> Result<(), String> { + let v = input.args.pop_front().ok_or_else(|| "Missing argument")?; + if value != v { + return Err(format!("Unexpected argument {v} (expected: {value}")); + } + Ok(()) +} + +// Removes the next argument if it equals `value`. +// +// Returns if it was equal. +pub(crate) fn shift_arg_if( + input: &mut Input, + value: &str, +) -> Result { + let eq = input.args.front().ok_or_else(|| "Missing argument")? == value; + if eq { + input.args.pop_front(); + } + Ok(eq) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn empty_state() { + let host = Host::new(); + + assert_eq!(0, host.global.id); + assert!(host.global.links.is_empty()); + assert!(host.global.ip_interfaces.is_empty()); + assert!(host.global.routes.is_empty()); + assert!(host.global.services.is_empty()); + assert!(host.zones.is_empty()); + } +} diff --git a/helios/tokamak/src/host/route.rs b/helios/tokamak/src/host/route.rs new file mode 100644 index 00000000000..23de4a279b9 --- /dev/null +++ b/helios/tokamak/src/host/route.rs @@ -0,0 +1,96 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// 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/. + +use crate::host::{no_args_remaining, shift_arg, shift_arg_if}; +use crate::host::{LinkName, RouteTarget}; + +use helios_fusion::Input; +use helios_fusion::ROUTE; + +pub(crate) enum Command { + Add { + destination: RouteTarget, + gateway: RouteTarget, + interface: Option, + }, +} + +impl TryFrom for Command { + type Error = String; + + fn try_from(mut input: Input) -> Result { + if input.program != ROUTE { + return Err(format!("Not route command: {}", input.program)); + } + + match shift_arg(&mut input)?.as_str() { + "add" => { + let destination = RouteTarget::shift_target(&mut input)?; + let gateway = RouteTarget::shift_target(&mut input)?; + + let interface = + if let Ok(true) = shift_arg_if(&mut input, "-ifp") { + Some(LinkName(shift_arg(&mut input)?)) + } else { + None + }; + no_args_remaining(&input)?; + Ok(Command::Add { destination, gateway, interface }) + } + command => return Err(format!("Unsupported command: {}", command)), + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use ipnetwork::IpNetwork; + use std::str::FromStr; + + #[test] + fn add() { + // Valid command + let Command::Add { destination, gateway, interface } = + Command::try_from(Input::shell(format!( + "{ROUTE} add -inet6 fd00::/16 default -ifp mylink" + ))) + .unwrap(); + assert_eq!( + destination, + RouteTarget::ByAddress(IpNetwork::from_str("fd00::/16").unwrap()) + ); + assert_eq!(gateway, RouteTarget::Default); + assert_eq!(interface.unwrap().0, "mylink"); + + // Valid command + let Command::Add { destination, gateway, interface } = + Command::try_from(Input::shell(format!( + "{ROUTE} add -inet default 127.0.0.1/8" + ))) + .unwrap(); + assert_eq!(destination, RouteTarget::DefaultV4); + assert_eq!( + gateway, + RouteTarget::ByAddress(IpNetwork::from_str("127.0.0.1/8").unwrap()) + ); + assert!(interface.is_none()); + + // Invalid address family + assert!(Command::try_from(Input::shell(format!( + "{ROUTE} add -inet -inet6 default 127.0.0.1/8" + ))) + .err() + .unwrap() + .contains("Cannot force both v4 and v6")); + + // Invalid address family + assert!(Command::try_from(Input::shell(format!( + "{ROUTE} add -inet6 default -inet6 127.0.0.1/8" + ))) + .err() + .unwrap() + .contains("127.0.0.1/8 is not ipv6")); + } +} diff --git a/helios/tokamak/src/host/svcadm.rs b/helios/tokamak/src/host/svcadm.rs new file mode 100644 index 00000000000..dcedb3aa57f --- /dev/null +++ b/helios/tokamak/src/host/svcadm.rs @@ -0,0 +1,91 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// 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/. + +use crate::host::{no_args_remaining, shift_arg, shift_arg_if}; +use crate::host::{ServiceName, ZoneName}; + +use helios_fusion::Input; +use helios_fusion::SVCADM; + +pub enum Command { + Enable { zone: Option, service: ServiceName }, + Disable { zone: Option, service: ServiceName }, +} + +impl TryFrom for Command { + type Error = String; + + fn try_from(mut input: Input) -> Result { + if input.program != SVCADM { + return Err(format!("Not svcadm command: {}", input.program)); + } + + let zone = if shift_arg_if(&mut input, "-z")? { + Some(ZoneName(shift_arg(&mut input)?)) + } else { + None + }; + + match shift_arg(&mut input)?.as_str() { + "enable" => { + // Intentionally ignored + shift_arg_if(&mut input, "-t")?; + let service = ServiceName(shift_arg(&mut input)?); + no_args_remaining(&input)?; + Ok(Command::Enable { zone, service }) + } + "disable" => { + // Intentionally ignored + shift_arg_if(&mut input, "-t")?; + let service = ServiceName(shift_arg(&mut input)?); + no_args_remaining(&input)?; + Ok(Command::Disable { zone, service }) + } + command => return Err(format!("Unexpected command: {command}")), + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn enable() { + let Command::Enable { zone, service } = Command::try_from( + Input::shell(format!( + "{SVCADM} -z myzone enable -t foobar" + )), + ).unwrap() else { + panic!("wrong command"); + }; + + assert_eq!(zone.unwrap().0, "myzone"); + assert_eq!(service.0, "foobar"); + + assert!(Command::try_from(Input::shell(format!("{SVCADM} enable"))) + .err() + .unwrap() + .contains("Missing argument")); + } + + #[test] + fn disable() { + let Command::Disable { zone, service } = Command::try_from( + Input::shell(format!( + "{SVCADM} -z myzone disable -t foobar" + )), + ).unwrap() else { + panic!("wrong command"); + }; + + assert_eq!(zone.unwrap().0, "myzone"); + assert_eq!(service.0, "foobar"); + + assert!(Command::try_from(Input::shell(format!("{SVCADM} disable"))) + .err() + .unwrap() + .contains("Missing argument")); + } +} diff --git a/helios/tokamak/src/host/svccfg.rs b/helios/tokamak/src/host/svccfg.rs new file mode 100644 index 00000000000..40ab730b571 --- /dev/null +++ b/helios/tokamak/src/host/svccfg.rs @@ -0,0 +1,369 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// 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/. + +use crate::host::{ + no_args_remaining, shift_arg, shift_arg_expect, shift_arg_if, +}; +use crate::host::{ServiceName, ZoneName}; + +use camino::Utf8PathBuf; +use helios_fusion::Input; +use helios_fusion::SVCCFG; +use std::str::FromStr; + +pub(crate) enum Command { + Addpropvalue { + zone: Option, + fmri: ServiceName, + key: smf::PropertyName, + ty: Option, + value: String, + }, + Addpg { + zone: Option, + fmri: ServiceName, + group: smf::PropertyGroupName, + group_type: String, + }, + Delpg { + zone: Option, + fmri: ServiceName, + group: smf::PropertyGroupName, + }, + Delpropvalue { + zone: Option, + fmri: ServiceName, + name: smf::PropertyName, + glob: String, + }, + Import { + zone: Option, + file: Utf8PathBuf, + }, + Refresh { + zone: Option, + fmri: ServiceName, + }, + Setprop { + zone: Option, + fmri: ServiceName, + name: smf::PropertyName, + value: String, + }, +} + +impl TryFrom for Command { + type Error = String; + + fn try_from(mut input: Input) -> Result { + if input.program != SVCCFG { + return Err(format!("Not svccfg command: {}", input.program)); + } + + let zone = if shift_arg_if(&mut input, "-z")? { + Some(ZoneName(shift_arg(&mut input)?)) + } else { + None + }; + + let fmri = if shift_arg_if(&mut input, "-s")? { + Some(ServiceName(shift_arg(&mut input)?)) + } else { + None + }; + + match shift_arg(&mut input)?.as_str() { + "addpropvalue" => { + let name = shift_arg(&mut input)?; + let name = smf::PropertyName::from_str(&name) + .map_err(|e| e.to_string())?; + + let type_or_value = shift_arg(&mut input)?; + let (ty, value) = match input.args.pop_front() { + Some(value) => { + let ty = type_or_value + .strip_suffix(':') + .ok_or_else(|| { + format!("Bad property type: {type_or_value}") + })? + .to_string(); + (Some(ty), value) + } + None => (None, type_or_value), + }; + + let fmri = fmri.ok_or_else(|| { + format!("-s option required for addpropvalue") + })?; + + no_args_remaining(&input)?; + Ok(Command::Addpropvalue { zone, fmri, key: name, ty, value }) + } + "addpg" => { + let name = shift_arg(&mut input)?; + let group = smf::PropertyGroupName::new(&name) + .map_err(|e| e.to_string())?; + + let group_type = shift_arg(&mut input)?; + if let Some(flags) = input.args.pop_front() { + return Err( + "Parsing of optional flags not implemented".to_string() + ); + } + let fmri = fmri + .ok_or_else(|| format!("-s option required for addpg"))?; + + no_args_remaining(&input)?; + Ok(Command::Addpg { zone, fmri, group, group_type }) + } + "delpg" => { + let name = shift_arg(&mut input)?; + let group = smf::PropertyGroupName::new(&name) + .map_err(|e| e.to_string())?; + let fmri = fmri + .ok_or_else(|| format!("-s option required for delpg"))?; + + no_args_remaining(&input)?; + Ok(Command::Delpg { zone, fmri, group }) + } + "delpropvalue" => { + let name = shift_arg(&mut input)?; + let name = smf::PropertyName::from_str(&name) + .map_err(|e| e.to_string())?; + let fmri = fmri.ok_or_else(|| { + format!("-s option required for delpropvalue") + })?; + let glob = shift_arg(&mut input)?; + + no_args_remaining(&input)?; + Ok(Command::Delpropvalue { zone, fmri, name, glob }) + } + "import" => { + let file = shift_arg(&mut input)?; + if let Some(_) = fmri { + return Err( + "Cannot use '-s' option with import".to_string() + ); + } + no_args_remaining(&input)?; + Ok(Command::Import { zone, file: file.into() }) + } + "refresh" => { + let fmri = fmri + .ok_or_else(|| format!("-s option required for refresh"))?; + no_args_remaining(&input)?; + Ok(Command::Refresh { zone, fmri }) + } + "setprop" => { + let fmri = fmri + .ok_or_else(|| format!("-s option required for setprop"))?; + + // Setprop seems fine accepting args of the form: + // - name=value + // - name = value + // - name = type: value (NOTE: not yet supported) + let first_arg = shift_arg(&mut input)?; + let (name, value) = + if let Some((name, value)) = first_arg.split_once('=') { + (name.to_string(), value.to_string()) + } else { + let name = first_arg; + shift_arg_expect(&mut input, "=")?; + let value = shift_arg(&mut input)?; + (name, value.to_string()) + }; + + let name = smf::PropertyName::from_str(&name) + .map_err(|e| e.to_string())?; + + no_args_remaining(&input)?; + Ok(Command::Setprop { zone, fmri, name, value }) + } + command => return Err(format!("Unexpected command: {command}")), + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn addpropvalue() { + let Command::Addpropvalue { zone, fmri, key, ty, value } = Command::try_from( + Input::shell(format!( + "{SVCCFG} -z myzone -s svc:/myservice:default addpropvalue foo/bar astring: baz" + )) + ).unwrap() else { + panic!("Wrong command"); + }; + + assert_eq!(zone.unwrap().0, "myzone"); + assert_eq!(fmri.0, "svc:/myservice:default"); + assert_eq!(key.to_string(), "foo/bar"); + assert_eq!(ty, Some("astring".to_string())); + assert_eq!(value, "baz"); + + assert!(Command::try_from(Input::shell(format!( + "{SVCCFG} addpropvalue foo/bar baz" + ))) + .err() + .unwrap() + .contains("-s option required")); + + assert!(Command::try_from(Input::shell(format!( + "{SVCCFG} -s svc:/mysvc addpropvalue foo/bar astring baz" + ))) + .err() + .unwrap() + .contains("Bad property type")); + } + + #[test] + fn addpg() { + let Command::Addpg { zone, fmri, group, group_type } = Command::try_from( + Input::shell(format!( + "{SVCCFG} -z myzone -s svc:/myservice:default addpg foo baz" + )) + ).unwrap() else { + panic!("Wrong command"); + }; + + assert_eq!(zone.unwrap().0, "myzone"); + assert_eq!(fmri.0, "svc:/myservice:default"); + assert_eq!(group.to_string(), "foo"); + assert_eq!(group_type, "baz"); + + assert!(Command::try_from(Input::shell(format!( + "{SVCCFG} addpg foo baz" + ))) + .err() + .unwrap() + .contains("-s option required")); + + assert!(Command::try_from(Input::shell(format!( + "{SVCCFG} addpg foo baz P" + ))) + .err() + .unwrap() + .contains("Parsing of optional flags not implemented")); + } + + #[test] + fn delpg() { + let Command::Delpg { zone, fmri, group } = Command::try_from( + Input::shell(format!( + "{SVCCFG} -z myzone -s svc:/myservice:default delpg foo" + )) + ).unwrap() else { + panic!("Wrong command"); + }; + + assert_eq!(zone.unwrap().0, "myzone"); + assert_eq!(fmri.0, "svc:/myservice:default"); + assert_eq!(group.to_string(), "foo"); + + assert!(Command::try_from(Input::shell(format!("{SVCCFG} delpg foo"))) + .err() + .unwrap() + .contains("-s option required")); + + assert!(Command::try_from(Input::shell(format!( + "{SVCCFG} -s mysvc delpg foo baz" + ))) + .err() + .unwrap() + .contains("Unexpected extra arguments")); + } + + #[test] + fn import() { + let Command::Import { zone, file } = Command::try_from( + Input::shell(format!( + "{SVCCFG} -z myzone import myfile" + )) + ).unwrap() else { + panic!("Wrong command"); + }; + + assert_eq!(zone.unwrap().0, "myzone"); + assert_eq!(file, "myfile"); + + assert!(Command::try_from(Input::shell(format!( + "{SVCCFG} import myfile myotherfile" + ))) + .err() + .unwrap() + .contains("Unexpected extra arguments")); + + assert!(Command::try_from(Input::shell(format!( + "{SVCCFG} -s myservice import myfile" + ))) + .err() + .unwrap() + .contains("Cannot use '-s' option with import")); + } + + #[test] + fn refresh() { + let Command::Refresh { zone, fmri } = Command::try_from( + Input::shell(format!( + "{SVCCFG} -z myzone -s myservice refresh" + )) + ).unwrap() else { + panic!("Wrong command"); + }; + + assert_eq!(zone.unwrap().0, "myzone"); + assert_eq!(fmri.0, "myservice"); + } + + #[test] + fn setprop() { + let Command::Setprop { zone, fmri, name, value } = Command::try_from( + Input::shell(format!( + "{SVCCFG} -z myzone -s myservice setprop foo/bar=baz" + )) + ).unwrap() else { + panic!("Wrong command"); + }; + + assert_eq!(zone.unwrap().0, "myzone"); + assert_eq!(fmri.0, "myservice"); + assert_eq!(name.to_string(), "foo/bar"); + assert_eq!(value, "baz"); + + // Try that command again, but with spaces + let Command::Setprop { zone, fmri, name, value } = Command::try_from( + Input::shell(format!( + "{SVCCFG} -z myzone -s myservice setprop foo/bar = baz" + )) + ).unwrap() else { + panic!("Wrong command"); + }; + assert_eq!(zone.unwrap().0, "myzone"); + assert_eq!(fmri.0, "myservice"); + assert_eq!(name.to_string(), "foo/bar"); + assert_eq!(value, "baz"); + + // Try that command again, but with quotes + let Command::Setprop { zone, fmri, name, value } = Command::try_from( + Input::shell(format!( + "{SVCCFG} -z myzone -s myservice setprop foo/bar = \"fizz buzz\"" + )) + ).unwrap() else { + panic!("Wrong command"); + }; + assert_eq!(zone.unwrap().0, "myzone"); + assert_eq!(fmri.0, "myservice"); + assert_eq!(name.to_string(), "foo/bar"); + assert_eq!(value, "fizz buzz"); + + assert!(Command::try_from( + Input::shell(format!( + "{SVCCFG} -z myzone -s myservice setprop foo/bar = \"fizz buzz\" blat" + )) + ).err().unwrap().contains("Unexpected extra arguments")); + } +} diff --git a/helios/tokamak/src/host/zfs.rs b/helios/tokamak/src/host/zfs.rs new file mode 100644 index 00000000000..7e668c36e7f --- /dev/null +++ b/helios/tokamak/src/host/zfs.rs @@ -0,0 +1,447 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// 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/. + +use crate::host::FilesystemName; +use crate::host::{no_args_remaining, shift_arg, shift_arg_if}; + +use helios_fusion::Input; +use helios_fusion::ZFS; + +pub(crate) enum Command { + CreateFilesystem { + properties: Vec<(String, String)>, + name: FilesystemName, + }, + CreateVolume { + properties: Vec<(String, String)>, + sparse: bool, + blocksize: Option, + size: u64, + name: FilesystemName, + }, + Destroy { + recursive_dependents: bool, + recursive_children: bool, + force_unmount: bool, + name: FilesystemName, + }, + Get { + recursive: bool, + depth: Option, + // name, property, value, source + fields: Vec, + properties: Vec, + datasets: Option>, + }, + List { + recursive: bool, + depth: Option, + properties: Vec, + datasets: Option>, + }, + Mount { + load_keys: bool, + filesystem: FilesystemName, + }, + Set { + properties: Vec<(String, String)>, + name: FilesystemName, + }, +} + +impl TryFrom for Command { + type Error = String; + + fn try_from(mut input: Input) -> Result { + if input.program != ZFS { + return Err(format!("Not zfs command: {}", input.program)); + } + + match shift_arg(&mut input)?.as_str() { + "create" => { + let mut size = None; + let mut blocksize = None; + let mut sparse = None; + let mut properties = vec![]; + + while input.args.len() > 1 { + // Volume Size (volumes only, required) + if shift_arg_if(&mut input, "-V")? { + size = Some( + shift_arg(&mut input)? + .parse::() + .map_err(|e| e.to_string())?, + ); + // Sparse (volumes only, optional) + } else if shift_arg_if(&mut input, "-s")? { + sparse = Some(true); + // Block size (volumes only, optional) + } else if shift_arg_if(&mut input, "-b")? { + blocksize = Some( + shift_arg(&mut input)? + .parse::() + .map_err(|e| e.to_string())?, + ); + // Properties + } else if shift_arg_if(&mut input, "-o")? { + let prop = shift_arg(&mut input)?; + let (k, v) = prop + .split_once('=') + .ok_or_else(|| format!("Bad property: {prop}"))?; + properties.push((k.to_string(), v.to_string())); + } + } + let name = FilesystemName(shift_arg(&mut input)?); + no_args_remaining(&input)?; + + if let Some(size) = size { + // Volume + let sparse = sparse.unwrap_or(false); + Ok(Command::CreateVolume { + properties, + sparse, + blocksize, + size, + name, + }) + } else { + // Filesystem + if sparse.is_some() || blocksize.is_some() { + return Err("Using volume arguments, but forgot to specify '-V size'?".to_string()); + } + Ok(Command::CreateFilesystem { properties, name }) + } + } + "destroy" => { + let mut recursive_dependents = false; + let mut recursive_children = false; + let mut force_unmount = false; + let mut name = None; + + while !input.args.is_empty() { + let arg = shift_arg(&mut input)?; + let mut chars = arg.chars(); + if let Some('-') = chars.next() { + while let Some(c) = chars.next() { + match c { + 'R' => recursive_dependents = true, + 'r' => recursive_children = true, + 'f' => force_unmount = true, + c => { + return Err(format!( + "Unrecognized option '-{c}'" + )) + } + } + } + } else { + name = Some(FilesystemName(arg)); + no_args_remaining(&input)?; + } + } + let name = name.ok_or_else(|| "Missing name".to_string())?; + Ok(Command::Destroy { + recursive_dependents, + recursive_children, + force_unmount, + name, + }) + } + "get" => { + let mut scripting = false; + let mut parsable = false; + let mut recursive = false; + let mut depth = None; + let mut fields = ["name", "property", "value", "source"] + .map(String::from) + .to_vec(); + let mut properties = vec![]; + + while !input.args.is_empty() { + let arg = shift_arg(&mut input)?; + let mut chars = arg.chars(); + // ZFS list lets callers pass in flags in groups, or + // separately. + if let Some('-') = chars.next() { + while let Some(c) = chars.next() { + match c { + 'r' => recursive = true, + 'H' => scripting = true, + 'p' => parsable = true, + 'd' => { + let depth_raw = + if chars.clone().next().is_some() { + chars.collect::() + } else { + shift_arg(&mut input)? + }; + depth = Some( + depth_raw + .parse::() + .map_err(|e| e.to_string())?, + ); + // Convince the compiler we won't use any + // more 'chars', because used them all + // parsing 'depth'. + break; + } + 'o' => { + if chars.next().is_some() { + return Err("-o should be immediately followed by fields".to_string()); + } + fields = shift_arg(&mut input)? + .split(',') + .map(|s| s.to_string()) + .collect(); + } + c => { + return Err(format!( + "Unrecognized option '-{c}'" + )) + } + } + } + } else { + properties = + arg.split(',').map(|s| s.to_string()).collect(); + break; + } + } + + let datasets = Some( + std::mem::take(&mut input.args) + .into_iter() + .collect::>(), + ); + if !scripting || !parsable { + return Err("You should run 'zfs get' commands with the '-Hp' flags enabled".to_string()); + } + + Ok(Command::Get { + recursive, + depth, + fields, + properties, + datasets, + }) + } + "list" => { + let mut scripting = false; + let mut parsable = false; + let mut recursive = false; + let mut depth = None; + let mut properties = vec![]; + let mut datasets = None; + + while !input.args.is_empty() { + let arg = shift_arg(&mut input)?; + let mut chars = arg.chars(); + // ZFS list lets callers pass in flags in groups, or + // separately. + if let Some('-') = chars.next() { + while let Some(c) = chars.next() { + match c { + 'r' => recursive = true, + 'H' => scripting = true, + 'p' => parsable = true, + 'd' => { + let depth_raw = + if chars.clone().next().is_some() { + chars.collect::() + } else { + shift_arg(&mut input)? + }; + depth = Some( + depth_raw + .parse::() + .map_err(|e| e.to_string())?, + ); + // Convince the compiler we won't use any + // more 'chars', because used them all + // parsing 'depth'. + break; + } + 'o' => { + if chars.next().is_some() { + return Err("-o should be immediately followed by properties".to_string()); + } + properties = shift_arg(&mut input)? + .split(',') + .map(|s| s.to_string()) + .collect(); + } + c => { + return Err(format!( + "Unrecognized option '-{c}'" + )) + } + } + } + } else { + // As soon as non-flag arguments are passed, the rest of + // the arguments are treated as datasets. + datasets = Some(vec![arg]); + break; + } + } + + let remaining_datasets = std::mem::take(&mut input.args); + if !remaining_datasets.is_empty() { + datasets + .get_or_insert(vec![]) + .extend(remaining_datasets.into_iter()); + }; + + if !scripting || !parsable { + return Err("You should run 'zfs list' commands with the '-Hp' flags enabled".to_string()); + } + + Ok(Command::List { recursive, depth, properties, datasets }) + } + "mount" => { + let load_keys = shift_arg_if(&mut input, "-l")?; + let filesystem = FilesystemName(shift_arg(&mut input)?); + no_args_remaining(&input)?; + Ok(Command::Mount { load_keys, filesystem }) + } + "set" => { + let mut properties = vec![]; + + while input.args.len() > 1 { + let prop = shift_arg(&mut input)?; + let (k, v) = prop + .split_once('=') + .ok_or_else(|| format!("Bad property: {prop}"))?; + properties.push((k.to_string(), v.to_string())); + } + let name = FilesystemName(shift_arg(&mut input)?); + no_args_remaining(&input)?; + + Ok(Command::Set { properties, name }) + } + command => return Err(format!("Unexpected command: {command}")), + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn create() { + let Command::CreateFilesystem { properties, name } = Command::try_from( + Input::shell(format!("{ZFS} create myfilesystem")) + ).unwrap() else { panic!("wrong command") }; + + assert_eq!(properties, vec![]); + assert_eq!(name.0, "myfilesystem"); + + let Command::CreateVolume { properties, sparse, blocksize, size, name } = Command::try_from( + Input::shell(format!("{ZFS} create -s -V 1024 -b 512 -o foo=bar myvolume")) + ).unwrap() else { panic!("wrong command") }; + + assert_eq!(properties, vec![("foo".to_string(), "bar".to_string())]); + assert_eq!(name.0, "myvolume"); + assert!(sparse); + assert_eq!(size, 1024); + assert_eq!(blocksize, Some(512)); + + assert!(Command::try_from(Input::shell(format!( + "{ZFS} create -s -b 512 -o foo=bar myvolume" + ))) + .err() + .unwrap() + .contains("Using volume arguments, but forgot to specify '-V size'")); + } + + #[test] + fn destroy() { + let Command::Destroy { recursive_dependents, recursive_children, force_unmount, name } = + Command::try_from( + Input::shell(format!("{ZFS} destroy -rf foobar")) + ).unwrap() else { panic!("wrong command") }; + + assert!(!recursive_dependents); + assert!(recursive_children); + assert!(force_unmount); + assert_eq!(name.0, "foobar"); + + assert!(Command::try_from(Input::shell(format!( + "{ZFS} destroy -x doit" + ))) + .err() + .unwrap() + .contains("Unrecognized option '-x'")); + } + + #[test] + fn get() { + let Command::Get { recursive, depth, fields, properties, datasets } = Command::try_from( + Input::shell(format!("{ZFS} get -Hrpd10 -o name,value mounted,available myvolume")) + ).unwrap() else { panic!("wrong command") }; + + assert!(recursive); + assert_eq!(depth, Some(10)); + assert_eq!(fields, vec!["name", "value"]); + assert_eq!(properties, vec!["mounted", "available"]); + assert_eq!(datasets.unwrap(), vec!["myvolume"]); + + assert!(Command::try_from(Input::shell(format!( + "{ZFS} get -o name,value mounted,available myvolume" + ))) + .err() + .unwrap() + .contains( + "You should run 'zfs get' commands with the '-Hp' flags enabled" + )); + } + + #[test] + fn list() { + let Command::List { recursive, depth, properties, datasets } = Command::try_from( + Input::shell(format!("{ZFS} list -d 1 -rHpo name myfilesystem")) + ).unwrap() else { panic!("wrong command") }; + + assert!(recursive); + assert_eq!(depth.unwrap(), 1); + assert_eq!(properties, vec!["name"]); + assert_eq!(datasets.unwrap(), vec!["myfilesystem"]); + + assert!(Command::try_from(Input::shell(format!( + "{ZFS} list name myfilesystem" + ))) + .err() + .unwrap() + .contains( + "You should run 'zfs list' commands with the '-Hp' flags enabled" + )); + } + + #[test] + fn mount() { + let Command::Mount { load_keys, filesystem } = Command::try_from( + Input::shell(format!("{ZFS} mount -l foobar")) + ).unwrap() else { panic!("wrong command") }; + + assert!(load_keys); + assert_eq!(filesystem.0, "foobar"); + } + + #[test] + fn set() { + let Command::Set { properties, name } = Command::try_from( + Input::shell(format!("{ZFS} set foo=bar baz=blat myfs")) + ).unwrap() else { panic!("wrong command") }; + + assert_eq!( + properties, + vec![ + ("foo".to_string(), "bar".to_string()), + ("baz".to_string(), "blat".to_string()) + ] + ); + assert_eq!(name.0, "myfs"); + } +} diff --git a/helios/tokamak/src/host/zoneadm.rs b/helios/tokamak/src/host/zoneadm.rs new file mode 100644 index 00000000000..080d0cf0cea --- /dev/null +++ b/helios/tokamak/src/host/zoneadm.rs @@ -0,0 +1,118 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// 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/. + +use crate::host::ZoneName; +use crate::host::{no_args_remaining, shift_arg, shift_arg_if}; + +use helios_fusion::Input; +use helios_fusion::ZONEADM; + +pub(crate) enum Command { + Boot { + name: ZoneName, + }, + Halt { + name: ZoneName, + }, + Install { + name: ZoneName, + brand_specific_args: Vec, + }, + List { + // Overrides the "list_installed" option + list_configured: bool, + list_installed: bool, + }, + Uninstall { + name: ZoneName, + force: bool, + }, +} + +impl TryFrom for Command { + type Error = String; + + fn try_from(mut input: Input) -> Result { + if input.program != ZONEADM { + return Err(format!("Not zoneadm command: {}", input.program)); + } + + let name = if shift_arg_if(&mut input, "-z")? { + Some(ZoneName(shift_arg(&mut input)?)) + } else { + None + }; + + match shift_arg(&mut input)?.as_str() { + "boot" => { + no_args_remaining(&input)?; + let name = name.ok_or_else(|| { + "No zone specified, try: zoneadm -z ZONE boot" + })?; + Ok(Command::Boot { name }) + } + "halt" => { + no_args_remaining(&input)?; + let name = name.ok_or_else(|| { + "No zone specified, try: zoneadm -z ZONE halt" + })?; + Ok(Command::Halt { name }) + } + "install" => { + let brand_specific_args = + std::mem::take(&mut input.args).into_iter().collect(); + let name = name.ok_or_else(|| { + "No zone specified, try: zoneadm -z ZONE install" + })?; + Ok(Command::Install { name, brand_specific_args }) + } + "list" => { + let mut list_configured = false; + let mut list_installed = false; + let mut parsable = false; + + while !input.args.is_empty() { + let arg = shift_arg(&mut input)?; + let mut chars = arg.chars(); + + if let Some('-') = chars.next() { + while let Some(c) = chars.next() { + match c { + 'c' => list_configured = true, + 'i' => list_installed = true, + 'p' => parsable = true, + c => { + return Err(format!( + "Unrecognized option '-{c}'" + )) + } + } + } + } else { + return Err(format!("Unrecognized argument {arg}")); + } + } + + if !parsable { + return Err("You should run 'zoneadm list' commands with the '-p' flag enabled".to_string()); + } + + Ok(Command::List { list_configured, list_installed }) + } + "uninstall" => { + let name = name.ok_or_else(|| { + "No zone specified, try: zoneadm -z ZONE uninstall" + })?; + let force = if !input.args.is_empty() { + shift_arg_if(&mut input, "-F")? + } else { + false + }; + no_args_remaining(&input)?; + Ok(Command::Uninstall { name, force }) + } + command => return Err(format!("Unexpected command: {command}")), + } + } +} diff --git a/helios/tokamak/src/host/zonecfg.rs b/helios/tokamak/src/host/zonecfg.rs new file mode 100644 index 00000000000..5c1480fec9b --- /dev/null +++ b/helios/tokamak/src/host/zonecfg.rs @@ -0,0 +1,283 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// 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/. + +use crate::host::{shift_arg, shift_arg_expect}; +use crate::host::{ZoneConfig, ZoneName}; + +use camino::Utf8PathBuf; +use helios_fusion::Input; +use helios_fusion::ZONECFG; + +pub(crate) enum Command { + Create { name: ZoneName, config: ZoneConfig }, + Delete { name: ZoneName }, +} + +impl TryFrom for Command { + type Error = String; + + fn try_from(mut input: Input) -> Result { + if input.program != ZONECFG { + return Err(format!("Not zonecfg command: {}", input.program)); + } + shift_arg_expect(&mut input, "-z")?; + let zone = ZoneName(shift_arg(&mut input)?); + match shift_arg(&mut input)?.as_str() { + "create" => { + shift_arg_expect(&mut input, "-F")?; + shift_arg_expect(&mut input, "-b")?; + + enum Scope { + Global, + Dataset(zone::Dataset), + Device(zone::Device), + Fs(zone::Fs), + Net(zone::Net), + } + let mut scope = Scope::Global; + + // Globally-scoped Resources + let mut brand = None; + let mut path = None; + + // Non-Global Resources + let mut datasets = vec![]; + let mut devices = vec![]; + let mut nets = vec![]; + let mut fs = vec![]; + + while !input.args.is_empty() { + shift_arg_expect(&mut input, ";")?; + match shift_arg(&mut input)?.as_str() { + "set" => { + let prop = shift_arg(&mut input)?; + let (k, v) = + prop.split_once('=').ok_or_else(|| { + format!("Bad property: {prop}") + })?; + + match &mut scope { + Scope::Global => { + match k { + "brand" => { + brand = Some(v.to_string()); + } + "zonepath" => { + path = Some(Utf8PathBuf::from(v)); + } + "autoboot" => { + if v != "false" { + return Err(format!("Unhandled autoboot value: {v}")); + } + } + "ip-type" => { + if v != "exclusive" { + return Err(format!("Unhandled ip-type value: {v}")); + } + } + k => { + return Err(format!( + "Unknown property name: {k}" + )) + } + } + } + Scope::Dataset(d) => match k { + "name" => d.name = v.to_string(), + k => { + return Err(format!( + "Unknown property name: {k}" + )) + } + }, + Scope::Device(d) => match k { + "match" => d.name = v.to_string(), + k => { + return Err(format!( + "Unknown property name: {k}" + )) + } + }, + Scope::Fs(f) => match k { + "type" => f.ty = v.to_string(), + "dir" => f.dir = v.to_string(), + "special" => f.special = v.to_string(), + "raw" => f.raw = Some(v.to_string()), + "options" => { + f.options = v + .split(',') + .map(|s| s.to_string()) + .collect() + } + k => { + return Err(format!( + "Unknown property name: {k}" + )) + } + }, + Scope::Net(n) => match k { + "physical" => n.physical = v.to_string(), + "address" => { + n.address = Some(v.to_string()) + } + "allowed-address" => { + n.allowed_address = Some(v.to_string()) + } + k => { + return Err(format!( + "Unknown property name: {k}" + )) + } + }, + } + } + "add" => { + if !matches!(scope, Scope::Global) { + return Err("Cannot add from non-global scope" + .to_string()); + } + match shift_arg(&mut input)?.as_str() { + "dataset" => { + scope = + Scope::Dataset(zone::Dataset::default()) + } + "device" => { + scope = + Scope::Device(zone::Device::default()) + } + "fs" => scope = Scope::Fs(zone::Fs::default()), + "net" => { + scope = Scope::Net(zone::Net::default()) + } + scope => { + return Err(format!( + "Unexpected scope: {scope}" + )) + } + } + } + "end" => { + match scope { + Scope::Global => { + return Err( + "Cannot end global scope".to_string() + ) + } + Scope::Dataset(d) => datasets.push(d), + Scope::Device(d) => devices.push(d), + Scope::Fs(f) => fs.push(f), + Scope::Net(n) => nets.push(n), + } + scope = Scope::Global; + } + sc => { + return Err(format!("Unexpected subcommand: {sc}")) + } + } + } + + if !matches!(scope, Scope::Global) { + return Err( + "Cannot end zonecfg outside global scope".to_string() + ); + } + + Ok(Command::Create { + name: zone, + config: ZoneConfig { + state: zone::State::Configured, + brand: brand.ok_or_else(|| "Missing brand")?, + path: path.ok_or_else(|| "Missing zonepath")?, + datasets, + devices, + nets, + fs, + layers: vec![], + }, + }) + } + "delete" => { + shift_arg_expect(&mut input, "-F")?; + Ok(Command::Delete { name: zone }) + } + command => return Err(format!("Unexpected command: {command}")), + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn create() { + let Command::Create { name, config } = Command::try_from( + Input::shell(format!( + "{ZONECFG} -z myzone \ + create -F -b ; \ + set brand=omicron1 ; \ + set zonepath=/zone/myzone ; \ + set autoboot=false ; \ + set ip-type=exclusive ; \ + add net ; \ + set physical=oxControlService0 ; \ + end" + )), + ).unwrap() else { + panic!("Wrong command"); + }; + + assert_eq!(name.0, "myzone"); + assert_eq!(config.state, zone::State::Configured); + assert_eq!(config.brand, "omicron1"); + assert_eq!(config.path, Utf8PathBuf::from("/zone/myzone")); + assert!(config.datasets.is_empty()); + assert_eq!(config.nets[0].physical, "oxControlService0"); + assert!(config.fs.is_empty()); + assert!(config.layers.is_empty()); + + // Missing brand + assert!(Command::try_from(Input::shell(format!( + "{ZONECFG} -z myzone \ + create -F -b ; \ + set zonepath=/zone/myzone" + )),) + .err() + .unwrap() + .contains("Missing brand")); + + // Missing zonepath + assert!(Command::try_from(Input::shell(format!( + "{ZONECFG} -z myzone \ + create -F -b ; \ + set brand=omicron1" + )),) + .err() + .unwrap() + .contains("Missing zonepath")); + + // Ending mid-scope + assert!(Command::try_from(Input::shell(format!( + "{ZONECFG} -z myzone \ + create -F -b ; \ + set brand=omicron1 ; \ + set zonepath=/zone/myzone ; \ + add net ; \ + set physical=oxControlService0" + )),) + .err() + .unwrap() + .contains("Cannot end zonecfg outside global scope")); + } + + #[test] + fn delete() { + let Command::Delete { name } = Command::try_from( + Input::shell(format!("{ZONECFG} -z myzone delete -F")), + ).unwrap() else { + panic!("Wrong command"); + }; + assert_eq!(name.0, "myzone"); + } +} diff --git a/helios/tokamak/src/host/zpool.rs b/helios/tokamak/src/host/zpool.rs new file mode 100644 index 00000000000..056c5b27973 --- /dev/null +++ b/helios/tokamak/src/host/zpool.rs @@ -0,0 +1,109 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// 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/. + +use crate::host::{no_args_remaining, shift_arg, shift_arg_if}; + +use helios_fusion::Input; +use helios_fusion::ZPOOL; + +// TODO: Consider using helios_fusion::zpool::ZpoolName here? + +pub(crate) enum Command { + Create { pool: String, vdev: String }, + Export { pool: String }, + Import { force: bool, pool: String }, + List { properties: Vec, pools: Option> }, + Set { property: String, value: String, pool: String }, +} + +impl TryFrom for Command { + type Error = String; + + fn try_from(mut input: Input) -> Result { + if input.program != ZPOOL { + return Err(format!("Not zpool command: {}", input.program)); + } + + match shift_arg(&mut input)?.as_str() { + "create" => { + let pool = shift_arg(&mut input)?; + let vdev = shift_arg(&mut input)?; + no_args_remaining(&input)?; + Ok(Command::Create { pool, vdev }) + } + "export" => { + let pool = shift_arg(&mut input)?; + no_args_remaining(&input)?; + Ok(Command::Export { pool }) + } + "import" => { + let force = shift_arg_if(&mut input, "-f")?; + let pool = shift_arg(&mut input)?; + Ok(Command::Import { force, pool }) + } + "list" => { + let mut scripting = false; + let mut parsable = false; + let mut properties = vec![]; + let mut pools = None; + + while !input.args.is_empty() { + let arg = shift_arg(&mut input)?; + let mut chars = arg.chars(); + // ZFS list lets callers pass in flags in groups, or + // separately. + if let Some('-') = chars.next() { + while let Some(c) = chars.next() { + match c { + 'H' => scripting = true, + 'p' => parsable = true, + 'o' => { + if chars.next().is_some() { + return Err("-o should be immediately followed by properties".to_string()); + } + properties = shift_arg(&mut input)? + .split(',') + .map(|s| s.to_string()) + .collect(); + } + c => { + return Err(format!( + "Unrecognized option '-{c}'" + )) + } + } + } + } else { + pools = Some(vec![arg]); + break; + } + } + + let remaining_pools = std::mem::take(&mut input.args); + if !remaining_pools.is_empty() { + pools + .get_or_insert(vec![]) + .extend(remaining_pools.into_iter()); + }; + if !scripting || !parsable { + return Err("You should run 'zpool list' commands with the '-Hp' flags enabled".to_string()); + } + Ok(Command::List { properties, pools }) + } + "set" => { + let prop = shift_arg(&mut input)?; + let (k, v) = prop + .split_once('=') + .ok_or_else(|| format!("Bad property: {prop}"))?; + let property = k.to_string(); + let value = v.to_string(); + + let pool = shift_arg(&mut input)?; + no_args_remaining(&input)?; + Ok(Command::Set { property, value, pool }) + } + command => return Err(format!("Unexpected command: {command}")), + } + } +} From 183a57e99fb1b4afb82d4d6da77ef10a4f72361c Mon Sep 17 00:00:00 2001 From: Sean Klein Date: Tue, 22 Aug 2023 15:06:08 -0700 Subject: [PATCH 07/18] Less destructive input parsing --- helios/fusion/src/input.rs | 3 +- helios/tokamak/src/host/dladm.rs | 132 +++++++++++++++-------------- helios/tokamak/src/host/ipadm.rs | 75 ++++++++-------- helios/tokamak/src/host/mod.rs | 101 ++++++---------------- helios/tokamak/src/host/parse.rs | 110 ++++++++++++++++++++++++ helios/tokamak/src/host/route.rs | 19 +++-- helios/tokamak/src/host/svcadm.rs | 22 ++--- helios/tokamak/src/host/svccfg.rs | 84 +++++++++--------- helios/tokamak/src/host/zfs.rs | 73 ++++++++-------- helios/tokamak/src/host/zoneadm.rs | 26 +++--- helios/tokamak/src/host/zonecfg.rs | 27 +++--- helios/tokamak/src/host/zpool.rs | 37 ++++---- 12 files changed, 399 insertions(+), 310 deletions(-) create mode 100644 helios/tokamak/src/host/parse.rs diff --git a/helios/fusion/src/input.rs b/helios/fusion/src/input.rs index 3d9728593fe..1450ebd42c0 100644 --- a/helios/fusion/src/input.rs +++ b/helios/fusion/src/input.rs @@ -2,14 +2,13 @@ // 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/. -use std::collections::VecDeque; use std::process::Command; /// Wrapper around the input of a [std::process::Command] as strings. #[derive(Clone, Debug, Eq, PartialEq)] pub struct Input { pub program: String, - pub args: VecDeque, + pub args: Vec, pub envs: Vec<(String, String)>, } diff --git a/helios/tokamak/src/host/dladm.rs b/helios/tokamak/src/host/dladm.rs index 83ce572cacf..f0cb4be318f 100644 --- a/helios/tokamak/src/host/dladm.rs +++ b/helios/tokamak/src/host/dladm.rs @@ -2,8 +2,8 @@ // 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/. +use crate::host::parse::InputParser; use crate::host::LinkName; -use crate::host::{no_args_remaining, shift_arg, shift_arg_if}; use helios_fusion::Input; use helios_fusion::DLADM; @@ -64,22 +64,22 @@ impl TryFrom for Command { return Err(format!("Not dladm command: {}", input.program)); } - match shift_arg(&mut input)?.as_str() { + let mut input = InputParser::new(input); + + match input.shift_arg()?.as_str() { "create-vnic" => { let mut link = None; let mut temporary = false; let mut mac = None; let mut vlan = None; let mut properties = HashMap::new(); - let name = LinkName( - input.args.pop_back().ok_or_else(|| "Missing name")?, - ); + let name = LinkName(input.shift_last_arg()?); - while !input.args.is_empty() { - if shift_arg_if(&mut input, "-t")? { + while !input.args().is_empty() { + if input.shift_arg_if("-t")? { temporary = true; - } else if shift_arg_if(&mut input, "-p")? { - let props = shift_arg(&mut input)?; + } else if input.shift_arg_if("-p")? { + let props = input.shift_arg()?; let props = props.split(','); for prop in props { let (k, v) = @@ -88,18 +88,21 @@ impl TryFrom for Command { })?; properties.insert(k.to_string(), v.to_string()); } - } else if shift_arg_if(&mut input, "-m")? { + } else if input.shift_arg_if("-m")? { // NOTE: Not yet supporting the keyword-based MACs. - mac = Some(shift_arg(&mut input)?); - } else if shift_arg_if(&mut input, "-l")? { - link = Some(LinkName(shift_arg(&mut input)?)); - } else if shift_arg_if(&mut input, "-v")? { + mac = Some(input.shift_arg()?); + } else if input.shift_arg_if("-l")? { + link = Some(LinkName(input.shift_arg()?)); + } else if input.shift_arg_if("-v")? { vlan = Some( - VlanID::from_str(&shift_arg(&mut input)?) + VlanID::from_str(&input.shift_arg()?) .map_err(|e| e.to_string())?, ); } else { - return Err(format!("Invalid arguments {}", input)); + return Err(format!( + "Invalid arguments {}", + input.input() + )); } } @@ -114,129 +117,131 @@ impl TryFrom for Command { } "create-etherstub" => { let mut temporary = false; - let name = LinkName( - input.args.pop_back().ok_or_else(|| "Missing name")?, - ); - while !input.args.is_empty() { - if shift_arg_if(&mut input, "-t")? { + let name = LinkName(input.shift_last_arg()?); + while !input.args().is_empty() { + if input.shift_arg_if("-t")? { temporary = true; } else { - return Err(format!("Invalid arguments {}", input)); + return Err(format!( + "Invalid arguments {}", + input.input() + )); } } Ok(Self::CreateEtherstub { temporary, name }) } "delete-etherstub" => { let mut temporary = false; - let name = LinkName( - input.args.pop_back().ok_or_else(|| "Missing name")?, - ); - while !input.args.is_empty() { - if shift_arg_if(&mut input, "-t")? { + let name = LinkName(input.shift_last_arg()?); + while !input.args().is_empty() { + if input.shift_arg_if("-t")? { temporary = true; } else { - return Err(format!("Invalid arguments {}", input)); + return Err(format!( + "Invalid arguments {}", + input.input() + )); } } Ok(Self::DeleteEtherstub { temporary, name }) } "delete-vnic" => { let mut temporary = false; - let name = LinkName( - input.args.pop_back().ok_or_else(|| "Missing name")?, - ); - while !input.args.is_empty() { - if shift_arg_if(&mut input, "-t")? { + let name = LinkName(input.shift_last_arg()?); + while !input.args().is_empty() { + if input.shift_arg_if("-t")? { temporary = true; } else { - return Err(format!("Invalid arguments {}", input)); + return Err(format!( + "Invalid arguments {}", + input.input() + )); } } Ok(Self::DeleteVnic { temporary, name }) } "show-etherstub" => { - let name = input.args.pop_back().map(|s| LinkName(s)); - no_args_remaining(&input)?; + let name = input.shift_last_arg().map(|s| LinkName(s)).ok(); + input.no_args_remaining()?; Ok(Self::ShowEtherstub { name }) } "show-link" => { - let name = LinkName( - input.args.pop_back().ok_or_else(|| "Missing name")?, - ); - if !shift_arg_if(&mut input, "-p")? { + let name = LinkName(input.shift_last_arg()?); + if !input.shift_arg_if("-p")? { return Err( "You should ask for parsable output ('-p')".into() ); } - if !shift_arg_if(&mut input, "-o")? { + if !input.shift_arg_if("-o")? { return Err( "You should ask for specific outputs ('-o')".into() ); } - let fields = shift_arg(&mut input)? + let fields = input + .shift_arg()? .split(',') .map(|s| s.to_string()) .collect(); - no_args_remaining(&input)?; + input.no_args_remaining()?; Ok(Self::ShowLink { name, fields }) } "show-phys" => { let mut mac = false; - if shift_arg_if(&mut input, "-m")? { + if input.shift_arg_if("-m")? { mac = true; } - if !shift_arg_if(&mut input, "-p")? { + if !input.shift_arg_if("-p")? { return Err( "You should ask for parsable output ('-p')".into() ); } - if !shift_arg_if(&mut input, "-o")? { + if !input.shift_arg_if("-o")? { return Err( "You should ask for specific outputs ('-o')".into() ); } - let fields = shift_arg(&mut input)? + let fields = input + .shift_arg()? .split(',') .map(|s| s.to_string()) .collect(); - let name = input.args.pop_front().map(|s| LinkName(s)); - no_args_remaining(&input)?; + let name = input.shift_arg().map(|s| LinkName(s)).ok(); + input.no_args_remaining()?; Ok(Self::ShowPhys { mac, fields, name }) } "show-vnic" => { let mut fields = None; - if shift_arg_if(&mut input, "-p")? { - if !shift_arg_if(&mut input, "-o")? { + if input.shift_arg_if("-p")? { + if !input.shift_arg_if("-o")? { return Err( "You should ask for specific outputs ('-o')".into(), ); } fields = Some( - shift_arg(&mut input)? + input + .shift_arg()? .split(',') .map(|s| s.to_string()) .collect(), ); } - let name = input.args.pop_front().map(|s| LinkName(s)); - no_args_remaining(&input)?; + let name = input.shift_arg().map(|s| LinkName(s)).ok(); + input.no_args_remaining()?; Ok(Self::ShowVnic { fields, name }) } "set-linkprop" => { let mut temporary = false; let mut properties = HashMap::new(); - let name = LinkName( - input.args.pop_back().ok_or_else(|| "Missing name")?, - ); + let name = LinkName(input.shift_last_arg()?); - while !input.args.is_empty() { - if shift_arg_if(&mut input, "-t")? { + while !input.args().is_empty() { + if input.shift_arg_if("-t")? { temporary = true; - } else if shift_arg_if(&mut input, "-p")? { - let props = shift_arg(&mut input)?; + } else if input.shift_arg_if("-p")? { + let props = input.shift_arg()?; let props = props.split(','); for prop in props { let (k, v) = @@ -246,7 +251,10 @@ impl TryFrom for Command { properties.insert(k.to_string(), v.to_string()); } } else { - return Err(format!("Invalid arguments {}", input)); + return Err(format!( + "Invalid arguments {}", + input.input() + )); } } diff --git a/helios/tokamak/src/host/ipadm.rs b/helios/tokamak/src/host/ipadm.rs index 3b77c713234..57fb6ee4ab4 100644 --- a/helios/tokamak/src/host/ipadm.rs +++ b/helios/tokamak/src/host/ipadm.rs @@ -2,9 +2,7 @@ // 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/. -use crate::host::{ - no_args_remaining, shift_arg, shift_arg_expect, shift_arg_if, -}; +use crate::host::parse::InputParser; use crate::host::{AddrType, IpInterfaceName}; use helios_fusion::addrobj::AddrObject; @@ -50,15 +48,17 @@ impl TryFrom for Command { return Err(format!("Not ipadm command: {}", input.program)); } - match shift_arg(&mut input)?.as_str() { + let mut input = InputParser::new(input); + + match input.shift_arg()?.as_str() { "create-addr" => { - let temporary = shift_arg_if(&mut input, "-t")?; - shift_arg_expect(&mut input, "-T")?; + let temporary = input.shift_arg_if("-t")?; + input.shift_arg_expect("-T")?; - let ty = match shift_arg(&mut input)?.as_str() { + let ty = match input.shift_arg()?.as_str() { "static" => { - shift_arg_expect(&mut input, "-a")?; - let addr = shift_arg(&mut input)?; + input.shift_arg_expect("-a")?; + let addr = input.shift_arg()?; AddrType::Static( IpNetwork::from_str(&addr) .map_err(|e| e.to_string())?, @@ -68,63 +68,63 @@ impl TryFrom for Command { "addrconf" => AddrType::Addrconf, ty => return Err(format!("Unknown address type {ty}")), }; - let addrobj = AddrObject::from_str(&shift_arg(&mut input)?) + let addrobj = AddrObject::from_str(&input.shift_arg()?) .map_err(|e| e.to_string())?; - no_args_remaining(&input)?; + input.no_args_remaining()?; Ok(Command::CreateAddr { temporary, ty, addrobj }) } "create-ip" | "create-if" => { - let temporary = shift_arg_if(&mut input, "-t")?; - let name = IpInterfaceName(shift_arg(&mut input)?); - no_args_remaining(&input)?; + let temporary = input.shift_arg_if("-t")?; + let name = IpInterfaceName(input.shift_arg()?); + input.no_args_remaining()?; Ok(Command::CreateIf { temporary, name }) } "delete-addr" => { - let addrobj = AddrObject::from_str(&shift_arg(&mut input)?) + let addrobj = AddrObject::from_str(&input.shift_arg()?) .map_err(|e| e.to_string())?; - no_args_remaining(&input)?; + input.no_args_remaining()?; Ok(Command::DeleteAddr { addrobj }) } "delete-ip" | "delete-if" => { - let name = IpInterfaceName(shift_arg(&mut input)?); - no_args_remaining(&input)?; + let name = IpInterfaceName(input.shift_arg()?); + input.no_args_remaining()?; Ok(Command::DeleteIf { name }) } "show-if" => { - let name = IpInterfaceName( - input.args.pop_back().ok_or_else(|| "Missing name")?, - ); + let name = IpInterfaceName(input.shift_last_arg()?); let mut properties = vec![]; - while !input.args.is_empty() { - if shift_arg_if(&mut input, "-p")? { - shift_arg_expect(&mut input, "-o")?; - properties = shift_arg(&mut input)? + while !input.args().is_empty() { + if input.shift_arg_if("-p")? { + input.shift_arg_expect("-o")?; + properties = input + .shift_arg()? .split(',') .map(|s| s.to_string()) .collect(); } else { - return Err(format!("Unexpected input: {input}")); + return Err(format!( + "Unexpected input: {}", + input.input() + )); } } Ok(Command::ShowIf { properties, name }) } "set-ifprop" => { - let name = IpInterfaceName( - input.args.pop_back().ok_or_else(|| "Missing name")?, - ); + let name = IpInterfaceName(input.shift_last_arg()?); let mut temporary = false; let mut properties = HashMap::new(); let mut module = "ip".to_string(); - while !input.args.is_empty() { - if shift_arg_if(&mut input, "-t")? { + while !input.args().is_empty() { + if input.shift_arg_if("-t")? { temporary = true; - } else if shift_arg_if(&mut input, "-m")? { - module = shift_arg(&mut input)?; - } else if shift_arg_if(&mut input, "-p")? { - let props = shift_arg(&mut input)?; + } else if input.shift_arg_if("-m")? { + module = input.shift_arg()?; + } else if input.shift_arg_if("-p")? { + let props = input.shift_arg()?; let props = props.split(','); for prop in props { let (k, v) = @@ -134,7 +134,10 @@ impl TryFrom for Command { properties.insert(k.to_string(), v.to_string()); } } else { - return Err(format!("Unexpected input: {input}")); + return Err(format!( + "Unexpected input: {}", + input.input() + )); } } diff --git a/helios/tokamak/src/host/mod.rs b/helios/tokamak/src/host/mod.rs index db4ea4d0b21..8f408112230 100644 --- a/helios/tokamak/src/host/mod.rs +++ b/helios/tokamak/src/host/mod.rs @@ -20,6 +20,7 @@ use ipnetwork::IpNetwork; use std::collections::{HashMap, HashSet}; use std::str::FromStr; +// Parsing command-line utilities mod dladm; mod ipadm; mod route; @@ -30,6 +31,10 @@ mod zoneadm; mod zonecfg; mod zpool; +mod parse; + +use crate::host::parse::InputExt; + enum LinkType { Etherstub, Vnic, @@ -142,11 +147,11 @@ pub enum RouteTarget { } impl RouteTarget { - fn shift_target(input: &mut Input) -> Result { - let force_v4 = shift_arg_if(input, "-inet")?; - let force_v6 = shift_arg_if(input, "-inet6")?; + fn shift_target(input: &mut parse::InputParser) -> Result { + let force_v4 = input.shift_arg_if("-inet")?; + let force_v6 = input.shift_arg_if("-inet6")?; - let target = match (force_v4, force_v6, shift_arg(input)?.as_str()) { + let target = match (force_v4, force_v6, input.shift_arg()?.as_str()) { (true, true, _) => { return Err("Cannot force both v4 and v6".to_string()) } @@ -172,11 +177,14 @@ impl RouteTarget { pub struct FilesystemName(String); enum KnownCommand { + Coreadm, // TODO Dladm(dladm::Command), + Dumpadm, // TODO Ipadm(ipadm::Command), - Fstyp, - RouteAdm, + Fstyp, // TODO + RouteAdm, // TODO Route(route::Command), + Savecore, // TODO Svccfg(svccfg::Command), Svcadm(svcadm::Command), Zfs(zfs::Command), @@ -200,27 +208,24 @@ impl TryFrom for Command { while input.program == PFEXEC { with_pfexec = true; - shift_program(&mut input)?; + input.shift_program()?; } if input.program == ZLOGIN { - shift_program(&mut input)?; - in_zone = Some(ZoneName(shift_program(&mut input)?)); + input.shift_program()?; + in_zone = Some(ZoneName(input.shift_program()?)); } + use KnownCommand::*; let cmd = match input.program.as_str() { - DLADM => KnownCommand::Dladm(dladm::Command::try_from(input)?), - IPADM => KnownCommand::Ipadm(ipadm::Command::try_from(input)?), - ROUTE => KnownCommand::Route(route::Command::try_from(input)?), - SVCCFG => KnownCommand::Svccfg(svccfg::Command::try_from(input)?), - SVCADM => KnownCommand::Svcadm(svcadm::Command::try_from(input)?), - ZFS => KnownCommand::Zfs(zfs::Command::try_from(input)?), - ZONEADM => { - KnownCommand::Zoneadm(zoneadm::Command::try_from(input)?) - } - ZONECFG => { - KnownCommand::Zonecfg(zonecfg::Command::try_from(input)?) - } - ZPOOL => KnownCommand::Zpool(zpool::Command::try_from(input)?), + DLADM => Dladm(dladm::Command::try_from(input)?), + IPADM => Ipadm(ipadm::Command::try_from(input)?), + ROUTE => Route(route::Command::try_from(input)?), + SVCCFG => Svccfg(svccfg::Command::try_from(input)?), + SVCADM => Svcadm(svcadm::Command::try_from(input)?), + ZFS => Zfs(zfs::Command::try_from(input)?), + ZONEADM => Zoneadm(zoneadm::Command::try_from(input)?), + ZONECFG => Zonecfg(zonecfg::Command::try_from(input)?), + ZPOOL => Zpool(zpool::Command::try_from(input)?), _ => return Err(format!("Unknown command: {}", input.program)), }; @@ -228,58 +233,6 @@ impl TryFrom for Command { } } -// Shifts out the program, putting the subsequent argument in its place. -// -// Returns the prior program value. -pub(crate) fn shift_program(input: &mut Input) -> Result { - let new = input - .args - .pop_front() - .ok_or_else(|| format!("Failed to parse {input}"))?; - - let old = std::mem::replace(&mut input.program, new); - - Ok(old) -} - -pub(crate) fn no_args_remaining(input: &Input) -> Result<(), String> { - if !input.args.is_empty() { - return Err(format!("Unexpected extra arguments: {input}")); - } - Ok(()) -} - -// Removes the next argument unconditionally. -pub(crate) fn shift_arg(input: &mut Input) -> Result { - Ok(input.args.pop_front().ok_or_else(|| "Missing argument")?) -} - -// Removes the next argument, which must equal the provided value. -pub(crate) fn shift_arg_expect( - input: &mut Input, - value: &str, -) -> Result<(), String> { - let v = input.args.pop_front().ok_or_else(|| "Missing argument")?; - if value != v { - return Err(format!("Unexpected argument {v} (expected: {value}")); - } - Ok(()) -} - -// Removes the next argument if it equals `value`. -// -// Returns if it was equal. -pub(crate) fn shift_arg_if( - input: &mut Input, - value: &str, -) -> Result { - let eq = input.args.front().ok_or_else(|| "Missing argument")? == value; - if eq { - input.args.pop_front(); - } - Ok(eq) -} - #[cfg(test)] mod test { use super::*; diff --git a/helios/tokamak/src/host/parse.rs b/helios/tokamak/src/host/parse.rs new file mode 100644 index 00000000000..582890e1271 --- /dev/null +++ b/helios/tokamak/src/host/parse.rs @@ -0,0 +1,110 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// 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/. + +use helios_fusion::Input; + +pub(crate) trait InputExt { + /// Shifts out the program, putting the subsequent argument in its place. + /// + /// Returns the prior program value. + fn shift_program(&mut self) -> Result; +} + +impl InputExt for Input { + fn shift_program(&mut self) -> Result { + if self.args.is_empty() { + return Err(format!("Failed to parse {self}")); + } + let new = self.args.remove(0); + let old = std::mem::replace(&mut self.program, new); + Ok(old) + } +} + +pub(crate) struct InputParser { + input: Input, + start: usize, + end: usize, +} + +impl InputParser { + pub(crate) fn new(input: Input) -> Self { + let end = input.args.len(); + Self { input, start: 0, end } + } + + pub(crate) fn input(&self) -> &Input { + &self.input + } + + pub(crate) fn args(&self) -> &[String] { + &self.input.args[self.start..self.end] + } + + pub(crate) fn no_args_remaining(&self) -> Result<(), String> { + if self.start < self.end { + return Err(format!( + "Unexpected extra arguments: {:?}", + self.args() + )); + } + Ok(()) + } + + /// Reemoves the last argument unconditionally. + pub(crate) fn shift_last_arg(&mut self) -> Result { + if self.start >= self.end { + return Err("Missing argument".to_string()); + } + let arg = self + .input + .args + .get(self.end - 1) + .ok_or_else(|| "Missing argument")?; + self.end -= 1; + Ok(arg.to_string()) + } + + /// Removes the next argument unconditionally. + pub(crate) fn shift_arg(&mut self) -> Result { + if self.start >= self.end { + return Err("Missing argument".to_string()); + } + let arg = self + .input + .args + .get(self.start) + .ok_or_else(|| "Missing argument")?; + self.start += 1; + Ok(arg.to_string()) + } + + /// Removes the next argument, which must equal the provided value. + pub(crate) fn shift_arg_expect( + &mut self, + value: &str, + ) -> Result<(), String> { + let v = self.shift_arg()?; + if value != v { + return Err(format!("Unexpected argument {v} (expected: {value}")); + } + Ok(()) + } + + /// Removes the next argument if it equals `value`. + /// + /// Returns if it was equal. + pub(crate) fn shift_arg_if(&mut self, value: &str) -> Result { + let eq = self + .input + .args + .get(self.start) + .ok_or_else(|| "Missing argument")? + == value; + if eq { + self.shift_arg()?; + } + Ok(eq) + } +} diff --git a/helios/tokamak/src/host/route.rs b/helios/tokamak/src/host/route.rs index 23de4a279b9..dcea1e14daf 100644 --- a/helios/tokamak/src/host/route.rs +++ b/helios/tokamak/src/host/route.rs @@ -2,7 +2,7 @@ // 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/. -use crate::host::{no_args_remaining, shift_arg, shift_arg_if}; +use crate::host::parse::InputParser; use crate::host::{LinkName, RouteTarget}; use helios_fusion::Input; @@ -24,18 +24,19 @@ impl TryFrom for Command { return Err(format!("Not route command: {}", input.program)); } - match shift_arg(&mut input)?.as_str() { + let mut input = InputParser::new(input); + + match input.shift_arg()?.as_str() { "add" => { let destination = RouteTarget::shift_target(&mut input)?; let gateway = RouteTarget::shift_target(&mut input)?; - let interface = - if let Ok(true) = shift_arg_if(&mut input, "-ifp") { - Some(LinkName(shift_arg(&mut input)?)) - } else { - None - }; - no_args_remaining(&input)?; + let interface = if let Ok(true) = input.shift_arg_if("-ifp") { + Some(LinkName(input.shift_arg()?)) + } else { + None + }; + input.no_args_remaining()?; Ok(Command::Add { destination, gateway, interface }) } command => return Err(format!("Unsupported command: {}", command)), diff --git a/helios/tokamak/src/host/svcadm.rs b/helios/tokamak/src/host/svcadm.rs index dcedb3aa57f..87165a26530 100644 --- a/helios/tokamak/src/host/svcadm.rs +++ b/helios/tokamak/src/host/svcadm.rs @@ -2,7 +2,7 @@ // 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/. -use crate::host::{no_args_remaining, shift_arg, shift_arg_if}; +use crate::host::parse::InputParser; use crate::host::{ServiceName, ZoneName}; use helios_fusion::Input; @@ -21,25 +21,27 @@ impl TryFrom for Command { return Err(format!("Not svcadm command: {}", input.program)); } - let zone = if shift_arg_if(&mut input, "-z")? { - Some(ZoneName(shift_arg(&mut input)?)) + let mut input = InputParser::new(input); + + let zone = if input.shift_arg_if("-z")? { + Some(ZoneName(input.shift_arg()?)) } else { None }; - match shift_arg(&mut input)?.as_str() { + match input.shift_arg()?.as_str() { "enable" => { // Intentionally ignored - shift_arg_if(&mut input, "-t")?; - let service = ServiceName(shift_arg(&mut input)?); - no_args_remaining(&input)?; + input.shift_arg_if("-t")?; + let service = ServiceName(input.shift_arg()?); + input.no_args_remaining()?; Ok(Command::Enable { zone, service }) } "disable" => { // Intentionally ignored - shift_arg_if(&mut input, "-t")?; - let service = ServiceName(shift_arg(&mut input)?); - no_args_remaining(&input)?; + input.shift_arg_if("-t")?; + let service = ServiceName(input.shift_arg()?); + input.no_args_remaining()?; Ok(Command::Disable { zone, service }) } command => return Err(format!("Unexpected command: {command}")), diff --git a/helios/tokamak/src/host/svccfg.rs b/helios/tokamak/src/host/svccfg.rs index 40ab730b571..28753d44ed7 100644 --- a/helios/tokamak/src/host/svccfg.rs +++ b/helios/tokamak/src/host/svccfg.rs @@ -2,9 +2,7 @@ // 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/. -use crate::host::{ - no_args_remaining, shift_arg, shift_arg_expect, shift_arg_if, -}; +use crate::host::parse::InputParser; use crate::host::{ServiceName, ZoneName}; use camino::Utf8PathBuf; @@ -61,26 +59,28 @@ impl TryFrom for Command { return Err(format!("Not svccfg command: {}", input.program)); } - let zone = if shift_arg_if(&mut input, "-z")? { - Some(ZoneName(shift_arg(&mut input)?)) + let mut input = InputParser::new(input); + + let zone = if input.shift_arg_if("-z")? { + Some(ZoneName(input.shift_arg()?)) } else { None }; - let fmri = if shift_arg_if(&mut input, "-s")? { - Some(ServiceName(shift_arg(&mut input)?)) + let fmri = if input.shift_arg_if("-s")? { + Some(ServiceName(input.shift_arg()?)) } else { None }; - match shift_arg(&mut input)?.as_str() { + match input.shift_arg()?.as_str() { "addpropvalue" => { - let name = shift_arg(&mut input)?; + let name = input.shift_arg()?; let name = smf::PropertyName::from_str(&name) .map_err(|e| e.to_string())?; - let type_or_value = shift_arg(&mut input)?; - let (ty, value) = match input.args.pop_front() { + let type_or_value = input.shift_arg()?; + let (ty, value) = match input.shift_arg().ok() { Some(value) => { let ty = type_or_value .strip_suffix(':') @@ -93,91 +93,89 @@ impl TryFrom for Command { None => (None, type_or_value), }; - let fmri = fmri.ok_or_else(|| { - format!("-s option required for addpropvalue") - })?; + let fmri = + fmri.ok_or_else(|| "-s option required for addpropvalue")?; - no_args_remaining(&input)?; + input.no_args_remaining()?; Ok(Command::Addpropvalue { zone, fmri, key: name, ty, value }) } "addpg" => { - let name = shift_arg(&mut input)?; + let name = input.shift_arg()?; let group = smf::PropertyGroupName::new(&name) .map_err(|e| e.to_string())?; - let group_type = shift_arg(&mut input)?; - if let Some(flags) = input.args.pop_front() { + let group_type = input.shift_arg()?; + if let Some(flags) = input.shift_arg().ok() { return Err( "Parsing of optional flags not implemented".to_string() ); } - let fmri = fmri - .ok_or_else(|| format!("-s option required for addpg"))?; + let fmri = + fmri.ok_or_else(|| "-s option required for addpg")?; - no_args_remaining(&input)?; + input.no_args_remaining()?; Ok(Command::Addpg { zone, fmri, group, group_type }) } "delpg" => { - let name = shift_arg(&mut input)?; + let name = input.shift_arg()?; let group = smf::PropertyGroupName::new(&name) .map_err(|e| e.to_string())?; - let fmri = fmri - .ok_or_else(|| format!("-s option required for delpg"))?; + let fmri = + fmri.ok_or_else(|| "-s option required for delpg")?; - no_args_remaining(&input)?; + input.no_args_remaining()?; Ok(Command::Delpg { zone, fmri, group }) } "delpropvalue" => { - let name = shift_arg(&mut input)?; + let name = input.shift_arg()?; let name = smf::PropertyName::from_str(&name) .map_err(|e| e.to_string())?; - let fmri = fmri.ok_or_else(|| { - format!("-s option required for delpropvalue") - })?; - let glob = shift_arg(&mut input)?; + let fmri = + fmri.ok_or_else(|| "-s option required for delpropvalue")?; + let glob = input.shift_arg()?; - no_args_remaining(&input)?; + input.no_args_remaining()?; Ok(Command::Delpropvalue { zone, fmri, name, glob }) } "import" => { - let file = shift_arg(&mut input)?; + let file = input.shift_arg()?; if let Some(_) = fmri { return Err( "Cannot use '-s' option with import".to_string() ); } - no_args_remaining(&input)?; + input.no_args_remaining()?; Ok(Command::Import { zone, file: file.into() }) } "refresh" => { - let fmri = fmri - .ok_or_else(|| format!("-s option required for refresh"))?; - no_args_remaining(&input)?; + let fmri = + fmri.ok_or_else(|| "-s option required for refresh")?; + input.no_args_remaining()?; Ok(Command::Refresh { zone, fmri }) } "setprop" => { - let fmri = fmri - .ok_or_else(|| format!("-s option required for setprop"))?; + let fmri = + fmri.ok_or_else(|| "-s option required for setprop")?; // Setprop seems fine accepting args of the form: // - name=value // - name = value // - name = type: value (NOTE: not yet supported) - let first_arg = shift_arg(&mut input)?; + let first_arg = input.shift_arg()?; let (name, value) = if let Some((name, value)) = first_arg.split_once('=') { (name.to_string(), value.to_string()) } else { let name = first_arg; - shift_arg_expect(&mut input, "=")?; - let value = shift_arg(&mut input)?; - (name, value.to_string()) + input.shift_arg_expect("=")?; + let value = input.shift_arg()?; + (name, value) }; let name = smf::PropertyName::from_str(&name) .map_err(|e| e.to_string())?; - no_args_remaining(&input)?; + input.no_args_remaining()?; Ok(Command::Setprop { zone, fmri, name, value }) } command => return Err(format!("Unexpected command: {command}")), diff --git a/helios/tokamak/src/host/zfs.rs b/helios/tokamak/src/host/zfs.rs index 7e668c36e7f..dfbea3b8bc8 100644 --- a/helios/tokamak/src/host/zfs.rs +++ b/helios/tokamak/src/host/zfs.rs @@ -2,8 +2,8 @@ // 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/. +use crate::host::parse::InputParser; use crate::host::FilesystemName; -use crate::host::{no_args_remaining, shift_arg, shift_arg_if}; use helios_fusion::Input; use helios_fusion::ZFS; @@ -58,42 +58,45 @@ impl TryFrom for Command { return Err(format!("Not zfs command: {}", input.program)); } - match shift_arg(&mut input)?.as_str() { + let mut input = InputParser::new(input); + match input.shift_arg()?.as_str() { "create" => { let mut size = None; let mut blocksize = None; let mut sparse = None; let mut properties = vec![]; - while input.args.len() > 1 { + while input.args().len() > 1 { // Volume Size (volumes only, required) - if shift_arg_if(&mut input, "-V")? { + if input.shift_arg_if("-V")? { size = Some( - shift_arg(&mut input)? + input + .shift_arg()? .parse::() .map_err(|e| e.to_string())?, ); // Sparse (volumes only, optional) - } else if shift_arg_if(&mut input, "-s")? { + } else if input.shift_arg_if("-s")? { sparse = Some(true); // Block size (volumes only, optional) - } else if shift_arg_if(&mut input, "-b")? { + } else if input.shift_arg_if("-b")? { blocksize = Some( - shift_arg(&mut input)? + input + .shift_arg()? .parse::() .map_err(|e| e.to_string())?, ); // Properties - } else if shift_arg_if(&mut input, "-o")? { - let prop = shift_arg(&mut input)?; + } else if input.shift_arg_if("-o")? { + let prop = input.shift_arg()?; let (k, v) = prop .split_once('=') .ok_or_else(|| format!("Bad property: {prop}"))?; properties.push((k.to_string(), v.to_string())); } } - let name = FilesystemName(shift_arg(&mut input)?); - no_args_remaining(&input)?; + let name = FilesystemName(input.shift_arg()?); + input.no_args_remaining()?; if let Some(size) = size { // Volume @@ -119,8 +122,8 @@ impl TryFrom for Command { let mut force_unmount = false; let mut name = None; - while !input.args.is_empty() { - let arg = shift_arg(&mut input)?; + while !input.args().is_empty() { + let arg = input.shift_arg()?; let mut chars = arg.chars(); if let Some('-') = chars.next() { while let Some(c) = chars.next() { @@ -137,7 +140,7 @@ impl TryFrom for Command { } } else { name = Some(FilesystemName(arg)); - no_args_remaining(&input)?; + input.no_args_remaining()?; } } let name = name.ok_or_else(|| "Missing name".to_string())?; @@ -158,8 +161,8 @@ impl TryFrom for Command { .to_vec(); let mut properties = vec![]; - while !input.args.is_empty() { - let arg = shift_arg(&mut input)?; + while !input.args().is_empty() { + let arg = input.shift_arg()?; let mut chars = arg.chars(); // ZFS list lets callers pass in flags in groups, or // separately. @@ -174,7 +177,7 @@ impl TryFrom for Command { if chars.clone().next().is_some() { chars.collect::() } else { - shift_arg(&mut input)? + input.shift_arg()? }; depth = Some( depth_raw @@ -190,7 +193,8 @@ impl TryFrom for Command { if chars.next().is_some() { return Err("-o should be immediately followed by fields".to_string()); } - fields = shift_arg(&mut input)? + fields = input + .shift_arg()? .split(',') .map(|s| s.to_string()) .collect(); @@ -210,8 +214,10 @@ impl TryFrom for Command { } let datasets = Some( - std::mem::take(&mut input.args) + input + .args() .into_iter() + .map(|s| s.into()) .collect::>(), ); if !scripting || !parsable { @@ -234,8 +240,8 @@ impl TryFrom for Command { let mut properties = vec![]; let mut datasets = None; - while !input.args.is_empty() { - let arg = shift_arg(&mut input)?; + while !input.args().is_empty() { + let arg = input.shift_arg()?; let mut chars = arg.chars(); // ZFS list lets callers pass in flags in groups, or // separately. @@ -250,7 +256,7 @@ impl TryFrom for Command { if chars.clone().next().is_some() { chars.collect::() } else { - shift_arg(&mut input)? + input.shift_arg()? }; depth = Some( depth_raw @@ -266,7 +272,8 @@ impl TryFrom for Command { if chars.next().is_some() { return Err("-o should be immediately followed by properties".to_string()); } - properties = shift_arg(&mut input)? + properties = input + .shift_arg()? .split(',') .map(|s| s.to_string()) .collect(); @@ -286,11 +293,11 @@ impl TryFrom for Command { } } - let remaining_datasets = std::mem::take(&mut input.args); + let remaining_datasets = input.args(); if !remaining_datasets.is_empty() { datasets .get_or_insert(vec![]) - .extend(remaining_datasets.into_iter()); + .extend(remaining_datasets.into_iter().cloned()); }; if !scripting || !parsable { @@ -300,23 +307,23 @@ impl TryFrom for Command { Ok(Command::List { recursive, depth, properties, datasets }) } "mount" => { - let load_keys = shift_arg_if(&mut input, "-l")?; - let filesystem = FilesystemName(shift_arg(&mut input)?); - no_args_remaining(&input)?; + let load_keys = input.shift_arg_if("-l")?; + let filesystem = FilesystemName(input.shift_arg()?); + input.no_args_remaining()?; Ok(Command::Mount { load_keys, filesystem }) } "set" => { let mut properties = vec![]; - while input.args.len() > 1 { - let prop = shift_arg(&mut input)?; + while input.args().len() > 1 { + let prop = input.shift_arg()?; let (k, v) = prop .split_once('=') .ok_or_else(|| format!("Bad property: {prop}"))?; properties.push((k.to_string(), v.to_string())); } - let name = FilesystemName(shift_arg(&mut input)?); - no_args_remaining(&input)?; + let name = FilesystemName(input.shift_arg()?); + input.no_args_remaining()?; Ok(Command::Set { properties, name }) } diff --git a/helios/tokamak/src/host/zoneadm.rs b/helios/tokamak/src/host/zoneadm.rs index 080d0cf0cea..a54f24ea619 100644 --- a/helios/tokamak/src/host/zoneadm.rs +++ b/helios/tokamak/src/host/zoneadm.rs @@ -2,8 +2,8 @@ // 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/. +use crate::host::parse::InputParser; use crate::host::ZoneName; -use crate::host::{no_args_remaining, shift_arg, shift_arg_if}; use helios_fusion::Input; use helios_fusion::ZONEADM; @@ -38,22 +38,24 @@ impl TryFrom for Command { return Err(format!("Not zoneadm command: {}", input.program)); } - let name = if shift_arg_if(&mut input, "-z")? { - Some(ZoneName(shift_arg(&mut input)?)) + let mut input = InputParser::new(input); + + let name = if input.shift_arg_if("-z")? { + Some(ZoneName(input.shift_arg()?)) } else { None }; - match shift_arg(&mut input)?.as_str() { + match input.shift_arg()?.as_str() { "boot" => { - no_args_remaining(&input)?; + input.no_args_remaining()?; let name = name.ok_or_else(|| { "No zone specified, try: zoneadm -z ZONE boot" })?; Ok(Command::Boot { name }) } "halt" => { - no_args_remaining(&input)?; + input.no_args_remaining()?; let name = name.ok_or_else(|| { "No zone specified, try: zoneadm -z ZONE halt" })?; @@ -61,7 +63,7 @@ impl TryFrom for Command { } "install" => { let brand_specific_args = - std::mem::take(&mut input.args).into_iter().collect(); + input.args().into_iter().cloned().collect(); let name = name.ok_or_else(|| { "No zone specified, try: zoneadm -z ZONE install" })?; @@ -72,8 +74,8 @@ impl TryFrom for Command { let mut list_installed = false; let mut parsable = false; - while !input.args.is_empty() { - let arg = shift_arg(&mut input)?; + while !input.args().is_empty() { + let arg = input.shift_arg()?; let mut chars = arg.chars(); if let Some('-') = chars.next() { @@ -104,12 +106,12 @@ impl TryFrom for Command { let name = name.ok_or_else(|| { "No zone specified, try: zoneadm -z ZONE uninstall" })?; - let force = if !input.args.is_empty() { - shift_arg_if(&mut input, "-F")? + let force = if !input.args().is_empty() { + input.shift_arg_if("-F")? } else { false }; - no_args_remaining(&input)?; + input.no_args_remaining()?; Ok(Command::Uninstall { name, force }) } command => return Err(format!("Unexpected command: {command}")), diff --git a/helios/tokamak/src/host/zonecfg.rs b/helios/tokamak/src/host/zonecfg.rs index 5c1480fec9b..e545cba7016 100644 --- a/helios/tokamak/src/host/zonecfg.rs +++ b/helios/tokamak/src/host/zonecfg.rs @@ -2,7 +2,7 @@ // 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/. -use crate::host::{shift_arg, shift_arg_expect}; +use crate::host::parse::InputParser; use crate::host::{ZoneConfig, ZoneName}; use camino::Utf8PathBuf; @@ -21,12 +21,15 @@ impl TryFrom for Command { if input.program != ZONECFG { return Err(format!("Not zonecfg command: {}", input.program)); } - shift_arg_expect(&mut input, "-z")?; - let zone = ZoneName(shift_arg(&mut input)?); - match shift_arg(&mut input)?.as_str() { + + let mut input = InputParser::new(input); + + input.shift_arg_expect("-z")?; + let zone = ZoneName(input.shift_arg()?); + match input.shift_arg()?.as_str() { "create" => { - shift_arg_expect(&mut input, "-F")?; - shift_arg_expect(&mut input, "-b")?; + input.shift_arg_expect("-F")?; + input.shift_arg_expect("-b")?; enum Scope { Global, @@ -47,11 +50,11 @@ impl TryFrom for Command { let mut nets = vec![]; let mut fs = vec![]; - while !input.args.is_empty() { - shift_arg_expect(&mut input, ";")?; - match shift_arg(&mut input)?.as_str() { + while !input.args().is_empty() { + input.shift_arg_expect(";")?; + match input.shift_arg()?.as_str() { "set" => { - let prop = shift_arg(&mut input)?; + let prop = input.shift_arg()?; let (k, v) = prop.split_once('=').ok_or_else(|| { format!("Bad property: {prop}") @@ -137,7 +140,7 @@ impl TryFrom for Command { return Err("Cannot add from non-global scope" .to_string()); } - match shift_arg(&mut input)?.as_str() { + match input.shift_arg()?.as_str() { "dataset" => { scope = Scope::Dataset(zone::Dataset::default()) @@ -198,7 +201,7 @@ impl TryFrom for Command { }) } "delete" => { - shift_arg_expect(&mut input, "-F")?; + input.shift_arg_expect("-F")?; Ok(Command::Delete { name: zone }) } command => return Err(format!("Unexpected command: {command}")), diff --git a/helios/tokamak/src/host/zpool.rs b/helios/tokamak/src/host/zpool.rs index 056c5b27973..297c83410ae 100644 --- a/helios/tokamak/src/host/zpool.rs +++ b/helios/tokamak/src/host/zpool.rs @@ -2,7 +2,7 @@ // 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/. -use crate::host::{no_args_remaining, shift_arg, shift_arg_if}; +use crate::host::parse::InputParser; use helios_fusion::Input; use helios_fusion::ZPOOL; @@ -25,21 +25,23 @@ impl TryFrom for Command { return Err(format!("Not zpool command: {}", input.program)); } - match shift_arg(&mut input)?.as_str() { + let mut input = InputParser::new(input); + + match input.shift_arg()?.as_str() { "create" => { - let pool = shift_arg(&mut input)?; - let vdev = shift_arg(&mut input)?; - no_args_remaining(&input)?; + let pool = input.shift_arg()?; + let vdev = input.shift_arg()?; + input.no_args_remaining()?; Ok(Command::Create { pool, vdev }) } "export" => { - let pool = shift_arg(&mut input)?; - no_args_remaining(&input)?; + let pool = input.shift_arg()?; + input.no_args_remaining()?; Ok(Command::Export { pool }) } "import" => { - let force = shift_arg_if(&mut input, "-f")?; - let pool = shift_arg(&mut input)?; + let force = input.shift_arg_if("-f")?; + let pool = input.shift_arg()?; Ok(Command::Import { force, pool }) } "list" => { @@ -48,8 +50,8 @@ impl TryFrom for Command { let mut properties = vec![]; let mut pools = None; - while !input.args.is_empty() { - let arg = shift_arg(&mut input)?; + while !input.args().is_empty() { + let arg = input.shift_arg()?; let mut chars = arg.chars(); // ZFS list lets callers pass in flags in groups, or // separately. @@ -62,7 +64,8 @@ impl TryFrom for Command { if chars.next().is_some() { return Err("-o should be immediately followed by properties".to_string()); } - properties = shift_arg(&mut input)? + properties = input + .shift_arg()? .split(',') .map(|s| s.to_string()) .collect(); @@ -80,11 +83,11 @@ impl TryFrom for Command { } } - let remaining_pools = std::mem::take(&mut input.args); + let remaining_pools = input.args(); if !remaining_pools.is_empty() { pools .get_or_insert(vec![]) - .extend(remaining_pools.into_iter()); + .extend(remaining_pools.into_iter().cloned()); }; if !scripting || !parsable { return Err("You should run 'zpool list' commands with the '-Hp' flags enabled".to_string()); @@ -92,15 +95,15 @@ impl TryFrom for Command { Ok(Command::List { properties, pools }) } "set" => { - let prop = shift_arg(&mut input)?; + let prop = input.shift_arg()?; let (k, v) = prop .split_once('=') .ok_or_else(|| format!("Bad property: {prop}"))?; let property = k.to_string(); let value = v.to_string(); - let pool = shift_arg(&mut input)?; - no_args_remaining(&input)?; + let pool = input.shift_arg()?; + input.no_args_remaining()?; Ok(Command::Set { property, value, pool }) } command => return Err(format!("Unexpected command: {command}")), From 3c2699a577c1102e197d670c07581a98512a68eb Mon Sep 17 00:00:00 2001 From: Sean Klein Date: Tue, 22 Aug 2023 15:24:11 -0700 Subject: [PATCH 08/18] move RouteTarget into route.rs --- helios/tokamak/src/host/mod.rs | 37 ----------------------------- helios/tokamak/src/host/route.rs | 40 +++++++++++++++++++++++++++++++- 2 files changed, 39 insertions(+), 38 deletions(-) diff --git a/helios/tokamak/src/host/mod.rs b/helios/tokamak/src/host/mod.rs index 8f408112230..fe26175b515 100644 --- a/helios/tokamak/src/host/mod.rs +++ b/helios/tokamak/src/host/mod.rs @@ -18,7 +18,6 @@ use helios_fusion::{ }; use ipnetwork::IpNetwork; use std::collections::{HashMap, HashSet}; -use std::str::FromStr; // Parsing command-line utilities mod dladm; @@ -138,42 +137,6 @@ pub enum AddrType { Addrconf, } -#[derive(Debug, PartialEq, Eq)] -pub enum RouteTarget { - Default, - DefaultV4, - DefaultV6, - ByAddress(IpNetwork), -} - -impl RouteTarget { - fn shift_target(input: &mut parse::InputParser) -> Result { - let force_v4 = input.shift_arg_if("-inet")?; - let force_v6 = input.shift_arg_if("-inet6")?; - - let target = match (force_v4, force_v6, input.shift_arg()?.as_str()) { - (true, true, _) => { - return Err("Cannot force both v4 and v6".to_string()) - } - (true, false, "default") => RouteTarget::DefaultV4, - (false, true, "default") => RouteTarget::DefaultV6, - (false, false, "default") => RouteTarget::Default, - (_, _, other) => { - let net = - IpNetwork::from_str(other).map_err(|e| e.to_string())?; - if force_v4 && !net.is_ipv4() { - return Err(format!("{net} is not ipv4")); - } - if force_v6 && !net.is_ipv6() { - return Err(format!("{net} is not ipv6")); - } - RouteTarget::ByAddress(net) - } - }; - Ok(target) - } -} - pub struct FilesystemName(String); enum KnownCommand { diff --git a/helios/tokamak/src/host/route.rs b/helios/tokamak/src/host/route.rs index dcea1e14daf..894a5a3b4bb 100644 --- a/helios/tokamak/src/host/route.rs +++ b/helios/tokamak/src/host/route.rs @@ -3,10 +3,48 @@ // file, You can obtain one at https://mozilla.org/MPL/2.0/. use crate::host::parse::InputParser; -use crate::host::{LinkName, RouteTarget}; +use crate::host::LinkName; use helios_fusion::Input; use helios_fusion::ROUTE; +use ipnetwork::IpNetwork; +use std::str::FromStr; + +#[derive(Debug, PartialEq, Eq)] +pub enum RouteTarget { + Default, + DefaultV4, + DefaultV6, + ByAddress(IpNetwork), +} + +impl RouteTarget { + fn shift_target(input: &mut InputParser) -> Result { + let force_v4 = input.shift_arg_if("-inet")?; + let force_v6 = input.shift_arg_if("-inet6")?; + + let target = match (force_v4, force_v6, input.shift_arg()?.as_str()) { + (true, true, _) => { + return Err("Cannot force both v4 and v6".to_string()) + } + (true, false, "default") => RouteTarget::DefaultV4, + (false, true, "default") => RouteTarget::DefaultV6, + (false, false, "default") => RouteTarget::Default, + (_, _, other) => { + let net = + IpNetwork::from_str(other).map_err(|e| e.to_string())?; + if force_v4 && !net.is_ipv4() { + return Err(format!("{net} is not ipv4")); + } + if force_v6 && !net.is_ipv6() { + return Err(format!("{net} is not ipv6")); + } + RouteTarget::ByAddress(net) + } + }; + Ok(target) + } +} pub(crate) enum Command { Add { From 6820c5b7abad7bfb51d2a912e63ee8f30f630859 Mon Sep 17 00:00:00 2001 From: Sean Klein Date: Wed, 23 Aug 2023 13:37:25 -0700 Subject: [PATCH 09/18] Refactoring, separate CLI from host, start to abstract ffi --- helios/fusion/src/host.rs | 26 +++ helios/fusion/src/interfaces/libc.rs | 10 + helios/fusion/src/interfaces/mod.rs | 6 + helios/fusion/src/interfaces/swapctl.rs | 46 ++++ helios/fusion/src/lib.rs | 3 + helios/protostar/src/executor.rs | 154 ++++++++++++++ helios/protostar/src/host.rs | 46 ++++ helios/protostar/src/lib.rs | 154 +------------- helios/protostar/src/libc.rs | 20 ++ helios/protostar/src/swapctl.rs | 224 ++++++++++++++++++++ helios/tokamak/src/{host => cli}/dladm.rs | 4 +- helios/tokamak/src/{host => cli}/ipadm.rs | 4 +- helios/tokamak/src/cli/mod.rs | 89 ++++++++ helios/tokamak/src/{host => cli}/parse.rs | 0 helios/tokamak/src/{host => cli}/route.rs | 4 +- helios/tokamak/src/{host => cli}/svcadm.rs | 4 +- helios/tokamak/src/{host => cli}/svccfg.rs | 6 +- helios/tokamak/src/{host => cli}/zfs.rs | 87 +++++--- helios/tokamak/src/{host => cli}/zoneadm.rs | 4 +- helios/tokamak/src/{host => cli}/zonecfg.rs | 4 +- helios/tokamak/src/{host => cli}/zpool.rs | 4 +- helios/tokamak/src/host.rs | 221 +++++++++++++++++++ helios/tokamak/src/host/mod.rs | 214 ------------------- helios/tokamak/src/lib.rs | 1 + illumos-utils/src/libc.rs | 2 + 25 files changed, 933 insertions(+), 404 deletions(-) create mode 100644 helios/fusion/src/host.rs create mode 100644 helios/fusion/src/interfaces/libc.rs create mode 100644 helios/fusion/src/interfaces/mod.rs create mode 100644 helios/fusion/src/interfaces/swapctl.rs create mode 100644 helios/protostar/src/executor.rs create mode 100644 helios/protostar/src/host.rs create mode 100644 helios/protostar/src/libc.rs create mode 100644 helios/protostar/src/swapctl.rs rename helios/tokamak/src/{host => cli}/dladm.rs (99%) rename helios/tokamak/src/{host => cli}/ipadm.rs (99%) create mode 100644 helios/tokamak/src/cli/mod.rs rename helios/tokamak/src/{host => cli}/parse.rs (100%) rename helios/tokamak/src/{host => cli}/route.rs (97%) rename helios/tokamak/src/{host => cli}/svcadm.rs (96%) rename helios/tokamak/src/{host => cli}/svccfg.rs (98%) rename helios/tokamak/src/{host => cli}/zfs.rs (85%) rename helios/tokamak/src/{host => cli}/zoneadm.rs (97%) rename helios/tokamak/src/{host => cli}/zonecfg.rs (99%) rename helios/tokamak/src/{host => cli}/zpool.rs (97%) create mode 100644 helios/tokamak/src/host.rs delete mode 100644 helios/tokamak/src/host/mod.rs diff --git a/helios/fusion/src/host.rs b/helios/fusion/src/host.rs new file mode 100644 index 00000000000..27433bc69a1 --- /dev/null +++ b/helios/fusion/src/host.rs @@ -0,0 +1,26 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// 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/. + +//! Represents the entire emulated host system + +use crate::interfaces::libc::Libc; +use crate::interfaces::swapctl::Swapctl; +use crate::Executor; +use std::sync::Arc; + +/// The common wrapper around the host system, which makes it trivially +/// shareable. +pub type HostSystem = Arc; + +/// Describes the interface used by Omicron when interacting with a host OS. +pub trait Host: Send + Sync { + /// Access the executor, for creating new processes + fn executor(&self) -> &dyn Executor; + + /// Access libswapctl + fn swapctl(&self) -> &dyn Swapctl; + + /// Access libc + fn libc(&self) -> &dyn Libc; +} diff --git a/helios/fusion/src/interfaces/libc.rs b/helios/fusion/src/interfaces/libc.rs new file mode 100644 index 00000000000..07c6f13f750 --- /dev/null +++ b/helios/fusion/src/interfaces/libc.rs @@ -0,0 +1,10 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// 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/. + +//! Interface to the libc API + +pub trait Libc { + /// sysconf(3c) + fn sysconf(&self, arg: i32) -> std::io::Result; +} diff --git a/helios/fusion/src/interfaces/mod.rs b/helios/fusion/src/interfaces/mod.rs new file mode 100644 index 00000000000..9c058cbcb03 --- /dev/null +++ b/helios/fusion/src/interfaces/mod.rs @@ -0,0 +1,6 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// 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/. + +pub mod libc; +pub mod swapctl; diff --git a/helios/fusion/src/interfaces/swapctl.rs b/helios/fusion/src/interfaces/swapctl.rs new file mode 100644 index 00000000000..835ec5831e5 --- /dev/null +++ b/helios/fusion/src/interfaces/swapctl.rs @@ -0,0 +1,46 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// 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/. + +//! Interface to the swapctl API + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("Error listing swap devices: {0}")] + ListDevices(String), + + #[error("Error adding swap device: {msg} (path=\"{path}\", start={start}, length={length})")] + AddDevice { msg: String, path: String, start: u64, length: u64 }, +} + +/// A representation of a swap device, as returned from swapctl(2) SC_LIST +#[derive(Debug, Clone)] +pub struct SwapDevice { + /// path to the resource + pub path: String, + + /// starting block on device used for swap + pub start: u64, + + /// length of swap area + pub length: u64, + + /// total number of pages used for swapping + pub total_pages: u64, + + /// free npages for swapping + pub free_pages: u64, + + pub flags: i64, +} + +pub trait Swapctl { + /// List swap devices on the system. + fn list_swap_devices(&self) -> Result, Error>; + fn add_swap_device( + &self, + path: String, + start: u64, + length: u64, + ) -> Result<(), Error>; +} diff --git a/helios/fusion/src/lib.rs b/helios/fusion/src/lib.rs index 0546764d9ad..a6349e9f43d 100644 --- a/helios/fusion/src/lib.rs +++ b/helios/fusion/src/lib.rs @@ -7,12 +7,15 @@ pub mod addrobj; mod error; mod executor; +mod host; mod input; +pub mod interfaces; mod output; pub mod zpool; pub use error::*; pub use executor::*; +pub use host::*; pub use input::*; pub use output::*; diff --git a/helios/protostar/src/executor.rs b/helios/protostar/src/executor.rs new file mode 100644 index 00000000000..5ad75e4f01c --- /dev/null +++ b/helios/protostar/src/executor.rs @@ -0,0 +1,154 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// 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/. + +//! A "real" [Executor] implementation, which sends commands to the host. + +use helios_fusion::{ + log_input, log_output, AsCommandStr, BoxedChild, BoxedExecutor, Child, + ExecutionError, Executor, Input, Output, +}; + +use async_trait::async_trait; +use slog::{error, Logger}; +use std::io::{Read, Write}; +use std::process::{Command, Stdio}; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::Arc; + +/// Implements [Executor] by running commands against the host system. +pub struct HostExecutor { + log: slog::Logger, + counter: std::sync::atomic::AtomicU64, +} + +impl HostExecutor { + pub fn new(log: Logger) -> Arc { + Arc::new(Self { log, counter: AtomicU64::new(0) }) + } + + pub fn as_executor(self: Arc) -> BoxedExecutor { + self + } + + fn prepare(&self, command: &Command) -> u64 { + let id = self.counter.fetch_add(1, Ordering::SeqCst); + log_input(&self.log, id, command); + id + } + + fn finalize( + &self, + command: &Command, + id: u64, + output: Output, + ) -> Result { + log_output(&self.log, id, &output); + if !output.status.success() { + return Err(ExecutionError::from_output(command, &output)); + } + Ok(output) + } +} + +#[async_trait] +impl Executor for HostExecutor { + async fn execute_async( + &self, + command: &mut tokio::process::Command, + ) -> Result { + let id = self.prepare(command.as_std()); + let output = command.output().await.map_err(|err| { + error!(self.log, "Could not start program asynchronously!"; "id" => id); + ExecutionError::ExecutionStart { + command: Input::from(command.as_std()).to_string(), + err, + } + })?; + self.finalize(command.as_std(), id, output) + } + + fn execute(&self, command: &mut Command) -> Result { + let id = self.prepare(command); + let output = command.output().map_err(|err| { + error!(self.log, "Could not start program!"; "id" => id); + ExecutionError::ExecutionStart { + command: Input::from(&*command).to_string(), + err, + } + })?; + self.finalize(command, id, output) + } + + fn spawn( + &self, + command: &mut Command, + ) -> Result { + let command_str = (&*command).into_str(); + Ok(Box::new(SpawnedChild { + child: Some( + command + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .map_err(|err| ExecutionError::ExecutionStart { + command: command_str.clone(), + err, + })?, + ), + command_str, + })) + } +} + +/// A real, host-controlled child process +pub struct SpawnedChild { + command_str: String, + child: Option, +} + +impl Child for SpawnedChild { + fn take_stdin(&mut self) -> Option> { + self.child + .as_mut()? + .stdin + .take() + .map(|s| Box::new(s) as Box) + } + + fn take_stdout(&mut self) -> Option> { + self.child + .as_mut()? + .stdout + .take() + .map(|s| Box::new(s) as Box) + } + + fn take_stderr(&mut self) -> Option> { + self.child + .as_mut()? + .stderr + .take() + .map(|s| Box::new(s) as Box) + } + + fn id(&self) -> u32 { + self.child.as_ref().expect("No child").id() + } + + fn wait(mut self: Box) -> Result { + let output = + self.child.take().unwrap().wait_with_output().map_err(|err| { + ExecutionError::ExecutionStart { + command: self.command_str.clone(), + err, + } + })?; + + if !output.status.success() { + return Err(ExecutionError::from_output(self.command_str, &output)); + } + + Ok(output) + } +} diff --git a/helios/protostar/src/host.rs b/helios/protostar/src/host.rs new file mode 100644 index 00000000000..410fe9fa781 --- /dev/null +++ b/helios/protostar/src/host.rs @@ -0,0 +1,46 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// 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/. + +use crate::executor::HostExecutor; +use crate::libc::RealLibc; +use crate::swapctl::RealSwapctl; +use helios_fusion::interfaces::{libc::Libc, swapctl::Swapctl}; +use helios_fusion::{Executor, Host, HostSystem}; +use slog::Logger; +use std::sync::Arc; + +struct RealHost { + executor: Arc, + + libc: RealLibc, + swapctl: RealSwapctl, +} + +impl RealHost { + pub fn new(log: Logger) -> Arc { + Arc::new(Self { + executor: HostExecutor::new(log), + libc: Default::default(), + swapctl: Default::default(), + }) + } + + pub fn as_host(self: Arc) -> HostSystem { + self + } +} + +impl Host for RealHost { + fn executor(&self) -> &dyn Executor { + &*self.executor + } + + fn libc(&self) -> &dyn Libc { + &self.libc + } + + fn swapctl(&self) -> &dyn Swapctl { + &self.swapctl + } +} diff --git a/helios/protostar/src/lib.rs b/helios/protostar/src/lib.rs index 5ad75e4f01c..2fe7f3d97ec 100644 --- a/helios/protostar/src/lib.rs +++ b/helios/protostar/src/lib.rs @@ -2,153 +2,9 @@ // 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/. -//! A "real" [Executor] implementation, which sends commands to the host. +mod executor; +mod host; +mod libc; +mod swapctl; -use helios_fusion::{ - log_input, log_output, AsCommandStr, BoxedChild, BoxedExecutor, Child, - ExecutionError, Executor, Input, Output, -}; - -use async_trait::async_trait; -use slog::{error, Logger}; -use std::io::{Read, Write}; -use std::process::{Command, Stdio}; -use std::sync::atomic::{AtomicU64, Ordering}; -use std::sync::Arc; - -/// Implements [Executor] by running commands against the host system. -pub struct HostExecutor { - log: slog::Logger, - counter: std::sync::atomic::AtomicU64, -} - -impl HostExecutor { - pub fn new(log: Logger) -> Arc { - Arc::new(Self { log, counter: AtomicU64::new(0) }) - } - - pub fn as_executor(self: Arc) -> BoxedExecutor { - self - } - - fn prepare(&self, command: &Command) -> u64 { - let id = self.counter.fetch_add(1, Ordering::SeqCst); - log_input(&self.log, id, command); - id - } - - fn finalize( - &self, - command: &Command, - id: u64, - output: Output, - ) -> Result { - log_output(&self.log, id, &output); - if !output.status.success() { - return Err(ExecutionError::from_output(command, &output)); - } - Ok(output) - } -} - -#[async_trait] -impl Executor for HostExecutor { - async fn execute_async( - &self, - command: &mut tokio::process::Command, - ) -> Result { - let id = self.prepare(command.as_std()); - let output = command.output().await.map_err(|err| { - error!(self.log, "Could not start program asynchronously!"; "id" => id); - ExecutionError::ExecutionStart { - command: Input::from(command.as_std()).to_string(), - err, - } - })?; - self.finalize(command.as_std(), id, output) - } - - fn execute(&self, command: &mut Command) -> Result { - let id = self.prepare(command); - let output = command.output().map_err(|err| { - error!(self.log, "Could not start program!"; "id" => id); - ExecutionError::ExecutionStart { - command: Input::from(&*command).to_string(), - err, - } - })?; - self.finalize(command, id, output) - } - - fn spawn( - &self, - command: &mut Command, - ) -> Result { - let command_str = (&*command).into_str(); - Ok(Box::new(SpawnedChild { - child: Some( - command - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .spawn() - .map_err(|err| ExecutionError::ExecutionStart { - command: command_str.clone(), - err, - })?, - ), - command_str, - })) - } -} - -/// A real, host-controlled child process -pub struct SpawnedChild { - command_str: String, - child: Option, -} - -impl Child for SpawnedChild { - fn take_stdin(&mut self) -> Option> { - self.child - .as_mut()? - .stdin - .take() - .map(|s| Box::new(s) as Box) - } - - fn take_stdout(&mut self) -> Option> { - self.child - .as_mut()? - .stdout - .take() - .map(|s| Box::new(s) as Box) - } - - fn take_stderr(&mut self) -> Option> { - self.child - .as_mut()? - .stderr - .take() - .map(|s| Box::new(s) as Box) - } - - fn id(&self) -> u32 { - self.child.as_ref().expect("No child").id() - } - - fn wait(mut self: Box) -> Result { - let output = - self.child.take().unwrap().wait_with_output().map_err(|err| { - ExecutionError::ExecutionStart { - command: self.command_str.clone(), - err, - } - })?; - - if !output.status.success() { - return Err(ExecutionError::from_output(self.command_str, &output)); - } - - Ok(output) - } -} +pub use executor::*; diff --git a/helios/protostar/src/libc.rs b/helios/protostar/src/libc.rs new file mode 100644 index 00000000000..ee78aa52b26 --- /dev/null +++ b/helios/protostar/src/libc.rs @@ -0,0 +1,20 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// 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/. + +//! Miscellaneous FFI wrapper functions for libc + +use helios_fusion::interfaces::libc::Libc; + +#[derive(Default)] +pub struct RealLibc {} + +impl Libc for RealLibc { + fn sysconf(&self, arg: i32) -> std::io::Result { + let res = unsafe { libc::sysconf(arg) }; + if res == -1 { + return Err(std::io::Error::last_os_error()); + } + Ok(res) + } +} diff --git a/helios/protostar/src/swapctl.rs b/helios/protostar/src/swapctl.rs new file mode 100644 index 00000000000..6130a9c82b2 --- /dev/null +++ b/helios/protostar/src/swapctl.rs @@ -0,0 +1,224 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// 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/. + +//! Operations for creating a system swap device. + +use helios_fusion::interfaces::swapctl::{Error, SwapDevice, Swapctl}; + +// swapctl(2) +#[cfg(target_os = "illumos")] +extern "C" { + fn swapctl(cmd: i32, arg: *mut libc::c_void) -> i32; +} + +// TODO: in the limit, we probably want to stub out all illumos-specific +// calls, and perhaps define an alternate version of this module for +// non-illumos targets. But currently, this code is only used by the real +// sled agent, and there is a fair amount of work there to make the real +// sled agent work on non-illumos targets. So for now, just stub out this +// piece. +#[cfg(not(target_os = "illumos"))] +fn swapctl(_cmd: i32, _arg: *mut libc::c_void) -> i32 { + panic!("swapctl(2) only on illumos"); +} + +// swapctl(2) commands +const SC_ADD: i32 = 0x1; +const SC_LIST: i32 = 0x2; +#[allow(dead_code)] +const SC_REMOVE: i32 = 0x3; +const SC_GETNSWP: i32 = 0x4; + +// argument for SC_ADD and SC_REMOVE +#[repr(C)] +#[derive(Debug, Copy, Clone)] +struct swapres { + sr_name: *const libc::c_char, + sr_start: libc::off_t, + sr_length: libc::off_t, +} + +// argument for SC_LIST: swaptbl with an embedded array of swt_n swapents +#[repr(C)] +#[derive(Debug, Clone)] +struct swaptbl { + swt_n: i32, + swt_ent: [swapent; N_SWAPENTS], +} +#[repr(C)] +#[derive(Debug, Copy, Clone)] +struct swapent { + ste_path: *const libc::c_char, + ste_start: libc::off_t, + ste_length: libc::off_t, + ste_pages: libc::c_long, + ste_free: libc::c_long, + ste_flags: libc::c_long, +} +impl Default for swapent { + fn default() -> Self { + Self { + ste_path: std::ptr::null_mut(), + ste_start: 0, + ste_length: 0, + ste_pages: 0, + ste_free: 0, + ste_flags: 0, + } + } +} + +/// The argument for SC_LIST (struct swaptbl) requires an embedded array in +/// the struct, with swt_n entries, each of which requires a pointer to store +/// the path to the device. +/// +/// Ideally, we would want to query the number of swap devices on the system +/// via SC_GETNSWP, allocate enough memory for each device entry, then pass +/// in pointers to memory to the list command. Unfortunately, creating a +/// generically large array embedded in a struct that can be passed to C is a +/// bit of a challenge in safe Rust. So instead, we just pick a reasonable +/// max number of devices to list. +/// +/// We pick a max of 3 devices, somewhat arbitrarily. We only ever expect to +/// see 0 or 1 swap device(s); if there are more, that is a bug. In the case +/// that we see more than 1 swap device, we log a warning, and eventually, we +/// should send an ereport. +const N_SWAPENTS: usize = 3; + +/// Wrapper around swapctl(2) call. All commands except SC_GETNSWP require an +/// argument, hence `data` being an optional parameter. +unsafe fn swapctl_cmd( + cmd: i32, + data: Option>, +) -> std::io::Result { + assert!(cmd >= SC_ADD && cmd <= SC_GETNSWP, "invalid swapctl cmd: {cmd}"); + + let ptr = match data { + Some(v) => v.as_ptr() as *mut libc::c_void, + None => std::ptr::null_mut(), + }; + + let res = swapctl(cmd, ptr); + if res == -1 { + return Err(std::io::Error::last_os_error()); + } + + Ok(res as u32) +} + +#[derive(Default)] +pub(crate) struct RealSwapctl {} + +impl Swapctl for RealSwapctl { + /// List swap devices on the system. + fn list_swap_devices(&self) -> Result, Error> { + // Each swapent requires a char * pointer in our control for the + // `ste_path` field,, which the kernel will fill in with a path if there + // is a swap device for that entry. Because these pointers are mutated + // by the kernel, we mark them as mutable. (Note that the compiler will + // happily accept these definitions as non-mutable, since it can't know + // what happens to the pointers on the C side, but not marking them as + // mutable when they may be in fact be mutated is undefined behavior). + // + // Per limits.h(3HEAD), PATH_MAX is the max number of bytes in a path + // name, including the null terminating character, so these buffers + // have sufficient space for paths on the system. + const MAXPATHLEN: usize = libc::PATH_MAX as usize; + let mut p1 = [0i8; MAXPATHLEN]; + let mut p2 = [0i8; MAXPATHLEN]; + let mut p3 = [0i8; MAXPATHLEN]; + let entries: [swapent; N_SWAPENTS] = [ + swapent { + ste_path: &mut p1 as *mut libc::c_char, + ..Default::default() + }, + swapent { + ste_path: &mut p2 as *mut libc::c_char, + ..Default::default() + }, + swapent { + ste_path: &mut p3 as *mut libc::c_char, + ..Default::default() + }, + ]; + + let mut list_req = + swaptbl { swt_n: N_SWAPENTS as i32, swt_ent: entries }; + // Unwrap safety: We know this isn't null because we just created it + let ptr = std::ptr::NonNull::new(&mut list_req).unwrap(); + let n_devices = unsafe { + swapctl_cmd(SC_LIST, Some(ptr)) + .map_err(|e| Error::ListDevices(e.to_string()))? + }; + + let mut devices = Vec::with_capacity(n_devices as usize); + for i in 0..n_devices as usize { + let e = list_req.swt_ent[i]; + + // Safety: CStr::from_ptr is documented as safe if: + // 1. The pointer contains a valid null terminator at the end of + // the string + // 2. The pointer is valid for reads of bytes up to and including + // the null terminator + // 3. The memory referenced by the return CStr is not mutated for + // the duration of lifetime 'a + // + // (1) is true because we initialize the buffers for ste_path as all + // 0s, and their length is long enough to include the null + // terminator for all paths on the system. + // (2) should be guaranteed by the syscall itself, and we can know + // how many entries are valid via its return value. + // (3) We aren't currently mutating the memory referenced by the + // CStr, though there's nothing here enforcing that. + let p = unsafe { std::ffi::CStr::from_ptr(e.ste_path) }; + let path = String::from_utf8_lossy(p.to_bytes()).to_string(); + + devices.push(SwapDevice { + path: path, + start: e.ste_start as u64, + length: e.ste_length as u64, + total_pages: e.ste_pages as u64, + free_pages: e.ste_free as u64, + flags: e.ste_flags, + }); + } + + Ok(devices) + } + + fn add_swap_device( + &self, + path: String, + start: u64, + length: u64, + ) -> Result<(), Error> { + let path_cp = path.clone(); + let name = + std::ffi::CString::new(path).map_err(|e| Error::AddDevice { + msg: format!("could not convert path to CString: {}", e,), + path: path_cp.clone(), + start: start, + length: length, + })?; + + let mut add_req = swapres { + sr_name: name.as_ptr(), + sr_start: start as i64, + sr_length: length as i64, + }; + // Unwrap safety: We know this isn't null because we just created it + let ptr = std::ptr::NonNull::new(&mut add_req).unwrap(); + let res = unsafe { + swapctl_cmd(SC_ADD, Some(ptr)).map_err(|e| Error::AddDevice { + msg: e.to_string(), + path: path_cp, + start: start, + length: length, + })? + }; + assert_eq!(res, 0); + + Ok(()) + } +} diff --git a/helios/tokamak/src/host/dladm.rs b/helios/tokamak/src/cli/dladm.rs similarity index 99% rename from helios/tokamak/src/host/dladm.rs rename to helios/tokamak/src/cli/dladm.rs index f0cb4be318f..46bcf8e55e8 100644 --- a/helios/tokamak/src/host/dladm.rs +++ b/helios/tokamak/src/cli/dladm.rs @@ -2,7 +2,7 @@ // 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/. -use crate::host::parse::InputParser; +use crate::cli::parse::InputParser; use crate::host::LinkName; use helios_fusion::Input; @@ -59,7 +59,7 @@ pub(crate) enum Command { impl TryFrom for Command { type Error = String; - fn try_from(mut input: Input) -> Result { + fn try_from(input: Input) -> Result { if input.program != DLADM { return Err(format!("Not dladm command: {}", input.program)); } diff --git a/helios/tokamak/src/host/ipadm.rs b/helios/tokamak/src/cli/ipadm.rs similarity index 99% rename from helios/tokamak/src/host/ipadm.rs rename to helios/tokamak/src/cli/ipadm.rs index 57fb6ee4ab4..95f8dbd7f5d 100644 --- a/helios/tokamak/src/host/ipadm.rs +++ b/helios/tokamak/src/cli/ipadm.rs @@ -2,7 +2,7 @@ // 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/. -use crate::host::parse::InputParser; +use crate::cli::parse::InputParser; use crate::host::{AddrType, IpInterfaceName}; use helios_fusion::addrobj::AddrObject; @@ -43,7 +43,7 @@ pub(crate) enum Command { impl TryFrom for Command { type Error = String; - fn try_from(mut input: Input) -> Result { + fn try_from(input: Input) -> Result { if input.program != IPADM { return Err(format!("Not ipadm command: {}", input.program)); } diff --git a/helios/tokamak/src/cli/mod.rs b/helios/tokamak/src/cli/mod.rs new file mode 100644 index 00000000000..992b4b9ebe3 --- /dev/null +++ b/helios/tokamak/src/cli/mod.rs @@ -0,0 +1,89 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// 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/. + +//! Emulates an illumos system + +// TODO: REMOVE +#![allow(dead_code)] + +use crate::host::ZoneName; + +use helios_fusion::Input; +use helios_fusion::{ + DLADM, IPADM, PFEXEC, ROUTE, SVCADM, SVCCFG, ZFS, ZLOGIN, ZONEADM, ZONECFG, + ZPOOL, +}; + +// Command-line utilities +mod dladm; +mod ipadm; +mod route; +mod svcadm; +mod svccfg; +mod zfs; +mod zoneadm; +mod zonecfg; +mod zpool; + +// Utilities for parsing +mod parse; + +use crate::cli::parse::InputExt; + +enum KnownCommand { + Coreadm, // TODO + Dladm(dladm::Command), + Dumpadm, // TODO + Ipadm(ipadm::Command), + Fstyp, // TODO + RouteAdm, // TODO + Route(route::Command), + Savecore, // TODO + Svccfg(svccfg::Command), + Svcadm(svcadm::Command), + Zfs(zfs::Command), + Zoneadm(zoneadm::Command), + Zonecfg(zonecfg::Command), + Zpool(zpool::Command), +} + +struct Command { + with_pfexec: bool, + in_zone: Option, + cmd: KnownCommand, +} + +impl TryFrom for Command { + type Error = String; + + fn try_from(mut input: Input) -> Result { + let mut with_pfexec = false; + let mut in_zone = None; + + while input.program == PFEXEC { + with_pfexec = true; + input.shift_program()?; + } + if input.program == ZLOGIN { + input.shift_program()?; + in_zone = Some(ZoneName(input.shift_program()?)); + } + + use KnownCommand::*; + let cmd = match input.program.as_str() { + DLADM => Dladm(dladm::Command::try_from(input)?), + IPADM => Ipadm(ipadm::Command::try_from(input)?), + ROUTE => Route(route::Command::try_from(input)?), + SVCCFG => Svccfg(svccfg::Command::try_from(input)?), + SVCADM => Svcadm(svcadm::Command::try_from(input)?), + ZFS => Zfs(zfs::Command::try_from(input)?), + ZONEADM => Zoneadm(zoneadm::Command::try_from(input)?), + ZONECFG => Zonecfg(zonecfg::Command::try_from(input)?), + ZPOOL => Zpool(zpool::Command::try_from(input)?), + _ => return Err(format!("Unknown command: {}", input.program)), + }; + + Ok(Command { with_pfexec, in_zone, cmd }) + } +} diff --git a/helios/tokamak/src/host/parse.rs b/helios/tokamak/src/cli/parse.rs similarity index 100% rename from helios/tokamak/src/host/parse.rs rename to helios/tokamak/src/cli/parse.rs diff --git a/helios/tokamak/src/host/route.rs b/helios/tokamak/src/cli/route.rs similarity index 97% rename from helios/tokamak/src/host/route.rs rename to helios/tokamak/src/cli/route.rs index 894a5a3b4bb..569a67e0173 100644 --- a/helios/tokamak/src/host/route.rs +++ b/helios/tokamak/src/cli/route.rs @@ -2,7 +2,7 @@ // 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/. -use crate::host::parse::InputParser; +use crate::cli::parse::InputParser; use crate::host::LinkName; use helios_fusion::Input; @@ -57,7 +57,7 @@ pub(crate) enum Command { impl TryFrom for Command { type Error = String; - fn try_from(mut input: Input) -> Result { + fn try_from(input: Input) -> Result { if input.program != ROUTE { return Err(format!("Not route command: {}", input.program)); } diff --git a/helios/tokamak/src/host/svcadm.rs b/helios/tokamak/src/cli/svcadm.rs similarity index 96% rename from helios/tokamak/src/host/svcadm.rs rename to helios/tokamak/src/cli/svcadm.rs index 87165a26530..eccea13a65f 100644 --- a/helios/tokamak/src/host/svcadm.rs +++ b/helios/tokamak/src/cli/svcadm.rs @@ -2,7 +2,7 @@ // 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/. -use crate::host::parse::InputParser; +use crate::cli::parse::InputParser; use crate::host::{ServiceName, ZoneName}; use helios_fusion::Input; @@ -16,7 +16,7 @@ pub enum Command { impl TryFrom for Command { type Error = String; - fn try_from(mut input: Input) -> Result { + fn try_from(input: Input) -> Result { if input.program != SVCADM { return Err(format!("Not svcadm command: {}", input.program)); } diff --git a/helios/tokamak/src/host/svccfg.rs b/helios/tokamak/src/cli/svccfg.rs similarity index 98% rename from helios/tokamak/src/host/svccfg.rs rename to helios/tokamak/src/cli/svccfg.rs index 28753d44ed7..5520702e3da 100644 --- a/helios/tokamak/src/host/svccfg.rs +++ b/helios/tokamak/src/cli/svccfg.rs @@ -2,7 +2,7 @@ // 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/. -use crate::host::parse::InputParser; +use crate::cli::parse::InputParser; use crate::host::{ServiceName, ZoneName}; use camino::Utf8PathBuf; @@ -54,7 +54,7 @@ pub(crate) enum Command { impl TryFrom for Command { type Error = String; - fn try_from(mut input: Input) -> Result { + fn try_from(input: Input) -> Result { if input.program != SVCCFG { return Err(format!("Not svccfg command: {}", input.program)); } @@ -105,7 +105,7 @@ impl TryFrom for Command { .map_err(|e| e.to_string())?; let group_type = input.shift_arg()?; - if let Some(flags) = input.shift_arg().ok() { + if let Some(_flags) = input.shift_arg().ok() { return Err( "Parsing of optional flags not implemented".to_string() ); diff --git a/helios/tokamak/src/host/zfs.rs b/helios/tokamak/src/cli/zfs.rs similarity index 85% rename from helios/tokamak/src/host/zfs.rs rename to helios/tokamak/src/cli/zfs.rs index dfbea3b8bc8..477f84e4a24 100644 --- a/helios/tokamak/src/host/zfs.rs +++ b/helios/tokamak/src/cli/zfs.rs @@ -2,8 +2,8 @@ // 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/. -use crate::host::parse::InputParser; -use crate::host::FilesystemName; +use crate::cli::parse::InputParser; +use crate::host::{DatasetName, FilesystemName, VolumeName}; use helios_fusion::Input; use helios_fusion::ZFS; @@ -18,13 +18,13 @@ pub(crate) enum Command { sparse: bool, blocksize: Option, size: u64, - name: FilesystemName, + name: VolumeName, }, Destroy { recursive_dependents: bool, recursive_children: bool, force_unmount: bool, - name: FilesystemName, + name: DatasetName, }, Get { recursive: bool, @@ -32,13 +32,13 @@ pub(crate) enum Command { // name, property, value, source fields: Vec, properties: Vec, - datasets: Option>, + datasets: Option>, }, List { recursive: bool, depth: Option, properties: Vec, - datasets: Option>, + datasets: Option>, }, Mount { load_keys: bool, @@ -46,14 +46,14 @@ pub(crate) enum Command { }, Set { properties: Vec<(String, String)>, - name: FilesystemName, + name: DatasetName, }, } impl TryFrom for Command { type Error = String; - fn try_from(mut input: Input) -> Result { + fn try_from(input: Input) -> Result { if input.program != ZFS { return Err(format!("Not zfs command: {}", input.program)); } @@ -69,11 +69,29 @@ impl TryFrom for Command { while input.args().len() > 1 { // Volume Size (volumes only, required) if input.shift_arg_if("-V")? { + let size_str = input.shift_arg()?; + + let (size_str, multiplier) = if let Some(size_str) = + size_str.strip_suffix('G') + { + (size_str, (1 << 30)) + } else if let Some(size_str) = + size_str.strip_suffix('M') + { + (size_str, (1 << 20)) + } else if let Some(size_str) = + size_str.strip_suffix('K') + { + (size_str, (1 << 10)) + } else { + (size_str.as_str(), 1) + }; + size = Some( - input - .shift_arg()? + size_str .parse::() - .map_err(|e| e.to_string())?, + .map_err(|e| e.to_string())? + * multiplier, ); // Sparse (volumes only, optional) } else if input.shift_arg_if("-s")? { @@ -95,12 +113,13 @@ impl TryFrom for Command { properties.push((k.to_string(), v.to_string())); } } - let name = FilesystemName(input.shift_arg()?); + let name = input.shift_arg()?; input.no_args_remaining()?; if let Some(size) = size { // Volume let sparse = sparse.unwrap_or(false); + let name = VolumeName(name); Ok(Command::CreateVolume { properties, sparse, @@ -113,6 +132,7 @@ impl TryFrom for Command { if sparse.is_some() || blocksize.is_some() { return Err("Using volume arguments, but forgot to specify '-V size'?".to_string()); } + let name = FilesystemName(name); Ok(Command::CreateFilesystem { properties, name }) } } @@ -139,7 +159,7 @@ impl TryFrom for Command { } } } else { - name = Some(FilesystemName(arg)); + name = Some(DatasetName(arg)); input.no_args_remaining()?; } } @@ -217,8 +237,8 @@ impl TryFrom for Command { input .args() .into_iter() - .map(|s| s.into()) - .collect::>(), + .map(|s| DatasetName(s.to_string())) + .collect::>(), ); if !scripting || !parsable { return Err("You should run 'zfs get' commands with the '-Hp' flags enabled".to_string()); @@ -288,16 +308,18 @@ impl TryFrom for Command { } else { // As soon as non-flag arguments are passed, the rest of // the arguments are treated as datasets. - datasets = Some(vec![arg]); + datasets = Some(vec![DatasetName(arg.to_string())]); break; } } let remaining_datasets = input.args(); if !remaining_datasets.is_empty() { - datasets - .get_or_insert(vec![]) - .extend(remaining_datasets.into_iter().cloned()); + datasets.get_or_insert(vec![]).extend( + remaining_datasets + .into_iter() + .map(|d| DatasetName(d.to_string())), + ); }; if !scripting || !parsable { @@ -322,7 +344,7 @@ impl TryFrom for Command { .ok_or_else(|| format!("Bad property: {prop}"))?; properties.push((k.to_string(), v.to_string())); } - let name = FilesystemName(input.shift_arg()?); + let name = DatasetName(input.shift_arg()?); input.no_args_remaining()?; Ok(Command::Set { properties, name }) @@ -338,23 +360,34 @@ mod test { #[test] fn create() { + // Create a filesystem let Command::CreateFilesystem { properties, name } = Command::try_from( Input::shell(format!("{ZFS} create myfilesystem")) ).unwrap() else { panic!("wrong command") }; - assert_eq!(properties, vec![]); assert_eq!(name.0, "myfilesystem"); + // Create a volume let Command::CreateVolume { properties, sparse, blocksize, size, name } = Command::try_from( Input::shell(format!("{ZFS} create -s -V 1024 -b 512 -o foo=bar myvolume")) ).unwrap() else { panic!("wrong command") }; - assert_eq!(properties, vec![("foo".to_string(), "bar".to_string())]); assert_eq!(name.0, "myvolume"); assert!(sparse); assert_eq!(size, 1024); assert_eq!(blocksize, Some(512)); + // Create a volume (using letter suffix) + let Command::CreateVolume { properties, sparse, blocksize, size, name } = Command::try_from( + Input::shell(format!("{ZFS} create -s -V 2G -b 512 -o foo=bar myvolume")) + ).unwrap() else { panic!("wrong command") }; + assert_eq!(properties, vec![("foo".to_string(), "bar".to_string())]); + assert_eq!(name.0, "myvolume"); + assert!(sparse); + assert_eq!(size, 2 << 30); + assert_eq!(blocksize, Some(512)); + + // Create volume (invalid) assert!(Command::try_from(Input::shell(format!( "{ZFS} create -s -b 512 -o foo=bar myvolume" ))) @@ -393,7 +426,10 @@ mod test { assert_eq!(depth, Some(10)); assert_eq!(fields, vec!["name", "value"]); assert_eq!(properties, vec!["mounted", "available"]); - assert_eq!(datasets.unwrap(), vec!["myvolume"]); + assert_eq!( + datasets.unwrap(), + vec![DatasetName("myvolume".to_string())] + ); assert!(Command::try_from(Input::shell(format!( "{ZFS} get -o name,value mounted,available myvolume" @@ -414,7 +450,10 @@ mod test { assert!(recursive); assert_eq!(depth.unwrap(), 1); assert_eq!(properties, vec!["name"]); - assert_eq!(datasets.unwrap(), vec!["myfilesystem"]); + assert_eq!( + datasets.unwrap(), + vec![DatasetName("myfilesystem".to_string())] + ); assert!(Command::try_from(Input::shell(format!( "{ZFS} list name myfilesystem" diff --git a/helios/tokamak/src/host/zoneadm.rs b/helios/tokamak/src/cli/zoneadm.rs similarity index 97% rename from helios/tokamak/src/host/zoneadm.rs rename to helios/tokamak/src/cli/zoneadm.rs index a54f24ea619..39f7af0e5e7 100644 --- a/helios/tokamak/src/host/zoneadm.rs +++ b/helios/tokamak/src/cli/zoneadm.rs @@ -2,7 +2,7 @@ // 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/. -use crate::host::parse::InputParser; +use crate::cli::parse::InputParser; use crate::host::ZoneName; use helios_fusion::Input; @@ -33,7 +33,7 @@ pub(crate) enum Command { impl TryFrom for Command { type Error = String; - fn try_from(mut input: Input) -> Result { + fn try_from(input: Input) -> Result { if input.program != ZONEADM { return Err(format!("Not zoneadm command: {}", input.program)); } diff --git a/helios/tokamak/src/host/zonecfg.rs b/helios/tokamak/src/cli/zonecfg.rs similarity index 99% rename from helios/tokamak/src/host/zonecfg.rs rename to helios/tokamak/src/cli/zonecfg.rs index e545cba7016..2e86342fdd7 100644 --- a/helios/tokamak/src/host/zonecfg.rs +++ b/helios/tokamak/src/cli/zonecfg.rs @@ -2,7 +2,7 @@ // 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/. -use crate::host::parse::InputParser; +use crate::cli::parse::InputParser; use crate::host::{ZoneConfig, ZoneName}; use camino::Utf8PathBuf; @@ -17,7 +17,7 @@ pub(crate) enum Command { impl TryFrom for Command { type Error = String; - fn try_from(mut input: Input) -> Result { + fn try_from(input: Input) -> Result { if input.program != ZONECFG { return Err(format!("Not zonecfg command: {}", input.program)); } diff --git a/helios/tokamak/src/host/zpool.rs b/helios/tokamak/src/cli/zpool.rs similarity index 97% rename from helios/tokamak/src/host/zpool.rs rename to helios/tokamak/src/cli/zpool.rs index 297c83410ae..274dc496640 100644 --- a/helios/tokamak/src/host/zpool.rs +++ b/helios/tokamak/src/cli/zpool.rs @@ -2,7 +2,7 @@ // 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/. -use crate::host::parse::InputParser; +use crate::cli::parse::InputParser; use helios_fusion::Input; use helios_fusion::ZPOOL; @@ -20,7 +20,7 @@ pub(crate) enum Command { impl TryFrom for Command { type Error = String; - fn try_from(mut input: Input) -> Result { + fn try_from(input: Input) -> Result { if input.program != ZPOOL { return Err(format!("Not zpool command: {}", input.program)); } diff --git a/helios/tokamak/src/host.rs b/helios/tokamak/src/host.rs new file mode 100644 index 00000000000..6e1331dfc49 --- /dev/null +++ b/helios/tokamak/src/host.rs @@ -0,0 +1,221 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// 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/. + +//! Emulates an illumos system + +// TODO: REMOVE +#![allow(dead_code)] + +use crate::{FakeExecutor, FakeExecutorBuilder}; + +use camino::Utf8PathBuf; +use helios_fusion::interfaces::libc; +use helios_fusion::interfaces::swapctl; +use helios_fusion::zpool::ZpoolName; +use ipnetwork::IpNetwork; +use slog::Logger; +use std::collections::{HashMap, HashSet}; +use std::sync::{Arc, Mutex}; + +pub enum LinkType { + Etherstub, + Vnic, +} + +#[derive(Debug, PartialEq, Eq)] +pub struct LinkName(pub String); +struct Link { + pub ty: LinkType, + pub parent: Option, + pub properties: HashMap, +} + +pub struct IpInterfaceName(pub String); +pub struct IpInterface {} + +pub enum RouteDestination { + Default, + Literal(IpNetwork), +} + +pub struct Route { + pub destination: RouteDestination, + pub gateway: IpNetwork, +} + +#[derive(Debug)] +pub struct ServiceName(pub String); + +pub struct Service { + pub state: smf::SmfState, + pub properties: HashMap, +} + +struct ZoneEnvironment { + id: u64, + links: HashMap, + ip_interfaces: HashMap, + routes: Vec, + services: HashMap, +} + +impl ZoneEnvironment { + fn new(id: u64) -> Self { + Self { + id, + links: HashMap::new(), + ip_interfaces: HashMap::new(), + routes: vec![], + services: HashMap::new(), + } + } +} + +#[derive(Debug)] +pub struct ZoneName(pub String); + +pub struct ZoneConfig { + pub state: zone::State, + pub brand: String, + // zonepath + pub path: Utf8PathBuf, + pub datasets: Vec, + pub devices: Vec, + pub nets: Vec, + pub fs: Vec, + // E.g. zone image, overlays, etc. + pub layers: Vec, +} + +struct Zone { + config: ZoneConfig, + environment: ZoneEnvironment, +} + +struct FakeHost { + executor: Arc, + global: ZoneEnvironment, + zones: HashMap, + + // TODO: Is this the right abstraction layer? + // How do you want to represent zpools & filesystems? + // + // TODO: Should filesystems be part of the "ZoneEnvironment" abstraction? + zpools: HashSet, + + swap_devices: Mutex>, +} + +impl FakeHost { + pub fn new(log: Logger) -> Arc { + Arc::new(Self { + executor: FakeExecutorBuilder::new(log).build(), + global: ZoneEnvironment::new(0), + zones: HashMap::new(), + zpools: HashSet::new(), + swap_devices: Mutex::new(vec![]), + }) + } + + fn page_size(&self) -> i64 { + 4096 + } +} + +impl libc::Libc for FakeHost { + fn sysconf(&self, arg: i32) -> std::io::Result { + use ::libc::_SC_PAGESIZE; + + match arg { + _SC_PAGESIZE => Ok(self.page_size()), + _ => Err(std::io::Error::new( + std::io::ErrorKind::Unsupported, + "unknown sysconf", + )), + } + } +} + +impl swapctl::Swapctl for FakeHost { + fn list_swap_devices( + &self, + ) -> Result, swapctl::Error> { + Ok(self.swap_devices.lock().unwrap().clone()) + } + + fn add_swap_device( + &self, + path: String, + start: u64, + length: u64, + ) -> Result<(), swapctl::Error> { + // TODO: Parse path, ensure the zvol exists? + + let mut swap_devices = self.swap_devices.lock().unwrap(); + for device in &*swap_devices { + if device.path == path { + let msg = "device already used for swap".to_string(); + return Err(swapctl::Error::AddDevice { + msg, + path, + start, + length, + }); + } + } + + swap_devices.push(swapctl::SwapDevice { + path, + start, + length, + // NOTE: Using dummy values until we have a reasonable way to + // populate this info. + total_pages: 0xffff, + free_pages: 0xffff, + flags: 0xffff, + }); + Ok(()) + } +} + +impl helios_fusion::Host for FakeHost { + fn executor(&self) -> &dyn helios_fusion::Executor { + &*self.executor + } + + fn swapctl(&self) -> &dyn swapctl::Swapctl { + self + } + + fn libc(&self) -> &dyn libc::Libc { + self + } +} + +#[derive(Debug, PartialEq)] +pub enum AddrType { + Dhcp, + Static(IpNetwork), + Addrconf, +} + +/// The name of a ZFS filesystem, volume, or snapshot +#[derive(Debug, Eq, PartialEq)] +pub struct DatasetName(pub String); + +#[derive(Debug, Eq, PartialEq)] +pub struct FilesystemName(pub String); +impl From for DatasetName { + fn from(name: FilesystemName) -> Self { + Self(name.0) + } +} + +#[derive(Debug, Eq, PartialEq)] +pub struct VolumeName(pub String); +impl From for DatasetName { + fn from(name: VolumeName) -> Self { + Self(name.0) + } +} diff --git a/helios/tokamak/src/host/mod.rs b/helios/tokamak/src/host/mod.rs deleted file mode 100644 index fe26175b515..00000000000 --- a/helios/tokamak/src/host/mod.rs +++ /dev/null @@ -1,214 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// 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/. - -//! Emulates an illumos system - -// TODO REMOVE ME -#![allow(dead_code)] -#![allow(unused_mut)] -#![allow(unused_variables)] - -use camino::Utf8PathBuf; -use helios_fusion::zpool::ZpoolName; -use helios_fusion::Input; -use helios_fusion::{ - DLADM, IPADM, PFEXEC, ROUTE, SVCADM, SVCCFG, ZFS, ZLOGIN, ZONEADM, ZONECFG, - ZPOOL, -}; -use ipnetwork::IpNetwork; -use std::collections::{HashMap, HashSet}; - -// Parsing command-line utilities -mod dladm; -mod ipadm; -mod route; -mod svcadm; -mod svccfg; -mod zfs; -mod zoneadm; -mod zonecfg; -mod zpool; - -mod parse; - -use crate::host::parse::InputExt; - -enum LinkType { - Etherstub, - Vnic, -} - -#[derive(Debug, PartialEq, Eq)] -pub struct LinkName(String); -struct Link { - ty: LinkType, - parent: Option, - properties: HashMap, -} - -pub struct IpInterfaceName(String); -struct IpInterface {} - -enum RouteDestination { - Default, - Literal(IpNetwork), -} - -struct Route { - destination: RouteDestination, - gateway: IpNetwork, -} - -#[derive(Debug)] -pub struct ServiceName(String); - -struct Service { - state: smf::SmfState, - properties: HashMap, -} - -struct ZoneEnvironment { - id: u64, - links: HashMap, - ip_interfaces: HashMap, - routes: Vec, - services: HashMap, -} - -impl ZoneEnvironment { - fn new(id: u64) -> Self { - Self { - id, - links: HashMap::new(), - ip_interfaces: HashMap::new(), - routes: vec![], - services: HashMap::new(), - } - } -} - -#[derive(Debug)] -pub struct ZoneName(String); - -pub struct ZoneConfig { - state: zone::State, - brand: String, - // zonepath - path: Utf8PathBuf, - datasets: Vec, - devices: Vec, - nets: Vec, - fs: Vec, - // E.g. zone image, overlays, etc. - layers: Vec, -} - -struct Zone { - config: ZoneConfig, - environment: ZoneEnvironment, -} - -struct Host { - global: ZoneEnvironment, - zones: HashMap, - - // TODO: Is this the right abstraction layer? - // How do you want to represent zpools & filesystems? - // - // TODO: Should filesystems be part of the "ZoneEnvironment" abstraction? - zpools: HashSet, -} - -impl Host { - pub fn new() -> Self { - Self { - global: ZoneEnvironment::new(0), - zones: HashMap::new(), - zpools: HashSet::new(), - } - } -} - -#[derive(Debug, PartialEq)] -pub enum AddrType { - Dhcp, - Static(IpNetwork), - Addrconf, -} - -pub struct FilesystemName(String); - -enum KnownCommand { - Coreadm, // TODO - Dladm(dladm::Command), - Dumpadm, // TODO - Ipadm(ipadm::Command), - Fstyp, // TODO - RouteAdm, // TODO - Route(route::Command), - Savecore, // TODO - Svccfg(svccfg::Command), - Svcadm(svcadm::Command), - Zfs(zfs::Command), - Zoneadm(zoneadm::Command), - Zonecfg(zonecfg::Command), - Zpool(zpool::Command), -} - -struct Command { - with_pfexec: bool, - in_zone: Option, - cmd: KnownCommand, -} - -impl TryFrom for Command { - type Error = String; - - fn try_from(mut input: Input) -> Result { - let mut with_pfexec = false; - let mut in_zone = None; - - while input.program == PFEXEC { - with_pfexec = true; - input.shift_program()?; - } - if input.program == ZLOGIN { - input.shift_program()?; - in_zone = Some(ZoneName(input.shift_program()?)); - } - - use KnownCommand::*; - let cmd = match input.program.as_str() { - DLADM => Dladm(dladm::Command::try_from(input)?), - IPADM => Ipadm(ipadm::Command::try_from(input)?), - ROUTE => Route(route::Command::try_from(input)?), - SVCCFG => Svccfg(svccfg::Command::try_from(input)?), - SVCADM => Svcadm(svcadm::Command::try_from(input)?), - ZFS => Zfs(zfs::Command::try_from(input)?), - ZONEADM => Zoneadm(zoneadm::Command::try_from(input)?), - ZONECFG => Zonecfg(zonecfg::Command::try_from(input)?), - ZPOOL => Zpool(zpool::Command::try_from(input)?), - _ => return Err(format!("Unknown command: {}", input.program)), - }; - - Ok(Command { with_pfexec, in_zone, cmd }) - } -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn empty_state() { - let host = Host::new(); - - assert_eq!(0, host.global.id); - assert!(host.global.links.is_empty()); - assert!(host.global.ip_interfaces.is_empty()); - assert!(host.global.routes.is_empty()); - assert!(host.global.services.is_empty()); - assert!(host.zones.is_empty()); - } -} diff --git a/helios/tokamak/src/lib.rs b/helios/tokamak/src/lib.rs index 918d2547f35..3b90c7fb1d9 100644 --- a/helios/tokamak/src/lib.rs +++ b/helios/tokamak/src/lib.rs @@ -2,6 +2,7 @@ // 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/. +mod cli; mod executor; mod host; mod shared_byte_queue; diff --git a/illumos-utils/src/libc.rs b/illumos-utils/src/libc.rs index 02d8c613307..a307a027668 100644 --- a/illumos-utils/src/libc.rs +++ b/illumos-utils/src/libc.rs @@ -4,6 +4,8 @@ //! Miscellaneous FFI wrapper functions for libc +// TODO: Mark deprecated in favor of helios_fusion interface + /// sysconf(3c) pub fn sysconf(arg: i32) -> std::io::Result { let res = unsafe { libc::sysconf(arg) }; From f04adf1e860fad8fb763ad819129bb80881a8d1c Mon Sep 17 00:00:00 2001 From: Sean Klein Date: Thu, 24 Aug 2023 11:58:38 -0700 Subject: [PATCH 10/18] Graphs, and some zfs / zpool commands --- Cargo.lock | 1 + helios/tokamak/Cargo.toml | 1 + helios/tokamak/src/cli/mod.rs | 39 +- helios/tokamak/src/cli/zfs.rs | 14 +- helios/tokamak/src/cli/zpool.rs | 36 +- helios/tokamak/src/executor.rs | 15 +- helios/tokamak/src/host.rs | 640 ++++++++++++++++++++++++++++++-- 7 files changed, 679 insertions(+), 67 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 198dcbd8822..06e30adaec9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3080,6 +3080,7 @@ dependencies = [ "libc", "omicron-common 0.1.0", "omicron-test-utils", + "petgraph", "schemars", "serde", "shlex", diff --git a/helios/tokamak/Cargo.toml b/helios/tokamak/Cargo.toml index 544801b7db1..16025d0cee7 100644 --- a/helios/tokamak/Cargo.toml +++ b/helios/tokamak/Cargo.toml @@ -16,6 +16,7 @@ itertools.workspace = true ipnetwork.workspace = true libc.workspace = true omicron-common.workspace = true +petgraph.workspace = true schemars.workspace = true serde.workspace = true shlex.workspace = true diff --git a/helios/tokamak/src/cli/mod.rs b/helios/tokamak/src/cli/mod.rs index 992b4b9ebe3..0cef9b4539d 100644 --- a/helios/tokamak/src/cli/mod.rs +++ b/helios/tokamak/src/cli/mod.rs @@ -2,7 +2,7 @@ // 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/. -//! Emulates an illumos system +//! Parsing of CLI-based arguments to a Helios system // TODO: REMOVE #![allow(dead_code)] @@ -16,22 +16,22 @@ use helios_fusion::{ }; // Command-line utilities -mod dladm; -mod ipadm; -mod route; -mod svcadm; -mod svccfg; -mod zfs; -mod zoneadm; -mod zonecfg; -mod zpool; +pub(crate) mod dladm; +pub(crate) mod ipadm; +pub(crate) mod route; +pub(crate) mod svcadm; +pub(crate) mod svccfg; +pub(crate) mod zfs; +pub(crate) mod zoneadm; +pub(crate) mod zonecfg; +pub(crate) mod zpool; // Utilities for parsing mod parse; use crate::cli::parse::InputExt; -enum KnownCommand { +pub(crate) enum KnownCommand { Coreadm, // TODO Dladm(dladm::Command), Dumpadm, // TODO @@ -48,12 +48,27 @@ enum KnownCommand { Zpool(zpool::Command), } -struct Command { +pub(crate) struct Command { with_pfexec: bool, in_zone: Option, cmd: KnownCommand, } +impl Command { + pub fn with_pfexec(&self) -> bool { + self.with_pfexec + } + pub fn in_zone(&self) -> &Option { + &self.in_zone + } + pub fn cmd(&self) -> &KnownCommand { + &self.cmd + } + pub fn as_cmd(self) -> KnownCommand { + self.cmd + } +} + impl TryFrom for Command { type Error = String; diff --git a/helios/tokamak/src/cli/zfs.rs b/helios/tokamak/src/cli/zfs.rs index 477f84e4a24..e5f79861fc1 100644 --- a/helios/tokamak/src/cli/zfs.rs +++ b/helios/tokamak/src/cli/zfs.rs @@ -119,7 +119,7 @@ impl TryFrom for Command { if let Some(size) = size { // Volume let sparse = sparse.unwrap_or(false); - let name = VolumeName(name); + let name = FilesystemName::new(name)?; Ok(Command::CreateVolume { properties, sparse, @@ -132,7 +132,7 @@ impl TryFrom for Command { if sparse.is_some() || blocksize.is_some() { return Err("Using volume arguments, but forgot to specify '-V size'?".to_string()); } - let name = FilesystemName(name); + let name = FilesystemName::new(name)?; Ok(Command::CreateFilesystem { properties, name }) } } @@ -330,7 +330,7 @@ impl TryFrom for Command { } "mount" => { let load_keys = input.shift_arg_if("-l")?; - let filesystem = FilesystemName(input.shift_arg()?); + let filesystem = FilesystemName::new(input.shift_arg()?)?; input.no_args_remaining()?; Ok(Command::Mount { load_keys, filesystem }) } @@ -365,14 +365,14 @@ mod test { Input::shell(format!("{ZFS} create myfilesystem")) ).unwrap() else { panic!("wrong command") }; assert_eq!(properties, vec![]); - assert_eq!(name.0, "myfilesystem"); + assert_eq!(name.as_str(), "myfilesystem"); // Create a volume let Command::CreateVolume { properties, sparse, blocksize, size, name } = Command::try_from( Input::shell(format!("{ZFS} create -s -V 1024 -b 512 -o foo=bar myvolume")) ).unwrap() else { panic!("wrong command") }; assert_eq!(properties, vec![("foo".to_string(), "bar".to_string())]); - assert_eq!(name.0, "myvolume"); + assert_eq!(name.as_str(), "myvolume"); assert!(sparse); assert_eq!(size, 1024); assert_eq!(blocksize, Some(512)); @@ -382,7 +382,7 @@ mod test { Input::shell(format!("{ZFS} create -s -V 2G -b 512 -o foo=bar myvolume")) ).unwrap() else { panic!("wrong command") }; assert_eq!(properties, vec![("foo".to_string(), "bar".to_string())]); - assert_eq!(name.0, "myvolume"); + assert_eq!(name.as_str(), "myvolume"); assert!(sparse); assert_eq!(size, 2 << 30); assert_eq!(blocksize, Some(512)); @@ -472,7 +472,7 @@ mod test { ).unwrap() else { panic!("wrong command") }; assert!(load_keys); - assert_eq!(filesystem.0, "foobar"); + assert_eq!(filesystem.as_str(), "foobar"); } #[test] diff --git a/helios/tokamak/src/cli/zpool.rs b/helios/tokamak/src/cli/zpool.rs index 274dc496640..41c491ebb33 100644 --- a/helios/tokamak/src/cli/zpool.rs +++ b/helios/tokamak/src/cli/zpool.rs @@ -4,17 +4,18 @@ use crate::cli::parse::InputParser; +use camino::Utf8PathBuf; +use helios_fusion::zpool::ZpoolName; use helios_fusion::Input; use helios_fusion::ZPOOL; - -// TODO: Consider using helios_fusion::zpool::ZpoolName here? +use std::str::FromStr; pub(crate) enum Command { - Create { pool: String, vdev: String }, - Export { pool: String }, - Import { force: bool, pool: String }, - List { properties: Vec, pools: Option> }, - Set { property: String, value: String, pool: String }, + Create { pool: ZpoolName, vdev: Utf8PathBuf }, + Export { pool: ZpoolName }, + Import { force: bool, pool: ZpoolName }, + List { properties: Vec, pools: Option> }, + Set { property: String, value: String, pool: ZpoolName }, } impl TryFrom for Command { @@ -29,19 +30,19 @@ impl TryFrom for Command { match input.shift_arg()?.as_str() { "create" => { - let pool = input.shift_arg()?; - let vdev = input.shift_arg()?; + let pool = ZpoolName::from_str(&input.shift_arg()?)?; + let vdev = Utf8PathBuf::from(input.shift_arg()?); input.no_args_remaining()?; Ok(Command::Create { pool, vdev }) } "export" => { - let pool = input.shift_arg()?; + let pool = ZpoolName::from_str(&input.shift_arg()?)?; input.no_args_remaining()?; Ok(Command::Export { pool }) } "import" => { let force = input.shift_arg_if("-f")?; - let pool = input.shift_arg()?; + let pool = ZpoolName::from_str(&input.shift_arg()?)?; Ok(Command::Import { force, pool }) } "list" => { @@ -78,16 +79,19 @@ impl TryFrom for Command { } } } else { - pools = Some(vec![arg]); + pools = Some(vec![ZpoolName::from_str(&arg)?]); break; } } let remaining_pools = input.args(); if !remaining_pools.is_empty() { - pools - .get_or_insert(vec![]) - .extend(remaining_pools.into_iter().cloned()); + pools.get_or_insert(vec![]).extend( + remaining_pools + .into_iter() + .map(|s| ZpoolName::from_str(s)) + .collect::, String>>()?, + ) }; if !scripting || !parsable { return Err("You should run 'zpool list' commands with the '-Hp' flags enabled".to_string()); @@ -102,7 +106,7 @@ impl TryFrom for Command { let property = k.to_string(); let value = v.to_string(); - let pool = input.shift_arg()?; + let pool = ZpoolName::from_str(&input.shift_arg()?)?; input.no_args_remaining()?; Ok(Command::Set { property, value, pool }) } diff --git a/helios/tokamak/src/executor.rs b/helios/tokamak/src/executor.rs index 888d900a220..043bfdb5003 100644 --- a/helios/tokamak/src/executor.rs +++ b/helios/tokamak/src/executor.rs @@ -145,8 +145,9 @@ impl Executor for FakeExecutor { ) -> Result { let id = self.inner.counter.fetch_add(1, Ordering::SeqCst); log_input(&self.inner.log, id, command); - - Ok(FakeChild::new(id, command, self.inner.clone())) + let mut child = FakeChild::new(id, command, self.inner.clone()); + self.inner.spawn_handler.lock().unwrap()(&mut child); + Ok(child) } } @@ -194,6 +195,16 @@ impl FakeChild { pub fn command(&self) -> &Command { &self.command } + + pub fn stdin(&self) -> &SharedByteQueue { + &self.stdin + } + pub fn stdout(&self) -> &SharedByteQueue { + &self.stdout + } + pub fn stderr(&self) -> &SharedByteQueue { + &self.stderr + } } impl Child for FakeChild { diff --git a/helios/tokamak/src/host.rs b/helios/tokamak/src/host.rs index 6e1331dfc49..87434cf5bf4 100644 --- a/helios/tokamak/src/host.rs +++ b/helios/tokamak/src/host.rs @@ -7,15 +7,19 @@ // TODO: REMOVE #![allow(dead_code)] -use crate::{FakeExecutor, FakeExecutorBuilder}; +use crate::{FakeChild, FakeExecutor, FakeExecutorBuilder}; use camino::Utf8PathBuf; use helios_fusion::interfaces::libc; use helios_fusion::interfaces::swapctl; use helios_fusion::zpool::ZpoolName; +use helios_fusion::{Child, Input, Output, OutputExt}; use ipnetwork::IpNetwork; +use petgraph::stable_graph::StableGraph; use slog::Logger; -use std::collections::{HashMap, HashSet}; +use std::collections::{HashMap, HashSet, VecDeque}; +use std::io::Read; +use std::str::FromStr; use std::sync::{Arc, Mutex}; pub enum LinkType { @@ -72,7 +76,7 @@ impl ZoneEnvironment { } } -#[derive(Debug)] +#[derive(Clone, Debug)] pub struct ZoneName(pub String); pub struct ZoneConfig { @@ -93,29 +97,547 @@ struct Zone { environment: ZoneEnvironment, } -struct FakeHost { - executor: Arc, +// A "process", which is either currently executing or completed. +// +// It's up to the caller to check-in on an "executing" process +// by calling "wait" on it. +enum ProcessState { + Executing(std::thread::JoinHandle), + Completed(Output), +} + +impl ProcessState { + fn wait(self) -> Output { + match self { + ProcessState::Executing(handle) => { + handle.join().expect("Failed to wait for spawned process") + } + ProcessState::Completed(output) => output, + } + } +} + +fn to_stderr>(s: S) -> Output { + Output::failure().set_stderr(s) +} + +struct FakeZpool { + zix: ZnodeIdx, + name: ZpoolName, + vdev: Utf8PathBuf, +} + +impl FakeZpool { + fn new(zix: ZnodeIdx, name: ZpoolName, vdev: Utf8PathBuf) -> Self { + Self { zix, name, vdev } + } +} + +enum DatasetFlavor { + Filesystem, + Volume { + sparse: bool, + // Defaults to 8KiB + blocksize: u64, + size: u64, + }, +} + +struct FakeDataset { + zix: ZnodeIdx, + properties: Vec<(String, String)>, + flavor: DatasetFlavor, +} + +impl FakeDataset { + fn new( + zix: ZnodeIdx, + properties: Vec<(String, String)>, + flavor: DatasetFlavor, + ) -> Self { + Self { zix, properties, flavor } + } +} + +enum Znode { + Root, + Zpool(ZpoolName), + Dataset(FilesystemName), +} + +impl Znode { + fn to_string(&self) -> String { + match self { + Znode::Root => "/".to_string(), + Znode::Zpool(name) => name.to_string(), + Znode::Dataset(name) => name.as_str().to_string(), + } + } +} + +type ZnodeIdx = petgraph::graph::NodeIndex; + +struct Znodes { + // Describes the connectivity between nodes + all_znodes: StableGraph, + zix_root: ZnodeIdx, + + // Individual nodes themselves + zpools: HashMap, + datasets: HashMap, +} + +impl Znodes { + fn new() -> Self { + let mut all_znodes = StableGraph::new(); + let zix_root = all_znodes.add_node(Znode::Root); + Self { + all_znodes, + zix_root, + zpools: HashMap::new(), + datasets: HashMap::new(), + } + } + + fn name_to_zix(&self, name: &DatasetName) -> Result { + if !name.0.contains('/') { + if let Some(pool) = self.zpools.get(&ZpoolName::from_str(&name.0)?) + { + return Ok(pool.zix); + } + } + if let Some(dataset) = self.datasets.get(&FilesystemName::new(&name.0)?) + { + return Ok(dataset.zix); + } + Err(format!("{} not found", name.0)) + } + + fn type_str(&self, zix: ZnodeIdx) -> Result<&'static str, String> { + match self + .all_znodes + .node_weight(zix) + .ok_or_else(|| "Unknown node".to_string())? + { + Znode::Root => { + Err("Invalid (root) node for type string".to_string()) + } + Znode::Zpool(_) => Ok("fileystem"), + Znode::Dataset(name) => { + let dataset = self + .datasets + .get(&name) + .ok_or_else(|| "Missing dataset".to_string())?; + match dataset.flavor { + DatasetFlavor::Filesystem => Ok("filesystem"), + DatasetFlavor::Volume { .. } => Ok("volume"), + } + } + } + } + + fn children(&self, zix: ZnodeIdx) -> impl Iterator + '_ { + self.all_znodes.neighbors_directed(zix, petgraph::Direction::Outgoing) + } + + fn add_zpool( + &mut self, + name: ZpoolName, + vdev: Utf8PathBuf, + ) -> Result<(), String> { + if self.zpools.contains_key(&name) { + return Err(format!( + "Cannot create pool name '{name}': already exists" + )); + } + + let zix = self.all_znodes.add_node(Znode::Zpool(name.clone())); + self.all_znodes.add_edge(self.zix_root, zix, ()); + self.zpools.insert(name.clone(), FakeZpool { zix, name, vdev }); + + Ok(()) + } + + fn add_dataset( + &mut self, + name: FilesystemName, + properties: Vec<(String, String)>, + flavor: DatasetFlavor, + ) -> Result<(), String> { + if self.datasets.contains_key(&name) { + return Err(format!("Cannot create '{}': already exists", name.0)); + } + + let parent = if let Some((parent, _)) = name.0.rsplit_once('/') { + parent + } else { + return Err(format!("Cannot create '{}': No parent dataset. Try creating one under an existing filesystem or zpool?", name.as_str())); + }; + + let parent_zix = self.name_to_zix(&DatasetName(parent.to_string()))?; + if !self.all_znodes.contains_node(parent_zix) { + return Err(format!( + "Cannot create fs '{}': Missing parent node: {}", + name.as_str(), + parent + )); + } + + let zix = self.all_znodes.add_node(Znode::Dataset(name.clone())); + self.all_znodes.add_edge(parent_zix, zix, ()); + self.datasets.insert(name, FakeDataset::new(zix, properties, flavor)); + + Ok(()) + } + + fn destroy_dataset(&mut self, name: &DatasetName) -> Result<(), String> { + let name = FilesystemName::new(&name.0)?; + let dataset = self.datasets.get(&name).ok_or_else(|| { + format!( + "Cannot destroy datasets '{}': Does not exist", + name.as_str() + ) + })?; + + let zix = dataset.zix; + + // TODO: Add additional validation that this dataset is removable. + // + // - TODO: Confirm, no children? (see: recursive flag) + // - TODO: Not being used by any zones right now? + // - TODO: Not mounted? + + self.all_znodes.remove_node(zix); + self.datasets.remove(&name); + + Ok(()) + } +} + +struct FakeHostInner { + log: Logger, global: ZoneEnvironment, zones: HashMap, - // TODO: Is this the right abstraction layer? - // How do you want to represent zpools & filesystems? + vdevs: HashSet, + znodes: Znodes, + swap_devices: Vec, + + processes: HashMap, +} + +impl FakeHostInner { + fn new(log: Logger) -> Self { + Self { + log, + global: ZoneEnvironment::new(0), + zones: HashMap::new(), + vdevs: HashSet::new(), + znodes: Znodes::new(), + swap_devices: vec![], + processes: HashMap::new(), + } + } + + fn run( + &mut self, + inner: &Arc>, + child: &mut FakeChild, + ) -> Result { + let input = Input::from(child.command()); + + let cmd = crate::cli::Command::try_from(input).map_err(to_stderr)?; + // TODO: Pick the right zone, act on it. + // + // TODO: If we can, complete immediately. + // Otherwise, spawn a ProcessState::Executing thread, and grab + // whatever stuff we need from the FakeChild. + + let _with_pfexec = cmd.with_pfexec(); + let zone = (*cmd.in_zone()).clone(); + + use crate::cli::KnownCommand::*; + match cmd.as_cmd() { + Zfs(zfs_cmd) => { + use crate::cli::zfs::Command::*; + if zone.is_some() { + return Err(to_stderr( + "Not Supported: 'zfs' commands within zone", + )); + } + match zfs_cmd { + CreateFilesystem { properties, name } => { + let flavor = DatasetFlavor::Filesystem; + self.znodes + .add_dataset(name.clone(), properties, flavor) + .map_err(to_stderr)?; + + Ok(ProcessState::Completed( + Output::success().set_stdout(format!( + "Created {} successfully", + name.as_str() + )), + )) + } + CreateVolume { + properties, + sparse, + blocksize, + size, + name, + } => { + let flavor = DatasetFlavor::Volume { + sparse, + blocksize: blocksize.unwrap_or(8192), + size, + }; + + let mut keylocation = None; + let mut keysize = 0; + + for (k, v) in &properties { + match k.as_str() { + "keylocation" => keylocation = Some(v.as_str()), + "encryption" => match v.as_str() { + "aes-256-gcm" => keysize = 32, + _ => { + return Err(Output::failure() + .set_stderr( + "Unsupported encryption", + )) + } + }, + _ => (), + } + } + + if keylocation == Some("file:///dev/stdin") + && keysize > 0 + { + let inner = inner.clone(); + let mut stdin = child.stdin().clone(); + return Ok(ProcessState::Executing( + std::thread::spawn(move || { + let mut secret = + Vec::with_capacity(keysize); + if let Err(err) = + stdin.read_exact(&mut secret) + { + return Output::failure().set_stderr( + format!( + "Cannot read from stdin: {err}" + ), + ); + } + + let mut inner = inner.lock().unwrap(); + match inner + .znodes + .add_dataset(name, properties, flavor) + { + Ok(()) => Output::success(), + Err(err) => { + Output::failure().set_stderr(err) + } + } + }), + )); + } + + self.znodes + .add_dataset(name.clone(), properties, flavor) + .map_err(to_stderr)?; + + Ok(ProcessState::Completed(Output::success())) + } + Destroy { + recursive_dependents, + recursive_children, + force_unmount, + name, + } => { + self.znodes + .destroy_dataset(&name) + .map_err(to_stderr)?; + + Ok(ProcessState::Completed( + Output::success() + .set_stdout(format!("{} destroyed", name.0)), + )) + } + List { recursive, depth, properties, datasets } => { + let mut targets = if let Some(datasets) = datasets { + let mut targets = VecDeque::new(); + + // If we explicitly request datasets, only return + // information for the exact matches, unless a + // recursive walk was requested. + let depth = if recursive { depth } else { Some(0) }; + + for dataset in datasets { + let zix = self + .znodes + .name_to_zix(&dataset) + .map_err(to_stderr)?; + targets.push_back((zix, depth)); + } + + targets + } else { + // Bump whatever the depth was up by one, since we + // don't display anything for the root node. + VecDeque::from([( + self.znodes.zix_root, + depth.map(|d| d + 1), + )]) + }; + + let mut output = String::new(); + + while let Some((target, depth)) = targets.pop_front() { + let node = self.znodes.all_znodes.node_weight(target) + .expect("We should have looked up the znode earlier..."); + + let (add_children, child_depth) = + if let Some(depth) = depth { + if depth > 0 { + (true, Some(depth - 1)) + } else { + (false, None) + } + } else { + (true, None) + }; + + if add_children { + for child in self.znodes.children(target) { + targets.push_front((child, child_depth)); + } + } + + if target == self.znodes.zix_root { + // Skip the root node, as there is nothing to + // display for it. + continue; + } + + for property in &properties { + match property.as_str() { + "name" => { + output.push_str(&node.to_string()) + } + "type" => output.push_str( + &self + .znodes + .type_str(target) + .map_err(to_stderr)?, + ), + _ => { + return Err(to_stderr(format!( + "Unknown property: {property}" + ))) + } + } + output.push_str("\t"); + } + output.push_str("\n"); + } + + Ok(ProcessState::Completed( + Output::success().set_stdout(output), + )) + } + // TODO: Finish these + _ => return Err(to_stderr("Not implemented (yet")), + } + } + Zpool(zpool_cmd) => { + use crate::cli::zpool::Command::*; + if zone.is_some() { + return Err(to_stderr( + "Not Supported: 'zpool' commands within zone", + )); + } + match zpool_cmd { + Create { pool, vdev } => { + if !self.vdevs.contains(&vdev) { + return Err(to_stderr(format!( + "Cannot create zpool: {vdev} does not exist" + ))); + } + + self.znodes + .add_zpool(pool.clone(), vdev.clone()) + .map_err(to_stderr)?; + } + // TODO: Finish these + _ => return Err(to_stderr("Not implemented (yet")), + } + todo!(); + } + _ => todo!(), + } + } + + // Handle requests from an executor to spawn a new child. // - // TODO: Should filesystems be part of the "ZoneEnvironment" abstraction? - zpools: HashSet, + // We aren't acting on "self" here to allow a background thread to clone + // access to ourselves. + fn handle_spawn(inner: &Arc>, child: &mut FakeChild) { + let mut me = inner.lock().unwrap(); + + assert!( + me.processes.get(&child.id()).is_none(), + "Process is already spawned: {}", + Input::from(child.command()), + ); + + let process = match me.run(inner, child) { + Ok(process) => process, + Err(err) => ProcessState::Completed(err), + }; + me.processes.insert(child.id(), process); + } + + // Handle requests from an executor to wait for a child to complete. + // + // NOTE: This function panics if the child was not previously spawned. + fn handle_wait(&mut self, child: &mut FakeChild) -> Output { + self.processes + .remove(&child.id()) + .unwrap_or_else(|| { + panic!( + "Waiting for a child that has not been spawned: {}", + Input::from(child.command()) + ); + }) + .wait() + } +} - swap_devices: Mutex>, +struct FakeHost { + executor: Arc, + inner: Arc>, } impl FakeHost { pub fn new(log: Logger) -> Arc { - Arc::new(Self { - executor: FakeExecutorBuilder::new(log).build(), - global: ZoneEnvironment::new(0), - zones: HashMap::new(), - zpools: HashSet::new(), - swap_devices: Mutex::new(vec![]), - }) + let inner = Arc::new(Mutex::new(FakeHostInner::new(log.clone()))); + + // Plumbing to ensure that commands through the executor act on + // "FakeHostInner", by going to an appropriate callback method. + let inner_for_spawn = inner.clone(); + let inner_for_wait = inner.clone(); + let builder = FakeExecutorBuilder::new(log) + .spawn_handler(Box::new(move |child| { + FakeHostInner::handle_spawn(&inner_for_spawn, child); + })) + .wait_handler(Box::new(move |child| { + let mut inner = inner_for_wait.lock().unwrap(); + inner.handle_wait(child) + })); + + Arc::new(Self { executor: builder.build(), inner }) } fn page_size(&self) -> i64 { @@ -141,7 +663,7 @@ impl swapctl::Swapctl for FakeHost { fn list_swap_devices( &self, ) -> Result, swapctl::Error> { - Ok(self.swap_devices.lock().unwrap().clone()) + Ok(self.inner.lock().unwrap().swap_devices.clone()) } fn add_swap_device( @@ -150,9 +672,54 @@ impl swapctl::Swapctl for FakeHost { start: u64, length: u64, ) -> Result<(), swapctl::Error> { - // TODO: Parse path, ensure the zvol exists? + let inner = &mut self.inner.lock().unwrap(); + + const PATH_PREFIX: &str = "/dev/zvol/dsk/"; + let volume = if let Some(volume) = path.strip_prefix(PATH_PREFIX) { + match FilesystemName::new(volume.to_string()) { + Ok(name) => name, + Err(err) => { + let msg = err.to_string(); + return Err(swapctl::Error::AddDevice { + msg, + path, + start, + length, + }); + } + } + } else { + let msg = format!("path does not start with: {PATH_PREFIX}"); + return Err(swapctl::Error::AddDevice { msg, path, start, length }); + }; + + if let Some(dataset) = inner.znodes.datasets.get(&volume) { + match dataset.flavor { + DatasetFlavor::Volume { .. } => (), + _ => { + let msg = format!( + "Dataset '{}' exists, but is not a volume", + volume.0 + ); + return Err(swapctl::Error::AddDevice { + msg, + path, + start, + length, + }); + } + } + } else { + let msg = format!("Volume '{}' does not exist", volume.0); + return Err(swapctl::Error::AddDevice { msg, path, start, length }); + } - let mut swap_devices = self.swap_devices.lock().unwrap(); + if start != 0 || length != 0 { + let msg = "Try setting start = 0 and length = 0".to_string(); + return Err(swapctl::Error::AddDevice { msg, path, start, length }); + }; + + let swap_devices = &mut inner.swap_devices; for device in &*swap_devices { if device.path == path { let msg = "device already used for swap".to_string(); @@ -201,21 +768,34 @@ pub enum AddrType { } /// The name of a ZFS filesystem, volume, or snapshot -#[derive(Debug, Eq, PartialEq)] +#[derive(Debug, Clone, Eq, PartialEq, Hash)] pub struct DatasetName(pub String); -#[derive(Debug, Eq, PartialEq)] -pub struct FilesystemName(pub String); -impl From for DatasetName { - fn from(name: FilesystemName) -> Self { - Self(name.0) +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct FilesystemName(String); + +impl FilesystemName { + pub fn new>(s: S) -> Result { + let s: String = s.into(); + if s.is_empty() { + return Err("Invalid name: Empty string".to_string()); + } + if s.ends_with('/') { + return Err(format!("Invalid name {s}: trailing slash in name")); + } + + Ok(Self(s)) + } + + pub fn as_str(&self) -> &str { + &self.0 } } -#[derive(Debug, Eq, PartialEq)] -pub struct VolumeName(pub String); -impl From for DatasetName { - fn from(name: VolumeName) -> Self { +impl From for DatasetName { + fn from(name: FilesystemName) -> Self { Self(name.0) } } + +pub type VolumeName = FilesystemName; From 4238a700eadee0b3d61014f34ad079c9a3dda3c3 Mon Sep 17 00:00:00 2001 From: Sean Klein Date: Fri, 25 Aug 2023 15:19:08 -0700 Subject: [PATCH 11/18] Add tokomaksh --- Cargo.lock | 54 +++++++ Cargo.toml | 1 + helios/fusion/src/lib.rs | 21 +++ helios/fusion/src/zpool.rs | 15 ++ helios/tokamak/Cargo.toml | 4 + helios/tokamak/src/bin/tokomaksh.rs | 111 ++++++++++++++ helios/tokamak/src/cli/mod.rs | 9 +- helios/tokamak/src/cli/parse.rs | 5 +- helios/tokamak/src/cli/zfs.rs | 34 +++-- helios/tokamak/src/cli/zpool.rs | 4 + helios/tokamak/src/host.rs | 223 ++++++++++++++++++++++++++-- helios/tokamak/src/lib.rs | 1 + 12 files changed, 452 insertions(+), 30 deletions(-) create mode 100644 helios/tokamak/src/bin/tokomaksh.rs diff --git a/Cargo.lock b/Cargo.lock index 9d0f65d7e36..545f07fd6e0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1111,6 +1111,17 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" +[[package]] +name = "clipboard-win" +version = "4.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7191c27c2357d9b7ef96baac1773290d4ca63b24205b82a3fd8a0637afcf0362" +dependencies = [ + "error-code", + "str-buf", + "winapi", +] + [[package]] name = "codespan-reporting" version = "0.11.1" @@ -2432,6 +2443,16 @@ dependencies = [ "libc", ] +[[package]] +name = "error-code" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64f18991e7bf11e7ffee451b5318b5c1a73c52d0d0ada6e5a3017c8c1ced6a21" +dependencies = [ + "libc", + "str-buf", +] + [[package]] name = "expectorate" version = "1.0.7" @@ -3120,6 +3141,7 @@ dependencies = [ "async-trait", "camino", "cfg-if 1.0.0", + "clap 4.3.21", "futures", "helios-fusion", "ipnetwork", @@ -3128,10 +3150,13 @@ dependencies = [ "omicron-common 0.1.0", "omicron-test-utils", "petgraph", + "rustyline", "schemars", "serde", "shlex", "slog", + "slog-async", + "slog-term", "smf", "thiserror", "tokio", @@ -7300,6 +7325,29 @@ dependencies = [ "wait-timeout", ] +[[package]] +name = "rustyline" +version = "12.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "994eca4bca05c87e86e15d90fc7a91d1be64b4482b38cb2d27474568fe7c9db9" +dependencies = [ + "bitflags 2.3.1", + "cfg-if 1.0.0", + "clipboard-win", + "fd-lock", + "home", + "libc", + "log", + "memchr", + "nix 0.26.2 (registry+https://github.com/rust-lang/crates.io-index)", + "radix_trie", + "scopeguard", + "unicode-segmentation", + "unicode-width", + "utf8parse", + "winapi", +] + [[package]] name = "ryu" version = "1.0.13" @@ -8215,6 +8263,12 @@ dependencies = [ "uuid", ] +[[package]] +name = "str-buf" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e08d8363704e6c71fc928674353e6b7c23dcea9d82d7012c8faf2a3a025f8d0" + [[package]] name = "string_cache" version = "0.8.7" diff --git a/Cargo.toml b/Cargo.toml index 9a7eb8d0928..f4959edfa8a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -296,6 +296,7 @@ ring = "0.16" rpassword = "7.2.0" rustfmt-wrapper = "0.2" rustls = "0.21.6" +rustyline = "12.0.0" samael = { git = "https://github.com/njaremko/samael", features = ["xmlsec"], branch = "master" } schemars = "0.8.12" secrecy = "0.8.0" diff --git a/helios/fusion/src/lib.rs b/helios/fusion/src/lib.rs index a6349e9f43d..9ce7b13c27a 100644 --- a/helios/fusion/src/lib.rs +++ b/helios/fusion/src/lib.rs @@ -34,3 +34,24 @@ pub const ZLOGIN: &str = "/usr/sbin/zlogin"; pub const ZONEADM: &str = "/usr/sbin/zoneadm"; pub const ZONECFG: &str = "/usr/sbin/zonecfg"; pub const ZPOOL: &str = "/usr/sbin/zpool"; + +pub fn which_binary(short: &str) -> &str { + match short { + "coreadm" => COREADM, + "dladm" => DLADM, + "dumpadm" => DUMPADM, + "fstyp" => FSTYP, + "ipadm" => IPADM, + "pfexec" => PFEXEC, + "route" => ROUTE, + "savecore" => SAVECORE, + "svcadm" => SVCADM, + "svccfg" => SVCCFG, + "zfs" => ZFS, + "zlogin" => ZLOGIN, + "zoneadm" => ZONEADM, + "zonecfg" => ZONECFG, + "zpool" => ZPOOL, + short => short, + } +} diff --git a/helios/fusion/src/zpool.rs b/helios/fusion/src/zpool.rs index 32d7f4ce9a1..6faac51b1eb 100644 --- a/helios/fusion/src/zpool.rs +++ b/helios/fusion/src/zpool.rs @@ -52,6 +52,21 @@ impl FromStr for ZpoolHealth { } } +impl ToString for ZpoolHealth { + fn to_string(&self) -> String { + use ZpoolHealth::*; + match self { + Online => "ONLINE", + Degraded => "DEGRADED", + Faulted => "FAULTED", + Offline => "OFFLINE", + Removed => "REMOVED", + Unavailable => "UNAVAIL", + } + .to_string() + } +} + /// Describes a Zpool. #[derive(Clone, Debug)] pub struct ZpoolInfo { diff --git a/helios/tokamak/Cargo.toml b/helios/tokamak/Cargo.toml index 16025d0cee7..ef8e0ba940b 100644 --- a/helios/tokamak/Cargo.toml +++ b/helios/tokamak/Cargo.toml @@ -9,6 +9,7 @@ license = "MPL-2.0" anyhow.workspace = true async-trait.workspace = true camino.workspace = true +clap.workspace = true cfg-if.workspace = true futures.workspace = true helios-fusion.workspace = true @@ -17,10 +18,13 @@ ipnetwork.workspace = true libc.workspace = true omicron-common.workspace = true petgraph.workspace = true +rustyline.workspace = true schemars.workspace = true serde.workspace = true shlex.workspace = true slog.workspace = true +slog-async.workspace = true +slog-term.workspace = true smf.workspace = true thiserror.workspace = true tokio.workspace = true diff --git a/helios/tokamak/src/bin/tokomaksh.rs b/helios/tokamak/src/bin/tokomaksh.rs new file mode 100644 index 00000000000..aa7a0bd022c --- /dev/null +++ b/helios/tokamak/src/bin/tokomaksh.rs @@ -0,0 +1,111 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// 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/. + +//! A shell-based interface to tokamak + +use anyhow::anyhow; +use camino::Utf8PathBuf; +use clap::{Parser, ValueEnum}; +use helios_fusion::Host; +use slog::Drain; +use slog::Level; +use slog::LevelFilter; +use slog::Logger; +use slog_term::FullFormat; +use slog_term::TermDecorator; + +#[derive(Clone, Debug, ValueEnum)] +#[clap(rename_all = "kebab_case")] +enum MachineMode { + /// The machine exists with no hardware + Empty, + /// The machine is pre-populated with some disk devices + Disks, +} + +fn parse_log_level(s: &str) -> anyhow::Result { + s.parse().map_err(|_| anyhow!("Invalid log level")) +} + +#[derive(Debug, Parser)] +struct Args { + /// Describes how to pre-populate the fake machine + #[clap(long = "machine-mode", default_value = "empty")] + machine_mode: MachineMode, + + /// The log level for the command. + #[arg(long, value_parser = parse_log_level, default_value_t = Level::Warning)] + log_level: Level, +} + +#[tokio::main] +async fn main() -> Result<(), anyhow::Error> { + let args = Args::parse(); + + let decorator = TermDecorator::new().build(); + let drain = FullFormat::new(decorator).build().fuse(); + let drain = slog_async::Async::new(drain).build().fuse(); + let drain = LevelFilter::new(drain, args.log_level).fuse(); + let log = Logger::root(drain, slog::o!("unit" => "zone-bundle")); + + let config = rustyline::Config::builder().auto_add_history(true).build(); + let mut rl = rustyline::Editor::<(), _>::with_history( + config, + rustyline::history::MemHistory::new(), + )?; + + let host = helios_tokamak::FakeHost::new(log); + + match args.machine_mode { + MachineMode::Disks => { + let vdevs = vec![ + Utf8PathBuf::from("/unreal/block/a"), + Utf8PathBuf::from("/unreal/block/b"), + Utf8PathBuf::from("/unreal/block/c"), + ]; + + for vdev in &vdevs { + println!("Adding virtual device: {vdev}"); + } + + host.add_devices(&vdevs); + } + MachineMode::Empty => (), + } + + const DEFAULT: &str = "🍩 "; + const OK: &str = "✅ "; + const ERR: &str = "❌ "; + let mut prompt = DEFAULT; + + while let Ok(line) = rl.readline(prompt) { + let Some(args) = shlex::split(&line) else { + eprintln!("Couldn't parse that, try again "); + continue; + }; + if args.is_empty() { + prompt = DEFAULT; + continue; + } + let program = helios_fusion::which_binary(&args[0]); + let mut cmd = tokio::process::Command::new(program); + cmd.args(&args[1..]); + match host.executor().execute_async(&mut cmd).await { + Ok(output) => { + print!("{}", String::from_utf8_lossy(&output.stdout)); + prompt = OK; + } + Err(err) => { + match err { + helios_fusion::ExecutionError::CommandFailure(info) => { + eprintln!("{}", info.stderr); + } + _ => eprintln!("{}", err), + } + prompt = ERR; + } + } + } + Ok(()) +} diff --git a/helios/tokamak/src/cli/mod.rs b/helios/tokamak/src/cli/mod.rs index 0cef9b4539d..f75c144bda4 100644 --- a/helios/tokamak/src/cli/mod.rs +++ b/helios/tokamak/src/cli/mod.rs @@ -81,8 +81,13 @@ impl TryFrom for Command { input.shift_program()?; } if input.program == ZLOGIN { - input.shift_program()?; - in_zone = Some(ZoneName(input.shift_program()?)); + input + .shift_program() + .map_err(|_| "Missing zone name".to_string())?; + in_zone = + Some(ZoneName(input.shift_program().map_err(|_| { + "Missing command to run in zone".to_string() + })?)); } use KnownCommand::*; diff --git a/helios/tokamak/src/cli/parse.rs b/helios/tokamak/src/cli/parse.rs index 582890e1271..b688d5e9101 100644 --- a/helios/tokamak/src/cli/parse.rs +++ b/helios/tokamak/src/cli/parse.rs @@ -14,9 +14,12 @@ pub(crate) trait InputExt { impl InputExt for Input { fn shift_program(&mut self) -> Result { if self.args.is_empty() { - return Err(format!("Failed to parse {self}")); + return Err(format!( + "Failed to parse {self}, expected more arguments" + )); } let new = self.args.remove(0); + let new = helios_fusion::which_binary(&new).to_string(); let old = std::mem::replace(&mut self.program, new); Ok(old) } diff --git a/helios/tokamak/src/cli/zfs.rs b/helios/tokamak/src/cli/zfs.rs index e5f79861fc1..c698a9df19a 100644 --- a/helios/tokamak/src/cli/zfs.rs +++ b/helios/tokamak/src/cli/zfs.rs @@ -7,14 +7,15 @@ use crate::host::{DatasetName, FilesystemName, VolumeName}; use helios_fusion::Input; use helios_fusion::ZFS; +use std::collections::HashMap; pub(crate) enum Command { CreateFilesystem { - properties: Vec<(String, String)>, + properties: HashMap, name: FilesystemName, }, CreateVolume { - properties: Vec<(String, String)>, + properties: HashMap, sparse: bool, blocksize: Option, size: u64, @@ -45,7 +46,7 @@ pub(crate) enum Command { filesystem: FilesystemName, }, Set { - properties: Vec<(String, String)>, + properties: HashMap, name: DatasetName, }, } @@ -64,7 +65,7 @@ impl TryFrom for Command { let mut size = None; let mut blocksize = None; let mut sparse = None; - let mut properties = vec![]; + let mut properties = HashMap::new(); while input.args().len() > 1 { // Volume Size (volumes only, required) @@ -110,7 +111,10 @@ impl TryFrom for Command { let (k, v) = prop .split_once('=') .ok_or_else(|| format!("Bad property: {prop}"))?; - properties.push((k.to_string(), v.to_string())); + properties.insert(k.to_string(), v.to_string()); + } else { + let arg = input.shift_arg()?; + return Err(format!("Unexpected argument: {arg}")); } } let name = input.shift_arg()?; @@ -335,14 +339,14 @@ impl TryFrom for Command { Ok(Command::Mount { load_keys, filesystem }) } "set" => { - let mut properties = vec![]; + let mut properties = HashMap::new(); while input.args().len() > 1 { let prop = input.shift_arg()?; let (k, v) = prop .split_once('=') .ok_or_else(|| format!("Bad property: {prop}"))?; - properties.push((k.to_string(), v.to_string())); + properties.insert(k.to_string(), v.to_string()); } let name = DatasetName(input.shift_arg()?); input.no_args_remaining()?; @@ -364,14 +368,17 @@ mod test { let Command::CreateFilesystem { properties, name } = Command::try_from( Input::shell(format!("{ZFS} create myfilesystem")) ).unwrap() else { panic!("wrong command") }; - assert_eq!(properties, vec![]); + assert_eq!(properties, HashMap::new()); assert_eq!(name.as_str(), "myfilesystem"); // Create a volume let Command::CreateVolume { properties, sparse, blocksize, size, name } = Command::try_from( Input::shell(format!("{ZFS} create -s -V 1024 -b 512 -o foo=bar myvolume")) ).unwrap() else { panic!("wrong command") }; - assert_eq!(properties, vec![("foo".to_string(), "bar".to_string())]); + assert_eq!( + properties, + HashMap::from([("foo".to_string(), "bar".to_string())]) + ); assert_eq!(name.as_str(), "myvolume"); assert!(sparse); assert_eq!(size, 1024); @@ -381,7 +388,10 @@ mod test { let Command::CreateVolume { properties, sparse, blocksize, size, name } = Command::try_from( Input::shell(format!("{ZFS} create -s -V 2G -b 512 -o foo=bar myvolume")) ).unwrap() else { panic!("wrong command") }; - assert_eq!(properties, vec![("foo".to_string(), "bar".to_string())]); + assert_eq!( + properties, + HashMap::from([("foo".to_string(), "bar".to_string())]) + ); assert_eq!(name.as_str(), "myvolume"); assert!(sparse); assert_eq!(size, 2 << 30); @@ -483,10 +493,10 @@ mod test { assert_eq!( properties, - vec![ + HashMap::from([ ("foo".to_string(), "bar".to_string()), ("baz".to_string(), "blat".to_string()) - ] + ]) ); assert_eq!(name.0, "myfs"); } diff --git a/helios/tokamak/src/cli/zpool.rs b/helios/tokamak/src/cli/zpool.rs index 41c491ebb33..4b3f2fee0a7 100644 --- a/helios/tokamak/src/cli/zpool.rs +++ b/helios/tokamak/src/cli/zpool.rs @@ -96,6 +96,10 @@ impl TryFrom for Command { if !scripting || !parsable { return Err("You should run 'zpool list' commands with the '-Hp' flags enabled".to_string()); } + + if properties.is_empty() { + properties = vec!["name".to_string(), "health".to_string()]; + } Ok(Command::List { properties, pools }) } "set" => { diff --git a/helios/tokamak/src/host.rs b/helios/tokamak/src/host.rs index 87434cf5bf4..9a3a2d56e40 100644 --- a/helios/tokamak/src/host.rs +++ b/helios/tokamak/src/host.rs @@ -12,7 +12,7 @@ use crate::{FakeChild, FakeExecutor, FakeExecutorBuilder}; use camino::Utf8PathBuf; use helios_fusion::interfaces::libc; use helios_fusion::interfaces::swapctl; -use helios_fusion::zpool::ZpoolName; +use helios_fusion::zpool::{ZpoolHealth, ZpoolName}; use helios_fusion::{Child, Input, Output, OutputExt}; use ipnetwork::IpNetwork; use petgraph::stable_graph::StableGraph; @@ -125,11 +125,22 @@ struct FakeZpool { zix: ZnodeIdx, name: ZpoolName, vdev: Utf8PathBuf, + + imported: bool, + health: ZpoolHealth, + properties: HashMap, } impl FakeZpool { fn new(zix: ZnodeIdx, name: ZpoolName, vdev: Utf8PathBuf) -> Self { - Self { zix, name, vdev } + Self { + zix, + name, + vdev, + imported: false, + health: ZpoolHealth::Online, + properties: HashMap::new(), + } } } @@ -145,14 +156,14 @@ enum DatasetFlavor { struct FakeDataset { zix: ZnodeIdx, - properties: Vec<(String, String)>, + properties: HashMap, flavor: DatasetFlavor, } impl FakeDataset { fn new( zix: ZnodeIdx, - properties: Vec<(String, String)>, + properties: HashMap, flavor: DatasetFlavor, ) -> Self { Self { zix, properties, flavor } @@ -244,6 +255,7 @@ impl Znodes { &mut self, name: ZpoolName, vdev: Utf8PathBuf, + import: bool, ) -> Result<(), String> { if self.zpools.contains_key(&name) { return Err(format!( @@ -253,7 +265,10 @@ impl Znodes { let zix = self.all_znodes.add_node(Znode::Zpool(name.clone())); self.all_znodes.add_edge(self.zix_root, zix, ()); - self.zpools.insert(name.clone(), FakeZpool { zix, name, vdev }); + + let mut pool = FakeZpool::new(zix, name.clone(), vdev); + pool.imported = import; + self.zpools.insert(name, pool); Ok(()) } @@ -261,7 +276,7 @@ impl Znodes { fn add_dataset( &mut self, name: FilesystemName, - properties: Vec<(String, String)>, + properties: HashMap, flavor: DatasetFlavor, ) -> Result<(), String> { if self.datasets.contains_key(&name) { @@ -374,7 +389,7 @@ impl FakeHostInner { Ok(ProcessState::Completed( Output::success().set_stdout(format!( - "Created {} successfully", + "Created {} successfully\n", name.as_str() )), )) @@ -465,6 +480,81 @@ impl FakeHostInner { .set_stdout(format!("{} destroyed", name.0)), )) } + Get { recursive, depth, fields, properties, datasets } => { + let mut targets = if let Some(datasets) = datasets { + let mut targets = VecDeque::new(); + + let depths = + if recursive { depth } else { Some(0) }; + for dataset in datasets { + let zix = self + .znodes + .name_to_zix(&dataset) + .map_err(to_stderr)?; + targets.push_back((zix, depth)); + } + targets + } else { + VecDeque::from([( + self.znodes.zix_root, + depth.map(|d| d + 1), + )]) + }; + + let mut output = String::new(); + + while let Some((target, depth)) = targets.pop_front() { + let node = self.znodes.all_znodes.node_weight(target) + .expect("We should have looked up the znode earlier..."); + + let (add_children, child_depth) = + if let Some(depth) = depth { + if depth > 0 { + (true, Some(depth - 1)) + } else { + (false, None) + } + } else { + (true, None) + }; + + if add_children { + for child in self.znodes.children(target) { + targets.push_front((child, child_depth)); + } + } + + if target == self.znodes.zix_root { + // Skip the root node, as there is nothing to + // display for it. + continue; + } + + for property in &properties { + for field in &fields { + match field.as_str() { + "name" => { + output.push_str(&node.to_string()) + } + "property" => { + output.push_str(&property) + } + "value" => { + // TODO: Look up, across whatever + // the node type is. + todo!(); + } + f => { + return Err(to_stderr(format!( + "Unknown field: {f}" + ))) + } + } + } + } + } + todo!(); + } List { recursive, depth, properties, datasets } => { let mut targets = if let Some(datasets) = datasets { let mut targets = VecDeque::new(); @@ -547,8 +637,12 @@ impl FakeHostInner { Output::success().set_stdout(output), )) } - // TODO: Finish these - _ => return Err(to_stderr("Not implemented (yet")), + Mount { load_keys, filesystem } => { + todo!(); + } + Set { properties, name } => { + todo!(); + } } } Zpool(zpool_cmd) => { @@ -562,18 +656,109 @@ impl FakeHostInner { Create { pool, vdev } => { if !self.vdevs.contains(&vdev) { return Err(to_stderr(format!( - "Cannot create zpool: {vdev} does not exist" + "Cannot create zpool: device '{vdev}' does not exist" ))); } + let import = true; self.znodes - .add_zpool(pool.clone(), vdev.clone()) + .add_zpool(pool.clone(), vdev.clone(), import) .map_err(to_stderr)?; + + Ok(ProcessState::Completed(Output::success())) + } + Export { pool: name } => { + let Some(mut pool) = self.znodes.zpools.get_mut(&name) else { + return Err(to_stderr(format!("pool does not exist"))); + }; + + if !pool.imported { + return Err(to_stderr(format!( + "cannot export pool which is already exported" + ))); + } + pool.imported = false; + Ok(ProcessState::Completed(Output::success())) + } + Import { force: _, pool: name } => { + let Some(mut pool) = self.znodes.zpools.get_mut(&name) else { + return Err(to_stderr(format!("pool does not exist"))); + }; + + if pool.imported { + return Err(to_stderr(format!( + "a pool with that name is already created" + ))); + } + pool.imported = true; + Ok(ProcessState::Completed(Output::success())) + } + List { properties, pools } => { + let mut output = String::new(); + let mut display = + |name: &ZpoolName, + pool: &FakeZpool, + properties: &Vec| + -> Result<(), _> { + for property in properties { + match property.as_str() { + "name" => output + .push_str(&format!("{}", name)), + "health" => output + .push_str(&pool.health.to_string()), + _ => { + return Err(to_stderr(format!( + "Unknown property: {property}" + ))) + } + } + output.push_str("\t"); + } + output.push_str("\n"); + Ok(()) + }; + + if let Some(pools) = pools { + for name in &pools { + let pool = + self.znodes.zpools.get(name).ok_or_else( + || { + to_stderr(format!( + "{} does not exist", + name + )) + }, + )?; + + if !pool.imported { + return Err(to_stderr(format!( + "{} not imported", + name + ))); + } + + display(&name, &pool, &properties)?; + } + } else { + for (name, pool) in &self.znodes.zpools { + if pool.imported { + display(&name, &pool, &properties)?; + } + } + } + + Ok(ProcessState::Completed( + Output::success().set_stdout(output), + )) + } + Set { property, value, pool: name } => { + let Some(pool) = self.znodes.zpools.get_mut(&name) else { + return Err(to_stderr(format!("{} does not exist", name))); + }; + pool.properties.insert(property, value); + Ok(ProcessState::Completed(Output::success())) } - // TODO: Finish these - _ => return Err(to_stderr("Not implemented (yet")), } - todo!(); } _ => todo!(), } @@ -615,7 +800,7 @@ impl FakeHostInner { } } -struct FakeHost { +pub struct FakeHost { executor: Arc, inner: Arc>, } @@ -643,6 +828,14 @@ impl FakeHost { fn page_size(&self) -> i64 { 4096 } + + pub fn add_devices(&self, vdevs: &Vec) { + let mut inner = self.inner.lock().unwrap(); + + for vdev in vdevs { + inner.vdevs.insert(vdev.clone()); + } + } } impl libc::Libc for FakeHost { diff --git a/helios/tokamak/src/lib.rs b/helios/tokamak/src/lib.rs index 3b90c7fb1d9..b11a05a766c 100644 --- a/helios/tokamak/src/lib.rs +++ b/helios/tokamak/src/lib.rs @@ -8,3 +8,4 @@ mod host; mod shared_byte_queue; pub use executor::*; +pub use host::FakeHost; From 3f4b5fdd8f24a93c71e04615549bdc980e677692 Mon Sep 17 00:00:00 2001 From: Sean Klein Date: Mon, 28 Aug 2023 23:44:09 -0700 Subject: [PATCH 12/18] Strongly-typed properties --- Cargo.lock | 47 +- Cargo.toml | 3 +- helios/tokamak/Cargo.toml | 3 + helios/tokamak/src/cli/zfs.rs | 96 +++-- helios/tokamak/src/{host.rs => host/mod.rs} | 454 +++++++------------- helios/tokamak/src/host/znode.rs | 438 +++++++++++++++++++ helios/tokamak/src/lib.rs | 1 + helios/tokamak/src/types.rs | 174 ++++++++ 8 files changed, 871 insertions(+), 345 deletions(-) rename helios/tokamak/src/{host.rs => host/mod.rs} (70%) create mode 100644 helios/tokamak/src/host/znode.rs create mode 100644 helios/tokamak/src/types.rs diff --git a/Cargo.lock b/Cargo.lock index 545f07fd6e0..9042f09ff3c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3139,6 +3139,7 @@ version = "0.1.0" dependencies = [ "anyhow", "async-trait", + "bitflags 1.3.2", "camino", "cfg-if 1.0.0", "clap 4.3.21", @@ -3149,6 +3150,7 @@ dependencies = [ "libc", "omicron-common 0.1.0", "omicron-test-utils", + "once_cell", "petgraph", "rustyline", "schemars", @@ -3158,6 +3160,7 @@ dependencies = [ "slog-async", "slog-term", "smf", + "strum 0.25.0", "thiserror", "tokio", "uuid", @@ -6015,6 +6018,17 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "phf" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" +dependencies = [ + "phf_macros", + "phf_shared 0.10.0", + "proc-macro-hack", +] + [[package]] name = "phf" version = "0.11.1" @@ -6024,6 +6038,30 @@ dependencies = [ "phf_shared 0.11.1", ] +[[package]] +name = "phf_generator" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" +dependencies = [ + "phf_shared 0.10.0", + "rand 0.8.5", +] + +[[package]] +name = "phf_macros" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58fdf3184dd560f160dd73922bea2d5cd6e8f064bf4b13110abd81b03697b4e0" +dependencies = [ + "phf_generator", + "phf_shared 0.10.0", + "proc-macro-hack", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "phf_shared" version = "0.10.0" @@ -6346,6 +6384,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "proc-macro-hack" +version = "0.5.20+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" + [[package]] name = "proc-macro2" version = "1.0.66" @@ -8376,6 +8420,7 @@ version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" dependencies = [ + "phf 0.10.1", "strum_macros 0.25.2", ] @@ -8822,7 +8867,7 @@ dependencies = [ "log", "parking_lot 0.12.1", "percent-encoding", - "phf", + "phf 0.11.1", "pin-project-lite", "postgres-protocol", "postgres-types", diff --git a/Cargo.toml b/Cargo.toml index f4959edfa8a..9a46059d3ed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -144,6 +144,7 @@ base64 = "0.21.2" bb8 = "0.8.1" bcs = "0.1.5" bincode = "1.3.3" +bitflags = "1.3.2" bootstore = { path = "bootstore" } bootstrap-agent-client = { path = "bootstrap-agent-client" } buf-list = { version = "1.0.3", features = ["tokio1"] } @@ -334,7 +335,7 @@ static_assertions = "1.1.0" # harder than expected to make breaking changes (even if you specify a specific # SHA). Cut a new Steno release instead. See omicron#2117. steno = "0.4.0" -strum = { version = "0.25", features = [ "derive" ] } +strum = { version = "0.25", features = [ "derive", "phf" ] } subprocess = "0.2.9" libsw = { version = "3.3.0", features = ["tokio"] } syn = { version = "2.0" } diff --git a/helios/tokamak/Cargo.toml b/helios/tokamak/Cargo.toml index ef8e0ba940b..1e8db3d4fa7 100644 --- a/helios/tokamak/Cargo.toml +++ b/helios/tokamak/Cargo.toml @@ -8,6 +8,7 @@ license = "MPL-2.0" [dependencies] anyhow.workspace = true async-trait.workspace = true +bitflags.workspace = true camino.workspace = true clap.workspace = true cfg-if.workspace = true @@ -17,6 +18,7 @@ itertools.workspace = true ipnetwork.workspace = true libc.workspace = true omicron-common.workspace = true +once_cell.workspace = true petgraph.workspace = true rustyline.workspace = true schemars.workspace = true @@ -26,6 +28,7 @@ slog.workspace = true slog-async.workspace = true slog-term.workspace = true smf.workspace = true +strum.workspace = true thiserror.workspace = true tokio.workspace = true uuid.workspace = true diff --git a/helios/tokamak/src/cli/zfs.rs b/helios/tokamak/src/cli/zfs.rs index c698a9df19a..a2d56cc4ca5 100644 --- a/helios/tokamak/src/cli/zfs.rs +++ b/helios/tokamak/src/cli/zfs.rs @@ -3,7 +3,7 @@ // file, You can obtain one at https://mozilla.org/MPL/2.0/. use crate::cli::parse::InputParser; -use crate::host::{DatasetName, FilesystemName, VolumeName}; +use crate::types::{DatasetName, DatasetProperty}; use helios_fusion::Input; use helios_fusion::ZFS; @@ -11,15 +11,15 @@ use std::collections::HashMap; pub(crate) enum Command { CreateFilesystem { - properties: HashMap, - name: FilesystemName, + properties: HashMap, + name: DatasetName, }, CreateVolume { - properties: HashMap, + properties: HashMap, sparse: bool, blocksize: Option, size: u64, - name: VolumeName, + name: DatasetName, }, Destroy { recursive_dependents: bool, @@ -32,21 +32,21 @@ pub(crate) enum Command { depth: Option, // name, property, value, source fields: Vec, - properties: Vec, + properties: Vec, datasets: Option>, }, List { recursive: bool, depth: Option, - properties: Vec, + properties: Vec, datasets: Option>, }, Mount { load_keys: bool, - filesystem: FilesystemName, + filesystem: DatasetName, }, Set { - properties: HashMap, + properties: HashMap, name: DatasetName, }, } @@ -111,7 +111,10 @@ impl TryFrom for Command { let (k, v) = prop .split_once('=') .ok_or_else(|| format!("Bad property: {prop}"))?; - properties.insert(k.to_string(), v.to_string()); + let prop = DatasetProperty::try_from(k) + .map_err(|e| format!("Unknown property: {e}"))?; + + properties.insert(prop, v.to_string()); } else { let arg = input.shift_arg()?; return Err(format!("Unexpected argument: {arg}")); @@ -123,7 +126,7 @@ impl TryFrom for Command { if let Some(size) = size { // Volume let sparse = sparse.unwrap_or(false); - let name = FilesystemName::new(name)?; + let name = DatasetName::new(name)?; Ok(Command::CreateVolume { properties, sparse, @@ -136,7 +139,7 @@ impl TryFrom for Command { if sparse.is_some() || blocksize.is_some() { return Err("Using volume arguments, but forgot to specify '-V size'?".to_string()); } - let name = FilesystemName::new(name)?; + let name = DatasetName::new(name)?; Ok(Command::CreateFilesystem { properties, name }) } } @@ -163,7 +166,7 @@ impl TryFrom for Command { } } } else { - name = Some(DatasetName(arg)); + name = Some(DatasetName::new(arg)?); input.no_args_remaining()?; } } @@ -231,8 +234,15 @@ impl TryFrom for Command { } } } else { - properties = - arg.split(',').map(|s| s.to_string()).collect(); + properties = arg + .split(',') + .map(|s| { + DatasetProperty::try_from(s).map_err(|err| { + format!("unknown property: {err}") + }) + }) + .collect::, String>>( + )?; break; } } @@ -241,8 +251,8 @@ impl TryFrom for Command { input .args() .into_iter() - .map(|s| DatasetName(s.to_string())) - .collect::>(), + .map(|s| DatasetName::new(s.to_string())) + .collect::, _>>()?, ); if !scripting || !parsable { return Err("You should run 'zfs get' commands with the '-Hp' flags enabled".to_string()); @@ -299,8 +309,11 @@ impl TryFrom for Command { properties = input .shift_arg()? .split(',') - .map(|s| s.to_string()) - .collect(); + .map(|s| { + DatasetProperty::try_from(s) + .map_err(|err| format!("unknown property: {err}")) + }) + .collect::, String>>()?; } c => { return Err(format!( @@ -312,7 +325,8 @@ impl TryFrom for Command { } else { // As soon as non-flag arguments are passed, the rest of // the arguments are treated as datasets. - datasets = Some(vec![DatasetName(arg.to_string())]); + datasets = + Some(vec![DatasetName::new(arg.to_string())?]); break; } } @@ -322,7 +336,8 @@ impl TryFrom for Command { datasets.get_or_insert(vec![]).extend( remaining_datasets .into_iter() - .map(|d| DatasetName(d.to_string())), + .map(|d| DatasetName::new(d.to_string())) + .collect::, _>>()?, ); }; @@ -334,7 +349,7 @@ impl TryFrom for Command { } "mount" => { let load_keys = input.shift_arg_if("-l")?; - let filesystem = FilesystemName::new(input.shift_arg()?)?; + let filesystem = DatasetName::new(input.shift_arg()?)?; input.no_args_remaining()?; Ok(Command::Mount { load_keys, filesystem }) } @@ -346,9 +361,11 @@ impl TryFrom for Command { let (k, v) = prop .split_once('=') .ok_or_else(|| format!("Bad property: {prop}"))?; - properties.insert(k.to_string(), v.to_string()); + let prop = DatasetProperty::try_from(k) + .map_err(|e| format!("Unknown property: {e}"))?; + properties.insert(prop, v.to_string()); } - let name = DatasetName(input.shift_arg()?); + let name = DatasetName::new(input.shift_arg()?)?; input.no_args_remaining()?; Ok(Command::Set { properties, name }) @@ -373,11 +390,11 @@ mod test { // Create a volume let Command::CreateVolume { properties, sparse, blocksize, size, name } = Command::try_from( - Input::shell(format!("{ZFS} create -s -V 1024 -b 512 -o foo=bar myvolume")) + Input::shell(format!("{ZFS} create -s -V 1024 -b 512 -o logbias=bar myvolume")) ).unwrap() else { panic!("wrong command") }; assert_eq!( properties, - HashMap::from([("foo".to_string(), "bar".to_string())]) + HashMap::from([(DatasetProperty::Logbias, "bar".to_string())]) ); assert_eq!(name.as_str(), "myvolume"); assert!(sparse); @@ -386,11 +403,11 @@ mod test { // Create a volume (using letter suffix) let Command::CreateVolume { properties, sparse, blocksize, size, name } = Command::try_from( - Input::shell(format!("{ZFS} create -s -V 2G -b 512 -o foo=bar myvolume")) + Input::shell(format!("{ZFS} create -s -V 2G -b 512 -o logbias=bar myvolume")) ).unwrap() else { panic!("wrong command") }; assert_eq!( properties, - HashMap::from([("foo".to_string(), "bar".to_string())]) + HashMap::from([(DatasetProperty::Logbias, "bar".to_string())]) ); assert_eq!(name.as_str(), "myvolume"); assert!(sparse); @@ -399,7 +416,7 @@ mod test { // Create volume (invalid) assert!(Command::try_from(Input::shell(format!( - "{ZFS} create -s -b 512 -o foo=bar myvolume" + "{ZFS} create -s -b 512 -o logbias=bar myvolume" ))) .err() .unwrap() @@ -416,7 +433,7 @@ mod test { assert!(!recursive_dependents); assert!(recursive_children); assert!(force_unmount); - assert_eq!(name.0, "foobar"); + assert_eq!(name.as_str(), "foobar"); assert!(Command::try_from(Input::shell(format!( "{ZFS} destroy -x doit" @@ -435,10 +452,13 @@ mod test { assert!(recursive); assert_eq!(depth, Some(10)); assert_eq!(fields, vec!["name", "value"]); - assert_eq!(properties, vec!["mounted", "available"]); + assert_eq!( + properties, + vec![DatasetProperty::Mounted, DatasetProperty::Available] + ); assert_eq!( datasets.unwrap(), - vec![DatasetName("myvolume".to_string())] + vec![DatasetName::new("myvolume".to_string()).unwrap()] ); assert!(Command::try_from(Input::shell(format!( @@ -459,10 +479,10 @@ mod test { assert!(recursive); assert_eq!(depth.unwrap(), 1); - assert_eq!(properties, vec!["name"]); + assert_eq!(properties, vec![DatasetProperty::Name]); assert_eq!( datasets.unwrap(), - vec![DatasetName("myfilesystem".to_string())] + vec![DatasetName::new("myfilesystem".to_string()).unwrap()] ); assert!(Command::try_from(Input::shell(format!( @@ -488,16 +508,16 @@ mod test { #[test] fn set() { let Command::Set { properties, name } = Command::try_from( - Input::shell(format!("{ZFS} set foo=bar baz=blat myfs")) + Input::shell(format!("{ZFS} set mountpoint=bar logbias=blat myfs")) ).unwrap() else { panic!("wrong command") }; assert_eq!( properties, HashMap::from([ - ("foo".to_string(), "bar".to_string()), - ("baz".to_string(), "blat".to_string()) + (DatasetProperty::Mountpoint, "bar".to_string()), + (DatasetProperty::Logbias, "blat".to_string()) ]) ); - assert_eq!(name.0, "myfs"); + assert_eq!(name.as_str(), "myfs"); } } diff --git a/helios/tokamak/src/host.rs b/helios/tokamak/src/host/mod.rs similarity index 70% rename from helios/tokamak/src/host.rs rename to helios/tokamak/src/host/mod.rs index 9a3a2d56e40..bdcf4b05d56 100644 --- a/helios/tokamak/src/host.rs +++ b/helios/tokamak/src/host/mod.rs @@ -7,21 +7,25 @@ // TODO: REMOVE #![allow(dead_code)] +use crate::host::znode::{FakeZpool, Znodes}; +use crate::types::{ + DatasetName, DatasetProperty, DatasetPropertyAccess, DatasetType, +}; use crate::{FakeChild, FakeExecutor, FakeExecutorBuilder}; use camino::Utf8PathBuf; use helios_fusion::interfaces::libc; use helios_fusion::interfaces::swapctl; -use helios_fusion::zpool::{ZpoolHealth, ZpoolName}; +use helios_fusion::zpool::ZpoolName; use helios_fusion::{Child, Input, Output, OutputExt}; use ipnetwork::IpNetwork; -use petgraph::stable_graph::StableGraph; use slog::Logger; use std::collections::{HashMap, HashSet, VecDeque}; use std::io::Read; -use std::str::FromStr; use std::sync::{Arc, Mutex}; +mod znode; + pub enum LinkType { Etherstub, Vnic, @@ -121,214 +125,6 @@ fn to_stderr>(s: S) -> Output { Output::failure().set_stderr(s) } -struct FakeZpool { - zix: ZnodeIdx, - name: ZpoolName, - vdev: Utf8PathBuf, - - imported: bool, - health: ZpoolHealth, - properties: HashMap, -} - -impl FakeZpool { - fn new(zix: ZnodeIdx, name: ZpoolName, vdev: Utf8PathBuf) -> Self { - Self { - zix, - name, - vdev, - imported: false, - health: ZpoolHealth::Online, - properties: HashMap::new(), - } - } -} - -enum DatasetFlavor { - Filesystem, - Volume { - sparse: bool, - // Defaults to 8KiB - blocksize: u64, - size: u64, - }, -} - -struct FakeDataset { - zix: ZnodeIdx, - properties: HashMap, - flavor: DatasetFlavor, -} - -impl FakeDataset { - fn new( - zix: ZnodeIdx, - properties: HashMap, - flavor: DatasetFlavor, - ) -> Self { - Self { zix, properties, flavor } - } -} - -enum Znode { - Root, - Zpool(ZpoolName), - Dataset(FilesystemName), -} - -impl Znode { - fn to_string(&self) -> String { - match self { - Znode::Root => "/".to_string(), - Znode::Zpool(name) => name.to_string(), - Znode::Dataset(name) => name.as_str().to_string(), - } - } -} - -type ZnodeIdx = petgraph::graph::NodeIndex; - -struct Znodes { - // Describes the connectivity between nodes - all_znodes: StableGraph, - zix_root: ZnodeIdx, - - // Individual nodes themselves - zpools: HashMap, - datasets: HashMap, -} - -impl Znodes { - fn new() -> Self { - let mut all_znodes = StableGraph::new(); - let zix_root = all_znodes.add_node(Znode::Root); - Self { - all_znodes, - zix_root, - zpools: HashMap::new(), - datasets: HashMap::new(), - } - } - - fn name_to_zix(&self, name: &DatasetName) -> Result { - if !name.0.contains('/') { - if let Some(pool) = self.zpools.get(&ZpoolName::from_str(&name.0)?) - { - return Ok(pool.zix); - } - } - if let Some(dataset) = self.datasets.get(&FilesystemName::new(&name.0)?) - { - return Ok(dataset.zix); - } - Err(format!("{} not found", name.0)) - } - - fn type_str(&self, zix: ZnodeIdx) -> Result<&'static str, String> { - match self - .all_znodes - .node_weight(zix) - .ok_or_else(|| "Unknown node".to_string())? - { - Znode::Root => { - Err("Invalid (root) node for type string".to_string()) - } - Znode::Zpool(_) => Ok("fileystem"), - Znode::Dataset(name) => { - let dataset = self - .datasets - .get(&name) - .ok_or_else(|| "Missing dataset".to_string())?; - match dataset.flavor { - DatasetFlavor::Filesystem => Ok("filesystem"), - DatasetFlavor::Volume { .. } => Ok("volume"), - } - } - } - } - - fn children(&self, zix: ZnodeIdx) -> impl Iterator + '_ { - self.all_znodes.neighbors_directed(zix, petgraph::Direction::Outgoing) - } - - fn add_zpool( - &mut self, - name: ZpoolName, - vdev: Utf8PathBuf, - import: bool, - ) -> Result<(), String> { - if self.zpools.contains_key(&name) { - return Err(format!( - "Cannot create pool name '{name}': already exists" - )); - } - - let zix = self.all_znodes.add_node(Znode::Zpool(name.clone())); - self.all_znodes.add_edge(self.zix_root, zix, ()); - - let mut pool = FakeZpool::new(zix, name.clone(), vdev); - pool.imported = import; - self.zpools.insert(name, pool); - - Ok(()) - } - - fn add_dataset( - &mut self, - name: FilesystemName, - properties: HashMap, - flavor: DatasetFlavor, - ) -> Result<(), String> { - if self.datasets.contains_key(&name) { - return Err(format!("Cannot create '{}': already exists", name.0)); - } - - let parent = if let Some((parent, _)) = name.0.rsplit_once('/') { - parent - } else { - return Err(format!("Cannot create '{}': No parent dataset. Try creating one under an existing filesystem or zpool?", name.as_str())); - }; - - let parent_zix = self.name_to_zix(&DatasetName(parent.to_string()))?; - if !self.all_znodes.contains_node(parent_zix) { - return Err(format!( - "Cannot create fs '{}': Missing parent node: {}", - name.as_str(), - parent - )); - } - - let zix = self.all_znodes.add_node(Znode::Dataset(name.clone())); - self.all_znodes.add_edge(parent_zix, zix, ()); - self.datasets.insert(name, FakeDataset::new(zix, properties, flavor)); - - Ok(()) - } - - fn destroy_dataset(&mut self, name: &DatasetName) -> Result<(), String> { - let name = FilesystemName::new(&name.0)?; - let dataset = self.datasets.get(&name).ok_or_else(|| { - format!( - "Cannot destroy datasets '{}': Does not exist", - name.as_str() - ) - })?; - - let zix = dataset.zix; - - // TODO: Add additional validation that this dataset is removable. - // - // - TODO: Confirm, no children? (see: recursive flag) - // - TODO: Not being used by any zones right now? - // - TODO: Not mounted? - - self.all_znodes.remove_node(zix); - self.datasets.remove(&name); - - Ok(()) - } -} - struct FakeHostInner { log: Logger, global: ZoneEnvironment, @@ -382,9 +178,22 @@ impl FakeHostInner { } match zfs_cmd { CreateFilesystem { properties, name } => { - let flavor = DatasetFlavor::Filesystem; + for property in properties.keys() { + if property.access() + == DatasetPropertyAccess::ReadOnly + { + return Err(to_stderr( + "Not supported: {property} is a read-only property", + )); + } + } + self.znodes - .add_dataset(name.clone(), properties, flavor) + .add_dataset( + name.clone(), + properties, + DatasetType::Filesystem, + ) .map_err(to_stderr)?; Ok(ProcessState::Completed( @@ -395,25 +204,61 @@ impl FakeHostInner { )) } CreateVolume { - properties, + mut properties, sparse, blocksize, size, name, } => { - let flavor = DatasetFlavor::Volume { - sparse, - blocksize: blocksize.unwrap_or(8192), - size, - }; + for property in properties.keys() { + if property.access() + == DatasetPropertyAccess::ReadOnly + { + return Err(to_stderr( + "Not supported: {property} is a read-only property", + )); + } + } + + let blocksize = blocksize.unwrap_or(8192); + if sparse { + properties.insert( + DatasetProperty::Reservation, + "0".to_string(), + ); + } else { + // NOTE: This isn't how much metadata is used, but it's + // a number we can use that represents "this is larger than + // the usable size of the volume". + // + // See: + // + // $ zfs get -Hp used,volsize,refreservation + // + // For any non-sparse zpool. + let reserved_size = size + (8 << 20); + properties.insert( + DatasetProperty::Reservation, + reserved_size.to_string(), + ); + } + properties.insert( + DatasetProperty::Volblocksize, + blocksize.to_string(), + ); + properties + .insert(DatasetProperty::Volsize, size.to_string()); let mut keylocation = None; let mut keysize = 0; for (k, v) in &properties { - match k.as_str() { - "keylocation" => keylocation = Some(v.as_str()), - "encryption" => match v.as_str() { + match k { + DatasetProperty::Keylocation => { + keylocation = Some(v.as_str()) + } + DatasetProperty::Encryption => match v.as_str() + { "aes-256-gcm" => keysize = 32, _ => { return Err(Output::failure() @@ -446,10 +291,11 @@ impl FakeHostInner { } let mut inner = inner.lock().unwrap(); - match inner - .znodes - .add_dataset(name, properties, flavor) - { + match inner.znodes.add_dataset( + name, + properties, + DatasetType::Volume, + ) { Ok(()) => Output::success(), Err(err) => { Output::failure().set_stderr(err) @@ -460,7 +306,11 @@ impl FakeHostInner { } self.znodes - .add_dataset(name.clone(), properties, flavor) + .add_dataset( + name.clone(), + properties, + DatasetType::Volume, + ) .map_err(to_stderr)?; Ok(ProcessState::Completed(Output::success())) @@ -472,31 +322,35 @@ impl FakeHostInner { name, } => { self.znodes - .destroy_dataset(&name) + .destroy_dataset( + &name, + recursive_dependents, + recursive_children, + force_unmount, + ) .map_err(to_stderr)?; Ok(ProcessState::Completed( Output::success() - .set_stdout(format!("{} destroyed", name.0)), + .set_stdout(format!("{} destroyed", name)), )) } Get { recursive, depth, fields, properties, datasets } => { let mut targets = if let Some(datasets) = datasets { let mut targets = VecDeque::new(); - let depths = - if recursive { depth } else { Some(0) }; + let depth = if recursive { depth } else { Some(0) }; for dataset in datasets { let zix = self .znodes - .name_to_zix(&dataset) + .index_of(dataset.as_str()) .map_err(to_stderr)?; targets.push_back((zix, depth)); } targets } else { VecDeque::from([( - self.znodes.zix_root, + self.znodes.root_index(), depth.map(|d| d + 1), )]) }; @@ -504,8 +358,12 @@ impl FakeHostInner { let mut output = String::new(); while let Some((target, depth)) = targets.pop_front() { - let node = self.znodes.all_znodes.node_weight(target) - .expect("We should have looked up the znode earlier..."); + let node = self + .znodes + .lookup_by_index(target) + .expect( + "We should have looked up the znode earlier...", + ); let (add_children, child_depth) = if let Some(depth) = depth { @@ -524,7 +382,7 @@ impl FakeHostInner { } } - if target == self.znodes.zix_root { + if target == self.znodes.root_index() { // Skip the root node, as there is nothing to // display for it. continue; @@ -536,9 +394,8 @@ impl FakeHostInner { "name" => { output.push_str(&node.to_string()) } - "property" => { - output.push_str(&property) - } + "property" => output + .push_str(&property.to_string()), "value" => { // TODO: Look up, across whatever // the node type is. @@ -567,7 +424,7 @@ impl FakeHostInner { for dataset in datasets { let zix = self .znodes - .name_to_zix(&dataset) + .index_of(dataset.as_str()) .map_err(to_stderr)?; targets.push_back((zix, depth)); } @@ -577,7 +434,7 @@ impl FakeHostInner { // Bump whatever the depth was up by one, since we // don't display anything for the root node. VecDeque::from([( - self.znodes.zix_root, + self.znodes.root_index(), depth.map(|d| d + 1), )]) }; @@ -585,8 +442,12 @@ impl FakeHostInner { let mut output = String::new(); while let Some((target, depth)) = targets.pop_front() { - let node = self.znodes.all_znodes.node_weight(target) - .expect("We should have looked up the znode earlier..."); + let node = self + .znodes + .lookup_by_index(target) + .expect( + "We should have looked up the znode earlier...", + ); let (add_children, child_depth) = if let Some(depth) = depth { @@ -605,23 +466,32 @@ impl FakeHostInner { } } - if target == self.znodes.zix_root { + if target == self.znodes.root_index() { // Skip the root node, as there is nothing to // display for it. continue; } for property in &properties { - match property.as_str() { - "name" => { + match property { + DatasetProperty::Name => { output.push_str(&node.to_string()) } - "type" => output.push_str( - &self + DatasetProperty::Type => { + let node = self .znodes - .type_str(target) - .map_err(to_stderr)?, - ), + .lookup_by_index(target) + .ok_or_else(|| { + to_stderr("Node not found") + })?; + + output.push_str( + self.znodes + .type_str(node) + .map_err(to_stderr)?, + ) + } + // TODO: Fix this _ => { return Err(to_stderr(format!( "Unknown property: {property}" @@ -638,10 +508,17 @@ impl FakeHostInner { )) } Mount { load_keys, filesystem } => { - todo!(); + self.znodes + .mount(load_keys, &filesystem) + .map_err(to_stderr)?; + Ok(ProcessState::Completed( + Output::success() + .set_stdout(format!("{} mounted", filesystem)), + )) } Set { properties, name } => { - todo!(); + // TODO + todo!("Calling zfs set with properties: {properties:?} on '{name}', not implemented"); } } } @@ -668,7 +545,7 @@ impl FakeHostInner { Ok(ProcessState::Completed(Output::success())) } Export { pool: name } => { - let Some(mut pool) = self.znodes.zpools.get_mut(&name) else { + let Some(mut pool) = self.znodes.get_zpool_mut(&name) else { return Err(to_stderr(format!("pool does not exist"))); }; @@ -681,7 +558,7 @@ impl FakeHostInner { Ok(ProcessState::Completed(Output::success())) } Import { force: _, pool: name } => { - let Some(mut pool) = self.znodes.zpools.get_mut(&name) else { + let Some(mut pool) = self.znodes.get_zpool_mut(&name) else { return Err(to_stderr(format!("pool does not exist"))); }; @@ -720,15 +597,15 @@ impl FakeHostInner { if let Some(pools) = pools { for name in &pools { - let pool = - self.znodes.zpools.get(name).ok_or_else( - || { - to_stderr(format!( - "{} does not exist", - name - )) - }, - )?; + let pool = self + .znodes + .get_zpool(name) + .ok_or_else(|| { + to_stderr(format!( + "{} does not exist", + name + )) + })?; if !pool.imported { return Err(to_stderr(format!( @@ -740,7 +617,7 @@ impl FakeHostInner { display(&name, &pool, &properties)?; } } else { - for (name, pool) in &self.znodes.zpools { + for (name, pool) in self.znodes.all_zpools() { if pool.imported { display(&name, &pool, &properties)?; } @@ -752,7 +629,7 @@ impl FakeHostInner { )) } Set { property, value, pool: name } => { - let Some(pool) = self.znodes.zpools.get_mut(&name) else { + let Some(pool) = self.znodes.get_zpool_mut(&name) else { return Err(to_stderr(format!("{} does not exist", name))); }; pool.properties.insert(property, value); @@ -869,7 +746,7 @@ impl swapctl::Swapctl for FakeHost { const PATH_PREFIX: &str = "/dev/zvol/dsk/"; let volume = if let Some(volume) = path.strip_prefix(PATH_PREFIX) { - match FilesystemName::new(volume.to_string()) { + match DatasetName::new(volume.to_string()) { Ok(name) => name, Err(err) => { let msg = err.to_string(); @@ -886,13 +763,13 @@ impl swapctl::Swapctl for FakeHost { return Err(swapctl::Error::AddDevice { msg, path, start, length }); }; - if let Some(dataset) = inner.znodes.datasets.get(&volume) { - match dataset.flavor { - DatasetFlavor::Volume { .. } => (), + if let Some(dataset) = inner.znodes.get_dataset(&volume) { + match dataset.ty() { + DatasetType::Volume => (), _ => { let msg = format!( "Dataset '{}' exists, but is not a volume", - volume.0 + volume.as_str() ); return Err(swapctl::Error::AddDevice { msg, @@ -903,7 +780,7 @@ impl swapctl::Swapctl for FakeHost { } } } else { - let msg = format!("Volume '{}' does not exist", volume.0); + let msg = format!("Volume '{}' does not exist", volume.as_str()); return Err(swapctl::Error::AddDevice { msg, path, start, length }); } @@ -959,36 +836,3 @@ pub enum AddrType { Static(IpNetwork), Addrconf, } - -/// The name of a ZFS filesystem, volume, or snapshot -#[derive(Debug, Clone, Eq, PartialEq, Hash)] -pub struct DatasetName(pub String); - -#[derive(Debug, Clone, Eq, PartialEq, Hash)] -pub struct FilesystemName(String); - -impl FilesystemName { - pub fn new>(s: S) -> Result { - let s: String = s.into(); - if s.is_empty() { - return Err("Invalid name: Empty string".to_string()); - } - if s.ends_with('/') { - return Err(format!("Invalid name {s}: trailing slash in name")); - } - - Ok(Self(s)) - } - - pub fn as_str(&self) -> &str { - &self.0 - } -} - -impl From for DatasetName { - fn from(name: FilesystemName) -> Self { - Self(name.0) - } -} - -pub type VolumeName = FilesystemName; diff --git a/helios/tokamak/src/host/znode.rs b/helios/tokamak/src/host/znode.rs new file mode 100644 index 00000000000..6ef67e277e9 --- /dev/null +++ b/helios/tokamak/src/host/znode.rs @@ -0,0 +1,438 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// 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/. + +//! Emulates zpools and datasets on an illumos system + +use crate::types::{DatasetName, DatasetProperty, DatasetType}; + +use camino::Utf8PathBuf; +use helios_fusion::zpool::{ZpoolHealth, ZpoolName}; +use petgraph::stable_graph::{StableGraph, WalkNeighbors}; +use std::collections::HashMap; +use std::str::FromStr; + +pub(crate) struct FakeZpool { + zix: NodeIndex, + name: ZpoolName, + vdev: Utf8PathBuf, + + pub imported: bool, + pub health: ZpoolHealth, + pub properties: HashMap, +} + +impl FakeZpool { + pub(crate) fn new( + zix: NodeIndex, + name: ZpoolName, + vdev: Utf8PathBuf, + ) -> Self { + Self { + zix, + name, + vdev, + imported: false, + health: ZpoolHealth::Online, + properties: HashMap::new(), + } + } +} + +struct DatasetProperties(HashMap); + +impl DatasetProperties { + fn get(&self, property: DatasetProperty) -> Result { + self.0 + .get(&property) + .map(|k| k.to_string()) + .ok_or_else(|| format!("Missing '{property}' property")) + } + + fn insert>(&mut self, k: DatasetProperty, v: V) { + self.0.insert(k, v.into()); + } +} + +pub(crate) struct FakeDataset { + zix: NodeIndex, + properties: DatasetProperties, + ty: DatasetType, +} + +impl FakeDataset { + fn new( + zix: NodeIndex, + properties: HashMap, + ty: DatasetType, + ) -> Self { + Self { zix, properties: DatasetProperties(properties), ty } + } + + pub fn ty(&self) -> DatasetType { + self.ty + } + + fn mountpoint(&self) -> Option { + self.properties + .get(DatasetProperty::Mountpoint) + .map(|s| Utf8PathBuf::from(s)) + .ok() + } + + fn mount(&mut self, new: Utf8PathBuf) -> Result<(), String> { + match self.ty { + DatasetType::Filesystem => { + if self.properties.get(DatasetProperty::Mountpoint)? != "none" { + return Err("Already mounted".to_string()); + } + } + _ => return Err("Not a filesystem".to_string()), + }; + self.properties.insert(DatasetProperty::Mountpoint, new); + self.properties.insert(DatasetProperty::Mounted, "yes"); + Ok(()) + } + + // TODO: Confirm that the filesystem isn't used by zones? + fn unmount(&mut self) -> Result<(), String> { + let mountpoint = match &mut self.ty { + DatasetType::Filesystem => { + self.properties.get(DatasetProperty::Mountpoint)? + } + _ => return Err("Not a filesystem".to_string()), + }; + if mountpoint == "none" { + return Err("Filesystem is not mounted".to_string()); + } + self.properties.insert(DatasetProperty::Mountpoint, "none"); + self.properties.insert(DatasetProperty::Mounted, "no"); + Ok(()) + } +} + +pub(crate) enum Znode { + Root, + Zpool(ZpoolName), + Dataset(DatasetName), +} + +impl Znode { + pub fn to_string(&self) -> String { + match self { + Znode::Root => "/".to_string(), + Znode::Zpool(name) => name.to_string(), + Znode::Dataset(name) => name.as_str().to_string(), + } + } +} + +/// The type of an index used to lookup nodes in the znode DAG. +pub(crate) type NodeIndex = + petgraph::graph::NodeIndex; + +/// Describes access to zpools and datasets that exist within the system. +/// +/// On Helios, datasets exist as children of zpools, within a DAG structure. +/// Understanding the relationship between these znodes (dubbed "znodes", to +/// include both zpools and datasets) is important to accurately emulate +/// many ZFS operations, such as deletion, which can conditionally succeed or +/// fail depeending on the prescence of children. +pub(crate) struct Znodes { + // Describes the connectivity between nodes + all_znodes: StableGraph, + zix_root: NodeIndex, + + // Individual nodes themselves + zpools: HashMap, + datasets: HashMap, +} + +impl Znodes { + pub(crate) fn new() -> Self { + let mut all_znodes = StableGraph::new(); + let zix_root = all_znodes.add_node(Znode::Root); + Self { + all_znodes, + zix_root, + zpools: HashMap::new(), + datasets: HashMap::new(), + } + } + + // Zpool access methods + + pub fn get_zpool(&self, name: &ZpoolName) -> Option<&FakeZpool> { + self.zpools.get(name) + } + + pub fn get_zpool_mut( + &mut self, + name: &ZpoolName, + ) -> Option<&mut FakeZpool> { + self.zpools.get_mut(name) + } + + pub fn all_zpools(&self) -> impl Iterator { + self.zpools.iter() + } + + // Dataset access methods + + pub fn get_dataset(&self, name: &DatasetName) -> Option<&FakeDataset> { + self.datasets.get(name) + } + + // Index-based access methods + + /// Returns the index of the "root" of the Znode DAG. + /// + /// This node does not actually exist, but the children of this node should + /// be zpools. + pub fn root_index(&self) -> NodeIndex { + self.zix_root + } + + /// Returns the node index of a znode + pub fn index_of(&self, name: &str) -> Result { + if !name.contains('/') { + if let Some(pool) = self.zpools.get(&ZpoolName::from_str(name)?) { + return Ok(pool.zix); + } + } + if let Some(dataset) = self.datasets.get(&DatasetName::new(name)?) { + return Ok(dataset.zix); + } + Err(format!("{} not found", name)) + } + + /// Looks up a Znode by an index + pub fn lookup_by_index(&self, index: NodeIndex) -> Option<&Znode> { + self.all_znodes.node_weight(index) + } + + /// Describe the type of a znode by string + pub fn type_str(&self, node: &Znode) -> Result<&'static str, String> { + match node { + Znode::Root => { + Err("Invalid (root) node for type string".to_string()) + } + Znode::Zpool(_) => Ok("fileystem"), + Znode::Dataset(name) => { + let dataset = self + .datasets + .get(&name) + .ok_or_else(|| "Missing dataset".to_string())?; + match dataset.ty { + DatasetType::Filesystem => Ok("filesystem"), + DatasetType::Snapshot => Ok("snapshot"), + DatasetType::Volume => Ok("volume"), + } + } + } + } + + pub fn children( + &self, + zix: NodeIndex, + ) -> impl Iterator + '_ { + self.all_znodes.neighbors_directed(zix, petgraph::Direction::Outgoing) + } + + pub fn children_mut( + &self, + zix: NodeIndex, + ) -> WalkNeighbors { + self.all_znodes + .neighbors_directed(zix, petgraph::Direction::Outgoing) + .detach() + } + + pub fn add_zpool( + &mut self, + name: ZpoolName, + vdev: Utf8PathBuf, + import: bool, + ) -> Result<(), String> { + if self.zpools.contains_key(&name) { + return Err(format!( + "Cannot create pool name '{name}': already exists" + )); + } + + let zix = self.all_znodes.add_node(Znode::Zpool(name.clone())); + self.all_znodes.add_edge(self.zix_root, zix, ()); + + let mut pool = FakeZpool::new(zix, name.clone(), vdev); + pool.imported = import; + self.zpools.insert(name, pool); + + Ok(()) + } + + pub fn add_dataset( + &mut self, + name: DatasetName, + mut properties: HashMap, + ty: DatasetType, + ) -> Result<(), String> { + for property in properties.keys() { + if !property.target().contains(ty.into()) { + return Err(format!( + "Cannot create {ty} with property {property}" + )); + } + } + + if self.datasets.contains_key(&name) { + return Err(format!( + "Cannot create '{}': already exists", + name.as_str() + )); + } + + properties.insert(DatasetProperty::Type, ty.to_string()); + properties.insert(DatasetProperty::Name, name.to_string()); + + match &ty { + DatasetType::Filesystem => { + properties.insert(DatasetProperty::Mounted, "no".to_string()); + properties + .entry(DatasetProperty::Mountpoint) + .or_insert("none".to_string()); + } + DatasetType::Volume => (), + DatasetType::Snapshot => (), + } + + let parent = if let Some((parent, _)) = name.as_str().rsplit_once('/') { + parent + } else { + return Err(format!("Cannot create '{}': No parent dataset. Try creating one under an existing filesystem or zpool?", name.as_str())); + }; + + let parent_zix = self.index_of(parent)?; + if !self.all_znodes.contains_node(parent_zix) { + return Err(format!( + "Cannot create fs '{}': Missing parent node: {}", + name.as_str(), + parent + )); + } + + let zix = self.all_znodes.add_node(Znode::Dataset(name.clone())); + self.all_znodes.add_edge(parent_zix, zix, ()); + self.datasets.insert(name, FakeDataset::new(zix, properties, ty)); + + Ok(()) + } + + pub fn mount( + &mut self, + load_keys: bool, + name: &DatasetName, + ) -> Result<(), String> { + let dataset = self + .datasets + .get_mut(&name) + .ok_or_else(|| format!("Cannot mount '{name}': Does not exist"))?; + let properties = &mut dataset.properties; + let mountpoint_property = + properties.get(DatasetProperty::Mountpoint)?; + if !mountpoint_property.starts_with('/') { + return Err(format!( + "Cannot mount '{name}' with mountpoint: {mountpoint_property}" + )); + } + + let mounted_property = properties.get(DatasetProperty::Mounted)?; + assert_eq!(mounted_property, "no"); + let encryption_property = + properties.get(DatasetProperty::Encryption)?; + let encrypted = match encryption_property.as_str() { + "off" => false, + "aes-256-gcm" => true, + _ => { + return Err(format!( + "Unsupported encryption: {encryption_property}" + )) + } + }; + + let keylocation_property = + properties.get(DatasetProperty::Keylocation)?; + if encrypted { + if !load_keys { + return Err(format!( + "Cannot mount '{name}': Use 'zfs mount -l {name}'" + )); + } + let Some(keylocation) = keylocation_property.strip_prefix("file://").map(|k| Utf8PathBuf::from(k)) else { + return Err(format!("Cannot read from key location: {keylocation_property}")); + }; + if !keylocation.exists() { + return Err(format!("Key at {keylocation} does not exist")); + } + + let keyformat = properties.get(DatasetProperty::Keyformat)?; + if keyformat != "raw" { + return Err(format!( + "Cannot mount '{name}': Unknown keyformat: {keyformat}" + )); + } + } + + // Perform modifications to "mount" filesystem + + dataset + .mount(Utf8PathBuf::from(mountpoint_property)) + .map_err(|err| format!("Cannot mount '{name}': {err}"))?; + + Ok(()) + } + + pub fn destroy_dataset( + &mut self, + name: &DatasetName, + // TODO: Not emulating this option + _recusive_dependents: bool, + recursive_children: bool, + force_unmount: bool, + ) -> Result<(), String> { + let dataset = self.datasets.get_mut(&name).ok_or_else(|| { + format!("Cannot destroy '{name}': Does not exist") + })?; + + if dataset.mountpoint().is_some() { + if !force_unmount { + return Err(format!("Cannot destroy '{name}': Mounted")); + } + dataset.unmount()?; + } + + let zix = dataset.zix; + + let mut children = self.children_mut(zix); + while let Some(child_idx) = children.next_node(&self.all_znodes) { + let child_name = self + .lookup_by_index(child_idx) + .map(|znode| znode.to_string()) + .ok_or_else(|| format!("Child node missing name"))?; + let child_name = DatasetName::new(child_name)?; + + if !recursive_children { + return Err(format!("Cannot delete dataset {name}: has children (e.g.: {child_name})")); + } + self.destroy_dataset( + &child_name, + _recusive_dependents, + recursive_children, + force_unmount, + )?; + } + self.all_znodes.remove_node(zix); + self.datasets.remove(&name); + + Ok(()) + } +} diff --git a/helios/tokamak/src/lib.rs b/helios/tokamak/src/lib.rs index b11a05a766c..94bf7bf9167 100644 --- a/helios/tokamak/src/lib.rs +++ b/helios/tokamak/src/lib.rs @@ -6,6 +6,7 @@ mod cli; mod executor; mod host; mod shared_byte_queue; +mod types; pub use executor::*; pub use host::FakeHost; diff --git a/helios/tokamak/src/types.rs b/helios/tokamak/src/types.rs new file mode 100644 index 00000000000..5d4a7642f1b --- /dev/null +++ b/helios/tokamak/src/types.rs @@ -0,0 +1,174 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// 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 which may be parsed from the CLI and used by a fake host + +use std::fmt; + +// TODO: nest under "dataset" module, eliminate prefix + +/// The name of a ZFS filesystem, volume, or snapshot +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct DatasetName(String); + +impl DatasetName { + pub fn new>(s: S) -> Result { + let s: String = s.into(); + if s.is_empty() { + return Err("Invalid name: Empty string".to_string()); + } + if s.ends_with('/') { + return Err(format!("Invalid name {s}: trailing slash in name")); + } + + Ok(Self(s)) + } + + pub fn as_str(&self) -> &str { + &self.0 + } +} + +impl fmt::Display for DatasetName { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +#[derive( + strum::Display, + strum::EnumString, + strum::IntoStaticStr, + Copy, + Clone, + Debug, + PartialEq, + Eq, + Hash, +)] +#[strum(use_phf, serialize_all = "lowercase")] +pub(crate) enum DatasetType { + Filesystem, + Snapshot, + Volume, +} + +bitflags::bitflags! { + /// The classes of datasets for which a property is valid. + pub(crate) struct DatasetPropertyTarget: u8 { + const FILESYSTEM = 0b0001; + const SNAPSHOT = 0b0010; + const VOLUME = 0b0100; + } +} + +impl From for DatasetPropertyTarget { + fn from(ty: DatasetType) -> Self { + use DatasetType::*; + match ty { + Filesystem => DatasetPropertyTarget::FILESYSTEM, + Snapshot => DatasetPropertyTarget::SNAPSHOT, + Volume => DatasetPropertyTarget::VOLUME, + } + } +} + +/// The ability of users to modify properties +#[derive(Eq, PartialEq)] +pub(crate) enum DatasetPropertyAccess { + ReadOnly, + ReadWrite, +} + +/// A property which is applicable to datasets +#[derive( + strum::Display, + strum::EnumString, + strum::IntoStaticStr, + Copy, + Clone, + Debug, + PartialEq, + Eq, + Hash, +)] +#[strum(use_phf, serialize_all = "lowercase")] +pub(crate) enum DatasetProperty { + Atime, + #[strum(serialize = "available", serialize = "avail")] + Available, + Encryption, + Logbias, + Mounted, + Mountpoint, + Name, + Keyformat, + Keylocation, + #[strum(serialize = "oxide:epoch")] + OxideEpoch, + Primarycache, + #[strum(serialize = "reservation", serialize = "refreservation")] + Reservation, + Secondarycache, + Type, + Volblocksize, + Volsize, + Zoned, +} + +impl DatasetProperty { + pub fn access(&self) -> DatasetPropertyAccess { + use DatasetProperty::*; + use DatasetPropertyAccess::*; + + match self { + Atime => ReadWrite, + Available => ReadOnly, + Encryption => ReadWrite, + Logbias => ReadWrite, + Mounted => ReadOnly, + Mountpoint => ReadWrite, + Name => ReadOnly, + Keyformat => ReadWrite, + Keylocation => ReadWrite, + OxideEpoch => ReadWrite, + Primarycache => ReadWrite, + Reservation => ReadOnly, + Secondarycache => ReadWrite, + Type => ReadOnly, + Volblocksize => ReadOnly, + Volsize => ReadOnly, + Zoned => ReadWrite, + } + } + + pub fn target(&self) -> DatasetPropertyTarget { + let fs = DatasetPropertyTarget::FILESYSTEM; + let all = DatasetPropertyTarget::all(); + let fs_and_vol = + DatasetPropertyTarget::FILESYSTEM | DatasetPropertyTarget::VOLUME; + let vol = DatasetPropertyTarget::VOLUME; + + use DatasetProperty::*; + match self { + Atime => fs, + Available => all, + Encryption => all, + Logbias => fs_and_vol, + Mounted => fs, + Mountpoint => fs, + Name => all, + Keyformat => fs_and_vol, + Keylocation => fs_and_vol, + OxideEpoch => all, + Primarycache => fs_and_vol, + Reservation => vol, + Secondarycache => fs_and_vol, + Type => all, + Volblocksize => vol, + Volsize => vol, + Zoned => all, + } + } +} From 0ce82fdaabf64aab4888faa803ade6b51a5f1b9d Mon Sep 17 00:00:00 2001 From: Sean Klein Date: Mon, 28 Aug 2023 23:50:36 -0700 Subject: [PATCH 13/18] namespaces in modules --- helios/tokamak/src/cli/zfs.rs | 72 ++++++++-------- helios/tokamak/src/host/mod.rs | 55 ++++++------ helios/tokamak/src/host/znode.rs | 85 ++++++++++--------- helios/tokamak/src/lib.rs | 2 +- .../src/{types.rs => types/dataset.rs} | 51 +++++------ helios/tokamak/src/types/mod.rs | 7 ++ 6 files changed, 138 insertions(+), 134 deletions(-) rename helios/tokamak/src/{types.rs => types/dataset.rs} (74%) create mode 100644 helios/tokamak/src/types/mod.rs diff --git a/helios/tokamak/src/cli/zfs.rs b/helios/tokamak/src/cli/zfs.rs index a2d56cc4ca5..8b27dc3430c 100644 --- a/helios/tokamak/src/cli/zfs.rs +++ b/helios/tokamak/src/cli/zfs.rs @@ -3,7 +3,7 @@ // file, You can obtain one at https://mozilla.org/MPL/2.0/. use crate::cli::parse::InputParser; -use crate::types::{DatasetName, DatasetProperty}; +use crate::types::dataset; use helios_fusion::Input; use helios_fusion::ZFS; @@ -11,43 +11,43 @@ use std::collections::HashMap; pub(crate) enum Command { CreateFilesystem { - properties: HashMap, - name: DatasetName, + properties: HashMap, + name: dataset::Name, }, CreateVolume { - properties: HashMap, + properties: HashMap, sparse: bool, blocksize: Option, size: u64, - name: DatasetName, + name: dataset::Name, }, Destroy { recursive_dependents: bool, recursive_children: bool, force_unmount: bool, - name: DatasetName, + name: dataset::Name, }, Get { recursive: bool, depth: Option, // name, property, value, source fields: Vec, - properties: Vec, - datasets: Option>, + properties: Vec, + datasets: Option>, }, List { recursive: bool, depth: Option, - properties: Vec, - datasets: Option>, + properties: Vec, + datasets: Option>, }, Mount { load_keys: bool, - filesystem: DatasetName, + filesystem: dataset::Name, }, Set { - properties: HashMap, - name: DatasetName, + properties: HashMap, + name: dataset::Name, }, } @@ -111,7 +111,7 @@ impl TryFrom for Command { let (k, v) = prop .split_once('=') .ok_or_else(|| format!("Bad property: {prop}"))?; - let prop = DatasetProperty::try_from(k) + let prop = dataset::Property::try_from(k) .map_err(|e| format!("Unknown property: {e}"))?; properties.insert(prop, v.to_string()); @@ -126,7 +126,7 @@ impl TryFrom for Command { if let Some(size) = size { // Volume let sparse = sparse.unwrap_or(false); - let name = DatasetName::new(name)?; + let name = dataset::Name::new(name)?; Ok(Command::CreateVolume { properties, sparse, @@ -139,7 +139,7 @@ impl TryFrom for Command { if sparse.is_some() || blocksize.is_some() { return Err("Using volume arguments, but forgot to specify '-V size'?".to_string()); } - let name = DatasetName::new(name)?; + let name = dataset::Name::new(name)?; Ok(Command::CreateFilesystem { properties, name }) } } @@ -166,7 +166,7 @@ impl TryFrom for Command { } } } else { - name = Some(DatasetName::new(arg)?); + name = Some(dataset::Name::new(arg)?); input.no_args_remaining()?; } } @@ -237,11 +237,11 @@ impl TryFrom for Command { properties = arg .split(',') .map(|s| { - DatasetProperty::try_from(s).map_err(|err| { + dataset::Property::try_from(s).map_err(|err| { format!("unknown property: {err}") }) }) - .collect::, String>>( + .collect::, String>>( )?; break; } @@ -251,8 +251,8 @@ impl TryFrom for Command { input .args() .into_iter() - .map(|s| DatasetName::new(s.to_string())) - .collect::, _>>()?, + .map(|s| dataset::Name::new(s.to_string())) + .collect::, _>>()?, ); if !scripting || !parsable { return Err("You should run 'zfs get' commands with the '-Hp' flags enabled".to_string()); @@ -310,10 +310,10 @@ impl TryFrom for Command { .shift_arg()? .split(',') .map(|s| { - DatasetProperty::try_from(s) + dataset::Property::try_from(s) .map_err(|err| format!("unknown property: {err}")) }) - .collect::, String>>()?; + .collect::, String>>()?; } c => { return Err(format!( @@ -326,7 +326,7 @@ impl TryFrom for Command { // As soon as non-flag arguments are passed, the rest of // the arguments are treated as datasets. datasets = - Some(vec![DatasetName::new(arg.to_string())?]); + Some(vec![dataset::Name::new(arg.to_string())?]); break; } } @@ -336,7 +336,7 @@ impl TryFrom for Command { datasets.get_or_insert(vec![]).extend( remaining_datasets .into_iter() - .map(|d| DatasetName::new(d.to_string())) + .map(|d| dataset::Name::new(d.to_string())) .collect::, _>>()?, ); }; @@ -349,7 +349,7 @@ impl TryFrom for Command { } "mount" => { let load_keys = input.shift_arg_if("-l")?; - let filesystem = DatasetName::new(input.shift_arg()?)?; + let filesystem = dataset::Name::new(input.shift_arg()?)?; input.no_args_remaining()?; Ok(Command::Mount { load_keys, filesystem }) } @@ -361,11 +361,11 @@ impl TryFrom for Command { let (k, v) = prop .split_once('=') .ok_or_else(|| format!("Bad property: {prop}"))?; - let prop = DatasetProperty::try_from(k) + let prop = dataset::Property::try_from(k) .map_err(|e| format!("Unknown property: {e}"))?; properties.insert(prop, v.to_string()); } - let name = DatasetName::new(input.shift_arg()?)?; + let name = dataset::Name::new(input.shift_arg()?)?; input.no_args_remaining()?; Ok(Command::Set { properties, name }) @@ -394,7 +394,7 @@ mod test { ).unwrap() else { panic!("wrong command") }; assert_eq!( properties, - HashMap::from([(DatasetProperty::Logbias, "bar".to_string())]) + HashMap::from([(dataset::Property::Logbias, "bar".to_string())]) ); assert_eq!(name.as_str(), "myvolume"); assert!(sparse); @@ -407,7 +407,7 @@ mod test { ).unwrap() else { panic!("wrong command") }; assert_eq!( properties, - HashMap::from([(DatasetProperty::Logbias, "bar".to_string())]) + HashMap::from([(dataset::Property::Logbias, "bar".to_string())]) ); assert_eq!(name.as_str(), "myvolume"); assert!(sparse); @@ -454,11 +454,11 @@ mod test { assert_eq!(fields, vec!["name", "value"]); assert_eq!( properties, - vec![DatasetProperty::Mounted, DatasetProperty::Available] + vec![dataset::Property::Mounted, dataset::Property::Available] ); assert_eq!( datasets.unwrap(), - vec![DatasetName::new("myvolume".to_string()).unwrap()] + vec![dataset::Name::new("myvolume".to_string()).unwrap()] ); assert!(Command::try_from(Input::shell(format!( @@ -479,10 +479,10 @@ mod test { assert!(recursive); assert_eq!(depth.unwrap(), 1); - assert_eq!(properties, vec![DatasetProperty::Name]); + assert_eq!(properties, vec![dataset::Property::Name]); assert_eq!( datasets.unwrap(), - vec![DatasetName::new("myfilesystem".to_string()).unwrap()] + vec![dataset::Name::new("myfilesystem".to_string()).unwrap()] ); assert!(Command::try_from(Input::shell(format!( @@ -514,8 +514,8 @@ mod test { assert_eq!( properties, HashMap::from([ - (DatasetProperty::Mountpoint, "bar".to_string()), - (DatasetProperty::Logbias, "blat".to_string()) + (dataset::Property::Mountpoint, "bar".to_string()), + (dataset::Property::Logbias, "blat".to_string()) ]) ); assert_eq!(name.as_str(), "myfs"); diff --git a/helios/tokamak/src/host/mod.rs b/helios/tokamak/src/host/mod.rs index bdcf4b05d56..af3cc7cb0b0 100644 --- a/helios/tokamak/src/host/mod.rs +++ b/helios/tokamak/src/host/mod.rs @@ -8,9 +8,7 @@ #![allow(dead_code)] use crate::host::znode::{FakeZpool, Znodes}; -use crate::types::{ - DatasetName, DatasetProperty, DatasetPropertyAccess, DatasetType, -}; +use crate::types::dataset; use crate::{FakeChild, FakeExecutor, FakeExecutorBuilder}; use camino::Utf8PathBuf; @@ -180,7 +178,7 @@ impl FakeHostInner { CreateFilesystem { properties, name } => { for property in properties.keys() { if property.access() - == DatasetPropertyAccess::ReadOnly + == dataset::PropertyAccess::ReadOnly { return Err(to_stderr( "Not supported: {property} is a read-only property", @@ -192,7 +190,7 @@ impl FakeHostInner { .add_dataset( name.clone(), properties, - DatasetType::Filesystem, + dataset::Type::Filesystem, ) .map_err(to_stderr)?; @@ -212,7 +210,7 @@ impl FakeHostInner { } => { for property in properties.keys() { if property.access() - == DatasetPropertyAccess::ReadOnly + == dataset::PropertyAccess::ReadOnly { return Err(to_stderr( "Not supported: {property} is a read-only property", @@ -223,7 +221,7 @@ impl FakeHostInner { let blocksize = blocksize.unwrap_or(8192); if sparse { properties.insert( - DatasetProperty::Reservation, + dataset::Property::Reservation, "0".to_string(), ); } else { @@ -238,35 +236,38 @@ impl FakeHostInner { // For any non-sparse zpool. let reserved_size = size + (8 << 20); properties.insert( - DatasetProperty::Reservation, + dataset::Property::Reservation, reserved_size.to_string(), ); } properties.insert( - DatasetProperty::Volblocksize, + dataset::Property::Volblocksize, blocksize.to_string(), ); - properties - .insert(DatasetProperty::Volsize, size.to_string()); + properties.insert( + dataset::Property::Volsize, + size.to_string(), + ); let mut keylocation = None; let mut keysize = 0; for (k, v) in &properties { match k { - DatasetProperty::Keylocation => { + dataset::Property::Keylocation => { keylocation = Some(v.as_str()) } - DatasetProperty::Encryption => match v.as_str() - { - "aes-256-gcm" => keysize = 32, - _ => { - return Err(Output::failure() - .set_stderr( - "Unsupported encryption", - )) + dataset::Property::Encryption => { + match v.as_str() { + "aes-256-gcm" => keysize = 32, + _ => { + return Err(Output::failure() + .set_stderr( + "Unsupported encryption", + )) + } } - }, + } _ => (), } } @@ -294,7 +295,7 @@ impl FakeHostInner { match inner.znodes.add_dataset( name, properties, - DatasetType::Volume, + dataset::Type::Volume, ) { Ok(()) => Output::success(), Err(err) => { @@ -309,7 +310,7 @@ impl FakeHostInner { .add_dataset( name.clone(), properties, - DatasetType::Volume, + dataset::Type::Volume, ) .map_err(to_stderr)?; @@ -474,10 +475,10 @@ impl FakeHostInner { for property in &properties { match property { - DatasetProperty::Name => { + dataset::Property::Name => { output.push_str(&node.to_string()) } - DatasetProperty::Type => { + dataset::Property::Type => { let node = self .znodes .lookup_by_index(target) @@ -746,7 +747,7 @@ impl swapctl::Swapctl for FakeHost { const PATH_PREFIX: &str = "/dev/zvol/dsk/"; let volume = if let Some(volume) = path.strip_prefix(PATH_PREFIX) { - match DatasetName::new(volume.to_string()) { + match dataset::Name::new(volume.to_string()) { Ok(name) => name, Err(err) => { let msg = err.to_string(); @@ -765,7 +766,7 @@ impl swapctl::Swapctl for FakeHost { if let Some(dataset) = inner.znodes.get_dataset(&volume) { match dataset.ty() { - DatasetType::Volume => (), + dataset::Type::Volume => (), _ => { let msg = format!( "Dataset '{}' exists, but is not a volume", diff --git a/helios/tokamak/src/host/znode.rs b/helios/tokamak/src/host/znode.rs index 6ef67e277e9..9947995da83 100644 --- a/helios/tokamak/src/host/znode.rs +++ b/helios/tokamak/src/host/znode.rs @@ -4,7 +4,7 @@ //! Emulates zpools and datasets on an illumos system -use crate::types::{DatasetName, DatasetProperty, DatasetType}; +use crate::types::dataset; use camino::Utf8PathBuf; use helios_fusion::zpool::{ZpoolHealth, ZpoolName}; @@ -39,17 +39,17 @@ impl FakeZpool { } } -struct DatasetProperties(HashMap); +struct DatasetProperties(HashMap); impl DatasetProperties { - fn get(&self, property: DatasetProperty) -> Result { + fn get(&self, property: dataset::Property) -> Result { self.0 .get(&property) .map(|k| k.to_string()) .ok_or_else(|| format!("Missing '{property}' property")) } - fn insert>(&mut self, k: DatasetProperty, v: V) { + fn insert>(&mut self, k: dataset::Property, v: V) { self.0.insert(k, v.into()); } } @@ -57,56 +57,57 @@ impl DatasetProperties { pub(crate) struct FakeDataset { zix: NodeIndex, properties: DatasetProperties, - ty: DatasetType, + ty: dataset::Type, } impl FakeDataset { fn new( zix: NodeIndex, - properties: HashMap, - ty: DatasetType, + properties: HashMap, + ty: dataset::Type, ) -> Self { Self { zix, properties: DatasetProperties(properties), ty } } - pub fn ty(&self) -> DatasetType { + pub fn ty(&self) -> dataset::Type { self.ty } fn mountpoint(&self) -> Option { self.properties - .get(DatasetProperty::Mountpoint) + .get(dataset::Property::Mountpoint) .map(|s| Utf8PathBuf::from(s)) .ok() } fn mount(&mut self, new: Utf8PathBuf) -> Result<(), String> { match self.ty { - DatasetType::Filesystem => { - if self.properties.get(DatasetProperty::Mountpoint)? != "none" { + dataset::Type::Filesystem => { + if self.properties.get(dataset::Property::Mountpoint)? != "none" + { return Err("Already mounted".to_string()); } } _ => return Err("Not a filesystem".to_string()), }; - self.properties.insert(DatasetProperty::Mountpoint, new); - self.properties.insert(DatasetProperty::Mounted, "yes"); + self.properties.insert(dataset::Property::Mountpoint, new); + self.properties.insert(dataset::Property::Mounted, "yes"); Ok(()) } // TODO: Confirm that the filesystem isn't used by zones? fn unmount(&mut self) -> Result<(), String> { let mountpoint = match &mut self.ty { - DatasetType::Filesystem => { - self.properties.get(DatasetProperty::Mountpoint)? + dataset::Type::Filesystem => { + self.properties.get(dataset::Property::Mountpoint)? } _ => return Err("Not a filesystem".to_string()), }; if mountpoint == "none" { return Err("Filesystem is not mounted".to_string()); } - self.properties.insert(DatasetProperty::Mountpoint, "none"); - self.properties.insert(DatasetProperty::Mounted, "no"); + self.properties.insert(dataset::Property::Mountpoint, "none"); + self.properties.insert(dataset::Property::Mounted, "no"); Ok(()) } } @@ -114,7 +115,7 @@ impl FakeDataset { pub(crate) enum Znode { Root, Zpool(ZpoolName), - Dataset(DatasetName), + Dataset(dataset::Name), } impl Znode { @@ -145,7 +146,7 @@ pub(crate) struct Znodes { // Individual nodes themselves zpools: HashMap, - datasets: HashMap, + datasets: HashMap, } impl Znodes { @@ -179,7 +180,7 @@ impl Znodes { // Dataset access methods - pub fn get_dataset(&self, name: &DatasetName) -> Option<&FakeDataset> { + pub fn get_dataset(&self, name: &dataset::Name) -> Option<&FakeDataset> { self.datasets.get(name) } @@ -200,7 +201,7 @@ impl Znodes { return Ok(pool.zix); } } - if let Some(dataset) = self.datasets.get(&DatasetName::new(name)?) { + if let Some(dataset) = self.datasets.get(&dataset::Name::new(name)?) { return Ok(dataset.zix); } Err(format!("{} not found", name)) @@ -224,9 +225,9 @@ impl Znodes { .get(&name) .ok_or_else(|| "Missing dataset".to_string())?; match dataset.ty { - DatasetType::Filesystem => Ok("filesystem"), - DatasetType::Snapshot => Ok("snapshot"), - DatasetType::Volume => Ok("volume"), + dataset::Type::Filesystem => Ok("filesystem"), + dataset::Type::Snapshot => Ok("snapshot"), + dataset::Type::Volume => Ok("volume"), } } } @@ -272,9 +273,9 @@ impl Znodes { pub fn add_dataset( &mut self, - name: DatasetName, - mut properties: HashMap, - ty: DatasetType, + name: dataset::Name, + mut properties: HashMap, + ty: dataset::Type, ) -> Result<(), String> { for property in properties.keys() { if !property.target().contains(ty.into()) { @@ -291,18 +292,18 @@ impl Znodes { )); } - properties.insert(DatasetProperty::Type, ty.to_string()); - properties.insert(DatasetProperty::Name, name.to_string()); + properties.insert(dataset::Property::Type, ty.to_string()); + properties.insert(dataset::Property::Name, name.to_string()); match &ty { - DatasetType::Filesystem => { - properties.insert(DatasetProperty::Mounted, "no".to_string()); + dataset::Type::Filesystem => { + properties.insert(dataset::Property::Mounted, "no".to_string()); properties - .entry(DatasetProperty::Mountpoint) + .entry(dataset::Property::Mountpoint) .or_insert("none".to_string()); } - DatasetType::Volume => (), - DatasetType::Snapshot => (), + dataset::Type::Volume => (), + dataset::Type::Snapshot => (), } let parent = if let Some((parent, _)) = name.as_str().rsplit_once('/') { @@ -330,7 +331,7 @@ impl Znodes { pub fn mount( &mut self, load_keys: bool, - name: &DatasetName, + name: &dataset::Name, ) -> Result<(), String> { let dataset = self .datasets @@ -338,17 +339,17 @@ impl Znodes { .ok_or_else(|| format!("Cannot mount '{name}': Does not exist"))?; let properties = &mut dataset.properties; let mountpoint_property = - properties.get(DatasetProperty::Mountpoint)?; + properties.get(dataset::Property::Mountpoint)?; if !mountpoint_property.starts_with('/') { return Err(format!( "Cannot mount '{name}' with mountpoint: {mountpoint_property}" )); } - let mounted_property = properties.get(DatasetProperty::Mounted)?; + let mounted_property = properties.get(dataset::Property::Mounted)?; assert_eq!(mounted_property, "no"); let encryption_property = - properties.get(DatasetProperty::Encryption)?; + properties.get(dataset::Property::Encryption)?; let encrypted = match encryption_property.as_str() { "off" => false, "aes-256-gcm" => true, @@ -360,7 +361,7 @@ impl Znodes { }; let keylocation_property = - properties.get(DatasetProperty::Keylocation)?; + properties.get(dataset::Property::Keylocation)?; if encrypted { if !load_keys { return Err(format!( @@ -374,7 +375,7 @@ impl Znodes { return Err(format!("Key at {keylocation} does not exist")); } - let keyformat = properties.get(DatasetProperty::Keyformat)?; + let keyformat = properties.get(dataset::Property::Keyformat)?; if keyformat != "raw" { return Err(format!( "Cannot mount '{name}': Unknown keyformat: {keyformat}" @@ -393,7 +394,7 @@ impl Znodes { pub fn destroy_dataset( &mut self, - name: &DatasetName, + name: &dataset::Name, // TODO: Not emulating this option _recusive_dependents: bool, recursive_children: bool, @@ -418,7 +419,7 @@ impl Znodes { .lookup_by_index(child_idx) .map(|znode| znode.to_string()) .ok_or_else(|| format!("Child node missing name"))?; - let child_name = DatasetName::new(child_name)?; + let child_name = dataset::Name::new(child_name)?; if !recursive_children { return Err(format!("Cannot delete dataset {name}: has children (e.g.: {child_name})")); diff --git a/helios/tokamak/src/lib.rs b/helios/tokamak/src/lib.rs index 94bf7bf9167..ee577bd5564 100644 --- a/helios/tokamak/src/lib.rs +++ b/helios/tokamak/src/lib.rs @@ -6,7 +6,7 @@ mod cli; mod executor; mod host; mod shared_byte_queue; -mod types; +pub mod types; pub use executor::*; pub use host::FakeHost; diff --git a/helios/tokamak/src/types.rs b/helios/tokamak/src/types/dataset.rs similarity index 74% rename from helios/tokamak/src/types.rs rename to helios/tokamak/src/types/dataset.rs index 5d4a7642f1b..17160b0d6c8 100644 --- a/helios/tokamak/src/types.rs +++ b/helios/tokamak/src/types/dataset.rs @@ -2,17 +2,13 @@ // 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 which may be parsed from the CLI and used by a fake host - use std::fmt; -// TODO: nest under "dataset" module, eliminate prefix - /// The name of a ZFS filesystem, volume, or snapshot #[derive(Debug, Clone, Eq, PartialEq, Hash)] -pub struct DatasetName(String); +pub struct Name(String); -impl DatasetName { +impl Name { pub fn new>(s: S) -> Result { let s: String = s.into(); if s.is_empty() { @@ -30,7 +26,7 @@ impl DatasetName { } } -impl fmt::Display for DatasetName { +impl fmt::Display for Name { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.0) } @@ -48,7 +44,7 @@ impl fmt::Display for DatasetName { Hash, )] #[strum(use_phf, serialize_all = "lowercase")] -pub(crate) enum DatasetType { +pub(crate) enum Type { Filesystem, Snapshot, Volume, @@ -56,27 +52,27 @@ pub(crate) enum DatasetType { bitflags::bitflags! { /// The classes of datasets for which a property is valid. - pub(crate) struct DatasetPropertyTarget: u8 { + pub(crate) struct PropertyTarget: u8 { const FILESYSTEM = 0b0001; const SNAPSHOT = 0b0010; const VOLUME = 0b0100; } } -impl From for DatasetPropertyTarget { - fn from(ty: DatasetType) -> Self { - use DatasetType::*; +impl From for PropertyTarget { + fn from(ty: Type) -> Self { + use Type::*; match ty { - Filesystem => DatasetPropertyTarget::FILESYSTEM, - Snapshot => DatasetPropertyTarget::SNAPSHOT, - Volume => DatasetPropertyTarget::VOLUME, + Filesystem => PropertyTarget::FILESYSTEM, + Snapshot => PropertyTarget::SNAPSHOT, + Volume => PropertyTarget::VOLUME, } } } /// The ability of users to modify properties #[derive(Eq, PartialEq)] -pub(crate) enum DatasetPropertyAccess { +pub(crate) enum PropertyAccess { ReadOnly, ReadWrite, } @@ -94,7 +90,7 @@ pub(crate) enum DatasetPropertyAccess { Hash, )] #[strum(use_phf, serialize_all = "lowercase")] -pub(crate) enum DatasetProperty { +pub(crate) enum Property { Atime, #[strum(serialize = "available", serialize = "avail")] Available, @@ -117,10 +113,10 @@ pub(crate) enum DatasetProperty { Zoned, } -impl DatasetProperty { - pub fn access(&self) -> DatasetPropertyAccess { - use DatasetProperty::*; - use DatasetPropertyAccess::*; +impl Property { + pub fn access(&self) -> PropertyAccess { + use Property::*; + use PropertyAccess::*; match self { Atime => ReadWrite, @@ -143,14 +139,13 @@ impl DatasetProperty { } } - pub fn target(&self) -> DatasetPropertyTarget { - let fs = DatasetPropertyTarget::FILESYSTEM; - let all = DatasetPropertyTarget::all(); - let fs_and_vol = - DatasetPropertyTarget::FILESYSTEM | DatasetPropertyTarget::VOLUME; - let vol = DatasetPropertyTarget::VOLUME; + pub fn target(&self) -> PropertyTarget { + let fs = PropertyTarget::FILESYSTEM; + let all = PropertyTarget::all(); + let fs_and_vol = PropertyTarget::FILESYSTEM | PropertyTarget::VOLUME; + let vol = PropertyTarget::VOLUME; - use DatasetProperty::*; + use Property::*; match self { Atime => fs, Available => all, diff --git a/helios/tokamak/src/types/mod.rs b/helios/tokamak/src/types/mod.rs new file mode 100644 index 00000000000..49b9a778e7d --- /dev/null +++ b/helios/tokamak/src/types/mod.rs @@ -0,0 +1,7 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// 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 which may be parsed from the CLI and used by a fake host + +pub mod dataset; From eda11792ceb27ba83841645a53fce95011467b93 Mon Sep 17 00:00:00 2001 From: Sean Klein Date: Tue, 29 Aug 2023 00:33:14 -0700 Subject: [PATCH 14/18] Split datasets and zpools --- .../src/host/{znode.rs => datasets.rs} | 158 ++++-------------- helios/tokamak/src/host/mod.rs | 72 ++++---- helios/tokamak/src/host/zpools.rs | 93 +++++++++++ 3 files changed, 169 insertions(+), 154 deletions(-) rename helios/tokamak/src/host/{znode.rs => datasets.rs} (71%) create mode 100644 helios/tokamak/src/host/zpools.rs diff --git a/helios/tokamak/src/host/znode.rs b/helios/tokamak/src/host/datasets.rs similarity index 71% rename from helios/tokamak/src/host/znode.rs rename to helios/tokamak/src/host/datasets.rs index 9947995da83..cfd92abe614 100644 --- a/helios/tokamak/src/host/znode.rs +++ b/helios/tokamak/src/host/datasets.rs @@ -2,42 +2,13 @@ // 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/. -//! Emulates zpools and datasets on an illumos system +//! Emulates datasets use crate::types::dataset; use camino::Utf8PathBuf; -use helios_fusion::zpool::{ZpoolHealth, ZpoolName}; use petgraph::stable_graph::{StableGraph, WalkNeighbors}; use std::collections::HashMap; -use std::str::FromStr; - -pub(crate) struct FakeZpool { - zix: NodeIndex, - name: ZpoolName, - vdev: Utf8PathBuf, - - pub imported: bool, - pub health: ZpoolHealth, - pub properties: HashMap, -} - -impl FakeZpool { - pub(crate) fn new( - zix: NodeIndex, - name: ZpoolName, - vdev: Utf8PathBuf, - ) -> Self { - Self { - zix, - name, - vdev, - imported: false, - health: ZpoolHealth::Online, - properties: HashMap::new(), - } - } -} struct DatasetProperties(HashMap); @@ -55,18 +26,18 @@ impl DatasetProperties { } pub(crate) struct FakeDataset { - zix: NodeIndex, + idx: NodeIndex, properties: DatasetProperties, ty: dataset::Type, } impl FakeDataset { fn new( - zix: NodeIndex, + idx: NodeIndex, properties: HashMap, ty: dataset::Type, ) -> Self { - Self { zix, properties: DatasetProperties(properties), ty } + Self { idx, properties: DatasetProperties(properties), ty } } pub fn ty(&self) -> dataset::Type { @@ -114,7 +85,6 @@ impl FakeDataset { pub(crate) enum Znode { Root, - Zpool(ZpoolName), Dataset(dataset::Name), } @@ -122,103 +92,68 @@ impl Znode { pub fn to_string(&self) -> String { match self { Znode::Root => "/".to_string(), - Znode::Zpool(name) => name.to_string(), Znode::Dataset(name) => name.as_str().to_string(), } } } -/// The type of an index used to lookup nodes in the znode DAG. +/// The type of an index used to lookup nodes in the DAG. pub(crate) type NodeIndex = petgraph::graph::NodeIndex; /// Describes access to zpools and datasets that exist within the system. /// /// On Helios, datasets exist as children of zpools, within a DAG structure. -/// Understanding the relationship between these znodes (dubbed "znodes", to -/// include both zpools and datasets) is important to accurately emulate -/// many ZFS operations, such as deletion, which can conditionally succeed or -/// fail depeending on the prescence of children. -pub(crate) struct Znodes { +/// Understanding the relationship between these datasets is important to +/// accurately emulate many ZFS operations, such as deletion, which can +/// conditionally succeed or fail depending on the prescence of children. +pub(crate) struct Datasets { // Describes the connectivity between nodes - all_znodes: StableGraph, - zix_root: NodeIndex, + dataset_graph: StableGraph, + dataset_graph_root: NodeIndex, // Individual nodes themselves - zpools: HashMap, datasets: HashMap, } -impl Znodes { +impl Datasets { pub(crate) fn new() -> Self { - let mut all_znodes = StableGraph::new(); - let zix_root = all_znodes.add_node(Znode::Root); - Self { - all_znodes, - zix_root, - zpools: HashMap::new(), - datasets: HashMap::new(), - } - } - - // Zpool access methods - - pub fn get_zpool(&self, name: &ZpoolName) -> Option<&FakeZpool> { - self.zpools.get(name) - } - - pub fn get_zpool_mut( - &mut self, - name: &ZpoolName, - ) -> Option<&mut FakeZpool> { - self.zpools.get_mut(name) - } - - pub fn all_zpools(&self) -> impl Iterator { - self.zpools.iter() + let mut dataset_graph = StableGraph::new(); + let dataset_graph_root = dataset_graph.add_node(Znode::Root); + Self { dataset_graph, dataset_graph_root, datasets: HashMap::new() } } - // Dataset access methods - pub fn get_dataset(&self, name: &dataset::Name) -> Option<&FakeDataset> { self.datasets.get(name) } - // Index-based access methods - /// Returns the index of the "root" of the Znode DAG. /// /// This node does not actually exist, but the children of this node should /// be zpools. pub fn root_index(&self) -> NodeIndex { - self.zix_root + self.dataset_graph_root } - /// Returns the node index of a znode + /// Returns the node index of a dataset pub fn index_of(&self, name: &str) -> Result { - if !name.contains('/') { - if let Some(pool) = self.zpools.get(&ZpoolName::from_str(name)?) { - return Ok(pool.zix); - } - } if let Some(dataset) = self.datasets.get(&dataset::Name::new(name)?) { - return Ok(dataset.zix); + return Ok(dataset.idx); } Err(format!("{} not found", name)) } /// Looks up a Znode by an index pub fn lookup_by_index(&self, index: NodeIndex) -> Option<&Znode> { - self.all_znodes.node_weight(index) + self.dataset_graph.node_weight(index) } - /// Describe the type of a znode by string + /// Describe the type of a dataset by string pub fn type_str(&self, node: &Znode) -> Result<&'static str, String> { match node { Znode::Root => { Err("Invalid (root) node for type string".to_string()) } - Znode::Zpool(_) => Ok("fileystem"), Znode::Dataset(name) => { let dataset = self .datasets @@ -235,42 +170,21 @@ impl Znodes { pub fn children( &self, - zix: NodeIndex, + idx: NodeIndex, ) -> impl Iterator + '_ { - self.all_znodes.neighbors_directed(zix, petgraph::Direction::Outgoing) + self.dataset_graph + .neighbors_directed(idx, petgraph::Direction::Outgoing) } pub fn children_mut( &self, - zix: NodeIndex, + idx: NodeIndex, ) -> WalkNeighbors { - self.all_znodes - .neighbors_directed(zix, petgraph::Direction::Outgoing) + self.dataset_graph + .neighbors_directed(idx, petgraph::Direction::Outgoing) .detach() } - pub fn add_zpool( - &mut self, - name: ZpoolName, - vdev: Utf8PathBuf, - import: bool, - ) -> Result<(), String> { - if self.zpools.contains_key(&name) { - return Err(format!( - "Cannot create pool name '{name}': already exists" - )); - } - - let zix = self.all_znodes.add_node(Znode::Zpool(name.clone())); - self.all_znodes.add_edge(self.zix_root, zix, ()); - - let mut pool = FakeZpool::new(zix, name.clone(), vdev); - pool.imported = import; - self.zpools.insert(name, pool); - - Ok(()) - } - pub fn add_dataset( &mut self, name: dataset::Name, @@ -312,8 +226,8 @@ impl Znodes { return Err(format!("Cannot create '{}': No parent dataset. Try creating one under an existing filesystem or zpool?", name.as_str())); }; - let parent_zix = self.index_of(parent)?; - if !self.all_znodes.contains_node(parent_zix) { + let parent_idx = self.index_of(parent)?; + if !self.dataset_graph.contains_node(parent_idx) { return Err(format!( "Cannot create fs '{}': Missing parent node: {}", name.as_str(), @@ -321,9 +235,9 @@ impl Znodes { )); } - let zix = self.all_znodes.add_node(Znode::Dataset(name.clone())); - self.all_znodes.add_edge(parent_zix, zix, ()); - self.datasets.insert(name, FakeDataset::new(zix, properties, ty)); + let idx = self.dataset_graph.add_node(Znode::Dataset(name.clone())); + self.dataset_graph.add_edge(parent_idx, idx, ()); + self.datasets.insert(name, FakeDataset::new(idx, properties, ty)); Ok(()) } @@ -411,13 +325,13 @@ impl Znodes { dataset.unmount()?; } - let zix = dataset.zix; + let idx = dataset.idx; - let mut children = self.children_mut(zix); - while let Some(child_idx) = children.next_node(&self.all_znodes) { + let mut children = self.children_mut(idx); + while let Some(child_idx) = children.next_node(&self.dataset_graph) { let child_name = self .lookup_by_index(child_idx) - .map(|znode| znode.to_string()) + .map(|n| n.to_string()) .ok_or_else(|| format!("Child node missing name"))?; let child_name = dataset::Name::new(child_name)?; @@ -431,7 +345,7 @@ impl Znodes { force_unmount, )?; } - self.all_znodes.remove_node(zix); + self.dataset_graph.remove_node(idx); self.datasets.remove(&name); Ok(()) diff --git a/helios/tokamak/src/host/mod.rs b/helios/tokamak/src/host/mod.rs index af3cc7cb0b0..282021dc235 100644 --- a/helios/tokamak/src/host/mod.rs +++ b/helios/tokamak/src/host/mod.rs @@ -7,7 +7,6 @@ // TODO: REMOVE #![allow(dead_code)] -use crate::host::znode::{FakeZpool, Znodes}; use crate::types::dataset; use crate::{FakeChild, FakeExecutor, FakeExecutorBuilder}; @@ -22,7 +21,11 @@ use std::collections::{HashMap, HashSet, VecDeque}; use std::io::Read; use std::sync::{Arc, Mutex}; -mod znode; +mod datasets; +mod zpools; + +use datasets::Datasets; +use zpools::{FakeZpool, Zpools}; pub enum LinkType { Etherstub, @@ -129,7 +132,8 @@ struct FakeHostInner { zones: HashMap, vdevs: HashSet, - znodes: Znodes, + datasets: Datasets, + zpools: Zpools, swap_devices: Vec, processes: HashMap, @@ -142,7 +146,8 @@ impl FakeHostInner { global: ZoneEnvironment::new(0), zones: HashMap::new(), vdevs: HashSet::new(), - znodes: Znodes::new(), + datasets: Datasets::new(), + zpools: Zpools::new(), swap_devices: vec![], processes: HashMap::new(), } @@ -186,7 +191,7 @@ impl FakeHostInner { } } - self.znodes + self.datasets .add_dataset( name.clone(), properties, @@ -292,7 +297,7 @@ impl FakeHostInner { } let mut inner = inner.lock().unwrap(); - match inner.znodes.add_dataset( + match inner.datasets.add_dataset( name, properties, dataset::Type::Volume, @@ -306,7 +311,7 @@ impl FakeHostInner { )); } - self.znodes + self.datasets .add_dataset( name.clone(), properties, @@ -322,7 +327,7 @@ impl FakeHostInner { force_unmount, name, } => { - self.znodes + self.datasets .destroy_dataset( &name, recursive_dependents, @@ -343,7 +348,7 @@ impl FakeHostInner { let depth = if recursive { depth } else { Some(0) }; for dataset in datasets { let zix = self - .znodes + .datasets .index_of(dataset.as_str()) .map_err(to_stderr)?; targets.push_back((zix, depth)); @@ -351,7 +356,7 @@ impl FakeHostInner { targets } else { VecDeque::from([( - self.znodes.root_index(), + self.datasets.root_index(), depth.map(|d| d + 1), )]) }; @@ -360,7 +365,7 @@ impl FakeHostInner { while let Some((target, depth)) = targets.pop_front() { let node = self - .znodes + .datasets .lookup_by_index(target) .expect( "We should have looked up the znode earlier...", @@ -378,12 +383,12 @@ impl FakeHostInner { }; if add_children { - for child in self.znodes.children(target) { + for child in self.datasets.children(target) { targets.push_front((child, child_depth)); } } - if target == self.znodes.root_index() { + if target == self.datasets.root_index() { // Skip the root node, as there is nothing to // display for it. continue; @@ -424,7 +429,7 @@ impl FakeHostInner { for dataset in datasets { let zix = self - .znodes + .datasets .index_of(dataset.as_str()) .map_err(to_stderr)?; targets.push_back((zix, depth)); @@ -435,7 +440,7 @@ impl FakeHostInner { // Bump whatever the depth was up by one, since we // don't display anything for the root node. VecDeque::from([( - self.znodes.root_index(), + self.datasets.root_index(), depth.map(|d| d + 1), )]) }; @@ -444,7 +449,7 @@ impl FakeHostInner { while let Some((target, depth)) = targets.pop_front() { let node = self - .znodes + .datasets .lookup_by_index(target) .expect( "We should have looked up the znode earlier...", @@ -462,12 +467,12 @@ impl FakeHostInner { }; if add_children { - for child in self.znodes.children(target) { + for child in self.datasets.children(target) { targets.push_front((child, child_depth)); } } - if target == self.znodes.root_index() { + if target == self.datasets.root_index() { // Skip the root node, as there is nothing to // display for it. continue; @@ -480,14 +485,14 @@ impl FakeHostInner { } dataset::Property::Type => { let node = self - .znodes + .datasets .lookup_by_index(target) .ok_or_else(|| { to_stderr("Node not found") })?; output.push_str( - self.znodes + self.datasets .type_str(node) .map_err(to_stderr)?, ) @@ -509,7 +514,7 @@ impl FakeHostInner { )) } Mount { load_keys, filesystem } => { - self.znodes + self.datasets .mount(load_keys, &filesystem) .map_err(to_stderr)?; Ok(ProcessState::Completed( @@ -539,14 +544,19 @@ impl FakeHostInner { } let import = true; - self.znodes - .add_zpool(pool.clone(), vdev.clone(), import) + self.zpools + .insert( + &mut self.datasets, + pool.clone(), + vdev.clone(), + import, + ) .map_err(to_stderr)?; Ok(ProcessState::Completed(Output::success())) } Export { pool: name } => { - let Some(mut pool) = self.znodes.get_zpool_mut(&name) else { + let Some(mut pool) = self.zpools.get_mut(&name) else { return Err(to_stderr(format!("pool does not exist"))); }; @@ -559,7 +569,7 @@ impl FakeHostInner { Ok(ProcessState::Completed(Output::success())) } Import { force: _, pool: name } => { - let Some(mut pool) = self.znodes.get_zpool_mut(&name) else { + let Some(mut pool) = self.zpools.get_mut(&name) else { return Err(to_stderr(format!("pool does not exist"))); }; @@ -598,10 +608,8 @@ impl FakeHostInner { if let Some(pools) = pools { for name in &pools { - let pool = self - .znodes - .get_zpool(name) - .ok_or_else(|| { + let pool = + self.zpools.get(name).ok_or_else(|| { to_stderr(format!( "{} does not exist", name @@ -618,7 +626,7 @@ impl FakeHostInner { display(&name, &pool, &properties)?; } } else { - for (name, pool) in self.znodes.all_zpools() { + for (name, pool) in self.zpools.all() { if pool.imported { display(&name, &pool, &properties)?; } @@ -630,7 +638,7 @@ impl FakeHostInner { )) } Set { property, value, pool: name } => { - let Some(pool) = self.znodes.get_zpool_mut(&name) else { + let Some(pool) = self.zpools.get_mut(&name) else { return Err(to_stderr(format!("{} does not exist", name))); }; pool.properties.insert(property, value); @@ -764,7 +772,7 @@ impl swapctl::Swapctl for FakeHost { return Err(swapctl::Error::AddDevice { msg, path, start, length }); }; - if let Some(dataset) = inner.znodes.get_dataset(&volume) { + if let Some(dataset) = inner.datasets.get_dataset(&volume) { match dataset.ty() { dataset::Type::Volume => (), _ => { diff --git a/helios/tokamak/src/host/zpools.rs b/helios/tokamak/src/host/zpools.rs new file mode 100644 index 00000000000..abcd1f9112a --- /dev/null +++ b/helios/tokamak/src/host/zpools.rs @@ -0,0 +1,93 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// 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/. + +//! Emulates zpools + +use crate::host::datasets::Datasets; +use crate::types::dataset; + +use camino::Utf8PathBuf; +use helios_fusion::zpool::{ZpoolHealth, ZpoolName}; +use std::collections::HashMap; + +pub(crate) struct FakeZpool { + name: ZpoolName, + vdev: Utf8PathBuf, + + pub imported: bool, + pub health: ZpoolHealth, + pub properties: HashMap, +} + +impl FakeZpool { + pub(crate) fn new( + name: ZpoolName, + vdev: Utf8PathBuf, + imported: bool, + ) -> Self { + Self { + name, + vdev, + imported, + health: ZpoolHealth::Online, + properties: HashMap::new(), + } + } +} + +/// Describes access to zpools that exist within the system. +pub(crate) struct Zpools { + zpools: HashMap, +} + +impl Zpools { + pub(crate) fn new() -> Self { + Self { zpools: HashMap::new() } + } + + // Zpool access methods + + pub fn get(&self, name: &ZpoolName) -> Option<&FakeZpool> { + self.zpools.get(name) + } + + pub fn get_mut(&mut self, name: &ZpoolName) -> Option<&mut FakeZpool> { + self.zpools.get_mut(name) + } + + pub fn all(&self) -> impl Iterator { + self.zpools.iter() + } + + pub fn insert( + &mut self, + datasets: &mut Datasets, + name: ZpoolName, + vdev: Utf8PathBuf, + import: bool, + ) -> Result<(), String> { + if self.zpools.contains_key(&name) { + return Err(format!( + "Cannot create pool name '{name}': already exists" + )); + } + + let pool = FakeZpool::new(name.clone(), vdev, import); + self.zpools.insert(name.clone(), pool); + + let mut dataset_properties = HashMap::new(); + dataset_properties + .insert(dataset::Property::Mountpoint, format!("/{name}")); + datasets + .add_dataset( + dataset::Name::new(name.to_string()) + .expect("Zpool names should be valid dataset names"), + dataset_properties, + dataset::Type::Filesystem, + ) + .expect("Failed to add dataset after creating zpool"); + + Ok(()) + } +} From 19d9d98751925007f8fccaa042646baaf296d083 Mon Sep 17 00:00:00 2001 From: Sean Klein Date: Tue, 29 Aug 2023 16:24:25 -0700 Subject: [PATCH 15/18] Some dataset tests, cleanup --- helios/tokamak/src/host/datasets.rs | 594 +++++++++++++++++++++++----- helios/tokamak/src/host/mod.rs | 118 +++--- helios/tokamak/src/host/zpools.rs | 18 +- 3 files changed, 568 insertions(+), 162 deletions(-) diff --git a/helios/tokamak/src/host/datasets.rs b/helios/tokamak/src/host/datasets.rs index cfd92abe614..3ad7e105b8c 100644 --- a/helios/tokamak/src/host/datasets.rs +++ b/helios/tokamak/src/host/datasets.rs @@ -7,13 +7,17 @@ use crate::types::dataset; use camino::Utf8PathBuf; +use helios_fusion::zpool::ZpoolName; use petgraph::stable_graph::{StableGraph, WalkNeighbors}; use std::collections::HashMap; -struct DatasetProperties(HashMap); +pub(crate) struct DatasetProperties(HashMap); impl DatasetProperties { - fn get(&self, property: dataset::Property) -> Result { + pub(crate) fn get( + &self, + property: dataset::Property, + ) -> Result { self.0 .get(&property) .map(|k| k.to_string()) @@ -40,10 +44,21 @@ impl FakeDataset { Self { idx, properties: DatasetProperties(properties), ty } } + pub fn properties(&self) -> &DatasetProperties { + &self.properties + } + pub fn ty(&self) -> dataset::Type { self.ty } + fn mounted(&self) -> bool { + self.properties + .get(dataset::Property::Mounted) + .map(|s| s == "yes") + .unwrap_or(false) + } + fn mountpoint(&self) -> Option { self.properties .get(dataset::Property::Mountpoint) @@ -51,48 +66,114 @@ impl FakeDataset { .ok() } - fn mount(&mut self, new: Utf8PathBuf) -> Result<(), String> { + fn mount(&mut self, load_keys: bool) -> Result<(), String> { + let properties = &mut self.properties; + match self.ty { dataset::Type::Filesystem => { - if self.properties.get(dataset::Property::Mountpoint)? != "none" - { + if properties.get(dataset::Property::Mounted)? != "no" { return Err("Already mounted".to_string()); } } _ => return Err("Not a filesystem".to_string()), }; - self.properties.insert(dataset::Property::Mountpoint, new); - self.properties.insert(dataset::Property::Mounted, "yes"); + + let encryption_property = + properties.get(dataset::Property::Encryption)?; + let encrypted = match encryption_property.as_str() { + "off" => false, + "aes-256-gcm" => true, + _ => { + return Err(format!( + "Unsupported encryption: {encryption_property}" + )) + } + }; + + if encrypted { + if !load_keys { + return Err(format!("Use 'zfs mount -l'")); + } + let keylocation_property = + properties.get(dataset::Property::Keylocation)?; + let Some(keylocation) = keylocation_property.strip_prefix("file://").map(|k| Utf8PathBuf::from(k)) else { + return Err(format!("Cannot read from key location: {keylocation_property}")); + }; + if !keylocation.exists() { + return Err(format!("Key at {keylocation} does not exist")); + } + + let keyformat = properties.get(dataset::Property::Keyformat)?; + if keyformat != "raw" { + return Err(format!("Unknown keyformat: {keyformat}")); + } + } + + let mountpoint = properties.get(dataset::Property::Mountpoint)?; + if !mountpoint.starts_with('/') { + return Err(format!("Cannot mount with mountpoint: {mountpoint}")); + } + + properties.insert(dataset::Property::Mounted, "yes"); Ok(()) } // TODO: Confirm that the filesystem isn't used by zones? fn unmount(&mut self) -> Result<(), String> { - let mountpoint = match &mut self.ty { + let mounted = match &mut self.ty { dataset::Type::Filesystem => { - self.properties.get(dataset::Property::Mountpoint)? + self.properties.get(dataset::Property::Mounted)? } _ => return Err("Not a filesystem".to_string()), }; - if mountpoint == "none" { + if mounted == "no" { return Err("Filesystem is not mounted".to_string()); } - self.properties.insert(dataset::Property::Mountpoint, "none"); self.properties.insert(dataset::Property::Mounted, "no"); Ok(()) } } -pub(crate) enum Znode { +pub(crate) enum DatasetNode { Root, Dataset(dataset::Name), } -impl Znode { +impl DatasetNode { pub fn to_string(&self) -> String { match self { - Znode::Root => "/".to_string(), - Znode::Dataset(name) => name.as_str().to_string(), + DatasetNode::Root => "/".to_string(), + DatasetNode::Dataset(name) => name.as_str().to_string(), + } + } + + pub fn dataset_name(&self) -> Option<&dataset::Name> { + match self { + DatasetNode::Root => None, + DatasetNode::Dataset(name) => Some(name), + } + } +} + +pub(crate) enum DatasetInsert { + // Used to create datasets that correspond with zpools. + // + // These datasets do not required parents, and may be directly + // attached to the root of the dataset DAG. + WithoutParent(ZpoolName), + + // Used to create datasets "the normal way", where the name should imply a + // parent dataset which already exists in the dataset DAG. + WithParent(dataset::Name), +} + +impl DatasetInsert { + fn name(&self) -> dataset::Name { + use DatasetInsert::*; + match self { + WithoutParent(zpool) => dataset::Name::new(zpool.to_string()) + .expect("Zpool names should be valid datasets"), + WithParent(name) => name.clone(), } } } @@ -109,7 +190,7 @@ pub(crate) type NodeIndex = /// conditionally succeed or fail depending on the prescence of children. pub(crate) struct Datasets { // Describes the connectivity between nodes - dataset_graph: StableGraph, + dataset_graph: StableGraph, dataset_graph_root: NodeIndex, // Individual nodes themselves @@ -119,7 +200,7 @@ pub(crate) struct Datasets { impl Datasets { pub(crate) fn new() -> Self { let mut dataset_graph = StableGraph::new(); - let dataset_graph_root = dataset_graph.add_node(Znode::Root); + let dataset_graph_root = dataset_graph.add_node(DatasetNode::Root); Self { dataset_graph, dataset_graph_root, datasets: HashMap::new() } } @@ -127,10 +208,17 @@ impl Datasets { self.datasets.get(name) } - /// Returns the index of the "root" of the Znode DAG. + pub fn get_dataset_mut( + &mut self, + name: &dataset::Name, + ) -> Option<&mut FakeDataset> { + self.datasets.get_mut(name) + } + + /// Returns the index of the "root" of the DatasetNode DAG. /// /// This node does not actually exist, but the children of this node should - /// be zpools. + /// be datasets from zpools. pub fn root_index(&self) -> NodeIndex { self.dataset_graph_root } @@ -143,31 +231,11 @@ impl Datasets { Err(format!("{} not found", name)) } - /// Looks up a Znode by an index - pub fn lookup_by_index(&self, index: NodeIndex) -> Option<&Znode> { + /// Looks up a DatasetNode by an index + pub fn lookup_by_index(&self, index: NodeIndex) -> Option<&DatasetNode> { self.dataset_graph.node_weight(index) } - /// Describe the type of a dataset by string - pub fn type_str(&self, node: &Znode) -> Result<&'static str, String> { - match node { - Znode::Root => { - Err("Invalid (root) node for type string".to_string()) - } - Znode::Dataset(name) => { - let dataset = self - .datasets - .get(&name) - .ok_or_else(|| "Missing dataset".to_string())?; - match dataset.ty { - dataset::Type::Filesystem => Ok("filesystem"), - dataset::Type::Snapshot => Ok("snapshot"), - dataset::Type::Volume => Ok("volume"), - } - } - } - } - pub fn children( &self, idx: NodeIndex, @@ -187,7 +255,7 @@ impl Datasets { pub fn add_dataset( &mut self, - name: dataset::Name, + insert: DatasetInsert, mut properties: HashMap, ty: dataset::Type, ) -> Result<(), String> { @@ -199,6 +267,7 @@ impl Datasets { } } + let name = insert.name(); if self.datasets.contains_key(&name) { return Err(format!( "Cannot create '{}': already exists", @@ -208,9 +277,16 @@ impl Datasets { properties.insert(dataset::Property::Type, ty.to_string()); properties.insert(dataset::Property::Name, name.to_string()); + properties + .entry(dataset::Property::Encryption) + .or_insert("off".to_string()); + properties.entry(dataset::Property::Zoned).or_insert("off".to_string()); match &ty { dataset::Type::Filesystem => { + properties + .entry(dataset::Property::Atime) + .or_insert("on".to_string()); properties.insert(dataset::Property::Mounted, "no".to_string()); properties .entry(dataset::Property::Mountpoint) @@ -220,13 +296,27 @@ impl Datasets { dataset::Type::Snapshot => (), } - let parent = if let Some((parent, _)) = name.as_str().rsplit_once('/') { - parent - } else { - return Err(format!("Cannot create '{}': No parent dataset. Try creating one under an existing filesystem or zpool?", name.as_str())); - }; + let (parent, parent_idx) = match insert { + DatasetInsert::WithoutParent(_) => { + if ty != dataset::Type::Filesystem { + return Err(format!("Cannot create '{name}' as anything other than a filesystem")); + } + ("/", self.root_index()) + } + DatasetInsert::WithParent(_) => { + let parent = if let Some((parent, _)) = + name.as_str().rsplit_once('/') + { + parent + } else { + return Err(format!("Cannot create '{}': No parent dataset. Try creating one under an existing filesystem or zpool?", name.as_str())); + }; + + let parent_idx = self.index_of(parent)?; - let parent_idx = self.index_of(parent)?; + (parent, parent_idx) + } + }; if !self.dataset_graph.contains_node(parent_idx) { return Err(format!( "Cannot create fs '{}': Missing parent node: {}", @@ -234,8 +324,8 @@ impl Datasets { parent )); } - - let idx = self.dataset_graph.add_node(Znode::Dataset(name.clone())); + let idx = + self.dataset_graph.add_node(DatasetNode::Dataset(name.clone())); self.dataset_graph.add_edge(parent_idx, idx, ()); self.datasets.insert(name, FakeDataset::new(idx, properties, ty)); @@ -251,62 +341,25 @@ impl Datasets { .datasets .get_mut(&name) .ok_or_else(|| format!("Cannot mount '{name}': Does not exist"))?; - let properties = &mut dataset.properties; - let mountpoint_property = - properties.get(dataset::Property::Mountpoint)?; - if !mountpoint_property.starts_with('/') { - return Err(format!( - "Cannot mount '{name}' with mountpoint: {mountpoint_property}" - )); - } - - let mounted_property = properties.get(dataset::Property::Mounted)?; - assert_eq!(mounted_property, "no"); - let encryption_property = - properties.get(dataset::Property::Encryption)?; - let encrypted = match encryption_property.as_str() { - "off" => false, - "aes-256-gcm" => true, - _ => { - return Err(format!( - "Unsupported encryption: {encryption_property}" - )) - } - }; - - let keylocation_property = - properties.get(dataset::Property::Keylocation)?; - if encrypted { - if !load_keys { - return Err(format!( - "Cannot mount '{name}': Use 'zfs mount -l {name}'" - )); - } - let Some(keylocation) = keylocation_property.strip_prefix("file://").map(|k| Utf8PathBuf::from(k)) else { - return Err(format!("Cannot read from key location: {keylocation_property}")); - }; - if !keylocation.exists() { - return Err(format!("Key at {keylocation} does not exist")); - } - - let keyformat = properties.get(dataset::Property::Keyformat)?; - if keyformat != "raw" { - return Err(format!( - "Cannot mount '{name}': Unknown keyformat: {keyformat}" - )); - } - } - - // Perform modifications to "mount" filesystem dataset - .mount(Utf8PathBuf::from(mountpoint_property)) + .mount(load_keys) .map_err(|err| format!("Cannot mount '{name}': {err}"))?; Ok(()) } - pub fn destroy_dataset( + pub fn unmount(&mut self, name: &dataset::Name) -> Result<(), String> { + let dataset = self.datasets.get_mut(&name).ok_or_else(|| { + format!("Cannot unmount '{name}': Does not exist") + })?; + dataset + .unmount() + .map_err(|err| format!("Cannot unmount '{name}': {err}"))?; + Ok(()) + } + + pub fn destroy( &mut self, name: &dataset::Name, // TODO: Not emulating this option @@ -318,7 +371,7 @@ impl Datasets { format!("Cannot destroy '{name}': Does not exist") })?; - if dataset.mountpoint().is_some() { + if dataset.mounted() { if !force_unmount { return Err(format!("Cannot destroy '{name}': Mounted")); } @@ -338,7 +391,7 @@ impl Datasets { if !recursive_children { return Err(format!("Cannot delete dataset {name}: has children (e.g.: {child_name})")); } - self.destroy_dataset( + self.destroy( &child_name, _recusive_dependents, recursive_children, @@ -351,3 +404,342 @@ impl Datasets { Ok(()) } } + +#[cfg(test)] +mod test { + use super::*; + use std::str::FromStr; + use uuid::Uuid; + + fn expect_err>( + result: Result, + expected: S, + ) -> Result<(), String> { + let expected: String = expected.into(); + let errmsg = result.err().unwrap(); + if !errmsg.contains(&expected) { + return Err(format!( + "Bad error: Expected: '{expected}', but saw '{errmsg}'" + )); + } + Ok(()) + } + + #[test] + fn create_dataset_tree() { + let mut datasets = Datasets::new(); + assert_eq!(None, datasets.children(datasets.root_index()).next()); + + let id = Uuid::new_v4(); + let zpool = format!("oxp_{id}"); + + // Create a filesystem for a fake zpool + + let zpool_dataset = dataset::Name::new(&zpool).unwrap(); + let zpool = ZpoolName::from_str(&zpool).unwrap(); + datasets + .add_dataset( + DatasetInsert::WithoutParent(zpool.clone()), + HashMap::new(), + dataset::Type::Filesystem, + ) + .expect("Failed to add dataset"); + + // Create a dataset as a child of that fake zpool filesystem + + let dataset_a = + dataset::Name::new(format!("{zpool}/dataset_a")).unwrap(); + datasets + .add_dataset( + DatasetInsert::WithParent(dataset_a.clone()), + HashMap::new(), + dataset::Type::Filesystem, + ) + .expect("Failed to add datasets"); + + // Create a child of the previous child + + let dataset_b = + dataset::Name::new(format!("{dataset_a}/dataset_b")).unwrap(); + datasets + .add_dataset( + DatasetInsert::WithParent(dataset_b.clone()), + HashMap::new(), + dataset::Type::Filesystem, + ) + .expect("Failed to add datasets"); + + let dataset_c = + dataset::Name::new(format!("{zpool}/dataset_c")).unwrap(); + datasets + .add_dataset( + DatasetInsert::WithParent(dataset_c.clone()), + HashMap::new(), + dataset::Type::Filesystem, + ) + .expect("Failed to add datasets"); + + // The layout should look like the following: + // + // oxp_ + // oxp_/dataset_a + // oxp_/dataset_a/dataset_b + // oxp_/dataset_c + + let z = datasets.get_dataset(&zpool_dataset).unwrap(); + let a = datasets.get_dataset(&dataset_a).unwrap(); + let b = datasets.get_dataset(&dataset_b).unwrap(); + let c = datasets.get_dataset(&dataset_c).unwrap(); + assert_eq!(z.ty, dataset::Type::Filesystem); + assert_eq!(a.ty, dataset::Type::Filesystem); + assert_eq!(b.ty, dataset::Type::Filesystem); + assert_eq!(c.ty, dataset::Type::Filesystem); + + // Root of datasets + let mut children = datasets.children(datasets.root_index()); + assert_eq!(Some(z.idx), children.next()); + assert_eq!(None, children.next()); + + // Zpool -> Datasets + let mut children = datasets.children(z.idx); + assert_eq!(Some(c.idx), children.next()); + assert_eq!(Some(a.idx), children.next()); + assert_eq!(None, children.next()); + + // Dataset with children + let mut children = datasets.children(a.idx); + assert_eq!(Some(b.idx), children.next()); + assert_eq!(None, children.next()); + + // Leaf nodes + assert_eq!(None, datasets.children(b.idx).next()); + assert_eq!(None, datasets.children(c.idx).next()); + } + + #[test] + fn filesystem_properties() { + let mut datasets = Datasets::new(); + + let id = Uuid::new_v4(); + let zpool_str_name = format!("oxp_{id}"); + + // Create a filesystem for a fake zpool + + let zpool_dataset = dataset::Name::new(&zpool_str_name).unwrap(); + let zpool = ZpoolName::from_str(&zpool_str_name).unwrap(); + datasets + .add_dataset( + DatasetInsert::WithoutParent(zpool.clone()), + HashMap::new(), + dataset::Type::Filesystem, + ) + .expect("Failed to add dataset"); + + let d = datasets.get_dataset(&zpool_dataset).unwrap(); + use dataset::Property::*; + assert_eq!("on", d.properties.get(Atime).unwrap()); + assert_eq!("off", d.properties.get(Encryption).unwrap()); + assert_eq!("no", d.properties.get(Mounted).unwrap()); + assert_eq!("none", d.properties.get(Mountpoint).unwrap()); + assert_eq!(zpool_str_name, d.properties.get(Name).unwrap()); + assert_eq!("filesystem", d.properties.get(Type).unwrap()); + assert_eq!("off", d.properties.get(Zoned).unwrap()); + } + + #[test] + fn filesystem_mount() { + let mut datasets = Datasets::new(); + + let id = Uuid::new_v4(); + let zpool_str_name = format!("oxp_{id}"); + + // Create a filesystem for a fake zpool + + let zpool_dataset = dataset::Name::new(&zpool_str_name).unwrap(); + let zpool = ZpoolName::from_str(&zpool_str_name).unwrap(); + datasets + .add_dataset( + DatasetInsert::WithoutParent(zpool.clone()), + HashMap::new(), + dataset::Type::Filesystem, + ) + .expect("Failed to add dataset"); + + let d = datasets.get_dataset(&zpool_dataset).unwrap(); + use dataset::Property::*; + assert_eq!("no", d.properties.get(Mounted).unwrap()); + assert_eq!("none", d.properties.get(Mountpoint).unwrap()); + drop(d); + + // Try to mount using the "none" mountpoint + let load_keys = false; + expect_err( + datasets.mount(load_keys, &zpool_dataset), + "Cannot mount with mountpoint: none", + ) + .unwrap(); + + // Update the mountpoint, try again + let d = datasets.get_dataset_mut(&zpool_dataset).unwrap(); + d.properties.insert(Mountpoint, "/foobar"); + drop(d); + + // We can mount it successfully + datasets.mount(load_keys, &zpool_dataset).unwrap(); + + // Re-mounting returns an error + expect_err( + datasets.mount(load_keys, &zpool_dataset), + "Already mounted", + ) + .unwrap(); + + // We can unmount successfully + datasets.unmount(&zpool_dataset).unwrap(); + + // Re-unmounting returns an error + expect_err( + datasets.unmount(&zpool_dataset), + "Filesystem is not mounted", + ) + .unwrap(); + } + + #[test] + fn invalid_dataset_insertion() { + let mut datasets = Datasets::new(); + + let id = Uuid::new_v4(); + let zpool_str_name = format!("oxp_{id}"); + let zpool = ZpoolName::from_str(&zpool_str_name).unwrap(); + + // Invalid property (meant for volume) + expect_err( + datasets.add_dataset( + DatasetInsert::WithoutParent(zpool.clone()), + HashMap::from([( + dataset::Property::Volsize, + "10G".to_string(), + )]), + dataset::Type::Filesystem, + ), + "Cannot create filesystem with property volsize", + ) + .unwrap(); + + // Cannot create volume for "without parent" + expect_err( + datasets.add_dataset( + DatasetInsert::WithoutParent(zpool.clone()), + HashMap::new(), + dataset::Type::Volume, + ), + format!("Cannot create '{zpool_str_name}' as anything other than a filesystem"), + ).unwrap(); + + // Cannot create filesystem "WithParent" that does not exist + expect_err( + datasets.add_dataset( + DatasetInsert::WithParent( + dataset::Name::new("mydataset").unwrap(), + ), + HashMap::new(), + dataset::Type::Filesystem, + ), + format!("No parent dataset"), + ) + .unwrap(); + + expect_err( + datasets.add_dataset( + DatasetInsert::WithParent( + dataset::Name::new("mydataset/nested").unwrap(), + ), + HashMap::new(), + dataset::Type::Filesystem, + ), + format!("mydataset not found"), + ) + .unwrap(); + } + + #[test] + fn destroy_datasets() { + let mut datasets = Datasets::new(); + assert_eq!(None, datasets.children(datasets.root_index()).next()); + + let id = Uuid::new_v4(); + let zpool = format!("oxp_{id}"); + + // Create a filesystem for a fake zpool + + let zpool_dataset = dataset::Name::new(&zpool).unwrap(); + let zpool = ZpoolName::from_str(&zpool).unwrap(); + datasets + .add_dataset( + DatasetInsert::WithoutParent(zpool.clone()), + HashMap::new(), + dataset::Type::Filesystem, + ) + .expect("Failed to add dataset"); + + let dataset_a = + dataset::Name::new(format!("{zpool}/dataset_a")).unwrap(); + datasets + .add_dataset( + DatasetInsert::WithParent(dataset_a.clone()), + HashMap::new(), + dataset::Type::Filesystem, + ) + .expect("Failed to add datasets"); + + let dataset_b = + dataset::Name::new(format!("{dataset_a}/dataset_b")).unwrap(); + datasets + .add_dataset( + DatasetInsert::WithParent(dataset_b.clone()), + HashMap::new(), + dataset::Type::Filesystem, + ) + .expect("Failed to add datasets"); + + // Cannot destroy dataset with children + + let recusive_dependents = true; + let recursive_children = false; + let force_unmount = false; + expect_err( + datasets.destroy( + &dataset_a, + recusive_dependents, + recursive_children, + force_unmount, + ), + &format!("has children"), + ) + .unwrap(); + + // The datasets still exist + datasets.get_dataset(&zpool_dataset).unwrap(); + datasets.get_dataset(&dataset_a).unwrap(); + datasets.get_dataset(&dataset_b).unwrap(); + + // Try with the recursive children option + + let recursive_children = true; + datasets + .destroy( + &dataset_a, + recusive_dependents, + recursive_children, + force_unmount, + ) + .unwrap(); + + // The destroyed datasets are gone + + datasets.get_dataset(&zpool_dataset).unwrap(); + assert!(datasets.get_dataset(&dataset_a).is_none()); + assert!(datasets.get_dataset(&dataset_b).is_none()); + } +} diff --git a/helios/tokamak/src/host/mod.rs b/helios/tokamak/src/host/mod.rs index 282021dc235..3232c22fd28 100644 --- a/helios/tokamak/src/host/mod.rs +++ b/helios/tokamak/src/host/mod.rs @@ -24,7 +24,7 @@ use std::sync::{Arc, Mutex}; mod datasets; mod zpools; -use datasets::Datasets; +use datasets::{DatasetInsert, Datasets}; use zpools::{FakeZpool, Zpools}; pub enum LinkType { @@ -193,7 +193,7 @@ impl FakeHostInner { self.datasets .add_dataset( - name.clone(), + DatasetInsert::WithParent(name.clone()), properties, dataset::Type::Filesystem, ) @@ -298,7 +298,7 @@ impl FakeHostInner { let mut inner = inner.lock().unwrap(); match inner.datasets.add_dataset( - name, + DatasetInsert::WithParent(name), properties, dataset::Type::Volume, ) { @@ -313,7 +313,7 @@ impl FakeHostInner { self.datasets .add_dataset( - name.clone(), + DatasetInsert::WithParent(name), properties, dataset::Type::Volume, ) @@ -328,7 +328,7 @@ impl FakeHostInner { name, } => { self.datasets - .destroy_dataset( + .destroy( &name, recursive_dependents, recursive_children, @@ -368,7 +368,7 @@ impl FakeHostInner { .datasets .lookup_by_index(target) .expect( - "We should have looked up the znode earlier...", + "We should have looked up the dataset earlier...", ); let (add_children, child_depth) = @@ -448,13 +448,6 @@ impl FakeHostInner { let mut output = String::new(); while let Some((target, depth)) = targets.pop_front() { - let node = self - .datasets - .lookup_by_index(target) - .expect( - "We should have looked up the znode earlier...", - ); - let (add_children, child_depth) = if let Some(depth) = depth { if depth > 0 { @@ -477,33 +470,25 @@ impl FakeHostInner { // display for it. continue; } + let dataset_name = self + .datasets + .lookup_by_index(target) + .expect("We should have looked up this node earlier...") + .dataset_name() + .expect("Cannot access name"); + + let dataset = self + .datasets + .get_dataset(&dataset_name) + .expect("Cannot access dataset"); for property in &properties { - match property { - dataset::Property::Name => { - output.push_str(&node.to_string()) - } - dataset::Property::Type => { - let node = self - .datasets - .lookup_by_index(target) - .ok_or_else(|| { - to_stderr("Node not found") - })?; - - output.push_str( - self.datasets - .type_str(node) - .map_err(to_stderr)?, - ) - } - // TODO: Fix this - _ => { - return Err(to_stderr(format!( - "Unknown property: {property}" - ))) - } - } + let value = dataset + .properties() + .get(*property) + .map_err(|err| to_stderr(err))?; + + output.push_str(&value); output.push_str("\t"); } output.push_str("\n"); @@ -545,14 +530,23 @@ impl FakeHostInner { let import = true; self.zpools - .insert( - &mut self.datasets, - pool.clone(), - vdev.clone(), - import, - ) + .insert(pool.clone(), vdev.clone(), import) .map_err(to_stderr)?; + let mut dataset_properties = HashMap::new(); + dataset_properties.insert( + dataset::Property::Mountpoint, + format!("/{pool}"), + ); + self.datasets + .add_dataset( + DatasetInsert::WithoutParent(pool), + dataset_properties, + dataset::Type::Filesystem, + ) + .expect( + "Failed to add dataset after creating zpool", + ); Ok(ProcessState::Completed(Output::success())) } Export { pool: name } => { @@ -845,3 +839,39 @@ pub enum AddrType { Static(IpNetwork), Addrconf, } + +#[cfg(test)] +mod test { + use super::*; + use helios_fusion::Host; + use omicron_test_utils::dev::test_setup_log; + use std::process::Command; + use uuid::Uuid; + + #[test] + fn create_zpool_creates_dataset_too() { + let logctx = test_setup_log("create_zpool_creates_dataset_too"); + let log = &logctx.log; + + let id = Uuid::new_v4(); + let zpool_name = format!("oxp_{id}"); + let vdev = "/mydevice"; + + let host = FakeHost::new(log.clone()); + host.add_devices(&vec![Utf8PathBuf::from(vdev)]); + + let output = host + .executor() + .execute(Command::new(helios_fusion::ZPOOL).args([ + "create", + &zpool_name, + vdev, + ])) + .expect("Failed to run zpool create command"); + assert!(output.status.success()); + + // TODO: Confirm dataset exists? + + logctx.cleanup_successful(); + } +} diff --git a/helios/tokamak/src/host/zpools.rs b/helios/tokamak/src/host/zpools.rs index abcd1f9112a..f849c9b7754 100644 --- a/helios/tokamak/src/host/zpools.rs +++ b/helios/tokamak/src/host/zpools.rs @@ -4,13 +4,11 @@ //! Emulates zpools -use crate::host::datasets::Datasets; -use crate::types::dataset; - use camino::Utf8PathBuf; use helios_fusion::zpool::{ZpoolHealth, ZpoolName}; use std::collections::HashMap; +#[derive(Debug, PartialEq, Eq)] pub(crate) struct FakeZpool { name: ZpoolName, vdev: Utf8PathBuf, @@ -62,7 +60,6 @@ impl Zpools { pub fn insert( &mut self, - datasets: &mut Datasets, name: ZpoolName, vdev: Utf8PathBuf, import: bool, @@ -75,19 +72,6 @@ impl Zpools { let pool = FakeZpool::new(name.clone(), vdev, import); self.zpools.insert(name.clone(), pool); - - let mut dataset_properties = HashMap::new(); - dataset_properties - .insert(dataset::Property::Mountpoint, format!("/{name}")); - datasets - .add_dataset( - dataset::Name::new(name.to_string()) - .expect("Zpool names should be valid dataset names"), - dataset_properties, - dataset::Type::Filesystem, - ) - .expect("Failed to add dataset after creating zpool"); - Ok(()) } } From b9df5bf8a470e4caae850a0e48d34bdcfba598da Mon Sep 17 00:00:00 2001 From: Sean Klein Date: Wed, 30 Aug 2023 11:29:38 -0700 Subject: [PATCH 16/18] Wrap up zpool creation test --- helios/tokamak/src/host/mod.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/helios/tokamak/src/host/mod.rs b/helios/tokamak/src/host/mod.rs index 3232c22fd28..860446200e3 100644 --- a/helios/tokamak/src/host/mod.rs +++ b/helios/tokamak/src/host/mod.rs @@ -860,6 +860,7 @@ mod test { let host = FakeHost::new(log.clone()); host.add_devices(&vec![Utf8PathBuf::from(vdev)]); + // Create the zpool let output = host .executor() .execute(Command::new(helios_fusion::ZPOOL).args([ @@ -870,7 +871,16 @@ mod test { .expect("Failed to run zpool create command"); assert!(output.status.success()); - // TODO: Confirm dataset exists? + // Observe the ZFS filesystem exists + let output = host + .executor() + .execute(Command::new(helios_fusion::ZFS).args([ + "list", + "-Hp", + &zpool_name, + ])) + .expect("Failed to run zfs list command"); + assert!(output.status.success()); logctx.cleanup_successful(); } From 86493eb682c985a180cd914df81a3998237b13f8 Mon Sep 17 00:00:00 2001 From: Sean Klein Date: Wed, 30 Aug 2023 15:30:30 -0700 Subject: [PATCH 17/18] Better (???) stdin handling --- helios/tokamak/src/executor.rs | 6 +- helios/tokamak/src/host/datasets.rs | 2 + helios/tokamak/src/host/mod.rs | 856 ++++++++++++------------ helios/tokamak/src/shared_byte_queue.rs | 133 +++- 4 files changed, 555 insertions(+), 442 deletions(-) diff --git a/helios/tokamak/src/executor.rs b/helios/tokamak/src/executor.rs index 043bfdb5003..6a5da5434cd 100644 --- a/helios/tokamak/src/executor.rs +++ b/helios/tokamak/src/executor.rs @@ -209,15 +209,15 @@ impl FakeChild { impl Child for FakeChild { fn take_stdin(&mut self) -> Option> { - Some(Box::new(self.stdin.clone())) + Some(Box::new(self.stdin.take_writer())) } fn take_stdout(&mut self) -> Option> { - Some(Box::new(self.stdout.clone())) + Some(Box::new(self.stdout.take_reader())) } fn take_stderr(&mut self) -> Option> { - Some(Box::new(self.stderr.clone())) + Some(Box::new(self.stderr.take_reader())) } fn id(&self) -> u32 { diff --git a/helios/tokamak/src/host/datasets.rs b/helios/tokamak/src/host/datasets.rs index 3ad7e105b8c..d7600c18ea8 100644 --- a/helios/tokamak/src/host/datasets.rs +++ b/helios/tokamak/src/host/datasets.rs @@ -96,6 +96,8 @@ impl FakeDataset { } let keylocation_property = properties.get(dataset::Property::Keylocation)?; + + // NOTE: This doesn't yet support reading from stdin, but it could. let Some(keylocation) = keylocation_property.strip_prefix("file://").map(|k| Utf8PathBuf::from(k)) else { return Err(format!("Cannot read from key location: {keylocation_property}")); }; diff --git a/helios/tokamak/src/host/mod.rs b/helios/tokamak/src/host/mod.rs index 860446200e3..5b603349c24 100644 --- a/helios/tokamak/src/host/mod.rs +++ b/helios/tokamak/src/host/mod.rs @@ -102,6 +102,43 @@ struct Zone { environment: ZoneEnvironment, } +// A context parameter which is passed between subcommands. +// +// Mostly used to simplify argument passing. +struct ProcessContext<'a> { + host: &'a Arc>, + child: &'a mut FakeChild, +} + +impl<'a> ProcessContext<'a> { + fn new( + host: &'a Arc>, + child: &'a mut FakeChild, + ) -> Self { + Self { host, child } + } + + // Spawns a thread which waits for stdin to be fully written, then executes + // a user-supplied function. + fn read_all_stdin_and_then< + F: FnOnce(Vec) -> Output + Send + 'static, + >( + &self, + f: F, + ) -> ProcessState { + let mut stdin = self.child.stdin().take_reader(); + ProcessState::Executing(std::thread::spawn(move || { + let mut buf = Vec::new(); + if let Err(err) = stdin.read_to_end(&mut buf) { + return Output::failure() + .set_stderr(format!("Cannot read from stdin: {err}")); + } + + f(buf) + })) + } +} + // A "process", which is either currently executing or completed. // // It's up to the caller to check-in on an "executing" process @@ -153,12 +190,11 @@ impl FakeHostInner { } } - fn run( + fn run_process( &mut self, - inner: &Arc>, - child: &mut FakeChild, + context: ProcessContext<'_>, ) -> Result { - let input = Input::from(child.command()); + let input = Input::from(context.child.command()); let cmd = crate::cli::Command::try_from(input).map_err(to_stderr)?; // TODO: Pick the right zone, act on it. @@ -172,475 +208,435 @@ impl FakeHostInner { use crate::cli::KnownCommand::*; match cmd.as_cmd() { - Zfs(zfs_cmd) => { - use crate::cli::zfs::Command::*; - if zone.is_some() { - return Err(to_stderr( - "Not Supported: 'zfs' commands within zone", - )); - } - match zfs_cmd { - CreateFilesystem { properties, name } => { - for property in properties.keys() { - if property.access() - == dataset::PropertyAccess::ReadOnly - { - return Err(to_stderr( - "Not supported: {property} is a read-only property", - )); - } - } + Zfs(cmd) => self.run_zfs(context, cmd, zone), + Zpool(cmd) => self.run_zpool(context, cmd, zone), + _ => todo!(), + } + } - self.datasets - .add_dataset( - DatasetInsert::WithParent(name.clone()), - properties, - dataset::Type::Filesystem, - ) - .map_err(to_stderr)?; + fn run_zfs( + &mut self, + context: ProcessContext<'_>, + cmd: crate::cli::zfs::Command, + zone: Option, + ) -> Result { + use crate::cli::zfs::Command::*; + if zone.is_some() { + return Err(to_stderr("Not Supported: 'zfs' commands within zone")); + } + match cmd { + CreateFilesystem { properties, name } => { + for property in properties.keys() { + if property.access() == dataset::PropertyAccess::ReadOnly { + return Err(to_stderr( + "Not supported: {property} is a read-only property", + )); + } + } - Ok(ProcessState::Completed( - Output::success().set_stdout(format!( - "Created {} successfully\n", - name.as_str() - )), - )) + self.datasets + .add_dataset( + DatasetInsert::WithParent(name.clone()), + properties, + dataset::Type::Filesystem, + ) + .map_err(to_stderr)?; + + Ok(ProcessState::Completed(Output::success().set_stdout( + format!("Created {} successfully\n", name.as_str()), + ))) + } + CreateVolume { mut properties, sparse, blocksize, size, name } => { + for property in properties.keys() { + if property.access() == dataset::PropertyAccess::ReadOnly { + return Err(to_stderr( + "Not supported: {property} is a read-only property", + )); } - CreateVolume { - mut properties, - sparse, - blocksize, - size, - name, - } => { - for property in properties.keys() { - if property.access() - == dataset::PropertyAccess::ReadOnly - { - return Err(to_stderr( - "Not supported: {property} is a read-only property", - )); - } - } + } - let blocksize = blocksize.unwrap_or(8192); - if sparse { - properties.insert( - dataset::Property::Reservation, - "0".to_string(), - ); - } else { - // NOTE: This isn't how much metadata is used, but it's - // a number we can use that represents "this is larger than - // the usable size of the volume". - // - // See: - // - // $ zfs get -Hp used,volsize,refreservation - // - // For any non-sparse zpool. - let reserved_size = size + (8 << 20); - properties.insert( - dataset::Property::Reservation, - reserved_size.to_string(), - ); + let blocksize = blocksize.unwrap_or(8192); + if sparse { + properties.insert( + dataset::Property::Reservation, + "0".to_string(), + ); + } else { + // NOTE: This isn't how much metadata is used, but it's + // a number we can use that represents "this is larger than + // the usable size of the volume". + // + // See: + // + // $ zfs get -Hp used,volsize,refreservation + // + // For any non-sparse zpool. + let reserved_size = size + (8 << 20); + properties.insert( + dataset::Property::Reservation, + reserved_size.to_string(), + ); + } + properties.insert( + dataset::Property::Volblocksize, + blocksize.to_string(), + ); + properties.insert(dataset::Property::Volsize, size.to_string()); + + let mut keylocation = None; + let mut keysize = 0; + + for (k, v) in &properties { + match k { + dataset::Property::Keylocation => { + keylocation = Some(v.to_string()) } - properties.insert( - dataset::Property::Volblocksize, - blocksize.to_string(), - ); - properties.insert( - dataset::Property::Volsize, - size.to_string(), - ); - - let mut keylocation = None; - let mut keysize = 0; - - for (k, v) in &properties { - match k { - dataset::Property::Keylocation => { - keylocation = Some(v.as_str()) - } - dataset::Property::Encryption => { - match v.as_str() { - "aes-256-gcm" => keysize = 32, - _ => { - return Err(Output::failure() - .set_stderr( - "Unsupported encryption", - )) - } - } - } - _ => (), + dataset::Property::Encryption => match v.as_str() { + "aes-256-gcm" => keysize = 32, + _ => { + return Err(Output::failure() + .set_stderr("Unsupported encryption")) } - } + }, + _ => (), + } + } - if keylocation == Some("file:///dev/stdin") - && keysize > 0 - { - let inner = inner.clone(); - let mut stdin = child.stdin().clone(); - return Ok(ProcessState::Executing( - std::thread::spawn(move || { - let mut secret = - Vec::with_capacity(keysize); - if let Err(err) = - stdin.read_exact(&mut secret) - { - return Output::failure().set_stderr( - format!( - "Cannot read from stdin: {err}" - ), - ); - } - - let mut inner = inner.lock().unwrap(); - match inner.datasets.add_dataset( - DatasetInsert::WithParent(name), - properties, - dataset::Type::Volume, - ) { - Ok(()) => Output::success(), - Err(err) => { - Output::failure().set_stderr(err) - } - } - }), + let inner = context.host.clone(); + let add_dataset = move || { + let mut inner = inner.lock().unwrap(); + match inner.datasets.add_dataset( + DatasetInsert::WithParent(name), + properties, + dataset::Type::Volume, + ) { + Ok(()) => Output::success(), + Err(err) => Output::failure().set_stderr(err), + } + }; + + if keylocation.as_deref() == Some("file:///dev/stdin") { + return Ok(context.read_all_stdin_and_then(move |input| { + if input.len() != keysize { + return Output::failure().set_stderr(format!( + "Bad key length: {}", + input.len() )); } - - self.datasets - .add_dataset( - DatasetInsert::WithParent(name), - properties, - dataset::Type::Volume, - ) - .map_err(to_stderr)?; - - Ok(ProcessState::Completed(Output::success())) - } - Destroy { + add_dataset() + })); + } + Ok(ProcessState::Completed(add_dataset())) + } + Destroy { + recursive_dependents, + recursive_children, + force_unmount, + name, + } => { + self.datasets + .destroy( + &name, recursive_dependents, recursive_children, force_unmount, - name, - } => { - self.datasets - .destroy( - &name, - recursive_dependents, - recursive_children, - force_unmount, - ) - .map_err(to_stderr)?; + ) + .map_err(to_stderr)?; - Ok(ProcessState::Completed( - Output::success() - .set_stdout(format!("{} destroyed", name)), - )) + Ok(ProcessState::Completed( + Output::success().set_stdout(format!("{} destroyed", name)), + )) + } + Get { recursive, depth, fields, properties, datasets } => { + let mut targets = if let Some(datasets) = datasets { + let mut targets = VecDeque::new(); + + let depth = if recursive { depth } else { Some(0) }; + for dataset in datasets { + let zix = self + .datasets + .index_of(dataset.as_str()) + .map_err(to_stderr)?; + targets.push_back((zix, depth)); } - Get { recursive, depth, fields, properties, datasets } => { - let mut targets = if let Some(datasets) = datasets { - let mut targets = VecDeque::new(); - - let depth = if recursive { depth } else { Some(0) }; - for dataset in datasets { - let zix = self - .datasets - .index_of(dataset.as_str()) - .map_err(to_stderr)?; - targets.push_back((zix, depth)); - } - targets + targets + } else { + VecDeque::from([( + self.datasets.root_index(), + depth.map(|d| d + 1), + )]) + }; + + let mut output = String::new(); + + while let Some((target, depth)) = targets.pop_front() { + let node = self.datasets.lookup_by_index(target).expect( + "We should have looked up the dataset earlier...", + ); + + let (add_children, child_depth) = if let Some(depth) = depth + { + if depth > 0 { + (true, Some(depth - 1)) } else { - VecDeque::from([( - self.datasets.root_index(), - depth.map(|d| d + 1), - )]) - }; - - let mut output = String::new(); - - while let Some((target, depth)) = targets.pop_front() { - let node = self - .datasets - .lookup_by_index(target) - .expect( - "We should have looked up the dataset earlier...", - ); - - let (add_children, child_depth) = - if let Some(depth) = depth { - if depth > 0 { - (true, Some(depth - 1)) - } else { - (false, None) - } - } else { - (true, None) - }; - - if add_children { - for child in self.datasets.children(target) { - targets.push_front((child, child_depth)); - } - } + (false, None) + } + } else { + (true, None) + }; - if target == self.datasets.root_index() { - // Skip the root node, as there is nothing to - // display for it. - continue; - } + if add_children { + for child in self.datasets.children(target) { + targets.push_front((child, child_depth)); + } + } + + if target == self.datasets.root_index() { + // Skip the root node, as there is nothing to + // display for it. + continue; + } - for property in &properties { - for field in &fields { - match field.as_str() { - "name" => { - output.push_str(&node.to_string()) - } - "property" => output - .push_str(&property.to_string()), - "value" => { - // TODO: Look up, across whatever - // the node type is. - todo!(); - } - f => { - return Err(to_stderr(format!( - "Unknown field: {f}" - ))) - } - } + for property in &properties { + for field in &fields { + match field.as_str() { + "name" => output.push_str(&node.to_string()), + "property" => { + output.push_str(&property.to_string()) + } + "value" => { + // TODO: Look up, across whatever + // the node type is. + todo!(); + } + f => { + return Err(to_stderr(format!( + "Unknown field: {f}" + ))) } } } - todo!(); } - List { recursive, depth, properties, datasets } => { - let mut targets = if let Some(datasets) = datasets { - let mut targets = VecDeque::new(); - - // If we explicitly request datasets, only return - // information for the exact matches, unless a - // recursive walk was requested. - let depth = if recursive { depth } else { Some(0) }; - - for dataset in datasets { - let zix = self - .datasets - .index_of(dataset.as_str()) - .map_err(to_stderr)?; - targets.push_back((zix, depth)); - } + } + todo!(); + } + List { recursive, depth, properties, datasets } => { + let mut targets = if let Some(datasets) = datasets { + let mut targets = VecDeque::new(); + + // If we explicitly request datasets, only return + // information for the exact matches, unless a + // recursive walk was requested. + let depth = if recursive { depth } else { Some(0) }; + + for dataset in datasets { + let zix = self + .datasets + .index_of(dataset.as_str()) + .map_err(to_stderr)?; + targets.push_back((zix, depth)); + } - targets + targets + } else { + // Bump whatever the depth was up by one, since we + // don't display anything for the root node. + VecDeque::from([( + self.datasets.root_index(), + depth.map(|d| d + 1), + )]) + }; + + let mut output = String::new(); + + while let Some((target, depth)) = targets.pop_front() { + let (add_children, child_depth) = if let Some(depth) = depth + { + if depth > 0 { + (true, Some(depth - 1)) } else { - // Bump whatever the depth was up by one, since we - // don't display anything for the root node. - VecDeque::from([( - self.datasets.root_index(), - depth.map(|d| d + 1), - )]) - }; - - let mut output = String::new(); - - while let Some((target, depth)) = targets.pop_front() { - let (add_children, child_depth) = - if let Some(depth) = depth { - if depth > 0 { - (true, Some(depth - 1)) - } else { - (false, None) - } - } else { - (true, None) - }; - - if add_children { - for child in self.datasets.children(target) { - targets.push_front((child, child_depth)); - } - } - - if target == self.datasets.root_index() { - // Skip the root node, as there is nothing to - // display for it. - continue; - } - let dataset_name = self - .datasets - .lookup_by_index(target) - .expect("We should have looked up this node earlier...") - .dataset_name() - .expect("Cannot access name"); - - let dataset = self - .datasets - .get_dataset(&dataset_name) - .expect("Cannot access dataset"); - - for property in &properties { - let value = dataset - .properties() - .get(*property) - .map_err(|err| to_stderr(err))?; - - output.push_str(&value); - output.push_str("\t"); - } - output.push_str("\n"); + (false, None) } + } else { + (true, None) + }; - Ok(ProcessState::Completed( - Output::success().set_stdout(output), - )) + if add_children { + for child in self.datasets.children(target) { + targets.push_front((child, child_depth)); + } } - Mount { load_keys, filesystem } => { - self.datasets - .mount(load_keys, &filesystem) - .map_err(to_stderr)?; - Ok(ProcessState::Completed( - Output::success() - .set_stdout(format!("{} mounted", filesystem)), - )) + + if target == self.datasets.root_index() { + // Skip the root node, as there is nothing to + // display for it. + continue; } - Set { properties, name } => { - // TODO - todo!("Calling zfs set with properties: {properties:?} on '{name}', not implemented"); + let dataset_name = self + .datasets + .lookup_by_index(target) + .expect("We should have looked up this node earlier...") + .dataset_name() + .expect("Cannot access name"); + + let dataset = self + .datasets + .get_dataset(&dataset_name) + .expect("Cannot access dataset"); + + for property in &properties { + let value = dataset + .properties() + .get(*property) + .map_err(|err| to_stderr(err))?; + + output.push_str(&value); + output.push_str("\t"); } + output.push_str("\n"); } + + Ok(ProcessState::Completed( + Output::success().set_stdout(output), + )) } - Zpool(zpool_cmd) => { - use crate::cli::zpool::Command::*; - if zone.is_some() { - return Err(to_stderr( - "Not Supported: 'zpool' commands within zone", - )); - } - match zpool_cmd { - Create { pool, vdev } => { - if !self.vdevs.contains(&vdev) { - return Err(to_stderr(format!( - "Cannot create zpool: device '{vdev}' does not exist" - ))); - } + Mount { load_keys, filesystem } => { + self.datasets + .mount(load_keys, &filesystem) + .map_err(to_stderr)?; + Ok(ProcessState::Completed( + Output::success() + .set_stdout(format!("{} mounted", filesystem)), + )) + } + Set { properties, name } => { + // TODO + todo!("Calling zfs set with properties: {properties:?} on '{name}', not implemented"); + } + } + } - let import = true; - self.zpools - .insert(pool.clone(), vdev.clone(), import) - .map_err(to_stderr)?; + fn run_zpool( + &mut self, + _context: ProcessContext<'_>, + cmd: crate::cli::zpool::Command, + zone: Option, + ) -> Result { + use crate::cli::zpool::Command::*; + if zone.is_some() { + return Err(to_stderr( + "Not Supported: 'zpool' commands within zone", + )); + } + match cmd { + Create { pool, vdev } => { + if !self.vdevs.contains(&vdev) { + return Err(to_stderr(format!( + "Cannot create zpool: device '{vdev}' does not exist" + ))); + } - let mut dataset_properties = HashMap::new(); - dataset_properties.insert( - dataset::Property::Mountpoint, - format!("/{pool}"), - ); - self.datasets - .add_dataset( - DatasetInsert::WithoutParent(pool), - dataset_properties, - dataset::Type::Filesystem, - ) - .expect( - "Failed to add dataset after creating zpool", - ); - Ok(ProcessState::Completed(Output::success())) + let import = true; + self.zpools + .insert(pool.clone(), vdev.clone(), import) + .map_err(to_stderr)?; + + let mut dataset_properties = HashMap::new(); + dataset_properties + .insert(dataset::Property::Mountpoint, format!("/{pool}")); + self.datasets + .add_dataset( + DatasetInsert::WithoutParent(pool), + dataset_properties, + dataset::Type::Filesystem, + ) + .expect("Failed to add dataset after creating zpool"); + Ok(ProcessState::Completed(Output::success())) + } + Export { pool: name } => { + let Some(mut pool) = self.zpools.get_mut(&name) else { + return Err(to_stderr(format!("pool does not exist"))); + }; + + if !pool.imported { + return Err(to_stderr(format!( + "cannot export pool which is already exported" + ))); + } + pool.imported = false; + Ok(ProcessState::Completed(Output::success())) + } + Import { force: _, pool: name } => { + let Some(mut pool) = self.zpools.get_mut(&name) else { + return Err(to_stderr(format!("pool does not exist"))); + }; + + if pool.imported { + return Err(to_stderr(format!( + "a pool with that name is already created" + ))); + } + pool.imported = true; + Ok(ProcessState::Completed(Output::success())) + } + List { properties, pools } => { + let mut output = String::new(); + let mut display = |name: &ZpoolName, + pool: &FakeZpool, + properties: &Vec| + -> Result<(), _> { + for property in properties { + match property.as_str() { + "name" => output.push_str(&format!("{}", name)), + "health" => { + output.push_str(&pool.health.to_string()) + } + _ => { + return Err(to_stderr(format!( + "Unknown property: {property}" + ))) + } + } + output.push_str("\t"); } - Export { pool: name } => { - let Some(mut pool) = self.zpools.get_mut(&name) else { - return Err(to_stderr(format!("pool does not exist"))); - }; + output.push_str("\n"); + Ok(()) + }; + + if let Some(pools) = pools { + for name in &pools { + let pool = self.zpools.get(name).ok_or_else(|| { + to_stderr(format!("{} does not exist", name)) + })?; if !pool.imported { return Err(to_stderr(format!( - "cannot export pool which is already exported" + "{} not imported", + name ))); } - pool.imported = false; - Ok(ProcessState::Completed(Output::success())) - } - Import { force: _, pool: name } => { - let Some(mut pool) = self.zpools.get_mut(&name) else { - return Err(to_stderr(format!("pool does not exist"))); - }; - if pool.imported { - return Err(to_stderr(format!( - "a pool with that name is already created" - ))); - } - pool.imported = true; - Ok(ProcessState::Completed(Output::success())) + display(&name, &pool, &properties)?; } - List { properties, pools } => { - let mut output = String::new(); - let mut display = - |name: &ZpoolName, - pool: &FakeZpool, - properties: &Vec| - -> Result<(), _> { - for property in properties { - match property.as_str() { - "name" => output - .push_str(&format!("{}", name)), - "health" => output - .push_str(&pool.health.to_string()), - _ => { - return Err(to_stderr(format!( - "Unknown property: {property}" - ))) - } - } - output.push_str("\t"); - } - output.push_str("\n"); - Ok(()) - }; - - if let Some(pools) = pools { - for name in &pools { - let pool = - self.zpools.get(name).ok_or_else(|| { - to_stderr(format!( - "{} does not exist", - name - )) - })?; - - if !pool.imported { - return Err(to_stderr(format!( - "{} not imported", - name - ))); - } - - display(&name, &pool, &properties)?; - } - } else { - for (name, pool) in self.zpools.all() { - if pool.imported { - display(&name, &pool, &properties)?; - } - } + } else { + for (name, pool) in self.zpools.all() { + if pool.imported { + display(&name, &pool, &properties)?; } - - Ok(ProcessState::Completed( - Output::success().set_stdout(output), - )) - } - Set { property, value, pool: name } => { - let Some(pool) = self.zpools.get_mut(&name) else { - return Err(to_stderr(format!("{} does not exist", name))); - }; - pool.properties.insert(property, value); - Ok(ProcessState::Completed(Output::success())) } } + + Ok(ProcessState::Completed( + Output::success().set_stdout(output), + )) + } + Set { property, value, pool: name } => { + let Some(pool) = self.zpools.get_mut(&name) else { + return Err(to_stderr(format!("{} does not exist", name))); + }; + pool.properties.insert(property, value); + Ok(ProcessState::Completed(Output::success())) } - _ => todo!(), } } @@ -657,7 +653,7 @@ impl FakeHostInner { Input::from(child.command()), ); - let process = match me.run(inner, child) { + let process = match me.run_process(ProcessContext::new(inner, child)) { Ok(process) => process, Err(err) => ProcessState::Completed(err), }; diff --git a/helios/tokamak/src/shared_byte_queue.rs b/helios/tokamak/src/shared_byte_queue.rs index 52e91c266bc..68bd8bbc53d 100644 --- a/helios/tokamak/src/shared_byte_queue.rs +++ b/helios/tokamak/src/shared_byte_queue.rs @@ -3,26 +3,78 @@ // file, You can obtain one at https://mozilla.org/MPL/2.0/. use std::collections::VecDeque; -use std::sync::{Arc, Mutex}; +use std::sync::{Arc, Condvar, Mutex}; + +struct ByteQueue { + bytes: VecDeque, + + reader_dropped: bool, + writer_dropped: bool, +} + +impl ByteQueue { + fn new() -> Self { + Self { + bytes: VecDeque::new(), + reader_dropped: false, + writer_dropped: false, + } + } +} + +struct SharedByteQueueInner { + byte_queue: Mutex, + + // Allows callers to block until data can be read. + cvar: Condvar, +} + +impl SharedByteQueueInner { + fn new() -> Self { + Self { byte_queue: Mutex::new(ByteQueue::new()), cvar: Condvar::new() } + } +} /// A queue of bytes that can selectively act as a reader or writer, /// which can also be cloned. /// /// This is primarily used to emulate stdin / stdout / stderr. #[derive(Clone)] -pub struct SharedByteQueue { - buf: Arc>>, -} +pub struct SharedByteQueue(Arc); impl SharedByteQueue { pub fn new() -> Self { - Self { buf: Arc::new(Mutex::new(VecDeque::new())) } + Self(Arc::new(SharedByteQueueInner::new())) + } + + pub fn take_writer(&self) -> SharedByteQueueWriter { + SharedByteQueueWriter(self.0.clone()) + } + + pub fn take_reader(&self) -> SharedByteQueueReader { + SharedByteQueueReader(self.0.clone()) + } +} + +pub struct SharedByteQueueWriter(Arc); + +impl Drop for SharedByteQueueWriter { + fn drop(&mut self) { + let mut bq = self.0.byte_queue.lock().unwrap(); + bq.writer_dropped = true; + self.0.cvar.notify_all(); } } -impl std::io::Write for SharedByteQueue { +impl std::io::Write for SharedByteQueueWriter { fn write(&mut self, buf: &[u8]) -> std::io::Result { - self.buf.lock().unwrap().write(buf) + let mut bq = self.0.byte_queue.lock().unwrap(); + if bq.reader_dropped { + return Ok(0); + } + let n = bq.bytes.write(buf)?; + self.0.cvar.notify_all(); + Ok(n) } fn flush(&mut self) -> std::io::Result<()> { @@ -30,8 +82,71 @@ impl std::io::Write for SharedByteQueue { } } -impl std::io::Read for SharedByteQueue { +pub struct SharedByteQueueReader(Arc); + +impl std::io::Read for SharedByteQueueReader { fn read(&mut self, buf: &mut [u8]) -> std::io::Result { - self.buf.lock().unwrap().read(buf) + let mut bq = self.0.byte_queue.lock().unwrap(); + + loop { + let n = bq.bytes.read(buf)?; + if n > 0 { + return Ok(n); + } + if bq.writer_dropped { + return Ok(0); + } + + bq = self + .0 + .cvar + .wait_while(bq, |bq| !bq.writer_dropped && bq.bytes.is_empty()) + .unwrap(); + } + } +} + +impl Drop for SharedByteQueueReader { + fn drop(&mut self) { + let mut bq = self.0.byte_queue.lock().unwrap(); + bq.reader_dropped = true; + self.0.cvar.notify_all(); + } +} + +#[cfg(test)] +mod test { + use super::*; + use std::io::{Read, Write}; + + #[test] + fn blocking_reader() { + let bq = SharedByteQueue::new(); + + let mut reader = bq.take_reader(); + let mut writer = bq.take_writer(); + + // This represents our "Process", which could be reading a collection + // of bytes until stdin completes. + let handle = std::thread::spawn(move || { + let mut buf = Vec::new(); + reader.read_to_end(&mut buf).expect("Failed to read"); + buf + }); + + // This represents someone interacting with the process, dumping to + // stdin. + let input1 = b"What you're referring to as bytes,\n"; + let input2 = b"is in fact, bytes/Vec"; + let input = [input1.as_slice(), input2.as_slice()].concat(); + + // Write all the bytes, observe that the reader doesn't exit early. + writer.write_all(input1).expect("Failed to write"); + std::thread::sleep(std::time::Duration::from_millis(10)); + writer.write_all(input2).expect("Failed to write"); + drop(writer); + + let output = handle.join().unwrap(); + assert_eq!(input, output.as_slice()); } } From 15eceb89ede231632d37c83163d56555bdc707a0 Mon Sep 17 00:00:00 2001 From: Sean Klein Date: Thu, 31 Aug 2023 17:18:26 -0700 Subject: [PATCH 18/18] Fix zfs get, add some tests --- helios/tokamak/src/bin/tokomaksh.rs | 59 +++++++++--- helios/tokamak/src/cli/zfs.rs | 4 + helios/tokamak/src/host/mod.rs | 143 ++++++++++++++++++++++++++-- 3 files changed, 187 insertions(+), 19 deletions(-) diff --git a/helios/tokamak/src/bin/tokomaksh.rs b/helios/tokamak/src/bin/tokomaksh.rs index aa7a0bd022c..3560284e4af 100644 --- a/helios/tokamak/src/bin/tokomaksh.rs +++ b/helios/tokamak/src/bin/tokomaksh.rs @@ -8,12 +8,15 @@ use anyhow::anyhow; use camino::Utf8PathBuf; use clap::{Parser, ValueEnum}; use helios_fusion::Host; +use helios_fusion::Input; +use helios_tokamak::FakeHost; use slog::Drain; use slog::Level; use slog::LevelFilter; use slog::Logger; use slog_term::FullFormat; use slog_term::TermDecorator; +use std::process::Command; #[derive(Clone, Debug, ValueEnum)] #[clap(rename_all = "kebab_case")] @@ -22,6 +25,8 @@ enum MachineMode { Empty, /// The machine is pre-populated with some disk devices Disks, + /// The machine is populated with a variety of running interfaces. + Populate, } fn parse_log_level(s: &str) -> anyhow::Result { @@ -39,6 +44,14 @@ struct Args { log_level: Level, } +fn run_command_during_setup(host: &FakeHost, command: &mut Command) { + println!("[POPULATING] $ {}", Input::from(&*command)); + let output = host.executor().execute(command).expect("Failed during setup"); + + print!("{}", String::from_utf8_lossy(&output.stdout)); + eprint!("{}", String::from_utf8_lossy(&output.stderr)); +} + #[tokio::main] async fn main() -> Result<(), anyhow::Error> { let args = Args::parse(); @@ -55,23 +68,43 @@ async fn main() -> Result<(), anyhow::Error> { rustyline::history::MemHistory::new(), )?; - let host = helios_tokamak::FakeHost::new(log); + let host = FakeHost::new(log); - match args.machine_mode { - MachineMode::Disks => { - let vdevs = vec![ - Utf8PathBuf::from("/unreal/block/a"), - Utf8PathBuf::from("/unreal/block/b"), - Utf8PathBuf::from("/unreal/block/c"), - ]; - - for vdev in &vdevs { - println!("Adding virtual device: {vdev}"); - } + let add_vdevs = || { + let vdevs = vec![ + Utf8PathBuf::from("/unreal/block/a"), + Utf8PathBuf::from("/unreal/block/b"), + Utf8PathBuf::from("/unreal/block/c"), + ]; - host.add_devices(&vdevs); + for vdev in &vdevs { + println!("[POPULATING] Adding virtual device: {vdev}"); } + + host.add_devices(&vdevs); + }; + + match args.machine_mode { MachineMode::Empty => (), + MachineMode::Disks => { + add_vdevs(); + } + MachineMode::Populate => { + add_vdevs(); + run_command_during_setup( + &host, + Command::new(helios_fusion::ZPOOL).args([ + "create", + "oxp_2f11d4e8-fa31-4230-a781-e800a51404e7", + "/unreal/block/a", + ]), + ); + run_command_during_setup( + &host, + Command::new(helios_fusion::ZFS) + .args(["create", "oxp_2f11d4e8-fa31-4230-a781-e800a51404e7/nested_filesystem"]) + ); + } } const DEFAULT: &str = "🍩 "; diff --git a/helios/tokamak/src/cli/zfs.rs b/helios/tokamak/src/cli/zfs.rs index 8b27dc3430c..6de77294c6c 100644 --- a/helios/tokamak/src/cli/zfs.rs +++ b/helios/tokamak/src/cli/zfs.rs @@ -345,6 +345,10 @@ impl TryFrom for Command { return Err("You should run 'zfs list' commands with the '-Hp' flags enabled".to_string()); } + if properties.is_empty() { + properties = vec![dataset::Property::Name]; + } + Ok(Command::List { recursive, depth, properties, datasets }) } "mount" => { diff --git a/helios/tokamak/src/host/mod.rs b/helios/tokamak/src/host/mod.rs index 5b603349c24..c8e247f960a 100644 --- a/helios/tokamak/src/host/mod.rs +++ b/helios/tokamak/src/host/mod.rs @@ -302,6 +302,10 @@ impl FakeHostInner { } } + // Create a closure to add the dataset. + // + // We either call this immediately, or in a background thread, + // depending on whether or not we need to read a key from stdin. let inner = context.host.clone(); let add_dataset = move || { let mut inner = inner.lock().unwrap(); @@ -400,25 +404,41 @@ impl FakeHostInner { for property in &properties { for field in &fields { match field.as_str() { - "name" => output.push_str(&node.to_string()), + "name" => { + output.push_str(&node.to_string()); + } "property" => { - output.push_str(&property.to_string()) + output.push_str(&property.to_string()); } "value" => { - // TODO: Look up, across whatever - // the node type is. - todo!(); + let name = node.dataset_name().expect( + "Non-root node should have name", + ); + let dataset = self + .datasets + .get_dataset(&name) + .expect("Dataset should exist"); + let value = dataset + .properties() + .get(*property) + .map_err(|e| to_stderr(e))?; + output.push_str(&value); } + "source" => output.push_str("???"), f => { return Err(to_stderr(format!( "Unknown field: {f}" ))) } } + output.push_str("\t"); } + output.push_str("\n"); } } - todo!(); + Ok(ProcessState::Completed( + Output::success().set_stdout(output), + )) } List { recursive, depth, properties, datasets } => { let mut targets = if let Some(datasets) = datasets { @@ -880,4 +900,115 @@ mod test { logctx.cleanup_successful(); } + + #[test] + fn zfs_list_and_get() { + let logctx = test_setup_log("zfs_list"); + let log = &logctx.log; + + let id = Uuid::new_v4(); + let vdev = "/mydevice"; + + let host = FakeHost::new(log.clone()); + host.add_devices(&vec![Utf8PathBuf::from(vdev)]); + + let zpool_name = format!("oxp_{id}"); + let dataset1_name = format!("{zpool_name}/dataset_1"); + let dataset2_name = format!("{dataset1_name}/dataset_2"); + + // Create the zpool and some datasets within + let output = host + .executor() + .execute(Command::new(helios_fusion::ZPOOL).args([ + "create", + &zpool_name, + vdev, + ])) + .expect("Failed to run zpool create command"); + assert!(output.status.success()); + let output = host + .executor() + .execute( + Command::new(helios_fusion::ZFS) + .args(["create", &dataset1_name]), + ) + .expect("Failed to run zfs create command"); + assert!(output.status.success()); + let output = host + .executor() + .execute( + Command::new(helios_fusion::ZFS) + .args(["create", &dataset2_name]), + ) + .expect("Failed to run zfs create command"); + assert!(output.status.success()); + + // ZFS List: Lists all datasets + + let output = host + .executor() + .execute(Command::new(helios_fusion::ZFS).args(["list", "-Hp"])) + .expect("Failed to run zfs list command"); + assert!(output.status.success()); + assert_eq!( + String::from_utf8(output.stdout).unwrap(), + format!("{zpool_name}\t\n{dataset1_name}\t\n{dataset2_name}\t\n") + ); + + // We can ask for properties explicitly. + let output = host + .executor() + .execute(Command::new(helios_fusion::ZFS).args([ + "list", + "-Hpo", + "name,type", + ])) + .expect("Failed to run zfs list command"); + assert!(output.status.success()); + assert_eq!( + String::from_utf8(output.stdout).unwrap(), + format!("{zpool_name}\tfilesystem\t\n{dataset1_name}\tfilesystem\t\n{dataset2_name}\tfilesystem\t\n") + ); + + // "zfs get" also works + let output = host + .executor() + .execute(Command::new(helios_fusion::ZFS).args([ + "get", + "-Hpo", + "value", + "mountpoint", + &zpool_name, + ])) + .expect("Failed to run zfs get command"); + assert!(output.status.success()); + assert_eq!( + String::from_utf8(output.stdout).unwrap(), + format!("/{zpool_name}\t\n") + ); + + // It also allows recursive traversal + // + // This only sees output from the zpool dataset, as well as "dataset 1", but not "dataset + // 2" due to the depth restriction. + let output = host + .executor() + .execute(Command::new(helios_fusion::ZFS).args([ + "get", + "-d", + "1", + "-rHpo", + "value", + "mountpoint", + &zpool_name, + ])) + .expect("Failed to run zfs get command"); + assert!(output.status.success()); + assert_eq!( + String::from_utf8(output.stdout).unwrap(), + format!("/{zpool_name}\t\nnone\t\n") + ); + + logctx.cleanup_successful(); + } }