From 803b1e43213cb63b2a93246e70032a5bfb126277 Mon Sep 17 00:00:00 2001 From: Navid Yaghoobi Date: Sun, 23 Jun 2024 11:13:23 +1000 Subject: [PATCH] Initial commit Signed-off-by: Navid Yaghoobi --- .github/ISSUE_TEMPLATE/bug_report.md | 23 ++ .github/ISSUE_TEMPLATE/feature_request.md | 17 ++ .github/workflows/stale.yml | 25 ++ .github/workflows/validation.yml | 55 ++++ .gitignore | 5 + .pre-commit-config.yaml | 11 + Cargo.toml | 16 ++ FEATURES.md | 56 +++++ Makefile | 35 +++ README.md | 45 +++- examples/clocksource.rs | 25 ++ examples/cooling.rs | 16 ++ examples/dmi.rs | 26 ++ examples/thermal.rs | 23 ++ examples/watchdog.rs | 24 ++ src/error.rs | 29 +++ src/lib.rs | 5 + src/sysfs/class_cooling.rs | 139 +++++++++++ src/sysfs/class_dmi.rs | 291 ++++++++++++++++++++++ src/sysfs/class_thermal.rs | 176 +++++++++++++ src/sysfs/class_watchdog.rs | 259 +++++++++++++++++++ src/sysfs/clocksource.rs | 152 +++++++++++ src/sysfs/mod.rs | 5 + src/utils.rs | 31 +++ 24 files changed, 1488 insertions(+), 1 deletion(-) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/workflows/stale.yml create mode 100644 .github/workflows/validation.yml create mode 100644 .pre-commit-config.yaml create mode 100644 Cargo.toml create mode 100644 FEATURES.md create mode 100644 Makefile create mode 100644 examples/clocksource.rs create mode 100644 examples/cooling.rs create mode 100644 examples/dmi.rs create mode 100644 examples/thermal.rs create mode 100644 examples/watchdog.rs create mode 100644 src/error.rs create mode 100644 src/lib.rs create mode 100644 src/sysfs/class_cooling.rs create mode 100644 src/sysfs/class_dmi.rs create mode 100644 src/sysfs/class_thermal.rs create mode 100644 src/sysfs/class_watchdog.rs create mode 100644 src/sysfs/clocksource.rs create mode 100644 src/sysfs/mod.rs create mode 100644 src/utils.rs diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..b342410 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,23 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: 'bug' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Additional context** +Add any other context about the problem here. \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..89c7eda --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,17 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: 'feature' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Additional context** +Add any other context or screenshots about the feature request here. \ No newline at end of file diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 0000000..25ae3a2 --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,25 @@ +name: Mark stale issues and pull requests + +# Please refer to https://github.com/actions/stale/blob/master/action.yml +# to see all config knobs of the stale action. + +on: + schedule: + - cron: "0 0 * * *" + +jobs: + stale: + + runs-on: ubuntu-latest + + steps: + - uses: actions/stale@v9 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + stale-issue-message: 'A friendly reminder that this issue had no activity for 30 days.' + stale-pr-message: 'A friendly reminder that this PR had no activity for 30 days.' + stale-issue-label: 'stale-issue' + stale-pr-label: 'stale-pr' + days-before-stale: 30 + days-before-close: 365 + remove-stale-when-updated: true \ No newline at end of file diff --git a/.github/workflows/validation.yml b/.github/workflows/validation.yml new file mode 100644 index 0000000..ff9f356 --- /dev/null +++ b/.github/workflows/validation.yml @@ -0,0 +1,55 @@ +name: validation +on: + pull_request: + push: + branches: [ main ] + +jobs: + pre-commit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + - uses: pre-commit/action@v3.0.1 + + DCO-check: + runs-on: ubuntu-latest + steps: + - name: get pr commits + id: 'get-pr-commits' + uses: tim-actions/get-pr-commits@v1.3.1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + + - name: DCO check + uses: tim-actions/dco@master + with: + commits: ${{ steps.get-pr-commits.outputs.commits }} + + - name: check subject line length + uses: tim-actions/commit-message-checker-with-regex@v0.3.2 + with: + commits: ${{ steps.get-pr-commits.outputs.commits }} + pattern: '^.{0,72}(\n.*)*$' + error: 'Subject too long (max 72)' + + codespell: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: codespell-project/actions-codespell@master + with: + check_filenames: true + ignore_words_list: crate + + validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: make validate + + tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: make test diff --git a/.gitignore b/.gitignore index 6985cf1..196e176 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,8 @@ Cargo.lock # MSVC Windows builds of rustc generate these, which store debugging information *.pdb + + +# Added by cargo + +/target diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..2d23d18 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,11 @@ +--- +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks.git + rev: v3.4.0 + hooks: + - id: end-of-file-fixer + - id: trailing-whitespace + - id: mixed-line-ending + - id: check-byte-order-marker + - id: check-executables-have-shebangs + - id: check-merge-conflict \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..edadf28 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "procsys" +version = "0.1.0" +edition = "2021" +authors = ["github.com/navidys/sysmetrics"] +license = "MIT" +readme = "README.md" +description = "Rust library to retrieve system, kernel, and process metrics from the pseudo-filesystems /proc and /sys" + +[dependencies] +env_logger = "0.11.3" +getset = "0.1.2" +log = "0.4.21" +serde = { version = "1.0.203", features = ["derive"] } +serde_json = "1.0.117" +walkdir = "2.5.0" diff --git a/FEATURES.md b/FEATURES.md new file mode 100644 index 0000000..e80d6b1 --- /dev/null +++ b/FEATURES.md @@ -0,0 +1,56 @@ +Supported Features + +* `/sys/class/` + * ✅ `dmi/id` + * `bios_date` + * `bios_release` + * `bios_vendor` + * `bios_version` + * `board_asset_tag` + * `board_name` + * `board_serial` + * `board_vendor` + * `board_version` + * `chassis_asset_tag` + * `chassis_serial` + * `chassis_type` + * `chassis_vendor` + * `product_family` + * `product_name` + * `product_serial` + * `product_sku` + * `product_uuid` + * `sys_vendor` + + * ✅ `thermal/cooling_device` + * `type` + * `max_state` + * `cur_state` + + * ✅ `thermal/thermal_zone` + * `type` + * `temp` + * `policy` + * `mode` + * `passive` + + * ✅ `watchdog/` + * `bootstatus` + * `options` + * `fw_version` + * `identity` + * `nowayout` + * `state` + * `status` + * `timeleft` + * `timeout` + * `min_timeout` + * `max_timeout` + * `pretimeout` + * `pretimeout_governor` + * `access_cs0` + +* `/sys/devices/system/` + * ✅ `clocksource/clocksource` + * `available_clocksource` + * `current_clocksource` \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..9d89dcc --- /dev/null +++ b/Makefile @@ -0,0 +1,35 @@ +# Get crate version by parsing the line that starts with version. +CRATE_VERSION ?= $(shell grep ^version Cargo.toml | awk '{print $$3}') + +# Set path to cargo executable +CARGO ?= cargo + +.PHONY: validate +validate: $(CARGO_TARGET_DIR) ## Validate code + $(CARGO) fmt --all -- --check + $(CARGO) clippy -p procsys@$(CRATE_VERSION) -- -D warnings + +.PHONY: clean +clean: ## Cleanup + rm -rf target + +.PHONY: test +test: $(CARGO_TARGET_DIR) ## Run unit tests + $(CARGO) test + +.PHONY: codespell +codespell: ## Run codespell + @echo "running codespell" + @codespell -S ./target,./targets -L crate + +_HLP_TGTS_RX = '^[[:print:]]+:.*?\#\# .*$$' +_HLP_TGTS_CMD = grep -E $(_HLP_TGTS_RX) $(MAKEFILE_LIST) +_HLP_TGTS_LEN = $(shell $(_HLP_TGTS_CMD) | cut -d : -f 1 | wc -L) +_HLPFMT = "%-$(_HLP_TGTS_LEN)s %s\n" +.PHONY: help +help: ## Print listing of key targets with their descriptions + @printf $(_HLPFMT) "Target:" "Description:" + @printf $(_HLPFMT) "--------------" "--------------------" + @$(_HLP_TGTS_CMD) | sort | \ + awk 'BEGIN {FS = ":(.*)?## "}; \ + {printf $(_HLPFMT), $$1, $$2}' \ No newline at end of file diff --git a/README.md b/README.md index 7b7257b..fc67b0e 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,45 @@ -# procsys +# procsys [![][img_crates]][crates] [![][img_doc]][doc] + Rust library to retrieve system, kernel, and process metrics from the pseudo-filesystems /proc and /sys. + +See the docs for more information about supported features, or view the [features.md](https://github.com/navidys/procsys/blob/main/FEATURES.md) file of the project repository. + +## Examples + +There are several examples in the documents and also in the [examples](https://github.com/navidys/procsys/tree/main/examples) directory of project repository. + + +``` +use procsys::sysfs::class_watchdog; + +fn main() { + let watchdog_devices = class_watchdog::collect(); + + for wdev in &watchdog_devices { + println!("name: {}", wdev.name()); + println!("boot status: {}", wdev.boot_status().unwrap_or_default()); + println!("timeout: {}", wdev.timeout().unwrap_or_default()); + println!("min_timeout: {}", wdev.min_timeout().unwrap_or_default()); + println!("max_timeout: {}", wdev.max_timeout().unwrap_or_default()); + } + + // print all watchdog devices information in json output + match serde_json::to_string_pretty(&watchdog_devices) { + Ok(output) => println!("{}", output), + Err(err) => { + log::error!("{}", err); + std::process::exit(1); + } + } +} +``` + +## License + +Licensed under the [MIT License](https://github.com/navidys/procsys/blob/main/LICENSE). + +[img_crates]: https://img.shields.io/crates/v/procsys.svg +[img_doc]: https://img.shields.io/badge/rust-documentation-blue.svg + +[crates]: https://crates.io/crates/procsys +[doc]: https://docs.rs/procsys/ \ No newline at end of file diff --git a/examples/clocksource.rs b/examples/clocksource.rs new file mode 100644 index 0000000..45f1fb0 --- /dev/null +++ b/examples/clocksource.rs @@ -0,0 +1,25 @@ +use procsys::sysfs; + +fn main() { + env_logger::init(); + + let clocksources = sysfs::clocksource::collect(); + + for clock_src in &clocksources { + println!("name: {}", clock_src.name()); + println!( + "available clocksource: {}", + clock_src.available_clocksource().join(" "), + ); + println!("current clocksource: {}", clock_src.current_clocksource()); + } + + // print all clocksources information in json output + match serde_json::to_string_pretty(&clocksources) { + Ok(output) => println!("{}", output), + Err(err) => { + log::error!("{}", err); + std::process::exit(1); + } + } +} diff --git a/examples/cooling.rs b/examples/cooling.rs new file mode 100644 index 0000000..a9336b0 --- /dev/null +++ b/examples/cooling.rs @@ -0,0 +1,16 @@ +use procsys::sysfs; + +fn main() { + env_logger::init(); + + let cooling_devices = sysfs::class_cooling::collect(); + + // print all cooling devices information in json output + match serde_json::to_string_pretty(&cooling_devices) { + Ok(output) => println!("{}", output), + Err(err) => { + log::error!("{}", err); + std::process::exit(1); + } + } +} diff --git a/examples/dmi.rs b/examples/dmi.rs new file mode 100644 index 0000000..6fce8d6 --- /dev/null +++ b/examples/dmi.rs @@ -0,0 +1,26 @@ +use procsys::sysfs; + +fn main() { + env_logger::init(); + + let dmi_info = sysfs::class_dmi::collect(); + + println!( + "bios date: {}", + dmi_info.bios_date().to_owned().unwrap_or_default(), + ); + + println!( + "bios release: {}", + dmi_info.bios_release().to_owned().unwrap_or_default(), + ); + + // print all DMI information in json output + match serde_json::to_string_pretty(&dmi_info) { + Ok(output) => println!("{}", output), + Err(err) => { + log::error!("{}", err); + std::process::exit(1); + } + } +} diff --git a/examples/thermal.rs b/examples/thermal.rs new file mode 100644 index 0000000..ff4cfb2 --- /dev/null +++ b/examples/thermal.rs @@ -0,0 +1,23 @@ +use procsys::sysfs::class_thermal; + +fn main() { + env_logger::init(); + + let thermal_devices = class_thermal::collect(); + + for tdev in &thermal_devices { + println!("name: {}", tdev.name()); + println!("temperature: {}", tdev.temp()); + println!("type: {}", tdev.zone_type()); + println!("policy: {}", tdev.zone_type()); + } + + // print all thermal devices information in json output + match serde_json::to_string_pretty(&thermal_devices) { + Ok(output) => println!("{}", output), + Err(err) => { + log::error!("{}", err); + std::process::exit(1); + } + } +} diff --git a/examples/watchdog.rs b/examples/watchdog.rs new file mode 100644 index 0000000..089cde0 --- /dev/null +++ b/examples/watchdog.rs @@ -0,0 +1,24 @@ +use procsys::sysfs::class_watchdog; + +fn main() { + env_logger::init(); + + let watchdog_devices = class_watchdog::collect(); + + for wdev in &watchdog_devices { + println!("name: {}", wdev.name()); + println!("boot status: {}", wdev.boot_status().unwrap_or_default()); + println!("timeout: {}", wdev.timeout().unwrap_or_default()); + println!("min_timeout: {}", wdev.min_timeout().unwrap_or_default()); + println!("max_timeout: {}", wdev.max_timeout().unwrap_or_default()); + } + + // print all watchdog devices information in json output + match serde_json::to_string_pretty(&watchdog_devices) { + Ok(output) => println!("{}", output), + Err(err) => { + log::error!("{}", err); + std::process::exit(1); + } + } +} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..bcb45b0 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,29 @@ +use std::{fmt, path::PathBuf}; + +/// An error received from sysmetrics +#[derive(Debug)] +pub enum MetricError { + /// Platform does not support Desktop Management Interface (DMI) information + DmiSupportError, + + /// IO read error + IOError(PathBuf, std::io::Error), + + /// json serde pretty error + SerdeJsonError(serde_json::Error), +} + +impl fmt::Display for MetricError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + MetricError::DmiSupportError => write!( + f, + "platform does not support Desktop Management Interface (DMI) information", + ), + MetricError::IOError(ref p, ref e) => { + write!(f, "cannot read sysfs {:?}: {}", p, e) + } + MetricError::SerdeJsonError(ref e) => write!(f, "json pretty error: {}", e), + } + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..0775349 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,5 @@ +#![doc = include_str!("../README.md")] + +pub mod error; +pub mod sysfs; +pub mod utils; diff --git a/src/sysfs/class_cooling.rs b/src/sysfs/class_cooling.rs new file mode 100644 index 0000000..cdff9bb --- /dev/null +++ b/src/sysfs/class_cooling.rs @@ -0,0 +1,139 @@ +use std::path::{Path, PathBuf}; + +use getset::Getters; +use serde::Serialize; +use walkdir::WalkDir; + +use crate::utils; + +enum CoolingInfo { + CoolingType, + MaxState, + CurState, + Unknown, +} + +impl CoolingInfo { + fn from(name: &str) -> CoolingInfo { + match name { + "type" => CoolingInfo::CoolingType, + "max_state" => CoolingInfo::MaxState, + "cur_state" => CoolingInfo::CurState, + _ => CoolingInfo::Unknown, + } + } +} + +/// Cooling contains a cooling device information. +/// # Example +/// ``` +/// use procsys::sysfs::class_cooling; +/// +/// let cooling_devices = class_cooling::collect(); +/// let json_output = serde_json::to_string_pretty(&cooling_devices).unwrap(); +/// println!("{}", json_output); +/// +/// ``` +#[derive(Debug, Serialize, Clone, Getters)] +pub struct Cooling { + #[getset(get = "pub")] + name: String, + + #[getset(get = "pub")] + cooling_type: String, + + #[getset(get = "pub")] + max_state: i64, + + #[getset(get = "pub")] + cur_state: i64, +} + +impl Cooling { + fn new(name: &str) -> Self { + Self { + name: name.to_string(), + cooling_type: String::new(), + max_state: i64::from(0), + cur_state: i64::from(0), + } + } +} + +pub fn collect() -> Vec { + let mut cooling_devs = Vec::new(); + let cooling_class_path = Path::new("/sys/class/thermal/"); + + for cdevice in cooling_devices(cooling_class_path) { + let mut cooling_device = Cooling::new(&cdevice); + let mut cdev_path = PathBuf::from(cooling_class_path); + + cdev_path.push(&cdevice); + + for cdev_info in WalkDir::new(&cdev_path).into_iter().filter_map(|e| e.ok()) { + let cdev_info_name = cdev_info + .file_name() + .to_str() + .unwrap_or_default() + .to_string(); + + if cdev_info_name == cdevice { + continue; + } + + match CoolingInfo::from(&cdev_info_name) { + CoolingInfo::CoolingType => { + if let Some(c) = + utils::collect_info_string(&cdev_info_name, cdev_path.as_path()) + { + cooling_device.cooling_type = c; + } + } + CoolingInfo::MaxState => { + if let Some(c) = utils::collect_info_i64(&cdev_info_name, cdev_path.as_path()) { + cooling_device.max_state = c; + } + } + CoolingInfo::CurState => { + if let Some(c) = utils::collect_info_i64(&cdev_info_name, cdev_path.as_path()) { + cooling_device.cur_state = c; + } + } + CoolingInfo::Unknown => {} + } + } + + cooling_devs.push(cooling_device); + } + + cooling_devs +} + +fn cooling_devices(class_path: &Path) -> Vec { + let mut devices = Vec::new(); + + for tdev in WalkDir::new(class_path).into_iter().filter_map(|e| e.ok()) { + if tdev.file_name() == "thermal" { + continue; + } + + let tdev_name = tdev.file_name().to_str().unwrap_or_default(); + + if tdev_name.starts_with("cooling_device") { + devices.push(tdev_name.to_string()); + } + } + + devices +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn cooling_devices() { + let cdev = collect(); + assert!(!cdev.is_empty()) + } +} diff --git a/src/sysfs/class_dmi.rs b/src/sysfs/class_dmi.rs new file mode 100644 index 0000000..d0d9834 --- /dev/null +++ b/src/sysfs/class_dmi.rs @@ -0,0 +1,291 @@ +use crate::{error::MetricError, utils}; +use getset::Getters; +use serde::Serialize; +use std::path::Path; +use walkdir::WalkDir; + +enum DMIType { + BiosDate, + BiosRelease, + BiosVendor, + BiosVersion, + BoardAssetTag, + BoardName, + BoardSerial, + BoardVendor, + BoardVersion, + ChassisAssetTag, + ChassisSerial, + ChassisType, + ChassisVendor, + ChassisVersion, + ProductFamily, + ProductName, + ProductSerial, + ProductSku, + ProductUuid, + SystemVendor, + Unknown, +} + +impl DMIType { + fn from(name: &str) -> DMIType { + match name { + "bios_date" => DMIType::BiosDate, + "bios_release" => DMIType::BiosRelease, + "bios_vendor" => DMIType::BiosVendor, + "bios_version" => DMIType::BiosVersion, + "board_asset_tag" => DMIType::BoardAssetTag, + "board_name" => DMIType::BoardName, + "board_serial" => DMIType::BoardSerial, + "board_vendor" => DMIType::BoardVendor, + "board_version" => DMIType::BoardVersion, + "chassis_asset_tag" => DMIType::ChassisAssetTag, + "chassis_serial" => DMIType::ChassisSerial, + "chassis_type" => DMIType::ChassisType, + "chassis_vendor" => DMIType::ChassisVendor, + "chassis_version" => DMIType::ChassisVersion, + "product_family" => DMIType::ProductFamily, + "product_name" => DMIType::ProductName, + "product_serial" => DMIType::ProductSerial, + "product_sku" => DMIType::ProductSku, + "product_uuid" => DMIType::ProductUuid, + "sys_vendor" => DMIType::SystemVendor, + _ => DMIType::Unknown, + } + } +} + +/// The DMI contains the content of Desktop Management Interface. +/// # Example +/// ``` +/// use procsys::sysfs::class_dmi; +/// +/// let dmi_info = class_dmi::collect(); +/// +/// println!("bios date: {}", dmi_info.bios_date().to_owned().unwrap_or_default()); +/// println!("board serial: {}", dmi_info.board_serial().to_owned().unwrap_or_default()); +/// +/// // print all dmi information in json format +/// let json_output = serde_json::to_string_pretty(&dmi_info).unwrap(); +/// println!("{}", json_output); +/// ``` +#[derive(Debug, Serialize, Clone, Getters)] +pub struct DMI { + #[getset(get = "pub")] + bios_date: Option, + + #[getset(get = "pub")] + bios_release: Option, + + #[getset(get = "pub")] + bios_vendor: Option, + + #[getset(get = "pub")] + bios_version: Option, + + #[getset(get = "pub")] + board_asset_tag: Option, + + #[getset(get = "pub")] + board_name: Option, + + #[getset(get = "pub")] + board_serial: Option, + + #[getset(get = "pub")] + board_vendor: Option, + + #[getset(get = "pub")] + board_version: Option, + + #[getset(get = "pub")] + chassis_asset_tag: Option, + + #[getset(get = "pub")] + chassis_serial: Option, + + #[getset(get = "pub")] + chassis_type: Option, + + #[getset(get = "pub")] + chassis_vendor: Option, + + #[getset(get = "pub")] + chassis_version: Option, + + #[getset(get = "pub")] + product_family: Option, + + #[getset(get = "pub")] + product_name: Option, + + #[getset(get = "pub")] + product_serial: Option, + + #[getset(get = "pub")] + product_sku: Option, + + #[getset(get = "pub")] + product_uuid: Option, + + #[getset(get = "pub")] + product_version: Option, + + #[getset(get = "pub")] + system_vendor: Option, +} + +impl DMI { + fn new() -> Self { + Self { + bios_date: None, + bios_release: None, + bios_vendor: None, + bios_version: None, + board_asset_tag: None, + board_name: None, + board_serial: None, + board_vendor: None, + board_version: None, + chassis_asset_tag: None, + chassis_serial: None, + chassis_type: None, + chassis_vendor: None, + chassis_version: None, + product_family: None, + product_name: None, + product_serial: None, + product_sku: None, + product_uuid: None, + product_version: None, + system_vendor: None, + } + } +} + +/// attempts to collect dmi information. +pub fn collect() -> DMI { + let mut dmi = DMI::new(); + + let dmi_class_path = Path::new("/sys/class/dmi/id"); + if !dmi_class_path.exists() { + log::error!("{}", MetricError::DmiSupportError); + + return dmi; + } + + for device in dmi_devices(dmi_class_path) { + match DMIType::from(device.as_str()) { + DMIType::BiosDate => { + dmi.bios_date = utils::collect_info_string(&device, dmi_class_path); + } + + DMIType::BiosRelease => { + dmi.bios_release = utils::collect_info_string(&device, dmi_class_path); + } + + DMIType::BiosVendor => { + dmi.bios_vendor = utils::collect_info_string(&device, dmi_class_path); + } + + DMIType::BiosVersion => { + dmi.bios_version = utils::collect_info_string(&device, dmi_class_path); + } + + DMIType::BoardAssetTag => { + dmi.board_asset_tag = utils::collect_info_string(&device, dmi_class_path); + } + + DMIType::BoardName => { + dmi.board_name = utils::collect_info_string(&device, dmi_class_path); + } + + DMIType::BoardSerial => { + dmi.board_serial = utils::collect_info_string(&device, dmi_class_path); + } + + DMIType::BoardVendor => { + dmi.board_vendor = utils::collect_info_string(&device, dmi_class_path); + } + + DMIType::BoardVersion => { + dmi.board_version = utils::collect_info_string(&device, dmi_class_path); + } + + DMIType::ChassisAssetTag => { + dmi.chassis_asset_tag = utils::collect_info_string(&device, dmi_class_path); + } + + DMIType::ChassisSerial => { + dmi.chassis_serial = utils::collect_info_string(&device, dmi_class_path); + } + + DMIType::ChassisType => { + dmi.chassis_type = utils::collect_info_string(&device, dmi_class_path); + } + + DMIType::ChassisVendor => { + dmi.chassis_vendor = utils::collect_info_string(&device, dmi_class_path); + } + + DMIType::ChassisVersion => { + dmi.chassis_version = utils::collect_info_string(&device, dmi_class_path); + } + + DMIType::ProductFamily => { + dmi.product_family = utils::collect_info_string(&device, dmi_class_path); + } + + DMIType::ProductName => { + dmi.product_name = utils::collect_info_string(&device, dmi_class_path); + } + + DMIType::ProductSerial => { + dmi.product_serial = utils::collect_info_string(&device, dmi_class_path); + } + + DMIType::ProductSku => { + dmi.product_sku = utils::collect_info_string(&device, dmi_class_path); + } + + DMIType::ProductUuid => { + dmi.product_uuid = utils::collect_info_string(&device, dmi_class_path); + } + + DMIType::SystemVendor => { + dmi.system_vendor = utils::collect_info_string(&device, dmi_class_path); + } + + DMIType::Unknown => {} + } + } + + dmi +} + +fn dmi_devices(class_path: &Path) -> Vec { + let mut devices = Vec::new(); + + for tdev in WalkDir::new(class_path).into_iter().filter_map(|e| e.ok()) { + if tdev.file_name() == "id" { + continue; + } + + let tdev_name = tdev.file_name().to_str().unwrap_or_default(); + devices.push(tdev_name.to_string()); + } + + devices +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn dmi_collect() { + let dmi_result = collect(); + assert!(dmi_result.bios_date.is_some()) + } +} diff --git a/src/sysfs/class_thermal.rs b/src/sysfs/class_thermal.rs new file mode 100644 index 0000000..7b22a83 --- /dev/null +++ b/src/sysfs/class_thermal.rs @@ -0,0 +1,176 @@ +use std::path::{Path, PathBuf}; + +use getset::Getters; +use serde::Serialize; +use walkdir::WalkDir; + +use crate::utils; + +enum ThermalZoneInfo { + ZoneType, + Temp, + Policy, + Mode, + Passive, + Unknown, +} + +impl ThermalZoneInfo { + fn from(name: &str) -> ThermalZoneInfo { + match name { + "type" => ThermalZoneInfo::ZoneType, + "temp" => ThermalZoneInfo::Temp, + "policy" => ThermalZoneInfo::Policy, + "mode" => ThermalZoneInfo::Mode, + "passive" => ThermalZoneInfo::Passive, + _ => ThermalZoneInfo::Unknown, + } + } +} + +/// ThermalZone contains info from files in /sys/class/thermal/thermal_zoneX. +/// #Example +/// ``` +/// use procsys::sysfs::class_thermal; +/// let thermal_devices = class_thermal::collect(); +/// +/// for tdev in &thermal_devices { +/// println!("name: {}", tdev.name()); +/// println!("temperature: {}", tdev.temp()); +/// println!("type: {}", tdev.zone_type()); +/// println!("policy: {}", tdev.zone_type()); +/// } +/// +/// // print all thermal devices information in json format +/// let json_output = serde_json::to_string_pretty(&thermal_devices).unwrap(); +/// println!("{}", json_output); +/// +/// ``` +#[derive(Debug, Serialize, Clone, Getters)] +pub struct ThermalZone { + #[getset(get = "pub")] + name: String, + + #[getset(get = "pub")] + zone_type: String, + + #[getset(get = "pub")] + policy: String, + + #[getset(get = "pub")] + temp: i64, + + #[getset(get = "pub")] + mode: Option, + + #[getset(get = "pub")] + passive: Option, +} + +impl ThermalZone { + fn new(name: &str) -> Self { + Self { + name: name.to_string(), + zone_type: String::new(), + policy: String::new(), + temp: i64::from(0), + mode: None, + passive: None, + } + } +} + +pub fn collect() -> Vec { + let mut thermal_zone_devices = Vec::new(); + let thermal_zone_class_path = Path::new("/sys/class/thermal/"); + + for tdevice in thermal_devices(thermal_zone_class_path) { + let mut thermal_device = ThermalZone::new(&tdevice); + let mut tdev_path = PathBuf::from(thermal_zone_class_path); + + tdev_path.push(&tdevice); + + for tdev_info in WalkDir::new(&tdev_path).into_iter().filter_map(|e| e.ok()) { + let tdev_info_name = tdev_info + .file_name() + .to_str() + .unwrap_or_default() + .to_string(); + + if tdev_info_name == tdevice { + continue; + } + + match ThermalZoneInfo::from(&tdev_info_name) { + ThermalZoneInfo::Mode => { + if let Some(c) = + utils::collect_info_string(&tdev_info_name, tdev_path.as_path()) + { + match c.as_str() { + "enabled" => thermal_device.mode = Some(true), + "disabled" => thermal_device.mode = Some(false), + _ => thermal_device.mode = None, + } + } + } + ThermalZoneInfo::Temp => { + if let Some(c) = utils::collect_info_i64(&tdev_info_name, tdev_path.as_path()) { + thermal_device.temp = c; + } + } + ThermalZoneInfo::Passive => { + thermal_device.passive = + utils::collect_info_i64(&tdev_info_name, tdev_path.as_path()); + } + ThermalZoneInfo::Policy => { + if let Some(c) = + utils::collect_info_string(&tdev_info_name, tdev_path.as_path()) + { + thermal_device.policy = c; + } + } + ThermalZoneInfo::ZoneType => { + if let Some(c) = + utils::collect_info_string(&tdev_info_name, tdev_path.as_path()) + { + thermal_device.zone_type = c; + } + } + ThermalZoneInfo::Unknown => {} + } + } + + thermal_zone_devices.push(thermal_device); + } + + thermal_zone_devices +} + +fn thermal_devices(class_path: &Path) -> Vec { + let mut devices = Vec::new(); + + for tdev in WalkDir::new(class_path).into_iter().filter_map(|e| e.ok()) { + if tdev.file_name() == "thermal" { + continue; + } + + let tdev_name = tdev.file_name().to_str().unwrap_or_default(); + + if tdev_name.starts_with("thermal_zone") { + devices.push(tdev_name.to_string()); + } + } + + devices +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn thermal_devices() { + let tdev = collect(); + assert!(!tdev.is_empty()) + } +} diff --git a/src/sysfs/class_watchdog.rs b/src/sysfs/class_watchdog.rs new file mode 100644 index 0000000..5d02bb4 --- /dev/null +++ b/src/sysfs/class_watchdog.rs @@ -0,0 +1,259 @@ +use std::path::{Path, PathBuf}; + +use getset::Getters; +use serde::Serialize; +use walkdir::WalkDir; + +use crate::utils; + +enum WatchdogInfo { + BootStatus, + Options, + FwVersion, + Identity, + Nowayout, + State, + Status, + Timeleft, + Timeout, + MinTimeout, + MaxTimeout, + Pretimeout, + PretimeoutGovernor, + AccessCs0, + Unknown, +} + +impl WatchdogInfo { + fn from(name: &str) -> WatchdogInfo { + match name { + "bootstatus" => WatchdogInfo::BootStatus, + "options" => WatchdogInfo::Options, + "fw_version" => WatchdogInfo::FwVersion, + "identity" => WatchdogInfo::Identity, + "nowayout" => WatchdogInfo::Nowayout, + "state" => WatchdogInfo::State, + "status" => WatchdogInfo::Status, + "timeleft" => WatchdogInfo::Timeleft, + "timeout" => WatchdogInfo::Timeout, + "min_timeout" => WatchdogInfo::MinTimeout, + "max_timeout" => WatchdogInfo::MaxTimeout, + "pretimeout" => WatchdogInfo::Pretimeout, + "pretimeout_governor" => WatchdogInfo::PretimeoutGovernor, + "access_cs0" => WatchdogInfo::AccessCs0, + _ => WatchdogInfo::Unknown, + } + } +} + +/// Watchdog contains a watchdog device stat information. +/// # Example +/// ``` +/// use procsys::sysfs::class_watchdog; +/// let watchdog_devices = class_watchdog::collect(); +/// +/// for wdev in &watchdog_devices { +/// println!("name: {}", wdev.name()); +/// println!("timeout: {}", wdev.timeout().unwrap_or_default()); +/// println!("min_timeout: {}", wdev.min_timeout().unwrap_or_default()); +/// println!("max_timeout: {}", wdev.max_timeout().unwrap_or_default()); +/// } +/// +/// // print all watchdog devices information in json format +/// let json_output = serde_json::to_string_pretty(&watchdog_devices).unwrap(); +/// println!("{}", json_output); +/// +/// ``` +#[derive(Debug, Serialize, Clone, Getters)] +pub struct Watchdog { + #[getset(get = "pub")] + name: String, + + #[getset(get = "pub")] + boot_status: Option, + + #[getset(get = "pub")] + options: Option, + + #[getset(get = "pub")] + fw_version: Option, + + #[getset(get = "pub")] + identity: Option, + + #[getset(get = "pub")] + nowayout: Option, + + #[getset(get = "pub")] + state: Option, + + #[getset(get = "pub")] + status: Option, + + #[getset(get = "pub")] + timeleft: Option, + + #[getset(get = "pub")] + timeout: Option, + + #[getset(get = "pub")] + min_timeout: Option, + + #[getset(get = "pub")] + max_timeout: Option, + + #[getset(get = "pub")] + pretimeout: Option, + + #[getset(get = "pub")] + pretimeout_governor: Option, + + #[getset(get = "pub")] + access_cs0: Option, +} + +impl Watchdog { + fn new(name: String) -> Self { + Self { + name, + boot_status: None, + options: None, + fw_version: None, + identity: None, + nowayout: None, + state: None, + status: None, + timeleft: None, + timeout: None, + min_timeout: None, + max_timeout: None, + pretimeout: None, + pretimeout_governor: None, + access_cs0: None, + } + } +} + +pub fn collect() -> Vec { + let mut devices = Vec::new(); + let watchdog_class_path = Path::new("/sys/class/watchdog/"); + for device in watchdog_devices(watchdog_class_path) { + let mut watchdog_dev = Watchdog::new(device.to_owned()); + + let mut wdev_path = PathBuf::from(watchdog_class_path); + wdev_path.push(&device); + + for dev_info in WalkDir::new(&wdev_path).into_iter().filter_map(|e| e.ok()) { + if *dev_info + .file_name() + .to_str() + .unwrap_or_default() + .to_string() + == device + { + continue; + } + + let wdev_filename = dev_info + .file_name() + .to_str() + .unwrap_or_default() + .to_string(); + + match WatchdogInfo::from(wdev_filename.as_str()) { + WatchdogInfo::BootStatus => { + watchdog_dev.boot_status = + utils::collect_info_i64(&wdev_filename, wdev_path.as_path()); + } + WatchdogInfo::Options => { + watchdog_dev.options = + utils::collect_info_string(&wdev_filename, wdev_path.as_path()); + } + WatchdogInfo::FwVersion => { + watchdog_dev.fw_version = + utils::collect_info_i64(&wdev_filename, wdev_path.as_path()); + } + WatchdogInfo::Identity => { + watchdog_dev.identity = + utils::collect_info_string(&wdev_filename, wdev_path.as_path()); + } + WatchdogInfo::Nowayout => { + watchdog_dev.nowayout = + utils::collect_info_i64(&wdev_filename, wdev_path.as_path()); + } + WatchdogInfo::State => { + watchdog_dev.state = + utils::collect_info_string(&wdev_filename, wdev_path.as_path()); + } + WatchdogInfo::Status => { + watchdog_dev.status = + utils::collect_info_string(&wdev_filename, wdev_path.as_path()); + } + WatchdogInfo::Timeleft => { + watchdog_dev.timeleft = + utils::collect_info_i64(&wdev_filename, wdev_path.as_path()); + } + WatchdogInfo::Timeout => { + watchdog_dev.timeout = + utils::collect_info_i64(&wdev_filename, wdev_path.as_path()); + } + WatchdogInfo::MinTimeout => { + watchdog_dev.min_timeout = + utils::collect_info_i64(&wdev_filename, wdev_path.as_path()); + } + WatchdogInfo::MaxTimeout => { + watchdog_dev.max_timeout = + utils::collect_info_i64(&wdev_filename, wdev_path.as_path()); + } + WatchdogInfo::Pretimeout => { + watchdog_dev.pretimeout = + utils::collect_info_i64(&wdev_filename, wdev_path.as_path()); + } + WatchdogInfo::PretimeoutGovernor => { + watchdog_dev.pretimeout_governor = + utils::collect_info_string(&wdev_filename, wdev_path.as_path()); + } + WatchdogInfo::AccessCs0 => { + watchdog_dev.access_cs0 = + utils::collect_info_i64(&wdev_filename, wdev_path.as_path()); + } + WatchdogInfo::Unknown => {} + } + } + + devices.push(watchdog_dev); + } + + devices +} + +fn watchdog_devices(class_path: &Path) -> Vec { + let mut devices = Vec::new(); + + for wdev in WalkDir::new(class_path).into_iter().filter_map(|e| e.ok()) { + if wdev.file_name() == "watchdog" { + continue; + } + + devices.push( + wdev.file_name() + .to_str() + .unwrap_or_default() + .trim() + .to_string(), + ) + } + + devices +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn watchdog_devices() { + let wdev = collect(); + assert!(!wdev.is_empty()); + } +} diff --git a/src/sysfs/clocksource.rs b/src/sysfs/clocksource.rs new file mode 100644 index 0000000..05c44da --- /dev/null +++ b/src/sysfs/clocksource.rs @@ -0,0 +1,152 @@ +use std::{fs, path::Path}; + +use getset::Getters; +use serde::Serialize; +use walkdir::WalkDir; + +use crate::error::MetricError; + +enum ClocksourceInfo { + AvailableClockSource, + CurrentClockSource, +} + +impl ClocksourceInfo { + fn into_string(self) -> String { + let info_str = match self { + ClocksourceInfo::AvailableClockSource => "available_clocksource", + ClocksourceInfo::CurrentClockSource => "current_clocksource", + }; + + info_str.to_string() + } +} + +/// Clocksource contains a clocksource information. +/// # Example +/// ``` +/// use procsys::sysfs::clocksource; +/// +/// let clocksources = clocksource::collect(); +/// +/// for clock_src in clocksources { +/// println!("name: {}", clock_src.name()); +/// println!("available clocksource: {:?}", clock_src.available_clocksource()); +/// println!("current clocksource: {}", clock_src.current_clocksource()); +/// } +/// +/// ``` +#[derive(Debug, Serialize, Clone, Getters)] +pub struct Clocksource { + #[getset(get = "pub")] + name: String, + + #[getset(get = "pub")] + available_clocksource: Vec, + + #[getset(get = "pub")] + current_clocksource: String, +} + +impl Clocksource { + fn new( + name: String, + available_clocksource: Vec, + current_clocksource: String, + ) -> Clocksource { + Self { + name, + available_clocksource, + current_clocksource, + } + } +} + +pub fn collect() -> Vec { + let mut clock_sources = Vec::new(); + let clock_source_path = Path::new("/sys/devices/system/clocksource"); + + for clock_dev in WalkDir::new(clock_source_path) + .into_iter() + .filter_map(|e| e.ok()) + { + if clock_dev.file_name() == "clocksource" { + continue; + } + + let clocksource_name = clock_dev + .file_name() + .to_str() + .unwrap_or_default() + .trim() + .to_string(); + + if clocksource_name.is_empty() { + continue; + } + + if !clocksource_name.starts_with("clocksource") { + continue; + } + + let current_clocksource = collect_clocksource_info( + ClocksourceInfo::CurrentClockSource, + &clocksource_name, + clock_source_path, + ) + .unwrap_or_default(); + + let available_clocksource = collect_clocksource_info( + ClocksourceInfo::AvailableClockSource, + &clocksource_name, + clock_source_path, + ) + .unwrap_or_default() + .split(' ') + .map(|v| v.to_string()) + .collect::>(); + + clock_sources.push(Clocksource::new( + clocksource_name, + available_clocksource, + current_clocksource, + )); + } + + clock_sources +} + +fn collect_clocksource_info( + info: ClocksourceInfo, + name: &str, + class_path: &Path, +) -> Option { + let info_str = info.into_string(); + let info_path = Path::new(class_path).join(name).join(info_str); + + match fs::read_to_string(info_path.as_path()) { + Ok(content) => return Some(content.trim().to_string()), + Err(err) => log::error!("{}", MetricError::IOError(info_path, err)), + } + + None +} + +#[cfg(test)] + +mod tests { + use super::*; + + #[test] + fn clocksource_collect() { + let clock_sources = collect(); + + assert!(!clock_sources.is_empty()); + + for clock_src in clock_sources { + assert!(!clock_src.name().is_empty()); + assert!(!clock_src.available_clocksource().is_empty()); + assert!(!clock_src.current_clocksource().is_empty()); + } + } +} diff --git a/src/sysfs/mod.rs b/src/sysfs/mod.rs new file mode 100644 index 0000000..0648a53 --- /dev/null +++ b/src/sysfs/mod.rs @@ -0,0 +1,5 @@ +pub mod class_cooling; +pub mod class_dmi; +pub mod class_thermal; +pub mod class_watchdog; +pub mod clocksource; diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..ba32163 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,31 @@ +use std::{fs, path::Path}; + +use crate::error::MetricError; + +pub fn collect_info_string(filename: &str, dir_path: &Path) -> Option { + if filename.is_empty() { + return None; + } + + let info_path = Path::new(dir_path).join(filename); + + match fs::read_to_string(info_path.as_path()) { + Ok(c) => return Some(c.trim().to_string()), + Err(err) => log::error!("{}", MetricError::IOError(info_path, err)), + } + + None +} + +pub fn collect_info_i64(filename: &str, dir_path: &Path) -> Option { + if let Some(c) = collect_info_string(filename, dir_path) { + if !c.is_empty() { + match c.parse::() { + Ok(i) => return Some(i), + Err(err) => log::error!("{}", err), + } + } + } + + None +}