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

PHD: refactor & add support Propolis server "environments" #547

Merged
merged 8 commits into from
Oct 12, 2023
Merged
Show file tree
Hide file tree
Changes from 2 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
10 changes: 10 additions & 0 deletions .github/buildomat/jobs/phd-build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@
#: output_rules = [
#: "/out/*",
#: ]
pfmooney marked this conversation as resolved.
Show resolved Hide resolved
#:
#: [[publish]]
#: series = "phd_build"
#: name = "propolis-server-debug.tar.gz"
#: from_output = "/out/propolis-server-debug.tar.gz"
#:
#: [[publish]]
#: series = "phd_build"
#: name = "propolis-server-debug.sha256.txt"
#: from_output = "/out/propolis-server-debug.sha256.txt"

set -o errexit
set -o pipefail
Expand Down
2 changes: 2 additions & 0 deletions phd-tests/framework/src/artifacts/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ mod store;

pub use store::Store as ArtifactStore;

pub const DEFAULT_PROPOLIS_ARTIFACT: &str = "__DEFAULT_PROPOLIS";

#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
enum ArtifactKind {
Expand Down
82 changes: 72 additions & 10 deletions phd-tests/framework/src/artifacts/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

use crate::{
artifacts::{manifest::Manifest, ArtifactSource},
artifacts::{
manifest::Manifest, ArtifactSource, DEFAULT_PROPOLIS_ARTIFACT,
},
guest_os::GuestOsKind,
};

Expand All @@ -24,6 +26,10 @@ struct StoredArtifact {
}

impl StoredArtifact {
fn new(description: super::Artifact) -> Self {
Self { description, cached_path: None }
}

fn ensure(
&mut self,
local_dir: &Utf8Path,
Expand Down Expand Up @@ -194,22 +200,56 @@ impl Store {
let Manifest { artifacts, remote_server_uris } = manifest;
let artifacts = artifacts
.into_iter()
.map(|(k, v)| {
(
k,
Mutex::new(StoredArtifact {
description: v,
cached_path: None,
}),
)
})
.map(|(k, v)| (k, Mutex::new(StoredArtifact::new(v))))
.collect();

let store = Self { local_dir, artifacts, remote_server_uris };
info!(?store, "Created new artifact store from manifest");
store
}

pub fn add_propolis_from_local_cmd(
&mut self,
propolis_server_cmd: &Utf8Path,
) -> anyhow::Result<()> {
if self.artifacts.contains_key(DEFAULT_PROPOLIS_ARTIFACT) {
anyhow::bail!(
"artifact store already contains key {}",
DEFAULT_PROPOLIS_ARTIFACT
);
}

let full_path = propolis_server_cmd.canonicalize_utf8()?;
let filename = full_path.file_name().ok_or_else(|| {
anyhow::anyhow!(
"Propolis server command '{}' contains no file component",
propolis_server_cmd
)
})?;
let dir = full_path.parent().ok_or_else(|| {
anyhow::anyhow!(
"canonicalized Propolis path '{}' has no directory component",
full_path
)
})?;

let artifact = super::Artifact {
filename: filename.to_string(),
kind: super::ArtifactKind::PropolisServer,
source: super::ArtifactSource::LocalPath {
path: dir.to_path_buf(),
sha256: None,
},
};

let _old = self.artifacts.insert(
DEFAULT_PROPOLIS_ARTIFACT.to_string(),
Mutex::new(StoredArtifact::new(artifact)),
);
assert!(_old.is_none());
Ok(())
}

pub fn get_guest_os_image(
&self,
artifact_name: &str,
Expand Down Expand Up @@ -255,6 +295,28 @@ impl Store {
)),
}
}

pub fn get_propolis_server(
&self,
artifact_name: &str,
) -> anyhow::Result<Utf8PathBuf> {
let entry = if let Some(e) = self.artifacts.get(artifact_name) {
pfmooney marked this conversation as resolved.
Show resolved Hide resolved
e
} else {
anyhow::bail!("artifact {} not found in store", artifact_name);
};

let mut guard = entry.lock().unwrap();
match guard.description.kind {
super::ArtifactKind::PropolisServer => {
guard.ensure(&self.local_dir, &self.remote_server_uris)
}
_ => Err(anyhow::anyhow!(
"artifact {} is not a Propolis server",
artifact_name
)),
}
}
}

fn sha256_digest(file: &mut File) -> anyhow::Result<Digest> {
Expand Down
26 changes: 19 additions & 7 deletions phd-tests/framework/src/disk/crucible.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ impl Drop for Downstairs {
/// An RAII wrapper around a Crucible disk.
#[derive(Debug)]
pub struct CrucibleDisk {
/// The name of the backend to use in instance specs that include this disk.
backend_name: String,

/// The UUID to insert into this disk's `VolumeConstructionRequest`s.
id: Uuid,

Expand Down Expand Up @@ -91,6 +94,7 @@ impl CrucibleDisk {
/// `data_dir`.
#[allow(clippy::too_many_arguments)]
pub(crate) fn new(
backend_name: String,
disk_size_gib: u64,
block_size: BlockSize,
downstairs_binary_path: &impl AsRef<std::ffi::OsStr>,
Expand Down Expand Up @@ -205,6 +209,7 @@ impl CrucibleDisk {
}

Ok(Self {
backend_name,
id: disk_uuid,
block_size,
blocks_per_extent,
Expand Down Expand Up @@ -233,7 +238,7 @@ impl CrucibleDisk {
}

impl super::DiskConfig for CrucibleDisk {
fn backend_spec(&self) -> instance_spec::v0::StorageBackendV0 {
fn backend_spec(&self) -> (String, instance_spec::v0::StorageBackendV0) {
let gen = self.generation.load(Ordering::Relaxed);
let downstairs_addrs =
self.downstairs_instances.iter().map(|ds| ds.address).collect();
Expand Down Expand Up @@ -268,18 +273,25 @@ impl super::DiskConfig for CrucibleDisk {
}),
};

instance_spec::v0::StorageBackendV0::Crucible(
instance_spec::components::backends::CrucibleStorageBackend {
request_json: serde_json::to_string(&vcr)
.expect("VolumeConstructionRequest should serialize"),
readonly: false,
},
(
self.backend_name.clone(),
instance_spec::v0::StorageBackendV0::Crucible(
instance_spec::components::backends::CrucibleStorageBackend {
request_json: serde_json::to_string(&vcr)
.expect("VolumeConstructionRequest should serialize"),
readonly: false,
},
),
)
}

fn guest_os(&self) -> Option<GuestOsKind> {
self.guest_os
}

fn as_crucible(&self) -> Option<&CrucibleDisk> {
Some(self)
}
}

fn run_crucible_downstairs(
Expand Down
19 changes: 13 additions & 6 deletions phd-tests/framework/src/disk/file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ use crate::guest_os::GuestOsKind;
/// An RAII wrapper for a disk wrapped by a file.
#[derive(Debug)]
pub struct FileBackedDisk {
/// The name to use for instance spec backends that refer to this disk.
backend_name: String,

/// The path at which the disk is stored.
disk_path: PathBuf,

Expand All @@ -29,6 +32,7 @@ impl FileBackedDisk {
/// Creates a new file-backed disk whose initial contents are copied from
/// the specified artifact on the host file system.
pub(crate) fn new_from_artifact(
backend_name: String,
artifact_path: &impl AsRef<Path>,
data_dir: &impl AsRef<Path>,
guest_os: Option<GuestOsKind>,
Expand All @@ -55,16 +59,19 @@ impl FileBackedDisk {
permissions.set_readonly(false);
disk_file.set_permissions(permissions)?;

Ok(Self { disk_path, guest_os })
Ok(Self { backend_name, disk_path, guest_os })
}
}

impl super::DiskConfig for FileBackedDisk {
fn backend_spec(&self) -> StorageBackendV0 {
StorageBackendV0::File(FileStorageBackend {
path: self.disk_path.to_string_lossy().to_string(),
readonly: false,
})
fn backend_spec(&self) -> (String, StorageBackendV0) {
(
self.backend_name.clone(),
StorageBackendV0::File(FileStorageBackend {
path: self.disk_path.to_string_lossy().to_string(),
readonly: false,
}),
)
}

fn guest_os(&self) -> Option<GuestOsKind> {
Expand Down
35 changes: 25 additions & 10 deletions phd-tests/framework/src/disk/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

use std::{
path::{Path, PathBuf},
rc::Rc,
sync::Arc,
};

Expand Down Expand Up @@ -63,11 +64,17 @@ impl BlockSize {
/// A trait for functions exposed by all disk backends (files, Crucible, etc.).
pub trait DiskConfig: std::fmt::Debug {
/// Yields the backend spec for this disk's storage backend.
fn backend_spec(&self) -> StorageBackendV0;
fn backend_spec(&self) -> (String, StorageBackendV0);

/// Yields the guest OS kind of the guest image the disk was created from,
/// or `None` if the disk was not created from a guest image.
fn guest_os(&self) -> Option<GuestOsKind>;

/// If this disk is a Crucible disk, yields `Some` reference to that disk as
/// a Crucible disk.
fn as_crucible(&self) -> Option<&CrucibleDisk> {
None
}
}

/// The possible sources for a disk's initial data.
Expand Down Expand Up @@ -100,35 +107,35 @@ pub enum DiskSource<'a> {
/// multiple VMs do use a single set of backend resources, the resulting
/// behavior will depend on the chosen backend's semantics and the way the
/// Propolis backend implementations interact with the disk.
pub struct DiskFactory<'a> {
pub(crate) struct DiskFactory {
/// The directory in which disk files should be stored.
storage_dir: PathBuf,

/// A reference to the artifact store to use to look up guest OS artifacts
/// when those are used as a disk source.
artifact_store: &'a ArtifactStore,
artifact_store: Rc<ArtifactStore>,

/// The path to the Crucible downstairs binary to launch to serve Crucible
/// disks.
crucible_downstairs_binary: Option<PathBuf>,

/// The port allocator to use to allocate ports to Crucible server
/// processes.
port_allocator: &'a PortAllocator,
port_allocator: Rc<PortAllocator>,

/// The logging discipline to use for Crucible server processes.
log_mode: ServerLogMode,
}

impl<'a> DiskFactory<'a> {
impl DiskFactory {
/// Creates a new disk factory. The disks this factory generates will store
/// their data in `storage_dir` and will look up guest OS images in the
/// supplied `artifact_store`.
pub fn new(
storage_dir: &impl AsRef<Path>,
artifact_store: &'a ArtifactStore,
artifact_store: Rc<ArtifactStore>,
crucible_downstairs_binary: Option<&impl AsRef<Path>>,
port_allocator: &'a PortAllocator,
port_allocator: Rc<PortAllocator>,
log_mode: ServerLogMode,
) -> Self {
Self {
Expand All @@ -140,9 +147,13 @@ impl<'a> DiskFactory<'a> {
log_mode,
}
}

pub fn crucible_enabled(&self) -> bool {
self.crucible_downstairs_binary.is_some()
}
}

impl DiskFactory<'_> {
impl DiskFactory {
fn get_guest_artifact_info(
&self,
artifact_name: &str,
Expand All @@ -158,15 +169,17 @@ impl DiskFactory<'_> {

/// Creates a new disk backed by a file whose initial contents are specified
/// by `source`.
pub fn create_file_backed_disk(
pub(crate) fn create_file_backed_disk(
&self,
name: String,
source: DiskSource,
) -> Result<Arc<FileBackedDisk>, DiskError> {
let DiskSource::Artifact(artifact_name) = source;
let (artifact_path, guest_os) =
self.get_guest_artifact_info(artifact_name)?;

FileBackedDisk::new_from_artifact(
name,
&artifact_path,
&self.storage_dir,
Some(guest_os),
Expand All @@ -186,8 +199,9 @@ impl DiskFactory<'_> {
/// file should be used as a read-only parent volume.
/// - disk_size_gib: The disk's expected size in GiB.
/// - block_size: The disk's block size.
pub fn create_crucible_disk(
pub(crate) fn create_crucible_disk(
&self,
name: String,
source: DiskSource,
disk_size_gib: u64,
block_size: BlockSize,
Expand All @@ -207,6 +221,7 @@ impl DiskFactory<'_> {
}

CrucibleDisk::new(
name,
disk_size_gib,
block_size,
binary_path,
Expand Down
Loading
Loading