#![deny(missing_docs)]
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::string::ToString;
use thiserror::Error;
#[derive(Error, Debug)]
#[error("{0}")]
pub struct CommandOutputError(String);
trait OutputExt {
fn read_stdout(&self) -> Result<String, CommandOutputError>;
}
impl OutputExt for std::process::Output {
fn read_stdout(&self) -> Result<String, CommandOutputError> {
let stdout = String::from_utf8_lossy(&self.stdout).trim().to_string();
let stderr = String::from_utf8_lossy(&self.stderr).trim().to_string();
if !self.status.success() {
let exit_code = self
.status
.code()
.map(|code| format!("{}", code))
.unwrap_or_else(|| "<No exit code>".to_string());
return Err(CommandOutputError(format!(
"exit code {}\nstdout:\n{}\nstderr:\n{}",
exit_code, stdout, stderr
)));
}
Ok(stdout)
}
}
#[derive(Debug, PartialEq)]
pub enum SmfState {
Disabled,
Degraded,
Maintenance,
Offline,
Online,
Legacy,
Uninitialized,
}
impl SmfState {
fn from_str(val: &str) -> Option<SmfState> {
match val {
"ON" => Some(SmfState::Online),
"OFF" => Some(SmfState::Offline),
"DGD" => Some(SmfState::Degraded),
"DIS" => Some(SmfState::Disabled),
"MNT" => Some(SmfState::Maintenance),
"UN" => Some(SmfState::Uninitialized),
"LRC" => Some(SmfState::Legacy),
_ => None,
}
}
}
impl ToString for SmfState {
fn to_string(&self) -> String {
match self {
SmfState::Disabled => "DIS",
SmfState::Degraded => "DGD",
SmfState::Maintenance => "MNT",
SmfState::Offline => "OFF",
SmfState::Online => "ON",
SmfState::Legacy => "LRC",
SmfState::Uninitialized => "UN",
}
.to_string()
}
}
#[derive(Error, Debug)]
pub enum QueryError {
#[error("Failed to parse output: {0}")]
Parse(String),
#[error("Failed to execute command: {0}")]
Command(std::io::Error),
#[error("Failed to parse command output: {0}")]
CommandOutput(#[from] CommandOutputError),
}
#[derive(Debug, PartialEq)]
pub struct SvcStatus {
pub fmri: String,
pub contract_id: Option<usize>,
pub instance_name: String,
pub next_state: Option<SmfState>,
pub scope_name: String,
pub service_name: String,
pub state: SmfState,
pub service_time: String,
pub zone: String,
pub description: Option<String>,
}
impl FromStr for SvcStatus {
type Err = QueryError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut iter = s.split_whitespace();
let status = || -> Result<SvcStatus, String> {
let fmri = iter.next().ok_or("Missing FMRI")?.to_string();
let contract_id = iter
.next()
.map::<Result<_, String>, _>(|s| match s {
"-" => Ok(None),
_ => Ok(Some(s.parse::<usize>().map_err(|e| e.to_string())?)),
})
.ok_or("Missing ContractID")??;
let instance_name = iter.next().ok_or("Missing Instance Name")?.to_string();
let next_state = SmfState::from_str(iter.next().ok_or("Missing Instance Name")?);
let scope_name = iter.next().ok_or("Missing Scope Name")?.to_string();
let service_name = iter.next().ok_or("Missing Service Name")?.to_string();
let state =
SmfState::from_str(iter.next().ok_or("Missing State")?).ok_or("Missing State")?;
let service_time = iter.next().ok_or("Missing Service Time")?.to_string();
let zone = iter.next().ok_or("Missing Zone")?.to_string();
let description = iter
.map(|s| s.to_owned())
.collect::<Vec<String>>()
.join(" ");
let description = {
if description == "-" || description.is_empty() {
None
} else {
Some(description)
}
};
Ok(SvcStatus {
fmri,
contract_id,
instance_name,
next_state,
scope_name,
service_name,
state,
service_time,
zone,
description,
})
}()
.map_err(QueryError::Parse)?;
Ok(status)
}
}
#[derive(Copy, Clone)]
enum SvcColumn {
Fmri,
ContractId,
InstanceName,
NextState,
ScopeName,
ServiceName,
State,
ServiceTime,
Zone,
Description,
}
impl SvcColumn {
fn to_str(&self) -> &str {
match self {
SvcColumn::Fmri => "FMRI",
SvcColumn::ContractId => "CTID",
SvcColumn::InstanceName => "INST",
SvcColumn::NextState => "NSTA",
SvcColumn::ScopeName => "SCOPE",
SvcColumn::ServiceName => "SVC",
SvcColumn::State => "STA",
SvcColumn::ServiceTime => "STIME",
SvcColumn::Zone => "ZONE",
SvcColumn::Description => "DESC",
}
}
}
pub enum QuerySelection<S = String, I = Vec<String>>
where
S: AsRef<str>,
I: IntoIterator<Item = S>,
{
All,
ByRestarter(S),
ByPattern(I),
}
pub struct Query {
zone: Option<String>,
}
impl Default for Query {
fn default() -> Self {
Self::new()
}
}
impl Query {
pub fn new() -> Query {
Query { zone: None }
}
pub fn zone<S: AsRef<str>>(&mut self, zone: S) -> &mut Query {
self.zone.replace(zone.as_ref().into());
self
}
fn add_zone_to_args(&self, args: &mut Vec<String>) {
if let Some(zone) = &self.zone {
args.push("-z".to_string());
args.push(zone.to_string());
}
}
fn add_columns_to_args(&self, args: &mut Vec<String>) {
args.push("-Ho".to_string());
args.push(
[
SvcColumn::Fmri,
SvcColumn::ContractId,
SvcColumn::InstanceName,
SvcColumn::NextState,
SvcColumn::ScopeName,
SvcColumn::ServiceName,
SvcColumn::State,
SvcColumn::ServiceTime,
SvcColumn::Zone,
SvcColumn::Description,
]
.iter()
.map(|col| col.to_str())
.collect::<Vec<&str>>()
.join(","),
);
}
fn issue_command(&self, args: Vec<String>) -> Result<String, QueryError> {
Ok(std::process::Command::new("/usr/bin/svcs")
.env_clear()
.args(args)
.output()
.map_err(QueryError::Command)?
.read_stdout()?)
}
fn issue_status_command(
&self,
args: Vec<String>,
) -> Result<impl Iterator<Item = SvcStatus>, QueryError> {
Ok(self
.issue_command(args)?
.split('\n')
.map(|s| s.parse::<SvcStatus>())
.collect::<Result<Vec<SvcStatus>, _>>()?
.into_iter())
}
fn add_patterns<S, I>(&self, args: &mut Vec<String>, patterns: I)
where
S: AsRef<str>,
I: IntoIterator<Item = S>,
{
args.append(
&mut patterns
.into_iter()
.map(|p| p.as_ref().to_owned())
.collect(),
);
}
pub fn get_status_all(&self) -> Result<impl Iterator<Item = SvcStatus>, QueryError> {
self.get_status(QuerySelection::<String, Vec<String>>::All)
}
pub fn get_status<S, I>(
&self,
selection: QuerySelection<S, I>,
) -> Result<impl Iterator<Item = SvcStatus>, QueryError>
where
S: AsRef<str>,
I: IntoIterator<Item = S>,
{
let mut args = vec![];
self.add_zone_to_args(&mut args);
self.add_columns_to_args(&mut args);
match selection {
QuerySelection::All => args.push("-a".to_string()),
QuerySelection::ByRestarter(restarter) => {
args.push(format!("-R {}", restarter.as_ref()));
}
QuerySelection::ByPattern(names) => self.add_patterns(&mut args, names),
}
self.issue_status_command(args)
}
fn get_dep_impl<S, I>(
&self,
mut args: Vec<String>,
patterns: I,
) -> Result<impl Iterator<Item = SvcStatus>, QueryError>
where
S: AsRef<str>,
I: IntoIterator<Item = S>,
{
self.add_zone_to_args(&mut args);
self.add_columns_to_args(&mut args);
self.add_patterns(&mut args, patterns);
self.issue_status_command(args)
}
pub fn get_dependencies_of<S, I>(
&self,
patterns: I,
) -> Result<impl Iterator<Item = SvcStatus>, QueryError>
where
S: AsRef<str>,
I: IntoIterator<Item = S>,
{
let args = vec!["-d".to_string()];
self.get_dep_impl(args, patterns)
}
pub fn get_dependents_of<S, I>(
&self,
patterns: I,
) -> Result<impl Iterator<Item = SvcStatus>, QueryError>
where
S: AsRef<str>,
I: IntoIterator<Item = S>,
{
let args = vec!["-D".to_string()];
self.get_dep_impl(args, patterns)
}
pub fn get_log_files<S, I>(
&self,
patterns: I,
) -> Result<impl Iterator<Item = PathBuf>, QueryError>
where
S: AsRef<str>,
I: IntoIterator<Item = S>,
{
let mut args = vec!["-L".to_string()];
self.add_zone_to_args(&mut args);
self.add_patterns(&mut args, patterns);
Ok(self
.issue_command(args)?
.split('\n')
.map(|s| s.parse::<PathBuf>())
.collect::<Result<Vec<PathBuf>, _>>()
.unwrap()
.into_iter())
}
}
#[derive(Error, Debug)]
pub enum ConfigError {
#[error("Failed to execute command: {0}")]
Command(std::io::Error),
#[error("Failed to parse command output: {0}")]
CommandOutput(#[from] CommandOutputError),
}
pub struct Config {}
impl Config {
pub fn export() -> ConfigExport {
ConfigExport::new()
}
pub fn import() -> ConfigImport {
ConfigImport::new()
}
pub fn delete() -> ConfigDelete {
ConfigDelete::new()
}
pub fn add<S: AsRef<str>>(fmri: S) -> ConfigAdd {
ConfigAdd::new(fmri.as_ref().into())
}
pub fn set_property<S: AsRef<str>>(fmri: S) -> ConfigSetProperty {
ConfigSetProperty::new(fmri.as_ref().into())
}
}
trait ConfigSubcommand {
fn name(&self) -> &str;
fn add_args(&self, args: &mut Vec<String>);
}
pub struct ConfigExport {
archive: bool,
}
impl ConfigExport {
fn new() -> Self {
ConfigExport { archive: false }
}
pub fn archive(&mut self) -> &mut Self {
self.archive = true;
self
}
pub fn run<S: AsRef<str>>(&mut self, fmri: S) -> Result<String, ConfigError> {
let mut args = vec!["export"];
if self.archive {
args.push("-a");
}
args.push(fmri.as_ref());
Ok(std::process::Command::new("/usr/sbin/svccfg")
.env_clear()
.args(args)
.output()
.map_err(ConfigError::Command)?
.read_stdout()?)
}
}
pub struct ConfigImport {
validate: bool,
}
impl ConfigImport {
fn new() -> Self {
ConfigImport { validate: true }
}
pub fn no_validate(&mut self) -> &mut Self {
self.validate = false;
self
}
pub fn run<P: AsRef<Path>>(&mut self, path: P) -> Result<(), ConfigError> {
let mut args = vec!["import"];
if self.validate {
args.push("-V");
}
let path_str = path.as_ref().to_string_lossy();
args.push(&path_str);
std::process::Command::new("/usr/sbin/svccfg")
.env_clear()
.args(args)
.output()
.map_err(ConfigError::Command)?
.read_stdout()
.map(|_| ())
.map_err(|err| err.into())
}
}
pub struct ConfigDelete {
force: bool,
}
impl ConfigDelete {
fn new() -> Self {
ConfigDelete { force: false }
}
pub fn force(&mut self) -> &mut Self {
self.force = true;
self
}
pub fn run<S: AsRef<str>>(&mut self, fmri: S) -> Result<(), ConfigError> {
let mut args = vec!["delete"];
if self.force {
args.push("-f");
}
args.push(fmri.as_ref());
std::process::Command::new("/usr/sbin/svccfg")
.env_clear()
.args(args)
.output()
.map_err(ConfigError::Command)?
.read_stdout()
.map(|_| ())
.map_err(|err| err.into())
}
}
pub struct ConfigAdd {
fmri: String,
}
impl ConfigAdd {
fn new(fmri: String) -> Self {
ConfigAdd { fmri }
}
pub fn run<S: AsRef<str>>(&mut self, child: S) -> Result<(), ConfigError> {
let args = vec!["-s", &self.fmri, "add", child.as_ref()];
std::process::Command::new("/usr/sbin/svccfg")
.env_clear()
.args(args)
.output()
.map_err(ConfigError::Command)?
.read_stdout()
.map(|_| ())
.map_err(|err| err.into())
}
}
pub struct ConfigSetProperty {
fmri: String,
}
impl ConfigSetProperty {
fn new(fmri: String) -> Self {
ConfigSetProperty { fmri }
}
pub fn run(&self, property: Property) -> Result<(), ConfigError> {
let prop = format!(
"{} = {}",
property.name.to_string(),
property.value.to_string()
);
let args = vec!["-s", &self.fmri, "setprop", &prop];
std::process::Command::new("/usr/sbin/svccfg")
.env_clear()
.args(args)
.output()
.map_err(ConfigError::Command)?
.read_stdout()
.map(|_| ())
.map_err(|err| err.into())
}
}
#[derive(Error, Debug)]
pub enum AdmError {
#[error("Failed to execute command: {0}")]
Command(std::io::Error),
#[error("Failed to parse command output: {0}")]
CommandOutput(#[from] CommandOutputError),
}
pub enum AdmSelection<S, I>
where
S: AsRef<str>,
I: IntoIterator<Item = S>,
{
ByState(SmfState),
ByPattern(I),
}
pub struct Adm {
zone: Option<String>,
}
impl Default for Adm {
fn default() -> Self {
Self::new()
}
}
impl Adm {
pub fn new() -> Adm {
Adm { zone: None }
}
fn add_zone_to_args(&self, args: &mut Vec<String>) {
if let Some(zone) = &self.zone {
args.push("-z".to_string());
args.push(zone.to_string());
}
}
pub fn zone<S: AsRef<str>>(&mut self, zone: S) -> &mut Adm {
self.zone.replace(zone.as_ref().into());
self
}
fn run(args: Vec<String>) -> Result<(), AdmError> {
std::process::Command::new("/usr/sbin/svcadm")
.env_clear()
.args(args)
.output()
.map_err(AdmError::Command)?
.read_stdout()?;
Ok(())
}
pub fn enable(&self) -> AdmEnable {
AdmEnable::new(self)
}
pub fn disable(&self) -> AdmDisable {
AdmDisable::new(self)
}
pub fn restart(&self) -> AdmRestart {
AdmRestart::new(self)
}
pub fn refresh(&self) -> AdmRefresh {
AdmRefresh::new(self)
}
pub fn clear(&self) -> AdmClear {
AdmClear::new(self)
}
}
trait AdmSubcommand {
fn adm(&self) -> &Adm;
fn command_name(&self) -> &str;
fn add_to_args(&self, args: &mut Vec<String>);
}
fn run_adm_subcommand<C, S, I>(
subcommand: &C,
selection: AdmSelection<S, I>,
) -> Result<(), AdmError>
where
C: AdmSubcommand,
S: AsRef<str>,
I: IntoIterator<Item = S>,
{
let mut args = vec![];
subcommand.adm().add_zone_to_args(&mut args);
match selection {
AdmSelection::ByState(state) => {
args.push("-S".to_string());
args.push(state.to_string());
args.push(subcommand.command_name().to_string());
subcommand.add_to_args(&mut args);
}
AdmSelection::ByPattern(pattern) => {
args.push(subcommand.command_name().to_string());
subcommand.add_to_args(&mut args);
args.extend(pattern.into_iter().map(|s| s.as_ref().to_string()));
}
}
Adm::run(args)
}
pub struct AdmEnable<'a> {
adm: &'a Adm,
recursive: bool,
synchronous: bool,
temporary: bool,
}
impl<'a> AdmSubcommand for AdmEnable<'a> {
fn adm(&self) -> &Adm {
&self.adm
}
fn command_name(&self) -> &str {
"enable"
}
fn add_to_args(&self, args: &mut Vec<String>) {
if self.recursive {
args.push("-r".to_string())
}
if self.synchronous {
args.push("-s".to_string())
}
if self.temporary {
args.push("-t".to_string())
}
}
}
impl<'a> AdmEnable<'a> {
fn new(adm: &'a Adm) -> Self {
AdmEnable {
adm,
recursive: false,
synchronous: false,
temporary: false,
}
}
pub fn recursive(&mut self) -> &mut Self {
self.recursive = true;
self
}
pub fn synchronous(&mut self) -> &mut Self {
self.synchronous = true;
self
}
pub fn temporary(&mut self) -> &mut Self {
self.temporary = true;
self
}
pub fn run<S, I>(&mut self, selection: AdmSelection<S, I>) -> Result<(), AdmError>
where
S: AsRef<str>,
I: IntoIterator<Item = S>,
{
run_adm_subcommand(self, selection)
}
}
pub struct AdmDisable<'a> {
adm: &'a Adm,
comment: Option<String>,
synchronous: bool,
temporary: bool,
}
impl<'a> AdmSubcommand for AdmDisable<'a> {
fn adm(&self) -> &Adm {
&self.adm
}
fn command_name(&self) -> &str {
"disable"
}
fn add_to_args(&self, args: &mut Vec<String>) {
if let Some(ref comment) = self.comment {
args.push("-c".to_string());
args.push(comment.to_string());
}
if self.synchronous {
args.push("-s".to_string())
}
if self.temporary {
args.push("-t".to_string())
}
}
}
impl<'a> AdmDisable<'a> {
fn new(adm: &'a Adm) -> Self {
AdmDisable {
adm,
comment: None,
synchronous: false,
temporary: false,
}
}
pub fn comment<S: AsRef<str>>(&mut self, comment: S) -> &mut Self {
self.comment = Some(comment.as_ref().to_owned());
self
}
pub fn synchronous(&mut self) -> &mut Self {
self.synchronous = true;
self
}
pub fn temporary(&mut self) -> &mut Self {
self.temporary = true;
self
}
pub fn run<S, I>(&mut self, selection: AdmSelection<S, I>) -> Result<(), AdmError>
where
S: AsRef<str>,
I: IntoIterator<Item = S>,
{
run_adm_subcommand(self, selection)
}
}
pub struct AdmRestart<'a> {
adm: &'a Adm,
abort: bool,
}
impl<'a> AdmSubcommand for AdmRestart<'a> {
fn adm(&self) -> &Adm {
&self.adm
}
fn command_name(&self) -> &str {
"restart"
}
fn add_to_args(&self, args: &mut Vec<String>) {
if self.abort {
args.push("-d".to_string())
}
}
}
impl<'a> AdmRestart<'a> {
fn new(adm: &'a Adm) -> Self {
Self { adm, abort: false }
}
pub fn abort(&mut self) -> &mut Self {
self.abort = true;
self
}
pub fn run<S, I>(&mut self, selection: AdmSelection<S, I>) -> Result<(), AdmError>
where
S: AsRef<str>,
I: IntoIterator<Item = S>,
{
run_adm_subcommand(self, selection)
}
}
pub struct AdmRefresh<'a> {
adm: &'a Adm,
}
impl<'a> AdmSubcommand for AdmRefresh<'a> {
fn adm(&self) -> &Adm {
&self.adm
}
fn command_name(&self) -> &str {
"refresh"
}
fn add_to_args(&self, _args: &mut Vec<String>) {}
}
impl<'a> AdmRefresh<'a> {
fn new(adm: &'a Adm) -> Self {
Self { adm }
}
pub fn run<S, I>(&mut self, selection: AdmSelection<S, I>) -> Result<(), AdmError>
where
S: AsRef<str>,
I: IntoIterator<Item = S>,
{
run_adm_subcommand(self, selection)
}
}
pub struct AdmClear<'a> {
adm: &'a Adm,
}
impl<'a> AdmSubcommand for AdmClear<'a> {
fn adm(&self) -> &Adm {
&self.adm
}
fn command_name(&self) -> &str {
"clear"
}
fn add_to_args(&self, _args: &mut Vec<String>) {}
}
impl<'a> AdmClear<'a> {
fn new(adm: &'a Adm) -> Self {
Self { adm }
}
pub fn run<S, I>(&mut self, selection: AdmSelection<S, I>) -> Result<(), AdmError>
where
S: AsRef<str>,
I: IntoIterator<Item = S>,
{
run_adm_subcommand(self, selection)
}
}
#[derive(Error, Debug)]
#[error("Invalid Property: {0}")]
pub struct PropertyParseError(String);
fn valid_property_substring(s: &str) -> bool {
!s.contains(char::is_whitespace) && !s.contains('/')
}
#[derive(Debug, PartialEq)]
pub struct PropertyGroupName {
group: String,
}
impl PropertyGroupName {
pub fn new<S>(group: S) -> Result<PropertyGroupName, PropertyParseError>
where
S: AsRef<str>,
{
if !valid_property_substring(group.as_ref()) {
return Err(PropertyParseError("Invalid property group".to_string()));
}
Ok(PropertyGroupName {
group: group.as_ref().into(),
})
}
pub fn group(&self) -> &str {
&self.group
}
}
impl AsRef<str> for PropertyGroupName {
fn as_ref(&self) -> &str {
&self.group
}
}
impl ToString for PropertyGroupName {
fn to_string(&self) -> String {
self.group.clone()
}
}
#[derive(Debug, PartialEq)]
pub struct PropertyName {
group: PropertyGroupName,
property: String,
}
impl PropertyName {
pub fn new<S1, S2>(group: S1, property: S2) -> Result<PropertyName, PropertyParseError>
where
S1: AsRef<str>,
S2: AsRef<str>,
{
let group = PropertyGroupName::new(group)?;
if !valid_property_substring(property.as_ref()) {
return Err(PropertyParseError("Invalid property value".to_string()));
}
Ok(PropertyName {
group,
property: property.as_ref().into(),
})
}
pub fn group(&self) -> &str {
&self.group.as_ref()
}
pub fn property(&self) -> &str {
&self.property
}
}
impl FromStr for PropertyName {
type Err = PropertyParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut iter = s.split('/');
let group = iter
.next()
.ok_or("Missing Group")
.map_err(|e| PropertyParseError(e.to_string()))?;
let property = iter
.next()
.ok_or("Missing Property")
.map_err(|e| PropertyParseError(e.to_string()))?;
if let Some(s) = iter.next() {
Err(PropertyParseError(format!(
"Unexpected string in property name: {}",
s
)))
} else {
PropertyName::new(group, property)
}
}
}
impl ToString for PropertyName {
fn to_string(&self) -> String {
format!("{}/{}", self.group.as_ref(), self.property)
}
}
#[derive(Debug, PartialEq)]
pub struct Property {
name: PropertyName,
value: PropertyValue,
}
impl Property {
pub fn new(name: PropertyName, value: PropertyValue) -> Self {
Property { name, value }
}
}
impl FromStr for Property {
type Err = PropertyParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut iter = s.split(' ');
let name_str = iter
.next()
.ok_or("Missing FMRI")
.map_err(|err| PropertyParseError(err.to_string()))?;
let name = str::parse::<PropertyName>(name_str)?;
let r = iter.collect::<Vec<&str>>().join(" ");
let value = str::parse::<PropertyValue>(&r)?;
Ok(Property { name, value })
}
}
#[derive(Debug, PartialEq)]
pub enum PropertyValue {
Boolean(bool),
Count(u64),
Integer(i64),
Astring(String),
Ustring(String),
Fmri(Vec<String>),
Other(String),
}
impl FromStr for PropertyValue {
type Err = PropertyParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut iter = s.split(' ');
let value = || -> Result<PropertyValue, String> {
let ty = iter.next().ok_or("Missing type")?;
let value = iter.collect::<Vec<&str>>().join(" ");
let pv = match ty {
"boolean" => {
PropertyValue::Boolean(value.parse::<bool>().map_err(|err| err.to_string())?)
}
"count" => {
PropertyValue::Count(value.parse::<u64>().map_err(|err| err.to_string())?)
}
"integer" => {
PropertyValue::Integer(value.parse::<i64>().map_err(|err| err.to_string())?)
}
"astring" => PropertyValue::Astring(value),
"ustring" => PropertyValue::Ustring(value),
"fmri" => PropertyValue::Fmri(
value
.split_whitespace()
.map(|s| s.to_string())
.collect::<Vec<String>>(),
),
_ => PropertyValue::Other(value),
};
Ok(pv)
}()
.map_err(PropertyParseError)?;
Ok(value)
}
}
impl ToString for PropertyValue {
fn to_string(&self) -> String {
match self {
PropertyValue::Boolean(b) => format!("boolean: {}", b),
PropertyValue::Count(c) => format!("count: {}", c),
PropertyValue::Integer(i) => format!("integer: {}", i),
PropertyValue::Astring(s) => format!("astring: {}", s),
PropertyValue::Ustring(s) => format!("ustring: {}", s),
PropertyValue::Fmri(fmris) => format!("fmri: {}", fmris.join(" ")),
PropertyValue::Other(s) => s.to_string(),
}
}
}
#[derive(Error, Debug)]
pub enum PropertyError {
#[error("Parse error: {0}")]
ParseError(#[from] PropertyParseError),
#[error("Failed to execute command: {0}")]
Command(std::io::Error),
#[error("Failed to parse command output: {0}")]
CommandOutput(#[from] CommandOutputError),
}
pub struct Properties {
zone: Option<String>,
}
impl Default for Properties {
fn default() -> Self {
Properties::new()
}
}
impl Properties {
pub fn new() -> Self {
Properties { zone: None }
}
pub fn zone<S: AsRef<str>>(&mut self, zone: S) -> &mut Properties {
self.zone.replace(zone.as_ref().into());
self
}
fn add_zone_to_args(&self, args: &mut Vec<String>) {
if let Some(zone) = &self.zone {
args.push("-z".to_string());
args.push(zone.to_string());
}
}
pub fn lookup(&self) -> PropertyLookup {
PropertyLookup::new(self)
}
pub fn wait(&self) -> PropertyWait {
PropertyWait::new(self)
}
}
pub enum PropertyClass {
Effective(Option<String>),
DirectlyAttachedUncomposed,
DirectlyAttachedComposed,
}
pub struct PropertyLookup<'a> {
property_base: &'a Properties,
attachment: PropertyClass,
}
impl<'a> PropertyLookup<'a> {
fn new(property_base: &'a Properties) -> Self {
PropertyLookup {
property_base,
attachment: PropertyClass::Effective(None),
}
}
pub fn attachment(&mut self, attachment: PropertyClass) -> &mut Self {
self.attachment = attachment;
self
}
fn add_attachment_to_args(&self, args: &mut Vec<String>) {
match &self.attachment {
PropertyClass::Effective(None) => (),
PropertyClass::Effective(Some(snap)) => {
args.push("-s".into());
args.push(snap.to_string());
}
PropertyClass::DirectlyAttachedUncomposed => args.push("-C".into()),
PropertyClass::DirectlyAttachedComposed => args.push("-c".into()),
}
}
pub fn run<S>(&mut self, property: &PropertyName, fmri: S) -> Result<Property, PropertyError>
where
S: AsRef<str>,
{
let mut args = vec!["-t".to_string()];
self.add_attachment_to_args(&mut args);
self.property_base.add_zone_to_args(&mut args);
args.push("-p".to_string());
args.push(property.to_string());
args.push(fmri.as_ref().into());
let out = std::process::Command::new("/usr/bin/svcprop")
.env_clear()
.args(args)
.output()
.map_err(PropertyError::Command)?
.read_stdout()?;
out.parse().map_err(|err: PropertyParseError| err.into())
}
}
pub struct PropertyWait<'a> {
property_base: &'a Properties,
}
impl<'a> PropertyWait<'a> {
fn new(property_base: &'a Properties) -> Self {
PropertyWait { property_base }
}
pub fn run<S>(
&mut self,
property: &PropertyGroupName,
fmri: S,
) -> Result<Property, PropertyError>
where
S: AsRef<str>,
{
let mut args = vec![
"-w".to_string(),
"-t".to_string(),
];
self.property_base.add_zone_to_args(&mut args);
args.push("-p".to_string());
args.push(property.to_string());
args.push(fmri.as_ref().into());
let out = std::process::Command::new("/usr/bin/svcprop")
.env_clear()
.args(args)
.output()
.map_err(PropertyError::Command)?
.read_stdout()?;
out.parse().map_err(|err: PropertyParseError| err.into())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_status() {
let s = "svc:/system/device/local:default - default - localhost system/device/local ON 14:57:25 global Standard Solaris device config";
let status = SvcStatus::from_str(&s);
assert!(status.is_ok());
let status = status.unwrap();
let expected = SvcStatus {
fmri: "svc:/system/device/local:default".to_string(),
contract_id: None,
instance_name: "default".to_string(),
next_state: None,
scope_name: "localhost".to_string(),
service_name: "system/device/local".to_string(),
state: SmfState::Online,
service_time: "14:57:25".to_string(),
zone: "global".to_string(),
description: Some("Standard Solaris device config".to_string()),
};
assert_eq!(status, expected);
}
#[test]
fn test_svcs_query_one() {
let inst = "default";
let svc = "system/filesystem/root";
let fmri = format!("svc:/{}:{}", svc, inst);
let pattern = [&fmri];
let query = Query::new().get_status(QuerySelection::ByPattern(&pattern));
assert!(query.is_ok(), "Unexpected err: {}", query.err().unwrap());
let mut results = query.unwrap();
let first = results.next().unwrap();
assert_eq!(first.fmri, fmri);
assert_eq!(first.service_name, svc);
assert_eq!(first.instance_name, inst);
assert!(results.next().is_none());
}
#[test]
fn test_svcs_query_multiple() {
let svc_root = "system/filesystem/root";
let svc_usr = "system/filesystem/usr";
let pattern = [svc_usr, svc_root];
let query = Query::new().get_status(QuerySelection::ByPattern(&pattern));
assert!(query.is_ok(), "Unexpected err: {}", query.err().unwrap());
let mut results = query.unwrap();
let root = results.next().unwrap();
assert_eq!(root.service_name, svc_root);
let usr = results.next().unwrap();
assert_eq!(usr.service_name, svc_usr);
assert!(results.next().is_none());
}
#[test]
fn test_svcs_get_status_all() {
let query = Query::new().get_status_all();
assert!(query.is_ok(), "Unexpected err: {}", query.err().unwrap());
}
#[test]
fn test_svcs_get_status_all_global_zone() {
let query = Query::new().zone("global").get_status_all();
assert!(query.is_ok(), "Unexpected err: {}", query.err().unwrap());
}
#[test]
fn test_svcprop_parse_property_value() {
let input = "astring hello";
let value = str::parse::<PropertyValue>(&input).unwrap();
assert!(matches!(value, PropertyValue::Astring(s) if s == "hello"));
}
#[test]
fn test_svcprop_parse_property() {
let input = "general/comment astring hello";
let property = str::parse::<Property>(&input).unwrap();
assert_eq!(property.name.to_string(), "general/comment");
assert!(matches!(property.value, PropertyValue::Astring(s) if s == "hello"));
}
#[test]
fn test_svcprop_lookup_property_astring() {
let property_name = PropertyName::new("restarter", "state").unwrap();
let property = Properties::new()
.lookup()
.run(&property_name, "svc:/system/filesystem/root:default")
.unwrap();
assert_eq!(property_name, property.name);
match &property.value {
PropertyValue::Astring(val) => assert_eq!(&val[..], "online"),
_ => panic!("Unexpected value: {:#?}", property.value),
}
}
#[test]
fn test_svcprop_lookup_property_integer() {
let property_name = PropertyName::new("restarter", "start_pid").unwrap();
let property = Properties::new()
.lookup()
.run(&property_name, "svc:/system/filesystem/root:default")
.unwrap();
assert_eq!(property_name, property.name);
match &property.value {
PropertyValue::Count(_) => (),
_ => panic!("Unexpected value: {:#?}", property.value),
}
}
}