use nagios_range::Error as RangeError;
use nagios_range::NagiosRange as ThresholdRange;
use serde::Deserialize;
use std::fmt;
use std::process::{self, Output};
pub const PLIST_FILE: &str = "/Library/Preferences/com.apple.SoftwareUpdate.plist";
#[derive(Clone, Debug, PartialEq)]
pub struct Thresholds {
pub warning: Option<ThresholdRange>,
pub critical: Option<ThresholdRange>,
}
#[non_exhaustive]
#[derive(Debug, PartialEq)]
pub enum UnkownVariant {
ClapError(String),
NotMacOS,
NoThresholds,
RangeParseError(String, RangeError),
UnableToDetermineUpdates,
UnableToParsePlist,
}
#[derive(Debug, PartialEq)]
pub enum Status {
Ok(usize),
Warning(usize),
Critical(usize),
Unknown(UnkownVariant),
}
impl fmt::Display for Status {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Status::Ok(n) => write!(f, "OK - {} updates available|'Available Updates'={}", n, n),
Status::Warning(n) => write!(
f,
"WARNING - Updates available: {}|'Available Updates'={}",
n, n
),
Status::Critical(n) => write!(
f,
"CRITICAL - Updates available: {}|'Available Updates'={}",
n, n
),
Status::Unknown(UnkownVariant::ClapError(s)) => {
let trimmed = s.trim_end();
let without_leading_error = trimmed.trim_start_matches("error: ");
write!(
f,
"UNKNOWN - Command line parsing produced an error: {}",
without_leading_error,
)
}
Status::Unknown(UnkownVariant::NotMacOS) => {
write!(f, "UNKNOWN - Not running on macOS")
}
Status::Unknown(UnkownVariant::NoThresholds) => {
write!(f, "UNKNOWN - No thresholds provided")
}
Status::Unknown(UnkownVariant::RangeParseError(s, e)) => {
write!(
f,
"UNKNOWN - Unable to parse range '{}' with error: {}",
s, e
)
}
Status::Unknown(UnkownVariant::UnableToDetermineUpdates) => {
write!(f, "UNKNOWN - Unable to determine available updates")
}
Status::Unknown(UnkownVariant::UnableToParsePlist) => {
write!(f, "UNKNOWN - Unable to parse plist file")
}
}
}
}
impl Status {
pub fn to_int(&self) -> i32 {
match self {
Status::Ok(_) => 0,
Status::Warning(_) => 1,
Status::Critical(_) => 2,
Status::Unknown(_) => 3,
}
}
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct SoftwareUpdate {
#[serde(default)]
pub automatic_check_enabled: bool,
pub last_updates_available: u8,
}
pub fn softwareupdate_output() -> Result<Output, std::io::Error> {
process::Command::new("softwareupdate").arg("-l").output()
}
fn evaluate_thresholds(n: usize, thresholds: &Thresholds) -> Status {
if let Some(c) = thresholds.critical {
if c.check(n as f64) {
return Status::Critical(n);
}
}
if let Some(w) = thresholds.warning {
if w.check(n as f64) {
return Status::Warning(n);
}
}
Status::Ok(n)
}
pub fn check_softwareupdate_output(
output: &Result<Output, std::io::Error>,
thresholds: &Thresholds,
) -> Status {
match output {
Ok(output) => {
let output_stderr = String::from_utf8_lossy(&output.stderr);
let output_stdout = String::from_utf8_lossy(&output.stdout);
let n: usize = if output_stderr.contains("No new software available.") {
0
} else {
output_stdout
.lines()
.filter(|l| l.contains("* Label:"))
.count()
};
evaluate_thresholds(n, thresholds)
}
Err(_) => Status::Unknown(UnkownVariant::UnableToDetermineUpdates),
}
}
pub fn determine_updates(update: &SoftwareUpdate, thresholds: &Thresholds) -> Status {
let n = update.last_updates_available as usize;
if !update.automatic_check_enabled && n == 0 {
check_softwareupdate_output(&softwareupdate_output(), thresholds)
} else {
evaluate_thresholds(n, thresholds)
}
}