Skip to content

Commit 86d13a7

Browse files
authored
[common] Share a common implementation of ZpoolName (#5600)
This removes dependencies on `illumos-utils`, and obviates some unnecessary parsing.
1 parent b3de513 commit 86d13a7

File tree

19 files changed

+306
-317
lines changed

19 files changed

+306
-317
lines changed

Cargo.lock

+1-3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

clients/sled-agent-client/src/lib.rs

+2
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ progenitor::generate_api!(
4848
Vni = omicron_common::api::external::Vni,
4949
NetworkInterface = omicron_common::api::internal::shared::NetworkInterface,
5050
TypedUuidForZpoolKind = omicron_uuid_kinds::ZpoolUuid,
51+
ZpoolKind = omicron_common::zpool_name::ZpoolKind,
52+
ZpoolName = omicron_common::zpool_name::ZpoolName,
5153
}
5254
);
5355

common/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ libc.workspace = true
5555
regress.workspace = true
5656
serde_urlencoded.workspace = true
5757
tokio = { workspace = true, features = ["test-util"] }
58+
toml.workspace = true
5859

5960
[features]
6061
testing = ["proptest", "test-strategy"]

common/src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ pub mod disk;
2828
pub mod ledger;
2929
pub mod update;
3030
pub mod vlan;
31+
pub mod zpool_name;
3132

3233
pub use update::hex_schema;
3334

common/src/zpool_name.rs

+279
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
1+
// This Source Code Form is subject to the terms of the Mozilla Public
2+
// License, v. 2.0. If a copy of the MPL was not distributed with this
3+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4+
5+
//! Zpool labels and kinds shared between Nexus and Sled Agents
6+
7+
use camino::{Utf8Path, Utf8PathBuf};
8+
use omicron_uuid_kinds::ZpoolUuid;
9+
use schemars::JsonSchema;
10+
use serde::{Deserialize, Deserializer, Serialize, Serializer};
11+
use std::fmt;
12+
use std::str::FromStr;
13+
pub const ZPOOL_EXTERNAL_PREFIX: &str = "oxp_";
14+
pub const ZPOOL_INTERNAL_PREFIX: &str = "oxi_";
15+
16+
/// Describes the different classes of Zpools.
17+
#[derive(
18+
Copy, Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, JsonSchema,
19+
)]
20+
#[serde(rename_all = "snake_case")]
21+
pub enum ZpoolKind {
22+
// This zpool is used for external storage (u.2)
23+
External,
24+
// This zpool is used for internal storage (m.2)
25+
Internal,
26+
}
27+
28+
/// A wrapper around a zpool name.
29+
///
30+
/// This expects that the format will be: `ox{i,p}_<UUID>` - we parse the prefix
31+
/// when reading the structure, and validate that the UUID can be utilized.
32+
#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
33+
pub struct ZpoolName {
34+
id: ZpoolUuid,
35+
kind: ZpoolKind,
36+
}
37+
38+
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}$";
39+
40+
/// Custom JsonSchema implementation to encode the constraints on Name.
41+
impl JsonSchema for ZpoolName {
42+
fn schema_name() -> String {
43+
"ZpoolName".to_string()
44+
}
45+
fn json_schema(
46+
_: &mut schemars::gen::SchemaGenerator,
47+
) -> schemars::schema::Schema {
48+
schemars::schema::SchemaObject {
49+
metadata: Some(Box::new(schemars::schema::Metadata {
50+
title: Some(
51+
"The name of a Zpool".to_string(),
52+
),
53+
description: Some(
54+
"Zpool names are of the format ox{i,p}_<UUID>. They are either \
55+
Internal or External, and should be unique"
56+
.to_string(),
57+
),
58+
..Default::default()
59+
})),
60+
instance_type: Some(schemars::schema::InstanceType::String.into()),
61+
string: Some(Box::new(schemars::schema::StringValidation {
62+
pattern: Some(ZPOOL_NAME_REGEX.to_owned()),
63+
..Default::default()
64+
})),
65+
..Default::default()
66+
}
67+
.into()
68+
}
69+
}
70+
71+
impl ZpoolName {
72+
pub fn new_internal(id: ZpoolUuid) -> Self {
73+
Self { id, kind: ZpoolKind::Internal }
74+
}
75+
76+
pub fn new_external(id: ZpoolUuid) -> Self {
77+
Self { id, kind: ZpoolKind::External }
78+
}
79+
80+
pub fn id(&self) -> ZpoolUuid {
81+
self.id
82+
}
83+
84+
pub fn kind(&self) -> ZpoolKind {
85+
self.kind
86+
}
87+
88+
/// Returns a path to a dataset's mountpoint within the zpool.
89+
///
90+
/// For example: oxp_(UUID) -> /pool/ext/(UUID)/(dataset)
91+
pub fn dataset_mountpoint(
92+
&self,
93+
root: &Utf8Path,
94+
dataset: &str,
95+
) -> Utf8PathBuf {
96+
let mut path = Utf8PathBuf::new();
97+
path.push(root);
98+
path.push("pool");
99+
match self.kind {
100+
ZpoolKind::External => path.push("ext"),
101+
ZpoolKind::Internal => path.push("int"),
102+
};
103+
path.push(self.id().to_string());
104+
path.push(dataset);
105+
path
106+
}
107+
}
108+
109+
impl<'de> Deserialize<'de> for ZpoolName {
110+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
111+
where
112+
D: Deserializer<'de>,
113+
{
114+
let s = String::deserialize(deserializer)?;
115+
ZpoolName::from_str(&s).map_err(serde::de::Error::custom)
116+
}
117+
}
118+
119+
impl Serialize for ZpoolName {
120+
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
121+
where
122+
S: Serializer,
123+
{
124+
serializer.serialize_str(&self.to_string())
125+
}
126+
}
127+
128+
impl FromStr for ZpoolName {
129+
type Err = String;
130+
131+
fn from_str(s: &str) -> Result<Self, Self::Err> {
132+
if let Some(s) = s.strip_prefix(ZPOOL_EXTERNAL_PREFIX) {
133+
let id = ZpoolUuid::from_str(s).map_err(|e| e.to_string())?;
134+
Ok(ZpoolName::new_external(id))
135+
} else if let Some(s) = s.strip_prefix(ZPOOL_INTERNAL_PREFIX) {
136+
let id = ZpoolUuid::from_str(s).map_err(|e| e.to_string())?;
137+
Ok(ZpoolName::new_internal(id))
138+
} else {
139+
Err(format!(
140+
"Bad zpool name {s}; must start with '{ZPOOL_EXTERNAL_PREFIX}' or '{ZPOOL_INTERNAL_PREFIX}'",
141+
))
142+
}
143+
}
144+
}
145+
146+
impl fmt::Display for ZpoolName {
147+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
148+
let prefix = match self.kind {
149+
ZpoolKind::External => ZPOOL_EXTERNAL_PREFIX,
150+
ZpoolKind::Internal => ZPOOL_INTERNAL_PREFIX,
151+
};
152+
write!(f, "{prefix}{}", self.id)
153+
}
154+
}
155+
156+
#[cfg(test)]
157+
mod test {
158+
use super::*;
159+
160+
#[test]
161+
fn test_zpool_name_regex() {
162+
let valid = [
163+
"oxi_d462a7f7-b628-40fe-80ff-4e4189e2d62b",
164+
"oxp_d462a7f7-b628-40fe-80ff-4e4189e2d62b",
165+
];
166+
167+
let invalid = [
168+
"",
169+
// Whitespace
170+
" oxp_d462a7f7-b628-40fe-80ff-4e4189e2d62b",
171+
"oxp_d462a7f7-b628-40fe-80ff-4e4189e2d62b ",
172+
// Case sensitivity
173+
"oxp_D462A7F7-b628-40fe-80ff-4e4189e2d62b",
174+
// Bad prefix
175+
"ox_d462a7f7-b628-40fe-80ff-4e4189e2d62b",
176+
"oxa_d462a7f7-b628-40fe-80ff-4e4189e2d62b",
177+
"oxi-d462a7f7-b628-40fe-80ff-4e4189e2d62b",
178+
"oxp-d462a7f7-b628-40fe-80ff-4e4189e2d62b",
179+
// Missing Prefix
180+
"d462a7f7-b628-40fe-80ff-4e4189e2d62b",
181+
// Bad UUIDs (Not following UUIDv4 format)
182+
"oxi_d462a7f7-b628-30fe-80ff-4e4189e2d62b",
183+
"oxi_d462a7f7-b628-40fe-c0ff-4e4189e2d62b",
184+
];
185+
186+
let r = regress::Regex::new(ZPOOL_NAME_REGEX)
187+
.expect("validation regex is valid");
188+
for input in valid {
189+
let m = r
190+
.find(input)
191+
.unwrap_or_else(|| panic!("input {input} did not match regex"));
192+
assert_eq!(m.start(), 0, "input {input} did not match start");
193+
assert_eq!(m.end(), input.len(), "input {input} did not match end");
194+
}
195+
196+
for input in invalid {
197+
assert!(
198+
r.find(input).is_none(),
199+
"invalid input {input} should not match validation regex"
200+
);
201+
}
202+
}
203+
204+
#[test]
205+
fn test_parse_zpool_name_json() {
206+
#[derive(Serialize, Deserialize, JsonSchema)]
207+
struct TestDataset {
208+
pool_name: ZpoolName,
209+
}
210+
211+
// Confirm that we can convert from a JSON string to a a ZpoolName
212+
let json_string =
213+
r#"{"pool_name":"oxi_d462a7f7-b628-40fe-80ff-4e4189e2d62b"}"#;
214+
let dataset: TestDataset = serde_json::from_str(json_string)
215+
.expect("Could not parse ZpoolName from Json Object");
216+
assert!(matches!(dataset.pool_name.kind, ZpoolKind::Internal));
217+
218+
// Confirm we can go the other way (ZpoolName to JSON string) too.
219+
let j = serde_json::to_string(&dataset)
220+
.expect("Cannot convert back to JSON string");
221+
assert_eq!(j, json_string);
222+
}
223+
224+
fn toml_string(s: &str) -> String {
225+
format!("zpool_name = \"{}\"", s)
226+
}
227+
228+
fn parse_name(s: &str) -> Result<ZpoolName, toml::de::Error> {
229+
toml_string(s)
230+
.parse::<toml::Value>()
231+
.expect("Cannot parse as TOML value")
232+
.get("zpool_name")
233+
.expect("Missing key")
234+
.clone()
235+
.try_into::<ZpoolName>()
236+
}
237+
238+
#[test]
239+
fn test_parse_external_zpool_name() {
240+
let uuid: ZpoolUuid =
241+
"d462a7f7-b628-40fe-80ff-4e4189e2d62b".parse().unwrap();
242+
let good_name = format!("{}{}", ZPOOL_EXTERNAL_PREFIX, uuid);
243+
244+
let name = parse_name(&good_name).expect("Cannot parse as ZpoolName");
245+
assert_eq!(uuid, name.id());
246+
assert_eq!(ZpoolKind::External, name.kind());
247+
}
248+
249+
#[test]
250+
fn test_parse_internal_zpool_name() {
251+
let uuid: ZpoolUuid =
252+
"d462a7f7-b628-40fe-80ff-4e4189e2d62b".parse().unwrap();
253+
let good_name = format!("{}{}", ZPOOL_INTERNAL_PREFIX, uuid);
254+
255+
let name = parse_name(&good_name).expect("Cannot parse as ZpoolName");
256+
assert_eq!(uuid, name.id());
257+
assert_eq!(ZpoolKind::Internal, name.kind());
258+
}
259+
260+
#[test]
261+
fn test_parse_bad_zpool_names() {
262+
let bad_names = vec![
263+
// Nonsense string
264+
"this string is GARBAGE",
265+
// Missing prefix
266+
"d462a7f7-b628-40fe-80ff-4e4189e2d62b",
267+
// Underscores
268+
"oxp_d462a7f7_b628_40fe_80ff_4e4189e2d62b",
269+
];
270+
271+
for bad_name in &bad_names {
272+
assert!(
273+
parse_name(&bad_name).is_err(),
274+
"Parsing {} should fail",
275+
bad_name
276+
);
277+
}
278+
}
279+
}

illumos-utils/src/zfs.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -622,7 +622,8 @@ pub fn get_all_omicron_datasets_for_delete() -> anyhow::Result<Vec<String>> {
622622
// This includes cockroachdb, clickhouse, and crucible datasets.
623623
let zpools = crate::zpool::Zpool::list()?;
624624
for pool in &zpools {
625-
let internal = pool.kind() == crate::zpool::ZpoolKind::Internal;
625+
let internal =
626+
pool.kind() == omicron_common::zpool_name::ZpoolKind::Internal;
626627
let pool = pool.to_string();
627628
for dataset in &Zfs::list_datasets(&pool)? {
628629
// Avoid erasing crashdump, backing data and swap datasets on

0 commit comments

Comments
 (0)