use std::fs;
use std::io::Write as _;
use std::os::unix::fs::PermissionsExt as _;
use std::path::{Path, PathBuf};
const KERNEL_HOSTNAME: &str = "/proc/sys/kernel/hostname";
const ETC_HOSTNAME: &str = "/etc/hostname";
const ETC_HOSTS: &str = "/etc/hosts";
const HOSTNAME_ALIAS_ADDRESS: &str = "127.0.1.1";
const NETWORKD_BINARIES: &[&str] = &[
"/usr/lib/systemd/systemd-networkd",
"/lib/systemd/systemd-networkd",
];
const NETWORKD_DROP_IN: &str = "/etc/systemd/network/05-arcbox-bridge.network";
const NETWORK_MANAGER_BINARIES: &[&str] = &["/usr/sbin/NetworkManager", "/usr/bin/NetworkManager"];
const NETWORK_MANAGER_DROP_IN: &str = "/etc/NetworkManager/conf.d/90-arcbox-bridge.conf";
struct Layout {
root: PathBuf,
}
impl Layout {
#[cfg(target_os = "linux")]
fn guest() -> Self {
Self {
root: PathBuf::from("/"),
}
}
fn path(&self, absolute: &str) -> PathBuf {
self.root.join(absolute.trim_start_matches('/'))
}
fn set_hostname(&self, name: &str) {
if let Err(e) = fs::write(self.path(KERNEL_HOSTNAME), name) {
tracing::warn!(error = %e, "failed to set the kernel hostname");
}
if let Err(e) = write_file(&self.path(ETC_HOSTNAME), &format!("{name}\n"), 0o644) {
tracing::warn!(error = %e, "failed to write /etc/hostname");
}
let hosts = self.path(ETC_HOSTS);
let current = fs::read_to_string(&hosts).unwrap_or_default();
let wanted = with_hostname_alias(¤t, name);
if wanted != current
&& let Err(e) = write_file(&hosts, &wanted, 0o644)
{
tracing::warn!(error = %e, "failed to add the hostname to /etc/hosts");
}
}
fn claim_bridge_nic(&self, mac: &str) {
if NETWORKD_BINARIES.iter().any(|b| self.path(b).exists())
&& let Err(e) = write_file(
&self.path(NETWORKD_DROP_IN),
&format!("[Match]\nMACAddress={mac}\n\n[Link]\nUnmanaged=yes\n"),
0o644,
)
{
tracing::warn!(error = %e, "failed to write the networkd bridge drop-in");
}
if NETWORK_MANAGER_BINARIES
.iter()
.any(|b| self.path(b).exists())
&& let Err(e) = write_file(
&self.path(NETWORK_MANAGER_DROP_IN),
&format!("[keyfile]\nunmanaged-devices+=mac:{mac}\n"),
0o644,
)
{
tracing::warn!(error = %e, "failed to write the NetworkManager bridge drop-in");
}
}
}
fn with_hostname_alias(hosts: &str, name: &str) -> String {
let alias = format!("{HOSTNAME_ALIAS_ADDRESS}\t{name}");
let mut lines: Vec<&str> = hosts.lines().collect();
match lines
.iter()
.position(|line| line.split_whitespace().next() == Some(HOSTNAME_ALIAS_ADDRESS))
{
Some(i) => lines[i] = &alias,
None => lines.push(&alias),
}
let mut out = lines.join("\n");
out.push('\n');
out
}
fn write_file(path: &Path, body: &str, mode: u32) -> std::io::Result<()> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
let staged = path.with_extension("arcbox-tmp");
let result = (|| {
let mut file = fs::File::create(&staged)?;
file.write_all(body.as_bytes())?;
file.sync_all()?;
fs::set_permissions(&staged, fs::Permissions::from_mode(mode))?;
fs::rename(&staged, path)
})();
if result.is_err() {
let _ = fs::remove_file(&staged);
}
result
}
#[cfg(target_os = "linux")]
pub fn set_hostname(name: &str) {
Layout::guest().set_hostname(name);
}
#[cfg(target_os = "linux")]
pub fn claim_bridge_nic(mac: &str) {
Layout::guest().claim_bridge_nic(mac);
}
#[cfg(test)]
mod tests {
use super::*;
fn image() -> (tempfile::TempDir, Layout) {
let dir = tempfile::tempdir().expect("tempdir");
let layout = Layout {
root: dir.path().to_path_buf(),
};
(dir, layout)
}
fn touch(layout: &Layout, absolute: &str) {
let path = layout.path(absolute);
fs::create_dir_all(path.parent().expect("parent")).expect("mkdir");
fs::write(&path, "").expect("write");
}
fn read(layout: &Layout, absolute: &str) -> String {
fs::read_to_string(layout.path(absolute)).expect(absolute)
}
#[test]
fn the_name_lands_in_every_file_an_init_reads() {
let (_dir, layout) = image();
touch(&layout, KERNEL_HOSTNAME);
fs::write(layout.path(KERNEL_HOSTNAME), "arcbox-vm").expect("write");
touch(&layout, ETC_HOSTNAME);
fs::write(layout.path(ETC_HOSTNAME), "distrobuilder-4b83\n").expect("write");
touch(&layout, ETC_HOSTS);
fs::write(
layout.path(ETC_HOSTS),
"127.0.0.1\tlocalhost\n::1\t\tlocalhost ip6-localhost\n",
)
.expect("write");
layout.set_hostname("dev");
assert_eq!(read(&layout, KERNEL_HOSTNAME), "dev");
assert_eq!(read(&layout, ETC_HOSTNAME), "dev\n");
assert_eq!(
read(&layout, ETC_HOSTS),
"127.0.0.1\tlocalhost\n::1\t\tlocalhost ip6-localhost\n127.0.1.1\tdev\n"
);
}
#[test]
fn a_missing_etc_hostname_is_created() {
let (_dir, layout) = image();
touch(&layout, KERNEL_HOSTNAME);
layout.set_hostname("dev");
assert_eq!(read(&layout, ETC_HOSTNAME), "dev\n");
assert_eq!(read(&layout, ETC_HOSTS), "127.0.1.1\tdev\n");
}
#[test]
fn the_alias_line_is_rewritten_once() {
let hosts = "127.0.0.1 localhost\n127.0.1.1\told-name\n";
let once = with_hostname_alias(hosts, "new-name");
assert_eq!(once, "127.0.0.1 localhost\n127.0.1.1\tnew-name\n");
assert_eq!(with_hostname_alias(&once, "new-name"), once);
}
#[test]
fn drop_ins_go_only_to_the_managers_the_image_ships() {
let (_dir, layout) = image();
touch(&layout, "/usr/lib/systemd/systemd-networkd");
layout.claim_bridge_nic("02:0c:a6:78:f0:a8");
assert_eq!(
read(&layout, NETWORKD_DROP_IN),
"[Match]\nMACAddress=02:0c:a6:78:f0:a8\n\n[Link]\nUnmanaged=yes\n"
);
assert!(!layout.path(NETWORK_MANAGER_DROP_IN).exists());
let (_dir, layout) = image();
touch(&layout, "/usr/sbin/NetworkManager");
layout.claim_bridge_nic("02:0c:a6:78:f0:a8");
assert!(!layout.path(NETWORKD_DROP_IN).exists());
assert_eq!(
read(&layout, NETWORK_MANAGER_DROP_IN),
"[keyfile]\nunmanaged-devices+=mac:02:0c:a6:78:f0:a8\n"
);
}
#[test]
fn an_image_without_a_manager_gets_no_drop_in() {
let (dir, layout) = image();
layout.claim_bridge_nic("02:0c:a6:78:f0:a8");
assert!(
!layout.path("/etc").exists(),
"{:?}",
fs::read_dir(dir.path())
);
}
}