mirror of
https://git.proxmox.com/git/proxmox-backup
synced 2025-04-30 05:18:26 +00:00
api: garbage collect job status
Adds an api endpoint on the datastore that reports the gc job status such as: - Schedule - State (of last run) - Duration (of last run) - Last Run - Next Run (if scheduled) - Pending Chunks (of last run) - Pending Bytes (of last run) - Removed Chunks (of last run) - Removed Bytes (of last run) Adds a dedicated endpoint admin/gc that reports gc job status for all datastores including the onces without a gc-schedule. Signed-off-by: Stefan Lendl <s.lendl@proxmox.com> Originally-by: Gabriel Goller <g.goller@proxmox.com> Tested-by: Gabriel Goller <g.goller@proxmox.com> Reviewd-by: Gabriel Goller <g.goller@proxmox.com> Tested-by: Lukas Wagner <l.wagner@proxmox.com> Reviewed-by: Lukas Wagner <l.wagner@proxmox.com>
This commit is contained in:
parent
83daeed72a
commit
fe1d34d2e4
@ -1307,6 +1307,52 @@ pub struct GarbageCollectionStatus {
|
|||||||
pub still_bad: usize,
|
pub still_bad: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[api(
|
||||||
|
properties: {
|
||||||
|
"last-run-upid": {
|
||||||
|
optional: true,
|
||||||
|
type: UPID,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)]
|
||||||
|
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
/// Garbage Collection general info
|
||||||
|
pub struct GarbageCollectionJobStatus {
|
||||||
|
/// Datastore
|
||||||
|
pub store: String,
|
||||||
|
/// upid of the last run gc job
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub last_run_upid: Option<String>,
|
||||||
|
/// Sum of removed bytes.
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub removed_bytes: Option<u64>,
|
||||||
|
/// Number of removed chunks
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub removed_chunks: Option<usize>,
|
||||||
|
/// Sum of pending bytes
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub pending_bytes: Option<u64>,
|
||||||
|
/// Number of pending chunks
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub pending_chunks: Option<usize>,
|
||||||
|
/// Schedule of the gc job
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub schedule: Option<String>,
|
||||||
|
/// Time of the next gc run
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub next_run: Option<i64>,
|
||||||
|
/// Endtime of the last gc run
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub last_run_endtime: Option<i64>,
|
||||||
|
/// State of the last gc run
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub last_run_state: Option<String>,
|
||||||
|
/// Duration of last gc run
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub duration: Option<i64>,
|
||||||
|
}
|
||||||
|
|
||||||
#[api(
|
#[api(
|
||||||
properties: {
|
properties: {
|
||||||
"gc-status": {
|
"gc-status": {
|
||||||
|
@ -27,18 +27,20 @@ use proxmox_sys::fs::{
|
|||||||
file_read_firstline, file_read_optional_string, replace_file, CreateOptions,
|
file_read_firstline, file_read_optional_string, replace_file, CreateOptions,
|
||||||
};
|
};
|
||||||
use proxmox_sys::{task_log, task_warn};
|
use proxmox_sys::{task_log, task_warn};
|
||||||
|
use proxmox_time::CalendarEvent;
|
||||||
|
|
||||||
use pxar::accessor::aio::Accessor;
|
use pxar::accessor::aio::Accessor;
|
||||||
use pxar::EntryKind;
|
use pxar::EntryKind;
|
||||||
|
|
||||||
use pbs_api_types::{
|
use pbs_api_types::{
|
||||||
print_ns_and_snapshot, print_store_and_ns, Authid, BackupContent, BackupNamespace, BackupType,
|
print_ns_and_snapshot, print_store_and_ns, Authid, BackupContent, BackupNamespace, BackupType,
|
||||||
Counts, CryptMode, DataStoreListItem, DataStoreStatus, GarbageCollectionStatus, GroupListItem,
|
Counts, CryptMode, DataStoreConfig, DataStoreListItem, DataStoreStatus,
|
||||||
|
GarbageCollectionJobStatus, GarbageCollectionStatus, GroupListItem, JobScheduleStatus,
|
||||||
KeepOptions, Operation, PruneJobOptions, RRDMode, RRDTimeFrame, SnapshotListItem,
|
KeepOptions, Operation, PruneJobOptions, RRDMode, RRDTimeFrame, SnapshotListItem,
|
||||||
SnapshotVerifyState, BACKUP_ARCHIVE_NAME_SCHEMA, BACKUP_ID_SCHEMA, BACKUP_NAMESPACE_SCHEMA,
|
SnapshotVerifyState, BACKUP_ARCHIVE_NAME_SCHEMA, BACKUP_ID_SCHEMA, BACKUP_NAMESPACE_SCHEMA,
|
||||||
BACKUP_TIME_SCHEMA, BACKUP_TYPE_SCHEMA, DATASTORE_SCHEMA, IGNORE_VERIFIED_BACKUPS_SCHEMA,
|
BACKUP_TIME_SCHEMA, BACKUP_TYPE_SCHEMA, DATASTORE_SCHEMA, IGNORE_VERIFIED_BACKUPS_SCHEMA,
|
||||||
MAX_NAMESPACE_DEPTH, NS_MAX_DEPTH_SCHEMA, PRIV_DATASTORE_AUDIT, PRIV_DATASTORE_BACKUP,
|
MAX_NAMESPACE_DEPTH, NS_MAX_DEPTH_SCHEMA, PRIV_DATASTORE_AUDIT, PRIV_DATASTORE_BACKUP,
|
||||||
PRIV_DATASTORE_MODIFY, PRIV_DATASTORE_PRUNE, PRIV_DATASTORE_READ, PRIV_DATASTORE_VERIFY,
|
PRIV_DATASTORE_MODIFY, PRIV_DATASTORE_PRUNE, PRIV_DATASTORE_READ, PRIV_DATASTORE_VERIFY, UPID,
|
||||||
UPID_SCHEMA, VERIFICATION_OUTDATED_AFTER_SCHEMA,
|
UPID_SCHEMA, VERIFICATION_OUTDATED_AFTER_SCHEMA,
|
||||||
};
|
};
|
||||||
use pbs_client::pxar::{create_tar, create_zip};
|
use pbs_client::pxar::{create_tar, create_zip};
|
||||||
@ -67,7 +69,7 @@ use crate::backup::{
|
|||||||
ListAccessibleBackupGroups, NS_PRIVS_OK,
|
ListAccessibleBackupGroups, NS_PRIVS_OK,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::server::jobstate::Job;
|
use crate::server::jobstate::{compute_schedule_status, Job, JobState};
|
||||||
|
|
||||||
const GROUP_NOTES_FILE_NAME: &str = "notes";
|
const GROUP_NOTES_FILE_NAME: &str = "notes";
|
||||||
|
|
||||||
@ -1238,6 +1240,125 @@ pub fn garbage_collection_status(
|
|||||||
Ok(status)
|
Ok(status)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[api(
|
||||||
|
input: {
|
||||||
|
properties: {
|
||||||
|
store: {
|
||||||
|
schema: DATASTORE_SCHEMA,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
returns: {
|
||||||
|
type: GarbageCollectionJobStatus,
|
||||||
|
},
|
||||||
|
access: {
|
||||||
|
permission: &Permission::Privilege(&["datastore", "{store}"], PRIV_DATASTORE_AUDIT, false),
|
||||||
|
},
|
||||||
|
)]
|
||||||
|
/// Garbage collection status.
|
||||||
|
pub fn garbage_collection_job_status(
|
||||||
|
store: String,
|
||||||
|
_info: &ApiMethod,
|
||||||
|
_rpcenv: &mut dyn RpcEnvironment,
|
||||||
|
) -> Result<GarbageCollectionJobStatus, Error> {
|
||||||
|
let (config, _) = pbs_config::datastore::config()?;
|
||||||
|
let store_config: DataStoreConfig = config.lookup("datastore", &store)?;
|
||||||
|
|
||||||
|
let mut info = GarbageCollectionJobStatus {
|
||||||
|
store: store.clone(),
|
||||||
|
schedule: store_config.gc_schedule,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let datastore = DataStore::lookup_datastore(&store, Some(Operation::Read))?;
|
||||||
|
let status_in_memory = datastore.last_gc_status();
|
||||||
|
let state_file = JobState::load("garbage_collection", &store)
|
||||||
|
.map_err(|err| {
|
||||||
|
log::error!(
|
||||||
|
"could not open statefile for {:?}: {}",
|
||||||
|
info.last_run_upid,
|
||||||
|
err
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
|
||||||
|
let mut selected_upid = None;
|
||||||
|
if status_in_memory.upid.is_some() {
|
||||||
|
selected_upid = status_in_memory.upid;
|
||||||
|
} else if let Some(JobState::Finished { upid, .. }) = &state_file {
|
||||||
|
selected_upid = Some(upid.to_owned());
|
||||||
|
}
|
||||||
|
|
||||||
|
info.last_run_upid = selected_upid.clone();
|
||||||
|
|
||||||
|
match selected_upid {
|
||||||
|
Some(upid) => {
|
||||||
|
info.removed_bytes = Some(status_in_memory.removed_bytes);
|
||||||
|
info.removed_chunks = Some(status_in_memory.removed_chunks);
|
||||||
|
info.pending_bytes = Some(status_in_memory.pending_bytes);
|
||||||
|
info.pending_chunks = Some(status_in_memory.pending_chunks);
|
||||||
|
|
||||||
|
let mut computed_schedule: JobScheduleStatus = JobScheduleStatus::default();
|
||||||
|
let mut duration = None;
|
||||||
|
if let Some(state) = state_file {
|
||||||
|
if let Ok(cs) = compute_schedule_status(&state, info.last_run_upid.as_deref()) {
|
||||||
|
computed_schedule = cs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(endtime) = computed_schedule.last_run_endtime {
|
||||||
|
computed_schedule.next_run = info
|
||||||
|
.schedule
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|s| {
|
||||||
|
s.parse::<CalendarEvent>()
|
||||||
|
.map_err(|err| log::error!("{err}"))
|
||||||
|
.ok()
|
||||||
|
})
|
||||||
|
.and_then(|e| {
|
||||||
|
e.compute_next_event(endtime)
|
||||||
|
.map_err(|err| log::error!("{err}"))
|
||||||
|
.ok()
|
||||||
|
})
|
||||||
|
.and_then(|ne| ne);
|
||||||
|
|
||||||
|
if let Ok(parsed_upid) = upid.parse::<UPID>() {
|
||||||
|
duration = Some(endtime - parsed_upid.starttime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
info.next_run = computed_schedule.next_run;
|
||||||
|
info.last_run_endtime = computed_schedule.last_run_endtime;
|
||||||
|
info.last_run_state = computed_schedule.last_run_state;
|
||||||
|
info.duration = duration;
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
if let Some(schedule) = &info.schedule {
|
||||||
|
info.next_run = schedule
|
||||||
|
.parse::<CalendarEvent>()
|
||||||
|
.map_err(|err| log::error!("{err}"))
|
||||||
|
.ok()
|
||||||
|
.and_then(|e| {
|
||||||
|
e.compute_next_event(proxmox_time::epoch_i64())
|
||||||
|
.map_err(|err| log::error!("{err}"))
|
||||||
|
.ok()
|
||||||
|
})
|
||||||
|
.and_then(|ne| ne);
|
||||||
|
|
||||||
|
if let Ok(event) = schedule.parse::<CalendarEvent>() {
|
||||||
|
if let Ok(next_event) = event.compute_next_event(proxmox_time::epoch_i64()) {
|
||||||
|
info.next_run = next_event;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Ok(info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(info)
|
||||||
|
}
|
||||||
|
|
||||||
#[api(
|
#[api(
|
||||||
returns: {
|
returns: {
|
||||||
description: "List the accessible datastores.",
|
description: "List the accessible datastores.",
|
||||||
@ -2304,6 +2425,10 @@ const DATASTORE_INFO_SUBDIRS: SubdirMap = &[
|
|||||||
.get(&API_METHOD_GARBAGE_COLLECTION_STATUS)
|
.get(&API_METHOD_GARBAGE_COLLECTION_STATUS)
|
||||||
.post(&API_METHOD_START_GARBAGE_COLLECTION),
|
.post(&API_METHOD_START_GARBAGE_COLLECTION),
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
"gc-job-status",
|
||||||
|
&Router::new().get(&API_METHOD_GARBAGE_COLLECTION_JOB_STATUS),
|
||||||
|
),
|
||||||
(
|
(
|
||||||
"group-notes",
|
"group-notes",
|
||||||
&Router::new()
|
&Router::new()
|
||||||
|
57
src/api2/admin/gc.rs
Normal file
57
src/api2/admin/gc.rs
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
use anyhow::Error;
|
||||||
|
use pbs_api_types::GarbageCollectionJobStatus;
|
||||||
|
|
||||||
|
use proxmox_router::{ApiMethod, Permission, Router, RpcEnvironment};
|
||||||
|
use proxmox_schema::api;
|
||||||
|
|
||||||
|
use pbs_api_types::DATASTORE_SCHEMA;
|
||||||
|
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
use crate::api2::admin::datastore::{garbage_collection_job_status, get_datastore_list};
|
||||||
|
|
||||||
|
#[api(
|
||||||
|
input: {
|
||||||
|
properties: {
|
||||||
|
store: {
|
||||||
|
schema: DATASTORE_SCHEMA,
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
returns: {
|
||||||
|
description: "List configured gc jobs and their status",
|
||||||
|
type: Array,
|
||||||
|
items: { type: GarbageCollectionJobStatus },
|
||||||
|
},
|
||||||
|
access: {
|
||||||
|
permission: &Permission::Anybody,
|
||||||
|
description: "Requires Datastore.Audit or Datastore.Modify on datastore.",
|
||||||
|
},
|
||||||
|
)]
|
||||||
|
/// List all GC jobs (max one per datastore)
|
||||||
|
pub fn list_all_gc_jobs(
|
||||||
|
store: Option<String>,
|
||||||
|
_param: Value,
|
||||||
|
_info: &ApiMethod,
|
||||||
|
rpcenv: &mut dyn RpcEnvironment,
|
||||||
|
) -> Result<Vec<GarbageCollectionJobStatus>, Error> {
|
||||||
|
let gc_info = match store {
|
||||||
|
Some(store) => {
|
||||||
|
garbage_collection_job_status(store, _info, rpcenv).map(|info| vec![info])?
|
||||||
|
}
|
||||||
|
None => get_datastore_list(Value::Null, _info, rpcenv)?
|
||||||
|
.into_iter()
|
||||||
|
.map(|store_list_item| store_list_item.store)
|
||||||
|
.filter_map(|store| garbage_collection_job_status(store, _info, rpcenv).ok())
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(gc_info)
|
||||||
|
}
|
||||||
|
|
||||||
|
const GC_ROUTER: Router = Router::new().get(&API_METHOD_LIST_ALL_GC_JOBS);
|
||||||
|
|
||||||
|
pub const ROUTER: Router = Router::new()
|
||||||
|
.get(&API_METHOD_LIST_ALL_GC_JOBS)
|
||||||
|
.match_all("store", &GC_ROUTER);
|
@ -5,6 +5,7 @@ use proxmox_router::{Router, SubdirMap};
|
|||||||
use proxmox_sortable_macro::sortable;
|
use proxmox_sortable_macro::sortable;
|
||||||
|
|
||||||
pub mod datastore;
|
pub mod datastore;
|
||||||
|
pub mod gc;
|
||||||
pub mod metrics;
|
pub mod metrics;
|
||||||
pub mod namespace;
|
pub mod namespace;
|
||||||
pub mod prune;
|
pub mod prune;
|
||||||
@ -17,6 +18,7 @@ const SUBDIRS: SubdirMap = &sorted!([
|
|||||||
("datastore", &datastore::ROUTER),
|
("datastore", &datastore::ROUTER),
|
||||||
("metrics", &metrics::ROUTER),
|
("metrics", &metrics::ROUTER),
|
||||||
("prune", &prune::ROUTER),
|
("prune", &prune::ROUTER),
|
||||||
|
("gc", &gc::ROUTER),
|
||||||
("sync", &sync::ROUTER),
|
("sync", &sync::ROUTER),
|
||||||
("traffic-control", &traffic_control::ROUTER),
|
("traffic-control", &traffic_control::ROUTER),
|
||||||
("verify", &verify::ROUTER),
|
("verify", &verify::ROUTER),
|
||||||
|
Loading…
Reference in New Issue
Block a user