use std::ffi::{CStr, CString};
use std::os::raw::{c_char, c_int, c_uint, c_void};
use std::time::{Duration, Instant};
use log::{debug, info};
use crate::error::{Error, Result};
use super::VmBackend;
const VIR_DOMAIN_AFFECT_LIVE: c_uint = 1;
type VirConnectPtr = *mut c_void;
type VirDomainPtr = *mut c_void;
type ConnectOpenFn = unsafe extern "C" fn(*const c_char) -> VirConnectPtr;
type ConnectCloseFn = unsafe extern "C" fn(VirConnectPtr) -> c_int;
type DomainLookupByNameFn = unsafe extern "C" fn(VirConnectPtr, *const c_char) -> VirDomainPtr;
type DomainFreeFn = unsafe extern "C" fn(VirDomainPtr) -> c_int;
type DomainIsActiveFn = unsafe extern "C" fn(VirDomainPtr) -> c_int;
type DomainDeviceFlagsFn = unsafe extern "C" fn(VirDomainPtr, *const c_char, c_uint) -> c_int;
type LastErrorMessageFn = unsafe extern "C" fn() -> *const c_char;
type InitializeFn = unsafe extern "C" fn() -> c_int;
type SetErrorFuncFn =
unsafe extern "C" fn(*mut c_void, extern "C" fn(*mut c_void, *mut c_void)) -> c_int;
extern "C" fn ignore_libvirt_error(_user_data: *mut c_void, _error: *mut c_void) {}
struct Api {
connect_open: ConnectOpenFn,
connect_close: ConnectCloseFn,
domain_lookup_by_name: DomainLookupByNameFn,
domain_free: DomainFreeFn,
domain_is_active: DomainIsActiveFn,
domain_attach_device_flags: DomainDeviceFlagsFn,
domain_detach_device_flags: DomainDeviceFlagsFn,
last_error_message: LastErrorMessageFn,
initialize: InitializeFn,
set_error_func: SetErrorFuncFn,
}
#[cfg(not(feature = "link-virt"))]
mod loading {
use super::Api;
use crate::error::{Error, Result};
use std::ffi::{CStr, CString};
use std::os::raw::{c_char, c_int, c_void};
const RTLD_NOW: c_int = 2;
unsafe extern "C" {
fn dlopen(filename: *const c_char, flags: c_int) -> *mut c_void;
fn dlsym(handle: *mut c_void, symbol: *const c_char) -> *mut c_void;
fn dlerror() -> *mut c_char;
}
impl Api {
pub fn load() -> Result<Self> {
unsafe {
let mut handle = std::ptr::null_mut();
for soname in ["libvirt.so.0", "libvirt.so"] {
let name = CString::new(soname).unwrap();
handle = dlopen(name.as_ptr(), RTLD_NOW);
if !handle.is_null() {
break;
}
}
if handle.is_null() {
return Err(Error::libvirt(format!(
"could not load libvirt ({}); install libvirt so that libvirt.so.0 \
is available, or build with --features link-virt",
last_dlerror()
)));
}
Ok(Api {
connect_open: resolve(handle, "virConnectOpen")?,
connect_close: resolve(handle, "virConnectClose")?,
domain_lookup_by_name: resolve(handle, "virDomainLookupByName")?,
domain_free: resolve(handle, "virDomainFree")?,
domain_is_active: resolve(handle, "virDomainIsActive")?,
domain_attach_device_flags: resolve(handle, "virDomainAttachDeviceFlags")?,
domain_detach_device_flags: resolve(handle, "virDomainDetachDeviceFlags")?,
last_error_message: resolve(handle, "virGetLastErrorMessage")?,
initialize: resolve(handle, "virInitialize")?,
set_error_func: resolve(handle, "virSetErrorFunc")?,
})
}
}
}
unsafe fn resolve<T: Copy>(handle: *mut c_void, symbol: &str) -> Result<T> {
let name = CString::new(symbol).unwrap();
let ptr = unsafe { dlsym(handle, name.as_ptr()) };
if ptr.is_null() {
return Err(Error::libvirt(format!(
"libvirt is missing the {symbol} symbol"
)));
}
Ok(unsafe { std::mem::transmute_copy::<*mut c_void, T>(&ptr) })
}
fn last_dlerror() -> String {
unsafe {
let p = dlerror();
if p.is_null() {
"unknown error".to_string()
} else {
CStr::from_ptr(p).to_string_lossy().into_owned()
}
}
}
}
#[cfg(feature = "link-virt")]
mod loading {
use super::Api;
use crate::error::Result;
use std::os::raw::{c_char, c_uint, c_void};
#[link(name = "virt")]
unsafe extern "C" {
fn virConnectOpen(name: *const c_char) -> *mut c_void;
fn virConnectClose(conn: *mut c_void) -> std::os::raw::c_int;
fn virDomainLookupByName(conn: *mut c_void, name: *const c_char) -> *mut c_void;
fn virDomainFree(domain: *mut c_void) -> std::os::raw::c_int;
fn virDomainIsActive(domain: *mut c_void) -> std::os::raw::c_int;
fn virDomainAttachDeviceFlags(
domain: *mut c_void,
xml: *const c_char,
flags: c_uint,
) -> std::os::raw::c_int;
fn virDomainDetachDeviceFlags(
domain: *mut c_void,
xml: *const c_char,
flags: c_uint,
) -> std::os::raw::c_int;
fn virGetLastErrorMessage() -> *const c_char;
fn virInitialize() -> std::os::raw::c_int;
fn virSetErrorFunc(
user_data: *mut c_void,
handler: extern "C" fn(*mut c_void, *mut c_void),
) -> std::os::raw::c_int;
}
impl Api {
pub fn load() -> Result<Self> {
Ok(Api {
connect_open: virConnectOpen,
connect_close: virConnectClose,
domain_lookup_by_name: virDomainLookupByName,
domain_free: virDomainFree,
domain_is_active: virDomainIsActive,
domain_attach_device_flags: virDomainAttachDeviceFlags,
domain_detach_device_flags: virDomainDetachDeviceFlags,
last_error_message: virGetLastErrorMessage,
initialize: virInitialize,
set_error_func: virSetErrorFunc,
})
}
}
}
pub struct LibvirtBackend {
api: Api,
uri: String,
domain_name: String,
conn: VirConnectPtr,
domain: VirDomainPtr,
next_retry: Instant,
retry_delay: Duration,
}
impl LibvirtBackend {
pub fn new(uri: impl Into<String>, domain: impl Into<String>) -> Result<Self> {
let api = Api::load()?;
unsafe {
(api.initialize)();
(api.set_error_func)(std::ptr::null_mut(), ignore_libvirt_error);
}
Ok(LibvirtBackend {
api,
uri: uri.into(),
domain_name: domain.into(),
conn: std::ptr::null_mut(),
domain: std::ptr::null_mut(),
next_retry: Instant::now(),
retry_delay: Duration::from_secs(1),
})
}
fn last_error(&self) -> String {
let p = unsafe { (self.api.last_error_message)() };
if p.is_null() {
"unknown libvirt error".to_string()
} else {
unsafe { CStr::from_ptr(p) }.to_string_lossy().into_owned()
}
}
fn backoff(&mut self) {
self.next_retry = Instant::now() + self.retry_delay;
self.retry_delay = (self.retry_delay * 2).min(Duration::from_secs(30));
}
fn invalidate_domain(&mut self) {
if !self.domain.is_null() {
unsafe { (self.api.domain_free)(self.domain) };
self.domain = std::ptr::null_mut();
}
}
fn ensure(&mut self) -> Result<()> {
if !self.conn.is_null() && !self.domain.is_null() {
return Ok(());
}
if Instant::now() < self.next_retry {
return Err(Error::libvirt("reconnect backoff in effect"));
}
if self.conn.is_null() {
let uri = CString::new(self.uri.as_str())?;
let conn = unsafe { (self.api.connect_open)(uri.as_ptr()) };
if conn.is_null() {
let msg = self.last_error();
self.backoff();
return Err(Error::libvirt(format!(
"cannot connect to {:?}: {msg}",
self.uri
)));
}
self.conn = conn;
self.retry_delay = Duration::from_secs(1);
self.next_retry = Instant::now();
debug!("connected to libvirt at {}", self.uri);
}
if self.domain.is_null() {
let name = CString::new(self.domain_name.as_str())?;
let domain = unsafe { (self.api.domain_lookup_by_name)(self.conn, name.as_ptr()) };
if domain.is_null() {
let msg = self.last_error();
self.backoff();
return Err(Error::libvirt(format!(
"cannot find domain {:?}: {msg}",
self.domain_name
)));
}
self.domain = domain;
self.next_retry = Instant::now();
}
Ok(())
}
}
impl VmBackend for LibvirtBackend {
fn is_active(&mut self) -> Result<bool> {
self.ensure()?;
let r = unsafe { (self.api.domain_is_active)(self.domain) };
if r < 0 {
let msg = self.last_error();
self.invalidate_domain();
return Err(Error::libvirt(msg));
}
Ok(r == 1)
}
fn attach(&mut self, xml: &str) -> Result<()> {
self.ensure()?;
let xml = CString::new(xml)?;
let r = unsafe {
(self.api.domain_attach_device_flags)(self.domain, xml.as_ptr(), VIR_DOMAIN_AFFECT_LIVE)
};
if r < 0 {
return Err(Error::libvirt(self.last_error()));
}
Ok(())
}
fn detach(&mut self, xml: &str) -> Result<()> {
self.ensure()?;
let xml = CString::new(xml)?;
let r = unsafe {
(self.api.domain_detach_device_flags)(self.domain, xml.as_ptr(), VIR_DOMAIN_AFFECT_LIVE)
};
if r < 0 {
return Err(Error::libvirt(self.last_error()));
}
Ok(())
}
}
impl Drop for LibvirtBackend {
fn drop(&mut self) {
unsafe {
if !self.domain.is_null() {
(self.api.domain_free)(self.domain);
}
if !self.conn.is_null() {
(self.api.connect_close)(self.conn);
}
}
info!("disconnected from libvirt");
}
}
#[cfg(test)]
mod tests {
use super::Api;
#[test]
fn libvirt_library_loads_when_present() {
let candidates = [
"/usr/lib/x86_64-linux-gnu/libvirt.so.0",
"/usr/lib/aarch64-linux-gnu/libvirt.so.0",
"/usr/lib64/libvirt.so.0",
"/usr/lib/libvirt.so.0",
];
let installed = candidates.iter().any(|p| std::path::Path::new(p).exists());
match Api::load() {
Ok(_) => {}
Err(e) => {
if installed || cfg!(feature = "link-virt") {
panic!("libvirt is installed but loading symbols failed: {e}");
}
eprintln!("skipping: libvirt not installed ({e})");
}
}
}
}