use std::net::{IpAddr, Ipv4Addr};
use std::path::Path;
use std::time::Duration;
use anyhow::{Result, bail};
use tracing::{debug, warn};
use crate::host_mount;
const MOUNT_TIMEOUT: Duration = Duration::from_secs(30);
const MOUNT_RETRY_INTERVAL: Duration = Duration::from_millis(500);
pub(super) async fn mount(address: &str, port: u16, mount_path: &Path) -> Result<()> {
mount_with_retry(
&render_mount_opts(port),
&format!("{address}:/"),
mount_path,
)
.await
}
pub(super) async fn release(path: &Path) -> Result<()> {
match host_mount::unmount(path).await {
Ok(()) => Ok(()),
Err(e) => {
debug!(path = %path.display(), error = %e, "umount failed; forcing");
host_mount::unmount_force(path).await
}
}
}
async fn mount_with_retry(opts: &str, source: &str, mount_path: &Path) -> Result<()> {
let deadline = tokio::time::Instant::now() + MOUNT_TIMEOUT;
loop {
match host_mount::mount_nfs(opts, source, mount_path).await {
Ok(()) => return Ok(()),
Err(e) if tokio::time::Instant::now() >= deadline => {
bail!("mount_nfs did not succeed within {MOUNT_TIMEOUT:?}: {e}");
}
Err(e) => debug!(error = %e, "mount_nfs attempt failed, retrying"),
}
tokio::time::sleep(MOUNT_RETRY_INTERVAL).await;
}
}
fn render_mount_opts(port: u16) -> String {
format!(
"rw,vers=3,tcp,port={port},mountport={port},locallocks,nfc,rdirplus,actimeo=10,deadtimeout=60,retrycnt=0"
)
}
pub(super) fn host_addresses_on_link(bridge: Ipv4Addr) -> Vec<IpAddr> {
let interfaces = match nix::ifaddrs::getifaddrs() {
Ok(interfaces) => interfaces,
Err(e) => {
warn!(error = %e, "getifaddrs failed; the machine export has no peer to admit");
return Vec::new();
}
};
interfaces
.filter_map(|interface| {
let address = interface.address?.as_sockaddr_in()?.ip();
let netmask = interface.netmask?.as_sockaddr_in()?.ip();
same_link(address, bridge, netmask).then_some(IpAddr::V4(address))
})
.collect()
}
fn same_link(a: Ipv4Addr, b: Ipv4Addr, netmask: Ipv4Addr) -> bool {
let mask = u32::from(netmask);
u32::from(a) & mask == u32::from(b) & mask
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn mount_opts_are_read_write_v3_on_the_export_port() {
let opts = render_mount_opts(51234);
assert!(opts.starts_with("rw,"));
assert!(opts.contains("vers=3"));
assert!(opts.contains("port=51234,mountport=51234"));
assert!(opts.contains("locallocks"));
assert!(!opts.contains("nolocks"));
assert!(!opts.contains("noowners"));
}
#[test]
fn a_link_is_shared_under_the_interface_netmask() {
let mask: Ipv4Addr = "255.255.255.0".parse().unwrap();
let host: Ipv4Addr = "192.168.64.1".parse().unwrap();
assert!(same_link(host, "192.168.64.7".parse().unwrap(), mask));
assert!(!same_link(host, "192.168.65.7".parse().unwrap(), mask));
assert!(!same_link(host, "10.0.2.15".parse().unwrap(), mask));
}
#[test]
fn loopback_is_never_on_a_machines_link() {
let peers = host_addresses_on_link("192.168.64.7".parse().unwrap());
assert!(!peers.contains(&IpAddr::V4(Ipv4Addr::LOCALHOST)));
}
}