From 85cc68458b623a6adb842f237bb530c635912fd1 Mon Sep 17 00:00:00 2001 From: Nikita Dubrovskii Date: Wed, 11 Mar 2020 10:30:09 -0400 Subject: [PATCH] s390x: add support for IBM Z DASD disks Support formatting, creating partitions, and installing onto s390x DASD disks. Co-authored-by: Benjamin Gilbert --- .travis.yml | 7 + Cargo.lock | 104 ++++++++++ Cargo.toml | 3 + scripts/coreos-installer-service | 19 +- src/blockdev.rs | 2 +- src/install.rs | 25 ++- src/main.rs | 2 + src/s390x/README.md | 11 ++ src/s390x/dasd.rs | 314 +++++++++++++++++++++++++++++++ src/s390x/mod.rs | 19 ++ src/s390x/zipl.rs | 130 +++++++++++++ 11 files changed, 628 insertions(+), 8 deletions(-) create mode 100644 src/s390x/README.md create mode 100644 src/s390x/dasd.rs create mode 100644 src/s390x/mod.rs create mode 100644 src/s390x/zipl.rs diff --git a/.travis.yml b/.travis.yml index 73e681349..eda4fb807 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,9 +6,16 @@ rust: - beta - nightly +arch: + - amd64 + - s390x + matrix: allow_failures: - rust: nightly + # VM apparently has broken network access + - rust: 1.41.0 + arch: s390x env: global: diff --git a/Cargo.lock b/Cargo.lock index f8356a5c8..3467ce8c4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -72,6 +72,12 @@ dependencies = [ "generic-array", ] +[[package]] +name = "build_const" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39092a32794787acd8525ee150305ff051b0aa6cc2abaf193924f5ab05425f39" + [[package]] name = "bumpalo" version = "3.2.1" @@ -152,6 +158,7 @@ dependencies = [ "cpio", "error-chain", "flate2", + "gptman", "hex", "libc", "maplit", @@ -182,6 +189,15 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d375c433320f6c5057ae04a04376eef4d04ce2801448cf8863a78da99107be4" +[[package]] +name = "crc" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d663548de7f5cca343f1e0a48d14dcfb0e9eb4e079ec58883b7251539fa10aeb" +dependencies = [ + "build_const", +] + [[package]] name = "crc32fast" version = "1.2.0" @@ -236,6 +252,20 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "err-derive" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22deed3a8124cff5fa835713fa105621e43bbdc46690c3a6b68328a012d350d4" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "rustversion", + "syn", + "synstructure", +] + [[package]] name = "error-chain" version = "0.12.2" @@ -362,6 +392,20 @@ dependencies = [ "wasi", ] +[[package]] +name = "gptman" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9938547ba5ebacce53add00e27014bd2e2689dace40636150ec8051a96f769bc" +dependencies = [ + "bincode", + "crc", + "err-derive", + "nix", + "serde", + "serde_derive", +] + [[package]] name = "h2" version = "0.2.4" @@ -776,6 +820,32 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74490b50b9fbe561ac330df47c08f3f33073d2d00c150f719147d7c54522fa1b" +[[package]] +name = "proc-macro-error" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc175e9777c3116627248584e8f8b3e2987405cabe1c0adf7d1dd28f09dc7880" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cc9795ca17eb581285ec44936da7fc2335a3f34f2ddd13118b6f4d515435c50" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "syn-mid", + "version_check", +] + [[package]] name = "proc-macro2" version = "1.0.18" @@ -908,6 +978,17 @@ dependencies = [ "winreg", ] +[[package]] +name = "rustversion" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9bdc5e856e51e685846fb6c13a1f5e5432946c2c90501bdc76a1319f19e29da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "ryu" version = "1.0.3" @@ -1041,6 +1122,29 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "syn-mid" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7be3539f6c128a931cf19dcee741c1af532c7fd387baa739c03dd2e96479338a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "synstructure" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b834f2d66f734cb897113e34aaff2f1ab4719ca946f9a7358dba8f8064148701" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "unicode-xid", +] + [[package]] name = "tempfile" version = "3.1.0" diff --git a/Cargo.toml b/Cargo.toml index 26da076b8..e690781db 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,9 @@ path = "src/main.rs" [profile.release] lto = true +[target.'cfg(target_arch = "s390x")'.dependencies] +gptman = { version = "^0.6", default-features = false } + [dependencies] bincode = "^1.3" byte-unit = "^4.0" diff --git a/scripts/coreos-installer-service b/scripts/coreos-installer-service index ac8439ee2..83c6242ac 100755 --- a/scripts/coreos-installer-service +++ b/scripts/coreos-installer-service @@ -10,6 +10,9 @@ PERSIST_KERNEL_NET_PARAMS=("ipv6.disable" "net.ifnames" "net.naming-scheme") # List from https://www.mankier.com/7/dracut.cmdline#Description-Network PERSIST_DRACUT_NET_PARAMS=("ip" "ifname" "rd.route" "bootdev" "BOOTIF" "rd.bootif" "nameserver" "rd.peerdns" "biosdevname" "vlan" "bond" "team" "bridge" "rd.net.timeout.carrier" "coreos.no_persist_ip") +# IBM S390X params to persist +PERSIST_S390X_PARAMS=("rd.dasd" "rd.zfcp" "rd.znet" "zfcp.allow_lun_scan" "cio_ignore") + args=("install") cmdline=( $( Result<()> { +pub fn udev_settle() -> Result<()> { // "udevadm settle" silently no-ops if the udev socket is missing, and // then lsblk can't find partition labels. Catch this early. if !Path::new("/run/udev/control").exists() { diff --git a/src/install.rs b/src/install.rs index fa1ffe733..f2dd5affd 100644 --- a/src/install.rs +++ b/src/install.rs @@ -24,6 +24,8 @@ use crate::cmdline::*; use crate::download::*; use crate::errors::*; use crate::io::*; +#[cfg(target_arch = "s390x")] +use crate::s390x; use crate::source::*; /// Integrity verification hash for an Ignition config. @@ -99,6 +101,13 @@ pub fn install(config: &InstallConfig) -> Result<()> { } } + #[cfg(target_arch = "s390x")] + { + if is_dasd(config)? { + s390x::prepare_dasd(&config)?; + } + } + // open output; ensure it's a block device and we have exclusive access let mut dest = OpenOptions::new() .write(true) @@ -179,11 +188,17 @@ fn write_disk( let sector_size = get_sector_size(dest)?; // copy the image + #[allow(clippy::match_bool, clippy::match_single_binding)] + let image_copy = match is_dasd(config)? { + #[cfg(target_arch = "s390x")] + true => s390x::image_copy_s390x, + _ => image_copy_default, + }; write_image( source, dest, Path::new(&config.device), - image_copy_default, + image_copy, true, Some(sector_size), )?; @@ -196,6 +211,7 @@ fn write_disk( || config.delete_kargs.is_some() || config.platform.is_some() || config.network_config.is_some() + || cfg!(target_arch = "s390x") { let mount = Disk::new(&config.device).mount_partition_by_label("boot", mount::MsFlags::empty())?; @@ -225,6 +241,8 @@ fn write_disk( if let Some(network_config) = config.network_config.as_ref() { copy_network_config(mount.mountpoint(), network_config)?; } + #[cfg(target_arch = "s390x")] + s390x::install_bootloader(mount.mountpoint(), &config.device)?; } Ok(()) @@ -463,6 +481,11 @@ fn clear_partition_table(dest: &mut File, table: &mut dyn PartTable) -> Result<( Ok(()) } +fn is_dasd(config: &InstallConfig) -> Result { + let (target, _) = resolve_link(&config.device)?; + Ok(target.to_string_lossy().starts_with("/dev/dasd")) +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/main.rs b/src/main.rs index 21e895219..836d215ed 100644 --- a/src/main.rs +++ b/src/main.rs @@ -20,6 +20,8 @@ mod install; mod io; mod iso; mod osmet; +#[cfg(target_arch = "s390x")] +mod s390x; mod source; mod verify; diff --git a/src/s390x/README.md b/src/s390x/README.md new file mode 100644 index 000000000..f3f1bb9eb --- /dev/null +++ b/src/s390x/README.md @@ -0,0 +1,11 @@ +# CoreOS installation on IBM Z + +IBM s390x machines usually have DASD (Direct Access Storage Device) disks. + +To use DASD as a Linux hard disk we have to perform several steps: + +1. Low-level format it using `dasdfmt` tool +2. Create partitions on it using `fdasd` tool +3. Copy each partition from CoreOS image to corresponding partition on DASD +4. Install boot loader using `zipl` tool +5. Mark DASD as next boot device using `chreipl` tool diff --git a/src/s390x/dasd.rs b/src/s390x/dasd.rs new file mode 100644 index 000000000..ece9bc59f --- /dev/null +++ b/src/s390x/dasd.rs @@ -0,0 +1,314 @@ +// Copyright 2020 CoreOS, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use error_chain::bail; +use gptman::GPT; +use std::fs::{read_to_string, File}; +use std::io::{self, Cursor, Read, Seek, SeekFrom, Write}; +use std::num::NonZeroU32; +use std::os::unix::io::AsRawFd; +use std::path::Path; +use std::process::{Command, Stdio}; + +use crate::blockdev::{get_sector_size, udev_settle}; +use crate::cmdline::*; +use crate::errors::*; +use crate::io::{copy, copy_exactly_n}; + +///////////////////////////////////////////////////////////////////////////// +// IBM DASD Support +///////////////////////////////////////////////////////////////////////////// + +#[derive(Debug)] +struct Range { + in_offset: u64, + out_offset: u64, + length: u64, +} + +pub fn prepare_dasd(config: &InstallConfig) -> Result<()> { + low_level_format(&config.device)?; + if is_invalid(&config.device)? { + eprintln!("Disk {} is invalid, formatting", &config.device); + default_format(&config.device)? + } + Ok(()) +} + +pub fn image_copy_s390x( + first_mb: &[u8], + source: &mut dyn Read, + dest: &mut File, + dest_path: &Path, +) -> Result<()> { + let (ranges, partitions) = partition_ranges(first_mb, dest)?; + make_partitions( + dest_path + .to_str() + .chain_err(|| format!("couldn't encode path {}", dest_path.display()))?, + &partitions, + )?; + + // copy each partition + eprintln!("Installing to {}", dest_path.display()); + let mut buf = [0u8; 1024 * 1024]; + // there shouldn't be any partition data in the first MiB, so don't + // worry about copying first_mb + let mut cursor: u64 = 1024 * 1024; + let sink = &mut io::sink(); + for range in ranges.iter() { + if range.in_offset < cursor { + bail!( + "found partition at {} when current stream location is {}", + range.in_offset, + cursor + ); + } + if range.in_offset > cursor { + copy_exactly_n(source, sink, range.in_offset - cursor, &mut buf) + .chain_err(|| "sinking input data")?; + cursor = range.in_offset; + } + dest.seek(SeekFrom::Start(range.out_offset)) + .chain_err(|| "seeking output")?; + copy_exactly_n(source, dest, range.length, &mut buf).chain_err(|| "copying partition")?; + cursor += range.length; + } + + // close out the stream + copy(source, sink).chain_err(|| "reading remainder of stream")?; + + Ok(()) +} + +/// Generate partition table entries and byte ranges to copy +fn partition_ranges(header: &[u8], device: &mut File) -> Result<(Vec, Vec)> { + let bytes_per_block: u64 = get_sector_size(device)?.get().into(); + let blocks_per_track: u64 = get_sectors_per_track(device)?.get().into(); + + let gpt = GPT::read_from(&mut Cursor::new(header), bytes_per_block) + .chain_err(|| "reading GPT of source image")?; + + let mut ranges = Vec::new(); + let mut partitions = Vec::new(); + let mut start_track: u64 = 2; // the first 2 tracks of the ECKD DASD are reserved + let entries = || gpt.iter().filter(|(_, pt)| pt.is_used()); + let (last_partition, _) = entries() + .last() + .chain_err(|| "source image has no partitions")?; + + for (i, pt) in entries() { + let blocks = pt.ending_lba - pt.starting_lba + 1; + let end_track = start_track + (blocks + blocks_per_track - 1) / blocks_per_track - 1; + + ranges.push(Range { + in_offset: pt.starting_lba * bytes_per_block, + out_offset: start_track * blocks_per_track * bytes_per_block, + length: blocks * bytes_per_block, + }); + + if i == last_partition { + partitions.push(format!("[{}, last, native]", start_track)); + } else { + partitions.push(format!("[{}, {}, native]", start_track, end_track)); + }; + start_track = end_track + 1; + } + // partitions should be in offset order, but just to be sure + ranges.sort_unstable_by_key(|r| r.in_offset); + Ok((ranges, partitions)) +} + +/// Get disk bus id +/// +/// # Arguments +/// * `dasd` - dasd device, i.e. smth like /dev/dasda +fn bus_id(dasd: &str) -> Result { + let cmd = Command::new("lszdev") + .arg("-n") + .arg("-c") + .arg("ID") + .arg("--by-node") + .arg(dasd) + .stderr(Stdio::inherit()) + .output() + .chain_err(|| format!("executing lszdev on {}", dasd))?; + if !cmd.status.success() { + bail!("lszdev on {} failed", dasd); + } + Ok(std::str::from_utf8(&cmd.stdout) + .chain_err(|| "decoding lszdev output")? + .trim_end() + .to_string()) +} + +/// Check if disk is already formatted or not +/// +/// # Arguments +/// * `dasd` - dasd device, i.e. smth like /dev/dasda +fn is_formatted(dasd: &str) -> Result { + let id = bus_id(dasd)?; + let path = format!("/sys/bus/ccw/devices/{}/status", id); + let contents = read_to_string(&path).chain_err(|| format!("reading {}", path))?; + Ok(!contents.contains("unformatted")) +} + +/// Check if disk is valid or not +/// +/// # Arguments +/// * `dasd` - dasd device, i.e. smth like /dev/dasda +fn is_invalid(dasd: &str) -> Result { + let cmd = Command::new("fdasd") + // we're looking for a hardcoded string in the output + .env("LC_ALL", "C") + .arg("-p") + .arg(dasd) + .output() + .chain_err(|| format!("executing fdasd -p {}", dasd))?; + if !cmd.status.success() { + bail!("fdasd -p {} failed", dasd); + } + Ok(std::str::from_utf8(&cmd.stdout) + .chain_err(|| "decoding fdasd output")? + .contains("disk label block is invalid")) +} + +/// Perform low-level format. This step is necessary before any further disk usage +/// +/// # Arguments +/// * `dasd` - dasd device, i.e. smth like /dev/dasda +fn low_level_format(dasd: &str) -> Result<()> { + if is_formatted(dasd)? { + eprintln!("Skipping low-level format for {}", dasd); + return Ok(()); + } + eprintln!("Performing low-level format for {}", dasd); + let status = Command::new("dasdfmt") + .arg("--blocksize") + .arg("4096") + .arg("--disk_layout") + .arg("cdl") + .arg("--mode") + .arg("full") + .arg("-y") + .arg("-p") + .arg(dasd) + .status() + .chain_err(|| format!("failed to execute dasdfmt on {}", dasd))?; + if !status.success() { + bail!("dasdfmt on {} failed", dasd); + } + udev_settle()?; + Ok(()) +} + +/// Format disk and create partitions +/// +/// # Arguments +/// * `dasd` - dasd device, i.e. smth like /dev/dasda +/// * `partitions` - configuration strings +fn make_partitions(dasd: &str, partitions: &[String]) -> Result<()> { + if partitions.len() > 3 { + // fdasd silently ignores partitions after the first 3 + bail!("Can't create {} partitions, maximum 3", partitions.len()); + } + let mut config = partitions.join("\n"); + config.push('\n'); + if try_format(dasd, &config).is_err() { + default_format(dasd)?; + try_format(dasd, &config)?; + } + Ok(()) +} + +/// If config-based format fails, then we have to perform +/// an auto-format on the whole disk +/// +/// # Arguments +/// * `dasd` - dasd device, i.e. smth like /dev/dasda +fn default_format(dasd: &str) -> Result<()> { + eprintln!("Auto-partitioning {}", dasd); + let status = Command::new("fdasd") + .arg("-a") + .arg("-s") + .arg(dasd) + .status() + .chain_err(|| "failed to execute fdasd")?; + if !status.success() { + bail!("auto-formatting {} failed", dasd); + } + udev_settle()?; + Ok(()) +} + +/// Format disk using a config file +/// +/// # Arguments +/// * `dasd` - dasd device, i.e. smth like /dev/dasda +/// * `config` - configuration file contents +fn try_format(dasd: &str, config: &str) -> Result<()> { + eprintln!("Partitioning {}", dasd); + let mut child = Command::new("fdasd") + .arg("-s") + .arg("--config") + .arg("/dev/stdin") + .arg(dasd) + .stdin(Stdio::piped()) + .spawn() + .chain_err(|| "failed to execute fdasd")?; + child + .stdin + .as_mut() + .chain_err(|| "couldn't open fdasd stdin")? + .write_all(config.as_bytes()) + .chain_err(|| "couldn't write fdasd stdin")?; + if !child + .wait() + .chain_err(|| "couldn't wait on fdasd")? + .success() + { + bail!("couldn't format {} based on:\n{}", dasd, config); + } + udev_settle()?; + Ok(()) +} + +/// Get the number of sectors per track of a block device. +fn get_sectors_per_track(file: &File) -> Result { + let fd = file.as_raw_fd(); + let mut geo: ioctl::hd_geometry = Default::default(); + match unsafe { ioctl::hdio_getgeo(fd, &mut geo) } { + Ok(_) => { + NonZeroU32::new(geo.sectors.into()).ok_or_else(|| "found sectors/track of zero".into()) + } + Err(e) => Err(Error::with_chain(e, "getting disk geometry")), + } +} + +// create unsafe ioctl wrappers +mod ioctl { + use nix::ioctl_read_bad; + use std::os::raw::{c_uchar, c_ulong, c_ushort}; + + #[repr(C)] + #[derive(Debug, Default)] + pub struct hd_geometry { + pub heads: c_uchar, + pub sectors: c_uchar, + pub cylinders: c_ushort, + pub start: c_ulong, + } + + ioctl_read_bad!(hdio_getgeo, 0x0301, hd_geometry); +} diff --git a/src/s390x/mod.rs b/src/s390x/mod.rs new file mode 100644 index 000000000..af0b06bd2 --- /dev/null +++ b/src/s390x/mod.rs @@ -0,0 +1,19 @@ +// Copyright 2020 CoreOS, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pub mod dasd; +pub mod zipl; + +pub use dasd::{image_copy_s390x, prepare_dasd}; +pub use zipl::install_bootloader; diff --git a/src/s390x/zipl.rs b/src/s390x/zipl.rs new file mode 100644 index 000000000..747307a5f --- /dev/null +++ b/src/s390x/zipl.rs @@ -0,0 +1,130 @@ +// Copyright 2020 CoreOS, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use error_chain::bail; +use regex::RegexBuilder; +use std::fs::read_to_string; +use std::path::{Path, PathBuf}; +use std::process::{Command, Stdio}; +use walkdir::WalkDir; + +use crate::errors::*; + +///////////////////////////////////////////////////////////////////////////// +// IBM Z Bootloader Support +///////////////////////////////////////////////////////////////////////////// + +/// Generate zipl config and set boot device +/// +/// # Arguments +/// * `boot` - path to boot partition mountpoint, i.e. smth like /boot +/// * `disk` - smth like /dev/dasda +pub fn install_bootloader>(boot: P, disk: &str) -> Result<()> { + eprintln!("Installing bootloader"); + + let bls_config_path = get_bls_config_path(&boot)?; + let kernel_path = get_kernel_path(&boot)?; + let initramfs_path = get_initramfs_path(&boot)?; + + let kargs = format!("{} ignition.firstboot", get_boot_kargs(bls_config_path)?); + + let status = Command::new("zipl") + .arg("-P") + .arg(kargs) + .arg("-i") + .arg(kernel_path.as_os_str()) + .arg("-r") + .arg(initramfs_path.as_os_str()) + .arg("--target") + .arg(boot.as_ref().as_os_str()) + .arg("-n") + .stdout(Stdio::null()) + .status() + .chain_err(|| format!("failed to execute zipl on {}", disk))?; + if !status.success() { + bail!("couldn't install bootloader on {}", disk); + } + + eprintln!("Updating re-IPL device"); + let status = Command::new("chreipl") + .arg(disk) + .stdout(Stdio::null()) + .status() + .chain_err(|| format!("failed to execute chreipl on {}", disk))?; + if !status.success() { + bail!("couldn't set {} as boot device", disk); + } + Ok(()) +} + +fn find_file>(root: P, pat: &str) -> Result { + for entry in WalkDir::new(root.as_ref()) + .into_iter() + .filter_map(|e| e.ok()) + { + if !entry.file_type().is_file() { + continue; + } + let name = entry.file_name().to_string_lossy(); + if pat.starts_with('.') { + if !name.ends_with(pat) { + continue; + } + } else if !name.starts_with(pat) { + continue; + } + + return Ok(entry.path().to_path_buf()); + } + bail!( + "Cannot find file with mask: {} in {}", + pat, + root.as_ref().display() + ) +} + +fn get_bls_config_path>(boot: P) -> Result { + find_file(boot.as_ref().join("loader").join("entries"), ".conf") +} + +fn get_kernel_path>(boot: P) -> Result { + find_file(boot.as_ref().join("ostree"), "vmlinuz") +} + +fn get_initramfs_path>(boot: P) -> Result { + find_file(boot.as_ref().join("ostree"), "initram") +} + +fn get_boot_kargs>(bls_config: P) -> Result { + let contents = read_to_string(&bls_config) + .chain_err(|| format!("reading {}", bls_config.as_ref().display()))?; + // read kargs from options line + let pt = r"^options (?P.+)$"; + let opts = RegexBuilder::new(pt) + .multi_line(true) + .build() + .unwrap() + .captures(&contents) + .chain_err(|| format!("capturing {:?}", pt))? + .name("v") + .map(|v| v.as_str()) + .chain_err(|| format!("matching {:?}", pt))?; + // filter out variable substitutions such as $ignition_firstboot + let opts = RegexBuilder::new(r"(^| )\$[a-zA-Z0-9_]+") + .build() + .unwrap() + .replace_all(opts, "") + .to_string(); + Ok(opts) +}