mod postinstall_cmd;
mod preinstall_cmd;
use clap::{Args, Parser, Subcommand};
use console::{Emoji, style};
use edera_check::helpers::{CheckGroup, CheckGroupResult, host_executor::HostNamespaceExecutor};
use anyhow::{Context, Result};
use chrono::Utc;
use flate2::{Compression, write::GzEncoder};
use log::debug;
use nix::unistd::Uid;
use std::{
fs,
fs::File,
path::{Path, PathBuf},
process,
};
use tokio::task::JoinHandle;
static SPARKLE: Emoji = Emoji("✨", "[*]");
#[derive(Parser)]
#[command(name = "edera-check")]
#[command(about = "CLI to run checks before installing or using Edera", long_about = None)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum PreinstallAction {
ListChecks,
}
#[derive(Subcommand)]
enum PostinstallAction {
ListChecks,
}
#[derive(Args)]
struct PreinstallArgs {
#[command(subcommand)]
action: Option<PreinstallAction>,
#[arg(short, long, default_value_t = false)]
byo_kernel: bool,
#[arg(short, long, default_value_t = true)]
record_hostinfo: bool,
#[arg(short, long, value_delimiter = ',')]
only_checks: Vec<String>,
#[arg(short = 'd', long)]
report_dir: Option<String>,
}
#[derive(Args)]
struct PostinstallArgs {
#[command(subcommand)]
action: Option<PostinstallAction>,
#[arg(short, long, default_value_t = true)]
record_hostinfo: bool,
#[arg(short, long, value_delimiter = ',')]
only_checks: Vec<String>,
#[arg(short = 'd', long)]
report_dir: Option<String>,
}
#[derive(Subcommand)]
enum Commands {
Preinstall(PreinstallArgs),
Postinstall(PostinstallArgs),
}
#[tokio::main(flavor = "multi_thread", worker_threads = 10)]
async fn main() -> Result<()> {
env_logger::init();
if !Uid::effective().is_root() {
println!("{}", style("This tool must be run as root").red().bold());
process::exit(1);
}
let cli = Cli::parse();
match cli.command {
Commands::Preinstall(args) => {
preinstall_cmd::do_preinstall(
matches!(args.action, Some(PreinstallAction::ListChecks)),
args.byo_kernel,
args.record_hostinfo,
args.only_checks,
args.report_dir,
)
.await
}
Commands::Postinstall(args) => {
postinstall_cmd::do_postinstall(
matches!(args.action, Some(PostinstallAction::ListChecks)),
args.record_hostinfo,
args.only_checks,
args.report_dir,
)
.await
}
}
}
async fn create_gzip_from(base_path: PathBuf, host_executor: HostNamespaceExecutor) -> Result<()> {
let mut archive_path = base_path.clone();
archive_path.set_extension("tar.gz");
let tar_gz = File::create(&archive_path)
.with_context(|| format!("failed to create {}", archive_path.display()))?;
let enc = GzEncoder::new(tar_gz, Compression::default());
let mut tar = tar::Builder::new(enc);
tar.append_dir_all(".", &base_path)
.context("failed to append to tar {}")?;
tar.into_inner().context("failed to finish tar")?;
let container_tarfile = archive_path.to_string_lossy().to_string();
let targz_content = std::fs::read(&container_tarfile).expect("could not read tar");
debug!("Read {} bytes of tar", targz_content.len());
std::fs::remove_dir_all(&base_path)
.with_context(|| format!("failed to remove results directory {}", base_path.display()))?;
let copy_to_host: JoinHandle<()> = host_executor.spawn_in_host_ns(async move {
tokio::fs::write(&container_tarfile, targz_content)
.await
.expect("could not write tar to host");
println!(
"{} {} Report saved: {}",
SPARKLE,
style("All Done!").green(),
style(container_tarfile).cyan()
);
});
Ok(copy_to_host.await?)
}
fn create_base_path(base_dir: PathBuf, hostname: &str, stage: &str) -> Result<PathBuf> {
let now = Utc::now();
let base_path = base_dir.join(format!(
"edera-{}-report-{}-{}",
stage,
hostname,
now.format("%Y%m%d-%H%M%S")
));
fs::create_dir_all(&base_path)
.with_context(|| format!("could not create {}", base_path.display()))?;
debug!("Writing all files to {}", base_path.to_string_lossy());
Ok(base_path)
}
fn write_group_report(
group: Box<dyn CheckGroup>,
result: &CheckGroupResult,
path: &Path,
) -> Result<()> {
let path = path.join(group.id());
fs::create_dir_all(&path).with_context(|| format!("could not create {}", path.display()))?;
for check in result.results.iter() {
let name = check
.name
.replace(" ", "_")
.replace("/", "_")
.replace(".", "");
let path = path.join(name);
match check.output_to_record.as_ref() {
Some(text) => fs::write(&path, text),
None => fs::write(&path, format!("{}", check.result)),
}
.with_context(|| format!("failed to write to {}", path.display()))?;
}
Ok(())
}
async fn booted_under_edera(host_executor: &HostNamespaceExecutor) -> Result<bool> {
host_executor
.spawn_in_host_ns(async {
if !Path::new("/var/lib/edera/protect/.install-completed").exists() {
return false;
}
let xen = Path::new("/sys/hypervisor/type");
xen.exists() && fs::read_to_string(xen).unwrap_or_default().trim() == "xen"
})
.await
.context("failed to check Edera boot status")
}