From f78f03a1f5d7001200397fdc8512525985cb2a14 Mon Sep 17 00:00:00 2001 From: Luca BRUNO Date: Mon, 20 Jan 2020 12:21:17 +0000 Subject: [PATCH] providers: setup network kernel arguments in initrd This adds initial/experimental support for providing supplemental network kernel arguments in the initrd. It is meant to support environment where custom network kargs may be provided via a back-channel. Such arguments are only retrieved and applied on first boot, and their format is defined by dracut [0]. The feature is currently reachable as a dedicated subcommand, but this does not contain any platform-specific implementation. [0] https://mirrors.edge.kernel.org/pub/linux/utils/boot/dracut/dracut.html#_network --- .../afterburn-network-kargs.service | 18 ++ dracut/30afterburn/module-setup.sh | 7 +- src/cli/exp.rs | 77 +++++++ src/cli/mod.rs | 198 +++++++++++++----- src/cli/multi.rs | 16 +- src/initrd/mod.rs | 33 +++ src/main.rs | 1 + src/providers/mod.rs | 13 +- 8 files changed, 284 insertions(+), 79 deletions(-) create mode 100644 dracut/30afterburn/afterburn-network-kargs.service create mode 100644 src/cli/exp.rs create mode 100644 src/initrd/mod.rs diff --git a/dracut/30afterburn/afterburn-network-kargs.service b/dracut/30afterburn/afterburn-network-kargs.service new file mode 100644 index 00000000..111407a9 --- /dev/null +++ b/dracut/30afterburn/afterburn-network-kargs.service @@ -0,0 +1,18 @@ +[Unit] +Description=Afterburn Initrd Setup Network Kernel Arguments + +# This service may produce additional kargs fragments, +# which are then consumed by dracut-cmdline(8). +DefaultDependencies=no +Before=dracut-cmdline.service +Before=ignition-fetch.service +After=systemd-journald.socket + +OnFailure=emergency.target +OnFailureJobMode=isolate + +[Service] +Environment=AFTERBURN_OPT_DEFAULT_VALUE=--default-value=ip=dhcp,dhcp6 +Environment=AFTERBURN_OPT_PROVIDER=--cmdline +ExecStart=/usr/bin/afterburn exp rd-network-kargs ${AFTERBURN_OPT_PROVIDER} ${AFTERBURN_OPT_DEFAULT_VALUE} +Type=oneshot diff --git a/dracut/30afterburn/module-setup.sh b/dracut/30afterburn/module-setup.sh index 0875c719..17c58806 100755 --- a/dracut/30afterburn/module-setup.sh +++ b/dracut/30afterburn/module-setup.sh @@ -16,9 +16,12 @@ install() { inst_simple "$moddir/afterburn-hostname.service" \ "$systemdutildir/system/afterburn-hostname.service" - # We want the afterburn-hostname to be firstboot only, so Ignition-provided - # hostname changes do not get overwritten on subsequent boots + inst_simple "$moddir/afterburn-network-kargs.service" \ + "$systemdutildir/system/afterburn-network-kargs.service" + # These services are only run once on first-boot, so they piggyback + # on Ignition completion target. mkdir -p "$initdir/$systemdsystemunitdir/ignition-complete.target.requires" ln -s "../afterburn-hostname.service" "$initdir/$systemdsystemunitdir/ignition-complete.target.requires/afterburn-hostname.service" + ln -s "../afterburn-network-kargs.service" "$initdir/$systemdsystemunitdir/ignition-complete.target.requires/afterburn-network-kargs.service" } diff --git a/src/cli/exp.rs b/src/cli/exp.rs new file mode 100644 index 00000000..9febad7d --- /dev/null +++ b/src/cli/exp.rs @@ -0,0 +1,77 @@ +//! `exp` CLI sub-command. + +use crate::errors::*; +use crate::{initrd, util}; +use clap::ArgMatches; +use error_chain::bail; + +/// Experimental subcommands. +#[derive(Debug)] +pub enum CliExp { + RdNetworkKargs(CliRdNetworkKargs), +} + +impl CliExp { + /// Parse sub-command into configuration. + pub(crate) fn parse(app_matches: &ArgMatches) -> Result { + if app_matches.subcommand_name().is_none() { + bail!("missing subcommand for 'exp'"); + } + + let cfg = match app_matches.subcommand() { + ("rd-network-kargs", Some(matches)) => CliRdNetworkKargs::parse(matches)?, + (x, _) => unreachable!("unrecognized subcommand for 'exp': '{}'", x), + }; + + Ok(super::CliConfig::Exp(cfg)) + } + + // Run sub-command. + pub(crate) fn run(&self) -> Result<()> { + match self { + CliExp::RdNetworkKargs(cmd) => cmd.run()?, + }; + Ok(()) + } +} + +/// Sub-command for network kernel arguments. +#[derive(Debug)] +pub struct CliRdNetworkKargs { + platform: String, + default_kargs: String, +} + +impl CliRdNetworkKargs { + /// Hard-coded fallback for default network arguments. + const DEFAULT_NETWORK_KARGS: &'static str = "ip=dhcp,dhcp6"; + + /// Parse sub-command into configuration. + pub(crate) fn parse(matches: &ArgMatches) -> Result { + let platform = super::parse_provider(matches)?; + let default_kargs = matches + .value_of("default-value") + .unwrap_or_else(|| Self::DEFAULT_NETWORK_KARGS) + .to_string(); + + let cfg = Self { + platform, + default_kargs, + }; + Ok(CliExp::RdNetworkKargs(cfg)) + } + + /// Run the sub-command. + pub(crate) fn run(&self) -> Result<()> { + if util::has_network_kargs(super::CMDLINE_PATH)? { + slog_scope::warn!("kernel cmdline already specifies network arguments, skipping"); + return Ok(()); + }; + + let provider_kargs = initrd::fetch_network_kargs(&self.platform)?; + let kargs = provider_kargs + .as_ref() + .unwrap_or_else(|| &self.default_kargs); + initrd::write_network_kargs(kargs) + } +} diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 0d303247..3d90c354 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -2,8 +2,10 @@ use crate::errors::*; use clap::{crate_version, App, Arg, ArgMatches, SubCommand}; +use error_chain::bail; use slog_scope::trace; +mod exp; mod multi; /// Path to kernel command-line (requires procfs mount). @@ -13,6 +15,7 @@ const CMDLINE_PATH: &str = "/proc/cmdline"; #[derive(Debug)] pub(crate) enum CliConfig { Multi(multi::CliMulti), + Exp(exp::CliExp), } impl CliConfig { @@ -20,6 +23,7 @@ impl CliConfig { pub fn parse_subcommands(app_matches: ArgMatches) -> Result { let cfg = match app_matches.subcommand() { ("multi", Some(matches)) => multi::CliMulti::parse(matches)?, + ("exp", Some(matches)) => exp::CliExp::parse(matches)?, (x, _) => unreachable!("unrecognized subcommand '{}'", x), }; @@ -30,6 +34,7 @@ impl CliConfig { pub fn run(self) -> Result<()> { match self { CliConfig::Multi(cmd) => cmd.run(), + CliConfig::Exp(cmd) => cmd.run(), } } } @@ -44,62 +49,103 @@ pub(crate) fn parse_args(argv: impl IntoIterator) -> Result Result { + let provider = match (matches.value_of("provider"), matches.is_present("cmdline")) { + (Some(provider), false) => String::from(provider), + (None, true) => crate::util::get_platform(CMDLINE_PATH)?, + (None, false) => bail!("must set either --provider or --cmdline"), + (Some(_), true) => bail!("cannot process both --provider and --cmdline"), + }; + + Ok(provider) +} + /// CLI setup, covering all sub-commands and arguments. fn cli_setup<'a, 'b>() -> App<'a, 'b> { // NOTE(lucab): due to legacy translation there can't be global arguments // here, i.e. a sub-command is always expected first. - App::new("Afterburn").version(crate_version!()).subcommand( - SubCommand::with_name("multi") - .about("Perform multiple tasks in a single call") - .arg( - Arg::with_name("legacy-cli") - .long("legacy-cli") - .help("Whether this command was translated from legacy CLI args") - .hidden(true), - ) - .arg( - Arg::with_name("provider") - .long("provider") - .help("The name of the cloud provider") - .global(true) - .takes_value(true), - ) - .arg( - Arg::with_name("cmdline") - .long("cmdline") - .global(true) - .help("Read the cloud provider from the kernel cmdline"), - ) - .arg( - Arg::with_name("attributes") - .long("attributes") - .help("The file into which the metadata attributes are written") - .takes_value(true), - ) - .arg( - Arg::with_name("check-in") - .long("check-in") - .help("Check-in this instance boot with the cloud provider"), - ) - .arg( - Arg::with_name("hostname") - .long("hostname") - .help("The file into which the hostname should be written") - .takes_value(true), - ) - .arg( - Arg::with_name("network-units") - .long("network-units") - .help("The directory into which network units are written") - .takes_value(true), - ) - .arg( - Arg::with_name("ssh-keys") - .long("ssh-keys") - .help("Update SSH keys for the given user") - .takes_value(true), - ), - ) + App::new("Afterburn") + .version(crate_version!()) + .subcommand( + SubCommand::with_name("multi") + .about("Perform multiple tasks in a single call") + .arg( + Arg::with_name("legacy-cli") + .long("legacy-cli") + .help("Whether this command was translated from legacy CLI args") + .hidden(true), + ) + .arg( + Arg::with_name("provider") + .long("provider") + .help("The name of the cloud provider") + .global(true) + .takes_value(true), + ) + .arg( + Arg::with_name("cmdline") + .long("cmdline") + .global(true) + .help("Read the cloud provider from the kernel cmdline"), + ) + .arg( + Arg::with_name("attributes") + .long("attributes") + .help("The file into which the metadata attributes are written") + .takes_value(true), + ) + .arg( + Arg::with_name("check-in") + .long("check-in") + .help("Check-in this instance boot with the cloud provider"), + ) + .arg( + Arg::with_name("hostname") + .long("hostname") + .help("The file into which the hostname should be written") + .takes_value(true), + ) + .arg( + Arg::with_name("network-units") + .long("network-units") + .help("The directory into which network units are written") + .takes_value(true), + ) + .arg( + Arg::with_name("ssh-keys") + .long("ssh-keys") + .help("Update SSH keys for the given user") + .takes_value(true), + ), + ) + .subcommand( + SubCommand::with_name("exp") + .about("experimental subcommands") + .subcommand( + SubCommand::with_name("rd-network-kargs") + .about("Supplement initrd with network configuration kargs") + .arg( + Arg::with_name("cmdline") + .long("cmdline") + .global(true) + .help("Read the cloud provider from the kernel cmdline"), + ) + .arg( + Arg::with_name("provider") + .long("provider") + .help("The name of the cloud provider") + .global(true) + .takes_value(true), + ) + .arg( + Arg::with_name("default-value") + .long("default-value") + .help("Default value for network kargs fallback") + .takes_value(true), + ), + ), + ) } /// Translate command-line arguments from legacy mode. @@ -139,8 +185,6 @@ fn translate_legacy_args(cli: impl IntoIterator) -> impl Iterator }) } -impl CliConfig {} - #[cfg(test)] mod tests { use super::*; @@ -167,7 +211,11 @@ mod tests { .map(ToString::to_string) .collect(); - parse_args(legacy).unwrap(); + let cmd = parse_args(legacy).unwrap(); + match cmd { + CliConfig::Multi(_) => {} + x => panic!("unexpected cmd: {:?}", x), + }; } #[test] @@ -183,7 +231,11 @@ mod tests { .map(ToString::to_string) .collect(); - parse_args(args).unwrap(); + let cmd = parse_args(args).unwrap(); + match cmd { + CliConfig::Multi(_) => {} + x => panic!("unexpected cmd: {:?}", x), + }; } #[test] @@ -193,6 +245,38 @@ mod tests { .map(ToString::to_string) .collect(); - parse_args(args).unwrap(); + let cmd = parse_args(args).unwrap(); + match cmd { + CliConfig::Multi(_) => {} + x => panic!("unexpected cmd: {:?}", x), + }; + } + + #[test] + fn test_exp_cmd() { + let args: Vec<_> = [ + "afterburn", + "exp", + "rd-network-kargs", + "--provider", + "gcp", + "--default-value", + "ip=dhcp", + ] + .iter() + .map(ToString::to_string) + .collect(); + + let cmd = parse_args(args).unwrap(); + let subcmd = match cmd { + CliConfig::Exp(v) => v, + x => panic!("unexpected cmd: {:?}", x), + }; + + match subcmd { + exp::CliExp::RdNetworkKargs(_) => {} + #[allow(unreachable_patterns)] + x => panic!("unexpected 'exp' sub-command: {:?}", x), + }; } } diff --git a/src/cli/multi.rs b/src/cli/multi.rs index 63df10e7..9e2c4a65 100644 --- a/src/cli/multi.rs +++ b/src/cli/multi.rs @@ -1,9 +1,7 @@ //! `multi` CLI sub-command. -use super::CMDLINE_PATH; use crate::errors::*; use crate::metadata; -use error_chain::bail; #[derive(Debug)] pub struct CliMulti { @@ -18,7 +16,7 @@ pub struct CliMulti { impl CliMulti { /// Parse flags for the `multi` sub-command. pub(crate) fn parse(matches: &clap::ArgMatches) -> Result { - let provider = Self::parse_provider(matches)?; + let provider = super::parse_provider(matches)?; let multi = Self { attributes_file: matches.value_of("attributes").map(String::from), @@ -42,18 +40,6 @@ impl CliMulti { Ok(super::CliConfig::Multi(multi)) } - /// Parse provider ID from flag or kargs. - fn parse_provider(matches: &clap::ArgMatches) -> Result { - let provider = match (matches.value_of("provider"), matches.is_present("cmdline")) { - (Some(provider), false) => String::from(provider), - (None, true) => crate::util::get_platform(CMDLINE_PATH)?, - (None, false) => bail!("must set either --provider or --cmdline"), - (Some(_), true) => bail!("cannot process both --provider and --cmdline"), - }; - - Ok(provider) - } - /// Run the `multi` sub-command. pub(crate) fn run(self) -> Result<()> { // fetch the metadata from the configured provider diff --git a/src/initrd/mod.rs b/src/initrd/mod.rs new file mode 100644 index 00000000..4f2fdc70 --- /dev/null +++ b/src/initrd/mod.rs @@ -0,0 +1,33 @@ +/// Agent logic running at early boot. +/// +/// This is run early-on in initrd, possibly before networking and other +/// services are configured, so it may not be able to use all usual metadata +/// fetcher. +use crate::errors::*; + +use std::fs::File; +use std::io::Write; + +/// Path to cmdline.d fragment for network kernel arguments. +static KARGS_PATH: &str = "/etc/cmdline.d/50-afterburn-network-kargs.conf"; + +/// Fetch network kargs for the given provider. +pub(crate) fn fetch_network_kargs(provider: &str) -> Result> { + match provider { + // TODO(lucab): wire-in vmware guestinfo logic. + "vmware" => Ok(None), + _ => Ok(None), + } +} + +/// Write network kargs into a cmdline.d fragment. +pub(crate) fn write_network_kargs(kargs: &str) -> Result<()> { + let mut fragment_file = + File::create(KARGS_PATH).chain_err(|| format!("failed to create file {:?}", KARGS_PATH))?; + + fragment_file + .write_all(kargs.as_bytes()) + .chain_err(|| "failed to write network arguments fragment")?; + + Ok(()) +} diff --git a/src/main.rs b/src/main.rs index cbb25143..928f94ef 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,6 +14,7 @@ mod cli; mod errors; +mod initrd; mod metadata; mod network; mod providers; diff --git a/src/providers/mod.rs b/src/providers/mod.rs index 0e2cf8b6..727cc000 100644 --- a/src/providers/mod.rs +++ b/src/providers/mod.rs @@ -37,17 +37,15 @@ pub mod packet; pub mod vagrant_virtualbox; pub mod vmware; +use crate::errors::*; +use crate::network; +use openssh_keys::PublicKey; use std::collections::HashMap; use std::fs::{self, File}; use std::io::prelude::*; use std::path::Path; - -use openssh_keys::PublicKey; use users::{self, User}; -use crate::errors::*; -use crate::network; - #[cfg(not(feature = "cl-legacy"))] const ENV_PREFIX: &str = "AFTERBURN_"; #[cfg(feature = "cl-legacy")] @@ -185,6 +183,11 @@ pub trait MetadataProvider { /// netdev: https://www.freedesktop.org/software/systemd/man/systemd.netdev.html fn virtual_network_devices(&self) -> Result>; + /// Setup initrd network kernel arguments. + fn rd_net_kargs(&self) -> Result<()> { + Ok(()) + } + fn write_attributes(&self, attributes_file_path: String) -> Result<()> { let mut attributes_file = create_file(&attributes_file_path)?; for (k, v) in self.attributes()? {