diff --git a/citadel-tool/src/realmfs/mod.rs b/citadel-tool/src/realmfs/mod.rs new file mode 100644 index 0000000..7475f0d --- /dev/null +++ b/citadel-tool/src/realmfs/mod.rs @@ -0,0 +1,244 @@ +use clap::App; +use clap::ArgMatches; + +use libcitadel::{Result,RealmFS,Logger,LogLevel}; +use clap::SubCommand; +use clap::AppSettings::*; +use clap::Arg; +use libcitadel::ResizeSize; +use libcitadel::format_error; +use std::process::exit; + +pub fn main(args: Vec) { + + Logger::set_log_level(LogLevel::Debug); + + let app = App::new("citadel-realmfs") + .about("Citadel realmfs image tool") + .settings(&[ArgRequiredElseHelp,ColoredHelp, DisableHelpSubcommand, DisableVersion, DeriveDisplayOrder,SubcommandsNegateReqs]) + + .subcommand(SubCommand::with_name("resize") + .about("Resize an existing RealmFS image. If the image is currently sealed, it will also be unsealed.") + .arg(Arg::with_name("image") + .help("Path or name of RealmFS image to resize") + .required(true)) + .arg(Arg::with_name("size") + .help("Size to increase RealmFS image to (or by if prefixed with '+')") + .long_help("\ +The size can be followed by a 'g' or 'm' character \ +to indicate a quantity of gigabytes or megabytes. If no size unit \ +is provided the size is measured in blocks (of 4096 bytes). \ +\ +If the size is prefixed with a '+' character it is understood \ +as a quantity to increase the current size by. Otherwise the size \ +is the final absolute size of the image.") + .required(true))) + + + .subcommand(SubCommand::with_name("fork") + .about("Create a new RealmFS image as an unsealed copy of an existing image") + .arg(Arg::with_name("image") + .help("Path or name of RealmFS image to fork") + .required(true)) + + .arg(Arg::with_name("forkname") + .help("Name of new image to create") + .required(true))) + + .subcommand(SubCommand::with_name("seal") + .about("Seal an unsealed RealmFS image") + .arg(Arg::with_name("image") + .help("Path or name of RealmFS image to seal") + .required(true))) + + .subcommand(SubCommand::with_name("autoresize") + .about("Increase size of RealmFS image if not enough free space remains") + .arg(Arg::with_name("image") + .help("Path or name of RealmFS image") + .required(true))) + + .subcommand(SubCommand::with_name("update") + .about("Open an update shell on the image") + .arg(Arg::with_name("image") + .help("Path or name of RealmFS image") + .required(true))) + + .subcommand(SubCommand::with_name("activate") + .about("Activate a RealmFS by creating a block device for the image and mounting it.") + .arg(Arg::with_name("image") + .help("Path or name of RealmFS image to activate") + .required(true))) + + .subcommand(SubCommand::with_name("deactivate") + .about("Deactivate a RealmFS by unmounting it and removing block device created during activation.") + .arg(Arg::with_name("image") + .help("Path or name of RealmFS image to deactivate") + .required(true))) + + + .arg(Arg::with_name("image") + .help("Name of or path to RealmFS image to display information about") + .required(true)); + + let matches = app.get_matches_from(args); + let result = match matches.subcommand() { + ("resize", Some(m)) => resize(m), + ("autoresize", Some(m)) => autoresize(m), + ("fork", Some(m)) => fork(m), + ("seal", Some(m)) => seal(m), + ("update", Some(m)) => update(m), + ("activate", Some(m)) => activate(m), + ("deactivate", Some(m)) => deactivate(m), + _ => image_info(&matches), + }; + + if let Err(ref e) = result { + eprintln!("Error: {}", format_error(e)); + exit(1); + } +} + + +fn realmfs_image(arg_matches: &ArgMatches) -> Result { + let image = match arg_matches.value_of("image") { + Some(s) => s, + None => bail!("Image argument required."), + }; + + let realmfs = if RealmFS::is_valid_name(image) { + RealmFS::load_by_name(image)? + } else if RealmFS::is_valid_realmfs_image(image) { + RealmFS::load_from_path(image)? + } else { + bail!("Not a valid realmfs name or path to realmfs image file: {}", image); + }; + Ok(realmfs) +} + +fn image_info(arg_matches: &ArgMatches) -> Result<()> { + let img = realmfs_image(arg_matches)?; + print!("{}", String::from_utf8(img.header().metainfo_bytes())?); + Ok(()) +} + +fn parse_resize_size(s: &str) -> Result { + let unit = s.chars().last().filter(|c| c.is_alphabetic()); + + let skip = if s.starts_with("+") { 1 } else { 0 }; + let size = s.chars() + .skip(skip) + .take_while(|c| c.is_numeric()) + .collect::() + .parse::() + .map_err(|_| format_err!("Unable to parse size value '{}'",s))?; + + match unit { + Some('g') | Some('G') => Ok(ResizeSize::gigs(size)), + Some('m') | Some('M') => Ok(ResizeSize::megs(size)), + Some(c) => Err(format_err!("Unknown size unit '{}'", c)), + None => Ok(ResizeSize::blocks(size)), + } +} + +fn resize(arg_matches: &ArgMatches) -> Result<()> { + let img = realmfs_image(arg_matches)?; + info!("image is {}", img.path().display()); + let size_arg = match arg_matches.value_of("size") { + Some(size) => size, + None => "No size argument", + + }; + info!("Size is {}", size_arg); + let mode_add = size_arg.starts_with("+"); + let size = parse_resize_size(size_arg)?; + + if mode_add { + img.resize_grow_by(size) + } else { + img.resize_grow_to(size) + } +} + +fn autoresize(arg_matches: &ArgMatches) -> Result<()> { + let img = realmfs_image(arg_matches)?; + + if let Some(size) = img.auto_resize_size() { + img.resize_grow_to(size) + } else { + info!("RealmFS image {} has sufficient free space, doing nothing", img.path().display()); + Ok(()) + } +} + +fn fork(arg_matches: &ArgMatches) -> Result<()> { + let img = realmfs_image(arg_matches)?; + let forkname = match arg_matches.value_of("forkname") { + Some(name) => name, + None => bail!("No fork name argument"), + }; + if !RealmFS::is_valid_name(forkname) { + bail!("Not a valid RealmFS image name '{}'", forkname); + } + if RealmFS::named_image_exists(forkname) { + bail!("A RealmFS image named '{}' already exists", forkname); + } + img.fork(forkname)?; + Ok(()) +} + +fn seal(arg_matches: &ArgMatches) -> Result<()> { + let img = realmfs_image(arg_matches)?; + let img_arg = arg_matches.value_of("image").unwrap(); + + if img.is_sealed() { + info!("RealmFS image {} is already sealed", img_arg); + } else if img.is_activated() { + info!("RealmFS image {} cannot be sealed because it is currently activated", img_arg); + } else { + img.seal(None)?; + } + + Ok(()) +} + +fn update(arg_matches: &ArgMatches) -> Result<()> { + let img = realmfs_image(arg_matches)?; + let mut update = img.update(); + update.setup()?; + update.open_update_shell()?; + update.apply_update()?; + update.cleanup()?; + Ok(()) +} + +fn activate(arg_matches: &ArgMatches) -> Result<()> { + let img = realmfs_image(arg_matches)?; + let img_arg = arg_matches.value_of("image").unwrap(); + + let activation = if let Some(activation) = img.activation() { + info!("RealmFS image {} is already activated", img_arg); + activation + } else { + info!("Activating {}", img_arg); + img.activate()? + }; + info!("Read-Only mountpoint: {}", activation.mountpoint()); + if let Some(rw) = activation.mountpoint_rw() { + info!("Read-Write mountpoint: {}", rw); + } + Ok(()) +} + +fn deactivate(arg_matches: &ArgMatches) -> Result<()> { + let img = realmfs_image(arg_matches)?; + let img_arg = arg_matches.value_of("image").unwrap(); + if !img.is_activated() { + info!("RealmFS image {} is not activated", img_arg); + } else if img.is_in_use() { + info!("Cannot deactivate RealmFS image {} because it is currently in use", img_arg); + } else { + info!("Deactivating {}", img_arg); + img.deactivate()?; + } + Ok(()) +}