Compare commits

...

101 Commits

Author SHA1 Message Date
709c15abaa bump version to 1.0.1-1
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-11-11 10:21:30 +01:00
b404e4d930 d/control: check in new dependnecies to generated control
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-11-11 10:21:30 +01:00
f507580c3f docs: faq: fix first releases
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-11-11 10:14:01 +01:00
291b786076 docs: fix prune retention example
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-11-11 10:14:01 +01:00
06c9059dac daemon: rename method, endless loop, bail on exec error
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-11-11 10:14:01 +01:00
d7c6ad60dd daemon: add hack for sd_notify
sd_notify is not synchronous, iow. it only waits until the message
reaches the queue not until it is processed by systemd

when the process that sent such a message exits before systemd could
process it, it cannot be associated to the correct pid

so in case of reloading, we send a message with 'MAINPID=<newpid>'
to signal that it will change. if now the old process exits before
systemd knows this, it will not accept the 'READY=1' message from the
child, since it rejects the MAINPID change

since there is no (AFAICS) library interface to check the unit status,
we use 'systemctl is-active <SERVICE_NAME>' to check the state until
it is not 'reloading' anymore.

on newer systemd versions, there is 'sd_notify_barrier' which would
allow us to wait for systemd to have all messages from the current
pid to be processed before acknowledging to the child, but on buster
the systemd version is to old...

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-11-11 09:43:00 +01:00
0a0ba0785b prune sim: avoid colon to separate keep desc from count
hack for space issues for monthly keeps and >9 counts

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-11-11 08:20:13 +01:00
6ed79592f2 prune sim: make backup schedule a form, bind update button to its validity
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-11-11 08:11:46 +01:00
4c75ee3471 prune sim: do not use unecesarry variable, declare in line
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-11-11 08:11:16 +01:00
6f997da8cd prune sim: set min-heigth for calendar day cells
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-11-11 08:10:43 +01:00
03e40aa4ee ui: datastore add: set default schedule
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-11-11 07:49:01 +01:00
be1d6cbcc6 ui: shorten automatic ID length a bit
Without hyphens, we had 20 hex digits, so ~80 bit which is probably overkill.
Use 12 (13 with hyphen), this is still 48 bit.

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-11-11 07:40:23 +01:00
ffaca016ad ui: datastore summary: drop removed bytes display
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-11-11 07:27:21 +01:00
71f82a98d7 d/control: add missing dependencies for non ISO installations
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-11-11 07:26:05 +01:00
deef6fbc0c cargo: extend authors list
this was mostly selected by executing

and adding those with more than a hand full of commits, so no hard
feelings here, this was definitively also a team effort to get stuff
polished!

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-11-10 14:47:48 +01:00
4ac529141f bump version to 1.0.0-1
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-11-10 14:47:48 +01:00
a108a2e967 ui: drop debug beta code
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-11-10 14:47:48 +01:00
ff7a29104c postinst: fix version check for remote.cfg cleanup
Signed-off-by: Fabian Grünbichler <f.gruenbichler@proxmox.com>
2020-11-10 14:35:37 +01:00
240b2ffb9b ui: improve activeTab selection from fragment and state
handle invalid fragments for tabs, as well as not rendered tabpanels

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-11-10 14:21:54 +01:00
a86e703661 tools::runtime: pin_mut instead of unsafe block
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2020-11-10 14:18:45 +01:00
1ecf4e6d20 async_io: require Unpin for EitherStream and HyperAccept
We use it with Unpin types and this way we get rid of a lot
of `unsafe` blocks.

Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2020-11-10 14:18:45 +01:00
9f9a661b1a verify: cleanup logging order/messages
otherwise we end up printing warnings before the start message..

Signed-off-by: Fabian Grünbichler <f.gruenbichler@proxmox.com>
2020-11-10 14:11:36 +01:00
1b1cab8321 verify: log/warn on invalid owner
in order to trigger a notification/make the problem more visible than
just in syslog.

Signed-off-by: Fabian Grünbichler <f.gruenbichler@proxmox.com>
2020-11-10 14:11:36 +01:00
f4f9a503de ui: add mising panel help buttons
add missing help buttons (question mark, top right) so that we are
consistent and each panel has it.

I chose the IMHO most fitting sections.

Signed-off-by: Aaron Lauterer <a.lauterer@proxmox.com>
2020-11-10 13:53:21 +01:00
a4971d5f90 docs: add ref for sysadmin host admin section
Signed-off-by: Aaron Lauterer <a.lauterer@proxmox.com>
2020-11-10 13:53:21 +01:00
477ebe6b78 docs: user management: avoid some inconsistencies
The space between '--' and 'path' in two of the commands was wrong. The other
changes make the names of the store and token consistent with the rest of the
section and should improve readability.

Also add the Datastore.Verify permission in the output of the command:
proxmox-backup-manager user permissions john@pbs --path /datastore/store1
A DatastoreAdmin now has this permission and that's what john@pbs is in the
example.

Signed-off-by: Fabian Ebner <f.ebner@proxmox.com>
2020-11-10 13:47:52 +01:00
38efbfc148 ui: app: fix fixme
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-11-10 13:38:30 +01:00
10052ea644 remote.cfg: rename userid to 'auth-id'
and fixup config file on upgrades accordingly

Signed-off-by: Fabian Grünbichler <f.gruenbichler@proxmox.com>
2020-11-10 13:25:24 +01:00
b57619ea29 ui: datastores sync: future proof and move local store column in front
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-11-10 13:22:54 +01:00
445b0043b2 ui: show (local)datastore column only in global sync/verifyview
its rather hacky, but our cbind mixin does not support columns (yet).
if it does sometime in the future, we could use that instead

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-11-10 13:14:47 +01:00
8b62cbe752 docs: update package repositories
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-11-10 13:14:04 +01:00
81f99362d9 docs: installation: don't mention ext3 as an option anymore
Support for ext3 was removed by commit 0abf0d3683b74421eca24ba61d1d4e100d35211a
in pve-installer.

Signed-off-by: Fabian Ebner <f.ebner@proxmox.com>
2020-11-10 13:13:44 +01:00
414c23facb fix #3060:: improve get_owner error handling
log invalid owners to system log, and continue with next group just as
if permission checks fail for the following operations:
- verify store with limited permissions
- list store groups
- list store snapshots

all other call sites either handle it correctly already (sync/pull), or
operate on a single group/snapshot and can bubble up the error.

Signed-off-by: Fabian Grünbichler <f.gruenbichler@proxmox.com>
2020-11-10 12:58:44 +01:00
c5608cf86c encryption: add best practice for storing master key
Further clarify that the paperkey should be a last resort
recovery option, after a password manager and usb drive.

Signed-off-by: Dylan Whyte <d.whyte@proxmox.com>
2020-11-10 12:51:30 +01:00
5d08c750ef HttpsConnector: include destination on connect errors
for more useful log output
old:
Nov 10 11:50:51 foo pvestatd[3378]: proxmox-backup-client failed: Error: error trying to connect: tcp connect error: No route to host (os error 113)
new:
Nov 10 11:55:21 foo pvestatd[3378]: proxmox-backup-client failed: Error: error trying to connect: error connecting to https://thebackuphost:8007/ - tcp connect error: No route to host (os error 113)

Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2020-11-10 11:58:19 +01:00
f3fde36beb client: error context when building HttpClient
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2020-11-10 11:58:19 +01:00
0c83e8891e ui: fix task description 2020-11-10 11:53:39 +01:00
133de2dd1f ui: add/fix help buttons
added a few more help buttons were appropriate:

* GC and Prune schedule windows
* Create Directory window
* API Tokens, link directly to token section
* verify jobs window

Signed-off-by: Aaron Lauterer <a.lauterer@proxmox.com>
2020-11-10 11:51:03 +01:00
c8219747f0 ui: add all online help refs found in docs
recommit the onlinehelp after the scanrefs script has been adapted and
the docs are up to date

Signed-off-by: Aaron Lauterer <a.lauterer@proxmox.com>
2020-11-10 11:50:56 +01:00
0247f794e9 docs: add network management reference
needed in order for the help button in the network edit window to work.

Signed-off-by: Aaron Lauterer <a.lauterer@proxmox.com>
2020-11-10 11:50:17 +01:00
710f787c41 docs: add maintenance chapter prefix to verification ref
Signed-off-by: Aaron Lauterer <a.lauterer@proxmox.com>
2020-11-10 11:50:12 +01:00
d8916a326c scanrefs: only scan docs, not JS files
This is a temporary hack until we find a sensible way to scan the
proxmox-widget-toolkit JS files as well.

Signed-off-by: Aaron Lauterer <a.lauterer@proxmox.com>
2020-11-10 11:50:09 +01:00
924d6d4072 prune sim: show count for rule
and rename 'all zero' to 'keep-all' to make it consistent with the prune dialog
in PBS.

Signed-off-by: Fabian Ebner <f.ebner@proxmox.com>
2020-11-10 11:47:37 +01:00
984ac33d5c ui: subscription: usage chart: render date as ISO 8601
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-11-10 11:46:22 +01:00
0a4dfd63c9 ui: usage graph: show axis and set maximum
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-11-10 11:46:05 +01:00
a6e746f652 ui: datastore list summary: add more padding between elements
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-11-10 11:46:05 +01:00
30f73fa2e0 fix bug #3060: continue sync if we cannot aquire the group lock 2020-11-10 11:29:36 +01:00
9f0ee346e9 ui: Datastores Summary: change layout and chart
changes the layout to look i little bit more like the statistics panel
we have for ceph in pve, while changing to the UsageChart and adding
some more datastore infos (from last garbage collect)

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-11-10 10:43:07 +01:00
48d6dede4a ui: refactor calculate_dedup_factor
so that we can reuse this

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-11-10 10:43:07 +01:00
8432e4a655 ui: add panel/UsageChart
heavily inspired by pveRunningChart, without the dynamically adding
of data and specific for the usage of datastores

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-11-10 10:43:07 +01:00
b35eb0a175 api2/status/datastore-usage: add gc-status and history start and delta
so that we can show more info and calculate the points in time for the
history

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-11-10 10:43:07 +01:00
c3a1b34ed3 ui: subscription: add more button icons, small UX fix
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-11-10 10:42:45 +01:00
bb26843cd6 ui/docs: add get help onlineHelp
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-11-10 10:35:35 +01:00
ee0ab12dd0 ui: move disks/directory stuff to tab panel
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-11-10 10:15:44 +01:00
d5f7755467 docs: online help scanner: also include help tool links
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-11-10 10:15:08 +01:00
5c64e83b1e ui: datastore: set onlineHelp for chaging group owner
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-11-10 09:53:05 +01:00
0f6f99b4ec ui: prune: set onlineHelp
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-11-10 09:51:30 +01:00
f668862ae0 ui: prune: add clear-trigger to keep fields
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-11-10 09:51:20 +01:00
c960d2b501 bail if mount point already exists for directories
similar to what we do for zfs. By bailing before partitioning, the disk is
still considered unused after a failed attempt.

Signed-off-by: Fabian Ebner <f.ebner@proxmox.com>
2020-11-10 09:25:58 +01:00
f5d9f2534b mount zpools created via API under /mnt/datastore
as we do for other file systems

Signed-off-by: Fabian Ebner <f.ebner@proxmox.com>
2020-11-10 09:25:58 +01:00
9a3ddcea33 ui: utils: eslint format fixes
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-11-10 09:24:35 +01:00
030464d3a9 docs: s/DataStore/Datastore/
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-11-10 09:24:13 +01:00
3f30b32c2e ui: prune: show count for rule
Signed-off-by: Fabian Ebner <f.ebner@proxmox.com>
2020-11-10 09:24:13 +01:00
5eafe6aabc ui: prune: show which rule keeps backup
and adjust layout so the description fits.

Signed-off-by: Fabian Ebner <f.ebner@proxmox.com>
2020-11-10 09:24:13 +01:00
2c9f274efa ui: add help tool to user and remote config 2020-11-10 09:23:22 +01:00
31112c79ac ui: add help tool to datastore panel 2020-11-10 09:15:12 +01:00
d89f91b538 ui: acl editor: disallow path editing for datastore permission views
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-11-10 08:19:17 +01:00
a6310ec294 ui: fix widget height in dashboard 2020-11-10 08:12:35 +01:00
98d9323534 ui: add link to www.proxmox.com for subscription plans 2020-11-10 08:07:49 +01:00
09f1f28800 ui: ACL view: fix path filtering
and add some comments about actual behavior of those config
properties..

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-11-10 07:33:20 +01:00
e1da9ca4bb ui: datastore dashboard: use gauge for usage, rework layout a bit
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-11-09 19:26:48 +01:00
625c7bfc0b ui: task summary: enable grid mouse track over
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-11-09 19:25:43 +01:00
d9503950e3 ui: tasl summary: add pointer cursor if clickable
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-11-09 18:09:05 +01:00
376e927980 ui: datastore summary: increase usage graph height
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-11-09 17:55:59 +01:00
5204cbcf0f ui: datastore summary: add line chart icon to full-estimation
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-11-09 17:48:53 +01:00
e373dcc564 ui: datastore/content: improve action button layout
Fix font-size to 14px to improve font-awesome rendering, add some
slight margin between the buttons so that they are not glued
together, add a slight text-shadow on mouse over.

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-11-09 17:45:08 +01:00
137a6ebcad apt: allow changelog retrieval from enterprise repo
If a package is or will be installed from the enterprise repo, retrieve
the changelog from there as well (securely via HTTPS and authenticated
with the subcription key).

Extends the get_string method to take additional headers, in this case
used for 'Authorization'. Hyper does not have built-in basic auth
support AFAICT but it's simple enough to just build the header manually.

Take the opportunity and also set the User-Agent sensibly for GET
requests, just like for POST.

Signed-off-by: Stefan Reiter <s.reiter@proxmox.com>
2020-11-09 17:28:58 +01:00
ed1329ecf7 ui: make Datastore clickable again
by showing the previously added pbsDataStores panel

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-11-09 16:37:24 +01:00
2371c1e371 ui: add Panels necessary for Datastores Overview
a panel for a single datastore that gets updated from an external caller
shows the usage, estimated full date, history and task summary grid

a panel that dynamically generates the panel above for each datastore

and a tabpanel that includes the panel above, as well as a global
syncview, verifiyview and aclview

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-11-09 16:37:24 +01:00
63c07d950c ui: TaskSummary: handle less defined parameters of tasks
this makes it a little easier to provide good data, without
hardcoding all types in the source object

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-11-09 16:37:24 +01:00
a3cdb19e33 ui: TaskSummary: add subPanelModal and datastore parameters
in preparation for the per-datastore grid

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-11-09 16:37:24 +01:00
4623cd6497 ui: TaskSummary: move state/types/titles out of the controller
it seems that under certain circumstances, extjs does not initialize
or remove the content from objects in controllers

move it to the view, were they always exist

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-11-09 16:37:24 +01:00
ab81bb13ad ui: make Sync/VerifyView and Edit usable without datastore
we want to use this panel again for a 'global' overview, without
any datastore preselected, so we have to handle that, and
adding a datastore selector in the editwindow

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-11-09 16:37:24 +01:00
616650a198 ui: Utils: add parse_datastore_worker_id
to parse the datastore out of a worker_id
for this we need some regexes that are the same as in the backend

for now we only parse out the datastore, but we can extend this
in the future to parse relevant info (e.g. remote for syncs,
id/type for backups)

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-11-09 16:37:24 +01:00
78763d21b1 ui: refactor render_size_usage to Utils
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-11-09 16:37:24 +01:00
f2d6324958 ui: refactor render_estimate
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-11-09 16:37:24 +01:00
6e880f19cc api2/node/tasks: add check_job_store and use it
to easily check the store of a worker_id
this fixes the issue that one could not filter by type 'syncjob' and
datastore simultaneously

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-11-09 16:37:24 +01:00
64623f329e ui: recommit onlinehelp
now that the last commit fixed the title generation

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-11-09 16:36:00 +01:00
407f3fb994 scanrefs: remove term prefix from title
It can happen, that a title is defined as term in the following way:
:term:`My title`

This patch checks for it and strips the leading part and the last `.

Signed-off-by: Aaron Lauterer <a.lauterer@proxmox.com>
2020-11-09 16:35:29 +01:00
0eb0c4bd63 proxy: fix log message for auth log rotation
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-11-09 16:34:03 +01:00
82422c115a ui: admin/summary: add versions button/window
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-11-09 16:33:22 +01:00
ed2beb334d api: node/apt: add versions call
very basic, based on API/concepts of PVE one.

Still missing, addint an extra_info string option to APTUpdateInfo
and pass along running kernel/PBS version there.

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-11-09 16:31:56 +01:00
f3b4820d06 www: show more ACLs in datastore panel
since just the ACLs defined on the exact datastore path don't give
anywhere near a complete picture of who has access to it.

Signed-off-by: Fabian Grünbichler <f.gruenbichler@proxmox.com>
2020-11-09 15:19:15 +01:00
8f7cd96df4 installation: minor wording fix
very minor but worthwhile edits

Signed-off-by: Dylan Whyte <d.whyte@proxmox.com>
2020-11-09 15:18:44 +01:00
4accbc5853 backup-client: encryption: discuss paperkey command
adds a paragraph to the encryption section about
encoding the master key into a qr code for printing

Signed-off-by: Dylan Whyte <d.whyte@proxmox.com>
2020-11-09 15:18:44 +01:00
2791318ff1 fix bug #3121: forbid removing used reemotes 2020-11-09 12:48:29 +01:00
47208b4147 pxar: log when skipping mount points
Clippy complains about the number of paramters we have for
create_archive and it really does need to be made somewhat
less awkward and more usable. For now we just log to stderr
as we previously did. Added todo-comments for this.

Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2020-11-09 12:43:16 +01:00
b783591fb5 ui: datastore content: ensure action column is wide enough
with the "change owner" action added we now need more than the
default of 100 px, so increase to 120 px for now.

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-11-09 12:31:14 +01:00
9dd6175808 ui: token selector: use same layout as auth id selector
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-11-09 12:24:54 +01:00
5e8b97178e ui: auth/token selector: tell ExtJS we injected data into the store
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-11-09 12:21:02 +01:00
38260cddf5 tools apt: include package name in filter data
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-11-09 08:55:08 +01:00
90 changed files with 1878 additions and 429 deletions

View File

@ -1,7 +1,16 @@
[package]
name = "proxmox-backup"
version = "0.9.7"
authors = ["Dietmar Maurer <dietmar@proxmox.com>"]
version = "1.0.1"
authors = [
"Dietmar Maurer <dietmar@proxmox.com>",
"Dominik Csapak <d.csapak@proxmox.com>",
"Christian Ebner <c.ebner@proxmox.com>",
"Fabian Grünbichler <f.gruenbichler@proxmox.com>",
"Stefan Reiter <s.reiter@proxmox.com>",
"Thomas Lamprecht <t.lamprecht@proxmox.com>",
"Wolfgang Bumiller <w.bumiller@proxmox.com>",
"Proxmox Support Team <support@proxmox.com>",
]
edition = "2018"
license = "AGPL-3"
description = "Proxmox Backup"

47
debian/changelog vendored
View File

@ -1,3 +1,50 @@
rust-proxmox-backup (1.0.1-1) unstable; urgency=medium
* ui: datastore summary: drop 'removed bytes' display
* ui: datastore add: set default schedule
* prune sim: make backup schedule a form, bind update button to its validity
* daemon: add workaround for race in reloading and systemd 'ready' notification
-- Proxmox Support Team <support@proxmox.com> Wed, 11 Nov 2020 10:18:12 +0100
rust-proxmox-backup (1.0.0-1) unstable; urgency=medium
* fix #3121: forbid removing used remotes
* docs: backup-client: encryption: discuss paperkey command
* pxar: log when skipping mount points
* ui: show also parent ACLs which affect a datastore in its panel
* api: node/apt: add versions call
* ui: make Datastore a selectable panel again. Show a datastore summary
list, and provide unfiltered access to all sync and verify jobs.
* ui: add help tool-button to various paneös
* ui: set various onlineHelp buttons
* zfs: mount new zpools created via API under /mnt/datastore/<id>
* ui: move disks/directory views to its own tab panel
* fix #3060: continue sync if we cannot aquire the group lock
* HttpsConnector: include destination on connect errors
* fix #3060:: improve get_owner error handling
* remote.cfg: rename userid to 'auth-id'
* verify: log/warn on invalid owner
-- Proxmox Support Team <support@proxmox.com> Tue, 10 Nov 2020 14:36:13 +0100
rust-proxmox-backup (0.9.7-1) unstable; urgency=medium
* ui: add remote store selector

3
debian/control vendored
View File

@ -104,7 +104,9 @@ Depends: fonts-font-awesome,
libjs-extjs (>= 6.0.1),
libzstd1 (>= 1.3.8),
lvm2,
openssh-server,
pbs-i18n,
postfix | mail-transport-agent,
proxmox-backup-docs,
proxmox-mini-journalreader,
proxmox-widget-toolkit (>= 2.3-6),
@ -113,6 +115,7 @@ Depends: fonts-font-awesome,
${misc:Depends},
${shlibs:Depends},
Recommends: zfsutils-linux,
ifupdown2,
Description: Proxmox Backup Server daemon with tools and GUI
This package contains the Proxmox Backup Server daemons and related
tools. This includes a web-based graphical user interface.

3
debian/control.in vendored
View File

@ -4,7 +4,9 @@ Depends: fonts-font-awesome,
libjs-extjs (>= 6.0.1),
libzstd1 (>= 1.3.8),
lvm2,
openssh-server,
pbs-i18n,
postfix | mail-transport-agent,
proxmox-backup-docs,
proxmox-mini-journalreader,
proxmox-widget-toolkit (>= 2.3-6),
@ -13,6 +15,7 @@ Depends: fonts-font-awesome,
${misc:Depends},
${shlibs:Depends},
Recommends: zfsutils-linux,
ifupdown2,
Description: Proxmox Backup Server daemon with tools and GUI
This package contains the Proxmox Backup Server daemons and related
tools. This includes a web-based graphical user interface.

View File

@ -1,2 +1,2 @@
proxmox-backup-server: package-installs-apt-sources etc/apt/sources.list.d/pbstest-beta.list
proxmox-backup-server: package-installs-apt-sources etc/apt/sources.list.d/pbs-enterprise.list
proxmox-backup-server: systemd-service-file-refers-to-unusual-wantedby-target lib/systemd/system/proxmox-backup-banner.service getty.target

9
debian/postinst vendored
View File

@ -28,6 +28,15 @@ case "$1" in
if dpkg --compare-versions "$2" 'le' '0.9.5-1'; then
chown --quiet backup:backup /var/log/proxmox-backup/api/auth.log || true
fi
if dpkg --compare-versions "$2" 'le' '0.9.7-1'; then
if [ -e /etc/proxmox-backup/remote.cfg ]; then
echo "NOTE: Switching over remote.cfg to new field names.."
flock -w 30 /etc/proxmox-backup/.remote.lck \
sed -i \
-e 's/^\s\+userid /\tauth-id /g' \
/etc/proxmox-backup/remote.cfg || true
fi
fi
fi
# FIXME: Remove in future version once we're sure no broken entries remain in anyone's files
if grep -q -e ':termproxy::[^@]\+: ' /var/log/proxmox-backup/tasks/active; then

View File

@ -3,7 +3,7 @@ etc/proxmox-backup.service /lib/systemd/system/
etc/proxmox-backup-banner.service /lib/systemd/system/
etc/proxmox-backup-daily-update.service /lib/systemd/system/
etc/proxmox-backup-daily-update.timer /lib/systemd/system/
etc/pbstest-beta.list /etc/apt/sources.list.d/
etc/pbs-enterprise.list /etc/apt/sources.list.d/
usr/lib/x86_64-linux-gnu/proxmox-backup/proxmox-backup-api
usr/lib/x86_64-linux-gnu/proxmox-backup/proxmox-backup-proxy
usr/lib/x86_64-linux-gnu/proxmox-backup/proxmox-backup-banner

View File

@ -0,0 +1 @@
rm_conffile /etc/apt/sources.list.d/pbstest-beta.list 1.0.0~ proxmox-backup-server

View File

@ -44,7 +44,7 @@ def scan_extjs_files(wwwdir="../www"): # a bit rough i know, but we can optimize
js_files.append(os.path.join(root, filename))
for js_file in js_files:
fd = open(js_file).read()
allmatch = re.findall("onlineHelp:\s*[\'\"](.*?)[\'\"]", fd, re.M)
allmatch = re.findall("(?:onlineHelp:|get_help_tool\s*\()\s*[\'\"](.*?)[\'\"]", fd, re.M)
for match in allmatch:
anchor = match
anchor = re.sub('_', '-', anchor) # normalize labels
@ -73,7 +73,9 @@ class ReflabelMapper(Builder):
'link': '/docs/index.html',
'title': 'Proxmox Backup Server Documentation Index',
}
self.env.used_anchors = scan_extjs_files()
# Disabled until we find a sensible way to scan proxmox-widget-toolkit
# as well
#self.env.used_anchors = scan_extjs_files()
if not os.path.isdir(self.outdir):
os.mkdir(self.outdir)
@ -93,6 +95,9 @@ class ReflabelMapper(Builder):
logger.info('traversing section {}'.format(title.astext()))
ref_name = getattr(title, 'rawsource', title.astext())
if (ref_name[:7] == ':term:`'):
ref_name = ref_name[7:-1]
self.env.online_help[labelid] = {'link': '', 'title': ''}
self.env.online_help[labelid]['link'] = "/docs/" + os.path.basename(filename_html) + "#{}".format(labelid)
self.env.online_help[labelid]['title'] = ref_name
@ -112,15 +117,18 @@ class ReflabelMapper(Builder):
def validate_anchors(self):
#pprint(self.env.online_help)
to_remove = []
for anchor in self.env.used_anchors:
if anchor not in self.env.online_help:
logger.info("[-] anchor {} is missing from onlinehelp!".format(anchor))
for anchor in self.env.online_help:
if anchor not in self.env.used_anchors and anchor != 'pbs_documentation_index':
logger.info("[*] anchor {} not used! deleting...".format(anchor))
to_remove.append(anchor)
for anchor in to_remove:
self.env.online_help.pop(anchor, None)
# Disabled until we find a sensible way to scan proxmox-widget-toolkit
# as well
#for anchor in self.env.used_anchors:
# if anchor not in self.env.online_help:
# logger.info("[-] anchor {} is missing from onlinehelp!".format(anchor))
#for anchor in self.env.online_help:
# if anchor not in self.env.used_anchors and anchor != 'pbs_documentation_index':
# logger.info("[*] anchor {} not used! deleting...".format(anchor))
# to_remove.append(anchor)
#for anchor in to_remove:
# self.env.online_help.pop(anchor, None)
return
def finish(self):

View File

@ -365,9 +365,22 @@ To set up a master key:
backed up. It can happen, for example, that you back up an entire system, using
a key on that system. If the system then becomes inaccessible for any reason
and needs to be restored, this will not be possible as the encryption key will be
lost along with the broken system. In preparation for the worst case scenario,
you should consider keeping a paper copy of this key locked away in
a safe place.
lost along with the broken system.
It is recommended that you keep your master key safe, but easily accessible, in
order for quick disaster recovery. For this reason, the best place to store it
is in your password manager, where it is immediately recoverable. As a backup to
this, you should also save the key to a USB drive and store that in a secure
place. This way, it is detached from any system, but is still easy to recover
from, in case of emergency. Finally, in preparation for the worst case scenario,
you should also consider keeping a paper copy of your master key locked away in
a safe place. The ``paperkey`` subcommand can be used to create a QR encoded
version of your master key. The following command sends the output of the
``paperkey`` command to a text file, for easy printing.
.. code-block:: console
proxmox-backup-client key paperkey --output-format text > qrkey.txt
Restoring Data

View File

@ -27,7 +27,7 @@ How long will my Proxmox Backup Server version be supported?
+-----------------------+--------------------+---------------+------------+--------------------+
|Proxmox Backup Version | Debian Version | First Release | Debian EOL | Proxmox Backup EOL |
+=======================+====================+===============+============+====================+
|Proxmox Backup 1.x | Debian 10 (Buster) | tba | tba | tba |
|Proxmox Backup 1.x | Debian 10 (Buster) | 2020-11 | tba | tba |
+-----------------------+--------------------+---------------+------------+--------------------+

View File

@ -132,5 +132,5 @@ top panel to view:
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
* **Verify Jobs**: Create, manage and run :ref:`maintenance_verification` jobs on the
datastore

View File

@ -37,16 +37,15 @@ Download the ISO from |DOWNLOADS|.
It includes the following:
* The `Proxmox Backup`_ server installer, which partitions the local
disk(s) with ext4, ext3, xfs or ZFS, and installs the operating
system
disk(s) with ext4, xfs or ZFS, and installs the operating system
* Complete operating system (Debian Linux, 64-bit)
* Our Linux kernel with ZFS support
* Proxmox Linux kernel with ZFS support
* Complete tool-set to administer backups and all necessary resources
* Web based GUI management interface
* Web based management interface
.. note:: During the installation process, the complete server
is used by default and all existing data is removed.

View File

@ -127,8 +127,7 @@ language.
-- `The Rust Programming Language <https://doc.rust-lang.org/book/ch00-00-introduction.html>`_
.. todo:: further explain the software stack
.. _get_help:
Getting Help
------------

View File

@ -77,6 +77,42 @@ edit the interval at which pruning takes place.
:alt: Prune and garbage collection options
Retention Settings Example
^^^^^^^^^^^^^^^^^^^^^^^^^^
The backup frequency and retention of old backups may depend on how often data
changes, and how important an older state may be, in a specific work load.
When backups act as a company's document archive, there may also be legal
requirements for how long backup snapshots must be kept.
For this example, we assume that you are doing daily backups, have a retention
period of 10 years, and the period between backups stored gradually grows.
- **keep-last:** ``3`` - even if only daily backups, an admin may want to create
an extra one just before or after a big upgrade. Setting keep-last ensures
this.
- **keep-hourly:** not set - for daily backups this is not relevant. You cover
extra manual backups already, with keep-last.
- **keep-daily:** ``13`` - together with keep-last, which covers at least one
day, this ensures that you have at least two weeks of backups.
- **keep-weekly:** ``8`` - ensures that you have at least two full months of
weekly backups.
- **keep-monthly:** ``11`` - together with the previous keep settings, this
ensures that you have at least a year of monthly backups.
- **keep-yearly:** ``9`` - this is for the long term archive. As you covered the
current year with the previous options, you would set this to nine for the
remaining ones, giving you a total of at least 10 years of coverage.
We recommend that you use a higher retention period than is minimally required
by your environment; you can always reduce it if you find it is unnecessarily
high, but you cannot recreate backup snapshots from the past.
.. _maintenance_gc:
Garbage Collection
@ -93,7 +129,7 @@ GC** from the top panel. From here, you can edit the schedule at which garbage
collection runs and manually start the operation.
.. _verification:
.. _maintenance_verification:
Verification
------------

View File

@ -1,3 +1,5 @@
.. _sysadmin_network_configuration:
Network Management
==================

View File

@ -26,11 +26,8 @@ update``.
.. FIXME for 7.0: change security update suite to bullseye-security
In addition, you need a package repository from Proxmox to get Proxmox Backup updates.
During the Proxmox Backup beta phase, only one repository (pbstest) will be
available. Once released, an Enterprise repository for production use and a
no-subscription repository will be provided.
In addition, you need a package repository from Proxmox to get Proxmox Backup
updates.
SecureApt
~~~~~~~~~
@ -72,68 +69,63 @@ Here, the output should be:
f3f6c5a3a67baf38ad178e5ff1ee270c /etc/apt/trusted.gpg.d/proxmox-ve-release-6.x.gpg
.. comment
`Proxmox Backup`_ Enterprise Repository
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
`Proxmox Backup`_ Enterprise Repository
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This will be the default, stable, and recommended repository. It is available for
all `Proxmox Backup`_ subscription users. It contains the most stable packages,
and is suitable for production use. The ``pbs-enterprise`` repository is
enabled by default:
This will be the default, stable, and recommended repository. It is available for
all `Proxmox Backup`_ subscription users. It contains the most stable packages,
and is suitable for production use. The ``pbs-enterprise`` repository is
enabled by default:
.. note:: During the Proxmox Backup beta phase only one repository (pbstest)
will be available.
.. code-block:: sources.list
:caption: File: ``/etc/apt/sources.list.d/pbs-enterprise.list``
.. code-block:: sources.list
:caption: File: ``/etc/apt/sources.list.d/pbs-enterprise.list``
deb https://enterprise.proxmox.com/debian/pbs buster pbs-enterprise
deb https://enterprise.proxmox.com/debian/pbs buster pbs-enterprise
To never miss important security fixes, the superuser (``root@pam`` user) is
notified via email about new packages as soon as they are available. The
change-log and details of each package can be viewed in the GUI (if available).
To never miss important security fixes, the superuser (``root@pam`` user) is
notified via email about new packages as soon as they are available. The
change-log and details of each package can be viewed in the GUI (if available).
Please note that you need a valid subscription key to access this
repository. More information regarding subscription levels and pricing can be
found at https://www.proxmox.com/en/proxmox-backup/pricing.
Please note that you need a valid subscription key to access this
repository. More information regarding subscription levels and pricing can be
found at https://www.proxmox.com/en/proxmox-backup-server/pricing
.. note:: You can disable this repository by commenting out the above
line using a `#` (at the start of the line). This prevents error
messages if you do not have a subscription key. Please configure the
``pbs-no-subscription`` repository in that case.
.. note:: You can disable this repository by commenting out the above line
using a `#` (at the start of the line). This prevents error messages if you do
not have a subscription key. Please configure the ``pbs-no-subscription``
repository in that case.
`Proxmox Backup`_ No-Subscription Repository
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
`Proxmox Backup`_ No-Subscription Repository
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
As the name suggests, you do not need a subscription key to access
this repository. It can be used for testing and non-production
use. It is not recommended to use it on production servers, because these
packages are not always heavily tested and validated.
As the name suggests, you do not need a subscription key to access
this repository. It can be used for testing and non-production
use. It is not recommended to use it on production servers, because these
packages are not always heavily tested and validated.
We recommend to configure this repository in ``/etc/apt/sources.list``.
We recommend to configure this repository in ``/etc/apt/sources.list``.
.. code-block:: sources.list
:caption: File: ``/etc/apt/sources.list``
.. code-block:: sources.list
:caption: File: ``/etc/apt/sources.list``
deb http://ftp.debian.org/debian buster main contrib
deb http://ftp.debian.org/debian buster-updates main contrib
deb http://ftp.debian.org/debian buster main contrib
deb http://ftp.debian.org/debian buster-updates main contrib
# PBS pbs-no-subscription repository provided by proxmox.com,
# NOT recommended for production use
deb http://download.proxmox.com/debian/pbs buster pbs-no-subscription
# PBS pbs-no-subscription repository provided by proxmox.com,
# NOT recommended for production use
deb http://download.proxmox.com/debian/pbs buster pbs-no-subscription
# security updates
deb http://security.debian.org/debian-security buster/updates main contrib
# security updates
deb http://security.debian.org/debian-security buster/updates main contrib
`Proxmox Backup`_ Beta Repository
`Proxmox Backup`_ Test Repository
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
During the public beta, there is a repository called ``pbstest``. This one
contains the latest packages and is heavily used by developers to test new
features.
This repository contains the latest packages and is heavily used by developers
to test new features.
.. .. warning:: the ``pbstest`` repository should (as the name implies)
only be used to test new features or bug fixes.
@ -145,7 +137,3 @@ You can access this repository by adding the following line to
:caption: sources.list entry for ``pbstest``
deb http://download.proxmox.com/debian/pbs buster pbstest
If you installed Proxmox Backup Server from the official beta ISO, you should
have this repository already configured in
``/etc/apt/sources.list.d/pbstest-beta.list``

View File

@ -13,6 +13,7 @@
.cal-day {
vertical-align: top;
width: 150px;
height: 75px; /* this is like min-height when used in tables */
border: #939393 1px solid;
color: #454545;
}

View File

@ -152,7 +152,12 @@ Ext.onReady(function() {
dataIndex: 'mark',
renderer: function(value, metaData, record) {
if (record.data.mark === 'keep') {
return 'keep (' + record.data.keepName + ')';
if (record.data.keepCount) {
return 'keep (' + record.data.keepName +
': ' + record.data.keepCount + ')';
} else {
return 'keep (' + record.data.keepName + ')';
}
} else {
return value;
}
@ -213,7 +218,11 @@ Ext.onReady(function() {
if (backup.data.mark === 'remove') {
html += `<span class="strikethrough">${text}</span>`;
} else {
text += ` (${backup.data.keepName})`;
if (backup.data.keepCount) {
text += ` (${backup.data.keepName} ${backup.data.keepCount})`;
} else {
text += ` (${backup.data.keepName})`;
}
if (me.useColors) {
let bgColor = COLORS[backup.data.keepName];
let textColor = TEXT_COLORS[backup.data.keepName];
@ -470,6 +479,7 @@ Ext.onReady(function() {
newlyIncludedCount++;
backup.mark = 'keep';
backup.keepName = keepName;
backup.keepCount = newlyIncludedCount;
} else {
backup.mark = 'remove';
}
@ -488,7 +498,7 @@ Ext.onReady(function() {
Number(keepParams['keep-yearly']) === 0) {
backups.forEach(function(backup) {
backup.mark = 'keep';
backup.keepName = 'all zero';
backup.keepName = 'keep-all';
});
return;
@ -613,42 +623,6 @@ Ext.onReady(function() {
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',
@ -684,6 +658,7 @@ Ext.onReady(function() {
},
{ xtype: "panel", width: 1, border: 1 },
{
xtype: 'form',
layout: 'anchor',
flex: 1,
border: false,
@ -692,7 +667,42 @@ Ext.onReady(function() {
labelWidth: 120,
},
bodyPadding: 10,
items: scheduleItems,
items: [
{
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',
formBind: true,
handler: 'reloadFull',
},
],
},
],
},

View File

@ -1,6 +1,8 @@
Storage
=======
.. _storage_disk_management:
Disk Management
---------------
@ -57,7 +59,7 @@ create a datastore at the location ``/mnt/datastore/store1``:
You can also create a ``zpool`` with various raid levels from **Administration
-> Disks -> Zpool** in the web interface, or by using ``zpool create``. The command
below creates a mirrored ``zpool`` using two disks (``sdb`` & ``sdc``) and
mounts it on the root directory (default):
mounts it under ``/mnt/datastore/zpool1``:
.. code-block:: console
@ -85,7 +87,7 @@ display S.M.A.R.T. attributes from the web interface or by using the command:
.. _datastore_intro:
:term:`DataStore`
:term:`Datastore`
-----------------
A datastore refers to a location at which backups are stored. The current
@ -121,6 +123,8 @@ number of backups to keep in that store. :ref:`backup-pruning` and
periodically based on a configured schedule (see :ref:`calendar-events`) per datastore.
.. _storage_datastore_create:
Creating a Datastore
^^^^^^^^^^^^^^^^^^^^
.. image:: images/screenshots/pbs-gui-datastore-create-general.png

View File

@ -1,3 +1,5 @@
.. _sysadmin_host_administration:
Host System Administration
==========================

View File

@ -230,11 +230,11 @@ You can list the ACLs of each user/token using the following command:
.. code-block:: console
# proxmox-backup-manager acl list
┌──────────┬──────────────────┬───────────┬────────────────┐
│ ugid │ path │ propagate │ roleid │
╞══════════╪══════════════════╪═══════════╪════════════════╡
│ john@pbs │ /datastore/disk1 │ 1 │ DatastoreAdmin │
└──────────┴──────────────────┴───────────┴────────────────┘
┌──────────┬──────────────────┬───────────┬────────────────┐
│ ugid │ path │ propagate │ roleid │
╞══════════╪══════════════════╪═══════════╪════════════════╡
│ john@pbs │ /datastore/store1 │ 1 │ DatastoreAdmin │
└──────────┴──────────────────┴───────────┴────────────────┘
A single user/token can be assigned multiple permission sets for different datastores.
@ -267,7 +267,7 @@ you can use the ``proxmox-backup-manager user permission`` command:
.. code-block:: console
# proxmox-backup-manager user permissions john@pbs -- path /datastore/store1
# proxmox-backup-manager user permissions john@pbs --path /datastore/store1
Privileges with (*) have the propagate flag set
Path: /datastore/store1
@ -276,9 +276,10 @@ you can use the ``proxmox-backup-manager user permission`` command:
- Datastore.Modify (*)
- Datastore.Prune (*)
- Datastore.Read (*)
- Datastore.Verify (*)
# proxmox-backup-manager acl update /datastore/store1 DatastoreBackup --auth-id 'john@pbs!client1'
# proxmox-backup-manager user permissions 'john@pbs!test' -- path /datastore/store1
# proxmox-backup-manager user permissions 'john@pbs!client1' --path /datastore/store1
Privileges with (*) have the propagate flag set
Path: /datastore/store1

View File

@ -9,7 +9,7 @@ DYNAMIC_UNITS := \
proxmox-backup.service \
proxmox-backup-proxy.service
all: $(UNITS) $(DYNAMIC_UNITS) pbstest-beta.list
all: $(UNITS) $(DYNAMIC_UNITS) pbs-enterprise.list
clean:
rm -f $(DYNAMIC_UNITS)

1
etc/pbs-enterprise.list Normal file
View File

@ -0,0 +1 @@
deb https://enterprise.proxmox.com/debian/pbs buster pbs-enterprise

View File

@ -1 +0,0 @@
deb http://download.proxmox.com/debian/pbs buster pbstest

View File

@ -187,7 +187,13 @@ fn list_groups(
let group = info.backup_dir.group();
let list_all = (user_privs & PRIV_DATASTORE_AUDIT) != 0;
let owner = datastore.get_owner(group)?;
let owner = match datastore.get_owner(group) {
Ok(auth_id) => auth_id,
Err(err) => {
println!("Failed to get owner of group '{}' - {}", group, err);
continue;
},
};
if !list_all && check_backup_owner(&owner, &auth_id).is_err() {
continue;
}
@ -369,7 +375,13 @@ pub fn list_snapshots (
}
let list_all = (user_privs & PRIV_DATASTORE_AUDIT) != 0;
let owner = datastore.get_owner(group)?;
let owner = match datastore.get_owner(group) {
Ok(auth_id) => auth_id,
Err(err) => {
println!("Failed to get owner of group '{}' - {}", group, err);
continue;
},
};
if !list_all && check_backup_owner(&owner, &auth_id).is_err() {
continue;
@ -636,7 +648,7 @@ pub fn verify(
verify_all_backups(datastore, worker.clone(), worker.upid(), owner, None)?
};
if failed_dirs.len() > 0 {
worker.log("Failed to verify following snapshots:");
worker.log("Failed to verify following snapshots/groups:");
for dir in failed_dirs {
worker.log(format!("\t{}", dir));
}

View File

@ -78,7 +78,7 @@ pub fn list_remotes(
optional: true,
default: 8007,
},
userid: {
"auth-id": {
type: Authid,
},
password: {
@ -178,7 +178,7 @@ pub enum DeletableProperty {
type: u16,
optional: true,
},
userid: {
"auth-id": {
optional: true,
type: Authid,
},
@ -214,7 +214,7 @@ pub fn update_remote(
comment: Option<String>,
host: Option<String>,
port: Option<u16>,
userid: Option<Authid>,
auth_id: Option<Authid>,
password: Option<String>,
fingerprint: Option<String>,
delete: Option<Vec<DeletableProperty>>,
@ -252,7 +252,7 @@ pub fn update_remote(
}
if let Some(host) = host { data.host = host; }
if port.is_some() { data.port = port; }
if let Some(userid) = userid { data.userid = userid; }
if let Some(auth_id) = auth_id { data.auth_id = auth_id; }
if let Some(password) = password { data.password = password; }
if let Some(fingerprint) = fingerprint { data.fingerprint = Some(fingerprint); }
@ -284,6 +284,17 @@ pub fn update_remote(
/// Remove a remote from the configuration file.
pub fn delete_remote(name: String, digest: Option<String>) -> Result<(), Error> {
use crate::config::sync::{self, SyncJobConfig};
let (sync_jobs, _) = sync::config()?;
let job_list: Vec<SyncJobConfig> = sync_jobs.convert_to_typed_array("sync")?;
for job in job_list {
if job.remote == name {
bail!("remote '{}' is used by sync job '{}' (datastore '{}')", name, job.id, job.store);
}
}
let _lock = open_file_locked(remote::REMOTE_CFG_LOCKFILE, std::time::Duration::new(10, 0), true)?;
let (mut config, expected_digest) = remote::config()?;
@ -312,7 +323,7 @@ pub async fn remote_client(remote: remote::Remote) -> Result<HttpClient, Error>
let client = HttpClient::new(
&remote.host,
remote.port.unwrap_or(8007),
&remote.userid,
&remote.auth_id,
options)?;
let _auth_info = client.login() // make sure we can auth
.await

View File

@ -1,12 +1,13 @@
use anyhow::{Error, bail, format_err};
use serde_json::{json, Value};
use std::collections::HashMap;
use proxmox::list_subdirs_api_method;
use proxmox::api::{api, RpcEnvironment, RpcEnvironmentType, Permission};
use proxmox::api::router::{Router, SubdirMap};
use crate::server::WorkerTask;
use crate::tools::{apt, http};
use crate::tools::{apt, http, subscription};
use crate::config::acl::{PRIV_SYS_AUDIT, PRIV_SYS_MODIFY};
use crate::api2::types::{Authid, APTUpdateInfo, NODE_SCHEMA, UPID_SCHEMA};
@ -202,9 +203,34 @@ fn apt_get_changelog(
let changelog_url = &pkg_info[0].change_log_url;
// FIXME: use 'apt-get changelog' for proxmox packages as well, once repo supports it
if changelog_url.starts_with("http://download.proxmox.com/") {
let changelog = crate::tools::runtime::block_on(http::get_string(changelog_url))
let changelog = crate::tools::runtime::block_on(http::get_string(changelog_url, None))
.map_err(|err| format_err!("Error downloading changelog from '{}': {}", changelog_url, err))?;
return Ok(json!(changelog));
} else if changelog_url.starts_with("https://enterprise.proxmox.com/") {
let sub = match subscription::read_subscription()? {
Some(sub) => sub,
None => bail!("cannot retrieve changelog from enterprise repo: no subscription info found")
};
let (key, id) = match sub.key {
Some(key) => {
match sub.serverid {
Some(id) => (key, id),
None =>
bail!("cannot retrieve changelog from enterprise repo: no server id found")
}
},
None => bail!("cannot retrieve changelog from enterprise repo: no subscription key found")
};
let mut auth_header = HashMap::new();
auth_header.insert("Authorization".to_owned(),
format!("Basic {}", base64::encode(format!("{}:{}", key, id))));
let changelog = crate::tools::runtime::block_on(http::get_string(changelog_url, Some(&auth_header)))
.map_err(|err| format_err!("Error downloading changelog from '{}': {}", changelog_url, err))?;
return Ok(json!(changelog));
} else {
let mut command = std::process::Command::new("apt-get");
command.arg("changelog");
@ -215,12 +241,113 @@ fn apt_get_changelog(
}
}
#[api(
input: {
properties: {
node: {
schema: NODE_SCHEMA,
},
},
},
returns: {
description: "List of more relevant packages.",
type: Array,
items: {
type: APTUpdateInfo,
},
},
access: {
permission: &Permission::Privilege(&[], PRIV_SYS_AUDIT, false),
},
)]
/// Get package information for important Proxmox Backup Server packages.
pub fn get_versions() -> Result<Value, Error> {
const PACKAGES: &[&str] = &[
"ifupdown2",
"libjs-extjs",
"proxmox-backup",
"proxmox-backup-docs",
"proxmox-backup-client",
"proxmox-backup-server",
"proxmox-mini-journalreader",
"proxmox-widget-toolkit",
"pve-xtermjs",
"smartmontools",
"zfsutils-linux",
];
fn unknown_package(package: String) -> APTUpdateInfo {
APTUpdateInfo {
package,
title: "unknown".into(),
arch: "unknown".into(),
description: "unknown".into(),
version: "unknown".into(),
old_version: "unknown".into(),
origin: "unknown".into(),
priority: "unknown".into(),
section: "unknown".into(),
change_log_url: "unknown".into(),
}
}
let is_kernel = |name: &str| name.starts_with("pve-kernel-");
let mut packages: Vec<APTUpdateInfo> = Vec::new();
let pbs_packages = apt::list_installed_apt_packages(
|filter| {
filter.installed_version == Some(filter.active_version)
&& (is_kernel(filter.package) || PACKAGES.contains(&filter.package))
},
None,
);
if let Some(proxmox_backup) = pbs_packages.iter().find(|pkg| pkg.package == "proxmox-backup") {
packages.push(proxmox_backup.to_owned());
} else {
packages.push(unknown_package("proxmox-backup".into()));
}
if let Some(pkg) = pbs_packages.iter().find(|pkg| pkg.package == "proxmox-backup-server") {
packages.push(pkg.to_owned());
}
let mut kernel_pkgs: Vec<APTUpdateInfo> = pbs_packages
.iter()
.filter(|pkg| is_kernel(&pkg.package))
.cloned()
.collect();
// make sure the cache mutex gets dropped before the next call to list_installed_apt_packages
{
let cache = apt_pkg_native::Cache::get_singleton();
kernel_pkgs.sort_by(|left, right| {
cache
.compare_versions(&left.old_version, &right.old_version)
.reverse()
});
}
packages.append(&mut kernel_pkgs);
// add entry for all packages we're interested in, even if not installed
for pkg in PACKAGES.iter() {
if pkg == &"proxmox-backup" || pkg == &"proxmox-backup-server" {
continue;
}
match pbs_packages.iter().find(|item| &item.package == pkg) {
Some(apt_pkg) => packages.push(apt_pkg.to_owned()),
None => packages.push(unknown_package(pkg.to_string())),
}
}
Ok(json!(packages))
}
const SUBDIRS: SubdirMap = &[
("changelog", &Router::new().get(&API_METHOD_APT_GET_CHANGELOG)),
("update", &Router::new()
.get(&API_METHOD_APT_UPDATE_AVAILABLE)
.post(&API_METHOD_APT_UPDATE_DATABASE)
),
("versions", &Router::new().get(&API_METHOD_GET_VERSIONS)),
];
pub const ROUTER: Router = Router::new()

View File

@ -142,6 +142,18 @@ pub fn create_datastore_disk(
bail!("disk '{}' is already in use.", disk);
}
let mount_point = format!("/mnt/datastore/{}", &name);
// check if the default path does exist already and bail if it does
let default_path = std::path::PathBuf::from(&mount_point);
match std::fs::metadata(&default_path) {
Err(_) => {}, // path does not exist
Ok(_) => {
bail!("path {:?} already exists", default_path);
}
}
let upid_str = WorkerTask::new_thread(
"dircreate", Some(name.clone()), auth_id, to_stdout, move |worker|
{
@ -160,7 +172,7 @@ pub fn create_datastore_disk(
let uuid = get_fs_uuid(&partition)?;
let uuid_path = format!("/dev/disk/by-uuid/{}", uuid);
let (mount_unit_name, mount_point) = create_datastore_mount_unit(&name, filesystem, &uuid_path)?;
let mount_unit_name = create_datastore_mount_unit(&name, &mount_point, filesystem, &uuid_path)?;
systemd::reload_daemon()?;
systemd::enable_unit(&mount_unit_name)?;
@ -243,11 +255,11 @@ pub const ROUTER: Router = Router::new()
fn create_datastore_mount_unit(
datastore_name: &str,
mount_point: &str,
fs_type: FileSystemType,
what: &str,
) -> Result<(String, String), Error> {
) -> Result<String, Error> {
let mount_point = format!("/mnt/datastore/{}", datastore_name);
let mut mount_unit_name = systemd::escape_unit(&mount_point, true);
mount_unit_name.push_str(".mount");
@ -265,7 +277,7 @@ fn create_datastore_mount_unit(
let mount = SystemdMountSection {
What: what.to_string(),
Where: mount_point.clone(),
Where: mount_point.to_string(),
Type: Some(fs_type.to_string()),
Options: Some(String::from("defaults")),
..Default::default()
@ -278,5 +290,5 @@ fn create_datastore_mount_unit(
systemd::config::save_systemd_mount(&mount_unit_path, &config)?;
Ok((mount_unit_name, mount_point))
Ok(mount_unit_name)
}

View File

@ -243,7 +243,7 @@ pub fn zpool_details(
permission: &Permission::Privilege(&["system", "disks"], PRIV_SYS_MODIFY, false),
},
)]
/// Create a new ZFS pool.
/// Create a new ZFS pool. Will be mounted under '/mnt/datastore/<name>'.
pub fn create_zpool(
name: String,
devices: String,
@ -303,10 +303,11 @@ pub fn create_zpool(
bail!("{:?} needs at least {} disks.", raidlevel, min_disks);
}
let mount_point = format!("/mnt/datastore/{}", &name);
// check if the default path does exist already and bail if it does
// otherwise we get an error on mounting
let mut default_path = std::path::PathBuf::from("/");
default_path.push(&name);
// otherwise 'zpool create' aborts after partitioning, but before creating the pool
let default_path = std::path::PathBuf::from(&mount_point);
match std::fs::metadata(&default_path) {
Err(_) => {}, // path does not exist
@ -322,7 +323,7 @@ pub fn create_zpool(
let mut command = std::process::Command::new("zpool");
command.args(&["create", "-o", &format!("ashift={}", ashift), &name]);
command.args(&["create", "-o", &format!("ashift={}", ashift), "-m", &mount_point, &name]);
match raidlevel {
ZfsRaidLevel::Single => {
@ -371,7 +372,6 @@ pub fn create_zpool(
}
if add_datastore {
let mount_point = format!("/{}", name);
crate::api2::config::datastore::create_datastore(json!({ "name": name, "path": mount_point }))?
}

View File

@ -71,6 +71,36 @@ fn check_job_privs(auth_id: &Authid, user_info: &CachedUserInfo, upid: &UPID) ->
bail!("not a scheduled job task");
}
// get the store out of the worker_id
fn check_job_store(upid: &UPID, store: &str) -> bool {
match (upid.worker_type.as_str(), &upid.worker_id) {
(workertype, Some(workerid)) if workertype.starts_with("verif") => {
if let Some(captures) = VERIFICATION_JOB_WORKER_ID_REGEX.captures(&workerid) {
if let Some(jobstore) = captures.get(1) {
return store == jobstore.as_str();
}
} else {
return workerid == store;
}
}
("syncjob", Some(workerid)) => {
if let Some(captures) = SYNC_JOB_WORKER_ID_REGEX.captures(&workerid) {
if let Some(local_store) = captures.get(3) {
return store == local_store.as_str();
}
}
}
("prune", Some(workerid))
| ("backup", Some(workerid))
| ("garbage_collection", Some(workerid)) => {
return workerid == store || workerid.starts_with(&format!("{}:", store));
}
_ => {}
};
false
}
fn check_task_access(auth_id: &Authid, upid: &UPID) -> Result<(), Error> {
let task_auth_id = &upid.auth_id;
if auth_id == task_auth_id
@ -455,21 +485,8 @@ pub fn list_tasks(
}
if let Some(store) = store {
// Note: useful to select all tasks spawned by proxmox-backup-client
let worker_id = match &info.upid.worker_id {
Some(w) => w,
None => return None, // skip
};
if info.upid.worker_type == "backup" || info.upid.worker_type == "restore" ||
info.upid.worker_type == "prune"
{
let prefix = format!("{}:", store);
if !worker_id.starts_with(&prefix) { return None; }
} else if info.upid.worker_type == "garbage_collection" {
if worker_id != store { return None; }
} else {
return None; // skip
if !check_job_store(&info.upid, store) {
return None;
}
}

View File

@ -50,7 +50,7 @@ pub async fn get_pull_parameters(
let (remote_config, _digest) = remote::config()?;
let remote: remote::Remote = remote_config.lookup("remote", remote)?;
let src_repo = BackupRepository::new(Some(remote.userid.clone()), Some(remote.host.clone()), remote.port, remote_store.to_string());
let src_repo = BackupRepository::new(Some(remote.auth_id.clone()), Some(remote.host.clone()), remote.port, remote_store.to_string());
let client = crate::api2::config::remote::remote_client(remote).await?;

View File

@ -103,6 +103,7 @@ fn datastore_status(
"total": status.total,
"used": status.used,
"avail": status.avail,
"gc-status": datastore.last_gc_status(),
});
let rrd_dir = format!("datastore/{}", store);
@ -152,6 +153,8 @@ fn datastore_status(
}
}
entry["history-start"] = start.into();
entry["history-delta"] = reso.into();
entry["history"] = history.into();
// we skip the calculation for datastores with not enough data

View File

@ -1153,7 +1153,7 @@ pub enum RRDTimeFrameResolution {
}
#[api()]
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
/// Describes a package for which an update is available.
pub struct APTUpdateInfo {

View File

@ -498,28 +498,38 @@ pub fn verify_all_backups(
) -> Result<Vec<String>, Error> {
let mut errors = Vec::new();
task_log!(worker, "verify datastore {}", datastore.name());
if let Some(owner) = &owner {
task_log!(
worker,
"verify datastore {} - limiting to backups owned by {}",
datastore.name(),
owner
);
task_log!(worker, "limiting to backups owned by {}", owner);
}
let filter_by_owner = |group: &BackupGroup| {
if let Some(owner) = &owner {
match datastore.get_owner(group) {
Ok(ref group_owner) => {
group_owner == owner
|| (group_owner.is_token()
&& !owner.is_token()
&& group_owner.user() == owner.user())
},
Err(_) => false,
}
} else {
true
match (datastore.get_owner(group), &owner) {
(Ok(ref group_owner), Some(owner)) => {
group_owner == owner
|| (group_owner.is_token()
&& !owner.is_token()
&& group_owner.user() == owner.user())
},
(Ok(_), None) => true,
(Err(err), Some(_)) => {
// intentionally not in task log
// the task user might not be allowed to see this group!
println!("Failed to get owner of group '{}' - {}", group, err);
false
},
(Err(err), None) => {
// we don't filter by owner, but we want to log the error
task_log!(
worker,
"Failed to get owner of group '{} - {}",
group,
err,
);
errors.push(group.to_string());
true
},
}
};
@ -532,8 +542,7 @@ pub fn verify_all_backups(
Err(err) => {
task_log!(
worker,
"verify datastore {} - unable to list backups: {}",
datastore.name(),
"unable to list backups: {}",
err,
);
return Ok(errors);
@ -553,7 +562,7 @@ pub fn verify_all_backups(
// start with 64 chunks since we assume there are few corrupt ones
let corrupt_chunks = Arc::new(Mutex::new(HashSet::with_capacity(64)));
task_log!(worker, "verify datastore {} ({} snapshots)", datastore.name(), snapshot_count);
task_log!(worker, "found {} snapshots", snapshot_count);
let mut done = 0;
for group in list {

View File

@ -76,6 +76,7 @@ async fn run() -> Result<(), Error> {
})
)
},
"proxmox-backup.service",
);
server::write_pid(buildcfg::PROXMOX_BACKUP_API_PID_FN)?;

View File

@ -193,8 +193,12 @@ pub fn complete_repository(_arg: &str, _param: &HashMap<String, String>) -> Vec<
result
}
fn connect(server: &str, port: u16, auth_id: &Authid) -> Result<HttpClient, Error> {
fn connect(repo: &BackupRepository) -> Result<HttpClient, Error> {
connect_do(repo.host(), repo.port(), repo.auth_id())
.map_err(|err| format_err!("error building client for repository {} - {}", repo, err))
}
fn connect_do(server: &str, port: u16, auth_id: &Authid) -> Result<HttpClient, Error> {
let fingerprint = std::env::var(ENV_VAR_PBS_FINGERPRINT).ok();
use std::env::VarError::*;
@ -366,7 +370,7 @@ async fn list_backup_groups(param: Value) -> Result<Value, Error> {
let repo = extract_repository_from_value(&param)?;
let client = connect(repo.host(), repo.port(), repo.auth_id())?;
let client = connect(&repo)?;
let path = format!("api2/json/admin/datastore/{}/groups", repo.store());
@ -435,7 +439,7 @@ async fn change_backup_owner(group: String, mut param: Value) -> Result<(), Erro
let repo = extract_repository_from_value(&param)?;
let mut client = connect(repo.host(), repo.port(), repo.auth_id())?;
let mut client = connect(&repo)?;
param.as_object_mut().unwrap().remove("repository");
@ -478,7 +482,7 @@ async fn list_snapshots(param: Value) -> Result<Value, Error> {
let output_format = get_output_format(&param);
let client = connect(repo.host(), repo.port(), repo.auth_id())?;
let client = connect(&repo)?;
let group: Option<BackupGroup> = if let Some(path) = param["group"].as_str() {
Some(path.parse()?)
@ -543,7 +547,7 @@ async fn forget_snapshots(param: Value) -> Result<Value, Error> {
let path = tools::required_string_param(&param, "snapshot")?;
let snapshot: BackupDir = path.parse()?;
let mut client = connect(repo.host(), repo.port(), repo.auth_id())?;
let mut client = connect(&repo)?;
let path = format!("api2/json/admin/datastore/{}/snapshots", repo.store());
@ -573,7 +577,7 @@ async fn api_login(param: Value) -> Result<Value, Error> {
let repo = extract_repository_from_value(&param)?;
let client = connect(repo.host(), repo.port(), repo.auth_id())?;
let client = connect(&repo)?;
client.login().await?;
record_repository(&repo);
@ -630,7 +634,7 @@ async fn api_version(param: Value) -> Result<(), Error> {
let repo = extract_repository_from_value(&param);
if let Ok(repo) = repo {
let client = connect(repo.host(), repo.port(), repo.auth_id())?;
let client = connect(&repo)?;
match client.get("api2/json/version", None).await {
Ok(mut result) => version_info["server"] = result["data"].take(),
@ -680,7 +684,7 @@ async fn list_snapshot_files(param: Value) -> Result<Value, Error> {
let output_format = get_output_format(&param);
let client = connect(repo.host(), repo.port(), repo.auth_id())?;
let client = connect(&repo)?;
let path = format!("api2/json/admin/datastore/{}/files", repo.store());
@ -724,7 +728,7 @@ async fn start_garbage_collection(param: Value) -> Result<Value, Error> {
let output_format = get_output_format(&param);
let mut client = connect(repo.host(), repo.port(), repo.auth_id())?;
let mut client = connect(&repo)?;
let path = format!("api2/json/admin/datastore/{}/gc", repo.store());
@ -1036,7 +1040,7 @@ async fn create_backup(
let backup_time = backup_time_opt.unwrap_or_else(|| epoch_i64());
let client = connect(repo.host(), repo.port(), repo.auth_id())?;
let client = connect(&repo)?;
record_repository(&repo);
println!("Starting backup: {}/{}/{}", backup_type, backup_id, BackupDir::backup_time_to_string(backup_time)?);
@ -1339,7 +1343,7 @@ async fn restore(param: Value) -> Result<Value, Error> {
let archive_name = tools::required_string_param(&param, "archive-name")?;
let client = connect(repo.host(), repo.port(), repo.auth_id())?;
let client = connect(&repo)?;
record_repository(&repo);
@ -1512,7 +1516,7 @@ async fn upload_log(param: Value) -> Result<Value, Error> {
let snapshot = tools::required_string_param(&param, "snapshot")?;
let snapshot: BackupDir = snapshot.parse()?;
let mut client = connect(repo.host(), repo.port(), repo.auth_id())?;
let mut client = connect(&repo)?;
let (keydata, crypt_mode) = keyfile_parameters(&param)?;
@ -1583,7 +1587,7 @@ fn prune<'a>(
async fn prune_async(mut param: Value) -> Result<Value, Error> {
let repo = extract_repository_from_value(&param)?;
let mut client = connect(repo.host(), repo.port(), repo.auth_id())?;
let mut client = connect(&repo)?;
let path = format!("api2/json/admin/datastore/{}/prune", repo.store());
@ -1669,7 +1673,7 @@ async fn status(param: Value) -> Result<Value, Error> {
let output_format = get_output_format(&param);
let client = connect(repo.host(), repo.port(), repo.auth_id())?;
let client = connect(&repo)?;
let path = format!("api2/json/admin/datastore/{}/status", repo.store());

View File

@ -133,6 +133,7 @@ async fn run() -> Result<(), Error> {
.map(|_| ())
)
},
"proxmox-backup-proxy.service",
);
server::write_pid(buildcfg::PROXMOX_BACKUP_PROXY_PID_FN)?;
@ -592,9 +593,9 @@ async fn schedule_task_log_rotate() {
.ok_or_else(|| format_err!("could not get API auth log file names"))?;
if logrotate.rotate(max_size, None, Some(max_files))? {
worker.log(format!("API access log was rotated"));
worker.log(format!("API authentication log was rotated"));
} else {
worker.log(format!("API access log was not rotated"));
worker.log(format!("API authentication log was not rotated"));
}
Ok(())

View File

@ -225,7 +225,7 @@ async fn test_upload_speed(
let backup_time = proxmox::tools::time::epoch_i64();
let client = connect(repo.host(), repo.port(), repo.auth_id())?;
let client = connect(&repo)?;
record_repository(&repo);
if verbose { eprintln!("Connecting to backup server"); }

View File

@ -79,7 +79,7 @@ async fn dump_catalog(param: Value) -> Result<Value, Error> {
}
};
let client = connect(repo.host(), repo.port(), repo.auth_id())?;
let client = connect(&repo)?;
let client = BackupReader::start(
client,
@ -153,7 +153,7 @@ async fn dump_catalog(param: Value) -> Result<Value, Error> {
/// Shell to interactively inspect and restore snapshots.
async fn catalog_shell(param: Value) -> Result<(), Error> {
let repo = extract_repository_from_value(&param)?;
let client = connect(repo.host(), repo.port(), repo.auth_id())?;
let client = connect(&repo)?;
let path = tools::required_string_param(&param, "snapshot")?;
let archive_name = tools::required_string_param(&param, "archive-name")?;

View File

@ -163,7 +163,7 @@ fn mount(
async fn mount_do(param: Value, pipe: Option<RawFd>) -> Result<Value, Error> {
let repo = extract_repository_from_value(&param)?;
let archive_name = tools::required_string_param(&param, "archive-name")?;
let client = connect(repo.host(), repo.port(), repo.auth_id())?;
let client = connect(&repo)?;
let target = param["target"].as_str();

View File

@ -48,7 +48,7 @@ async fn task_list(param: Value) -> Result<Value, Error> {
let output_format = get_output_format(&param);
let repo = extract_repository_from_value(&param)?;
let client = connect(repo.host(), repo.port(), repo.auth_id())?;
let client = connect(&repo)?;
let limit = param["limit"].as_u64().unwrap_or(50) as usize;
let running = !param["all"].as_bool().unwrap_or(false);
@ -96,7 +96,7 @@ async fn task_log(param: Value) -> Result<Value, Error> {
let repo = extract_repository_from_value(&param)?;
let upid = tools::required_string_param(&param, "upid")?;
let client = connect(repo.host(), repo.port(), repo.auth_id())?;
let client = connect(&repo)?;
display_task_log(client, upid, true).await?;
@ -122,7 +122,7 @@ async fn task_stop(param: Value) -> Result<Value, Error> {
let repo = extract_repository_from_value(&param)?;
let upid_str = tools::required_string_param(&param, "upid")?;
let mut client = connect(repo.host(), repo.port(), repo.auth_id())?;
let mut client = connect(&repo)?;
let path = format!("api2/json/nodes/localhost/tasks/{}", upid_str);
let _ = client.delete(&path, None).await?;

View File

@ -528,7 +528,16 @@ pub async fn pull_store(
for (groups_done, item) in list.into_iter().enumerate() {
let group = BackupGroup::new(&item.backup_type, &item.backup_id);
let (owner, _lock_guard) = tgt_store.create_locked_backup_group(&group, &auth_id)?;
let (owner, _lock_guard) = match tgt_store.create_locked_backup_group(&group, &auth_id) {
Ok(result) => result,
Err(err) => {
worker.log(format!("sync group {}/{} failed - group lock failed: {}",
item.backup_type, item.backup_id, err));
errors = true; // do not stop here, instead continue
continue;
}
};
// permission check
if auth_id != owner { // only the owner is allowed to create additional snapshots
worker.log(format!("sync group {}/{} failed - owner check failed ({} != {})",

View File

@ -44,7 +44,7 @@ pub const REMOTE_PASSWORD_SCHEMA: Schema = StringSchema::new("Password or auth t
description: "The (optional) port",
type: u16,
},
userid: {
"auth-id": {
type: Authid,
},
password: {
@ -57,6 +57,7 @@ pub const REMOTE_PASSWORD_SCHEMA: Schema = StringSchema::new("Password or auth t
}
)]
#[derive(Serialize,Deserialize)]
#[serde(rename_all = "kebab-case")]
/// Remote properties.
pub struct Remote {
pub name: String,
@ -65,7 +66,7 @@ pub struct Remote {
pub host: String,
#[serde(skip_serializing_if="Option::is_none")]
pub port: Option<u16>,
pub userid: Authid,
pub auth_id: Authid,
#[serde(skip_serializing_if="String::is_empty")]
#[serde(with = "proxmox::tools::serde::string_as_base64")]
pub password: String,

View File

@ -89,7 +89,21 @@ struct HardLinkInfo {
st_ino: u64,
}
/// In case we want to collect them or redirect them we can just add this here:
/// TODO: make a builder for the create_archive call for fewer parameters and add a method to add a
/// logger which does not write to stderr.
struct Logger;
impl std::io::Write for Logger {
fn write(&mut self, data: &[u8]) -> io::Result<usize> {
std::io::stderr().write(data)
}
fn flush(&mut self) -> io::Result<()> {
std::io::stderr().flush()
}
}
/// And the error case.
struct ErrorReporter;
impl std::io::Write for ErrorReporter {
@ -116,6 +130,7 @@ struct Archiver<'a, 'b> {
device_set: Option<HashSet<u64>>,
hardlinks: HashMap<HardLinkInfo, (PathBuf, LinkOffset)>,
errors: ErrorReporter,
logger: Logger,
file_copy_buffer: Vec<u8>,
}
@ -181,6 +196,7 @@ where
device_set,
hardlinks: HashMap::new(),
errors: ErrorReporter,
logger: Logger,
file_copy_buffer: vec::undefined(4 * 1024 * 1024),
};
@ -632,6 +648,7 @@ impl<'a, 'b> Archiver<'a, 'b> {
}
let result = if skip_contents {
writeln!(self.logger, "skipping mount point: {:?}", self.path)?;
Ok(())
} else {
self.archive_dir_contents(&mut encoder, dir, false)

View File

@ -128,19 +128,26 @@ fn get_changelog_url(
None => bail!("incompatible filename, doesn't match regex")
};
return Ok(format!("http://download.proxmox.com/{}/{}_{}.changelog",
base, package, version));
if component == "pbs-enterprise" {
return Ok(format!("https://enterprise.proxmox.com/{}/{}_{}.changelog",
base, package, version));
} else {
return Ok(format!("http://download.proxmox.com/{}/{}_{}.changelog",
base, package, version));
}
}
bail!("unknown origin ({}) or component ({})", origin, component)
}
pub struct FilterData<'a> {
// this is version info returned by APT
/// package name
pub package: &'a str,
/// this is version info returned by APT
pub installed_version: Option<&'a str>,
pub candidate_version: &'a str,
// this is the version info the filter is supposed to check
/// this is the version info the filter is supposed to check
pub active_version: &'a str,
}
@ -270,6 +277,7 @@ where
let mut long_desc = "".to_owned();
let fd = FilterData {
package: package.as_str(),
installed_version: current_version.as_deref(),
candidate_version: &candidate_version,
active_version: &version,

View File

@ -16,18 +16,18 @@ pub enum EitherStream<L, R> {
Right(R),
}
impl<L: AsyncRead, R: AsyncRead> AsyncRead for EitherStream<L, R> {
impl<L: AsyncRead + Unpin, R: AsyncRead + Unpin> AsyncRead for EitherStream<L, R> {
fn poll_read(
self: Pin<&mut Self>,
cx: &mut Context,
buf: &mut [u8],
) -> Poll<Result<usize, io::Error>> {
match unsafe { self.get_unchecked_mut() } {
match self.get_mut() {
EitherStream::Left(ref mut s) => {
unsafe { Pin::new_unchecked(s) }.poll_read(cx, buf)
Pin::new(s).poll_read(cx, buf)
}
EitherStream::Right(ref mut s) => {
unsafe { Pin::new_unchecked(s) }.poll_read(cx, buf)
Pin::new(s).poll_read(cx, buf)
}
}
}
@ -47,51 +47,51 @@ impl<L: AsyncRead, R: AsyncRead> AsyncRead for EitherStream<L, R> {
where
B: bytes::BufMut,
{
match unsafe { self.get_unchecked_mut() } {
match self.get_mut() {
EitherStream::Left(ref mut s) => {
unsafe { Pin::new_unchecked(s) }.poll_read_buf(cx, buf)
Pin::new(s).poll_read_buf(cx, buf)
}
EitherStream::Right(ref mut s) => {
unsafe { Pin::new_unchecked(s) }.poll_read_buf(cx, buf)
Pin::new(s).poll_read_buf(cx, buf)
}
}
}
}
impl<L: AsyncWrite, R: AsyncWrite> AsyncWrite for EitherStream<L, R> {
impl<L: AsyncWrite + Unpin, R: AsyncWrite + Unpin> AsyncWrite for EitherStream<L, R> {
fn poll_write(
self: Pin<&mut Self>,
cx: &mut Context,
buf: &[u8],
) -> Poll<Result<usize, io::Error>> {
match unsafe { self.get_unchecked_mut() } {
match self.get_mut() {
EitherStream::Left(ref mut s) => {
unsafe { Pin::new_unchecked(s) }.poll_write(cx, buf)
Pin::new(s).poll_write(cx, buf)
}
EitherStream::Right(ref mut s) => {
unsafe { Pin::new_unchecked(s) }.poll_write(cx, buf)
Pin::new(s).poll_write(cx, buf)
}
}
}
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Result<(), io::Error>> {
match unsafe { self.get_unchecked_mut() } {
match self.get_mut() {
EitherStream::Left(ref mut s) => {
unsafe { Pin::new_unchecked(s) }.poll_flush(cx)
Pin::new(s).poll_flush(cx)
}
EitherStream::Right(ref mut s) => {
unsafe { Pin::new_unchecked(s) }.poll_flush(cx)
Pin::new(s).poll_flush(cx)
}
}
}
fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Result<(), io::Error>> {
match unsafe { self.get_unchecked_mut() } {
match self.get_mut() {
EitherStream::Left(ref mut s) => {
unsafe { Pin::new_unchecked(s) }.poll_shutdown(cx)
Pin::new(s).poll_shutdown(cx)
}
EitherStream::Right(ref mut s) => {
unsafe { Pin::new_unchecked(s) }.poll_shutdown(cx)
Pin::new(s).poll_shutdown(cx)
}
}
}
@ -104,12 +104,12 @@ impl<L: AsyncWrite, R: AsyncWrite> AsyncWrite for EitherStream<L, R> {
where
B: bytes::Buf,
{
match unsafe { self.get_unchecked_mut() } {
match self.get_mut() {
EitherStream::Left(ref mut s) => {
unsafe { Pin::new_unchecked(s) }.poll_write_buf(cx, buf)
Pin::new(s).poll_write_buf(cx, buf)
}
EitherStream::Right(ref mut s) => {
unsafe { Pin::new_unchecked(s) }.poll_write_buf(cx, buf)
Pin::new(s).poll_write_buf(cx, buf)
}
}
}
@ -180,7 +180,7 @@ pub struct HyperAccept<T>(pub T);
impl<T, I> hyper::server::accept::Accept for HyperAccept<T>
where
T: TryStream<Ok = I>,
T: TryStream<Ok = I> + Unpin,
I: AsyncRead + AsyncWrite,
{
type Conn = I;
@ -190,7 +190,7 @@ where
self: Pin<&mut Self>,
cx: &mut Context,
) -> Poll<Option<Result<Self::Conn, Self::Error>>> {
let this = unsafe { self.map_unchecked_mut(|this| &mut this.0) };
let this = Pin::new(&mut self.get_mut().0);
this.try_poll_next(cx)
}
}

View File

@ -260,6 +260,7 @@ impl Future for NotifyReady {
pub async fn create_daemon<F, S>(
address: std::net::SocketAddr,
create_service: F,
service_name: &str,
) -> Result<(), Error>
where
F: FnOnce(tokio::net::TcpListener, NotifyReady) -> Result<S, Error>,
@ -301,10 +302,39 @@ where
if let Some(future) = finish_future {
future.await;
}
// FIXME: this is a hack, replace with sd_notify_barrier when available
if server::is_reload_request() {
wait_service_is_active(service_name).await?;
}
log::info!("daemon shut down...");
Ok(())
}
// hack, do not use if unsure!
async fn wait_service_is_active(service: &str) -> Result<(), Error> {
tokio::time::delay_for(std::time::Duration::new(1, 0)).await;
loop {
let text = match tokio::process::Command::new("systemctl")
.args(&["is-active", service])
.output()
.await
{
Ok(output) => match String::from_utf8(output.stdout) {
Ok(text) => text,
Err(err) => bail!("output of 'systemctl is-active' not valid UTF-8 - {}", err),
},
Err(err) => bail!("executing 'systemctl is-active' failed - {}", err),
};
if text.trim().trim_start() != "reloading" {
return Ok(());
}
tokio::time::delay_for(std::time::Duration::new(5, 0)).await;
}
}
#[link(name = "systemd")]
extern "C" {
fn sd_notify(unset_environment: c_int, state: *const c_char) -> c_int;

View File

@ -2,6 +2,7 @@ use anyhow::{Error, format_err, bail};
use lazy_static::lazy_static;
use std::task::{Context, Poll};
use std::os::unix::io::AsRawFd;
use std::collections::HashMap;
use hyper::{Uri, Body};
use hyper::client::{Client, HttpConnector};
@ -26,8 +27,21 @@ lazy_static! {
};
}
pub async fn get_string(uri: &str) -> Result<String, Error> {
let res = HTTP_CLIENT.get(uri.parse()?).await?;
pub async fn get_string(uri: &str, extra_headers: Option<&HashMap<String, String>>) -> Result<String, Error> {
let mut request = Request::builder()
.method("GET")
.uri(uri)
.header("User-Agent", "proxmox-backup-client/1.0");
if let Some(hs) = extra_headers {
for (h, v) in hs.iter() {
request = request.header(h, v);
}
}
let request = request.body(Body::empty())?;
let res = HTTP_CLIENT.request(request).await?;
let status = res.status();
if !status.is_success() {
@ -115,7 +129,12 @@ impl hyper::service::Service<Uri> for HttpsConnector {
.to_string();
let config = this.ssl_connector.configure();
let conn = this.http.call(dst).await?;
let dst_str = dst.to_string(); // for error messages
let conn = this
.http
.call(dst)
.await
.map_err(|err| format_err!("error connecting to {} - {}", dst_str, err))?;
let _ = set_tcp_keepalive(conn.as_raw_fd(), PROXMOX_BACKUP_TCP_KEEPALIVE_TIME);

View File

@ -7,6 +7,7 @@ use std::task::{Context, Poll, RawWaker, Waker};
use std::thread::{self, Thread};
use lazy_static::lazy_static;
use pin_utils::pin_mut;
use tokio::runtime::{self, Runtime};
thread_local! {
@ -154,9 +155,8 @@ pub fn main<F: Future>(fut: F) -> F::Output {
block_on(fut)
}
fn block_on_local_future<F: Future>(mut fut: F) -> F::Output {
use std::pin::Pin;
let mut fut = unsafe { Pin::new_unchecked(&mut fut) };
fn block_on_local_future<F: Future>(fut: F) -> F::Output {
pin_mut!(fut);
let waker = Arc::new(thread::current());
let waker = thread_waker_clone(Arc::into_raw(waker) as *const ());

View File

@ -36,8 +36,7 @@ Ext.define('PBS.Application', {
xtype: view,
});
if (skipCheck !== true) {
// fixme:
// Proxmox.Utils.checked_command(function() {}); // display subscription status
Proxmox.Utils.checked_command(Ext.emptyFn);
}
},

View File

@ -332,12 +332,13 @@ Ext.define('PBS.Dashboard', {
Ext.String.format(gettext('{0} days'), '{days}') + ')',
},
xtype: 'pbsTaskSummary',
height: 200,
reference: 'tasksummary',
},
{
iconCls: 'fa fa-ticket',
title: 'Subscription',
height: 166,
height: 200,
reference: 'subscription',
xtype: 'pbsSubscriptionInfo',
},
@ -394,7 +395,7 @@ Ext.define('PBS.dashboard.SubscriptionInfo', {
break;
case 0:
icon = 'times-circle critical';
message = gettext('This node does not have a subscription.');
message = gettext('<h1>No valid subscription</h1>' + PBS.Utils.noSubKeyHtml);
break;
default:
throw 'invalid subscription status';

View File

@ -202,11 +202,6 @@ Ext.define('PBS.MainView', {
padding: '0 0 0 5',
xtype: 'versioninfo',
},
{
padding: 5,
html: '<a href="https://bugzilla.proxmox.com" target="_blank">BETA</a>',
baseCls: 'x-plain',
},
{
flex: 1,
baseCls: 'x-plain',

View File

@ -43,6 +43,8 @@ JSSRC= \
panel/Tasks.js \
panel/XtermJsConsole.js \
panel/AccessControl.js \
panel/StorageAndDisks.js \
panel/UsageChart.js \
ZFSList.js \
DirectoryList.js \
LoginView.js \
@ -56,6 +58,8 @@ JSSRC= \
datastore/Content.js \
datastore/OptionView.js \
datastore/Panel.js \
datastore/DataStoreListSummary.js \
datastore/DataStoreList.js \
ServerStatus.js \
ServerAdministration.js \
Dashboard.js \

View File

@ -62,24 +62,10 @@ Ext.define('PBS.store.NavigationStore', {
leaf: true,
},
{
text: gettext('Disks'),
text: gettext('Storage / Disks'),
iconCls: 'fa fa-hdd-o',
path: 'pmxDiskList',
leaf: false,
children: [
{
text: Proxmox.Utils.directoryText,
iconCls: 'fa fa-folder',
path: 'pbsDirectoryList',
leaf: true,
},
{
text: "ZFS",
iconCls: 'fa fa-th-large',
path: 'pbsZFSList',
leaf: true,
},
],
path: 'pbsStorageAndDiskPanel',
leaf: true,
},
],
},
@ -87,6 +73,7 @@ Ext.define('PBS.store.NavigationStore', {
text: gettext('Datastore'),
iconCls: 'fa fa-archive',
id: 'datastores',
path: 'pbsDataStores',
expanded: true,
expandable: false,
leaf: false,
@ -174,9 +161,6 @@ Ext.define('PBS.view.main.NavigationTree', {
listeners: {
itemclick: function(tl, info) {
if (info.node.data.id === 'datastores') {
return false;
}
if (info.node.data.id === 'addbutton') {
let me = this;
Ext.create('PBS.DataStoreEdit', {

View File

@ -3,38 +3,138 @@ const proxmoxOnlineHelpInfo = {
"link": "/docs/index.html",
"title": "Proxmox Backup Server Documentation Index"
},
"id1": {
"link": "/docs/calendarevents.html#id1",
"title": "Calendar Events"
},
"id2": {
"link": "/docs/backup-client.html#id2",
"title": "Encryption"
},
"changing-backup-owner": {
"link": "/docs/backup-client.html#changing-backup-owner",
"title": "Changing the Owner of a Backup Group"
},
"backup-pruning": {
"link": "/docs/backup-client.html#backup-pruning",
"title": "Pruning and Removing Backups"
},
"id3": {
"link": "/docs/backup-client.html#id3",
"title": "Garbage Collection"
},
"pxar-format": {
"link": "/docs/file-formats.html#pxar-format",
"title": "Proxmox File Archive Format (``.pxar``)"
},
"sysadmin-package-repositories": {
"link": "/docs/package-repositories.html#sysadmin-package-repositories",
"title": "Debian Package Repositories"
},
"get-help": {
"link": "/docs/introduction.html#get-help",
"title": "Getting Help"
},
"chapter-zfs": {
"link": "/docs/sysadmin.html#chapter-zfs",
"title": "ZFS on Linux"
},
"local-zfs-special-device": {
"link": "/docs/sysadmin.html#local-zfs-special-device",
"title": "ZFS Special Device"
},
"maintenance-pruning": {
"link": "/docs/maintenance.html#maintenance-pruning",
"title": "Pruning"
},
"maintenance-gc": {
"link": "/docs/maintenance.html#maintenance-gc",
"title": "Garbage Collection"
},
"maintenance-verification": {
"link": "/docs/maintenance.html#maintenance-verification",
"title": "Verification"
},
"maintenance-notification": {
"link": "/docs/maintenance.html#maintenance-notification",
"title": "Notifications"
},
"backup-remote": {
"link": "/docs/managing-remotes.html#backup-remote",
"title": ":term:`Remote`"
"title": "Remote"
},
"syncjobs": {
"link": "/docs/managing-remotes.html#syncjobs",
"title": "Sync Jobs"
},
"sysadmin-network-configuration": {
"link": "/docs/network-management.html#sysadmin-network-configuration",
"title": "Network Management"
},
"pve-integration": {
"link": "/docs/pve-integration.html#pve-integration",
"title": "`Proxmox VE`_ Integration"
},
"rst-primer": {
"link": "/docs/reStructuredText-primer.html#rst-primer",
"title": "reStructuredText Primer"
},
"rst-inline-markup": {
"link": "/docs/reStructuredText-primer.html#rst-inline-markup",
"title": "Inline markup"
},
"rst-literal-blocks": {
"link": "/docs/reStructuredText-primer.html#rst-literal-blocks",
"title": "Literal blocks"
},
"rst-doctest-blocks": {
"link": "/docs/reStructuredText-primer.html#rst-doctest-blocks",
"title": "Doctest blocks"
},
"rst-tables": {
"link": "/docs/reStructuredText-primer.html#rst-tables",
"title": "Tables"
},
"rst-field-lists": {
"link": "/docs/reStructuredText-primer.html#rst-field-lists",
"title": "Field Lists"
},
"rst-roles-alt": {
"link": "/docs/reStructuredText-primer.html#rst-roles-alt",
"title": "Roles"
},
"rst-directives": {
"link": "/docs/reStructuredText-primer.html#rst-directives",
"title": "Directives"
},
"html-meta": {
"link": "/docs/reStructuredText-primer.html#html-meta",
"title": "HTML Metadata"
},
"storage-disk-management": {
"link": "/docs/storage.html#storage-disk-management",
"title": "Disk Management"
},
"datastore-intro": {
"link": "/docs/storage.html#datastore-intro",
"title": ":term:`DataStore`"
"title": "Datastore"
},
"storage-datastore-create": {
"link": "/docs/storage.html#storage-datastore-create",
"title": "Creating a Datastore"
},
"sysadmin-host-administration": {
"link": "/docs/sysadmin.html#sysadmin-host-administration",
"title": "Host System Administration"
},
"user-mgmt": {
"link": "/docs/user-management.html#user-mgmt",
"title": "User Management"
},
"user-tokens": {
"link": "/docs/user-management.html#user-tokens",
"title": "API Tokens"
},
"user-acl": {
"link": "/docs/user-management.html#user-acl",
"title": "Access Control"

View File

@ -7,6 +7,8 @@ Ext.define('PBS.ServerAdministration', {
border: true,
defaults: { border: false },
tools: [PBS.Utils.get_help_tool("sysadmin-host-administration")],
controller: {
xclass: 'Ext.app.ViewController',

View File

@ -51,6 +51,78 @@ Ext.define('PBS.ServerStatus', {
scrollable: true,
showVersions: function() {
let me = this;
// Note: use simply text/html here, as ExtJS grid has problems with cut&paste
let panel = Ext.createWidget('component', {
autoScroll: true,
id: 'pkgversions',
padding: 5,
style: {
'background-color': 'white',
'white-space': 'pre',
'font-family': 'monospace',
},
});
let win = Ext.create('Ext.window.Window', {
title: gettext('Package versions'),
width: 600,
height: 600,
layout: 'fit',
modal: true,
items: [panel],
buttons: [
{
xtype: 'button',
iconCls: 'fa fa-clipboard',
handler: function(button) {
window.getSelection().selectAllChildren(
document.getElementById('pkgversions'),
);
document.execCommand("copy");
},
text: gettext('Copy'),
},
{
text: gettext('Ok'),
handler: function() {
this.up('window').close();
},
},
],
});
Proxmox.Utils.API2Request({
waitMsgTarget: me,
url: `/nodes/localhost/apt/versions`,
method: 'GET',
failure: function(response, opts) {
win.close();
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
},
success: function(response, opts) {
let text = '';
Ext.Array.each(response.result.data, function(rec) {
let version = "not correctly installed";
let pkg = rec.Package;
if (rec.OldVersion && rec.OldVersion !== 'unknown') {
version = rec.OldVersion;
}
if (rec.ExtraInfo) {
text += `${pkg}: ${version} (${rec.ExtraInfo})\n`;
} else {
text += `${pkg}: ${version}\n`;
}
});
win.show();
panel.update(Ext.htmlEncode(text));
},
});
},
initComponent: function() {
var me = this;
@ -94,7 +166,15 @@ Ext.define('PBS.ServerStatus', {
},
});
me.tbar = [consoleBtn, restartBtn, shutdownBtn, '->', { xtype: 'proxmoxRRDTypeSelector' }];
let version_btn = new Ext.Button({
text: gettext('Package versions'),
iconCls: 'fa fa-gift',
handler: function() {
Proxmox.Utils.checked_command(function() { me.showVersions(); });
},
});
me.tbar = [version_btn, '-', consoleBtn, '-', restartBtn, shutdownBtn, '->', { xtype: 'proxmoxRRDTypeSelector' }];
var rrdstore = Ext.create('Proxmox.data.RRDStore', {
rrdurl: "/api2/json/nodes/localhost/rrd",

View File

@ -2,13 +2,14 @@ Ext.define('PBS.SubscriptionKeyEdit', {
extend: 'Proxmox.window.Edit',
title: gettext('Upload Subscription Key'),
width: 300,
width: 320,
autoLoad: true,
onlineHelp: 'getting_help',
onlineHelp: 'get_help',
items: {
xtype: 'textfield',
labelWidth: 120,
name: 'key',
value: '',
fieldLabel: gettext('Subscription Key'),
@ -22,7 +23,8 @@ Ext.define('PBS.Subscription', {
title: gettext('Subscription'),
border: true,
onlineHelp: 'getting_help',
onlineHelp: 'get_help',
tools: [PBS.Utils.get_help_tool("get_help")],
viewConfig: {
enableTextSelection: true,
@ -140,16 +142,18 @@ Ext.define('PBS.Subscription', {
tbar: [
{
text: gettext('Upload Subscription Key'),
iconCls: 'fa fa-ticket',
handler: function() {
let win = Ext.create('PBS.SubscriptionKeyEdit', {
url: '/api2/extjs/' + baseurl,
autoShow: true,
});
win.show();
win.on('destroy', reload);
},
},
{
text: gettext('Check'),
iconCls: 'fa fa-check-square-o',
handler: function() {
Proxmox.Utils.API2Request({
params: { force: 1 },
@ -171,10 +175,12 @@ Ext.define('PBS.Subscription', {
dangerous: true,
selModel: false,
callback: reload,
iconCls: 'fa fa-trash-o',
},
'-',
{
text: gettext('System Report'),
iconCls: 'fa fa-stethoscope',
handler: function() {
Proxmox.Utils.checked_command(function() { me.showReport(); });
},

View File

@ -6,6 +6,7 @@ Ext.define('PBS.SystemConfiguration', {
border: true,
scrollable: true,
defaults: { border: false },
tools: [PBS.Utils.get_help_tool("sysadmin-network-configuration")],
items: [
{
title: gettext('Network/Time'),

View File

@ -50,6 +50,8 @@ Ext.define('PBS.Utils', {
}
},
noSubKeyHtml: 'You do not have a valid subscription for this server. Please visit <a target="_blank" href="https://www.proxmox.com/proxmox-backup-server/pricing">www.proxmox.com</a> to get a list of available options.',
getDataStoreFromPath: function(path) {
return path.slice(PBS.Utils.dataStorePrefix.length);
},
@ -145,7 +147,7 @@ Ext.define('PBS.Utils', {
},
render_datastore_worker_id: function(id, what) {
const res = id.match(/^(\S+?)_(\S+?)_(\S+?)(_(.+))?$/);
const res = id.match(/^(\S+?):(\S+?)\/(\S+?)(\/(.+))?$/);
if (res) {
let datastore = res[1], backupGroup = `${res[2]}/${res[3]}`;
if (res[4] !== undefined) {
@ -159,6 +161,34 @@ Ext.define('PBS.Utils', {
return `Datastore ${what} ${id}`;
},
parse_datastore_worker_id: function(type, id) {
let result;
let res;
if (type.startsWith('verif')) {
res = PBS.Utils.VERIFICATION_JOB_ID_RE.exec(id);
if (res) {
result = res[1];
}
} else if (type.startsWith('sync')) {
res = PBS.Utils.SYNC_JOB_ID_RE.exec(id);
if (res) {
result = res[3];
}
} else if (type === 'backup') {
res = PBS.Utils.BACKUP_JOB_ID_RE.exec(id);
if (res) {
result = res[1];
}
} else if (type === 'garbage_collection') {
return id;
} else if (type === 'prune') {
return id;
}
return result;
},
extractTokenUser: function(tokenid) {
return tokenid.match(/^(.+)!([^!]+)$/)[1];
},
@ -167,9 +197,75 @@ Ext.define('PBS.Utils', {
return tokenid.match(/^(.+)!([^!]+)$/)[2];
},
render_estimate: function(value) {
if (!value) {
return gettext('Not enough data');
}
let now = new Date();
let estimate = new Date(value*1000);
let timespan = (estimate - now)/1000;
if (Number(estimate) <= Number(now) || isNaN(timespan)) {
return gettext('Never');
}
let duration = Proxmox.Utils.format_duration_human(timespan);
return Ext.String.format(gettext("in {0}"), duration);
},
render_size_usage: function(val, max) {
if (max === 0) {
return gettext('N/A');
}
return (val*100/max).toFixed(2) + '% (' +
Ext.String.format(gettext('{0} of {1}'),
Proxmox.Utils.format_size(val), Proxmox.Utils.format_size(max)) + ')';
},
get_help_tool: function(blockid) {
let info = Proxmox.Utils.get_help_info(blockid);
if (info === undefined) {
info = Proxmox.Utils.get_help_info('pbs_documentation_index');
}
if (info === undefined) {
throw "get_help_info failed"; // should not happen
}
let docsURI = window.location.origin + info.link;
let title = info.title;
if (info.subtitle) {
title += ' - ' + info.subtitle;
}
return {
type: 'help',
tooltip: title,
handler: function() {
window.open(docsURI);
},
};
},
calculate_dedup_factor: function(gcstatus) {
let dedup = 1.0;
if (gcstatus['disk-bytes'] > 0) {
dedup = (gcstatus['index-data-bytes'] || 0)/gcstatus['disk-bytes'];
}
return dedup;
},
constructor: function() {
var me = this;
let PROXMOX_SAFE_ID_REGEX = "([A-Za-z0-9_][A-Za-z0-9._-]*)";
// only anchored at beginning
// only parses datastore for now
me.VERIFICATION_JOB_ID_RE = new RegExp("^" + PROXMOX_SAFE_ID_REGEX + ':?');
me.SYNC_JOB_ID_RE = new RegExp("^" + PROXMOX_SAFE_ID_REGEX + ':' +
PROXMOX_SAFE_ID_REGEX + ':' + PROXMOX_SAFE_ID_REGEX + ':');
me.BACKUP_JOB_ID_RE = new RegExp("^" + PROXMOX_SAFE_ID_REGEX + ':');
// do whatever you want here
Proxmox.Utils.override_task_descriptions({
backup: (type, id) => PBS.Utils.render_datastore_worker_id(id, gettext('Backup')),

View File

@ -22,7 +22,12 @@ Ext.define('PBS.config.ACLView', {
title: gettext('Permissions'),
// Show only those permissions, which can affect this and children paths.
// That means that also higher up, "shorter" paths are included, as those
// can have a say in the rights on the asked path.
aclPath: undefined,
// tell API to only return ACLs matching exactly the aclPath config.
aclExact: undefined,
controller: {
@ -83,12 +88,37 @@ Ext.define('PBS.config.ACLView', {
let proxy = view.getStore().rstore.getProxy();
let params = {};
if (view.aclPath !== undefined) {
params.path = view.aclPath;
if (typeof view.aclPath === "string") {
let pathFilter = Ext.create('Ext.util.Filter', {
filterPath: view.aclPath,
filterAtoms: view.aclPath.split('/'),
filterFn: function(item) {
let me = this;
let path = item.data.path;
if (path === "/" || path === me.filterPath) {
return true;
} else if (path.length > me.filterPath.length) {
return path.startsWith(me.filterPath + '/');
}
let pathAtoms = path.split('/');
let commonLength = Math.min(pathAtoms.length, me.filterAtoms.length);
for (let i = 1; i < commonLength; i++) {
if (me.filterAtoms[i] !== pathAtoms[i]) {
return false;
}
}
return true;
},
});
view.getStore().addFilter(pathFilter);
}
if (view.aclExact !== undefined) {
if (view.aclPath !== undefined) {
params.path = view.aclPath;
}
params.exact = view.aclExact;
}
proxy.setExtraParams(params);
Proxmox.Utils.monStoreErrors(view, view.getStore().rstore);
},

View File

@ -1,6 +1,6 @@
Ext.define('pmx-remotes', {
extend: 'Ext.data.Model',
fields: ['name', 'host', 'port', 'userid', 'fingerprint', 'comment',
fields: ['name', 'host', 'port', 'auth-id', 'fingerprint', 'comment',
{
name: 'server',
calculate: function(data) {
@ -32,6 +32,8 @@ Ext.define('PBS.config.RemoteView', {
title: gettext('Remotes'),
tools: [PBS.Utils.get_help_tool("backup-remote")],
controller: {
xclass: 'Ext.app.ViewController',
@ -127,11 +129,11 @@ Ext.define('PBS.config.RemoteView', {
dataIndex: 'server',
},
{
header: gettext('User name'),
header: gettext('Auth ID'),
width: 200,
sortable: true,
renderer: Ext.String.htmlEncode,
dataIndex: 'userid',
dataIndex: 'auth-id',
},
{
header: gettext('Fingerprint'),

View File

@ -162,9 +162,11 @@ Ext.define('PBS.config.SyncJobView', {
reload: function() { this.getView().getStore().rstore.load(); },
init: function(view) {
view.getStore().rstore.getProxy().setExtraParams({
store: view.datastore,
});
let params = {};
if (view.datastore !== undefined) {
params.store = view.datastore;
}
view.getStore().rstore.getProxy().setExtraParams(params);
Proxmox.Utils.monStoreErrors(view, view.getStore().rstore);
},
},
@ -237,6 +239,12 @@ Ext.define('PBS.config.SyncJobView', {
flex: 1,
sortable: true,
},
{
header: gettext('Local Store'),
dataIndex: 'store',
width: 120,
sortable: true,
},
{
header: gettext('Remote'),
dataIndex: 'remote',
@ -249,12 +257,6 @@ Ext.define('PBS.config.SyncJobView', {
width: 120,
sortable: true,
},
{
header: gettext('Local Store'),
dataIndex: 'store',
width: 120,
sortable: true,
},
{
header: gettext('Owner'),
dataIndex: 'owner',
@ -304,4 +306,18 @@ Ext.define('PBS.config.SyncJobView', {
sortable: true,
},
],
initComponent: function() {
let me = this;
let hideLocalDatastore = !!me.datastore;
for (let column of me.columns) {
if (column.dataIndex === 'store') {
column.hidden = hideLocalDatastore;
break;
}
}
me.callParent();
},
});

View File

@ -157,9 +157,11 @@ Ext.define('PBS.config.VerifyJobView', {
reload: function() { this.getView().getStore().rstore.load(); },
init: function(view) {
view.getStore().rstore.getProxy().setExtraParams({
store: view.datastore,
});
let params = {};
if (view.datastore !== undefined) {
params.store = view.datastore;
}
view.getStore().rstore.getProxy().setExtraParams(params);
Proxmox.Utils.monStoreErrors(view, view.getStore().rstore);
},
},
@ -232,6 +234,11 @@ Ext.define('PBS.config.VerifyJobView', {
flex: 1,
sortable: true,
},
{
header: gettext('Datastore'),
dataIndex: 'store',
flex: 1,
},
{
header: gettext('Skip Verified'),
dataIndex: 'ignore-verified',
@ -288,4 +295,18 @@ Ext.define('PBS.config.VerifyJobView', {
sortable: true,
},
],
initComponent: function() {
let me = this;
let hideDatastore = !!me.datastore;
for (let column of me.columns) {
if (column.dataIndex === 'store') {
column.hidden = hideDatastore;
break;
}
}
me.callParent();
},
});

View File

@ -241,3 +241,15 @@ span.snapshot-comment-column {
font-weight: bold;
content: "V.";
}
.x-action-col-icon {
margin: 0 1px;
font-size: 14px;
}
.x-action-col-icon:before, .x-action-col-icon:after {
font-size: 14px;
}
.x-action-col-icon:hover:before, .x-action-col-icon:hover:after {
text-shadow: 1px 1px 1px #AAA;
font-weight: 800;
}

View File

@ -47,24 +47,6 @@ Ext.define('PBS.DatastoreStatistics', {
controller: {
xclass: 'Ext.app.ViewController',
render_estimate: function(value) {
if (!value) {
return gettext('Not enough data');
}
let now = new Date();
let estimate = new Date(value*1000);
let timespan = (estimate - now)/1000;
if (Number(estimate) <= Number(now) || isNaN(timespan)) {
return gettext('Never');
}
let duration = Proxmox.Utils.format_duration_human(timespan);
return Ext.String.format(gettext("in {0}"), duration);
},
init: function(view) {
Proxmox.Utils.monStoreErrors(view, view.getStore().rstore);
},
@ -111,7 +93,7 @@ Ext.define('PBS.DatastoreStatistics', {
text: gettext('Estimated Full'),
dataIndex: 'estimated-full-date',
sortable: true,
renderer: 'render_estimate',
renderer: PBS.Utils.render_estimate,
flex: 1,
minWidth: 130,
maxWidth: 200,

View File

@ -4,31 +4,38 @@ Ext.define('PBS.TaskSummary', {
title: gettext('Task Summary'),
states: [
"",
"error",
"warning",
"ok",
],
types: [
"backup",
"prune",
"garbage_collection",
"sync",
"verify",
],
titles: {
"backup": gettext('Backups'),
"prune": gettext('Prunes'),
"garbage_collection": gettext('Garbage collections'),
"sync": gettext('Syncs'),
"verify": gettext('Verify'),
},
// set true to show the onclick panel as modal grid
subPanelModal: false,
// the datastore the onclick panel is filtered by
datastore: undefined,
controller: {
xclass: 'Ext.app.ViewController',
states: [
"",
"error",
"warning",
"ok",
],
types: [
"backup",
"prune",
"garbage_collection",
"sync",
"verify",
],
titles: {
"backup": gettext('Backups'),
"prune": gettext('Prunes'),
"garbage_collection": gettext('Garbage collections'),
"sync": gettext('Syncs'),
"verify": gettext('Verify'),
},
openTaskList: function(grid, td, cellindex, record, tr, rowindex) {
let me = this;
@ -36,8 +43,8 @@ Ext.define('PBS.TaskSummary', {
if (cellindex > 0) {
let tasklist = view.tasklist;
let state = me.states[cellindex];
let type = me.types[rowindex];
let state = view.states[cellindex];
let type = view.types[rowindex];
let filterParam = {
limit: 0,
'statusfilter': state,
@ -48,7 +55,11 @@ Ext.define('PBS.TaskSummary', {
filterParam.since = me.since;
}
if (record.data[state] === 0) {
if (view.datastore) {
filterParam.store = view.datastore;
}
if (record.data[state] === 0 || record.data[state] === undefined) {
return;
}
@ -137,7 +148,7 @@ Ext.define('PBS.TaskSummary', {
tasklist.cidx = cellindex;
tasklist.ridx = rowindex;
let task = me.titles[type];
let task = view.titles[type];
let status = "";
switch (state) {
case 'ok': status = gettext("OK"); break;
@ -149,7 +160,13 @@ Ext.define('PBS.TaskSummary', {
tasklist.getStore().getProxy().setExtraParams(filterParam);
tasklist.getStore().removeAll();
tasklist.showBy(td, 'bl-tl');
if (view.subPanelModal) {
tasklist.modal = true;
tasklist.showBy(Ext.getBody(), 'c-c');
} else {
tasklist.modal = false;
tasklist.showBy(td, 'bl-tl');
}
setTimeout(() => tasklist.getStore().reload(), 10);
}
},
@ -182,8 +199,13 @@ Ext.define('PBS.TaskSummary', {
render_count: function(value, md, record, rowindex, colindex) {
let me = this;
let icon = me.render_icon(me.states[colindex], value);
return `${icon} ${value}`;
let view = me.getView();
let count = value || 0;
if (count > 0) {
md.tdCls = 'pointer';
}
let icon = me.render_icon(view.states[colindex], count);
return `${icon} ${value || 0}`;
},
},
@ -191,8 +213,18 @@ Ext.define('PBS.TaskSummary', {
let me = this;
let controller = me.getController();
let data = [];
controller.types.forEach((type) => {
source[type].type = controller.titles[type];
if (!source) {
source = {};
}
me.types.forEach((type) => {
if (!source[type]) {
source[type] = {
error: 0,
warning: 0,
ok: 0,
};
}
source[type].type = me.titles[type];
data.push(source[type]);
});
me.lookup('grid').getStore().setData(data);
@ -214,7 +246,7 @@ Ext.define('PBS.TaskSummary', {
rowLines: false,
viewConfig: {
stripeRows: false,
trackOver: false,
trackOver: true,
},
scrollable: false,
disableSelection: true,

View File

@ -595,6 +595,7 @@ Ext.define('PBS.DataStoreContent', {
header: gettext('Actions'),
xtype: 'actioncolumn',
dataIndex: 'text',
width: 140,
items: [
{
handler: 'onVerify',

View File

@ -0,0 +1,245 @@
// Overview over all datastores
Ext.define('PBS.datastore.DataStoreList', {
extend: 'Ext.panel.Panel',
alias: 'widget.pbsDataStoreList',
title: gettext('Summary'),
scrollable: true,
bodyPadding: 5,
defaults: {
xtype: 'pbsDataStoreListSummary',
padding: 5,
},
datastores: {},
tasks: {},
updateTasks: function(taskStore, records, success) {
let me = this;
if (!success) {
return;
}
for (const store of Object.keys(me.tasks)) {
me.tasks[store] = {};
}
records.forEach(record => {
let task = record.data;
if (!task.worker_id) {
return;
}
let type = task.worker_type;
if (type === 'syncjob') {
type = 'sync';
}
if (type.startsWith('verif')) {
type = 'verify';
}
let datastore = PBS.Utils.parse_datastore_worker_id(type, task.worker_id);
if (!datastore) {
return;
}
if (!me.tasks[datastore]) {
me.tasks[datastore] = {};
}
if (!me.tasks[datastore][type]) {
me.tasks[datastore][type] = {};
}
if (me.tasks[datastore][type] && task.status) {
let parsed = Proxmox.Utils.parse_task_status(task.status);
if (!me.tasks[datastore][type][parsed]) {
me.tasks[datastore][type][parsed] = 0;
}
me.tasks[datastore][type][parsed]++;
}
});
for (const [store, panel] of Object.entries(me.datastores)) {
panel.setTasks(me.tasks[store], me.since);
}
},
updateStores: function(usageStore, records, success) {
let me = this;
if (!success) {
return;
}
let found = {};
records.forEach((rec) => {
found[rec.data.store] = true;
me.addSorted(rec.data);
});
for (const [store, panel] of Object.entries(me.datastores)) {
if (!found[store]) {
me.remove(panel);
}
}
},
addSorted: function(data) {
let me = this;
let i = 0;
let datastores = Object
.keys(me.datastores)
.sort((a, b) => a.localeCompare(b));
for (const datastore of datastores) {
let result = datastore.localeCompare(data.store);
if (result === 0) {
me.datastores[datastore].setStatus(data);
return;
} else if (result > 0) {
break;
}
i++;
}
me.datastores[data.store] = me.insert(i, {
datastore: data.store,
});
me.datastores[data.store].setStatus(data);
me.datastores[data.store].setTasks(me.tasks[data.store], me.since);
},
initComponent: function() {
let me = this;
me.items = [];
me.datastores = {};
// todo make configurable?
me.since = (Date.now()/1000 - 30 * 24*3600).toFixed(0);
me.usageStore = Ext.create('Proxmox.data.UpdateStore', {
storeid: 'datastore-overview-usage',
interval: 5000,
proxy: {
type: 'proxmox',
url: '/api2/json/status/datastore-usage',
},
listeners: {
load: {
fn: me.updateStores,
scope: me,
},
},
});
me.taskStore = Ext.create('Proxmox.data.UpdateStore', {
storeid: 'datastore-overview-tasks',
interval: 15000,
model: 'proxmox-tasks',
proxy: {
type: 'proxmox',
url: '/api2/json/nodes/localhost/tasks',
extraParams: {
limit: 0,
since: me.since,
},
},
listeners: {
load: {
fn: me.updateTasks,
scope: me,
},
},
});
me.callParent();
Proxmox.Utils.monStoreErrors(me, me.usageStore);
Proxmox.Utils.monStoreErrors(me, me.taskStore);
me.on('activate', function() {
me.usageStore.startUpdate();
me.taskStore.startUpdate();
});
me.on('destroy', function() {
me.usageStore.stopUpdate();
me.taskStore.stopUpdate();
});
me.on('deactivate', function() {
me.usageStore.stopUpdate();
me.taskStore.stopUpdate();
});
},
});
Ext.define('PBS.datastore.DataStores', {
extend: 'Ext.tab.Panel',
alias: 'widget.pbsDataStores',
title: gettext('Datastores'),
stateId: 'pbs-datastores-panel',
stateful: true,
stateEvents: ['tabchange'],
applyState: function(state) {
let me = this;
if (state.tab !== undefined && me.rendered) {
me.setActiveTab(state.tab);
} else if (state.tab) {
// if we are not rendered yet, defer setting the activetab
setTimeout(function() {
me.setActiveTab(state.tab);
}, 10);
}
},
getState: function() {
let me = this;
return {
tab: me.getActiveTab().getItemId(),
};
},
border: false,
defaults: {
border: false,
},
tools: [PBS.Utils.get_help_tool("datastore_intro")],
items: [
{
xtype: 'pbsDataStoreList',
iconCls: 'fa fa-book',
},
{
iconCls: 'fa fa-refresh',
itemId: 'syncjobs',
xtype: 'pbsSyncJobView',
},
{
iconCls: 'fa fa-check-circle',
itemId: 'verifyjobs',
xtype: 'pbsVerifyJobView',
},
{
itemId: 'acl',
xtype: 'pbsACLView',
iconCls: 'fa fa-unlock',
aclPath: '/datastore',
},
],
initComponent: function() {
let me = this;
// remove invalid activeTab settings
if (me.activeTab && !me.items.some((item) => item.itemId === me.activeTab)) {
delete me.activeTab;
}
me.callParent();
},
});

View File

@ -0,0 +1,182 @@
// Summary Panel for a single datastore in overview
Ext.define('PBS.datastore.DataStoreListSummary', {
extend: 'Ext.panel.Panel',
alias: 'widget.pbsDataStoreListSummary',
mixins: ['Proxmox.Mixin.CBind'],
cbind: {
title: '{datastore}',
},
referenceHolder: true,
bodyPadding: 10,
layout: {
type: 'hbox',
align: 'stretch',
},
viewModel: {
data: {
full: "N/A",
stillbad: 0,
deduplication: 1.0,
},
},
setTasks: function(taskdata, since) {
let me = this;
me.down('pbsTaskSummary').updateTasks(taskdata, since);
},
setStatus: function(statusData) {
let me = this;
let vm = me.getViewModel();
let usage = statusData.used/statusData.total;
let usagetext = Ext.String.format(gettext('{0} of {1}'),
Proxmox.Utils.format_size(statusData.used),
Proxmox.Utils.format_size(statusData.total),
);
let usagePanel = me.lookup('usage');
usagePanel.updateValue(usage, usagetext);
let estimate = PBS.Utils.render_estimate(statusData['estimated-full-date']);
vm.set('full', estimate);
vm.set('deduplication', PBS.Utils.calculate_dedup_factor(statusData['gc-status']).toFixed(2));
vm.set('stillbad', statusData['gc-status']['still-bad']);
let last = 0;
let time = statusData['history-start'];
let delta = statusData['history-delta'];
let data = statusData.history.map((val) => {
if (val === null) {
val = last;
} else {
last = val;
}
let entry = {
time: time*1000, // js Dates are ms since epoch
val,
};
time += delta;
return entry;
});
me.lookup('historychart').setData(data);
},
items: [
{
xtype: 'container',
layout: {
type: 'vbox',
align: 'stretch',
},
width: 375,
padding: '5 25 5 5',
defaults: {
padding: 2,
},
items: [
{
xtype: 'proxmoxGauge',
warningThreshold: 0.8,
criticalThreshold: 0.95,
flex: 1,
reference: 'usage',
},
{
xtype: 'pmxInfoWidget',
iconCls: 'fa fa-fw fa-line-chart',
title: gettext('Estimated Full'),
printBar: false,
bind: {
data: {
text: '{full}',
},
},
},
{
xtype: 'pmxInfoWidget',
iconCls: 'fa fa-fw fa-compress',
title: gettext('Deduplication Factor'),
printBar: false,
bind: {
data: {
text: '{deduplication}',
},
},
},
{
xtype: 'pmxInfoWidget',
iconCls: 'fa critical fa-fw fa-exclamation-triangle',
title: gettext('Bad Chunks'),
printBar: false,
hidden: true,
bind: {
data: {
text: '{stillbad}',
},
visible: '{stillbad}',
},
},
],
},
{
xtype: 'container',
layout: {
type: 'vbox',
align: 'stretch',
},
flex: 1,
items: [
{
padding: 5,
xtype: 'pbsUsageChart',
reference: 'historychart',
title: gettext('Usage History'),
height: 100,
},
{
xtype: 'container',
flex: 1,
layout: {
type: 'vbox',
align: 'stretch',
},
defaults: {
padding: 5,
},
items: [
{
xtype: 'label',
text: gettext('Task Summary')
+ ` (${Ext.String.format(gettext('{0} days'), 30)})`,
},
{
xtype: 'pbsTaskSummary',
border: false,
header: false,
subPanelModal: true,
flex: 2,
bodyPadding: 0,
minHeight: 0,
cbind: {
datastore: '{datastore}',
},
},
],
},
],
},
],
});

View File

@ -17,8 +17,13 @@ Ext.define('PBS.DataStorePanel', {
applyState: function(state) {
let me = this;
if (state.tab !== undefined) {
if (state.tab !== undefined && me.rendered) {
me.setActiveTab(state.tab);
} else if (state.tab) {
// if we are not rendered yet, defer setting the activetab
setTimeout(function() {
me.setActiveTab(state.tab);
}, 10);
}
},
@ -34,6 +39,8 @@ Ext.define('PBS.DataStorePanel', {
border: false,
},
tools: [PBS.Utils.get_help_tool("datastore_intro")],
items: [
{
xtype: 'pbsDataStoreSummary',
@ -90,7 +97,6 @@ Ext.define('PBS.DataStorePanel', {
itemId: 'acl',
xtype: 'pbsACLView',
iconCls: 'fa fa-unlock',
aclExact: true,
cbind: {
aclPath: '{aclPath}',
},
@ -100,6 +106,10 @@ Ext.define('PBS.DataStorePanel', {
initComponent: function() {
let me = this;
me.title = `${gettext("Datastore")}: ${me.datastore}`;
// remove invalid activeTab settings
if (me.activeTab && !me.items.some((item) => item.itemId === me.activeTab)) {
delete me.activeTab;
}
me.callParent();
},
});

View File

@ -11,7 +11,35 @@ Ext.define('pbs-prune-list', {
],
});
Ext.define('PBS.DataStorePruneInputPanel', {
Ext.define('PBS.PruneKeepInput', {
extend: 'Proxmox.form.field.Integer',
alias: 'widget.pbsPruneKeepInput',
allowBlank: true,
minValue: 1,
listeners: {
change: function(field, newValue, oldValue) {
if (newValue !== this.originalValue) {
this.triggers.clear.setVisible(true);
}
},
},
triggers: {
clear: {
cls: 'pmx-clear-trigger',
weight: -1,
hidden: true,
handler: function() {
this.triggers.clear.setVisible(false);
this.setValue(this.originalValue);
},
},
},
});
Ext.define('PBS.Datastore.PruneInputPanel', {
extend: 'Proxmox.panel.InputPanel',
alias: 'widget.pbsDataStorePruneInputPanel',
mixins: ['Proxmox.Mixin.CBind'],
@ -44,6 +72,50 @@ Ext.define('PBS.DataStorePruneInputPanel', {
reload: function() {
var view = this.getView();
// helper to allow showing why a backup is kept
let addKeepReasons = function(backups, params) {
const rules = [
'keep-last',
'keep-hourly',
'keep-daily',
'keep-weekly',
'keep-monthly',
'keep-yearly',
'keep-all', // when all keep options are not set
];
let counter = {};
backups.sort(function(a, b) {
return a["backup-time"] < b["backup-time"];
});
let ruleIndex = -1;
let nextRule = function() {
let rule;
do {
ruleIndex++;
rule = rules[ruleIndex];
} while (!params[rule] && rule !== 'keep-all');
counter[rule] = 0;
return rule;
};
let rule = nextRule();
for (let backup of backups) {
if (backup.keep) {
counter[rule]++;
if (rule !== 'keep-all') {
backup.keepReason = rule + ': ' + counter[rule];
if (counter[rule] >= params[rule]) {
rule = nextRule();
}
} else {
backup.keepReason = rule;
}
}
}
};
let params = view.getValues();
params["dry-run"] = true;
@ -59,6 +131,7 @@ Ext.define('PBS.DataStorePruneInputPanel', {
},
success: function(response, options) {
var data = response.result.data;
addKeepReasons(data, params);
view.prune_store.setData(data);
},
});
@ -71,46 +144,34 @@ Ext.define('PBS.DataStorePruneInputPanel', {
column1: [
{
xtype: 'proxmoxintegerfield',
xtype: 'pbsPruneKeepInput',
name: 'keep-last',
allowBlank: true,
fieldLabel: gettext('keep-last'),
minValue: 1,
},
{
xtype: 'proxmoxintegerfield',
xtype: 'pbsPruneKeepInput',
name: 'keep-hourly',
allowBlank: true,
fieldLabel: gettext('keep-hourly'),
minValue: 1,
},
{
xtype: 'proxmoxintegerfield',
xtype: 'pbsPruneKeepInput',
name: 'keep-daily',
allowBlank: true,
fieldLabel: gettext('keep-daily'),
minValue: 1,
},
{
xtype: 'proxmoxintegerfield',
xtype: 'pbsPruneKeepInput',
name: 'keep-weekly',
allowBlank: true,
fieldLabel: gettext('keep-weekly'),
minValue: 1,
},
{
xtype: 'proxmoxintegerfield',
xtype: 'pbsPruneKeepInput',
name: 'keep-monthly',
allowBlank: true,
fieldLabel: gettext('keep-monthly'),
minValue: 1,
},
{
xtype: 'proxmoxintegerfield',
xtype: 'pbsPruneKeepInput',
name: 'keep-yearly',
allowBlank: true,
fieldLabel: gettext('keep-yearly'),
minValue: 1,
},
],
@ -144,8 +205,16 @@ Ext.define('PBS.DataStorePruneInputPanel', {
flex: 1,
},
{
text: "keep",
text: 'Keep (reason)',
dataIndex: 'keep',
renderer: function(value, metaData, record) {
if (record.data.keep) {
return 'true (' + record.data.keepReason + ')';
} else {
return 'false';
}
},
flex: 1,
},
],
},
@ -158,11 +227,15 @@ Ext.define('PBS.DataStorePruneInputPanel', {
Ext.define('PBS.DataStorePrune', {
extend: 'Proxmox.window.Edit',
onlineHelp: 'maintenance_pruning',
method: 'POST',
submitText: "Prune",
isCreate: true,
fieldDefaults: { labelWidth: 130 },
initComponent: function() {
var me = this;

View File

@ -70,6 +70,7 @@ Ext.define('PBS.DataStorePruneAndGC', {
editor: {
xtype: 'proxmoxWindowEdit',
title: gettext('GC Schedule'),
onlineHelp: 'maintenance_gc',
items: {
xtype: 'pbsCalendarEvent',
name: 'gc-schedule',
@ -86,6 +87,7 @@ Ext.define('PBS.DataStorePruneAndGC', {
editor: {
xtype: 'proxmoxWindowEdit',
title: gettext('Prune Schedule'),
onlineHelp: 'maintenance_pruning',
items: {
xtype: 'pbsCalendarEvent',
name: 'prune-schedule',

View File

@ -34,7 +34,6 @@ Ext.define('PBS.DataStoreInfo', {
countstext: '',
usage: {},
stillbad: 0,
removedbytes: 0,
mountpoint: "",
},
},
@ -51,27 +50,13 @@ Ext.define('PBS.DataStoreInfo', {
let total = store.getById('total').data.value;
let used = store.getById('used').data.value;
let percent = 100*used/total;
if (total === 0) {
percent = 0;
}
let used_percent = `${percent.toFixed(2)}%`;
let usage = used_percent + ' (' +
Ext.String.format(
gettext('{0} of {1}'),
Proxmox.Utils.format_size(used),
Proxmox.Utils.format_size(total),
) + ')';
let usage = PBS.Utils.render_size_usage(used, total);
vm.set('usagetext', usage);
vm.set('usage', used/total);
let gcstatus = store.getById('gc-status').data.value;
let dedup = 1.0;
if (gcstatus['disk-bytes'] > 0) {
dedup = (gcstatus['index-data-bytes'] || 0)/gcstatus['disk-bytes'];
}
let dedup = PBS.Utils.calculate_dedup_factor(gcstatus);
let countstext = function(count) {
count = count || {};
@ -83,7 +68,6 @@ Ext.define('PBS.DataStoreInfo', {
vm.set('hostcount', countstext(counts.host));
vm.set('deduplication', dedup.toFixed(2));
vm.set('stillbad', gcstatus['still-bad']);
vm.set('removedbytes', Proxmox.Utils.format_size(gcstatus['removed-bytes']));
},
startStore: function() { this.store.startUpdate(); },
@ -173,16 +157,6 @@ Ext.define('PBS.DataStoreInfo', {
},
},
},
{
iconCls: 'fa fa-fw fa-trash-o',
title: gettext('Removed Bytes'),
printBar: false,
bind: {
data: {
text: '{removedbytes}',
},
},
},
{
iconCls: 'fa critical fa-fw fa-exclamation-triangle',
title: gettext('Bad Chunks'),

View File

@ -60,6 +60,8 @@ Ext.define('PBS.form.AuthidSelector', {
}
me.store.loadData(records);
// we need to re-set the value, ExtJS doesn't knows that we injected data into the store
me.setValue(me.value);
me.validate();
},

View File

@ -49,7 +49,6 @@ Ext.define('PBS.form.CalendarEvent', {
return data;
},
store: {
type: 'calendarEventExamples',
},

View File

@ -29,9 +29,9 @@ Ext.define('PBS.form.RemoteSelector', {
flex: 1,
},
{
header: gettext('User name'),
header: gettext('Auth ID'),
sortable: true,
dataIndex: 'userid',
dataIndex: 'auth-id',
renderer: Ext.String.htmlEncode,
flex: 1,
},

View File

@ -31,6 +31,7 @@ Ext.define('PBS.form.TokenSelector', {
},
onLoad: function(store, data, success) {
let me = this;
if (!success) return;
let tokenStore = this.store;
@ -49,23 +50,27 @@ Ext.define('PBS.form.TokenSelector', {
});
tokenStore.loadData(records);
// we need to re-set the value, ExtJS doesn't knows that we injected data into the store
me.setValue(me.value);
me.validate();
},
listConfig: {
width: 500,
columns: [
{
header: gettext('API Token'),
sortable: true,
dataIndex: 'tokenid',
renderer: Ext.String.htmlEncode,
flex: 1,
flex: 2,
},
{
header: gettext('Comment'),
sortable: false,
dataIndex: 'comment',
renderer: Ext.String.htmlEncode,
flex: 1,
flex: 3,
},
],
},

View File

@ -5,6 +5,8 @@ Ext.define('PBS.AccessControlPanel', {
title: gettext('Access Control'),
tools: [PBS.Utils.get_help_tool("user-mgmt")],
border: false,
defaults: {
border: false,

View File

@ -0,0 +1,37 @@
Ext.define('PBS.StorageAndDiskPanel', {
extend: 'Ext.tab.Panel',
alias: 'widget.pbsStorageAndDiskPanel',
mixins: ['Proxmox.Mixin.CBind'],
title: gettext('Storage / Disks'),
tools: [PBS.Utils.get_help_tool("storage-disk-management")],
border: false,
defaults: {
border: false,
},
items: [
{
xtype: 'pmxDiskList',
title: gettext('Disks'),
itemId: 'disks',
iconCls: 'fa fa-hdd-o',
},
{
xtype: 'pbsDirectoryList',
title: Proxmox.Utils.directoryText,
itemId: 'directorystorage',
iconCls: 'fa fa-folder',
},
{
xtype: 'pbsZFSList',
title: "ZFS",
nodename: 'localhost',
iconCls: 'fa fa-th-large',
itemId: 'zfsstorage',
},
],
});

119
www/panel/UsageChart.js Normal file
View File

@ -0,0 +1,119 @@
Ext.define('PBS.widget.UsageChart', {
extend: 'Ext.container.Container',
alias: 'widget.pbsUsageChart',
layout: {
type: 'hbox',
align: 'center',
},
items: [
{
width: 80,
xtype: 'box',
itemId: 'title',
data: {
title: '',
},
tpl: '<h3>{title}:</h3>',
},
{
flex: 1,
xtype: 'cartesian',
height: '100%',
itemId: 'chart',
border: false,
axes: [
{
type: 'numeric',
position: 'right',
hidden: false,
minimum: 0,
// TODO: make this configurable?!
maximum: 1,
renderer: (axis, label) => `${label*100}%`,
},
{
type: 'time',
position: 'bottom',
hidden: true,
},
],
store: {
trackRemoved: false,
data: {},
},
series: [{
type: 'line',
xField: 'time',
yField: 'val',
fill: 'true',
colors: ['#cfcfcf'],
tooltip: {
trackMouse: true,
renderer: function(tooltip, record, ctx) {
if (!record || !record.data) return;
let date = new Date(record.data.time);
date = Ext.Date.format(date, 'c');
let value = (100*record.data.val).toFixed(2);
tooltip.setHtml(
`${value} %<br />
${date}`,
);
},
},
style: {
lineWidth: 1.5,
opacity: 0.60,
},
marker: {
opacity: 0,
scaling: 0.01,
fx: {
duration: 200,
easing: 'easeOut',
},
},
highlightCfg: {
opacity: 1,
scaling: 1.5,
},
}],
},
],
setData: function(data) {
let me = this;
let chart = me.chart;
chart.store.setData(data);
chart.redraw();
},
// the renderer for the tooltip and last value, default just the value
renderer: Ext.identityFn,
setTitle: function(title) {
let me = this;
me.title = title;
me.getComponent('title').update({ title: title });
},
initComponent: function() {
var me = this;
me.callParent();
if (me.title) {
me.getComponent('title').update({ title: me.title });
}
me.chart = me.getComponent('chart');
me.chart.timeaxis = me.chart.getAxes()[1];
if (me.color) {
me.chart.series[0].setStyle({
fill: me.color,
stroke: me.color,
});
}
},
});

View File

@ -20,43 +20,46 @@ Ext.define('PBS.window.ACLEdit', {
me.items = [];
me.items.push({
xtype: 'pbsPermissionPathSelector',
xtype: 'pmxDisplayEditField',
name: 'path',
fieldLabel: gettext('Path'),
editConfig: {
xtype: 'pbsPermissionPathSelector',
allowBlank: false,
},
editable: !me.path,
value: me.path,
name: 'path',
allowBlank: false,
});
if (me.aclType === 'user') {
me.subject = gettext('User Permission');
me.items.push({
xtype: 'pbsUserSelector',
fieldLabel: gettext('User'),
name: 'auth-id',
fieldLabel: gettext('User'),
allowBlank: false,
});
} else if (me.aclType === 'token') {
me.subject = gettext('API Token Permission');
me.items.push({
xtype: 'pbsTokenSelector',
fieldLabel: gettext('API Token'),
name: 'auth-id',
fieldLabel: gettext('API Token'),
allowBlank: false,
});
}
me.items.push({
xtype: 'pmxRoleSelector',
name: 'role',
value: 'NoAccess',
fieldLabel: gettext('Role'),
value: 'NoAccess',
});
me.items.push({
xtype: 'proxmoxcheckbox',
name: 'propagate',
fieldLabel: gettext('Propagate'),
checked: true,
uncheckedValue: 0,
fieldLabel: gettext('Propagate'),
});
me.callParent();

View File

@ -2,6 +2,8 @@ Ext.define('PBS.BackupGroupChangeOwner', {
extend: 'Proxmox.window.Edit',
alias: 'widget.pbsBackupGroupChangeOwner',
onlineHelp: 'changing-backup-owner',
submitText: gettext("Change Owner"),
width: 350,

View File

@ -8,6 +8,8 @@ Ext.define('PBS.window.CreateDirectory', {
url: '/nodes/localhost/disks/directory',
method: 'POST',
onlineHelp: 'storage-disk-management',
items: [
{
xtype: 'pmxDiskSelector',

View File

@ -100,6 +100,7 @@ Ext.define('PBS.DataStoreEdit', {
}
me.url = name ? baseurl + '/' + name : baseurl;
me.method = name ? 'PUT' : 'POST';
me.scheduleValue = name ? null : 'daily';
me.autoLoad = !!name;
return {};
},
@ -146,15 +147,18 @@ Ext.define('PBS.DataStoreEdit', {
emptyText: gettext('none'),
cbind: {
deleteEmpty: '{!isCreate}',
value: '{scheduleValue}',
},
},
{
xtype: 'pbsCalendarEvent',
name: 'prune-schedule',
fieldLabel: gettext("Prune Schedule"),
value: 'daily',
emptyText: gettext('none'),
cbind: {
deleteEmpty: '{!isCreate}',
value: '{scheduleValue}',
},
},
],

View File

@ -5,8 +5,6 @@ Ext.define('PBS.window.RemoteEdit', {
onlineHelp: 'backup_remote',
userid: undefined,
isAdd: true,
subject: gettext('Remote'),
@ -93,8 +91,8 @@ Ext.define('PBS.window.RemoteEdit', {
{
xtype: 'proxmoxtextfield',
allowBlank: false,
name: 'userid',
fieldLabel: gettext('Userid'),
name: 'auth-id',
fieldLabel: gettext('Auth ID'),
},
{
xtype: 'textfield',

View File

@ -113,6 +113,7 @@ Ext.define('PBS.window.SyncJobEdit', {
me.autoLoad = !!id;
me.scheduleValue = id ? null : 'hourly';
me.authid = id ? null : Proxmox.UserName;
me.editDatastore = me.datastore === undefined && me.isCreate;
return { };
},
@ -122,20 +123,24 @@ Ext.define('PBS.window.SyncJobEdit', {
let me = this;
if (!values.id && me.up('pbsSyncJobEdit').isCreate) {
values.id = 'auto-' + Ext.data.identifier.Uuid.Global.generate().slice(0, 23);
values.id = 's-' + Ext.data.identifier.Uuid.Global.generate().slice(0, 13);
}
return values;
},
column1: [
{
xtype: 'displayfield',
name: 'store',
xtype: 'pmxDisplayEditField',
fieldLabel: gettext('Local Datastore'),
allowBlank: false,
name: 'store',
submitValue: true,
cbind: {
editable: '{editDatastore}',
value: '{datastore}',
},
editConfig: {
xtype: 'pbsDataStoreSelector',
allowBlank: false,
},
},
{
fieldLabel: gettext('Local Owner'),

View File

@ -3,7 +3,7 @@ Ext.define('PBS.window.TokenEdit', {
alias: 'widget.pbsTokenEdit',
mixins: ['Proxmox.Mixin.CBind'],
onlineHelp: 'user_mgmt',
onlineHelp: 'user_tokens',
user: undefined,
tokenname: undefined,

View File

@ -5,7 +5,7 @@ Ext.define('PBS.window.VerifyJobEdit', {
userid: undefined,
onlineHelp: 'verifyjobs',
onlineHelp: 'maintenance-verification',
isAdd: true,
@ -24,6 +24,7 @@ Ext.define('PBS.window.VerifyJobEdit', {
me.url = id ? `${baseurl}/${id}` : baseurl;
me.method = id ? 'PUT' : 'POST';
me.autoLoad = !!id;
me.editDatastore = me.datastore === undefined && me.isCreate;
return { };
},
@ -39,20 +40,24 @@ Ext.define('PBS.window.VerifyJobEdit', {
let me = this;
if (!values.id && me.up('pbsVerifyJobEdit').isCreate) {
values.id = 'auto-' + Ext.data.identifier.Uuid.Global.generate().slice(0, 23);
values.id = 'v-' + Ext.data.identifier.Uuid.Global.generate().slice(0, 13);
}
return values;
},
column1: [
{
xtype: 'displayfield',
xtype: 'pmxDisplayEditField',
fieldLabel: gettext('Local Datastore'),
name: 'store',
fieldLabel: gettext('Datastore'),
allowBlank: false,
submitValue: true,
cbind: {
editable: '{editDatastore}',
value: '{datastore}',
},
editConfig: {
xtype: 'pbsDataStoreSelector',
allowBlank: false,
},
},
{
xtype: 'pbsCalendarEvent',