qcow2 SAL + rhai script to test functionality
This commit is contained in:
@@ -24,6 +24,7 @@
|
||||
pub mod buildah;
|
||||
pub mod nerdctl;
|
||||
pub mod rfs;
|
||||
pub mod qcow2;
|
||||
|
||||
pub mod rhai;
|
||||
|
||||
|
@@ -1,3 +1,4 @@
|
||||
pub mod buildah;
|
||||
pub mod nerdctl;
|
||||
pub mod rfs;
|
||||
pub mod rfs;
|
||||
pub mod qcow2;
|
200
packages/system/virt/src/qcow2/mod.rs
Normal file
200
packages/system/virt/src/qcow2/mod.rs
Normal file
@@ -0,0 +1,200 @@
|
||||
use serde_json::Value;
|
||||
use std::error::Error;
|
||||
use std::fmt;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
use sal_os;
|
||||
use sal_process::{self, RunError};
|
||||
|
||||
/// Error type for qcow2 operations
|
||||
#[derive(Debug)]
|
||||
pub enum Qcow2Error {
|
||||
/// Failed to execute a system command
|
||||
CommandExecutionFailed(String),
|
||||
/// Command executed but returned non-zero or failed semantics
|
||||
CommandFailed(String),
|
||||
/// JSON parsing error
|
||||
JsonParseError(String),
|
||||
/// IO error (filesystem)
|
||||
IoError(String),
|
||||
/// Dependency missing or invalid input
|
||||
Other(String),
|
||||
}
|
||||
|
||||
impl fmt::Display for Qcow2Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Qcow2Error::CommandExecutionFailed(e) => write!(f, "Command execution failed: {}", e),
|
||||
Qcow2Error::CommandFailed(e) => write!(f, "{}", e),
|
||||
Qcow2Error::JsonParseError(e) => write!(f, "JSON parse error: {}", e),
|
||||
Qcow2Error::IoError(e) => write!(f, "IO error: {}", e),
|
||||
Qcow2Error::Other(e) => write!(f, "{}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for Qcow2Error {}
|
||||
|
||||
fn from_run_error(e: RunError) -> Qcow2Error {
|
||||
Qcow2Error::CommandExecutionFailed(e.to_string())
|
||||
}
|
||||
|
||||
fn ensure_parent_dir(path: &str) -> Result<(), Qcow2Error> {
|
||||
if let Some(parent) = Path::new(path).parent() {
|
||||
fs::create_dir_all(parent).map_err(|e| Qcow2Error::IoError(e.to_string()))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ensure_qemu_img() -> Result<(), Qcow2Error> {
|
||||
if sal_process::which("qemu-img").is_none() {
|
||||
return Err(Qcow2Error::Other(
|
||||
"qemu-img not found on PATH. Please install qemu-utils (Debian/Ubuntu) or the QEMU tools for your distro.".to_string(),
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_quiet(cmd: &str) -> Result<sal_process::CommandResult, Qcow2Error> {
|
||||
sal_process::run(cmd)
|
||||
.silent(true)
|
||||
.execute()
|
||||
.map_err(from_run_error)
|
||||
.and_then(|res| {
|
||||
if res.success {
|
||||
Ok(res)
|
||||
} else {
|
||||
Err(Qcow2Error::CommandFailed(format!(
|
||||
"Command failed (code {}): {}\n{}",
|
||||
res.code, cmd, res.stderr
|
||||
)))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Create a qcow2 image at path with a given virtual size (in GiB)
|
||||
pub fn create(path: &str, size_gb: i64) -> Result<String, Qcow2Error> {
|
||||
ensure_qemu_img()?;
|
||||
if size_gb <= 0 {
|
||||
return Err(Qcow2Error::Other(
|
||||
"size_gb must be > 0 for qcow2.create".to_string(),
|
||||
));
|
||||
}
|
||||
ensure_parent_dir(path)?;
|
||||
let cmd = format!("qemu-img create -f qcow2 {} {}G", path, size_gb);
|
||||
run_quiet(&cmd)?;
|
||||
Ok(path.to_string())
|
||||
}
|
||||
|
||||
/// Return qemu-img info as a JSON value
|
||||
pub fn info(path: &str) -> Result<Value, Qcow2Error> {
|
||||
ensure_qemu_img()?;
|
||||
if !Path::new(path).exists() {
|
||||
return Err(Qcow2Error::IoError(format!("Image not found: {}", path)));
|
||||
}
|
||||
let cmd = format!("qemu-img info --output=json {}", path);
|
||||
let res = run_quiet(&cmd)?;
|
||||
serde_json::from_str::<Value>(&res.stdout).map_err(|e| Qcow2Error::JsonParseError(e.to_string()))
|
||||
}
|
||||
|
||||
/// Create an offline snapshot on a qcow2 image
|
||||
pub fn snapshot_create(path: &str, name: &str) -> Result<(), Qcow2Error> {
|
||||
ensure_qemu_img()?;
|
||||
if name.trim().is_empty() {
|
||||
return Err(Qcow2Error::Other("snapshot name cannot be empty".to_string()));
|
||||
}
|
||||
let cmd = format!("qemu-img snapshot -c {} {}", name, path);
|
||||
run_quiet(&cmd).map(|_| ())
|
||||
}
|
||||
|
||||
/// Delete a snapshot on a qcow2 image
|
||||
pub fn snapshot_delete(path: &str, name: &str) -> Result<(), Qcow2Error> {
|
||||
ensure_qemu_img()?;
|
||||
if name.trim().is_empty() {
|
||||
return Err(Qcow2Error::Other("snapshot name cannot be empty".to_string()));
|
||||
}
|
||||
let cmd = format!("qemu-img snapshot -d {} {}", name, path);
|
||||
run_quiet(&cmd).map(|_| ())
|
||||
}
|
||||
|
||||
/// Snapshot representation (subset of qemu-img info snapshots)
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Qcow2Snapshot {
|
||||
pub id: Option<String>,
|
||||
pub name: Option<String>,
|
||||
pub vm_state_size: Option<i64>,
|
||||
pub date_sec: Option<i64>,
|
||||
pub date_nsec: Option<i64>,
|
||||
pub vm_clock_nsec: Option<i64>,
|
||||
}
|
||||
|
||||
/// List snapshots on a qcow2 image (offline)
|
||||
pub fn snapshot_list(path: &str) -> Result<Vec<Qcow2Snapshot>, Qcow2Error> {
|
||||
let v = info(path)?;
|
||||
let mut out = Vec::new();
|
||||
if let Some(snaps) = v.get("snapshots").and_then(|s| s.as_array()) {
|
||||
for s in snaps {
|
||||
let snap = Qcow2Snapshot {
|
||||
id: s.get("id").and_then(|x| x.as_str()).map(|s| s.to_string()),
|
||||
name: s.get("name").and_then(|x| x.as_str()).map(|s| s.to_string()),
|
||||
vm_state_size: s.get("vm-state-size").and_then(|x| x.as_i64()),
|
||||
date_sec: s.get("date-sec").and_then(|x| x.as_i64()),
|
||||
date_nsec: s.get("date-nsec").and_then(|x| x.as_i64()),
|
||||
vm_clock_nsec: s.get("vm-clock-nsec").and_then(|x| x.as_i64()),
|
||||
};
|
||||
out.push(snap);
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Result for building the base image
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BuildBaseResult {
|
||||
pub base_image_path: String,
|
||||
pub snapshot: String,
|
||||
pub url: String,
|
||||
pub resized_to_gb: Option<i64>,
|
||||
}
|
||||
|
||||
/// Build/download Ubuntu 24.04 base image (Noble cloud image), optionally resize, and create a base snapshot
|
||||
pub fn build_ubuntu_24_04_base(dest_dir: &str, size_gb: Option<i64>) -> Result<BuildBaseResult, Qcow2Error> {
|
||||
ensure_qemu_img()?;
|
||||
|
||||
// Ensure destination directory exists
|
||||
sal_os::mkdir(dest_dir).map_err(|e| Qcow2Error::IoError(e.to_string()))?;
|
||||
|
||||
// Canonical Ubuntu Noble cloud image (amd64)
|
||||
let url = "https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img";
|
||||
|
||||
// Build destination path
|
||||
let dest_dir_sanitized = dest_dir.trim_end_matches('/');
|
||||
let dest_path = format!("{}/noble-server-cloudimg-amd64.img", dest_dir_sanitized);
|
||||
|
||||
// Download if not present
|
||||
let path_obj = Path::new(&dest_path);
|
||||
if !path_obj.exists() {
|
||||
// 50MB minimum for sanity; the actual image is much larger
|
||||
sal_os::download_file(url, &dest_path, 50_000)
|
||||
.map_err(|e| Qcow2Error::IoError(e.to_string()))?;
|
||||
}
|
||||
|
||||
// Resize if requested
|
||||
if let Some(sz) = size_gb {
|
||||
if sz > 0 {
|
||||
let cmd = format!("qemu-img resize {} {}G", dest_path, sz);
|
||||
run_quiet(&cmd)?;
|
||||
}
|
||||
}
|
||||
|
||||
// Create "base" snapshot
|
||||
snapshot_create(&dest_path, "base")?;
|
||||
|
||||
Ok(BuildBaseResult {
|
||||
base_image_path: dest_path,
|
||||
snapshot: "base".to_string(),
|
||||
url: url.to_string(),
|
||||
resized_to_gb: size_gb.filter(|v| *v > 0),
|
||||
})
|
||||
}
|
@@ -8,6 +8,7 @@ use rhai::{Engine, EvalAltResult};
|
||||
pub mod buildah;
|
||||
pub mod nerdctl;
|
||||
pub mod rfs;
|
||||
pub mod qcow2;
|
||||
|
||||
/// Register all Virt module functions with the Rhai engine
|
||||
///
|
||||
@@ -28,6 +29,9 @@ pub fn register_virt_module(engine: &mut Engine) -> Result<(), Box<EvalAltResult
|
||||
// Register RFS module functions
|
||||
rfs::register_rfs_module(engine)?;
|
||||
|
||||
// Register QCOW2 module functions
|
||||
qcow2::register_qcow2_module(engine)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -35,3 +39,4 @@ pub fn register_virt_module(engine: &mut Engine) -> Result<(), Box<EvalAltResult
|
||||
pub use buildah::{bah_new, register_bah_module};
|
||||
pub use nerdctl::register_nerdctl_module;
|
||||
pub use rfs::register_rfs_module;
|
||||
pub use qcow2::register_qcow2_module;
|
||||
|
139
packages/system/virt/src/rhai/qcow2.rs
Normal file
139
packages/system/virt/src/rhai/qcow2.rs
Normal file
@@ -0,0 +1,139 @@
|
||||
use crate::qcow2;
|
||||
use crate::qcow2::{BuildBaseResult, Qcow2Error, Qcow2Snapshot};
|
||||
use rhai::{Array, Dynamic, Engine, EvalAltResult, Map};
|
||||
use serde_json::Value;
|
||||
|
||||
// Convert Qcow2Error to Rhai error
|
||||
fn qcow2_error_to_rhai<T>(result: Result<T, Qcow2Error>) -> Result<T, Box<EvalAltResult>> {
|
||||
result.map_err(|e| {
|
||||
Box::new(EvalAltResult::ErrorRuntime(
|
||||
format!("qcow2 error: {}", e).into(),
|
||||
rhai::Position::NONE,
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
// Convert serde_json::Value to Rhai Dynamic recursively (maps, arrays, scalars)
|
||||
fn json_to_dynamic(v: &Value) -> Dynamic {
|
||||
match v {
|
||||
Value::Null => Dynamic::UNIT,
|
||||
Value::Bool(b) => (*b).into(),
|
||||
Value::Number(n) => {
|
||||
if let Some(i) = n.as_i64() {
|
||||
i.into()
|
||||
} else {
|
||||
// Avoid float dependency differences; fall back to string
|
||||
n.to_string().into()
|
||||
}
|
||||
}
|
||||
Value::String(s) => s.clone().into(),
|
||||
Value::Array(arr) => {
|
||||
let mut a = Array::new();
|
||||
for item in arr {
|
||||
a.push(json_to_dynamic(item));
|
||||
}
|
||||
a.into()
|
||||
}
|
||||
Value::Object(obj) => {
|
||||
let mut m = Map::new();
|
||||
for (k, val) in obj {
|
||||
m.insert(k.into(), json_to_dynamic(val));
|
||||
}
|
||||
m.into()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wrappers exposed to Rhai
|
||||
|
||||
pub fn qcow2_create(path: &str, size_gb: i64) -> Result<String, Box<EvalAltResult>> {
|
||||
qcow2_error_to_rhai(qcow2::create(path, size_gb))
|
||||
}
|
||||
|
||||
pub fn qcow2_info(path: &str) -> Result<Dynamic, Box<EvalAltResult>> {
|
||||
let v = qcow2_error_to_rhai(qcow2::info(path))?;
|
||||
Ok(json_to_dynamic(&v))
|
||||
}
|
||||
|
||||
pub fn qcow2_snapshot_create(path: &str, name: &str) -> Result<(), Box<EvalAltResult>> {
|
||||
qcow2_error_to_rhai(qcow2::snapshot_create(path, name))
|
||||
}
|
||||
|
||||
pub fn qcow2_snapshot_delete(path: &str, name: &str) -> Result<(), Box<EvalAltResult>> {
|
||||
qcow2_error_to_rhai(qcow2::snapshot_delete(path, name))
|
||||
}
|
||||
|
||||
pub fn qcow2_snapshot_list(path: &str) -> Result<Array, Box<EvalAltResult>> {
|
||||
let snaps = qcow2_error_to_rhai(qcow2::snapshot_list(path))?;
|
||||
let mut arr = Array::new();
|
||||
for s in snaps {
|
||||
arr.push(snapshot_to_map(&s).into());
|
||||
}
|
||||
Ok(arr)
|
||||
}
|
||||
|
||||
fn snapshot_to_map(s: &Qcow2Snapshot) -> Map {
|
||||
let mut m = Map::new();
|
||||
if let Some(id) = &s.id {
|
||||
m.insert("id".into(), id.clone().into());
|
||||
} else {
|
||||
m.insert("id".into(), Dynamic::UNIT);
|
||||
}
|
||||
if let Some(name) = &s.name {
|
||||
m.insert("name".into(), name.clone().into());
|
||||
} else {
|
||||
m.insert("name".into(), Dynamic::UNIT);
|
||||
}
|
||||
if let Some(v) = s.vm_state_size {
|
||||
m.insert("vm_state_size".into(), v.into());
|
||||
} else {
|
||||
m.insert("vm_state_size".into(), Dynamic::UNIT);
|
||||
}
|
||||
if let Some(v) = s.date_sec {
|
||||
m.insert("date_sec".into(), v.into());
|
||||
} else {
|
||||
m.insert("date_sec".into(), Dynamic::UNIT);
|
||||
}
|
||||
if let Some(v) = s.date_nsec {
|
||||
m.insert("date_nsec".into(), v.into());
|
||||
} else {
|
||||
m.insert("date_nsec".into(), Dynamic::UNIT);
|
||||
}
|
||||
if let Some(v) = s.vm_clock_nsec {
|
||||
m.insert("vm_clock_nsec".into(), v.into());
|
||||
} else {
|
||||
m.insert("vm_clock_nsec".into(), Dynamic::UNIT);
|
||||
}
|
||||
m
|
||||
}
|
||||
|
||||
pub fn qcow2_build_ubuntu_24_04_base(
|
||||
dest_dir: &str,
|
||||
size_gb: i64,
|
||||
) -> Result<Map, Box<EvalAltResult>> {
|
||||
// size_gb: pass None if <=0
|
||||
let size_opt = if size_gb > 0 { Some(size_gb) } else { None };
|
||||
let r: BuildBaseResult = qcow2_error_to_rhai(qcow2::build_ubuntu_24_04_base(dest_dir, size_opt))?;
|
||||
let mut m = Map::new();
|
||||
m.insert("base_image_path".into(), r.base_image_path.into());
|
||||
m.insert("snapshot".into(), r.snapshot.into());
|
||||
m.insert("url".into(), r.url.into());
|
||||
if let Some(sz) = r.resized_to_gb {
|
||||
m.insert("resized_to_gb".into(), sz.into());
|
||||
} else {
|
||||
m.insert("resized_to_gb".into(), Dynamic::UNIT);
|
||||
}
|
||||
Ok(m)
|
||||
}
|
||||
|
||||
// Module registration
|
||||
|
||||
pub fn register_qcow2_module(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> {
|
||||
engine.register_fn("qcow2_create", qcow2_create);
|
||||
engine.register_fn("qcow2_info", qcow2_info);
|
||||
engine.register_fn("qcow2_snapshot_create", qcow2_snapshot_create);
|
||||
engine.register_fn("qcow2_snapshot_delete", qcow2_snapshot_delete);
|
||||
engine.register_fn("qcow2_snapshot_list", qcow2_snapshot_list);
|
||||
engine.register_fn("qcow2_build_ubuntu_24_04_base", qcow2_build_ubuntu_24_04_base);
|
||||
Ok(())
|
||||
}
|
82
packages/system/virt/tests/rhai/04_qcow2_basic.rhai
Normal file
82
packages/system/virt/tests/rhai/04_qcow2_basic.rhai
Normal file
@@ -0,0 +1,82 @@
|
||||
// Basic tests for QCOW2 SAL (offline, will skip if qemu-img is not present)
|
||||
|
||||
print("=== QCOW2 Basic Tests ===");
|
||||
|
||||
// Dependency check
|
||||
let qemu = which("qemu-img");
|
||||
if qemu == () {
|
||||
print("⚠️ qemu-img not available - skipping QCOW2 tests");
|
||||
print("Install qemu-utils (Debian/Ubuntu) or QEMU tools for your distro.");
|
||||
print("=== QCOW2 Tests Skipped ===");
|
||||
exit();
|
||||
}
|
||||
|
||||
// Helper: unique temp path
|
||||
let now = 0;
|
||||
try {
|
||||
// if process module exists you could pull a timestamp; fallback to random-ish suffix
|
||||
now = 100000 + (rand() % 100000);
|
||||
} catch (err) {
|
||||
now = 100000 + (rand() % 100000);
|
||||
}
|
||||
let img_path = `/tmp/qcow2_test_${now}.img`;
|
||||
|
||||
print("\n--- Test 1: Create image ---");
|
||||
let create_res = qcow2_create(img_path, 1);
|
||||
if create_res.is_err() {
|
||||
print(`❌ Create failed: ${create_res.unwrap_err()}`);
|
||||
exit();
|
||||
}
|
||||
print(`✓ Created qcow2: ${img_path}`);
|
||||
|
||||
print("\n--- Test 2: Info ---");
|
||||
let info_res = qcow2_info(img_path);
|
||||
if info_res.is_err() {
|
||||
print(`❌ Info failed: ${info_res.unwrap_err()}`);
|
||||
exit();
|
||||
}
|
||||
let info = info_res.unwrap();
|
||||
print("✓ Info fetched");
|
||||
if info.format != () { print(` format: ${info.format}`); }
|
||||
if info["virtual-size"] != () { print(` virtual-size: ${info["virtual-size"]}`); }
|
||||
|
||||
print("\n--- Test 3: Snapshot create/list/delete (offline) ---");
|
||||
let snap_name = "s1";
|
||||
let screate = qcow2_snapshot_create(img_path, snap_name);
|
||||
if screate.is_err() {
|
||||
print(`❌ snapshot_create failed: ${screate.unwrap_err()}`);
|
||||
exit();
|
||||
}
|
||||
print("✓ snapshot created: s1");
|
||||
|
||||
let slist = qcow2_snapshot_list(img_path);
|
||||
if slist.is_err() {
|
||||
print(`❌ snapshot_list failed: ${slist.unwrap_err()}`);
|
||||
exit();
|
||||
}
|
||||
let snaps = slist.unwrap();
|
||||
print(`✓ snapshot_list ok, count=${snaps.len()}`);
|
||||
|
||||
let sdel = qcow2_snapshot_delete(img_path, snap_name);
|
||||
if sdel.is_err() {
|
||||
print(`❌ snapshot_delete failed: ${sdel.unwrap_err()}`);
|
||||
exit();
|
||||
}
|
||||
print("✓ snapshot deleted: s1");
|
||||
|
||||
// Optional: Base image builder (commented to avoid big downloads by default)
|
||||
// Uncomment to test manually on a dev machine with bandwidth.
|
||||
// print("\n--- Optional: Build Ubuntu 24.04 Base ---");
|
||||
// let base_dir = "/tmp/virt_images";
|
||||
// let base = qcow2_build_ubuntu_24_04_base(base_dir, 10);
|
||||
// if base.is_err() {
|
||||
// print(`⚠️ base build failed or skipped: ${base.unwrap_err()}`);
|
||||
// } else {
|
||||
// let m = base.unwrap();
|
||||
// print(`✓ Base image path: ${m.base_image_path}`);
|
||||
// print(`✓ Base snapshot: ${m.snapshot}`);
|
||||
// print(`✓ Source URL: ${m.url}`);
|
||||
// if m.resized_to_gb != () { print(`✓ Resized to: ${m.resized_to_gb}G`); }
|
||||
// }
|
||||
|
||||
print("\n=== QCOW2 Basic Tests Completed ===");
|
Reference in New Issue
Block a user