diff --git a/Cargo.lock b/Cargo.lock index 4b86e3a68..f8aec9f48 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -235,6 +235,7 @@ dependencies = [ "serde_with", "serde_yaml", "tempfile", + "textwrap", "thiserror", "url", "uuid 1.1.2", diff --git a/Cargo.toml b/Cargo.toml index 07310c6c6..f288ce6eb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -77,6 +77,7 @@ serde_json = "^1.0" serde_with = ">= 1.9.4, < 2" serde_yaml = ">= 0.8, < 0.10" tempfile = ">= 3.1, < 4" +textwrap = { version = ">= 0.15.0, < 0.16.0", default-features = false } thiserror = "1.0" url = ">= 2.1, < 3.0" uuid = { version = ">= 0.8, < 2.0", features = ["v4"] } diff --git a/data/example-config.yaml b/data/example-config.yaml index fcf7c1acf..e0a8aa299 100644 --- a/data/example-config.yaml +++ b/data/example-config.yaml @@ -17,6 +17,8 @@ ignition-hash: digest architecture: name # Override the Ignition platform ID platform: name +# Kernel and bootloader console +console: [spec, spec] # Append default kernel arguments append-karg: [arg, arg] # Delete default kernel arguments diff --git a/docs/cmd/install.md b/docs/cmd/install.md index 150afe84a..155b29d46 100644 --- a/docs/cmd/install.md +++ b/docs/cmd/install.md @@ -69,6 +69,12 @@ OPTIONS: Install a system that will run on the specified cloud or virtualization platform, such as "vmware". + --console + Kernel and bootloader console + + Set the kernel and bootloader console, using the same syntax as the parameter to + the "console=" kernel argument. + --append-karg Append default kernel arg diff --git a/docs/cmd/iso.md b/docs/cmd/iso.md index a29c58601..3dbe7aa57 100644 --- a/docs/cmd/iso.md +++ b/docs/cmd/iso.md @@ -34,6 +34,13 @@ OPTIONS: Automatically run installer, installing to the specified destination device. The resulting boot media will overwrite the destination device without confirmation. + --dest-console + Kernel and bootloader console for dest + + Automatically run installer, configuring the specified kernel and bootloader + console for the destination system. The argument uses the same syntax as the + parameter to the "console=" kernel argument. + --dest-karg-append Destination kernel argument to append diff --git a/docs/cmd/pxe.md b/docs/cmd/pxe.md index df1d59854..00d8c9c40 100644 --- a/docs/cmd/pxe.md +++ b/docs/cmd/pxe.md @@ -34,6 +34,13 @@ OPTIONS: Automatically run installer, installing to the specified destination device. The resulting boot media will overwrite the destination device without confirmation. + --dest-console + Kernel and bootloader console for dest + + Automatically run installer, configuring the specified kernel and bootloader + console for the destination system. The argument uses the same syntax as the + parameter to the "console=" kernel argument. + --dest-karg-append Destination kernel argument to append diff --git a/docs/customizing-install.md b/docs/customizing-install.md index 9612b4885..57bbe9958 100644 --- a/docs/customizing-install.md +++ b/docs/customizing-install.md @@ -62,6 +62,18 @@ Available customizations include: for Ignition to fetch remote resources. - Specifying HTTPS certificate authorities to be trusted by Ignition, in both the installed system and the live environment (`--ignition-ca`). +- Specifying consoles to be used by the installed system (`--dest-console`), + using the syntax of the `console` + [kernel argument](https://www.kernel.org/doc/html/latest/admin-guide/serial-console.html). + Consoles are configured for both the bootloader (GRUB) and the booted OS + (kernel). Consoles are subject to the Linux kernel rules: the first + specified console of each type is used, and the last specified console + is the primary console. + Supported graphical consoles are `tty0`, `hvc0`, and `ttysclp0`. + Supported serial consoles are `ttyS` and `ttyAMA`, with optional + baud rate, parity, and number of data bits. + Examples: `--console tty0`, `--console ttyAMA0,115200`, + `--console ttyS1,115200n8`. - Modifying kernel arguments of the installed system (`--dest-karg-append`, `--dest-karg-delete`) or the live ISO environment (`--live-karg-append`, `--live-karg-replace`, `--live-karg-delete`). These options are useful if @@ -130,6 +142,8 @@ ignition-hash: digest architecture: name # Override the Ignition platform ID platform: name +# Kernel and bootloader console +console: [spec, spec] # Append default kernel arguments append-karg: [arg, arg] # Delete default kernel arguments diff --git a/docs/release-notes.md b/docs/release-notes.md index 3aeda7ab3..d5e9a4579 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -8,6 +8,8 @@ nav_order: 8 Major changes: +- install: Add `--console` for configuring kernel and bootloader console +- customize: Add `--dest-console` for configuring kernel and bootloader console - Support reading initrd images compressed with zstd - Add Fedora 38 signing key; drop Fedora 35 signing key @@ -18,6 +20,7 @@ Minor changes: - Fix unlikely decompression error reading initrd - Add release notes to documentation - iso: Detect incomplete ISO files +- Warn if console kargs could have used `--console`/`--dest-console` instead Internal changes: @@ -28,7 +31,8 @@ Internal changes: Packaging changes: - Require Rust ≥ 1.58.0 -- Add dependency on `zstd` crate and `libzstd` shared library +- Add dependencies on `textwrap` and `zstd` crates +- Add dependency on `libzstd` shared library - Support `serde_yaml` 0.9 - Remove non-Linux dependencies from vendor archive - Install example installer config file in `/usr/share/coreos-installer` diff --git a/fixtures/customize/dest.bu b/fixtures/customize/dest.bu index 004b32f61..1e17035c9 100644 --- a/fixtures/customize/dest.bu +++ b/fixtures/customize/dest.bu @@ -18,10 +18,12 @@ systemd: ConditionKernelCommandLine=dest-karg ConditionKernelCommandLine=!ignition.platform.id=metal ConditionKernelCommandLine=ignition.platform.id=qemu + ConditionKernelCommandLine=console=ttyS0,115200n8 [Service] Type=oneshot RemainAfterExit=true + ExecStart=/bin/grep -qz 'serial --unit=0 --speed=115200 --word=8 --parity=no.terminal_input serial.terminal_output serial.#' /boot/grub2/grub.cfg ExecStart=/bin/echo @applied-dest-ign@ StandardOutput=tty diff --git a/fixtures/customize/dest.ign b/fixtures/customize/dest.ign index b8ece97a2..fa60b6a83 100644 --- a/fixtures/customize/dest.ign +++ b/fixtures/customize/dest.ign @@ -5,7 +5,7 @@ "systemd": { "units": [ { - "contents": "[Unit]\nDescription=Dest Ignition Applied\nBefore=multi-user.target\nConditionPathExists=/etc/NetworkManager/system-connections/installer-test.nmconnection\nConditionPathExists=/etc/NetworkManager/system-connections/nmstate-json-eth1.nmconnection\nConditionPathExists=/etc/NetworkManager/system-connections/nmstate-json-eth2.nmconnection\nConditionPathExists=/etc/NetworkManager/system-connections/nmstate-yaml-eth1.nmconnection\nConditionPathExists=/etc/NetworkManager/system-connections/nmstate-yaml-eth2.nmconnection\nConditionKernelCommandLine=install-config-karg-1\nConditionKernelCommandLine=install-config-karg-2\nConditionKernelCommandLine=dest-karg\nConditionKernelCommandLine=!ignition.platform.id=metal\nConditionKernelCommandLine=ignition.platform.id=qemu\n\n[Service]\nType=oneshot\nRemainAfterExit=true\nExecStart=/bin/echo @applied-dest-ign@\nStandardOutput=tty\n\n[Install]\nRequiredBy=multi-user.target\n", + "contents": "[Unit]\nDescription=Dest Ignition Applied\nBefore=multi-user.target\nConditionPathExists=/etc/NetworkManager/system-connections/installer-test.nmconnection\nConditionPathExists=/etc/NetworkManager/system-connections/nmstate-json-eth1.nmconnection\nConditionPathExists=/etc/NetworkManager/system-connections/nmstate-json-eth2.nmconnection\nConditionPathExists=/etc/NetworkManager/system-connections/nmstate-yaml-eth1.nmconnection\nConditionPathExists=/etc/NetworkManager/system-connections/nmstate-yaml-eth2.nmconnection\nConditionKernelCommandLine=install-config-karg-1\nConditionKernelCommandLine=install-config-karg-2\nConditionKernelCommandLine=dest-karg\nConditionKernelCommandLine=!ignition.platform.id=metal\nConditionKernelCommandLine=ignition.platform.id=qemu\nConditionKernelCommandLine=console=ttyS0,115200n8\n\n[Service]\nType=oneshot\nRemainAfterExit=true\nExecStart=/bin/grep -qz 'serial --unit=0 --speed=115200 --word=8 --parity=no.terminal_input serial.terminal_output serial.#' /boot/grub2/grub.cfg\nExecStart=/bin/echo @applied-dest-ign@\nStandardOutput=tty\n\n[Install]\nRequiredBy=multi-user.target\n", "enabled": true, "name": "dest-ignition-applied.service" } diff --git a/fixtures/iso/INDEX b/fixtures/iso/INDEX index 468ccfaac..688d2bcf8 100644 --- a/fixtures/iso/INDEX +++ b/fixtures/iso/INDEX @@ -25,3 +25,10 @@ embed-areas-2022-02.iso.xz kargs.json pointing to kargs areas and embedding defaults miniso.dat version 1 features.json: installer-config, live-initrd-network + +embed-areas-2022-09.iso.xz + kargs.json pointing to kargs areas and embedding defaults + miniso.dat version 1 + features.json: installer-config, installer-config-directives, + live-initrd-network + installer-config-directives from 0.16.0 diff --git a/fixtures/iso/embed-areas-2022-09.iso.xz b/fixtures/iso/embed-areas-2022-09.iso.xz new file mode 100644 index 000000000..c82b97583 Binary files /dev/null and b/fixtures/iso/embed-areas-2022-09.iso.xz differ diff --git a/man/coreos-installer-install.8 b/man/coreos-installer-install.8 index e90e702c1..deb390448 100644 --- a/man/coreos-installer-install.8 +++ b/man/coreos-installer-install.8 @@ -4,7 +4,7 @@ .SH NAME coreos\-installer\-install \- Install Fedora CoreOS or RHEL CoreOS .SH SYNOPSIS -\fBcoreos\-installer\-install\fR [\fB\-h\fR|\fB\-\-help\fR] [\fB\-V\fR|\fB\-\-version\fR] [\fB\-c\fR|\fB\-\-config\-file\fR] [\fB\-s\fR|\fB\-\-stream\fR] [\fB\-u\fR|\fB\-\-image\-url\fR] [\fB\-f\fR|\fB\-\-image\-file\fR] [\fB\-i\fR|\fB\-\-ignition\-file\fR] [\fB\-I\fR|\fB\-\-ignition\-url\fR] [\fB\-\-ignition\-hash\fR] [\fB\-a\fR|\fB\-\-architecture\fR] [\fB\-p\fR|\fB\-\-platform\fR] [\fB\-\-append\-karg\fR] [\fB\-\-delete\-karg\fR] [\fB\-n\fR|\fB\-\-copy\-network\fR] [\fB\-\-network\-dir\fR] [\fB\-\-save\-partlabel\fR] [\fB\-\-save\-partindex\fR] [\fB\-\-offline\fR] [\fB\-\-insecure\fR] [\fB\-\-insecure\-ignition\fR] [\fB\-\-stream\-base\-url\fR] [\fB\-\-preserve\-on\-error\fR] [\fB\-\-fetch\-retries\fR] [\fIDEST_DEVICE\fR] +\fBcoreos\-installer\-install\fR [\fB\-h\fR|\fB\-\-help\fR] [\fB\-V\fR|\fB\-\-version\fR] [\fB\-c\fR|\fB\-\-config\-file\fR] [\fB\-s\fR|\fB\-\-stream\fR] [\fB\-u\fR|\fB\-\-image\-url\fR] [\fB\-f\fR|\fB\-\-image\-file\fR] [\fB\-i\fR|\fB\-\-ignition\-file\fR] [\fB\-I\fR|\fB\-\-ignition\-url\fR] [\fB\-\-ignition\-hash\fR] [\fB\-a\fR|\fB\-\-architecture\fR] [\fB\-p\fR|\fB\-\-platform\fR] [\fB\-\-console\fR] [\fB\-\-append\-karg\fR] [\fB\-\-delete\-karg\fR] [\fB\-n\fR|\fB\-\-copy\-network\fR] [\fB\-\-network\-dir\fR] [\fB\-\-save\-partlabel\fR] [\fB\-\-save\-partindex\fR] [\fB\-\-offline\fR] [\fB\-\-insecure\fR] [\fB\-\-insecure\-ignition\fR] [\fB\-\-stream\-base\-url\fR] [\fB\-\-preserve\-on\-error\fR] [\fB\-\-fetch\-retries\fR] [\fIDEST_DEVICE\fR] .SH DESCRIPTION Install Fedora CoreOS or RHEL CoreOS .SH OPTIONS @@ -56,6 +56,11 @@ Override the Ignition platform ID Install a system that will run on the specified cloud or virtualization platform, such as "vmware". .TP +\fB\-\-console\fR=\fIspec\fR +Kernel and bootloader console + +Set the kernel and bootloader console, using the same syntax as the parameter to the "console=" kernel argument. +.TP \fB\-\-append\-karg\fR=\fIarg\fR Append default kernel arg diff --git a/man/coreos-installer-iso-customize.8 b/man/coreos-installer-iso-customize.8 index e11bc8d7b..2a112118d 100644 --- a/man/coreos-installer-iso-customize.8 +++ b/man/coreos-installer-iso-customize.8 @@ -4,7 +4,7 @@ .SH NAME coreos\-installer\-iso\-customize \- Customize a CoreOS live ISO image .SH SYNOPSIS -\fBcoreos\-installer\-iso\-customize\fR [\fB\-h\fR|\fB\-\-help\fR] [\fB\-V\fR|\fB\-\-version\fR] [\fB\-\-dest\-ignition\fR] [\fB\-\-dest\-device\fR] [\fB\-\-dest\-karg\-append\fR] [\fB\-\-dest\-karg\-delete\fR] [\fB\-\-network\-keyfile\fR] [\fB\-\-network\-nmstate\fR] [\fB\-\-ignition\-ca\fR] [\fB\-\-pre\-install\fR] [\fB\-\-post\-install\fR] [\fB\-\-installer\-config\fR] [\fB\-\-live\-ignition\fR] [\fB\-\-live\-karg\-append\fR] [\fB\-\-live\-karg\-delete\fR] [\fB\-\-live\-karg\-replace\fR] [\fB\-f\fR|\fB\-\-force\fR] [\fB\-o\fR|\fB\-\-output\fR] <\fIISO\fR> +\fBcoreos\-installer\-iso\-customize\fR [\fB\-h\fR|\fB\-\-help\fR] [\fB\-V\fR|\fB\-\-version\fR] [\fB\-\-dest\-ignition\fR] [\fB\-\-dest\-device\fR] [\fB\-\-dest\-console\fR] [\fB\-\-dest\-karg\-append\fR] [\fB\-\-dest\-karg\-delete\fR] [\fB\-\-network\-keyfile\fR] [\fB\-\-network\-nmstate\fR] [\fB\-\-ignition\-ca\fR] [\fB\-\-pre\-install\fR] [\fB\-\-post\-install\fR] [\fB\-\-installer\-config\fR] [\fB\-\-live\-ignition\fR] [\fB\-\-live\-karg\-append\fR] [\fB\-\-live\-karg\-delete\fR] [\fB\-\-live\-karg\-replace\fR] [\fB\-f\fR|\fB\-\-force\fR] [\fB\-o\fR|\fB\-\-output\fR] <\fIISO\fR> .SH DESCRIPTION Customize a CoreOS live ISO image .SH OPTIONS @@ -25,6 +25,11 @@ Install destination device Automatically run installer, installing to the specified destination device. The resulting boot media will overwrite the destination device without confirmation. .TP +\fB\-\-dest\-console\fR=\fIspec\fR +Kernel and bootloader console for dest + +Automatically run installer, configuring the specified kernel and bootloader console for the destination system. The argument uses the same syntax as the parameter to the "console=" kernel argument. +.TP \fB\-\-dest\-karg\-append\fR=\fIarg\fR Destination kernel argument to append diff --git a/man/coreos-installer-pxe-customize.8 b/man/coreos-installer-pxe-customize.8 index b08c26528..d24a85614 100644 --- a/man/coreos-installer-pxe-customize.8 +++ b/man/coreos-installer-pxe-customize.8 @@ -4,7 +4,7 @@ .SH NAME coreos\-installer\-pxe\-customize \- Create a custom live PXE boot config .SH SYNOPSIS -\fBcoreos\-installer\-pxe\-customize\fR [\fB\-h\fR|\fB\-\-help\fR] [\fB\-V\fR|\fB\-\-version\fR] [\fB\-\-dest\-ignition\fR] [\fB\-\-dest\-device\fR] [\fB\-\-dest\-karg\-append\fR] [\fB\-\-dest\-karg\-delete\fR] [\fB\-\-network\-keyfile\fR] [\fB\-\-network\-nmstate\fR] [\fB\-\-ignition\-ca\fR] [\fB\-\-pre\-install\fR] [\fB\-\-post\-install\fR] [\fB\-\-installer\-config\fR] [\fB\-\-live\-ignition\fR] <\fB\-o\fR|\fB\-\-output\fR> <\fIpath\fR> +\fBcoreos\-installer\-pxe\-customize\fR [\fB\-h\fR|\fB\-\-help\fR] [\fB\-V\fR|\fB\-\-version\fR] [\fB\-\-dest\-ignition\fR] [\fB\-\-dest\-device\fR] [\fB\-\-dest\-console\fR] [\fB\-\-dest\-karg\-append\fR] [\fB\-\-dest\-karg\-delete\fR] [\fB\-\-network\-keyfile\fR] [\fB\-\-network\-nmstate\fR] [\fB\-\-ignition\-ca\fR] [\fB\-\-pre\-install\fR] [\fB\-\-post\-install\fR] [\fB\-\-installer\-config\fR] [\fB\-\-live\-ignition\fR] <\fB\-o\fR|\fB\-\-output\fR> <\fIpath\fR> .SH DESCRIPTION Create a custom live PXE boot config .SH OPTIONS @@ -25,6 +25,11 @@ Install destination device Automatically run installer, installing to the specified destination device. The resulting boot media will overwrite the destination device without confirmation. .TP +\fB\-\-dest\-console\fR=\fIspec\fR +Kernel and bootloader console for dest + +Automatically run installer, configuring the specified kernel and bootloader console for the destination system. The argument uses the same syntax as the parameter to the "console=" kernel argument. +.TP \fB\-\-dest\-karg\-append\fR=\fIarg\fR Destination kernel argument to append diff --git a/src/cmdline/console.rs b/src/cmdline/console.rs new file mode 100644 index 000000000..4f542d620 --- /dev/null +++ b/src/cmdline/console.rs @@ -0,0 +1,347 @@ +// Copyright 2022 Red Hat, 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. + +//! Helper types for console argument. + +use anyhow::{bail, Context, Error, Result}; +use lazy_static::lazy_static; +use regex::Regex; +use serde_with::{DeserializeFromStr, SerializeDisplay}; +use std::fmt; +use std::str::FromStr; + +const KARG_PREFIX: &str = "console="; + +#[derive(Clone, Debug, DeserializeFromStr, SerializeDisplay, PartialEq, Eq)] +pub enum Console { + Graphical(GraphicalConsole), + Serial(SerialConsole), +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct GraphicalConsole { + device: String, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SerialConsole { + prefix: String, + port: u8, + speed: u32, + data_bits: u8, + parity: Parity, + // Linux console doesn't support stop bits + // GRUB doesn't support RTS/CTS flow control +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +enum Parity { + None, + Odd, + Even, +} + +impl Parity { + fn for_grub(&self) -> &'static str { + match self { + Self::None => "no", + Self::Odd => "odd", + Self::Even => "even", + } + } + + fn for_karg(&self) -> &'static str { + match self { + Self::None => "n", + Self::Odd => "o", + Self::Even => "e", + } + } +} + +impl Console { + pub fn grub_terminal(&self) -> &'static str { + match self { + Self::Graphical(_) => "console", + Self::Serial(_) => "serial", + } + } + + pub fn grub_command(&self) -> Option { + match self { + Self::Graphical(_) => None, + Self::Serial(c) => Some(format!( + "serial --unit={} --speed={} --word={} --parity={}", + c.port, + c.speed, + c.data_bits, + c.parity.for_grub() + )), + } + } + + pub fn karg(&self) -> String { + format!("{KARG_PREFIX}{}", self) + } + + /// Write a warning message to stdout if kargs contains "console=" + /// arguments we can parse and no "console=" arguments we can't. The + /// warning suggests that users use console_option instead of + /// karg_option to specify the desired console. + pub fn maybe_warn_on_kargs(kargs: &[String], karg_option: &str, console_option: &str) { + use textwrap::{fill, Options, WordSplitter}; + if let Some(args) = Self::maybe_console_args_from_kargs(kargs) { + // automatically wrap the message, but use Unicode non-breaking + // spaces to avoid wrapping in the middle of the argument + // strings, and then replace the non-breaking spaces afterward + const NBSP: &str = "\u{a0}"; + let msg = format!( + "Note: consider using \"{}\" instead of \"{}\" to configure both kernel and bootloader consoles.", + args.iter() + .map(|a| format!("{console_option}{NBSP}{a}")) + .collect::>() + .join(NBSP), + args.iter() + .map(|a| format!("{karg_option}{NBSP}console={a}")) + .collect::>() + .join(NBSP), + ); + let wrapped = fill( + &msg, + Options::new(80) + .break_words(false) + .word_splitter(WordSplitter::NoHyphenation), + ) + .replace(NBSP, " "); + eprintln!("\n{}\n", wrapped); + } + } + + /// If kargs contains at least one console argument and all console + /// arguments are parseable as consoles, return a vector of verbatim + /// (unparsed) console arguments with the console= prefixes removed. + fn maybe_console_args_from_kargs(kargs: &[String]) -> Option> { + let (parseable, unparseable): (Vec<&str>, Vec<&str>) = kargs + .iter() + .filter(|a| a.starts_with(KARG_PREFIX)) + .map(|a| &a[KARG_PREFIX.len()..]) + .partition(|a| Console::from_str(a).is_ok()); + if !parseable.is_empty() && unparseable.is_empty() { + Some(parseable) + } else { + None + } + } +} + +impl FromStr for Console { + type Err = Error; + + fn from_str(s: &str) -> Result { + // help the user with possible misunderstandings + for prefix in [KARG_PREFIX, "/dev/"] { + if s.starts_with(prefix) { + bail!(r#"spec should not start with "{prefix}""#); + } + } + + // first, parse serial console parameters + lazy_static! { + static ref SERIAL_REGEX: Regex = Regex::new("^(?PttyS|ttyAMA)(?P[0-9]+)(?:,(?P[0-9]+)(?:(?Pn|o|e)(?P[5-8])?)?)?$").expect("compiling console regex"); + } + if let Some(c) = SERIAL_REGEX.captures(s) { + return Ok(Console::Serial(SerialConsole { + prefix: c + .name("prefix") + .expect("prefix is mandatory") + .as_str() + .to_string(), + port: c + .name("port") + .expect("port is mandatory") + .as_str() + .parse() + .context("couldn't parse port")?, + speed: c + .name("speed") + .map(|v| v.as_str().parse().context("couldn't parse speed")) + .unwrap_or(Ok(9600))?, + data_bits: c + .name("data_bits") + .map(|v| v.as_str().parse().expect("unexpected data bits")) + .unwrap_or(8), + parity: match c.name("parity").map(|v| v.as_str()) { + // default + None => Parity::None, + Some("n") => Parity::None, + Some("e") => Parity::Even, + Some("o") => Parity::Odd, + _ => unreachable!(), + }, + })); + } + + // then try hardcoded strings for graphical consoles + match s { + "tty0" | "hvc0" | "ttysclp0" => Ok(Console::Graphical(GraphicalConsole { + device: s.to_string(), + })), + _ => bail!("invalid or unsupported console argument"), + } + } +} + +impl fmt::Display for Console { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Graphical(c) => write!(f, "{}", c.device), + Self::Serial(c) => write!( + f, + "{}{},{}{}{}", + c.prefix, + c.port, + c.speed, + c.parity.for_karg(), + c.data_bits + ), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn valid_console_args() { + let cases = vec![ + ("tty0", "console=tty0", "console", None), + ("hvc0", "console=hvc0", "console", None), + ("ttysclp0", "console=ttysclp0", "console", None), + ( + "ttyS1", + "console=ttyS1,9600n8", + "serial", + Some("serial --unit=1 --speed=9600 --word=8 --parity=no"), + ), + ( + "ttyAMA1", + "console=ttyAMA1,9600n8", + "serial", + Some("serial --unit=1 --speed=9600 --word=8 --parity=no"), + ), + ( + "ttyS1,1234567e5", + "console=ttyS1,1234567e5", + "serial", + Some("serial --unit=1 --speed=1234567 --word=5 --parity=even"), + ), + ( + "ttyS2,5o", + "console=ttyS2,5o8", + "serial", + Some("serial --unit=2 --speed=5 --word=8 --parity=odd"), + ), + ( + "ttyS3,17", + "console=ttyS3,17n8", + "serial", + Some("serial --unit=3 --speed=17 --word=8 --parity=no"), + ), + ]; + for (input, karg, grub_terminal, grub_command) in cases { + let console = Console::from_str(input).unwrap(); + assert_eq!( + console.grub_terminal(), + grub_terminal, + "GRUB terminal for {}", + input + ); + assert_eq!( + console.grub_command().as_deref(), + grub_command, + "GRUB command for {}", + input + ); + assert_eq!(console.karg(), karg, "karg for {}", input); + } + } + + #[test] + fn invalid_console_args() { + let cases = vec![ + "foo", + "/dev/tty0", + "/dev/ttyS0", + "console=tty0", + "console=ttyS0", + "ztty0", + "zttyS0", + "tty0z", + "ttyS0z", + "tty1", + "hvc1", + "ttysclp1", + "ttyS0,", + "ttyS0,z", + "ttyS0,115200p8", + "ttyS0,115200n4", + "ttyS0,115200n8r", + "ttyB0", + "ttyS9999999999999999999", + "ttyS0,999999999999999999999", + ]; + for input in cases { + Console::from_str(input).unwrap_err(); + } + } + + #[test] + fn maybe_console_args_from_kargs() { + assert_eq!( + Console::maybe_console_args_from_kargs(&[ + "foo".into(), + "console=ttyS0".into(), + "bar".into() + ]), + Some(vec!["ttyS0"]) + ); + assert_eq!( + Console::maybe_console_args_from_kargs(&[ + "foo".into(), + "console=ttyS0".into(), + "console=tty0".into(), + "console=tty0".into(), + "console=ttyAMA1,115200n8".into(), + "bar".into() + ]), + Some(vec!["ttyS0", "tty0", "tty0", "ttyAMA1,115200n8"]) + ); + assert_eq!( + Console::maybe_console_args_from_kargs(&[ + "foo".into(), + "console=ttyS0".into(), + "console=ttyS1z".into(), + "console=tty0".into(), + "bar".into() + ]), + None + ); + assert_eq!( + Console::maybe_console_args_from_kargs(&["foo".into(), "bar".into()]), + None + ); + assert_eq!(Console::maybe_console_args_from_kargs(&[]), None); + } +} diff --git a/src/cmdline/install.rs b/src/cmdline/install.rs index b49e95f95..3da8abeaa 100644 --- a/src/cmdline/install.rs +++ b/src/cmdline/install.rs @@ -25,6 +25,7 @@ use std::fs::OpenOptions; use crate::io::IgnitionHash; +use super::console::Console; use super::serializer; use super::types::*; use super::Cmd; @@ -123,6 +124,13 @@ pub struct InstallConfig { /// virtualization platform, such as "vmware". #[clap(short, long, value_name = "name")] pub platform: Option, + /// Kernel and bootloader console + /// + /// Set the kernel and bootloader console, using the same syntax as the + /// parameter to the "console=" kernel argument. + #[serde(skip_serializing_if = "is_default")] + #[clap(long, value_name = "spec")] + pub console: Vec, /// 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 @@ -305,6 +313,10 @@ mod test { ), architecture: DefaultedString::::from_str("h").unwrap(), platform: Some("i".into()), + console: vec![ + Console::from_str("ttyS0").unwrap(), + Console::from_str("ttyS1,115200n8").unwrap(), + ], // skipped firstboot_args: Some("j".into()), append_karg: vec!["k".into(), "l".into()], @@ -338,6 +350,11 @@ mod test { "h", "--platform", "i", + "--console", + // we round-trip to an equivalent but not identical value + "ttyS0,9600n8", + "--console", + "ttyS1,115200n8", "--append-karg", "k", "--append-karg", @@ -382,6 +399,7 @@ ignition-url: http://example.com/g ignition-hash: sha256-e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 architecture: h platform: i +console: [ttyS0, "ttyS1,115200n8"] append-karg: [k, l] delete-karg: [m, n] copy-network: true @@ -418,6 +436,10 @@ dest-device: u ), architecture: DefaultedString::::from_str("h").unwrap(), platform: Some("i".into()), + console: vec![ + Console::from_str("ttyS0").unwrap(), + Console::from_str("ttyS1,115200n8").unwrap(), + ], // skipped firstboot_args: None, append_karg: vec!["k".into(), "l".into()], diff --git a/src/cmdline/mod.rs b/src/cmdline/mod.rs index d8c7bc2dc..5cb9c0327 100644 --- a/src/cmdline/mod.rs +++ b/src/cmdline/mod.rs @@ -18,12 +18,14 @@ use clap::{AppSettings, Parser}; use reqwest::Url; +mod console; #[cfg(feature = "docgen")] mod doc; mod install; mod serializer; mod types; +pub use self::console::*; #[cfg(feature = "docgen")] pub use self::doc::*; pub use self::install::InstallConfig; @@ -263,6 +265,13 @@ pub struct CommonCustomizeConfig { /// device without confirmation. #[clap(long, value_name = "path")] pub dest_device: Option, + /// Kernel and bootloader console for dest + /// + /// Automatically run installer, configuring the specified kernel and + /// bootloader console for the destination system. The argument uses + /// the same syntax as the parameter to the "console=" kernel argument. + #[clap(long, value_name = "spec")] + pub dest_console: Vec, /// Destination kernel argument to append /// /// Automatically run installer, adding the specified kernel argument diff --git a/src/install.rs b/src/install.rs index 2ee95631c..cd7abc904 100644 --- a/src/install.rs +++ b/src/install.rs @@ -394,6 +394,7 @@ fn write_disk( || !config.append_karg.is_empty() || !config.delete_karg.is_empty() || config.platform.is_some() + || !config.console.is_empty() || network_config.is_some() || cfg!(target_arch = "s390x") { @@ -405,6 +406,14 @@ fn write_disk( if let Some(platform) = config.platform.as_ref() { write_platform(mount.mountpoint(), platform).context("writing platform ID")?; } + if config.platform.is_some() || !config.console.is_empty() { + write_console( + mount.mountpoint(), + config.platform.as_deref(), + &config.console, + ) + .context("configuring console")?; + } if let Some(firstboot_args) = config.firstboot_args.as_ref() { write_firstboot_kargs(mount.mountpoint(), firstboot_args) .context("writing firstboot kargs")?; @@ -412,6 +421,7 @@ fn write_disk( if !config.append_karg.is_empty() || !config.delete_karg.is_empty() { eprintln!("Modifying kernel arguments"); + Console::maybe_warn_on_kargs(&config.append_karg, "--append-karg", "--console"); visit_bls_entry_options(mount.mountpoint(), |orig_options: &str| { KargsEditor::new() .append(config.append_karg.as_slice()) @@ -532,81 +542,95 @@ struct PlatformSpec { kernel_arguments: Vec, } -/// Override the platform ID. Add any kernel arguments and grub.cfg -/// directives specified for this platform in platforms.json. +/// Override the platform ID. fn write_platform(mountpoint: &Path, platform: &str) -> Result<()> { // early return if setting the platform to the default value, since // otherwise we'll think we failed to set it if platform == "metal" { return Ok(()); } + eprintln!("Setting platform to {}", platform); - // read platforms table - let (spec, metal_spec) = match fs::read_to_string(mountpoint.join("coreos/platforms.json")) { - Ok(json) => { - let mut table: HashMap = - serde_json::from_str(&json).context("parsing platform table")?; - // no spec for this platform, or for metal? - ( - table.remove(platform).unwrap_or_default(), - table.remove("metal").unwrap_or_default(), - ) + // We assume that we will only install from metal images and that the + // bootloader configs will always set ignition.platform.id. + visit_bls_entry_options(mountpoint, |orig_options: &str| { + let new_options = KargsEditor::new() + .replace(&[format!("ignition.platform.id=metal={}", platform)]) + .apply_to(orig_options) + .context("setting platform ID argument")?; + if orig_options == new_options { + bail!("couldn't locate platform ID"); } + Ok(Some(new_options)) + })?; + Ok(()) +} + +/// Configure console kernel arguments and GRUB commands. +fn write_console(mountpoint: &Path, platform: Option<&str>, consoles: &[Console]) -> Result<()> { + // read platforms table + let platforms = match fs::read_to_string(mountpoint.join("coreos/platforms.json")) { + Ok(json) => serde_json::from_str::>(&json) + .context("parsing platform table")?, // no table for this image? Err(e) if e.kind() == std::io::ErrorKind::NotFound => Default::default(), Err(e) => return Err(e).context("reading platform table"), }; + let mut kargs = Vec::new(); + let mut grub_commands = Vec::new(); + if !consoles.is_empty() { + // custom console settings completely override platform-specific + // defaults + let mut grub_terminals = Vec::new(); + for console in consoles { + kargs.push(console.karg()); + if let Some(cmd) = console.grub_command() { + grub_commands.push(cmd); + } + grub_terminals.push(console.grub_terminal()); + } + grub_terminals.sort_unstable(); + grub_terminals.dedup(); + for direction in ["input", "output"] { + grub_commands.push(format!("terminal_{direction} {}", grub_terminals.join(" "))); + } + } else if let Some(platform) = platform { + // platform-specific defaults + if platform == "metal" { + // we're just being asked to apply the defaults which are already + // applied + return Ok(()); + } + let spec = platforms.get(platform).cloned().unwrap_or_default(); + kargs.extend(spec.kernel_arguments); + grub_commands.extend(spec.grub_commands); + } else { + // nothing to do and the caller shouldn't have called us + unreachable!(); + } + // set kargs, removing any metal-specific ones - eprintln!("Setting platform to {}", platform); + let metal_spec = platforms.get("metal").cloned().unwrap_or_default(); visit_bls_entry_options(mountpoint, |orig_options: &str| { - bls_entry_options_write_platform( - orig_options, - platform, - &spec.kernel_arguments, - &metal_spec.kernel_arguments, - ) + KargsEditor::new() + .append(&kargs) + .delete(&metal_spec.kernel_arguments) + .maybe_apply_to(orig_options) + .context("setting platform kernel arguments") })?; // set grub commands - if spec.grub_commands != metal_spec.grub_commands { + if grub_commands != metal_spec.grub_commands { let path = mountpoint.join("grub2/grub.cfg"); let grub_cfg = fs::read_to_string(&path).context("reading grub.cfg")?; - let new_grub_cfg = update_grub_cfg_console_settings(&grub_cfg, &spec.grub_commands) + let new_grub_cfg = update_grub_cfg_console_settings(&grub_cfg, &grub_commands) .context("updating grub.cfg")?; fs::write(&path, new_grub_cfg).context("writing grub.cfg")?; } Ok(()) } -/// To be used with `visit_bls_entry_options()`. Modifies the BLS config, -/// changing the `ignition.platform.id` and then appending/deleting any -/// specified kargs. This assumes that we will only install from metal -/// images and that the bootloader configs will always set -/// ignition.platform.id. Fail if those assumptions change. This is -/// deliberately simplistic. -fn bls_entry_options_write_platform( - orig_options: &str, - platform: &str, - append_kargs: &[String], - delete_kargs: &[String], -) -> Result> { - let new_options = KargsEditor::new() - .replace(&[format!("ignition.platform.id=metal={}", platform)]) - .apply_to(orig_options) - .context("updating platform ID")?; - if orig_options == new_options { - bail!("Couldn't locate platform ID"); - } - Ok(Some( - KargsEditor::new() - .append(append_kargs) - .delete(delete_kargs) - .apply_to(&new_options) - .context("adding platform-specific kernel arguments")?, - )) -} - /// Rewrite the grub.cfg CONSOLE-SETTINGS block to use the specified GRUB /// commands, and return the result. fn update_grub_cfg_console_settings(grub_cfg: &str, commands: &[String]) -> Result { @@ -778,56 +802,6 @@ mod tests { } } - #[test] - fn test_platform_id() { - let orig_content = "ignition.platform.id=metal foo bar"; - let new_content = bls_entry_options_write_platform( - orig_content, - "openstack", - &vec!["baz".to_string(), "blah".to_string()], - &[], - ) - .unwrap(); - assert_eq!( - new_content.unwrap(), - "ignition.platform.id=openstack foo bar baz blah" - ); - - let orig_content = "foo ignition.platform.id=metal bar"; - let new_content = bls_entry_options_write_platform( - orig_content, - "openstack", - &vec!["baz".to_string(), "blah".to_string()], - &vec!["foo".to_string()], - ) - .unwrap(); - assert_eq!( - new_content.unwrap(), - "ignition.platform.id=openstack bar baz blah" - ); - - let orig_content = "foo bar ignition.platform.id=metal"; - let new_content = bls_entry_options_write_platform( - orig_content, - "openstack", - &vec!["baz".to_string(), "blah".to_string()], - &[], - ) - .unwrap(); - assert_eq!( - new_content.unwrap(), - "foo bar ignition.platform.id=openstack baz blah" - ); - - let orig_content = "foo bar ignition.platform.id=metal"; - let new_content = - bls_entry_options_write_platform(orig_content, "openstack", &[], &[]).unwrap(); - assert_eq!( - new_content.unwrap(), - "foo bar ignition.platform.id=openstack" - ); - } - #[test] fn test_update_grub_cfg() { let base_cfgs = vec![ diff --git a/src/live/customize.rs b/src/live/customize.rs index c7cb063a6..416792e42 100644 --- a/src/live/customize.rs +++ b/src/live/customize.rs @@ -36,14 +36,22 @@ const COREOS_ISO_FEATURES_PATH: &str = "COREOS/FEATURES.JSO"; /// and /coreos/features.json in the live ISO. Written by /// cosa buildextend-live. #[derive(Default, Deserialize)] -#[serde(rename_all = "kebab-case")] +#[serde(default, rename_all = "kebab-case")] pub(super) struct OsFeatures { /// Installer reads config files from /etc/coreos/installer.d pub installer_config: bool, + /// Directives supported in installer config files + pub installer_config_directives: InstallerDirectives, /// Live initrd reads NM keyfiles from /etc/coreos-firstboot-network pub live_initrd_network: bool, } +#[derive(Default, Deserialize)] +#[serde(default, rename_all = "kebab-case")] +pub(super) struct InstallerDirectives { + pub console: bool, +} + impl OsFeatures { pub fn for_iso(iso: &mut IsoFs) -> Result { match iso.get_path(COREOS_ISO_FEATURES_PATH) { @@ -97,6 +105,14 @@ impl LiveInitrd { if let Some(path) = &common.dest_device { conf.dest_device(path)?; } + for arg in &common.dest_console { + conf.dest_console(arg)?; + } + Console::maybe_warn_on_kargs( + &common.dest_karg_append, + "--dest-karg-append", + "--dest-console", + ); for arg in &common.dest_karg_append { conf.dest_karg_append(arg); } @@ -146,6 +162,17 @@ impl LiveInitrd { Ok(()) } + pub fn dest_console(&mut self, console: &Console) -> Result<()> { + if !self.features.installer_config_directives.console { + bail!("This OS image does not support customizing the destination console."); + } + self.installer + .get_or_insert_with(Default::default) + .console + .push(console.clone()); + Ok(()) + } + pub fn dest_karg_append(&mut self, arg: &str) { self.installer .get_or_insert_with(Default::default) diff --git a/tests/images.sh b/tests/images.sh index f70bbfa27..1974ff529 100755 --- a/tests/images.sh +++ b/tests/images.sh @@ -3,7 +3,16 @@ set -euo pipefail export PATH="$(realpath $(dirname $0)/../target/${PROFILE:-debug}):$PATH" -fixtures="$(realpath $(dirname $0)/../fixtures)" +fixturesdir="$(realpath $(dirname $0)/../fixtures)" + +fixtures=( + embed-areas-2020-09.iso.xz + embed-areas-2021-01.iso.xz + embed-areas-2021-09.iso.xz + embed-areas-2021-12.iso.xz + embed-areas-2022-02.iso.xz + embed-areas-2022-09.iso.xz +) msg() { cat < ipxe <