feat: add bump-version.sh script for version management
Automates version bumps across all version source files: - Cargo.toml (PRIMARY - workspace.package.version) - debian/changelog (prepend new entry) - debian/control (update Version field) - scripts/build-package.sh (update VERSION variable) - frontend/package.json (update version field) - Stale references check after bump Usage: ./scripts/bump-version.sh <new_version> <old_version>
This commit is contained in:
19
crates/pm-agent-client/Cargo.toml
Normal file
19
crates/pm-agent-client/Cargo.toml
Normal file
@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "pm-agent-client"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
pm-core = { path = "../pm-core" }
|
||||
tokio = { workspace = true }
|
||||
reqwest = { workspace = true }
|
||||
rustls = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
274
crates/pm-agent-client/src/client.rs
Executable file
274
crates/pm-agent-client/src/client.rs
Executable file
@ -0,0 +1,274 @@
|
||||
//! mTLS HTTP client for communicating with Linux Patch API agents.
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
//! ```no_run
|
||||
//! use pm_agent_client::client::AgentClient;
|
||||
//!
|
||||
//! # async fn example() -> Result<(), pm_agent_client::error::AgentClientError> {
|
||||
//! let client = AgentClient::new(
|
||||
//! "192.168.1.10",
|
||||
//! 12443,
|
||||
//! include_bytes!("../certs/client.crt"),
|
||||
//! include_bytes!("../certs/client.key"),
|
||||
//! include_bytes!("../certs/ca.crt"),
|
||||
//! )?;
|
||||
//!
|
||||
//! let health = client.health().await?;
|
||||
//! println!("Agent status: {}", health.status);
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use reqwest::{tls::Version, Certificate, ClientBuilder, Identity};
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
use tracing::{debug, instrument};
|
||||
|
||||
use crate::{
|
||||
error::AgentClientError,
|
||||
types::{
|
||||
AgentEnvelope, AgentJobStatus, ApplyPatchesRequest, ApplyPatchesResponse, HealthData,
|
||||
PackagesData, PatchesData, RollbackResponse, ServiceStatusData, SystemInfoData,
|
||||
},
|
||||
};
|
||||
|
||||
/// Default TCP port that the Linux Patch API agent listens on.
|
||||
pub const DEFAULT_AGENT_PORT: u16 = 12443;
|
||||
|
||||
/// Request timeout applied to every agent API call.
|
||||
const REQUEST_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
|
||||
// ============================================================
|
||||
// AgentClient
|
||||
// ============================================================
|
||||
|
||||
/// Async HTTP client that speaks mTLS to a single Linux Patch API agent.
|
||||
///
|
||||
/// Construct once via [`AgentClient::new`] and reuse across calls;
|
||||
/// the underlying [`reqwest::Client`] maintains a connection pool.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AgentClient {
|
||||
/// Underlying HTTP client (configured for mTLS + TLS 1.3).
|
||||
inner: reqwest::Client,
|
||||
/// Base URL of the agent, e.g. `https://10.0.0.5:12443/api/v1`.
|
||||
base_url: String,
|
||||
}
|
||||
|
||||
impl AgentClient {
|
||||
/// Create a new [`AgentClient`] configured for mTLS.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `host_ip` – IP address (or hostname) of the agent.
|
||||
/// * `port` – TCP port the agent listens on (default [`DEFAULT_AGENT_PORT`]).
|
||||
/// * `client_cert_pem` – PEM-encoded client certificate presented during the TLS handshake.
|
||||
/// * `client_key_pem` – PEM-encoded private key matching `client_cert_pem`.
|
||||
/// * `ca_cert_pem` – PEM-encoded CA certificate used to verify the agent's server cert.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`AgentClientError::Tls`] when certificate parsing fails, or
|
||||
/// [`AgentClientError::Request`] when `reqwest` client construction fails.
|
||||
pub fn new(
|
||||
host_ip: &str,
|
||||
port: u16,
|
||||
client_cert_pem: &[u8],
|
||||
client_key_pem: &[u8],
|
||||
ca_cert_pem: &[u8],
|
||||
) -> Result<Self, AgentClientError> {
|
||||
// Build client identity: reqwest expects cert + key concatenated as PEM.
|
||||
let mut identity_pem = Vec::with_capacity(client_cert_pem.len() + client_key_pem.len());
|
||||
identity_pem.extend_from_slice(client_cert_pem);
|
||||
identity_pem.extend_from_slice(client_key_pem);
|
||||
|
||||
let identity = Identity::from_pem(&identity_pem)
|
||||
.map_err(|e| AgentClientError::Tls(format!("invalid client identity PEM: {e}")))?;
|
||||
|
||||
// Parse the CA certificate used to verify the agent's server certificate.
|
||||
let ca_cert = Certificate::from_pem(ca_cert_pem)
|
||||
.map_err(|e| AgentClientError::Tls(format!("invalid CA certificate PEM: {e}")))?;
|
||||
|
||||
// Build the reqwest client:
|
||||
// - force rustls TLS backend
|
||||
// - disable built-in OS/system trust roots (only trust our internal CA)
|
||||
// - enforce TLS 1.3 minimum
|
||||
// - attach client identity (mTLS)
|
||||
// - add our CA as a trusted root
|
||||
// - apply a global request timeout
|
||||
let inner = ClientBuilder::new()
|
||||
.use_rustls_tls()
|
||||
.tls_built_in_root_certs(false)
|
||||
.min_tls_version(Version::TLS_1_3)
|
||||
.identity(identity)
|
||||
.add_root_certificate(ca_cert)
|
||||
.timeout(REQUEST_TIMEOUT)
|
||||
.build()
|
||||
.map_err(AgentClientError::Request)?;
|
||||
|
||||
let clean_ip = host_ip.split('/').next().unwrap_or(host_ip);
|
||||
let base_url = format!("https://{}:{}/api/v1", clean_ip, port);
|
||||
|
||||
Ok(Self { inner, base_url })
|
||||
}
|
||||
|
||||
// --------------------------------------------------------
|
||||
// Public API methods
|
||||
// --------------------------------------------------------
|
||||
|
||||
/// `GET /api/v1/health` — check agent liveness and retrieve uptime.
|
||||
#[instrument(skip(self), fields(base_url = %self.base_url))]
|
||||
pub async fn health(&self) -> Result<HealthData, AgentClientError> {
|
||||
self.get("health", &[]).await
|
||||
}
|
||||
|
||||
/// `GET /api/v1/system/info` — retrieve host system information.
|
||||
#[instrument(skip(self), fields(base_url = %self.base_url))]
|
||||
pub async fn system_info(&self) -> Result<SystemInfoData, AgentClientError> {
|
||||
self.get("system/info", &[]).await
|
||||
}
|
||||
|
||||
/// `GET /api/v1/packages?status=upgradable` — list packages with available upgrades.
|
||||
#[instrument(skip(self), fields(base_url = %self.base_url))]
|
||||
pub async fn packages_upgradable(&self) -> Result<PackagesData, AgentClientError> {
|
||||
self.get("packages", &[("status", "upgradable")]).await
|
||||
}
|
||||
|
||||
/// `GET /api/v1/patches` — list available patches with severity and CVE data.
|
||||
#[instrument(skip(self), fields(base_url = %self.base_url))]
|
||||
pub async fn patches(&self) -> Result<PatchesData, AgentClientError> {
|
||||
self.get("patches", &[]).await
|
||||
}
|
||||
|
||||
// --------------------------------------------------------
|
||||
// Private helpers
|
||||
// --------------------------------------------------------
|
||||
|
||||
/// Execute a GET request against `{base_url}/{path}` with optional query
|
||||
/// parameters, deserialize the [`AgentEnvelope`], and extract the `data`
|
||||
/// field — or propagate an [`AgentClientError::ApiError`].
|
||||
async fn get<T>(&self, path: &str, query: &[(&str, &str)]) -> Result<T, AgentClientError>
|
||||
where
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
let url = format!("{}/{}", self.base_url, path);
|
||||
debug!(url = %url, ?query, "Sending GET request to agent");
|
||||
|
||||
let mut request = self.inner.get(&url);
|
||||
if !query.is_empty() {
|
||||
request = request.query(query);
|
||||
}
|
||||
|
||||
let response = request.send().await?;
|
||||
let status = response.status();
|
||||
debug!(url = %url, status = %status, "Received response from agent");
|
||||
|
||||
// Capture body text so we can attempt to deserialise the error envelope
|
||||
// even for non-2xx responses.
|
||||
let body = response.text().await?;
|
||||
|
||||
// Attempt to parse the standard agent envelope regardless of HTTP status.
|
||||
// The agent may embed a structured error body on 4xx/5xx responses.
|
||||
let envelope: AgentEnvelope<T> = serde_json::from_str(&body)?;
|
||||
|
||||
if !status.is_success() || !envelope.success {
|
||||
// Prefer the structured error from the envelope when present.
|
||||
if let Some(err) = envelope.error {
|
||||
return Err(AgentClientError::ApiError {
|
||||
code: err.code,
|
||||
message: err.message,
|
||||
});
|
||||
}
|
||||
// Fallback: use the HTTP status as the error indicator.
|
||||
return Err(AgentClientError::ApiError {
|
||||
code: status.as_str().to_string(),
|
||||
message: format!("Agent returned HTTP {} for {}", status.as_u16(), url),
|
||||
});
|
||||
}
|
||||
|
||||
// On success the `data` field must be present.
|
||||
envelope.data.ok_or_else(|| AgentClientError::ApiError {
|
||||
code: "MISSING_DATA".to_string(),
|
||||
message: "Agent response success=true but data field is absent".to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
// --------------------------------------------------------
|
||||
// Patch apply / job management methods
|
||||
// --------------------------------------------------------
|
||||
|
||||
/// `POST /api/v1/patches/apply` — trigger patch application on the agent.
|
||||
#[instrument(skip(self, req), fields(base_url = %self.base_url))]
|
||||
pub async fn apply_patches(
|
||||
&self,
|
||||
req: &ApplyPatchesRequest,
|
||||
) -> Result<ApplyPatchesResponse, AgentClientError> {
|
||||
self.post("patches/apply", req).await
|
||||
}
|
||||
|
||||
/// `GET /api/v1/jobs/{id}` — poll an async agent job for status.
|
||||
#[instrument(skip(self), fields(base_url = %self.base_url, job_id = %job_id))]
|
||||
pub async fn job_status(&self, job_id: &str) -> Result<AgentJobStatus, AgentClientError> {
|
||||
self.get(&format!("jobs/{}", job_id), &[]).await
|
||||
}
|
||||
|
||||
/// `POST /api/v1/jobs/{id}/rollback` — trigger rollback on the agent.
|
||||
#[instrument(skip(self), fields(base_url = %self.base_url, job_id = %job_id))]
|
||||
pub async fn rollback_job(&self, job_id: &str) -> Result<RollbackResponse, AgentClientError> {
|
||||
let empty: serde_json::Value = serde_json::json!({});
|
||||
self.post(&format!("jobs/{}/rollback", job_id), &empty)
|
||||
.await
|
||||
}
|
||||
|
||||
/// `GET /api/v1/system/services/{name}` — check status of a specific service on the agent.
|
||||
#[instrument(skip(self), fields(base_url = %self.base_url, service_name = %service_name))]
|
||||
pub async fn service_status(
|
||||
&self,
|
||||
service_name: &str,
|
||||
) -> Result<ServiceStatusData, AgentClientError> {
|
||||
self.get(&format!("system/services/{}", service_name), &[])
|
||||
.await
|
||||
}
|
||||
|
||||
// --------------------------------------------------------
|
||||
// Private POST helper
|
||||
// --------------------------------------------------------
|
||||
|
||||
/// Execute a POST request against `{base_url}/{path}`, serialize `body` as
|
||||
/// JSON, deserialize the [`AgentEnvelope`], and extract the `data` field —
|
||||
/// or propagate an [`AgentClientError::ApiError`].
|
||||
async fn post<Req, Resp>(&self, path: &str, body: &Req) -> Result<Resp, AgentClientError>
|
||||
where
|
||||
Req: Serialize,
|
||||
Resp: DeserializeOwned,
|
||||
{
|
||||
let url = format!("{}/{}", self.base_url, path);
|
||||
debug!(url = %url, "Sending POST request to agent");
|
||||
|
||||
let response = self.inner.post(&url).json(body).send().await?;
|
||||
let status = response.status();
|
||||
debug!(url = %url, status = %status, "Received POST response from agent");
|
||||
|
||||
let body_text = response.text().await?;
|
||||
let envelope: AgentEnvelope<Resp> = serde_json::from_str(&body_text)?;
|
||||
|
||||
if !status.is_success() || !envelope.success {
|
||||
if let Some(err) = envelope.error {
|
||||
return Err(AgentClientError::ApiError {
|
||||
code: err.code,
|
||||
message: err.message,
|
||||
});
|
||||
}
|
||||
return Err(AgentClientError::ApiError {
|
||||
code: status.as_str().to_string(),
|
||||
message: format!("Agent returned HTTP {} for {}", status.as_u16(), url),
|
||||
});
|
||||
}
|
||||
|
||||
envelope.data.ok_or_else(|| AgentClientError::ApiError {
|
||||
code: "MISSING_DATA".to_string(),
|
||||
message: "Agent response success=true but data field is absent".to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
49
crates/pm-agent-client/src/error.rs
Executable file
49
crates/pm-agent-client/src/error.rs
Executable file
@ -0,0 +1,49 @@
|
||||
//! Error types for the pm-agent-client crate.
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
/// Top-level error type returned by [`crate::client::AgentClient`] methods.
|
||||
#[derive(Debug, Error)]
|
||||
pub enum AgentClientError {
|
||||
/// TLS configuration or handshake failure.
|
||||
#[error("TLS error: {0}")]
|
||||
Tls(String),
|
||||
|
||||
/// Unable to establish a TCP/TLS connection to the agent.
|
||||
#[error("Connection error: {0}")]
|
||||
Connect(#[source] reqwest::Error),
|
||||
|
||||
/// An HTTP request or response transport error (not a timeout).
|
||||
#[error("Request error: {0}")]
|
||||
Request(#[source] reqwest::Error),
|
||||
|
||||
/// The request did not complete within the configured timeout.
|
||||
#[error("Request timed out")]
|
||||
Timeout,
|
||||
|
||||
/// The agent returned a non-2xx HTTP status or `success: false` in the
|
||||
/// response envelope.
|
||||
#[error("Agent API error [{code}]: {message}")]
|
||||
ApiError {
|
||||
/// Machine-readable error code supplied by the agent (e.g. `"NOT_FOUND"`).
|
||||
code: String,
|
||||
/// Human-readable description returned by the agent.
|
||||
message: String,
|
||||
},
|
||||
|
||||
/// JSON deserialization of the agent response failed.
|
||||
#[error("Failed to deserialise agent response: {0}")]
|
||||
Deserialize(#[from] serde_json::Error),
|
||||
}
|
||||
|
||||
impl From<reqwest::Error> for AgentClientError {
|
||||
fn from(err: reqwest::Error) -> Self {
|
||||
if err.is_timeout() {
|
||||
AgentClientError::Timeout
|
||||
} else if err.is_connect() {
|
||||
AgentClientError::Connect(err)
|
||||
} else {
|
||||
AgentClientError::Request(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
43
crates/pm-agent-client/src/lib.rs
Executable file
43
crates/pm-agent-client/src/lib.rs
Executable file
@ -0,0 +1,43 @@
|
||||
//! `pm-agent-client` — mTLS HTTP client for Linux Patch API agent communication.
|
||||
//!
|
||||
//! This crate provides [`client::AgentClient`], an async HTTP client that
|
||||
//! establishes mutual-TLS connections (TLS 1.3) to `linux_patch_api` agents
|
||||
//! running on managed hosts.
|
||||
//!
|
||||
//! # Quick start
|
||||
//!
|
||||
//! ```no_run
|
||||
//! use pm_agent_client::AgentClient;
|
||||
//!
|
||||
//! # async fn run() -> Result<(), pm_agent_client::AgentClientError> {
|
||||
//! let client = AgentClient::new(
|
||||
//! "10.0.1.5",
|
||||
//! 12443,
|
||||
//! include_bytes!("../certs/client.crt"),
|
||||
//! include_bytes!("../certs/client.key"),
|
||||
//! include_bytes!("../certs/ca.crt"),
|
||||
//! )?;
|
||||
//!
|
||||
//! let health = client.health().await?;
|
||||
//! println!("Agent {}: {}", health.status, health.version);
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
|
||||
pub mod client;
|
||||
pub mod error;
|
||||
pub mod types;
|
||||
|
||||
// ── Convenience re-exports ──────────────────────────────────────────────────
|
||||
|
||||
/// Primary client — re-exported from [`client::AgentClient`].
|
||||
pub use client::{AgentClient, DEFAULT_AGENT_PORT};
|
||||
|
||||
/// Error type — re-exported from [`error::AgentClientError`].
|
||||
pub use error::AgentClientError;
|
||||
|
||||
/// Response envelope and all data types.
|
||||
pub use types::{
|
||||
AgentEnvelope, AgentErrorBody, HealthData, Package, PackagesData, Patch, PatchesData,
|
||||
RollbackResponse, ServiceStatusData, SystemInfoData,
|
||||
};
|
||||
230
crates/pm-agent-client/src/types.rs
Executable file
230
crates/pm-agent-client/src/types.rs
Executable file
@ -0,0 +1,230 @@
|
||||
//! Response and request types for the Linux Patch API agent endpoints.
|
||||
//!
|
||||
//! All agent responses are wrapped in [`AgentEnvelope<T>`].
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
// ============================================================
|
||||
// Envelope & error
|
||||
// ============================================================
|
||||
|
||||
/// Generic response wrapper returned by every agent endpoint.
|
||||
///
|
||||
/// ```json
|
||||
/// { "success": true, "request_id": "…", "timestamp": "…", "data": {…}, "error": null }
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct AgentEnvelope<T> {
|
||||
/// `true` when the request succeeded; `false` on error.
|
||||
pub success: bool,
|
||||
/// Server-assigned request identifier (UUID v4).
|
||||
pub request_id: Uuid,
|
||||
/// Server timestamp for the response (ISO-8601 / RFC-3339).
|
||||
pub timestamp: DateTime<Utc>,
|
||||
/// Response payload — present when `success` is `true`.
|
||||
pub data: Option<T>,
|
||||
/// Error detail — present when `success` is `false`.
|
||||
pub error: Option<AgentErrorBody>,
|
||||
}
|
||||
|
||||
/// Structured error returned inside [`AgentEnvelope::error`].
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct AgentErrorBody {
|
||||
/// Machine-readable error code (e.g. `"INTERNAL_ERROR"`).
|
||||
pub code: String,
|
||||
/// Human-readable description of what went wrong.
|
||||
pub message: String,
|
||||
/// Optional free-form extra detail from the agent.
|
||||
#[serde(default)]
|
||||
pub details: Option<serde_json::Value>,
|
||||
/// Whether the caller may safely retry the request.
|
||||
#[serde(default)]
|
||||
pub retryable: bool,
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// GET /api/v1/health
|
||||
// ============================================================
|
||||
|
||||
/// Payload returned by `GET /api/v1/health`.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct HealthData {
|
||||
/// Agent status string, e.g. `"ok"` or `"degraded"`.
|
||||
pub status: String,
|
||||
/// Seconds elapsed since the agent process started.
|
||||
pub uptime_seconds: u64,
|
||||
/// Agent software version string.
|
||||
pub version: String,
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// GET /api/v1/system/info
|
||||
// ============================================================
|
||||
|
||||
/// Payload returned by `GET /api/v1/system/info`.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct SystemInfoData {
|
||||
/// Hostname of the managed system.
|
||||
pub hostname: String,
|
||||
/// OS family / distribution name (e.g. `"Ubuntu"`).
|
||||
pub os: String,
|
||||
/// OS version string.
|
||||
pub os_version: String,
|
||||
/// Kernel version string.
|
||||
pub kernel: String,
|
||||
/// CPU architecture (e.g. `"x86_64"`).
|
||||
pub architecture: String,
|
||||
/// When the agent last checked for updates (`null` if never).
|
||||
pub last_update_check: Option<DateTime<Utc>>,
|
||||
/// When updates were last applied (`null` if never).
|
||||
pub last_update_apply: Option<DateTime<Utc>>,
|
||||
/// Whether the system has a pending reboot.
|
||||
pub pending_reboot: bool,
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// GET /api/v1/packages?status=upgradable
|
||||
// ============================================================
|
||||
|
||||
/// A single package entry.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct Package {
|
||||
/// Package name.
|
||||
pub name: String,
|
||||
/// Installed version.
|
||||
pub version: String,
|
||||
/// Package status string (e.g. `"installed"`, `"upgradable"`).
|
||||
pub status: String,
|
||||
/// Whether a newer version is available.
|
||||
pub upgradable: bool,
|
||||
/// Latest available version (`null` if not upgradable).
|
||||
pub latest_version: Option<String>,
|
||||
/// Short package description.
|
||||
pub description: String,
|
||||
/// CVE identifiers associated with this package.
|
||||
#[serde(default)]
|
||||
pub cve_ids: Vec<String>,
|
||||
}
|
||||
|
||||
/// Payload returned by `GET /api/v1/packages`.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct PackagesData {
|
||||
/// List of packages matching the query filters.
|
||||
pub packages: Vec<Package>,
|
||||
/// Total count of matching packages.
|
||||
pub total: u64,
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// GET /api/v1/patches
|
||||
// ============================================================
|
||||
|
||||
/// A single available patch.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct Patch {
|
||||
/// Package / patch name.
|
||||
pub name: String,
|
||||
/// Currently installed version.
|
||||
pub current_version: String,
|
||||
/// Version available after applying this patch.
|
||||
pub available_version: String,
|
||||
/// Severity level (e.g. `"critical"`, `"high"`, `"medium"`, `"low"`).
|
||||
pub severity: String,
|
||||
/// Human-readable description of the patch.
|
||||
pub description: String,
|
||||
/// CVE identifiers addressed by this patch.
|
||||
#[serde(default)]
|
||||
pub cve_ids: Vec<String>,
|
||||
/// Whether applying this patch requires a system reboot.
|
||||
pub requires_reboot: bool,
|
||||
}
|
||||
|
||||
/// Payload returned by `GET /api/v1/patches`.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct PatchesData {
|
||||
/// List of available patches.
|
||||
pub patches: Vec<Patch>,
|
||||
/// Total patch count.
|
||||
pub total: u64,
|
||||
/// Number of patches classified as security updates.
|
||||
pub security_updates: u64,
|
||||
/// Whether any patch in the list requires a reboot.
|
||||
pub requires_reboot: bool,
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// POST /api/v1/patches/apply
|
||||
// ============================================================
|
||||
|
||||
/// Request body for `POST /api/v1/patches/apply`.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ApplyPatchesRequest {
|
||||
/// Package names to apply. Empty = apply all available patches.
|
||||
pub packages: Vec<String>,
|
||||
/// If true, allow automatic reboot after patching if required.
|
||||
pub allow_reboot: bool,
|
||||
}
|
||||
|
||||
/// Response from `POST /api/v1/patches/apply`.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct ApplyPatchesResponse {
|
||||
/// Agent-assigned async job ID for status polling.
|
||||
pub job_id: String,
|
||||
/// Initial status: typically `"running"` or `"queued"`.
|
||||
pub status: String,
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// GET /api/v1/jobs/{id}
|
||||
// ============================================================
|
||||
|
||||
/// Status of an async agent job returned by `GET /api/v1/jobs/{id}`.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct AgentJobStatus {
|
||||
pub job_id: String,
|
||||
/// Current status: `"queued"`, `"running"`, `"succeeded"`, `"completed"`, `"failed"`, or `"cancelled"`.
|
||||
pub status: String,
|
||||
pub progress_percent: Option<u8>,
|
||||
pub output: Option<String>,
|
||||
pub error: Option<String>,
|
||||
pub started_at: Option<DateTime<Utc>>,
|
||||
pub completed_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// GET /api/v1/system/services/{name}
|
||||
// ============================================================
|
||||
|
||||
/// Payload returned by `GET /api/v1/system/services/{name}`.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct ServiceStatusData {
|
||||
/// Service name.
|
||||
pub name: String,
|
||||
/// Human-readable service name.
|
||||
pub display_name: String,
|
||||
/// Active state (e.g. `"active"`, `"inactive"`, `"failed"`).
|
||||
pub active_state: String,
|
||||
/// Sub state (e.g. `"running"`, `"dead"`, `"exited"`).
|
||||
pub sub_state: String,
|
||||
/// Load state (e.g. `"loaded"`, `"not-found"`).
|
||||
pub load_state: String,
|
||||
/// Enabled state (e.g. `"enabled"`, `"disabled"`).
|
||||
pub enabled_state: String,
|
||||
/// Main PID of the service process.
|
||||
pub main_pid: Option<u32>,
|
||||
/// Whether the service is considered healthy.
|
||||
pub healthy: bool,
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// POST /api/v1/jobs/{id}/rollback
|
||||
// ============================================================
|
||||
|
||||
/// Response from `POST /api/v1/jobs/{id}/rollback`.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct RollbackResponse {
|
||||
pub job_id: String,
|
||||
pub status: String,
|
||||
}
|
||||
Reference in New Issue
Block a user