Private
Public Access
1
0

fix: FQDN resolution and display_name blank bug; fix: Arch/Alpine/RPM packages

Bug fixes:
- get_fqdn() now prioritizes 'hostname -f' (returns full FQDN) over /etc/hostname (returns short hostname)
- Added get_hostname() for short hostname extraction
- Added hostname field to EnrollmentRequest for manager display_name population
- Updated SPEC.md and API_DOCUMENTATION.md

Package fixes:
- Arch: Added linux-patch-api.install with post_install/upgrade/remove hooks, user creation, directory creation, config handling
- Alpine: Added linux-patch-api.apk-install with pre/post install/deinstall hooks, user creation, directory creation, config handling, missing config.yaml.example
- RPM: Dynamic version from Cargo.toml, %ghost %config(noreplace) for live configs, tarball exclusions, /var/log in %files
This commit is contained in:
2026-05-18 23:51:00 +00:00
parent 13da27364b
commit b6809dc935
14 changed files with 540 additions and 38 deletions

View File

@ -31,36 +31,109 @@ pub fn get_machine_id() -> Result<String> {
}
/// Resolve the fully-qualified domain name.
/// Strategy: `gethostname` via std → fallback to `hostname` CLI → "localhost".
///
/// Strategy (in priority order):
/// 1. `hostname -f` → if result contains `.`, it's a real FQDN
/// 2. `hostname` + `hostname -d` → combine short hostname + domain
/// 3. `/etc/hostname` → short hostname fallback
/// 4. `hostname` command → last resort
/// 5. `"localhost"` → final fallback
pub fn get_fqdn() -> Result<String> {
// Try reading from hostname file first (common on systemd systems)
// 1. Try `hostname -f` — returns FQDN on properly configured systems
if let Ok(output) = Command::new("hostname").arg("-f").output() {
if output.status.success() {
let name = String::from_utf8_lossy(&output.stdout).trim().to_string();
if !name.is_empty() && name.contains('.') && name != "(none)" {
tracing::debug!(fqdn = %name, "Resolved FQDN via hostname -f");
return Ok(name);
}
}
}
// 2. Try combining short hostname + domain from `hostname -d`
if let Ok(short_output) = Command::new("hostname").output() {
if short_output.status.success() {
let short = String::from_utf8_lossy(&short_output.stdout).trim().to_string();
if !short.is_empty() && short != "(none)" {
if let Ok(domain_output) = Command::new("hostname").arg("-d").output() {
if domain_output.status.success() {
let domain = String::from_utf8_lossy(&domain_output.stdout).trim().to_string();
if !domain.is_empty() {
let fqdn = format!("{}.{}", short, domain);
tracing::debug!(fqdn = %fqdn, "Resolved FQDN via hostname + hostname -d");
return Ok(fqdn);
}
}
}
// Domain not available — fall through to try other methods
}
}
}
// 3. Try reading from /etc/hostname (common on systemd systems, usually short hostname)
if let Ok(name) = fs::read_to_string("/etc/hostname") {
let trimmed = name.trim().to_string();
if !trimmed.is_empty() && trimmed != "(none)" {
tracing::debug!(hostname = %trimmed, "Resolved hostname via /etc/hostname (may be short)");
return Ok(trimmed);
}
}
// Fallback to hostname command
if let Ok(output) = Command::new("hostname").arg("-f").output() {
if output.status.success() {
let name = String::from_utf8_lossy(&output.stdout).trim().to_string();
if !name.is_empty() {
return Ok(name);
}
}
}
// Fallback to plain hostname
// 4. Fallback to plain hostname command
if let Ok(output) = Command::new("hostname").output() {
if output.status.success() {
let name = String::from_utf8_lossy(&output.stdout).trim().to_string();
if !name.is_empty() {
tracing::debug!(hostname = %name, "Resolved hostname via hostname command");
return Ok(name);
}
}
}
// 5. Final fallback
tracing::warn!("Could not determine hostname — falling back to localhost");
Ok("localhost".into())
}
/// Resolve the short hostname (without domain).
///
/// Strategy: `/etc/hostname` → `hostname` command → split FQDN on `.` → `"localhost"`.
pub fn get_hostname() -> Result<String> {
// Try reading from /etc/hostname (usually contains the short hostname)
if let Ok(name) = fs::read_to_string("/etc/hostname") {
let trimmed = name.trim().to_string();
if !trimmed.is_empty() && trimmed != "(none)" {
// If it contains a dot, take just the first component
let short = trimmed.split('.').next().unwrap_or(&trimmed).to_string();
tracing::debug!(hostname = %short, "Resolved short hostname via /etc/hostname");
return Ok(short);
}
}
// Try hostname command
if let Ok(output) = Command::new("hostname").output() {
if output.status.success() {
let name = String::from_utf8_lossy(&output.stdout).trim().to_string();
if !name.is_empty() {
// If it contains a dot, take just the first component
let short = name.split('.').next().unwrap_or(&name).to_string();
tracing::debug!(hostname = %short, "Resolved short hostname via hostname command");
return Ok(short);
}
}
}
// Try splitting FQDN from get_fqdn()
if let Ok(fqdn) = get_fqdn() {
if fqdn != "localhost" && fqdn.contains('.') {
let short = fqdn.split('.').next().unwrap_or(&fqdn).to_string();
tracing::debug!(hostname = %short, "Resolved short hostname by splitting FQDN");
return Ok(short);
}
}
// Final fallback
tracing::warn!("Could not determine short hostname — falling back to localhost");
Ok("localhost".into())
}
@ -366,6 +439,56 @@ mod tests {
assert!(!fqdn.is_empty(), "FQDN should not be empty");
}
#[test]
fn fqdn_prefers_full_domain() {
// If hostname -f returns a value with a dot, get_fqdn should return it
// (not the short hostname from /etc/hostname)
let fqdn = get_fqdn().expect("Failed to get FQDN");
// On properly configured systems, FQDN should contain at least one dot
// If it doesn't, it's likely a short hostname from /etc/hostname
if fqdn.contains('.') {
// FQDN contains domain — good
assert!(
fqdn.split('.').count() >= 2,
"FQDN should have at least host.domain format, got: {}",
fqdn
);
}
// If no dot, it's a short hostname — acceptable fallback but not ideal
}
#[test]
fn hostname_is_not_empty() {
let hostname = get_hostname().expect("Failed to get hostname");
assert!(!hostname.is_empty(), "Hostname should not be empty");
}
#[test]
fn hostname_is_short_form() {
let hostname = get_hostname().expect("Failed to get hostname");
// Short hostname should NOT contain dots
assert!(
!hostname.contains('.'),
"Short hostname should not contain dots, got: {}",
hostname
);
}
#[test]
fn hostname_is_prefix_of_fqdn() {
let hostname = get_hostname().expect("Failed to get hostname");
let fqdn = get_fqdn().expect("Failed to get FQDN");
// If FQDN contains a dot, hostname should be the first component
if fqdn.contains('.') {
let fqdn_prefix = fqdn.split('.').next().unwrap_or(&fqdn);
assert_eq!(
hostname, fqdn_prefix,
"Short hostname '{}' should match FQDN prefix '{}'",
hostname, fqdn_prefix
);
}
}
#[test]
fn os_details_contains_kernel() {
let details = get_os_details().expect("Failed to get OS details");