More routes
This commit is contained in:
parent
a580b7bccf
commit
a9a10a6d52
12 changed files with 2400 additions and 211 deletions
1884
Cargo.lock
generated
1884
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
12
Cargo.toml
12
Cargo.toml
|
|
@ -5,8 +5,18 @@ edition = "2024"
|
|||
|
||||
[dependencies]
|
||||
axum = "0.8.8"
|
||||
base64 = "0.22.1"
|
||||
bcrypt = "0.19.0"
|
||||
chrono = "0.4.44"
|
||||
jsonwebtoken = { version = "10.3.0", features = ["rust_crypto"] }
|
||||
pam = { version = "0.8.0", features = ["client"] }
|
||||
rustls = { version = "0.23.37", features = ["ring"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1.0.149"
|
||||
sysinfo = "0.38.4"
|
||||
tokio = { version = "1.50.0", features = ["macros", "rt-multi-thread", "process"] }
|
||||
tokio = { version = "1.50.0", features = [
|
||||
"macros",
|
||||
"rt-multi-thread",
|
||||
"process",
|
||||
] }
|
||||
zbus = "5.14.0"
|
||||
|
|
|
|||
8
README.md
Normal file
8
README.md
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
# Server Stats API
|
||||
|
||||
# Installation & Usage
|
||||
|
||||
## Dependencies
|
||||
|
||||
- rust, cargo
|
||||
|
||||
35
flake.nix
35
flake.nix
|
|
@ -1,6 +1,5 @@
|
|||
{
|
||||
description = "sysapi - system stats & command execution REST API in Rust";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
rust-overlay = {
|
||||
|
|
@ -9,7 +8,6 @@
|
|||
};
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
};
|
||||
|
||||
outputs =
|
||||
{
|
||||
self,
|
||||
|
|
@ -23,9 +21,6 @@
|
|||
let
|
||||
overlays = [ (import rust-overlay) ];
|
||||
pkgs = import nixpkgs { inherit system overlays; };
|
||||
|
||||
# Pin to stable. Swap to rust-overlay's nightly if you need it:
|
||||
# pkgs.rust-bin.nightly.latest.default
|
||||
rustToolchain = pkgs.rust-bin.stable.latest.default.override {
|
||||
extensions = [
|
||||
"rust-src"
|
||||
|
|
@ -34,34 +29,38 @@
|
|||
"rustfmt"
|
||||
];
|
||||
};
|
||||
|
||||
# Native build inputs required to compile Rust crates with C deps
|
||||
nativeBuildInputs = with pkgs; [
|
||||
rustToolchain
|
||||
pkg-config
|
||||
];
|
||||
|
||||
# Runtime / link-time dependencies
|
||||
buildInputs = with pkgs; [
|
||||
openssl # required by jsonwebtoken / reqwest
|
||||
openssl
|
||||
linux-pam
|
||||
libclang
|
||||
glibc.dev
|
||||
];
|
||||
|
||||
in
|
||||
{
|
||||
packages.default = pkgs.rustPlatform.buildRustPackage {
|
||||
pname = "sysapi";
|
||||
version = "0.1.0";
|
||||
src = ./.;
|
||||
cargoHash = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
|
||||
inherit nativeBuildInputs buildInputs;
|
||||
OPENSSL_NO_VENDOR = 1;
|
||||
PKG_CONFIG_PATH = "${pkgs.openssl.dev}/lib/pkgconfig";
|
||||
LIBCLANG_PATH = "${pkgs.libclang.lib}/lib";
|
||||
BINDGEN_EXTRA_CLANG_ARGS = "-I${pkgs.linux-pam}/include -I${pkgs.glibc.dev}/include";
|
||||
};
|
||||
devShells.default = pkgs.mkShell {
|
||||
inherit nativeBuildInputs buildInputs;
|
||||
|
||||
# Let pkg-config find openssl
|
||||
PKG_CONFIG_PATH = "${pkgs.openssl.dev}/lib/pkgconfig";
|
||||
|
||||
# Tells the openssl-sys crate where to look (fallback if pkg-config fails)
|
||||
OPENSSL_DIR = "${pkgs.openssl.dev}";
|
||||
OPENSSL_LIB_DIR = "${pkgs.openssl.out}/lib";
|
||||
OPENSSL_INCLUDE_DIR = "${pkgs.openssl.dev}/include";
|
||||
|
||||
# Points rust-analyzer at the stdlib source
|
||||
LIBCLANG_PATH = "${pkgs.libclang.lib}/lib";
|
||||
BINDGEN_EXTRA_CLANG_ARGS = "-I${pkgs.linux-pam}/include -I${pkgs.glibc.dev}/include";
|
||||
RUST_SRC_PATH = "${rustToolchain}/lib/rustlib/src/rust/library";
|
||||
|
||||
shellHook = ''
|
||||
echo "🦀 sysapi dev shell ready"
|
||||
echo " rustc $(rustc --version)"
|
||||
|
|
|
|||
86
src/auth.rs
Normal file
86
src/auth.rs
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
use axum::response::Redirect;
|
||||
use axum::{
|
||||
Router, extract::Path, http::HeaderMap, http::StatusCode, response::IntoResponse,
|
||||
response::Json, routing::get, routing::post,
|
||||
};
|
||||
use base64::{Engine, engine::general_purpose};
|
||||
use jsonwebtoken::{DecodingKey, EncodingKey, Header, Validation, decode, encode};
|
||||
use pam::Client;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use sysinfo::{Components, Disks, Networks, System};
|
||||
use tokio::process::Command;
|
||||
use zbus::Connection;
|
||||
|
||||
const JWT_SECRET: &str = "change-me-to-a-long-random-string";
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct Claims {
|
||||
sub: String,
|
||||
exp: usize,
|
||||
}
|
||||
|
||||
pub fn create_token(username: &str) -> String {
|
||||
let claims = Claims {
|
||||
sub: username.to_owned(),
|
||||
exp: (chrono::Utc::now() + chrono::Duration::hours(8)).timestamp() as usize,
|
||||
};
|
||||
encode(
|
||||
&Header::default(),
|
||||
&claims,
|
||||
&EncodingKey::from_secret(JWT_SECRET.as_bytes()),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub fn verify_token(headers: &HeaderMap) -> bool {
|
||||
let Some(val) = headers.get("Authorization") else {
|
||||
return false;
|
||||
};
|
||||
let token = val.to_str().unwrap_or("").replace("Bearer ", "");
|
||||
decode::<Claims>(
|
||||
&token,
|
||||
&DecodingKey::from_secret(JWT_SECRET.as_bytes()),
|
||||
&Validation::default(),
|
||||
)
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
pub fn decode_basic_auth(headers: &HeaderMap) -> Option<(String, String)> {
|
||||
let val = headers.get("Authorization")?.to_str().ok()?;
|
||||
let encoded = val.strip_prefix("Basic ")?;
|
||||
let decoded = general_purpose::STANDARD.decode(encoded).ok()?;
|
||||
let s = String::from_utf8(decoded).ok()?;
|
||||
let (user, pass) = s.split_once(':')?;
|
||||
Some((user.to_string(), pass.to_string()))
|
||||
}
|
||||
|
||||
pub fn verify_system_credentials(username: &str, password: &str) -> bool {
|
||||
let mut client = match Client::with_password("login") {
|
||||
Ok(c) => c,
|
||||
Err(_) => return false,
|
||||
};
|
||||
client
|
||||
.conversation_mut()
|
||||
.set_credentials(username, password);
|
||||
client.authenticate().is_ok()
|
||||
}
|
||||
|
||||
// POST /auth/login
|
||||
pub async fn post_login(headers: HeaderMap) -> impl IntoResponse {
|
||||
let (username, password) = match decode_basic_auth(&headers) {
|
||||
Some(c) => c,
|
||||
None => {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
"Missing or invalid Authorization header",
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
if !verify_system_credentials(&username, &password) {
|
||||
return (StatusCode::UNAUTHORIZED, "Invalid credentials").into_response();
|
||||
}
|
||||
let token = create_token(&username);
|
||||
(StatusCode::OK, Json(serde_json::json!({ "token": token }))).into_response()
|
||||
}
|
||||
8
src/config.rs
Normal file
8
src/config.rs
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
pub const ALLOWED_SERVICES: &[&str] = &[
|
||||
"syncthing",
|
||||
"caddy",
|
||||
"sshd",
|
||||
"cloudflare-dyndns.timer",
|
||||
"cloudflare-dyndns",
|
||||
"docker",
|
||||
];
|
||||
162
src/main.rs
162
src/main.rs
|
|
@ -1,141 +1,41 @@
|
|||
use axum::{Router, body::Body, response::Json, routing::get};
|
||||
use serde_json::{Value, json};
|
||||
use std::collections::HashMap;
|
||||
use sysinfo::{Components, Disks, Networks, System};
|
||||
use std::path::Path;
|
||||
use axum::response::Redirect;
|
||||
use axum::{
|
||||
Router, extract::Path, http::HeaderMap, http::StatusCode, response::IntoResponse,
|
||||
response::Json, routing::get, routing::post,
|
||||
};
|
||||
use tokio::process::Command;
|
||||
|
||||
mod auth;
|
||||
mod config;
|
||||
mod models;
|
||||
|
||||
use models::SystemStats;
|
||||
|
||||
use self::models::MemoryStats;
|
||||
mod routes;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let app = Router::new()
|
||||
.route("/", get(root))
|
||||
.route("/stats", get(get_stats));
|
||||
.route("/", get(|| async { Redirect::permanent("/stats") }))
|
||||
.route("/stats", get(routes::stats::get_stats))
|
||||
.route("/auth/login", post(auth::post_login))
|
||||
.route(
|
||||
"/services/{service}/restart",
|
||||
post(routes::services::restart_service),
|
||||
)
|
||||
.route(
|
||||
"/services/{service}/start",
|
||||
post(routes::services::start_service),
|
||||
)
|
||||
.route(
|
||||
"/services/{service}/stop",
|
||||
post(routes::services::stop_service),
|
||||
)
|
||||
.route(
|
||||
"/services/{service}/logs",
|
||||
get(routes::services::service_logs),
|
||||
)
|
||||
.route("/system/reboot", post(routes::system::system_reboot));
|
||||
|
||||
let listener = tokio::net::TcpListener::bind("0.0.0.0:3001").await.unwrap();
|
||||
let listener = tokio::net::TcpListener::bind("127.0.0.1:3001")
|
||||
.await
|
||||
.unwrap();
|
||||
axum::serve(listener, app).await.unwrap();
|
||||
}
|
||||
|
||||
// which calls one of these handlers
|
||||
async fn root() {}
|
||||
async fn get_stats() -> Json<models::SystemStats> {
|
||||
let mut sys = System::new_all();
|
||||
sys.refresh_all();
|
||||
|
||||
// Memory (MB)
|
||||
let memory = models::MemoryStats {
|
||||
total: sys.total_memory() / 1_000_000,
|
||||
used: sys.used_memory() / 1_000_000,
|
||||
available: sys.available_memory() / 1_000_000,
|
||||
percent: (sys.used_memory() as f64 / sys.total_memory() as f64 * 100.0) as u64,
|
||||
};
|
||||
|
||||
// CPU
|
||||
let cpu = models::CpuStats {
|
||||
percent: sys.global_cpu_usage(),
|
||||
model: sys.cpus()[0].brand().to_string(),
|
||||
cores: sys.cpus().len(),
|
||||
};
|
||||
|
||||
// Disk (Fixed)
|
||||
let disks = Disks::new_with_refreshed_list();
|
||||
|
||||
// Find only the root partition
|
||||
let root_disk = disks.iter().find(|d| d.mount_point() == std::path::Path::new("/"));
|
||||
|
||||
let disk = if let Some(d) = root_disk {
|
||||
let total_bytes = d.total_space();
|
||||
let available_bytes = d.available_space();
|
||||
let used_bytes = total_bytes - available_bytes;
|
||||
let mb_factor = 1024 * 1024;
|
||||
|
||||
models::DiskStats {
|
||||
total: total_bytes / mb_factor,
|
||||
used: used_bytes / mb_factor,
|
||||
available: available_bytes / mb_factor,
|
||||
percent: (used_bytes as f64 / total_bytes as f64 * 100.0) as u64,
|
||||
}
|
||||
} else {
|
||||
// Fallback if "/" isn't found (prevents crash)
|
||||
models::DiskStats { total: 0, used: 0, available: 0, percent: 0 }
|
||||
};
|
||||
|
||||
// Uptime
|
||||
let seconds = System::uptime();
|
||||
let uptime = models::UptimeStats {
|
||||
seconds,
|
||||
days: seconds / 86400,
|
||||
hours: (seconds % 86400) / 3600,
|
||||
minutes: (seconds % 3600) / 60,
|
||||
};
|
||||
|
||||
// Network (bytes, same as original)
|
||||
let networks = Networks::new_with_refreshed_list();
|
||||
let network: HashMap<String, models::NetworkStats> = networks
|
||||
.iter()
|
||||
.map(|(name, data): (&String, &sysinfo::NetworkData)| {
|
||||
(
|
||||
name.clone(),
|
||||
models::NetworkStats {
|
||||
rx: data.total_received(),
|
||||
tx: data.total_transmitted(),
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Load average
|
||||
let load = System::load_average();
|
||||
let load_avg = models::LoadAvgStats {
|
||||
one: load.one,
|
||||
five: load.five,
|
||||
fifteen: load.fifteen,
|
||||
};
|
||||
|
||||
// Temperature
|
||||
let components = Components::new_with_refreshed_list();
|
||||
let temperature: f32 = components
|
||||
.iter()
|
||||
.next()
|
||||
.and_then(|c: &sysinfo::Component| c.temperature())
|
||||
.unwrap_or(0.0f32);
|
||||
|
||||
// Services — check a list of known services via systemctl
|
||||
let service_names = vec![
|
||||
"syncthing",
|
||||
"caddy",
|
||||
"sshd",
|
||||
"cloudflare-dyndns.timer",
|
||||
"cloudflare-dyndns",
|
||||
"docker",
|
||||
];
|
||||
let mut services: HashMap<String, String> = HashMap::new();
|
||||
for name in service_names {
|
||||
let output = Command::new("systemctl")
|
||||
.args(["is-active", name])
|
||||
.output()
|
||||
.await;
|
||||
let status = match output {
|
||||
Ok(out) => String::from_utf8_lossy(&out.stdout).trim().to_string(),
|
||||
Err(_) => "unknown".to_string(),
|
||||
};
|
||||
services.insert(name.to_string(), status);
|
||||
}
|
||||
|
||||
Json(models::SystemStats {
|
||||
timestamp: chrono::Utc::now().to_rfc3339(),
|
||||
memory,
|
||||
cpu,
|
||||
disk,
|
||||
uptime,
|
||||
network,
|
||||
load_avg,
|
||||
temperature,
|
||||
services,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,41 @@
|
|||
use axum::http::StatusCode;
|
||||
use axum::response::Json;
|
||||
use serde::Serialize;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ActionResponse {
|
||||
pub success: bool,
|
||||
pub message: String,
|
||||
pub stdout: String,
|
||||
pub stderr: String,
|
||||
}
|
||||
|
||||
impl ActionResponse {
|
||||
pub fn ok(message: String) -> (StatusCode, Json<Self>) {
|
||||
(
|
||||
StatusCode::OK,
|
||||
Json(Self {
|
||||
success: true,
|
||||
message,
|
||||
stdout: String::new(),
|
||||
stderr: String::new(),
|
||||
}),
|
||||
)
|
||||
}
|
||||
pub fn err(status: StatusCode, message: &str) -> (StatusCode, Json<Self>) {
|
||||
(
|
||||
status,
|
||||
Json(Self {
|
||||
success: false,
|
||||
message: message.to_string(),
|
||||
stdout: String::new(),
|
||||
stderr: String::new(),
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct SystemStats {
|
||||
pub timestamp: String,
|
||||
|
|
|
|||
3
src/routes/mod.rs
Normal file
3
src/routes/mod.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
pub mod services;
|
||||
pub mod stats;
|
||||
pub mod system;
|
||||
116
src/routes/services.rs
Normal file
116
src/routes/services.rs
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
use axum::response::Redirect;
|
||||
use axum::{
|
||||
Router, extract::Path, http::HeaderMap, http::StatusCode, response::IntoResponse,
|
||||
response::Json, routing::get, routing::post,
|
||||
};
|
||||
use base64::{Engine, engine::general_purpose};
|
||||
use jsonwebtoken::{DecodingKey, EncodingKey, Header, Validation, decode, encode};
|
||||
use pam::Client;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use sysinfo::{Components, Disks, Networks, System};
|
||||
use tokio::process::Command;
|
||||
use zbus::Connection;
|
||||
|
||||
use crate::auth;
|
||||
use crate::config;
|
||||
use crate::models::ActionResponse;
|
||||
|
||||
async fn systemd_action(action: &str, service: &str) -> (StatusCode, Json<ActionResponse>) {
|
||||
let unit = if service.contains('.') {
|
||||
service.to_string() // already has extension e.g. cloudflare-dyndns.timer
|
||||
} else {
|
||||
format!("{}.service", service)
|
||||
};
|
||||
|
||||
let conn = match Connection::system().await {
|
||||
Ok(c) => c,
|
||||
Err(e) => return ActionResponse::err(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
|
||||
};
|
||||
|
||||
let method = match action {
|
||||
"restart" => "RestartUnit",
|
||||
"start" => "StartUnit",
|
||||
"stop" => "StopUnit",
|
||||
_ => return ActionResponse::err(StatusCode::BAD_REQUEST, "Invalid action"),
|
||||
};
|
||||
|
||||
let result = conn
|
||||
.call_method(
|
||||
Some("org.freedesktop.systemd1"),
|
||||
"/org/freedesktop/systemd1",
|
||||
Some("org.freedesktop.systemd1.Manager"),
|
||||
method,
|
||||
&(unit.as_str(), "replace"),
|
||||
)
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(_) => ActionResponse::ok(format!("{} {}ed successfully", service, action)),
|
||||
Err(e) => ActionResponse::err(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
// POST /services/:service/restart
|
||||
pub async fn restart_service(headers: HeaderMap, Path(service): Path<String>) -> impl IntoResponse {
|
||||
if !auth::verify_token(&headers) {
|
||||
return ActionResponse::err(StatusCode::UNAUTHORIZED, "Unauthorized").into_response();
|
||||
}
|
||||
if !config::ALLOWED_SERVICES.contains(&service.as_str()) {
|
||||
return ActionResponse::err(StatusCode::BAD_REQUEST, "Service not allowed").into_response();
|
||||
}
|
||||
systemd_action("restart", &service).await.into_response()
|
||||
}
|
||||
|
||||
// POST /services/:service/start
|
||||
pub async fn start_service(headers: HeaderMap, Path(service): Path<String>) -> impl IntoResponse {
|
||||
if !auth::verify_token(&headers) {
|
||||
return ActionResponse::err(StatusCode::UNAUTHORIZED, "Unauthorized").into_response();
|
||||
}
|
||||
if !config::ALLOWED_SERVICES.contains(&service.as_str()) {
|
||||
return ActionResponse::err(StatusCode::BAD_REQUEST, "Service not allowed").into_response();
|
||||
}
|
||||
systemd_action("start", &service).await.into_response()
|
||||
}
|
||||
|
||||
// POST /services/:service/stop
|
||||
pub async fn stop_service(headers: HeaderMap, Path(service): Path<String>) -> impl IntoResponse {
|
||||
if !auth::verify_token(&headers) {
|
||||
return ActionResponse::err(StatusCode::UNAUTHORIZED, "Unauthorized").into_response();
|
||||
}
|
||||
if !config::ALLOWED_SERVICES.contains(&service.as_str()) {
|
||||
return ActionResponse::err(StatusCode::BAD_REQUEST, "Service not allowed").into_response();
|
||||
}
|
||||
systemd_action("stop", &service).await.into_response()
|
||||
}
|
||||
|
||||
// GET /services/:service/logs
|
||||
pub async fn service_logs(headers: HeaderMap, Path(service): Path<String>) -> impl IntoResponse {
|
||||
if !auth::verify_token(&headers) {
|
||||
return ActionResponse::err(StatusCode::UNAUTHORIZED, "Unauthorized").into_response();
|
||||
}
|
||||
if !config::ALLOWED_SERVICES.contains(&service.as_str()) {
|
||||
return ActionResponse::err(StatusCode::BAD_REQUEST, "Service not allowed").into_response();
|
||||
}
|
||||
|
||||
let out = Command::new("/run/current-system/sw/bin/journalctl")
|
||||
.args(["-u", &service, "-n", "100", "--no-pager"])
|
||||
.output()
|
||||
.await;
|
||||
|
||||
match out {
|
||||
Ok(o) => (
|
||||
StatusCode::OK,
|
||||
Json(ActionResponse {
|
||||
success: true,
|
||||
message: String::new(),
|
||||
stdout: String::from_utf8_lossy(&o.stdout).to_string(),
|
||||
stderr: String::from_utf8_lossy(&o.stderr).to_string(),
|
||||
}),
|
||||
)
|
||||
.into_response(),
|
||||
Err(e) => {
|
||||
ActionResponse::err(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()).into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
118
src/routes/stats.rs
Normal file
118
src/routes/stats.rs
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
use axum::response::Redirect;
|
||||
use axum::{
|
||||
Router, extract::Path, http::HeaderMap, http::StatusCode, response::IntoResponse,
|
||||
response::Json, routing::get, routing::post,
|
||||
};
|
||||
use base64::{Engine, engine::general_purpose};
|
||||
use jsonwebtoken::{DecodingKey, EncodingKey, Header, Validation, decode, encode};
|
||||
use pam::Client;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use sysinfo::{Components, Disks, Networks, System};
|
||||
use tokio::process::Command;
|
||||
use zbus::Connection;
|
||||
|
||||
use crate::models;
|
||||
|
||||
pub async fn get_stats() -> Json<models::SystemStats> {
|
||||
let mut sys = System::new_all();
|
||||
sys.refresh_all();
|
||||
|
||||
let memory = models::MemoryStats {
|
||||
total: sys.total_memory() / 1_000_000,
|
||||
used: sys.used_memory() / 1_000_000,
|
||||
available: sys.available_memory() / 1_000_000,
|
||||
percent: (sys.used_memory() as f64 / sys.total_memory() as f64 * 100.0) as u64,
|
||||
};
|
||||
|
||||
let cpu = models::CpuStats {
|
||||
percent: sys.global_cpu_usage(),
|
||||
model: sys.cpus()[0].brand().to_string(),
|
||||
cores: sys.cpus().len(),
|
||||
};
|
||||
|
||||
let disks = Disks::new_with_refreshed_list();
|
||||
let root_disk = disks
|
||||
.iter()
|
||||
.find(|d| d.mount_point() == std::path::Path::new("/"));
|
||||
let disk = if let Some(d) = root_disk {
|
||||
let total = d.total_space();
|
||||
let available = d.available_space();
|
||||
let used = total - available;
|
||||
let mb = 1024 * 1024;
|
||||
models::DiskStats {
|
||||
total: total / mb,
|
||||
used: used / mb,
|
||||
available: available / mb,
|
||||
percent: (used as f64 / total as f64 * 100.0) as u64,
|
||||
}
|
||||
} else {
|
||||
models::DiskStats {
|
||||
total: 0,
|
||||
used: 0,
|
||||
available: 0,
|
||||
percent: 0,
|
||||
}
|
||||
};
|
||||
|
||||
let seconds = System::uptime();
|
||||
let uptime = models::UptimeStats {
|
||||
seconds,
|
||||
days: seconds / 86400,
|
||||
hours: (seconds % 86400) / 3600,
|
||||
minutes: (seconds % 3600) / 60,
|
||||
};
|
||||
|
||||
let networks = Networks::new_with_refreshed_list();
|
||||
let network: HashMap<String, models::NetworkStats> = networks
|
||||
.iter()
|
||||
.map(|(name, data): (&String, &sysinfo::NetworkData)| {
|
||||
(
|
||||
name.clone(),
|
||||
models::NetworkStats {
|
||||
rx: data.total_received(),
|
||||
tx: data.total_transmitted(),
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let load = System::load_average();
|
||||
let load_avg = models::LoadAvgStats {
|
||||
one: load.one,
|
||||
five: load.five,
|
||||
fifteen: load.fifteen,
|
||||
};
|
||||
|
||||
let components = Components::new_with_refreshed_list();
|
||||
let temperature: f32 = components
|
||||
.iter()
|
||||
.next()
|
||||
.and_then(|c: &sysinfo::Component| c.temperature())
|
||||
.unwrap_or(0.0f32);
|
||||
|
||||
let mut services: HashMap<String, String> = HashMap::new();
|
||||
for name in crate::config::ALLOWED_SERVICES {
|
||||
let output = Command::new("/run/current-system/sw/bin/systemctl")
|
||||
.args(["is-active", name])
|
||||
.output()
|
||||
.await;
|
||||
let status = match output {
|
||||
Ok(out) => String::from_utf8_lossy(&out.stdout).trim().to_string(),
|
||||
Err(_) => "unknown".to_string(),
|
||||
};
|
||||
services.insert(name.to_string(), status);
|
||||
}
|
||||
|
||||
Json(models::SystemStats {
|
||||
timestamp: chrono::Utc::now().to_rfc3339(),
|
||||
memory,
|
||||
cpu,
|
||||
disk,
|
||||
uptime,
|
||||
network,
|
||||
load_avg,
|
||||
temperature,
|
||||
services,
|
||||
})
|
||||
}
|
||||
40
src/routes/system.rs
Normal file
40
src/routes/system.rs
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
use axum::{
|
||||
Router, extract::Path, http::HeaderMap, http::StatusCode, response::IntoResponse,
|
||||
response::Json, routing::get, routing::post,
|
||||
};
|
||||
use zbus::Connection;
|
||||
|
||||
use crate::auth;
|
||||
use crate::models;
|
||||
|
||||
// POST /system/reboot
|
||||
pub async fn system_reboot(headers: HeaderMap) -> impl IntoResponse {
|
||||
if !auth::verify_token(&headers) {
|
||||
return models::ActionResponse::err(StatusCode::UNAUTHORIZED, "Unauthorized")
|
||||
.into_response();
|
||||
}
|
||||
|
||||
let conn = match Connection::system().await {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
return models::ActionResponse::err(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string())
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let result = conn
|
||||
.call_method(
|
||||
Some("org.freedesktop.login1"),
|
||||
"/org/freedesktop/login1",
|
||||
Some("org.freedesktop.login1.Manager"),
|
||||
"Reboot",
|
||||
&(false,), // false = don't ask for confirmation
|
||||
)
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(_) => models::ActionResponse::ok("Rebooting...".to_string()).into_response(),
|
||||
Err(e) => models::ActionResponse::err(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string())
|
||||
.into_response(),
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue