Skip to content

Commit f493515

Browse files
authored
Add two features for local storage support (#9386)
Bump the sled-agent API to add two new features for local storage support: - Add an endpoint for Nexus to create and destroy local storage datasets. These will be allocated and deallocated as part of the higher level Disk lifecycle for the forthcoming local storage disk type. - Add the ability to delegate a specific zvol to a Propolis zone. This required accepting a new `DelegatedZvol` parameter during vmm registration.
1 parent f83ca6f commit f493515

File tree

18 files changed

+9679
-51
lines changed

18 files changed

+9679
-51
lines changed

common/src/api/internal/shared.rs

Lines changed: 89 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,12 @@
77
use crate::{
88
address::NUM_SOURCE_NAT_PORTS,
99
api::external::{self, BfdMode, ImportExportPolicy, Name, Vni},
10+
disk::DatasetName,
11+
zpool_name::ZpoolName,
1012
};
1113
use daft::Diffable;
14+
use omicron_uuid_kinds::DatasetUuid;
15+
use omicron_uuid_kinds::ExternalZpoolUuid;
1216
use oxnet::{IpNet, Ipv4Net, Ipv6Net};
1317
use schemars::JsonSchema;
1418
use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
@@ -944,7 +948,7 @@ pub enum DatasetKind {
944948
// Other datasets
945949
Debug,
946950

947-
/// Used for transient storage, contains volumes delegated to VMMs
951+
/// Used for local storage disk types, contains volumes delegated to VMMs
948952
LocalStorage,
949953
}
950954

@@ -1114,6 +1118,71 @@ pub struct SledIdentifiers {
11141118
pub serial: String,
11151119
}
11161120

1121+
/// Delegate a ZFS volume to a zone
1122+
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
1123+
#[serde(tag = "type", rename_all = "snake_case")]
1124+
pub enum DelegatedZvol {
1125+
/// Delegate a slice of the local storage dataset present on this pool into
1126+
/// the zone.
1127+
LocalStorage { zpool_id: ExternalZpoolUuid, dataset_id: DatasetUuid },
1128+
}
1129+
1130+
impl DelegatedZvol {
1131+
/// Return the fully qualified dataset name that the volume is in.
1132+
pub fn parent_dataset_name(&self) -> String {
1133+
match &self {
1134+
DelegatedZvol::LocalStorage { zpool_id, dataset_id } => {
1135+
// The local storage dataset is the parent for an allocation
1136+
let local_storage_parent = DatasetName::new(
1137+
ZpoolName::External(*zpool_id),
1138+
DatasetKind::LocalStorage,
1139+
);
1140+
1141+
format!("{}/{}", local_storage_parent.full_name(), dataset_id)
1142+
}
1143+
}
1144+
}
1145+
1146+
/// Return the mountpoint for the parent dataset in the zone
1147+
pub fn parent_dataset_mountpoint(&self) -> String {
1148+
match &self {
1149+
DelegatedZvol::LocalStorage { dataset_id, .. } => {
1150+
format!("/{}", dataset_id)
1151+
}
1152+
}
1153+
}
1154+
1155+
/// Return the fully qualified volume name
1156+
pub fn volume_name(&self) -> String {
1157+
match &self {
1158+
DelegatedZvol::LocalStorage { .. } => {
1159+
// For now, all local storage zvols use the same name
1160+
format!("{}/vol", self.parent_dataset_name())
1161+
}
1162+
}
1163+
}
1164+
1165+
/// Return the device that should be delegated into the zone
1166+
pub fn zvol_device(&self) -> String {
1167+
match &self {
1168+
DelegatedZvol::LocalStorage { .. } => {
1169+
// Use the `rdsk` device to avoid interacting with an additional
1170+
// buffer cache that would be used if we used `dsk`.
1171+
format!("/dev/zvol/rdsk/{}", self.volume_name())
1172+
}
1173+
}
1174+
}
1175+
1176+
pub fn volblocksize(&self) -> u32 {
1177+
match &self {
1178+
DelegatedZvol::LocalStorage { .. } => {
1179+
// all Local storage zvols use 4096 byte blocks
1180+
4096
1181+
}
1182+
}
1183+
}
1184+
}
1185+
11171186
#[cfg(test)]
11181187
mod tests {
11191188
use super::*;
@@ -1207,4 +1276,23 @@ mod tests {
12071276
);
12081277
}
12091278
}
1279+
1280+
#[test]
1281+
fn test_delegated_zvol_device_name() {
1282+
let delegated_zvol = DelegatedZvol::LocalStorage {
1283+
zpool_id: "cb832c2e-fa94-4911-89a9-895ac8b1e8f3".parse().unwrap(),
1284+
dataset_id: "2bbf0908-21da-4bc3-882b-1a1e715c54bd".parse().unwrap(),
1285+
};
1286+
1287+
assert_eq!(
1288+
delegated_zvol.zvol_device(),
1289+
[
1290+
String::from("/dev/zvol/rdsk"),
1291+
String::from("oxp_cb832c2e-fa94-4911-89a9-895ac8b1e8f3/crypt"),
1292+
String::from("local_storage"),
1293+
String::from("2bbf0908-21da-4bc3-882b-1a1e715c54bd/vol"),
1294+
]
1295+
.join("/"),
1296+
);
1297+
}
12101298
}

illumos-utils/src/zfs.rs

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,76 @@ pub struct DestroySnapshotError {
220220
err: crate::ExecutionError,
221221
}
222222

223+
#[derive(thiserror::Error, Debug)]
224+
pub enum EnsureDatasetVolumeErrorInner {
225+
#[error(transparent)]
226+
Execution(#[from] crate::ExecutionError),
227+
228+
#[error(transparent)]
229+
GetValue(#[from] GetValueError),
230+
231+
#[error("value {value_name} parse error: {value} not a number!")]
232+
ValueParseError { value_name: String, value: String },
233+
234+
#[error("expected {value_name} to be {expected}, but saw {actual}")]
235+
ValueMismatch { value_name: String, expected: u64, actual: u64 },
236+
}
237+
238+
/// Error returned by [`Zfs::ensure_dataset_volume`].
239+
#[derive(thiserror::Error, Debug)]
240+
#[error("Failed to ensure volume '{name}': {err}")]
241+
pub struct EnsureDatasetVolumeError {
242+
name: String,
243+
#[source]
244+
err: EnsureDatasetVolumeErrorInner,
245+
}
246+
247+
impl EnsureDatasetVolumeError {
248+
pub fn execution(name: String, err: crate::ExecutionError) -> Self {
249+
EnsureDatasetVolumeError {
250+
name,
251+
err: EnsureDatasetVolumeErrorInner::Execution(err),
252+
}
253+
}
254+
255+
pub fn get_value(name: String, err: GetValueError) -> Self {
256+
EnsureDatasetVolumeError {
257+
name,
258+
err: EnsureDatasetVolumeErrorInner::GetValue(err),
259+
}
260+
}
261+
262+
pub fn value_parse(
263+
name: String,
264+
value_name: String,
265+
value: String,
266+
) -> Self {
267+
EnsureDatasetVolumeError {
268+
name,
269+
err: EnsureDatasetVolumeErrorInner::ValueParseError {
270+
value_name,
271+
value,
272+
},
273+
}
274+
}
275+
276+
pub fn value_mismatch(
277+
name: String,
278+
value_name: String,
279+
expected: u64,
280+
actual: u64,
281+
) -> Self {
282+
EnsureDatasetVolumeError {
283+
name,
284+
err: EnsureDatasetVolumeErrorInner::ValueMismatch {
285+
value_name,
286+
expected,
287+
actual,
288+
},
289+
}
290+
}
291+
}
292+
223293
/// Wraps commands for interacting with ZFS.
224294
pub struct Zfs {}
225295

@@ -1339,6 +1409,88 @@ impl Zfs {
13391409
}
13401410
Ok(result)
13411411
}
1412+
1413+
pub async fn ensure_dataset_volume(
1414+
name: String,
1415+
size: ByteCount,
1416+
block_size: u32,
1417+
) -> Result<(), EnsureDatasetVolumeError> {
1418+
let mut command = Command::new(PFEXEC);
1419+
let cmd = command.args(&[ZFS, "create"]);
1420+
1421+
cmd.args(&[
1422+
"-V",
1423+
&size.to_bytes().to_string(),
1424+
"-o",
1425+
&format!("volblocksize={}", block_size),
1426+
&name,
1427+
]);
1428+
1429+
// The command to create a dataset is not idempotent and will fail with
1430+
// "dataset already exists" if the volume is created already. Eat this
1431+
// and return Ok instead.
1432+
1433+
match execute_async(cmd).await {
1434+
Ok(_) => Ok(()),
1435+
1436+
Err(crate::ExecutionError::CommandFailure(info))
1437+
if info.stderr.contains("dataset already exists") =>
1438+
{
1439+
// Validate that the total size and volblocksize are what is
1440+
// being requested: these cannot be changed once the volume is
1441+
// created.
1442+
1443+
let [actual_size, actual_block_size] =
1444+
Self::get_values(&name, &["volsize", "volblocksize"], None)
1445+
.await
1446+
.map_err(|err| {
1447+
EnsureDatasetVolumeError::get_value(
1448+
name.clone(),
1449+
err,
1450+
)
1451+
})?;
1452+
1453+
let actual_size: u64 = actual_size.parse().map_err(|_| {
1454+
EnsureDatasetVolumeError::value_parse(
1455+
name.clone(),
1456+
String::from("volsize"),
1457+
actual_size,
1458+
)
1459+
})?;
1460+
1461+
let actual_block_size: u32 =
1462+
actual_block_size.parse().map_err(|_| {
1463+
EnsureDatasetVolumeError::value_parse(
1464+
name.clone(),
1465+
String::from("volblocksize"),
1466+
actual_block_size,
1467+
)
1468+
})?;
1469+
1470+
if actual_size != size.to_bytes() {
1471+
return Err(EnsureDatasetVolumeError::value_mismatch(
1472+
name.clone(),
1473+
String::from("volsize"),
1474+
size.to_bytes(),
1475+
actual_size,
1476+
));
1477+
}
1478+
1479+
if actual_block_size != block_size {
1480+
return Err(EnsureDatasetVolumeError::value_mismatch(
1481+
name.clone(),
1482+
String::from("volblocksize"),
1483+
u64::from(block_size),
1484+
u64::from(actual_block_size),
1485+
));
1486+
}
1487+
1488+
Ok(())
1489+
}
1490+
1491+
Err(err) => Err(EnsureDatasetVolumeError::execution(name, err)),
1492+
}
1493+
}
13421494
}
13431495

13441496
/// A read-only snapshot of a ZFS filesystem.

nexus/mgs-updates/src/test_util/host_phase_2_test_state.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -890,5 +890,20 @@ mod api_impl {
890890
) -> Result<HttpResponseUpdatedNoContent, HttpError> {
891891
unimplemented!()
892892
}
893+
894+
async fn local_storage_dataset_ensure(
895+
_request_context: RequestContext<Self::Context>,
896+
_path_params: Path<LocalStoragePathParam>,
897+
_body: TypedBody<LocalStorageDatasetEnsureRequest>,
898+
) -> Result<HttpResponseUpdatedNoContent, HttpError> {
899+
unimplemented!()
900+
}
901+
902+
async fn local_storage_dataset_delete(
903+
_request_context: RequestContext<Self::Context>,
904+
_path_params: Path<LocalStoragePathParam>,
905+
) -> Result<HttpResponseUpdatedNoContent, HttpError> {
906+
unimplemented!()
907+
}
893908
}
894909
}

nexus/src/app/instance.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1511,6 +1511,7 @@ impl super::Nexus {
15111511
host_domain: None,
15121512
search_domains: Vec::new(),
15131513
},
1514+
delegated_zvols: vec![],
15141515
};
15151516

15161517
let instance_id = InstanceUuid::from_untyped_uuid(db_instance.id());

0 commit comments

Comments
 (0)