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

netdog: refactor to prepare for upcoming additions #2330

Merged
merged 4 commits into from
Aug 12, 2022
Merged
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
32 changes: 32 additions & 0 deletions sources/api/netdog/src/cli/generate_hostname.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
use super::{error, print_json, Result};
use crate::CURRENT_IP;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about moving the constants into some other module, like the parent?

Copy link
Contributor Author

@zmrow zmrow Aug 12, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I spent more time than I'd like to admit playing and thinking about that... I ended up keeping them where they were since they get used in the CLI and other modules so I didn't really like them in cli/mod.rs and I also didn't really like the idea of making a top-level lib.rs just for constants.

Totally willing to entertain this though - perhaps there's an elegant way I just couldn't come up with! 😄

use argh::FromArgs;
use dns_lookup::lookup_addr;
use snafu::ResultExt;
use std::fs;
use std::net::IpAddr;
use std::str::FromStr;

#[derive(FromArgs, PartialEq, Debug)]
#[argh(subcommand, name = "generate-hostname")]
/// Generate hostname from DNS reverse lookup or use current IP
pub(crate) struct GenerateHostnameArgs {}

/// Attempt to resolve assigned IP address, if unsuccessful use the IP as the hostname.
///
/// The result is returned as JSON. (intended for use as a settings generator)
pub(crate) fn run() -> Result<()> {
let ip_string = fs::read_to_string(CURRENT_IP)
.context(error::CurrentIpReadFailedSnafu { path: CURRENT_IP })?;
let ip = IpAddr::from_str(&ip_string).context(error::IpFromStringSnafu { ip: &ip_string })?;
let hostname = match lookup_addr(&ip) {
Ok(hostname) => hostname,
Err(e) => {
eprintln!("Reverse DNS lookup failed: {}", e);
ip_string
}
};

// sundog expects JSON-serialized output
print_json(hostname)
}
56 changes: 56 additions & 0 deletions sources/api/netdog/src/cli/generate_net_config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
use super::{error, Result};
use crate::{net_config, DEFAULT_NET_CONFIG_FILE, KERNEL_CMDLINE, PRIMARY_INTERFACE};
use argh::FromArgs;
use snafu::{OptionExt, ResultExt};
use std::{fs, path::Path};

#[derive(FromArgs, PartialEq, Debug)]
#[argh(subcommand, name = "generate-net-config")]
/// Generate wicked network configuration
pub(crate) struct GenerateNetConfigArgs {}

/// Generate configuration for network interfaces.
pub(crate) fn run() -> Result<()> {
let maybe_net_config = if Path::exists(Path::new(DEFAULT_NET_CONFIG_FILE)) {
net_config::from_path(DEFAULT_NET_CONFIG_FILE).context(error::NetConfigParseSnafu {
path: DEFAULT_NET_CONFIG_FILE,
})?
} else {
net_config::from_command_line(KERNEL_CMDLINE).context(error::NetConfigParseSnafu {
path: KERNEL_CMDLINE,
})?
};

// `maybe_net_config` could be `None` if no interfaces were defined
let net_config = match maybe_net_config {
Some(net_config) => net_config,
None => {
eprintln!("No network interfaces were configured");
return Ok(());
}
};

let primary_interface = net_config
.primary_interface()
.context(error::GetPrimaryInterfaceSnafu)?;
write_primary_interface(primary_interface)?;

let wicked_interfaces = net_config.as_wicked_interfaces();
for interface in wicked_interfaces {
interface
.write_config_file()
.context(error::InterfaceConfigWriteSnafu)?;
}
Ok(())
}

/// Persist the primary interface name to file
fn write_primary_interface<S>(interface: S) -> Result<()>
where
S: AsRef<str>,
{
let interface = interface.as_ref();
fs::write(PRIMARY_INTERFACE, interface).context(error::PrimaryInterfaceWriteSnafu {
path: PRIMARY_INTERFACE,
})
}
93 changes: 93 additions & 0 deletions sources/api/netdog/src/cli/install.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
use super::{error, InterfaceFamily, InterfaceType, Result};
use crate::lease::LeaseInfo;
use crate::{CURRENT_IP, PRIMARY_INTERFACE, RESOLV_CONF};
use argh::FromArgs;
use rand::prelude::SliceRandom;
use rand::thread_rng;
use snafu::ResultExt;
use std::fmt::Write;
use std::fs;
use std::net::IpAddr;
use std::path::PathBuf;

#[derive(FromArgs, PartialEq, Debug)]
#[argh(subcommand, name = "install")]
/// Write resolv.conf and current IP to disk
pub(crate) struct InstallArgs {
#[argh(option, short = 'i')]
/// name of the network interface
interface_name: String,

#[argh(option, short = 't')]
/// network interface type
interface_type: InterfaceType,

#[argh(option, short = 'f')]
/// network interface family (ipv4/6)
interface_family: InterfaceFamily,

#[argh(positional)]
/// lease info data file
data_file: PathBuf,

#[argh(positional)]
// wicked adds `info` to the call to this program. We don't do anything with it but must
// be able to parse the option to avoid failing
/// ignored
info: Option<String>,
}

pub(crate) fn run(args: InstallArgs) -> Result<()> {
// Wicked doesn't mangle interface names, but let's be defensive.
let install_interface = args.interface_name.trim().to_lowercase();
let primary_interface = fs::read_to_string(PRIMARY_INTERFACE)
.context(error::PrimaryInterfaceReadSnafu {
path: PRIMARY_INTERFACE,
})?
.trim()
.to_lowercase();

if install_interface != primary_interface {
return Ok(());
}

match (&args.interface_type, &args.interface_family) {
(InterfaceType::Dhcp, InterfaceFamily::Ipv4) => {
let info =
LeaseInfo::from_lease(&args.data_file).context(error::LeaseParseFailedSnafu {
path: &args.data_file,
})?;
// Randomize name server order, for libc implementations like musl that send
// queries to the first N servers.
let mut dns_servers: Vec<_> = info.dns_servers.iter().collect();
dns_servers.shuffle(&mut thread_rng());
write_resolv_conf(&dns_servers, &info.dns_search)?;
write_current_ip(&info.ip_address.addr())?;
}
_ => eprintln!("Unhandled 'install' command: {:?}", &args),
}
Ok(())
}

/// Write resolver configuration for libc.
fn write_resolv_conf(dns_servers: &[&IpAddr], dns_search: &Option<Vec<String>>) -> Result<()> {
let mut output = String::new();

if let Some(s) = dns_search {
writeln!(output, "search {}", s.join(" ")).context(error::ResolvConfBuildFailedSnafu)?;
}

for n in dns_servers {
writeln!(output, "nameserver {}", n).context(error::ResolvConfBuildFailedSnafu)?;
}

fs::write(RESOLV_CONF, output)
.context(error::ResolvConfWriteFailedSnafu { path: RESOLV_CONF })?;
Ok(())
}

/// Persist the current IP address to file
fn write_current_ip(ip: &IpAddr) -> Result<()> {
fs::write(CURRENT_IP, ip.to_string())
.context(error::CurrentIpWriteFailedSnafu { path: CURRENT_IP })
}
120 changes: 120 additions & 0 deletions sources/api/netdog/src/cli/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
pub(crate) mod generate_hostname;
pub(crate) mod generate_net_config;
pub(crate) mod install;
pub(crate) mod node_ip;
pub(crate) mod prepare_primary_interface;
pub(crate) mod remove;
pub(crate) mod set_hostname;

pub(crate) use generate_hostname::GenerateHostnameArgs;
pub(crate) use generate_net_config::GenerateNetConfigArgs;
pub(crate) use install::InstallArgs;
pub(crate) use node_ip::NodeIpArgs;
pub(crate) use prepare_primary_interface::PreparePrimaryInterfaceArgs;
pub(crate) use remove::RemoveArgs;
use serde::{Deserialize, Serialize};
pub(crate) use set_hostname::SetHostnameArgs;
use snafu::ResultExt;

#[derive(Debug, PartialEq, Deserialize)]
#[serde(rename_all = "kebab-case")]
enum InterfaceType {
Dhcp,
}

#[derive(Debug, PartialEq, Deserialize)]
#[serde(rename_all = "kebab-case")]
enum InterfaceFamily {
Ipv4,
Ipv6,
}

// Implement `from_str()` so argh can attempt to deserialize args into their proper types
derive_fromstr_from_deserialize!(InterfaceType);
derive_fromstr_from_deserialize!(InterfaceFamily);

/// Helper function that serializes the input to JSON and prints it
fn print_json<S>(val: S) -> Result<()>
where
S: AsRef<str> + Serialize,
{
let val = val.as_ref();
let output = serde_json::to_string(val).context(error::JsonSerializeSnafu { output: val })?;
println!("{}", output);
Ok(())
}

/// Potential errors during netdog execution
mod error {
use crate::{lease, net_config, wicked};
use snafu::Snafu;
use std::io;
use std::path::PathBuf;

#[derive(Debug, Snafu)]
#[snafu(visibility(pub(crate)))]
#[allow(clippy::enum_variant_names)]
pub(crate) enum Error {
#[snafu(display("Failed to write current IP to '{}': {}", path.display(), source))]
CurrentIpWriteFailed { path: PathBuf, source: io::Error },

#[snafu(display("Failed to read current IP data in '{}': {}", path.display(), source))]
CurrentIpReadFailed { path: PathBuf, source: io::Error },

#[snafu(display("'systemd-sysctl' failed: {}", stderr))]
FailedSystemdSysctl { stderr: String },

#[snafu(display("Failed to discern primary interface"))]
GetPrimaryInterface,

#[snafu(display("Failed to write hostname to '{}': {}", path.display(), source))]
HostnameWriteFailed { path: PathBuf, source: io::Error },

#[snafu(display("Failed to write network interface configuration: {}", source))]
InterfaceConfigWrite { source: wicked::Error },

#[snafu(display("Invalid IP address '{}': {}", ip, source))]
IpFromString {
ip: String,
source: std::net::AddrParseError,
},

#[snafu(display("Error serializing to JSON: '{}': {}", output, source))]
JsonSerialize {
output: String,
source: serde_json::error::Error,
},

#[snafu(display("Failed to read/parse lease data in '{}': {}", path.display(), source))]
LeaseParseFailed { path: PathBuf, source: lease::Error },

#[snafu(display("Unable to read/parse network config from '{}': {}", path.display(), source))]
NetConfigParse {
path: PathBuf,
source: net_config::Error,
},

#[snafu(display("Failed to write primary interface to '{}': {}", path.display(), source))]
PrimaryInterfaceWrite { path: PathBuf, source: io::Error },

#[snafu(display("Failed to read primary interface from '{}': {}", path.display(), source))]
PrimaryInterfaceRead { path: PathBuf, source: io::Error },

#[snafu(display("Failed to build resolver configuration: {}", source))]
ResolvConfBuildFailed { source: std::fmt::Error },

#[snafu(display("Failed to write resolver configuration to '{}': {}", path.display(), source))]
ResolvConfWriteFailed { path: PathBuf, source: io::Error },

#[snafu(display("Failed to build sysctl config: {}", source))]
SysctlConfBuild { source: std::fmt::Error },

#[snafu(display("Failed to write sysctl config to '{}': {}", path.display(), source))]
SysctlConfWrite { path: PathBuf, source: io::Error },

#[snafu(display("Failed to run 'systemd-sysctl': {}", source))]
SystemdSysctlExecution { source: io::Error },
}
}

pub(crate) type Result<T> = std::result::Result<T, error::Error>;
23 changes: 23 additions & 0 deletions sources/api/netdog/src/cli/node_ip.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
use super::{error, print_json, Result};
use crate::CURRENT_IP;
use argh::FromArgs;
use snafu::ResultExt;
use std::fs;
use std::net::IpAddr;
use std::str::FromStr;

#[derive(FromArgs, PartialEq, Debug)]
#[argh(subcommand, name = "node-ip")]
/// Return the current IP address
pub(crate) struct NodeIpArgs {}

/// Return the current IP address as JSON (intended for use as a settings generator)
pub(crate) fn run() -> Result<()> {
let ip_string = fs::read_to_string(CURRENT_IP)
.context(error::CurrentIpReadFailedSnafu { path: CURRENT_IP })?;
// Validate that we read a proper IP address
let _ = IpAddr::from_str(&ip_string).context(error::IpFromStringSnafu { ip: &ip_string })?;

// sundog expects JSON-serialized output
print_json(ip_string)
}
Loading