mirror of
https://github.com/rzuasti/oott.git
synced 2026-07-08 19:21:54 +02:00
Add ARP scanner status API endpoint and rename scanner module
Renames scanner.rs to arp_scanner.rs to future-proof for additional scanner types. Renames the [timings] config section to [arp_scanner] and simplifies field names (arp_sender_timeout -> sender_timeout, arp_scan_duration -> scan_duration). Introduces arp_scanner_status module to track whether the scan is actively running or sleeping, and exposes this via a new GET /api/arp_scanner/status endpoint returning is_running, running_for_seconds, and next_run_in_seconds. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
97e4bb1000
commit
020c7d2c49
@@ -1,12 +1,16 @@
|
|||||||
|
use crate::arp_scanner_status;
|
||||||
use crate::db;
|
use crate::db;
|
||||||
use crate::device_finders;
|
use crate::device_finders;
|
||||||
use crate::events;
|
use crate::events;
|
||||||
use crate::settings::get_settings;
|
use crate::settings::get_settings;
|
||||||
|
use chrono::Utc;
|
||||||
use log::{debug, info};
|
use log::{debug, info};
|
||||||
use tokio::time::{Duration, sleep};
|
use tokio::time::{Duration, sleep};
|
||||||
|
|
||||||
pub async fn scan() -> Result<(), Box<dyn std::error::Error>> {
|
pub async fn scan() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
loop {
|
loop {
|
||||||
|
arp_scanner_status::set_running();
|
||||||
|
|
||||||
// Find online devices via ARP
|
// Find online devices via ARP
|
||||||
let devices =
|
let devices =
|
||||||
device_finders::arp::find(get_settings().networking.interface.to_string()).await?;
|
device_finders::arp::find(get_settings().networking.interface.to_string()).await?;
|
||||||
@@ -43,10 +47,14 @@ pub async fn scan() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let wait = Duration::from(get_settings().arp_scanner.wait_between_scans);
|
||||||
|
let next_scan_at = Utc::now() + chrono::Duration::from_std(wait).unwrap();
|
||||||
info!(
|
info!(
|
||||||
"Scan finished. Sleeping for {}",
|
"Scan finished. Sleeping for {}",
|
||||||
get_settings().timings.wait_between_scans
|
get_settings().arp_scanner.wait_between_scans
|
||||||
);
|
);
|
||||||
sleep(Duration::from(get_settings().timings.wait_between_scans)).await;
|
arp_scanner_status::set_waiting(next_scan_at);
|
||||||
|
sleep(wait).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use once_cell::sync::OnceCell;
|
||||||
|
use std::sync::Mutex;
|
||||||
|
|
||||||
|
pub struct ArpScannerStatus {
|
||||||
|
pub is_running: bool,
|
||||||
|
pub scan_started_at: Option<DateTime<Utc>>,
|
||||||
|
pub next_scan_at: Option<DateTime<Utc>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct ArpScannerStatusSnapshot {
|
||||||
|
pub is_running: bool,
|
||||||
|
pub scan_started_at: Option<DateTime<Utc>>,
|
||||||
|
pub next_scan_at: Option<DateTime<Utc>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
static STATUS: OnceCell<Mutex<ArpScannerStatus>> = OnceCell::new();
|
||||||
|
|
||||||
|
pub fn init() {
|
||||||
|
STATUS
|
||||||
|
.set(Mutex::new(ArpScannerStatus {
|
||||||
|
is_running: false,
|
||||||
|
scan_started_at: None,
|
||||||
|
next_scan_at: None,
|
||||||
|
}))
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_running() {
|
||||||
|
if let Some(m) = STATUS.get() {
|
||||||
|
let mut s = m.lock().unwrap();
|
||||||
|
s.is_running = true;
|
||||||
|
s.scan_started_at = Some(Utc::now());
|
||||||
|
s.next_scan_at = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_waiting(next_scan_at: DateTime<Utc>) {
|
||||||
|
if let Some(m) = STATUS.get() {
|
||||||
|
let mut s = m.lock().unwrap();
|
||||||
|
s.is_running = false;
|
||||||
|
s.scan_started_at = None;
|
||||||
|
s.next_scan_at = Some(next_scan_at);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get() -> Option<ArpScannerStatusSnapshot> {
|
||||||
|
STATUS.get().map(|m| {
|
||||||
|
let s = m.lock().unwrap();
|
||||||
|
ArpScannerStatusSnapshot {
|
||||||
|
is_running: s.is_running,
|
||||||
|
scan_started_at: s.scan_started_at,
|
||||||
|
next_scan_at: s.next_scan_at,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn reset_for_test() {
|
||||||
|
if let Some(m) = STATUS.get() {
|
||||||
|
let mut s = m.lock().unwrap();
|
||||||
|
s.is_running = false;
|
||||||
|
s.scan_started_at = None;
|
||||||
|
s.next_scan_at = None;
|
||||||
|
} else {
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_set_running() {
|
||||||
|
reset_for_test();
|
||||||
|
set_running();
|
||||||
|
let snapshot = get().unwrap();
|
||||||
|
assert!(snapshot.is_running);
|
||||||
|
assert!(snapshot.scan_started_at.is_some());
|
||||||
|
assert!(snapshot.next_scan_at.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_set_waiting() {
|
||||||
|
reset_for_test();
|
||||||
|
let next = Utc::now() + chrono::Duration::seconds(60);
|
||||||
|
set_waiting(next);
|
||||||
|
let snapshot = get().unwrap();
|
||||||
|
assert!(!snapshot.is_running);
|
||||||
|
assert!(snapshot.scan_started_at.is_none());
|
||||||
|
assert_eq!(snapshot.next_scan_at.unwrap(), next);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_initial_state() {
|
||||||
|
reset_for_test();
|
||||||
|
let snapshot = get().unwrap();
|
||||||
|
assert!(!snapshot.is_running);
|
||||||
|
assert!(snapshot.scan_started_at.is_none());
|
||||||
|
assert!(snapshot.next_scan_at.is_none());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -80,16 +80,16 @@ pub async fn find(interface: String) -> Result<Vec<Device>, Box<dyn std::error::
|
|||||||
let send_interface = network_interface.clone();
|
let send_interface = network_interface.clone();
|
||||||
|
|
||||||
// Get timeouts
|
// Get timeouts
|
||||||
let sender_timeout: Duration = get_settings().timings.arp_sender_timeout.into();
|
let sender_timeout: Duration = get_settings().arp_scanner.sender_timeout.into();
|
||||||
info!(
|
info!(
|
||||||
"Sender timeout set to {}",
|
"Sender timeout set to {}",
|
||||||
get_settings().timings.arp_sender_timeout
|
get_settings().arp_scanner.sender_timeout
|
||||||
);
|
);
|
||||||
let scan_duration: Duration = get_settings().timings.arp_scan_duration.into();
|
let scan_duration: Duration = get_settings().arp_scanner.scan_duration.into();
|
||||||
let receiver_timeout: Duration = scan_duration * 2;
|
let receiver_timeout: Duration = scan_duration * 2;
|
||||||
info!(
|
info!(
|
||||||
"Scan duration set to {}",
|
"Scan duration set to {}",
|
||||||
get_settings().timings.arp_scan_duration
|
get_settings().arp_scanner.scan_duration
|
||||||
);
|
);
|
||||||
info!(
|
info!(
|
||||||
"Receiver timeout set to {}",
|
"Receiver timeout set to {}",
|
||||||
|
|||||||
+6
-2
@@ -2,6 +2,8 @@ use crate::settings::get_settings;
|
|||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use log::{LevelFilter, info};
|
use log::{LevelFilter, info};
|
||||||
|
|
||||||
|
mod arp_scanner;
|
||||||
|
mod arp_scanner_status;
|
||||||
mod db;
|
mod db;
|
||||||
mod device_finders;
|
mod device_finders;
|
||||||
mod events;
|
mod events;
|
||||||
@@ -9,7 +11,6 @@ mod mac_vendor_finder;
|
|||||||
mod vendor_device_type_finder;
|
mod vendor_device_type_finder;
|
||||||
mod model;
|
mod model;
|
||||||
mod retention;
|
mod retention;
|
||||||
mod scanner;
|
|
||||||
mod settings;
|
mod settings;
|
||||||
mod utils;
|
mod utils;
|
||||||
mod web_server;
|
mod web_server;
|
||||||
@@ -58,6 +59,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
// Initialize database
|
// Initialize database
|
||||||
db::init_db().await?;
|
db::init_db().await?;
|
||||||
|
|
||||||
|
// Initialize ARP scanner status tracking
|
||||||
|
arp_scanner_status::init();
|
||||||
|
|
||||||
// Start the device scanner, web server, and retention cleaner in parallel
|
// Start the device scanner, web server, and retention cleaner in parallel
|
||||||
tokio::join!(scanner::scan(), web_server::serve(), retention::run()).0
|
tokio::join!(arp_scanner::scan(), web_server::serve(), retention::run()).0
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,10 +22,10 @@ pub struct Log {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Clone)]
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
pub struct Timings {
|
pub struct ArpScanner {
|
||||||
pub wait_between_scans: DurationString,
|
pub wait_between_scans: DurationString,
|
||||||
pub arp_sender_timeout: DurationString,
|
pub sender_timeout: DurationString,
|
||||||
pub arp_scan_duration: DurationString,
|
pub scan_duration: DurationString,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Clone)]
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
@@ -66,7 +66,7 @@ pub struct Settings {
|
|||||||
pub database: Database,
|
pub database: Database,
|
||||||
pub networking: Networking,
|
pub networking: Networking,
|
||||||
pub log: Log,
|
pub log: Log,
|
||||||
pub timings: Timings,
|
pub arp_scanner: ArpScanner,
|
||||||
pub notifications: Notifications,
|
pub notifications: Notifications,
|
||||||
pub web_server: WebServer,
|
pub web_server: WebServer,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ use crate::model::device_events::{DeviceEvent, DeviceEventType};
|
|||||||
use crate::model::devices::Device;
|
use crate::model::devices::Device;
|
||||||
use crate::model::notifications::{Notification, NotificationType};
|
use crate::model::notifications::{Notification, NotificationType};
|
||||||
use crate::settings::get_settings;
|
use crate::settings::get_settings;
|
||||||
|
use crate::web_server::arp_scanner::ArpScannerStatusResponse;
|
||||||
use crate::web_server::devices::RegisterDevicePayload;
|
use crate::web_server::devices::RegisterDevicePayload;
|
||||||
use axum::extract::Request;
|
use axum::extract::Request;
|
||||||
use axum::http::StatusCode;
|
use axum::http::StatusCode;
|
||||||
@@ -21,6 +22,7 @@ use utoipa::openapi::security::{HttpAuthScheme, HttpBuilder, SecurityScheme};
|
|||||||
use utoipa::Modify;
|
use utoipa::Modify;
|
||||||
use utoipa_swagger_ui::SwaggerUi;
|
use utoipa_swagger_ui::SwaggerUi;
|
||||||
|
|
||||||
|
pub mod arp_scanner;
|
||||||
pub mod device_events;
|
pub mod device_events;
|
||||||
pub mod devices;
|
pub mod devices;
|
||||||
pub mod notifications;
|
pub mod notifications;
|
||||||
@@ -45,6 +47,7 @@ pub mod utils;
|
|||||||
notifications::mark_as_new,
|
notifications::mark_as_new,
|
||||||
notifications::mark_all_as_old,
|
notifications::mark_all_as_old,
|
||||||
device_events::list,
|
device_events::list,
|
||||||
|
arp_scanner::status,
|
||||||
),
|
),
|
||||||
components(schemas(
|
components(schemas(
|
||||||
Device,
|
Device,
|
||||||
@@ -53,12 +56,14 @@ pub mod utils;
|
|||||||
RegisterDevicePayload,
|
RegisterDevicePayload,
|
||||||
DeviceEvent,
|
DeviceEvent,
|
||||||
DeviceEventType,
|
DeviceEventType,
|
||||||
|
ArpScannerStatusResponse,
|
||||||
)),
|
)),
|
||||||
modifiers(&SecurityAddon),
|
modifiers(&SecurityAddon),
|
||||||
tags(
|
tags(
|
||||||
(name = "devices", description = "Device management"),
|
(name = "devices", description = "Device management"),
|
||||||
(name = "notifications", description = "Notification management"),
|
(name = "notifications", description = "Notification management"),
|
||||||
(name = "device_events", description = "Device event history"),
|
(name = "device_events", description = "Device event history"),
|
||||||
|
(name = "arp_scanner", description = "ARP scanner process status"),
|
||||||
)
|
)
|
||||||
)]
|
)]
|
||||||
struct ApiDoc;
|
struct ApiDoc;
|
||||||
@@ -101,6 +106,7 @@ pub async fn serve() -> Result<(), Box<dyn Error>> {
|
|||||||
.route("/api/devices/{mac_address}", delete(devices::unregister))
|
.route("/api/devices/{mac_address}", delete(devices::unregister))
|
||||||
.route("/api/devices/{mac_address}", get(devices::read))
|
.route("/api/devices/{mac_address}", get(devices::read))
|
||||||
.route("/api/devices/{mac_address}/events", get(device_events::list))
|
.route("/api/devices/{mac_address}/events", get(device_events::list))
|
||||||
|
.route("/api/arp_scanner/status", get(arp_scanner::status))
|
||||||
.route("/api/notifications", get(notifications::list))
|
.route("/api/notifications", get(notifications::list))
|
||||||
.route(
|
.route(
|
||||||
"/api/notifications/mark_all_as_old",
|
"/api/notifications/mark_all_as_old",
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
use axum::{Json, http::StatusCode};
|
||||||
|
use chrono::Utc;
|
||||||
|
use log::error;
|
||||||
|
use serde::Serialize;
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
|
use crate::arp_scanner_status;
|
||||||
|
|
||||||
|
#[derive(Serialize, ToSchema)]
|
||||||
|
pub struct ArpScannerStatusResponse {
|
||||||
|
pub is_running: bool,
|
||||||
|
/// Seconds the current scan has been running (only set when is_running is true)
|
||||||
|
pub running_for_seconds: Option<f64>,
|
||||||
|
/// Seconds until the next scan starts (only set when is_running is false; clamped to 0)
|
||||||
|
pub next_run_in_seconds: Option<f64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/api/arp_scanner/status",
|
||||||
|
tag = "arp_scanner",
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "ARP scanner status", body = ArpScannerStatusResponse),
|
||||||
|
(status = 500, description = "Internal server error"),
|
||||||
|
),
|
||||||
|
security(("bearer_auth" = []))
|
||||||
|
)]
|
||||||
|
pub async fn status() -> Result<Json<ArpScannerStatusResponse>, StatusCode> {
|
||||||
|
let snapshot = match arp_scanner_status::get() {
|
||||||
|
Some(s) => s,
|
||||||
|
None => {
|
||||||
|
error!("ARP scanner status not initialized");
|
||||||
|
return Err(StatusCode::INTERNAL_SERVER_ERROR);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let now = Utc::now();
|
||||||
|
|
||||||
|
let (running_for_seconds, next_run_in_seconds) = if snapshot.is_running {
|
||||||
|
let running_for = snapshot
|
||||||
|
.scan_started_at
|
||||||
|
.map(|t| (now - t).num_milliseconds() as f64 / 1000.0);
|
||||||
|
(running_for, None)
|
||||||
|
} else {
|
||||||
|
let next_run_in = snapshot.next_scan_at.map(|t| {
|
||||||
|
let secs = (t - now).num_milliseconds() as f64 / 1000.0;
|
||||||
|
secs.max(0.0)
|
||||||
|
});
|
||||||
|
(None, next_run_in)
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Json(ArpScannerStatusResponse {
|
||||||
|
is_running: snapshot.is_running,
|
||||||
|
running_for_seconds,
|
||||||
|
next_run_in_seconds,
|
||||||
|
}))
|
||||||
|
}
|
||||||
@@ -7,10 +7,10 @@ interface = "eno1" # Network interface to use for scans
|
|||||||
[log]
|
[log]
|
||||||
level = "info" # off, error, warn, info, debug, trace
|
level = "info" # off, error, warn, info, debug, trace
|
||||||
|
|
||||||
[timings]
|
[arp_scanner]
|
||||||
wait_between_scans="10m" # Wait time between scans. This does not include the scan time
|
wait_between_scans="10m" # Wait time between scans. This does not include the scan time
|
||||||
arp_sender_timeout="1m" # If the ARP sender process takes longer than this it will be stopped (for a class C network - 254 IPs - it should take less than a minute)
|
sender_timeout="1m" # If the ARP sender process takes longer than this it will be stopped (for a class C network - 254 IPs - it should take less than a minute)
|
||||||
arp_scan_duration="10m" # How long to wait for response packets on each scan (5m to 10m is a good timeframe for a class B or C network)
|
scan_duration="10m" # How long to wait for response packets on each scan (5m to 10m is a good timeframe for a class B or C network)
|
||||||
|
|
||||||
[notifications]
|
[notifications]
|
||||||
method="pushover" # For now just pushover, you can set this to "none" to avoid sending notifications (it will just log)
|
method="pushover" # For now just pushover, you can set this to "none" to avoid sending notifications (it will just log)
|
||||||
|
|||||||
Reference in New Issue
Block a user