use std::{
io,
net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
time::Duration,
};
use serde::Serialize;
use sysinfo::{CpuRefreshKind, Disks, MemoryRefreshKind, RefreshKind, System};
use tokio::net::TcpStream;
use tracing::debug;
use bestool_tamanu::server_info::{detect_node_version, detect_virtualisation};
const PROBE_TIMEOUT: Duration = Duration::from_secs(3);
const IPV4_PROBE_ADDR: SocketAddr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(1, 1, 1, 1)), 443);
const IPV6_PROBE_ADDR: SocketAddr = SocketAddr::new(
IpAddr::V6(Ipv6Addr::new(0x2606, 0x4700, 0x4700, 0, 0, 0, 0, 0x1111)),
443,
);
const NAT64_PROBE_HOST: &str = "ipv4only.arpa";
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Filesystem {
pub mountpoint: String,
pub fs_type: String,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ServerInfo {
pub bestool_version: String,
pub tamanu_version: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub node_version: Option<String>,
pub hostname: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub canonical_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub current_sync_tick: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub timezone: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub os_timezone: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pg_version: Option<String>,
pub uptime_secs: u64,
pub cpu_cores: usize,
pub total_memory_bytes: u64,
pub os_kind: &'static str,
#[serde(skip_serializing_if = "Option::is_none")]
pub os_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub os_version: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub kernel: Option<String>,
pub arch: String,
pub virtualised: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub virtualisation: Option<String>,
pub filesystems: Vec<Filesystem>,
pub ipv4: bool,
pub ipv6: bool,
pub nat64: bool,
}
#[derive(Debug, Clone, Default)]
pub struct ServerFacts {
pub canonical_url: Option<String>,
pub current_sync_tick: Option<String>,
pub timezone: Option<String>,
pub pg_version: Option<String>,
}
pub async fn gather(bestool_version: &str, tamanu_version: &str, facts: ServerFacts) -> ServerInfo {
let disks = Disks::new_with_refreshed_list();
let filesystems = disks
.iter()
.map(|d| Filesystem {
mountpoint: d.mount_point().to_string_lossy().to_string(),
fs_type: d.file_system().to_string_lossy().to_string(),
})
.collect();
let virt = detect_virtualisation().await;
let virtualised = !matches!(virt.as_deref(), None | Some("none"));
let (ipv4, ipv6, nat64) = futures::join!(probe_ipv4(), probe_ipv6(), probe_nat64());
let os_timezone = jiff::tz::TimeZone::system()
.iana_name()
.map(|s| s.to_string());
let sys = System::new_with_specifics(
RefreshKind::nothing()
.with_cpu(CpuRefreshKind::nothing())
.with_memory(MemoryRefreshKind::nothing().with_ram()),
);
let cpu_cores = sys.cpus().len();
let total_memory_bytes = sys.total_memory();
ServerInfo {
bestool_version: bestool_version.to_string(),
tamanu_version: tamanu_version.to_string(),
node_version: detect_node_version().await,
hostname: System::host_name(),
canonical_url: facts.canonical_url,
current_sync_tick: facts.current_sync_tick,
timezone: facts.timezone,
os_timezone,
pg_version: facts.pg_version,
uptime_secs: System::uptime(),
cpu_cores,
total_memory_bytes,
os_kind: if cfg!(target_os = "linux") {
"linux"
} else if cfg!(target_os = "windows") {
"windows"
} else if cfg!(target_os = "macos") {
"macos"
} else {
"other"
},
os_name: System::name(),
os_version: System::os_version().or_else(System::long_os_version),
kernel: System::kernel_version(),
arch: std::env::consts::ARCH.to_string(),
virtualised,
virtualisation: virt,
filesystems,
ipv4,
ipv6,
nat64,
}
}
async fn probe_tcp(addr: SocketAddr) -> bool {
match tokio::time::timeout(PROBE_TIMEOUT, TcpStream::connect(addr)).await {
Ok(Ok(_)) => true,
Ok(Err(err)) => {
debug!(?addr, %err, "tcp probe failed");
false
}
Err(_) => {
debug!(?addr, "tcp probe timed out");
false
}
}
}
async fn probe_ipv4() -> bool {
probe_tcp(IPV4_PROBE_ADDR).await
}
async fn probe_ipv6() -> bool {
probe_tcp(IPV6_PROBE_ADDR).await
}
async fn probe_nat64() -> bool {
let result = tokio::time::timeout(PROBE_TIMEOUT, resolve_aaaa(NAT64_PROBE_HOST)).await;
match result {
Ok(Ok(present)) => present,
Ok(Err(err)) => {
debug!(%err, "nat64 probe failed");
false
}
Err(_) => {
debug!("nat64 probe timed out");
false
}
}
}
async fn resolve_aaaa(host: &str) -> io::Result<bool> {
use hickory_resolver::TokioResolver;
let resolver = TokioResolver::builder_tokio()
.map_err(io::Error::other)?
.build()
.map_err(io::Error::other)?;
let response = resolver.ipv6_lookup(host).await.map_err(io::Error::other)?;
Ok(!response.answers().is_empty())
}