use crate::error::Result;
use crate::models::traits::{InspectorOS, ResultadoAnalisis, VmDriver};
use crate::models::{HerramientasGuest, Opciones, Particion, Programa, SistemaArchivos, VMInfo};
use crate::vms::stream::DiscoVirtual;
use ext4::SuperBlock;
use positioned_io::ReadAt;
use std::io::Read;
pub(crate) struct LinuxInspector;
impl InspectorOS for LinuxInspector {
fn analizar(
&self,
driver: &dyn VmDriver,
particiones: &[Particion],
tamano_chunk: u64,
opciones: &Opciones,
) -> Result<ResultadoAnalisis> {
if !opciones.debe_analizar_sistema() && !opciones.debe_analizar_apps() {
return Ok(ResultadoAnalisis::default());
}
let mut candidatas: Vec<_> = particiones
.iter()
.filter(|p| {
matches!(
p.sistema_archivos,
SistemaArchivos::Ext4 | SistemaArchivos::Ext3 | SistemaArchivos::Ext2
)
})
.collect();
candidatas.sort_by_key(|p| std::cmp::Reverse(p.tamano));
for particion in candidatas {
let mut disco =
DiscoVirtual::new(driver, particion.inicio, particion.tamano, tamano_chunk);
if let Ok(sb) = SuperBlock::new(&mut disco) {
if let Ok(resultado) = analizar_sistema_archivos(&sb, opciones) {
return Ok(resultado);
}
}
}
Ok(ResultadoAnalisis {
vm_info: if opciones.debe_analizar_sistema() {
VMInfo {
os_nombre: "Linux (No se pudo leer rootfs)".to_string(),
..VMInfo::default()
}
} else {
VMInfo::default()
},
programas: Vec::new(),
advertencias: Vec::new(),
})
}
}
fn analizar_sistema_archivos<R: ReadAt>(
sb: &SuperBlock<R>,
opciones: &Opciones,
) -> Result<ResultadoAnalisis> {
let mut vm_info = VMInfo::default();
let mut programas = Vec::new();
if opciones.debe_analizar_sistema() {
if let Ok(contenido) = leer_archivo_texto(sb, "/etc/os-release")
.or_else(|_| leer_archivo_texto(sb, "/usr/lib/os-release"))
{
parsear_os_release(&contenido, &mut vm_info);
}
if let Ok(hostname) = leer_archivo_texto(sb, "/etc/hostname") {
let name = hostname.trim();
if !name.is_empty() {
vm_info.os_edition_version = format!("Host: {}", name);
}
}
}
if let Ok(dpkg_status) = leer_archivo_texto(sb, "/var/lib/dpkg/status") {
let (pkgs, guest_tools) = parsear_dpkg_status(&dpkg_status, opciones);
if opciones.debe_analizar_apps() {
programas.extend(pkgs);
}
if opciones.debe_analizar_sistema() {
if let Some(tools) = guest_tools {
vm_info.guest_tools = Some(tools);
}
}
}
if opciones.debe_analizar_sistema() && vm_info.guest_tools.is_none() {
vm_info.guest_tools = detectar_guest_tools_archivos_linux(sb);
}
if opciones.debe_analizar_apps() {
programas.sort_by(|a, b| a.nombre.cmp(&b.nombre));
programas.dedup_by(|a, b| a.nombre == b.nombre && a.version == b.version);
}
Ok(ResultadoAnalisis {
vm_info,
programas,
advertencias: Vec::new(),
})
}
fn detectar_guest_tools_archivos_linux<R: ReadAt>(sb: &SuperBlock<R>) -> Option<HerramientasGuest> {
let rutas_vmware = [
"/usr/bin/vmtoolsd",
"/usr/sbin/vmtoolsd",
"/bin/vmtoolsd",
"/sbin/vmtoolsd",
"/etc/vmware-tools",
"/lib/systemd/system/open-vm-tools.service",
"/etc/systemd/system/open-vm-tools.service",
];
for ruta in &rutas_vmware {
if sb.resolve_path(ruta).is_ok() {
return Some(HerramientasGuest {
tipo: "VMware Tools".to_string(),
version: None,
presente: true,
});
}
}
let rutas_vbox = [
"/usr/sbin/VBoxService",
"/usr/bin/VBoxService",
"/usr/sbin/vboxservice",
"/usr/bin/vboxservice",
"/sbin/VBoxService",
"/lib/systemd/system/vboxadd-service.service",
"/etc/systemd/system/vboxadd-service.service",
"/opt/VBoxGuestAdditions",
];
for ruta in &rutas_vbox {
if sb.resolve_path(ruta).is_ok() {
return Some(HerramientasGuest {
tipo: "VirtualBox Guest Additions".to_string(),
version: None,
presente: true,
});
}
}
let rutas_qemu = [
"/usr/bin/qemu-ga",
"/usr/sbin/qemu-ga",
"/lib/systemd/system/qemu-guest-agent.service",
"/etc/systemd/system/qemu-guest-agent.service",
];
for ruta in &rutas_qemu {
if sb.resolve_path(ruta).is_ok() {
return Some(HerramientasGuest {
tipo: "QEMU Guest Agent".to_string(),
version: None,
presente: true,
});
}
}
let rutas_hyperv = [
"/usr/sbin/hv_kvp_daemon",
"/usr/bin/hv_kvp_daemon",
"/usr/sbin/hv_vss_daemon",
"/usr/sbin/hv_fcopy_daemon",
"/lib/systemd/system/hv-kvp-daemon.service",
"/lib/systemd/system/hypervkvp.service",
];
for ruta in &rutas_hyperv {
if sb.resolve_path(ruta).is_ok() {
return Some(HerramientasGuest {
tipo: "Hyper-V Integration Services".to_string(),
version: None,
presente: true,
});
}
}
None
}
fn parsear_dpkg_status(
contenido: &str,
opciones: &Opciones,
) -> (Vec<Programa>, Option<HerramientasGuest>) {
let mut paquetes = Vec::new();
let mut guest_tools: Option<HerramientasGuest> = None;
let mut pkg_actual = String::new();
let mut ver_actual = String::new();
let mut seccion_actual = String::new();
let mut instalado = false;
let debe_apps = opciones.debe_analizar_apps();
let procesar_paquete = |pkg: &str,
ver: &str,
sec: &str,
inst: bool,
pkgs: &mut Vec<Programa>,
tools: &mut Option<HerramientasGuest>| {
if inst && !pkg.is_empty() {
if tools.is_none() {
let ver_opt = if !ver.is_empty() {
Some(ver.to_string())
} else {
None
};
if pkg == "open-vm-tools" || pkg == "open-vm-tools-desktop" {
*tools = Some(HerramientasGuest {
tipo: "VMware Tools".to_string(),
version: ver_opt,
presente: true,
});
} else if pkg == "virtualbox-guest-utils"
|| pkg == "virtualbox-guest-x11"
|| pkg == "virtualbox-guest-dkms"
|| pkg == "virtualbox-guest-additions-iso"
{
*tools = Some(HerramientasGuest {
tipo: "VirtualBox Guest Additions".to_string(),
version: ver_opt,
presente: true,
});
} else if pkg == "qemu-guest-agent" {
*tools = Some(HerramientasGuest {
tipo: "QEMU Guest Agent".to_string(),
version: ver_opt,
presente: true,
});
} else if pkg == "hyperv-daemons"
|| pkg == "hv-kvp-daemon-init"
|| pkg == "linux-cloud-tools-virtual"
{
*tools = Some(HerramientasGuest {
tipo: "Hyper-V Integration Services".to_string(),
version: ver_opt,
presente: true,
});
}
}
if debe_apps {
let version_opt = if !ver.is_empty() {
Some(ver.to_string())
} else {
None
};
let editor_opt = if !sec.is_empty() {
Some(sec.to_string())
} else {
None
};
pkgs.push(Programa {
nombre: pkg.to_string(),
version: version_opt,
editor: editor_opt,
origen: None,
});
}
}
};
for linea in contenido.lines() {
if let Some(cancel) = &opciones.cancel_token {
if cancel.load(std::sync::atomic::Ordering::Relaxed) {
break;
}
}
if linea.starts_with("Package: ") {
pkg_actual = linea.trim_start_matches("Package: ").trim().to_string();
} else if linea.starts_with("Status: ") {
instalado = linea.contains("install ok installed");
} else if linea.starts_with("Version: ") {
ver_actual = linea.trim_start_matches("Version: ").trim().to_string();
} else if linea.starts_with("Section: ") {
seccion_actual = linea.trim_start_matches("Section: ").trim().to_string();
} else if linea.trim().is_empty() {
procesar_paquete(
&pkg_actual,
&ver_actual,
&seccion_actual,
instalado,
&mut paquetes,
&mut guest_tools,
);
pkg_actual.clear();
ver_actual.clear();
seccion_actual.clear();
instalado = false;
}
}
procesar_paquete(
&pkg_actual,
&ver_actual,
&seccion_actual,
instalado,
&mut paquetes,
&mut guest_tools,
);
(paquetes, guest_tools)
}
fn leer_archivo_texto<R: ReadAt>(sb: &SuperBlock<R>, ruta: &str) -> Result<String> {
let entry = sb
.resolve_path(ruta)
.map_err(|e| crate::error::VmSpectError::FileSystem(format!("{:?}", e)))?;
let inode = sb
.load_inode(entry.inode)
.map_err(|e| crate::error::VmSpectError::FileSystem(format!("{:?}", e)))?;
let mut reader = sb
.open(&inode)
.map_err(|e| crate::error::VmSpectError::FileSystem(format!("{:?}", e)))?;
let mut buffer = Vec::new();
reader.read_to_end(&mut buffer)?;
Ok(String::from_utf8_lossy(&buffer).to_string())
}
fn parsear_os_release(contenido: &str, info: &mut VMInfo) {
for linea in contenido.lines() {
let linea = linea.trim();
if linea.starts_with('#') || !linea.contains('=') {
continue;
}
let mut partes = linea.splitn(2, '=');
let clave = partes.next().unwrap_or("").trim();
let valor = partes
.next()
.unwrap_or("")
.trim()
.trim_matches('"')
.trim_matches('\'');
match clave {
"PRETTY_NAME" => info.os_nombre = valor.to_string(),
"VERSION_ID" if info.os_build.is_empty() => info.os_build = valor.to_string(),
"NAME" if info.os_nombre.is_empty() => info.os_nombre = valor.to_string(),
_ => {}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parsear_os_release() {
let os_release = r#"
NAME="Ubuntu"
VERSION="22.04.3 LTS (Jammy Jellyfish)"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 22.04.3 LTS"
VERSION_ID="22.04"
"#;
let mut info = VMInfo::default();
parsear_os_release(os_release, &mut info);
assert_eq!(info.os_nombre, "Ubuntu 22.04.3 LTS");
assert_eq!(info.os_build, "22.04");
}
#[test]
fn test_parsear_dpkg_status_agnostico() {
let dpkg = r#"
Package: open-vm-tools
Status: install ok installed
Priority: optional
Section: admin
Installed-Size: 3120
Maintainer: Ubuntu Developers
Architecture: amd64
Version: 2:12.1.5-1ubuntu0.22.04.4
Package: mosquitto
Status: install ok installed
Priority: optional
Section: net
Version: 2.0.11-1ubuntu1.1
Package: libc6
Status: install ok installed
Priority: required
Section: libs
Version: 2.35-0ubuntu3.4
Package: python3-minimal
Status: install ok installed
Priority: optional
Section: python
Version: 3.10.6-1~22.04
"#;
let opciones = Opciones::default();
let (paquetes, tools) = parsear_dpkg_status(dpkg, &opciones);
assert_eq!(
tools,
Some(HerramientasGuest {
tipo: "VMware Tools".to_string(),
version: Some("2:12.1.5-1ubuntu0.22.04.4".to_string()),
presente: true,
})
);
assert_eq!(paquetes.len(), 4);
assert!(paquetes.iter().any(|p| p.nombre == "open-vm-tools"));
assert!(paquetes.iter().any(|p| p.nombre == "mosquitto"));
assert!(paquetes.iter().any(|p| p.nombre == "libc6"));
assert!(paquetes.iter().any(|p| p.nombre == "python3-minimal"));
}
#[test]
fn test_parsear_dpkg_status_noapps() {
let dpkg = r#"
Package: open-vm-tools
Status: install ok installed
Priority: optional
Section: admin
Version: 2:12.1.5-1ubuntu0.22.04.4
Package: mosquitto
Status: install ok installed
Priority: optional
Section: net
Version: 2.0.11-1ubuntu1.1
"#;
let opciones = Opciones {
noapps: true,
..Opciones::default()
};
let (paquetes, tools) = parsear_dpkg_status(dpkg, &opciones);
assert_eq!(
tools,
Some(HerramientasGuest {
tipo: "VMware Tools".to_string(),
version: Some("2:12.1.5-1ubuntu0.22.04.4".to_string()),
presente: true,
})
);
assert!(paquetes.is_empty());
}
#[test]
fn test_parsear_dpkg_status_hipervisores_multiples() {
let dpkg_vbox = r#"
Package: virtualbox-guest-utils
Status: install ok installed
Priority: optional
Section: admin
Version: 7.0.12-dfsg-1
"#;
let (_, tools_vbox) = parsear_dpkg_status(dpkg_vbox, &Opciones::default());
assert_eq!(
tools_vbox,
Some(HerramientasGuest {
tipo: "VirtualBox Guest Additions".to_string(),
version: Some("7.0.12-dfsg-1".to_string()),
presente: true,
})
);
let dpkg_qemu = r#"
Package: qemu-guest-agent
Status: install ok installed
Priority: optional
Section: admin
Version: 1:8.2.2+ds-0ubuntu1
"#;
let (_, tools_qemu) = parsear_dpkg_status(dpkg_qemu, &Opciones::default());
assert_eq!(
tools_qemu,
Some(HerramientasGuest {
tipo: "QEMU Guest Agent".to_string(),
version: Some("1:8.2.2+ds-0ubuntu1".to_string()),
presente: true,
})
);
let dpkg_hyperv = r#"
Package: hyperv-daemons
Status: install ok installed
Priority: optional
Section: admin
Version: 5.15.0-91.101
"#;
let (_, tools_hyperv) = parsear_dpkg_status(dpkg_hyperv, &Opciones::default());
assert_eq!(
tools_hyperv,
Some(HerramientasGuest {
tipo: "Hyper-V Integration Services".to_string(),
version: Some("5.15.0-91.101".to_string()),
presente: true,
})
);
}
}