diff --git a/Cargo.lock b/Cargo.lock index 10759a3..ced18b6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -141,6 +141,12 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + [[package]] name = "base64" version = "0.21.7" @@ -208,6 +214,16 @@ dependencies = [ "tempfile", ] +[[package]] +name = "cargo_toml" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02260d489095346e5cafd04dea8e8cb54d1d74fcd759022a9b72986ebe9a1257" +dependencies = [ + "serde", + "toml", +] + [[package]] name = "cc" version = "1.2.19" @@ -1143,25 +1159,31 @@ version = "0.1.0" dependencies = [ "anstyle", "anyhow", + "base16ct", "camino", "camino-tempfile", + "cargo_toml", "clap", "colored", "futures", "indicatif", "libc", "libnet", + "prettyplease", "propolis-client", + "quote", "rand 0.8.5", "regex", "reqwest", "ron", "serde", + "sha2", "slog", "slog-async", "slog-envlogger", "slog-term", "smf", + "syn 2.0.100", "tabwriter", "thiserror 1.0.69", "tokio", @@ -1438,6 +1460,16 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbc83ee4a840062f368f9096d80077a9841ec117e17e7f700df81958f1451254" +[[package]] +name = "prettyplease" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "664ec5419c51e34154eec046ebcba56312d5a2fc3b09a06da188e1ad21afadf6" +dependencies = [ + "proc-macro2", + "syn 2.0.100", +] + [[package]] name = "proc-macro-crate" version = "3.3.0" @@ -1549,7 +1581,7 @@ dependencies = [ [[package]] name = "propolis-client" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/propolis?rev=bdaaf207c7d7f9a6d905a3589eb8e159aa78df12#bdaaf207c7d7f9a6d905a3589eb8e159aa78df12" +source = "git+https://github.com/oxidecomputer/propolis?rev=02fdf06bb279fc1b1393f993b90cbe84b7e9f281#02fdf06bb279fc1b1393f993b90cbe84b7e9f281" dependencies = [ "async-trait", "base64 0.21.7", @@ -1573,7 +1605,7 @@ dependencies = [ [[package]] name = "propolis_api_types" version = "0.0.0" -source = "git+https://github.com/oxidecomputer/propolis?rev=bdaaf207c7d7f9a6d905a3589eb8e159aa78df12#bdaaf207c7d7f9a6d905a3589eb8e159aa78df12" +source = "git+https://github.com/oxidecomputer/propolis?rev=02fdf06bb279fc1b1393f993b90cbe84b7e9f281#02fdf06bb279fc1b1393f993b90cbe84b7e9f281" dependencies = [ "crucible-client-types", "propolis_types", @@ -1587,7 +1619,7 @@ dependencies = [ [[package]] name = "propolis_types" version = "0.0.0" -source = "git+https://github.com/oxidecomputer/propolis?rev=bdaaf207c7d7f9a6d905a3589eb8e159aa78df12#bdaaf207c7d7f9a6d905a3589eb8e159aa78df12" +source = "git+https://github.com/oxidecomputer/propolis?rev=02fdf06bb279fc1b1393f993b90cbe84b7e9f281#02fdf06bb279fc1b1393f993b90cbe84b7e9f281" dependencies = [ "schemars", "serde", @@ -2122,6 +2154,17 @@ dependencies = [ "digest", ] +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "shlex" version = "1.3.0" diff --git a/Cargo.toml b/Cargo.toml index 5a0e026..8e9c6aa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,7 +30,7 @@ slog-term = "2.7" slog-async = "2.7" slog-envlogger = "2.2" toml = "0.8" -propolis-client = { git = "https://github.com/oxidecomputer/propolis", rev = "bdaaf207c7d7f9a6d905a3589eb8e159aa78df12" } +propolis-client = { git = "https://github.com/oxidecomputer/propolis", rev = "02fdf06bb279fc1b1393f993b90cbe84b7e9f281" } libc = "0.2" tokio = { version = "1.44.2", features = ["full"] } tokio-tungstenite = "0.21" @@ -45,3 +45,10 @@ oxnet = { version = "0.1.1", default-features = false } indicatif = "0.17.11" xz2 = "0.1.7" camino-tempfile = "1.1.1" +cargo_toml = "0.22.1" +quote = "1.0.40" +prettyplease = "0.2" +syn = "2.0" +sha2 = "0.10.8" +anstyle = "1.0.10" +base16ct = { version = "0.2.0", features = ["alloc"] } diff --git a/README.md b/README.md index 4571762..a670c18 100644 --- a/README.md +++ b/README.md @@ -15,22 +15,6 @@ development environment for networked systems. recommended. While nested virt can be made to work, it often requires wizardry and is known to have flaky behaviors. -## Installing - -Install `propolis-server`. The`get-propolis.sh` script can also be used to -automatically install propolis-server form the current Falcon CI build. - -Set up propolis, firmware and OS base images. -``` -./get-propolis.sh -./get-ovmf.sh -./setup-base-images.sh -``` - -Falcon-enabled propolis builds are kicked out by Propolis CI. See -[this run](https://github.com/oxidecomputer/propolis/runs/18723647907) -as an example. - ## QuickStart To get a ready-to-go Falcon project use the diff --git a/get-ovmf.sh b/get-ovmf.sh deleted file mode 100755 index d0fa1d6..0000000 --- a/get-ovmf.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash - -set -e - -mkdir -p .img -pushd .img - -if [[ ! -f OVMF_CODE.fd ]]; then - echo "Pulling OVMF_CODE.fd" - curl -OL https://oxide-falcon-assets.s3.us-west-2.amazonaws.com/OVMF_CODE.fd -fi - -echo "Copying OVMF to /var/ovmf" -pfexec mkdir -p /var/ovmf -pfexec cp OVMF_CODE.fd /var/ovmf/OVMF_CODE.fd diff --git a/get-propolis.sh b/get-propolis.sh deleted file mode 100755 index dc0da7e..0000000 --- a/get-propolis.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash - -# get propolis version from Cargo.toml -rev=`cat Cargo.toml | grep propolis | sed -E 's/.*rev = "([^"]+)".*/\1/'` -echo "Fetching propolis version $rev" - -curl -OL https://buildomat.eng.oxide.computer/public/file/oxidecomputer/propolis/falcon/$rev/propolis-server -chmod +x propolis-server -pfexec mv propolis-server /usr/bin/ diff --git a/import-raw-img.sh b/import-raw-img.sh index e8632ec..60972b0 100755 --- a/import-raw-img.sh +++ b/import-raw-img.sh @@ -1,5 +1,10 @@ #!/bin/bash +# This script is useful for importing a raw image onto your local system for +# testing the construction of a new falcon image. This is not necessary for +# falcon images that have been uploaded to the falcon image bucket. Those +# images are automatically managed by falcon. + set -o xtrace set -o errexit set -o pipefail diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 5b15127..75f9594 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -3,8 +3,6 @@ name = "libfalcon" version = "0.1.0" edition = "2018" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - [dependencies] thiserror.workspace = true anyhow.workspace = true @@ -33,7 +31,15 @@ camino.workspace = true reqwest.workspace = true indicatif.workspace = true xz2.workspace = true -anstyle = "1.0.10" +anstyle.workspace = true +sha2.workspace = true +base16ct.workspace = true [dev-dependencies] camino-tempfile.workspace = true + +[build-dependencies] +cargo_toml.workspace = true +quote.workspace = true +prettyplease.workspace = true +syn.workspace = true diff --git a/lib/build.rs b/lib/build.rs new file mode 100644 index 0000000..b9a649d --- /dev/null +++ b/lib/build.rs @@ -0,0 +1,46 @@ +//! This file generates a rust file with a single constant in it, +//! PROPOLIS_REV, that holds the git revision of propolis that +//! this build expects. The expected revision is pulled from +//! the workspace Cargo.toml. This constant is used to automatically +//! download propolis from buildomat CI artifacts as a part of +//! the topology preflight process, ensuring that the propolis binary +//! we use matches the propolis API revision falcon was built against. + +use cargo_toml::Manifest; +use quote::quote; +use std::env; +use std::fs; +use std::path::Path; + +fn main() { + get_propolis_version(); +} + +fn get_propolis_version() { + let manifest = Manifest::from_path("../Cargo.toml") + .expect("read workspace Cargo.toml"); + + let workspace = manifest.workspace.expect("get workspace"); + + let propolis = workspace + .dependencies + .get("propolis-client") + .expect("build.rs: get propolis client dependency"); + + let Some(rev) = propolis.git_rev() else { + panic!("build.rs: expected git rev for propolis client"); + }; + + let out_dir = env::var_os("OUT_DIR").unwrap(); + let dest_path = Path::new(&out_dir).join("propolis_version.rs"); + + let tokens = quote! { const PROPOLIS_REV: &str = #rev; }; + + let file: syn::File = + syn::parse2(tokens).expect("build.rs: parse generated code"); + let code = prettyplease::unparse(&file); + + fs::write(&dest_path, code).unwrap(); + + println!("cargo::rerun-if-changed=../Cargo.toml"); +} diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 007e2dd..94655d2 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -10,15 +10,19 @@ mod util; pub mod cli; pub mod error; +pub mod ovmf; +pub mod propolis; pub mod serial; pub mod unit; -use anyhow::Context; +use anyhow::{anyhow, Context}; use camino::{Utf8Path, Utf8PathBuf}; use error::Error; use futures::future::join_all; use futures::StreamExt; use indicatif::{ProgressBar, ProgressStyle}; +use ovmf::ensure_ovmf_fd; +use propolis::ensure_propolis_binary; use ron::ser::{to_string_pretty, PrettyConfig}; use serde::{Deserialize, Serialize}; use slog::Drain; @@ -49,6 +53,9 @@ use propolis_client::types::{ use propolis_client::PciPath; use propolis_client::SpecKey; +// See build.rs for how this file is generated +include!(concat!(env!("OUT_DIR"), "/propolis_version.rs")); + #[macro_export] macro_rules! node { ($d:ident, $name:ident, $img:literal, $cores:literal, $mem:expr) => { @@ -83,6 +90,7 @@ macro_rules! cmd { } pub const DEFAULT_FALCON_DIR: &str = ".falcon"; +pub const DEFAULT_PROPOLIS_SERVER: &str = ".falcon/bin/propolis-server"; const ZFS_BIN: &str = "/usr/sbin/zfs"; const DLADM_BIN: &str = "/usr/sbin/dladm"; const DD_BIN: &str = "/usr/bin/dd"; @@ -291,7 +299,7 @@ impl Runner { deployment: Deployment::new(name), log: slog::Logger::root(drain, slog::o!()), persistent: false, - propolis_binary: "propolis-server".into(), + propolis_binary: DEFAULT_PROPOLIS_SERVER.into(), dataset: dataset(), falcon_dir: DEFAULT_FALCON_DIR.into(), } @@ -532,11 +540,21 @@ impl Runner { } async fn preflight(&mut self) -> Result<(), Error> { - // Verify all required executables are discoverable. + if self.propolis_binary == DEFAULT_PROPOLIS_SERVER { + ensure_propolis_binary( + // Tied to cargo dependency, see build.rs for details. + PROPOLIS_REV, + self.falcon_dir.as_str(), + &self.log, + ) + .await?; + } + ensure_ovmf_fd(self.falcon_dir.as_str(), &self.log).await?; + // Verify all required executables are usable. let out = Command::new(&self.propolis_binary).args(["-V"]).output(); if out.is_err() { return Err(Error::Exec(format!( - "failed to find {} on PATH", + "failed to find {}", &self.propolis_binary ))); } @@ -626,11 +644,31 @@ impl Runner { // Destroy workspace info!(self.log, "destroying workspace at {}", self.falcon_dir); - fs::remove_dir_all(&self.falcon_dir)?; + self.clean_workspace()?; Ok(()) } + fn clean_workspace(&self) -> Result<(), Error> { + let falcon_dir = std::fs::read_dir(&self.falcon_dir)?; + for ent in falcon_dir { + let p = ent?.path(); + // Don't delete downloaded binaries on each launch. These are + // checked to ensure they are what falcon expects. Everything + // else (topology models and state) get deleted. + if p == Path::new(&format!("{}/bin", self.falcon_dir.as_str())) { + continue; + } + if p.is_dir() { + std::fs::remove_dir_all(&p)?; + } + if p.is_file() { + std::fs::remove_file(p)?; + } + } + Ok(()) + } + /// Run a command synchronously in the vm. pub async fn exec(&self, n: NodeRef, cmd: &str) -> Result { let name = self.deployment.nodes[n.index].name.clone(); @@ -999,7 +1037,7 @@ impl Node { "/dev/zvol/rdsk/{}/img/{}", self.dataset, self.image ))?; - let pb = Self::new_progress_bar(); + let pb = new_progress_bar(); pb.inc_length(dst.metadata().context("zvol dst metadata")?.len()); let mut dst = BufWriter::with_capacity(1024 * 1024, dst); @@ -1040,7 +1078,7 @@ impl Node { .context("file size as usize")?); } info!(log, "extracting image to {to}"); - let pb = Self::new_progress_bar(); + let pb = new_progress_bar(); let in_file = std::fs::File::open(from)?; let len = in_file .metadata() @@ -1060,20 +1098,6 @@ impl Node { .context("file size as usize")?) } - fn new_progress_bar() -> ProgressBar { - let pb = ProgressBar::new(0); - let sty = ProgressStyle::with_template( - "[{elapsed_precise}] \ - {bar:40.cyan/blue} \ - {bytes}/{total_bytes} \ - {msg}", - ) - .unwrap() - .progress_chars("##-"); - pb.set_style(sty); - pb - } - async fn try_download_base_image( &self, log: &Logger, @@ -1089,45 +1113,7 @@ impl Node { ); info!(log, "trying to download {url}"); - let pb = Self::new_progress_bar(); - - let client = reqwest::ClientBuilder::new() - .timeout(Duration::from_secs(3600)) - .tcp_keepalive(Duration::from_secs(3600)) - .connect_timeout(Duration::from_secs(15)) - .build() - .unwrap(); - let response = client - .get(&url) - .send() - .await - .with_context(|| format!("failed to get url {url}"))?; - - if !response.status().is_success() { - Err(anyhow::anyhow!( - "failed to download image: {}", - response.status() - ))?; - } - pb.inc_length( - response - .content_length() - .ok_or_else(|| anyhow::anyhow!("Missing content length"))?, - ); - let mut file = tokio::fs::File::create(path) - .await - .with_context(|| format!("failed to create {path}"))?; - let mut stream = response.bytes_stream(); - while let Some(chunk) = stream.next().await { - let chunk = chunk.with_context(|| { - format!("failed reading response from {url}") - })?; - file.write_all(&chunk) - .await - .with_context(|| format!("failed writing {path:?}"))?; - pb.inc(chunk.len().try_into().unwrap()); - } - pb.finish(); + download_large_file(url.as_str(), path).await?; Ok(()) } @@ -1529,7 +1515,7 @@ pub(crate) async fn launch_vm( let mut cmd = Command::new(propolis_binary); let mut args = vec![ "run".to_string(), - "/var/ovmf/OVMF_CODE.fd".to_string(), + format!("{}/bin/OVMF_CODE.fd", falcon_dir.as_str()), sockaddr.clone(), ]; if let Some(vnc_port) = node.vnc_port { @@ -1713,3 +1699,69 @@ async fn do_find_propolis_port_in_log( } } } + +pub(crate) fn new_progress_bar() -> ProgressBar { + let pb = ProgressBar::new(0); + let sty = ProgressStyle::with_template( + "[{elapsed_precise}] \ + {bar:40.cyan/blue} \ + {bytes}/{total_bytes} \ + {msg}", + ) + .unwrap() + .progress_chars("##-"); + pb.set_style(sty); + pb +} + +pub(crate) async fn download_large_file( + url: &str, + destination_path: &str, +) -> anyhow::Result<()> { + let path = Path::new(destination_path); + let dir = path.parent().ok_or_else(|| { + anyhow!("could not determine parent dir for {destination_path}") + })?; + std::fs::create_dir_all(dir)?; + + let pb = new_progress_bar(); + + let client = reqwest::ClientBuilder::new() + .timeout(Duration::from_secs(3600)) + .tcp_keepalive(Duration::from_secs(3600)) + .connect_timeout(Duration::from_secs(15)) + .build() + .unwrap(); + let response = client + .get(url) + .send() + .await + .with_context(|| format!("failed to get url {url}"))?; + + if !response.status().is_success() { + Err(anyhow::anyhow!( + "failed to download image: {}", + response.status() + ))?; + } + pb.inc_length( + response + .content_length() + .ok_or_else(|| anyhow::anyhow!("Missing content length"))?, + ); + let mut file = tokio::fs::File::create(path) + .await + .with_context(|| format!("failed to create {destination_path}"))?; + let mut stream = response.bytes_stream(); + while let Some(chunk) = stream.next().await { + let chunk = chunk + .with_context(|| format!("failed reading response from {url}"))?; + file.write_all(&chunk) + .await + .with_context(|| format!("failed writing {path:?}"))?; + pb.inc(chunk.len().try_into().unwrap()); + } + pb.finish(); + + Ok(()) +} diff --git a/lib/src/ovmf.rs b/lib/src/ovmf.rs new file mode 100644 index 0000000..72749ed --- /dev/null +++ b/lib/src/ovmf.rs @@ -0,0 +1,53 @@ +use anyhow::Result; +use sha2::{Digest, Sha256}; +use slog::{info, Logger}; +use std::fs; +use std::io; + +const OVMF_URL: &str = + "https://oxide-falcon-assets.s3.us-west-2.amazonaws.com/OVMF_CODE.fd"; +const OVMF_DIGEST_URL: &str = + "https://oxide-falcon-assets.s3.us-west-2.amazonaws.com/OVMF_CODE.fd.sha256.txt"; + +pub(crate) async fn ensure_ovmf_fd( + falcon_dir: &str, + log: &Logger, +) -> Result<()> { + let path = format!("{falcon_dir}/bin/OVMF_CODE.fd"); + let Some(local_digest) = get_downloaded_ovmf_digest(&path)? else { + info!(log, "ovmf fd not found"); + return download_ovmf(&path, log).await; + }; + let remote_digest = get_expected_ovmf_digest().await?; + if local_digest != remote_digest { + info!(log, + "ovmf digest '{local_digest}' does not match expected '{remote_digest}'" + ); + return download_ovmf(&path, log).await; + } + Ok(()) +} + +async fn download_ovmf(path: &str, log: &Logger) -> Result<()> { + info!(log, "downloading ovmf"); + crate::download_large_file(OVMF_URL, path).await?; + Ok(()) +} + +fn get_downloaded_ovmf_digest(path: &str) -> Result> { + let mut file = match fs::File::open(path) { + Ok(f) => f, + Err(_) => return Ok(None), + }; + let mut hasher = Sha256::new(); + io::copy(&mut file, &mut hasher)?; + let hash = hasher.finalize(); + let hash = base16ct::lower::encode_string(&hash); + Ok(Some(hash)) +} + +async fn get_expected_ovmf_digest() -> Result { + let response = reqwest::get(OVMF_DIGEST_URL).await?; + let text = response.text().await?; + Ok(text.trim().to_owned()) +} diff --git a/lib/src/propolis.rs b/lib/src/propolis.rs new file mode 100644 index 0000000..dcbc353 --- /dev/null +++ b/lib/src/propolis.rs @@ -0,0 +1,58 @@ +use anyhow::Result; +use sha2::{Digest, Sha256}; +use slog::{info, Logger}; +use std::fs; +use std::io; +use std::os::unix::fs::PermissionsExt; + +pub(crate) async fn ensure_propolis_binary( + rev: &str, + falcon_dir: &str, + log: &Logger, +) -> Result<()> { + let path = format!("{falcon_dir}/bin/propolis-server"); + let Some(local_digest) = get_downloaded_propolis_digest(&path)? else { + info!(log, "propolis-server binary not found"); + return download_propolis(rev, &path, log).await; + }; + let remote_digest = get_expected_propolis_digest(rev).await?; + if local_digest != remote_digest { + info!(log, + "propolis-server digest '{local_digest}' does not match expected '{remote_digest}'" + ); + return download_propolis(rev, &path, log).await; + } + Ok(()) +} + +async fn download_propolis(rev: &str, path: &str, log: &Logger) -> Result<()> { + info!(log, "downloading propolis server rev {rev}"); + let url = format!( + "https://buildomat.eng.oxide.computer/public/file/oxidecomputer/propolis/falcon/{rev}/propolis-server" + ); + crate::download_large_file(url.as_str(), path).await?; + fs::set_permissions(path, fs::Permissions::from_mode(0o755))?; + Ok(()) +} + +fn get_downloaded_propolis_digest(path: &str) -> Result> { + let mut file = match fs::File::open(path) { + Ok(f) => f, + Err(_) => return Ok(None), + }; + let mut hasher = Sha256::new(); + io::copy(&mut file, &mut hasher)?; + let hash = hasher.finalize(); + let hash = base16ct::lower::encode_string(&hash); + Ok(Some(hash)) +} + +async fn get_expected_propolis_digest(rev: &str) -> Result { + let digest_url = format!( + "https://buildomat.eng.oxide.computer/public/file/oxidecomputer/propolis/falcon/{rev}/propolis-server.sha256.txt" + ); + + let response = reqwest::get(digest_url).await?; + let text = response.text().await?; + Ok(text.trim().to_owned()) +} diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..4c1d586 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,5 @@ +[toolchain] +# We choose a specific toolchain (rather than "stable") for repeatability. The +# intent is to keep this up-to-date with recently-released stable Rust. +channel = "1.81.0" +profile = "default" diff --git a/setup-base-images.sh b/setup-base-images.sh deleted file mode 100755 index edd4415..0000000 --- a/setup-base-images.sh +++ /dev/null @@ -1,55 +0,0 @@ -#!/bin/bash - -set -o xtrace -set -o errexit -set -o pipefail -set -o nounset - -dataset=${FALCON_DATASET:-rpool/falcon} -echo "dataset is: $dataset" - -# Images follow the naming scheme -# -- -# -# Old method of pulling statically defined images -# TODO create new build pipelines for the following images -images="debian-11.0_0 helios-2.5_0" -mkdir -p .img -pushd .img - -for img in $images; do - name=${img%_*} - file=$img.raw.xz - if [[ -n "${FORCE+x}" ]]; then - rm -f $file - rm -rf $img.raw - echo "Deleting $name image" - pfexec zfs destroy -r $dataset/img/$name || true - fi - - if [[ -b /dev/zvol/dsk/$dataset/img/$name ]]; then - continue - fi - - if [[ ! -f $file ]]; then - echo "Pulling $file" - echo "https://oxide-falcon-assets.s3.us-west-2.amazonaws.com/$file" - curl -OL https://oxide-falcon-assets.s3.us-west-2.amazonaws.com/$file - fi - if [[ ! -f $img.raw ]]; then - echo "Extracting $file" - unxz --keep -T 0 $file - fi - file=$img.raw - - echo "Creating ZFS volume $name" - fsize=`stat --format "%s" $img.raw` - (( vsize = fsize + 4096 - ( fsize % 4096 ) )) - pfexec zfs create -p -V $vsize -o volblocksize=4k "$dataset/img/$name" - echo "Copying contents of image into volume" - pfexec dd if=$img.raw of="/dev/zvol/rdsk/$dataset/img/$name" bs=1024k status=progress - echo "Creating base image snapshot" - pfexec zfs snapshot "$dataset/img/$name@base" -done - -popd