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 all 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.tar.gz"
#: from_output = "/out/propolis-server-debug.tar.gz"
#:
#: [[publish]]
#: series = "phd_build"
#: name = "propolis-server.sha256.txt"
#: from_output = "/out/propolis-server-debug.sha256.txt"

set -o errexit
set -o pipefail
Expand Down
7 changes: 4 additions & 3 deletions phd-tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ path = "/home/oxide/propolis/target/debug"
## Authoring tests

PHD's test cases live in the `tests` crate. To write a new test, add a function
of the form `fn my_test(ctx: &TestContext)` and tag it with the
of the form `fn my_test(ctx: &Framework)` and tag it with the
`#[phd_testcase]` attribute macro. The framework will automatically register the
test into the crate's test inventory for the runner to discover.

Expand All @@ -156,8 +156,9 @@ function body is reached. This means that

### Test context

The `TestContext` structure contains a `VmFactory` helper object that tests can
use to construct VMs. See the module documentation for more information.
Every test gets a `phd_testcase::Framework` that contains helper methods for
constructing VMs and their execution environments. See the module documentation
for more information.

The tests in `tests/src/smoke.rs` provide some simple examples of using the
factory to customize and launch a new VM.
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
96 changes: 76 additions & 20 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,31 +200,63 @@ 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,
) -> anyhow::Result<(Utf8PathBuf, GuestOsKind)> {
let entry = if let Some(e) = self.artifacts.get(artifact_name) {
e
} else {
anyhow::bail!("artifact {} not found in store", artifact_name);
};
let entry = self.artifacts.get(artifact_name).ok_or_else(|| {
anyhow::anyhow!("artifact {} not found in store", artifact_name)
})?;

let mut guard = entry.lock().unwrap();
match guard.description.kind {
Expand All @@ -238,11 +276,9 @@ impl Store {
&self,
artifact_name: &str,
) -> anyhow::Result<Utf8PathBuf> {
let entry = if let Some(e) = self.artifacts.get(artifact_name) {
e
} else {
anyhow::bail!("artifact {} not found in store", artifact_name);
};
let entry = self.artifacts.get(artifact_name).ok_or_else(|| {
anyhow::anyhow!("artifact {} not found in store", artifact_name)
})?;

let mut guard = entry.lock().unwrap();
match guard.description.kind {
Expand All @@ -255,6 +291,26 @@ impl Store {
)),
}
}

pub fn get_propolis_server(
&self,
artifact_name: &str,
) -> anyhow::Result<Utf8PathBuf> {
let entry = self.artifacts.get(artifact_name).ok_or_else(|| {
anyhow::anyhow!("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
Loading
Loading