Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[reconfigurator] Only place Crucible zones on provisionable zpools #5601

Merged
merged 6 commits into from
Apr 23, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions clients/sled-agent-client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ progenitor::generate_api!(
Vni = omicron_common::api::external::Vni,
NetworkInterface = omicron_common::api::internal::shared::NetworkInterface,
TypedUuidForZpoolKind = omicron_uuid_kinds::ZpoolUuid,
ZpoolKind = omicron_common::zpool_name::ZpoolKind,
ZpoolName = omicron_common::zpool_name::ZpoolName,
}
);

Expand Down
1 change: 1 addition & 0 deletions common/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ libc.workspace = true
regress.workspace = true
serde_urlencoded.workspace = true
tokio = { workspace = true, features = ["test-util"] }
toml.workspace = true

[features]
testing = ["proptest", "test-strategy"]
1 change: 1 addition & 0 deletions common/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ pub mod disk;
pub mod ledger;
pub mod update;
pub mod vlan;
pub mod zpool_name;

pub use update::hex_schema;

Expand Down
279 changes: 279 additions & 0 deletions common/src/zpool_name.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,279 @@
// 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/.

//! Zpool labels and kinds shared between Nexus and Sled Agents

use camino::{Utf8Path, Utf8PathBuf};
use omicron_uuid_kinds::ZpoolUuid;
use schemars::JsonSchema;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::fmt;
use std::str::FromStr;
pub const ZPOOL_EXTERNAL_PREFIX: &str = "oxp_";
pub const ZPOOL_INTERNAL_PREFIX: &str = "oxi_";

/// Describes the different classes of Zpools.
#[derive(
Copy, Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, JsonSchema,
)]
#[serde(rename_all = "snake_case")]
pub enum ZpoolKind {
// This zpool is used for external storage (u.2)
External,
// This zpool is used for internal storage (m.2)
Internal,
}

/// A wrapper around a zpool name.
///
/// This expects that the format will be: `ox{i,p}_<UUID>` - we parse the prefix
/// when reading the structure, and validate that the UUID can be utilized.
#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub struct ZpoolName {
id: ZpoolUuid,
kind: ZpoolKind,
}

const ZPOOL_NAME_REGEX: &str = r"^ox[ip]_[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$";

/// Custom JsonSchema implementation to encode the constraints on Name.
impl JsonSchema for ZpoolName {
fn schema_name() -> String {
"ZpoolName".to_string()
}
fn json_schema(
_: &mut schemars::gen::SchemaGenerator,
) -> schemars::schema::Schema {
schemars::schema::SchemaObject {
metadata: Some(Box::new(schemars::schema::Metadata {
title: Some(
"The name of a Zpool".to_string(),
),
description: Some(
"Zpool names are of the format ox{i,p}_<UUID>. They are either \
Internal or External, and should be unique"
.to_string(),
),
..Default::default()
})),
instance_type: Some(schemars::schema::InstanceType::String.into()),
string: Some(Box::new(schemars::schema::StringValidation {
pattern: Some(ZPOOL_NAME_REGEX.to_owned()),
..Default::default()
})),
..Default::default()
}
.into()
}
}

impl ZpoolName {
pub fn new_internal(id: ZpoolUuid) -> Self {
Self { id, kind: ZpoolKind::Internal }
}

pub fn new_external(id: ZpoolUuid) -> Self {
Self { id, kind: ZpoolKind::External }
}

pub fn id(&self) -> ZpoolUuid {
self.id
}

pub fn kind(&self) -> ZpoolKind {
self.kind
}

/// Returns a path to a dataset's mountpoint within the zpool.
///
/// For example: oxp_(UUID) -> /pool/ext/(UUID)/(dataset)
pub fn dataset_mountpoint(
&self,
root: &Utf8Path,
dataset: &str,
) -> Utf8PathBuf {
let mut path = Utf8PathBuf::new();
path.push(root);
path.push("pool");
match self.kind {
ZpoolKind::External => path.push("ext"),
ZpoolKind::Internal => path.push("int"),
};
path.push(self.id().to_string());
path.push(dataset);
path
}
}

impl<'de> Deserialize<'de> for ZpoolName {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
ZpoolName::from_str(&s).map_err(serde::de::Error::custom)
}
}

impl Serialize for ZpoolName {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.to_string())
}
}

impl FromStr for ZpoolName {
type Err = String;

fn from_str(s: &str) -> Result<Self, Self::Err> {
if let Some(s) = s.strip_prefix(ZPOOL_EXTERNAL_PREFIX) {
let id = ZpoolUuid::from_str(s).map_err(|e| e.to_string())?;
Ok(ZpoolName::new_external(id))
} else if let Some(s) = s.strip_prefix(ZPOOL_INTERNAL_PREFIX) {
let id = ZpoolUuid::from_str(s).map_err(|e| e.to_string())?;
Ok(ZpoolName::new_internal(id))
} else {
Err(format!(
"Bad zpool name {s}; must start with '{ZPOOL_EXTERNAL_PREFIX}' or '{ZPOOL_INTERNAL_PREFIX}'",
))
}
}
}

impl fmt::Display for ZpoolName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let prefix = match self.kind {
ZpoolKind::External => ZPOOL_EXTERNAL_PREFIX,
ZpoolKind::Internal => ZPOOL_INTERNAL_PREFIX,
};
write!(f, "{prefix}{}", self.id)
}
}

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

#[test]
fn test_zpool_name_regex() {
let valid = [
"oxi_d462a7f7-b628-40fe-80ff-4e4189e2d62b",
"oxp_d462a7f7-b628-40fe-80ff-4e4189e2d62b",
];

let invalid = [
"",
// Whitespace
" oxp_d462a7f7-b628-40fe-80ff-4e4189e2d62b",
"oxp_d462a7f7-b628-40fe-80ff-4e4189e2d62b ",
// Case sensitivity
"oxp_D462A7F7-b628-40fe-80ff-4e4189e2d62b",
// Bad prefix
"ox_d462a7f7-b628-40fe-80ff-4e4189e2d62b",
"oxa_d462a7f7-b628-40fe-80ff-4e4189e2d62b",
"oxi-d462a7f7-b628-40fe-80ff-4e4189e2d62b",
"oxp-d462a7f7-b628-40fe-80ff-4e4189e2d62b",
// Missing Prefix
"d462a7f7-b628-40fe-80ff-4e4189e2d62b",
// Bad UUIDs (Not following UUIDv4 format)
"oxi_d462a7f7-b628-30fe-80ff-4e4189e2d62b",
"oxi_d462a7f7-b628-40fe-c0ff-4e4189e2d62b",
];

let r = regress::Regex::new(ZPOOL_NAME_REGEX)
.expect("validation regex is valid");
for input in valid {
let m = r
.find(input)
.unwrap_or_else(|| panic!("input {input} did not match regex"));
assert_eq!(m.start(), 0, "input {input} did not match start");
assert_eq!(m.end(), input.len(), "input {input} did not match end");
}

for input in invalid {
assert!(
r.find(input).is_none(),
"invalid input {input} should not match validation regex"
);
}
}

#[test]
fn test_parse_zpool_name_json() {
#[derive(Serialize, Deserialize, JsonSchema)]
struct TestDataset {
pool_name: ZpoolName,
}

// Confirm that we can convert from a JSON string to a a ZpoolName
let json_string =
r#"{"pool_name":"oxi_d462a7f7-b628-40fe-80ff-4e4189e2d62b"}"#;
let dataset: TestDataset = serde_json::from_str(json_string)
.expect("Could not parse ZpoolName from Json Object");
assert!(matches!(dataset.pool_name.kind, ZpoolKind::Internal));

// Confirm we can go the other way (ZpoolName to JSON string) too.
let j = serde_json::to_string(&dataset)
.expect("Cannot convert back to JSON string");
assert_eq!(j, json_string);
}

fn toml_string(s: &str) -> String {
format!("zpool_name = \"{}\"", s)
}

fn parse_name(s: &str) -> Result<ZpoolName, toml::de::Error> {
toml_string(s)
.parse::<toml::Value>()
.expect("Cannot parse as TOML value")
.get("zpool_name")
.expect("Missing key")
.clone()
.try_into::<ZpoolName>()
}

#[test]
fn test_parse_external_zpool_name() {
let uuid: ZpoolUuid =
"d462a7f7-b628-40fe-80ff-4e4189e2d62b".parse().unwrap();
let good_name = format!("{}{}", ZPOOL_EXTERNAL_PREFIX, uuid);

let name = parse_name(&good_name).expect("Cannot parse as ZpoolName");
assert_eq!(uuid, name.id());
assert_eq!(ZpoolKind::External, name.kind());
}

#[test]
fn test_parse_internal_zpool_name() {
let uuid: ZpoolUuid =
"d462a7f7-b628-40fe-80ff-4e4189e2d62b".parse().unwrap();
let good_name = format!("{}{}", ZPOOL_INTERNAL_PREFIX, uuid);

let name = parse_name(&good_name).expect("Cannot parse as ZpoolName");
assert_eq!(uuid, name.id());
assert_eq!(ZpoolKind::Internal, name.kind());
}

#[test]
fn test_parse_bad_zpool_names() {
let bad_names = vec![
// Nonsense string
"this string is GARBAGE",
// Missing prefix
"d462a7f7-b628-40fe-80ff-4e4189e2d62b",
// Underscores
"oxp_d462a7f7_b628_40fe_80ff_4e4189e2d62b",
];

for bad_name in &bad_names {
assert!(
parse_name(&bad_name).is_err(),
"Parsing {} should fail",
bad_name
);
}
}
}
3 changes: 2 additions & 1 deletion illumos-utils/src/zfs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -622,7 +622,8 @@ pub fn get_all_omicron_datasets_for_delete() -> anyhow::Result<Vec<String>> {
// This includes cockroachdb, clickhouse, and crucible datasets.
let zpools = crate::zpool::Zpool::list()?;
for pool in &zpools {
let internal = pool.kind() == crate::zpool::ZpoolKind::Internal;
let internal =
pool.kind() == omicron_common::zpool_name::ZpoolKind::Internal;
let pool = pool.to_string();
for dataset in &Zfs::list_datasets(&pool)? {
// Avoid erasing crashdump, backing data and swap datasets on
Expand Down
Loading