From ae1f4a79bfcc9caf442190acca714856fa6790cb Mon Sep 17 00:00:00 2001 From: Benjamin Gilbert Date: Thu, 12 Aug 2021 18:23:42 -0400 Subject: [PATCH] cmdline: move all postprocessing into command implementations Drop the separate pre/postprocessed structs for install and download, and the separate subcommand enum. --- src/bin/rdcore/cmdline.rs | 23 -- src/bin/rdcore/kargs.rs | 12 +- src/bin/rdcore/main.rs | 7 +- src/cmdline.rs | 480 +++++--------------------------------- src/download.rs | 18 +- src/install.rs | 263 +++++++++++++++++++-- src/live.rs | 22 +- src/main.rs | 55 +++-- 8 files changed, 379 insertions(+), 501 deletions(-) diff --git a/src/bin/rdcore/cmdline.rs b/src/bin/rdcore/cmdline.rs index 9951ccda9..110f42880 100644 --- a/src/bin/rdcore/cmdline.rs +++ b/src/bin/rdcore/cmdline.rs @@ -15,7 +15,6 @@ // For consistency, have all parse_*() functions return Result. #![allow(clippy::unnecessary_wraps)] -use anyhow::{bail, Result}; use structopt::clap::AppSettings; use structopt::StructOpt; @@ -96,25 +95,3 @@ pub struct StreamHashConfig { #[structopt(value_name = "hash-file")] pub hash_file: String, } - -/// Parse command-line arguments. -pub fn parse_args() -> Result { - let config = Cmd::from_args(); - if let Cmd::Kargs(ref config) = config { - check_kargs(config)?; - } - Ok(config) -} - -fn check_kargs(config: &KargsConfig) -> Result<()> { - // we could enforce these via clap's ArgGroup, but I don't like how the --help text looks - if !(config.boot_device.is_some() - || config.boot_mount.is_some() - || config.current - || config.override_options.is_some()) - { - // --override-options is undocumented on purpose - bail!("one of --boot-device, --boot-mount, or --current required"); - } - Ok(()) -} diff --git a/src/bin/rdcore/kargs.rs b/src/bin/rdcore/kargs.rs index bea024212..a63125804 100644 --- a/src/bin/rdcore/kargs.rs +++ b/src/bin/rdcore/kargs.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use anyhow::{Context, Result}; +use anyhow::{bail, Context, Result}; use std::fs::read_to_string; use libcoreinst::install::*; @@ -23,6 +23,16 @@ use crate::cmdline::*; use crate::rootmap::get_boot_mount_from_cmdline_args; pub fn kargs(config: &KargsConfig) -> Result<()> { + // we could enforce these via clap's ArgGroup, but I don't like how the --help text looks + if !(config.boot_device.is_some() + || config.boot_mount.is_some() + || config.current + || config.override_options.is_some()) + { + // --override-options is undocumented on purpose + bail!("one of --boot-device, --boot-mount, or --current required"); + } + if let Some(ref orig_options) = config.override_options { modify_and_print(config, orig_options.trim()).context("modifying options")?; } else if config.current { diff --git a/src/bin/rdcore/main.rs b/src/bin/rdcore/main.rs index 5d13396ab..1e312cf2c 100644 --- a/src/bin/rdcore/main.rs +++ b/src/bin/rdcore/main.rs @@ -17,14 +17,13 @@ mod kargs; mod rootmap; mod stream_hash; -use anyhow::{Context, Result}; +use anyhow::Result; +use structopt::StructOpt; use crate::cmdline::*; fn main() -> Result<()> { - let config = cmdline::parse_args().context("parsing arguments")?; - - match config { + match Cmd::from_args() { Cmd::Kargs(c) => kargs::kargs(&c), Cmd::Rootmap(c) => rootmap::rootmap(&c), Cmd::StreamHash(c) => stream_hash::stream_hash(&c), diff --git a/src/cmdline.rs b/src/cmdline.rs index 7616fe2cf..43ea797c9 100644 --- a/src/cmdline.rs +++ b/src/cmdline.rs @@ -12,48 +12,22 @@ // See the License for the specific language governing permissions and // limitations under the License. -use anyhow::{anyhow, bail, Context, Error, Result}; +use anyhow::{anyhow, Error, Result}; use reqwest::Url; use std::default::Default; -use std::fs::{File, OpenOptions}; use std::num::NonZeroU32; -use std::path::Path; use std::str::FromStr; use std::string::ToString; use structopt::clap::AppSettings; use structopt::StructOpt; -use crate::blockdev::*; -use crate::download::*; use crate::io::IgnitionHash; #[cfg(target_arch = "s390x")] use crate::s390x::dasd_try_get_sector_size; -use crate::source::*; // Args are listed in --help in the order declared in these structs/enums. // Please keep the entire help text to 80 columns. -// Exported, flattened subcommand enum with postprocessed configs -pub enum Config { - Install(InstallConfig), - Download(DownloadConfig), - ListStream(ListStreamConfig), - IsoEmbed(IsoIgnitionEmbedConfig), - IsoShow(IsoIgnitionShowConfig), - IsoRemove(IsoIgnitionRemoveConfig), - IsoIgnitionEmbed(IsoIgnitionEmbedConfig), - IsoIgnitionShow(IsoIgnitionShowConfig), - IsoIgnitionRemove(IsoIgnitionRemoveConfig), - IsoKargsModify(IsoKargsModifyConfig), - IsoKargsReset(IsoKargsResetConfig), - IsoKargsShow(IsoKargsShowConfig), - OsmetFiemap(OsmetFiemapConfig), - OsmetPack(OsmetPackConfig), - OsmetUnpack(OsmetUnpackConfig), - PxeIgnitionWrap(PxeIgnitionWrapConfig), - PxeIgnitionUnwrap(PxeIgnitionUnwrapConfig), -} - #[derive(Debug, StructOpt)] #[structopt(name = "coreos-installer")] #[structopt(global_setting(AppSettings::ArgsNegateSubcommands))] @@ -61,11 +35,11 @@ pub enum Config { #[structopt(global_setting(AppSettings::DisableHelpSubcommand))] #[structopt(global_setting(AppSettings::UnifiedHelpMessage))] #[structopt(global_setting(AppSettings::VersionlessSubcommands))] -enum Cmd { +pub enum Cmd { /// Install Fedora CoreOS or RHEL CoreOS - Install(InstallCmd), + Install(InstallConfig), /// Download a CoreOS image - Download(DownloadCmd), + Download(DownloadConfig), /// List available images in a Fedora CoreOS stream ListStream(ListStreamConfig), /// Commands to manage a CoreOS live ISO image @@ -79,7 +53,7 @@ enum Cmd { } #[derive(Debug, StructOpt)] -enum IsoCmd { +pub enum IsoCmd { /// Embed an Ignition config in an ISO image // deprecated #[structopt(setting(AppSettings::Hidden))] @@ -99,7 +73,7 @@ enum IsoCmd { } #[derive(Debug, StructOpt)] -enum IsoIgnitionCmd { +pub enum IsoIgnitionCmd { /// Embed an Ignition config in an ISO image Embed(IsoIgnitionEmbedConfig), /// Show the embedded Ignition config from an ISO image @@ -109,7 +83,7 @@ enum IsoIgnitionCmd { } #[derive(Debug, StructOpt)] -enum IsoKargsCmd { +pub enum IsoKargsCmd { /// Modify kernel args in an ISO image Modify(IsoKargsModifyConfig), /// Reset kernel args in an ISO image to defaults @@ -119,7 +93,7 @@ enum IsoKargsCmd { } #[derive(Debug, StructOpt)] -enum OsmetCmd { +pub enum OsmetCmd { /// Create osmet file from CoreOS block device Pack(OsmetPackConfig), /// Generate raw metal image from osmet file and OSTree repo @@ -129,67 +103,66 @@ enum OsmetCmd { } #[derive(Debug, StructOpt)] -enum PxeCmd { +pub enum PxeCmd { /// Commands to manage a live PXE Ignition config Ignition(PxeIgnitionCmd), } #[derive(Debug, StructOpt)] -enum PxeIgnitionCmd { +pub enum PxeIgnitionCmd { /// Wrap an Ignition config in an initrd image Wrap(PxeIgnitionWrapConfig), /// Show the wrapped Ignition config in an initrd image Unwrap(PxeIgnitionUnwrapConfig), } -// Raw command-line arguments before postprocessing into InstallConfig. #[derive(Debug, StructOpt)] -struct InstallCmd { +pub struct InstallConfig { // ways to specify the image source /// Fedora CoreOS stream #[structopt(short, long, value_name = "name")] #[structopt(conflicts_with = "image-file", conflicts_with = "image-url")] - stream: Option, + pub stream: Option, /// Manually specify the image URL #[structopt(short = "u", long, value_name = "URL")] #[structopt(conflicts_with = "stream", conflicts_with = "image-file")] - image_url: Option, + pub image_url: Option, /// Manually specify a local image file #[structopt(short = "f", long, value_name = "path")] #[structopt(conflicts_with = "stream", conflicts_with = "image-url")] - image_file: Option, + pub image_file: Option, // postprocessing options /// Embed an Ignition config from a file // deprecated long name from <= 0.1.2 #[structopt(short, long, alias = "ignition", value_name = "path")] #[structopt(conflicts_with = "ignition-url")] - ignition_file: Option, + pub ignition_file: Option, /// Embed an Ignition config from a URL #[structopt(short = "I", long, value_name = "URL")] #[structopt(conflicts_with = "ignition-file")] - ignition_url: Option, + pub ignition_url: Option, /// Digest (type-value) of the Ignition config #[structopt(long, value_name = "digest")] - ignition_hash: Option, + pub ignition_hash: Option, /// Override the Ignition platform ID #[structopt(short, long, value_name = "name")] - platform: Option, + pub platform: Option, /// Additional kernel args for the first boot // This used to be for configuring networking from the cmdline, but it has // been obsoleted by the nicer `--copy-network` approach. We still need it // for now though. It's used at least by `coreos-installer.service`. #[structopt(long, hidden = true, value_name = "args")] - firstboot_args: Option, + pub firstboot_args: Option, /// Append default kernel arg #[structopt(long, value_name = "arg", number_of_values = 1)] - append_karg: Vec, + pub append_karg: Vec, /// Delete default kernel arg #[structopt(long, value_name = "arg", number_of_values = 1)] - delete_karg: Vec, + pub delete_karg: Vec, /// Copy network config from install environment #[structopt(short = "n", long)] - copy_network: bool, + pub copy_network: bool, /// For use with -n. #[structopt(long, value_name = "path", empty_values = false)] #[structopt(default_value = "/etc/NetworkManager/system-connections/")] @@ -197,13 +170,13 @@ struct InstallCmd { #[structopt(verbatim_doc_comment)] // so we can stay under 80 chars #[structopt(next_line_help(true))] - network_dir: String, + pub network_dir: String, /// Save partitions with this label glob #[structopt(long, value_name = "lx")] // Allow argument multiple times, but one value each. Allow "a,b" in // one argument. #[structopt(number_of_values = 1, require_delimiter = true)] - save_partlabel: Vec, + pub save_partlabel: Vec, /// Save partitions with this number or range #[structopt(long, value_name = "id")] // Allow argument multiple times, but one value each. Allow "1-5,7" in @@ -211,50 +184,34 @@ struct InstallCmd { #[structopt(number_of_values = 1, require_delimiter = true)] // Allow ranges like "-2". #[structopt(allow_hyphen_values = true)] - save_partindex: Vec, + pub save_partindex: Vec, // obscure options without short names /// Force offline installation #[structopt(long)] - offline: bool, + pub offline: bool, /// Skip signature verification #[structopt(long)] - insecure: bool, + pub insecure: bool, /// Allow Ignition URL without HTTPS or hash #[structopt(long)] - insecure_ignition: bool, + pub insecure_ignition: bool, /// Base URL for Fedora CoreOS stream metadata #[structopt(long, value_name = "URL")] - stream_base_url: Option, + pub stream_base_url: Option, /// Target CPU architecture #[structopt(long, default_value, value_name = "name")] - architecture: Architecture, + pub architecture: Architecture, /// Don't clear partition table on error #[structopt(long)] - preserve_on_error: bool, + pub preserve_on_error: bool, /// Fetch retries, or "infinite" #[structopt(long, value_name = "N", default_value)] - fetch_retries: FetchRetries, + pub fetch_retries: FetchRetries, // positional args /// Destination device - device: String, -} - -pub struct InstallConfig { pub device: String, - pub location: Box, - pub ignition: Option, - pub ignition_hash: Option, - pub platform: Option, - pub firstboot_kargs: Option, - pub append_kargs: Vec, - pub delete_kargs: Vec, - pub insecure: bool, - pub preserve_on_error: bool, - pub network_config: Option, - pub save_partitions: Vec, - pub fetch_retries: FetchRetries, } #[derive(Debug, Clone, Copy)] @@ -270,46 +227,38 @@ pub enum PartitionFilter { Index(Option, Option), } -// Raw command-line arguments before postprocessing into DownloadConfig. #[derive(Debug, StructOpt)] -struct DownloadCmd { +pub struct DownloadConfig { /// Fedora CoreOS stream #[structopt(short, long, value_name = "name", default_value = "stable")] - stream: String, + pub stream: String, /// Target CPU architecture #[structopt(long, value_name = "name", default_value)] - architecture: Architecture, + pub architecture: Architecture, /// Fedora CoreOS platform name #[structopt(short, long, value_name = "name", default_value = "metal")] - platform: String, + pub platform: String, /// Image format #[structopt(short, long, value_name = "name", default_value = "raw.xz")] - format: String, + pub format: String, /// Manually specify the image URL #[structopt(short = "u", long, value_name = "URL")] - image_url: Option, + pub image_url: Option, /// Destination directory #[structopt(short = "C", long, value_name = "path", default_value = ".")] - directory: String, + pub directory: String, /// Decompress image and don't save signature #[structopt(short, long)] - decompress: bool, + pub decompress: bool, /// Skip signature verification #[structopt(long)] - insecure: bool, + pub insecure: bool, /// Base URL for Fedora CoreOS stream metadata #[structopt(long, value_name = "URL")] - stream_base_url: Option, + pub stream_base_url: Option, /// Fetch retries, or "infinite" #[structopt(long, value_name = "N", default_value)] - fetch_retries: FetchRetries, -} - -pub struct DownloadConfig { - pub location: Box, - pub directory: String, - pub decompress: bool, - pub insecure: bool, + pub fetch_retries: FetchRetries, } #[derive(Debug, StructOpt)] @@ -323,36 +272,36 @@ pub struct ListStreamConfig { } #[derive(Debug, StructOpt)] -struct IsoEmbedConfig { +pub struct IsoEmbedConfig { /// Ignition config to embed [default: stdin] #[structopt(short, long, value_name = "path")] - config: Option, + pub config: Option, /// Overwrite an existing embedded Ignition config #[structopt(short, long)] - force: bool, + pub force: bool, /// Write ISO to a new output file #[structopt(short, long, value_name = "path")] - output: Option, + pub output: Option, /// ISO image #[structopt(value_name = "ISO")] - input: String, + pub input: String, } #[derive(Debug, StructOpt)] -struct IsoShowConfig { +pub struct IsoShowConfig { /// ISO image #[structopt(value_name = "ISO")] - input: String, + pub input: String, } #[derive(Debug, StructOpt)] -struct IsoRemoveConfig { +pub struct IsoRemoveConfig { /// Write ISO to a new output file #[structopt(short, long, value_name = "path")] - output: Option, + pub output: Option, /// ISO image #[structopt(value_name = "ISO")] - input: String, + pub input: String, } #[derive(Debug, StructOpt)] @@ -490,166 +439,6 @@ pub struct PxeIgnitionUnwrapConfig { pub input: String, } -/// Parse command-line arguments. -pub fn parse_args() -> Result { - Ok(match Cmd::from_args() { - Cmd::Install(config) => Config::Install(parse_install(config)?), - Cmd::Download(config) => Config::Download(parse_download(config)?), - Cmd::ListStream(config) => Config::ListStream(config), - Cmd::Iso(config) => match config { - IsoCmd::Embed(config) => Config::IsoEmbed(config.into()), - IsoCmd::Show(config) => Config::IsoShow(config.into()), - IsoCmd::Remove(config) => Config::IsoRemove(config.into()), - IsoCmd::Ignition(config) => match config { - IsoIgnitionCmd::Embed(config) => Config::IsoIgnitionEmbed(config), - IsoIgnitionCmd::Show(config) => Config::IsoIgnitionShow(config), - IsoIgnitionCmd::Remove(config) => Config::IsoIgnitionRemove(config), - }, - IsoCmd::Kargs(config) => match config { - IsoKargsCmd::Modify(config) => Config::IsoKargsModify(config), - IsoKargsCmd::Reset(config) => Config::IsoKargsReset(config), - IsoKargsCmd::Show(config) => Config::IsoKargsShow(config), - }, - }, - Cmd::Osmet(config) => match config { - OsmetCmd::Pack(config) => Config::OsmetPack(config), - OsmetCmd::Unpack(config) => Config::OsmetUnpack(config), - OsmetCmd::Fiemap(config) => Config::OsmetFiemap(config), - }, - Cmd::Pxe(config) => match config { - PxeCmd::Ignition(config) => match config { - PxeIgnitionCmd::Wrap(config) => Config::PxeIgnitionWrap(config), - PxeIgnitionCmd::Unwrap(config) => Config::PxeIgnitionUnwrap(config), - }, - }, - }) -} - -fn parse_install(cmd: InstallCmd) -> Result { - // Uninitialized ECKD DASD's blocksize is 512, but after formatting - // it changes to the recommended 4096 - // https://bugzilla.redhat.com/show_bug.cgi?id=1905159 - #[allow(clippy::match_bool, clippy::match_single_binding)] - let sector_size = match is_dasd(&cmd.device, None) - .with_context(|| format!("checking whether {} is an IBM DASD disk", &cmd.device))? - { - #[cfg(target_arch = "s390x")] - true => dasd_try_get_sector_size(&cmd.device).transpose(), - _ => None, - }; - let sector_size = sector_size - .unwrap_or_else(|| get_sector_size_for_path(Path::new(&cmd.device))) - .with_context(|| format!("getting sector size of {}", &cmd.device))? - .get(); - - let location: Box = if let Some(image_file) = cmd.image_file { - Box::new(FileLocation::new(&image_file)) - } else if let Some(image_url) = cmd.image_url { - Box::new(UrlLocation::new(&image_url, cmd.fetch_retries)) - } else if cmd.offline { - match OsmetLocation::new(cmd.architecture.as_str(), sector_size)? { - Some(osmet) => Box::new(osmet), - None => bail!("cannot perform offline install; metadata missing"), - } - } else { - // For now, using --stream automatically will cause a download. In the future, we could - // opportunistically use osmet if the version and stream match an osmet file/the live ISO. - - let maybe_osmet = if cmd.stream.is_some() { - None - } else { - OsmetLocation::new(cmd.architecture.as_str(), sector_size)? - }; - - if let Some(osmet) = maybe_osmet { - Box::new(osmet) - } else { - let format = match sector_size { - 4096 => "4k.raw.xz", - 512 => "raw.xz", - n => { - // could bail on non-512, but let's be optimistic and just warn but try the regular - // 512b image - eprintln!( - "Found non-standard sector size {} for {}, assuming 512b-compatible", - n, &cmd.device - ); - "raw.xz" - } - }; - Box::new(StreamLocation::new( - cmd.stream.as_deref().unwrap_or("stable"), - cmd.architecture.as_str(), - "metal", - format, - cmd.stream_base_url.as_ref(), - cmd.fetch_retries, - )?) - } - }; - - let ignition = if let Some(file) = cmd.ignition_file { - Some( - OpenOptions::new() - .read(true) - .open(&file) - .with_context(|| format!("opening source Ignition config {}", file))?, - ) - } else if let Some(url) = cmd.ignition_url { - if url.scheme() == "http" { - if cmd.ignition_hash.is_none() && !cmd.insecure_ignition { - bail!("refusing to fetch Ignition config over HTTP without --ignition-hash or --insecure-ignition"); - } - } else if url.scheme() != "https" { - bail!("unknown protocol for URL '{}'", url); - } - Some( - download_to_tempfile(&url, cmd.fetch_retries) - .with_context(|| format!("downloading source Ignition config {}", url))?, - ) - } else { - None - }; - - // and report it to the user - eprintln!("{}", location); - - // If the user requested us to copy networking config by passing - // -n or --copy-network then copy networking config from the - // directory defined by --network-dir. - let network_config = if cmd.copy_network { - Some(cmd.network_dir) - } else { - None - }; - - // build configuration - Ok(InstallConfig { - device: cmd.device, - location, - ignition, - fetch_retries: cmd.fetch_retries, - ignition_hash: cmd.ignition_hash, - platform: cmd.platform, - firstboot_kargs: cmd.firstboot_args, - append_kargs: cmd.append_karg, - delete_kargs: cmd.delete_karg, - insecure: cmd.insecure, - preserve_on_error: cmd.preserve_on_error, - network_config, - save_partitions: parse_partition_filters( - &cmd.save_partlabel - .iter() - .map(|s| s.as_str()) - .collect::>(), - &cmd.save_partindex - .iter() - .map(|s| s.as_str()) - .collect::>(), - )?, - }) -} - impl FromStr for FetchRetries { type Err = Error; fn from_str(s: &str) -> Result { @@ -679,108 +468,6 @@ impl Default for FetchRetries { } } -fn parse_partition_filters(labels: &[&str], indexes: &[&str]) -> Result> { - use PartitionFilter::*; - let mut filters: Vec = Vec::new(); - - // partition label globs - for glob in labels { - let filter = Label( - glob::Pattern::new(glob) - .with_context(|| format!("couldn't parse label glob '{}'", glob))?, - ); - filters.push(filter); - } - - // partition index ranges - let parse_index = |i: &str| -> Result> { - match i { - "" => Ok(None), // open end of range - _ => Ok(Some( - NonZeroU32::new( - i.parse() - .with_context(|| format!("couldn't parse partition index '{}'", i))?, - ) - .context("partition index cannot be zero")?, - )), - } - }; - for range in indexes { - let parts: Vec<&str> = range.split('-').collect(); - let filter = match parts.len() { - 1 => Index(parse_index(parts[0])?, parse_index(parts[0])?), - 2 => Index(parse_index(parts[0])?, parse_index(parts[1])?), - _ => bail!("couldn't parse partition index range '{}'", range), - }; - match filter { - Index(None, None) => bail!( - "both ends of partition index range '{}' cannot be open", - range - ), - Index(Some(x), Some(y)) if x > y => bail!( - "start of partition index range '{}' cannot be greater than end", - range - ), - _ => filters.push(filter), - }; - } - Ok(filters) -} - -fn parse_download(cmd: DownloadCmd) -> Result { - // Build image location. Ideally we'd use conflicts_with (and an - // ArgGroup for streams), but that doesn't play well with default - // arguments, so we manually prioritize modes. - let location: Box = if let Some(image_url) = cmd.image_url { - Box::new(UrlLocation::new(&image_url, cmd.fetch_retries)) - } else { - Box::new(StreamLocation::new( - &cmd.stream, - cmd.architecture.as_str(), - &cmd.platform, - &cmd.format, - cmd.stream_base_url.as_ref(), - cmd.fetch_retries, - )?) - }; - - // build configuration - Ok(DownloadConfig { - location, - directory: cmd.directory, - decompress: cmd.decompress, - insecure: cmd.insecure, - }) -} - -impl From for IsoIgnitionEmbedConfig { - fn from(config: IsoEmbedConfig) -> Self { - Self { - force: config.force, - ignition_file: config.config, - output: config.output, - input: config.input, - } - } -} - -impl From for IsoIgnitionShowConfig { - fn from(config: IsoShowConfig) -> Self { - Self { - input: config.input, - } - } -} - -impl From for IsoIgnitionRemoveConfig { - fn from(config: IsoRemoveConfig) -> Self { - Self { - output: config.output, - input: config.input, - } - } -} - // A String wrapper with a default of `uname -m`. #[derive(Debug)] pub struct Architecture(String); @@ -809,64 +496,3 @@ impl Default for Architecture { Architecture(nix::sys::utsname::uname().machine().to_string()) } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_parse_partition_filters() { - use PartitionFilter::*; - - let g = |v| Label(glob::Pattern::new(v).unwrap()); - let i = |v| Some(NonZeroU32::new(v).unwrap()); - - assert_eq!( - parse_partition_filters(&["foo", "z*b?", ""], &["1", "7-7", "2-4", "-3", "4-"]) - .unwrap(), - vec![ - g("foo"), - g("z*b?"), - g(""), - Index(i(1), i(1)), - Index(i(7), i(7)), - Index(i(2), i(4)), - Index(None, i(3)), - Index(i(4), None) - ] - ); - - let bad_globs = vec![("***", "couldn't parse label glob '***'")]; - for (glob, err) in bad_globs { - assert_eq!( - &parse_partition_filters(&["f", glob, "z*"], &["7-", "34"]) - .unwrap_err() - .to_string(), - err - ); - } - - let bad_ranges = vec![ - ("", "both ends of partition index range '' cannot be open"), - ("-", "both ends of partition index range '-' cannot be open"), - ("--", "couldn't parse partition index range '--'"), - ("0", "partition index cannot be zero"), - ("-2-3", "couldn't parse partition index range '-2-3'"), - ("23q", "couldn't parse partition index '23q'"), - ("23-45.7", "couldn't parse partition index '45.7'"), - ("0x7", "couldn't parse partition index '0x7'"), - ( - "9-7", - "start of partition index range '9-7' cannot be greater than end", - ), - ]; - for (range, err) in bad_ranges { - assert_eq!( - &parse_partition_filters(&["f", "z*"], &["7-", range, "34"]) - .unwrap_err() - .to_string(), - err - ); - } - } -} diff --git a/src/download.rs b/src/download.rs index b0299f086..8814113f0 100644 --- a/src/download.rs +++ b/src/download.rs @@ -32,8 +32,24 @@ use crate::verify::*; // Download all artifacts for an image and verify their signatures. pub fn download(config: &DownloadConfig) -> Result<()> { + // Build image location. Ideally the parser would use conflicts_with + // (and an ArgGroup for streams), but that doesn't play well with + // default arguments, so we manually prioritize modes. + let location: Box = if let Some(ref image_url) = config.image_url { + Box::new(UrlLocation::new(image_url, config.fetch_retries)) + } else { + Box::new(StreamLocation::new( + &config.stream, + config.architecture.as_str(), + &config.platform, + &config.format, + config.stream_base_url.as_ref(), + config.fetch_retries, + )?) + }; + // walk sources - let mut sources = config.location.sources()?; + let mut sources = location.sources()?; if sources.is_empty() { bail!("no artifacts found"); } diff --git a/src/install.rs b/src/install.rs index 9cf4cff9d..ce552daff 100644 --- a/src/install.rs +++ b/src/install.rs @@ -20,6 +20,7 @@ use std::fs::{ copy as fscopy, create_dir_all, read_dir, set_permissions, File, OpenOptions, Permissions, }; use std::io::{copy, Read, Seek, SeekFrom, Write}; +use std::num::NonZeroU32; use std::os::unix::fs::{FileTypeExt, PermissionsExt}; use std::path::{Path, PathBuf}; @@ -32,14 +33,127 @@ use crate::s390x; use crate::source::*; pub fn install(config: &InstallConfig) -> Result<()> { + // find Ignition config + let ignition = if let Some(ref file) = config.ignition_file { + Some( + OpenOptions::new() + .read(true) + .open(file) + .with_context(|| format!("opening source Ignition config {}", file))?, + ) + } else if let Some(ref url) = config.ignition_url { + if url.scheme() == "http" { + if config.ignition_hash.is_none() && !config.insecure_ignition { + bail!("refusing to fetch Ignition config over HTTP without --ignition-hash or --insecure-ignition"); + } + } else if url.scheme() != "https" { + bail!("unknown protocol for URL '{}'", url); + } + Some( + download_to_tempfile(url, config.fetch_retries) + .with_context(|| format!("downloading source Ignition config {}", url))?, + ) + } else { + None + }; + + // find network config + // If the user requested us to copy networking config by passing + // -n or --copy-network then copy networking config from the + // directory defined by --network-dir. + let network_config = if config.copy_network { + Some(config.network_dir.as_str()) + } else { + None + }; + + // parse partition saving filters + let save_partitions = parse_partition_filters( + &config + .save_partlabel + .iter() + .map(|s| s.as_str()) + .collect::>(), + &config + .save_partindex + .iter() + .map(|s| s.as_str()) + .collect::>(), + )?; + + // compute sector size + // Uninitialized ECKD DASD's blocksize is 512, but after formatting + // it changes to the recommended 4096 + // https://bugzilla.redhat.com/show_bug.cgi?id=1905159 + #[allow(clippy::match_bool, clippy::match_single_binding)] + let sector_size = match is_dasd(&config.device, None) + .with_context(|| format!("checking whether {} is an IBM DASD disk", &config.device))? + { + #[cfg(target_arch = "s390x")] + true => dasd_try_get_sector_size(&config.device).transpose(), + _ => None, + }; + let sector_size = sector_size + .unwrap_or_else(|| get_sector_size_for_path(Path::new(&config.device))) + .with_context(|| format!("getting sector size of {}", &config.device))? + .get(); + // set up image source + // create location + let location: Box = if let Some(ref image_file) = config.image_file { + Box::new(FileLocation::new(image_file)) + } else if let Some(ref image_url) = config.image_url { + Box::new(UrlLocation::new(image_url, config.fetch_retries)) + } else if config.offline { + match OsmetLocation::new(config.architecture.as_str(), sector_size)? { + Some(osmet) => Box::new(osmet), + None => bail!("cannot perform offline install; metadata missing"), + } + } else { + // For now, using --stream automatically will cause a download. In the future, we could + // opportunistically use osmet if the version and stream match an osmet file/the live ISO. + + let maybe_osmet = if config.stream.is_some() { + None + } else { + OsmetLocation::new(config.architecture.as_str(), sector_size)? + }; + + if let Some(osmet) = maybe_osmet { + Box::new(osmet) + } else { + let format = match sector_size { + 4096 => "4k.raw.xz", + 512 => "raw.xz", + n => { + // could bail on non-512, but let's be optimistic and just warn but try the regular + // 512b image + eprintln!( + "Found non-standard sector size {} for {}, assuming 512b-compatible", + n, &config.device + ); + "raw.xz" + } + }; + Box::new(StreamLocation::new( + config.stream.as_deref().unwrap_or("stable"), + config.architecture.as_str(), + "metal", + format, + config.stream_base_url.as_ref(), + config.fetch_retries, + )?) + } + }; + // report it to the user + eprintln!("{}", location); // we only support installing from a single artifact - let mut sources = config.location.sources()?; + let mut sources = location.sources()?; let mut source = sources.pop().context("no artifacts found")?; if !sources.is_empty() { bail!("found multiple artifacts"); } - if source.signature.is_none() && config.location.require_signature() { + if source.signature.is_none() && location.require_signature() { if config.insecure { eprintln!("Signature not found; skipping verification as requested"); } else { @@ -47,10 +161,11 @@ pub fn install(config: &InstallConfig) -> Result<()> { } } + // set up DASD #[cfg(target_arch = "s390x")] { if is_dasd(&config.device, None)? { - if !config.save_partitions.is_empty() { + if !save_partitions.is_empty() { // The user requested partition saving, but SavedPartitions // doesn't understand DASD VTOCs and won't find any partitions // to save. @@ -78,7 +193,7 @@ pub fn install(config: &InstallConfig) -> Result<()> { .with_context(|| format!("checking for exclusive access to {}", &config.device))?; // save partitions that we plan to keep - let saved = SavedPartitions::new_from_disk(&mut dest, &config.save_partitions) + let saved = SavedPartitions::new_from_disk(&mut dest, &save_partitions) .with_context(|| format!("saving partitions from {}", config.device))?; // get reference to partition table @@ -93,7 +208,15 @@ pub fn install(config: &InstallConfig) -> Result<()> { // from accidentally being used. dest.seek(SeekFrom::Start(0)) .with_context(|| format!("seeking {}", config.device))?; - if let Err(err) = write_disk(config, &mut source, &mut dest, &mut *table, &saved) { + if let Err(err) = write_disk( + config, + &mut source, + &mut dest, + &mut *table, + &saved, + ignition, + network_config, + ) { // log the error so the details aren't dropped if we encounter // another error during cleanup eprintln!("\nError: {:?}\n", err); @@ -122,6 +245,54 @@ pub fn install(config: &InstallConfig) -> Result<()> { Ok(()) } +fn parse_partition_filters(labels: &[&str], indexes: &[&str]) -> Result> { + use PartitionFilter::*; + let mut filters: Vec = Vec::new(); + + // partition label globs + for glob in labels { + let filter = Label( + glob::Pattern::new(glob) + .with_context(|| format!("couldn't parse label glob '{}'", glob))?, + ); + filters.push(filter); + } + + // partition index ranges + let parse_index = |i: &str| -> Result> { + match i { + "" => Ok(None), // open end of range + _ => Ok(Some( + NonZeroU32::new( + i.parse() + .with_context(|| format!("couldn't parse partition index '{}'", i))?, + ) + .context("partition index cannot be zero")?, + )), + } + }; + for range in indexes { + let parts: Vec<&str> = range.split('-').collect(); + let filter = match parts.len() { + 1 => Index(parse_index(parts[0])?, parse_index(parts[0])?), + 2 => Index(parse_index(parts[0])?, parse_index(parts[1])?), + _ => bail!("couldn't parse partition index range '{}'", range), + }; + match filter { + Index(None, None) => bail!( + "both ends of partition index range '{}' cannot be open", + range + ), + Index(Some(x), Some(y)) if x > y => bail!( + "start of partition index range '{}' cannot be greater than end", + range + ), + _ => filters.push(filter), + }; + } + Ok(filters) +} + fn ensure_exclusive_access(device: &str) -> Result<()> { let mut parts = Disk::new(device)?.get_busy_partitions()?; if parts.is_empty() { @@ -152,6 +323,8 @@ fn write_disk( dest: &mut File, table: &mut dyn PartTable, saved: &SavedPartitions, + ignition: Option, + network_config: Option<&str>, ) -> Result<()> { // Get sector size of destination, for comparing with image let sector_size = get_sector_size(dest)?; @@ -175,12 +348,12 @@ fn write_disk( table.reread()?; // postprocess - if config.ignition.is_some() - || config.firstboot_kargs.is_some() - || !config.append_kargs.is_empty() - || !config.delete_kargs.is_empty() + if ignition.is_some() + || config.firstboot_args.is_some() + || !config.append_karg.is_empty() + || !config.delete_karg.is_empty() || config.platform.is_some() - || config.network_config.is_some() + || network_config.is_some() || cfg!(target_arch = "s390x") { let mount = Disk::new(&config.device)?.mount_partition_by_label( @@ -188,22 +361,22 @@ fn write_disk( false, mount::MsFlags::empty(), )?; - if let Some(ignition) = config.ignition.as_ref() { + if let Some(ignition) = ignition.as_ref() { write_ignition(mount.mountpoint(), &config.ignition_hash, ignition) .context("writing Ignition configuration")?; } - if let Some(firstboot_kargs) = config.firstboot_kargs.as_ref() { - write_firstboot_kargs(mount.mountpoint(), firstboot_kargs) + if let Some(firstboot_args) = config.firstboot_args.as_ref() { + write_firstboot_kargs(mount.mountpoint(), firstboot_args) .context("writing firstboot kargs")?; } - if !config.append_kargs.is_empty() || !config.delete_kargs.is_empty() { + if !config.append_karg.is_empty() || !config.delete_karg.is_empty() { eprintln!("Modifying kernel arguments"); visit_bls_entry_options(mount.mountpoint(), |orig_options: &str| { bls_entry_options_delete_and_append_kargs( orig_options, - config.delete_kargs.as_slice(), - config.append_kargs.as_slice(), + config.delete_karg.as_slice(), + config.append_karg.as_slice(), &[], ) }) @@ -212,7 +385,7 @@ fn write_disk( if let Some(platform) = config.platform.as_ref() { write_platform(mount.mountpoint(), platform).context("writing platform ID")?; } - if let Some(network_config) = config.network_config.as_ref() { + if let Some(network_config) = network_config.as_ref() { copy_network_config(mount.mountpoint(), network_config)?; } #[cfg(target_arch = "s390x")] @@ -604,6 +777,62 @@ fn stash_saved_partitions(disk: &mut File, saved: &SavedPartitions) -> Result<() mod tests { use super::*; + #[test] + fn test_parse_partition_filters() { + use PartitionFilter::*; + + let g = |v| Label(glob::Pattern::new(v).unwrap()); + let i = |v| Some(NonZeroU32::new(v).unwrap()); + + assert_eq!( + parse_partition_filters(&["foo", "z*b?", ""], &["1", "7-7", "2-4", "-3", "4-"]) + .unwrap(), + vec![ + g("foo"), + g("z*b?"), + g(""), + Index(i(1), i(1)), + Index(i(7), i(7)), + Index(i(2), i(4)), + Index(None, i(3)), + Index(i(4), None) + ] + ); + + let bad_globs = vec![("***", "couldn't parse label glob '***'")]; + for (glob, err) in bad_globs { + assert_eq!( + &parse_partition_filters(&["f", glob, "z*"], &["7-", "34"]) + .unwrap_err() + .to_string(), + err + ); + } + + let bad_ranges = vec![ + ("", "both ends of partition index range '' cannot be open"), + ("-", "both ends of partition index range '-' cannot be open"), + ("--", "couldn't parse partition index range '--'"), + ("0", "partition index cannot be zero"), + ("-2-3", "couldn't parse partition index range '-2-3'"), + ("23q", "couldn't parse partition index '23q'"), + ("23-45.7", "couldn't parse partition index '45.7'"), + ("0x7", "couldn't parse partition index '0x7'"), + ( + "9-7", + "start of partition index range '9-7' cannot be greater than end", + ), + ]; + for (range, err) in bad_ranges { + assert_eq!( + &parse_partition_filters(&["f", "z*"], &["7-", range, "34"]) + .unwrap_err() + .to_string(), + err + ); + } + } + #[test] fn test_platform_id() { let orig_content = "ignition.platform.id=metal foo bar"; diff --git a/src/live.rs b/src/live.rs index e25bdc50e..a1d6357bd 100644 --- a/src/live.rs +++ b/src/live.rs @@ -36,19 +36,29 @@ const COREOS_KARG_EMBED_AREA_HEADER_SIZE: u64 = 72; const COREOS_KARG_EMBED_AREA_HEADER_MAX_OFFSETS: usize = 6; const COREOS_KARG_EMBED_AREA_MAX_SIZE: usize = 2048; -pub fn iso_embed(config: &IsoIgnitionEmbedConfig) -> Result<()> { +pub fn iso_embed(config: &IsoEmbedConfig) -> Result<()> { eprintln!("`iso embed` is deprecated; use `iso ignition embed`. Continuing."); - iso_ignition_embed(config) + iso_ignition_embed(&IsoIgnitionEmbedConfig { + force: config.force, + ignition_file: config.config.clone(), + output: config.output.clone(), + input: config.input.clone(), + }) } -pub fn iso_show(config: &IsoIgnitionShowConfig) -> Result<()> { +pub fn iso_show(config: &IsoShowConfig) -> Result<()> { eprintln!("`iso show` is deprecated; use `iso ignition show`. Continuing."); - iso_ignition_show(config) + iso_ignition_show(&IsoIgnitionShowConfig { + input: config.input.clone(), + }) } -pub fn iso_remove(config: &IsoIgnitionRemoveConfig) -> Result<()> { +pub fn iso_remove(config: &IsoRemoveConfig) -> Result<()> { eprintln!("`iso remove` is deprecated; use `iso ignition remove`. Continuing."); - iso_ignition_remove(config) + iso_ignition_remove(&IsoIgnitionRemoveConfig { + output: config.output.clone(), + input: config.input.clone(), + }) } pub fn iso_ignition_embed(config: &IsoIgnitionEmbedConfig) -> Result<()> { diff --git a/src/main.rs b/src/main.rs index 8cf344fef..907d4b1b4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,32 +12,43 @@ // See the License for the specific language governing permissions and // limitations under the License. -use anyhow::{Context, Result}; +use anyhow::Result; +use structopt::StructOpt; use libcoreinst::{cmdline, download, install, live, osmet, source}; -use cmdline::Config; +use cmdline::*; fn main() -> Result<()> { - let config = cmdline::parse_args().context("parsing arguments")?; - - match config { - Config::Download(c) => download::download(&c), - Config::ListStream(c) => source::list_stream(&c), - Config::Install(c) => install::install(&c), - Config::IsoEmbed(c) => live::iso_embed(&c), - Config::IsoShow(c) => live::iso_show(&c), - Config::IsoRemove(c) => live::iso_remove(&c), - Config::IsoIgnitionEmbed(c) => live::iso_ignition_embed(&c), - Config::IsoIgnitionShow(c) => live::iso_ignition_show(&c), - Config::IsoIgnitionRemove(c) => live::iso_ignition_remove(&c), - Config::IsoKargsModify(c) => live::iso_kargs_modify(&c), - Config::IsoKargsReset(c) => live::iso_kargs_reset(&c), - Config::IsoKargsShow(c) => live::iso_kargs_show(&c), - Config::OsmetFiemap(c) => osmet::osmet_fiemap(&c), - Config::OsmetPack(c) => osmet::osmet_pack(&c), - Config::OsmetUnpack(c) => osmet::osmet_unpack(&c), - Config::PxeIgnitionWrap(c) => live::pxe_ignition_wrap(&c), - Config::PxeIgnitionUnwrap(c) => live::pxe_ignition_unwrap(&c), + match Cmd::from_args() { + Cmd::Download(c) => download::download(&c), + Cmd::Install(c) => install::install(&c), + Cmd::ListStream(c) => source::list_stream(&c), + Cmd::Iso(c) => match c { + IsoCmd::Embed(c) => live::iso_embed(&c), + IsoCmd::Show(c) => live::iso_show(&c), + IsoCmd::Remove(c) => live::iso_remove(&c), + IsoCmd::Ignition(c) => match c { + IsoIgnitionCmd::Embed(c) => live::iso_ignition_embed(&c), + IsoIgnitionCmd::Show(c) => live::iso_ignition_show(&c), + IsoIgnitionCmd::Remove(c) => live::iso_ignition_remove(&c), + }, + IsoCmd::Kargs(c) => match c { + IsoKargsCmd::Modify(c) => live::iso_kargs_modify(&c), + IsoKargsCmd::Reset(c) => live::iso_kargs_reset(&c), + IsoKargsCmd::Show(c) => live::iso_kargs_show(&c), + }, + }, + Cmd::Osmet(c) => match c { + OsmetCmd::Fiemap(c) => osmet::osmet_fiemap(&c), + OsmetCmd::Pack(c) => osmet::osmet_pack(&c), + OsmetCmd::Unpack(c) => osmet::osmet_unpack(&c), + }, + Cmd::Pxe(c) => match c { + PxeCmd::Ignition(c) => match c { + PxeIgnitionCmd::Wrap(c) => live::pxe_ignition_wrap(&c), + PxeIgnitionCmd::Unwrap(c) => live::pxe_ignition_unwrap(&c), + }, + }, } }