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

Add a hidden persist-nic-names command #2301

Merged
merged 3 commits into from Apr 11, 2023
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions .dockerignore
@@ -0,0 +1 @@
rust/target
60 changes: 58 additions & 2 deletions rust/src/cli/ncl.rs
Expand Up @@ -9,6 +9,8 @@ mod format;
#[cfg(feature = "gen_conf")]
mod gen_conf;
#[cfg(feature = "query_apply")]
pub(crate) mod persist_nic;
#[cfg(feature = "query_apply")]
mod policy;
#[cfg(feature = "query_apply")]
mod query;
Expand Down Expand Up @@ -49,6 +51,7 @@ const SUB_CMD_EDIT: &str = "edit";
const SUB_CMD_VERSION: &str = "version";
const SUB_CMD_AUTOCONF: &str = "autoconf";
const SUB_CMD_SERVICE: &str = "service";
const SUB_CMD_PERSIST_NIC_NAMES: &str = "persist-nic-names";
const SUB_CMD_POLICY: &str = "policy";
const SUB_CMD_FORMAT: &str = "format";

Expand All @@ -61,7 +64,7 @@ fn main() {
print_result_and_exit(autoconf(&argv[1..]));
}

let matches = clap::Command::new(APP_NAME)
let mut app = clap::Command::new(APP_NAME)
.version(clap::crate_version!())
.author("Gris Ge <fge@redhat.com>")
.about("Command line of nmstate")
Expand Down Expand Up @@ -317,7 +320,40 @@ fn main() {
.subcommand(
clap::Command::new(SUB_CMD_VERSION)
.about("Show version")
).get_matches();
);
if cfg!(feature = "query_apply") {
app = app.subcommand(
clap::Command::new(SUB_CMD_PERSIST_NIC_NAMES)
.about("Generate .link files which persist active network interfaces to their current names")
.arg(
clap::Arg::new("DRY_RUN")
.long("dry-run")
.takes_value(false)
.help(
"Only output changes that would be made",
),
)
.arg(
clap::Arg::new("INSPECT")
.long("inspect")
.takes_value(false)
.help(
"Print the state of any persisted nics",
),
)
.arg(
clap::Arg::new("ROOT")
.long("root")
.short('r')
.required(false)
.takes_value(true)
.default_value("/")
.help("Target root filesystem for writing state"),
)
// We don't want to expose this outside of OCP yet
.hide(true));
};
let matches = app.get_matches();
let (log_module_filters, log_level) =
match matches.occurrences_of("verbose") {
0 => (vec!["nmstate", "nm_dbus"], LevelFilter::Info),
Expand Down Expand Up @@ -387,6 +423,26 @@ fn main() {
APP_NAME,
clap::crate_version!()
)));
} else {
// Conditionally-built commands
#[cfg(feature = "query_apply")]
if let Some(matches) =
matches.subcommand_matches(SUB_CMD_PERSIST_NIC_NAMES)
{
let action =
if matches.try_contains_id("DRY_RUN").unwrap_or_default() {
persist_nic::PersistAction::DryRun
} else if matches.try_contains_id("INSPECT").unwrap_or_default()
{
persist_nic::PersistAction::Inspect
} else {
persist_nic::PersistAction::Save
};
print_result_and_exit(crate::persist_nic::run_persist_immediately(
matches.value_of("ROOT").unwrap(),
action,
));
}
}
}

Expand Down
275 changes: 275 additions & 0 deletions rust/src/cli/persist_nic.rs
@@ -0,0 +1,275 @@
//! # Handling writing .link files for NICs
//!
//! This module implements logic for generating systemd [`.link`] files
//! based on active networking state.
//!
//! The logic currently is:
//!
//! - Iterate over all active NICs
//! - Skip any that don't have a MAC address (hence are likely non-physical)
//! - Skip any that are virtual (by looking for devices/virtual)
//! - Persist any that have a static IP address assigned (no DHCP)
//! - Persist any that are subordinate (part of a bridge/bond)
//! - Otherwise, skip
//!
//! ## Known broken cases
//!
//! - bond devices over DHCP
//!
//! [`.link`]: https://www.freedesktop.org/software/systemd/man/systemd.link.html
use std::path::Path;

use nmstate::{Interface, InterfaceType, NetworkState};

use crate::error::CliError;

/// Comment added into our generated link files
const PERSIST_GENERATED_BY: &str = "# Generated by nmstate";
/// The file prefix for our generated persisted NIC names.
/// 98 here is important as it should be invoked after others but before
/// 99-default.link
const PERSIST_FILE_PREFIX: &str = "98-nmstate";
/// See https://www.freedesktop.org/software/systemd/man/systemd.link.html
const SYSTEMD_NETWORK_LINK_FOLDER: &str = "etc/systemd/network";
/// File which if present signals that we have already performed NIC name persistence.
const NMSTATE_PERSIST_STAMP: &str = ".nmstate-persist.stamp";

/// The action to take
pub(crate) enum PersistAction {
/// Persist NIC name state
Save,
/// Print what we would do in Save mode
DryRun,
/// Output any persisted state
Inspect,
}

fn gather_state() -> Result<NetworkState, CliError> {
let mut state = NetworkState::new();
state.set_kernel_only(true);
state.set_running_config_only(true);
state.retrieve()?;
Ok(state)
}

fn device_is_virtual(iface: &Interface) -> bool {
let name = iface.name();
let p = format!("/sys/class/net/{name}");
match std::fs::read_link(&p) {
Ok(r) => match r.to_str() {
Some(s) => s.contains("devices/virtual"),
None => {
log::warn!("Invalid link with non-UTF8 {p}");
false
}
},
Err(e) => {
log::warn!("Failed to readlink {p}: {e}");
false
}
}
}

enum PersistenceReason {
StaticAddressing,
Subordinate,
}

fn should_persist_nic_for(iface: &Interface) -> Option<PersistenceReason> {
let base_iface = iface.base_iface();
let name = iface.name();

if device_is_virtual(iface) {
log::debug!("Skipping virtual device {name}");
return None;
}

let ipv4_manual = base_iface
.ipv4
.as_ref()
.map(|ip| ip.is_static())
.unwrap_or_default();
let ipv6_manual = base_iface
.ipv6
.as_ref()
.map(|ip| ip.is_static())
.unwrap_or_default();
if ipv4_manual || ipv6_manual {
return Some(PersistenceReason::StaticAddressing);
}

// Unconditionally persist NICs that are subordinate e.g.
// - Bonds over DHCP
// - OpenShift cases where the primary NIC is moved into `ovs-system`
// Because there is likely external configuration referencing the NIC name
if let Some(controller) = base_iface.controller.as_deref() {
log::debug!("Device {name} is subordinate to controller {controller}");
return Some(PersistenceReason::Subordinate);
}

None
}

fn process_interfaces<F>(state: &NetworkState, mut f: F) -> Result<(), CliError>
where
F: FnMut(&nmstate::Interface, &str) -> Result<(), CliError>,
{
for iface in state
.interfaces
.iter()
.filter(|i| i.iface_type() == InterfaceType::Ethernet)
{
let name = iface.name();
let iface_name = iface.name();
let mac = match iface.base_iface().mac_address.as_ref() {
Some(c) => c,
None => continue,
};
if let Some(reason) = should_persist_nic_for(iface) {
match reason {
PersistenceReason::StaticAddressing => {
log::info!("Device {name} has static address");
}
PersistenceReason::Subordinate => {
log::info!("Device {name} is subordinate");
}
}
} else {
log::info!("Skipping interface {iface_name}");
continue;
}

f(iface, mac.as_str())?;
}
Ok(())
}

/// For all active interfaces, write a systemd .link file which persists the currently
/// active name.
pub(crate) fn run_persist_immediately(
root: &str,
action: PersistAction,
) -> Result<String, CliError> {
let dry_run = match action {
PersistAction::Save => false,
PersistAction::DryRun => true,
PersistAction::Inspect => return inspect(root),
};

let stamp_path = Path::new(root)
.join(SYSTEMD_NETWORK_LINK_FOLDER)
.join(NMSTATE_PERSIST_STAMP);
if stamp_path.exists() {
log::info!("{} exists; nothing to do", stamp_path.display());
return Ok("".to_string());
}

let state = gather_state()?;
let mut changed = false;
process_interfaces(&state, |iface, mac| {
let iface_name = iface.name();
let action = if dry_run {
"Would persist"
} else {
"Persisting"
};
log::info!(
"{action} the interface with MAC {mac} to \
interface name {iface_name}"
);
if !dry_run {
changed |=
persist_iface_name_via_systemd_link(root, mac, iface.name())?;
}
Ok(())
})?;

if !changed {
log::info!("No changes.");
}

if !dry_run {
std::fs::write(stamp_path, b"")?;
}

Ok("".to_string())
}

pub(crate) fn inspect(root: &str) -> Result<String, CliError> {
let netdir = Path::new(root).join(SYSTEMD_NETWORK_LINK_FOLDER);
let stamp_path = netdir.join(NMSTATE_PERSIST_STAMP);
if !stamp_path.exists() {
log::info!(
"{} does not exist, no prior persisted state",
stamp_path.display()
);
return Ok("".to_string());
}

let mut n = 0;
for e in netdir.read_dir()? {
let e = e?;
let name = e.file_name();
let name = if let Some(n) = name.to_str() {
n
} else {
continue;
};
if !name.ends_with(".link") {
continue;
}
if !name.starts_with(PERSIST_FILE_PREFIX) {
continue;
}
log::info!("Found persisted NIC file: {name}");
n += 1;
}
if n == 0 {
log::info!("No persisted NICs found");
}

let state = gather_state()?;
process_interfaces(&state, |iface, mac| {
let iface_name = iface.name();
log::info!(
"NOTE: would persist the interface with MAC {mac} to interface name {iface_name}"
);
Ok(())
})?;

Ok("".to_string())
}

fn persist_iface_name_via_systemd_link(
root: &str,
mac: &str,
iface_name: &str,
) -> Result<bool, CliError> {
let link_dir = Path::new(root).join(SYSTEMD_NETWORK_LINK_FOLDER);

let file_path =
link_dir.join(format!("{PERSIST_FILE_PREFIX}-{iface_name}.link"));
if file_path.exists() {
log::info!("Network link file {} already exists", file_path.display());
return Ok(false);
}

if !link_dir.exists() {
std::fs::create_dir(&link_dir)?;
}

let content =
format!("{PERSIST_GENERATED_BY}\n[Match]\nMACAddress={mac}\n\n[Link]\nName={iface_name}\n");

std::fs::write(&file_path, content.as_bytes()).map_err(|e| {
CliError::from(format!(
"Failed to store captured states to file {}: {e}",
file_path.display()
))
})?;
log::info!(
"Systemd network link file created at {}",
file_path.display()
);
Ok(true)
}
2 changes: 1 addition & 1 deletion rust/src/cli/service.rs
Expand Up @@ -89,7 +89,7 @@ fn get_config_files(folder: &str) -> Result<Vec<PathBuf>, CliError> {
}

// rename file by adding a suffix `.applied`.
fn relocate_file(file_path: &Path) -> Result<(), CliError> {
pub(crate) fn relocate_file(file_path: &Path) -> Result<(), CliError> {
let new_path = file_path.with_extension(RELOCATE_FILE_EXTENTION);
std::fs::rename(file_path, &new_path)?;
log::info!(
Expand Down
1 change: 1 addition & 0 deletions rust/src/clib/Cargo.toml
Expand Up @@ -11,6 +11,7 @@ rust-version = "1.58"
name = "nmstate"
path = "lib.rs"
crate-type = ["cdylib", "staticlib"]
doc = false

[dependencies]
nmstate = { path = "../lib", default-features = false }
Expand Down