diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..bf4302f --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,9 @@ +FROM fedora:latest + +RUN bash -c "$(curl -fsSL "https://raw.githubusercontent.com/microsoft/vscode-dev-containers/main/script-library/common-redhat.sh")" -- "true" "vscode" "1000" "1000" "true" + +RUN dnf install -y \ + sudo git cargo rust rust-src git-core clippy rustfmt \ + && dnf clean all + +USER vscode diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..ec23047 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,28 @@ +{ + "name": "greenboot-rs", + "build": { + "dockerfile": "Dockerfile" + }, + "runArgs": [ + "--cap-add=SYS_PTRACE", + "--security-opt", + "seccomp=unconfined" + ], + "settings": { + "rust-analyzer.checkOnSave.command": "clippy" + }, + "extensions": [ + "mutantdino.resourcemonitor", + "matklad.rust-analyzer", + "serayuzgur.crates" + ], + "hostRequirements": { + "memory": "4gb" + }, + "remoteUser": "vscode", + "updateContentCommand": [ + "cargo", + "build" + ], + "waitFor": "onCreateCommand" +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dd37da8..97a45c3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,7 +1,7 @@ on: push: branches: - - main + - greenboot-rs pull_request: name: Continuous integration @@ -22,20 +22,18 @@ jobs: skip: "./docs/Gemfile.lock,./docs/_config.yml,./.github,./.git,./greenboot.spec,./dist" fmt: - name: Rustfmt + name: Cargo fmt runs-on: ubuntu-latest + container: fedora:latest steps: + - name: Install deps + run: | + dnf install -y cargo rustfmt - uses: actions/checkout@v3 - - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: stable - override: true - - run: rustup component add rustfmt - uses: actions-rs/cargo@v1 with: command: fmt - args: --all -- --check + args: --check --all clippy: name: Clippy @@ -67,7 +65,7 @@ jobs: steps: - name: Install deps run: | - dnf install -y make gcc git cargo rust git clevis + dnf install -y make gcc git cargo rust git - uses: actions/checkout@v3 with: persist-credentials: false @@ -91,30 +89,8 @@ jobs: uses: actions-rs/cargo@v1 with: command: test - # This is primarily to ensure that changes to fdo_data.h are committed, - # which is critical for determining whether any stability changes were made - # during the PR review. - - name: Ensure building did not change any code - run: | - git diff --exit-code - - commitlint: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - uses: actions/setup-node@v3 - with: - node-version: 'latest' - - name: Install commitlint dependencies - run: npm install commitlint - - uses: wagoid/commitlint-github-action@v5 - env: - NODE_PATH: ${{ github.workspace }}/node_modules - with: - configFile: .github/commitlint.config.js - failOnWarnings: true + args: -- --test-threads=1 + # manpages: # name: Test man page generation @@ -136,6 +112,6 @@ jobs: # - name: Install devcontainer CLI # run: npm install -g @vscode/dev-container-cli # - name: Build devcontainer - # run: devcontainer build --image-name devcontainer-fdo-rs . + # run: devcontainer build --image-name greenboot-rs . # - name: Test building in devcontainer - # run: docker run --rm -v `pwd`:/code:z --workdir /code --user root devcontainer-fdo-rs cargo build --verbose + # run: docker run --rm -v `pwd`:/code:z --workdir /code --user root greenboot-rs cargo build --verbose diff --git a/Cargo.toml b/Cargo.toml index 37051a1..b51b2d6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,3 +15,5 @@ nix = "0.25.0" glob = "0.3.0" serde = "1.0" serde_json = "1.0" +thiserror = "1.0.38" +figlet-rs = "0.1.5" \ No newline at end of file diff --git a/dist/systemd/system/greenboot-trigger.service b/dist/systemd/system/greenboot-trigger.service deleted file mode 100644 index 3dfd0bf..0000000 --- a/dist/systemd/system/greenboot-trigger.service +++ /dev/null @@ -1,21 +0,0 @@ -[Unit] -Description=Greenboot - TODO 2 -DefaultDependencies=no -Conflicts=shutdown.target -Before=shutdown.target - -Wants=local-fs.target -After=local-fs.target - -Before=multi-user.target systemd-update-done.service -ConditionNeedsUpdate=|/etc -ConditionNeedsUpdate=|/var - -[Service] -Type=oneshot -RemainAfterExit=true -ExecStart=/usr/libexec/greenboot/greenboot stamp -Restart=no - -[Install] -WantedBy=multi-user.target diff --git a/dist/systemd/system/greenboot.service b/dist/systemd/system/greenboot.service index 1d525d6..85fb5b7 100644 --- a/dist/systemd/system/greenboot.service +++ b/dist/systemd/system/greenboot.service @@ -1,13 +1,13 @@ [Unit] -Description=Greenboot - TODO -After=multi-user.target +Description=greenboot Health Checks Runner Before=boot-complete.target +OnFailureJobMode=fail [Service] Type=oneshot RemainAfterExit=yes -ExecStart=/usr/libexec/greenboot/greenboot check -Restart=no +ExecStart=/usr/libexec/greenboot/greenboot health-check [Install] RequiredBy=boot-complete.target +WantedBy=multi-user.target diff --git a/etc/greenboot/greenboot.conf b/etc/greenboot/greenboot.conf new file mode 100644 index 0000000..b989931 --- /dev/null +++ b/etc/greenboot/greenboot.conf @@ -0,0 +1,4 @@ +# Greenboot configuration file + +## Generic +GREENBOOT_MAX_BOOT_ATTEMPTS=3 \ No newline at end of file diff --git a/greenboot.spec b/greenboot.spec index 1424bb2..579bdaa 100644 --- a/greenboot.spec +++ b/greenboot.spec @@ -41,94 +41,108 @@ Requires: rpm-ostree Requires: pam >= 1.4.0 # While not strictly necessary to generate the motd, the main use-case of this package is to display it on SSH login Recommends: openssh -Provides: greenboot-auto-update-fallback -Obsoletes: greenboot-auto-update-fallback <= 0.12.0 -Provides: greenboot-grub2 -Obsoletes: greenboot-grub2 <= 0.12.0 -Provides: greenboot-reboot -Obsoletes: greenboot-reboot <= 0.12.0 -Provides: greenboot-status -Obsoletes: greenboot-status <= 0.12.0 -Provides: greenboot-rpm-ostree-grub2 -Obsoletes: greenboot-rpm-ostree-grub2 <= 0.12.0 # List of bundled crate in vendor tarball, generated with: # cargo metadata --locked --format-version 1 | CRATE_NAME="greenboot" ./bundled-provides.jq Provides: bundled(crate(ahash)) = 0.7.6 -Provides: bundled(crate(aho-corasick)) = 0.7.19 -Provides: bundled(crate(anyhow)) = 1.0.65 -Provides: bundled(crate(async-trait)) = 0.1.57 +Provides: bundled(crate(aho-corasick)) = 0.7.20 +Provides: bundled(crate(anstream)) = 0.2.6 +Provides: bundled(crate(anstyle)) = 0.3.5 +Provides: bundled(crate(anstyle-parse)) = 0.1.1 +Provides: bundled(crate(anstyle-wincon)) = 0.2.0 +Provides: bundled(crate(anyhow)) = 1.0.70 +Provides: bundled(crate(async-trait)) = 0.1.68 Provides: bundled(crate(atty)) = 0.2.14 Provides: bundled(crate(autocfg)) = 1.1.0 -Provides: bundled(crate(base64)) = 0.13.0 +Provides: bundled(crate(base64)) = 0.13.1 Provides: bundled(crate(bitflags)) = 1.3.2 -Provides: bundled(crate(block-buffer)) = 0.10.3 +Provides: bundled(crate(block-buffer)) = 0.10.4 +Provides: bundled(crate(cc)) = 1.0.79 Provides: bundled(crate(cfg-if)) = 1.0.0 -Provides: bundled(crate(clap)) = 4.0.4 -Provides: bundled(crate(clap_derive)) = 4.0.1 -Provides: bundled(crate(clap_lex)) = 0.3.0 -Provides: bundled(crate(config)) = 0.13.2 -Provides: bundled(crate(cpufeatures)) = 0.2.5 +Provides: bundled(crate(clap)) = 4.2.0 +Provides: bundled(crate(clap_builder)) = 4.2.0 +Provides: bundled(crate(clap_derive)) = 4.2.0 +Provides: bundled(crate(clap_lex)) = 0.4.1 +Provides: bundled(crate(concolor-override)) = 1.0.0 +Provides: bundled(crate(concolor-query)) = 0.3.3 +Provides: bundled(crate(config)) = 0.13.3 +Provides: bundled(crate(cpufeatures)) = 0.2.6 Provides: bundled(crate(crypto-common)) = 0.1.6 -Provides: bundled(crate(digest)) = 0.10.5 +Provides: bundled(crate(digest)) = 0.10.6 Provides: bundled(crate(dlv-list)) = 0.3.0 Provides: bundled(crate(env_logger)) = 0.7.1 -Provides: bundled(crate(generic-array)) = 0.14.6 -Provides: bundled(crate(getrandom)) = 0.2.7 -Provides: bundled(crate(glob)) = 0.3.0 +Provides: bundled(crate(errno)) = 0.3.0 +Provides: bundled(crate(errno-dragonfly)) = 0.1.2 +Provides: bundled(crate(figlet-rs)) = 0.1.5 +Provides: bundled(crate(generic-array)) = 0.14.7 +Provides: bundled(crate(getrandom)) = 0.2.8 +Provides: bundled(crate(glob)) = 0.3.1 Provides: bundled(crate(hashbrown)) = 0.12.3 -Provides: bundled(crate(heck)) = 0.4.0 +Provides: bundled(crate(heck)) = 0.4.1 Provides: bundled(crate(hermit-abi)) = 0.1.19 +Provides: bundled(crate(hermit-abi)) = 0.3.1 Provides: bundled(crate(humantime)) = 1.3.0 -Provides: bundled(crate(itoa)) = 1.0.3 +Provides: bundled(crate(io-lifetimes)) = 1.0.9 +Provides: bundled(crate(is-terminal)) = 0.4.6 +Provides: bundled(crate(itoa)) = 1.0.6 Provides: bundled(crate(json5)) = 0.4.1 Provides: bundled(crate(lazy_static)) = 1.4.0 -Provides: bundled(crate(libc)) = 0.2.133 +Provides: bundled(crate(libc)) = 0.2.140 Provides: bundled(crate(linked-hash-map)) = 0.5.6 +Provides: bundled(crate(linux-raw-sys)) = 0.3.0 Provides: bundled(crate(log)) = 0.4.17 Provides: bundled(crate(memchr)) = 2.5.0 Provides: bundled(crate(memoffset)) = 0.6.5 Provides: bundled(crate(minimal-lexical)) = 0.2.1 -Provides: bundled(crate(nix)) = 0.25.0 -Provides: bundled(crate(nom)) = 7.1.1 -Provides: bundled(crate(once_cell)) = 1.15.0 +Provides: bundled(crate(nix)) = 0.25.1 +Provides: bundled(crate(nom)) = 7.1.3 +Provides: bundled(crate(once_cell)) = 1.17.1 Provides: bundled(crate(ordered-multimap)) = 0.4.3 -Provides: bundled(crate(os_str_bytes)) = 6.3.0 Provides: bundled(crate(pathdiff)) = 0.2.1 -Provides: bundled(crate(pest)) = 2.3.1 -Provides: bundled(crate(pest_derive)) = 2.3.1 -Provides: bundled(crate(pest_generator)) = 2.3.1 -Provides: bundled(crate(pest_meta)) = 2.3.1 +Provides: bundled(crate(pest)) = 2.5.6 +Provides: bundled(crate(pest_derive)) = 2.5.6 +Provides: bundled(crate(pest_generator)) = 2.5.6 +Provides: bundled(crate(pest_meta)) = 2.5.6 Provides: bundled(crate(pin-utils)) = 0.1.0 Provides: bundled(crate(pretty_env_logger)) = 0.4.0 -Provides: bundled(crate(proc-macro-error)) = 1.0.4 -Provides: bundled(crate(proc-macro-error-attr)) = 1.0.4 -Provides: bundled(crate(proc-macro2)) = 1.0.43 +Provides: bundled(crate(proc-macro2)) = 1.0.54 Provides: bundled(crate(quick-error)) = 1.2.3 -Provides: bundled(crate(quote)) = 1.0.21 -Provides: bundled(crate(regex)) = 1.6.0 -Provides: bundled(crate(regex-syntax)) = 0.6.27 +Provides: bundled(crate(quote)) = 1.0.26 +Provides: bundled(crate(regex)) = 1.7.3 +Provides: bundled(crate(regex-syntax)) = 0.6.29 Provides: bundled(crate(ron)) = 0.7.1 Provides: bundled(crate(rust-ini)) = 0.18.0 -Provides: bundled(crate(ryu)) = 1.0.11 -Provides: bundled(crate(serde)) = 1.0.144 -Provides: bundled(crate(serde_derive)) = 1.0.144 -Provides: bundled(crate(serde_json)) = 1.0.85 -Provides: bundled(crate(sha1)) = 0.10.5 +Provides: bundled(crate(rustix)) = 0.37.4 +Provides: bundled(crate(ryu)) = 1.0.13 +Provides: bundled(crate(serde)) = 1.0.159 +Provides: bundled(crate(serde_derive)) = 1.0.159 +Provides: bundled(crate(serde_json)) = 1.0.95 +Provides: bundled(crate(sha2)) = 0.10.6 Provides: bundled(crate(strsim)) = 0.10.0 -Provides: bundled(crate(syn)) = 1.0.100 -Provides: bundled(crate(termcolor)) = 1.1.3 -Provides: bundled(crate(thiserror)) = 1.0.35 -Provides: bundled(crate(thiserror-impl)) = 1.0.35 -Provides: bundled(crate(toml)) = 0.5.9 -Provides: bundled(crate(typenum)) = 1.15.0 +Provides: bundled(crate(syn)) = 1.0.109 +Provides: bundled(crate(syn)) = 2.0.11 +Provides: bundled(crate(termcolor)) = 1.2.0 +Provides: bundled(crate(thiserror)) = 1.0.40 +Provides: bundled(crate(thiserror-impl)) = 1.0.40 +Provides: bundled(crate(toml)) = 0.5.11 +Provides: bundled(crate(typenum)) = 1.16.0 Provides: bundled(crate(ucd-trie)) = 0.1.5 -Provides: bundled(crate(unicode-ident)) = 1.0.4 +Provides: bundled(crate(unicode-ident)) = 1.0.8 +Provides: bundled(crate(utf8parse)) = 0.2.1 Provides: bundled(crate(version_check)) = 0.9.4 Provides: bundled(crate(wasi)) = 0.11.0+wasi_snapshot_preview1 Provides: bundled(crate(winapi)) = 0.3.9 Provides: bundled(crate(winapi-i686-pc-windows-gnu)) = 0.4.0 Provides: bundled(crate(winapi-util)) = 0.1.5 Provides: bundled(crate(winapi-x86_64-pc-windows-gnu)) = 0.4.0 +Provides: bundled(crate(windows-sys)) = 0.45.0 +Provides: bundled(crate(windows-targets)) = 0.42.2 +Provides: bundled(crate(windows_aarch64_gnullvm)) = 0.42.2 +Provides: bundled(crate(windows_aarch64_msvc)) = 0.42.2 +Provides: bundled(crate(windows_i686_gnu)) = 0.42.2 +Provides: bundled(crate(windows_i686_msvc)) = 0.42.2 +Provides: bundled(crate(windows_x86_64_gnu)) = 0.42.2 +Provides: bundled(crate(windows_x86_64_gnullvm)) = 0.42.2 +Provides: bundled(crate(windows_x86_64_msvc)) = 0.42.2 Provides: bundled(crate(yaml-rust)) = 0.4.5 %description @@ -147,7 +161,11 @@ cat >.cargo/config << EOF [build] rustc = "%{__rustc}" rustdoc = "%{__rustdoc}" +%if 0%{?rhel} && !0%{?eln} rustflags = %{__global_rustflags_toml} +%else +rustflags = "%{__global_rustflags_toml}" +%endif [install] root = "%{buildroot}%{_prefix}" @@ -182,6 +200,7 @@ install -Dpm0644 -t %{buildroot}%{_unitdir} \ # add config mkdir -p %{buildroot}%{_exec_prefix}/lib/motd.d/ mkdir -p %{buildroot}%{_libexecdir}/%{name} +install -Dpm0644 -t %{buildroot}%{_sysconfdir}/%{name} etc/greenboot/greenboot.conf mkdir -p %{buildroot}%{_sysconfdir}/%{name}/check/required.d mkdir %{buildroot}%{_sysconfdir}/%{name}/check/wanted.d mkdir %{buildroot}%{_sysconfdir}/%{name}/green.d @@ -196,15 +215,12 @@ mkdir -p %{buildroot}%{_tmpfilesdir} %post %systemd_post greenboot.service -%systemd_post greenboot-trigger.service %preun %systemd_preun greenboot.service -%systemd_preun greenboot-trigger.service %postun %systemd_postun greenboot.service -%systemd_postun greenboot-trigger.service %files %doc README.md @@ -212,7 +228,7 @@ mkdir -p %{buildroot}%{_tmpfilesdir} %dir %{_libexecdir}/%{name} %{_libexecdir}/%{name}/%{name} %{_unitdir}/greenboot.service -%{_unitdir}/greenboot-trigger.service +%{_sysconfdir}/%{name}/greenboot.conf %dir %{_prefix}/lib/%{name} %dir %{_prefix}/lib/%{name}/check %dir %{_prefix}/lib/%{name}/check/required.d diff --git a/src/handler/mod.rs b/src/handler/mod.rs new file mode 100644 index 0000000..4144f73 --- /dev/null +++ b/src/handler/mod.rs @@ -0,0 +1,106 @@ +use anyhow::{Error, Result}; +use figlet_rs::FIGfont; +use std::fs::OpenOptions; +use std::io::Write; +use std::process::Command; +use std::str; + +pub fn handle_reboot(force: bool) -> Result<(), Error> { + let mut boot_counter: &str = ""; + if !force { + let grub_vars = Command::new("grub2-editenv") + .arg("-") + .arg("list") + .output()?; + let grub_vars = str::from_utf8(&grub_vars.stdout[..]).unwrap().split('\n'); + + for var in grub_vars { + if var.contains("boot_counter") { + boot_counter = var.split('=').last().unwrap(); + } + } + + if boot_counter.parse::().unwrap() <= -1 { + log::error!("cannot reboot as boot_counter is -1 "); + return Ok(()); + } + } + + Command::new("systemctl") + .arg("reboot") + .output() + .expect("unable to reboot"); + Ok(()) +} + +pub fn handle_rollback() -> Result<(), Error> { + Command::new("rpm-ostree") + .arg("rollback") + .spawn() + .expect("unable to rollback"); + Ok(()) +} + +pub fn handle_boot_counter(reboot_count: i64) -> Result<(), Error> { + let grub_vars = Command::new("grub2-editenv") + .arg("-") + .arg("list") + .output()?; + let grub_vars = str::from_utf8(&grub_vars.stdout[..]).unwrap().split('\n'); + + for var in grub_vars { + if var.contains("boot_counter") { + return Ok(()); + } + } + Command::new("grub2-editenv") + .arg("-") + .arg("set") + .arg(format!("boot_counter={}", reboot_count)) + .spawn() + .expect("unable to set boot_counter"); + Ok(()) +} + +pub fn handle_boot_success(success: bool) -> Result<(), Error> { + if success { + Command::new("grub2-editenv") + .arg("-") + .arg("set") + .arg("boot_success=1") + .spawn() + .expect("unable to set boot_success"); + Command::new("grub2-editenv") + .arg("-") + .arg("unset") + .arg("boot_counter") + .spawn() + .expect("unable to set boot counter"); + } else { + Command::new("grub2-editenv") + .arg("-") + .arg("set") + .arg("boot_success=0") + .spawn() + .expect("unable to set boot_success"); + } + Ok(()) +} + +//Motd will be GREENBOOT/REDBOOT in figlet +pub fn handle_motd(state: &str) { + let figlet_font = FIGfont::standard().unwrap(); + + let motd = figlet_font + .convert(&(state.to_uppercase() + "BOOT")) + .unwrap() + .to_string(); + + let mut motd_file = OpenOptions::new() + .create(true) + .write(true) + .append(true) + .open("/run/motd.d/boot-status") + .expect("Unable to open file"); + motd_file.write_all(motd.as_bytes()).expect("write failed"); +} diff --git a/src/main.rs b/src/main.rs index a55df5b..eeb99af 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,16 +1,17 @@ -use std::hash::Hash; -use std::io::ErrorKind; -use std::iter::FromIterator; -use std::{ - collections::HashSet, - fs::{self, File}, - process::Command, -}; - +mod handler; +use anyhow::anyhow; use anyhow::{bail, Error, Result}; use clap::{Parser, Subcommand, ValueEnum}; +use config::{Config, File, FileFormat}; use glob::glob; -use serde::{Deserialize, Serialize}; +use handler::*; +use serde::Deserialize; +use std::path::Path; +use std::process::Command; +use std::str; + +static GREENBOOT_INSTALL_PATHS: [&str; 2] = ["/usr/lib/greenboot", "/etc/greenboot"]; +static GREENBOOT_CONFIG_FILE: &str = "/etc/greenboot/greenboot.conf"; #[derive(Parser)] #[clap(author, version, about, long_about = None)] @@ -18,10 +19,13 @@ use serde::{Deserialize, Serialize}; struct Cli { #[clap(value_enum, short, long, default_value_t = LogLevel::Info)] log_level: LogLevel, - #[clap(subcommand)] command: Commands, } +#[derive(Debug, Deserialize)] +struct GreenbootConfig { + max_reboot: i64, //max reboot attemmpts if diagnostics fails +} #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] enum LogLevel { @@ -46,126 +50,219 @@ impl LogLevel { } } -#[derive(Subcommand)] -enum Commands { - Check, - Stamp, +fn get_config() -> Result { + let mut config = GreenbootConfig { max_reboot: 3 }; + let parsed = Config::builder() + .set_default("GREENBOOT_MAX_BOOT_ATTEMPTS", 3)? + .add_source(File::new(GREENBOOT_CONFIG_FILE, FileFormat::Ini)); + match parsed.build() { + Ok(c) => { + config.max_reboot = match c.get_int("GREENBOOT_MAX_BOOT_ATTEMPTS") { + Ok(c) => c, + Err(e) => { + log::warn!("{}, using default value", e); + config.max_reboot + } + }; + Ok(config) + } + Err(e) => { + log::warn!("{}, using default value", e); + Ok(config) + } + } } -#[derive(Serialize, Deserialize, PartialEq, Eq, Hash)] -struct ServiceStatus { - unit: String, +#[derive(Subcommand)] +enum Commands { + HealthCheck, + RollBack, } -fn check() -> Result<(), Error> { - match File::open("/etc/greenboot/upgrade.stamp") { - Ok(_) => { - log::info!("stamp on disk, removing and running greenboot"); - std::fs::remove_file("/etc/greenboot/upgrade.stamp")? - } - Err(e) => match e.kind() { - ErrorKind::NotFound => return Ok(()), - _ => { - bail!("unknown error when opening stamp file: {:?}", e); - } - }, - } - let mut failure = false; - for path in [ - "/usr/lib/greenboot/check/required.d/*.sh", - "/etc/greenboot/check/required.d/*.sh", - ] { - for entry in glob(path)?.flatten() { - log::info!("running required check {}", entry.to_string_lossy()); - let output = Command::new("bash").arg("-C").arg(entry).output()?; - if !output.status.success() { - // combine and print stderr/stdout - log::warn!("required script failed..."); - failure = true; +fn run_diagnostics() -> Result<(), Error> { + let mut script_failure: bool = false; + let mut path_exists: bool = false; + for path in GREENBOOT_INSTALL_PATHS { + let gereenboot_required_path = format!("{}{}", path, "/check/required.d/"); + if Path::new(&gereenboot_required_path).is_dir() { + path_exists = true; + let gereenboot_required_path = format!("{}{}", gereenboot_required_path, "*.sh"); + for entry in glob(&gereenboot_required_path)?.flatten() { + log::info!("running required check {}", entry.to_string_lossy()); + let output = Command::new("bash").arg("-C").arg(entry.clone()).output()?; + if !output.status.success() { + log::error!("required script {} failed!", entry.to_string_lossy()); + //add error + script_failure = true; + } } } } - for path in [ - "/usr/lib/greenboot/check/wanted.d/*.sh", - "/etc/greenboot/check/wanted.d/*.sh", - ] { - for entry in glob(path)?.flatten() { - log::info!("running required check {}", entry.to_string_lossy()); - let output = Command::new("bash").arg("-C").arg(entry).output()?; + if !path_exists { + return Err(anyhow!("ParseError")); + } + + for path in GREENBOOT_INSTALL_PATHS { + let gereenboot_wanted_path = format!("{}{}", path, "/check/wanted.d/*.sh"); + for entry in glob(&gereenboot_wanted_path)?.flatten() { + log::info!("running wanted check {}", entry.to_string_lossy()); + let output = Command::new("bash").arg("-C").arg(entry.clone()).output()?; if !output.status.success() { // combine and print stderr/stdout - log::warn!("wanted script failed..."); + log::warn!("wanted script {} failed!", entry.to_string_lossy()); } } } - // if a command with restart option in systemd fails to start we don't get it as "failed" - // reversing the check makes sure that if by the time After=multi-user the service isn't running then it's failing at least - let output = Command::new("systemctl") - .arg("list-units") - .arg("--state") - .arg("active") - .arg("--no-page") - .arg("--output") - .arg("json") - .output()?; - let services: Vec = serde_json::from_str(&String::from_utf8(output.stdout)?)?; - let ss: Vec = services.iter().map(|x| x.unit.clone()).collect(); - let active_units: HashSet = HashSet::from_iter(ss); - for service in ["sshd.service", "NetworkManager.service"] { - if !active_units.contains(service) { - log::warn!("service {} failed, see journal", service); - failure = true; - } + + if script_failure { + log::error!("Greenboot health-check failed!"); + return Err(anyhow!("health-check failed!")); } - if failure { - for path in ["/etc/greenboot/red.d/*.sh"] { - for entry in glob(path)?.flatten() { - log::info!("running red check {}", entry.to_string_lossy()); - let output = Command::new("bash").arg("-C").arg(entry).output()?; - if !output.status.success() { - // combine and print stderr/stdout - log::warn!("red script failed..."); - } + Ok(()) +} + +fn run_red() -> Result<(), Error> { + for path in GREENBOOT_INSTALL_PATHS { + let red_path = format!("{}{}", path, "/red.d/*.*"); + for entry in glob(&red_path)?.flatten() { + log::info!("running red check {}", entry.to_string_lossy()); + let output = Command::new("bash").arg("-C").arg(entry.clone()).output()?; + if !output.status.success() { + // combine and print stderr/stdout + log::warn!("red script: {} failed!", entry.to_string_lossy()); } } - log::warn!("SYSTEM is UNHEALTHY. Rolling back and rebooting..."); - Command::new("rpm-ostree").arg("rollback").status()?; - reboot()?; - return Ok(()); } - for path in ["/etc/greenboot/green.d/*.sh"] { - for entry in glob(path)?.flatten() { + Ok(()) +} + +fn run_green() -> Result<(), Error> { + for path in GREENBOOT_INSTALL_PATHS { + let green_path = format!("{}{}", path, "/green.d/*.*"); + for entry in glob(&green_path)?.flatten() { log::info!("running green check {}", entry.to_string_lossy()); - let output = Command::new("bash").arg("-C").arg(entry).output()?; + let output = Command::new("bash").arg("-C").arg(entry.clone()).output()?; if !output.status.success() { // combine and print stderr/stdout - log::warn!("green script failed..."); + log::warn!("green script {} failed!", entry.to_string_lossy()); } } } Ok(()) } -fn reboot() -> Result<(), Error> { - Command::new("systemctl").arg("reboot").spawn()?; - Ok(()) -} +fn health_check() -> Result<()> { + let config = get_config().unwrap(); + log::info!("{config:?}"); -fn stamp() -> Result<(), Error> { - fs::create_dir_all("/etc/greenboot/")?; - File::create("/etc/greenboot/upgrade.stamp")?; - Ok(()) + let run_status = run_diagnostics(); + match run_status { + Ok(()) => { + log::info!("greenboot health-check passed."); + run_green().expect("Green scripts failed!"); + handle_motd("green"); + handle_boot_success(true).expect("unable to set grub variable"); + Ok(()) + } + Err(e) => { + handle_motd("red"); + run_red().expect("Red scripts failed!"); + handle_boot_success(false).expect("unable to set boot_success"); + handle_boot_counter(config.max_reboot).expect("unable to set boot_counter"); + handle_reboot(false)?; + bail!(e); + } + } } fn main() -> Result<()> { let cli = Cli::parse(); - pretty_env_logger::formatted_builder() .filter_level(cli.log_level.to_log()) .init(); match cli.command { - Commands::Check => check(), - Commands::Stamp => stamp(), + Commands::HealthCheck => health_check(), + Commands::RollBack => handle_rollback(), // will tackle the functionality later + } +} + +#[cfg(test)] +mod tests { + use std::fs; + + use anyhow::Context; + + use super::*; + + //valiadte when required folder is not found + #[test] + fn missing_required_folder() { + assert_eq!( + run_diagnostics().unwrap_err().to_string(), + String::from("ParseError") + ); + } + + #[test] + fn test_passed_diagnostics() { + setup_folder_structure(true) + .context("Test setup failed") + .unwrap(); + let state = run_diagnostics(); + assert!(state.is_ok()); + tear_down().context("Test teardown failed").unwrap(); + } + + #[test] + fn test_failed_diagnostics() { + setup_folder_structure(false) + .context("Test setup failed") + .unwrap(); + let failed_msg = run_diagnostics().unwrap_err().to_string(); + assert_eq!(failed_msg, String::from("health-check failed!")); + tear_down().context("Test teardown failed").unwrap(); + } + + fn setup_folder_structure(passing: bool) -> Result<()> { + let required_path = format!("{}/check/required.d", GREENBOOT_INSTALL_PATHS[1]); + let wanted_path = format!("{}/check/wanted.d", GREENBOOT_INSTALL_PATHS[1]); + let passing_test_scripts = "testing_assets/passing_script.sh"; + let failing_test_scripts = "testing_assets/failing_script.sh"; + + fs::create_dir_all(&required_path).expect("cannot create folder"); + fs::create_dir_all(&wanted_path).expect("cannot create folder"); + let _a = fs::copy( + passing_test_scripts, + format!("{}/passing_script.sh", &required_path), + ) + .context("unable to copy test assets"); + + let _a = fs::copy( + passing_test_scripts, + format!("{}/passing_script.sh", &wanted_path), + ) + .context("unable to copy test assets"); + + let _a = fs::copy( + failing_test_scripts, + format!("{}/failing_script.sh", &wanted_path), + ) + .context("unable to copy test assets"); + + if !passing { + let _a = fs::copy( + failing_test_scripts, + format!("{}/ailing_script.sh", &required_path), + ) + .context("unable to copy test assets"); + return Ok(()); + } + Ok(()) + } + + fn tear_down() -> Result<()> { + fs::remove_dir_all(GREENBOOT_INSTALL_PATHS[1]).expect("Unable to delete folder"); + Ok(()) } } diff --git a/testing_assets/failing_script.sh b/testing_assets/failing_script.sh new file mode 100644 index 0000000..fc39ca2 --- /dev/null +++ b/testing_assets/failing_script.sh @@ -0,0 +1,6 @@ +#!/bin/bash +set -euo pipefail + +echo "This is a failing script" + +exit 1 diff --git a/testing_assets/passing_script.sh b/testing_assets/passing_script.sh new file mode 100644 index 0000000..e8022bc --- /dev/null +++ b/testing_assets/passing_script.sh @@ -0,0 +1,4 @@ +#!/bin/bash +set -euo pipefail + +echo "This is a passing script"