diff options
Diffstat
| -rw-r--r-- | Cargo.lock | 115 | +115 −0 |
| -rw-r--r-- | crates/hule/Cargo.toml | 1 | +1 −0 |
| -rw-r--r-- | crates/hule/src/main.rs | 124 | +75 −49 |
| -rw-r--r-- | docs/whitepaper.md | 22 | +13 −9 |
4 files changed, 204 insertions, 58 deletions
diff --git a/Cargo.lock b/Cargo.lock index 38d3abe..cff42e0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -21,6 +21,56 @@ dependencies = [ ] [[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] name = "async-trait" version = "0.1.89" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -159,6 +209,46 @@ dependencies = [ ] [[package]] +name = "clap" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] name = "cmake" version = "0.1.58" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -168,6 +258,12 @@ dependencies = [ ] [[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] name = "combine" version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -570,6 +666,7 @@ checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" name = "hule" version = "0.1.0" dependencies = [ + "clap", "hule-hmi", "hule-oci", "hule-vmm", @@ -816,6 +913,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] name = "itoa" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1040,6 +1143,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] name = "openssl-probe" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1870,6 +1979,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] name = "uuid" version = "1.23.4" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/crates/hule/Cargo.toml b/crates/hule/Cargo.toml index 12f361d..354290b 100644 --- a/crates/hule/Cargo.toml +++ b/crates/hule/Cargo.toml @@ -13,6 +13,7 @@ authors.workspace = true repository.workspace = true [dependencies] +clap = { version = "4.6.1", features = ["derive"] } hule-hmi.workspace = true hule-oci.workspace = true hule-vmm.workspace = true diff --git a/crates/hule/src/main.rs b/crates/hule/src/main.rs index 3f316f5..644545b 100644 --- a/crates/hule/src/main.rs +++ b/crates/hule/src/main.rs @@ -4,6 +4,7 @@ use std::path::Path; use std::process::exit; +use clap::{Parser, Subcommand}; use hule_hmi::{Access, MachineImage}; use hule_vmm::backend::qemu::QemuHypervisor; use hule_vmm::{Hypervisor, MachineId, Settings}; @@ -11,6 +12,54 @@ use uuid::Uuid; type R<T> = std::result::Result<T, String>; +#[derive(Parser)] +#[command(version, about)] +#[command(propagate_version = true)] +struct Cli { + #[command(subcommand)] + command: Commands, +} + +#[derive(Subcommand)] +enum Commands { + Image { + #[command(subcommand)] + command: ImageCommands, + }, + + Machine { + #[command(subcommand)] + command: MachineCommands, + }, +} + +#[derive(Subcommand)] +enum ImageCommands { + Pull { + name: String, + }, + + Push { + name: String, + }, + + Load { + image: String, + + reference: Option<String>, + }, +} + +#[derive(Subcommand)] +enum MachineCommands { + Run { + image: String, + + #[arg(default_value_t = 8022)] + port: u16, + }, +} + fn die(msg: impl AsRef<str>) -> ! { eprintln!("hule: {}", msg.as_ref()); exit(1); @@ -74,60 +123,37 @@ async fn cmd_run(reference: &str, port: u16) -> R<()> { #[tokio::main] async fn main() { - let args: Vec<String> = std::env::args().collect(); - let usage = |prog: &str| -> ! { - eprintln!("usage:"); - eprintln!(" {prog} import <image-dir> [ref]"); - eprintln!(" {prog} push <ref>"); - eprintln!(" {prog} pull <ref>"); - eprintln!(" {prog} run <ref> [port]"); - exit(2); - }; + let cli = Cli::parse(); - if args.len() < 2 { - usage(&args[0]); - } + match &cli.command { + Commands::Image { command } => match &command { + ImageCommands::Pull { name } => { + let digest = hule_oci::pull(name).await.unwrap_or_else(|e| die(e)); + eprintln!("hule: pulled {} ({digest})", name); + } - match args[1].as_str() { - "import" => { - if args.len() < 3 { - usage(&args[0]); + ImageCommands::Push { name } => { + hule_oci::push(name).await.unwrap_or_else(|e| die(e)); + eprintln!("hule: pushed {}", name); } - let reference = args.get(3).map(String::as_str); - let digest = hule_oci::import(Path::new(&args[2]), reference) - .await - .unwrap_or_else(|e| die(e)); - match reference { - Some(reference) => { - eprintln!("hule: imported {} as {reference} ({digest})", args[2]) + + ImageCommands::Load { reference, image } => { + let digest = hule_oci::import(Path::new(image), reference.as_deref()) + .await + .unwrap_or_else(|e| die(e)); + + match reference { + Some(reference) => { + eprintln!("hule: loaded {} as {reference} ({digest})", image) + } + None => eprintln!("hule: loaded {} ({digest}, untagged)", image), } - None => eprintln!("hule: imported {} ({digest}, untagged)", args[2]), - } - } - "push" => { - if args.len() < 3 { - usage(&args[0]); - } - hule_oci::push(&args[2]).await.unwrap_or_else(|e| die(e)); - eprintln!("hule: pushed {}", args[2]); - } - "pull" => { - if args.len() < 3 { - usage(&args[0]); } - let digest = hule_oci::pull(&args[2]).await.unwrap_or_else(|e| die(e)); - eprintln!("hule: pulled {} ({digest})", args[2]); - } - "run" => { - if args.len() < 3 { - usage(&args[0]); + }, + Commands::Machine { command } => match &command { + MachineCommands::Run { image, port } => { + cmd_run(image, *port).await.unwrap_or_else(|e| die(e)); } - let port = args - .get(3) - .map_or(Ok(8022), |s| s.parse::<u16>()) - .unwrap_or_else(|_| die(format!("invalid port '{}'", args[3]))); - cmd_run(&args[2], port).await.unwrap_or_else(|e| die(e)); - } - _ => usage(&args[0]), + }, } } diff --git a/docs/whitepaper.md b/docs/whitepaper.md index 948430e..2661601 100644 --- a/docs/whitepaper.md +++ b/docs/whitepaper.md @@ -106,7 +106,7 @@ chunks so they can be transferred and verified in parallel. It is responsible for: -- importing an already prepared machine; +- loading and saving already prepared Hule machines; - pushing and pulling OCI artifacts; - maintaining a content-addressed local cache; - resolving complete backing chains; @@ -142,12 +142,12 @@ builder, an exported VM, or a directory copied by the user. A Hulefile is a recipe for deriving one Hule image from another. Unlike an external image builder, it does not install an operating system from scratch. -Its input is an already imported Hule machine that can be started and controlled +Its input is an already loaded Hule machine that can be started and controlled through an `exec`-capable access method. Every Hulefile has exactly one parent image. There is no empty base and no equivalent of `FROM scratch`: creating the first bootable machine always happens -outside the Hulefile workflow and enters Hule through import. +outside the Hulefile workflow and enters Hule through load or import. The intended execution model is deliberately simple: @@ -172,12 +172,15 @@ implicitly portable across operating systems. ## Image Lifecycle -### Prepare and Import +### Prepare, Import, and Load Preparing a base guest operating system is outside Hule. Users may use Packer, a -`genimg` script, an unattended installer, or a manually configured VM. Hule -imports the finished, powered-off machine, which can then be distributed or used -as the parent of a Hulefile. +`genimg` script, an unattended installer, or a manually configured VM. A +finished Hule machine with HMI disks and a `config.json` enters the local store +through load and can be exported again through save. A qcow2 or raw disk image +instead enters through import, which converts it to HMI and constructs the +machine configuration where possible. The resulting Hule image can then be +distributed or used as the parent of a Hulefile. This boundary is important for systems whose prebuilt images cannot be freely redistributed. A project can publish a recipe that downloads official @@ -186,8 +189,9 @@ to the user's own OCI registry. ### Publish -The prepared machine is imported, validated, split into OCI blobs, and pushed to -a registry. Tags provide convenient names; digests identify immutable versions. +The prepared Hule machine is loaded, validated, split into OCI blobs, and pushed +to a registry. Tags provide convenient names; digests identify immutable +versions. ### Run |
