From 9dc7d862e714096f756cef60ec6a281f41299811 Mon Sep 17 00:00:00 2001 From: Luca BRUNO Date: Mon, 20 Jan 2020 12:21:07 +0000 Subject: [PATCH 1/3] network: add helpers for shelling out to `ip` --- src/network/ip_cli.rs | 61 ++++++++++++++++++++++++++++++ src/{network.rs => network/mod.rs} | 25 ++++++++++++ 2 files changed, 86 insertions(+) create mode 100644 src/network/ip_cli.rs rename src/{network.rs => network/mod.rs} (95%) diff --git a/src/network/ip_cli.rs b/src/network/ip_cli.rs new file mode 100644 index 00000000..e4d21056 --- /dev/null +++ b/src/network/ip_cli.rs @@ -0,0 +1,61 @@ +//! Helpers for shelling out to the `ip` command. + +use crate::errors::*; +use error_chain::bail; +use ipnetwork::IpNetwork; +use slog_scope::trace; +use std::process::Command; + +/// Create a new interface. +#[allow(dead_code)] +pub(crate) fn ip_link_add(dev_name: &str, mac_addr: &str) -> Result<()> { + let link_type = "ether"; + let mut cmd = Command::new("ip"); + cmd.args(&["link", "add"]) + .arg(&dev_name) + .arg("address") + .arg(&mac_addr) + .args(&["type", link_type]); + try_exec(cmd).chain_err(|| "'ip link add' failed") +} + +/// Bring up a named interface. +pub(crate) fn ip_link_set_up(dev_name: &str) -> Result<()> { + let mut cmd = Command::new("ip"); + cmd.args(&["link", "set"]) + .args(&["dev", dev_name]) + .arg("up"); + try_exec(cmd).chain_err(|| "'ip link set up' failed") +} + +/// Add an address to an interface. +pub(crate) fn ip_address_add(dev_name: &str, ip_addr: &IpNetwork) -> Result<()> { + let mut cmd = Command::new("ip"); + cmd.args(&["address", "add"]) + .arg(ip_addr.to_string()) + .args(&["dev", dev_name]); + try_exec(cmd).chain_err(|| "'ip address add' failed") +} + +/// Add a route. +pub(crate) fn ip_route_add(route: &super::NetworkRoute) -> Result<()> { + let mut cmd = Command::new("ip"); + cmd.args(&["route", "add"]) + .arg(&route.destination.to_string()) + .args(&["via", &route.gateway.to_string()]); + try_exec(cmd).chain_err(|| "'ip route add' failed") +} + +/// Try to execute, and log stderr on failure. +fn try_exec(cmd: Command) -> Result<()> { + let mut cmd = cmd; + trace!("{:?}", &cmd); + + let output = cmd.output()?; + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + bail!("{}", stderr); + }; + + Ok(()) +} diff --git a/src/network.rs b/src/network/mod.rs similarity index 95% rename from src/network.rs rename to src/network/mod.rs index 15acffde..a3bb02dc 100644 --- a/src/network.rs +++ b/src/network/mod.rs @@ -24,6 +24,8 @@ use std::net::IpAddr; use std::string::String; use std::string::ToString; +mod ip_cli; + pub const BONDING_MODE_BALANCE_RR: u32 = 0; pub const BONDING_MODE_ACTIVE_BACKUP: u32 = 1; pub const BONDING_MODE_BALANCE_XOR: u32 = 2; @@ -177,6 +179,29 @@ impl Interface { config } + + /// Bring up interfaces and apply network configuration via `ip`. + pub fn ip_apply(&self) -> Result<()> { + let name = match self.name { + Some(ref n) => n, + None => bail!("missing interface name"), + }; + + // Bring up. + ip_cli::ip_link_set_up(&name)?; + + // Add addresses. + for ip_addr in &self.ip_addresses { + ip_cli::ip_address_add(&name, ip_addr)?; + } + + // Add routes + for route in &self.routes { + ip_cli::ip_route_add(route)?; + } + + Ok(()) + } } impl VirtualNetDev { From 833b80dab6323b134f1567cea0fc184f4ec847b1 Mon Sep 17 00:00:00 2001 From: Luca BRUNO Date: Mon, 20 Jan 2020 13:30:50 +0000 Subject: [PATCH 2/3] network/utils: add resolvconf helpers --- src/network/mod.rs | 1 + src/network/utils.rs | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 src/network/utils.rs diff --git a/src/network/mod.rs b/src/network/mod.rs index a3bb02dc..ebb78bdc 100644 --- a/src/network/mod.rs +++ b/src/network/mod.rs @@ -25,6 +25,7 @@ use std::string::String; use std::string::ToString; mod ip_cli; +pub mod utils; pub const BONDING_MODE_BALANCE_RR: u32 = 0; pub const BONDING_MODE_ACTIVE_BACKUP: u32 = 1; diff --git a/src/network/utils.rs b/src/network/utils.rs new file mode 100644 index 00000000..bb7dd765 --- /dev/null +++ b/src/network/utils.rs @@ -0,0 +1,35 @@ +/// Misc network-related helpers. +use crate::errors::*; +use std::io::Write; +use std::net::IpAddr; + +/// Write nameservers in `resolv.conf` format. +pub(crate) fn write_resolvconf(writer: &mut T, nameservers: &[IpAddr]) -> Result<()> +where + T: Write, +{ + slog_scope::trace!("writing {} nameservers", nameservers.len()); + + for ns in nameservers { + let entry = format!("nameserver {}\n", ns); + writer.write_all(&entry.as_bytes())?; + writer.flush()?; + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_write_resolvconf() { + let nameservers = vec![IpAddr::from([4, 4, 4, 4]), IpAddr::from([8, 8, 8, 8])]; + let expected = "nameserver 4.4.4.4\nnameserver 8.8.8.8\n"; + let mut buf = vec![]; + + write_resolvconf(&mut buf, &nameservers).unwrap(); + assert_eq!(buf, expected.as_bytes()); + } +} From aaf746cb9ef16b940262e251e2a3f63d924e8551 Mon Sep 17 00:00:00 2001 From: Luca BRUNO Date: Mon, 20 Jan 2020 12:21:17 +0000 Subject: [PATCH 3/3] providers: add experimental initrd network bootstrap This adds initial/experimental support for bootstrapping the network in the initrd. It is meant to support weird cloud providers where DHCP is not available or not usable. This feature is currently reachable as a dedicated `exp rd-net-bootstrap` subcommand. The first provider where this logic is required is `ibmcloud-classic`. --- .../afterburn-net-bootstrap.service | 13 ++ dracut/30afterburn/module-setup.sh | 7 +- src/cli/exp.rs | 58 +++++++ src/cli/mod.rs | 146 +++++++++++------- src/cli/multi.rs | 16 +- src/providers/ibmcloud_classic/mod.rs | 23 +++ src/providers/mod.rs | 15 +- 7 files changed, 203 insertions(+), 75 deletions(-) create mode 100644 dracut/30afterburn/afterburn-net-bootstrap.service create mode 100644 src/cli/exp.rs diff --git a/dracut/30afterburn/afterburn-net-bootstrap.service b/dracut/30afterburn/afterburn-net-bootstrap.service new file mode 100644 index 00000000..f0558c23 --- /dev/null +++ b/dracut/30afterburn/afterburn-net-bootstrap.service @@ -0,0 +1,13 @@ +[Unit] +Description=Afterburn network bootstrapping +# IBM Cloud (Classic) has no DHCP. +ConditionKernelCommandLine=|ignition.platform.id=ibmcloud-classic + +Before=ignition-fetch.service + +OnFailure=emergency.target +OnFailureJobMode=isolate + +[Service] +ExecStart=/usr/bin/afterburn exp rd-net-bootstrap --cmdline +Type=oneshot diff --git a/dracut/30afterburn/module-setup.sh b/dracut/30afterburn/module-setup.sh index 0875c719..faff7751 100755 --- a/dracut/30afterburn/module-setup.sh +++ b/dracut/30afterburn/module-setup.sh @@ -16,9 +16,14 @@ install() { inst_simple "$moddir/afterburn-hostname.service" \ "$systemdutildir/system/afterburn-hostname.service" + inst_simple "$moddir/afterburn-net-bootstrap.service" \ + "$systemdutildir/system/afterburn-net-bootstrap.service" + # We want the afterburn-hostname to be firstboot only, so Ignition-provided # hostname changes do not get overwritten on subsequent boots - mkdir -p "$initdir/$systemdsystemunitdir/ignition-complete.target.requires" ln -s "../afterburn-hostname.service" "$initdir/$systemdsystemunitdir/ignition-complete.target.requires/afterburn-hostname.service" + + mkdir -p "$initdir/$systemdsystemunitdir/ignition-fetch.service.requires" + ln -s "../afterburn-net-boostrap.service" "$initdir/$systemdsystemunitdir/ignition-fetch.service.requires/afterburn-net-bootstrap.service" } diff --git a/src/cli/exp.rs b/src/cli/exp.rs new file mode 100644 index 00000000..2369b9a9 --- /dev/null +++ b/src/cli/exp.rs @@ -0,0 +1,58 @@ +//! `exp` CLI sub-command. + +use crate::errors::*; +use crate::metadata; +use clap::ArgMatches; +use error_chain::bail; + +#[derive(Debug)] +pub enum CliExp { + NetBootstrap(CliNetBootstrap), +} + +impl CliExp { + /// Parse sub-command into configuration. + pub(crate) fn parse(app_matches: &ArgMatches) -> Result { + if app_matches.subcommand_name().is_none() { + bail!("missing exp subcommand"); + } + + let cfg = match app_matches.subcommand() { + ("rd-net-bootstrap", Some(matches)) => CliNetBootstrap::parse(matches)?, + (x, _) => unreachable!("unrecognized exp subcommand '{}'", x), + }; + + Ok(super::CliConfig::Exp(cfg)) + } + + // Run sub-command. + pub(crate) fn run(&self) -> Result<()> { + match self { + CliExp::NetBootstrap(cmd) => cmd.run()?, + }; + Ok(()) + } +} + +/// Sub-command for network bootstrap. +#[derive(Debug)] +pub struct CliNetBootstrap { + platform: String, +} + +impl CliNetBootstrap { + /// Parse sub-command into configuration. + pub(crate) fn parse(matches: &ArgMatches) -> Result { + let platform = super::parse_provider(matches)?; + + let cfg = Self { platform }; + Ok(CliExp::NetBootstrap(cfg)) + } + + /// Run the sub-command. + pub(crate) fn run(&self) -> Result<()> { + let provider = metadata::fetch_metadata(&self.platform)?; + provider.rd_net_bootstrap()?; + Ok(()) + } +} diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 0d303247..1eadd23a 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,97 @@ 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 commands") + .subcommand( + SubCommand::with_name("rd-net-bootstrap") + .about("Bootstrap network in initrd") + .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"), + ), + ), + ) } /// Translate command-line arguments from legacy mode. @@ -139,8 +179,6 @@ fn translate_legacy_args(cli: impl IntoIterator) -> impl Iterator }) } -impl CliConfig {} - #[cfg(test)] mod tests { use super::*; 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/providers/ibmcloud_classic/mod.rs b/src/providers/ibmcloud_classic/mod.rs index 811b3742..7f250137 100644 --- a/src/providers/ibmcloud_classic/mod.rs +++ b/src/providers/ibmcloud_classic/mod.rs @@ -295,6 +295,29 @@ impl MetadataProvider for IBMClassicProvider { warn!("boot check-in requested, but not supported on this platform"); Ok(()) } + + fn rd_net_bootstrap(&self) -> Result<()> { + let net_ifaces = self.networks()?; + let mut nameservers = vec![]; + + // Configure network. + for iface in net_ifaces { + iface.ip_apply()?; + + // Collect nameservers for later. + for ns in iface.nameservers { + if !nameservers.contains(&ns) { + nameservers.push(ns); + } + } + } + + // Configure DNS resolvers. + let mut resolvconf = File::create("/etc/resolv.conf")?; + crate::network::utils::write_resolvconf(&mut resolvconf, &nameservers)?; + + Ok(()) + } } impl Drop for IBMClassicProvider { diff --git a/src/providers/mod.rs b/src/providers/mod.rs index 02fc9e62..acd8dbf5 100644 --- a/src/providers/mod.rs +++ b/src/providers/mod.rs @@ -36,17 +36,16 @@ pub mod openstack; pub mod packet; pub mod vagrant_virtualbox; +use crate::errors::*; +use crate::network; +use openssh_keys::PublicKey; +use slog_scope::warn; 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")] @@ -184,6 +183,12 @@ pub trait MetadataProvider { /// netdev: https://www.freedesktop.org/software/systemd/man/systemd.netdev.html fn virtual_network_devices(&self) -> Result>; + /// Bootstrap initramfs networking. + fn rd_net_bootstrap(&self) -> Result<()> { + warn!("initramfs network bootstrap requested, but not supported on this platform"); + 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()? {