Skip to content

Commit b2467e3

Browse files
authored
sled agent: implement OmicronZoneImageSource::Artifact (#7781)
Closes #7281.
1 parent 2b830dc commit b2467e3

File tree

6 files changed

+198
-37
lines changed

6 files changed

+198
-37
lines changed

illumos-utils/src/running_zone.rs

+21-3
Original file line numberDiff line numberDiff line change
@@ -1044,6 +1044,9 @@ pub struct ZoneBuilder<'a> {
10441044
/// The directories that will be searched for the image tarball for the
10451045
/// provided zone type ([`Self::with_zone_type`]).
10461046
zone_image_paths: Option<&'a [Utf8PathBuf]>,
1047+
/// The file name of the zone image to search for in [`Self::zone_image_paths`].
1048+
/// If unset, defaults to `{zone_type}.tar.gz`.
1049+
zone_image_file_name: Option<&'a str>,
10471050
/// The name of the type of zone being created (e.g. "propolis-server")
10481051
zone_type: Option<&'a str>,
10491052
/// Unique ID of the instance of the zone being created. (optional)
@@ -1110,6 +1113,17 @@ impl<'a> ZoneBuilder<'a> {
11101113
self
11111114
}
11121115

1116+
/// The file name of the zone image to search for in the zone image
1117+
/// paths ([`Self::with_zone_image_paths`]). If unset, defaults to
1118+
/// `{zone_type}.tar.gz`.
1119+
pub fn with_zone_image_file_name(
1120+
mut self,
1121+
image_file_name: &'a str,
1122+
) -> Self {
1123+
self.zone_image_file_name = Some(image_file_name);
1124+
self
1125+
}
1126+
11131127
/// The name of the type of zone being created (e.g. "propolis-server")
11141128
pub fn with_zone_type(mut self, zone_type: &'a str) -> Self {
11151129
self.zone_type = Some(zone_type);
@@ -1227,6 +1241,7 @@ impl<'a> ZoneBuilder<'a> {
12271241
underlay_vnic_allocator: Some(underlay_vnic_allocator),
12281242
zone_root_path: Some(mut zone_root_path),
12291243
zone_image_paths: Some(zone_image_paths),
1244+
zone_image_file_name,
12301245
zone_type: Some(zone_type),
12311246
unique_name,
12321247
datasets: Some(datasets),
@@ -1255,15 +1270,18 @@ impl<'a> ZoneBuilder<'a> {
12551270
InstalledZone::get_zone_name(zone_type, unique_name);
12561271

12571272
// Looks for the image within `zone_image_path`, in order.
1258-
let image = format!("{}.tar.gz", zone_type);
1273+
let image_file_name = match zone_image_file_name {
1274+
Some(image) => image,
1275+
None => &format!("{}.tar.gz", zone_type),
1276+
};
12591277
let zone_image_path = zone_image_paths
12601278
.iter()
12611279
.find_map(|image_path| {
1262-
let path = image_path.join(&image);
1280+
let path = image_path.join(image_file_name);
12631281
if path.exists() { Some(path) } else { None }
12641282
})
12651283
.ok_or_else(|| InstallZoneError::ImageNotFound {
1266-
image: image.to_string(),
1284+
image: image_file_name.to_string(),
12671285
paths: zone_image_paths
12681286
.iter()
12691287
.map(|p| p.to_path_buf())

nexus-sled-agent-shared/src/inventory.rs

+12
Original file line numberDiff line numberDiff line change
@@ -654,6 +654,18 @@ pub enum OmicronZoneImageSource {
654654
Artifact { hash: ArtifactHash },
655655
}
656656

657+
impl OmicronZoneImageSource {
658+
/// Return the artifact hash used for the zone image, if the zone's image
659+
/// source is from the artifact store.
660+
pub fn artifact_hash(&self) -> Option<ArtifactHash> {
661+
if let OmicronZoneImageSource::Artifact { hash } = self {
662+
Some(*hash)
663+
} else {
664+
None
665+
}
666+
}
667+
}
668+
657669
// See `OmicronZoneConfig`. This is a separate function instead of being `impl
658670
// Default` because we don't want to accidentally use this default in Rust code.
659671
fn deserialize_image_source_default() -> OmicronZoneImageSource {

sled-agent/src/artifact_store.rs

+55-11
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
//! Operations that list or modify artifacts or the configuration are called by
1818
//! Nexus and handled by the Sled Agent API.
1919
20+
use std::collections::BTreeMap;
2021
use std::future::Future;
2122
use std::io::{ErrorKind, Write};
2223
use std::net::SocketAddrV6;
@@ -48,6 +49,8 @@ use tokio::sync::{mpsc, oneshot, watch};
4849
use tokio::task::JoinSet;
4950
use tufaceous_artifact::ArtifactHash;
5051

52+
use crate::services::ServiceManager;
53+
5154
// These paths are defined under the artifact storage dataset. They
5255
// cannot conflict with any artifact paths because all artifact paths are
5356
// hexadecimal-encoded SHA-256 checksums.
@@ -87,7 +90,11 @@ pub(crate) struct ArtifactStore<T: DatasetsManager> {
8790
}
8891

8992
impl<T: DatasetsManager> ArtifactStore<T> {
90-
pub(crate) async fn new(log: &Logger, storage: T) -> ArtifactStore<T> {
93+
pub(crate) async fn new(
94+
log: &Logger,
95+
storage: T,
96+
services: Option<ServiceManager>,
97+
) -> ArtifactStore<T> {
9198
let log = log.new(slog::o!("component" => "ArtifactStore"));
9299

93100
let mut ledger_paths = Vec::new();
@@ -126,6 +133,7 @@ impl<T: DatasetsManager> ArtifactStore<T> {
126133
tokio::task::spawn(ledger_manager(
127134
log.clone(),
128135
ledger_paths,
136+
services,
129137
ledger_rx,
130138
config_tx,
131139
));
@@ -244,15 +252,27 @@ impl<T: DatasetsManager> ArtifactStore<T> {
244252
pub(crate) async fn get(
245253
&self,
246254
sha256: ArtifactHash,
255+
) -> Result<File, Error> {
256+
Self::get_from_storage(&self.storage, &self.log, sha256).await
257+
}
258+
259+
/// Open an artifact file by hash from a storage handle.
260+
///
261+
/// This is the same as [ArtifactStore::get], but can be called with only
262+
/// a [StorageHandle].
263+
pub(crate) async fn get_from_storage(
264+
storage: &T,
265+
log: &Logger,
266+
sha256: ArtifactHash,
247267
) -> Result<File, Error> {
248268
let sha256_str = sha256.to_string();
249269
let mut last_error = None;
250-
for mountpoint in self.storage.artifact_storage_paths().await {
270+
for mountpoint in storage.artifact_storage_paths().await {
251271
let path = mountpoint.join(&sha256_str);
252272
match File::open(&path).await {
253273
Ok(file) => {
254274
info!(
255-
&self.log,
275+
&log,
256276
"Retrieved artifact";
257277
"sha256" => &sha256_str,
258278
"path" => path.as_str(),
@@ -261,7 +281,7 @@ impl<T: DatasetsManager> ArtifactStore<T> {
261281
}
262282
Err(err) if err.kind() == ErrorKind::NotFound => {}
263283
Err(err) => {
264-
log_and_store!(last_error, &self.log, "open", path, err);
284+
log_and_store!(last_error, &log, "open", path, err);
265285
}
266286
}
267287
}
@@ -430,9 +450,11 @@ type LedgerManagerRequest =
430450
async fn ledger_manager(
431451
log: Logger,
432452
ledger_paths: Vec<Utf8PathBuf>,
453+
services: Option<ServiceManager>,
433454
mut rx: mpsc::Receiver<LedgerManagerRequest>,
434455
config_channel: watch::Sender<Option<ArtifactConfig>>,
435456
) {
457+
let services = services.as_ref();
436458
let handle_request = async |new_config: ArtifactConfig| {
437459
if ledger_paths.is_empty() {
438460
return Err(Error::NoUpdateDataset);
@@ -441,7 +463,25 @@ async fn ledger_manager(
441463
Ledger::<ArtifactConfig>::new(&log, ledger_paths.clone()).await
442464
{
443465
if new_config.generation > ledger.data().generation {
444-
// New config generation; update the ledger.
466+
// New config generation. First check that the configuration
467+
// contains all artifacts that are presently in use.
468+
let mut missing = BTreeMap::new();
469+
// Check artifacts from the current zone configuration.
470+
if let Some(services) = services {
471+
for zone in services.omicron_zones_list().await.zones {
472+
if let Some(hash) = zone.image_source.artifact_hash() {
473+
if !new_config.artifacts.contains(&hash) {
474+
missing
475+
.insert(hash, "current zone configuration");
476+
}
477+
}
478+
}
479+
}
480+
if !missing.is_empty() {
481+
return Err(Error::InUseArtifactsMissing(missing));
482+
}
483+
484+
// Everything looks okay; update the ledger.
445485
*ledger.data_mut() = new_config;
446486
ledger
447487
} else if new_config == *ledger.data() {
@@ -740,7 +780,7 @@ impl RepoDepotApi for RepoDepotImpl {
740780
}
741781

742782
#[derive(Debug, thiserror::Error, SlogInlineError)]
743-
pub(crate) enum Error {
783+
pub enum Error {
744784
#[error("Error while reading request body")]
745785
Body(dropshot::HttpError),
746786

@@ -784,6 +824,9 @@ pub(crate) enum Error {
784824
#[error("Digest mismatch: expected {expected}, actual {actual}")]
785825
HashMismatch { expected: ArtifactHash, actual: ArtifactHash },
786826

827+
#[error("Artifacts in use are not present in new config: {0:?}")]
828+
InUseArtifactsMissing(BTreeMap<ArtifactHash, &'static str>),
829+
787830
#[error("Blocking task failed")]
788831
Join(#[source] tokio::task::JoinError),
789832

@@ -813,6 +856,7 @@ impl From<Error> for HttpError {
813856
match err {
814857
// 4xx errors
815858
Error::HashMismatch { .. }
859+
| Error::InUseArtifactsMissing { .. }
816860
| Error::NoConfig
817861
| Error::NotInConfig { .. } => {
818862
HttpError::for_bad_request(None, err.to_string())
@@ -951,7 +995,7 @@ mod test {
951995

952996
let log = test_setup_log("generations");
953997
let backend = TestBackend::new(2);
954-
let store = ArtifactStore::new(&log.log, backend).await;
998+
let store = ArtifactStore::new(&log.log, backend, None).await;
955999

9561000
// get_config returns None
9571001
assert!(store.get_config().is_none());
@@ -1004,7 +1048,7 @@ mod test {
10041048
async fn list_get_put() {
10051049
let log = test_setup_log("list_get_put");
10061050
let backend = TestBackend::new(2);
1007-
let mut store = ArtifactStore::new(&log.log, backend).await;
1051+
let mut store = ArtifactStore::new(&log.log, backend, None).await;
10081052

10091053
// get fails, because it doesn't exist yet
10101054
assert!(matches!(
@@ -1126,7 +1170,7 @@ mod test {
11261170

11271171
let log = test_setup_log("no_dataset");
11281172
let backend = TestBackend::new(0);
1129-
let store = ArtifactStore::new(&log.log, backend).await;
1173+
let store = ArtifactStore::new(&log.log, backend, None).await;
11301174

11311175
assert!(matches!(
11321176
store.get(TEST_HASH).await,
@@ -1154,7 +1198,7 @@ mod test {
11541198

11551199
let log = test_setup_log("wrong_hash");
11561200
let backend = TestBackend::new(2);
1157-
let store = ArtifactStore::new(&log.log, backend).await;
1201+
let store = ArtifactStore::new(&log.log, backend, None).await;
11581202
let mut config = ArtifactConfig {
11591203
generation: 1u32.into(),
11601204
artifacts: BTreeSet::new(),
@@ -1214,7 +1258,7 @@ mod test {
12141258

12151259
let log = test_setup_log("issue_7796");
12161260
let backend = TestBackend::new(2);
1217-
let store = ArtifactStore::new(&log.log, backend).await;
1261+
let store = ArtifactStore::new(&log.log, backend, None).await;
12181262

12191263
let mut config = ArtifactConfig {
12201264
generation: 1u32.into(),

0 commit comments

Comments
 (0)