use anyhow::{bail, Context, Result};
use lazy_static::lazy_static;
use serde::Serialize;
use std::collections::HashMap;
use std::fs::{create_dir_all, read, File, OpenOptions};
use std::io::{self, copy, BufReader, BufWriter, Read, Seek, SeekFrom, Write};
use std::path::{Component, Path, PathBuf};
use crate::cmdline::*;
use crate::io::*;
use crate::iso9660::{self, IsoFs};
use crate::miniso;
use crate::util::set_die_on_sigpipe;
mod customize;
mod embed;
mod util;
use self::customize::*;
use self::embed::*;
use self::util::*;
const INITRD_LIVE_STAMP_PATH: &str = "etc/coreos-live-initramfs";
const COREOS_ISO_PXEBOOT_DIR: &str = "IMAGES/PXEBOOT";
const COREOS_ISO_ROOTFS_IMG: &str = "IMAGES/PXEBOOT/ROOTFS.IMG";
const COREOS_ISO_MINISO_FILE: &str = "COREOS/MINISO.DAT";
lazy_static! {
static ref ALL_GLOB: GlobMatcher = GlobMatcher::new(&["*"]).unwrap();
}
pub fn iso_embed(config: IsoEmbedConfig) -> Result<()> {
eprintln!("`iso embed` is deprecated; use `iso ignition embed`. Continuing.");
iso_ignition_embed(IsoIgnitionEmbedConfig {
force: config.force,
ignition_file: config.config,
output: config.output,
input: config.input,
})
}
pub fn iso_show(config: IsoShowConfig) -> Result<()> {
eprintln!("`iso show` is deprecated; use `iso ignition show`. Continuing.");
iso_ignition_show(IsoIgnitionShowConfig {
input: config.input,
})
}
pub fn iso_remove(config: IsoRemoveConfig) -> Result<()> {
eprintln!("`iso remove` is deprecated; use `iso ignition remove`. Continuing.");
iso_ignition_remove(IsoIgnitionRemoveConfig {
output: config.output,
input: config.input,
})
}
pub fn iso_ignition_embed(config: IsoIgnitionEmbedConfig) -> Result<()> {
let ignition = match &config.ignition_file {
Some(ignition_path) => {
read(ignition_path).with_context(|| format!("reading {}", ignition_path))?
}
None => {
let mut data = Vec::new();
io::stdin()
.lock()
.read_to_end(&mut data)
.context("reading stdin")?;
data
}
};
let mut iso_file = open_live_iso(&config.input, Some(config.output.as_ref()))?;
let mut iso = IsoConfig::for_file(&mut iso_file)?;
if !config.force && iso.have_ignition() {
bail!("This ISO image already has an embedded Ignition config; use -f to force.");
}
iso.initrd_mut().add(INITRD_IGNITION_PATH, ignition);
write_live_iso(&iso, &mut iso_file, config.output.as_ref())
}
pub fn iso_ignition_show(config: IsoIgnitionShowConfig) -> Result<()> {
set_die_on_sigpipe()?;
let mut iso_file = open_live_iso(&config.input, None)?;
let iso = IsoConfig::for_file(&mut iso_file)?;
if !iso.have_ignition() {
bail!("No embedded Ignition config.");
}
let stdout = io::stdout();
let mut out = stdout.lock();
out.write_all(
iso.initrd()
.get(INITRD_IGNITION_PATH)
.context("couldn't find Ignition config in archive")?,
)
.context("writing output")?;
out.flush().context("flushing output")?;
Ok(())
}
pub fn iso_ignition_remove(config: IsoIgnitionRemoveConfig) -> Result<()> {
let mut iso_file = open_live_iso(&config.input, Some(config.output.as_ref()))?;
let mut iso = IsoConfig::for_file(&mut iso_file)?;
iso.initrd_mut().remove(INITRD_IGNITION_PATH);
write_live_iso(&iso, &mut iso_file, config.output.as_ref())
}
pub fn iso_network_embed(config: IsoNetworkEmbedConfig) -> Result<()> {
let mut iso_file = open_live_iso(&config.input, Some(config.output.as_ref()))?;
let mut iso_fs = IsoFs::from_file(iso_file.try_clone().context("cloning file")?)
.context("parsing ISO9660 image")?;
let mut iso = IsoConfig::for_iso(&mut iso_fs)?;
if !OsFeatures::for_iso(&mut iso_fs)?.live_initrd_network {
bail!("This OS image does not support customizing network settings.");
}
if !config.force && iso.have_network() {
bail!("This ISO image already has embedded network settings; use -f to force.");
}
iso.remove_network();
initrd_network_embed(iso.initrd_mut(), &config.keyfile)?;
write_live_iso(&iso, &mut iso_file, config.output.as_ref())
}
pub fn iso_network_extract(config: IsoNetworkExtractConfig) -> Result<()> {
let mut iso_file = open_live_iso(&config.input, None)?;
let iso = IsoConfig::for_file(&mut iso_file)?;
initrd_network_extract(iso.initrd(), config.directory.as_ref())
}
pub fn iso_network_remove(config: IsoNetworkRemoveConfig) -> Result<()> {
let mut iso_file = open_live_iso(&config.input, Some(config.output.as_ref()))?;
let mut iso = IsoConfig::for_file(&mut iso_file)?;
iso.remove_network();
write_live_iso(&iso, &mut iso_file, config.output.as_ref())
}
pub fn pxe_ignition_wrap(config: PxeIgnitionWrapConfig) -> Result<()> {
if config.output.is_none() {
verify_stdout_not_tty()?;
}
let ignition = match &config.ignition_file {
Some(ignition_path) => {
read(ignition_path).with_context(|| format!("reading {}", ignition_path))?
}
None => {
let mut data = Vec::new();
io::stdin()
.lock()
.read_to_end(&mut data)
.context("reading stdin")?;
data
}
};
let mut initrd = Initrd::default();
initrd.add(INITRD_IGNITION_PATH, ignition);
write_live_pxe(&initrd, config.output.as_ref())
}
pub fn pxe_ignition_unwrap(config: PxeIgnitionUnwrapConfig) -> Result<()> {
set_die_on_sigpipe()?;
let stdin = io::stdin();
let mut f: Box<dyn Read> = if let Some(path) = &config.input {
Box::new(
OpenOptions::new()
.read(true)
.open(path)
.with_context(|| format!("opening {}", path))?,
)
} else {
Box::new(stdin.lock())
};
let stdout = io::stdout();
let mut out = stdout.lock();
out.write_all(
Initrd::from_reader_filtered(&mut f, &INITRD_IGNITION_GLOB)?
.get(INITRD_IGNITION_PATH)
.context("couldn't find Ignition config in archive")?,
)
.context("writing output")?;
out.flush().context("flushing output")?;
Ok(())
}
pub fn pxe_network_wrap(config: PxeNetworkWrapConfig) -> Result<()> {
if config.output.is_none() {
verify_stdout_not_tty()?;
}
let mut initrd = Initrd::default();
initrd_network_embed(&mut initrd, &config.keyfile)?;
write_live_pxe(&initrd, config.output.as_ref())
}
fn initrd_network_embed(initrd: &mut Initrd, keyfiles: &[String]) -> Result<()> {
for path in keyfiles {
let data = read(path).with_context(|| format!("reading {}", path))?;
let name = filename(path)?;
let path = format!("{}/{}", INITRD_NETWORK_DIR, name);
if initrd.get(&path).is_some() {
bail!("multiple input files named '{}'", name);
}
initrd.add(&path, data);
}
Ok(())
}
pub fn pxe_network_unwrap(config: PxeNetworkUnwrapConfig) -> Result<()> {
let stdin = io::stdin();
let f: Box<dyn Read> = if let Some(path) = &config.input {
Box::new(
OpenOptions::new()
.read(true)
.open(path)
.with_context(|| format!("opening {}", path))?,
)
} else {
Box::new(stdin.lock())
};
initrd_network_extract(
&Initrd::from_reader_filtered(f, &INITRD_NETWORK_GLOB)?,
config.directory.as_ref(),
)
}
fn initrd_network_extract(initrd: &Initrd, directory: Option<&String>) -> Result<()> {
let files = initrd.find(&INITRD_NETWORK_GLOB);
if files.is_empty() {
bail!("No embedded network settings.");
}
if let Some(dir) = directory {
create_dir_all(&dir)?;
for (path, contents) in files {
let path = Path::new(dir).join(filename(path)?);
OpenOptions::new()
.create_new(true)
.write(true)
.open(&path)
.with_context(|| format!("opening {}", path.display()))?
.write_all(contents)
.with_context(|| format!("writing {}", path.display()))?;
println!("{}", path.display());
}
} else {
set_die_on_sigpipe()?;
for (i, (path, contents)) in files.iter().enumerate() {
if i > 0 {
println!();
}
println!("########## {} ##########", filename(path)?);
io::stdout()
.lock()
.write_all(contents)
.context("writing network settings to stdout")?;
}
}
Ok(())
}
pub fn iso_kargs_modify(config: IsoKargsModifyConfig) -> Result<()> {
let mut iso_file = open_live_iso(&config.input, Some(config.output.as_ref()))?;
let mut iso = IsoConfig::for_file(&mut iso_file)?;
let kargs = KargsEditor::new()
.append(&config.append)
.replace(&config.replace)
.delete(&config.delete)
.apply_to(iso.kargs()?)?;
iso.set_kargs(&kargs)?;
write_live_iso(&iso, &mut iso_file, config.output.as_ref())
}
pub fn iso_kargs_reset(config: IsoKargsResetConfig) -> Result<()> {
let mut iso_file = open_live_iso(&config.input, Some(config.output.as_ref()))?;
let mut iso = IsoConfig::for_file(&mut iso_file)?;
#[allow(clippy::unnecessary_to_owned)]
iso.set_kargs(&iso.kargs_default()?.to_string())?;
write_live_iso(&iso, &mut iso_file, config.output.as_ref())
}
pub fn iso_kargs_show(config: IsoKargsShowConfig) -> Result<()> {
set_die_on_sigpipe()?;
let mut iso_file = open_live_iso(&config.input, None)?;
let iso = IsoConfig::for_file(&mut iso_file)?;
let kargs = if config.default {
iso.kargs_default()?
} else {
iso.kargs()?
};
println!("{}", kargs);
Ok(())
}
pub fn iso_customize(config: IsoCustomizeConfig) -> Result<()> {
let mut iso_file = open_live_iso(&config.input, Some(config.output.as_ref()))?;
let mut iso_fs = IsoFs::from_file(iso_file.try_clone().context("cloning file")?)
.context("parsing ISO9660 image")?;
let mut iso = IsoConfig::for_iso(&mut iso_fs)?;
if !config.force
&& (iso.have_ignition()
|| iso.have_network()
|| (iso.kargs_supported() && iso.kargs()? != iso.kargs_default()?))
{
bail!("This ISO image is already customized; use -f to force.");
}
let live = LiveInitrd::from_common(&config.common, OsFeatures::for_iso(&mut iso_fs)?)?;
*iso.initrd_mut() = live.into_initrd()?;
if [
&config.live_karg_append,
&config.live_karg_replace,
&config.live_karg_delete,
]
.iter()
.any(|v| !v.is_empty())
{
if !iso.kargs_supported() {
bail!("This OS image does not support customizing live kernel arguments.");
}
let kargs = KargsEditor::new()
.append(&config.live_karg_append)
.replace(&config.live_karg_replace)
.delete(&config.live_karg_delete)
.apply_to(iso.kargs_default()?)?;
iso.set_kargs(&kargs)?;
}
write_live_iso(&iso, &mut iso_file, config.output.as_ref())
}
pub fn iso_reset(config: IsoResetConfig) -> Result<()> {
let mut iso_file = open_live_iso(&config.input, Some(config.output.as_ref()))?;
let mut iso = IsoConfig::for_file(&mut iso_file)?;
*iso.initrd_mut() = Initrd::default();
if iso.kargs_supported() {
#[allow(clippy::unnecessary_to_owned)]
iso.set_kargs(&iso.kargs_default()?.to_string())?;
};
write_live_iso(&iso, &mut iso_file, config.output.as_ref())
}
pub fn pxe_customize(config: PxeCustomizeConfig) -> Result<()> {
let mut input = BufReader::with_capacity(
BUFFER_SIZE,
OpenOptions::new()
.read(true)
.open(&config.input)
.with_context(|| format!("opening {}", &config.input))?,
);
let mut tempfile = match &*config.output {
"-" => {
verify_stdout_not_tty()?;
None
}
path => {
let dir = Path::new(path)
.parent()
.with_context(|| format!("no parent directory of {}", path))?;
let tempfile = tempfile::Builder::new()
.prefix(".coreos-installer-temp-")
.tempfile_in(dir)
.context("creating temporary file")?;
Some(tempfile)
}
};
let filter = GlobMatcher::new(&[
INITRD_LIVE_STAMP_PATH,
INITRD_FEATURES_PATH,
INITRD_IGNITION_PATH,
&format!("{}/*", INITRD_NETWORK_DIR),
])
.unwrap();
let base_initrd = match &*config.output {
"-" => {
Initrd::from_reader_filtered(TeeReader::new(&mut input, io::stdout().lock()), &filter)
.context("reading/copying input initrd")?
}
_ => Initrd::from_reader_filtered(
TeeReader::new(&mut input, tempfile.as_mut().unwrap()),
&filter,
)
.context("reading/copying input initrd")?,
};
if base_initrd.get(INITRD_LIVE_STAMP_PATH).is_none() {
bail!("not a CoreOS live initramfs image");
}
if base_initrd.get(INITRD_IGNITION_PATH).is_some()
|| !base_initrd.find(&INITRD_NETWORK_GLOB).is_empty()
{
bail!("input is already customized");
}
let features = match base_initrd.get(INITRD_FEATURES_PATH) {
Some(json) => serde_json::from_slice::<OsFeatures>(json).context("parsing OS features")?,
None => OsFeatures::default(),
};
let live = LiveInitrd::from_common(&config.common, features)?;
let initrd = live.into_initrd()?;
let do_write = |writer: &mut dyn Write| -> Result<()> {
let mut buf = BufWriter::with_capacity(BUFFER_SIZE, writer);
buf.write_all(&initrd.to_bytes()?)
.context("writing initrd")?;
buf.flush().context("flushing initrd")
};
match &*config.output {
"-" => do_write(&mut io::stdout().lock()),
path => {
let mut tempfile = tempfile.unwrap();
do_write(tempfile.as_file_mut())?;
tempfile
.persist_noclobber(&path)
.map_err(|e| e.error)
.with_context(|| format!("persisting output file to {}", path))?;
Ok(())
}
}
}
#[derive(Serialize)]
struct DevShowIsoOutput {
header: IsoFs,
records: Vec<String>,
}
pub fn dev_show_iso(config: DevShowIsoConfig) -> Result<()> {
set_die_on_sigpipe()?;
let mut iso_file = open_live_iso(&config.input, None)?;
let stdout = io::stdout();
let mut out = stdout.lock();
if config.ignition || config.kargs {
let iso = IsoConfig::for_file(&mut iso_file)?;
let data = if config.ignition {
iso.initrd_header_json()?
} else {
iso.kargs_header_json()?
};
out.write_all(&data).context("failed to write header")?;
} else {
let mut iso = IsoFs::from_file(iso_file)?;
let records = iso
.walk()?
.map(|r| r.map(|(path, _)| path))
.collect::<Result<Vec<String>>>()
.context("while walking ISO filesystem")?;
let info = DevShowIsoOutput {
header: iso,
records,
};
serde_json::to_writer_pretty(&mut out, &info)
.context("failed to serialize ISO metadata")?;
out.write_all(b"\n").context("failed to write newline")?;
}
Ok(())
}
pub fn dev_show_initrd(config: DevShowInitrdConfig) -> Result<()> {
set_die_on_sigpipe()?;
let initrd = read_initrd(&config.input, &config.filter)?;
for path in initrd.find(&ALL_GLOB).keys() {
println!("{}", path);
}
Ok(())
}
pub fn dev_extract_initrd(config: DevExtractInitrdConfig) -> Result<()> {
let initrd = read_initrd(&config.input, &config.filter)?;
let base_path = Path::new(&config.directory);
for (path, contents) in initrd.find(&ALL_GLOB) {
if Path::new(path)
.components()
.any(|c| matches!(c, Component::RootDir | Component::ParentDir))
{
bail!("path {} contains path traversal", path);
}
let out_path = base_path.join(path);
if config.verbose {
println!("{}", out_path.display());
}
let out_parent = out_path
.parent()
.with_context(|| format!("finding parent of {}", out_path.display()))?;
create_dir_all(out_parent).with_context(|| format!("creating {}", out_parent.display()))?;
OpenOptions::new()
.create_new(true)
.write(true)
.open(&out_path)
.with_context(|| format!("opening {}", out_path.display()))?
.write_all(contents)
.with_context(|| format!("writing {}", out_path.display()))?;
}
Ok(())
}
fn read_initrd(path: &str, filter: &[String]) -> Result<Initrd> {
let filter = if filter.is_empty() {
vec!["*"]
} else {
filter.iter().map(String::as_str).collect()
};
let filter = GlobMatcher::new(&filter).context("parsing glob patterns")?;
match path {
"-" => Initrd::from_reader_filtered(io::stdin().lock(), &filter),
path => Initrd::from_reader_filtered(
OpenOptions::new()
.read(true)
.open(path)
.with_context(|| format!("opening {}", path))?,
&filter,
),
}
.context("decoding initrd")
}
pub fn iso_extract_pxe(config: IsoExtractPxeConfig) -> Result<()> {
let mut iso = IsoFs::from_file(open_live_iso(&config.input, None)?)?;
let pxeboot = iso
.get_path(COREOS_ISO_PXEBOOT_DIR)
.context("Unrecognized CoreOS ISO image.")?
.try_into_dir()?;
create_dir_all(&config.output_dir)?;
let base = {
let mut s = Path::new(&config.input).file_stem().unwrap().to_os_string();
s.push("-");
s
};
for record in iso.list_dir(&pxeboot)? {
match record? {
iso9660::DirectoryRecord::Directory(_) => continue,
iso9660::DirectoryRecord::File(file) => {
let filename = {
let mut s = base.clone();
s.push(file.name.to_lowercase());
s
};
let path = Path::new(&config.output_dir).join(&filename);
println!("{}", path.display());
copy_file_from_iso(&mut iso, &file, &path)?;
}
}
}
Ok(())
}
pub fn iso_extract_minimal_iso(config: IsoExtractMinimalIsoConfig) -> Result<()> {
let mut full_iso = IsoFs::from_file(open_live_iso(&config.input, None)?)?;
let iso = IsoConfig::for_iso(&mut full_iso)?;
if !iso.initrd().is_empty() || iso.kargs()? != iso.kargs_default()? {
bail!("Cannot operate on ISO with embedded customizations.\nReset it with `coreos-installer iso reset` and try again.");
}
let output_dir: PathBuf = if &config.output == "-" {
verify_stdout_not_tty()?;
std::env::temp_dir()
} else {
Path::new(&config.output)
.parent()
.with_context(|| format!("no parent directory of {}", &config.output))?
.into()
};
if let Some(path) = &config.output_rootfs {
let rootfs = full_iso
.get_path(COREOS_ISO_ROOTFS_IMG)
.with_context(|| format!("looking up '{}'", COREOS_ISO_ROOTFS_IMG))?
.try_into_file()?;
copy_file_from_iso(&mut full_iso, &rootfs, Path::new(path))?;
}
let miniso_data_file = match full_iso.get_path(COREOS_ISO_MINISO_FILE) {
Ok(record) => record.try_into_file()?,
Err(e) if e.is::<iso9660::NotFound>() => {
bail!("This ISO image does not support extracting a minimal ISO.")
}
Err(e) => {
return Err(e).with_context(|| format!("looking up '{}'", COREOS_ISO_MINISO_FILE))
}
};
let data = {
let mut f = full_iso.read_file(&miniso_data_file)?;
miniso::Data::deserialize(&mut f).context("reading miniso data file")?
};
let mut outf = tempfile::Builder::new()
.prefix(".coreos-installer-temp-")
.tempfile_in(&output_dir)
.context("creating temporary file")?;
data.unxzpack(full_iso.as_file()?, &mut outf)
.context("unpacking miniso")?;
modify_miniso_kargs(outf.as_file_mut(), config.rootfs_url.as_ref())
.context("modifying miniso kernel args")?;
if &config.output == "-" {
outf.seek(SeekFrom::Start(0))
.context("seeking back to start of miniso tempfile")?;
copy(&mut outf, &mut io::stdout().lock()).context("writing output")?;
} else {
outf.persist_noclobber(&config.output)
.map_err(|e| e.error)?;
}
Ok(())
}
pub fn pack_minimal_iso(config: PackMinimalIsoConfig) -> Result<()> {
let mut full_iso = IsoFs::from_file(open_live_iso(&config.full, Some(None))?)?;
let mut minimal_iso = IsoFs::from_file(open_live_iso(&config.minimal, None)?)?;
let full_files = collect_iso_files(&mut full_iso)
.with_context(|| format!("collecting files from {}", &config.full))?;
let minimal_files = collect_iso_files(&mut minimal_iso)
.with_context(|| format!("collecting files from {}", &config.minimal))?;
if full_files.is_empty() {
bail!("No files found in {}", &config.full);
} else if minimal_files.is_empty() {
bail!("No files found in {}", &config.minimal);
}
eprintln!("Packing minimal ISO");
let (data, matches, skipped, written, written_compressed) =
miniso::Data::xzpack(minimal_iso.as_file()?, &full_files, &minimal_files)
.context("packing miniso")?;
eprintln!("Matched {} files of {}", matches, minimal_files.len());
eprintln!("Total bytes skipped: {}", skipped);
eprintln!("Total bytes written: {}", written);
eprintln!("Total bytes written (compressed): {}", written_compressed);
eprintln!("Verifying that packed image matches digest");
data.unxzpack(full_iso.as_file()?, std::io::sink())
.context("unpacking miniso for verification")?;
let miniso_entry = full_iso
.get_path(COREOS_ISO_MINISO_FILE)
.with_context(|| format!("looking up '{}'", COREOS_ISO_MINISO_FILE))?
.try_into_file()?;
let mut w = full_iso.overwrite_file(&miniso_entry)?;
data.serialize(&mut w).context("writing miniso data file")?;
w.flush().context("flushing full ISO")?;
if config.consume {
std::fs::remove_file(&config.minimal)
.with_context(|| format!("consuming {}", &config.minimal))?;
}
eprintln!("Packing successful!");
Ok(())
}
fn collect_iso_files(iso: &mut IsoFs) -> Result<HashMap<String, iso9660::File>> {
iso.walk()?
.filter_map(|r| match r {
Err(e) => Some(Err(e)),
Ok((s, iso9660::DirectoryRecord::File(f))) => Some(Ok((s, f))),
Ok(_) => None,
})
.collect::<Result<HashMap<String, iso9660::File>>>()
.context("while walking ISO filesystem")
}
fn modify_miniso_kargs(f: &mut File, rootfs_url: Option<&String>) -> Result<()> {
let mut iso = IsoFs::from_file(f.try_clone().context("cloning a file")?)?;
let mut cfg = IsoConfig::for_file(f)?;
let kargs = cfg.kargs()?;
let liveiso_karg = kargs
.split_ascii_whitespace()
.find(|&karg| karg.starts_with("coreos.liveiso="))
.context("minimal ISO does not have coreos.liveiso= karg")?
.to_string();
let new_default_kargs = KargsEditor::new().delete(&[liveiso_karg]).apply_to(kargs)?;
cfg.set_kargs(&new_default_kargs)?;
if let Some(url) = rootfs_url {
if url.split_ascii_whitespace().count() > 1 {
bail!("forbidden whitespace found in '{}'", url);
}
let final_kargs = KargsEditor::new()
.append(&[format!("coreos.live.rootfs_url={}", url)])
.apply_to(&new_default_kargs)?;
cfg.set_kargs(&final_kargs)?;
}
write_live_iso(&cfg, f, None)?;
set_default_kargs(&mut iso, new_default_kargs)
}