Files
harmony/opnsense-api/examples/firmware_update.rs
Jean-Gabriel Gill-Couture da90dc55ad
Some checks failed
Run Check Script / check (pull_request) Failing after 19s
chore: cargo fmt across workspace
2026-03-25 23:20:57 -04:00

194 lines
6.7 KiB
Rust

//! Example: check for firmware updates and apply them.
//!
//! ```text
//! cargo run --example firmware_update
//! ```
//!
//! This runs a full firmware update workflow:
//! 1. POST /api/core/firmware/check — triggers a background check for updates
//! 2. POST /api/core/firmware/status — retrieves the check results
//! 3. If updates are available, POST /api/core/firmware/update — applies them
//!
//! The check can take a while since it connects to the update mirror.
use std::env;
use opnsense_api::client::OpnsenseClient;
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize)]
pub struct FirmwareCheckResponse {
pub status: String,
pub msg_uuid: String,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct FirmwareStatusResponse {
pub status: String,
#[serde(default)]
pub status_msg: Option<String>,
#[serde(default)]
pub all_packages: serde_json::Value,
#[serde(default)]
pub product: serde_json::Value,
#[serde(default)]
pub status_reboot: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct FirmwareActionResponse {
pub status: String,
#[serde(default)]
pub msg_uuid: String,
#[serde(default)]
pub status_msg: Option<String>,
}
fn build_client() -> OpnsenseClient {
let base_url =
env::var("OPNSENSE_BASE_URL").unwrap_or_else(|_| "https://192.168.1.1/api".to_string());
match (
env::var("OPNSENSE_API_KEY").ok(),
env::var("OPNSENSE_API_SECRET").ok(),
) {
(Some(key), Some(secret)) => OpnsenseClient::builder()
.base_url(&base_url)
.auth_from_key_secret(&key, &secret)
.skip_tls_verify()
.timeout_secs(120)
.build()
.expect("failed to build HTTP client"),
_ => {
eprintln!("ERROR: OPNSENSE_API_KEY and OPNSENSE_API_SECRET must be set.");
eprintln!(" export OPNSENSE_API_KEY=your_key");
eprintln!(" export OPNSENSE_API_SECRET=your_secret");
eprintln!(" export OPNSENSE_BASE_URL=https://your-firewall/api");
std::process::exit(1);
}
}
}
#[tokio::main]
async fn main() {
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
let client = build_client();
println!();
println!("╔═══════════════════════════════════════════════════════════╗");
println!("║ OPNsense Firmware Update ║");
println!("╚═══════════════════════════════════════════════════════════╝");
println!();
println!(" [1/2] Checking for updates (this may take a moment) ...");
println!();
log::info!("POST /api/core/firmware/check");
let check: FirmwareCheckResponse = client
.post_typed("core", "firmware", "check", None::<&()>)
.await
.expect("check request failed");
println!(" Check triggered, msg_uuid: {}", check.msg_uuid);
println!();
println!(" Waiting for check to complete ...");
tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
println!();
println!(" [2/2] Fetching update status ...");
log::info!("POST /api/core/firmware/status");
let status: FirmwareStatusResponse = client
.post_typed("core", "firmware", "status", None::<&()>)
.await
.expect("status request failed");
println!();
match status.status.as_str() {
"none" => {
println!(
"{}",
status
.status_msg
.as_deref()
.unwrap_or("No updates available.")
);
}
"update" | "upgrade" => {
println!(
"{}",
status.status_msg.as_deref().unwrap_or("Updates available.")
);
if let Some(reboot) = status.status_reboot {
if reboot == "1" {
println!(" ⚠ This update requires a reboot.");
}
}
let pkg_count = if let serde_json::Value::Object(ref map) = status.all_packages {
map.len()
} else {
0
};
if pkg_count > 0 {
println!(" {} package(s) to update:", pkg_count);
if let serde_json::Value::Object(ref packages) = status.all_packages {
for (name, info) in packages.iter().take(10) {
let reason = info.get("reason").and_then(|v| v.as_str()).unwrap_or("?");
let old_ver = info.get("old").and_then(|v| v.as_str()).unwrap_or("N/A");
let new_ver = info.get("new").and_then(|v| v.as_str()).unwrap_or("?");
println!(" {:40} {} {}{}", name, reason, old_ver, new_ver);
}
if pkg_count > 10 {
println!(" ... and {} more", pkg_count - 10);
}
}
}
println!();
println!(
" Run with environment variable OPNSENSE_FIRMWARE_UPDATE=1 to apply updates."
);
println!(" WARNING: firmware updates can cause connectivity interruptions.");
}
"error" => {
println!(
" ✗ Error: {}",
status.status_msg.as_deref().unwrap_or("Unknown error")
);
}
other => {
println!(" ? Unexpected status: {other}");
println!(
" Full response: {}",
serde_json::to_string_pretty(&status).unwrap()
);
}
}
if env::var("OPNSENSE_FIRMWARE_UPDATE").as_deref() == Ok("1") {
if status.status == "update" || status.status == "upgrade" {
println!();
println!(" Applying firmware update ...");
log::info!("POST /api/core/firmware/update");
let result: FirmwareActionResponse = client
.post_typed("core", "firmware", "update", None::<&()>)
.await
.expect("update request failed");
if result.status == "ok" {
println!(" ✓ Update started, msg_uuid: {}", result.msg_uuid);
println!(" The firewall is updating in the background.");
} else {
println!(" ✗ Update failed: {:?}", result.status_msg);
}
} else {
println!();
println!(" No updates to apply.");
}
}
}