From f8fb15dba7c5440a35165676dc5796d168b03a79 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 cffc183d72179b070ac174400b5d094e51c3b1d4 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 30307c8137268bb7e05ad4874cbe9b42f2328036 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. --- src/cli/exp.rs | 58 ++++++++++++ src/cli/mod.rs | 146 +++++++++++++++++++----------- src/cli/multi.rs | 16 +--- src/providers/ibmcloud/classic.rs | 23 +++++ src/providers/mod.rs | 15 ++- 5 files changed, 184 insertions(+), 74 deletions(-) create mode 100644 src/cli/exp.rs 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.rs b/src/providers/ibmcloud/classic.rs index d2dd8280..b77350f7 100644 --- a/src/providers/ibmcloud/classic.rs +++ b/src/providers/ibmcloud/classic.rs @@ -293,6 +293,29 @@ impl MetadataProvider for ClassicProvider { 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 ClassicProvider { diff --git a/src/providers/mod.rs b/src/providers/mod.rs index f48c4f93..a82e6bcf 100644 --- a/src/providers/mod.rs +++ b/src/providers/mod.rs @@ -34,17 +34,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")] @@ -182,6 +181,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()? {