diff --git a/peach-stats/Cargo.toml b/peach-stats/Cargo.toml index 488d637..a715323 100644 --- a/peach-stats/Cargo.toml +++ b/peach-stats/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "peach-stats" -version = "0.2.0" +version = "0.3.0" authors = ["Andrew Reid "] edition = "2018" description = "Query system statistics. Provides a wrapper around the probes and systemstat crates." diff --git a/peach-stats/README.md b/peach-stats/README.md index 1245532..9ea56ce 100644 --- a/peach-stats/README.md +++ b/peach-stats/README.md @@ -1,10 +1,10 @@ # peach-stats -![Generic badge](https://img.shields.io/badge/version-0.2.0-.svg) +![Generic badge](https://img.shields.io/badge/version-0.3.0-.svg) System statistics library for PeachCloud. Provides a wrapper around the [probes](https://crates.io/crates/probes) and [systemstat](https://crates.io/crates/systemstat) crates. -Currently offers the following statistics and associated data structures: +Currently offers the following system statistics and associated data structures: - CPU: `user`, `system`, `nice`, `idle` (as values or percentages) - Disk usage: `filesystem`, `one_k_blocks`, `one_k_blocks_used`, @@ -13,10 +13,14 @@ Currently offers the following statistics and associated data structures: - Memory: `total`, `free`, `used` - Uptime: `seconds` +As well as the following go-sbot process statistics: + + - Sbot: `state`, `memory`, `uptime`, `downtime` + ## Example Usage ```rust -use peach_stats::{stats, StatsError}; +use peach_stats::{sbot, stats, StatsError}; fn main() -> Result<(), StatsError> { let cpu = stats::cpu_stats()?; @@ -25,6 +29,7 @@ fn main() -> Result<(), StatsError> { let load = stats::load_average()?; let mem = stats::mem_stats()?; let uptime = stats::uptime()?; + let sbot_process = sbot::sbot_stats()?; // do things with the retrieved values... diff --git a/peach-stats/src/error.rs b/peach-stats/src/error.rs index d782e35..e75a5e2 100644 --- a/peach-stats/src/error.rs +++ b/peach-stats/src/error.rs @@ -1,7 +1,7 @@ //! Custom error type for `peach-stats`. use probes::ProbeError; -use std::{error, fmt, io::Error as IoError}; +use std::{error, fmt, io::Error as IoError, str::Utf8Error}; /// Custom error type encapsulating all possible errors when retrieving system /// statistics. @@ -17,6 +17,10 @@ pub enum StatsError { MemStat(ProbeError), /// Failed to retrieve system uptime. Uptime(IoError), + /// Systemctl command returned an error. + Systemctl(IoError), + /// Failed to interpret sequence of `u8` as a string. + Utf8String(Utf8Error), } impl error::Error for StatsError {} @@ -39,6 +43,12 @@ impl fmt::Display for StatsError { StatsError::Uptime(ref source) => { write!(f, "Failed to retrieve system uptime: {}", source) } + StatsError::Systemctl(ref source) => { + write!(f, "Systemctl command returned an error: {}", source) + } + StatsError::Utf8String(ref source) => { + write!(f, "Failed to convert stdout to string: {}", source) + } } } } diff --git a/peach-stats/src/lib.rs b/peach-stats/src/lib.rs index cae7d50..7a0e0b0 100644 --- a/peach-stats/src/lib.rs +++ b/peach-stats/src/lib.rs @@ -43,6 +43,7 @@ //! ``` pub mod error; +pub mod sbot; pub mod stats; pub use crate::error::StatsError; diff --git a/peach-stats/src/sbot.rs b/peach-stats/src/sbot.rs new file mode 100644 index 0000000..b67a417 --- /dev/null +++ b/peach-stats/src/sbot.rs @@ -0,0 +1,90 @@ +//! Systemd go-sbot process statistics retrieval functions and associated data types. + +use std::{process::Command, str}; + +use crate::StatsError; + +/// go-sbot process statistics. +#[derive(Debug)] +pub struct SbotStat { + /// Current process state. + state: String, + /// Current process memory usage in bytes. + memory: Option, + /// Uptime for the process (if state is `active`). + uptime: Option, + /// Downtime for the process (if state is `inactive`). + downtime: Option, +} + +impl SbotStat { + /// Default builder for `SbotStat`. + fn default() -> Self { + Self { + state: String::new(), + memory: None, + uptime: None, + downtime: None, + } + } +} + +/// Retrieve statistics for the go-sbot systemd process by querying `systemctl`. +pub fn sbot_stats() -> Result { + let mut status = SbotStat::default(); + + let info_output = Command::new("/usr/bin/systemctl") + .arg("--user") + .arg("show") + .arg("go-sbot.service") + .arg("--no-page") + .output() + .map_err(StatsError::Systemctl)?; + + let service_info = std::str::from_utf8(&info_output.stdout).map_err(StatsError::Utf8String)?; + + for line in service_info.lines() { + if line.starts_with("ActiveState=") { + if let Some(state) = line.strip_prefix("ActiveState=") { + status.state = state.to_string() + } + } else if line.starts_with("MemoryCurrent=") { + if let Some(memory) = line.strip_prefix("MemoryCurrent=") { + status.memory = memory.parse().ok() + } + } + } + + let status_output = Command::new("/usr/bin/systemctl") + .arg("--user") + .arg("status") + .arg("go-sbot.service") + .output() + .unwrap(); + + let service_status = str::from_utf8(&status_output.stdout).map_err(StatsError::Utf8String)?; + + // example of the output line we're looking for: + // `Active: active (running) since Mon 2022-01-24 16:22:51 SAST; 4min 14s ago` + + for line in service_status.lines() { + if line.contains("Active:") { + let before_time = line.find(';'); + let after_time = line.find(" ago"); + if let (Some(start), Some(end)) = (before_time, after_time) { + // extract the uptime / downtime from the `Active: ...` line + // using the index of ';' + 2 and the index of " ago" + let time = Some(&line[start + 2..end]); + // if service is active then the `time` reading is uptime + if status.state == "active" { + status.uptime = time.map(|t| t.to_string()) + // if service is inactive then the `time` reading is downtime + } else if status.state == "inactive" { + status.downtime = time.map(|t| t.to_string()) + } + } + } + } + + Ok(status) +} diff --git a/peach-web/Cargo.toml b/peach-web/Cargo.toml index ddd4b0d..90e0295 100644 --- a/peach-web/Cargo.toml +++ b/peach-web/Cargo.toml @@ -36,7 +36,6 @@ maintenance = { status = "actively-developed" } [dependencies] env_logger = "0.8" -lazy_static = "1.4.0" log = "0.4" nest = "1.0.0" peach-lib = { path = "../peach-lib" } diff --git a/peach-web/README.md b/peach-web/README.md index ec03092..b686101 100644 --- a/peach-web/README.md +++ b/peach-web/README.md @@ -25,7 +25,7 @@ Move into the repo and compile: Run the tests: -`ROCKET_DISABLE_AUTH=true PEACH_STANDALONE_MODE=false cargo test` +`ROCKET_DISABLE_AUTH=true ROCKET_STANDALONE_MODE=false cargo test` Move back to the `peach-workspace` directory: @@ -37,21 +37,23 @@ Run the binary: ### Environment -**Deployment Mode** +**Deployment Profile** -The web application deployment mode is configured with the `ROCKET_ENV` environment variable: +The web application deployment profile can be configured with the `ROCKET_ENV` environment variable: `export ROCKET_ENV=stage` -Other deployment modes are `dev` and `prod`. Read the [Rocket Environment Configurations docs](https://rocket.rs/v0.5-rc/guide/configuration/#environment-variables) for further information. +Default configuration parameters are defined in `Rocket.toml`. This file defines a set of default parameters, some of which are overwritten when running in `debug` mode (ie. `cargo run` or `cargo build`) or `release` mode (ie. `cargo run --release` or `cargo build --release`). + +Read the [Rocket Environment Configurations docs](https://rocket.rs/v0.5-rc/guide/configuration/#environment-variables) for further information. **Configuration Mode** -The web application can be run with a minimal set of routes and functionality (PeachPub - a simple sbot manager) or with the full-suite of capabilities, including network management and access to device statistics (PeachCloud). The mode is configured with the `PEACH_STANDALONE_MODE` environment variable: `true` or `false`. If the variable is unset or the value is incorrectly set, the application defaults to standalone mode. +The web application can be run with a minimal set of routes and functionality (PeachPub - a simple sbot manager) or with the full-suite of capabilities, including network management and access to device statistics (PeachCloud). The mode is enabled by default (as defined in `Rocket.toml`) but can be overwritten using the `ROCKET_STANDALONE_MODE` environment variable: `true` or `false`. If the variable is unset or the value is incorrectly set, the application defaults to standalone mode. **Authentication** -Authentication is disabled in `development` mode and enabled by default when running the application in `production` mode. It can be disabled by setting the `ROCKET_DISABLE_AUTH` environment variable to `true`: +Authentication is disabled in `debug` mode and enabled by default when running the application in `release` mode. It can be disabled by setting the `ROCKET_DISABLE_AUTH` environment variable to `true`: `export ROCKET_DISABLE_AUTH=true` diff --git a/peach-web/Rocket.toml b/peach-web/Rocket.toml index 1af1b71..c1f285e 100644 --- a/peach-web/Rocket.toml +++ b/peach-web/Rocket.toml @@ -1,10 +1,11 @@ [default] secret_key = "VYVUDivXvu8g6llxeJd9F92pMfocml5xl/Jjv5Sk4yw=" +disable_auth = false +standalone_mode = true -[development] +[debug] template_dir = "templates/" disable_auth = true -[production] +[release] template_dir = "templates/" -disable_auth = false diff --git a/peach-web/src/main.rs b/peach-web/src/main.rs index ded1686..238e561 100644 --- a/peach-web/src/main.rs +++ b/peach-web/src/main.rs @@ -32,21 +32,23 @@ pub mod routes; mod tests; pub mod utils; -use std::{env, process}; +use std::process; -use lazy_static::lazy_static; -use log::{error, info}; -use rocket::{Build, Rocket}; +use log::{debug, error, info}; +use rocket::{fairing::AdHoc, serde::Deserialize, Build, Rocket}; pub type BoxError = Box; -lazy_static! { - // determine run-mode from env var; default to standalone mode (aka peachpub) - static ref STANDALONE_MODE: bool = match env::var("PEACH_STANDALONE_MODE") { - // parse the value to a boolean; default to true for any error - Ok(val) => val.parse().unwrap_or(true), - Err(_) => true - }; +/// Application configuration parameters. +/// These values are extracted from Rocket's default configuration provider: +/// `Config::figment()`. As such, the values are drawn from `Rocket.toml` or +/// the TOML file path in the `ROCKET_CONFIG` environment variable. The TOML +/// file parameters are automatically overruled by any `ROCKET_` variables +/// which might be set. +#[derive(Debug, Deserialize)] +pub struct RocketConfig { + disable_auth: bool, + standalone_mode: bool, } static WLAN_IFACE: &str = "wlan0"; @@ -54,11 +56,27 @@ static AP_IFACE: &str = "ap0"; pub fn init_rocket() -> Rocket { info!("Initializing Rocket"); - if *STANDALONE_MODE { - router::mount_peachpub_routes() + // build a basic rocket instance + let rocket = rocket::build(); + + // return the default provider figment used by `rocket::build()` + let figment = rocket.figment(); + + // deserialize configuration parameters into our `RocketConfig` struct (defined above) + // since we're in the intialisation phase, panic if the extraction fails + let config: RocketConfig = figment.extract().expect("configuration extraction failed"); + + debug!("{:?}", config); + + info!("Mounting Rocket routes"); + let mounted_rocket = if config.standalone_mode { + router::mount_peachpub_routes(rocket) } else { - router::mount_peachcloud_routes() - } + router::mount_peachcloud_routes(rocket) + }; + + info!("Attaching application configuration to managed state"); + mounted_rocket.attach(AdHoc::config::()) } /// Launch the peach-web rocket server. diff --git a/peach-web/src/router.rs b/peach-web/src/router.rs index 341d9aa..99bcb7b 100644 --- a/peach-web/src/router.rs +++ b/peach-web/src/router.rs @@ -14,8 +14,8 @@ use crate::routes::{ /// catchers. This gives us everything we need to run PeachPub and excludes /// settings and status routes related to networking and the device (memory, /// hard disk, CPU etc.). -pub fn mount_peachpub_routes() -> Rocket { - rocket::build() +pub fn mount_peachpub_routes(rocket: Rocket) -> Rocket { + rocket .mount( "/", routes![ @@ -66,8 +66,8 @@ pub fn mount_peachpub_routes() -> Rocket { /// Create a Rocket instance with PeachPub routes, fileserver and catchers by /// calling `mount_peachpub_routes()` and then mount all additional routes /// required to run a complete PeachCloud build. -pub fn mount_peachcloud_routes() -> Rocket { - mount_peachpub_routes() +pub fn mount_peachcloud_routes(rocket: Rocket) -> Rocket { + mount_peachpub_routes(rocket) .mount( "/settings/network", routes![ diff --git a/peach-web/src/routes/authentication.rs b/peach-web/src/routes/authentication.rs index 97ea167..e3e51a3 100644 --- a/peach-web/src/routes/authentication.rs +++ b/peach-web/src/routes/authentication.rs @@ -7,7 +7,6 @@ use rocket::{ request::{self, FlashMessage, FromRequest, Request}, response::{Flash, Redirect}, serde::Deserialize, - Config, }; use rocket_dyn_templates::{tera::Context, Template}; @@ -15,6 +14,8 @@ use peach_lib::{error::PeachError, password_utils}; use crate::error::PeachWebError; use crate::utils::TemplateOrRedirect; +//use crate::DisableAuth; +use crate::RocketConfig; // HELPERS AND STRUCTS FOR AUTHENTICATION WITH COOKIES @@ -42,14 +43,14 @@ impl<'r> FromRequest<'r> for Authenticated { type Error = LoginError; async fn from_request(req: &'r Request<'_>) -> request::Outcome { - // check for `disable_auth` config value; set to `false` if unset - // can be set via the `ROCKET_DISABLE_AUTH` environment variable - // - env var, if set, takes precedence over value defined in `Rocket.toml` - let authentication_is_disabled: bool = match Config::figment().find_value("disable_auth") { - // deserialize the boolean value; set to `false` if an error is encountered - Ok(value) => value.deserialize().unwrap_or(false), - Err(_) => false, - }; + // retrieve auth state from managed state (returns `Option`). + // this value is read from the Rocket.toml config file on start-up + let authentication_is_disabled: bool = *req + .rocket() + .state::() + .map(|config| (&config.disable_auth)) + .unwrap_or(&false); + if authentication_is_disabled { let auth = Authenticated {}; request::Outcome::Success(auth) diff --git a/peach-web/src/routes/index.rs b/peach-web/src/routes/index.rs index 54867b7..a703ed1 100644 --- a/peach-web/src/routes/index.rs +++ b/peach-web/src/routes/index.rs @@ -1,19 +1,20 @@ -use rocket::{get, request::FlashMessage}; +use rocket::{get, request::FlashMessage, State}; use rocket_dyn_templates::{tera::Context, Template}; use crate::routes::authentication::Authenticated; -use crate::STANDALONE_MODE; +use crate::RocketConfig; // HELPERS AND ROUTES FOR / (HOME PAGE) #[get("/")] -pub fn home(_auth: Authenticated) -> Template { +pub fn home(_auth: Authenticated, config: &State) -> Template { let mut context = Context::new(); context.insert("flash_name", &None::<()>); context.insert("flash_msg", &None::<()>); context.insert("title", &None::<()>); - // pass in mode so we can define appropriate urls in template - context.insert("standalone_mode", &*STANDALONE_MODE); + + // pass in mode from managed state so we can define appropriate urls in template + context.insert("standalone_mode", &config.standalone_mode); Template::render("home", &context.into_json()) } diff --git a/peach-web/src/routes/status/scuttlebutt.rs b/peach-web/src/routes/status/scuttlebutt.rs index c1f532a..5ed3df1 100644 --- a/peach-web/src/routes/status/scuttlebutt.rs +++ b/peach-web/src/routes/status/scuttlebutt.rs @@ -1,4 +1,4 @@ -use rocket::{get, request::FlashMessage}; +use rocket::get; use rocket_dyn_templates::{tera::Context, Template}; use crate::routes::authentication::Authenticated;