From 33486b4e1d0b7223197d0a090217c72b3d664812 Mon Sep 17 00:00:00 2001 From: glyph Date: Fri, 10 Dec 2021 11:01:51 +0200 Subject: [PATCH] jsonrpc server with peach-stats methods --- Cargo.toml | 1 + peach-jsonrpc-server/Cargo.toml | 25 ++++++ peach-jsonrpc-server/README.md | 72 +++++++++++++++ peach-jsonrpc-server/src/error.rs | 46 ++++++++++ peach-jsonrpc-server/src/lib.rs | 141 ++++++++++++++++++++++++++++++ peach-jsonrpc-server/src/main.rs | 34 +++++++ 6 files changed, 319 insertions(+) create mode 100644 peach-jsonrpc-server/Cargo.toml create mode 100644 peach-jsonrpc-server/README.md create mode 100644 peach-jsonrpc-server/src/error.rs create mode 100644 peach-jsonrpc-server/src/lib.rs create mode 100644 peach-jsonrpc-server/src/main.rs diff --git a/Cargo.toml b/Cargo.toml index dd1560b..1ccc7d9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ members = [ "peach-menu", "peach-monitor", "peach-stats", + "peach-jsonrpc-server", "peach-probe", "peach-dyndns-updater" ] diff --git a/peach-jsonrpc-server/Cargo.toml b/peach-jsonrpc-server/Cargo.toml new file mode 100644 index 0000000..7011cbb --- /dev/null +++ b/peach-jsonrpc-server/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "peach-jsonrpc-server" +authors = ["Andrew Reid "] +version = "0.1.0" +edition = "2021" +description = "JSON-RPC over HTTP for the PeachCloud system. Provides a JSON-RPC wrapper around the stats, network and oled libraries." +homepage = "https://opencollective.com/peachcloud" +repository = "https://git.coopcloud.tech/PeachCloud/peach-workspace" +readme = "README.md" +license = "AGPL-3.0-only" +publish = false + +[badges] +maintenance = { status = "actively-developed" } + +[dependencies] +env_logger = "0.9" +jsonrpc-core = "18" +jsonrpc-http-server = "18" +log = "0.4" +miniserde = "0.1.15" +peach-stats = { path = "../peach-stats", features = ["miniserde_support"] } + +[dev-dependencies] +jsonrpc-test = "18" diff --git a/peach-jsonrpc-server/README.md b/peach-jsonrpc-server/README.md new file mode 100644 index 0000000..e13a0eb --- /dev/null +++ b/peach-jsonrpc-server/README.md @@ -0,0 +1,72 @@ +# peach-jsonrpc-server + +A JSON-RPC server for the PeachCloud system which exposes an API over HTTP. + +Currently includes peach-stats capability (system statistics). + +## JSON-RPC API + +| Method | Description | Returns | +| --- | --- | --- | +| `cpu_stats` | CPU statistics | `user`, `system`, `nice`, `idle` | +| `cpu_stats_percent` | CPU statistics as percentages | `user`, `system`, `nice`, `idle` | +| `disk_usage` | Disk usage statistics (array of disks) | `filesystem`, `one_k_blocks`, `one_k_blocks_used`, `one_k_blocks_free`, `used_percentage`, `mountpoint` | +| `load_average` | Load average statistics | `one`, `five`, `fifteen` | +| `mem_stats` | Memory statistics | `total`, `free`, `used` | +| `ping` | Microservice status | `success` if running | +| `uptime` | System uptime | `secs` | + +## Environment + +The JSON-RPC HTTP server is currently hardcoded to run on "127.0.0.1:5110". Address and port configuration settings will later be exposed via CLI arguments and possibly an environment variable. + +Logging is made available with `env_logger`: + +`export RUST_LOG=info` + +Other logging levels include `debug`, `warn` and `error`. + +## Setup + +Clone the peach-workspace repo: + +`git clone https://git.coopcloud.tech/PeachCloud/peach-workspace` + +Move into the repo peaach-jsonrpc-server directory and compile a release build: + +`cd peach-jsonrpc-server` +`cargo build --release` + +Run the binary: + +`./peach-workspace/target/release/peach-jsonrpc-server` + +## Debian Packaging + +TODO. + +## Example Usage + +**Get CPU Statistics** + +With microservice running, open a second terminal window and use `curl` to call server methods: + +`curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "method": "cpu_stats", "id":1 }' 127.0.0.1:5110` + +Server responds with: + +`{"jsonrpc":"2.0","result":"{\"user\":4661083,\"system\":1240371,\"idle\":326838290,\"nice\":0}","id":1}` + +**Get System Uptime** + +With microservice running, open a second terminal window and use `curl` to call server methods: + +`curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "method": "uptime", "id":1 }' 127.0.0.1:5110` + +Server responds with: + +`{"jsonrpc":"2.0","result":"{\"secs\":840968}","id":1}` + +### Licensing + +AGPL-3.0 diff --git a/peach-jsonrpc-server/src/error.rs b/peach-jsonrpc-server/src/error.rs new file mode 100644 index 0000000..9e14dbf --- /dev/null +++ b/peach-jsonrpc-server/src/error.rs @@ -0,0 +1,46 @@ +use std::fmt; + +use jsonrpc_core::{Error as JsonRpcError, ErrorCode}; +use peach_stats::StatsError; + +/// Custom error type encapsulating all possible errors for a JSON-RPC server +/// and associated methods. +#[derive(Debug)] +pub enum JsonRpcServerError { + /// An error returned from the `peach-stats` library. + Stats(StatsError), + /// An expected JSON-RPC method parameter was not provided. + MissingParameter(JsonRpcError), + /// Failed to parse a provided JSON-RPC method parameter. + ParseParameter(JsonRpcError), +} + +impl fmt::Display for JsonRpcServerError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + JsonRpcServerError::ParseParameter(ref source) => { + write!(f, "Failed to parse parameter: {}", source) + } + JsonRpcServerError::MissingParameter(ref source) => { + write!(f, "Missing expected parameter: {}", source) + } + JsonRpcServerError::Stats(ref source) => { + write!(f, "{}", source) + } + } + } +} + +impl From for JsonRpcError { + fn from(err: JsonRpcServerError) -> Self { + match &err { + JsonRpcServerError::Stats(source) => JsonRpcError { + code: ErrorCode::ServerError(-32001), + message: format!("{}", source), + data: None, + }, + JsonRpcServerError::MissingParameter(source) => source.clone(), + JsonRpcServerError::ParseParameter(source) => source.clone(), + } + } +} diff --git a/peach-jsonrpc-server/src/lib.rs b/peach-jsonrpc-server/src/lib.rs new file mode 100644 index 0000000..43ecbf7 --- /dev/null +++ b/peach-jsonrpc-server/src/lib.rs @@ -0,0 +1,141 @@ +//! # peach-jsonrpc-server +//! +//! A JSON-RPC server which exposes an API over HTTP. + +use std::env; +use std::result::Result; + +use jsonrpc_core::{IoHandler, Value}; +use jsonrpc_http_server::{AccessControlAllowOrigin, DomainsValidation, ServerBuilder}; +use log::info; +use miniserde::json; +use peach_stats::stats; + +mod error; +use crate::error::JsonRpcServerError; + +/// Create JSON-RPC I/O handler, add RPC methods and launch HTTP server. +pub fn run() -> Result<(), JsonRpcServerError> { + info!("Starting up."); + + info!("Creating JSON-RPC I/O handler."); + let mut io = IoHandler::default(); + + io.add_sync_method("ping", |_| Ok(Value::String("success".to_string()))); + + // TODO: add blocks of methods according to provided flags + + /* PEACH-STATS RPC METHODS */ + + io.add_sync_method("cpu_stats", move |_| { + info!("Fetching CPU statistics."); + let cpu = stats::cpu_stats().map_err(JsonRpcServerError::Stats)?; + let json_cpu = json::to_string(&cpu); + + Ok(Value::String(json_cpu)) + }); + + io.add_sync_method("cpu_stats_percent", move |_| { + info!("Fetching CPU statistics as percentages."); + let cpu = stats::cpu_stats_percent().map_err(JsonRpcServerError::Stats)?; + let json_cpu = json::to_string(&cpu); + + Ok(Value::String(json_cpu)) + }); + + io.add_sync_method("disk_usage", move |_| { + info!("Fetching disk usage statistics."); + let disks = stats::disk_usage().map_err(JsonRpcServerError::Stats)?; + let json_disks = json::to_string(&disks); + + Ok(Value::String(json_disks)) + }); + + io.add_sync_method("load_average", move |_| { + info!("Fetching system load average statistics."); + let avg = stats::load_average().map_err(JsonRpcServerError::Stats)?; + let json_avg = json::to_string(&avg); + + Ok(Value::String(json_avg)) + }); + + io.add_sync_method("mem_stats", move |_| { + info!("Fetching current memory statistics."); + let mem = stats::mem_stats().map_err(JsonRpcServerError::Stats)?; + let json_mem = json::to_string(&mem); + + Ok(Value::String(json_mem)) + }); + + io.add_sync_method("uptime", move |_| { + info!("Fetching system uptime."); + let uptime = stats::uptime().map_err(JsonRpcServerError::Stats)?; + let json_uptime = json::to_string(&uptime); + + Ok(Value::String(json_uptime)) + }); + + let http_server = + env::var("PEACH_JSONRPC_SERVER").unwrap_or_else(|_| "127.0.0.1:5110".to_string()); + + info!("Starting JSON-RPC server on {}.", http_server); + let server = ServerBuilder::new(io) + .cors(DomainsValidation::AllowOnly(vec![ + AccessControlAllowOrigin::Null, + ])) + .start_http( + &http_server + .parse() + .expect("Invalid HTTP address and port combination"), + ) + .expect("Unable to start RPC server"); + + server.wait(); + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + use jsonrpc_core::{Error as JsonRpcError, ErrorCode}; + use jsonrpc_test as test_rpc; + + #[test] + fn rpc_success() { + let rpc = { + let mut io = IoHandler::new(); + io.add_sync_method("rpc_success_response", |_| { + Ok(Value::String("success".into())) + }); + test_rpc::Rpc::from(io) + }; + + assert_eq!(rpc.request("rpc_success_response", &()), r#""success""#); + } + + #[test] + fn rpc_parse_error() { + let rpc = { + let mut io = IoHandler::new(); + io.add_sync_method("rpc_parse_error", |_| { + let e = JsonRpcError { + code: ErrorCode::ParseError, + message: String::from("Parse error"), + data: None, + }; + Err(JsonRpcError::from(JsonRpcServerError::MissingParameter(e))) + }); + test_rpc::Rpc::from(io) + }; + + assert_eq!( + rpc.request("rpc_parse_error", &()), + r#"{ + "code": -32700, + "message": "Parse error" +}"# + ); + } +} diff --git a/peach-jsonrpc-server/src/main.rs b/peach-jsonrpc-server/src/main.rs new file mode 100644 index 0000000..cfbf2c6 --- /dev/null +++ b/peach-jsonrpc-server/src/main.rs @@ -0,0 +1,34 @@ +#![warn(missing_docs)] + +//! # peach-jsonrpc-server +//! +//! A JSON-RPC server which exposes an over HTTP. +//! +//! Currently includes peach-stats capability (system statistics). +//! +//! ## API +//! +//! | Method | Description | Returns | +//! | --- | --- | --- | +//! | `cpu_stats` | CPU statistics | `user`, `system`, `nice`, `idle` | +//! | `cpu_stats_percent` | CPU statistics as percentages | `user`, `system`, `nice`, `idle` | +//! | `disk_usage` | Disk usage statistics (array of disks) | `filesystem`, `one_k_blocks`, `one_k_blocks_used`, `one_k_blocks_free`, `used_percentage`, `mountpoint` | +//! | `load_average` | Load average statistics | `one`, `five`, `fifteen` | +//! | `mem_stats` | Memory statistics | `total`, `free`, `used` | +//! | `ping` | Microservice status | `success` if running | +//! | `uptime` | System uptime | `secs` | + +use std::process; + +use log::error; + +fn main() { + // initalize the logger + env_logger::init(); + + // handle errors returned from `run` + if let Err(e) = peach_jsonrpc_server::run() { + error!("Application error: {}", e); + process::exit(1); + } +}