mod platform;
mod strutils;
mod utils;
pub mod controlvm;
pub mod err;
pub mod guest;
pub mod nics;
pub mod snapshot;
pub mod storage;
pub mod vmid;
use std::collections::HashMap;
use std::path::PathBuf;
use std::process::Command;
use std::thread;
use std::time::{Duration, Instant};
use regex::Regex;
pub use err::Error;
use strutils::{buf_to_strlines, EmptyLine};
pub use vmid::VmId;
#[derive(Copy, Clone)]
pub enum Headless {
Detached,
Blocking
}
#[derive(Copy, Clone)]
pub enum RunContext {
GUI,
Headless(Headless)
}
pub enum TimeoutAction {
Error,
Kill
}
#[derive(Copy, Clone)]
pub enum Shutdown {
PowerOff,
AcpiPowerOff,
SaveState
}
pub fn have_vm(vid: &VmId) -> Result<bool, Error> {
let lst = get_vm_list()?;
for (name, uuid) in lst {
match vid {
VmId::Name(nm) => {
if name == *nm {
return Ok(true);
}
}
VmId::Uuid(u) => {
if uuid == *u {
return Ok(true);
}
}
}
}
Ok(false)
}
pub fn is_vm_running(vid: &VmId) -> Result<bool, Error> {
let lst = get_running_vms_list()?;
for (name, uuid) in lst {
match vid {
VmId::Name(nm) => {
if name == *nm {
return Ok(true);
}
}
VmId::Uuid(u) => {
if uuid == *u {
return Ok(true);
}
}
}
}
Ok(false)
}
pub fn get_vm_list() -> Result<Vec<(String, uuid::Uuid)>, Error> {
let mut cmd = Command::new(platform::get_cmd("VBoxManage"));
cmd.args(&["list", "vms"]);
let output = match cmd.output() {
Ok(out) => out,
Err(_) => {
return Err(Error::FailedToExecute(format!("{:?}", cmd)));
}
};
if !output.status.success() {
return Err(Error::CommandFailed(format!("{:?}", cmd), output));
}
let lines = buf_to_strlines(&output.stdout, EmptyLine::Ignore);
let mut out = Vec::new();
for line in lines {
match line.find('"') {
Some(idx) => {
if idx != 0 {
continue;
}
}
None => continue
}
let idx = match line.rfind('"') {
Some(idx) => {
if idx == 0 {
continue;
}
idx
}
None => continue
};
let idx_ub = match line.rfind('{') {
Some(idx) => {
if idx == 0 {
continue;
}
idx
}
None => continue
};
let idx_ue = match line.rfind('}') {
Some(idx) => {
if idx == 0 {
continue;
}
idx
}
None => continue
};
let name = &line[1..idx];
let uuidstr = &line[(idx_ub + 1)..idx_ue];
let u = match uuid::Uuid::parse_str(uuidstr) {
Ok(u) => u,
Err(_) => continue
};
out.push((name.to_string(), u));
}
Ok(out)
}
pub fn get_running_vms_list() -> Result<Vec<(String, uuid::Uuid)>, Error> {
let mut cmd = Command::new(platform::get_cmd("VBoxManage"));
cmd.args(&["list", "runningvms"]);
let output = match cmd.output() {
Ok(out) => out,
Err(_) => {
return Err(Error::FailedToExecute(format!("{:?}", cmd)));
}
};
if !output.status.success() {
return Err(Error::CommandFailed(format!("{:?}", cmd), output));
}
let lines = buf_to_strlines(&output.stdout, EmptyLine::Ignore);
let mut out = Vec::new();
for line in lines {
match line.find('"') {
Some(idx) => {
if idx != 0 {
continue;
}
}
None => continue
}
let idx = match line.rfind('"') {
Some(idx) => {
if idx == 0 {
continue;
}
idx
}
None => continue
};
let idx_ub = match line.rfind('{') {
Some(idx) => {
if idx == 0 {
continue;
}
idx
}
None => continue
};
let idx_ue = match line.rfind('}') {
Some(idx) => {
if idx == 0 {
continue;
}
idx
}
None => continue
};
let name = &line[1..idx];
let uuidstr = &line[(idx_ub + 1)..idx_ue];
let u = match uuid::Uuid::parse_str(uuidstr) {
Ok(u) => u,
Err(_) => {
return Err(Error::BadFormat(
"Unable to parse output UUID.".to_string()
));
}
};
out.push((name.to_string(), u));
}
Ok(out)
}
pub fn get_pid(vid: &VmId) -> Result<u32, Error> {
let is_running = is_vm_state(vid, VmState::Running)?;
if !is_running {
return Err(Error::invalid_state("VM doesn't exist or isn't running"));
}
let mut cmd = Command::new(platform::get_cmd("VBoxManage"));
cmd.arg("showvminfo");
cmd.arg(vid.to_string());
cmd.arg("--log");
cmd.arg("0");
let output = cmd.output().expect("Failed to execute VBoxManage");
let lines = strutils::buf_to_strlines(&output.stdout, EmptyLine::Ignore);
let mut lines = lines.iter();
let re_pid =
Regex::new(r#"^\d{2}:\d{2}:\d{2}\.\d+ Process ID: (?P<pid>\d+)$"#)
.unwrap();
let mut pid = None;
while let Some(line) = lines.next() {
let line = line.trim_end();
if let Some(cap) = re_pid.captures(&line) {
pid = Some(cap[1].parse::<u32>().unwrap());
break;
}
}
pid.ok_or(Error::missing("Unable to find Process ID."))
}
pub fn try_get_pid(
vid: &VmId,
max_retries: usize,
delay: Duration
) -> Result<u32, Error> {
let mut remain = max_retries;
let pid = loop {
match get_pid(vid) {
Ok(pid) => break pid,
Err(Error::Missing(s)) => {
if remain == 0 {
return Err(Error::Missing(s));
}
remain -= 1;
thread::sleep(delay);
}
Err(e) => {
return Err(e);
}
}
};
Ok(pid)
}
pub fn get_vm_info_map(vid: &VmId) -> Result<HashMap<String, String>, Error> {
let mut cmd = Command::new(platform::get_cmd("VBoxManage"));
cmd.arg("showvminfo");
cmd.arg(vid.to_string());
cmd.arg("--machinereadable");
let output = cmd.output().expect("Failed to execute VBoxManage");
let lines = strutils::buf_to_strlines(&output.stdout, EmptyLine::Ignore);
let mut map = HashMap::new();
let re1 = Regex::new(r#"^(?P<key>[^"=]+)="(?P<val>[^"=]*)"$"#).unwrap();
let re2 = Regex::new(r#"^"(?P<key>[^"=]+)"="(?P<val>[^"=]*)"$"#).unwrap();
let re3 = Regex::new(r#"^(?P<key>[^"=]+)=(?P<val>[^"=]*)$"#).unwrap();
let mut lines = lines.iter();
while let Some(line) = lines.next() {
let line = line.trim_end();
let cap = if let Some(cap) = re1.captures(&line) {
Some(cap)
} else if let Some(cap) = re2.captures(&line) {
Some(cap)
} else if let Some(cap) = re3.captures(&line) {
Some(cap)
} else {
dbg!(format!("Ignored line: {}", line));
None
};
if let Some(cap) = cap {
map.insert(cap[1].to_string(), cap[2].to_string());
}
}
Ok(map)
}
#[derive(PartialEq, Eq, Copy, Clone)]
pub enum VmState {
Unknown,
PowerOff,
Starting,
Running,
Paused,
Stopping
}
impl From<&str> for VmState {
fn from(s: &str) -> Self {
match s {
"poweroff" => VmState::PowerOff,
"starting" => VmState::Starting,
"running" => VmState::Running,
"paused" => VmState::Paused,
"stopping" => VmState::Stopping,
_ => VmState::Unknown
}
}
}
impl From<&String> for VmState {
fn from(s: &String) -> Self {
match s.as_ref() {
"poweroff" => VmState::PowerOff,
"starting" => VmState::Starting,
"running" => VmState::Running,
"paused" => VmState::Paused,
"stopping" => VmState::Stopping,
_ => VmState::Unknown
}
}
}
pub struct VmInfo {
pub shares_map: HashMap<String, PathBuf>,
pub shares_list: Vec<(String, PathBuf)>,
pub state: VmState,
pub snapshots: Option<snapshot::Snapshots>,
pub nics: Vec<nics::NICInfo>
}
pub fn get_vm_info(vid: &VmId) -> Result<VmInfo, Error> {
let map = get_vm_info_map(vid)?;
let mut shares_list = Vec::new();
let mut shares_map = HashMap::new();
let mut idx = 1;
loop {
let name_key = format!("SharedFolderNameMachineMapping{}", idx);
let path_key = format!("SharedFolderPathMachineMapping{}", idx);
let name = match map.get(&name_key) {
Some(nm) => nm.clone(),
None => break
};
let pathname = match map.get(&path_key) {
Some(pn) => PathBuf::from(pn),
None => break
};
shares_map.insert(name.clone(), pathname.clone());
shares_list.push((name, pathname));
idx += 1;
}
let state = match map.get("VMState") {
Some(s) => VmState::from(s),
None => VmState::Unknown
};
let snaps = snapshot::get_from_map(&map)?;
let nics = nics::get_from_map(&map)?;
Ok(VmInfo {
state,
shares_map,
shares_list,
snapshots: snaps,
nics
})
}
pub fn is_vm_state(vid: &VmId, state: VmState) -> Result<bool, Error> {
let vmi = get_vm_info(vid)?;
Ok(vmi.state == state)
}
pub fn wait_for_state(
vid: &VmId,
state: VmState,
max_rechecks: usize,
recheck_delay: Duration
) -> Result<(), Error> {
let mut remain = max_rechecks;
loop {
let is_expected_state = is_vm_state(vid, state)?;
if is_expected_state {
break;
}
if remain == 0 {
return Err(Error::Timeout);
}
remain -= 1;
thread::sleep(recheck_delay);
}
Ok(())
}
pub fn wait_for_croak(
vid: &VmId,
timeout: Option<(Duration, TimeoutAction)>
) -> Result<(), Error> {
let start = Instant::now();
loop {
let poweroff = is_vm_state(vid, VmState::PowerOff)?;
if poweroff {
break;
}
if let Some((ref max_dur, ref action)) = timeout {
let duration = start.elapsed();
if duration > *max_dur {
match action {
TimeoutAction::Error => return Err(Error::Timeout),
TimeoutAction::Kill => {
controlvm::kill(vid)?;
break;
}
}
}
}
let eleven_secs = Duration::from_secs(11);
thread::sleep(eleven_secs);
}
Ok(())
}