scuffle provides an illumos-only wrapper around libscf,
focused on interactions with property groups and properties of existing
services, instances, and snapshots. Understanding SMF Properties is
highly recommended background reading.
There are large swathes of libscf that are currently not available via
scuffle due to its focus on properties, including:
- The ability to create or delete services
- Interacting with running instances or services in any way other than refreshing instances
- All functionality related to
snaplevels - All functionality related to templates
Reading all properties from a specific property group in a service
instance’s running snapshot:
fn read_from_snapshot(
service_name: &str,
instance_name: &str,
property_group_name: &str,
) -> anyhow::Result<BTreeMap<String, Vec<Value>>> {
// Get a handle to scf and the local scope.
let scf = Scf::connect_current_zone()?;
let scope = scf.scope_local()?;
// Look up the property group within our snapshot by stepping through
// each level.
let Some(service) = scope.service(service_name)? else {
bail!("service {service_name} not found");
};
let Some(instance) = service.instance(instance_name)? else {
bail!("instance {instance_name} not found within {}", service.fmri());
};
let Some(snapshot) = instance.snapshot("running")? else {
bail!("no running snapshot found for {}", instance.fmri());
};
let Some(pg) = snapshot.property_group_composed(property_group_name)? else {
bail!(
"property group {property_group_name} not found for {}",
instance.fmri(),
);
};
let mut all_properties = BTreeMap::new();
for property in pg.properties()? {
let property = property?;
let values = property.values()?.collect::<Result<_, _>>()?;
all_properties.insert(property.name().to_string(), values);
}
Ok(all_properties)
}Adding a new property to an existing property group of a service instance:
fn add_new_property(
service_name: &str,
instance_name: &str,
property_group_name: &str,
property_name: &str,
value: ValueRef<'_>,
) -> anyhow::Result<()> {
// Get a handle to scf and the local scope.
let scf = Scf::connect_current_zone()?;
let scope = scf.scope_local()?;
// Look up the property group within our instance by stepping through
// each level.
let Some(service) = scope.service(service_name)? else {
bail!("service {service_name} not found");
};
let Some(instance) = service.instance(instance_name)? else {
bail!("instance {instance_name} not found within {}", service.fmri());
};
let Some(mut pg) = instance.property_group_direct(property_group_name)? else {
bail!(
"property group {property_group_name} not found for {}",
instance.fmri(),
);
};
// Open a transaction on the property group.
let tx = pg.transaction()?;
// Start the transaction. This takes a snapshot of the property group's
// current version; if it's modified by someone else between this point
// and our attempt to `commit()` below, we'll get an `OutOfDate` result.
let mut tx = tx.start()?;
// Add an entry to the transaction to create the new property.
tx.property_new(property_name, value)?;
// Commit the transaction.
match tx.commit()? {
TransactionCommitResult::Success(_committed_tx) => Ok(()),
TransactionCommitResult::OutOfDate(_reset_tx) => {
// We'll return an error for this example, but real code may
// want to call `pg.update()` and try again.
bail!("property group concurrently modified");
}
}
}See the examples/ directory for more complete examples.
scuffle’s errors aim to provide extensive context; e.g., an error that
occurs while operating on an instance will include that instance’s FMRI.
These error types make extensive use of source errors as discussed in
Defining Error Types and Logging Errors. It is critical that
printing or logging of these error types walk the entire error chain as
discussed in that document, or the underlying error(s) will not be emitted.
The examples above use anyhow to allow easy ?-propagation despite the
multiple errors involved. scuffle does not currently provide a catch-all
error type of its own.
scuffle has three optional Cargo features:
- Enabling the
daftfeature adds implementations ofdaft::DiffabletoValue,ValueRef, andValueKind. - Enabling the
smf-by-instancefeature adds severalInstance::smf_*methods for controlling the SMF state of an instance, but requires alibscfthat includes recently-stabilized APIs. - Enabling the
testingfeature adds types to support writing tests that interact with SMF without needing to modify system-level SMF services / instances / properties; see “Testing Support” below.
scuffle makes use of some non-public interfaces. Specifically:
- [
Scf::connect_zone()] uses an undocumented SCF handle decoration to connect tosvc.configdinside the specified zone. This matches howsvcadmandsvcpropimplement their-z zoneflags. - If the
smf-by-instanceCargo feature is not enabled, [Instance::smf_refresh()] uses a non-public function defined bylibscf_priv.h(_smf_refresh_instance_i()). - If the
testingfeature is enabled, it uses several other non-public interfaces; see “Testing Support” below.
If the testing feature is enabled, scuffle exports an
isolated::IsolatedConfigd type that can run an instance of svc.configd
inside a temporary directory. After creating an instance of this type, tests
can connect to it via [Scf::connect_isolated()], and then freely read and
write properties within that instance without touching the real system’s
svc.configd and without the permissions normally required to do that.
This comes with several caveats:
IsolatedConfigdtakes advantage of undocumented and uncommitted interfaces in at leastsvccfg,svc.configd, andlibscfitself. These may break in the future.- Refreshing instances inside an
IsolatedConfigdvialibscfdoes not work.scuffleworks around this by refreshing instances viasvccfgpointed at the isolatedsvc.configd, but this is a divergence from what production code will do forrefresh. - Non-persistent property groups do not work inside
IsolatedConfigd.
scuffle uses IsolatedConfigd for its own tests, and therefore does not
have test coverage on features that interact with these restrictions.