Compare commits

..

60 Commits

Author SHA1 Message Date
80b0423d54 bump version to 0.9.7-1
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-11-09 07:37:05 +01:00
b690bb69eb prune sim: align documentation style with sphinx/alabaster ones
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-11-08 14:02:27 +01:00
8a40e22691 docs: scroll navigation to current active section
Add a custom JavaScript file to all HTML rendered docs output.

For now it only hosts a small code snipped which gets the current
active section link and bring it into view.
Needs to be triggered after DOM is initially loaded (which is still
before *all* resources like images, iframes, ... are necessarily
loaded), else the query cannot work.

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-11-08 13:29:09 +01:00
f5c6a2c956 prune sim: slight layout adaptions
add some margin to the calendar table, to not make it seem glued to
the left and top, this follow what ExtJS does in general.

Further, adapt layout flex so that docs has 2/5 and calendar has 3/5
of space on small screens (e.g., 720p), makes it look much better
there.

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-11-08 13:24:27 +01:00
6d5803399b ui: add some onlineHelp reference uses for pruning
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-11-07 16:03:07 +01:00
3896f80cb3 docs: expand prune section, mention simulator, add onlineHelp refs
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-11-07 15:51:09 +01:00
60d2a6157a prune sim: make prune options panel scrollable
Else it's cutoff on 720p resolution

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-11-07 14:33:15 +01:00
b83b12cf80 prune sim: add daily 00:00 as predefined schedule in selector
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-11-07 14:08:41 +01:00
86847f487b prune sim: allow simulating up to 5 years
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-11-07 14:08:41 +01:00
1b03910dea prune sim: spell out PBS, add some flex to layout
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-11-07 14:08:41 +01:00
435a6c5e0a prune sim: fine tune calendar layout/style
Avoid black on white, to much contrast hurts the eye, use a dark grey
instead.

Highlight Sundays, and show month boundaries explicitly with strong
dashed border.

Factor out some manual set styles to classes and use them instead,
decoupling logic and styling a bit more.

Use span elements for plain text stuff, which should not be a block
(e.g., div) element.

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-11-07 14:08:41 +01:00
1f4befe136 prune sim: enable calendar by default
it has a really good non-intrusive layout now, so show it's glory by
default.

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-11-07 13:36:58 +01:00
7f0f366675 prune sim: do not continue with reload if we caught an exception
as we then try to dereference hours which is null, for example.

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-11-07 13:35:58 +01:00
362e69610c prune sim: set update button handler directly
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-11-07 13:35:26 +01:00
bad26df102 prune sim: factor out toggling color, and default to true
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-11-07 13:34:20 +01:00
790627b4bf prune sim: avoid unnecessary viewmodel formula
we set a reference on the checkbox, so we get this for free

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-11-07 13:33:08 +01:00
6de14a55ed prune sim: fix numberfield spinner scroll with firefox
copied over from widget toolkit

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-11-07 13:32:04 +01:00
8b24c6880a prune sim: eslint fixes, do not define console
really not required nowadays, and we do not use it anyway here..

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-11-07 13:31:14 +01:00
5174956548 prune sim: improve documentation layout
Better line height, some margin on the edges, and max width to avoid
very long lines on wide displays.

Avoid to much contrast by using black on white, use a very dark grey
instead.

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-11-07 13:28:50 +01:00
d669a739b2 ui: datastore: backup owner change: fix layout
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-11-06 19:48:08 +01:00
c7fa61619e ui: move backup group owner changer into window folder
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-11-06 19:47:45 +01:00
009a04f8d0 ui: auth-id selector: validity, code-style and layout fixes
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-11-06 19:46:08 +01:00
0953044cfb ui: use AuthidSelector for selecting new owner
Signed-off-by: Fabian Grünbichler <f.gruenbichler@proxmox.com>
2020-11-06 19:06:35 +01:00
d923671a7b ui: use AuthidSelector for sync job owner
Signed-off-by: Fabian Grünbichler <f.gruenbichler@proxmox.com>
2020-11-06 19:06:34 +01:00
db8a606707 proxmox-backup-proxy: remove unnecessary alias
the basedir is already /usr/share/javascript/proxmox-backup/
so adding a subdir of that as alias is not needed

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-11-06 18:08:18 +01:00
b614b29bea ui: datastore: add option view tab
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-11-06 17:52:15 +01:00
65595e169f ui: add NotifyOptions edit window
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-11-06 17:52:15 +01:00
10db4717f1 docs: maintenance: document notifications
can surely be improved, just to have anything..

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-11-06 17:52:15 +01:00
1d9d2f0f7c ui: utils: add property format string helpers from PVE
slightly adapted, i.e., the delete_if_default helper always sets the
delete property to an array if not existing.

Also, filtering out undefined values when printing properties.

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-11-06 17:52:15 +01:00
ad53c1d6dd api: datastore: allow to set "verify-new" option over API
Until now, one could only set this by editing the configuration file
manually.

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-11-06 17:24:14 +01:00
beeadb8a4b Remove reference to backup@pam 2020-11-06 16:32:35 +01:00
b997524912 Add screenshots
For:
- api tokens
- new user management interface
- updatae server administration

Signed-off-by: Dylan Whyte <d.whyte@proxmox.com>
2020-11-06 16:30:59 +01:00
cc4a9d250a maintenance: add verification and prune to section
Includes new screen shots of interface

Signed-off-by: Dylan Whyte <d.whyte@proxmox.com>
2020-11-06 16:29:59 +01:00
6227b9bab0 Update where to find certain items since GUI update
- Sync jobs in datastore
- "User management" is now section of Access Control

Signed-off-by: Dylan Whyte <d.whyte@proxmox.com>
2020-11-06 16:28:47 +01:00
f608e74c8b datastore: description of new datastore view
- Add screenshots from new datastore view
- Add description of comment field in create datastore window
- Add description of each tab in the datastore panel
- Update instructions to add datastore from GUI

Signed-off-by: Dylan Whyte <d.whyte@proxmox.com>
2020-11-06 16:28:16 +01:00
08379a21d1 backup-client: add section on change-owner command
Add section "Changing the Owner of a Backup Group"

Signed-off-by: Dylan Whyte <d.whyte@proxmox.com>
2020-11-06 16:27:20 +01:00
8f1d972149 installation & gui: Formatting fixup
Fix some minor formatting errors in the docs

Signed-off-by: Dylan Whyte <d.whyte@proxmox.com>
2020-11-06 16:26:09 +01:00
b59c308219 Vec::new is Vec's default default
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2020-11-06 14:55:34 +01:00
0224c3c273 client: properly complete new-owner
with remote Authids, not local Userids.

Signed-off-by: Fabian Grünbichler <f.gruenbichler@proxmox.com>
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2020-11-06 14:54:08 +01:00
f0609851fc www: add AuthidSelector
similar to TokenSelector, but with different fields / mapping of data.

Signed-off-by: Fabian Grünbichler <f.gruenbichler@proxmox.com>
2020-11-06 13:06:16 +01:00
dbd45a72c3 tasks: allow access to job tasks
if the user/token could have either configured/manually executed the
task, but it was either executed via the schedule (root@pam) or
another user/token.

without this change, semi-privileged users (that cannot read all tasks
globally, but are DatastoreAdmin) could schedule jobs, but not read
their logs once the schedule executes them. it also makes sense for
multiple such users to see eachothers manually executed jobs, as long as
the privilege level on the datastore (or remote/remote_store/local
store) itself is sufficient.

Signed-off-by: Fabian Grünbichler <f.gruenbichler@proxmox.com>
2020-11-06 12:56:06 +01:00
4c979d5450 verify: allow unprivileged access to admin API
which is the one used by the GUI.

Signed-off-by: Fabian Grünbichler <f.gruenbichler@proxmox.com>
2020-11-06 12:41:41 +01:00
35c80d696f verify: fix unprivileged verification jobs
since the store is not a path parameter, we need to do manual instead of
schema checks. also dropping Datastore.Backup here

Signed-off-by: Fabian Grünbichler <f.gruenbichler@proxmox.com>
2020-11-06 12:39:06 +01:00
6823fdc7f9 ui: improve prune simulator layout 2020-11-06 12:12:59 +01:00
3323798b54 include prune simulator in build
Signed-off-by: Fabian Ebner <f.ebner@proxmox.com>
Signed-off-by: Dietmar Maurer <dietmar@proxmox.com>
2020-11-06 09:59:24 +01:00
67fd09791f create prune simulator
A stand-alone ExtJS app that allows experimenting with different backup
schedules and prune parameters.

The HTML for the documentation was taken from the PBS docs and adapted to the
context of the simulator.

For performance reasons, the week table does not use
subcomponents, but raw HTML.

Signed-off-by: Fabian Ebner <f.ebner@proxmox.com>
2020-11-06 09:13:43 +01:00
1b37ebf6f6 ui: require owner for sync jobs 2020-11-06 08:48:07 +01:00
043406d662 ui: use pbsUserSelector for BackupGroupChangeOwner 2020-11-06 08:48:07 +01:00
61db0851d6 gui: Add button for changing backup group owner
Extension of fix #2847

Adds an action button to the datastore content view,
to change the owner of a backup.

Signed-off-by: Dylan Whyte <d.whyte@proxmox.com>
2020-11-06 08:48:00 +01:00
ad54df3178 get rid of backup@pam 2020-11-06 08:39:30 +01:00
71103afd69 fixup: acutally commit all changes..
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-11-06 08:24:30 +01:00
6465d809cd ui: move datastore related files into own folder
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-11-06 08:11:22 +01:00
ae8635c307 www: add remote store selector
(hopefully) improved upon NFS export selection in PVE

Signed-off-by: Fabian Grünbichler <f.gruenbichler@proxmox.com>
2020-11-05 12:56:20 +01:00
e0100d618e api: refactor remote client and add remote scan
to allow on-demand scanning of remote datastores accessible for the
configured remote user.

Signed-off-by: Fabian Grünbichler <f.gruenbichler@proxmox.com>
2020-11-05 12:56:20 +01:00
455e5f7110 types: extract DataStoreListItem
for reuse in remote scan API call

Signed-off-by: Fabian Grünbichler <f.gruenbichler@proxmox.com>
2020-11-05 12:56:20 +01:00
c26c9390ff config: make notify a property string
For example "gc=never,verify=always,sync=error".
2020-11-05 11:35:14 +01:00
9e45e03aef tools/daemon: fix reload with open connections
instead of await'ing the result of 'create_service' directly,
poll it together with the shutdown_future

if we reached that, fork_restart the new daemon, and await
the open future from 'create_service'

this way the old process still handles open connections until they finish,
while we already start a new process that handles new incoming connections

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2020-11-05 11:14:56 +01:00
e144810d73 pxar: more concise EOF handling
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2020-11-05 10:32:48 +01:00
3c2dd8ad05 pxar/create: handle ErrorKind::Interrupted for file reads
they are not an error and we should retry the read

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2020-11-05 10:27:36 +01:00
91e3b38da4 pxar/create: fix endless loop for shrinking files
when a file shrunk during backup, we endlessly looped, reading/copying 0 bytes
we already have code that handles shrunk files, but we forgot to
break from the read loop

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2020-11-05 10:27:30 +01:00
61 changed files with 2213 additions and 212 deletions

View File

@ -1,6 +1,6 @@
[package]
name = "proxmox-backup"
version = "0.9.6"
version = "0.9.7"
authors = ["Dietmar Maurer <dietmar@proxmox.com>"]
edition = "2018"
license = "AGPL-3"

29
debian/changelog vendored
View File

@ -1,3 +1,32 @@
rust-proxmox-backup (0.9.7-1) unstable; urgency=medium
* ui: add remote store selector
* tools/daemon: fix reload with open connections
* pxar/create: fix endless loop for shrinking files
* pxar/create: handle ErrorKind::Interrupted for file reads
* ui: add action-button for changing backup group owner
* docs: add interactive prune simulator
* verify: fix unprivileged verification jobs
* tasks: allow access to job tasks
* drop internal 'backup@pam' owner, sync jobs need to set a explicit owner
* api: datastore: allow to set "verify-new" option over API
* ui: datastore: add Options tab, allowing one to change per-datastore
notification and verify-new options
* docs: scroll navigation bar to current active section
-- Proxmox Support Team <support@proxmox.com> Mon, 09 Nov 2020 07:36:58 +0100
rust-proxmox-backup (0.9.6-1) unstable; urgency=medium
* fix #3106: improve queueing new incoming connections

View File

@ -1 +1,2 @@
/usr/share/doc/proxmox-backup/proxmox-backup.pdf /usr/share/doc/proxmox-backup/html/proxmox-backup.pdf
/usr/share/javascript/extjs /usr/share/doc/proxmox-backup/html/prune-simulator/extjs

View File

@ -14,6 +14,10 @@ MANUAL_PAGES := \
proxmox-backup-client.1 \
proxmox-backup-manager.1
PRUNE_SIMULATOR_FILES := \
prune-simulator/index.html \
prune-simulator/documentation.html \
prune-simulator/prune-simulator.js
# Sphinx documentation setup
SPHINXOPTS =
@ -74,10 +78,11 @@ onlinehelpinfo:
@echo "Build finished. OnlineHelpInfo.js is in $(BUILDDIR)/scanrefs."
.PHONY: html
html: ${GENERATED_SYNOPSIS} images/proxmox-logo.svg custom.css conf.py
html: ${GENERATED_SYNOPSIS} images/proxmox-logo.svg custom.css conf.py ${PRUNE_SIMULATOR_FILES}
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
cp images/proxmox-logo.svg $(BUILDDIR)/html/_static/
cp custom.css $(BUILDDIR)/html/_static/
install -m 0644 custom.js custom.css images/proxmox-logo.svg $(BUILDDIR)/html/_static/
install -dm 0755 $(BUILDDIR)/html/prune-simulator
install -m 0644 ${PRUNE_SIMULATOR_FILES} $(BUILDDIR)/html/prune-simulator
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."

View File

@ -535,6 +535,29 @@ To remove the ticket, issue a logout:
# proxmox-backup-client logout
.. _changing-backup-owner:
Changing the Owner of a Backup Group
------------------------------------
By default, the owner of a backup group is the user which was used to originally
create that backup group (or in the case of sync jobs, ``root@pam``). This
means that if a user ``mike@pbs`` created a backup, another user ``john@pbs``
can not be used to create backups in that same backup group. In case you want
to change the owner of a backup, you can do so with the below command, using a
user that has ``Datastore.Modify`` privileges on the datastore.
.. code-block:: console
# proxmox-backup-client change-owner vm/103 john@pbs
This can also be done from within the web interface, by navigating to the
`Content` section of the datastore that contains the backup group and
selecting the user icon under the `Actions` column. Common cases for this could
be to change the owner of a sync job from ``root@pam``, or to repurpose a
backup group.
.. _backup-pruning:
Pruning and Removing Backups

View File

@ -171,6 +171,7 @@ html_theme_options = {
'extra_nav_links': {
'Proxmox Homepage': 'https://proxmox.com',
'PDF': 'proxmox-backup.pdf',
'Prune Simulator' : 'prune-simulator/index.html',
},
'sidebar_width': '320px',
@ -228,6 +229,10 @@ html_favicon = 'images/favicon.ico'
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
html_js_files = [
'custom.js',
]
# Add any extra paths that contain custom files (such as robots.txt or
# .htaccess) here, relative to this directory. These files are copied
# directly to the root of the documentation.

7
docs/custom.js Normal file
View File

@ -0,0 +1,7 @@
window.addEventListener('DOMContentLoaded', (event) => {
let activeSection = document.querySelector("a.current");
if (activeSection) {
// https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView
activeSection.scrollIntoView({ block: 'center' });
}
});

View File

@ -4,7 +4,7 @@ Graphical User Interface
Proxmox Backup Server offers an integrated, web-based interface to manage the
server. This means that you can carry out all administration tasks through your
web browser, and that you don't have to worry about installing extra management
tools. The web interface also provides a built in console, so if you prefer the
tools. The web interface also provides a built-in console, so if you prefer the
command line or need some extra control, you have this option.
The web interface can be accessed via https://youripaddress:8007. The default
@ -28,7 +28,6 @@ Login
-----
.. image:: images/screenshots/pbs-gui-login-window.png
:width: 250
:align: right
:alt: PBS login window
@ -44,14 +43,13 @@ GUI Overview
------------
.. image:: images/screenshots/pbs-gui-dashboard.png
:width: 250
:align: right
:alt: PBS GUI Dashboard
The Proxmox Backup Server web interface consists of 3 main sections:
* **Header**: At the top. This shows version information, and contains buttons to view
documentation, monitor running tasks, and logout.
documentation, monitor running tasks, set the language and logout.
* **Sidebar**: On the left. This contains the configuration options for
the server.
* **Configuration Panel**: In the center. This contains the control interface for the
@ -79,18 +77,17 @@ Configuration
The Configuration section contains some system configuration options, such as
time and network configuration. It also contains the following subsections:
* **User Management**: Add users and manage accounts
* **Permissions**: Manage permissions for various users
* **Access Control**: Add and manage users, API tokens, and the permissions
associated with these items
* **Remotes**: Add, edit and remove remotes (see :term:`Remote`)
* **Sync Jobs**: Manage and run sync jobs to remotes
* **Subscription**: Upload a subscription key and view subscription status
* **Subscription**: Upload a subscription key, view subscription status and
access a text-based system report.
Administration
^^^^^^^^^^^^^^
.. image:: images/screenshots/pbs-gui-administration-serverstatus.png
:width: 250
:align: right
:alt: Administration: Server Status overview
@ -105,7 +102,6 @@ tasks and information. These are:
* **Tasks**: Task history with multiple filter options
.. image:: images/screenshots/pbs-gui-disks.png
:width: 250
:align: right
:alt: Administration: Disks
@ -120,16 +116,21 @@ The administration menu item also contains a disk management subsection:
Datastore
^^^^^^^^^
.. image:: images/screenshots/pbs-gui-datastore.png
:width: 250
.. image:: images/screenshots/pbs-gui-datastore-summary.png
:align: right
:alt: Datastore Configuration
The Datastore section provides an interface for creating and managing
datastores. It contains a subsection for each datastore on the system, in
which you can use the top panel to view:
The Datastore section contains interfaces for creating and managing
datastores. It contains a button to create a new datastore on the server, as
well as a subsection for each datastore on the system, in which you can use the
top panel to view:
* **Summary**: Access a range of datastore usage statistics
* **Content**: Information on the datastore's backup groups and their respective
contents
* **Statistics**: Usage statistics for the datastore
* **Permissions**: View and manage permissions for the datastore
* **Prune & GC**: Schedule :ref:`pruning <backup-pruning>` and :ref:`garbage
collection <garbage-collection>` operations, and run garbage collection
manually
* **Sync Jobs**: Create, manage and run :ref:`syncjobs` from remote servers
* **Verify Jobs**: Create, manage and run :ref:`verification` jobs on the
datastore

Binary file not shown.

Before

Width:  |  Height:  |  Size: 127 KiB

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 62 KiB

View File

@ -9,7 +9,7 @@ Debian_ from the provided package repository.
.. include:: package-repositories.rst
Server installation
Server Installation
-------------------
The backup server stores the actual backed up data and provides a web based GUI
@ -52,7 +52,7 @@ It includes the following:
is used by default and all existing data is removed.
Install `Proxmox Backup`_ server on Debian
Install `Proxmox Backup`_ Server on Debian
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Proxmox ships as a set of Debian packages which can be installed on top of a
@ -84,11 +84,11 @@ support, and a set of common and useful packages.
when LVM_ or ZFS_ is used. The network configuration is completely up to you
as well.
.. note:: You can access the web interface of the Proxmox Backup Server with
your web browser, using HTTPS on port 8007. For example at
``https://<ip-or-dns-name>:8007``
.. Note:: You can access the web interface of the Proxmox Backup Server with
your web browser, using HTTPS on port 8007. For example at
``https://<ip-or-dns-name>:8007``
Install Proxmox Backup server on `Proxmox VE`_
Install Proxmox Backup Server on `Proxmox VE`_
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
After configuring the
@ -104,14 +104,14 @@ After configuring the
server to store backups. Should the hypervisor server fail, you can
still access the backups.
.. note::
You can access the web interface of the Proxmox Backup Server with your web
browser, using HTTPS on port 8007. For example at ``https://<ip-or-dns-name>:8007``
.. Note:: You can access the web interface of the Proxmox Backup Server with
your web browser, using HTTPS on port 8007. For example at
``https://<ip-or-dns-name>:8007``
Client installation
Client Installation
-------------------
Install `Proxmox Backup`_ client on Debian
Install `Proxmox Backup`_ Client on Debian
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Proxmox ships as a set of Debian packages to be installed on

View File

@ -1,13 +1,148 @@
Maintenance Tasks
=================
.. _maintenance_pruning:
Pruning
-------
Prune lets you specify which backup snapshots you want to keep. The
following retention options are available:
``keep-last <N>``
Keep the last ``<N>`` backup snapshots.
``keep-hourly <N>``
Keep backups for the last ``<N>`` hours. If there is more than one
backup for a single hour, only the latest is kept.
``keep-daily <N>``
Keep backups for the last ``<N>`` days. If there is more than one
backup for a single day, only the latest is kept.
``keep-weekly <N>``
Keep backups for the last ``<N>`` weeks. If there is more than one
backup for a single week, only the latest is kept.
.. note:: Weeks start on Monday and end on Sunday. The software
uses the `ISO week date`_ system and handles weeks at
the end of the year correctly.
``keep-monthly <N>``
Keep backups for the last ``<N>`` months. If there is more than one
backup for a single month, only the latest is kept.
``keep-yearly <N>``
Keep backups for the last ``<N>`` years. If there is more than one
backup for a single year, only the latest is kept.
The retention options are processed in the order given above. Each option
only covers backups within its time period. The next option does not take care
of already covered backups. It will only consider older backups.
Unfinished and incomplete backups will be removed by the prune command unless
they are newer than the last successful backup. In this case, the last failed
backup is retained.
Prune Simulator
^^^^^^^^^^^^^^^
You can use the built-in `prune simulator <prune-simulator/index.html>`_
to explore the effect of different retetion options with various backup
schedules.
Manual Pruning
^^^^^^^^^^^^^^
.. image:: images/screenshots/pbs-gui-datastore-content-prune-group.png
:target: _images/pbs-gui-datastore-content-prune-group.png
:align: right
:alt: Prune and garbage collection options
To access pruning functionality for a specific backup group, you can use the
prune command line option discussed in :ref:`backup-pruning`, or navigate to
the **Content** tab of the datastore and click the scissors icon in the
**Actions** column of the relevant backup group.
Prune Schedules
^^^^^^^^^^^^^^^
To prune on a datastore level, scheduling options can be found under the
**Prune & GC** tab of the datastore. Here you can set retention settings and
edit the interval at which pruning takes place.
.. image:: images/screenshots/pbs-gui-datastore-prunegc.png
:target: _images/pbs-gui-datastore-prunegc.png
:align: right
:alt: Prune and garbage collection options
.. _maintenance_gc:
Garbage Collection
------------------
You can monitor and run :ref:`garbage collection <garbage-collection>` on the
Proxmox Backup Server using the ``garbage-collection`` subcommand of
``proxmox-backup-manager``. You can use the ``start`` subcommand to manually start garbage
collection on an entire datastore and the ``status`` subcommand to see
attributes relating to the :ref:`garbage collection <garbage-collection>`.
``proxmox-backup-manager``. You can use the ``start`` subcommand to manually
start garbage collection on an entire datastore and the ``status`` subcommand to
see attributes relating to the :ref:`garbage collection <garbage-collection>`.
.. todo:: Add section on verification
This functionality can also be accessed in the GUI, by navigating to **Prune &
GC** from the top panel. From here, you can edit the schedule at which garbage
collection runs and manually start the operation.
.. _verification:
Verification
------------
.. image:: images/screenshots/pbs-gui-datastore-verifyjob-add.png
:target: _images/pbs-gui-datastore-verifyjob-add.png
:align: right
:alt: Adding a verify job
Proxmox Backup offers various verification options to ensure that backup data is
intact. Verification is generally carried out through the creation of verify
jobs. These are scheduled tasks that run verification at a given interval (see
:ref:`calendar-events`). With these, you can set whether already verified
snapshots are ignored, as well as set a time period, after which verified jobs
are checked again. The interface for creating verify jobs can be found under the
**Verify Jobs** tab of the datastore.
.. Note:: It is recommended that you reverify all backups at least monthly, even
if a previous verification was successful. This is becuase physical drives
are susceptible to damage over time, which can cause an old, working backup
to become corrupted in a process known as `bit rot/data degradation
<https://en.wikipedia.org/wiki/Data_degradation>`_. It is good practice to
have a regularly recurring (hourly/daily) verification job, which checks new
and expired backups, then another weekly/monthly job that will reverify
everything. This way, there will be no surprises when it comes to restoring
data.
Aside from using verify jobs, you can also run verification manually on entire
datastores, backup groups, or snapshots. To do this, navigate to the **Content**
tab of the datastore and either click *Verify All*, or select the *V.* icon from
the *Actions* column in the table.
.. _maintenance_notification:
Notifications
-------------
Proxmox Backup Server can send you notification emails about automatically
scheduled verification, garbage-collection and synchronization tasks results.
By default, notifications are send to the email address configured for the
`root@pam` user. You can set that user for each datastore.
You can also change the level of notification received per task type, the
following options are available:
* Always: send a notification for any scheduled task, independent of the
outcome
* Errors: send a notification for any scheduled task resulting in an error
* Never: do not send any notification at all

View File

@ -59,13 +59,13 @@ Sync Jobs
:alt: Add a Sync Job
Sync jobs are configured to pull the contents of a datastore on a **Remote** to
a local datastore. You can manage sync jobs under **Configuration -> Sync Jobs**
in the web interface, or using the ``proxmox-backup-manager sync-job`` command.
The configuration information for sync jobs is stored at
``/etc/proxmox-backup/sync.cfg``. To create a new sync job, click the add button
in the GUI, or use the ``create`` subcommand. After creating a sync job, you can
either start it manually on the GUI or provide it with a schedule (see
:ref:`calendar-events`) to run regularly.
a local datastore. You can manage sync jobs in the web interface, from the
**Sync Jobs** tab of the datastore which you'd like to set one up for, or using
the ``proxmox-backup-manager sync-job`` command. The configuration information
for sync jobs is stored at ``/etc/proxmox-backup/sync.cfg``. To create a new
sync job, click the add button in the GUI, or use the ``create`` subcommand.
After creating a sync job, you can either start it manually from the GUI or
provide it with a schedule (see :ref:`calendar-events`) to run regularly.
.. code-block:: console
@ -86,7 +86,7 @@ For setting up sync jobs, the configuring user needs the following permissions:
If the ``remove-vanished`` option is set, ``Datastore.Prune`` is required on
the local datastore as well. If the ``owner`` option is not set (defaulting to
``backup@pam``) or set to something other than the configuring user,
``root@pam``) or set to something other than the configuring user,
``Datastore.Modify`` is required as well.
.. note:: A sync job can only sync backup groups that the configured remote's

View File

@ -0,0 +1,102 @@
<!DOCTYPE html>
<html>
<head>
<style>
/* similar to sphinx alabaster theme ones */
body {
max-width: 90ch;
margin-left: 2ch;
margin-right: 2ch;
line-height: 1.4em;
/* avoid the very high contrast of black on white, tone it down a bit */
color: #3E4349;
hyphens: auto;
text-align: left;
font-family: 'Open Sans', sans-serif;
font-size: 17px;
}
h1, h2, h3 {
font-family: Lato, sans-serif;
font-size: 150%;
line-height:1.2
}
tt, code {
background-color: #ecf0f3;
color: #222;
}
pre, tt, code {
font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace;
font-size: 0.9em;
}
div.note {
background-color: #EEE;
border: 1px solid #CCC;
margin: 10px 0;
padding: 0px 20px;
}
p.note-title {
font-weight: bolder;
padding: 0;
margin: 10px 0 0 0;
}
div.note > p.last {
margin: 5px 0 10px 0;
}
</style>
</head>
<body>
<p>A simulator to experiment with different backup schedules and prune
options.</p>
<h3>Schedule</h3>
<p>Select weekdays with the combobox and input hour and minute
specification separated by a colon, i.e. <code>HOUR:MINUTE</code>. Each of
<code>HOUR</code> and <code>MINUTE</code> can be either a single value or
one of the following:</p>
<ul class="simple">
<li>a comma-separated list: e.g., <code>01,02,03</code></li>
<li>a range: e.g., <code>01..10</code></li>
<li>a repetition: e.g, <code>05/10</code> (means starting at <code>5</code> every <code>10</code>)</li>
<li>a combination of the above: e.g., <code>01,05..10,12/02</code></li>
<li>a <code>*</code> for every possible value</li>
</ul>
<h3>Pruning</h3>
<p>Prune lets you systematically delete older backups, retaining backups for
the last given number of time intervals. The following retention options are
available:</p>
<dl class="docutils">
<dt><code class="docutils literal notranslate"><span class="pre">keep-last</span> <span class="pre">&lt;N&gt;</span></code></dt>
<dd>Keep the last <code class="docutils literal notranslate"><span class="pre">&lt;N&gt;</span></code> backup snapshots.</dd>
<dt><code class="docutils literal notranslate"><span class="pre">keep-hourly</span> <span class="pre">&lt;N&gt;</span></code></dt>
<dd>Keep backups for the last <code class="docutils literal notranslate"><span class="pre">&lt;N&gt;</span></code> hours. If there is more than one
backup for a single hour, only the latest is kept.</dd>
<dt><code class="docutils literal notranslate"><span class="pre">keep-daily</span> <span class="pre">&lt;N&gt;</span></code></dt>
<dd>Keep backups for the last <code class="docutils literal notranslate"><span class="pre">&lt;N&gt;</span></code> days. If there is more than one
backup for a single day, only the latest is kept.</dd>
<dt><code class="docutils literal notranslate"><span class="pre">keep-weekly</span> <span class="pre">&lt;N&gt;</span></code></dt>
<dd>Keep backups for the last <code class="docutils literal notranslate"><span class="pre">&lt;N&gt;</span></code> weeks. If there is more than one
backup for a single week, only the latest is kept.
<div class="last admonition note">
<p class="note-title">Note:</p>
<p class="last">Weeks start on Monday and end on Sunday. The software
uses the <a class="reference external" href="https://en.wikipedia.org/wiki/ISO_week_date">ISO week date</a> system and handles weeks at
the end of the year correctly.</p>
</div>
</dd>
<dt><code class="docutils literal notranslate"><span class="pre">keep-monthly</span> <span class="pre">&lt;N&gt;</span></code></dt>
<dd>Keep backups for the last <code class="docutils literal notranslate"><span class="pre">&lt;N&gt;</span></code> months. If there is more than one
backup for a single month, only the latest is kept.</dd>
<dt><code class="docutils literal notranslate"><span class="pre">keep-yearly</span> <span class="pre">&lt;N&gt;</span></code></dt>
<dd>Keep backups for the last <code class="docutils literal notranslate"><span class="pre">&lt;N&gt;</span></code> years. If there is more than one
backup for a single year, only the latest is kept.</dd>
</dl>
<p>The retention options are processed in the order given above. Each option
only covers backups within its time period. The next option does not take care
of already covered backups. It will only consider older backups.</p>
<p>For example, in a week covered by <code>keep-weekly</code>, one backup is
kept while all others are removed; <code>keep-monthly</code> then does not
consider backups from that week anymore, even if part of the week is part of
an earlier month.</p>
</body>
</html>

View File

@ -0,0 +1,41 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<title>PBS Prune Simulator</title>
<link rel="stylesheet" type="text/css" href="extjs/theme-crisp/resources/theme-crisp-all.css">
<style>
.cal {
margin: 5px;
}
.cal-day {
vertical-align: top;
width: 150px;
border: #939393 1px solid;
color: #454545;
}
.cal-day-date {
border-bottom: #444 1px solid;
color: #000;
}
.strikethrough {
text-decoration: line-through;
}
.black {
color: #000;
}
.sun {
background-color: #ededed;
}
.first-of-month {
border-right: dashed black 4px;
}
</style>
<script type="text/javascript" src="extjs/ext-all.js"></script>
<script type="text/javascript" src="prune-simulator.js"></script>
</head>
<body></body>
</html>

View File

@ -0,0 +1,773 @@
// FIXME: HACK! Makes scrolling in number spinner work again. fixed in ExtJS >= 6.1
if (Ext.isFirefox) {
Ext.$eventNameMap.DOMMouseScroll = 'DOMMouseScroll';
}
Ext.onReady(function() {
const NOW = new Date();
const COLORS = {
'keep-last': 'orange',
'keep-hourly': 'purple',
'keep-daily': 'yellow',
'keep-weekly': 'green',
'keep-monthly': 'blue',
'keep-yearly': 'red',
'all zero': 'white',
};
const TEXT_COLORS = {
'keep-last': 'black',
'keep-hourly': 'white',
'keep-daily': 'black',
'keep-weekly': 'white',
'keep-monthly': 'white',
'keep-yearly': 'white',
'all zero': 'black',
};
Ext.define('PBS.prunesimulator.Documentation', {
extend: 'Ext.Panel',
alias: 'widget.prunesimulatorDocumentation',
html: '<iframe style="width:100%;height:100%;border:0px;" src="./documentation.html"/>',
});
Ext.define('PBS.prunesimulator.CalendarEvent', {
extend: 'Ext.form.field.ComboBox',
alias: 'widget.prunesimulatorCalendarEvent',
editable: true,
displayField: 'text',
valueField: 'value',
queryMode: 'local',
store: {
field: ['value', 'text'],
data: [
{ value: '0/2:00', text: "Every two hours" },
{ value: '0/6:00', text: "Every six hours" },
{ value: '2,22:30', text: "At 02:30 and 22:30" },
{ value: '00:00', text: "At 00:00" },
{ value: '08..17:00/30', text: "From 08:00 to 17:30 every 30 minutes" },
{ value: 'HOUR:MINUTE', text: "Custom schedule" },
],
},
tpl: [
'<ul class="x-list-plain"><tpl for=".">',
'<li role="option" class="x-boundlist-item">{text}</li>',
'</tpl></ul>',
],
displayTpl: [
'<tpl for=".">',
'{value}',
'</tpl>',
],
});
Ext.define('PBS.prunesimulator.DayOfWeekSelector', {
extend: 'Ext.form.field.ComboBox',
alias: 'widget.prunesimulatorDayOfWeekSelector',
editable: false,
displayField: 'text',
valueField: 'value',
queryMode: 'local',
store: {
field: ['value', 'text'],
data: [
{ value: 'mon', text: Ext.util.Format.htmlDecode(Ext.Date.dayNames[1]) },
{ value: 'tue', text: Ext.util.Format.htmlDecode(Ext.Date.dayNames[2]) },
{ value: 'wed', text: Ext.util.Format.htmlDecode(Ext.Date.dayNames[3]) },
{ value: 'thu', text: Ext.util.Format.htmlDecode(Ext.Date.dayNames[4]) },
{ value: 'fri', text: Ext.util.Format.htmlDecode(Ext.Date.dayNames[5]) },
{ value: 'sat', text: Ext.util.Format.htmlDecode(Ext.Date.dayNames[6]) },
{ value: 'sun', text: Ext.util.Format.htmlDecode(Ext.Date.dayNames[0]) },
],
},
});
Ext.define('pbs-prune-list', {
extend: 'Ext.data.Model',
fields: [
{
name: 'backuptime',
type: 'date',
dateFormat: 'timestamp',
},
{
name: 'mark',
type: 'string',
},
{
name: 'keepName',
type: 'string',
},
],
});
Ext.define('PBS.prunesimulator.PruneList', {
extend: 'Ext.panel.Panel',
alias: 'widget.prunesimulatorPruneList',
initComponent: function() {
let me = this;
if (!me.store) {
throw "no store specified";
}
me.items = [
{
xtype: 'grid',
store: me.store,
border: false,
columns: [
{
header: 'Backup Time',
dataIndex: 'backuptime',
renderer: function(value, metaData, record) {
let text = Ext.Date.format(value, 'Y-m-d H:i:s');
if (record.data.mark === 'keep') {
if (me.useColors) {
let bgColor = COLORS[record.data.keepName];
let textColor = TEXT_COLORS[record.data.keepName];
return '<div style="background-color: ' + bgColor + '; ' +
'color: ' + textColor + ';">' + text + '</div>';
} else {
return text;
}
} else {
return '<div style="text-decoration: line-through;">' + text + '</div>';
}
},
flex: 1,
sortable: false,
},
{
header: 'Keep (reason)',
dataIndex: 'mark',
renderer: function(value, metaData, record) {
if (record.data.mark === 'keep') {
return 'keep (' + record.data.keepName + ')';
} else {
return value;
}
},
width: 200,
sortable: false,
},
],
},
];
me.callParent();
},
});
Ext.define('PBS.prunesimulator.WeekTable', {
extend: 'Ext.panel.Panel',
alias: 'widget.prunesimulatorWeekTable',
reload: function() {
let me = this;
let backups = me.store.data.items;
let html = '<table class="cal">';
let now = new Date(NOW.getTime());
let skip = 7 - parseInt(Ext.Date.format(now, 'N'), 10);
let tableStartDate = Ext.Date.add(now, Ext.Date.DAY, skip);
let bIndex = 0;
for (let i = 0; bIndex < backups.length; i++) {
html += '<tr>';
for (let j = 0; j < 7; j++) {
let date = Ext.Date.subtract(tableStartDate, Ext.Date.DAY, j + 7 * i);
let currentDay = Ext.Date.format(date, 'd/m/Y');
let dayOfWeekCls = Ext.Date.format(date, 'D').toLowerCase();
let firstOfMonthCls = Ext.Date.format(date, 'd') === '01'
? 'first-of-month'
: '';
html += `<td class="cal-day ${dayOfWeekCls} ${firstOfMonthCls}">`;
const isBackupOnDay = function(backup, day) {
return backup && Ext.Date.format(backup.data.backuptime, 'd/m/Y') === day;
};
let backup = backups[bIndex];
html += '<table><tr>';
html += `<th class="cal-day-date">${Ext.Date.format(date, 'D, d M Y')}</th>`;
while (isBackupOnDay(backup, currentDay)) {
html += '<tr><td>';
let text = Ext.Date.format(backup.data.backuptime, 'H:i');
if (backup.data.mark === 'remove') {
html += `<span class="strikethrough">${text}</span>`;
} else {
text += ` (${backup.data.keepName})`;
if (me.useColors) {
let bgColor = COLORS[backup.data.keepName];
let textColor = TEXT_COLORS[backup.data.keepName];
html += `<span style="background-color: ${bgColor};
color: ${textColor};">${text}</span>`;
} else {
html += `<span class="black">${text}</span>`;
}
}
html += '</td></tr>';
backup = backups[++bIndex];
}
html += '</table>';
html += '</div>';
html += '</td>';
}
html += '</tr>';
}
me.setHtml(html);
},
initComponent: function() {
let me = this;
if (!me.store) {
throw "no store specified";
}
let reload = function() {
me.reload();
};
me.store.on("datachanged", reload);
me.callParent();
me.reload();
},
});
Ext.define('PBS.PruneSimulatorPanel', {
extend: 'Ext.panel.Panel',
alias: 'widget.prunesimulatorPanel',
viewModel: {
},
getValues: function() {
let me = this;
let values = {};
Ext.Array.each(me.query('[isFormField]'), function(field) {
let data = field.getSubmitData();
Ext.Object.each(data, function(name, val) {
values[name] = val;
});
});
return values;
},
controller: {
xclass: 'Ext.app.ViewController',
init: function(view) {
this.reloadFull(); // initial load
this.switchColor(true);
},
control: {
'field[fieldGroup=keep]': { change: 'reloadPrune' },
},
reloadFull: function() {
let me = this;
let view = me.getView();
let params = view.getValues();
let [hourSpec, minuteSpec] = params['schedule-time'].split(':');
if (!hourSpec || !minuteSpec) {
Ext.Msg.alert('Error', 'Invalid schedule');
return;
}
let matchTimeSpec = function(timeSpec, rangeMin, rangeMax) {
let specValues = timeSpec.split(',');
let matches = {};
let assertValid = function(value) {
let num = Number(value);
if (isNaN(num)) {
throw value + " is not an integer";
} else if (value < rangeMin || value > rangeMax) {
throw "number '" + value + "' is not in the range '" + rangeMin + ".." + rangeMax + "'";
}
return num;
};
specValues.forEach(function(value) {
if (value.includes('..')) {
let [start, end] = value.split('..');
start = assertValid(start);
end = assertValid(end);
if (start > end) {
throw "interval start is bigger then interval end '" + start + " > " + end + "'";
}
for (let i = start; i <= end; i++) {
matches[i] = 1;
}
} else if (value.includes('/')) {
let [start, step] = value.split('/');
start = assertValid(start);
step = assertValid(step);
for (let i = start; i <= rangeMax; i += step) {
matches[i] = 1;
}
} else if (value === '*') {
for (let i = rangeMin; i <= rangeMax; i++) {
matches[i] = 1;
}
} else {
value = assertValid(value);
matches[value] = 1;
}
});
return Object.keys(matches);
};
let hours, minutes;
try {
hours = matchTimeSpec(hourSpec, 0, 23);
minutes = matchTimeSpec(minuteSpec, 0, 59);
} catch (err) {
Ext.Msg.alert('Error', err);
return;
}
let backups = me.populateFromSchedule(
params['schedule-weekdays'],
hours,
minutes,
params.numberOfWeeks,
);
me.pruneSelect(backups, params);
view.pruneStore.setData(backups);
},
reloadPrune: function() {
let me = this;
let view = me.getView();
let params = view.getValues();
let backups = [];
view.pruneStore.getData().items.forEach(function(item) {
backups.push({
backuptime: item.data.backuptime,
});
});
me.pruneSelect(backups, params);
view.pruneStore.setData(backups);
},
// backups are sorted descending by date
populateFromSchedule: function(weekdays, hours, minutes, weekCount) {
let weekdayFlags = [
weekdays.includes('sun'),
weekdays.includes('mon'),
weekdays.includes('tue'),
weekdays.includes('wed'),
weekdays.includes('thu'),
weekdays.includes('fri'),
weekdays.includes('sat'),
];
let todaysDate = new Date(NOW.getTime());
let timesOnSingleDay = [];
hours.forEach(function(hour) {
minutes.forEach(function(minute) {
todaysDate.setHours(hour);
todaysDate.setMinutes(minute);
timesOnSingleDay.push(todaysDate.getTime());
});
});
// ordering here and iterating backwards through days
// ensures that everything is ordered
timesOnSingleDay.sort(function(a, b) {
return a < b;
});
let backups = [];
for (let i = 0; i < 7 * weekCount; i++) {
let daysDate = Ext.Date.subtract(todaysDate, Ext.Date.DAY, i);
let weekday = parseInt(Ext.Date.format(daysDate, 'w'), 10);
if (weekdayFlags[weekday]) {
timesOnSingleDay.forEach(function(time) {
backups.push({
backuptime: Ext.Date.subtract(new Date(time), Ext.Date.DAY, i),
});
});
}
}
return backups;
},
pruneMark: function(backups, keepCount, keepName, idFunc) {
if (!keepCount) {
return;
}
let alreadyIncluded = {};
let newlyIncluded = {};
let newlyIncludedCount = 0;
let finished = false;
backups.forEach(function(backup) {
let mark = backup.mark;
let id = idFunc(backup);
if (finished || alreadyIncluded[id]) {
return;
}
if (mark) {
if (mark === 'keep') {
alreadyIncluded[id] = true;
}
return;
}
if (!newlyIncluded[id]) {
if (newlyIncludedCount >= keepCount) {
finished = true;
return;
}
newlyIncluded[id] = true;
newlyIncludedCount++;
backup.mark = 'keep';
backup.keepName = keepName;
} else {
backup.mark = 'remove';
}
});
},
// backups need to be sorted descending by date
pruneSelect: function(backups, keepParams) {
let me = this;
if (Number(keepParams['keep-last']) +
Number(keepParams['keep-hourly']) +
Number(keepParams['keep-daily']) +
Number(keepParams['keep-weekly']) +
Number(keepParams['keep-monthly']) +
Number(keepParams['keep-yearly']) === 0) {
backups.forEach(function(backup) {
backup.mark = 'keep';
backup.keepName = 'all zero';
});
return;
}
me.pruneMark(backups, keepParams['keep-last'], 'keep-last', function(backup) {
return backup.backuptime;
});
me.pruneMark(backups, keepParams['keep-hourly'], 'keep-hourly', function(backup) {
return Ext.Date.format(backup.backuptime, 'H/d/m/Y');
});
me.pruneMark(backups, keepParams['keep-daily'], 'keep-daily', function(backup) {
return Ext.Date.format(backup.backuptime, 'd/m/Y');
});
me.pruneMark(backups, keepParams['keep-weekly'], 'keep-weekly', function(backup) {
// ISO-8601 week and week-based year
return Ext.Date.format(backup.backuptime, 'W/o');
});
me.pruneMark(backups, keepParams['keep-monthly'], 'keep-monthly', function(backup) {
return Ext.Date.format(backup.backuptime, 'm/Y');
});
me.pruneMark(backups, keepParams['keep-yearly'], 'keep-yearly', function(backup) {
return Ext.Date.format(backup.backuptime, 'Y');
});
backups.forEach(function(backup) {
backup.mark = backup.mark || 'remove';
});
},
toggleColors: function(checkbox, checked) {
this.switchColor(checked);
},
switchColor: function(useColors) {
let me = this;
let view = me.getView();
const getStyle = name =>
`background-color: ${COLORS[name]}; color: ${TEXT_COLORS[name]};`;
for (const field of view.query('[isFormField]')) {
if (field.fieldGroup !== 'keep') {
continue;
}
if (useColors) {
field.setFieldStyle(getStyle(field.name));
} else {
field.setFieldStyle('background-color: white; color: #444;');
}
}
me.lookup('weekTable').useColors = useColors;
me.lookup('pruneList').useColors = useColors;
me.reloadPrune();
},
},
keepItems: [
{
xtype: 'numberfield',
name: 'keep-last',
allowBlank: true,
fieldLabel: 'keep-last',
minValue: 0,
value: 4,
fieldGroup: 'keep',
},
{
xtype: 'numberfield',
name: 'keep-hourly',
allowBlank: true,
fieldLabel: 'keep-hourly',
minValue: 0,
value: 0,
fieldGroup: 'keep',
},
{
xtype: 'numberfield',
name: 'keep-daily',
allowBlank: true,
fieldLabel: 'keep-daily',
minValue: 0,
value: 5,
fieldGroup: 'keep',
},
{
xtype: 'numberfield',
name: 'keep-weekly',
allowBlank: true,
fieldLabel: 'keep-weekly',
minValue: 0,
value: 2,
fieldGroup: 'keep',
},
{
xtype: 'numberfield',
name: 'keep-monthly',
allowBlank: true,
fieldLabel: 'keep-monthly',
minValue: 0,
value: 0,
fieldGroup: 'keep',
},
{
xtype: 'numberfield',
name: 'keep-yearly',
allowBlank: true,
fieldLabel: 'keep-yearly',
minValue: 0,
value: 0,
fieldGroup: 'keep',
},
],
initComponent: function() {
var me = this;
me.pruneStore = Ext.create('Ext.data.Store', {
model: 'pbs-prune-list',
sorters: { property: 'backuptime', direction: 'DESC' },
});
let scheduleItems = [
{
xtype: 'prunesimulatorDayOfWeekSelector',
name: 'schedule-weekdays',
fieldLabel: 'Day of week',
value: ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'],
allowBlank: false,
multiSelect: true,
padding: '0 0 0 10',
},
{
xtype: 'prunesimulatorCalendarEvent',
name: 'schedule-time',
allowBlank: false,
value: '0/6:00',
fieldLabel: 'Backup schedule',
padding: '0 0 0 10',
},
{
xtype: 'numberfield',
name: 'numberOfWeeks',
allowBlank: false,
fieldLabel: 'Number of weeks',
minValue: 1,
value: 15,
maxValue: 260, // five years
padding: '0 0 0 10',
},
{
xtype: 'button',
name: 'schedule-button',
text: 'Update Schedule',
handler: 'reloadFull',
},
];
me.items = [
{
xtype: 'panel',
layout: {
type: 'hbox',
align: 'stretch',
},
border: false,
items: [
{
title: 'View',
layout: 'anchor',
flex: 1,
border: false,
bodyPadding: 10,
items: [
{
xtype: 'checkbox',
name: 'showCalendar',
reference: 'showCalendar',
fieldLabel: 'Show Calendar:',
checked: true,
},
{
xtype: 'checkbox',
name: 'showColors',
reference: 'showColors',
fieldLabel: 'Show Colors:',
checked: true,
handler: 'toggleColors',
},
],
},
{ xtype: "panel", width: 1, border: 1 },
{
layout: 'anchor',
flex: 1,
border: false,
title: 'Simulated Backup Schedule',
defaults: {
labelWidth: 120,
},
bodyPadding: 10,
items: scheduleItems,
},
],
},
{
xtype: 'panel',
layout: {
type: 'hbox',
align: 'stretch',
},
flex: 1,
border: false,
items: [
{
layout: 'anchor',
title: 'Prune Options',
border: false,
bodyPadding: 10,
scrollable: true,
items: me.keepItems,
flex: 1,
},
{ xtype: "panel", width: 1, border: 1 },
{
layout: 'fit',
title: 'Backups',
border: false,
xtype: 'prunesimulatorPruneList',
store: me.pruneStore,
reference: 'pruneList',
flex: 1,
},
],
},
{
layout: 'anchor',
title: 'Calendar',
autoScroll: true,
flex: 2,
xtype: 'prunesimulatorWeekTable',
reference: 'weekTable',
store: me.pruneStore,
bind: {
hidden: '{!showCalendar.checked}',
},
},
];
me.callParent();
},
});
Ext.create('Ext.container.Viewport', {
layout: 'border',
renderTo: Ext.getBody(),
items: [
{
xtype: 'prunesimulatorPanel',
title: 'Proxmox Backup Server - Prune Simulator',
region: 'west',
layout: {
type: 'vbox',
align: 'stretch',
pack: 'start',
},
flex: 3,
maxWidth: 1090,
},
{
xtype: 'prunesimulatorDocumentation',
title: 'Usage',
border: false,
flex: 2,
region: 'center',
},
],
});
});

View File

@ -107,7 +107,7 @@ is stored in the file ``/etc/proxmox-backup/datastore.cfg``.
Datastore Configuration
~~~~~~~~~~~~~~~~~~~~~~~
.. image:: images/screenshots/pbs-gui-datastore.png
.. image:: images/screenshots/pbs-gui-datastore-content.png
:align: right
:alt: Datastore Overview
@ -127,8 +127,9 @@ Creating a Datastore
:align: right
:alt: Create a datastore
You can create a new datastore from the web GUI, by navigating to **Datastore** in
the menu tree and clicking **Create**. Here:
You can create a new datastore from the web interface, by clicking **Add
Datastore** in the side menu, under the **Datastore** section. In the setup
window:
* *Name* refers to the name of the datastore
* *Backing Path* is the path to the directory upon which you want to create the
@ -136,7 +137,9 @@ the menu tree and clicking **Create**. Here:
* *GC Schedule* refers to the time and intervals at which garbage collection
runs
* *Prune Schedule* refers to the frequency at which pruning takes place
* *Prune Options* set the amount of backups which you would like to keep (see :ref:`backup-pruning`).
* *Prune Options* set the amount of backups which you would like to keep (see
:ref:`backup-pruning`).
* *Comment* can be used to add some contextual information to the datastore.
Alternatively you can create a new datastore from the command line. The
following command creates a new datastore called ``store1`` on :file:`/backup/disk1/store1`

View File

@ -41,11 +41,12 @@ users:
:alt: Add a new user
The superuser has full administration rights on everything, so you
normally want to add other users with less privileges. You can create a new
user with the ``user create`` subcommand or through the web interface, under
**Configuration -> User Management**. The ``create`` subcommand lets you specify
many options like ``--email`` or ``--password``. You can update or change any
user properties using the ``update`` subcommand later (**Edit** in the GUI):
normally want to add other users with less privileges. You can add a new
user with the ``user create`` subcommand or through the web
interface, under the **User Management** tab of **Configuration -> Access
Control**. The ``create`` subcommand lets you specify many options like
``--email`` or ``--password``. You can update or change any user properties
using the ``update`` subcommand later (**Edit** in the GUI):
.. code-block:: console
@ -90,6 +91,10 @@ Or completely remove the user with:
API Tokens
----------
.. image:: images/screenshots/pbs-gui-apitoken-overview.png
:align: right
:alt: API Token Overview
Any authenticated user can generate API tokens which can in turn be used to
configure various clients, instead of directly providing the username and
password.
@ -104,6 +109,10 @@ the realm and a tokenname (``user@realm!tokenname``), and a secret value. Both
need to be provided to the client in place of the user ID (``user@realm``) and
the user password, respectively.
.. image:: images/screenshots/pbs-gui-apitoken-secret-value.png
:align: right
:alt: API secret value
The API token is passed from the client to the server by setting the
``Authorization`` HTTP header with method ``PBSAPIToken`` to the value
``TOKENID:TOKENSECRET``.
@ -184,7 +193,7 @@ following roles exist:
**RemoteSyncOperator**
Is allowed to read data from a remote.
.. image:: images/screenshots/pbs-gui-permissions-add.png
.. image:: images/screenshots/pbs-gui-user-management-add-user.png
:align: right
:alt: Add permissions for user

View File

@ -75,7 +75,7 @@ pub struct UserWithTokens {
pub lastname: Option<String>,
#[serde(skip_serializing_if="Option::is_none")]
pub email: Option<String>,
#[serde(skip_serializing_if="Vec::is_empty")]
#[serde(skip_serializing_if="Vec::is_empty", default)]
pub tokens: Vec<user::ApiToken>,
}

View File

@ -902,15 +902,7 @@ pub fn garbage_collection_status(
type: Array,
items: {
description: "Datastore name and description.",
properties: {
store: {
schema: DATASTORE_SCHEMA,
},
comment: {
optional: true,
schema: SINGLE_LINE_COMMENT_SCHEMA,
},
},
type: DataStoreListItem,
},
},
access: {
@ -922,7 +914,7 @@ fn get_datastore_list(
_param: Value,
_info: &ApiMethod,
rpcenv: &mut dyn RpcEnvironment,
) -> Result<Value, Error> {
) -> Result<Vec<DataStoreListItem>, Error> {
let (config, _digest) = datastore::config()?;
@ -935,11 +927,12 @@ fn get_datastore_list(
let user_privs = user_info.lookup_privs(&auth_id, &["datastore", &store]);
let allowed = (user_privs & (PRIV_DATASTORE_AUDIT| PRIV_DATASTORE_BACKUP)) != 0;
if allowed {
let mut entry = json!({ "store": store });
if let Some(comment) = data["comment"].as_str() {
entry["comment"] = comment.into();
}
list.push(entry);
list.push(
DataStoreListItem {
store: store.clone(),
comment: data["comment"].as_str().map(String::from),
}
);
}
}

View File

@ -2,11 +2,16 @@ use anyhow::{format_err, Error};
use proxmox::api::router::SubdirMap;
use proxmox::{list_subdirs_api_method, sortable};
use proxmox::api::{api, ApiMethod, Router, RpcEnvironment};
use proxmox::api::{api, ApiMethod, Permission, Router, RpcEnvironment};
use crate::api2::types::*;
use crate::server::do_verification_job;
use crate::server::jobstate::{Job, JobState};
use crate::config::acl::{
PRIV_DATASTORE_AUDIT,
PRIV_DATASTORE_VERIFY,
};
use crate::config::cached_user_info::CachedUserInfo;
use crate::config::verify;
use crate::config::verify::{VerificationJobConfig, VerificationJobStatus};
use serde_json::Value;
@ -23,10 +28,14 @@ use crate::server::UPID;
},
},
returns: {
description: "List configured jobs and their status.",
description: "List configured jobs and their status (filtered by access)",
type: Array,
items: { type: verify::VerificationJobStatus },
},
access: {
permission: &Permission::Anybody,
description: "Requires Datastore.Audit or Datastore.Verify on datastore.",
},
)]
/// List all verification jobs
pub fn list_verification_jobs(
@ -34,6 +43,10 @@ pub fn list_verification_jobs(
_param: Value,
mut rpcenv: &mut dyn RpcEnvironment,
) -> Result<Vec<VerificationJobStatus>, Error> {
let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
let user_info = CachedUserInfo::new()?;
let required_privs = PRIV_DATASTORE_AUDIT | PRIV_DATASTORE_VERIFY;
let (config, digest) = verify::config()?;
@ -41,6 +54,11 @@ pub fn list_verification_jobs(
.convert_to_typed_array("verification")?
.into_iter()
.filter(|job: &VerificationJobStatus| {
let privs = user_info.lookup_privs(&auth_id, &["datastore", &job.store]);
if privs & required_privs == 0 {
return false;
}
if let Some(store) = &store {
&job.store == store
} else {
@ -90,7 +108,11 @@ pub fn list_verification_jobs(
schema: JOB_ID_SCHEMA,
}
}
}
},
access: {
permission: &Permission::Anybody,
description: "Requires Datastore.Verify on job's datastore.",
},
)]
/// Runs a verification job manually.
fn run_verification_job(
@ -98,10 +120,13 @@ fn run_verification_job(
_info: &ApiMethod,
rpcenv: &mut dyn RpcEnvironment,
) -> Result<String, Error> {
let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
let user_info = CachedUserInfo::new()?;
let (config, _digest) = verify::config()?;
let verification_job: VerificationJobConfig = config.lookup("verification", &id)?;
let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
user_info.check_privs(&auth_id, &["datastore", &verification_job.store], PRIV_DATASTORE_VERIFY, true)?;
let job = Job::new("verificationjob", &id)?;

View File

@ -5,6 +5,7 @@ use serde_json::Value;
use ::serde::{Deserialize, Serialize};
use proxmox::api::{api, Router, RpcEnvironment, Permission};
use proxmox::api::schema::parse_property_string;
use proxmox::tools::fs::open_file_locked;
use crate::api2::types::*;
@ -74,7 +75,7 @@ pub fn list_datastores(
},
"notify": {
optional: true,
type: Notify,
schema: DATASTORE_NOTIFY_STRING_SCHEMA,
},
"gc-schedule": {
optional: true,
@ -195,6 +196,8 @@ pub enum DeletableProperty {
keep_monthly,
/// Delete the keep-yearly property
keep_yearly,
/// Delete the verify-new property
verify_new,
/// Delete the notify-user property
notify_user,
/// Delete the notify property
@ -218,7 +221,7 @@ pub enum DeletableProperty {
},
"notify": {
optional: true,
type: Notify,
schema: DATASTORE_NOTIFY_STRING_SCHEMA,
},
"gc-schedule": {
optional: true,
@ -252,6 +255,12 @@ pub enum DeletableProperty {
optional: true,
schema: PRUNE_SCHEMA_KEEP_YEARLY,
},
"verify-new": {
description: "If enabled, all new backups will be verified right after completion.",
type: bool,
optional: true,
default: false,
},
delete: {
description: "List of properties to delete.",
type: Array,
@ -282,7 +291,8 @@ pub fn update_datastore(
keep_weekly: Option<u64>,
keep_monthly: Option<u64>,
keep_yearly: Option<u64>,
notify: Option<Notify>,
verify_new: Option<bool>,
notify: Option<String>,
notify_user: Option<Userid>,
delete: Option<Vec<DeletableProperty>>,
digest: Option<String>,
@ -312,6 +322,7 @@ pub fn update_datastore(
DeletableProperty::keep_weekly => { data.keep_weekly = None; },
DeletableProperty::keep_monthly => { data.keep_monthly = None; },
DeletableProperty::keep_yearly => { data.keep_yearly = None; },
DeletableProperty::verify_new => { data.verify_new = None; },
DeletableProperty::notify => { data.notify = None; },
DeletableProperty::notify_user => { data.notify_user = None; },
}
@ -346,7 +357,17 @@ pub fn update_datastore(
if keep_monthly.is_some() { data.keep_monthly = keep_monthly; }
if keep_yearly.is_some() { data.keep_yearly = keep_yearly; }
if notify.is_some() { data.notify = notify; }
if let Some(notify_str) = notify {
let value = parse_property_string(&notify_str, &DatastoreNotify::API_SCHEMA)?;
let notify: DatastoreNotify = serde_json::from_value(value)?;
if let DatastoreNotify { gc: None, verify: None, sync: None } = notify {
data.notify = None;
} else {
data.notify = Some(notify_str);
}
}
if verify_new.is_some() { data.verify_new = verify_new; }
if notify_user.is_some() { data.notify_user = notify_user; }
config.set_data(&name, "datastore", &data)?;

View File

@ -1,11 +1,13 @@
use anyhow::{bail, Error};
use anyhow::{bail, format_err, Error};
use serde_json::Value;
use ::serde::{Deserialize, Serialize};
use proxmox::api::{api, ApiMethod, Router, RpcEnvironment, Permission};
use proxmox::http_err;
use proxmox::tools::fs::open_file_locked;
use crate::api2::types::*;
use crate::client::{HttpClient, HttpClientOptions};
use crate::config::cached_user_info::CachedUserInfo;
use crate::config::remote;
use crate::config::acl::{PRIV_REMOTE_AUDIT, PRIV_REMOTE_MODIFY};
@ -301,10 +303,83 @@ pub fn delete_remote(name: String, digest: Option<String>) -> Result<(), Error>
Ok(())
}
/// Helper to get client for remote.cfg entry
pub async fn remote_client(remote: remote::Remote) -> Result<HttpClient, Error> {
let options = HttpClientOptions::new()
.password(Some(remote.password.clone()))
.fingerprint(remote.fingerprint.clone());
let client = HttpClient::new(
&remote.host,
remote.port.unwrap_or(8007),
&remote.userid,
options)?;
let _auth_info = client.login() // make sure we can auth
.await
.map_err(|err| format_err!("remote connection to '{}' failed - {}", remote.host, err))?;
Ok(client)
}
#[api(
input: {
properties: {
name: {
schema: REMOTE_ID_SCHEMA,
},
},
},
access: {
permission: &Permission::Privilege(&["remote", "{name}"], PRIV_REMOTE_AUDIT, false),
},
returns: {
description: "List the accessible datastores.",
type: Array,
items: {
description: "Datastore name and description.",
type: DataStoreListItem,
},
},
)]
/// List datastores of a remote.cfg entry
pub async fn scan_remote_datastores(name: String) -> Result<Vec<DataStoreListItem>, Error> {
let (remote_config, _digest) = remote::config()?;
let remote: remote::Remote = remote_config.lookup("remote", &name)?;
let map_remote_err = |api_err| {
http_err!(INTERNAL_SERVER_ERROR,
"failed to scan remote '{}' - {}",
&name,
api_err)
};
let client = remote_client(remote)
.await
.map_err(map_remote_err)?;
let api_res = client
.get("api2/json/admin/datastore", None)
.await
.map_err(map_remote_err)?;
let parse_res = match api_res.get("data") {
Some(data) => serde_json::from_value::<Vec<DataStoreListItem>>(data.to_owned()),
None => bail!("remote {} did not return any datastore list data", &name),
};
match parse_res {
Ok(parsed) => Ok(parsed),
Err(_) => bail!("Failed to parse remote scan api result."),
}
}
const SCAN_ROUTER: Router = Router::new()
.get(&API_METHOD_SCAN_REMOTE_DATASTORES);
const ITEM_ROUTER: Router = Router::new()
.get(&API_METHOD_READ_REMOTE)
.put(&API_METHOD_UPDATE_REMOTE)
.delete(&API_METHOD_DELETE_REMOTE);
.delete(&API_METHOD_DELETE_REMOTE)
.subdirs(&[("scan", &SCAN_ROUTER)]);
pub const ROUTER: Router = Router::new()
.get(&API_METHOD_LIST_REMOTES)

View File

@ -58,7 +58,7 @@ pub fn check_sync_job_modify_access(
&& owner.user() == auth_id.user())
},
// default sync owner
None => auth_id == Authid::backup_auth_id(),
None => auth_id == Authid::root_auth_id(),
};
// same permission as changing ownership after syncing
@ -511,7 +511,7 @@ acl:1:/remote/remote1/remotestore1:write@pbs:RemoteSyncOperator
job.owner = Some(read_auth_id.clone());
assert_eq!(check_sync_job_modify_access(&user_info, &write_auth_id, &job), false);
// also not to the default 'backup@pam'
// also not to the default 'root@pam'
job.owner = None;
assert_eq!(check_sync_job_modify_access(&user_info, &write_auth_id, &job), false);

View File

@ -9,10 +9,11 @@ use crate::api2::types::*;
use crate::config::acl::{
PRIV_DATASTORE_AUDIT,
PRIV_DATASTORE_BACKUP,
PRIV_DATASTORE_VERIFY,
};
use crate::config::cached_user_info::CachedUserInfo;
use crate::config::verify::{self, VerificationJobConfig};
#[api(
@ -25,10 +26,8 @@ use crate::config::verify::{self, VerificationJobConfig};
items: { type: verify::VerificationJobConfig },
},
access: {
permission: &Permission::Privilege(
&["datastore", "{store}"],
PRIV_DATASTORE_AUDIT | PRIV_DATASTORE_BACKUP | PRIV_DATASTORE_VERIFY,
true),
permission: &Permission::Anybody,
description: "Requires Datastore.Audit or Datastore.Verify on datastore.",
},
)]
/// List all verification jobs
@ -36,11 +35,22 @@ pub fn list_verification_jobs(
_param: Value,
mut rpcenv: &mut dyn RpcEnvironment,
) -> Result<Vec<VerificationJobConfig>, Error> {
let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
let user_info = CachedUserInfo::new()?;
let required_privs = PRIV_DATASTORE_AUDIT | PRIV_DATASTORE_VERIFY;
let (config, digest) = verify::config()?;
let list = config.convert_to_typed_array("verification")?;
let list = list.into_iter()
.filter(|job: &VerificationJobConfig| {
let privs = user_info.lookup_privs(&auth_id, &["datastore", &job.store]);
privs & required_privs != 00
}).collect();
rpcenv["digest"] = proxmox::tools::digest_to_hex(&digest).into();
Ok(list)
@ -76,19 +86,24 @@ pub fn list_verification_jobs(
}
},
access: {
permission: &Permission::Privilege(
&["datastore", "{store}"],
PRIV_DATASTORE_VERIFY,
true),
permission: &Permission::Anybody,
description: "Requires Datastore.Verify on job's datastore.",
},
)]
/// Create a new verification job.
pub fn create_verification_job(param: Value) -> Result<(), Error> {
let _lock = open_file_locked(verify::VERIFICATION_CFG_LOCKFILE, std::time::Duration::new(10, 0), true)?;
pub fn create_verification_job(
param: Value,
rpcenv: &mut dyn RpcEnvironment
) -> Result<(), Error> {
let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
let user_info = CachedUserInfo::new()?;
let verification_job: verify::VerificationJobConfig = serde_json::from_value(param.clone())?;
user_info.check_privs(&auth_id, &["datastore", &verification_job.store], PRIV_DATASTORE_VERIFY, false)?;
let _lock = open_file_locked(verify::VERIFICATION_CFG_LOCKFILE, std::time::Duration::new(10, 0), true)?;
let (mut config, _digest) = verify::config()?;
if let Some(_) = config.sections.get(&verification_job.id) {
@ -117,10 +132,8 @@ pub fn create_verification_job(param: Value) -> Result<(), Error> {
type: verify::VerificationJobConfig,
},
access: {
permission: &Permission::Privilege(
&["datastore", "{store}"],
PRIV_DATASTORE_AUDIT | PRIV_DATASTORE_BACKUP | PRIV_DATASTORE_VERIFY,
true),
permission: &Permission::Anybody,
description: "Requires Datastore.Audit or Datastore.Verify on job's datastore.",
},
)]
/// Read a verification job configuration.
@ -128,9 +141,16 @@ pub fn read_verification_job(
id: String,
mut rpcenv: &mut dyn RpcEnvironment,
) -> Result<VerificationJobConfig, Error> {
let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
let user_info = CachedUserInfo::new()?;
let (config, digest) = verify::config()?;
let verification_job = config.lookup("verification", &id)?;
let verification_job: verify::VerificationJobConfig = config.lookup("verification", &id)?;
let required_privs = PRIV_DATASTORE_AUDIT | PRIV_DATASTORE_VERIFY;
user_info.check_privs(&auth_id, &["datastore", &verification_job.store], required_privs, true)?;
rpcenv["digest"] = proxmox::tools::digest_to_hex(&digest).into();
Ok(verification_job)
@ -193,10 +213,8 @@ pub enum DeletableProperty {
},
},
access: {
permission: &Permission::Privilege(
&["datastore", "{store}"],
PRIV_DATASTORE_VERIFY,
true),
permission: &Permission::Anybody,
description: "Requires Datastore.Verify on job's datastore.",
},
)]
/// Update verification job config.
@ -209,7 +227,10 @@ pub fn update_verification_job(
schedule: Option<String>,
delete: Option<Vec<DeletableProperty>>,
digest: Option<String>,
rpcenv: &mut dyn RpcEnvironment,
) -> Result<(), Error> {
let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
let user_info = CachedUserInfo::new()?;
let _lock = open_file_locked(verify::VERIFICATION_CFG_LOCKFILE, std::time::Duration::new(10, 0), true)?;
@ -223,7 +244,10 @@ pub fn update_verification_job(
let mut data: verify::VerificationJobConfig = config.lookup("verification", &id)?;
if let Some(delete) = delete {
// check existing store
user_info.check_privs(&auth_id, &["datastore", &data.store], PRIV_DATASTORE_VERIFY, true)?;
if let Some(delete) = delete {
for delete_prop in delete {
match delete_prop {
DeletableProperty::IgnoreVerified => { data.ignore_verified = None; },
@ -243,7 +267,12 @@ pub fn update_verification_job(
}
}
if let Some(store) = store { data.store = store; }
if let Some(store) = store {
// check new store
user_info.check_privs(&auth_id, &["datastore", &store], PRIV_DATASTORE_VERIFY, true)?;
data.store = store;
}
if ignore_verified.is_some() { data.ignore_verified = ignore_verified; }
if outdated_after.is_some() { data.outdated_after = outdated_after; }
@ -270,19 +299,26 @@ pub fn update_verification_job(
},
},
access: {
permission: &Permission::Privilege(
&["datastore", "{store}"],
PRIV_DATASTORE_VERIFY,
true),
permission: &Permission::Anybody,
description: "Requires Datastore.Verify on job's datastore.",
},
)]
/// Remove a verification job configuration
pub fn delete_verification_job(id: String, digest: Option<String>) -> Result<(), Error> {
pub fn delete_verification_job(
id: String,
digest: Option<String>,
rpcenv: &mut dyn RpcEnvironment,
) -> Result<(), Error> {
let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
let user_info = CachedUserInfo::new()?;
let _lock = open_file_locked(verify::VERIFICATION_CFG_LOCKFILE, std::time::Duration::new(10, 0), true)?;
let (mut config, expected_digest) = verify::config()?;
let job: verify::VerificationJobConfig = config.lookup("verification", &id)?;
user_info.check_privs(&auth_id, &["datastore", &job.store], PRIV_DATASTORE_VERIFY, true)?;
if let Some(ref digest) = digest {
let digest = proxmox::tools::hex_to_digest(digest)?;
crate::tools::detect_modified_configuration_file(&digest, &expected_digest)?;

View File

@ -1,7 +1,7 @@
use std::fs::File;
use std::io::{BufRead, BufReader};
use anyhow::{Error};
use anyhow::{bail, Error};
use serde_json::{json, Value};
use proxmox::api::{api, Router, RpcEnvironment, Permission};
@ -9,19 +9,87 @@ use proxmox::api::router::SubdirMap;
use proxmox::{identity, list_subdirs_api_method, sortable};
use crate::tools;
use crate::api2::types::*;
use crate::api2::pull::check_pull_privs;
use crate::server::{self, UPID, TaskState, TaskListInfoIterator};
use crate::config::acl::{PRIV_SYS_AUDIT, PRIV_SYS_MODIFY};
use crate::config::acl::{
PRIV_DATASTORE_MODIFY,
PRIV_DATASTORE_VERIFY,
PRIV_SYS_AUDIT,
PRIV_SYS_MODIFY,
};
use crate::config::cached_user_info::CachedUserInfo;
// matches respective job execution privileges
fn check_job_privs(auth_id: &Authid, user_info: &CachedUserInfo, upid: &UPID) -> Result<(), Error> {
match (upid.worker_type.as_str(), &upid.worker_id) {
("verificationjob", Some(workerid)) => {
if let Some(captures) = VERIFICATION_JOB_WORKER_ID_REGEX.captures(&workerid) {
if let Some(store) = captures.get(1) {
return user_info.check_privs(&auth_id,
&["datastore", store.as_str()],
PRIV_DATASTORE_VERIFY,
true);
}
}
},
("syncjob", Some(workerid)) => {
if let Some(captures) = SYNC_JOB_WORKER_ID_REGEX.captures(&workerid) {
let remote = captures.get(1);
let remote_store = captures.get(2);
let local_store = captures.get(3);
if let (Some(remote), Some(remote_store), Some(local_store)) =
(remote, remote_store, local_store) {
return check_pull_privs(&auth_id,
local_store.as_str(),
remote.as_str(),
remote_store.as_str(),
false);
}
}
},
("garbage_collection", Some(workerid)) => {
return user_info.check_privs(&auth_id,
&["datastore", &workerid],
PRIV_DATASTORE_MODIFY,
true)
},
("prune", Some(workerid)) => {
return user_info.check_privs(&auth_id,
&["datastore",
&workerid],
PRIV_DATASTORE_MODIFY,
true);
},
_ => bail!("not a scheduled job task"),
};
bail!("not a scheduled job task");
}
fn check_task_access(auth_id: &Authid, upid: &UPID) -> Result<(), Error> {
let task_auth_id = &upid.auth_id;
if auth_id == task_auth_id
|| (task_auth_id.is_token() && &Authid::from(task_auth_id.user().clone()) == auth_id) {
// task owner can always read
Ok(())
} else {
let user_info = CachedUserInfo::new()?;
user_info.check_privs(auth_id, &["system", "tasks"], PRIV_SYS_AUDIT, false)
let task_privs = user_info.lookup_privs(auth_id, &["system", "tasks"]);
if task_privs & PRIV_SYS_AUDIT != 0 {
// allowed to read all tasks in general
Ok(())
} else if check_job_privs(&auth_id, &user_info, upid).is_ok() {
// job which the user/token could have configured/manually executed
Ok(())
} else {
bail!("task access not allowed");
}
}
}

View File

@ -9,7 +9,7 @@ use proxmox::api::{ApiMethod, Router, RpcEnvironment, Permission};
use crate::server::{WorkerTask, jobstate::Job};
use crate::backup::DataStore;
use crate::client::{HttpClient, HttpClientOptions, BackupRepository, pull::pull_store};
use crate::client::{HttpClient, BackupRepository, pull::pull_store};
use crate::api2::types::*;
use crate::config::{
remote,
@ -50,17 +50,9 @@ pub async fn get_pull_parameters(
let (remote_config, _digest) = remote::config()?;
let remote: remote::Remote = remote_config.lookup("remote", remote)?;
let options = HttpClientOptions::new()
.password(Some(remote.password.clone()))
.fingerprint(remote.fingerprint.clone());
let src_repo = BackupRepository::new(Some(remote.userid.clone()), Some(remote.host.clone()), remote.port, remote_store.to_string());
let client = HttpClient::new(&src_repo.host(), src_repo.port(), &src_repo.auth_id(), options)?;
let _auth_info = client.login() // make sure we can auth
.await
.map_err(|err| format_err!("remote connection to '{}' failed - {}", remote.host, err))?;
let client = crate::api2::config::remote::remote_client(remote).await?;
Ok((client, src_repo, tgt_store))
}
@ -72,14 +64,18 @@ pub fn do_sync_job(
schedule: Option<String>,
) -> Result<String, Error> {
let job_id = job.jobname().to_string();
let job_id = format!("{}:{}:{}:{}",
sync_job.remote,
sync_job.remote_store,
sync_job.store,
job.jobname());
let worker_type = job.jobtype().to_string();
let (email, notify) = crate::server::lookup_datastore_notify_settings(&sync_job.store);
let upid_str = WorkerTask::spawn(
&worker_type,
Some(job.jobname().to_string()),
Some(job_id.clone()),
auth_id.clone(),
false,
move |worker| async move {
@ -92,7 +88,7 @@ pub fn do_sync_job(
let worker_future = async move {
let delete = sync_job.remove_vanished.unwrap_or(true);
let sync_owner = sync_job.owner.unwrap_or(Authid::backup_auth_id().clone());
let sync_owner = sync_job.owner.unwrap_or(Authid::root_auth_id().clone());
let (client, src_repo, tgt_store) = get_pull_parameters(&sync_job.store, &sync_job.remote, &sync_job.remote_store).await?;
worker.log(format!("Starting datastore sync job '{}'", job_id));

View File

@ -59,6 +59,11 @@ const_regex!{
/// any identifier command line tools work with.
pub PROXMOX_SAFE_ID_REGEX = concat!(r"^", PROXMOX_SAFE_ID_REGEX_STR!(), r"$");
/// Regex for verification jobs 'DATASTORE:ACTUAL_JOB_ID'
pub VERIFICATION_JOB_WORKER_ID_REGEX = concat!(r"^(", PROXMOX_SAFE_ID_REGEX_STR!(), r"):");
/// Regex for sync jobs 'REMOTE:REMOTE_DATASTORE:LOCAL_DATASTORE:ACTUAL_JOB_ID'
pub SYNC_JOB_WORKER_ID_REGEX = concat!(r"^(", PROXMOX_SAFE_ID_REGEX_STR!(), r"):(", PROXMOX_SAFE_ID_REGEX_STR!(), r"):(", PROXMOX_SAFE_ID_REGEX_STR!(), r"):");
pub SINGLE_LINE_COMMENT_REGEX = r"^[[:^cntrl:]]*$";
pub HOSTNAME_REGEX = r"^(?:[a-zA-Z0-9](?:[a-zA-Z0-9\-]*[a-zA-Z0-9])?)$";
@ -370,6 +375,25 @@ pub const BLOCKDEVICE_NAME_SCHEMA: Schema = StringSchema::new("Block device name
// Complex type definitions
#[api(
properties: {
store: {
schema: DATASTORE_SCHEMA,
},
comment: {
optional: true,
schema: SINGLE_LINE_COMMENT_SCHEMA,
},
},
)]
#[derive(Serialize, Deserialize)]
#[serde(rename_all="kebab-case")]
/// Basic information about a datastore.
pub struct DataStoreListItem {
pub store: String,
pub comment: Option<String>,
}
#[api(
properties: {
"backup-type": {
@ -1167,3 +1191,35 @@ pub enum Notify {
/// Send notifications for failed jobs only
Error,
}
#[api(
properties: {
gc: {
type: Notify,
optional: true,
},
verify: {
type: Notify,
optional: true,
},
sync: {
type: Notify,
optional: true,
},
},
)]
#[derive(Debug, Serialize, Deserialize)]
/// Datastore notify settings
pub struct DatastoreNotify {
/// Garbage collection settings
pub gc: Option<Notify>,
/// Verify job setting
pub verify: Option<Notify>,
/// Sync job setting
pub sync: Option<Notify>,
}
pub const DATASTORE_NOTIFY_STRING_SCHEMA: Schema = StringSchema::new(
"Datastore notification setting")
.format(&ApiStringFormat::PropertyString(&DatastoreNotify::API_SCHEMA))
.schema();

View File

@ -450,11 +450,6 @@ impl Userid {
&self.data
}
/// Get the "backup@pam" user id.
pub fn backup_userid() -> &'static Self {
&*BACKUP_USERID
}
/// Get the "root@pam" user id.
pub fn root_userid() -> &'static Self {
&*ROOT_USERID
@ -462,7 +457,6 @@ impl Userid {
}
lazy_static! {
pub static ref BACKUP_USERID: Userid = Userid::new("backup@pam".to_string(), 6);
pub static ref ROOT_USERID: Userid = Userid::new("root@pam".to_string(), 4);
}
@ -596,11 +590,6 @@ impl Authid {
}
}
/// Get the "backup@pam" auth id.
pub fn backup_auth_id() -> &'static Self {
&*BACKUP_AUTHID
}
/// Get the "root@pam" auth id.
pub fn root_auth_id() -> &'static Self {
&*ROOT_AUTHID
@ -608,7 +597,6 @@ impl Authid {
}
lazy_static! {
pub static ref BACKUP_AUTHID: Authid = Authid::from(Userid::new("backup@pam".to_string(), 6));
pub static ref ROOT_AUTHID: Authid = Authid::from(Userid::new("root@pam".to_string(), 4));
}

View File

@ -32,11 +32,11 @@ use proxmox::{
use pxar::accessor::{MaybeReady, ReadAt, ReadAtOperation};
use proxmox_backup::tools;
use proxmox_backup::api2::access::user::UserWithTokens;
use proxmox_backup::api2::types::*;
use proxmox_backup::api2::version;
use proxmox_backup::client::*;
use proxmox_backup::pxar::catalog::*;
use proxmox_backup::config::user::complete_userid;
use proxmox_backup::backup::{
archive_type,
decrypt_key,
@ -1904,6 +1904,33 @@ fn complete_chunk_size(_arg: &str, _param: &HashMap<String, String>) -> Vec<Stri
result
}
fn complete_auth_id(_arg: &str, param: &HashMap<String, String>) -> Vec<String> {
proxmox_backup::tools::runtime::main(async { complete_auth_id_do(param).await })
}
async fn complete_auth_id_do(param: &HashMap<String, String>) -> Vec<String> {
let mut result = vec![];
let repo = match extract_repository_from_map(param) {
Some(v) => v,
_ => return result,
};
let data = try_get(&repo, "api2/json/access/users?include_tokens=true").await;
if let Ok(parsed) = serde_json::from_value::<Vec<UserWithTokens>>(data) {
for user in parsed {
result.push(user.userid.to_string());
for token in user.tokens {
result.push(token.tokenid.to_string());
}
}
};
result
}
use proxmox_backup::client::RemoteChunkReader;
/// This is a workaround until we have cleaned up the chunk/reader/... infrastructure for better
/// async use!
@ -2013,7 +2040,7 @@ fn main() {
let change_owner_cmd_def = CliCommand::new(&API_METHOD_CHANGE_BACKUP_OWNER)
.arg_param(&["group", "new-owner"])
.completion_cb("group", complete_backup_group)
.completion_cb("new-owner", complete_userid)
.completion_cb("new-owner", complete_auth_id)
.completion_cb("repository", complete_repository);
let cmd_def = CliCommandMap::new()

View File

@ -413,29 +413,13 @@ pub fn complete_remote_datastore_name(_arg: &str, param: &HashMap<String, String
let _ = proxmox::try_block!({
let remote = param.get("remote").ok_or_else(|| format_err!("no remote"))?;
let (remote_config, _digest) = config::remote::config()?;
let remote: config::remote::Remote = remote_config.lookup("remote", &remote)?;
let data = crate::tools::runtime::block_on(async move {
crate::api2::config::remote::scan_remote_datastores(remote.clone()).await
})?;
let options = HttpClientOptions::new()
.password(Some(remote.password.clone()))
.fingerprint(remote.fingerprint.clone());
let client = HttpClient::new(
&remote.host,
remote.port.unwrap_or(8007),
&remote.userid,
options,
)?;
let result = crate::tools::runtime::block_on(client.get("api2/json/admin/datastore", None))?;
if let Some(data) = result["data"].as_array() {
for item in data {
if let Some(store) = item["store"].as_str() {
list.push(store.to_owned());
}
}
for item in data {
list.push(item.store);
}
Ok(())

View File

@ -90,7 +90,6 @@ async fn run() -> Result<(), Error> {
config.add_alias("xtermjs", "/usr/share/pve-xtermjs");
config.add_alias("locale", "/usr/share/pbs-i18n");
config.add_alias("widgettoolkit", "/usr/share/javascript/proxmox-widget-toolkit");
config.add_alias("css", "/usr/share/javascript/proxmox-backup/css");
config.add_alias("docs", "/usr/share/doc/proxmox-backup/html");
let mut indexpath = PathBuf::from(buildcfg::JS_DIR);
@ -377,7 +376,7 @@ async fn schedule_datastore_garbage_collection() {
Err(_) => continue, // could not get lock
};
let auth_id = Authid::backup_auth_id();
let auth_id = Authid::root_auth_id();
if let Err(err) = crate::server::do_garbage_collection_job(job, datastore, auth_id, Some(event_str), false) {
eprintln!("unable to start garbage collection job on datastore {} - {}", store, err);
@ -440,7 +439,7 @@ async fn schedule_datastore_prune() {
Err(_) => continue, // could not get lock
};
let auth_id = Authid::backup_auth_id().clone();
let auth_id = Authid::root_auth_id().clone();
if let Err(err) = do_prune_job(job, prune_options, store.clone(), &auth_id, Some(event_str)) {
eprintln!("unable to start datastore prune job {} - {}", &store, err);
}
@ -484,7 +483,7 @@ async fn schedule_datastore_sync_jobs() {
Err(_) => continue, // could not get lock
};
let auth_id = Authid::backup_auth_id().clone();
let auth_id = Authid::root_auth_id().clone();
if let Err(err) = do_sync_job(job, job_config, &auth_id, Some(event_str)) {
eprintln!("unable to start datastore sync job {} - {}", &job_id, err);
}
@ -520,7 +519,7 @@ async fn schedule_datastore_verify_jobs() {
};
let worker_type = "verificationjob";
let auth_id = Authid::backup_auth_id().clone();
let auth_id = Authid::root_auth_id().clone();
if check_schedule(worker_type, &event_str, &job_id) {
let job = match Job::new(&worker_type, &job_id) {
Ok(job) => job,
@ -560,7 +559,7 @@ async fn schedule_task_log_rotate() {
if let Err(err) = WorkerTask::new_thread(
worker_type,
None,
Authid::backup_auth_id().clone(),
Authid::root_auth_id().clone(),
false,
move |worker| {
job.start(&worker.upid().to_string())?;

View File

@ -38,7 +38,7 @@ pub const DIR_NAME_SCHEMA: Schema = StringSchema::new("Directory name").schema()
},
"notify": {
optional: true,
type: Notify,
schema: DATASTORE_NOTIFY_STRING_SCHEMA,
},
comment: {
optional: true,
@ -114,7 +114,7 @@ pub struct DataStoreConfig {
pub notify_user: Option<Userid>,
/// Send notification only for job errors
#[serde(skip_serializing_if="Option::is_none")]
pub notify: Option<Notify>,
pub notify: Option<String>,
}
fn init() -> SectionConfig {

View File

@ -657,7 +657,12 @@ impl<'a, 'b> Archiver<'a, 'b> {
let mut remaining = file_size;
let mut out = encoder.create_file(metadata, file_name, file_size)?;
while remaining != 0 {
let mut got = file.read(&mut self.file_copy_buffer[..])?;
let mut got = match file.read(&mut self.file_copy_buffer[..]) {
Ok(0) => break,
Ok(got) => got,
Err(err) if err.kind() == std::io::ErrorKind::Interrupted => continue,
Err(err) => bail!(err),
};
if got as u64 > remaining {
self.report_file_grew_while_reading()?;
got = remaining as usize;

View File

@ -4,6 +4,7 @@ use serde_json::json;
use handlebars::{Handlebars, Helper, Context, RenderError, RenderContext, Output, HelperResult};
use proxmox::tools::email::sendmail;
use proxmox::api::schema::parse_property_string;
use crate::{
config::datastore::DataStoreConfig,
@ -14,6 +15,7 @@ use crate::{
GarbageCollectionStatus,
Userid,
Notify,
DatastoreNotify,
},
tools::format::HumanByte,
};
@ -190,14 +192,19 @@ fn send_job_status_mail(
pub fn send_gc_status(
email: &str,
notify: Notify,
notify: DatastoreNotify,
datastore: &str,
status: &GarbageCollectionStatus,
result: &Result<(), Error>,
) -> Result<(), Error> {
if notify == Notify::Never || (result.is_ok() && notify == Notify::Error) {
return Ok(());
match notify.gc {
None => { /* send notifications by default */ },
Some(notify) => {
if notify == Notify::Never || (result.is_ok() && notify == Notify::Error) {
return Ok(());
}
}
}
let (fqdn, port) = get_server_url();
@ -244,15 +251,11 @@ pub fn send_gc_status(
pub fn send_verify_status(
email: &str,
notify: Notify,
notify: DatastoreNotify,
job: VerificationJobConfig,
result: &Result<Vec<String>, Error>,
) -> Result<(), Error> {
if notify == Notify::Never || (result.is_ok() && notify == Notify::Error) {
return Ok(());
}
let (fqdn, port) = get_server_url();
let mut data = json!({
"job": job,
@ -260,8 +263,11 @@ pub fn send_verify_status(
"port": port,
});
let mut result_is_ok = false;
let text = match result {
Ok(errors) if errors.is_empty() => {
result_is_ok = true;
HANDLEBARS.render("verify_ok_template", &data)?
}
Ok(errors) => {
@ -274,6 +280,15 @@ pub fn send_verify_status(
}
};
match notify.verify {
None => { /* send notifications by default */ },
Some(notify) => {
if notify == Notify::Never || (result_is_ok && notify == Notify::Error) {
return Ok(());
}
}
}
let subject = match result {
Ok(errors) if errors.is_empty() => format!(
"Verify Datastore '{}' successful",
@ -292,13 +307,18 @@ pub fn send_verify_status(
pub fn send_sync_status(
email: &str,
notify: Notify,
notify: DatastoreNotify,
job: &SyncJobConfig,
result: &Result<(), Error>,
) -> Result<(), Error> {
if notify == Notify::Never || (result.is_ok() && notify == Notify::Error) {
return Ok(());
match notify.sync {
None => { /* send notifications by default */ },
Some(notify) => {
if notify == Notify::Never || (result.is_ok() && notify == Notify::Error) {
return Ok(());
}
}
}
let (fqdn, port) = get_server_url();
@ -377,16 +397,10 @@ pub fn send_updates_available(
}
/// Lookup users email address
///
/// For "backup@pam", this returns the address from "root@pam".
fn lookup_user_email(userid: &Userid) -> Option<String> {
use crate::config::user::{self, User};
if userid == Userid::backup_userid() {
return lookup_user_email(Userid::root_userid());
}
if let Ok(user_config) = user::cached_config() {
if let Ok(user) = user_config.lookup::<User>("user", userid.as_str()) {
return user.email.clone();
@ -399,11 +413,12 @@ fn lookup_user_email(userid: &Userid) -> Option<String> {
/// Lookup Datastore notify settings
pub fn lookup_datastore_notify_settings(
store: &str,
) -> (Option<String>, Notify) {
) -> (Option<String>, DatastoreNotify) {
let mut notify = Notify::Always;
let mut email = None;
let notify = DatastoreNotify { gc: None, verify: None, sync: None };
let (config, _digest) = match crate::config::datastore::config() {
Ok(result) => result,
Err(_) => return (email, notify),
@ -416,11 +431,15 @@ pub fn lookup_datastore_notify_settings(
email = match config.notify_user {
Some(ref userid) => lookup_user_email(userid),
None => lookup_user_email(Userid::backup_userid()),
None => lookup_user_email(Userid::root_userid()),
};
if let Some(value) = config.notify {
notify = value;
let notify_str = config.notify.unwrap_or(String::new());
if let Ok(value) = parse_property_string(&notify_str, &DatastoreNotify::API_SCHEMA) {
if let Ok(notify) = serde_json::from_value(value) {
return (email, notify);
}
}
(email, notify)

View File

@ -50,11 +50,13 @@ pub fn do_verification_job(
let (email, notify) = crate::server::lookup_datastore_notify_settings(&verification_job.store);
let job_id = job.jobname().to_string();
let job_id = format!("{}:{}",
&verification_job.store,
job.jobname());
let worker_type = job.jobtype().to_string();
let upid_str = WorkerTask::new_thread(
&worker_type,
Some(job.jobname().to_string()),
Some(job_id.clone()),
auth_id.clone(),
false,
move |worker| {

View File

@ -12,6 +12,7 @@ use std::task::{Context, Poll};
use std::path::PathBuf;
use anyhow::{bail, format_err, Error};
use futures::future::{self, Either};
use proxmox::tools::io::{ReadExt, WriteExt};
@ -262,7 +263,7 @@ pub async fn create_daemon<F, S>(
) -> Result<(), Error>
where
F: FnOnce(tokio::net::TcpListener, NotifyReady) -> Result<S, Error>,
S: Future<Output = ()>,
S: Future<Output = ()> + Unpin,
{
let mut reloader = Reloader::new()?;
@ -271,11 +272,19 @@ where
move || async move { Ok(tokio::net::TcpListener::bind(&address).await?) },
).await?;
create_service(listener, NotifyReady)?.await;
let server_future = create_service(listener, NotifyReady)?;
let shutdown_future = server::shutdown_future();
let finish_future = match future::select(server_future, shutdown_future).await {
Either::Left((_, _)) => {
crate::tools::request_shutdown(); // make sure we are in shutdown mode
None
}
Either::Right((_, server_future)) => Some(server_future),
};
let mut reloader = Some(reloader);
crate::tools::request_shutdown(); // make sure we are in shutdown mode
if server::is_reload_request() {
log::info!("daemon reload...");
if let Err(e) = systemd_notify(SystemdNotify::Reloading) {
@ -288,6 +297,11 @@ where
} else {
log::info!("daemon shutting down...");
}
if let Some(future) = finish_future {
future.await;
}
log::info!("daemon shut down...");
Ok(())
}

View File

@ -5,8 +5,10 @@ IMAGES := \
images/proxmox_logo.png
JSSRC= \
Utils.js \
form/UserSelector.js \
form/TokenSelector.js \
form/AuthidSelector.js \
form/RemoteSelector.js \
form/DataStoreSelector.js \
form/CalendarEvent.js \
@ -21,11 +23,13 @@ JSSRC= \
config/VerifyView.js \
window/ACLEdit.js \
window/BackupFileDownloader.js \
window/BackupGroupChangeOwner.js \
window/CreateDirectory.js \
window/DataStoreEdit.js \
window/FileBrowser.js \
window/NotesEdit.js \
window/RemoteEdit.js \
window/NotifyOptions.js \
window/SyncJobEdit.js \
window/UserEdit.js \
window/UserPassword.js \
@ -38,20 +42,20 @@ JSSRC= \
dashboard/TaskSummary.js \
panel/Tasks.js \
panel/XtermJsConsole.js \
Utils.js \
AccessControlPanel.js \
panel/AccessControl.js \
ZFSList.js \
DirectoryList.js \
LoginView.js \
VersionInfo.js \
SystemConfiguration.js \
Subscription.js \
DataStoreSummary.js \
DataStoreNotes.js \
DataStorePruneAndGC.js \
DataStorePrune.js \
DataStoreContent.js \
DataStorePanel.js \
datastore/Summary.js \
datastore/Notes.js \
datastore/PruneAndGC.js \
datastore/Prune.js \
datastore/Content.js \
datastore/OptionView.js \
datastore/Panel.js \
ServerStatus.js \
ServerAdministration.js \
Dashboard.js \

View File

@ -11,6 +11,14 @@ const proxmoxOnlineHelpInfo = {
"link": "/docs/sysadmin.html#chapter-zfs",
"title": "ZFS on Linux"
},
"maintenance-pruning": {
"link": "/docs/maintenance.html#maintenance-pruning",
"title": "Pruning"
},
"maintenance-notification": {
"link": "/docs/maintenance.html#maintenance-notification",
"title": "Notifications"
},
"backup-remote": {
"link": "/docs/managing-remotes.html#backup-remote",
"title": ":term:`Remote`"

View File

@ -58,6 +58,81 @@ Ext.define('PBS.Utils', {
return path.indexOf(PBS.Utils.dataStorePrefix) === 0;
},
parsePropertyString: function(value, defaultKey) {
var res = {},
error;
if (typeof value !== 'string' || value === '') {
return res;
}
Ext.Array.each(value.split(','), function(p) {
var kv = p.split('=', 2);
if (Ext.isDefined(kv[1])) {
res[kv[0]] = kv[1];
} else if (Ext.isDefined(defaultKey)) {
if (Ext.isDefined(res[defaultKey])) {
error = 'defaultKey may be only defined once in propertyString';
return false; // break
}
res[defaultKey] = kv[0];
} else {
error = 'invalid propertyString, not a key=value pair and no defaultKey defined';
return false; // break
}
return true;
});
if (error !== undefined) {
console.error(error);
return null;
}
return res;
},
printPropertyString: function(data, defaultKey) {
var stringparts = [],
gotDefaultKeyVal = false,
defaultKeyVal;
Ext.Object.each(data, function(key, value) {
if (defaultKey !== undefined && key === defaultKey) {
gotDefaultKeyVal = true;
defaultKeyVal = value;
} else if (value !== '' && value !== undefined) {
stringparts.push(key + '=' + value);
}
});
stringparts = stringparts.sort();
if (gotDefaultKeyVal) {
stringparts.unshift(defaultKeyVal);
}
return stringparts.join(',');
},
// helper for deleting field which are set to there default values
delete_if_default: function(values, fieldname, default_val, create) {
if (values[fieldname] === '' || values[fieldname] === default_val) {
if (!create) {
if (values.delete) {
if (Ext.isArray(values.delete)) {
values.delete.push(fieldname);
} else {
values.delete += ',' + fieldname;
}
} else {
values.delete = [fieldname];
}
}
delete values[fieldname];
}
},
render_datetime_utc: function(datetime) {
let pad = (number) => number < 10 ? '0' + number : number;
return datetime.getUTCFullYear() +

View File

@ -268,6 +268,25 @@ Ext.define('PBS.DataStoreContent', {
}
},
onChangeOwner: function(view, rI, cI, item, e, rec) {
view = this.getView();
if (!rec || !rec.data || rec.parentNode.id !== 'root' || !view.datastore) {
return;
}
let data = rec.data;
let win = Ext.create('PBS.BackupGroupChangeOwner', {
datastore: view.datastore,
backup_type: data.backup_type,
backup_id: data.backup_id,
owner: data.owner,
autoShow: true,
});
win.on('destroy', this.reload, this);
},
onPrune: function(view, rI, cI, item, e, rec) {
view = this.getView();
@ -582,7 +601,13 @@ Ext.define('PBS.DataStoreContent', {
getTip: (v, m, rec) => Ext.String.format(gettext("Verify '{0}'"), v),
getClass: (v, m, rec) => rec.data.leaf ? 'pmx-hidden' : 'pve-icon-verify-lettering',
isDisabled: (v, r, c, i, rec) => !!rec.data.leaf,
},
},
{
handler: 'onChangeOwner',
getClass: (v, m, rec) => rec.parentNode.id ==='root' ? 'fa fa-user' : 'pmx-hidden',
getTip: (v, m, rec) => Ext.String.format(gettext("Change owner of '{0}'"), v),
isDisabled: (v, r, c, i, rec) => rec.parentNode.id !=='root',
},
{
handler: 'onPrune',
getTip: (v, m, rec) => Ext.String.format(gettext("Prune '{0}'"), v),

View File

@ -0,0 +1,87 @@
Ext.define('PBS.Datastore.Options', {
extend: 'Proxmox.grid.ObjectGrid',
xtype: 'pbsDatastoreOptionView',
mixins: ['Proxmox.Mixin.CBind'],
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}`,
datastore: me.datastore,
};
return {};
},
controller: {
xclass: 'Ext.app.ViewController',
edit: function() {
this.getView().run_editor();
},
},
tbar: [
{
xtype: 'proxmoxButton',
text: gettext('Edit'),
disabled: true,
handler: 'edit',
},
],
listeners: {
activate: function() { this.rstore.startUpdate(); },
destroy: function() { this.rstore.stopUpdate(); },
deactivate: function() { this.rstore.stopUpdate(); },
itemdblclick: 'edit',
},
rows: {
"notify": {
required: true,
header: gettext('Notfiy'),
renderer: (value) => {
let notify = PBS.Utils.parsePropertyString(value);
let res = [];
for (const k of ['Verify', 'Sync', 'GC']) {
let v = Ext.String.capitalize(notify[k.toLowerCase()]) || 'Always';
res.push(`${k}=${v}`);
}
return res.join(', ');
},
editor: {
xtype: 'pbsNotifyOptionEdit',
},
},
"notify-user": {
required: true,
defaultValue: 'root@pam',
header: gettext('Notfiy User'),
editor: {
xtype: 'pbsNotifyOptionEdit',
},
},
"verify-new": {
required: true,
header: gettext('Verify New Snapshots'),
defaultValue: false,
renderer: Proxmox.Utils.format_boolean,
editor: {
xtype: 'proxmoxWindowEdit',
title: gettext('Verify New'),
width: 350,
items: {
xtype: 'proxmoxcheckbox',
name: 'verify-new',
boxLabel: gettext("Verify new backups immediately after completion"),
defaultValue: false,
deleteDefaultValue: true,
deleteEmpty: true,
},
},
},
},
});

View File

@ -77,6 +77,15 @@ Ext.define('PBS.DataStorePanel', {
datastore: '{datastore}',
},
},
{
xtype: 'pbsDatastoreOptionView',
itemId: 'options',
title: gettext('Options'),
iconCls: 'fa fa-cog',
cbind: {
datastore: '{datastore}',
},
},
{
itemId: 'acl',
xtype: 'pbsACLView',

View File

@ -3,6 +3,8 @@ Ext.define('PBS.DataStorePruneAndGC', {
alias: 'widget.pbsDataStorePruneAndGC',
mixins: ['Proxmox.Mixin.CBind'],
onlineHelp: 'maintenance_pruning',
cbindData: function(initial) {
let me = this;
@ -99,6 +101,7 @@ Ext.define('PBS.DataStorePruneAndGC', {
editor: {
xtype: 'proxmoxWindowEdit',
title: gettext('Prune Options'),
onlineHelp: 'maintenance_pruning',
items: {
xtype: 'pbsPruneInputPanel',
isCreate: false,
@ -111,6 +114,7 @@ Ext.define('PBS.DataStorePruneAndGC', {
editor: {
xtype: 'proxmoxWindowEdit',
title: gettext('Prune Options'),
onlineHelp: 'maintenance_pruning',
items: {
xtype: 'pbsPruneInputPanel',
},
@ -122,6 +126,7 @@ Ext.define('PBS.DataStorePruneAndGC', {
editor: {
xtype: 'proxmoxWindowEdit',
title: gettext('Prune Options'),
onlineHelp: 'maintenance_pruning',
items: {
xtype: 'pbsPruneInputPanel',
},
@ -133,6 +138,7 @@ Ext.define('PBS.DataStorePruneAndGC', {
editor: {
xtype: 'proxmoxWindowEdit',
title: gettext('Prune Options'),
onlineHelp: 'maintenance_pruning',
items: {
xtype: 'pbsPruneInputPanel',
},
@ -144,6 +150,7 @@ Ext.define('PBS.DataStorePruneAndGC', {
editor: {
xtype: 'proxmoxWindowEdit',
title: gettext('Prune Options'),
onlineHelp: 'maintenance_pruning',
items: {
xtype: 'pbsPruneInputPanel',
},
@ -155,6 +162,7 @@ Ext.define('PBS.DataStorePruneAndGC', {
editor: {
xtype: 'proxmoxWindowEdit',
title: gettext('Prune Options'),
onlineHelp: 'maintenance_pruning',
items: {
xtype: 'pbsPruneInputPanel',
},

View File

@ -0,0 +1,99 @@
Ext.define('pbs-authids', {
extend: 'Ext.data.Model',
fields: [
'authid', 'comment', 'type',
],
idProperty: 'authid',
});
Ext.define('PBS.form.AuthidSelector', {
extend: 'Proxmox.form.ComboGrid',
alias: 'widget.pbsAuthidSelector',
allowBlank: false,
autoSelect: false,
valueField: 'authid',
displayField: 'authid',
editable: true,
anyMatch: true,
forceSelection: true,
store: {
model: 'pbs-authids',
params: {
enabled: 1,
},
sorters: 'authid',
},
initComponent: function() {
let me = this;
me.userStore = Ext.create('Ext.data.Store', {
model: 'pbs-users-with-tokens',
});
me.userStore.on('load', this.onLoad, this);
me.userStore.load();
me.callParent();
},
onLoad: function(store, data, success) {
let me = this;
if (!success) return;
let records = [];
for (const rec of data) {
records.push({
authid: rec.data.userid,
comment: rec.data.comment,
type: 'u',
});
let tokens = rec.data.tokens || [];
for (const token of tokens) {
records.push({
authid: token.tokenid,
comment: token.comment,
type: 't',
});
}
}
me.store.loadData(records);
me.validate();
},
listConfig: {
width: 500,
columns: [
{
header: gettext('Type'),
sortable: true,
dataIndex: 'type',
renderer: function(value) {
switch (value) {
case 'u': return gettext('User');
case 't': return gettext('API Token');
default: return Proxmox.Utils.unknownText;
}
},
width: 80,
},
{
header: gettext('Auth ID'),
sortable: true,
dataIndex: 'authid',
renderer: Ext.String.htmlEncode,
flex: 2,
},
{
header: gettext('Comment'),
sortable: false,
dataIndex: 'comment',
renderer: Ext.String.htmlEncode,
flex: 3,
},
],
},
});

View File

@ -0,0 +1,48 @@
Ext.define('PBS.BackupGroupChangeOwner', {
extend: 'Proxmox.window.Edit',
alias: 'widget.pbsBackupGroupChangeOwner',
submitText: gettext("Change Owner"),
width: 350,
initComponent: function() {
let me = this;
if (!me.datastore) {
throw "no datastore specified";
}
if (!me.backup_type) {
throw "no backup_type specified";
}
if (!me.backup_id) {
throw "no backup_id specified";
}
Ext.apply(me, {
url: `/api2/extjs/admin/datastore/${me.datastore}/change-owner`,
method: 'POST',
subject: gettext("Change Owner") + ` - ${me.backup_type}/${me.backup_id}`,
items: {
xtype: 'inputpanel',
onGetValues: function(values) {
values["backup-type"] = me.backup_type;
values["backup-id"] = me.backup_id;
return values;
},
items: [
{
xtype: 'pbsAuthidSelector',
name: 'new-owner',
value: me.owner,
fieldLabel: gettext('New Owner'),
minLength: 8,
allowBlank: false,
},
],
},
});
me.callParent();
},
});

View File

@ -4,6 +4,8 @@ Ext.define('PBS.panel.PruneInputPanel', {
mixins: ['Proxmox.Mixin.CBind'],
onlineHelp: 'maintenance_pruning',
cbindData: function() {
let me = this;
me.isCreate = !!me.isCreate;

101
www/window/NotifyOptions.js Normal file
View File

@ -0,0 +1,101 @@
Ext.define('PBS.form.NotifyType', {
extend: 'Proxmox.form.KVComboBox',
alias: 'widget.pbsNotifyType',
comboItems: [
['__default__', gettext('Default (Always)')],
['always', gettext('Always')],
['error', gettext('Errors')],
['never', gettext('Never')],
],
});
Ext.define('PBS.window.NotifyOptions', {
extend: 'Proxmox.window.Edit',
xtype: 'pbsNotifyOptionEdit',
mixins: ['Proxmox.Mixin.CBind'],
onlineHelp: 'maintenance_notification',
user: undefined,
tokenname: undefined,
isAdd: false,
isCreate: false,
subject: gettext('Datastore Options'),
// hack to avoid that the trigger of the combogrid fields open on window show
defaultFocus: 'proxmoxHelpButton',
width: 450,
fieldDefaults: {
labelWidth: 120,
},
items: {
xtype: 'inputpanel',
onGetValues: function(values) {
let notify = {};
for (const k of ['verify', 'sync', 'gc']) {
notify[k] = values[k];
delete values[k];
}
values.notify = PBS.Utils.printPropertyString(notify);
PBS.Utils.delete_if_default(values, 'notify', '');
PBS.Utils.delete_if_default(values, 'notify-user', '');
return values;
},
items: [
{
xtype: 'pbsUserSelector',
name: 'notify-user',
fieldLabel: gettext('Notify User'),
emptyText: gettext('root@pam'),
value: null,
allowBlank: true,
renderer: Ext.String.htmlEncode,
deleteEmpty: true,
},
{
xtype: 'pbsNotifyType',
name: 'verify',
fieldLabel: gettext('Verification Jobs'),
value: '__default__',
deleteEmpty: false,
},
{
xtype: 'pbsNotifyType',
name: 'sync',
fieldLabel: gettext('Sync Jobs'),
value: '__default__',
deleteEmpty: false,
},
{
xtype: 'pbsNotifyType',
name: 'gc',
fieldLabel: gettext('Garbage Collection'),
value: '__default__',
deleteEmpty: false,
},
],
},
setValues: function(values) {
let me = this;
// we only handle a reduced set of options here
let options = {
'notify-user': values['notify-user'],
'verify-new': values['verify-new'],
};
let notify = {};
if (values.notify) {
notify = PBS.Utils.parsePropertyString(values.notify);
}
Object.assign(options, notify);
me.callParent([options]);
},
});

View File

@ -1,3 +1,90 @@
Ext.define('PBS.form.RemoteStoreSelector', {
extend: 'Proxmox.form.ComboGrid',
alias: 'widget.pbsRemoteStoreSelector',
queryMode: 'local',
valueField: 'store',
displayField: 'store',
notFoundIsValid: true,
matchFieldWidth: false,
listConfig: {
loadingText: gettext('Scanning...'),
width: 350,
columns: [
{
header: gettext('Datastore'),
sortable: true,
dataIndex: 'store',
renderer: Ext.String.htmlEncode,
flex: 1,
},
{
header: gettext('Comment'),
dataIndex: 'comment',
renderer: Ext.String.htmlEncode,
flex: 1,
},
],
},
doRawQuery: function() {
// do nothing.
},
setRemote: function(remote) {
let me = this;
if (me.remote === remote) {
return;
}
me.remote = remote;
let store = me.store;
store.removeAll();
if (me.remote) {
me.setDisabled(false);
if (!me.firstLoad) {
me.clearValue();
}
store.proxy.url = '/api2/json/config/remote/' + encodeURIComponent(me.remote) + '/scan';
store.load();
me.firstLoad = false;
} else {
me.setDisabled(true);
me.clearValue();
}
},
initComponent: function() {
let me = this;
me.firstLoad = true;
let store = Ext.create('Ext.data.Store', {
fields: ['store', 'comment'],
proxy: {
type: 'proxmox',
url: '/api2/json/config/remote/' + encodeURIComponent(me.remote) + '/scan',
},
});
store.sort('store', 'ASC');
Ext.apply(me, {
store: store,
});
me.callParent();
},
});
Ext.define('PBS.window.SyncJobEdit', {
extend: 'Proxmox.window.Edit',
alias: 'widget.pbsSyncJobEdit',
@ -25,6 +112,7 @@ Ext.define('PBS.window.SyncJobEdit', {
me.method = id ? 'PUT' : 'POST';
me.autoLoad = !!id;
me.scheduleValue = id ? null : 'hourly';
me.authid = id ? null : Proxmox.UserName;
return { };
},
@ -51,13 +139,10 @@ Ext.define('PBS.window.SyncJobEdit', {
},
{
fieldLabel: gettext('Local Owner'),
xtype: 'pbsUserSelector',
xtype: 'pbsAuthidSelector',
name: 'owner',
allowBlank: true,
value: null,
emptyText: 'backup@pam',
skipEmptyText: true,
cbind: {
value: '{authid}',
deleteEmpty: '{!isCreate}',
},
},
@ -80,12 +165,20 @@ Ext.define('PBS.window.SyncJobEdit', {
xtype: 'pbsRemoteSelector',
allowBlank: false,
name: 'remote',
listeners: {
change: function(f, value) {
let me = this;
let remoteStoreField = me.up('pbsSyncJobEdit').down('field[name=remote-store]');
remoteStoreField.setRemote(value);
},
},
},
{
fieldLabel: gettext('Source Datastore'),
xtype: 'proxmoxtextfield',
xtype: 'pbsRemoteStoreSelector',
allowBlank: false,
name: 'remote-store',
disabled: true,
},
{
fieldLabel: gettext('Sync Schedule'),