[pbs-devel] [Stefan Lendl] Re: [PATCH proxmox-backup v3] close #4723: updated gc view in the ui
Stefan Lendl
s.lendl at proxmox.com
Fri Sep 22 11:13:15 CEST 2023
Handling of GC tasks that never ran works great now. GUI alignment looks good.
> Updated the GC overview in the datastore page. This enables
> us to see:
> - schedule
> - State (of last run)
> - Duration (of last run)
> - Last Run
> - Next Run
> - Pending Chunks (of last run)
> - Removed Chunks (of last run)
>
> Added `ObjectGrid` for GCView, moved to different file. Added
> endpoint `gc_info` that returns all the necessary config and
> last run stats.
>
> Signed-off-by: Gabriel Goller <g.goller at proxmox.com>
> ---
>
> update v3:
> - ui: removed `required` attribute on items to get the sorting right
> - made `pending_chunks` and `removed_chunks` options, so that they
> are not shown when no gc run exists
>
> update v2:
> - skip serializing if value is `None`
> - return just the schedule if `upid` doesn't exist (means no gc has been
> run)
> - ui: removed default values on timestamps
> - ui: removed flex and minHeight properties
>
> pbs-api-types/src/datastore.rs | 38 +++++++++++
> src/api2/admin/datastore.rs | 98 +++++++++++++++++++++++++---
> www/Makefile | 1 +
> www/Utils.js | 12 ++--
> www/config/GCView.js | 116 +++++++++++++++++++++++++++++++++
> www/datastore/PruneAndGC.js | 91 +-------------------------
> 6 files changed, 253 insertions(+), 103 deletions(-)
> mode change 100644 => 100755 src/api2/admin/datastore.rs
> create mode 100644 www/config/GCView.js
>
> diff --git a/pbs-api-types/src/datastore.rs b/pbs-api-types/src/datastore.rs
> index 73c4890e..55caa963 100644
> --- a/pbs-api-types/src/datastore.rs
> +++ b/pbs-api-types/src/datastore.rs
> @@ -1250,6 +1250,44 @@ pub struct GarbageCollectionStatus {
> pub still_bad: usize,
> }
>
> +#[api(
> + properties: {
> + "last-run-upid": {
> + optional: true,
> + type: UPID,
> + },
> + },
> +)]
> +#[derive(Clone, Default, Serialize, Deserialize, PartialEq)]
> +#[serde(rename_all = "kebab-case")]
> +/// Garbage Collection general info
> +pub struct GarbageCollectionInfo {
> + /// upid of the last run gc job
> + #[serde(skip_serializing_if = "Option::is_none")]
> + pub last_run_upid: Option<String>,
> + /// Number of removed chunks
> + #[serde(skip_serializing_if = "Option::is_none")]
> + pub removed_chunks: Option<usize>,
> + /// 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(
> properties: {
> "gc-status": {
> diff --git a/src/api2/admin/datastore.rs b/src/api2/admin/datastore.rs
> old mode 100644
> new mode 100755
> index a95031e7..1127bd95
> --- a/src/api2/admin/datastore.rs
> +++ b/src/api2/admin/datastore.rs
> @@ -10,6 +10,7 @@ use anyhow::{bail, format_err, Error};
> use futures::*;
> use hyper::http::request::Parts;
> use hyper::{header, Body, Response, StatusCode};
> +use proxmox_time::CalendarEvent;
> use serde::Deserialize;
> use serde_json::{json, Value};
> use tokio_stream::wrappers::ReceiverStream;
> @@ -33,13 +34,14 @@ use pxar::EntryKind;
>
> use pbs_api_types::{
> print_ns_and_snapshot, print_store_and_ns, Authid, BackupContent, BackupNamespace, BackupType,
> - Counts, CryptMode, DataStoreListItem, DataStoreStatus, GarbageCollectionStatus, GroupListItem,
> - KeepOptions, Operation, PruneJobOptions, RRDMode, RRDTimeFrame, SnapshotListItem,
> - SnapshotVerifyState, BACKUP_ARCHIVE_NAME_SCHEMA, BACKUP_ID_SCHEMA, BACKUP_NAMESPACE_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,
> - PRIV_DATASTORE_MODIFY, PRIV_DATASTORE_PRUNE, PRIV_DATASTORE_READ, PRIV_DATASTORE_VERIFY,
> - UPID_SCHEMA, VERIFICATION_OUTDATED_AFTER_SCHEMA,
> + Counts, CryptMode, DataStoreConfig, DataStoreListItem, DataStoreStatus, GarbageCollectionInfo,
> + GarbageCollectionStatus, GroupListItem, JobScheduleStatus, KeepOptions, Operation,
> + PruneJobOptions, RRDMode, RRDTimeFrame, SnapshotListItem, SnapshotVerifyState,
> + BACKUP_ARCHIVE_NAME_SCHEMA, BACKUP_ID_SCHEMA, BACKUP_NAMESPACE_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, PRIV_DATASTORE_MODIFY,
> + PRIV_DATASTORE_PRUNE, PRIV_DATASTORE_READ, PRIV_DATASTORE_VERIFY, UPID, UPID_SCHEMA,
> + VERIFICATION_OUTDATED_AFTER_SCHEMA,
> };
> use pbs_client::pxar::{create_tar, create_zip};
> use pbs_config::CachedUserInfo;
> @@ -67,7 +69,7 @@ use crate::backup::{
> 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";
>
> @@ -1199,6 +1201,82 @@ pub fn garbage_collection_status(
> Ok(status)
> }
>
> +#[api(
> + input: {
> + properties: {
> + store: {
> + schema: DATASTORE_SCHEMA,
> + },
> + },
> + },
> + returns: {
> + type: GarbageCollectionInfo,
> + },
> + access: {
> + permission: &Permission::Privilege(&["datastore", "{store}"], PRIV_DATASTORE_AUDIT, false),
> + },
> +)]
> +/// Garbage collection status.
> +pub fn garbage_collection_info(
> + store: String,
> + _info: &ApiMethod,
> + _rpcenv: &mut dyn RpcEnvironment,
> +) -> Result<GarbageCollectionInfo, Error> {
> + let (config, _) = pbs_config::datastore::config()?;
> + let store_config: DataStoreConfig = config.lookup("datastore", &store)?;
> +
> + let datastore = DataStore::lookup_datastore(&store, Some(Operation::Read))?;
> + let status = datastore.last_gc_status();
> +
> + let upid = match status.upid {
> + Some(upid) => upid,
> + None => {
> + return Ok(GarbageCollectionInfo {
> + schedule: store_config.gc_schedule,
> + ..Default::default()
> + })
> + }
> + };
> + let last_state = JobState::load("garbage_collection", &store)
> + .map_err(|err| format_err!("could not open statefile for {}: {}", upid, err))
> + .ok();
> +
> + let mut computed_schedule: JobScheduleStatus = JobScheduleStatus::default();
> + if let Some(state) = last_state {
> + computed_schedule = compute_schedule_status(&state, Some(&upid)).unwrap();
> + }
> +
> + // calculate next event
> + if let Some(schedule) = &store_config.gc_schedule {
> + if let (Ok(event), Some(last_run)) = (
> + schedule.parse::<CalendarEvent>(),
> + computed_schedule.last_run_endtime,
> + ) {
> + if let Ok(next_event) = event.compute_next_event(last_run) {
> + computed_schedule.next_run = next_event;
> + }
> + }
> + }
If GC never ran but is scheduled, it would also be nice to show the next
scheduled run.
This might be usefull if people had not GC configured, then create a job
to further indicate that the job is scheduled correctly.
> +
> + let mut duration = None;
> + if let (Ok(upid), Some(endtime)) = (upid.parse::<UPID>(), computed_schedule.last_run_endtime) {
> + duration = Some(endtime - upid.starttime);
> + }
> +
> + let info = GarbageCollectionInfo {
> + last_run_upid: Some(upid),
> + removed_chunks: Some(status.removed_chunks),
> + pending_chunks: Some(status.pending_chunks),
> + schedule: store_config.gc_schedule,
> + next_run: computed_schedule.next_run,
> + last_run_endtime: computed_schedule.last_run_endtime,
> + last_run_state: computed_schedule.last_run_state,
> + duration,
> + };
> +
> + Ok(info)
> +}
> +
> #[api(
> returns: {
> description: "List the accessible datastores.",
> @@ -2265,6 +2343,10 @@ const DATASTORE_INFO_SUBDIRS: SubdirMap = &[
> .get(&API_METHOD_GARBAGE_COLLECTION_STATUS)
> .post(&API_METHOD_START_GARBAGE_COLLECTION),
> ),
> + (
> + "gc_info",
> + &Router::new().get(&API_METHOD_GARBAGE_COLLECTION_INFO),
> + ),
> (
> "group-notes",
> &Router::new()
> diff --git a/www/Makefile b/www/Makefile
> index 04c12b31..b67fd73f 100644
> --- a/www/Makefile
> +++ b/www/Makefile
> @@ -62,6 +62,7 @@ JSSRC= \
> config/SyncView.js \
> config/VerifyView.js \
> config/PruneView.js \
> + config/GCView.js \
> config/WebauthnView.js \
> config/CertificateView.js \
> config/NodeOptionView.js \
> diff --git a/www/Utils.js b/www/Utils.js
> index 2eca600e..66293b17 100644
> --- a/www/Utils.js
> +++ b/www/Utils.js
> @@ -199,14 +199,14 @@ Ext.define('PBS.Utils', {
> return fingerprint.substring(0, 23);
> },
>
> - render_task_status: function(value, metadata, record) {
> - if (!record.data['last-run-upid']) {
> - return '-';
> + render_task_status: function(value, metadata, record, rowIndex, colIndex, store) {
> + if (!record.data['last-run-upid'] && !store.getById('last-run-upid')?.data.value) {
> + return '-';
> }
>
> - if (!record.data['last-run-endtime']) {
> - metadata.tdCls = 'x-grid-row-loading';
> - return '';
> + if (!record.data['last-run-endtime'] && !store.getById('last-run-endtime')?.data.value) {
> + metadata.tdCls = 'x-grid-row-loading';
> + return '';
> }
>
> let parsed = Proxmox.Utils.parse_task_status(value);
> diff --git a/www/config/GCView.js b/www/config/GCView.js
> new file mode 100644
> index 00000000..1a79bb01
> --- /dev/null
> +++ b/www/config/GCView.js
> @@ -0,0 +1,116 @@
> +Ext.define('PBS.Datastore.GCOptions', {
> + extend: 'Proxmox.grid.ObjectGrid',
> + alias: 'widget.pbsGCJobView',
> + mixins: ['Proxmox.Mixin.CBind'],
> +
> + onlineHelp: 'maintenance_pruning',
> +
> + cbindData: function(initial) {
> + let me = this;
> +
> + me.datastore = encodeURIComponent(me.datastore);
> + me.url = `/api2/json/admin/datastore/${me.datastore}/gc_info`;
> + me.editorConfig = {
> + url: `/api2/extjs/config/datastore/${me.datastore}`,
> + };
> + return {};
> + },
> +
> + controller: {
> + xclass: 'Ext.app.ViewController',
> +
> + edit: function() {
> + this.getView().run_editor();
> + },
> +
> + garbageCollect: function() {
> + let me = this;
> + let view = me.getView();
> + Proxmox.Utils.API2Request({
> + url: `/admin/datastore/${view.datastore}/gc`,
> + method: 'POST',
> + failure: function(response) {
> + Ext.Msg.alert(gettext('Error'), response.htmlStatus);
> + },
> + success: function(response, options) {
> + Ext.create('Proxmox.window.TaskViewer', {
> + upid: response.result.data,
> + }).show();
> + },
> + });
> + },
> + },
> +
> + tbar: [
> + {
> + xtype: 'proxmoxButton',
> + text: gettext('Edit'),
> + enableFn: (rec) => rec.id === 'schedule',
> + disabled: true,
> + handler: 'edit',
> + },
> + '-',
> + {
> + xtype: 'proxmoxButton',
> + text: gettext('Start Garbage Collection'),
> + selModel: null,
> + handler: 'garbageCollect',
> + },
> + ],
> +
> + listeners: {
> + activate: function() { this.rstore.startUpdate(); },
> + destroy: function() { this.rstore.stopUpdate(); },
> + deactivate: function() { this.rstore.stopUpdate(); },
> + itemdblclick: 'edit',
> + },
> +
> + rows: {
> + "schedule": {
> + required: true,
> + defaultValue: Proxmox.Utils.NoneText,
> + header: gettext('Schedule'),
> + editor: {
> + xtype: 'proxmoxWindowEdit',
> + title: gettext('GC Schedule'),
> + onlineHelp: 'maintenance_gc',
> + items: {
> + xtype: 'pbsCalendarEvent',
> + name: 'gc-schedule',
> + fieldLabel: gettext("GC Schedule"),
> + emptyText: Proxmox.Utils.NoneText,
> + deleteEmpty: true,
> + },
> + },
> + },
> + "last-run-state": {
> + header: gettext('State'),
> + renderer: PBS.Utils.render_task_status,
> + disabled: true,
> + },
> + "duration": {
> + header: gettext('Duration'),
> + renderer: Proxmox.Utils.render_duration,
> + disabled: true,
> + },
> + "last-run-endtime": {
> + header: gettext('Last Run'),
> + renderer: PBS.Utils.render_optional_timestamp,
> + disabled: true,
> + },
> + "next-run": {
> + header: gettext('Next Run'),
> + renderer: PBS.Utils.render_next_task_run,
> + disabled: true,
> + },
> + "pending-chunks": {
> + header: gettext('Pending Chunks'),
> + disabled: true,
> + },
> + "removed-chunks": {
> + header: gettext('Removed Chunks'),
> + disabled: true,
> + },
> + "last-run-upid": { visible: false },
> + },
> +});
> diff --git a/www/datastore/PruneAndGC.js b/www/datastore/PruneAndGC.js
> index aab98dad..3b9878a3 100644
> --- a/www/datastore/PruneAndGC.js
> +++ b/www/datastore/PruneAndGC.js
> @@ -1,88 +1,3 @@
> -Ext.define('PBS.Datastore.GCOptions', {
> - extend: 'Proxmox.grid.ObjectGrid',
> - alias: 'widget.pbsDatastoreGCOpts',
> - mixins: ['Proxmox.Mixin.CBind'],
> -
> - onlineHelp: 'maintenance_pruning',
> -
> - cbindData: function(initial) {
> - let me = this;
> -
> - me.datastore = encodeURIComponent(me.datastore);
> - me.url = `/api2/json/config/datastore/${me.datastore}`;
> - me.editorConfig = {
> - url: `/api2/extjs/config/datastore/${me.datastore}`,
> - };
> - return {};
> - },
> -
> - controller: {
> - xclass: 'Ext.app.ViewController',
> -
> - edit: function() { this.getView().run_editor(); },
> -
> - garbageCollect: function() {
> - let me = this;
> - let view = me.getView();
> - Proxmox.Utils.API2Request({
> - url: `/admin/datastore/${view.datastore}/gc`,
> - method: 'POST',
> - failure: function(response) {
> - Ext.Msg.alert(gettext('Error'), response.htmlStatus);
> - },
> - success: function(response, options) {
> - Ext.create('Proxmox.window.TaskViewer', {
> - upid: response.result.data,
> - }).show();
> - },
> - });
> - },
> - },
> -
> - tbar: [
> - {
> - xtype: 'proxmoxButton',
> - text: gettext('Edit'),
> - disabled: true,
> - handler: 'edit',
> - },
> - '-',
> - {
> - xtype: 'proxmoxButton',
> - text: gettext('Start Garbage Collection'),
> - selModel: null,
> - handler: 'garbageCollect',
> - },
> - ],
> -
> - listeners: {
> - activate: function() { this.rstore.startUpdate(); },
> - destroy: function() { this.rstore.stopUpdate(); },
> - deactivate: function() { this.rstore.stopUpdate(); },
> - itemdblclick: 'edit',
> - },
> -
> - rows: {
> - "gc-schedule": {
> - required: true,
> - defaultValue: Proxmox.Utils.NoneText,
> - header: gettext('Garbage Collection Schedule'),
> - editor: {
> - xtype: 'proxmoxWindowEdit',
> - title: gettext('GC Schedule'),
> - onlineHelp: 'maintenance_gc',
> - items: {
> - xtype: 'pbsCalendarEvent',
> - name: 'gc-schedule',
> - fieldLabel: gettext("GC Schedule"),
> - emptyText: Proxmox.Utils.noneText,
> - deleteEmpty: true,
> - },
> - },
> - },
> - },
> -});
> -
> Ext.define('PBS.Datastore.PruneAndGC', {
> extend: 'Ext.panel.Panel',
> alias: 'widget.pbsDatastorePruneAndGC',
> @@ -99,9 +14,9 @@ Ext.define('PBS.Datastore.PruneAndGC', {
> },
> items: [
> {
> - xtype: 'pbsDatastoreGCOpts',
> + xtype: 'pbsGCJobView',
> title: gettext('Garbage Collection'),
> - itemId: 'datastore-gc',
> + itemId: 'datastore-gc-jobs',
> nodename: 'localhost',
> cbind: {
> datastore: '{datastore}',
> @@ -111,8 +26,6 @@ Ext.define('PBS.Datastore.PruneAndGC', {
> xtype: 'pbsPruneJobView',
> nodename: 'localhost',
> itemId: 'datastore-prune-jobs',
> - flex: 1,
> - minHeight: 200,
> cbind: {
> datastore: '{datastore}',
> },
> --
> 2.39.2
>
>
>
> _______________________________________________
> pbs-devel mailing list
> pbs-devel at lists.proxmox.com
> https://lists.proxmox.com/cgi-bin/mailman/listinfo/pbs-devel
-------------------- Start of forwarded message --------------------
Date: Fri, 22 Sep 2023 10:51:35 +0200
Subject: Re: [pbs-devel] [PATCH proxmox-backup v3] close #4723: updated gc
view in the ui
To: Stefan Lendl <s.lendl at proxmox.com>
From: Gabriel Goller <g.goller at proxmox.com>
Thanks for having a look at it!
Could you post the message again on the mailing list (using "reply all"
instead of "reply"). Thanks!
On 9/22/23 10:49, Stefan Lendl wrote:
> Gabriel Goller <g.goller at proxmox.com> writes:
>
> Handling of GC tasks that never ran works great now. GUI alignment looks good.
>
>> Updated the GC overview in the datastore page. This enables
>> us to see:
>> - schedule
>> - State (of last run)
>> - Duration (of last run)
>> - Last Run
>> - Next Run
>> - Pending Chunks (of last run)
>> - Removed Chunks (of last run)
>>
>> Added `ObjectGrid` for GCView, moved to different file. Added
>> endpoint `gc_info` that returns all the necessary config and
>> last run stats.
>>
>> Signed-off-by: Gabriel Goller <g.goller at proxmox.com>
>> ---
>>
>> update v3:
>> - ui: removed `required` attribute on items to get the sorting right
>> - made `pending_chunks` and `removed_chunks` options, so that they
>> are not shown when no gc run exists
>>
>> update v2:
>> - skip serializing if value is `None`
>> - return just the schedule if `upid` doesn't exist (means no gc has been
>> run)
>> - ui: removed default values on timestamps
>> - ui: removed flex and minHeight properties
>>
>> pbs-api-types/src/datastore.rs | 38 +++++++++++
>> src/api2/admin/datastore.rs | 98 +++++++++++++++++++++++++---
>> www/Makefile | 1 +
>> www/Utils.js | 12 ++--
>> www/config/GCView.js | 116 +++++++++++++++++++++++++++++++++
>> www/datastore/PruneAndGC.js | 91 +-------------------------
>> 6 files changed, 253 insertions(+), 103 deletions(-)
>> mode change 100644 => 100755 src/api2/admin/datastore.rs
>> create mode 100644 www/config/GCView.js
>>
>> diff --git a/pbs-api-types/src/datastore.rs b/pbs-api-types/src/datastore.rs
>> index 73c4890e..55caa963 100644
>> --- a/pbs-api-types/src/datastore.rs
>> +++ b/pbs-api-types/src/datastore.rs
>> @@ -1250,6 +1250,44 @@ pub struct GarbageCollectionStatus {
>> pub still_bad: usize,
>> }
>>
>> +#[api(
>> + properties: {
>> + "last-run-upid": {
>> + optional: true,
>> + type: UPID,
>> + },
>> + },
>> +)]
>> +#[derive(Clone, Default, Serialize, Deserialize, PartialEq)]
>> +#[serde(rename_all = "kebab-case")]
>> +/// Garbage Collection general info
>> +pub struct GarbageCollectionInfo {
>> + /// upid of the last run gc job
>> + #[serde(skip_serializing_if = "Option::is_none")]
>> + pub last_run_upid: Option<String>,
>> + /// Number of removed chunks
>> + #[serde(skip_serializing_if = "Option::is_none")]
>> + pub removed_chunks: Option<usize>,
>> + /// 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(
>> properties: {
>> "gc-status": {
>> diff --git a/src/api2/admin/datastore.rs b/src/api2/admin/datastore.rs
>> old mode 100644
>> new mode 100755
>> index a95031e7..1127bd95
>> --- a/src/api2/admin/datastore.rs
>> +++ b/src/api2/admin/datastore.rs
>> @@ -10,6 +10,7 @@ use anyhow::{bail, format_err, Error};
>> use futures::*;
>> use hyper::http::request::Parts;
>> use hyper::{header, Body, Response, StatusCode};
>> +use proxmox_time::CalendarEvent;
>> use serde::Deserialize;
>> use serde_json::{json, Value};
>> use tokio_stream::wrappers::ReceiverStream;
>> @@ -33,13 +34,14 @@ use pxar::EntryKind;
>>
>> use pbs_api_types::{
>> print_ns_and_snapshot, print_store_and_ns, Authid, BackupContent, BackupNamespace, BackupType,
>> - Counts, CryptMode, DataStoreListItem, DataStoreStatus, GarbageCollectionStatus, GroupListItem,
>> - KeepOptions, Operation, PruneJobOptions, RRDMode, RRDTimeFrame, SnapshotListItem,
>> - SnapshotVerifyState, BACKUP_ARCHIVE_NAME_SCHEMA, BACKUP_ID_SCHEMA, BACKUP_NAMESPACE_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,
>> - PRIV_DATASTORE_MODIFY, PRIV_DATASTORE_PRUNE, PRIV_DATASTORE_READ, PRIV_DATASTORE_VERIFY,
>> - UPID_SCHEMA, VERIFICATION_OUTDATED_AFTER_SCHEMA,
>> + Counts, CryptMode, DataStoreConfig, DataStoreListItem, DataStoreStatus, GarbageCollectionInfo,
>> + GarbageCollectionStatus, GroupListItem, JobScheduleStatus, KeepOptions, Operation,
>> + PruneJobOptions, RRDMode, RRDTimeFrame, SnapshotListItem, SnapshotVerifyState,
>> + BACKUP_ARCHIVE_NAME_SCHEMA, BACKUP_ID_SCHEMA, BACKUP_NAMESPACE_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, PRIV_DATASTORE_MODIFY,
>> + PRIV_DATASTORE_PRUNE, PRIV_DATASTORE_READ, PRIV_DATASTORE_VERIFY, UPID, UPID_SCHEMA,
>> + VERIFICATION_OUTDATED_AFTER_SCHEMA,
>> };
>> use pbs_client::pxar::{create_tar, create_zip};
>> use pbs_config::CachedUserInfo;
>> @@ -67,7 +69,7 @@ use crate::backup::{
>> 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";
>>
>> @@ -1199,6 +1201,82 @@ pub fn garbage_collection_status(
>> Ok(status)
>> }
>>
>> +#[api(
>> + input: {
>> + properties: {
>> + store: {
>> + schema: DATASTORE_SCHEMA,
>> + },
>> + },
>> + },
>> + returns: {
>> + type: GarbageCollectionInfo,
>> + },
>> + access: {
>> + permission: &Permission::Privilege(&["datastore", "{store}"], PRIV_DATASTORE_AUDIT, false),
>> + },
>> +)]
>> +/// Garbage collection status.
>> +pub fn garbage_collection_info(
>> + store: String,
>> + _info: &ApiMethod,
>> + _rpcenv: &mut dyn RpcEnvironment,
>> +) -> Result<GarbageCollectionInfo, Error> {
>> + let (config, _) = pbs_config::datastore::config()?;
>> + let store_config: DataStoreConfig = config.lookup("datastore", &store)?;
>> +
>> + let datastore = DataStore::lookup_datastore(&store, Some(Operation::Read))?;
>> + let status = datastore.last_gc_status();
>> +
>> + let upid = match status.upid {
>> + Some(upid) => upid,
>> + None => {
>> + return Ok(GarbageCollectionInfo {
>> + schedule: store_config.gc_schedule,
>> + ..Default::default()
>> + })
>> + }
>> + };
>> + let last_state = JobState::load("garbage_collection", &store)
>> + .map_err(|err| format_err!("could not open statefile for {}: {}", upid, err))
>> + .ok();
>> +
>> + let mut computed_schedule: JobScheduleStatus = JobScheduleStatus::default();
>> + if let Some(state) = last_state {
>> + computed_schedule = compute_schedule_status(&state, Some(&upid)).unwrap();
>> + }
>> +
>> + // calculate next event
>> + if let Some(schedule) = &store_config.gc_schedule {
>> + if let (Ok(event), Some(last_run)) = (
>> + schedule.parse::<CalendarEvent>(),
>> + computed_schedule.last_run_endtime,
>> + ) {
>> + if let Ok(next_event) = event.compute_next_event(last_run) {
>> + computed_schedule.next_run = next_event;
>> + }
>> + }
>> + }
>
> If GC never ran but is scheduled, it would also be nice to show the next
> scheduled run.
> This might be usefull if people had not GC configured, then create a job
> to further indicate that the job is scheduled correctly.
>
>
>> +
>> + let mut duration = None;
>> + if let (Ok(upid), Some(endtime)) = (upid.parse::<UPID>(), computed_schedule.last_run_endtime) {
>> + duration = Some(endtime - upid.starttime);
>> + }
>> +
>> + let info = GarbageCollectionInfo {
>> + last_run_upid: Some(upid),
>> + removed_chunks: Some(status.removed_chunks),
>> + pending_chunks: Some(status.pending_chunks),
>> + schedule: store_config.gc_schedule,
>> + next_run: computed_schedule.next_run,
>> + last_run_endtime: computed_schedule.last_run_endtime,
>> + last_run_state: computed_schedule.last_run_state,
>> + duration,
>> + };
>> +
>> + Ok(info)
>> +}
>> +
>> #[api(
>> returns: {
>> description: "List the accessible datastores.",
>> @@ -2265,6 +2343,10 @@ const DATASTORE_INFO_SUBDIRS: SubdirMap = &[
>> .get(&API_METHOD_GARBAGE_COLLECTION_STATUS)
>> .post(&API_METHOD_START_GARBAGE_COLLECTION),
>> ),
>> + (
>> + "gc_info",
>> + &Router::new().get(&API_METHOD_GARBAGE_COLLECTION_INFO),
>> + ),
>> (
>> "group-notes",
>> &Router::new()
>> diff --git a/www/Makefile b/www/Makefile
>> index 04c12b31..b67fd73f 100644
>> --- a/www/Makefile
>> +++ b/www/Makefile
>> @@ -62,6 +62,7 @@ JSSRC= \
>> config/SyncView.js \
>> config/VerifyView.js \
>> config/PruneView.js \
>> + config/GCView.js \
>> config/WebauthnView.js \
>> config/CertificateView.js \
>> config/NodeOptionView.js \
>> diff --git a/www/Utils.js b/www/Utils.js
>> index 2eca600e..66293b17 100644
>> --- a/www/Utils.js
>> +++ b/www/Utils.js
>> @@ -199,14 +199,14 @@ Ext.define('PBS.Utils', {
>> return fingerprint.substring(0, 23);
>> },
>>
>> - render_task_status: function(value, metadata, record) {
>> - if (!record.data['last-run-upid']) {
>> - return '-';
>> + render_task_status: function(value, metadata, record, rowIndex, colIndex, store) {
>> + if (!record.data['last-run-upid'] && !store.getById('last-run-upid')?.data.value) {
>> + return '-';
>> }
>>
>> - if (!record.data['last-run-endtime']) {
>> - metadata.tdCls = 'x-grid-row-loading';
>> - return '';
>> + if (!record.data['last-run-endtime'] && !store.getById('last-run-endtime')?.data.value) {
>> + metadata.tdCls = 'x-grid-row-loading';
>> + return '';
>> }
>>
>> let parsed = Proxmox.Utils.parse_task_status(value);
>> diff --git a/www/config/GCView.js b/www/config/GCView.js
>> new file mode 100644
>> index 00000000..1a79bb01
>> --- /dev/null
>> +++ b/www/config/GCView.js
>> @@ -0,0 +1,116 @@
>> +Ext.define('PBS.Datastore.GCOptions', {
>> + extend: 'Proxmox.grid.ObjectGrid',
>> + alias: 'widget.pbsGCJobView',
>> + mixins: ['Proxmox.Mixin.CBind'],
>> +
>> + onlineHelp: 'maintenance_pruning',
>> +
>> + cbindData: function(initial) {
>> + let me = this;
>> +
>> + me.datastore = encodeURIComponent(me.datastore);
>> + me.url = `/api2/json/admin/datastore/${me.datastore}/gc_info`;
>> + me.editorConfig = {
>> + url: `/api2/extjs/config/datastore/${me.datastore}`,
>> + };
>> + return {};
>> + },
>> +
>> + controller: {
>> + xclass: 'Ext.app.ViewController',
>> +
>> + edit: function() {
>> + this.getView().run_editor();
>> + },
>> +
>> + garbageCollect: function() {
>> + let me = this;
>> + let view = me.getView();
>> + Proxmox.Utils.API2Request({
>> + url: `/admin/datastore/${view.datastore}/gc`,
>> + method: 'POST',
>> + failure: function(response) {
>> + Ext.Msg.alert(gettext('Error'), response.htmlStatus);
>> + },
>> + success: function(response, options) {
>> + Ext.create('Proxmox.window.TaskViewer', {
>> + upid: response.result.data,
>> + }).show();
>> + },
>> + });
>> + },
>> + },
>> +
>> + tbar: [
>> + {
>> + xtype: 'proxmoxButton',
>> + text: gettext('Edit'),
>> + enableFn: (rec) => rec.id === 'schedule',
>> + disabled: true,
>> + handler: 'edit',
>> + },
>> + '-',
>> + {
>> + xtype: 'proxmoxButton',
>> + text: gettext('Start Garbage Collection'),
>> + selModel: null,
>> + handler: 'garbageCollect',
>> + },
>> + ],
>> +
>> + listeners: {
>> + activate: function() { this.rstore.startUpdate(); },
>> + destroy: function() { this.rstore.stopUpdate(); },
>> + deactivate: function() { this.rstore.stopUpdate(); },
>> + itemdblclick: 'edit',
>> + },
>> +
>> + rows: {
>> + "schedule": {
>> + required: true,
>> + defaultValue: Proxmox.Utils.NoneText,
>> + header: gettext('Schedule'),
>> + editor: {
>> + xtype: 'proxmoxWindowEdit',
>> + title: gettext('GC Schedule'),
>> + onlineHelp: 'maintenance_gc',
>> + items: {
>> + xtype: 'pbsCalendarEvent',
>> + name: 'gc-schedule',
>> + fieldLabel: gettext("GC Schedule"),
>> + emptyText: Proxmox.Utils.NoneText,
>> + deleteEmpty: true,
>> + },
>> + },
>> + },
>> + "last-run-state": {
>> + header: gettext('State'),
>> + renderer: PBS.Utils.render_task_status,
>> + disabled: true,
>> + },
>> + "duration": {
>> + header: gettext('Duration'),
>> + renderer: Proxmox.Utils.render_duration,
>> + disabled: true,
>> + },
>> + "last-run-endtime": {
>> + header: gettext('Last Run'),
>> + renderer: PBS.Utils.render_optional_timestamp,
>> + disabled: true,
>> + },
>> + "next-run": {
>> + header: gettext('Next Run'),
>> + renderer: PBS.Utils.render_next_task_run,
>> + disabled: true,
>> + },
>> + "pending-chunks": {
>> + header: gettext('Pending Chunks'),
>> + disabled: true,
>> + },
>> + "removed-chunks": {
>> + header: gettext('Removed Chunks'),
>> + disabled: true,
>> + },
>> + "last-run-upid": { visible: false },
>> + },
>> +});
>> diff --git a/www/datastore/PruneAndGC.js b/www/datastore/PruneAndGC.js
>> index aab98dad..3b9878a3 100644
>> --- a/www/datastore/PruneAndGC.js
>> +++ b/www/datastore/PruneAndGC.js
>> @@ -1,88 +1,3 @@
>> -Ext.define('PBS.Datastore.GCOptions', {
>> - extend: 'Proxmox.grid.ObjectGrid',
>> - alias: 'widget.pbsDatastoreGCOpts',
>> - mixins: ['Proxmox.Mixin.CBind'],
>> -
>> - onlineHelp: 'maintenance_pruning',
>> -
>> - cbindData: function(initial) {
>> - let me = this;
>> -
>> - me.datastore = encodeURIComponent(me.datastore);
>> - me.url = `/api2/json/config/datastore/${me.datastore}`;
>> - me.editorConfig = {
>> - url: `/api2/extjs/config/datastore/${me.datastore}`,
>> - };
>> - return {};
>> - },
>> -
>> - controller: {
>> - xclass: 'Ext.app.ViewController',
>> -
>> - edit: function() { this.getView().run_editor(); },
>> -
>> - garbageCollect: function() {
>> - let me = this;
>> - let view = me.getView();
>> - Proxmox.Utils.API2Request({
>> - url: `/admin/datastore/${view.datastore}/gc`,
>> - method: 'POST',
>> - failure: function(response) {
>> - Ext.Msg.alert(gettext('Error'), response.htmlStatus);
>> - },
>> - success: function(response, options) {
>> - Ext.create('Proxmox.window.TaskViewer', {
>> - upid: response.result.data,
>> - }).show();
>> - },
>> - });
>> - },
>> - },
>> -
>> - tbar: [
>> - {
>> - xtype: 'proxmoxButton',
>> - text: gettext('Edit'),
>> - disabled: true,
>> - handler: 'edit',
>> - },
>> - '-',
>> - {
>> - xtype: 'proxmoxButton',
>> - text: gettext('Start Garbage Collection'),
>> - selModel: null,
>> - handler: 'garbageCollect',
>> - },
>> - ],
>> -
>> - listeners: {
>> - activate: function() { this.rstore.startUpdate(); },
>> - destroy: function() { this.rstore.stopUpdate(); },
>> - deactivate: function() { this.rstore.stopUpdate(); },
>> - itemdblclick: 'edit',
>> - },
>> -
>> - rows: {
>> - "gc-schedule": {
>> - required: true,
>> - defaultValue: Proxmox.Utils.NoneText,
>> - header: gettext('Garbage Collection Schedule'),
>> - editor: {
>> - xtype: 'proxmoxWindowEdit',
>> - title: gettext('GC Schedule'),
>> - onlineHelp: 'maintenance_gc',
>> - items: {
>> - xtype: 'pbsCalendarEvent',
>> - name: 'gc-schedule',
>> - fieldLabel: gettext("GC Schedule"),
>> - emptyText: Proxmox.Utils.noneText,
>> - deleteEmpty: true,
>> - },
>> - },
>> - },
>> - },
>> -});
>> -
>> Ext.define('PBS.Datastore.PruneAndGC', {
>> extend: 'Ext.panel.Panel',
>> alias: 'widget.pbsDatastorePruneAndGC',
>> @@ -99,9 +14,9 @@ Ext.define('PBS.Datastore.PruneAndGC', {
>> },
>> items: [
>> {
>> - xtype: 'pbsDatastoreGCOpts',
>> + xtype: 'pbsGCJobView',
>> title: gettext('Garbage Collection'),
>> - itemId: 'datastore-gc',
>> + itemId: 'datastore-gc-jobs',
>> nodename: 'localhost',
>> cbind: {
>> datastore: '{datastore}',
>> @@ -111,8 +26,6 @@ Ext.define('PBS.Datastore.PruneAndGC', {
>> xtype: 'pbsPruneJobView',
>> nodename: 'localhost',
>> itemId: 'datastore-prune-jobs',
>> - flex: 1,
>> - minHeight: 200,
>> cbind: {
>> datastore: '{datastore}',
>> },
>> --
>> 2.39.2
>>
>>
>>
>> _______________________________________________
>> pbs-devel mailing list
>> pbs-devel at lists.proxmox.com
>> https://lists.proxmox.com/cgi-bin/mailman/listinfo/pbs-devel
-------------------- End of forwarded message --------------------
More information about the pbs-devel
mailing list