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::device_finders;
|
||||
use crate::events;
|
||||
use crate::settings::get_settings;
|
||||
use chrono::Utc;
|
||||
use log::{debug, info};
|
||||
use tokio::time::{Duration, sleep};
|
||||
|
||||
pub async fn scan() -> Result<(), Box<dyn std::error::Error>> {
|
||||
loop {
|
||||
arp_scanner_status::set_running();
|
||||
|
||||
// Find online devices via ARP
|
||||
let devices =
|
||||
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!(
|
||||
"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();
|
||||
|
||||
// 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!(
|
||||
"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;
|
||||
info!(
|
||||
"Scan duration set to {}",
|
||||
get_settings().timings.arp_scan_duration
|
||||
get_settings().arp_scanner.scan_duration
|
||||
);
|
||||
info!(
|
||||
"Receiver timeout set to {}",
|
||||
|
||||
+6
-2
@@ -2,6 +2,8 @@ use crate::settings::get_settings;
|
||||
use clap::Parser;
|
||||
use log::{LevelFilter, info};
|
||||
|
||||
mod arp_scanner;
|
||||
mod arp_scanner_status;
|
||||
mod db;
|
||||
mod device_finders;
|
||||
mod events;
|
||||
@@ -9,7 +11,6 @@ mod mac_vendor_finder;
|
||||
mod vendor_device_type_finder;
|
||||
mod model;
|
||||
mod retention;
|
||||
mod scanner;
|
||||
mod settings;
|
||||
mod utils;
|
||||
mod web_server;
|
||||
@@ -58,6 +59,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Initialize database
|
||||
db::init_db().await?;
|
||||
|
||||
// Initialize ARP scanner status tracking
|
||||
arp_scanner_status::init();
|
||||
|
||||
// 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)]
|
||||
pub struct Timings {
|
||||
pub struct ArpScanner {
|
||||
pub wait_between_scans: DurationString,
|
||||
pub arp_sender_timeout: DurationString,
|
||||
pub arp_scan_duration: DurationString,
|
||||
pub sender_timeout: DurationString,
|
||||
pub scan_duration: DurationString,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
@@ -66,7 +66,7 @@ pub struct Settings {
|
||||
pub database: Database,
|
||||
pub networking: Networking,
|
||||
pub log: Log,
|
||||
pub timings: Timings,
|
||||
pub arp_scanner: ArpScanner,
|
||||
pub notifications: Notifications,
|
||||
pub web_server: WebServer,
|
||||
#[serde(default)]
|
||||
|
||||
@@ -4,6 +4,7 @@ use crate::model::device_events::{DeviceEvent, DeviceEventType};
|
||||
use crate::model::devices::Device;
|
||||
use crate::model::notifications::{Notification, NotificationType};
|
||||
use crate::settings::get_settings;
|
||||
use crate::web_server::arp_scanner::ArpScannerStatusResponse;
|
||||
use crate::web_server::devices::RegisterDevicePayload;
|
||||
use axum::extract::Request;
|
||||
use axum::http::StatusCode;
|
||||
@@ -21,6 +22,7 @@ use utoipa::openapi::security::{HttpAuthScheme, HttpBuilder, SecurityScheme};
|
||||
use utoipa::Modify;
|
||||
use utoipa_swagger_ui::SwaggerUi;
|
||||
|
||||
pub mod arp_scanner;
|
||||
pub mod device_events;
|
||||
pub mod devices;
|
||||
pub mod notifications;
|
||||
@@ -45,6 +47,7 @@ pub mod utils;
|
||||
notifications::mark_as_new,
|
||||
notifications::mark_all_as_old,
|
||||
device_events::list,
|
||||
arp_scanner::status,
|
||||
),
|
||||
components(schemas(
|
||||
Device,
|
||||
@@ -53,12 +56,14 @@ pub mod utils;
|
||||
RegisterDevicePayload,
|
||||
DeviceEvent,
|
||||
DeviceEventType,
|
||||
ArpScannerStatusResponse,
|
||||
)),
|
||||
modifiers(&SecurityAddon),
|
||||
tags(
|
||||
(name = "devices", description = "Device management"),
|
||||
(name = "notifications", description = "Notification management"),
|
||||
(name = "device_events", description = "Device event history"),
|
||||
(name = "arp_scanner", description = "ARP scanner process status"),
|
||||
)
|
||||
)]
|
||||
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}", get(devices::read))
|
||||
.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/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]
|
||||
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
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
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]
|
||||
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