Skip to content

Commit

Permalink
The sled agent side of datasets in inventory
Browse files Browse the repository at this point in the history
  • Loading branch information
smklein committed Jul 24, 2024
1 parent fb23666 commit 817a397
Show file tree
Hide file tree
Showing 4 changed files with 305 additions and 0 deletions.
214 changes: 214 additions & 0 deletions illumos-utils/src/zfs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@
//! Utilities for poking at ZFS.

use crate::{execute, PFEXEC};
use anyhow::Context;
use camino::{Utf8Path, Utf8PathBuf};
use omicron_common::disk::DiskIdentity;
use omicron_uuid_kinds::DatasetUuid;
use std::fmt;
use std::str::FromStr;

// These locations in the ramdisk must only be used by the switch zone.
//
Expand Down Expand Up @@ -207,9 +210,71 @@ pub struct SizeDetails {
pub compression: Option<String>,
}

#[derive(Debug)]
pub struct DatasetProperties {
/// The Uuid of the dataset
pub id: Option<DatasetUuid>,
/// The full name of the dataset.
pub name: String,
/// Remaining space in the dataset and descendents.
pub avail: u64,
/// Space used by dataset and descendents.
pub used: u64,
/// Maximum space usable by dataset and descendents.
pub quota: Option<u64>,
/// Minimum space guaranteed to dataset and descendents.
pub reservation: Option<u64>,
/// The compression algorithm used for this dataset.
pub compression: String,
}

impl FromStr for DatasetProperties {
type Err = anyhow::Error;

fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut iter = s.split_whitespace();

let id = match iter.next().context("Missing UUID")? {
"-" => None,
anything_else => Some(anything_else.parse::<DatasetUuid>()?),
};

let name = iter.next().context("Missing 'name'")?.to_string();
let avail = iter.next().context("Missing 'avail'")?.parse::<u64>()?;
let used = iter.next().context("Missing 'used'")?.parse::<u64>()?;
let quota =
match iter.next().context("Missing 'quota'")?.parse::<u64>()? {
0 => None,
q => Some(q),
};
let reservation = match iter
.next()
.context("Missing 'reservation'")?
.parse::<u64>()?
{
0 => None,
r => Some(r),
};
let compression =
iter.next().context("Missing 'compression'")?.to_string();

Ok(DatasetProperties {
id,
name,
avail,
used,
quota,
reservation,
compression,
})
}
}

#[cfg_attr(any(test, feature = "testing"), mockall::automock, allow(dead_code))]
impl Zfs {
/// Lists all datasets within a pool or existing dataset.
///
/// Strips the input `name` from the output dataset names.
pub fn list_datasets(name: &str) -> Result<Vec<String>, ListDatasetsError> {
let mut command = std::process::Command::new(ZFS);
let cmd = command.args(&["list", "-d", "1", "-rHpo", "name", name]);
Expand All @@ -228,6 +293,38 @@ impl Zfs {
Ok(filesystems)
}

/// Get information about datasets within a list of zpools / datasets.
///
/// This function is similar to [Zfs::list_datasets], but provides a more
/// substantial results about the datasets found.
///
/// Sorts results and de-duplicates them by name.
pub fn get_dataset_properties(
datasets: &[String],
) -> Result<Vec<DatasetProperties>, anyhow::Error> {
let mut command = std::process::Command::new(ZFS);
let cmd = command.args(&["list", "-d", "1", "-rHpo"]);

// Note: this is tightly coupled with the layout of DatasetProperties
cmd.arg("oxide:uuid,name,avail,used,quota,reservation,compression");
cmd.args(datasets);

let output = execute(cmd).with_context(|| {
format!("Failed to get dataset properties for {datasets:?}")
})?;
let stdout = String::from_utf8_lossy(&output.stdout);
let mut datasets = stdout
.trim()
.split('\n')
.map(|row| row.parse::<DatasetProperties>())
.collect::<Result<Vec<_>, _>>()?;

datasets.sort_by(|d1, d2| d1.name.partial_cmp(&d2.name).unwrap());
datasets.dedup_by(|d1, d2| d1.name.eq(&d2.name));

Ok(datasets)
}

/// Return the name of a dataset for a ZFS object.
///
/// The object can either be a dataset name, or a path, in which case it
Expand Down Expand Up @@ -679,3 +776,120 @@ pub fn get_all_omicron_datasets_for_delete() -> anyhow::Result<Vec<String>> {

Ok(datasets)
}

#[cfg(test)]
mod test {
use super::*;

#[test]
fn parse_dataset_props() {
let input =
"- dataset_name 1234 5678 0 0 off";
let props = DatasetProperties::from_str(&input)
.expect("Should have parsed data");

assert_eq!(props.id, None);
assert_eq!(props.name, "dataset_name");
assert_eq!(props.avail, 1234);
assert_eq!(props.used, 5678);
assert_eq!(props.quota, None);
assert_eq!(props.reservation, None);
assert_eq!(props.compression, "off");
}

#[test]
fn parse_dataset_props_with_optionals() {
let input = "d4e1e554-7b98-4413-809e-4a42561c3d0c dataset_name 1234 5678 111 222 off";
let props = DatasetProperties::from_str(&input)
.expect("Should have parsed data");

assert_eq!(
props.id,
Some("d4e1e554-7b98-4413-809e-4a42561c3d0c".parse().unwrap())
);
assert_eq!(props.name, "dataset_name");
assert_eq!(props.avail, 1234);
assert_eq!(props.used, 5678);
assert_eq!(props.quota, Some(111));
assert_eq!(props.reservation, Some(222));
assert_eq!(props.compression, "off");
}

#[test]
fn parse_dataset_bad_uuid() {
let input = "bad dataset_name 1234 5678 111 222 off";
let err = DatasetProperties::from_str(&input)
.expect_err("Should have failed to parse");
assert!(
err.to_string().contains("error parsing UUID (dataset)"),
"{err}"
);
}

#[test]
fn parse_dataset_bad_avail() {
let input = "- dataset_name BADAVAIL 5678 111 222 off";
let err = DatasetProperties::from_str(&input)
.expect_err("Should have failed to parse");
assert!(
err.to_string().contains("invalid digit found in string"),
"{err}"
);
}

#[test]
fn parse_dataset_bad_usage() {
let input = "- dataset_name 1234 BADUSAGE 111 222 off";
let err = DatasetProperties::from_str(&input)
.expect_err("Should have failed to parse");
assert!(
err.to_string().contains("invalid digit found in string"),
"{err}"
);
}

#[test]
fn parse_dataset_bad_quota() {
let input = "- dataset_name 1234 5678 BADQUOTA 222 off";
let err = DatasetProperties::from_str(&input)
.expect_err("Should have failed to parse");
assert!(
err.to_string().contains("invalid digit found in string"),
"{err}"
);
}

#[test]
fn parse_dataset_bad_reservation() {
let input = "- dataset_name 1234 5678 111 BADRES off";
let err = DatasetProperties::from_str(&input)
.expect_err("Should have failed to parse");
assert!(
err.to_string().contains("invalid digit found in string"),
"{err}"
);
}

#[test]
fn parse_dataset_missing_fields() {
let expect_missing = |input: &str, what: &str| {
let err = DatasetProperties::from_str(input)
.expect_err("Should have failed to parse");
assert!(err.to_string().contains(&format!("Missing {what}")));
};

expect_missing(
"- dataset_name 1234 5678 111 222",
"'compression'",
);
expect_missing(
"- dataset_name 1234 5678 111",
"'reservation'",
);
expect_missing("- dataset_name 1234 5678", "'quota'");
expect_missing("- dataset_name 1234", "'used'");
expect_missing("- dataset_name", "'avail'");
expect_missing("-", "'name'");
expect_missing("", "UUID");
}
}
45 changes: 45 additions & 0 deletions sled-agent/src/params.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ use omicron_common::api::internal::nexus::{
use omicron_common::api::internal::shared::{
NetworkInterface, SourceNatConfig,
};
use omicron_uuid_kinds::DatasetUuid;
use omicron_uuid_kinds::PropolisUuid;
use omicron_uuid_kinds::ZpoolUuid;
use schemars::JsonSchema;
Expand Down Expand Up @@ -763,6 +764,49 @@ pub struct InventoryZpool {
pub total_size: ByteCount,
}

/// Identifies information about datasets within Oxide-managed zpools
#[derive(Clone, Debug, Deserialize, JsonSchema, Serialize)]
pub struct InventoryDataset {
/// Although datasets mandated by the control plane will have UUIDs,
/// datasets can be created (and have been created) without UUIDs.
pub id: Option<DatasetUuid>,

/// This name is the full path of the dataset.
// This is akin to [sled_storage::dataset::DatasetName::full_name],
// and it's also what you'd see when running "zfs list".
pub name: String,

/// The amount of remaining space usable by the dataset (and children)
/// assuming there is no other activity within the pool.
pub available: u64,

/// The amount of space consumed by this dataset and descendents.
pub used: u64,

/// The maximum amount of space usable by a dataset and all descendents.
pub quota: Option<u64>,

/// The minimum amount of space guaranteed to a dataset and descendents.
pub reservation: Option<u64>,

/// The compression algorithm used for this dataset, if any.
pub compression: String,
}

impl From<illumos_utils::zfs::DatasetProperties> for InventoryDataset {
fn from(props: illumos_utils::zfs::DatasetProperties) -> Self {
Self {
id: props.id,
name: props.name,
available: props.avail,
used: props.used,
quota: props.quota,
reservation: props.reservation,
compression: props.compression,
}
}
}

/// Identity and basic status information about this sled agent
#[derive(Clone, Debug, Deserialize, JsonSchema, Serialize)]
pub struct Inventory {
Expand All @@ -775,6 +819,7 @@ pub struct Inventory {
pub reservoir_size: ByteCount,
pub disks: Vec<InventoryDisk>,
pub zpools: Vec<InventoryZpool>,
pub datasets: Vec<InventoryDataset>,
}

#[derive(Clone, Debug, Deserialize, JsonSchema, Serialize)]
Expand Down
2 changes: 2 additions & 0 deletions sled-agent/src/sim/sled_agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -866,6 +866,8 @@ impl SledAgent {
})
})
.collect::<Result<Vec<_>, anyhow::Error>>()?,
// TODO: Make this more real?
datasets: vec![],
})
}

Expand Down
44 changes: 44 additions & 0 deletions sled-agent/src/sled_agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ use sled_agent_types::early_networking::EarlyNetworkConfig;
use sled_hardware::{underlay, HardwareManager};
use sled_hardware_types::underlay::BootstrapInterface;
use sled_hardware_types::Baseboard;
use sled_storage::dataset::{CRYPT_DATASET, ZONE_DATASET};
use sled_storage::manager::StorageHandle;
use sled_storage::resources::DatasetsManagementResult;
use sled_storage::resources::DisksManagementResult;
Expand Down Expand Up @@ -1250,6 +1251,7 @@ impl SledAgent {

let mut disks = vec![];
let mut zpools = vec![];
let mut datasets = vec![];
let all_disks = self.storage().get_latest_disks().await;
for (identity, variant, slot, _firmware) in all_disks.iter_all() {
disks.push(crate::params::InventoryDisk {
Expand Down Expand Up @@ -1278,6 +1280,47 @@ impl SledAgent {
id: zpool.id(),
total_size: ByteCount::try_from(info.size())?,
});

// We do care about the total space usage within zpools, but mapping
// the layering back to "datasets we care about" is a little
// awkward.
//
// We could query for all datasets within a pool, but the sled agent
// doesn't really care about the children of datasets that it
// allocates. As an example: Sled Agent might provision a "crucible"
// dataset, but how region allocation occurs within that dataset
// is a detail for Crucible to care about, not the Sled Agent.
//
// To balance this effort, we ask for information about datasets
// that the Sled Agent is directly resopnsible for managing.
let datasets_of_interest = [
// We care about the zpool itself, and all direct children.
zpool.to_string(),
// Likewise, we care about the encrypted dataset, and all
// direct children.
format!("{zpool}/{CRYPT_DATASET}"),
// The zone dataset gives us additional context on "what zones
// have datasets provisioned".
format!("{zpool}/{ZONE_DATASET}"),
];
let inv_props =
match illumos_utils::zfs::Zfs::get_dataset_properties(
datasets_of_interest.as_slice(),
) {
Ok(props) => props.into_iter().map(|prop| {
crate::params::InventoryDataset::from(prop)
}),
Err(err) => {
warn!(
self.log,
"Failed to access dataset info within zpool";
"zpool" => %zpool,
"err" => %err
);
continue;
}
};
datasets.extend(inv_props);
}

Ok(Inventory {
Expand All @@ -1290,6 +1333,7 @@ impl SledAgent {
reservoir_size,
disks,
zpools,
datasets,
})
}
}
Expand Down

0 comments on commit 817a397

Please sign in to comment.