diff --git a/src/config.rs b/src/config.rs index 49cf250b..19bd36d1 100644 --- a/src/config.rs +++ b/src/config.rs @@ -76,6 +76,7 @@ pub enum Step { Tldr, Wsl, Tmux, + Vagrant, } #[derive(Deserialize, Default, Debug)] @@ -86,6 +87,12 @@ pub struct Git { pull_predefined: Option, } +#[derive(Deserialize, Default, Debug)] +pub struct Vagrant { + directories: Option>, + power_on: Option, +} + #[derive(Deserialize, Default, Debug)] pub struct Windows { accept_all_updates: Option, @@ -136,6 +143,7 @@ pub struct ConfigFile { linux: Option, git: Option, windows: Option, + vagrant: Option, } impl ConfigFile { @@ -487,6 +495,19 @@ impl Config { self.config_file.git.as_ref().and_then(|git| git.max_concurrency) } + /// Should we power on vagrant boxes if needed + pub fn vagrant_power_on(&self) -> Option { + self.config_file.vagrant.as_ref().and_then(|vagrant| vagrant.power_on) + } + + /// Vagrant directories + pub fn vagrant_directories(&self) -> Option<&Vec> { + self.config_file + .vagrant + .as_ref() + .and_then(|vagrant| vagrant.directories.as_ref()) + } + /// Extra yay arguments #[allow(dead_code)] pub fn enable_tlmgr_linux(&self) -> bool { diff --git a/src/main.rs b/src/main.rs index dff68d91..31810136 100644 --- a/src/main.rs +++ b/src/main.rs @@ -388,6 +388,10 @@ fn run() -> Result<()> { } } + if config.should_run(Step::Vagrant) { + runner.execute("Vagrant", || vagrant::topgrade_vagrant_boxes(&ctx))?; + } + if !runner.report().data().is_empty() { print_separator("Summary"); diff --git a/src/steps/mod.rs b/src/steps/mod.rs index 09410d66..7757c070 100644 --- a/src/steps/mod.rs +++ b/src/steps/mod.rs @@ -6,6 +6,7 @@ pub mod os; pub mod powershell; #[cfg(unix)] pub mod tmux; +pub mod vagrant; pub mod vim; #[cfg(unix)] pub mod zsh; diff --git a/src/steps/vagrant.rs b/src/steps/vagrant.rs new file mode 100644 index 00000000..dcd2e534 --- /dev/null +++ b/src/steps/vagrant.rs @@ -0,0 +1,151 @@ +use crate::execution_context::ExecutionContext; +use crate::executor::CommandExt; +use crate::terminal::print_separator; +use crate::utils; +use anyhow::Result; +use log::debug; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::{fmt::Display, str::FromStr}; +use strum::EnumString; + +#[derive(Debug, Copy, Clone, EnumString)] +#[strum(serialize_all = "lowercase")] +enum BoxStatus { + PowerOff, + Running, +} + +impl BoxStatus { + fn powered_on(self) -> bool { + match self { + BoxStatus::PowerOff => false, + BoxStatus::Running => true, + } + } +} + +#[derive(Debug)] +struct VagrantBox<'a> { + path: &'a str, + name: String, +} + +impl<'a> Display for VagrantBox<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{} @ {}", self.name, self.path) + } +} + +struct Vagrant { + path: PathBuf, +} + +impl Vagrant { + fn get_boxes<'a>(&self, directory: &'a str) -> Result, BoxStatus)>> { + let output = Command::new(&self.path) + .arg("status") + .current_dir(directory) + .check_output()?; + debug!("Vagrant output in {}: {}", directory, output); + + let boxes = output + .split('\n') + .skip(2) + .take_while(|line| !(line.is_empty() || line.starts_with('\r'))) + .map(|line| { + debug!("Vagrant line: {:?}", line); + let mut elements = line.split_whitespace(); + let vagrant_box = VagrantBox { + name: elements.next().unwrap().to_string(), + path: directory, + }; + let box_status = BoxStatus::from_str(elements.next().unwrap()).unwrap(); + debug!("{:?}: {:?}", vagrant_box, box_status); + (vagrant_box, box_status) + }) + .collect(); + + Ok(boxes) + } + + fn temporary_power_on<'a>( + &'a self, + vagrant_box: &'a VagrantBox, + ctx: &'a ExecutionContext, + ) -> Result> { + TemporaryPowerOn::create(&self.path, vagrant_box, ctx) + } +} + +struct TemporaryPowerOn<'a> { + vagrant: &'a Path, + vagrant_box: &'a VagrantBox<'a>, + ctx: &'a ExecutionContext<'a>, +} + +impl<'a> TemporaryPowerOn<'a> { + fn create(vagrant: &'a Path, vagrant_box: &'a VagrantBox<'a>, ctx: &'a ExecutionContext<'a>) -> Result { + println!("Powering on {}", vagrant_box); + ctx.run_type() + .execute(vagrant) + .args(&["up", &vagrant_box.name]) + .current_dir(vagrant_box.path) + .check_run()?; + Ok(TemporaryPowerOn { + vagrant, + vagrant_box, + ctx, + }) + } +} + +impl<'a> Drop for TemporaryPowerOn<'a> { + fn drop(&mut self) { + println!("Powering off {}", self.vagrant_box); + self.ctx + .run_type() + .execute(self.vagrant) + .args(&["halt", &self.vagrant_box.name]) + .current_dir(self.vagrant_box.path) + .check_run() + .ok(); + } +} + +pub fn topgrade_vagrant_boxes(ctx: &ExecutionContext) -> Result<()> { + let directories = utils::require_option(ctx.config().vagrant_directories())?; + let vagrant = Vagrant { + path: utils::require("vagrant")?, + }; + + print_separator("Vagrant"); + + for directory in directories { + let boxes = vagrant.get_boxes(directory)?; + debug!("{:?}", boxes); + for (vagrant_box, status) in boxes { + let mut _poweron = None; + if !status.powered_on() { + if !(ctx.config().vagrant_power_on().unwrap_or(true)) { + debug!("Skipping powered off box {}", vagrant_box); + continue; + } else { + _poweron = Some(vagrant.temporary_power_on(&vagrant_box, ctx)?); + } + } + + println!("Running Topgrade in {} @ {}", vagrant_box, directory); + let mut command = format!("env TOPGRADE_PREFIX={} topgrade", vagrant_box.name); + if ctx.config().yes() { + command.push_str(" -y"); + } + + ctx.run_type() + .execute(&vagrant.path) + .args(&["ssh", "-c", &command]) + .check_run()?; + } + } + Ok(()) +}