Skip to content

Commit

Permalink
providers: add experimental initrd network bootstrap
Browse files Browse the repository at this point in the history
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`.
  • Loading branch information
lucab committed Feb 20, 2020
1 parent 8baa07f commit b03edc9
Show file tree
Hide file tree
Showing 7 changed files with 203 additions and 75 deletions.
13 changes: 13 additions & 0 deletions dracut/30afterburn/afterburn-net-bootstrap.service
Original file line number Diff line number Diff line change
@@ -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
7 changes: 6 additions & 1 deletion dracut/30afterburn/module-setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
58 changes: 58 additions & 0 deletions src/cli/exp.rs
Original file line number Diff line number Diff line change
@@ -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<super::CliConfig> {
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<CliExp> {
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(())
}
}
146 changes: 92 additions & 54 deletions src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand All @@ -13,13 +15,15 @@ const CMDLINE_PATH: &str = "/proc/cmdline";
#[derive(Debug)]
pub(crate) enum CliConfig {
Multi(multi::CliMulti),
Exp(exp::CliExp),
}

impl CliConfig {
/// Parse CLI sub-commands into configuration.
pub fn parse_subcommands(app_matches: ArgMatches) -> Result<Self> {
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),
};

Expand All @@ -30,6 +34,7 @@ impl CliConfig {
pub fn run(self) -> Result<()> {
match self {
CliConfig::Multi(cmd) => cmd.run(),
CliConfig::Exp(cmd) => cmd.run(),
}
}
}
Expand All @@ -44,62 +49,97 @@ pub(crate) fn parse_args(argv: impl IntoIterator<Item = String>) -> Result<CliCo
Ok(cfg)
}

/// Parse provider ID from flag or kargs.
fn parse_provider(matches: &clap::ArgMatches) -> Result<String> {
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.
Expand Down Expand Up @@ -139,8 +179,6 @@ fn translate_legacy_args(cli: impl IntoIterator<Item = String>) -> impl Iterator
})
}

impl CliConfig {}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
16 changes: 1 addition & 15 deletions src/cli/multi.rs
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -18,7 +16,7 @@ pub struct CliMulti {
impl CliMulti {
/// Parse flags for the `multi` sub-command.
pub(crate) fn parse(matches: &clap::ArgMatches) -> Result<super::CliConfig> {
let provider = Self::parse_provider(matches)?;
let provider = super::parse_provider(matches)?;

let multi = Self {
attributes_file: matches.value_of("attributes").map(String::from),
Expand All @@ -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<String> {
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
Expand Down
23 changes: 23 additions & 0 deletions src/providers/ibmcloud_classic/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,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 {
Expand Down
15 changes: 10 additions & 5 deletions src/providers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,17 +35,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")]
Expand Down Expand Up @@ -183,6 +182,12 @@ pub trait MetadataProvider {
/// netdev: https://www.freedesktop.org/software/systemd/man/systemd.netdev.html
fn virtual_network_devices(&self) -> Result<Vec<network::VirtualNetDev>>;

/// 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()? {
Expand Down

0 comments on commit b03edc9

Please sign in to comment.