Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] providers: add experimental initrd network bootstrap #362

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
61 changes: 61 additions & 0 deletions src/network/ip_cli.rs
Original file line number Diff line number Diff line change
@@ -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(())
}
Loading