168 lines
5.3 KiB
Rust
168 lines
5.3 KiB
Rust
use std::sync::Arc;
|
|
use anyhow::Result;
|
|
use tokio::time::{interval, Duration};
|
|
use tracing::{info, error, warn};
|
|
use serde::Deserialize;
|
|
|
|
use crate::command::CommandDispatcher;
|
|
use crate::server::ServerManager;
|
|
|
|
/// A scheduled task received from the panel API.
|
|
#[derive(Debug, Clone, Deserialize)]
|
|
pub struct ScheduledTask {
|
|
pub id: String,
|
|
pub server_uuid: String,
|
|
pub action: String, // "command", "power", "backup"
|
|
pub payload: String, // command string, power action, or "backup"
|
|
pub schedule_type: String,
|
|
pub is_active: bool,
|
|
pub next_run_at: Option<String>, // ISO 8601
|
|
}
|
|
|
|
/// Scheduler that polls the panel API for due tasks and executes them.
|
|
pub struct Scheduler {
|
|
server_manager: Arc<ServerManager>,
|
|
command_dispatcher: Arc<CommandDispatcher>,
|
|
api_url: String,
|
|
node_token: String,
|
|
poll_interval_secs: u64,
|
|
}
|
|
|
|
impl Scheduler {
|
|
pub fn new(
|
|
server_manager: Arc<ServerManager>,
|
|
command_dispatcher: Arc<CommandDispatcher>,
|
|
api_url: String,
|
|
node_token: String,
|
|
) -> Self {
|
|
Self {
|
|
server_manager,
|
|
command_dispatcher,
|
|
api_url,
|
|
node_token,
|
|
poll_interval_secs: 15,
|
|
}
|
|
}
|
|
|
|
/// Run the scheduler loop. This should be spawned as a tokio task.
|
|
pub async fn run(self: Arc<Self>) {
|
|
info!("Scheduler started (poll interval: {}s)", self.poll_interval_secs);
|
|
let mut tick = interval(Duration::from_secs(self.poll_interval_secs));
|
|
|
|
loop {
|
|
tick.tick().await;
|
|
if let Err(e) = self.poll_and_execute().await {
|
|
error!(error = %e, "Scheduler poll failed");
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Poll the API for due tasks and execute them.
|
|
async fn poll_and_execute(&self) -> Result<()> {
|
|
let client = reqwest::Client::new();
|
|
let url = format!("{}/api/internal/schedules/due", self.api_url);
|
|
|
|
let resp = client
|
|
.get(&url)
|
|
.bearer_auth(&self.node_token)
|
|
.send()
|
|
.await?;
|
|
|
|
if !resp.status().is_success() {
|
|
warn!(status = %resp.status(), "Failed to fetch due tasks");
|
|
return Ok(());
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
struct DueResponse {
|
|
tasks: Vec<ScheduledTask>,
|
|
}
|
|
|
|
let due: DueResponse = resp.json().await?;
|
|
if due.tasks.is_empty() {
|
|
return Ok(());
|
|
}
|
|
|
|
info!(count = due.tasks.len(), "Processing due scheduled tasks");
|
|
|
|
for task in &due.tasks {
|
|
if let Err(e) = self.execute_task(task).await {
|
|
error!(
|
|
task_id = %task.id,
|
|
server = %task.server_uuid,
|
|
error = %e,
|
|
"Failed to execute scheduled task"
|
|
);
|
|
}
|
|
|
|
// Notify API that task was executed
|
|
let ack_url = format!(
|
|
"{}/api/internal/schedules/{}/ack",
|
|
self.api_url, task.id
|
|
);
|
|
let _ = client
|
|
.post(&ack_url)
|
|
.bearer_auth(&self.node_token)
|
|
.send()
|
|
.await;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Execute a single scheduled task.
|
|
async fn execute_task(&self, task: &ScheduledTask) -> Result<()> {
|
|
info!(
|
|
task_id = %task.id,
|
|
action = %task.action,
|
|
server = %task.server_uuid,
|
|
"Executing scheduled task"
|
|
);
|
|
|
|
match task.action.as_str() {
|
|
"command" => {
|
|
self.command_dispatcher
|
|
.send_command(&task.server_uuid, &task.payload)
|
|
.await?;
|
|
}
|
|
"power" => {
|
|
match task.payload.as_str() {
|
|
"start" => self.server_manager.start_server(&task.server_uuid).await?,
|
|
"stop" => self.server_manager.stop_server(&task.server_uuid, None, 0).await?,
|
|
"restart" => {
|
|
let _ = self.server_manager.stop_server(&task.server_uuid, None, 0).await;
|
|
tokio::time::sleep(Duration::from_secs(3)).await;
|
|
self.server_manager.start_server(&task.server_uuid).await?;
|
|
}
|
|
"kill" => self.server_manager.kill_server(&task.server_uuid).await?,
|
|
_ => warn!(payload = %task.payload, "Unknown power action"),
|
|
}
|
|
}
|
|
"backup" => {
|
|
// Trigger backup via the backup module
|
|
info!(
|
|
server = %task.server_uuid,
|
|
"Backup scheduled task — delegating to backup module"
|
|
);
|
|
// Backup is handled by sending callback to API
|
|
let client = reqwest::Client::new();
|
|
let url = format!(
|
|
"{}/api/internal/servers/{}/backup",
|
|
self.api_url, task.server_uuid
|
|
);
|
|
let _ = client
|
|
.post(&url)
|
|
.bearer_auth(&self.node_token)
|
|
.json(&serde_json::json!({ "name": format!("auto-{}", task.id) }))
|
|
.send()
|
|
.await;
|
|
}
|
|
_ => {
|
|
warn!(action = %task.action, "Unknown scheduled action");
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|