Compare commits
123 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
5a2e6ccf77 | ||
|
f31e32a006 | ||
|
2ad96e1635 | ||
|
7bc2e240b1 | ||
|
20a04cf07c | ||
|
a40ffb92ac | ||
|
e2aeff40eb | ||
|
d20137e5a9 | ||
|
6a35698796 | ||
|
2981cdd4c0 | ||
|
8c9c6c0755 | ||
|
2c69b69108 | ||
|
0bed1f2956 | ||
|
4ef6b7d1f0 | ||
|
87d8aa4278 | ||
|
51d900d187 | ||
|
519ca9d010 | ||
|
615a50c108 | ||
|
f418f4e48b | ||
|
c66fa32c08 | ||
|
2515ff35c2 | ||
|
33a1ef7aae | ||
|
9c12e82006 | ||
|
9f19057036 | ||
|
c7f7236b88 | ||
|
fdefe192ac | ||
|
1ed8698b7e | ||
|
0bd9c87010 | ||
|
fbfb64a6b2 | ||
|
c39852abdc | ||
|
1ec167ee8c | ||
|
11ca834317 | ||
|
68a6e970d4 | ||
|
4e851c26a2 | ||
|
ceb815d295 | ||
|
14433718fb | ||
|
3dc8783af7 | ||
|
6d89534929 | ||
|
aa19d5b917 | ||
|
a8d3f1943b | ||
|
3cf12ffac9 | ||
|
2017a47eec | ||
|
21185350fb | ||
|
17b079918e | ||
|
fbfc439372 | ||
|
27d3a232d0 | ||
|
1fa6083bc8 | ||
|
aa32a46171 | ||
|
6283d7d13a | ||
|
d4dd7ac842 | ||
|
451da4923b | ||
|
f15e094408 | ||
|
134779664e | ||
|
9ce2f903fb | ||
|
6802a68356 | ||
|
c69884a459 | ||
|
93205cbe92 | ||
|
434dd3cc84 | ||
|
dba37e212b | ||
|
db4b8683cf | ||
|
5557af0efb | ||
|
8721b42e2f | ||
|
5408e30ab1 | ||
|
70493f1823 | ||
|
069720f510 | ||
|
a93c96823c | ||
|
2393943fbb | ||
|
49d604aec1 | ||
|
246275e203 | ||
|
c9fb0f3887 | ||
|
84de101272 | ||
|
de77a20d3d | ||
|
997c96d6a3 | ||
|
513da8ed10 | ||
|
e87e4499fd | ||
|
a19b8c2e24 | ||
|
b8858d5186 | ||
|
bc001e12e2 | ||
|
abd8248520 | ||
|
974a3e521a | ||
|
ea2e91e52f | ||
|
77bd14f68a | ||
|
d1fba4de1d | ||
|
3e4994a54f | ||
|
75b377219d | ||
|
c8dc51e41f | ||
|
7d0dbaa013 | ||
|
efa62d44d4 | ||
|
210ded9803 | ||
|
99e1399729 | ||
|
0aa5815fb6 | ||
|
bb5c77fffa | ||
|
ebfcf75e14 | ||
|
83e3000349 | ||
|
4a4dd66c26 | ||
|
b9b2d635fe | ||
|
9f8aa8c5e2 | ||
|
b11693b2f7 | ||
|
53435bc4d5 | ||
|
8bec3ff691 | ||
|
789e22d905 | ||
|
a1c30e0194 | ||
|
d4c6e68bf0 | ||
|
e642344f98 | ||
|
d4574bb138 | ||
|
26b40687b3 | ||
|
e3c26aea31 | ||
|
65aba79a9b | ||
|
4b6a653a0f | ||
|
3c41d86010 | ||
|
93821e87e6 | ||
|
f12f408e91 | ||
|
71cad8cac0 | ||
|
49bea6b5d9 | ||
|
f7247e2b84 | ||
|
5664b41c30 | ||
|
33612525e1 | ||
|
a502bc5617 | ||
|
8772ca727c | ||
|
7f3b4a94e6 | ||
|
327d14b3d1 | ||
|
0f8fd71093 | ||
|
8d3b84e719 |
16
Cargo.toml
16
Cargo.toml
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "proxmox-backup"
|
name = "proxmox-backup"
|
||||||
version = "2.2.1"
|
version = "2.2.3"
|
||||||
authors = [
|
authors = [
|
||||||
"Dietmar Maurer <dietmar@proxmox.com>",
|
"Dietmar Maurer <dietmar@proxmox.com>",
|
||||||
"Dominik Csapak <d.csapak@proxmox.com>",
|
"Dominik Csapak <d.csapak@proxmox.com>",
|
||||||
@ -61,7 +61,7 @@ hyper = { version = "0.14", features = [ "full" ] }
|
|||||||
lazy_static = "1.4"
|
lazy_static = "1.4"
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
log = "0.4.17"
|
log = "0.4.17"
|
||||||
nix = "0.19.1"
|
nix = "0.24"
|
||||||
num-traits = "0.2"
|
num-traits = "0.2"
|
||||||
once_cell = "1.3.1"
|
once_cell = "1.3.1"
|
||||||
openssl = "0.10.38" # currently patched!
|
openssl = "0.10.38" # currently patched!
|
||||||
@ -69,7 +69,7 @@ pam = "0.7"
|
|||||||
pam-sys = "0.5"
|
pam-sys = "0.5"
|
||||||
percent-encoding = "2.1"
|
percent-encoding = "2.1"
|
||||||
regex = "1.5.5"
|
regex = "1.5.5"
|
||||||
rustyline = "7"
|
rustyline = "9"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
siphasher = "0.3"
|
siphasher = "0.3"
|
||||||
@ -77,7 +77,7 @@ syslog = "4.0"
|
|||||||
tokio = { version = "1.6", features = [ "fs", "io-util", "io-std", "macros", "net", "parking_lot", "process", "rt", "rt-multi-thread", "signal", "time" ] }
|
tokio = { version = "1.6", features = [ "fs", "io-util", "io-std", "macros", "net", "parking_lot", "process", "rt", "rt-multi-thread", "signal", "time" ] }
|
||||||
tokio-openssl = "0.6.1"
|
tokio-openssl = "0.6.1"
|
||||||
tokio-stream = "0.1.0"
|
tokio-stream = "0.1.0"
|
||||||
tokio-util = { version = "0.6", features = [ "codec", "io" ] }
|
tokio-util = { version = "0.7", features = [ "codec", "io" ] }
|
||||||
tower-service = "0.3.0"
|
tower-service = "0.3.0"
|
||||||
udev = "0.4"
|
udev = "0.4"
|
||||||
url = "2.1"
|
url = "2.1"
|
||||||
@ -104,7 +104,7 @@ proxmox-time = "1.1.2"
|
|||||||
proxmox-uuid = "1"
|
proxmox-uuid = "1"
|
||||||
proxmox-serde = "0.1"
|
proxmox-serde = "0.1"
|
||||||
proxmox-shared-memory = "0.2"
|
proxmox-shared-memory = "0.2"
|
||||||
proxmox-sys = { version = "0.2", features = [ "sortable-macro" ] }
|
proxmox-sys = { version = "0.3", features = [ "sortable-macro" ] }
|
||||||
proxmox-compression = "0.1"
|
proxmox-compression = "0.1"
|
||||||
|
|
||||||
|
|
||||||
@ -126,18 +126,22 @@ pbs-tape = { path = "pbs-tape" }
|
|||||||
# Local path overrides
|
# Local path overrides
|
||||||
# NOTE: You must run `cargo update` after changing this for it to take effect!
|
# NOTE: You must run `cargo update` after changing this for it to take effect!
|
||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
#proxmox = { path = "../proxmox/proxmox" }
|
#proxmox-acme-rs = { path = "../proxmox-acme-rs" }
|
||||||
|
#proxmox-apt = { path = "../proxmox-apt" }
|
||||||
#proxmox-async = { path = "../proxmox/proxmox-async" }
|
#proxmox-async = { path = "../proxmox/proxmox-async" }
|
||||||
|
#proxmox-compression = { path = "../proxmox/proxmox-compression" }
|
||||||
#proxmox-borrow = { path = "../proxmox/proxmox-borrow" }
|
#proxmox-borrow = { path = "../proxmox/proxmox-borrow" }
|
||||||
#proxmox-fuse = { path = "../proxmox-fuse" }
|
#proxmox-fuse = { path = "../proxmox-fuse" }
|
||||||
#proxmox-http = { path = "../proxmox/proxmox-http" }
|
#proxmox-http = { path = "../proxmox/proxmox-http" }
|
||||||
#proxmox-io = { path = "../proxmox/proxmox-io" }
|
#proxmox-io = { path = "../proxmox/proxmox-io" }
|
||||||
#proxmox-lang = { path = "../proxmox/proxmox-lang" }
|
#proxmox-lang = { path = "../proxmox/proxmox-lang" }
|
||||||
|
#proxmox-openid = { path = "../proxmox-openid-rs" }
|
||||||
#proxmox-router = { path = "../proxmox/proxmox-router" }
|
#proxmox-router = { path = "../proxmox/proxmox-router" }
|
||||||
#proxmox-schema = { path = "../proxmox/proxmox-schema" }
|
#proxmox-schema = { path = "../proxmox/proxmox-schema" }
|
||||||
#proxmox-section-config = { path = "../proxmox/proxmox-section-config" }
|
#proxmox-section-config = { path = "../proxmox/proxmox-section-config" }
|
||||||
#proxmox-shared-memory = { path = "../proxmox/proxmox-shared-memory" }
|
#proxmox-shared-memory = { path = "../proxmox/proxmox-shared-memory" }
|
||||||
#proxmox-sys = { path = "../proxmox/proxmox-sys" }
|
#proxmox-sys = { path = "../proxmox/proxmox-sys" }
|
||||||
|
#proxmox-serde = { path = "../proxmox/proxmox-serde" }
|
||||||
#proxmox-tfa = { path = "../proxmox/proxmox-tfa" }
|
#proxmox-tfa = { path = "../proxmox/proxmox-tfa" }
|
||||||
#proxmox-time = { path = "../proxmox/proxmox-time" }
|
#proxmox-time = { path = "../proxmox/proxmox-time" }
|
||||||
#proxmox-uuid = { path = "../proxmox/proxmox-uuid" }
|
#proxmox-uuid = { path = "../proxmox/proxmox-uuid" }
|
||||||
|
76
debian/changelog
vendored
76
debian/changelog
vendored
@ -1,4 +1,78 @@
|
|||||||
rust-proxmox-backup (2.2.1-1) UNRELEASED; urgency=medium
|
rust-proxmox-backup (2.2.3-1) bullseye; urgency=medium
|
||||||
|
|
||||||
|
* datastore: swap dirtying the datastore cache every 60s by just using the
|
||||||
|
available config digest to detect any changes accuratly when the actually
|
||||||
|
happen
|
||||||
|
|
||||||
|
* api: datastore list and datastore status: avoid opening datastore and
|
||||||
|
possibly iterating over namespace (for lesser privileged users), but
|
||||||
|
rather use the in-memory ACL tree directly to check if there's access to
|
||||||
|
any namespace below.
|
||||||
|
|
||||||
|
-- Proxmox Support Team <support@proxmox.com> Sat, 04 Jun 2022 16:30:05 +0200
|
||||||
|
|
||||||
|
rust-proxmox-backup (2.2.2-3) bullseye; urgency=medium
|
||||||
|
|
||||||
|
* datastore: lookup: reuse ChunkStore on stale datastore re-open
|
||||||
|
|
||||||
|
* bump tokio (async framework) dependency
|
||||||
|
|
||||||
|
-- Proxmox Support Team <support@proxmox.com> Thu, 02 Jun 2022 17:25:01 +0200
|
||||||
|
|
||||||
|
rust-proxmox-backup (2.2.2-2) bullseye; urgency=medium
|
||||||
|
|
||||||
|
* improvement of error handling when removing status files and locks from
|
||||||
|
jobs that were never executed.
|
||||||
|
|
||||||
|
-- Proxmox Support Team <support@proxmox.com> Wed, 01 Jun 2022 16:22:22 +0200
|
||||||
|
|
||||||
|
rust-proxmox-backup (2.2.2-1) bullseye; urgency=medium
|
||||||
|
|
||||||
|
* Revert "verify: allow '0' days for reverification", was already possible
|
||||||
|
by setting "ignore-verified" to false
|
||||||
|
|
||||||
|
* ui: datastore permissions: allow ACL path edit & query namespaces
|
||||||
|
|
||||||
|
* accessible group iter: allow NS descending with DATASTORE_READ privilege
|
||||||
|
|
||||||
|
* prune datastore: rework worker tak log
|
||||||
|
|
||||||
|
* prune datastore: support max-depth and improve priv checks
|
||||||
|
|
||||||
|
* ui: prune input: support opt-in recursive/max-depth field
|
||||||
|
|
||||||
|
* add prune job config and api, allowing one to setup a scheduled pruning
|
||||||
|
for a specific namespace only
|
||||||
|
|
||||||
|
* ui: add ui for prune jobs
|
||||||
|
|
||||||
|
* api: disable setting prune options in datastore.cfg and transform any
|
||||||
|
existing prune tasks from datastore config to new prune job config in a
|
||||||
|
post installation hook
|
||||||
|
|
||||||
|
* proxmox-tape: use correct api call for 'load-media-from-slot'
|
||||||
|
|
||||||
|
* avoid overly strict privilege restrictions for some API endpoints and
|
||||||
|
actions when using namespaces. Better support navigating the user
|
||||||
|
interface when only having Datastore.Admin on a (sub) namespace.
|
||||||
|
|
||||||
|
* include required privilege names in some permission errors
|
||||||
|
|
||||||
|
* docs: fix some typos
|
||||||
|
|
||||||
|
* api: status: include empty entry for stores with ns-only privs
|
||||||
|
|
||||||
|
* ui: datastore options: avoid breakage if rrd store ore active-ops cannot
|
||||||
|
be queried
|
||||||
|
|
||||||
|
* ui: datastore content: only mask the inner treeview, not the top bar on
|
||||||
|
error to allow a user to trigger a manual reload
|
||||||
|
|
||||||
|
* ui: system config: improve bottom margins and scroll behavior
|
||||||
|
|
||||||
|
-- Proxmox Support Team <support@proxmox.com> Wed, 01 Jun 2022 15:09:36 +0200
|
||||||
|
|
||||||
|
rust-proxmox-backup (2.2.1-1) bullseye; urgency=medium
|
||||||
|
|
||||||
* docs: update some screenshots and add new ones
|
* docs: update some screenshots and add new ones
|
||||||
|
|
||||||
|
23
debian/control
vendored
23
debian/control
vendored
@ -31,7 +31,7 @@ Build-Depends: debhelper (>= 12),
|
|||||||
librust-lazy-static-1+default-dev (>= 1.4-~~),
|
librust-lazy-static-1+default-dev (>= 1.4-~~),
|
||||||
librust-libc-0.2+default-dev,
|
librust-libc-0.2+default-dev,
|
||||||
librust-log-0.4+default-dev (>= 0.4.17-~~) <!nocheck>,
|
librust-log-0.4+default-dev (>= 0.4.17-~~) <!nocheck>,
|
||||||
librust-nix-0.19+default-dev (>= 0.19.1-~~),
|
librust-nix-0.24+default-dev,
|
||||||
librust-nom-5+default-dev (>= 5.1-~~),
|
librust-nom-5+default-dev (>= 5.1-~~),
|
||||||
librust-num-traits-0.2+default-dev,
|
librust-num-traits-0.2+default-dev,
|
||||||
librust-once-cell-1+default-dev (>= 1.3.1-~~),
|
librust-once-cell-1+default-dev (>= 1.3.1-~~),
|
||||||
@ -47,10 +47,10 @@ Build-Depends: debhelper (>= 12),
|
|||||||
librust-proxmox-borrow-1+default-dev,
|
librust-proxmox-borrow-1+default-dev,
|
||||||
librust-proxmox-compression-0.1+default-dev (>= 0.1.1-~~),
|
librust-proxmox-compression-0.1+default-dev (>= 0.1.1-~~),
|
||||||
librust-proxmox-fuse-0.1+default-dev (>= 0.1.1-~~),
|
librust-proxmox-fuse-0.1+default-dev (>= 0.1.1-~~),
|
||||||
librust-proxmox-http-0.6.1+client-dev,
|
librust-proxmox-http-0.6+client-dev (>= 0.6.1-~~),
|
||||||
librust-proxmox-http-0.6.1+default-dev,
|
librust-proxmox-http-0.6+default-dev (>= 0.6.1-~~),
|
||||||
librust-proxmox-http-0.6.1+http-helpers-dev,
|
librust-proxmox-http-0.6+http-helpers-dev (>= 0.6.1-~~),
|
||||||
librust-proxmox-http-0.6.1+websocket-dev,
|
librust-proxmox-http-0.6+websocket-dev (>= 0.6.1-~~),
|
||||||
librust-proxmox-io-1+default-dev (>= 1.0.1-~~),
|
librust-proxmox-io-1+default-dev (>= 1.0.1-~~),
|
||||||
librust-proxmox-io-1+tokio-dev (>= 1.0.1-~~),
|
librust-proxmox-io-1+tokio-dev (>= 1.0.1-~~),
|
||||||
librust-proxmox-lang-1+default-dev (>= 1.1-~~),
|
librust-proxmox-lang-1+default-dev (>= 1.1-~~),
|
||||||
@ -63,8 +63,9 @@ Build-Depends: debhelper (>= 12),
|
|||||||
librust-proxmox-section-config-1+default-dev,
|
librust-proxmox-section-config-1+default-dev,
|
||||||
librust-proxmox-serde-0.1+default-dev,
|
librust-proxmox-serde-0.1+default-dev,
|
||||||
librust-proxmox-shared-memory-0.2+default-dev,
|
librust-proxmox-shared-memory-0.2+default-dev,
|
||||||
librust-proxmox-sys-0.2+default-dev (>= 0.2.1-~~),
|
librust-proxmox-sys-0.3+default-dev,
|
||||||
librust-proxmox-sys-0.2+sortable-macro-dev (>= 0.2.1-~~),
|
librust-proxmox-sys-0.3+logrotate-dev,
|
||||||
|
librust-proxmox-sys-0.3+sortable-macro-dev,
|
||||||
librust-proxmox-tfa-2+api-dev,
|
librust-proxmox-tfa-2+api-dev,
|
||||||
librust-proxmox-tfa-2+api-types-dev,
|
librust-proxmox-tfa-2+api-types-dev,
|
||||||
librust-proxmox-tfa-2+default-dev,
|
librust-proxmox-tfa-2+default-dev,
|
||||||
@ -74,7 +75,7 @@ Build-Depends: debhelper (>= 12),
|
|||||||
librust-pxar-0.10+default-dev (>= 0.10.1-~~),
|
librust-pxar-0.10+default-dev (>= 0.10.1-~~),
|
||||||
librust-pxar-0.10+tokio-io-dev (>= 0.10.1-~~),
|
librust-pxar-0.10+tokio-io-dev (>= 0.10.1-~~),
|
||||||
librust-regex-1+default-dev (>= 1.5.5-~~),
|
librust-regex-1+default-dev (>= 1.5.5-~~),
|
||||||
librust-rustyline-7+default-dev,
|
librust-rustyline-9+default-dev,
|
||||||
librust-serde-1+default-dev,
|
librust-serde-1+default-dev,
|
||||||
librust-serde-1+derive-dev,
|
librust-serde-1+derive-dev,
|
||||||
librust-serde-cbor-0.11+default-dev (>= 0.11.1-~~),
|
librust-serde-cbor-0.11+default-dev (>= 0.11.1-~~),
|
||||||
@ -97,9 +98,9 @@ Build-Depends: debhelper (>= 12),
|
|||||||
librust-tokio-1+time-dev (>= 1.6-~~),
|
librust-tokio-1+time-dev (>= 1.6-~~),
|
||||||
librust-tokio-openssl-0.6+default-dev (>= 0.6.1-~~),
|
librust-tokio-openssl-0.6+default-dev (>= 0.6.1-~~),
|
||||||
librust-tokio-stream-0.1+default-dev,
|
librust-tokio-stream-0.1+default-dev,
|
||||||
librust-tokio-util-0.6+codec-dev,
|
librust-tokio-util-0.7+codec-dev,
|
||||||
librust-tokio-util-0.6+default-dev,
|
librust-tokio-util-0.7+default-dev,
|
||||||
librust-tokio-util-0.6+io-dev,
|
librust-tokio-util-0.7+io-dev,
|
||||||
librust-tower-service-0.3+default-dev,
|
librust-tower-service-0.3+default-dev,
|
||||||
librust-udev-0.4+default-dev,
|
librust-udev-0.4+default-dev,
|
||||||
librust-url-2+default-dev (>= 2.1-~~),
|
librust-url-2+default-dev (>= 2.1-~~),
|
||||||
|
9
debian/postinst
vendored
9
debian/postinst
vendored
@ -41,7 +41,14 @@ case "$1" in
|
|||||||
flock -w 30 /var/log/proxmox-backup/tasks/active.lock sed -i 's/:termproxy::\([^@]\+\): /:termproxy::\1@pam: /' /var/log/proxmox-backup/tasks/active || true
|
flock -w 30 /var/log/proxmox-backup/tasks/active.lock sed -i 's/:termproxy::\([^@]\+\): /:termproxy::\1@pam: /' /var/log/proxmox-backup/tasks/active || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if dpkg --compare-versions "$2" 'lt' '7.1-1' && test -e /etc/proxmox-backup/sync.cfg; then
|
if dpkg --compare-versions "$2" 'lt' '2.2.2~'; then
|
||||||
|
echo "moving prune schedule from datacenter config to new prune job config"
|
||||||
|
proxmox-backup-manager update-to-prune-jobs-config \
|
||||||
|
|| echo "Failed to move prune jobs, please check manually"
|
||||||
|
true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if dpkg --compare-versions "$2" 'lt' '2.1.3~' && test -e /etc/proxmox-backup/sync.cfg; then
|
||||||
prev_job=""
|
prev_job=""
|
||||||
|
|
||||||
# read from HERE doc because POSIX sh limitations
|
# read from HERE doc because POSIX sh limitations
|
||||||
|
@ -29,7 +29,7 @@ How long will my Proxmox Backup Server version be supported?
|
|||||||
+=======================+======================+===============+============+====================+
|
+=======================+======================+===============+============+====================+
|
||||||
|Proxmox Backup 2.x | Debian 11 (Bullseye) | 2021-07 | tba | tba |
|
|Proxmox Backup 2.x | Debian 11 (Bullseye) | 2021-07 | tba | tba |
|
||||||
+-----------------------+----------------------+---------------+------------+--------------------+
|
+-----------------------+----------------------+---------------+------------+--------------------+
|
||||||
|Proxmox Backup 1.x | Debian 10 (Buster) | 2020-11 | ~Q2/2022 | Q2-Q3/2022 |
|
|Proxmox Backup 1.x | Debian 10 (Buster) | 2020-11 | 2022-08 | 2022-07 |
|
||||||
+-----------------------+----------------------+---------------+------------+--------------------+
|
+-----------------------+----------------------+---------------+------------+--------------------+
|
||||||
|
|
||||||
|
|
||||||
|
@ -217,7 +217,7 @@ errors. Newer ZFS packages ship the daemon in a separate package ``zfs-zed``,
|
|||||||
which should already be installed by default in `Proxmox Backup`_.
|
which should already be installed by default in `Proxmox Backup`_.
|
||||||
|
|
||||||
You can configure the daemon via the file ``/etc/zfs/zed.d/zed.rc`` with your
|
You can configure the daemon via the file ``/etc/zfs/zed.d/zed.rc`` with your
|
||||||
favorite editor. The required setting for email notfication is
|
favorite editor. The required setting for email notification is
|
||||||
``ZED_EMAIL_ADDR``, which is set to ``root`` by default.
|
``ZED_EMAIL_ADDR``, which is set to ``root`` by default.
|
||||||
|
|
||||||
.. code-block:: console
|
.. code-block:: console
|
||||||
|
@ -125,7 +125,7 @@ execution:
|
|||||||
|
|
||||||
- ``remote-ns``: the remote namespace anchor (default: the root namespace)
|
- ``remote-ns``: the remote namespace anchor (default: the root namespace)
|
||||||
|
|
||||||
- ``ns``: the local namespace anchor (default: the root naemspace)
|
- ``ns``: the local namespace anchor (default: the root namespace)
|
||||||
|
|
||||||
- ``max-depth``: whether to recursively iterate over sub-namespaces of the remote
|
- ``max-depth``: whether to recursively iterate over sub-namespaces of the remote
|
||||||
namespace anchor (default: `None`)
|
namespace anchor (default: `None`)
|
||||||
|
@ -51,7 +51,7 @@ ENVIRONMENT
|
|||||||
:CHANGER: If set, replaces the `--device` option
|
:CHANGER: If set, replaces the `--device` option
|
||||||
|
|
||||||
:PROXMOX_TAPE_DRIVE: If set, use the Proxmox Backup Server
|
:PROXMOX_TAPE_DRIVE: If set, use the Proxmox Backup Server
|
||||||
configuration to find the associcated changer device.
|
configuration to find the associated changer device.
|
||||||
|
|
||||||
|
|
||||||
.. include:: ../pbs-copyright.rst
|
.. include:: ../pbs-copyright.rst
|
||||||
|
@ -262,7 +262,7 @@ categorized by checksum, after a backup operation has been executed.
|
|||||||
|
|
||||||
|
|
||||||
Once you uploaded some backups, or created namespaces, you may see the Backup
|
Once you uploaded some backups, or created namespaces, you may see the Backup
|
||||||
Type (`ct`, `vm`, `host`) and the start of the namespace hierachy (`ns`).
|
Type (`ct`, `vm`, `host`) and the start of the namespace hierarchy (`ns`).
|
||||||
|
|
||||||
.. _storage_namespaces:
|
.. _storage_namespaces:
|
||||||
|
|
||||||
|
@ -682,7 +682,7 @@ To remove a job, please use:
|
|||||||
# proxmox-tape backup-job remove job2
|
# proxmox-tape backup-job remove job2
|
||||||
|
|
||||||
By default, all (recursive) namespaces of the datastore are included in a tape
|
By default, all (recursive) namespaces of the datastore are included in a tape
|
||||||
backup. You can specify a single namespace wth ``ns`` and a depth with
|
backup. You can specify a single namespace with ``ns`` and a depth with
|
||||||
``max-depth``. For example:
|
``max-depth``. For example:
|
||||||
|
|
||||||
.. code-block:: console
|
.. code-block:: console
|
||||||
|
@ -95,7 +95,7 @@ The backup server groups backups by *type*, where *type* is one of:
|
|||||||
Backup ID
|
Backup ID
|
||||||
---------
|
---------
|
||||||
|
|
||||||
A unique ID for a specific Backup Type and Backup Namesapce. Usually the
|
A unique ID for a specific Backup Type and Backup Namespace. Usually the
|
||||||
virtual machine or container ID. ``host`` type backups normally use the
|
virtual machine or container ID. ``host`` type backups normally use the
|
||||||
hostname.
|
hostname.
|
||||||
|
|
||||||
|
@ -73,6 +73,17 @@ constnamedbitmap! {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn privs_to_priv_names(privs: u64) -> Vec<&'static str> {
|
||||||
|
PRIVILEGES
|
||||||
|
.iter()
|
||||||
|
.fold(Vec::new(), |mut priv_names, (name, value)| {
|
||||||
|
if value & privs != 0 {
|
||||||
|
priv_names.push(name);
|
||||||
|
}
|
||||||
|
priv_names
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Admin always has all privileges. It can do everything except a few actions
|
/// Admin always has all privileges. It can do everything except a few actions
|
||||||
/// which are limited to the 'root@pam` superuser
|
/// which are limited to the 'root@pam` superuser
|
||||||
pub const ROLE_ADMIN: u64 = u64::MAX;
|
pub const ROLE_ADMIN: u64 = u64::MAX;
|
||||||
|
@ -157,52 +157,6 @@ pub const PRUNE_SCHEMA_KEEP_YEARLY: Schema =
|
|||||||
.minimum(1)
|
.minimum(1)
|
||||||
.schema();
|
.schema();
|
||||||
|
|
||||||
#[api(
|
|
||||||
properties: {
|
|
||||||
"keep-last": {
|
|
||||||
schema: PRUNE_SCHEMA_KEEP_LAST,
|
|
||||||
optional: true,
|
|
||||||
},
|
|
||||||
"keep-hourly": {
|
|
||||||
schema: PRUNE_SCHEMA_KEEP_HOURLY,
|
|
||||||
optional: true,
|
|
||||||
},
|
|
||||||
"keep-daily": {
|
|
||||||
schema: PRUNE_SCHEMA_KEEP_DAILY,
|
|
||||||
optional: true,
|
|
||||||
},
|
|
||||||
"keep-weekly": {
|
|
||||||
schema: PRUNE_SCHEMA_KEEP_WEEKLY,
|
|
||||||
optional: true,
|
|
||||||
},
|
|
||||||
"keep-monthly": {
|
|
||||||
schema: PRUNE_SCHEMA_KEEP_MONTHLY,
|
|
||||||
optional: true,
|
|
||||||
},
|
|
||||||
"keep-yearly": {
|
|
||||||
schema: PRUNE_SCHEMA_KEEP_YEARLY,
|
|
||||||
optional: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)]
|
|
||||||
#[derive(Serialize, Deserialize, Default)]
|
|
||||||
#[serde(rename_all = "kebab-case")]
|
|
||||||
/// Common pruning options
|
|
||||||
pub struct PruneOptions {
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub keep_last: Option<u64>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub keep_hourly: Option<u64>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub keep_daily: Option<u64>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub keep_weekly: Option<u64>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub keep_monthly: Option<u64>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub keep_yearly: Option<u64>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[api]
|
#[api]
|
||||||
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
@ -264,29 +218,8 @@ pub const DATASTORE_TUNING_STRING_SCHEMA: Schema = StringSchema::new("Datastore
|
|||||||
optional: true,
|
optional: true,
|
||||||
schema: PRUNE_SCHEDULE_SCHEMA,
|
schema: PRUNE_SCHEDULE_SCHEMA,
|
||||||
},
|
},
|
||||||
"keep-last": {
|
keep: {
|
||||||
optional: true,
|
type: crate::KeepOptions,
|
||||||
schema: PRUNE_SCHEMA_KEEP_LAST,
|
|
||||||
},
|
|
||||||
"keep-hourly": {
|
|
||||||
optional: true,
|
|
||||||
schema: PRUNE_SCHEMA_KEEP_HOURLY,
|
|
||||||
},
|
|
||||||
"keep-daily": {
|
|
||||||
optional: true,
|
|
||||||
schema: PRUNE_SCHEMA_KEEP_DAILY,
|
|
||||||
},
|
|
||||||
"keep-weekly": {
|
|
||||||
optional: true,
|
|
||||||
schema: PRUNE_SCHEMA_KEEP_WEEKLY,
|
|
||||||
},
|
|
||||||
"keep-monthly": {
|
|
||||||
optional: true,
|
|
||||||
schema: PRUNE_SCHEMA_KEEP_MONTHLY,
|
|
||||||
},
|
|
||||||
"keep-yearly": {
|
|
||||||
optional: true,
|
|
||||||
schema: PRUNE_SCHEMA_KEEP_YEARLY,
|
|
||||||
},
|
},
|
||||||
"verify-new": {
|
"verify-new": {
|
||||||
description: "If enabled, all new backups will be verified right after completion.",
|
description: "If enabled, all new backups will be verified right after completion.",
|
||||||
@ -310,38 +243,38 @@ pub const DATASTORE_TUNING_STRING_SCHEMA: Schema = StringSchema::new("Datastore
|
|||||||
pub struct DataStoreConfig {
|
pub struct DataStoreConfig {
|
||||||
#[updater(skip)]
|
#[updater(skip)]
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
|
||||||
#[updater(skip)]
|
#[updater(skip)]
|
||||||
pub path: String,
|
pub path: String,
|
||||||
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub comment: Option<String>,
|
pub comment: Option<String>,
|
||||||
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub gc_schedule: Option<String>,
|
pub gc_schedule: Option<String>,
|
||||||
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub prune_schedule: Option<String>,
|
pub prune_schedule: Option<String>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub keep_last: Option<u64>,
|
#[serde(flatten)]
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
pub keep: crate::KeepOptions,
|
||||||
pub keep_hourly: Option<u64>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub keep_daily: Option<u64>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub keep_weekly: Option<u64>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub keep_monthly: Option<u64>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub keep_yearly: Option<u64>,
|
|
||||||
/// If enabled, all backups will be verified right after completion.
|
/// If enabled, all backups will be verified right after completion.
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub verify_new: Option<bool>,
|
pub verify_new: Option<bool>,
|
||||||
|
|
||||||
/// Send job email notification to this user
|
/// Send job email notification to this user
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub notify_user: Option<Userid>,
|
pub notify_user: Option<Userid>,
|
||||||
|
|
||||||
/// Send notification only for job errors
|
/// Send notification only for job errors
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub notify: Option<String>,
|
pub notify: Option<String>,
|
||||||
|
|
||||||
/// Datastore tuning options
|
/// Datastore tuning options
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub tuning: Option<String>,
|
pub tuning: Option<String>,
|
||||||
|
|
||||||
/// Maintenance mode, type is either 'offline' or 'read-only', message should be enclosed in "
|
/// Maintenance mode, type is either 'offline' or 'read-only', message should be enclosed in "
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub maintenance_mode: Option<String>,
|
pub maintenance_mode: Option<String>,
|
||||||
@ -355,12 +288,7 @@ impl DataStoreConfig {
|
|||||||
comment: None,
|
comment: None,
|
||||||
gc_schedule: None,
|
gc_schedule: None,
|
||||||
prune_schedule: None,
|
prune_schedule: None,
|
||||||
keep_last: None,
|
keep: Default::default(),
|
||||||
keep_hourly: None,
|
|
||||||
keep_daily: None,
|
|
||||||
keep_weekly: None,
|
|
||||||
keep_monthly: None,
|
|
||||||
keep_yearly: None,
|
|
||||||
verify_new: None,
|
verify_new: None,
|
||||||
notify_user: None,
|
notify_user: None,
|
||||||
notify: None,
|
notify: None,
|
||||||
@ -694,6 +622,39 @@ impl BackupNamespace {
|
|||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn acl_path<'a>(&'a self, store: &'a str) -> Vec<&'a str> {
|
||||||
|
let mut path: Vec<&str> = vec!["datastore", store];
|
||||||
|
|
||||||
|
if self.is_root() {
|
||||||
|
path
|
||||||
|
} else {
|
||||||
|
path.extend(self.inner.iter().map(|comp| comp.as_str()));
|
||||||
|
path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check whether this namespace contains another namespace.
|
||||||
|
///
|
||||||
|
/// If so, the depth is returned.
|
||||||
|
///
|
||||||
|
/// Example:
|
||||||
|
/// ```
|
||||||
|
/// # use pbs_api_types::BackupNamespace;
|
||||||
|
/// let main: BackupNamespace = "a/b".parse().unwrap();
|
||||||
|
/// let sub: BackupNamespace = "a/b/c/d".parse().unwrap();
|
||||||
|
/// let other: BackupNamespace = "x/y".parse().unwrap();
|
||||||
|
/// assert_eq!(main.contains(&main), Some(0));
|
||||||
|
/// assert_eq!(main.contains(&sub), Some(2));
|
||||||
|
/// assert_eq!(sub.contains(&main), None);
|
||||||
|
/// assert_eq!(main.contains(&other), None);
|
||||||
|
/// ```
|
||||||
|
pub fn contains(&self, other: &BackupNamespace) -> Option<usize> {
|
||||||
|
other
|
||||||
|
.inner
|
||||||
|
.strip_prefix(&self.inner[..])
|
||||||
|
.map(|suffix| suffix.len())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for BackupNamespace {
|
impl fmt::Display for BackupNamespace {
|
||||||
@ -983,7 +944,7 @@ impl BackupDir {
|
|||||||
where
|
where
|
||||||
T: Into<String>,
|
T: Into<String>,
|
||||||
{
|
{
|
||||||
let time = proxmox_time::parse_rfc3339(&backup_time_string)?;
|
let time = proxmox_time::parse_rfc3339(backup_time_string)?;
|
||||||
let group = BackupGroup::new(ty, id.into());
|
let group = BackupGroup::new(ty, id.into());
|
||||||
Ok(Self { group, time })
|
Ok(Self { group, time })
|
||||||
}
|
}
|
||||||
@ -1026,35 +987,6 @@ impl fmt::Display for BackupDir {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Helper struct for places where sensible formatting of store+NS combo is required
|
|
||||||
pub struct DatastoreWithNamespace {
|
|
||||||
pub store: String,
|
|
||||||
pub ns: BackupNamespace,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for DatastoreWithNamespace {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
if self.ns.is_root() {
|
|
||||||
write!(f, "datastore {}, root namespace", self.store)
|
|
||||||
} else {
|
|
||||||
write!(f, "datastore '{}', namespace '{}'", self.store, self.ns)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DatastoreWithNamespace {
|
|
||||||
pub fn acl_path(&self) -> Vec<&str> {
|
|
||||||
let mut path: Vec<&str> = vec!["datastore", &self.store];
|
|
||||||
|
|
||||||
if self.ns.is_root() {
|
|
||||||
path
|
|
||||||
} else {
|
|
||||||
path.extend(self.ns.inner.iter().map(|comp| comp.as_str()));
|
|
||||||
path
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Used when both a backup group or a directory can be valid.
|
/// Used when both a backup group or a directory can be valid.
|
||||||
pub enum BackupPart {
|
pub enum BackupPart {
|
||||||
Group(BackupGroup),
|
Group(BackupGroup),
|
||||||
@ -1379,6 +1311,23 @@ pub struct DataStoreStatusListItem {
|
|||||||
pub gc_status: Option<GarbageCollectionStatus>,
|
pub gc_status: Option<GarbageCollectionStatus>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl DataStoreStatusListItem {
|
||||||
|
pub fn empty(store: &str, err: Option<String>) -> Self {
|
||||||
|
DataStoreStatusListItem {
|
||||||
|
store: store.to_owned(),
|
||||||
|
total: -1,
|
||||||
|
used: -1,
|
||||||
|
avail: -1,
|
||||||
|
history: None,
|
||||||
|
history_start: None,
|
||||||
|
history_delta: None,
|
||||||
|
estimated_full_date: None,
|
||||||
|
error: err,
|
||||||
|
gc_status: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub const ADMIN_DATASTORE_LIST_SNAPSHOTS_RETURN_TYPE: ReturnType = ReturnType {
|
pub const ADMIN_DATASTORE_LIST_SNAPSHOTS_RETURN_TYPE: ReturnType = ReturnType {
|
||||||
optional: false,
|
optional: false,
|
||||||
schema: &ArraySchema::new(
|
schema: &ArraySchema::new(
|
||||||
@ -1479,3 +1428,12 @@ pub fn print_ns_and_snapshot(ns: &BackupNamespace, dir: &BackupDir) -> String {
|
|||||||
format!("{}/{}", ns.display_as_path(), dir)
|
format!("{}/{}", ns.display_as_path(), dir)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Prints a Datastore name and [`BackupNamespace`] for logs/errors.
|
||||||
|
pub fn print_store_and_ns(store: &str, ns: &BackupNamespace) -> String {
|
||||||
|
if ns.is_root() {
|
||||||
|
format!("datastore '{}', root namespace", store)
|
||||||
|
} else {
|
||||||
|
format!("datastore '{}', namespace '{}'", store, ns)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -18,7 +18,7 @@ const_regex! {
|
|||||||
/// Regex for verification jobs 'DATASTORE:ACTUAL_JOB_ID'
|
/// Regex for verification jobs 'DATASTORE:ACTUAL_JOB_ID'
|
||||||
pub VERIFICATION_JOB_WORKER_ID_REGEX = concat!(r"^(", PROXMOX_SAFE_ID_REGEX_STR!(), r"):");
|
pub VERIFICATION_JOB_WORKER_ID_REGEX = concat!(r"^(", PROXMOX_SAFE_ID_REGEX_STR!(), r"):");
|
||||||
/// Regex for sync jobs 'REMOTE:REMOTE_DATASTORE:LOCAL_DATASTORE:(?:LOCAL_NS_ANCHOR:)ACTUAL_JOB_ID'
|
/// Regex for sync jobs 'REMOTE:REMOTE_DATASTORE:LOCAL_DATASTORE:(?:LOCAL_NS_ANCHOR:)ACTUAL_JOB_ID'
|
||||||
pub SYNC_JOB_WORKER_ID_REGEX = concat!(r"^(", PROXMOX_SAFE_ID_REGEX_STR!(), r"):(", PROXMOX_SAFE_ID_REGEX_STR!(), r"):(", PROXMOX_SAFE_ID_REGEX_STR!(), r"):(?:", BACKUP_NS_RE!(), r"):");
|
pub SYNC_JOB_WORKER_ID_REGEX = concat!(r"^(", PROXMOX_SAFE_ID_REGEX_STR!(), r"):(", PROXMOX_SAFE_ID_REGEX_STR!(), r"):(", PROXMOX_SAFE_ID_REGEX_STR!(), r")(?::(", BACKUP_NS_RE!(), r"))?:");
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const JOB_ID_SCHEMA: Schema = StringSchema::new("Job ID.")
|
pub const JOB_ID_SCHEMA: Schema = StringSchema::new("Job ID.")
|
||||||
@ -155,7 +155,7 @@ pub const IGNORE_VERIFIED_BACKUPS_SCHEMA: Schema = BooleanSchema::new(
|
|||||||
.schema();
|
.schema();
|
||||||
|
|
||||||
pub const VERIFICATION_OUTDATED_AFTER_SCHEMA: Schema =
|
pub const VERIFICATION_OUTDATED_AFTER_SCHEMA: Schema =
|
||||||
IntegerSchema::new("Days after that a verification becomes outdated. (0 means always)")
|
IntegerSchema::new("Days after that a verification becomes outdated. (0 is deprecated)'")
|
||||||
.minimum(0)
|
.minimum(0)
|
||||||
.schema();
|
.schema();
|
||||||
|
|
||||||
@ -200,7 +200,7 @@ pub struct VerificationJobConfig {
|
|||||||
/// unique ID to address this job
|
/// unique ID to address this job
|
||||||
#[updater(skip)]
|
#[updater(skip)]
|
||||||
pub id: String,
|
pub id: String,
|
||||||
/// the datastore ID this verificaiton job affects
|
/// the datastore ID this verification job affects
|
||||||
pub store: String,
|
pub store: String,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
/// if not set to false, check the age of the last snapshot verification to filter
|
/// if not set to false, check the age of the last snapshot verification to filter
|
||||||
@ -223,6 +223,15 @@ pub struct VerificationJobConfig {
|
|||||||
pub max_depth: Option<usize>,
|
pub max_depth: Option<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl VerificationJobConfig {
|
||||||
|
pub fn acl_path(&self) -> Vec<&str> {
|
||||||
|
match self.ns.as_ref() {
|
||||||
|
Some(ns) => ns.acl_path(&self.store),
|
||||||
|
None => vec!["datastore", &self.store],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[api(
|
#[api(
|
||||||
properties: {
|
properties: {
|
||||||
config: {
|
config: {
|
||||||
@ -381,7 +390,7 @@ impl std::str::FromStr for GroupFilter {
|
|||||||
type Err = anyhow::Error;
|
type Err = anyhow::Error;
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
match s.split_once(":") {
|
match s.split_once(':') {
|
||||||
Some(("group", value)) => BACKUP_GROUP_SCHEMA.parse_simple_value(value).map(|_| GroupFilter::Group(value.to_string())),
|
Some(("group", value)) => BACKUP_GROUP_SCHEMA.parse_simple_value(value).map(|_| GroupFilter::Group(value.to_string())),
|
||||||
Some(("type", value)) => Ok(GroupFilter::BackupType(value.parse()?)),
|
Some(("type", value)) => Ok(GroupFilter::BackupType(value.parse()?)),
|
||||||
Some(("regex", value)) => Ok(GroupFilter::Regex(Regex::new(value)?)),
|
Some(("regex", value)) => Ok(GroupFilter::Regex(Regex::new(value)?)),
|
||||||
@ -498,6 +507,15 @@ pub struct SyncJobConfig {
|
|||||||
pub limit: RateLimitConfig,
|
pub limit: RateLimitConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl SyncJobConfig {
|
||||||
|
pub fn acl_path(&self) -> Vec<&str> {
|
||||||
|
match self.ns.as_ref() {
|
||||||
|
Some(ns) => ns.acl_path(&self.store),
|
||||||
|
None => vec!["datastore", &self.store],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[api(
|
#[api(
|
||||||
properties: {
|
properties: {
|
||||||
config: {
|
config: {
|
||||||
@ -517,3 +535,186 @@ pub struct SyncJobStatus {
|
|||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
pub status: JobScheduleStatus,
|
pub status: JobScheduleStatus,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// These are used separately without `ns`/`max-depth` sometimes in the API, specifically in the API
|
||||||
|
/// call to prune a specific group, where `max-depth` makes no sense.
|
||||||
|
#[api(
|
||||||
|
properties: {
|
||||||
|
"keep-last": {
|
||||||
|
schema: crate::PRUNE_SCHEMA_KEEP_LAST,
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
"keep-hourly": {
|
||||||
|
schema: crate::PRUNE_SCHEMA_KEEP_HOURLY,
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
"keep-daily": {
|
||||||
|
schema: crate::PRUNE_SCHEMA_KEEP_DAILY,
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
"keep-weekly": {
|
||||||
|
schema: crate::PRUNE_SCHEMA_KEEP_WEEKLY,
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
"keep-monthly": {
|
||||||
|
schema: crate::PRUNE_SCHEMA_KEEP_MONTHLY,
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
"keep-yearly": {
|
||||||
|
schema: crate::PRUNE_SCHEMA_KEEP_YEARLY,
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)]
|
||||||
|
#[derive(Serialize, Deserialize, Default, Updater)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
/// Common pruning options
|
||||||
|
pub struct KeepOptions {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub keep_last: Option<u64>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub keep_hourly: Option<u64>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub keep_daily: Option<u64>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub keep_weekly: Option<u64>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub keep_monthly: Option<u64>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub keep_yearly: Option<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl KeepOptions {
|
||||||
|
pub fn keeps_something(&self) -> bool {
|
||||||
|
self.keep_last.unwrap_or(0)
|
||||||
|
+ self.keep_hourly.unwrap_or(0)
|
||||||
|
+ self.keep_daily.unwrap_or(0)
|
||||||
|
+ self.keep_weekly.unwrap_or(0)
|
||||||
|
+ self.keep_monthly.unwrap_or(0)
|
||||||
|
+ self.keep_yearly.unwrap_or(0)
|
||||||
|
> 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[api(
|
||||||
|
properties: {
|
||||||
|
keep: {
|
||||||
|
type: KeepOptions,
|
||||||
|
},
|
||||||
|
ns: {
|
||||||
|
type: BackupNamespace,
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
"max-depth": {
|
||||||
|
schema: NS_MAX_DEPTH_REDUCED_SCHEMA,
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)]
|
||||||
|
#[derive(Serialize, Deserialize, Default, Updater)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
/// Common pruning options
|
||||||
|
pub struct PruneJobOptions {
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub keep: KeepOptions,
|
||||||
|
|
||||||
|
/// The (optional) recursion depth
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub max_depth: Option<usize>,
|
||||||
|
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub ns: Option<BackupNamespace>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PruneJobOptions {
|
||||||
|
pub fn keeps_something(&self) -> bool {
|
||||||
|
self.keep.keeps_something()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn acl_path<'a>(&'a self, store: &'a str) -> Vec<&'a str> {
|
||||||
|
match &self.ns {
|
||||||
|
Some(ns) => ns.acl_path(store),
|
||||||
|
None => vec!["datastore", store],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[api(
|
||||||
|
properties: {
|
||||||
|
disable: {
|
||||||
|
type: Boolean,
|
||||||
|
optional: true,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
id: {
|
||||||
|
schema: JOB_ID_SCHEMA,
|
||||||
|
},
|
||||||
|
store: {
|
||||||
|
schema: DATASTORE_SCHEMA,
|
||||||
|
},
|
||||||
|
schedule: {
|
||||||
|
schema: PRUNE_SCHEDULE_SCHEMA,
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
comment: {
|
||||||
|
optional: true,
|
||||||
|
schema: SINGLE_LINE_COMMENT_SCHEMA,
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
type: PruneJobOptions,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)]
|
||||||
|
#[derive(Deserialize, Serialize, Updater)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
/// Prune configuration.
|
||||||
|
pub struct PruneJobConfig {
|
||||||
|
/// unique ID to address this job
|
||||||
|
#[updater(skip)]
|
||||||
|
pub id: String,
|
||||||
|
|
||||||
|
pub store: String,
|
||||||
|
|
||||||
|
/// Disable this job.
|
||||||
|
#[serde(default, skip_serializing_if = "is_false")]
|
||||||
|
#[updater(serde(skip_serializing_if = "Option::is_none"))]
|
||||||
|
pub disable: bool,
|
||||||
|
|
||||||
|
pub schedule: String,
|
||||||
|
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub comment: Option<String>,
|
||||||
|
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub options: PruneJobOptions,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PruneJobConfig {
|
||||||
|
pub fn acl_path(&self) -> Vec<&str> {
|
||||||
|
self.options.acl_path(&self.store)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_false(b: &bool) -> bool {
|
||||||
|
!b
|
||||||
|
}
|
||||||
|
|
||||||
|
#[api(
|
||||||
|
properties: {
|
||||||
|
config: {
|
||||||
|
type: PruneJobConfig,
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
type: JobScheduleStatus,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)]
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
/// Status of prune job
|
||||||
|
pub struct PruneJobStatus {
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub config: PruneJobConfig,
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub status: JobScheduleStatus,
|
||||||
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "pbs-buildcfg"
|
name = "pbs-buildcfg"
|
||||||
version = "2.2.1"
|
version = "2.2.3"
|
||||||
authors = ["Proxmox Support Team <support@proxmox.com>"]
|
authors = ["Proxmox Support Team <support@proxmox.com>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
description = "macros used for pbs related paths such as configdir and rundir"
|
description = "macros used for pbs related paths such as configdir and rundir"
|
||||||
|
@ -16,12 +16,12 @@ http = "0.2"
|
|||||||
hyper = { version = "0.14", features = [ "full" ] }
|
hyper = { version = "0.14", features = [ "full" ] }
|
||||||
lazy_static = "1.4"
|
lazy_static = "1.4"
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
nix = "0.19.1"
|
nix = "0.24"
|
||||||
openssl = "0.10"
|
openssl = "0.10"
|
||||||
percent-encoding = "2.1"
|
percent-encoding = "2.1"
|
||||||
pin-project-lite = "0.2"
|
pin-project-lite = "0.2"
|
||||||
regex = "1.5"
|
regex = "1.5"
|
||||||
rustyline = "7"
|
rustyline = "9"
|
||||||
serde = "1.0"
|
serde = "1.0"
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
tokio = { version = "1.6", features = [ "fs", "signal" ] }
|
tokio = { version = "1.6", features = [ "fs", "signal" ] }
|
||||||
@ -41,7 +41,7 @@ proxmox-lang = "1.1"
|
|||||||
proxmox-router = { version = "1.2", features = [ "cli" ] }
|
proxmox-router = { version = "1.2", features = [ "cli" ] }
|
||||||
proxmox-schema = "1.3.1"
|
proxmox-schema = "1.3.1"
|
||||||
proxmox-time = "1"
|
proxmox-time = "1"
|
||||||
proxmox-sys = "0.2"
|
proxmox-sys = "0.3"
|
||||||
|
|
||||||
pxar = { version = "0.10.1", features = [ "tokio-io" ] }
|
pxar = { version = "0.10.1", features = [ "tokio-io" ] }
|
||||||
|
|
||||||
|
@ -329,13 +329,13 @@ impl Archiver {
|
|||||||
Mode::empty(),
|
Mode::empty(),
|
||||||
) {
|
) {
|
||||||
Ok(fd) => Ok(Some(fd)),
|
Ok(fd) => Ok(Some(fd)),
|
||||||
Err(nix::Error::Sys(Errno::ENOENT)) => {
|
Err(Errno::ENOENT) => {
|
||||||
if existed {
|
if existed {
|
||||||
self.report_vanished_file()?;
|
self.report_vanished_file()?;
|
||||||
}
|
}
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
Err(nix::Error::Sys(Errno::EACCES)) => {
|
Err(Errno::EACCES) => {
|
||||||
writeln!(
|
writeln!(
|
||||||
self.errors,
|
self.errors,
|
||||||
"failed to open file: {:?}: access denied",
|
"failed to open file: {:?}: access denied",
|
||||||
@ -343,7 +343,7 @@ impl Archiver {
|
|||||||
)?;
|
)?;
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
Err(nix::Error::Sys(Errno::EPERM)) if !noatime.is_empty() => {
|
Err(Errno::EPERM) if !noatime.is_empty() => {
|
||||||
// Retry without O_NOATIME:
|
// Retry without O_NOATIME:
|
||||||
noatime = OFlag::empty();
|
noatime = OFlag::empty();
|
||||||
continue;
|
continue;
|
||||||
@ -899,7 +899,7 @@ fn get_chattr(metadata: &mut Metadata, fd: RawFd) -> Result<(), Error> {
|
|||||||
|
|
||||||
match unsafe { fs::read_attr_fd(fd, &mut attr) } {
|
match unsafe { fs::read_attr_fd(fd, &mut attr) } {
|
||||||
Ok(_) => (),
|
Ok(_) => (),
|
||||||
Err(nix::Error::Sys(errno)) if errno_is_unsupported(errno) => {
|
Err(errno) if errno_is_unsupported(errno) => {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
Err(err) => bail!("failed to read file attributes: {}", err),
|
Err(err) => bail!("failed to read file attributes: {}", err),
|
||||||
@ -921,7 +921,7 @@ fn get_fat_attr(metadata: &mut Metadata, fd: RawFd, fs_magic: i64) -> Result<(),
|
|||||||
|
|
||||||
match unsafe { fs::read_fat_attr_fd(fd, &mut attr) } {
|
match unsafe { fs::read_fat_attr_fd(fd, &mut attr) } {
|
||||||
Ok(_) => (),
|
Ok(_) => (),
|
||||||
Err(nix::Error::Sys(errno)) if errno_is_unsupported(errno) => {
|
Err(errno) if errno_is_unsupported(errno) => {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
Err(err) => bail!("failed to read fat attributes: {}", err),
|
Err(err) => bail!("failed to read fat attributes: {}", err),
|
||||||
@ -959,10 +959,7 @@ fn get_quota_project_id(
|
|||||||
|
|
||||||
// On some FUSE filesystems it can happen that ioctl is not supported.
|
// On some FUSE filesystems it can happen that ioctl is not supported.
|
||||||
// For these cases projid is set to 0 while the error is ignored.
|
// For these cases projid is set to 0 while the error is ignored.
|
||||||
if let Err(err) = res {
|
if let Err(errno) = res {
|
||||||
let errno = err
|
|
||||||
.as_errno()
|
|
||||||
.ok_or_else(|| format_err!("error while reading quota project id"))?;
|
|
||||||
if errno_is_unsupported(errno) {
|
if errno_is_unsupported(errno) {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
} else {
|
} else {
|
||||||
|
@ -428,7 +428,7 @@ impl Extractor {
|
|||||||
if result.seeked_last {
|
if result.seeked_last {
|
||||||
while match nix::unistd::ftruncate(file.as_raw_fd(), size as i64) {
|
while match nix::unistd::ftruncate(file.as_raw_fd(), size as i64) {
|
||||||
Ok(_) => false,
|
Ok(_) => false,
|
||||||
Err(nix::Error::Sys(errno)) if errno == nix::errno::Errno::EINTR => true,
|
Err(errno) if errno == nix::errno::Errno::EINTR => true,
|
||||||
Err(err) => bail!("error setting file size: {}", err),
|
Err(err) => bail!("error setting file size: {}", err),
|
||||||
} {}
|
} {}
|
||||||
}
|
}
|
||||||
@ -485,7 +485,7 @@ impl Extractor {
|
|||||||
if result.seeked_last {
|
if result.seeked_last {
|
||||||
while match nix::unistd::ftruncate(file.as_raw_fd(), size as i64) {
|
while match nix::unistd::ftruncate(file.as_raw_fd(), size as i64) {
|
||||||
Ok(_) => false,
|
Ok(_) => false,
|
||||||
Err(nix::Error::Sys(errno)) if errno == nix::errno::Errno::EINTR => true,
|
Err(errno) if errno == nix::errno::Errno::EINTR => true,
|
||||||
Err(err) => bail!("error setting file size: {}", err),
|
Err(err) => bail!("error setting file size: {}", err),
|
||||||
} {}
|
} {}
|
||||||
}
|
}
|
||||||
@ -584,8 +584,7 @@ where
|
|||||||
match entry.kind() {
|
match entry.kind() {
|
||||||
EntryKind::File { .. } => {
|
EntryKind::File { .. } => {
|
||||||
let size = decoder.content_size().unwrap_or(0);
|
let size = decoder.content_size().unwrap_or(0);
|
||||||
tar_add_file(&mut tarencoder, decoder.contents(), size, &metadata, &path)
|
tar_add_file(&mut tarencoder, decoder.contents(), size, metadata, path).await?
|
||||||
.await?
|
|
||||||
}
|
}
|
||||||
EntryKind::Hardlink(link) => {
|
EntryKind::Hardlink(link) => {
|
||||||
if !link.data.is_empty() {
|
if !link.data.is_empty() {
|
||||||
@ -614,7 +613,7 @@ where
|
|||||||
decoder.contents(),
|
decoder.contents(),
|
||||||
size,
|
size,
|
||||||
metadata,
|
metadata,
|
||||||
&path,
|
path,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
hardlinks.insert(realpath.to_owned(), path.to_owned());
|
hardlinks.insert(realpath.to_owned(), path.to_owned());
|
||||||
|
@ -372,7 +372,7 @@ fn apply_chattr(fd: RawFd, chattr: libc::c_long, mask: libc::c_long) -> Result<(
|
|||||||
let mut fattr: libc::c_long = 0;
|
let mut fattr: libc::c_long = 0;
|
||||||
match unsafe { fs::read_attr_fd(fd, &mut fattr) } {
|
match unsafe { fs::read_attr_fd(fd, &mut fattr) } {
|
||||||
Ok(_) => (),
|
Ok(_) => (),
|
||||||
Err(nix::Error::Sys(errno)) if errno_is_unsupported(errno) => {
|
Err(errno) if errno_is_unsupported(errno) => {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
Err(err) => bail!("failed to read file attributes: {}", err),
|
Err(err) => bail!("failed to read file attributes: {}", err),
|
||||||
@ -386,7 +386,7 @@ fn apply_chattr(fd: RawFd, chattr: libc::c_long, mask: libc::c_long) -> Result<(
|
|||||||
|
|
||||||
match unsafe { fs::write_attr_fd(fd, &attr) } {
|
match unsafe { fs::write_attr_fd(fd, &attr) } {
|
||||||
Ok(_) => Ok(()),
|
Ok(_) => Ok(()),
|
||||||
Err(nix::Error::Sys(errno)) if errno_is_unsupported(errno) => Ok(()),
|
Err(errno) if errno_is_unsupported(errno) => Ok(()),
|
||||||
Err(err) => bail!("failed to set file attributes: {}", err),
|
Err(err) => bail!("failed to set file attributes: {}", err),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -400,7 +400,7 @@ fn apply_flags(flags: Flags, fd: RawFd, entry_flags: u64) -> Result<(), Error> {
|
|||||||
if fatattr != 0 {
|
if fatattr != 0 {
|
||||||
match unsafe { fs::write_fat_attr_fd(fd, &fatattr) } {
|
match unsafe { fs::write_fat_attr_fd(fd, &fatattr) } {
|
||||||
Ok(_) => (),
|
Ok(_) => (),
|
||||||
Err(nix::Error::Sys(errno)) if errno_is_unsupported(errno) => (),
|
Err(errno) if errno_is_unsupported(errno) => (),
|
||||||
Err(err) => bail!("failed to set file FAT attributes: {}", err),
|
Err(err) => bail!("failed to set file FAT attributes: {}", err),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -72,7 +72,7 @@ impl tower_service::Service<Uri> for VsockConnector {
|
|||||||
)?;
|
)?;
|
||||||
|
|
||||||
let sock_addr = VsockAddr::new(cid, port as u32);
|
let sock_addr = VsockAddr::new(cid, port as u32);
|
||||||
connect(sock_fd, &SockAddr::Vsock(sock_addr))?;
|
connect(sock_fd, &sock_addr)?;
|
||||||
|
|
||||||
// connect sync, but set nonblock after (tokio requires it)
|
// connect sync, but set nonblock after (tokio requires it)
|
||||||
let std_stream = unsafe { std::os::unix::net::UnixStream::from_raw_fd(sock_fd) };
|
let std_stream = unsafe { std::os::unix::net::UnixStream::from_raw_fd(sock_fd) };
|
||||||
|
@ -10,7 +10,7 @@ anyhow = "1.0"
|
|||||||
hex = "0.4.3"
|
hex = "0.4.3"
|
||||||
lazy_static = "1.4"
|
lazy_static = "1.4"
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
nix = "0.19.1"
|
nix = "0.24"
|
||||||
once_cell = "1.3.1"
|
once_cell = "1.3.1"
|
||||||
openssl = "0.10"
|
openssl = "0.10"
|
||||||
regex = "1.5"
|
regex = "1.5"
|
||||||
@ -24,7 +24,7 @@ proxmox-section-config = "1"
|
|||||||
proxmox-time = "1"
|
proxmox-time = "1"
|
||||||
proxmox-serde = "0.1"
|
proxmox-serde = "0.1"
|
||||||
proxmox-shared-memory = "0.2"
|
proxmox-shared-memory = "0.2"
|
||||||
proxmox-sys = "0.2"
|
proxmox-sys = "0.3"
|
||||||
|
|
||||||
pbs-api-types = { path = "../pbs-api-types" }
|
pbs-api-types = { path = "../pbs-api-types" }
|
||||||
pbs-buildcfg = { path = "../pbs-buildcfg" }
|
pbs-buildcfg = { path = "../pbs-buildcfg" }
|
||||||
|
@ -4,7 +4,7 @@ use std::path::{Path, PathBuf};
|
|||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::sync::{Arc, RwLock};
|
use std::sync::{Arc, RwLock};
|
||||||
|
|
||||||
use anyhow::{bail, Error};
|
use anyhow::{bail, format_err, Error};
|
||||||
|
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
|
|
||||||
@ -301,6 +301,45 @@ impl AclTreeNode {
|
|||||||
map.insert(role, propagate);
|
map.insert(role, propagate);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Check if auth_id has any of the provided privileges on the current note.
|
||||||
|
///
|
||||||
|
/// If `only_propagated` is set to true only propagating privileges will be checked.
|
||||||
|
fn check_any_privs(
|
||||||
|
&self,
|
||||||
|
auth_id: &Authid,
|
||||||
|
privs: u64,
|
||||||
|
only_propagated: bool,
|
||||||
|
) -> Result<bool, Error> {
|
||||||
|
for role in self.extract_roles(&auth_id, !only_propagated).into_keys() {
|
||||||
|
let current_privs = Role::from_str(&role)
|
||||||
|
.map_err(|e| format_err!("invalid role in current node: {role} - {e}"))?
|
||||||
|
as u64;
|
||||||
|
|
||||||
|
if privs & current_privs != 0 {
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if the given auth_id has any of the privileges specified by `privs` on the sub-tree
|
||||||
|
/// below the current node.
|
||||||
|
fn any_privs_below(&self, auth_id: &Authid, privs: u64) -> Result<bool, Error> {
|
||||||
|
// set only_propagated to false to check all roles on the current node
|
||||||
|
if self.check_any_privs(auth_id, privs, false)? {
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (_comp, child) in self.children.iter() {
|
||||||
|
if child.any_privs_below(auth_id, privs)? {
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AclTree {
|
impl AclTree {
|
||||||
@ -603,15 +642,22 @@ impl AclTree {
|
|||||||
let mut node = &self.root;
|
let mut node = &self.root;
|
||||||
let mut role_map = node.extract_roles(auth_id, path.is_empty());
|
let mut role_map = node.extract_roles(auth_id, path.is_empty());
|
||||||
|
|
||||||
for (pos, comp) in path.iter().enumerate() {
|
let mut comp_iter = path.iter().peekable();
|
||||||
let last_comp = (pos + 1) == path.len();
|
|
||||||
for scomp in comp.split('/') {
|
while let Some(comp) = comp_iter.next() {
|
||||||
node = match node.children.get(scomp) {
|
let last_comp = comp_iter.peek().is_none();
|
||||||
|
|
||||||
|
let mut sub_comp_iter = comp.split('/').peekable();
|
||||||
|
|
||||||
|
while let Some(sub_comp) = sub_comp_iter.next() {
|
||||||
|
let last_sub_comp = last_comp && sub_comp_iter.peek().is_none();
|
||||||
|
|
||||||
|
node = match node.children.get(sub_comp) {
|
||||||
Some(n) => n,
|
Some(n) => n,
|
||||||
None => return role_map, // path not found
|
None => return role_map, // path not found
|
||||||
};
|
};
|
||||||
|
|
||||||
let new_map = node.extract_roles(auth_id, last_comp);
|
let new_map = node.extract_roles(auth_id, last_sub_comp);
|
||||||
if !new_map.is_empty() {
|
if !new_map.is_empty() {
|
||||||
// overwrite previous mappings
|
// overwrite previous mappings
|
||||||
role_map = new_map;
|
role_map = new_map;
|
||||||
@ -621,6 +667,44 @@ impl AclTree {
|
|||||||
|
|
||||||
role_map
|
role_map
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Checks whether the `auth_id` has any of the privilegs `privs` on any object below `path`.
|
||||||
|
pub fn any_privs_below(
|
||||||
|
&self,
|
||||||
|
auth_id: &Authid,
|
||||||
|
path: &[&str],
|
||||||
|
privs: u64,
|
||||||
|
) -> Result<bool, Error> {
|
||||||
|
let mut node = &self.root;
|
||||||
|
|
||||||
|
// check first if there's any propagated priv we need to be aware of
|
||||||
|
for outer in path {
|
||||||
|
for c in outer.split('/') {
|
||||||
|
if node.check_any_privs(auth_id, privs, true)? {
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
// check next component
|
||||||
|
node = node.children.get(&c.to_string()).ok_or(format_err!(
|
||||||
|
"component '{c}' of path '{path:?}' does not exist in current acl tree"
|
||||||
|
))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check last node in the path too
|
||||||
|
if node.check_any_privs(auth_id, privs, true)? {
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// now search trough the sub-tree
|
||||||
|
for (_comp, child) in node.children.iter() {
|
||||||
|
if child.any_privs_below(auth_id, privs)? {
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// we could not find any privileges, return false
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Filename where [AclTree] is stored.
|
/// Filename where [AclTree] is stored.
|
||||||
@ -660,7 +744,7 @@ pub fn cached_config() -> Result<Arc<AclTree>, Error> {
|
|||||||
|
|
||||||
let stat = match nix::sys::stat::stat(ACL_CFG_FILENAME) {
|
let stat = match nix::sys::stat::stat(ACL_CFG_FILENAME) {
|
||||||
Ok(stat) => Some(stat),
|
Ok(stat) => Some(stat),
|
||||||
Err(nix::Error::Sys(nix::errno::Errno::ENOENT)) => None,
|
Err(nix::errno::Errno::ENOENT) => None,
|
||||||
Err(err) => bail!("unable to stat '{}' - {}", ACL_CFG_FILENAME, err),
|
Err(err) => bail!("unable to stat '{}' - {}", ACL_CFG_FILENAME, err),
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -707,7 +791,7 @@ mod test {
|
|||||||
use super::AclTree;
|
use super::AclTree;
|
||||||
use anyhow::Error;
|
use anyhow::Error;
|
||||||
|
|
||||||
use pbs_api_types::Authid;
|
use pbs_api_types::{Authid, ROLE_ADMIN, ROLE_DATASTORE_READER, ROLE_TAPE_READER};
|
||||||
|
|
||||||
fn check_roles(tree: &AclTree, auth_id: &Authid, path: &str, expected_roles: &str) {
|
fn check_roles(tree: &AclTree, auth_id: &Authid, path: &str, expected_roles: &str) {
|
||||||
let path_vec = super::split_acl_path(path);
|
let path_vec = super::split_acl_path(path);
|
||||||
@ -849,4 +933,45 @@ acl:1:/storage/store1:user1@pbs:DatastoreBackup
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_any_privs_below() -> Result<(), Error> {
|
||||||
|
let tree = AclTree::from_raw(
|
||||||
|
"\
|
||||||
|
acl:0:/store/store2:user1@pbs:Admin\n\
|
||||||
|
acl:1:/store/store2/store31/store4/store6:user2@pbs:DatastoreReader\n\
|
||||||
|
acl:0:/store/store2/store3:user1@pbs:Admin\n\
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.expect("failed to parse acl tree");
|
||||||
|
|
||||||
|
let user1: Authid = "user1@pbs".parse()?;
|
||||||
|
let user2: Authid = "user2@pbs".parse()?;
|
||||||
|
|
||||||
|
// user1 has admin on "/store/store2/store3" -> return true
|
||||||
|
assert!(tree.any_privs_below(&user1, &["store"], ROLE_ADMIN)?);
|
||||||
|
|
||||||
|
// user2 has not privileges under "/store/store2/store3" --> return false
|
||||||
|
assert!(!tree.any_privs_below(
|
||||||
|
&user2,
|
||||||
|
&["store", "store2", "store3"],
|
||||||
|
ROLE_DATASTORE_READER
|
||||||
|
)?);
|
||||||
|
|
||||||
|
// user2 has DatastoreReader privileges under "/store/store2/store31" --> return true
|
||||||
|
assert!(tree.any_privs_below(&user2, &["store/store2/store31"], ROLE_DATASTORE_READER)?);
|
||||||
|
|
||||||
|
// user2 has no TapeReader privileges under "/store/store2/store31" --> return false
|
||||||
|
assert!(!tree.any_privs_below(&user2, &["store/store2/store31"], ROLE_TAPE_READER)?);
|
||||||
|
|
||||||
|
// user2 has no DatastoreReader propagating privileges on
|
||||||
|
// "/store/store2/store31/store4/store6" --> return true
|
||||||
|
assert!(tree.any_privs_below(
|
||||||
|
&user2,
|
||||||
|
&["store/store2/store31/store4/store6"],
|
||||||
|
ROLE_DATASTORE_READER
|
||||||
|
)?);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@ use proxmox_router::UserInformation;
|
|||||||
use proxmox_section_config::SectionConfigData;
|
use proxmox_section_config::SectionConfigData;
|
||||||
use proxmox_time::epoch_i64;
|
use proxmox_time::epoch_i64;
|
||||||
|
|
||||||
use pbs_api_types::{ApiToken, Authid, User, Userid, ROLE_ADMIN};
|
use pbs_api_types::{privs_to_priv_names, ApiToken, Authid, User, Userid, ROLE_ADMIN};
|
||||||
|
|
||||||
use crate::acl::{AclTree, ROLE_NAMES};
|
use crate::acl::{AclTree, ROLE_NAMES};
|
||||||
use crate::ConfigVersionCache;
|
use crate::ConfigVersionCache;
|
||||||
@ -123,7 +123,16 @@ impl CachedUserInfo {
|
|||||||
if !allowed {
|
if !allowed {
|
||||||
// printing the path doesn't leaks any information as long as we
|
// printing the path doesn't leaks any information as long as we
|
||||||
// always check privilege before resource existence
|
// always check privilege before resource existence
|
||||||
bail!("no permissions on '/{}'", path.join("/"));
|
let priv_names = privs_to_priv_names(required_privs);
|
||||||
|
let priv_names = if partial {
|
||||||
|
priv_names.join("|")
|
||||||
|
} else {
|
||||||
|
priv_names.join("&")
|
||||||
|
};
|
||||||
|
bail!(
|
||||||
|
"missing permissions '{priv_names}' on '/{}'",
|
||||||
|
path.join("/")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -170,6 +179,16 @@ impl CachedUserInfo {
|
|||||||
|
|
||||||
(privs, propagated_privs)
|
(privs, propagated_privs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Checks whether the `auth_id` has any of the privilegs `privs` on any object below `path`.
|
||||||
|
pub fn any_privs_below(
|
||||||
|
&self,
|
||||||
|
auth_id: &Authid,
|
||||||
|
path: &[&str],
|
||||||
|
privs: u64,
|
||||||
|
) -> Result<bool, Error> {
|
||||||
|
self.acl_tree.any_privs_below(auth_id, path, privs)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UserInformation for CachedUserInfo {
|
impl UserInformation for CachedUserInfo {
|
||||||
|
@ -26,6 +26,7 @@ struct ConfigVersionCacheDataInner {
|
|||||||
// Traffic control (traffic-control.cfg) generation/version.
|
// Traffic control (traffic-control.cfg) generation/version.
|
||||||
traffic_control_generation: AtomicUsize,
|
traffic_control_generation: AtomicUsize,
|
||||||
// datastore (datastore.cfg) generation/version
|
// datastore (datastore.cfg) generation/version
|
||||||
|
// FIXME: remove with PBS 3.0
|
||||||
datastore_generation: AtomicUsize,
|
datastore_generation: AtomicUsize,
|
||||||
// Add further atomics here
|
// Add further atomics here
|
||||||
}
|
}
|
||||||
@ -144,19 +145,12 @@ impl ConfigVersionCache {
|
|||||||
.fetch_add(1, Ordering::AcqRel);
|
.fetch_add(1, Ordering::AcqRel);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the datastore generation number.
|
|
||||||
pub fn datastore_generation(&self) -> usize {
|
|
||||||
self.shmem
|
|
||||||
.data()
|
|
||||||
.datastore_generation
|
|
||||||
.load(Ordering::Acquire)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Increase the datastore generation number.
|
/// Increase the datastore generation number.
|
||||||
|
// FIXME: remove with PBS 3.0 or make actually useful again in datastore lookup
|
||||||
pub fn increase_datastore_generation(&self) -> usize {
|
pub fn increase_datastore_generation(&self) -> usize {
|
||||||
self.shmem
|
self.shmem
|
||||||
.data()
|
.data()
|
||||||
.datastore_generation
|
.datastore_generation
|
||||||
.fetch_add(1, Ordering::Acquire)
|
.fetch_add(1, Ordering::AcqRel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@ use anyhow::Error;
|
|||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use proxmox_schema::{ApiType, Schema};
|
use proxmox_schema::{AllOfSchema, ApiType};
|
||||||
use proxmox_section_config::{SectionConfig, SectionConfigData, SectionConfigPlugin};
|
use proxmox_section_config::{SectionConfig, SectionConfigData, SectionConfigPlugin};
|
||||||
|
|
||||||
use pbs_api_types::{DataStoreConfig, DATASTORE_SCHEMA};
|
use pbs_api_types::{DataStoreConfig, DATASTORE_SCHEMA};
|
||||||
@ -14,15 +14,12 @@ lazy_static! {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn init() -> SectionConfig {
|
fn init() -> SectionConfig {
|
||||||
let obj_schema = match DataStoreConfig::API_SCHEMA {
|
const OBJ_SCHEMA: &AllOfSchema = DataStoreConfig::API_SCHEMA.unwrap_all_of_schema();
|
||||||
Schema::Object(ref obj_schema) => obj_schema,
|
|
||||||
_ => unreachable!(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let plugin = SectionConfigPlugin::new(
|
let plugin = SectionConfigPlugin::new(
|
||||||
"datastore".to_string(),
|
"datastore".to_string(),
|
||||||
Some(String::from("name")),
|
Some(String::from("name")),
|
||||||
obj_schema,
|
OBJ_SCHEMA,
|
||||||
);
|
);
|
||||||
let mut config = SectionConfig::new(&DATASTORE_SCHEMA);
|
let mut config = SectionConfig::new(&DATASTORE_SCHEMA);
|
||||||
config.register_plugin(plugin);
|
config.register_plugin(plugin);
|
||||||
@ -67,11 +64,11 @@ pub fn complete_datastore_name(_arg: &str, _param: &HashMap<String, String>) ->
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn complete_acl_path(_arg: &str, _param: &HashMap<String, String>) -> Vec<String> {
|
pub fn complete_acl_path(_arg: &str, _param: &HashMap<String, String>) -> Vec<String> {
|
||||||
let mut list = Vec::new();
|
let mut list = vec![
|
||||||
|
String::from("/"),
|
||||||
list.push(String::from("/"));
|
String::from("/datastore"),
|
||||||
list.push(String::from("/datastore"));
|
String::from("/datastore/"),
|
||||||
list.push(String::from("/datastore/"));
|
];
|
||||||
|
|
||||||
if let Ok((data, _digest)) = config() {
|
if let Ok((data, _digest)) = config() {
|
||||||
for id in data.sections.keys() {
|
for id in data.sections.keys() {
|
||||||
|
@ -370,8 +370,8 @@ fn fingerprint_checks() -> Result<(), Error> {
|
|||||||
131, 185, 101, 156, 10, 87, 174, 25, 144, 144, 21, 155,
|
131, 185, 101, 156, 10, 87, 174, 25, 144, 144, 21, 155,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
let mut data = serde_json::to_vec(&key).expect("encoding KeyConfig failed");
|
let data = serde_json::to_vec(&key).expect("encoding KeyConfig failed");
|
||||||
decrypt_key(&mut data, &{ || Ok(Vec::new()) })
|
decrypt_key(&data, &{ || Ok(Vec::new()) })
|
||||||
.expect_err("decoding KeyConfig with wrong fingerprint worked");
|
.expect_err("decoding KeyConfig with wrong fingerprint worked");
|
||||||
|
|
||||||
let key = KeyConfig {
|
let key = KeyConfig {
|
||||||
@ -383,8 +383,8 @@ fn fingerprint_checks() -> Result<(), Error> {
|
|||||||
hint: None,
|
hint: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut data = serde_json::to_vec(&key).expect("encoding KeyConfig failed");
|
let data = serde_json::to_vec(&key).expect("encoding KeyConfig failed");
|
||||||
let (key_data, created, fingerprint) = decrypt_key(&mut data, &{ || Ok(Vec::new()) })
|
let (key_data, created, fingerprint) = decrypt_key(&data, &{ || Ok(Vec::new()) })
|
||||||
.expect("decoding KeyConfig without fingerprint failed");
|
.expect("decoding KeyConfig without fingerprint failed");
|
||||||
|
|
||||||
assert_eq!(key.data, key_data);
|
assert_eq!(key.data, key_data);
|
||||||
|
@ -7,6 +7,7 @@ pub mod drive;
|
|||||||
pub mod key_config;
|
pub mod key_config;
|
||||||
pub mod media_pool;
|
pub mod media_pool;
|
||||||
pub mod network;
|
pub mod network;
|
||||||
|
pub mod prune;
|
||||||
pub mod remote;
|
pub mod remote;
|
||||||
pub mod sync;
|
pub mod sync;
|
||||||
pub mod tape_encryption_keys;
|
pub mod tape_encryption_keys;
|
||||||
|
@ -101,7 +101,7 @@ pub fn parse_address_or_cidr(cidr: &str) -> Result<(String, Option<u8>, bool), E
|
|||||||
if let Some(caps) = CIDR_V4_REGEX.captures(cidr) {
|
if let Some(caps) = CIDR_V4_REGEX.captures(cidr) {
|
||||||
let address = &caps[1];
|
let address = &caps[1];
|
||||||
if let Some(mask) = caps.get(2) {
|
if let Some(mask) = caps.get(2) {
|
||||||
let mask = u8::from_str_radix(mask.as_str(), 10)?;
|
let mask: u8 = mask.as_str().parse()?;
|
||||||
check_netmask(mask, false)?;
|
check_netmask(mask, false)?;
|
||||||
Ok((address.to_string(), Some(mask), false))
|
Ok((address.to_string(), Some(mask), false))
|
||||||
} else {
|
} else {
|
||||||
@ -110,7 +110,7 @@ pub fn parse_address_or_cidr(cidr: &str) -> Result<(String, Option<u8>, bool), E
|
|||||||
} else if let Some(caps) = CIDR_V6_REGEX.captures(cidr) {
|
} else if let Some(caps) = CIDR_V6_REGEX.captures(cidr) {
|
||||||
let address = &caps[1];
|
let address = &caps[1];
|
||||||
if let Some(mask) = caps.get(2) {
|
if let Some(mask) = caps.get(2) {
|
||||||
let mask = u8::from_str_radix(mask.as_str(), 10)?;
|
let mask: u8 = mask.as_str().parse()?;
|
||||||
check_netmask(mask, true)?;
|
check_netmask(mask, true)?;
|
||||||
Ok((address.to_string(), Some(mask), true))
|
Ok((address.to_string(), Some(mask), true))
|
||||||
} else {
|
} else {
|
||||||
|
@ -164,7 +164,7 @@ impl<R: BufRead> NetworkParser<R> {
|
|||||||
let mask = if let Some(mask) = IPV4_MASK_HASH_LOCALNET.get(netmask.as_str()) {
|
let mask = if let Some(mask) = IPV4_MASK_HASH_LOCALNET.get(netmask.as_str()) {
|
||||||
*mask
|
*mask
|
||||||
} else {
|
} else {
|
||||||
match u8::from_str_radix(netmask.as_str(), 10) {
|
match netmask.as_str().parse::<u8>() {
|
||||||
Ok(mask) => mask,
|
Ok(mask) => mask,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
bail!("unable to parse netmask '{}' - {}", netmask, err);
|
bail!("unable to parse netmask '{}' - {}", netmask, err);
|
||||||
@ -211,7 +211,7 @@ impl<R: BufRead> NetworkParser<R> {
|
|||||||
self.eat(Token::MTU)?;
|
self.eat(Token::MTU)?;
|
||||||
|
|
||||||
let mtu = self.next_text()?;
|
let mtu = self.next_text()?;
|
||||||
let mtu = match u64::from_str_radix(&mtu, 10) {
|
let mtu = match mtu.parse::<u64>() {
|
||||||
Ok(mtu) => mtu,
|
Ok(mtu) => mtu,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
bail!("unable to parse mtu value '{}' - {}", mtu, err);
|
bail!("unable to parse mtu value '{}' - {}", mtu, err);
|
||||||
|
57
pbs-config/src/prune.rs
Normal file
57
pbs-config/src/prune.rs
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use anyhow::Error;
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
|
||||||
|
use proxmox_schema::*;
|
||||||
|
use proxmox_section_config::{SectionConfig, SectionConfigData, SectionConfigPlugin};
|
||||||
|
|
||||||
|
use pbs_api_types::{PruneJobConfig, JOB_ID_SCHEMA};
|
||||||
|
|
||||||
|
use crate::{open_backup_lockfile, replace_backup_config, BackupLockGuard};
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
pub static ref CONFIG: SectionConfig = init();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init() -> SectionConfig {
|
||||||
|
const OBJ_SCHEMA: &AllOfSchema = PruneJobConfig::API_SCHEMA.unwrap_all_of_schema();
|
||||||
|
|
||||||
|
let plugin =
|
||||||
|
SectionConfigPlugin::new("prune".to_string(), Some(String::from("id")), OBJ_SCHEMA);
|
||||||
|
let mut config = SectionConfig::new(&JOB_ID_SCHEMA);
|
||||||
|
config.register_plugin(plugin);
|
||||||
|
|
||||||
|
config
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const PRUNE_CFG_FILENAME: &str = "/etc/proxmox-backup/prune.cfg";
|
||||||
|
pub const PRUNE_CFG_LOCKFILE: &str = "/etc/proxmox-backup/.prune.lck";
|
||||||
|
|
||||||
|
/// Get exclusive lock
|
||||||
|
pub fn lock_config() -> Result<BackupLockGuard, Error> {
|
||||||
|
open_backup_lockfile(PRUNE_CFG_LOCKFILE, None, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn config() -> Result<(SectionConfigData, [u8; 32]), Error> {
|
||||||
|
let content = proxmox_sys::fs::file_read_optional_string(PRUNE_CFG_FILENAME)?;
|
||||||
|
let content = content.unwrap_or_default();
|
||||||
|
|
||||||
|
let digest = openssl::sha::sha256(content.as_bytes());
|
||||||
|
let data = CONFIG.parse(PRUNE_CFG_FILENAME, &content)?;
|
||||||
|
|
||||||
|
Ok((data, digest))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn save_config(config: &SectionConfigData) -> Result<(), Error> {
|
||||||
|
let raw = CONFIG.write(PRUNE_CFG_FILENAME, config)?;
|
||||||
|
replace_backup_config(PRUNE_CFG_FILENAME, raw.as_bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
// shell completion helper
|
||||||
|
pub fn complete_prune_job_id(_arg: &str, _param: &HashMap<String, String>) -> Vec<String> {
|
||||||
|
match config() {
|
||||||
|
Ok((data, _digest)) => data.sections.iter().map(|(id, _)| id.to_string()).collect(),
|
||||||
|
Err(_) => return vec![],
|
||||||
|
}
|
||||||
|
}
|
@ -90,7 +90,7 @@ pub fn cached_config() -> Result<Arc<SectionConfigData>, Error> {
|
|||||||
|
|
||||||
let stat = match nix::sys::stat::stat(USER_CFG_FILENAME) {
|
let stat = match nix::sys::stat::stat(USER_CFG_FILENAME) {
|
||||||
Ok(stat) => Some(stat),
|
Ok(stat) => Some(stat),
|
||||||
Err(nix::Error::Sys(nix::errno::Errno::ENOENT)) => None,
|
Err(nix::errno::Errno::ENOENT) => None,
|
||||||
Err(err) => bail!("unable to stat '{}' - {}", USER_CFG_FILENAME, err),
|
Err(err) => bail!("unable to stat '{}' - {}", USER_CFG_FILENAME, err),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -41,7 +41,7 @@ pub fn lock_config() -> Result<BackupLockGuard, Error> {
|
|||||||
|
|
||||||
pub fn config() -> Result<(SectionConfigData, [u8; 32]), Error> {
|
pub fn config() -> Result<(SectionConfigData, [u8; 32]), Error> {
|
||||||
let content = proxmox_sys::fs::file_read_optional_string(VERIFICATION_CFG_FILENAME)?;
|
let content = proxmox_sys::fs::file_read_optional_string(VERIFICATION_CFG_FILENAME)?;
|
||||||
let content = content.unwrap_or_else(String::new);
|
let content = content.unwrap_or_default();
|
||||||
|
|
||||||
let digest = openssl::sha::sha256(content.as_bytes());
|
let digest = openssl::sha::sha256(content.as_bytes());
|
||||||
let data = CONFIG.parse(VERIFICATION_CFG_FILENAME, &content)?;
|
let data = CONFIG.parse(VERIFICATION_CFG_FILENAME, &content)?;
|
||||||
|
@ -15,7 +15,7 @@ hex = { version = "0.4.3", features = [ "serde" ] }
|
|||||||
lazy_static = "1.4"
|
lazy_static = "1.4"
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
log = "0.4.17"
|
log = "0.4.17"
|
||||||
nix = "0.19.1"
|
nix = "0.24"
|
||||||
openssl = "0.10"
|
openssl = "0.10"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
@ -32,7 +32,7 @@ proxmox-lang = "1.1"
|
|||||||
proxmox-schema = { version = "1.3.1", features = [ "api-macro" ] }
|
proxmox-schema = { version = "1.3.1", features = [ "api-macro" ] }
|
||||||
proxmox-time = "1"
|
proxmox-time = "1"
|
||||||
proxmox-uuid = "1"
|
proxmox-uuid = "1"
|
||||||
proxmox-sys = "0.2"
|
proxmox-sys = "0.3"
|
||||||
|
|
||||||
pbs-api-types = { path = "../pbs-api-types" }
|
pbs-api-types = { path = "../pbs-api-types" }
|
||||||
pbs-buildcfg = { path = "../pbs-buildcfg" }
|
pbs-buildcfg = { path = "../pbs-buildcfg" }
|
||||||
|
@ -147,7 +147,7 @@ impl BackupGroup {
|
|||||||
/* close else this leaks! */
|
/* close else this leaks! */
|
||||||
nix::unistd::close(rawfd)?;
|
nix::unistd::close(rawfd)?;
|
||||||
}
|
}
|
||||||
Err(nix::Error::Sys(nix::errno::Errno::ENOENT)) => {
|
Err(nix::errno::Errno::ENOENT) => {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
|
@ -153,7 +153,12 @@ impl ChunkStore {
|
|||||||
lockfile_path
|
lockfile_path
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn open<P: Into<PathBuf>>(name: &str, base: P) -> Result<Self, Error> {
|
/// Opens the chunk store with a new process locker.
|
||||||
|
///
|
||||||
|
/// Note that this must be used with care, as it's dangerous to create two instances on the
|
||||||
|
/// same base path, as closing the underlying ProcessLocker drops all locks from this process
|
||||||
|
/// on the lockfile (even if separate FDs)
|
||||||
|
pub(crate) fn open<P: Into<PathBuf>>(name: &str, base: P) -> Result<Self, Error> {
|
||||||
let base: PathBuf = base.into();
|
let base: PathBuf = base.into();
|
||||||
|
|
||||||
if !base.is_absolute() {
|
if !base.is_absolute() {
|
||||||
@ -221,7 +226,7 @@ impl ChunkStore {
|
|||||||
})?;
|
})?;
|
||||||
|
|
||||||
if let Err(err) = res {
|
if let Err(err) = res {
|
||||||
if !assert_exists && err.as_errno() == Some(nix::errno::Errno::ENOENT) {
|
if !assert_exists && err == nix::errno::Errno::ENOENT {
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
}
|
}
|
||||||
bail!("update atime failed for chunk/file {path:?} - {err}");
|
bail!("update atime failed for chunk/file {path:?} - {err}");
|
||||||
@ -304,7 +309,7 @@ impl ChunkStore {
|
|||||||
// start reading:
|
// start reading:
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
Err(ref err) if err.as_errno() == Some(nix::errno::Errno::ENOENT) => {
|
Err(ref err) if err == &nix::errno::Errno::ENOENT => {
|
||||||
// non-existing directories are okay, just keep going:
|
// non-existing directories are okay, just keep going:
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,6 @@ use pbs_api_types::{
|
|||||||
Authid, BackupNamespace, BackupType, ChunkOrder, DataStoreConfig, DatastoreTuning,
|
Authid, BackupNamespace, BackupType, ChunkOrder, DataStoreConfig, DatastoreTuning,
|
||||||
GarbageCollectionStatus, HumanByte, Operation, UPID,
|
GarbageCollectionStatus, HumanByte, Operation, UPID,
|
||||||
};
|
};
|
||||||
use pbs_config::ConfigVersionCache;
|
|
||||||
|
|
||||||
use crate::backup_info::{BackupDir, BackupGroup};
|
use crate::backup_info::{BackupDir, BackupGroup};
|
||||||
use crate::chunk_store::ChunkStore;
|
use crate::chunk_store::ChunkStore;
|
||||||
@ -59,22 +58,20 @@ pub struct DataStoreImpl {
|
|||||||
last_gc_status: Mutex<GarbageCollectionStatus>,
|
last_gc_status: Mutex<GarbageCollectionStatus>,
|
||||||
verify_new: bool,
|
verify_new: bool,
|
||||||
chunk_order: ChunkOrder,
|
chunk_order: ChunkOrder,
|
||||||
last_generation: usize,
|
last_digest: Option<[u8; 32]>,
|
||||||
last_update: i64,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DataStoreImpl {
|
impl DataStoreImpl {
|
||||||
// This one just panics on everything
|
// This one just panics on everything
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
pub unsafe fn new_test() -> Arc<Self> {
|
pub(crate) unsafe fn new_test() -> Arc<Self> {
|
||||||
Arc::new(Self {
|
Arc::new(Self {
|
||||||
chunk_store: Arc::new(unsafe { ChunkStore::panic_store() }),
|
chunk_store: Arc::new(unsafe { ChunkStore::panic_store() }),
|
||||||
gc_mutex: Mutex::new(()),
|
gc_mutex: Mutex::new(()),
|
||||||
last_gc_status: Mutex::new(GarbageCollectionStatus::default()),
|
last_gc_status: Mutex::new(GarbageCollectionStatus::default()),
|
||||||
verify_new: false,
|
verify_new: false,
|
||||||
chunk_order: ChunkOrder::None,
|
chunk_order: ChunkOrder::None,
|
||||||
last_generation: 0,
|
last_digest: None,
|
||||||
last_update: 0,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -114,7 +111,7 @@ impl Drop for DataStore {
|
|||||||
impl DataStore {
|
impl DataStore {
|
||||||
// This one just panics on everything
|
// This one just panics on everything
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
pub unsafe fn new_test() -> Arc<Self> {
|
pub(crate) unsafe fn new_test() -> Arc<Self> {
|
||||||
Arc::new(Self {
|
Arc::new(Self {
|
||||||
inner: unsafe { DataStoreImpl::new_test() },
|
inner: unsafe { DataStoreImpl::new_test() },
|
||||||
operation: None,
|
operation: None,
|
||||||
@ -125,11 +122,9 @@ impl DataStore {
|
|||||||
name: &str,
|
name: &str,
|
||||||
operation: Option<Operation>,
|
operation: Option<Operation>,
|
||||||
) -> Result<Arc<DataStore>, Error> {
|
) -> Result<Arc<DataStore>, Error> {
|
||||||
let version_cache = ConfigVersionCache::new()?;
|
// we could use the ConfigVersionCache's generation for staleness detection, but we load
|
||||||
let generation = version_cache.datastore_generation();
|
// the config anyway -> just use digest, additional benefit: manual changes get detected
|
||||||
let now = proxmox_time::epoch_i64();
|
let (config, digest) = pbs_config::datastore::config()?;
|
||||||
|
|
||||||
let (config, _digest) = pbs_config::datastore::config()?;
|
|
||||||
let config: DataStoreConfig = config.lookup("datastore", name)?;
|
let config: DataStoreConfig = config.lookup("datastore", name)?;
|
||||||
|
|
||||||
if let Some(maintenance_mode) = config.get_maintenance_mode() {
|
if let Some(maintenance_mode) = config.get_maintenance_mode() {
|
||||||
@ -142,23 +137,27 @@ impl DataStore {
|
|||||||
update_active_operations(name, operation, 1)?;
|
update_active_operations(name, operation, 1)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut map = DATASTORE_MAP.lock().unwrap();
|
let mut datastore_cache = DATASTORE_MAP.lock().unwrap();
|
||||||
let entry = map.get(name);
|
let entry = datastore_cache.get(name);
|
||||||
|
|
||||||
if let Some(datastore) = &entry {
|
// reuse chunk store so that we keep using the same process locker instance!
|
||||||
if datastore.last_generation == generation && now < (datastore.last_update + 60) {
|
let chunk_store = if let Some(datastore) = &entry {
|
||||||
|
let last_digest = datastore.last_digest.as_ref();
|
||||||
|
if let Some(true) = last_digest.map(|last_digest| last_digest == &digest) {
|
||||||
return Ok(Arc::new(Self {
|
return Ok(Arc::new(Self {
|
||||||
inner: Arc::clone(datastore),
|
inner: Arc::clone(datastore),
|
||||||
operation,
|
operation,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
Arc::clone(&datastore.chunk_store)
|
||||||
|
} else {
|
||||||
|
Arc::new(ChunkStore::open(name, &config.path)?)
|
||||||
|
};
|
||||||
|
|
||||||
let chunk_store = ChunkStore::open(name, &config.path)?;
|
let datastore = DataStore::with_store_and_config(chunk_store, config, Some(digest))?;
|
||||||
let datastore = DataStore::with_store_and_config(chunk_store, config, generation, now)?;
|
|
||||||
|
|
||||||
let datastore = Arc::new(datastore);
|
let datastore = Arc::new(datastore);
|
||||||
map.insert(name.to_string(), datastore.clone());
|
datastore_cache.insert(name.to_string(), datastore.clone());
|
||||||
|
|
||||||
Ok(Arc::new(Self {
|
Ok(Arc::new(Self {
|
||||||
inner: datastore,
|
inner: datastore,
|
||||||
@ -177,6 +176,9 @@ impl DataStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Open a raw database given a name and a path.
|
/// Open a raw database given a name and a path.
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// See the safety section in `open_from_config`
|
||||||
pub unsafe fn open_path(
|
pub unsafe fn open_path(
|
||||||
name: &str,
|
name: &str,
|
||||||
path: impl AsRef<Path>,
|
path: impl AsRef<Path>,
|
||||||
@ -191,14 +193,26 @@ impl DataStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Open a datastore given a raw configuration.
|
/// Open a datastore given a raw configuration.
|
||||||
pub unsafe fn open_from_config(
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// There's no memory saftey implication, but as this is opening a new ChunkStore it will
|
||||||
|
/// create a new process locker instance, potentially on the same path as existing safely
|
||||||
|
/// created ones. This is dangerous as dropping the reference of this and thus the underlying
|
||||||
|
/// chunkstore's process locker will close all locks from our process on the config.path,
|
||||||
|
/// breaking guarantees we need to uphold for safe long backup + GC interaction on newer/older
|
||||||
|
/// process instances (from package update).
|
||||||
|
unsafe fn open_from_config(
|
||||||
config: DataStoreConfig,
|
config: DataStoreConfig,
|
||||||
operation: Option<Operation>,
|
operation: Option<Operation>,
|
||||||
) -> Result<Arc<Self>, Error> {
|
) -> Result<Arc<Self>, Error> {
|
||||||
let name = config.name.clone();
|
let name = config.name.clone();
|
||||||
|
|
||||||
let chunk_store = ChunkStore::open(&name, &config.path)?;
|
let chunk_store = ChunkStore::open(&name, &config.path)?;
|
||||||
let inner = Arc::new(Self::with_store_and_config(chunk_store, config, 0, 0)?);
|
let inner = Arc::new(Self::with_store_and_config(
|
||||||
|
Arc::new(chunk_store),
|
||||||
|
config,
|
||||||
|
None,
|
||||||
|
)?);
|
||||||
|
|
||||||
if let Some(operation) = operation {
|
if let Some(operation) = operation {
|
||||||
update_active_operations(&name, operation, 1)?;
|
update_active_operations(&name, operation, 1)?;
|
||||||
@ -208,10 +222,9 @@ impl DataStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn with_store_and_config(
|
fn with_store_and_config(
|
||||||
chunk_store: ChunkStore,
|
chunk_store: Arc<ChunkStore>,
|
||||||
config: DataStoreConfig,
|
config: DataStoreConfig,
|
||||||
last_generation: usize,
|
last_digest: Option<[u8; 32]>,
|
||||||
last_update: i64,
|
|
||||||
) -> Result<DataStoreImpl, Error> {
|
) -> Result<DataStoreImpl, Error> {
|
||||||
let mut gc_status_path = chunk_store.base_path();
|
let mut gc_status_path = chunk_store.base_path();
|
||||||
gc_status_path.push(".gc-status");
|
gc_status_path.push(".gc-status");
|
||||||
@ -235,13 +248,12 @@ impl DataStore {
|
|||||||
let chunk_order = tuning.chunk_order.unwrap_or(ChunkOrder::Inode);
|
let chunk_order = tuning.chunk_order.unwrap_or(ChunkOrder::Inode);
|
||||||
|
|
||||||
Ok(DataStoreImpl {
|
Ok(DataStoreImpl {
|
||||||
chunk_store: Arc::new(chunk_store),
|
chunk_store,
|
||||||
gc_mutex: Mutex::new(()),
|
gc_mutex: Mutex::new(()),
|
||||||
last_gc_status: Mutex::new(gc_status),
|
last_gc_status: Mutex::new(gc_status),
|
||||||
verify_new: config.verify_new.unwrap_or(false),
|
verify_new: config.verify_new.unwrap_or(false),
|
||||||
chunk_order,
|
chunk_order,
|
||||||
last_generation,
|
last_digest,
|
||||||
last_update,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -433,7 +445,7 @@ impl DataStore {
|
|||||||
ty_dir.push(ty.to_string());
|
ty_dir.push(ty.to_string());
|
||||||
// best effort only, but we probably should log the error
|
// best effort only, but we probably should log the error
|
||||||
if let Err(err) = unlinkat(Some(base_fd), &ty_dir, UnlinkatFlags::RemoveDir) {
|
if let Err(err) = unlinkat(Some(base_fd), &ty_dir, UnlinkatFlags::RemoveDir) {
|
||||||
if err.as_errno() != Some(nix::errno::Errno::ENOENT) {
|
if err != nix::errno::Errno::ENOENT {
|
||||||
log::error!("failed to remove backup type {ty} in {ns} - {err}");
|
log::error!("failed to remove backup type {ty} in {ns} - {err}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -470,7 +482,7 @@ impl DataStore {
|
|||||||
.recursive_iter_backup_ns(ns.to_owned())?
|
.recursive_iter_backup_ns(ns.to_owned())?
|
||||||
.collect::<Result<Vec<BackupNamespace>, Error>>()?;
|
.collect::<Result<Vec<BackupNamespace>, Error>>()?;
|
||||||
|
|
||||||
children.sort_by(|a, b| b.depth().cmp(&a.depth()));
|
children.sort_by_key(|b| std::cmp::Reverse(b.depth()));
|
||||||
|
|
||||||
let base_file = std::fs::File::open(self.base_path())?;
|
let base_file = std::fs::File::open(self.base_path())?;
|
||||||
let base_fd = base_file.as_raw_fd();
|
let base_fd = base_file.as_raw_fd();
|
||||||
@ -483,10 +495,10 @@ impl DataStore {
|
|||||||
if !ns.is_root() {
|
if !ns.is_root() {
|
||||||
match unlinkat(Some(base_fd), &ns.path(), UnlinkatFlags::RemoveDir) {
|
match unlinkat(Some(base_fd), &ns.path(), UnlinkatFlags::RemoveDir) {
|
||||||
Ok(()) => log::debug!("removed namespace {ns}"),
|
Ok(()) => log::debug!("removed namespace {ns}"),
|
||||||
Err(nix::Error::Sys(nix::errno::Errno::ENOENT)) => {
|
Err(nix::errno::Errno::ENOENT) => {
|
||||||
log::debug!("namespace {ns} already removed")
|
log::debug!("namespace {ns} already removed")
|
||||||
}
|
}
|
||||||
Err(nix::Error::Sys(nix::errno::Errno::ENOTEMPTY)) if !delete_groups => {
|
Err(nix::errno::Errno::ENOTEMPTY) if !delete_groups => {
|
||||||
removed_all_requested = false;
|
removed_all_requested = false;
|
||||||
log::debug!("skip removal of non-empty namespace {ns}")
|
log::debug!("skip removal of non-empty namespace {ns}")
|
||||||
}
|
}
|
||||||
@ -982,8 +994,10 @@ impl DataStore {
|
|||||||
.oldest_writer()
|
.oldest_writer()
|
||||||
.unwrap_or(phase1_start_time);
|
.unwrap_or(phase1_start_time);
|
||||||
|
|
||||||
let mut gc_status = GarbageCollectionStatus::default();
|
let mut gc_status = GarbageCollectionStatus {
|
||||||
gc_status.upid = Some(upid.to_string());
|
upid: Some(upid.to_string()),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
task_log!(worker, "Start GC phase1 (mark used chunks)");
|
task_log!(worker, "Start GC phase1 (mark used chunks)");
|
||||||
|
|
||||||
@ -1140,8 +1154,8 @@ impl DataStore {
|
|||||||
self.inner.verify_new
|
self.inner.verify_new
|
||||||
}
|
}
|
||||||
|
|
||||||
/// returns a list of chunks sorted by their inode number on disk
|
/// returns a list of chunks sorted by their inode number on disk chunks that couldn't get
|
||||||
/// chunks that could not be stat'ed are at the end of the list
|
/// stat'ed are placed at the end of the list
|
||||||
pub fn get_chunks_in_order<F, A>(
|
pub fn get_chunks_in_order<F, A>(
|
||||||
&self,
|
&self,
|
||||||
index: &Box<dyn IndexFile + Send>,
|
index: &Box<dyn IndexFile + Send>,
|
||||||
|
@ -373,14 +373,14 @@ impl DynamicIndexWriter {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let offset_le: &[u8; 8] = unsafe { &std::mem::transmute::<u64, [u8; 8]>(offset.to_le()) };
|
let offset_le: [u8; 8] = offset.to_le().to_ne_bytes();
|
||||||
|
|
||||||
if let Some(ref mut csum) = self.csum {
|
if let Some(ref mut csum) = self.csum {
|
||||||
csum.update(offset_le);
|
csum.update(&offset_le);
|
||||||
csum.update(digest);
|
csum.update(digest);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.writer.write_all(offset_le)?;
|
self.writer.write_all(&offset_le)?;
|
||||||
self.writer.write_all(digest)?;
|
self.writer.write_all(digest)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -196,7 +196,7 @@ impl Iterator for ListNamespaces {
|
|||||||
|
|
||||||
let ns_dirfd = match proxmox_sys::fs::read_subdir(libc::AT_FDCWD, &base_path) {
|
let ns_dirfd = match proxmox_sys::fs::read_subdir(libc::AT_FDCWD, &base_path) {
|
||||||
Ok(dirfd) => dirfd,
|
Ok(dirfd) => dirfd,
|
||||||
Err(nix::Error::Sys(nix::errno::Errno::ENOENT)) => return None,
|
Err(nix::errno::Errno::ENOENT) => return None,
|
||||||
Err(err) => return Some(Err(err.into())),
|
Err(err) => return Some(Err(err.into())),
|
||||||
};
|
};
|
||||||
// found a ns directory, descend into it to scan all it's namespaces
|
// found a ns directory, descend into it to scan all it's namespaces
|
||||||
|
@ -3,7 +3,7 @@ use std::path::PathBuf;
|
|||||||
|
|
||||||
use anyhow::Error;
|
use anyhow::Error;
|
||||||
|
|
||||||
use pbs_api_types::PruneOptions;
|
use pbs_api_types::KeepOptions;
|
||||||
|
|
||||||
use super::BackupInfo;
|
use super::BackupInfo;
|
||||||
|
|
||||||
@ -103,81 +103,10 @@ fn remove_incomplete_snapshots(mark: &mut HashMap<PathBuf, PruneMark>, list: &[B
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn keeps_something(options: &PruneOptions) -> bool {
|
/// This filters incomplete and kept backups.
|
||||||
let mut keep_something = false;
|
|
||||||
if let Some(count) = options.keep_last {
|
|
||||||
if count > 0 {
|
|
||||||
keep_something = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Some(count) = options.keep_hourly {
|
|
||||||
if count > 0 {
|
|
||||||
keep_something = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Some(count) = options.keep_daily {
|
|
||||||
if count > 0 {
|
|
||||||
keep_something = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Some(count) = options.keep_weekly {
|
|
||||||
if count > 0 {
|
|
||||||
keep_something = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Some(count) = options.keep_monthly {
|
|
||||||
if count > 0 {
|
|
||||||
keep_something = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Some(count) = options.keep_yearly {
|
|
||||||
if count > 0 {
|
|
||||||
keep_something = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
keep_something
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn cli_options_string(options: &PruneOptions) -> String {
|
|
||||||
let mut opts = Vec::new();
|
|
||||||
|
|
||||||
if let Some(count) = options.keep_last {
|
|
||||||
if count > 0 {
|
|
||||||
opts.push(format!("--keep-last {}", count));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Some(count) = options.keep_hourly {
|
|
||||||
if count > 0 {
|
|
||||||
opts.push(format!("--keep-hourly {}", count));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Some(count) = options.keep_daily {
|
|
||||||
if count > 0 {
|
|
||||||
opts.push(format!("--keep-daily {}", count));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Some(count) = options.keep_weekly {
|
|
||||||
if count > 0 {
|
|
||||||
opts.push(format!("--keep-weekly {}", count));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Some(count) = options.keep_monthly {
|
|
||||||
if count > 0 {
|
|
||||||
opts.push(format!("--keep-monthly {}", count));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Some(count) = options.keep_yearly {
|
|
||||||
if count > 0 {
|
|
||||||
opts.push(format!("--keep-yearly {}", count));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
opts.join(" ")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn compute_prune_info(
|
pub fn compute_prune_info(
|
||||||
mut list: Vec<BackupInfo>,
|
mut list: Vec<BackupInfo>,
|
||||||
options: &PruneOptions,
|
options: &KeepOptions,
|
||||||
) -> Result<Vec<(BackupInfo, PruneMark)>, Error> {
|
) -> Result<Vec<(BackupInfo, PruneMark)>, Error> {
|
||||||
let mut mark = HashMap::new();
|
let mut mark = HashMap::new();
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ use nix::dir::Dir;
|
|||||||
|
|
||||||
use proxmox_sys::fs::lock_dir_noblock_shared;
|
use proxmox_sys::fs::lock_dir_noblock_shared;
|
||||||
|
|
||||||
use pbs_api_types::{BackupNamespace, DatastoreWithNamespace, Operation};
|
use pbs_api_types::{print_store_and_ns, BackupNamespace, Operation};
|
||||||
|
|
||||||
use crate::backup_info::BackupDir;
|
use crate::backup_info::BackupDir;
|
||||||
use crate::dynamic_index::DynamicIndexReader;
|
use crate::dynamic_index::DynamicIndexReader;
|
||||||
@ -39,10 +39,6 @@ impl SnapshotReader {
|
|||||||
|
|
||||||
pub(crate) fn new_do(snapshot: BackupDir) -> Result<Self, Error> {
|
pub(crate) fn new_do(snapshot: BackupDir) -> Result<Self, Error> {
|
||||||
let datastore = snapshot.datastore();
|
let datastore = snapshot.datastore();
|
||||||
let store_with_ns = DatastoreWithNamespace {
|
|
||||||
store: datastore.name().to_owned(),
|
|
||||||
ns: snapshot.backup_ns().clone(),
|
|
||||||
};
|
|
||||||
let snapshot_path = snapshot.full_path();
|
let snapshot_path = snapshot.full_path();
|
||||||
|
|
||||||
let locked_dir =
|
let locked_dir =
|
||||||
@ -54,7 +50,7 @@ impl SnapshotReader {
|
|||||||
Err(err) => {
|
Err(err) => {
|
||||||
bail!(
|
bail!(
|
||||||
"manifest load error on {}, snapshot '{}' - {}",
|
"manifest load error on {}, snapshot '{}' - {}",
|
||||||
store_with_ns,
|
print_store_and_ns(datastore.name(), snapshot.backup_ns()),
|
||||||
snapshot.dir(),
|
snapshot.dir(),
|
||||||
err
|
err
|
||||||
);
|
);
|
||||||
@ -64,8 +60,7 @@ impl SnapshotReader {
|
|||||||
let mut client_log_path = snapshot_path;
|
let mut client_log_path = snapshot_path;
|
||||||
client_log_path.push(CLIENT_LOG_BLOB_NAME);
|
client_log_path.push(CLIENT_LOG_BLOB_NAME);
|
||||||
|
|
||||||
let mut file_list = Vec::new();
|
let mut file_list = vec![MANIFEST_BLOB_NAME.to_string()];
|
||||||
file_list.push(MANIFEST_BLOB_NAME.to_string());
|
|
||||||
for item in manifest.files() {
|
for item in manifest.files() {
|
||||||
file_list.push(item.filename.clone());
|
file_list.push(item.filename.clone());
|
||||||
}
|
}
|
||||||
|
@ -10,10 +10,10 @@ anyhow = "1.0"
|
|||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
lazy_static = "1.4"
|
lazy_static = "1.4"
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
nix = "0.19.1"
|
nix = "0.24"
|
||||||
regex = "1.5"
|
regex = "1.5"
|
||||||
tokio = { version = "1.6", features = [] }
|
tokio = { version = "1.6", features = [] }
|
||||||
|
|
||||||
proxmox-time = "1"
|
proxmox-time = "1"
|
||||||
proxmox-fuse = "0.1.1"
|
proxmox-fuse = "0.1.1"
|
||||||
proxmox-sys = "0.2"
|
proxmox-sys = "0.3"
|
@ -330,7 +330,7 @@ fn unmap_from_backing(backing_file: &Path, loopdev: Option<&str>) -> Result<(),
|
|||||||
// send SIGINT to trigger cleanup and exit in target process
|
// send SIGINT to trigger cleanup and exit in target process
|
||||||
match signal::kill(pid, Signal::SIGINT) {
|
match signal::kill(pid, Signal::SIGINT) {
|
||||||
Ok(()) => {}
|
Ok(()) => {}
|
||||||
Err(nix::Error::Sys(nix::errno::Errno::ESRCH)) => {
|
Err(nix::errno::Errno::ESRCH) => {
|
||||||
emerg_cleanup(loopdev, backing_file.to_owned());
|
emerg_cleanup(loopdev, backing_file.to_owned());
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
@ -348,7 +348,7 @@ fn unmap_from_backing(backing_file: &Path, loopdev: Option<&str>) -> Result<(),
|
|||||||
}
|
}
|
||||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||||
}
|
}
|
||||||
Err(nix::Error::Sys(nix::errno::Errno::ESRCH)) => {
|
Err(nix::errno::Errno::ESRCH) => {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
Err(e) => return Err(e.into()),
|
Err(e) => return Err(e.into()),
|
||||||
|
@ -12,7 +12,7 @@ anyhow = "1.0"
|
|||||||
thiserror = "1.0"
|
thiserror = "1.0"
|
||||||
endian_trait = { version = "0.6", features = ["arrays"] }
|
endian_trait = { version = "0.6", features = ["arrays"] }
|
||||||
hex = "0.4.3"
|
hex = "0.4.3"
|
||||||
nix = "0.19.1"
|
nix = "0.24"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
bitflags = "1.2.1"
|
bitflags = "1.2.1"
|
||||||
@ -28,7 +28,7 @@ proxmox-uuid = "1"
|
|||||||
|
|
||||||
# router::cli is only used by binaries, so maybe we should split them out
|
# router::cli is only used by binaries, so maybe we should split them out
|
||||||
proxmox-router = "1.2"
|
proxmox-router = "1.2"
|
||||||
proxmox-sys = "0.2"
|
proxmox-sys = "0.3"
|
||||||
|
|
||||||
pbs-api-types = { path = "../pbs-api-types" }
|
pbs-api-types = { path = "../pbs-api-types" }
|
||||||
pbs-config = { path = "../pbs-config" }
|
pbs-config = { path = "../pbs-config" }
|
||||||
|
@ -442,7 +442,7 @@ impl<'a, F: AsRawFd> SgRaw<'a, F> {
|
|||||||
SCSI_PT_DO_TIMEOUT => return Err(format_err!("do_scsi_pt failed - timeout").into()),
|
SCSI_PT_DO_TIMEOUT => return Err(format_err!("do_scsi_pt failed - timeout").into()),
|
||||||
code if code < 0 => {
|
code if code < 0 => {
|
||||||
let errno = unsafe { get_scsi_pt_os_err(ptvp.as_ptr()) };
|
let errno = unsafe { get_scsi_pt_os_err(ptvp.as_ptr()) };
|
||||||
let err = nix::Error::from_errno(nix::errno::Errno::from_i32(errno));
|
let err = nix::errno::Errno::from_i32(errno);
|
||||||
return Err(format_err!("do_scsi_pt failed with err {}", err).into());
|
return Err(format_err!("do_scsi_pt failed with err {}", err).into());
|
||||||
}
|
}
|
||||||
unknown => {
|
unknown => {
|
||||||
@ -524,7 +524,7 @@ impl<'a, F: AsRawFd> SgRaw<'a, F> {
|
|||||||
}
|
}
|
||||||
SCSI_PT_RESULT_OS_ERR => {
|
SCSI_PT_RESULT_OS_ERR => {
|
||||||
let errno = unsafe { get_scsi_pt_os_err(ptvp.as_ptr()) };
|
let errno = unsafe { get_scsi_pt_os_err(ptvp.as_ptr()) };
|
||||||
let err = nix::Error::from_errno(nix::errno::Errno::from_i32(errno));
|
let err = nix::errno::Errno::from_i32(errno);
|
||||||
return Err(format_err!("scsi command failed with err {}", err).into());
|
return Err(format_err!("scsi command failed with err {}", err).into());
|
||||||
}
|
}
|
||||||
unknown => {
|
unknown => {
|
||||||
|
@ -19,7 +19,7 @@ hex = "0.4.3"
|
|||||||
lazy_static = "1.4"
|
lazy_static = "1.4"
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
log = "0.4.17"
|
log = "0.4.17"
|
||||||
nix = "0.19.1"
|
nix = "0.24"
|
||||||
nom = "5.1"
|
nom = "5.1"
|
||||||
openssl = "0.10"
|
openssl = "0.10"
|
||||||
percent-encoding = "2.1"
|
percent-encoding = "2.1"
|
||||||
@ -38,7 +38,7 @@ proxmox-borrow = "1"
|
|||||||
proxmox-io = { version = "1", features = [ "tokio" ] }
|
proxmox-io = { version = "1", features = [ "tokio" ] }
|
||||||
proxmox-lang = { version = "1.1" }
|
proxmox-lang = { version = "1.1" }
|
||||||
proxmox-time = { version = "1" }
|
proxmox-time = { version = "1" }
|
||||||
proxmox-sys = "0.2"
|
proxmox-sys = "0.3"
|
||||||
|
|
||||||
pbs-buildcfg = { path = "../pbs-buildcfg" }
|
pbs-buildcfg = { path = "../pbs-buildcfg" }
|
||||||
pbs-api-types = { path = "../pbs-api-types" }
|
pbs-api-types = { path = "../pbs-api-types" }
|
||||||
|
@ -19,7 +19,7 @@ pub fn render_backup_file_list<S: Borrow<str>>(files: &[S]) -> String {
|
|||||||
.map(|v| strip_server_file_extension(v.borrow()))
|
.map(|v| strip_server_file_extension(v.borrow()))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
files.sort();
|
files.sort_unstable();
|
||||||
|
|
||||||
files.join(" ")
|
files.join(" ")
|
||||||
}
|
}
|
||||||
|
@ -5,4 +5,5 @@ authors = ["Proxmox Support Team <support@proxmox.com>"]
|
|||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
nix = "0.19.1"
|
anyhow = "1"
|
||||||
|
nix = "0.24"
|
||||||
|
@ -1,12 +1,29 @@
|
|||||||
|
use anyhow::{format_err, Error};
|
||||||
|
|
||||||
use std::fmt::Write;
|
use std::fmt::Write;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::net::ToSocketAddrs;
|
use std::net::ToSocketAddrs;
|
||||||
|
use std::os::unix::prelude::OsStrExt;
|
||||||
|
|
||||||
use nix::sys::utsname::uname;
|
use nix::sys::utsname::uname;
|
||||||
|
|
||||||
|
fn nodename() -> Result<String, Error> {
|
||||||
|
let uname = uname().map_err(|err| format_err!("uname() failed - {err}"))?; // save on stack to avoid to_owned() allocation below
|
||||||
|
std::str::from_utf8(uname.nodename().as_bytes())?
|
||||||
|
.split('.')
|
||||||
|
.next()
|
||||||
|
.ok_or_else(|| format_err!("Failed to split FQDN to get hostname"))
|
||||||
|
.map(|s| s.to_owned())
|
||||||
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let uname = uname(); // save on stack to avoid to_owned() allocation below
|
let nodename = match nodename() {
|
||||||
let nodename = uname.nodename().split('.').next().unwrap();
|
Ok(value) => value,
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!("Failed to retrieve hostname: {err}");
|
||||||
|
"INVALID".to_string()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let addr = format!("{}:8007", nodename);
|
let addr = format!("{}:8007", nodename);
|
||||||
|
|
||||||
|
@ -9,13 +9,13 @@ anyhow = "1.0"
|
|||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
hyper = { version = "0.14", features = [ "full" ] }
|
hyper = { version = "0.14", features = [ "full" ] }
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
nix = "0.19.1"
|
nix = "0.24"
|
||||||
openssl = "0.10"
|
openssl = "0.10"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
tokio = { version = "1.6", features = [ "rt", "rt-multi-thread" ] }
|
tokio = { version = "1.6", features = [ "rt", "rt-multi-thread" ] }
|
||||||
tokio-stream = "0.1.0"
|
tokio-stream = "0.1.0"
|
||||||
tokio-util = { version = "0.6", features = [ "codec", "io" ] }
|
tokio-util = { version = "0.7", features = [ "codec", "io" ] }
|
||||||
xdg = "2.2"
|
xdg = "2.2"
|
||||||
zstd = { version = "0.6", features = [ "bindgen" ] }
|
zstd = { version = "0.6", features = [ "bindgen" ] }
|
||||||
|
|
||||||
@ -27,7 +27,7 @@ proxmox-io = "1.0.1"
|
|||||||
proxmox-router = { version = "1.2", features = [ "cli" ] }
|
proxmox-router = { version = "1.2", features = [ "cli" ] }
|
||||||
proxmox-schema = { version = "1.3.1", features = [ "api-macro" ] }
|
proxmox-schema = { version = "1.3.1", features = [ "api-macro" ] }
|
||||||
proxmox-time = "1"
|
proxmox-time = "1"
|
||||||
proxmox-sys = { version = "0.2.1", features = [ "sortable-macro" ] }
|
proxmox-sys = { version = "0.3", features = [ "sortable-macro" ] }
|
||||||
|
|
||||||
pbs-api-types = { path = "../pbs-api-types" }
|
pbs-api-types = { path = "../pbs-api-types" }
|
||||||
pbs-buildcfg = { path = "../pbs-buildcfg" }
|
pbs-buildcfg = { path = "../pbs-buildcfg" }
|
||||||
|
@ -163,7 +163,7 @@ async fn catalog_shell(param: Value) -> Result<(), Error> {
|
|||||||
let path = required_string_param(¶m, "snapshot")?;
|
let path = required_string_param(¶m, "snapshot")?;
|
||||||
let archive_name = required_string_param(¶m, "archive-name")?;
|
let archive_name = required_string_param(¶m, "archive-name")?;
|
||||||
|
|
||||||
let backup_dir = dir_or_last_from_group(&client, &repo, &backup_ns, &path).await?;
|
let backup_dir = dir_or_last_from_group(&client, &repo, &backup_ns, path).await?;
|
||||||
|
|
||||||
let crypto = crypto_parameters(¶m)?;
|
let crypto = crypto_parameters(¶m)?;
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@ use pxar::accessor::{MaybeReady, ReadAt, ReadAtOperation};
|
|||||||
|
|
||||||
use pbs_api_types::{
|
use pbs_api_types::{
|
||||||
Authid, BackupDir, BackupGroup, BackupNamespace, BackupPart, BackupType, CryptMode,
|
Authid, BackupDir, BackupGroup, BackupNamespace, BackupPart, BackupType, CryptMode,
|
||||||
Fingerprint, GroupListItem, HumanByte, PruneListItem, PruneOptions, RateLimitConfig,
|
Fingerprint, GroupListItem, HumanByte, PruneJobOptions, PruneListItem, RateLimitConfig,
|
||||||
SnapshotListItem, StorageStatus, BACKUP_ID_SCHEMA, BACKUP_NAMESPACE_SCHEMA, BACKUP_TIME_SCHEMA,
|
SnapshotListItem, StorageStatus, BACKUP_ID_SCHEMA, BACKUP_NAMESPACE_SCHEMA, BACKUP_TIME_SCHEMA,
|
||||||
BACKUP_TYPE_SCHEMA, TRAFFIC_CONTROL_BURST_SCHEMA, TRAFFIC_CONTROL_RATE_SCHEMA,
|
BACKUP_TYPE_SCHEMA, TRAFFIC_CONTROL_BURST_SCHEMA, TRAFFIC_CONTROL_RATE_SCHEMA,
|
||||||
};
|
};
|
||||||
@ -176,7 +176,7 @@ pub async fn dir_or_last_from_group(
|
|||||||
match path.parse::<BackupPart>()? {
|
match path.parse::<BackupPart>()? {
|
||||||
BackupPart::Dir(dir) => Ok(dir),
|
BackupPart::Dir(dir) => Ok(dir),
|
||||||
BackupPart::Group(group) => {
|
BackupPart::Group(group) => {
|
||||||
api_datastore_latest_snapshot(&client, repo.store(), ns, group).await
|
api_datastore_latest_snapshot(client, repo.store(), ns, group).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1245,7 +1245,7 @@ async fn restore(param: Value) -> Result<Value, Error> {
|
|||||||
let ns = optional_ns_param(¶m)?;
|
let ns = optional_ns_param(¶m)?;
|
||||||
let path = json::required_string_param(¶m, "snapshot")?;
|
let path = json::required_string_param(¶m, "snapshot")?;
|
||||||
|
|
||||||
let backup_dir = dir_or_last_from_group(&client, &repo, &ns, &path).await?;
|
let backup_dir = dir_or_last_from_group(&client, &repo, &ns, path).await?;
|
||||||
|
|
||||||
let target = json::required_string_param(¶m, "target")?;
|
let target = json::required_string_param(¶m, "target")?;
|
||||||
let target = if target == "-" { None } else { Some(target) };
|
let target = if target == "-" { None } else { Some(target) };
|
||||||
@ -1417,12 +1417,8 @@ async fn restore(param: Value) -> Result<Value, Error> {
|
|||||||
type: String,
|
type: String,
|
||||||
description: "Backup group",
|
description: "Backup group",
|
||||||
},
|
},
|
||||||
ns: {
|
|
||||||
type: BackupNamespace,
|
|
||||||
optional: true,
|
|
||||||
},
|
|
||||||
"prune-options": {
|
"prune-options": {
|
||||||
type: PruneOptions,
|
type: PruneJobOptions,
|
||||||
flatten: true,
|
flatten: true,
|
||||||
},
|
},
|
||||||
"output-format": {
|
"output-format": {
|
||||||
@ -1446,12 +1442,11 @@ async fn restore(param: Value) -> Result<Value, Error> {
|
|||||||
async fn prune(
|
async fn prune(
|
||||||
dry_run: Option<bool>,
|
dry_run: Option<bool>,
|
||||||
group: String,
|
group: String,
|
||||||
prune_options: PruneOptions,
|
prune_options: PruneJobOptions,
|
||||||
quiet: bool,
|
quiet: bool,
|
||||||
mut param: Value,
|
mut param: Value,
|
||||||
) -> Result<Value, Error> {
|
) -> Result<Value, Error> {
|
||||||
let repo = extract_repository_from_value(¶m)?;
|
let repo = extract_repository_from_value(¶m)?;
|
||||||
let ns = optional_ns_param(¶m)?;
|
|
||||||
|
|
||||||
let client = connect(&repo)?;
|
let client = connect(&repo)?;
|
||||||
|
|
||||||
@ -1466,9 +1461,6 @@ async fn prune(
|
|||||||
api_param["dry-run"] = dry_run.into();
|
api_param["dry-run"] = dry_run.into();
|
||||||
}
|
}
|
||||||
merge_group_into(api_param.as_object_mut().unwrap(), group);
|
merge_group_into(api_param.as_object_mut().unwrap(), group);
|
||||||
if !ns.is_root() {
|
|
||||||
api_param["ns"] = serde_json::to_value(ns)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut result = client.post(&path, Some(api_param)).await?;
|
let mut result = client.post(&path, Some(api_param)).await?;
|
||||||
|
|
||||||
|
@ -205,7 +205,7 @@ async fn mount_do(param: Value, pipe: Option<Fd>) -> Result<Value, Error> {
|
|||||||
|
|
||||||
let backup_ns = optional_ns_param(¶m)?;
|
let backup_ns = optional_ns_param(¶m)?;
|
||||||
let path = required_string_param(¶m, "snapshot")?;
|
let path = required_string_param(¶m, "snapshot")?;
|
||||||
let backup_dir = dir_or_last_from_group(&client, &repo, &backup_ns, &path).await?;
|
let backup_dir = dir_or_last_from_group(&client, &repo, &backup_ns, path).await?;
|
||||||
|
|
||||||
let keyfile = param["keyfile"].as_str().map(PathBuf::from);
|
let keyfile = param["keyfile"].as_str().map(PathBuf::from);
|
||||||
let crypt_config = match keyfile {
|
let crypt_config = match keyfile {
|
||||||
|
@ -9,7 +9,7 @@ anyhow = "1.0"
|
|||||||
base64 = "0.13"
|
base64 = "0.13"
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
nix = "0.19.1"
|
nix = "0.24"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
tokio = { version = "1.6", features = [ "io-std", "rt", "rt-multi-thread", "time" ] }
|
tokio = { version = "1.6", features = [ "io-std", "rt", "rt-multi-thread", "time" ] }
|
||||||
@ -23,7 +23,7 @@ proxmox-router = { version = "1.2", features = [ "cli" ] }
|
|||||||
proxmox-schema = { version = "1.3.1", features = [ "api-macro" ] }
|
proxmox-schema = { version = "1.3.1", features = [ "api-macro" ] }
|
||||||
proxmox-time = "1"
|
proxmox-time = "1"
|
||||||
proxmox-uuid = "1"
|
proxmox-uuid = "1"
|
||||||
proxmox-sys = "0.2"
|
proxmox-sys = "0.3"
|
||||||
|
|
||||||
pbs-api-types = { path = "../pbs-api-types" }
|
pbs-api-types = { path = "../pbs-api-types" }
|
||||||
pbs-buildcfg = { path = "../pbs-buildcfg" }
|
pbs-buildcfg = { path = "../pbs-buildcfg" }
|
||||||
|
@ -204,7 +204,6 @@ pub fn complete_block_driver_ids<S: BuildHasher>(
|
|||||||
ALL_DRIVERS
|
ALL_DRIVERS
|
||||||
.iter()
|
.iter()
|
||||||
.map(BlockDriverType::resolve)
|
.map(BlockDriverType::resolve)
|
||||||
.map(|d| d.list())
|
.flat_map(|d| d.list())
|
||||||
.flatten()
|
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@ hyper = { version = "0.14.5", features = [ "full" ] }
|
|||||||
lazy_static = "1.4"
|
lazy_static = "1.4"
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
log = "0.4.17"
|
log = "0.4.17"
|
||||||
nix = "0.19.1"
|
nix = "0.24"
|
||||||
once_cell = "1.3.1"
|
once_cell = "1.3.1"
|
||||||
percent-encoding = "2.1"
|
percent-encoding = "2.1"
|
||||||
regex = "1.5"
|
regex = "1.5"
|
||||||
@ -40,4 +40,4 @@ proxmox-http = { version = "0.6", features = [ "client" ] }
|
|||||||
proxmox-router = "1.2"
|
proxmox-router = "1.2"
|
||||||
proxmox-schema = { version = "1.3.1", features = [ "api-macro", "upid-api-impl" ] }
|
proxmox-schema = { version = "1.3.1", features = [ "api-macro", "upid-api-impl" ] }
|
||||||
proxmox-time = "1"
|
proxmox-time = "1"
|
||||||
proxmox-sys = "0.2"
|
proxmox-sys = { version = "0.3", features = [ "logrotate" ] }
|
||||||
|
@ -262,10 +262,8 @@ pub fn rotate_task_log_archive(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else if let Err(err) = std::fs::remove_file(&file_name) {
|
||||||
if let Err(err) = std::fs::remove_file(&file_name) {
|
log::error!("could not remove {:?}: {}", file_name, err);
|
||||||
log::error!("could not remove {:?}: {}", file_name, err);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -966,7 +964,7 @@ impl WorkerTask {
|
|||||||
|
|
||||||
/// Set progress indicator
|
/// Set progress indicator
|
||||||
pub fn progress(&self, progress: f64) {
|
pub fn progress(&self, progress: f64) {
|
||||||
if progress >= 0.0 && progress <= 1.0 {
|
if (0.0..=1.0).contains(&progress) {
|
||||||
let mut data = self.data.lock().unwrap();
|
let mut data = self.data.lock().unwrap();
|
||||||
data.progress = progress;
|
data.progress = progress;
|
||||||
} else {
|
} else {
|
||||||
|
@ -15,13 +15,13 @@ hyper = { version = "0.14", features = [ "full" ] }
|
|||||||
lazy_static = "1.4"
|
lazy_static = "1.4"
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
log = "0.4.17"
|
log = "0.4.17"
|
||||||
nix = "0.19.1"
|
nix = "0.24"
|
||||||
regex = "1.5"
|
regex = "1.5"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
tokio = { version = "1.6", features = ["parking_lot", "sync"] }
|
tokio = { version = "1.6", features = ["parking_lot", "sync"] }
|
||||||
tokio-stream = "0.1.0"
|
tokio-stream = "0.1.0"
|
||||||
tokio-util = { version = "0.6", features = [ "codec", "io" ] }
|
tokio-util = { version = "0.7", features = [ "codec", "io" ] }
|
||||||
|
|
||||||
pathpatterns = "0.1.2"
|
pathpatterns = "0.1.2"
|
||||||
pxar = { version = "0.10.1", features = [ "tokio-io" ] }
|
pxar = { version = "0.10.1", features = [ "tokio-io" ] }
|
||||||
@ -31,7 +31,7 @@ proxmox-compression = "0.1.1"
|
|||||||
proxmox-router = { version = "1.2", features = [ "cli" ] }
|
proxmox-router = { version = "1.2", features = [ "cli" ] }
|
||||||
proxmox-schema = { version = "1.3.1", features = [ "api-macro" ] }
|
proxmox-schema = { version = "1.3.1", features = [ "api-macro" ] }
|
||||||
proxmox-time = "1"
|
proxmox-time = "1"
|
||||||
proxmox-sys = { version = "0.2", features = [ "sortable-macro" ] }
|
proxmox-sys = { version = "0.3", features = [ "sortable-macro" ] }
|
||||||
|
|
||||||
pbs-api-types = { path = "../pbs-api-types" }
|
pbs-api-types = { path = "../pbs-api-types" }
|
||||||
pbs-tools = { path = "../pbs-tools" }
|
pbs-tools = { path = "../pbs-tools" }
|
||||||
|
@ -171,7 +171,7 @@ fn get_vsock_fd() -> Result<RawFd, Error> {
|
|||||||
None,
|
None,
|
||||||
)?;
|
)?;
|
||||||
let sock_addr = VsockAddr::new(libc::VMADDR_CID_ANY, DEFAULT_VSOCK_PORT as u32);
|
let sock_addr = VsockAddr::new(libc::VMADDR_CID_ANY, DEFAULT_VSOCK_PORT as u32);
|
||||||
bind(sock_fd, &SockAddr::Vsock(sock_addr))?;
|
bind(sock_fd, &sock_addr)?;
|
||||||
listen(sock_fd, MAX_PENDING)?;
|
listen(sock_fd, MAX_PENDING)?;
|
||||||
Ok(sock_fd)
|
Ok(sock_fd)
|
||||||
}
|
}
|
||||||
|
@ -107,14 +107,14 @@ impl Bucket {
|
|||||||
Bucket::RawFs(_) => ty == "raw",
|
Bucket::RawFs(_) => ty == "raw",
|
||||||
Bucket::ZPool(data) => {
|
Bucket::ZPool(data) => {
|
||||||
if let Some(ref comp) = comp.get(0) {
|
if let Some(ref comp) = comp.get(0) {
|
||||||
ty == "zpool" && comp.as_ref() == &data.name
|
ty == "zpool" && comp.as_ref() == data.name
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Bucket::LVM(data) => {
|
Bucket::LVM(data) => {
|
||||||
if let (Some(ref vg), Some(ref lv)) = (comp.get(0), comp.get(1)) {
|
if let (Some(ref vg), Some(ref lv)) = (comp.get(0), comp.get(1)) {
|
||||||
ty == "lvm" && vg.as_ref() == &data.vg_name && lv.as_ref() == &data.lv_name
|
ty == "lvm" && vg.as_ref() == data.vg_name && lv.as_ref() == data.lv_name
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
@ -336,8 +336,8 @@ impl Filesystems {
|
|||||||
info!("mounting '{}' succeeded, fstype: '{}'", source, fs);
|
info!("mounting '{}' succeeded, fstype: '{}'", source, fs);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
Err(nix::Error::Sys(nix::errno::Errno::EINVAL)) => {}
|
Err(nix::errno::Errno::EINVAL) => {}
|
||||||
Err(nix::Error::Sys(nix::errno::Errno::EBUSY)) => return Ok(()),
|
Err(nix::errno::Errno::EBUSY) => return Ok(()),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
warn!("mount error on '{}' ({}) - {}", source, fs, err);
|
warn!("mount error on '{}' ({}) - {}", source, fs, err);
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,7 @@ bitflags = "1.2.1"
|
|||||||
crossbeam-channel = "0.5"
|
crossbeam-channel = "0.5"
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
log = "0.4.17"
|
log = "0.4.17"
|
||||||
nix = "0.19.1"
|
nix = "0.24"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
serde_cbor = "0.11.1"
|
serde_cbor = "0.11.1"
|
||||||
@ -22,4 +22,4 @@ serde_cbor = "0.11.1"
|
|||||||
#proxmox = { version = "0.15.3" }
|
#proxmox = { version = "0.15.3" }
|
||||||
proxmox-time = "1"
|
proxmox-time = "1"
|
||||||
proxmox-schema = { version = "1.3.1", features = [ "api-macro" ] }
|
proxmox-schema = { version = "1.3.1", features = [ "api-macro" ] }
|
||||||
proxmox-sys = "0.2"
|
proxmox-sys = "0.3"
|
@ -11,7 +11,7 @@ path = "src/main.rs"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
nix = "0.19.1"
|
nix = "0.24"
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
tokio = { version = "1.6", features = [ "rt", "rt-multi-thread" ] }
|
tokio = { version = "1.6", features = [ "rt", "rt-multi-thread" ] }
|
||||||
|
|
||||||
@ -20,7 +20,7 @@ pathpatterns = "0.1.2"
|
|||||||
proxmox-async = "0.4"
|
proxmox-async = "0.4"
|
||||||
proxmox-schema = { version = "1.3.1", features = [ "api-macro" ] }
|
proxmox-schema = { version = "1.3.1", features = [ "api-macro" ] }
|
||||||
proxmox-router = "1.2"
|
proxmox-router = "1.2"
|
||||||
proxmox-sys = "0.2"
|
proxmox-sys = "0.3"
|
||||||
pxar = { version = "0.10.1", features = [ "tokio-io" ] }
|
pxar = { version = "0.10.1", features = [ "tokio-io" ] }
|
||||||
|
|
||||||
pbs-client = { path = "../pbs-client" }
|
pbs-client = { path = "../pbs-client" }
|
||||||
|
@ -147,7 +147,7 @@ fn extract_archive(
|
|||||||
feature_flags.remove(Flags::WITH_SOCKETS);
|
feature_flags.remove(Flags::WITH_SOCKETS);
|
||||||
}
|
}
|
||||||
|
|
||||||
let pattern = pattern.unwrap_or_else(Vec::new);
|
let pattern = pattern.unwrap_or_default();
|
||||||
let target = target.as_ref().map_or_else(|| ".", String::as_str);
|
let target = target.as_ref().map_or_else(|| ".", String::as_str);
|
||||||
|
|
||||||
let mut match_list = Vec::new();
|
let mut match_list = Vec::new();
|
||||||
@ -297,7 +297,7 @@ async fn create_archive(
|
|||||||
entries_max: isize,
|
entries_max: isize,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let patterns = {
|
let patterns = {
|
||||||
let input = exclude.unwrap_or_else(Vec::new);
|
let input = exclude.unwrap_or_default();
|
||||||
let mut patterns = Vec::with_capacity(input.len());
|
let mut patterns = Vec::with_capacity(input.len());
|
||||||
for entry in input {
|
for entry in input {
|
||||||
patterns.push(
|
patterns.push(
|
||||||
|
@ -161,7 +161,7 @@ impl AcmeClient {
|
|||||||
let mut data = Vec::<u8>::new();
|
let mut data = Vec::<u8>::new();
|
||||||
self.write_to(&mut data)?;
|
self.write_to(&mut data)?;
|
||||||
let account_path = self.account_path.as_ref().ok_or_else(|| {
|
let account_path = self.account_path.as_ref().ok_or_else(|| {
|
||||||
format_err!("no account path set, cannot save upated account information")
|
format_err!("no account path set, cannot save updated account information")
|
||||||
})?;
|
})?;
|
||||||
crate::config::acme::make_acme_account_dir()?;
|
crate::config::acme::make_acme_account_dir()?;
|
||||||
replace_file(
|
replace_file(
|
||||||
|
@ -32,9 +32,9 @@ use pxar::accessor::aio::Accessor;
|
|||||||
use pxar::EntryKind;
|
use pxar::EntryKind;
|
||||||
|
|
||||||
use pbs_api_types::{
|
use pbs_api_types::{
|
||||||
print_ns_and_snapshot, Authid, BackupContent, BackupNamespace, BackupType, Counts, CryptMode,
|
print_ns_and_snapshot, print_store_and_ns, Authid, BackupContent, BackupNamespace, BackupType,
|
||||||
DataStoreListItem, DataStoreStatus, DatastoreWithNamespace, GarbageCollectionStatus,
|
Counts, CryptMode, DataStoreListItem, DataStoreStatus, GarbageCollectionStatus, GroupListItem,
|
||||||
GroupListItem, Operation, PruneOptions, RRDMode, RRDTimeFrame, SnapshotListItem,
|
KeepOptions, Operation, PruneJobOptions, RRDMode, RRDTimeFrame, SnapshotListItem,
|
||||||
SnapshotVerifyState, BACKUP_ARCHIVE_NAME_SCHEMA, BACKUP_ID_SCHEMA, BACKUP_NAMESPACE_SCHEMA,
|
SnapshotVerifyState, BACKUP_ARCHIVE_NAME_SCHEMA, BACKUP_ID_SCHEMA, BACKUP_NAMESPACE_SCHEMA,
|
||||||
BACKUP_TIME_SCHEMA, BACKUP_TYPE_SCHEMA, DATASTORE_SCHEMA, IGNORE_VERIFIED_BACKUPS_SCHEMA,
|
BACKUP_TIME_SCHEMA, BACKUP_TYPE_SCHEMA, DATASTORE_SCHEMA, IGNORE_VERIFIED_BACKUPS_SCHEMA,
|
||||||
MAX_NAMESPACE_DEPTH, NS_MAX_DEPTH_SCHEMA, PRIV_DATASTORE_AUDIT, PRIV_DATASTORE_BACKUP,
|
MAX_NAMESPACE_DEPTH, NS_MAX_DEPTH_SCHEMA, PRIV_DATASTORE_AUDIT, PRIV_DATASTORE_BACKUP,
|
||||||
@ -63,8 +63,8 @@ use proxmox_rest_server::{formatter, WorkerTask};
|
|||||||
use crate::api2::backup::optional_ns_param;
|
use crate::api2::backup::optional_ns_param;
|
||||||
use crate::api2::node::rrd::create_value_from_rrd;
|
use crate::api2::node::rrd::create_value_from_rrd;
|
||||||
use crate::backup::{
|
use crate::backup::{
|
||||||
verify_all_backups, verify_backup_dir, verify_backup_group, verify_filter,
|
check_ns_privs_full, verify_all_backups, verify_backup_dir, verify_backup_group, verify_filter,
|
||||||
ListAccessibleBackupGroups,
|
ListAccessibleBackupGroups, NS_PRIVS_OK,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::server::jobstate::Job;
|
use crate::server::jobstate::Job;
|
||||||
@ -81,38 +81,6 @@ fn get_group_note_path(
|
|||||||
note_path
|
note_path
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: move somewhere we can reuse it from (namespace has its own copy atm.)
|
|
||||||
fn get_ns_privs(store: &str, ns: &BackupNamespace, auth_id: &Authid) -> Result<u64, Error> {
|
|
||||||
let user_info = CachedUserInfo::new()?;
|
|
||||||
|
|
||||||
Ok(if ns.is_root() {
|
|
||||||
user_info.lookup_privs(auth_id, &["datastore", store])
|
|
||||||
} else {
|
|
||||||
user_info.lookup_privs(auth_id, &["datastore", store, &ns.to_string()])
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// asserts that either either `full_access_privs` or `partial_access_privs` are fulfilled,
|
|
||||||
// returning value indicates whether further checks like group ownerships are required
|
|
||||||
fn check_ns_privs(
|
|
||||||
store: &str,
|
|
||||||
ns: &BackupNamespace,
|
|
||||||
auth_id: &Authid,
|
|
||||||
full_access_privs: u64,
|
|
||||||
partial_access_privs: u64,
|
|
||||||
) -> Result<bool, Error> {
|
|
||||||
let privs = get_ns_privs(store, ns, auth_id)?;
|
|
||||||
|
|
||||||
if full_access_privs != 0 && (privs & full_access_privs) != 0 {
|
|
||||||
return Ok(false);
|
|
||||||
}
|
|
||||||
if partial_access_privs != 0 && (privs & partial_access_privs) != 0 {
|
|
||||||
return Ok(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
proxmox_router::http_bail!(FORBIDDEN, "permission check failed");
|
|
||||||
}
|
|
||||||
|
|
||||||
// helper to unify common sequence of checks:
|
// helper to unify common sequence of checks:
|
||||||
// 1. check privs on NS (full or limited access)
|
// 1. check privs on NS (full or limited access)
|
||||||
// 2. load datastore
|
// 2. load datastore
|
||||||
@ -126,12 +94,12 @@ fn check_privs_and_load_store(
|
|||||||
operation: Option<Operation>,
|
operation: Option<Operation>,
|
||||||
backup_group: &pbs_api_types::BackupGroup,
|
backup_group: &pbs_api_types::BackupGroup,
|
||||||
) -> Result<Arc<DataStore>, Error> {
|
) -> Result<Arc<DataStore>, Error> {
|
||||||
let limited = check_ns_privs(store, ns, auth_id, full_access_privs, partial_access_privs)?;
|
let limited = check_ns_privs_full(store, ns, auth_id, full_access_privs, partial_access_privs)?;
|
||||||
|
|
||||||
let datastore = DataStore::lookup_datastore(&store, operation)?;
|
let datastore = DataStore::lookup_datastore(store, operation)?;
|
||||||
|
|
||||||
if limited {
|
if limited {
|
||||||
let owner = datastore.get_owner(&ns, backup_group)?;
|
let owner = datastore.get_owner(ns, backup_group)?;
|
||||||
check_backup_owner(&owner, &auth_id)?;
|
check_backup_owner(&owner, &auth_id)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -214,9 +182,9 @@ pub fn list_groups(
|
|||||||
rpcenv: &mut dyn RpcEnvironment,
|
rpcenv: &mut dyn RpcEnvironment,
|
||||||
) -> Result<Vec<GroupListItem>, Error> {
|
) -> Result<Vec<GroupListItem>, Error> {
|
||||||
let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
|
let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
|
||||||
|
|
||||||
let ns = ns.unwrap_or_default();
|
let ns = ns.unwrap_or_default();
|
||||||
let list_all = !check_ns_privs(
|
|
||||||
|
let list_all = !check_ns_privs_full(
|
||||||
&store,
|
&store,
|
||||||
&ns,
|
&ns,
|
||||||
&auth_id,
|
&auth_id,
|
||||||
@ -225,10 +193,6 @@ pub fn list_groups(
|
|||||||
)?;
|
)?;
|
||||||
|
|
||||||
let datastore = DataStore::lookup_datastore(&store, Some(Operation::Read))?;
|
let datastore = DataStore::lookup_datastore(&store, Some(Operation::Read))?;
|
||||||
let store_with_ns = DatastoreWithNamespace {
|
|
||||||
store: store.to_owned(),
|
|
||||||
ns: ns.clone(),
|
|
||||||
};
|
|
||||||
|
|
||||||
datastore
|
datastore
|
||||||
.iter_backup_groups(ns.clone())? // FIXME: Namespaces and recursion parameters!
|
.iter_backup_groups(ns.clone())? // FIXME: Namespaces and recursion parameters!
|
||||||
@ -241,7 +205,7 @@ pub fn list_groups(
|
|||||||
eprintln!(
|
eprintln!(
|
||||||
"Failed to get owner of group '{}' in {} - {}",
|
"Failed to get owner of group '{}' in {} - {}",
|
||||||
group.group(),
|
group.group(),
|
||||||
store_with_ns,
|
print_store_and_ns(&store, &ns),
|
||||||
err
|
err
|
||||||
);
|
);
|
||||||
return Ok(group_info);
|
return Ok(group_info);
|
||||||
@ -317,7 +281,6 @@ pub fn delete_group(
|
|||||||
rpcenv: &mut dyn RpcEnvironment,
|
rpcenv: &mut dyn RpcEnvironment,
|
||||||
) -> Result<Value, Error> {
|
) -> Result<Value, Error> {
|
||||||
let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
|
let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
|
||||||
|
|
||||||
let ns = ns.unwrap_or_default();
|
let ns = ns.unwrap_or_default();
|
||||||
|
|
||||||
let datastore = check_privs_and_load_store(
|
let datastore = check_privs_and_load_store(
|
||||||
@ -367,7 +330,6 @@ pub fn list_snapshot_files(
|
|||||||
rpcenv: &mut dyn RpcEnvironment,
|
rpcenv: &mut dyn RpcEnvironment,
|
||||||
) -> Result<Vec<BackupContent>, Error> {
|
) -> Result<Vec<BackupContent>, Error> {
|
||||||
let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
|
let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
|
||||||
|
|
||||||
let ns = ns.unwrap_or_default();
|
let ns = ns.unwrap_or_default();
|
||||||
|
|
||||||
let datastore = check_privs_and_load_store(
|
let datastore = check_privs_and_load_store(
|
||||||
@ -418,8 +380,8 @@ pub fn delete_snapshot(
|
|||||||
rpcenv: &mut dyn RpcEnvironment,
|
rpcenv: &mut dyn RpcEnvironment,
|
||||||
) -> Result<Value, Error> {
|
) -> Result<Value, Error> {
|
||||||
let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
|
let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
|
||||||
|
|
||||||
let ns = ns.unwrap_or_default();
|
let ns = ns.unwrap_or_default();
|
||||||
|
|
||||||
let datastore = check_privs_and_load_store(
|
let datastore = check_privs_and_load_store(
|
||||||
&store,
|
&store,
|
||||||
&ns,
|
&ns,
|
||||||
@ -477,7 +439,7 @@ pub fn list_snapshots(
|
|||||||
|
|
||||||
let ns = ns.unwrap_or_default();
|
let ns = ns.unwrap_or_default();
|
||||||
|
|
||||||
let list_all = !check_ns_privs(
|
let list_all = !check_ns_privs_full(
|
||||||
&store,
|
&store,
|
||||||
&ns,
|
&ns,
|
||||||
&auth_id,
|
&auth_id,
|
||||||
@ -486,29 +448,25 @@ pub fn list_snapshots(
|
|||||||
)?;
|
)?;
|
||||||
|
|
||||||
let datastore = DataStore::lookup_datastore(&store, Some(Operation::Read))?;
|
let datastore = DataStore::lookup_datastore(&store, Some(Operation::Read))?;
|
||||||
let store_with_ns = DatastoreWithNamespace {
|
|
||||||
store: store.to_owned(),
|
|
||||||
ns: ns.clone(),
|
|
||||||
};
|
|
||||||
|
|
||||||
// FIXME: filter also owner before collecting, for doing that nicely the owner should move into
|
// FIXME: filter also owner before collecting, for doing that nicely the owner should move into
|
||||||
// backup group and provide an error free (Err -> None) accessor
|
// backup group and provide an error free (Err -> None) accessor
|
||||||
let groups = match (backup_type, backup_id) {
|
let groups = match (backup_type, backup_id) {
|
||||||
(Some(backup_type), Some(backup_id)) => {
|
(Some(backup_type), Some(backup_id)) => {
|
||||||
vec![datastore.backup_group_from_parts(ns, backup_type, backup_id)]
|
vec![datastore.backup_group_from_parts(ns.clone(), backup_type, backup_id)]
|
||||||
}
|
}
|
||||||
// FIXME: Recursion
|
// FIXME: Recursion
|
||||||
(Some(backup_type), None) => datastore
|
(Some(backup_type), None) => datastore
|
||||||
.iter_backup_groups_ok(ns)?
|
.iter_backup_groups_ok(ns.clone())?
|
||||||
.filter(|group| group.backup_type() == backup_type)
|
.filter(|group| group.backup_type() == backup_type)
|
||||||
.collect(),
|
.collect(),
|
||||||
// FIXME: Recursion
|
// FIXME: Recursion
|
||||||
(None, Some(backup_id)) => datastore
|
(None, Some(backup_id)) => datastore
|
||||||
.iter_backup_groups_ok(ns)?
|
.iter_backup_groups_ok(ns.clone())?
|
||||||
.filter(|group| group.backup_id() == backup_id)
|
.filter(|group| group.backup_id() == backup_id)
|
||||||
.collect(),
|
.collect(),
|
||||||
// FIXME: Recursion
|
// FIXME: Recursion
|
||||||
(None, None) => datastore.list_backup_groups(ns)?,
|
(None, None) => datastore.list_backup_groups(ns.clone())?,
|
||||||
};
|
};
|
||||||
|
|
||||||
let info_to_snapshot_list_item = |group: &BackupGroup, owner, info: BackupInfo| {
|
let info_to_snapshot_list_item = |group: &BackupGroup, owner, info: BackupInfo| {
|
||||||
@ -589,8 +547,8 @@ pub fn list_snapshots(
|
|||||||
Err(err) => {
|
Err(err) => {
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"Failed to get owner of group '{}' in {} - {}",
|
"Failed to get owner of group '{}' in {} - {}",
|
||||||
&store_with_ns,
|
|
||||||
group.group(),
|
group.group(),
|
||||||
|
print_store_and_ns(&store, &ns),
|
||||||
err
|
err
|
||||||
);
|
);
|
||||||
return Ok(snapshots);
|
return Ok(snapshots);
|
||||||
@ -615,30 +573,35 @@ pub fn list_snapshots(
|
|||||||
|
|
||||||
fn get_snapshots_count(store: &Arc<DataStore>, owner: Option<&Authid>) -> Result<Counts, Error> {
|
fn get_snapshots_count(store: &Arc<DataStore>, owner: Option<&Authid>) -> Result<Counts, Error> {
|
||||||
let root_ns = Default::default();
|
let root_ns = Default::default();
|
||||||
ListAccessibleBackupGroups::new(store, root_ns, MAX_NAMESPACE_DEPTH, owner)?.try_fold(
|
ListAccessibleBackupGroups::new_with_privs(
|
||||||
Counts::default(),
|
store,
|
||||||
|mut counts, group| {
|
root_ns,
|
||||||
let group = match group {
|
MAX_NAMESPACE_DEPTH,
|
||||||
Ok(group) => group,
|
Some(PRIV_DATASTORE_AUDIT | PRIV_DATASTORE_READ),
|
||||||
Err(_) => return Ok(counts), // TODO: add this as error counts?
|
None,
|
||||||
|
owner,
|
||||||
|
)?
|
||||||
|
.try_fold(Counts::default(), |mut counts, group| {
|
||||||
|
let group = match group {
|
||||||
|
Ok(group) => group,
|
||||||
|
Err(_) => return Ok(counts), // TODO: add this as error counts?
|
||||||
|
};
|
||||||
|
let snapshot_count = group.list_backups()?.len() as u64;
|
||||||
|
|
||||||
|
// only include groups with snapshots, counting/displaying empty groups can confuse
|
||||||
|
if snapshot_count > 0 {
|
||||||
|
let type_count = match group.backup_type() {
|
||||||
|
BackupType::Ct => counts.ct.get_or_insert(Default::default()),
|
||||||
|
BackupType::Vm => counts.vm.get_or_insert(Default::default()),
|
||||||
|
BackupType::Host => counts.host.get_or_insert(Default::default()),
|
||||||
};
|
};
|
||||||
let snapshot_count = group.list_backups()?.len() as u64;
|
|
||||||
|
|
||||||
// only include groups with snapshots, counting/displaying emtpy groups can confuse
|
type_count.groups += 1;
|
||||||
if snapshot_count > 0 {
|
type_count.snapshots += snapshot_count;
|
||||||
let type_count = match group.backup_type() {
|
}
|
||||||
BackupType::Ct => counts.ct.get_or_insert(Default::default()),
|
|
||||||
BackupType::Vm => counts.vm.get_or_insert(Default::default()),
|
|
||||||
BackupType::Host => counts.host.get_or_insert(Default::default()),
|
|
||||||
};
|
|
||||||
|
|
||||||
type_count.groups += 1;
|
Ok(counts)
|
||||||
type_count.snapshots += snapshot_count;
|
})
|
||||||
}
|
|
||||||
|
|
||||||
Ok(counts)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[api(
|
#[api(
|
||||||
@ -660,8 +623,9 @@ fn get_snapshots_count(store: &Arc<DataStore>, owner: Option<&Authid>) -> Result
|
|||||||
type: DataStoreStatus,
|
type: DataStoreStatus,
|
||||||
},
|
},
|
||||||
access: {
|
access: {
|
||||||
permission: &Permission::Privilege(
|
permission: &Permission::Anybody,
|
||||||
&["datastore", "{store}"], PRIV_DATASTORE_AUDIT | PRIV_DATASTORE_BACKUP, true),
|
description: "Requires on /datastore/{store} either DATASTORE_AUDIT or DATASTORE_BACKUP for \
|
||||||
|
the full statistics. Counts of accessible groups are always returned, if any",
|
||||||
},
|
},
|
||||||
)]
|
)]
|
||||||
/// Get datastore status.
|
/// Get datastore status.
|
||||||
@ -671,13 +635,26 @@ pub fn status(
|
|||||||
_info: &ApiMethod,
|
_info: &ApiMethod,
|
||||||
rpcenv: &mut dyn RpcEnvironment,
|
rpcenv: &mut dyn RpcEnvironment,
|
||||||
) -> Result<DataStoreStatus, Error> {
|
) -> Result<DataStoreStatus, Error> {
|
||||||
let datastore = DataStore::lookup_datastore(&store, Some(Operation::Read))?;
|
let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
|
||||||
let storage = crate::tools::disks::disk_usage(&datastore.base_path())?;
|
let user_info = CachedUserInfo::new()?;
|
||||||
let (counts, gc_status) = if verbose {
|
let store_privs = user_info.lookup_privs(&auth_id, &["datastore", &store]);
|
||||||
let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
|
|
||||||
let user_info = CachedUserInfo::new()?;
|
|
||||||
|
|
||||||
let store_privs = user_info.lookup_privs(&auth_id, &["datastore", &store]);
|
let datastore = DataStore::lookup_datastore(&store, Some(Operation::Read));
|
||||||
|
|
||||||
|
let store_stats = if store_privs & (PRIV_DATASTORE_AUDIT | PRIV_DATASTORE_BACKUP) != 0 {
|
||||||
|
true
|
||||||
|
} else if store_privs & PRIV_DATASTORE_READ != 0 {
|
||||||
|
false // allow at least counts, user can read groups anyway..
|
||||||
|
} else {
|
||||||
|
match user_info.any_privs_below(&auth_id, &["datastore", &store], NS_PRIVS_OK) {
|
||||||
|
// avoid leaking existence info if users hasn't at least any priv. below
|
||||||
|
Ok(false) | Err(_) => return Err(http_err!(FORBIDDEN, "permission check failed")),
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let datastore = datastore?; // only unwrap no to avoid leaking existence info
|
||||||
|
|
||||||
|
let (counts, gc_status) = if verbose {
|
||||||
let filter_owner = if store_privs & PRIV_DATASTORE_AUDIT != 0 {
|
let filter_owner = if store_privs & PRIV_DATASTORE_AUDIT != 0 {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
@ -685,19 +662,34 @@ pub fn status(
|
|||||||
};
|
};
|
||||||
|
|
||||||
let counts = Some(get_snapshots_count(&datastore, filter_owner)?);
|
let counts = Some(get_snapshots_count(&datastore, filter_owner)?);
|
||||||
let gc_status = Some(datastore.last_gc_status());
|
let gc_status = if store_stats {
|
||||||
|
Some(datastore.last_gc_status())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
(counts, gc_status)
|
(counts, gc_status)
|
||||||
} else {
|
} else {
|
||||||
(None, None)
|
(None, None)
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(DataStoreStatus {
|
Ok(if store_stats {
|
||||||
total: storage.total,
|
let storage = crate::tools::disks::disk_usage(&datastore.base_path())?;
|
||||||
used: storage.used,
|
DataStoreStatus {
|
||||||
avail: storage.avail,
|
total: storage.total,
|
||||||
gc_status,
|
used: storage.used,
|
||||||
counts,
|
avail: storage.avail,
|
||||||
|
gc_status,
|
||||||
|
counts,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
DataStoreStatus {
|
||||||
|
total: 0,
|
||||||
|
used: 0,
|
||||||
|
avail: 0,
|
||||||
|
gc_status,
|
||||||
|
counts,
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -763,7 +755,8 @@ pub fn verify(
|
|||||||
) -> Result<Value, Error> {
|
) -> Result<Value, Error> {
|
||||||
let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
|
let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
|
||||||
let ns = ns.unwrap_or_default();
|
let ns = ns.unwrap_or_default();
|
||||||
let owner_check_required = check_ns_privs(
|
|
||||||
|
let owner_check_required = check_ns_privs_full(
|
||||||
&store,
|
&store,
|
||||||
&ns,
|
&ns,
|
||||||
&auth_id,
|
&auth_id,
|
||||||
@ -821,9 +814,9 @@ pub fn verify(
|
|||||||
}
|
}
|
||||||
(None, None, None) => {
|
(None, None, None) => {
|
||||||
worker_id = if ns.is_root() {
|
worker_id = if ns.is_root() {
|
||||||
store.clone()
|
store
|
||||||
} else {
|
} else {
|
||||||
format!("{store}:{}", ns.display_as_path())
|
format!("{}:{}", store, ns.display_as_path())
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
_ => bail!("parameters do not specify a backup group or snapshot"),
|
_ => bail!("parameters do not specify a backup group or snapshot"),
|
||||||
@ -894,10 +887,6 @@ pub fn verify(
|
|||||||
#[api(
|
#[api(
|
||||||
input: {
|
input: {
|
||||||
properties: {
|
properties: {
|
||||||
ns: {
|
|
||||||
type: BackupNamespace,
|
|
||||||
optional: true,
|
|
||||||
},
|
|
||||||
group: {
|
group: {
|
||||||
type: pbs_api_types::BackupGroup,
|
type: pbs_api_types::BackupGroup,
|
||||||
flatten: true,
|
flatten: true,
|
||||||
@ -908,13 +897,17 @@ pub fn verify(
|
|||||||
default: false,
|
default: false,
|
||||||
description: "Just show what prune would do, but do not delete anything.",
|
description: "Just show what prune would do, but do not delete anything.",
|
||||||
},
|
},
|
||||||
"prune-options": {
|
"keep-options": {
|
||||||
type: PruneOptions,
|
type: KeepOptions,
|
||||||
flatten: true,
|
flatten: true,
|
||||||
},
|
},
|
||||||
store: {
|
store: {
|
||||||
schema: DATASTORE_SCHEMA,
|
schema: DATASTORE_SCHEMA,
|
||||||
},
|
},
|
||||||
|
ns: {
|
||||||
|
type: BackupNamespace,
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
returns: pbs_api_types::ADMIN_DATASTORE_PRUNE_RETURN_TYPE,
|
returns: pbs_api_types::ADMIN_DATASTORE_PRUNE_RETURN_TYPE,
|
||||||
@ -926,11 +919,11 @@ pub fn verify(
|
|||||||
)]
|
)]
|
||||||
/// Prune a group on the datastore
|
/// Prune a group on the datastore
|
||||||
pub fn prune(
|
pub fn prune(
|
||||||
ns: Option<BackupNamespace>,
|
|
||||||
group: pbs_api_types::BackupGroup,
|
group: pbs_api_types::BackupGroup,
|
||||||
dry_run: bool,
|
dry_run: bool,
|
||||||
prune_options: PruneOptions,
|
keep_options: KeepOptions,
|
||||||
store: String,
|
store: String,
|
||||||
|
ns: Option<BackupNamespace>,
|
||||||
_param: Value,
|
_param: Value,
|
||||||
rpcenv: &mut dyn RpcEnvironment,
|
rpcenv: &mut dyn RpcEnvironment,
|
||||||
) -> Result<Value, Error> {
|
) -> Result<Value, Error> {
|
||||||
@ -945,23 +938,19 @@ pub fn prune(
|
|||||||
Some(Operation::Write),
|
Some(Operation::Write),
|
||||||
&group,
|
&group,
|
||||||
)?;
|
)?;
|
||||||
let store_with_ns = DatastoreWithNamespace {
|
|
||||||
store: store.to_owned(),
|
|
||||||
ns: ns.clone(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let worker_id = format!("{}:{}:{}", store, ns, group);
|
let worker_id = format!("{}:{}:{}", store, ns, group);
|
||||||
let group = datastore.backup_group(ns, group);
|
let group = datastore.backup_group(ns.clone(), group);
|
||||||
|
|
||||||
let mut prune_result = Vec::new();
|
let mut prune_result = Vec::new();
|
||||||
|
|
||||||
let list = group.list_backups()?;
|
let list = group.list_backups()?;
|
||||||
|
|
||||||
let mut prune_info = compute_prune_info(list, &prune_options)?;
|
let mut prune_info = compute_prune_info(list, &keep_options)?;
|
||||||
|
|
||||||
prune_info.reverse(); // delete older snapshots first
|
prune_info.reverse(); // delete older snapshots first
|
||||||
|
|
||||||
let keep_all = !pbs_datastore::prune::keeps_something(&prune_options);
|
let keep_all = !keep_options.keeps_something();
|
||||||
|
|
||||||
if dry_run {
|
if dry_run {
|
||||||
for (info, mark) in prune_info {
|
for (info, mark) in prune_info {
|
||||||
@ -989,15 +978,17 @@ pub fn prune(
|
|||||||
if keep_all {
|
if keep_all {
|
||||||
task_log!(worker, "No prune selection - keeping all files.");
|
task_log!(worker, "No prune selection - keeping all files.");
|
||||||
} else {
|
} else {
|
||||||
task_log!(
|
let mut opts = Vec::new();
|
||||||
worker,
|
if !ns.is_root() {
|
||||||
"retention options: {}",
|
opts.push(format!("--ns {ns}"));
|
||||||
pbs_datastore::prune::cli_options_string(&prune_options)
|
}
|
||||||
);
|
crate::server::cli_keep_options(&mut opts, &keep_options);
|
||||||
|
|
||||||
|
task_log!(worker, "retention options: {}", opts.join(" "));
|
||||||
task_log!(
|
task_log!(
|
||||||
worker,
|
worker,
|
||||||
"Starting prune on {} group \"{}\"",
|
"Starting prune on {} group \"{}\"",
|
||||||
store_with_ns,
|
print_store_and_ns(&store, &ns),
|
||||||
group.group(),
|
group.group(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1048,52 +1039,54 @@ pub fn prune(
|
|||||||
description: "Just show what prune would do, but do not delete anything.",
|
description: "Just show what prune would do, but do not delete anything.",
|
||||||
},
|
},
|
||||||
"prune-options": {
|
"prune-options": {
|
||||||
type: PruneOptions,
|
type: PruneJobOptions,
|
||||||
flatten: true,
|
flatten: true,
|
||||||
},
|
},
|
||||||
store: {
|
store: {
|
||||||
schema: DATASTORE_SCHEMA,
|
schema: DATASTORE_SCHEMA,
|
||||||
},
|
},
|
||||||
ns: {
|
|
||||||
type: BackupNamespace,
|
|
||||||
optional: true,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
returns: {
|
returns: {
|
||||||
schema: UPID_SCHEMA,
|
schema: UPID_SCHEMA,
|
||||||
},
|
},
|
||||||
access: {
|
access: {
|
||||||
permission: &Permission::Privilege(
|
permission: &Permission::Anybody,
|
||||||
&["datastore", "{store}"], PRIV_DATASTORE_MODIFY | PRIV_DATASTORE_PRUNE, true),
|
description: "Requires Datastore.Modify or Datastore.Prune on the datastore/namespace.",
|
||||||
},
|
},
|
||||||
)]
|
)]
|
||||||
/// Prune the datastore
|
/// Prune the datastore
|
||||||
pub fn prune_datastore(
|
pub fn prune_datastore(
|
||||||
dry_run: bool,
|
dry_run: bool,
|
||||||
prune_options: PruneOptions,
|
prune_options: PruneJobOptions,
|
||||||
store: String,
|
store: String,
|
||||||
ns: Option<BackupNamespace>,
|
|
||||||
_param: Value,
|
_param: Value,
|
||||||
rpcenv: &mut dyn RpcEnvironment,
|
rpcenv: &mut dyn RpcEnvironment,
|
||||||
) -> Result<String, Error> {
|
) -> Result<String, Error> {
|
||||||
|
let user_info = CachedUserInfo::new()?;
|
||||||
|
|
||||||
let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
|
let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
|
||||||
|
|
||||||
|
user_info.check_privs(
|
||||||
|
&auth_id,
|
||||||
|
&prune_options.acl_path(&store),
|
||||||
|
PRIV_DATASTORE_MODIFY | PRIV_DATASTORE_PRUNE,
|
||||||
|
true,
|
||||||
|
)?;
|
||||||
|
|
||||||
let datastore = DataStore::lookup_datastore(&store, Some(Operation::Write))?;
|
let datastore = DataStore::lookup_datastore(&store, Some(Operation::Write))?;
|
||||||
let ns = ns.unwrap_or_default();
|
let ns = prune_options.ns.clone().unwrap_or_default();
|
||||||
let worker_id = format!("{}:{}", store, ns);
|
let worker_id = format!("{}:{}", store, ns);
|
||||||
|
|
||||||
let to_stdout = rpcenv.env_type() == RpcEnvironmentType::CLI;
|
let to_stdout = rpcenv.env_type() == RpcEnvironmentType::CLI;
|
||||||
|
|
||||||
// FIXME: add max-depth
|
|
||||||
|
|
||||||
let upid_str = WorkerTask::new_thread(
|
let upid_str = WorkerTask::new_thread(
|
||||||
"prune",
|
"prune",
|
||||||
Some(worker_id),
|
Some(worker_id),
|
||||||
auth_id.to_string(),
|
auth_id.to_string(),
|
||||||
to_stdout,
|
to_stdout,
|
||||||
move |worker| {
|
move |worker| {
|
||||||
crate::server::prune_datastore(worker, auth_id, prune_options, datastore, ns, dry_run)
|
crate::server::prune_datastore(worker, auth_id, prune_options, datastore, dry_run)
|
||||||
},
|
},
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
@ -1170,24 +1163,6 @@ pub fn garbage_collection_status(
|
|||||||
Ok(status)
|
Ok(status)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn can_access_any_ns(store: Arc<DataStore>, auth_id: &Authid, user_info: &CachedUserInfo) -> bool {
|
|
||||||
// NOTE: traversing the datastore could be avoided if we had an "ACL tree: is there any priv
|
|
||||||
// below /datastore/{store}" helper
|
|
||||||
let mut iter =
|
|
||||||
if let Ok(iter) = store.recursive_iter_backup_ns_ok(BackupNamespace::root(), None) {
|
|
||||||
iter
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
let wanted =
|
|
||||||
PRIV_DATASTORE_AUDIT | PRIV_DATASTORE_MODIFY | PRIV_DATASTORE_READ | PRIV_DATASTORE_BACKUP;
|
|
||||||
let name = store.name();
|
|
||||||
iter.any(|ns| -> bool {
|
|
||||||
let user_privs = user_info.lookup_privs(&auth_id, &["datastore", name, &ns.to_string()]);
|
|
||||||
user_privs & wanted != 0
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[api(
|
#[api(
|
||||||
returns: {
|
returns: {
|
||||||
description: "List the accessible datastores.",
|
description: "List the accessible datastores.",
|
||||||
@ -1212,15 +1187,14 @@ pub fn get_datastore_list(
|
|||||||
let mut list = Vec::new();
|
let mut list = Vec::new();
|
||||||
|
|
||||||
for (store, (_, data)) in &config.sections {
|
for (store, (_, data)) in &config.sections {
|
||||||
let user_privs = user_info.lookup_privs(&auth_id, &["datastore", store]);
|
let acl_path = &["datastore", store];
|
||||||
|
let user_privs = user_info.lookup_privs(&auth_id, acl_path);
|
||||||
let allowed = (user_privs & (PRIV_DATASTORE_AUDIT | PRIV_DATASTORE_BACKUP)) != 0;
|
let allowed = (user_privs & (PRIV_DATASTORE_AUDIT | PRIV_DATASTORE_BACKUP)) != 0;
|
||||||
|
|
||||||
let mut allow_id = false;
|
let mut allow_id = false;
|
||||||
if !allowed {
|
if !allowed {
|
||||||
let scfg: pbs_api_types::DataStoreConfig = serde_json::from_value(data.to_owned())?;
|
if let Ok(any_privs) = user_info.any_privs_below(&auth_id, acl_path, NS_PRIVS_OK) {
|
||||||
// safety: we just cannot go through lookup as we must avoid an operation check
|
allow_id = any_privs;
|
||||||
if let Ok(datastore) = unsafe { DataStore::open_from_config(scfg, None) } {
|
|
||||||
allow_id = can_access_any_ns(datastore, &auth_id, &user_info);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1275,10 +1249,6 @@ pub fn download_file(
|
|||||||
let store = required_string_param(¶m, "store")?;
|
let store = required_string_param(¶m, "store")?;
|
||||||
let backup_ns = optional_ns_param(¶m)?;
|
let backup_ns = optional_ns_param(¶m)?;
|
||||||
|
|
||||||
let store_with_ns = DatastoreWithNamespace {
|
|
||||||
store: store.to_owned(),
|
|
||||||
ns: backup_ns.clone(),
|
|
||||||
};
|
|
||||||
let backup_dir: pbs_api_types::BackupDir = Deserialize::deserialize(¶m)?;
|
let backup_dir: pbs_api_types::BackupDir = Deserialize::deserialize(¶m)?;
|
||||||
let datastore = check_privs_and_load_store(
|
let datastore = check_privs_and_load_store(
|
||||||
&store,
|
&store,
|
||||||
@ -1294,7 +1264,10 @@ pub fn download_file(
|
|||||||
|
|
||||||
println!(
|
println!(
|
||||||
"Download {} from {} ({}/{})",
|
"Download {} from {} ({}/{})",
|
||||||
file_name, store_with_ns, backup_dir, file_name
|
file_name,
|
||||||
|
print_store_and_ns(&store, &backup_ns),
|
||||||
|
backup_dir,
|
||||||
|
file_name
|
||||||
);
|
);
|
||||||
|
|
||||||
let backup_dir = datastore.backup_dir(backup_ns, backup_dir)?;
|
let backup_dir = datastore.backup_dir(backup_ns, backup_dir)?;
|
||||||
@ -1360,10 +1333,7 @@ pub fn download_file_decoded(
|
|||||||
let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
|
let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
|
||||||
let store = required_string_param(¶m, "store")?;
|
let store = required_string_param(¶m, "store")?;
|
||||||
let backup_ns = optional_ns_param(¶m)?;
|
let backup_ns = optional_ns_param(¶m)?;
|
||||||
let store_with_ns = DatastoreWithNamespace {
|
|
||||||
store: store.to_owned(),
|
|
||||||
ns: backup_ns.clone(),
|
|
||||||
};
|
|
||||||
let backup_dir_api: pbs_api_types::BackupDir = Deserialize::deserialize(¶m)?;
|
let backup_dir_api: pbs_api_types::BackupDir = Deserialize::deserialize(¶m)?;
|
||||||
let datastore = check_privs_and_load_store(
|
let datastore = check_privs_and_load_store(
|
||||||
&store,
|
&store,
|
||||||
@ -1376,7 +1346,7 @@ pub fn download_file_decoded(
|
|||||||
)?;
|
)?;
|
||||||
|
|
||||||
let file_name = required_string_param(¶m, "file-name")?.to_owned();
|
let file_name = required_string_param(¶m, "file-name")?.to_owned();
|
||||||
let backup_dir = datastore.backup_dir(backup_ns, backup_dir_api.clone())?;
|
let backup_dir = datastore.backup_dir(backup_ns.clone(), backup_dir_api.clone())?;
|
||||||
|
|
||||||
let (manifest, files) = read_backup_index(&backup_dir)?;
|
let (manifest, files) = read_backup_index(&backup_dir)?;
|
||||||
for file in files {
|
for file in files {
|
||||||
@ -1387,7 +1357,10 @@ pub fn download_file_decoded(
|
|||||||
|
|
||||||
println!(
|
println!(
|
||||||
"Download {} from {} ({}/{})",
|
"Download {} from {} ({}/{})",
|
||||||
file_name, store_with_ns, backup_dir_api, file_name
|
file_name,
|
||||||
|
print_store_and_ns(&store, &backup_ns),
|
||||||
|
backup_dir_api,
|
||||||
|
file_name
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut path = datastore.base_path();
|
let mut path = datastore.base_path();
|
||||||
@ -1490,10 +1463,7 @@ pub fn upload_backup_log(
|
|||||||
let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
|
let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
|
||||||
let store = required_string_param(¶m, "store")?;
|
let store = required_string_param(¶m, "store")?;
|
||||||
let backup_ns = optional_ns_param(¶m)?;
|
let backup_ns = optional_ns_param(¶m)?;
|
||||||
let store_with_ns = DatastoreWithNamespace {
|
|
||||||
store: store.to_owned(),
|
|
||||||
ns: backup_ns.clone(),
|
|
||||||
};
|
|
||||||
let backup_dir_api: pbs_api_types::BackupDir = Deserialize::deserialize(¶m)?;
|
let backup_dir_api: pbs_api_types::BackupDir = Deserialize::deserialize(¶m)?;
|
||||||
|
|
||||||
let datastore = check_privs_and_load_store(
|
let datastore = check_privs_and_load_store(
|
||||||
@ -1505,7 +1475,7 @@ pub fn upload_backup_log(
|
|||||||
Some(Operation::Write),
|
Some(Operation::Write),
|
||||||
&backup_dir_api.group,
|
&backup_dir_api.group,
|
||||||
)?;
|
)?;
|
||||||
let backup_dir = datastore.backup_dir(backup_ns, backup_dir_api.clone())?;
|
let backup_dir = datastore.backup_dir(backup_ns.clone(), backup_dir_api.clone())?;
|
||||||
|
|
||||||
let file_name = CLIENT_LOG_BLOB_NAME;
|
let file_name = CLIENT_LOG_BLOB_NAME;
|
||||||
|
|
||||||
@ -1516,7 +1486,10 @@ pub fn upload_backup_log(
|
|||||||
bail!("backup already contains a log.");
|
bail!("backup already contains a log.");
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("Upload backup log to {store_with_ns} {backup_dir_api}/{file_name}");
|
println!(
|
||||||
|
"Upload backup log to {} {backup_dir_api}/{file_name}",
|
||||||
|
print_store_and_ns(&store, &backup_ns),
|
||||||
|
);
|
||||||
|
|
||||||
let data = req_body
|
let data = req_body
|
||||||
.map_err(Error::from)
|
.map_err(Error::from)
|
||||||
@ -1571,6 +1544,7 @@ pub fn catalog(
|
|||||||
) -> Result<Vec<ArchiveEntry>, Error> {
|
) -> Result<Vec<ArchiveEntry>, Error> {
|
||||||
let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
|
let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
|
||||||
let ns = ns.unwrap_or_default();
|
let ns = ns.unwrap_or_default();
|
||||||
|
|
||||||
let datastore = check_privs_and_load_store(
|
let datastore = check_privs_and_load_store(
|
||||||
&store,
|
&store,
|
||||||
&ns,
|
&ns,
|
||||||
@ -1650,6 +1624,7 @@ pub fn pxar_file_download(
|
|||||||
let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
|
let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
|
||||||
let store = required_string_param(¶m, "store")?;
|
let store = required_string_param(¶m, "store")?;
|
||||||
let ns = optional_ns_param(¶m)?;
|
let ns = optional_ns_param(¶m)?;
|
||||||
|
|
||||||
let backup_dir: pbs_api_types::BackupDir = Deserialize::deserialize(¶m)?;
|
let backup_dir: pbs_api_types::BackupDir = Deserialize::deserialize(¶m)?;
|
||||||
let datastore = check_privs_and_load_store(
|
let datastore = check_privs_and_load_store(
|
||||||
&store,
|
&store,
|
||||||
@ -1857,6 +1832,7 @@ pub fn get_group_notes(
|
|||||||
) -> Result<String, Error> {
|
) -> Result<String, Error> {
|
||||||
let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
|
let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
|
||||||
let ns = ns.unwrap_or_default();
|
let ns = ns.unwrap_or_default();
|
||||||
|
|
||||||
let datastore = check_privs_and_load_store(
|
let datastore = check_privs_and_load_store(
|
||||||
&store,
|
&store,
|
||||||
&ns,
|
&ns,
|
||||||
@ -1904,6 +1880,7 @@ pub fn set_group_notes(
|
|||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
|
let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
|
||||||
let ns = ns.unwrap_or_default();
|
let ns = ns.unwrap_or_default();
|
||||||
|
|
||||||
let datastore = check_privs_and_load_store(
|
let datastore = check_privs_and_load_store(
|
||||||
&store,
|
&store,
|
||||||
&ns,
|
&ns,
|
||||||
@ -1949,6 +1926,7 @@ pub fn get_notes(
|
|||||||
) -> Result<String, Error> {
|
) -> Result<String, Error> {
|
||||||
let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
|
let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
|
||||||
let ns = ns.unwrap_or_default();
|
let ns = ns.unwrap_or_default();
|
||||||
|
|
||||||
let datastore = check_privs_and_load_store(
|
let datastore = check_privs_and_load_store(
|
||||||
&store,
|
&store,
|
||||||
&ns,
|
&ns,
|
||||||
@ -2001,6 +1979,7 @@ pub fn set_notes(
|
|||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
|
let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
|
||||||
let ns = ns.unwrap_or_default();
|
let ns = ns.unwrap_or_default();
|
||||||
|
|
||||||
let datastore = check_privs_and_load_store(
|
let datastore = check_privs_and_load_store(
|
||||||
&store,
|
&store,
|
||||||
&ns,
|
&ns,
|
||||||
@ -2147,7 +2126,7 @@ pub fn set_backup_owner(
|
|||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
|
let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
|
||||||
let ns = ns.unwrap_or_default();
|
let ns = ns.unwrap_or_default();
|
||||||
let owner_check_required = check_ns_privs(
|
let owner_check_required = check_ns_privs_full(
|
||||||
&store,
|
&store,
|
||||||
&ns,
|
&ns,
|
||||||
&auth_id,
|
&auth_id,
|
||||||
|
@ -2,19 +2,23 @@
|
|||||||
|
|
||||||
use proxmox_router::list_subdirs_api_method;
|
use proxmox_router::list_subdirs_api_method;
|
||||||
use proxmox_router::{Router, SubdirMap};
|
use proxmox_router::{Router, SubdirMap};
|
||||||
|
use proxmox_sys::sortable;
|
||||||
|
|
||||||
pub mod datastore;
|
pub mod datastore;
|
||||||
pub mod namespace;
|
pub mod namespace;
|
||||||
|
pub mod prune;
|
||||||
pub mod sync;
|
pub mod sync;
|
||||||
pub mod traffic_control;
|
pub mod traffic_control;
|
||||||
pub mod verify;
|
pub mod verify;
|
||||||
|
|
||||||
const SUBDIRS: SubdirMap = &[
|
#[sortable]
|
||||||
|
const SUBDIRS: SubdirMap = &sorted!([
|
||||||
("datastore", &datastore::ROUTER),
|
("datastore", &datastore::ROUTER),
|
||||||
|
("prune", &prune::ROUTER),
|
||||||
("sync", &sync::ROUTER),
|
("sync", &sync::ROUTER),
|
||||||
("traffic-control", &traffic_control::ROUTER),
|
("traffic-control", &traffic_control::ROUTER),
|
||||||
("verify", &verify::ROUTER),
|
("verify", &verify::ROUTER),
|
||||||
];
|
]);
|
||||||
|
|
||||||
pub const ROUTER: Router = Router::new()
|
pub const ROUTER: Router = Router::new()
|
||||||
.get(&list_subdirs_api_method!(SUBDIRS))
|
.get(&list_subdirs_api_method!(SUBDIRS))
|
||||||
|
@ -7,21 +7,12 @@ use proxmox_schema::*;
|
|||||||
|
|
||||||
use pbs_api_types::{
|
use pbs_api_types::{
|
||||||
Authid, BackupNamespace, NamespaceListItem, Operation, DATASTORE_SCHEMA, NS_MAX_DEPTH_SCHEMA,
|
Authid, BackupNamespace, NamespaceListItem, Operation, DATASTORE_SCHEMA, NS_MAX_DEPTH_SCHEMA,
|
||||||
PRIV_DATASTORE_AUDIT, PRIV_DATASTORE_BACKUP, PRIV_DATASTORE_MODIFY, PROXMOX_SAFE_ID_FORMAT,
|
PROXMOX_SAFE_ID_FORMAT,
|
||||||
};
|
};
|
||||||
|
|
||||||
use pbs_datastore::DataStore;
|
use pbs_datastore::DataStore;
|
||||||
|
|
||||||
// TODO: move somewhere we can reuse it from (datastore has its own copy atm.)
|
use crate::backup::{check_ns_modification_privs, check_ns_privs, NS_PRIVS_OK};
|
||||||
fn get_ns_privs(store: &str, ns: &BackupNamespace, auth_id: &Authid) -> Result<u64, Error> {
|
|
||||||
let user_info = CachedUserInfo::new()?;
|
|
||||||
|
|
||||||
Ok(if ns.is_root() {
|
|
||||||
user_info.lookup_privs(auth_id, &["datastore", store])
|
|
||||||
} else {
|
|
||||||
user_info.lookup_privs(auth_id, &["datastore", store, &ns.to_string()])
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[api(
|
#[api(
|
||||||
input: {
|
input: {
|
||||||
@ -59,9 +50,10 @@ pub fn create_namespace(
|
|||||||
let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
|
let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
|
||||||
let parent = parent.unwrap_or_default();
|
let parent = parent.unwrap_or_default();
|
||||||
|
|
||||||
if get_ns_privs(&store, &parent, &auth_id)? & PRIV_DATASTORE_MODIFY == 0 {
|
let mut ns = parent.clone();
|
||||||
proxmox_router::http_bail!(FORBIDDEN, "permission check failed");
|
ns.push(name.clone())?;
|
||||||
}
|
|
||||||
|
check_ns_modification_privs(&store, &ns, &auth_id)?;
|
||||||
|
|
||||||
let datastore = DataStore::lookup_datastore(&store, Some(Operation::Write))?;
|
let datastore = DataStore::lookup_datastore(&store, Some(Operation::Write))?;
|
||||||
|
|
||||||
@ -102,29 +94,34 @@ pub fn list_namespaces(
|
|||||||
) -> Result<Vec<NamespaceListItem>, Error> {
|
) -> Result<Vec<NamespaceListItem>, Error> {
|
||||||
let parent = parent.unwrap_or_default();
|
let parent = parent.unwrap_or_default();
|
||||||
let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
|
let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
|
||||||
const PRIVS_OK: u64 = PRIV_DATASTORE_MODIFY | PRIV_DATASTORE_BACKUP | PRIV_DATASTORE_AUDIT;
|
|
||||||
// first do a base check to avoid leaking if a NS exists or not
|
|
||||||
if get_ns_privs(&store, &parent, &auth_id)? & PRIVS_OK == 0 {
|
|
||||||
proxmox_router::http_bail!(FORBIDDEN, "permission check failed");
|
|
||||||
}
|
|
||||||
let user_info = CachedUserInfo::new()?;
|
let user_info = CachedUserInfo::new()?;
|
||||||
|
// get result up-front to avoid cloning NS, it's relatively cheap anyway (no IO normally)
|
||||||
|
let parent_access = check_ns_privs(&store, &parent, &auth_id, NS_PRIVS_OK);
|
||||||
|
|
||||||
let datastore = DataStore::lookup_datastore(&store, Some(Operation::Read))?;
|
let datastore = DataStore::lookup_datastore(&store, Some(Operation::Read))?;
|
||||||
|
|
||||||
|
let iter = match datastore.recursive_iter_backup_ns_ok(parent, max_depth) {
|
||||||
|
Ok(iter) => iter,
|
||||||
|
// parent NS doesn't exists and user has no privs on it, avoid info leakage.
|
||||||
|
Err(_) if parent_access.is_err() => http_bail!(FORBIDDEN, "permission check failed"),
|
||||||
|
Err(err) => return Err(err),
|
||||||
|
};
|
||||||
|
|
||||||
let ns_to_item =
|
let ns_to_item =
|
||||||
|ns: BackupNamespace| -> NamespaceListItem { NamespaceListItem { ns, comment: None } };
|
|ns: BackupNamespace| -> NamespaceListItem { NamespaceListItem { ns, comment: None } };
|
||||||
|
|
||||||
Ok(datastore
|
let namespace_list: Vec<NamespaceListItem> = iter
|
||||||
.recursive_iter_backup_ns_ok(parent, max_depth)?
|
|
||||||
.filter(|ns| {
|
.filter(|ns| {
|
||||||
if ns.is_root() {
|
let privs = user_info.lookup_privs(&auth_id, &ns.acl_path(&store));
|
||||||
return true; // already covered by access permission above
|
privs & NS_PRIVS_OK != 0
|
||||||
}
|
|
||||||
let privs = user_info.lookup_privs(&auth_id, &["datastore", &store, &ns.to_string()]);
|
|
||||||
privs & PRIVS_OK != 0
|
|
||||||
})
|
})
|
||||||
.map(ns_to_item)
|
.map(ns_to_item)
|
||||||
.collect())
|
.collect();
|
||||||
|
|
||||||
|
if namespace_list.is_empty() && parent_access.is_err() {
|
||||||
|
http_bail!(FORBIDDEN, "permission check failed"); // avoid leakage
|
||||||
|
}
|
||||||
|
Ok(namespace_list)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[api(
|
#[api(
|
||||||
@ -136,7 +133,7 @@ pub fn list_namespaces(
|
|||||||
},
|
},
|
||||||
"delete-groups": {
|
"delete-groups": {
|
||||||
type: bool,
|
type: bool,
|
||||||
description: "If set, all groups will be destroyed in the whole hierachy below and\
|
description: "If set, all groups will be destroyed in the whole hierarchy below and\
|
||||||
including `ns`. If not set, only empty namespaces will be pruned.",
|
including `ns`. If not set, only empty namespaces will be pruned.",
|
||||||
optional: true,
|
optional: true,
|
||||||
default: false,
|
default: false,
|
||||||
@ -155,15 +152,9 @@ pub fn delete_namespace(
|
|||||||
_info: &ApiMethod,
|
_info: &ApiMethod,
|
||||||
rpcenv: &mut dyn RpcEnvironment,
|
rpcenv: &mut dyn RpcEnvironment,
|
||||||
) -> Result<Value, Error> {
|
) -> Result<Value, Error> {
|
||||||
// we could allow it as easy purge-whole datastore, but lets be more restrictive for now
|
|
||||||
if ns.is_root() {
|
|
||||||
bail!("cannot delete root namespace!");
|
|
||||||
};
|
|
||||||
let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
|
let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
|
||||||
let parent = ns.parent(); // must have MODIFY permission on parent to allow deletion
|
|
||||||
if get_ns_privs(&store, &parent, &auth_id)? & PRIV_DATASTORE_MODIFY == 0 {
|
check_ns_modification_privs(&store, &ns, &auth_id)?;
|
||||||
http_bail!(FORBIDDEN, "permission check failed");
|
|
||||||
}
|
|
||||||
|
|
||||||
let datastore = DataStore::lookup_datastore(&store, Some(Operation::Write))?;
|
let datastore = DataStore::lookup_datastore(&store, Some(Operation::Write))?;
|
||||||
|
|
||||||
|
138
src/api2/admin/prune.rs
Normal file
138
src/api2/admin/prune.rs
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
//! Datastore Prune Job Management
|
||||||
|
|
||||||
|
use anyhow::{format_err, Error};
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
use proxmox_router::{
|
||||||
|
list_subdirs_api_method, ApiMethod, Permission, Router, RpcEnvironment, SubdirMap,
|
||||||
|
};
|
||||||
|
use proxmox_schema::api;
|
||||||
|
use proxmox_sys::sortable;
|
||||||
|
|
||||||
|
use pbs_api_types::{
|
||||||
|
Authid, PruneJobConfig, PruneJobStatus, DATASTORE_SCHEMA, JOB_ID_SCHEMA, PRIV_DATASTORE_AUDIT,
|
||||||
|
PRIV_DATASTORE_MODIFY,
|
||||||
|
};
|
||||||
|
use pbs_config::prune;
|
||||||
|
use pbs_config::CachedUserInfo;
|
||||||
|
|
||||||
|
use crate::server::{
|
||||||
|
do_prune_job,
|
||||||
|
jobstate::{compute_schedule_status, Job, JobState},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[api(
|
||||||
|
input: {
|
||||||
|
properties: {
|
||||||
|
store: {
|
||||||
|
schema: DATASTORE_SCHEMA,
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
returns: {
|
||||||
|
description: "List configured jobs and their status (filtered by access)",
|
||||||
|
type: Array,
|
||||||
|
items: { type: PruneJobStatus },
|
||||||
|
},
|
||||||
|
access: {
|
||||||
|
permission: &Permission::Anybody,
|
||||||
|
description: "Requires Datastore.Audit or Datastore.Modify on datastore.",
|
||||||
|
},
|
||||||
|
)]
|
||||||
|
/// List all prune jobs
|
||||||
|
pub fn list_prune_jobs(
|
||||||
|
store: Option<String>,
|
||||||
|
_param: Value,
|
||||||
|
rpcenv: &mut dyn RpcEnvironment,
|
||||||
|
) -> Result<Vec<PruneJobStatus>, Error> {
|
||||||
|
let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
|
||||||
|
let user_info = CachedUserInfo::new()?;
|
||||||
|
|
||||||
|
let required_privs = PRIV_DATASTORE_AUDIT | PRIV_DATASTORE_MODIFY;
|
||||||
|
|
||||||
|
let (config, digest) = prune::config()?;
|
||||||
|
|
||||||
|
let job_config_iter =
|
||||||
|
config
|
||||||
|
.convert_to_typed_array("prune")?
|
||||||
|
.into_iter()
|
||||||
|
.filter(|job: &PruneJobConfig| {
|
||||||
|
let privs = user_info.lookup_privs(&auth_id, &job.acl_path());
|
||||||
|
if privs & required_privs == 0 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(store) = &store {
|
||||||
|
&job.store == store
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut list = Vec::new();
|
||||||
|
|
||||||
|
for job in job_config_iter {
|
||||||
|
let last_state = JobState::load("prunejob", &job.id)
|
||||||
|
.map_err(|err| format_err!("could not open statefile for {}: {}", &job.id, err))?;
|
||||||
|
|
||||||
|
let mut status = compute_schedule_status(&last_state, Some(&job.schedule))?;
|
||||||
|
if job.disable {
|
||||||
|
status.next_run = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
list.push(PruneJobStatus {
|
||||||
|
config: job,
|
||||||
|
status,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
rpcenv["digest"] = hex::encode(&digest).into();
|
||||||
|
|
||||||
|
Ok(list)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[api(
|
||||||
|
input: {
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
schema: JOB_ID_SCHEMA,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
access: {
|
||||||
|
permission: &Permission::Anybody,
|
||||||
|
description: "Requires Datastore.Modify on job's datastore.",
|
||||||
|
},
|
||||||
|
)]
|
||||||
|
/// Runs a prune job manually.
|
||||||
|
pub fn run_prune_job(
|
||||||
|
id: String,
|
||||||
|
_info: &ApiMethod,
|
||||||
|
rpcenv: &mut dyn RpcEnvironment,
|
||||||
|
) -> Result<String, Error> {
|
||||||
|
let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
|
||||||
|
let user_info = CachedUserInfo::new()?;
|
||||||
|
|
||||||
|
let (config, _digest) = prune::config()?;
|
||||||
|
let prune_job: PruneJobConfig = config.lookup("prune", &id)?;
|
||||||
|
|
||||||
|
user_info.check_privs(&auth_id, &prune_job.acl_path(), PRIV_DATASTORE_MODIFY, true)?;
|
||||||
|
|
||||||
|
let job = Job::new("prunejob", &id)?;
|
||||||
|
|
||||||
|
let upid_str = do_prune_job(job, prune_job.options, prune_job.store, &auth_id, None)?;
|
||||||
|
|
||||||
|
Ok(upid_str)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[sortable]
|
||||||
|
const PRUNE_INFO_SUBDIRS: SubdirMap = &[("run", &Router::new().post(&API_METHOD_RUN_PRUNE_JOB))];
|
||||||
|
|
||||||
|
const PRUNE_INFO_ROUTER: Router = Router::new()
|
||||||
|
.get(&list_subdirs_api_method!(PRUNE_INFO_SUBDIRS))
|
||||||
|
.subdirs(PRUNE_INFO_SUBDIRS);
|
||||||
|
|
||||||
|
pub const ROUTER: Router = Router::new()
|
||||||
|
.get(&API_METHOD_LIST_PRUNE_JOBS)
|
||||||
|
.match_all("id", &PRUNE_INFO_ROUTER);
|
@ -58,7 +58,7 @@ pub fn list_verification_jobs(
|
|||||||
.convert_to_typed_array("verification")?
|
.convert_to_typed_array("verification")?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|job: &VerificationJobConfig| {
|
.filter(|job: &VerificationJobConfig| {
|
||||||
let privs = user_info.lookup_privs(&auth_id, &["datastore", &job.store]);
|
let privs = user_info.lookup_privs(&auth_id, &job.acl_path());
|
||||||
if privs & required_privs == 0 {
|
if privs & required_privs == 0 {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -116,7 +116,7 @@ pub fn run_verification_job(
|
|||||||
|
|
||||||
user_info.check_privs(
|
user_info.check_privs(
|
||||||
&auth_id,
|
&auth_id,
|
||||||
&["datastore", &verification_job.store],
|
&verification_job.acl_path(),
|
||||||
PRIV_DATASTORE_VERIFY,
|
PRIV_DATASTORE_VERIFY,
|
||||||
true,
|
true,
|
||||||
)?;
|
)?;
|
||||||
|
@ -9,7 +9,7 @@ use hyper::{Body, Request, Response, StatusCode};
|
|||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde_json::{json, Value};
|
use serde_json::{json, Value};
|
||||||
|
|
||||||
use proxmox_router::list_subdirs_api_method;
|
use proxmox_router::{http_err, list_subdirs_api_method};
|
||||||
use proxmox_router::{
|
use proxmox_router::{
|
||||||
ApiHandler, ApiMethod, ApiResponseFuture, Permission, Router, RpcEnvironment, SubdirMap,
|
ApiHandler, ApiMethod, ApiResponseFuture, Permission, Router, RpcEnvironment, SubdirMap,
|
||||||
};
|
};
|
||||||
@ -85,14 +85,14 @@ fn upgrade_to_backup_protocol(
|
|||||||
|
|
||||||
let user_info = CachedUserInfo::new()?;
|
let user_info = CachedUserInfo::new()?;
|
||||||
|
|
||||||
let privs = if backup_ns.is_root() {
|
user_info
|
||||||
user_info.lookup_privs(&auth_id, &["datastore", &store])
|
.check_privs(
|
||||||
} else {
|
&auth_id,
|
||||||
user_info.lookup_privs(&auth_id, &["datastore", &store, &backup_ns.to_string()])
|
&backup_ns.acl_path(&store),
|
||||||
};
|
PRIV_DATASTORE_BACKUP,
|
||||||
if privs & PRIV_DATASTORE_BACKUP == 0 {
|
false,
|
||||||
proxmox_router::http_bail!(FORBIDDEN, "permission check failed");
|
)
|
||||||
}
|
.map_err(|err| http_err!(FORBIDDEN, "{err}"))?;
|
||||||
|
|
||||||
let datastore = DataStore::lookup_datastore(&store, Some(Operation::Write))?;
|
let datastore = DataStore::lookup_datastore(&store, Some(Operation::Write))?;
|
||||||
|
|
||||||
@ -117,6 +117,7 @@ fn upgrade_to_backup_protocol(
|
|||||||
proxmox_router::http_bail!(NOT_FOUND, "namespace not found");
|
proxmox_router::http_bail!(NOT_FOUND, "namespace not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FIXME: include namespace here?
|
||||||
let worker_id = format!("{}:{}/{}", store, backup_dir_arg.ty(), backup_dir_arg.id());
|
let worker_id = format!("{}:{}/{}", store, backup_dir_arg.ty(), backup_dir_arg.id());
|
||||||
|
|
||||||
let env_type = rpcenv.env_type();
|
let env_type = rpcenv.env_type();
|
||||||
|
@ -121,7 +121,7 @@ pub fn update_webauthn_config(
|
|||||||
} else {
|
} else {
|
||||||
let rp = webauthn
|
let rp = webauthn
|
||||||
.rp
|
.rp
|
||||||
.ok_or_else(|| format_err!("missing proeprty: 'rp'"))?;
|
.ok_or_else(|| format_err!("missing property: 'rp'"))?;
|
||||||
let origin = webauthn.origin;
|
let origin = webauthn.origin;
|
||||||
let id = webauthn
|
let id = webauthn
|
||||||
.id
|
.id
|
||||||
|
@ -251,22 +251,22 @@ pub fn update_datastore(
|
|||||||
data.prune_schedule = None;
|
data.prune_schedule = None;
|
||||||
}
|
}
|
||||||
DeletableProperty::keep_last => {
|
DeletableProperty::keep_last => {
|
||||||
data.keep_last = None;
|
data.keep.keep_last = None;
|
||||||
}
|
}
|
||||||
DeletableProperty::keep_hourly => {
|
DeletableProperty::keep_hourly => {
|
||||||
data.keep_hourly = None;
|
data.keep.keep_hourly = None;
|
||||||
}
|
}
|
||||||
DeletableProperty::keep_daily => {
|
DeletableProperty::keep_daily => {
|
||||||
data.keep_daily = None;
|
data.keep.keep_daily = None;
|
||||||
}
|
}
|
||||||
DeletableProperty::keep_weekly => {
|
DeletableProperty::keep_weekly => {
|
||||||
data.keep_weekly = None;
|
data.keep.keep_weekly = None;
|
||||||
}
|
}
|
||||||
DeletableProperty::keep_monthly => {
|
DeletableProperty::keep_monthly => {
|
||||||
data.keep_monthly = None;
|
data.keep.keep_monthly = None;
|
||||||
}
|
}
|
||||||
DeletableProperty::keep_yearly => {
|
DeletableProperty::keep_yearly => {
|
||||||
data.keep_yearly = None;
|
data.keep.keep_yearly = None;
|
||||||
}
|
}
|
||||||
DeletableProperty::verify_new => {
|
DeletableProperty::verify_new => {
|
||||||
data.verify_new = None;
|
data.verify_new = None;
|
||||||
@ -302,29 +302,26 @@ pub fn update_datastore(
|
|||||||
data.gc_schedule = update.gc_schedule;
|
data.gc_schedule = update.gc_schedule;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut prune_schedule_changed = false;
|
macro_rules! prune_disabled {
|
||||||
if update.prune_schedule.is_some() {
|
($(($param:literal, $($member:tt)+)),+) => {
|
||||||
prune_schedule_changed = data.prune_schedule != update.prune_schedule;
|
$(
|
||||||
data.prune_schedule = update.prune_schedule;
|
if update.$($member)+.is_some() {
|
||||||
|
param_bail!(
|
||||||
|
$param,
|
||||||
|
"datastore prune settings have been replaced by prune jobs",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)+
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
prune_disabled! {
|
||||||
if update.keep_last.is_some() {
|
("keep-last", keep.keep_last),
|
||||||
data.keep_last = update.keep_last;
|
("keep-hourly", keep.keep_hourly),
|
||||||
}
|
("keep-daily", keep.keep_daily),
|
||||||
if update.keep_hourly.is_some() {
|
("keep-weekly", keep.keep_weekly),
|
||||||
data.keep_hourly = update.keep_hourly;
|
("keep-monthly", keep.keep_monthly),
|
||||||
}
|
("keep-yearly", keep.keep_yearly),
|
||||||
if update.keep_daily.is_some() {
|
("prune-schedule", prune_schedule)
|
||||||
data.keep_daily = update.keep_daily;
|
|
||||||
}
|
|
||||||
if update.keep_weekly.is_some() {
|
|
||||||
data.keep_weekly = update.keep_weekly;
|
|
||||||
}
|
|
||||||
if update.keep_monthly.is_some() {
|
|
||||||
data.keep_monthly = update.keep_monthly;
|
|
||||||
}
|
|
||||||
if update.keep_yearly.is_some() {
|
|
||||||
data.keep_yearly = update.keep_yearly;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(notify_str) = update.notify {
|
if let Some(notify_str) = update.notify {
|
||||||
@ -367,10 +364,6 @@ pub fn update_datastore(
|
|||||||
jobstate::update_job_last_run_time("garbage_collection", &name)?;
|
jobstate::update_job_last_run_time("garbage_collection", &name)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if prune_schedule_changed {
|
|
||||||
jobstate::update_job_last_run_time("prune", &name)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
use proxmox_router::list_subdirs_api_method;
|
use proxmox_router::list_subdirs_api_method;
|
||||||
use proxmox_router::{Router, SubdirMap};
|
use proxmox_router::{Router, SubdirMap};
|
||||||
|
use proxmox_sys::sortable;
|
||||||
|
|
||||||
pub mod access;
|
pub mod access;
|
||||||
pub mod acme;
|
pub mod acme;
|
||||||
@ -9,6 +10,7 @@ pub mod changer;
|
|||||||
pub mod datastore;
|
pub mod datastore;
|
||||||
pub mod drive;
|
pub mod drive;
|
||||||
pub mod media_pool;
|
pub mod media_pool;
|
||||||
|
pub mod prune;
|
||||||
pub mod remote;
|
pub mod remote;
|
||||||
pub mod sync;
|
pub mod sync;
|
||||||
pub mod tape_backup_job;
|
pub mod tape_backup_job;
|
||||||
@ -16,20 +18,22 @@ pub mod tape_encryption_keys;
|
|||||||
pub mod traffic_control;
|
pub mod traffic_control;
|
||||||
pub mod verify;
|
pub mod verify;
|
||||||
|
|
||||||
const SUBDIRS: SubdirMap = &[
|
#[sortable]
|
||||||
|
const SUBDIRS: SubdirMap = &sorted!([
|
||||||
("access", &access::ROUTER),
|
("access", &access::ROUTER),
|
||||||
("acme", &acme::ROUTER),
|
("acme", &acme::ROUTER),
|
||||||
("changer", &changer::ROUTER),
|
("changer", &changer::ROUTER),
|
||||||
("datastore", &datastore::ROUTER),
|
("datastore", &datastore::ROUTER),
|
||||||
("drive", &drive::ROUTER),
|
("drive", &drive::ROUTER),
|
||||||
("media-pool", &media_pool::ROUTER),
|
("media-pool", &media_pool::ROUTER),
|
||||||
|
("prune", &prune::ROUTER),
|
||||||
("remote", &remote::ROUTER),
|
("remote", &remote::ROUTER),
|
||||||
("sync", &sync::ROUTER),
|
("sync", &sync::ROUTER),
|
||||||
("tape-backup-job", &tape_backup_job::ROUTER),
|
("tape-backup-job", &tape_backup_job::ROUTER),
|
||||||
("tape-encryption-keys", &tape_encryption_keys::ROUTER),
|
("tape-encryption-keys", &tape_encryption_keys::ROUTER),
|
||||||
("traffic-control", &traffic_control::ROUTER),
|
("traffic-control", &traffic_control::ROUTER),
|
||||||
("verify", &verify::ROUTER),
|
("verify", &verify::ROUTER),
|
||||||
];
|
]);
|
||||||
|
|
||||||
pub const ROUTER: Router = Router::new()
|
pub const ROUTER: Router = Router::new()
|
||||||
.get(&list_subdirs_api_method!(SUBDIRS))
|
.get(&list_subdirs_api_method!(SUBDIRS))
|
||||||
|
378
src/api2/config/prune.rs
Normal file
378
src/api2/config/prune.rs
Normal file
@ -0,0 +1,378 @@
|
|||||||
|
use anyhow::Error;
|
||||||
|
use hex::FromHex;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
use proxmox_router::{http_bail, Permission, Router, RpcEnvironment};
|
||||||
|
use proxmox_schema::{api, param_bail};
|
||||||
|
|
||||||
|
use pbs_api_types::{
|
||||||
|
Authid, PruneJobConfig, PruneJobConfigUpdater, JOB_ID_SCHEMA, PRIV_DATASTORE_AUDIT,
|
||||||
|
PRIV_DATASTORE_MODIFY, PROXMOX_CONFIG_DIGEST_SCHEMA,
|
||||||
|
};
|
||||||
|
use pbs_config::prune;
|
||||||
|
|
||||||
|
use pbs_config::CachedUserInfo;
|
||||||
|
|
||||||
|
#[api(
|
||||||
|
input: {
|
||||||
|
properties: {},
|
||||||
|
},
|
||||||
|
returns: {
|
||||||
|
description: "List configured prune schedules.",
|
||||||
|
type: Array,
|
||||||
|
items: { type: PruneJobConfig },
|
||||||
|
},
|
||||||
|
access: {
|
||||||
|
permission: &Permission::Anybody,
|
||||||
|
// FIXME: Audit on namespaces
|
||||||
|
description: "Requires Datastore.Audit.",
|
||||||
|
},
|
||||||
|
)]
|
||||||
|
/// List all scheduled prune jobs.
|
||||||
|
pub fn list_prune_jobs(
|
||||||
|
_param: Value,
|
||||||
|
rpcenv: &mut dyn RpcEnvironment,
|
||||||
|
) -> Result<Vec<PruneJobConfig>, Error> {
|
||||||
|
let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
|
||||||
|
let user_info = CachedUserInfo::new()?;
|
||||||
|
|
||||||
|
let required_privs = PRIV_DATASTORE_AUDIT | PRIV_DATASTORE_MODIFY;
|
||||||
|
|
||||||
|
let (config, digest) = prune::config()?;
|
||||||
|
|
||||||
|
let list = config.convert_to_typed_array("prune")?;
|
||||||
|
|
||||||
|
let list = list
|
||||||
|
.into_iter()
|
||||||
|
.filter(|job: &PruneJobConfig| {
|
||||||
|
let privs = user_info.lookup_privs(&auth_id, &job.acl_path());
|
||||||
|
privs & required_privs != 00
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
rpcenv["digest"] = hex::encode(&digest).into();
|
||||||
|
|
||||||
|
Ok(list)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[api(
|
||||||
|
protected: true,
|
||||||
|
input: {
|
||||||
|
properties: {
|
||||||
|
config: {
|
||||||
|
type: PruneJobConfig,
|
||||||
|
flatten: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
access: {
|
||||||
|
permission: &Permission::Anybody,
|
||||||
|
description: "Requires Datastore.Modify on job's datastore.",
|
||||||
|
},
|
||||||
|
)]
|
||||||
|
/// Create a new prune job.
|
||||||
|
pub fn create_prune_job(
|
||||||
|
config: PruneJobConfig,
|
||||||
|
rpcenv: &mut dyn RpcEnvironment,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
|
||||||
|
let user_info = CachedUserInfo::new()?;
|
||||||
|
|
||||||
|
user_info.check_privs(&auth_id, &config.acl_path(), PRIV_DATASTORE_MODIFY, true)?;
|
||||||
|
|
||||||
|
let _lock = prune::lock_config()?;
|
||||||
|
|
||||||
|
let (mut section_config, _digest) = prune::config()?;
|
||||||
|
|
||||||
|
if section_config.sections.get(&config.id).is_some() {
|
||||||
|
param_bail!("id", "job '{}' already exists.", config.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
section_config.set_data(&config.id, "prune", &config)?;
|
||||||
|
|
||||||
|
prune::save_config(§ion_config)?;
|
||||||
|
|
||||||
|
crate::server::jobstate::create_state_file("prunejob", &config.id)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[api(
|
||||||
|
input: {
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
schema: JOB_ID_SCHEMA,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
returns: { type: PruneJobConfig },
|
||||||
|
access: {
|
||||||
|
permission: &Permission::Anybody,
|
||||||
|
description: "Requires Datastore.Audit or Datastore.Verify on job's datastore.",
|
||||||
|
},
|
||||||
|
)]
|
||||||
|
/// Read a prune job configuration.
|
||||||
|
pub fn read_prune_job(
|
||||||
|
id: String,
|
||||||
|
rpcenv: &mut dyn RpcEnvironment,
|
||||||
|
) -> Result<PruneJobConfig, Error> {
|
||||||
|
let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
|
||||||
|
let user_info = CachedUserInfo::new()?;
|
||||||
|
|
||||||
|
let (config, digest) = prune::config()?;
|
||||||
|
|
||||||
|
let prune_job: PruneJobConfig = config.lookup("prune", &id)?;
|
||||||
|
|
||||||
|
let required_privs = PRIV_DATASTORE_AUDIT;
|
||||||
|
user_info.check_privs(&auth_id, &prune_job.acl_path(), required_privs, true)?;
|
||||||
|
|
||||||
|
rpcenv["digest"] = hex::encode(&digest).into();
|
||||||
|
|
||||||
|
Ok(prune_job)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[api]
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
/// Deletable property name
|
||||||
|
pub enum DeletableProperty {
|
||||||
|
/// Delete the comment.
|
||||||
|
Comment,
|
||||||
|
/// Unset the disable flag.
|
||||||
|
Disable,
|
||||||
|
/// Reset the namespace to the root namespace.
|
||||||
|
Ns,
|
||||||
|
/// Reset the maximum depth to full recursion.
|
||||||
|
MaxDepth,
|
||||||
|
/// Delete number of last backups to keep.
|
||||||
|
KeepLast,
|
||||||
|
/// Delete number of hourly backups to keep.
|
||||||
|
KeepHourly,
|
||||||
|
/// Delete number of daily backups to keep.
|
||||||
|
KeepDaily,
|
||||||
|
/// Delete number of weekly backups to keep.
|
||||||
|
KeepWeekly,
|
||||||
|
/// Delete number of monthly backups to keep.
|
||||||
|
KeepMonthly,
|
||||||
|
/// Delete number of yearly backups to keep.
|
||||||
|
KeepYearly,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[api(
|
||||||
|
protected: true,
|
||||||
|
input: {
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
schema: JOB_ID_SCHEMA,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
type: PruneJobConfigUpdater,
|
||||||
|
flatten: true,
|
||||||
|
},
|
||||||
|
delete: {
|
||||||
|
description: "List of properties to delete.",
|
||||||
|
type: Array,
|
||||||
|
optional: true,
|
||||||
|
items: {
|
||||||
|
type: DeletableProperty,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
digest: {
|
||||||
|
optional: true,
|
||||||
|
schema: PROXMOX_CONFIG_DIGEST_SCHEMA,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
access: {
|
||||||
|
permission: &Permission::Anybody,
|
||||||
|
description: "Requires Datastore.Modify on job's datastore.",
|
||||||
|
},
|
||||||
|
)]
|
||||||
|
/// Update prune job config.
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub fn update_prune_job(
|
||||||
|
id: String,
|
||||||
|
update: PruneJobConfigUpdater,
|
||||||
|
delete: Option<Vec<DeletableProperty>>,
|
||||||
|
digest: Option<String>,
|
||||||
|
rpcenv: &mut dyn RpcEnvironment,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
|
||||||
|
let user_info = CachedUserInfo::new()?;
|
||||||
|
|
||||||
|
let _lock = prune::lock_config()?;
|
||||||
|
|
||||||
|
// pass/compare digest
|
||||||
|
let (mut config, expected_digest) = prune::config()?;
|
||||||
|
|
||||||
|
if let Some(ref digest) = digest {
|
||||||
|
let digest = <[u8; 32]>::from_hex(digest)?;
|
||||||
|
crate::tools::detect_modified_configuration_file(&digest, &expected_digest)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut data: PruneJobConfig = config.lookup("prune", &id)?;
|
||||||
|
|
||||||
|
user_info.check_privs(&auth_id, &data.acl_path(), PRIV_DATASTORE_MODIFY, true)?;
|
||||||
|
|
||||||
|
if let Some(delete) = delete {
|
||||||
|
for delete_prop in delete {
|
||||||
|
match delete_prop {
|
||||||
|
DeletableProperty::Comment => {
|
||||||
|
data.comment = None;
|
||||||
|
}
|
||||||
|
DeletableProperty::Disable => {
|
||||||
|
data.disable = false;
|
||||||
|
}
|
||||||
|
DeletableProperty::Ns => {
|
||||||
|
data.options.ns = None;
|
||||||
|
}
|
||||||
|
DeletableProperty::MaxDepth => {
|
||||||
|
data.options.max_depth = None;
|
||||||
|
}
|
||||||
|
DeletableProperty::KeepLast => {
|
||||||
|
data.options.keep.keep_last = None;
|
||||||
|
}
|
||||||
|
DeletableProperty::KeepHourly => {
|
||||||
|
data.options.keep.keep_hourly = None;
|
||||||
|
}
|
||||||
|
DeletableProperty::KeepDaily => {
|
||||||
|
data.options.keep.keep_daily = None;
|
||||||
|
}
|
||||||
|
DeletableProperty::KeepWeekly => {
|
||||||
|
data.options.keep.keep_weekly = None;
|
||||||
|
}
|
||||||
|
DeletableProperty::KeepMonthly => {
|
||||||
|
data.options.keep.keep_monthly = None;
|
||||||
|
}
|
||||||
|
DeletableProperty::KeepYearly => {
|
||||||
|
data.options.keep.keep_yearly = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut recheck_privs = false;
|
||||||
|
if let Some(store) = update.store {
|
||||||
|
// check new store with possibly new ns:
|
||||||
|
recheck_privs = true;
|
||||||
|
data.store = store;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(ns) = update.options.ns {
|
||||||
|
recheck_privs = true;
|
||||||
|
data.options.ns = if ns.is_root() { None } else { Some(ns) };
|
||||||
|
}
|
||||||
|
|
||||||
|
if recheck_privs {
|
||||||
|
user_info.check_privs(&auth_id, &data.acl_path(), PRIV_DATASTORE_MODIFY, true)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut schedule_changed = false;
|
||||||
|
if let Some(schedule) = update.schedule {
|
||||||
|
schedule_changed = data.schedule != schedule;
|
||||||
|
data.schedule = schedule;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(max_depth) = update.options.max_depth {
|
||||||
|
if max_depth <= pbs_api_types::MAX_NAMESPACE_DEPTH {
|
||||||
|
data.options.max_depth = Some(max_depth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(value) = update.disable {
|
||||||
|
data.disable = value;
|
||||||
|
}
|
||||||
|
if let Some(value) = update.comment {
|
||||||
|
data.comment = Some(value);
|
||||||
|
}
|
||||||
|
if let Some(value) = update.options.keep.keep_last {
|
||||||
|
data.options.keep.keep_last = Some(value);
|
||||||
|
}
|
||||||
|
if let Some(value) = update.options.keep.keep_hourly {
|
||||||
|
data.options.keep.keep_hourly = Some(value);
|
||||||
|
}
|
||||||
|
if let Some(value) = update.options.keep.keep_daily {
|
||||||
|
data.options.keep.keep_daily = Some(value);
|
||||||
|
}
|
||||||
|
if let Some(value) = update.options.keep.keep_weekly {
|
||||||
|
data.options.keep.keep_weekly = Some(value);
|
||||||
|
}
|
||||||
|
if let Some(value) = update.options.keep.keep_monthly {
|
||||||
|
data.options.keep.keep_monthly = Some(value);
|
||||||
|
}
|
||||||
|
if let Some(value) = update.options.keep.keep_yearly {
|
||||||
|
data.options.keep.keep_yearly = Some(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
config.set_data(&id, "prune", &data)?;
|
||||||
|
|
||||||
|
prune::save_config(&config)?;
|
||||||
|
|
||||||
|
if schedule_changed {
|
||||||
|
crate::server::jobstate::update_job_last_run_time("prunejob", &id)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[api(
|
||||||
|
protected: true,
|
||||||
|
input: {
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
schema: JOB_ID_SCHEMA,
|
||||||
|
},
|
||||||
|
digest: {
|
||||||
|
optional: true,
|
||||||
|
schema: PROXMOX_CONFIG_DIGEST_SCHEMA,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
access: {
|
||||||
|
permission: &Permission::Anybody,
|
||||||
|
description: "Requires Datastore.Verify on job's datastore.",
|
||||||
|
},
|
||||||
|
)]
|
||||||
|
/// Remove a prune job configuration
|
||||||
|
pub fn delete_prune_job(
|
||||||
|
id: String,
|
||||||
|
digest: Option<String>,
|
||||||
|
rpcenv: &mut dyn RpcEnvironment,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
|
||||||
|
let user_info = CachedUserInfo::new()?;
|
||||||
|
|
||||||
|
let _lock = prune::lock_config()?;
|
||||||
|
|
||||||
|
let (mut config, expected_digest) = prune::config()?;
|
||||||
|
|
||||||
|
let job: PruneJobConfig = config.lookup("prune", &id)?;
|
||||||
|
|
||||||
|
user_info.check_privs(&auth_id, &job.acl_path(), PRIV_DATASTORE_MODIFY, true)?;
|
||||||
|
|
||||||
|
if let Some(ref digest) = digest {
|
||||||
|
let digest = <[u8; 32]>::from_hex(digest)?;
|
||||||
|
crate::tools::detect_modified_configuration_file(&digest, &expected_digest)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.sections.remove(&id).is_none() {
|
||||||
|
http_bail!(NOT_FOUND, "job '{}' does not exist.", id);
|
||||||
|
}
|
||||||
|
|
||||||
|
prune::save_config(&config)?;
|
||||||
|
|
||||||
|
crate::server::jobstate::remove_state_file("prunejob", &id)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
const ITEM_ROUTER: Router = Router::new()
|
||||||
|
.get(&API_METHOD_READ_PRUNE_JOB)
|
||||||
|
.put(&API_METHOD_UPDATE_PRUNE_JOB)
|
||||||
|
.delete(&API_METHOD_DELETE_PRUNE_JOB);
|
||||||
|
|
||||||
|
pub const ROUTER: Router = Router::new()
|
||||||
|
.get(&API_METHOD_LIST_PRUNE_JOBS)
|
||||||
|
.post(&API_METHOD_CREATE_PRUNE_JOB)
|
||||||
|
.match_all("id", &ITEM_ROUTER);
|
@ -503,13 +503,13 @@ pub async fn scan_remote_groups(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[sortable]
|
#[sortable]
|
||||||
const DATASTORE_SCAN_SUBDIRS: SubdirMap = &[
|
const DATASTORE_SCAN_SUBDIRS: SubdirMap = &sorted!([
|
||||||
("groups", &Router::new().get(&API_METHOD_SCAN_REMOTE_GROUPS)),
|
("groups", &Router::new().get(&API_METHOD_SCAN_REMOTE_GROUPS)),
|
||||||
(
|
(
|
||||||
"namespaces",
|
"namespaces",
|
||||||
&Router::new().get(&API_METHOD_SCAN_REMOTE_NAMESPACES),
|
&Router::new().get(&API_METHOD_SCAN_REMOTE_NAMESPACES),
|
||||||
),
|
),
|
||||||
];
|
]);
|
||||||
|
|
||||||
const DATASTORE_SCAN_ROUTER: Router = Router::new()
|
const DATASTORE_SCAN_ROUTER: Router = Router::new()
|
||||||
.get(&list_subdirs_api_method!(DATASTORE_SCAN_SUBDIRS))
|
.get(&list_subdirs_api_method!(DATASTORE_SCAN_SUBDIRS))
|
||||||
|
@ -20,18 +20,11 @@ pub fn check_sync_job_read_access(
|
|||||||
auth_id: &Authid,
|
auth_id: &Authid,
|
||||||
job: &SyncJobConfig,
|
job: &SyncJobConfig,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
let datastore_privs = user_info.lookup_privs(auth_id, &["datastore", &job.store]);
|
let ns_anchor_privs = user_info.lookup_privs(auth_id, &job.acl_path());
|
||||||
if datastore_privs & PRIV_DATASTORE_AUDIT == 0 {
|
if ns_anchor_privs & PRIV_DATASTORE_AUDIT == 0 {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(ref ns) = job.ns {
|
|
||||||
let ns_privs = user_info.lookup_privs(auth_id, &["datastore", &job.store, &ns.to_string()]);
|
|
||||||
if ns_privs & PRIV_DATASTORE_AUDIT == 0 {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let remote_privs = user_info.lookup_privs(auth_id, &["remote", &job.remote]);
|
let remote_privs = user_info.lookup_privs(auth_id, &["remote", &job.remote]);
|
||||||
remote_privs & PRIV_REMOTE_AUDIT != 0
|
remote_privs & PRIV_REMOTE_AUDIT != 0
|
||||||
}
|
}
|
||||||
@ -45,20 +38,13 @@ pub fn check_sync_job_modify_access(
|
|||||||
auth_id: &Authid,
|
auth_id: &Authid,
|
||||||
job: &SyncJobConfig,
|
job: &SyncJobConfig,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
let datastore_privs = user_info.lookup_privs(auth_id, &["datastore", &job.store]);
|
let ns_anchor_privs = user_info.lookup_privs(auth_id, &job.acl_path());
|
||||||
if datastore_privs & PRIV_DATASTORE_BACKUP == 0 {
|
if ns_anchor_privs & PRIV_DATASTORE_BACKUP == 0 {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(ref ns) = job.ns {
|
|
||||||
let ns_privs = user_info.lookup_privs(auth_id, &["datastore", &job.store, &ns.to_string()]);
|
|
||||||
if ns_privs & PRIV_DATASTORE_BACKUP == 0 {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(true) = job.remove_vanished {
|
if let Some(true) = job.remove_vanished {
|
||||||
if datastore_privs & PRIV_DATASTORE_PRUNE == 0 {
|
if ns_anchor_privs & PRIV_DATASTORE_PRUNE == 0 {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -73,7 +59,7 @@ pub fn check_sync_job_modify_access(
|
|||||||
};
|
};
|
||||||
|
|
||||||
// same permission as changing ownership after syncing
|
// same permission as changing ownership after syncing
|
||||||
if !correct_owner && datastore_privs & PRIV_DATASTORE_MODIFY == 0 {
|
if !correct_owner && ns_anchor_privs & PRIV_DATASTORE_MODIFY == 0 {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,7 +45,7 @@ pub fn list_verification_jobs(
|
|||||||
let list = list
|
let list = list
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|job: &VerificationJobConfig| {
|
.filter(|job: &VerificationJobConfig| {
|
||||||
let privs = user_info.lookup_privs(&auth_id, &["datastore", &job.store]);
|
let privs = user_info.lookup_privs(&auth_id, &job.acl_path());
|
||||||
|
|
||||||
privs & required_privs != 00
|
privs & required_privs != 00
|
||||||
})
|
})
|
||||||
@ -79,12 +79,7 @@ pub fn create_verification_job(
|
|||||||
let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
|
let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
|
||||||
let user_info = CachedUserInfo::new()?;
|
let user_info = CachedUserInfo::new()?;
|
||||||
|
|
||||||
user_info.check_privs(
|
user_info.check_privs(&auth_id, &config.acl_path(), PRIV_DATASTORE_VERIFY, false)?;
|
||||||
&auth_id,
|
|
||||||
&["datastore", &config.store],
|
|
||||||
PRIV_DATASTORE_VERIFY,
|
|
||||||
false,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let _lock = verify::lock_config()?;
|
let _lock = verify::lock_config()?;
|
||||||
|
|
||||||
@ -130,12 +125,7 @@ pub fn read_verification_job(
|
|||||||
let verification_job: VerificationJobConfig = config.lookup("verification", &id)?;
|
let verification_job: VerificationJobConfig = config.lookup("verification", &id)?;
|
||||||
|
|
||||||
let required_privs = PRIV_DATASTORE_AUDIT | PRIV_DATASTORE_VERIFY;
|
let required_privs = PRIV_DATASTORE_AUDIT | PRIV_DATASTORE_VERIFY;
|
||||||
user_info.check_privs(
|
user_info.check_privs(&auth_id, &verification_job.acl_path(), required_privs, true)?;
|
||||||
&auth_id,
|
|
||||||
&["datastore", &verification_job.store],
|
|
||||||
required_privs,
|
|
||||||
true,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
rpcenv["digest"] = hex::encode(&digest).into();
|
rpcenv["digest"] = hex::encode(&digest).into();
|
||||||
|
|
||||||
@ -215,13 +205,8 @@ pub fn update_verification_job(
|
|||||||
|
|
||||||
let mut data: VerificationJobConfig = config.lookup("verification", &id)?;
|
let mut data: VerificationJobConfig = config.lookup("verification", &id)?;
|
||||||
|
|
||||||
// check existing store
|
// check existing store and NS
|
||||||
user_info.check_privs(
|
user_info.check_privs(&auth_id, &data.acl_path(), PRIV_DATASTORE_VERIFY, true)?;
|
||||||
&auth_id,
|
|
||||||
&["datastore", &data.store],
|
|
||||||
PRIV_DATASTORE_VERIFY,
|
|
||||||
true,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
if let Some(delete) = delete {
|
if let Some(delete) = delete {
|
||||||
for delete_prop in delete {
|
for delete_prop in delete {
|
||||||
@ -258,13 +243,6 @@ pub fn update_verification_job(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some(store) = update.store {
|
if let Some(store) = update.store {
|
||||||
// check new store
|
|
||||||
user_info.check_privs(
|
|
||||||
&auth_id,
|
|
||||||
&["datastore", &store],
|
|
||||||
PRIV_DATASTORE_VERIFY,
|
|
||||||
true,
|
|
||||||
)?;
|
|
||||||
data.store = store;
|
data.store = store;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -289,6 +267,9 @@ pub fn update_verification_job(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// check new store and NS
|
||||||
|
user_info.check_privs(&auth_id, &data.acl_path(), PRIV_DATASTORE_VERIFY, true)?;
|
||||||
|
|
||||||
config.set_data(&id, "verification", &data)?;
|
config.set_data(&id, "verification", &data)?;
|
||||||
|
|
||||||
verify::save_config(&config)?;
|
verify::save_config(&config)?;
|
||||||
@ -332,12 +313,7 @@ pub fn delete_verification_job(
|
|||||||
let (mut config, expected_digest) = verify::config()?;
|
let (mut config, expected_digest) = verify::config()?;
|
||||||
|
|
||||||
let job: VerificationJobConfig = config.lookup("verification", &id)?;
|
let job: VerificationJobConfig = config.lookup("verification", &id)?;
|
||||||
user_info.check_privs(
|
user_info.check_privs(&auth_id, &job.acl_path(), PRIV_DATASTORE_VERIFY, true)?;
|
||||||
&auth_id,
|
|
||||||
&["datastore", &job.store],
|
|
||||||
PRIV_DATASTORE_VERIFY,
|
|
||||||
true,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
if let Some(ref digest) = digest {
|
if let Some(ref digest) = digest {
|
||||||
let digest = <[u8; 32]>::from_hex(digest)?;
|
let digest = <[u8; 32]>::from_hex(digest)?;
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
//! The Proxmox Backup Server API
|
//! The Proxmox Backup Server API
|
||||||
|
|
||||||
|
use proxmox_sys::sortable;
|
||||||
|
|
||||||
pub mod access;
|
pub mod access;
|
||||||
pub mod admin;
|
pub mod admin;
|
||||||
pub mod backup;
|
pub mod backup;
|
||||||
@ -16,7 +18,8 @@ pub mod version;
|
|||||||
|
|
||||||
use proxmox_router::{list_subdirs_api_method, Router, SubdirMap};
|
use proxmox_router::{list_subdirs_api_method, Router, SubdirMap};
|
||||||
|
|
||||||
const SUBDIRS: SubdirMap = &[
|
#[sortable]
|
||||||
|
const SUBDIRS: SubdirMap = &sorted!([
|
||||||
("access", &access::ROUTER),
|
("access", &access::ROUTER),
|
||||||
("admin", &admin::ROUTER),
|
("admin", &admin::ROUTER),
|
||||||
("backup", &backup::ROUTER),
|
("backup", &backup::ROUTER),
|
||||||
@ -28,7 +31,7 @@ const SUBDIRS: SubdirMap = &[
|
|||||||
("status", &status::ROUTER),
|
("status", &status::ROUTER),
|
||||||
("tape", &tape::ROUTER),
|
("tape", &tape::ROUTER),
|
||||||
("version", &version::ROUTER),
|
("version", &version::ROUTER),
|
||||||
];
|
]);
|
||||||
|
|
||||||
pub const ROUTER: Router = Router::new()
|
pub const ROUTER: Router = Router::new()
|
||||||
.get(&list_subdirs_api_method!(SUBDIRS))
|
.get(&list_subdirs_api_method!(SUBDIRS))
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
use anyhow::{bail, format_err, Error};
|
use anyhow::{bail, format_err, Error};
|
||||||
use serde_json::{json, Value};
|
use serde_json::{json, Value};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::os::unix::prelude::OsStrExt;
|
||||||
|
|
||||||
use proxmox_router::{
|
use proxmox_router::{
|
||||||
list_subdirs_api_method, Permission, Router, RpcEnvironment, RpcEnvironmentType, SubdirMap,
|
list_subdirs_api_method, Permission, Router, RpcEnvironment, RpcEnvironmentType, SubdirMap,
|
||||||
@ -360,7 +361,7 @@ pub fn get_versions() -> Result<Vec<APTUpdateInfo>, Error> {
|
|||||||
|
|
||||||
let running_kernel = format!(
|
let running_kernel = format!(
|
||||||
"running kernel: {}",
|
"running kernel: {}",
|
||||||
nix::sys::utsname::uname().release().to_owned()
|
std::str::from_utf8(nix::sys::utsname::uname()?.release().as_bytes())?.to_owned()
|
||||||
);
|
);
|
||||||
if let Some(proxmox_backup) = pbs_packages
|
if let Some(proxmox_backup) = pbs_packages
|
||||||
.iter()
|
.iter()
|
||||||
|
@ -317,7 +317,7 @@ fn upgrade_to_websocket(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[api]
|
#[api]
|
||||||
/// List Nodes (only for compatiblity)
|
/// List Nodes (only for compatibility)
|
||||||
fn list_nodes() -> Result<Value, Error> {
|
fn list_nodes() -> Result<Value, Error> {
|
||||||
Ok(json!([ { "node": proxmox_sys::nodename().to_string() } ]))
|
Ok(json!([ { "node": proxmox_sys::nodename().to_string() } ]))
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
use std::path::Path;
|
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
use std::{os::unix::prelude::OsStrExt, path::Path};
|
||||||
|
|
||||||
use anyhow::{bail, format_err, Error};
|
use anyhow::{bail, format_err, Error};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
@ -69,12 +69,12 @@ fn get_status(
|
|||||||
let cpuinfo = procfs::read_cpuinfo()?;
|
let cpuinfo = procfs::read_cpuinfo()?;
|
||||||
let cpuinfo = cpuinfo.into();
|
let cpuinfo = cpuinfo.into();
|
||||||
|
|
||||||
let uname = nix::sys::utsname::uname();
|
let uname = nix::sys::utsname::uname()?;
|
||||||
let kversion = format!(
|
let kversion = format!(
|
||||||
"{} {} {}",
|
"{} {} {}",
|
||||||
uname.sysname(),
|
std::str::from_utf8(uname.sysname().as_bytes())?,
|
||||||
uname.release(),
|
std::str::from_utf8(uname.release().as_bytes())?,
|
||||||
uname.version()
|
std::str::from_utf8(uname.version().as_bytes())?
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(NodeStatus {
|
Ok(NodeStatus {
|
||||||
|
@ -22,6 +22,7 @@ use proxmox_rest_server::{upid_log_path, upid_read_status, TaskListInfoIterator,
|
|||||||
// matches respective job execution privileges
|
// matches respective job execution privileges
|
||||||
fn check_job_privs(auth_id: &Authid, user_info: &CachedUserInfo, upid: &UPID) -> Result<(), Error> {
|
fn check_job_privs(auth_id: &Authid, user_info: &CachedUserInfo, upid: &UPID) -> Result<(), Error> {
|
||||||
match (upid.worker_type.as_str(), &upid.worker_id) {
|
match (upid.worker_type.as_str(), &upid.worker_id) {
|
||||||
|
// FIXME: parse namespace here?
|
||||||
("verificationjob", Some(workerid)) => {
|
("verificationjob", Some(workerid)) => {
|
||||||
if let Some(captures) = VERIFICATION_JOB_WORKER_ID_REGEX.captures(workerid) {
|
if let Some(captures) = VERIFICATION_JOB_WORKER_ID_REGEX.captures(workerid) {
|
||||||
if let Some(store) = captures.get(1) {
|
if let Some(store) = captures.get(1) {
|
||||||
|
@ -263,6 +263,7 @@ async fn pull(
|
|||||||
let client = pull_params.client().await?;
|
let client = pull_params.client().await?;
|
||||||
|
|
||||||
// fixme: set to_stdout to false?
|
// fixme: set to_stdout to false?
|
||||||
|
// FIXME: add namespace to worker id?
|
||||||
let upid_str = WorkerTask::spawn(
|
let upid_str = WorkerTask::spawn(
|
||||||
"sync",
|
"sync",
|
||||||
Some(store.clone()),
|
Some(store.clone()),
|
||||||
|
@ -78,21 +78,22 @@ fn upgrade_to_backup_reader_protocol(
|
|||||||
|
|
||||||
let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
|
let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
|
||||||
let store = required_string_param(¶m, "store")?.to_owned();
|
let store = required_string_param(¶m, "store")?.to_owned();
|
||||||
|
let backup_ns = optional_ns_param(¶m)?;
|
||||||
|
|
||||||
let user_info = CachedUserInfo::new()?;
|
let user_info = CachedUserInfo::new()?;
|
||||||
let privs = user_info.lookup_privs(&auth_id, &["datastore", &store]);
|
let acl_path = backup_ns.acl_path(&store);
|
||||||
|
let privs = user_info.lookup_privs(&auth_id, &acl_path);
|
||||||
|
|
||||||
let priv_read = privs & PRIV_DATASTORE_READ != 0;
|
let priv_read = privs & PRIV_DATASTORE_READ != 0;
|
||||||
let priv_backup = privs & PRIV_DATASTORE_BACKUP != 0;
|
let priv_backup = privs & PRIV_DATASTORE_BACKUP != 0;
|
||||||
|
|
||||||
// priv_backup needs owner check further down below!
|
// priv_backup needs owner check further down below!
|
||||||
if !priv_read && !priv_backup {
|
if !priv_read && !priv_backup {
|
||||||
bail!("no permissions on /datastore/{}", store);
|
bail!("no permissions on /{}", acl_path.join("/"));
|
||||||
}
|
}
|
||||||
|
|
||||||
let datastore = DataStore::lookup_datastore(&store, Some(Operation::Read))?;
|
let datastore = DataStore::lookup_datastore(&store, Some(Operation::Read))?;
|
||||||
|
|
||||||
let backup_ns = optional_ns_param(¶m)?;
|
|
||||||
let backup_dir = pbs_api_types::BackupDir::deserialize(¶m)?;
|
let backup_dir = pbs_api_types::BackupDir::deserialize(¶m)?;
|
||||||
|
|
||||||
let protocols = parts
|
let protocols = parts
|
||||||
@ -134,6 +135,7 @@ fn upgrade_to_backup_reader_protocol(
|
|||||||
|
|
||||||
//let files = BackupInfo::list_files(&path, &backup_dir)?;
|
//let files = BackupInfo::list_files(&path, &backup_dir)?;
|
||||||
|
|
||||||
|
// FIXME: include namespace here?
|
||||||
let worker_id = format!(
|
let worker_id = format!(
|
||||||
"{}:{}/{}/{:08X}",
|
"{}:{}/{}/{:08X}",
|
||||||
store,
|
store,
|
||||||
|
@ -18,6 +18,8 @@ use pbs_datastore::DataStore;
|
|||||||
use crate::rrd_cache::extract_rrd_data;
|
use crate::rrd_cache::extract_rrd_data;
|
||||||
use crate::tools::statistics::linear_regression;
|
use crate::tools::statistics::linear_regression;
|
||||||
|
|
||||||
|
use crate::backup::can_access_any_namespace;
|
||||||
|
|
||||||
#[api(
|
#[api(
|
||||||
returns: {
|
returns: {
|
||||||
description: "Lists the Status of the Datastores.",
|
description: "Lists the Status of the Datastores.",
|
||||||
@ -47,24 +49,18 @@ pub fn datastore_status(
|
|||||||
let user_privs = user_info.lookup_privs(&auth_id, &["datastore", store]);
|
let user_privs = user_info.lookup_privs(&auth_id, &["datastore", store]);
|
||||||
let allowed = (user_privs & (PRIV_DATASTORE_AUDIT | PRIV_DATASTORE_BACKUP)) != 0;
|
let allowed = (user_privs & (PRIV_DATASTORE_AUDIT | PRIV_DATASTORE_BACKUP)) != 0;
|
||||||
if !allowed {
|
if !allowed {
|
||||||
|
if let Ok(datastore) = DataStore::lookup_datastore(&store, Some(Operation::Lookup)) {
|
||||||
|
if can_access_any_namespace(datastore, &auth_id, &user_info) {
|
||||||
|
list.push(DataStoreStatusListItem::empty(store, None));
|
||||||
|
}
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let datastore = match DataStore::lookup_datastore(&store, Some(Operation::Read)) {
|
let datastore = match DataStore::lookup_datastore(&store, Some(Operation::Read)) {
|
||||||
Ok(datastore) => datastore,
|
Ok(datastore) => datastore,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
list.push(DataStoreStatusListItem {
|
list.push(DataStoreStatusListItem::empty(store, Some(err.to_string())));
|
||||||
store: store.clone(),
|
|
||||||
total: -1,
|
|
||||||
used: -1,
|
|
||||||
avail: -1,
|
|
||||||
history: None,
|
|
||||||
history_start: None,
|
|
||||||
history_delta: None,
|
|
||||||
estimated_full_date: None,
|
|
||||||
error: Some(err.to_string()),
|
|
||||||
gc_status: None,
|
|
||||||
});
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -10,7 +10,7 @@ use proxmox_schema::api;
|
|||||||
use proxmox_sys::{task_log, task_warn, WorkerTaskContext};
|
use proxmox_sys::{task_log, task_warn, WorkerTaskContext};
|
||||||
|
|
||||||
use pbs_api_types::{
|
use pbs_api_types::{
|
||||||
print_ns_and_snapshot, Authid, DatastoreWithNamespace, GroupFilter, MediaPoolConfig, Operation,
|
print_ns_and_snapshot, print_store_and_ns, Authid, GroupFilter, MediaPoolConfig, Operation,
|
||||||
TapeBackupJobConfig, TapeBackupJobSetup, TapeBackupJobStatus, Userid, JOB_ID_SCHEMA,
|
TapeBackupJobConfig, TapeBackupJobSetup, TapeBackupJobStatus, Userid, JOB_ID_SCHEMA,
|
||||||
PRIV_DATASTORE_READ, PRIV_TAPE_AUDIT, PRIV_TAPE_WRITE, UPID_SCHEMA,
|
PRIV_DATASTORE_READ, PRIV_TAPE_AUDIT, PRIV_TAPE_WRITE, UPID_SCHEMA,
|
||||||
};
|
};
|
||||||
@ -47,20 +47,11 @@ fn check_backup_permission(
|
|||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let user_info = CachedUserInfo::new()?;
|
let user_info = CachedUserInfo::new()?;
|
||||||
|
|
||||||
let privs = user_info.lookup_privs(auth_id, &["datastore", store]);
|
user_info.check_privs(auth_id, &["datastore", store], PRIV_DATASTORE_READ, false)?;
|
||||||
if (privs & PRIV_DATASTORE_READ) == 0 {
|
|
||||||
bail!("no permissions on /datastore/{}", store);
|
|
||||||
}
|
|
||||||
|
|
||||||
let privs = user_info.lookup_privs(auth_id, &["tape", "drive", drive]);
|
user_info.check_privs(auth_id, &["tape", "drive", drive], PRIV_TAPE_WRITE, false)?;
|
||||||
if (privs & PRIV_TAPE_WRITE) == 0 {
|
|
||||||
bail!("no permissions on /tape/drive/{}", drive);
|
|
||||||
}
|
|
||||||
|
|
||||||
let privs = user_info.lookup_privs(auth_id, &["tape", "pool", pool]);
|
user_info.check_privs(auth_id, &["tape", "pool", pool], PRIV_TAPE_WRITE, false)?;
|
||||||
if (privs & PRIV_TAPE_WRITE) == 0 {
|
|
||||||
bail!("no permissions on /tape/pool/{}", pool);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -237,11 +228,7 @@ pub fn do_tape_backup_job(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Err(err) = job.finish(status) {
|
if let Err(err) = job.finish(status) {
|
||||||
eprintln!(
|
eprintln!("could not finish job state for {}: {}", job.jobtype(), err);
|
||||||
"could not finish job state for {}: {}",
|
|
||||||
job.jobtype().to_string(),
|
|
||||||
err
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Err(err) = set_tape_device_state(&setup.drive, "") {
|
if let Err(err) = set_tape_device_state(&setup.drive, "") {
|
||||||
@ -462,11 +449,6 @@ fn backup_worker(
|
|||||||
let mut need_catalog = false; // avoid writing catalog for empty jobs
|
let mut need_catalog = false; // avoid writing catalog for empty jobs
|
||||||
|
|
||||||
for (group_number, group) in group_list.into_iter().enumerate() {
|
for (group_number, group) in group_list.into_iter().enumerate() {
|
||||||
let store_with_ns = DatastoreWithNamespace {
|
|
||||||
store: datastore_name.to_owned(),
|
|
||||||
ns: group.backup_ns().clone(),
|
|
||||||
};
|
|
||||||
|
|
||||||
progress.done_groups = group_number as u64;
|
progress.done_groups = group_number as u64;
|
||||||
progress.done_snapshots = 0;
|
progress.done_snapshots = 0;
|
||||||
progress.group_snapshots = 0;
|
progress.group_snapshots = 0;
|
||||||
@ -483,7 +465,7 @@ fn backup_worker(
|
|||||||
task_log!(
|
task_log!(
|
||||||
worker,
|
worker,
|
||||||
"{}, group {} was empty",
|
"{}, group {} was empty",
|
||||||
store_with_ns,
|
print_store_and_ns(datastore_name, group.backup_ns()),
|
||||||
group.group()
|
group.group()
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
@ -496,7 +478,11 @@ fn backup_worker(
|
|||||||
if let Some(info) = snapshot_list.pop() {
|
if let Some(info) = snapshot_list.pop() {
|
||||||
let rel_path =
|
let rel_path =
|
||||||
print_ns_and_snapshot(info.backup_dir.backup_ns(), info.backup_dir.as_ref());
|
print_ns_and_snapshot(info.backup_dir.backup_ns(), info.backup_dir.as_ref());
|
||||||
if pool_writer.contains_snapshot(datastore_name, &rel_path) {
|
if pool_writer.contains_snapshot(
|
||||||
|
datastore_name,
|
||||||
|
&info.backup_dir.backup_ns(),
|
||||||
|
info.backup_dir.as_ref(),
|
||||||
|
) {
|
||||||
task_log!(worker, "skip snapshot {}", rel_path);
|
task_log!(worker, "skip snapshot {}", rel_path);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -517,7 +503,11 @@ fn backup_worker(
|
|||||||
let rel_path =
|
let rel_path =
|
||||||
print_ns_and_snapshot(info.backup_dir.backup_ns(), info.backup_dir.as_ref());
|
print_ns_and_snapshot(info.backup_dir.backup_ns(), info.backup_dir.as_ref());
|
||||||
|
|
||||||
if pool_writer.contains_snapshot(datastore_name, &rel_path) {
|
if pool_writer.contains_snapshot(
|
||||||
|
datastore_name,
|
||||||
|
&info.backup_dir.backup_ns(),
|
||||||
|
info.backup_dir.as_ref(),
|
||||||
|
) {
|
||||||
task_log!(worker, "skip snapshot {}", rel_path);
|
task_log!(worker, "skip snapshot {}", rel_path);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -18,9 +18,10 @@ use proxmox_uuid::Uuid;
|
|||||||
|
|
||||||
use pbs_api_types::{
|
use pbs_api_types::{
|
||||||
parse_ns_and_snapshot, print_ns_and_snapshot, Authid, BackupDir, BackupNamespace, CryptMode,
|
parse_ns_and_snapshot, print_ns_and_snapshot, Authid, BackupDir, BackupNamespace, CryptMode,
|
||||||
Operation, TapeRestoreNamespace, Userid, DATASTORE_MAP_ARRAY_SCHEMA, DATASTORE_MAP_LIST_SCHEMA,
|
HumanByte, Operation, TapeRestoreNamespace, Userid, DATASTORE_MAP_ARRAY_SCHEMA,
|
||||||
DRIVE_NAME_SCHEMA, MAX_NAMESPACE_DEPTH, PRIV_DATASTORE_BACKUP, PRIV_DATASTORE_MODIFY,
|
DATASTORE_MAP_LIST_SCHEMA, DRIVE_NAME_SCHEMA, MAX_NAMESPACE_DEPTH, PRIV_DATASTORE_BACKUP,
|
||||||
PRIV_TAPE_READ, TAPE_RESTORE_NAMESPACE_SCHEMA, TAPE_RESTORE_SNAPSHOT_SCHEMA, UPID_SCHEMA,
|
PRIV_DATASTORE_MODIFY, PRIV_TAPE_READ, TAPE_RESTORE_NAMESPACE_SCHEMA,
|
||||||
|
TAPE_RESTORE_SNAPSHOT_SCHEMA, UPID_SCHEMA,
|
||||||
};
|
};
|
||||||
use pbs_config::CachedUserInfo;
|
use pbs_config::CachedUserInfo;
|
||||||
use pbs_datastore::dynamic_index::DynamicIndexReader;
|
use pbs_datastore::dynamic_index::DynamicIndexReader;
|
||||||
@ -33,6 +34,7 @@ use pbs_tape::{
|
|||||||
};
|
};
|
||||||
use proxmox_rest_server::WorkerTask;
|
use proxmox_rest_server::WorkerTask;
|
||||||
|
|
||||||
|
use crate::backup::check_ns_modification_privs;
|
||||||
use crate::{
|
use crate::{
|
||||||
server::lookup_user_email,
|
server::lookup_user_email,
|
||||||
tape::{
|
tape::{
|
||||||
@ -51,12 +53,6 @@ use crate::{
|
|||||||
tools::parallel_handler::ParallelHandler,
|
tools::parallel_handler::ParallelHandler,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct DataStoreMap {
|
|
||||||
map: HashMap<String, Arc<DataStore>>,
|
|
||||||
default: Option<Arc<DataStore>>,
|
|
||||||
ns_map: Option<NamespaceMap>,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct NamespaceMap {
|
struct NamespaceMap {
|
||||||
map: HashMap<String, HashMap<BackupNamespace, (BackupNamespace, usize)>>,
|
map: HashMap<String, HashMap<BackupNamespace, (BackupNamespace, usize)>>,
|
||||||
}
|
}
|
||||||
@ -122,6 +118,12 @@ impl NamespaceMap {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct DataStoreMap {
|
||||||
|
map: HashMap<String, Arc<DataStore>>,
|
||||||
|
default: Option<Arc<DataStore>>,
|
||||||
|
ns_map: Option<NamespaceMap>,
|
||||||
|
}
|
||||||
|
|
||||||
impl TryFrom<String> for DataStoreMap {
|
impl TryFrom<String> for DataStoreMap {
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
|
|
||||||
@ -179,20 +181,26 @@ impl DataStoreMap {
|
|||||||
map
|
map
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn target_ns(&self, datastore: &str, ns: &BackupNamespace) -> Option<Vec<BackupNamespace>> {
|
||||||
|
self.ns_map
|
||||||
|
.as_ref()
|
||||||
|
.map(|mapping| mapping.get_namespaces(datastore, ns))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn target_store(&self, source_datastore: &str) -> Option<Arc<DataStore>> {
|
||||||
|
self.map
|
||||||
|
.get(source_datastore)
|
||||||
|
.or_else(|| self.default.as_ref())
|
||||||
|
.map(|store| Arc::clone(store))
|
||||||
|
}
|
||||||
|
|
||||||
fn get_targets(
|
fn get_targets(
|
||||||
&self,
|
&self,
|
||||||
source_ds: &str,
|
source_datastore: &str,
|
||||||
source_ns: &BackupNamespace,
|
source_ns: &BackupNamespace,
|
||||||
) -> Option<(Arc<DataStore>, Option<Vec<BackupNamespace>>)> {
|
) -> Option<(Arc<DataStore>, Option<Vec<BackupNamespace>>)> {
|
||||||
if let Some(store) = self.map.get(source_ds).or(self.default.as_ref()) {
|
self.target_store(source_datastore)
|
||||||
let ns = self
|
.map(|store| (store, self.target_ns(source_datastore, source_ns)))
|
||||||
.ns_map
|
|
||||||
.as_ref()
|
|
||||||
.map(|map| map.get_namespaces(source_ds, source_ns));
|
|
||||||
return Some((Arc::clone(store), ns));
|
|
||||||
}
|
|
||||||
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -203,17 +211,10 @@ fn check_datastore_privs(
|
|||||||
auth_id: &Authid,
|
auth_id: &Authid,
|
||||||
owner: Option<&Authid>,
|
owner: Option<&Authid>,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let privs = if ns.is_root() {
|
let acl_path = ns.acl_path(store);
|
||||||
user_info.lookup_privs(auth_id, &["datastore", store])
|
let privs = user_info.lookup_privs(auth_id, &acl_path);
|
||||||
} else {
|
|
||||||
user_info.lookup_privs(auth_id, &["datastore", store, &ns.to_string()])
|
|
||||||
};
|
|
||||||
if (privs & PRIV_DATASTORE_BACKUP) == 0 {
|
if (privs & PRIV_DATASTORE_BACKUP) == 0 {
|
||||||
if ns.is_root() {
|
bail!("no permissions on /{}", acl_path.join("/"));
|
||||||
bail!("no permissions on /datastore/{}", store);
|
|
||||||
} else {
|
|
||||||
bail!("no permissions on /datastore/{}/{}", store, &ns.to_string());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(ref owner) = owner {
|
if let Some(ref owner) = owner {
|
||||||
@ -241,25 +242,16 @@ fn check_and_create_namespaces(
|
|||||||
|
|
||||||
// try create recursively if it does not exist
|
// try create recursively if it does not exist
|
||||||
if !store.namespace_exists(ns) {
|
if !store.namespace_exists(ns) {
|
||||||
let mut tmp_ns: BackupNamespace = Default::default();
|
let mut tmp_ns = BackupNamespace::root();
|
||||||
let has_datastore_priv = user_info.lookup_privs(auth_id, &["datastore", store.name()])
|
|
||||||
& PRIV_DATASTORE_MODIFY
|
|
||||||
!= 0;
|
|
||||||
|
|
||||||
for comp in ns.components() {
|
for comp in ns.components() {
|
||||||
tmp_ns.push(comp.to_string())?;
|
tmp_ns.push(comp.to_string())?;
|
||||||
if !store.namespace_exists(&tmp_ns) {
|
if !store.namespace_exists(&tmp_ns) {
|
||||||
if has_datastore_priv
|
check_ns_modification_privs(store.name(), &tmp_ns, auth_id).map_err(|_err| {
|
||||||
|| user_info.lookup_privs(
|
format_err!("no permission to create namespace '{}'", tmp_ns)
|
||||||
auth_id,
|
})?;
|
||||||
&["datastore", store.name(), &tmp_ns.parent().to_string()],
|
|
||||||
) & PRIV_DATASTORE_MODIFY
|
store.create_namespace(&tmp_ns.parent(), comp.to_string())?;
|
||||||
!= 0
|
|
||||||
{
|
|
||||||
store.create_namespace(&tmp_ns.parent(), comp.to_string())?;
|
|
||||||
} else {
|
|
||||||
bail!("no permissions to create '{}'", tmp_ns);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -312,9 +304,9 @@ pub const ROUTER: Router = Router::new().post(&API_METHOD_RESTORE);
|
|||||||
},
|
},
|
||||||
access: {
|
access: {
|
||||||
// Note: parameters are no uri parameter, so we need to test inside function body
|
// Note: parameters are no uri parameter, so we need to test inside function body
|
||||||
description: "The user needs Tape.Read privilege on /tape/pool/{pool} \
|
description: "The user needs Tape.Read privilege on /tape/pool/{pool} and \
|
||||||
and /tape/drive/{drive}, Datastore.Backup privilege on /datastore/{store}/[{namespace}],\
|
/tape/drive/{drive}, Datastore.Backup privilege on /datastore/{store}/[{namespace}], \
|
||||||
Datastore.Modify privileges to create namespaces (if they don't exist).",
|
Datastore.Modify privileges to create namespaces (if they don't exist).",
|
||||||
permission: &Permission::Anybody,
|
permission: &Permission::Anybody,
|
||||||
},
|
},
|
||||||
)]
|
)]
|
||||||
@ -333,11 +325,11 @@ pub fn restore(
|
|||||||
let user_info = CachedUserInfo::new()?;
|
let user_info = CachedUserInfo::new()?;
|
||||||
|
|
||||||
let mut store_map = DataStoreMap::try_from(store)
|
let mut store_map = DataStoreMap::try_from(store)
|
||||||
.map_err(|err| format_err!("cannot parse store mapping: {}", err))?;
|
.map_err(|err| format_err!("cannot parse store mapping: {err}"))?;
|
||||||
let namespaces = if let Some(maps) = namespaces {
|
let namespaces = if let Some(maps) = namespaces {
|
||||||
store_map
|
store_map
|
||||||
.add_namespaces_maps(maps)
|
.add_namespaces_maps(maps)
|
||||||
.map_err(|err| format_err!("cannot parse namespace mapping: {}", err))?
|
.map_err(|err| format_err!("cannot parse namespace mapping: {err}"))?
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
};
|
};
|
||||||
@ -351,25 +343,19 @@ pub fn restore(
|
|||||||
check_datastore_privs(
|
check_datastore_privs(
|
||||||
&user_info,
|
&user_info,
|
||||||
target.name(),
|
target.name(),
|
||||||
&Default::default(),
|
&BackupNamespace::root(),
|
||||||
&auth_id,
|
&auth_id,
|
||||||
owner.as_ref(),
|
owner.as_ref(),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
if let Some(namespaces) = namespaces {
|
if let Some(namespaces) = namespaces {
|
||||||
for ns in namespaces {
|
for ns in namespaces {
|
||||||
check_and_create_namespaces(&user_info, target, ns, &auth_id, owner.as_ref())?;
|
check_and_create_namespaces(&user_info, target, ns, &auth_id, owner.as_ref())?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
user_info.check_privs(&auth_id, &["tape", "drive", &drive], PRIV_TAPE_READ, false)?;
|
||||||
let privs = user_info.lookup_privs(&auth_id, &["tape", "drive", &drive]);
|
|
||||||
if (privs & PRIV_TAPE_READ) == 0 {
|
|
||||||
bail!("no permissions on /tape/drive/{}", drive);
|
|
||||||
}
|
|
||||||
|
|
||||||
let media_set_uuid = media_set.parse()?;
|
let media_set_uuid = media_set.parse()?;
|
||||||
|
|
||||||
let status_path = Path::new(TAPE_STATUS_DIR);
|
let status_path = Path::new(TAPE_STATUS_DIR);
|
||||||
|
|
||||||
let _lock = lock_media_set(status_path, &media_set_uuid, None)?;
|
let _lock = lock_media_set(status_path, &media_set_uuid, None)?;
|
||||||
@ -377,11 +363,7 @@ pub fn restore(
|
|||||||
let inventory = Inventory::load(status_path)?;
|
let inventory = Inventory::load(status_path)?;
|
||||||
|
|
||||||
let pool = inventory.lookup_media_set_pool(&media_set_uuid)?;
|
let pool = inventory.lookup_media_set_pool(&media_set_uuid)?;
|
||||||
|
user_info.check_privs(&auth_id, &["tape", "pool", &pool], PRIV_TAPE_READ, false)?;
|
||||||
let privs = user_info.lookup_privs(&auth_id, &["tape", "pool", &pool]);
|
|
||||||
if (privs & PRIV_TAPE_READ) == 0 {
|
|
||||||
bail!("no permissions on /tape/pool/{}", pool);
|
|
||||||
}
|
|
||||||
|
|
||||||
let (drive_config, _digest) = pbs_config::drive::config()?;
|
let (drive_config, _digest) = pbs_config::drive::config()?;
|
||||||
|
|
||||||
@ -413,8 +395,8 @@ pub fn restore(
|
|||||||
.and_then(|userid| lookup_user_email(userid))
|
.and_then(|userid| lookup_user_email(userid))
|
||||||
.or_else(|| lookup_user_email(&auth_id.clone().into()));
|
.or_else(|| lookup_user_email(&auth_id.clone().into()));
|
||||||
|
|
||||||
task_log!(worker, "Mediaset '{}'", media_set);
|
task_log!(worker, "Mediaset '{media_set}'");
|
||||||
task_log!(worker, "Pool: {}", pool);
|
task_log!(worker, "Pool: {pool}");
|
||||||
|
|
||||||
let res = if snapshots.is_some() || namespaces {
|
let res = if snapshots.is_some() || namespaces {
|
||||||
restore_list_worker(
|
restore_list_worker(
|
||||||
@ -443,13 +425,11 @@ pub fn restore(
|
|||||||
&auth_id,
|
&auth_id,
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
if res.is_ok() {
|
if res.is_ok() {
|
||||||
task_log!(worker, "Restore mediaset '{}' done", media_set);
|
task_log!(worker, "Restore mediaset '{media_set}' done");
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Err(err) = set_tape_device_state(&drive, "") {
|
if let Err(err) = set_tape_device_state(&drive, "") {
|
||||||
task_log!(worker, "could not unset drive state for {}: {}", drive, err);
|
task_log!(worker, "could not unset drive state for {drive}: {err}");
|
||||||
}
|
}
|
||||||
|
|
||||||
res
|
res
|
||||||
@ -481,11 +461,7 @@ fn restore_full_worker(
|
|||||||
for (seq_nr, media_uuid) in media_list.iter().enumerate() {
|
for (seq_nr, media_uuid) in media_list.iter().enumerate() {
|
||||||
match media_uuid {
|
match media_uuid {
|
||||||
None => {
|
None => {
|
||||||
bail!(
|
bail!("media set {media_set_uuid} is incomplete (missing member {seq_nr}).");
|
||||||
"media set {} is incomplete (missing member {}).",
|
|
||||||
media_set_uuid,
|
|
||||||
seq_nr
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
Some(media_uuid) => {
|
Some(media_uuid) => {
|
||||||
let media_id = inventory.lookup_media(media_uuid).unwrap();
|
let media_id = inventory.lookup_media(media_uuid).unwrap();
|
||||||
@ -503,30 +479,23 @@ fn restore_full_worker(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some(fingerprint) = encryption_key_fingerprint {
|
if let Some(fingerprint) = encryption_key_fingerprint {
|
||||||
task_log!(worker, "Encryption key fingerprint: {}", fingerprint);
|
task_log!(worker, "Encryption key fingerprint: {fingerprint}");
|
||||||
}
|
}
|
||||||
|
|
||||||
let used_datastores = store_map.used_datastores();
|
let used_datastores = store_map.used_datastores();
|
||||||
task_log!(
|
let datastore_list = used_datastores
|
||||||
worker,
|
.values()
|
||||||
"Datastore(s): {}",
|
.map(|(t, _)| String::from(t.name()))
|
||||||
used_datastores
|
.collect::<Vec<String>>()
|
||||||
.values()
|
.join(", ");
|
||||||
.map(|(t, _)| String::from(t.name()))
|
task_log!(worker, "Datastore(s): {datastore_list}",);
|
||||||
.collect::<Vec<String>>()
|
task_log!(worker, "Drive: {drive_name}");
|
||||||
.join(", "),
|
let required_media = media_id_list
|
||||||
);
|
.iter()
|
||||||
|
.map(|media_id| media_id.label.label_text.as_str())
|
||||||
task_log!(worker, "Drive: {}", drive_name);
|
.collect::<Vec<&str>>()
|
||||||
task_log!(
|
.join(";");
|
||||||
worker,
|
task_log!(worker, "Required media list: {required_media}",);
|
||||||
"Required media list: {}",
|
|
||||||
media_id_list
|
|
||||||
.iter()
|
|
||||||
.map(|media_id| media_id.label.label_text.as_str())
|
|
||||||
.collect::<Vec<&str>>()
|
|
||||||
.join(";")
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut datastore_locks = Vec::new();
|
let mut datastore_locks = Vec::new();
|
||||||
for (target, _) in used_datastores.values() {
|
for (target, _) in used_datastores.values() {
|
||||||
@ -568,9 +537,8 @@ fn check_snapshot_restorable(
|
|||||||
) -> Result<bool, Error> {
|
) -> Result<bool, Error> {
|
||||||
let (datastore, namespaces) = if required {
|
let (datastore, namespaces) = if required {
|
||||||
let (datastore, namespaces) = match store_map.get_targets(store, ns) {
|
let (datastore, namespaces) = match store_map.get_targets(store, ns) {
|
||||||
Some((target_ds, target_ns)) => {
|
Some((target_ds, Some(target_ns))) => (target_ds, target_ns),
|
||||||
(target_ds, target_ns.unwrap_or_else(|| vec![ns.clone()]))
|
Some((target_ds, None)) => (target_ds, vec![ns.clone()]),
|
||||||
}
|
|
||||||
None => bail!("could not find target datastore for {store}:{snapshot}"),
|
None => bail!("could not find target datastore for {store}:{snapshot}"),
|
||||||
};
|
};
|
||||||
if namespaces.is_empty() {
|
if namespaces.is_empty() {
|
||||||
@ -580,14 +548,9 @@ fn check_snapshot_restorable(
|
|||||||
(datastore, namespaces)
|
(datastore, namespaces)
|
||||||
} else {
|
} else {
|
||||||
match store_map.get_targets(store, ns) {
|
match store_map.get_targets(store, ns) {
|
||||||
Some((ds, Some(ns))) => {
|
Some((_, Some(ns))) if ns.is_empty() => return Ok(false),
|
||||||
if ns.is_empty() {
|
Some((datastore, Some(ns))) => (datastore, ns),
|
||||||
return Ok(false);
|
Some((_, None)) | None => return Ok(false),
|
||||||
}
|
|
||||||
(ds, ns)
|
|
||||||
}
|
|
||||||
Some((_, None)) => return Ok(false),
|
|
||||||
None => return Ok(false),
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -612,11 +575,8 @@ fn check_snapshot_restorable(
|
|||||||
// only the owner is allowed to create additional snapshots
|
// only the owner is allowed to create additional snapshots
|
||||||
task_warn!(
|
task_warn!(
|
||||||
worker,
|
worker,
|
||||||
"restore '{}' to {} failed - owner check failed ({} != {})",
|
"restore of '{snapshot}' to {ns} failed, owner check failed ({restore_owner} \
|
||||||
&snapshot,
|
!= {owner})",
|
||||||
ns,
|
|
||||||
restore_owner,
|
|
||||||
owner,
|
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -627,8 +587,7 @@ fn check_snapshot_restorable(
|
|||||||
if datastore.snapshot_path(&ns, &dir).exists() {
|
if datastore.snapshot_path(&ns, &dir).exists() {
|
||||||
task_warn!(
|
task_warn!(
|
||||||
worker,
|
worker,
|
||||||
"found snapshot {} on target datastore/namespace, skipping...",
|
"found snapshot {snapshot} on target datastore/namespace, skipping...",
|
||||||
&snapshot,
|
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -636,10 +595,7 @@ fn check_snapshot_restorable(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !have_some_permissions {
|
if !have_some_permissions {
|
||||||
bail!(
|
bail!("cannot restore {snapshot} to any target namespace due to permissions");
|
||||||
"cannot restore {} to any target namespace due to permissions",
|
|
||||||
&snapshot
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Ok(can_restore_some);
|
return Ok(can_restore_some);
|
||||||
@ -741,9 +697,11 @@ fn restore_list_worker(
|
|||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
};
|
};
|
||||||
for (store, snapshot, ns, _) in snapshots.iter() {
|
for (store, snapshot, _ns, _) in snapshots.iter() {
|
||||||
// unwrap ok, we already checked those snapshots
|
let datastore = match store_map.target_store(store) {
|
||||||
let (datastore, _) = store_map.get_targets(store, &ns).unwrap();
|
Some(store) => store,
|
||||||
|
None => bail!("unexpected error"), // we already checked those
|
||||||
|
};
|
||||||
let (media_id, file_num) =
|
let (media_id, file_num) =
|
||||||
if let Some((media_uuid, file_num)) = catalog.lookup_snapshot(store, &snapshot) {
|
if let Some((media_uuid, file_num)) = catalog.lookup_snapshot(store, &snapshot) {
|
||||||
let media_id = inventory.lookup_media(media_uuid).unwrap();
|
let media_id = inventory.lookup_media(media_uuid).unwrap();
|
||||||
@ -767,10 +725,8 @@ fn restore_list_worker(
|
|||||||
|
|
||||||
task_log!(
|
task_log!(
|
||||||
worker,
|
worker,
|
||||||
"found snapshot {} on {}: file {}",
|
"found snapshot {snapshot} on {}: file {file_num}",
|
||||||
&snapshot,
|
|
||||||
media_id.label.label_text,
|
media_id.label.label_text,
|
||||||
file_num
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -817,14 +773,9 @@ fn restore_list_worker(
|
|||||||
BTreeMap::new();
|
BTreeMap::new();
|
||||||
|
|
||||||
for (source_datastore, chunks) in datastore_chunk_map.into_iter() {
|
for (source_datastore, chunks) in datastore_chunk_map.into_iter() {
|
||||||
let (datastore, _) = store_map
|
let datastore = store_map.target_store(&source_datastore).ok_or_else(|| {
|
||||||
.get_targets(&source_datastore, &Default::default())
|
format_err!("could not find mapping for source datastore: {source_datastore}")
|
||||||
.ok_or_else(|| {
|
})?;
|
||||||
format_err!(
|
|
||||||
"could not find mapping for source datastore: {}",
|
|
||||||
source_datastore
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
for digest in chunks.into_iter() {
|
for digest in chunks.into_iter() {
|
||||||
// we only want to restore chunks that we do not have yet
|
// we only want to restore chunks that we do not have yet
|
||||||
if !datastore.cond_touch_chunk(&digest, false)? {
|
if !datastore.cond_touch_chunk(&digest, false)? {
|
||||||
@ -845,7 +796,7 @@ fn restore_list_worker(
|
|||||||
if !media_file_chunk_map.is_empty() {
|
if !media_file_chunk_map.is_empty() {
|
||||||
task_log!(worker, "Phase 2: restore chunks to datastores");
|
task_log!(worker, "Phase 2: restore chunks to datastores");
|
||||||
} else {
|
} else {
|
||||||
task_log!(worker, "all chunks exist already, skipping phase 2...");
|
task_log!(worker, "All chunks are already present, skip phase 2...");
|
||||||
}
|
}
|
||||||
|
|
||||||
for (media_uuid, file_chunk_map) in media_file_chunk_map.iter_mut() {
|
for (media_uuid, file_chunk_map) in media_file_chunk_map.iter_mut() {
|
||||||
@ -873,9 +824,7 @@ fn restore_list_worker(
|
|||||||
format_err!("unexpected source datastore: {}", source_datastore)
|
format_err!("unexpected source datastore: {}", source_datastore)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let namespaces = target_ns.unwrap_or_else(|| vec![source_ns.clone()]);
|
for ns in target_ns.unwrap_or_else(|| vec![source_ns.clone()]) {
|
||||||
|
|
||||||
for ns in namespaces {
|
|
||||||
if let Err(err) = proxmox_lang::try_block!({
|
if let Err(err) = proxmox_lang::try_block!({
|
||||||
check_and_create_namespaces(
|
check_and_create_namespaces(
|
||||||
&user_info,
|
&user_info,
|
||||||
@ -892,11 +841,9 @@ fn restore_list_worker(
|
|||||||
)?;
|
)?;
|
||||||
if restore_owner != &owner {
|
if restore_owner != &owner {
|
||||||
bail!(
|
bail!(
|
||||||
"cannot restore snapshot '{}' into group '{}', owner check failed ({} != {})",
|
"cannot restore snapshot '{snapshot}' into group '{}', owner check \
|
||||||
snapshot,
|
failed ({restore_owner} != {owner})",
|
||||||
backup_dir.group,
|
backup_dir.group,
|
||||||
restore_owner,
|
|
||||||
owner,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -936,10 +883,7 @@ fn restore_list_worker(
|
|||||||
}) {
|
}) {
|
||||||
task_warn!(
|
task_warn!(
|
||||||
worker,
|
worker,
|
||||||
"could not copy {}:{}: {}",
|
"could not copy {source_datastore}:{snapshot}: {err}"
|
||||||
source_datastore,
|
|
||||||
snapshot,
|
|
||||||
err,
|
|
||||||
);
|
);
|
||||||
errors = true;
|
errors = true;
|
||||||
}
|
}
|
||||||
@ -950,12 +894,7 @@ fn restore_list_worker(
|
|||||||
std::fs::remove_dir_all(&tmp_path)
|
std::fs::remove_dir_all(&tmp_path)
|
||||||
.map_err(|err| format_err!("remove_dir_all failed - {err}"))
|
.map_err(|err| format_err!("remove_dir_all failed - {err}"))
|
||||||
}) {
|
}) {
|
||||||
task_warn!(
|
task_warn!(worker, "could not clean up temp dir {tmp_path:?}: {err}");
|
||||||
worker,
|
|
||||||
"could not clean up temp dir {:?}: {}",
|
|
||||||
tmp_path,
|
|
||||||
err,
|
|
||||||
);
|
|
||||||
errors = true;
|
errors = true;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -1004,11 +943,7 @@ fn get_media_set_catalog(
|
|||||||
for (seq_nr, media_uuid) in media_list.iter().enumerate() {
|
for (seq_nr, media_uuid) in media_list.iter().enumerate() {
|
||||||
match media_uuid {
|
match media_uuid {
|
||||||
None => {
|
None => {
|
||||||
bail!(
|
bail!("media set {media_set_uuid} is incomplete (missing member {seq_nr}).");
|
||||||
"media set {} is incomplete (missing member {}).",
|
|
||||||
media_set_uuid,
|
|
||||||
seq_nr
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
Some(media_uuid) => {
|
Some(media_uuid) => {
|
||||||
let media_id = inventory.lookup_media(media_uuid).unwrap();
|
let media_id = inventory.lookup_media(media_uuid).unwrap();
|
||||||
@ -1081,9 +1016,7 @@ fn restore_snapshots_to_tmpdir(
|
|||||||
if current_file_number != *file_num {
|
if current_file_number != *file_num {
|
||||||
task_log!(
|
task_log!(
|
||||||
worker,
|
worker,
|
||||||
"was at file {}, moving to {}",
|
"was at file {current_file_number}, moving to {file_num}"
|
||||||
current_file_number,
|
|
||||||
file_num
|
|
||||||
);
|
);
|
||||||
drive.move_to_file(*file_num)?;
|
drive.move_to_file(*file_num)?;
|
||||||
let current_file_number = drive.current_file_number()?;
|
let current_file_number = drive.current_file_number()?;
|
||||||
@ -1103,7 +1036,7 @@ fn restore_snapshots_to_tmpdir(
|
|||||||
|
|
||||||
let archive_header: SnapshotArchiveHeader = serde_json::from_slice(&header_data)
|
let archive_header: SnapshotArchiveHeader = serde_json::from_slice(&header_data)
|
||||||
.map_err(|err| {
|
.map_err(|err| {
|
||||||
format_err!("unable to parse snapshot archive header - {}", err)
|
format_err!("unable to parse snapshot archive header - {err}")
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let source_datastore = archive_header.store;
|
let source_datastore = archive_header.store;
|
||||||
@ -1111,27 +1044,21 @@ fn restore_snapshots_to_tmpdir(
|
|||||||
|
|
||||||
task_log!(
|
task_log!(
|
||||||
worker,
|
worker,
|
||||||
"File {}: snapshot archive {}:{}",
|
"File {file_num}: snapshot archive {source_datastore}:{snapshot}",
|
||||||
file_num,
|
|
||||||
source_datastore,
|
|
||||||
snapshot
|
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut decoder = pxar::decoder::sync::Decoder::from_std(reader)?;
|
let mut decoder = pxar::decoder::sync::Decoder::from_std(reader)?;
|
||||||
|
|
||||||
let target_datastore =
|
let target_datastore = match store_map.target_store(&source_datastore) {
|
||||||
match store_map.get_targets(&source_datastore, &Default::default()) {
|
Some(datastore) => datastore,
|
||||||
Some((datastore, _)) => datastore,
|
None => {
|
||||||
None => {
|
task_warn!(
|
||||||
task_warn!(
|
worker,
|
||||||
worker,
|
"could not find target datastore for {source_datastore}:{snapshot}",
|
||||||
"could not find target datastore for {}:{}",
|
);
|
||||||
source_datastore,
|
continue;
|
||||||
snapshot
|
}
|
||||||
);
|
};
|
||||||
continue;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let tmp_path = snapshot_tmpdir(
|
let tmp_path = snapshot_tmpdir(
|
||||||
&source_datastore,
|
&source_datastore,
|
||||||
@ -1166,7 +1093,7 @@ fn restore_snapshots_to_tmpdir(
|
|||||||
}
|
}
|
||||||
tmp_paths.push(tmp_path);
|
tmp_paths.push(tmp_path);
|
||||||
}
|
}
|
||||||
other => bail!("unexpected file type: {:?}", other),
|
other => bail!("unexpected file type: {other:?}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1182,12 +1109,7 @@ fn restore_file_chunk_map(
|
|||||||
for (nr, chunk_map) in file_chunk_map.iter_mut() {
|
for (nr, chunk_map) in file_chunk_map.iter_mut() {
|
||||||
let current_file_number = drive.current_file_number()?;
|
let current_file_number = drive.current_file_number()?;
|
||||||
if current_file_number != *nr {
|
if current_file_number != *nr {
|
||||||
task_log!(
|
task_log!(worker, "was at file {current_file_number}, moving to {nr}");
|
||||||
worker,
|
|
||||||
"was at file {}, moving to {}",
|
|
||||||
current_file_number,
|
|
||||||
nr
|
|
||||||
);
|
|
||||||
drive.move_to_file(*nr)?;
|
drive.move_to_file(*nr)?;
|
||||||
let current_file_number = drive.current_file_number()?;
|
let current_file_number = drive.current_file_number()?;
|
||||||
task_log!(worker, "now at file {}", current_file_number);
|
task_log!(worker, "now at file {}", current_file_number);
|
||||||
@ -1195,7 +1117,7 @@ fn restore_file_chunk_map(
|
|||||||
let mut reader = drive.read_next_file()?;
|
let mut reader = drive.read_next_file()?;
|
||||||
let header: MediaContentHeader = unsafe { reader.read_le_value()? };
|
let header: MediaContentHeader = unsafe { reader.read_le_value()? };
|
||||||
if header.magic != PROXMOX_BACKUP_CONTENT_HEADER_MAGIC_1_0 {
|
if header.magic != PROXMOX_BACKUP_CONTENT_HEADER_MAGIC_1_0 {
|
||||||
bail!("missing MediaContentHeader");
|
bail!("file is missing the MediaContentHeader");
|
||||||
}
|
}
|
||||||
|
|
||||||
match header.content_magic {
|
match header.content_magic {
|
||||||
@ -1203,22 +1125,18 @@ fn restore_file_chunk_map(
|
|||||||
let header_data = reader.read_exact_allocated(header.size as usize)?;
|
let header_data = reader.read_exact_allocated(header.size as usize)?;
|
||||||
|
|
||||||
let archive_header: ChunkArchiveHeader = serde_json::from_slice(&header_data)
|
let archive_header: ChunkArchiveHeader = serde_json::from_slice(&header_data)
|
||||||
.map_err(|err| format_err!("unable to parse chunk archive header - {}", err))?;
|
.map_err(|err| format_err!("unable to parse chunk archive header - {err}"))?;
|
||||||
|
|
||||||
let source_datastore = archive_header.store;
|
let source_datastore = archive_header.store;
|
||||||
|
|
||||||
task_log!(
|
task_log!(
|
||||||
worker,
|
worker,
|
||||||
"File {}: chunk archive for datastore '{}'",
|
"File {nr}: chunk archive for datastore '{source_datastore}'",
|
||||||
nr,
|
|
||||||
source_datastore
|
|
||||||
);
|
);
|
||||||
|
|
||||||
let (datastore, _) = store_map
|
let datastore = store_map.target_store(&source_datastore).ok_or_else(|| {
|
||||||
.get_targets(&source_datastore, &Default::default())
|
format_err!("unexpected chunk archive for store: {source_datastore}")
|
||||||
.ok_or_else(|| {
|
})?;
|
||||||
format_err!("unexpected chunk archive for store: {}", source_datastore)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let count = restore_partial_chunk_archive(
|
let count = restore_partial_chunk_archive(
|
||||||
worker.clone(),
|
worker.clone(),
|
||||||
@ -1226,7 +1144,7 @@ fn restore_file_chunk_map(
|
|||||||
datastore.clone(),
|
datastore.clone(),
|
||||||
chunk_map,
|
chunk_map,
|
||||||
)?;
|
)?;
|
||||||
task_log!(worker, "restored {} chunks", count);
|
task_log!(worker, "restored {count} chunks");
|
||||||
}
|
}
|
||||||
_ => bail!("unexpected content magic {:?}", header.content_magic),
|
_ => bail!("unexpected content magic {:?}", header.content_magic),
|
||||||
}
|
}
|
||||||
@ -1273,14 +1191,12 @@ fn restore_partial_chunk_archive<'a>(
|
|||||||
Some((digest, blob)) => (digest, blob),
|
Some((digest, blob)) => (digest, blob),
|
||||||
None => break,
|
None => break,
|
||||||
};
|
};
|
||||||
|
|
||||||
worker.check_abort()?;
|
worker.check_abort()?;
|
||||||
|
|
||||||
if chunk_list.remove(&digest) {
|
if chunk_list.remove(&digest) {
|
||||||
verify_and_write_channel.send((blob, digest.clone()))?;
|
verify_and_write_channel.send((blob, digest.clone()))?;
|
||||||
count += 1;
|
count += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if chunk_list.is_empty() {
|
if chunk_list.is_empty() {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -1291,14 +1207,12 @@ fn restore_partial_chunk_archive<'a>(
|
|||||||
writer_pool.complete()?;
|
writer_pool.complete()?;
|
||||||
|
|
||||||
let elapsed = start_time.elapsed()?.as_secs_f64();
|
let elapsed = start_time.elapsed()?.as_secs_f64();
|
||||||
|
let bytes = bytes.load(std::sync::atomic::Ordering::SeqCst) as f64;
|
||||||
let bytes = bytes.load(std::sync::atomic::Ordering::SeqCst);
|
|
||||||
|
|
||||||
task_log!(
|
task_log!(
|
||||||
worker,
|
worker,
|
||||||
"restored {} bytes ({:.2} MB/s)",
|
"restored {} ({:.2}/s)",
|
||||||
bytes,
|
HumanByte::new_decimal(bytes),
|
||||||
(bytes as f64) / (1_000_000.0 * elapsed)
|
HumanByte::new_decimal(bytes / elapsed),
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(count)
|
Ok(count)
|
||||||
@ -1462,7 +1376,7 @@ fn restore_archive<'a>(
|
|||||||
let (backup_ns, backup_dir) = parse_ns_and_snapshot(&snapshot)?;
|
let (backup_ns, backup_dir) = parse_ns_and_snapshot(&snapshot)?;
|
||||||
|
|
||||||
if let Some((store_map, restore_owner)) = target.as_ref() {
|
if let Some((store_map, restore_owner)) = target.as_ref() {
|
||||||
if let Some((datastore, _)) = store_map.get_targets(&datastore_name, &backup_ns) {
|
if let Some(datastore) = store_map.target_store(&datastore_name) {
|
||||||
check_and_create_namespaces(
|
check_and_create_namespaces(
|
||||||
&user_info,
|
&user_info,
|
||||||
&datastore,
|
&datastore,
|
||||||
@ -1551,20 +1465,20 @@ fn restore_archive<'a>(
|
|||||||
);
|
);
|
||||||
let datastore = target
|
let datastore = target
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|t| t.0.get_targets(&source_datastore, &Default::default()));
|
.and_then(|t| t.0.target_store(&source_datastore));
|
||||||
|
|
||||||
if datastore.is_some() || target.is_none() {
|
if datastore.is_some() || target.is_none() {
|
||||||
let checked_chunks = checked_chunks_map
|
let checked_chunks = checked_chunks_map
|
||||||
.entry(
|
.entry(
|
||||||
datastore
|
datastore
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|(d, _)| d.name())
|
.map(|d| d.name())
|
||||||
.unwrap_or("_unused_")
|
.unwrap_or("_unused_")
|
||||||
.to_string(),
|
.to_string(),
|
||||||
)
|
)
|
||||||
.or_insert(HashSet::new());
|
.or_insert(HashSet::new());
|
||||||
|
|
||||||
let chunks = if let Some((datastore, _)) = datastore {
|
let chunks = if let Some(datastore) = datastore {
|
||||||
restore_chunk_archive(
|
restore_chunk_archive(
|
||||||
worker.clone(),
|
worker.clone(),
|
||||||
reader,
|
reader,
|
||||||
@ -1739,14 +1653,12 @@ fn restore_chunk_archive<'a>(
|
|||||||
writer_pool.complete()?;
|
writer_pool.complete()?;
|
||||||
|
|
||||||
let elapsed = start_time.elapsed()?.as_secs_f64();
|
let elapsed = start_time.elapsed()?.as_secs_f64();
|
||||||
|
let bytes = bytes.load(std::sync::atomic::Ordering::SeqCst) as f64;
|
||||||
let bytes = bytes.load(std::sync::atomic::Ordering::SeqCst);
|
|
||||||
|
|
||||||
task_log!(
|
task_log!(
|
||||||
worker,
|
worker,
|
||||||
"restored {} bytes ({:.2} MB/s)",
|
"restored {} ({:.2}/s)",
|
||||||
bytes,
|
HumanByte::new_decimal(bytes),
|
||||||
(bytes as f64) / (1_000_000.0 * elapsed)
|
HumanByte::new_decimal(bytes / elapsed),
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(Some(chunks))
|
Ok(Some(chunks))
|
||||||
@ -2009,7 +1921,7 @@ pub fn fast_catalog_restore(
|
|||||||
if &media_uuid != catalog_uuid {
|
if &media_uuid != catalog_uuid {
|
||||||
task_log!(
|
task_log!(
|
||||||
worker,
|
worker,
|
||||||
"catalog uuid missmatch at pos {}",
|
"catalog uuid mismatch at pos {}",
|
||||||
current_file_number
|
current_file_number
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
@ -2017,7 +1929,7 @@ pub fn fast_catalog_restore(
|
|||||||
if media_set_uuid != archive_header.media_set_uuid {
|
if media_set_uuid != archive_header.media_set_uuid {
|
||||||
task_log!(
|
task_log!(
|
||||||
worker,
|
worker,
|
||||||
"catalog media_set missmatch at pos {}",
|
"catalog media_set mismatch at pos {}",
|
||||||
current_file_number
|
current_file_number
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
|
@ -1,14 +1,96 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use anyhow::Error;
|
use anyhow::{bail, Error};
|
||||||
|
|
||||||
use pbs_api_types::{
|
use pbs_api_types::{
|
||||||
Authid, BackupNamespace, PRIV_DATASTORE_AUDIT, PRIV_DATASTORE_BACKUP, PRIV_DATASTORE_MODIFY,
|
privs_to_priv_names, Authid, BackupNamespace, PRIV_DATASTORE_AUDIT, PRIV_DATASTORE_BACKUP,
|
||||||
|
PRIV_DATASTORE_MODIFY, PRIV_DATASTORE_READ,
|
||||||
};
|
};
|
||||||
use pbs_config::CachedUserInfo;
|
use pbs_config::CachedUserInfo;
|
||||||
use pbs_datastore::{backup_info::BackupGroup, DataStore, ListGroups, ListNamespacesRecursive};
|
use pbs_datastore::{backup_info::BackupGroup, DataStore, ListGroups, ListNamespacesRecursive};
|
||||||
|
|
||||||
/// A priviledge aware iterator for all backup groups in all Namespaces below an anchor namespace,
|
/// Asserts that `privs` are fulfilled on datastore + (optional) namespace.
|
||||||
|
pub fn check_ns_privs(
|
||||||
|
store: &str,
|
||||||
|
ns: &BackupNamespace,
|
||||||
|
auth_id: &Authid,
|
||||||
|
privs: u64,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
check_ns_privs_full(store, ns, auth_id, privs, 0).map(|_| ())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Asserts that `privs` for creating/destroying namespace in datastore are fulfilled.
|
||||||
|
pub fn check_ns_modification_privs(
|
||||||
|
store: &str,
|
||||||
|
ns: &BackupNamespace,
|
||||||
|
auth_id: &Authid,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
// we could allow it as easy purge-whole datastore, but lets be more restrictive for now
|
||||||
|
if ns.is_root() {
|
||||||
|
// TODO
|
||||||
|
bail!("Cannot create/delete root namespace!");
|
||||||
|
}
|
||||||
|
|
||||||
|
let parent = ns.parent();
|
||||||
|
|
||||||
|
check_ns_privs(store, &parent, auth_id, PRIV_DATASTORE_MODIFY)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Asserts that either either `full_access_privs` or `partial_access_privs` are fulfilled on
|
||||||
|
/// datastore + (optional) namespace.
|
||||||
|
///
|
||||||
|
/// Return value indicates whether further checks like group ownerships are required because
|
||||||
|
/// `full_access_privs` are missing.
|
||||||
|
pub fn check_ns_privs_full(
|
||||||
|
store: &str,
|
||||||
|
ns: &BackupNamespace,
|
||||||
|
auth_id: &Authid,
|
||||||
|
full_access_privs: u64,
|
||||||
|
partial_access_privs: u64,
|
||||||
|
) -> Result<bool, Error> {
|
||||||
|
let user_info = CachedUserInfo::new()?;
|
||||||
|
let acl_path = ns.acl_path(store);
|
||||||
|
let privs = user_info.lookup_privs(auth_id, &acl_path);
|
||||||
|
|
||||||
|
if full_access_privs != 0 && (privs & full_access_privs) != 0 {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
if partial_access_privs != 0 && (privs & partial_access_privs) != 0 {
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
let priv_names = privs_to_priv_names(full_access_privs | partial_access_privs).join("|");
|
||||||
|
let path = format!("/{}", acl_path.join("/"));
|
||||||
|
|
||||||
|
proxmox_router::http_bail!(
|
||||||
|
FORBIDDEN,
|
||||||
|
"permission check failed - missing {priv_names} on {path}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn can_access_any_namespace(
|
||||||
|
store: Arc<DataStore>,
|
||||||
|
auth_id: &Authid,
|
||||||
|
user_info: &CachedUserInfo,
|
||||||
|
) -> bool {
|
||||||
|
// NOTE: traversing the datastore could be avoided if we had an "ACL tree: is there any priv
|
||||||
|
// below /datastore/{store}" helper
|
||||||
|
let mut iter =
|
||||||
|
if let Ok(iter) = store.recursive_iter_backup_ns_ok(BackupNamespace::root(), None) {
|
||||||
|
iter
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let wanted =
|
||||||
|
PRIV_DATASTORE_AUDIT | PRIV_DATASTORE_MODIFY | PRIV_DATASTORE_READ | PRIV_DATASTORE_BACKUP;
|
||||||
|
let name = store.name();
|
||||||
|
iter.any(|ns| -> bool {
|
||||||
|
let user_privs = user_info.lookup_privs(&auth_id, &["datastore", name, &ns.to_string()]);
|
||||||
|
user_privs & wanted != 0
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A privilege aware iterator for all backup groups in all Namespaces below an anchor namespace,
|
||||||
/// most often that will be the `BackupNamespace::root()` one.
|
/// most often that will be the `BackupNamespace::root()` one.
|
||||||
///
|
///
|
||||||
/// Is basically just a filter-iter for pbs_datastore::ListNamespacesRecursive including access and
|
/// Is basically just a filter-iter for pbs_datastore::ListNamespacesRecursive including access and
|
||||||
@ -17,23 +99,42 @@ pub struct ListAccessibleBackupGroups<'a> {
|
|||||||
store: &'a Arc<DataStore>,
|
store: &'a Arc<DataStore>,
|
||||||
auth_id: Option<&'a Authid>,
|
auth_id: Option<&'a Authid>,
|
||||||
user_info: Arc<CachedUserInfo>,
|
user_info: Arc<CachedUserInfo>,
|
||||||
state: Option<ListGroups>,
|
/// The priv on NS level that allows auth_id trump the owner check
|
||||||
|
override_owner_priv: u64,
|
||||||
|
/// The priv that auth_id is required to have on NS level additionally to being owner
|
||||||
|
owner_and_priv: u64,
|
||||||
|
/// Contains the intertnal state, group iter and a bool flag for override_owner_priv
|
||||||
|
state: Option<(ListGroups, bool)>,
|
||||||
ns_iter: ListNamespacesRecursive,
|
ns_iter: ListNamespacesRecursive,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> ListAccessibleBackupGroups<'a> {
|
impl<'a> ListAccessibleBackupGroups<'a> {
|
||||||
// TODO: builder pattern
|
// TODO: builder pattern
|
||||||
|
|
||||||
pub fn new(
|
pub fn new_owned(
|
||||||
store: &'a Arc<DataStore>,
|
store: &'a Arc<DataStore>,
|
||||||
ns: BackupNamespace,
|
ns: BackupNamespace,
|
||||||
max_depth: usize,
|
max_depth: usize,
|
||||||
auth_id: Option<&'a Authid>,
|
auth_id: Option<&'a Authid>,
|
||||||
|
) -> Result<Self, Error> {
|
||||||
|
// only owned groups by default and no extra priv required
|
||||||
|
Self::new_with_privs(store, ns, max_depth, None, None, auth_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_with_privs(
|
||||||
|
store: &'a Arc<DataStore>,
|
||||||
|
ns: BackupNamespace,
|
||||||
|
max_depth: usize,
|
||||||
|
override_owner_priv: Option<u64>,
|
||||||
|
owner_and_priv: Option<u64>,
|
||||||
|
auth_id: Option<&'a Authid>,
|
||||||
) -> Result<Self, Error> {
|
) -> Result<Self, Error> {
|
||||||
let ns_iter = ListNamespacesRecursive::new_max_depth(Arc::clone(store), ns, max_depth)?;
|
let ns_iter = ListNamespacesRecursive::new_max_depth(Arc::clone(store), ns, max_depth)?;
|
||||||
Ok(ListAccessibleBackupGroups {
|
Ok(ListAccessibleBackupGroups {
|
||||||
auth_id,
|
auth_id,
|
||||||
ns_iter,
|
ns_iter,
|
||||||
|
override_owner_priv: override_owner_priv.unwrap_or(0),
|
||||||
|
owner_and_priv: owner_and_priv.unwrap_or(0),
|
||||||
state: None,
|
state: None,
|
||||||
store: store,
|
store: store,
|
||||||
user_info: CachedUserInfo::new()?,
|
user_info: CachedUserInfo::new()?,
|
||||||
@ -41,15 +142,20 @@ impl<'a> ListAccessibleBackupGroups<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub static NS_PRIVS_OK: u64 =
|
||||||
|
PRIV_DATASTORE_MODIFY | PRIV_DATASTORE_READ | PRIV_DATASTORE_BACKUP | PRIV_DATASTORE_AUDIT;
|
||||||
|
|
||||||
impl<'a> Iterator for ListAccessibleBackupGroups<'a> {
|
impl<'a> Iterator for ListAccessibleBackupGroups<'a> {
|
||||||
type Item = Result<BackupGroup, Error>;
|
type Item = Result<BackupGroup, Error>;
|
||||||
|
|
||||||
fn next(&mut self) -> Option<Self::Item> {
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
const PRIVS_OK: u64 = PRIV_DATASTORE_MODIFY | PRIV_DATASTORE_BACKUP | PRIV_DATASTORE_AUDIT;
|
|
||||||
loop {
|
loop {
|
||||||
if let Some(ref mut state) = self.state {
|
if let Some((ref mut state, override_owner)) = self.state {
|
||||||
match state.next() {
|
match state.next() {
|
||||||
Some(Ok(group)) => {
|
Some(Ok(group)) => {
|
||||||
|
if override_owner {
|
||||||
|
return Some(Ok(group));
|
||||||
|
}
|
||||||
if let Some(auth_id) = &self.auth_id {
|
if let Some(auth_id) = &self.auth_id {
|
||||||
match self.store.owns_backup(
|
match self.store.owns_backup(
|
||||||
&group.backup_ns(),
|
&group.backup_ns(),
|
||||||
@ -72,22 +178,26 @@ impl<'a> Iterator for ListAccessibleBackupGroups<'a> {
|
|||||||
} else {
|
} else {
|
||||||
match self.ns_iter.next() {
|
match self.ns_iter.next() {
|
||||||
Some(Ok(ns)) => {
|
Some(Ok(ns)) => {
|
||||||
|
let mut override_owner = false;
|
||||||
if let Some(auth_id) = &self.auth_id {
|
if let Some(auth_id) = &self.auth_id {
|
||||||
let info = &self.user_info;
|
let info = &self.user_info;
|
||||||
let privs = if ns.is_root() {
|
|
||||||
info.lookup_privs(&auth_id, &["datastore", self.store.name()])
|
let privs =
|
||||||
} else {
|
info.lookup_privs(&auth_id, &ns.acl_path(self.store.name()));
|
||||||
info.lookup_privs(
|
|
||||||
&auth_id,
|
if privs & NS_PRIVS_OK == 0 {
|
||||||
&["datastore", self.store.name(), &ns.to_string()],
|
|
||||||
)
|
|
||||||
};
|
|
||||||
if privs & PRIVS_OK == 0 {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// check first if *any* override owner priv is available up front
|
||||||
|
if privs & self.override_owner_priv != 0 {
|
||||||
|
override_owner = true;
|
||||||
|
} else if privs & self.owner_and_priv != self.owner_and_priv {
|
||||||
|
continue; // no owner override and no extra privs -> nothing visible
|
||||||
|
}
|
||||||
}
|
}
|
||||||
self.state = match ListGroups::new(Arc::clone(&self.store), ns) {
|
self.state = match ListGroups::new(Arc::clone(&self.store), ns) {
|
||||||
Ok(iter) => Some(iter),
|
Ok(iter) => Some((iter, override_owner)),
|
||||||
Err(err) => return Some(Err(err)),
|
Err(err) => return Some(Err(err)),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -9,8 +9,8 @@ use anyhow::{bail, format_err, Error};
|
|||||||
use proxmox_sys::{task_log, WorkerTaskContext};
|
use proxmox_sys::{task_log, WorkerTaskContext};
|
||||||
|
|
||||||
use pbs_api_types::{
|
use pbs_api_types::{
|
||||||
print_ns_and_snapshot, Authid, BackupNamespace, BackupType, CryptMode, DatastoreWithNamespace,
|
print_ns_and_snapshot, print_store_and_ns, Authid, BackupNamespace, BackupType, CryptMode,
|
||||||
SnapshotVerifyState, VerifyState, UPID,
|
SnapshotVerifyState, VerifyState, PRIV_DATASTORE_BACKUP, PRIV_DATASTORE_VERIFY, UPID,
|
||||||
};
|
};
|
||||||
use pbs_datastore::backup_info::{BackupDir, BackupGroup, BackupInfo};
|
use pbs_datastore::backup_info::{BackupDir, BackupGroup, BackupInfo};
|
||||||
use pbs_datastore::index::IndexFile;
|
use pbs_datastore::index::IndexFile;
|
||||||
@ -453,14 +453,10 @@ pub fn verify_backup_group(
|
|||||||
let mut list = match group.list_backups() {
|
let mut list = match group.list_backups() {
|
||||||
Ok(list) => list,
|
Ok(list) => list,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
let store_with_ns = DatastoreWithNamespace {
|
|
||||||
store: verify_worker.datastore.name().to_owned(),
|
|
||||||
ns: group.backup_ns().clone(),
|
|
||||||
};
|
|
||||||
task_log!(
|
task_log!(
|
||||||
verify_worker.worker,
|
verify_worker.worker,
|
||||||
"verify {}, group {} - unable to list backups: {}",
|
"verify {}, group {} - unable to list backups: {}",
|
||||||
store_with_ns,
|
print_store_and_ns(verify_worker.datastore.name(), group.backup_ns()),
|
||||||
group.group(),
|
group.group(),
|
||||||
err,
|
err,
|
||||||
);
|
);
|
||||||
@ -529,7 +525,14 @@ pub fn verify_all_backups(
|
|||||||
let store = &verify_worker.datastore;
|
let store = &verify_worker.datastore;
|
||||||
let max_depth = max_depth.unwrap_or(pbs_api_types::MAX_NAMESPACE_DEPTH);
|
let max_depth = max_depth.unwrap_or(pbs_api_types::MAX_NAMESPACE_DEPTH);
|
||||||
|
|
||||||
let mut list = match ListAccessibleBackupGroups::new(store, ns.clone(), max_depth, owner) {
|
let mut list = match ListAccessibleBackupGroups::new_with_privs(
|
||||||
|
store,
|
||||||
|
ns.clone(),
|
||||||
|
max_depth,
|
||||||
|
Some(PRIV_DATASTORE_VERIFY),
|
||||||
|
Some(PRIV_DATASTORE_BACKUP),
|
||||||
|
owner,
|
||||||
|
) {
|
||||||
Ok(list) => list
|
Ok(list) => list
|
||||||
.filter_map(|group| match group {
|
.filter_map(|group| match group {
|
||||||
Ok(group) => Some(group),
|
Ok(group) => Some(group),
|
||||||
@ -575,7 +578,7 @@ pub fn verify_all_backups(
|
|||||||
Ok(errors)
|
Ok(errors)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Filter for the verification of snapshots
|
/// Filter out any snapshot from being (re-)verified where this fn returns false.
|
||||||
pub fn verify_filter(
|
pub fn verify_filter(
|
||||||
ignore_verified_snapshots: bool,
|
ignore_verified_snapshots: bool,
|
||||||
outdated_after: Option<i64>,
|
outdated_after: Option<i64>,
|
||||||
@ -595,7 +598,7 @@ pub fn verify_filter(
|
|||||||
let now = proxmox_time::epoch_i64();
|
let now = proxmox_time::epoch_i64();
|
||||||
let days_since_last_verify = (now - last_verify.upid.starttime) / 86400;
|
let days_since_last_verify = (now - last_verify.upid.starttime) / 86400;
|
||||||
|
|
||||||
max_age == 0 || days_since_last_verify > max_age
|
days_since_last_verify > max_age
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -430,6 +430,7 @@ async fn run() -> Result<(), Error> {
|
|||||||
.insert("subscription", subscription_commands())
|
.insert("subscription", subscription_commands())
|
||||||
.insert("sync-job", sync_job_commands())
|
.insert("sync-job", sync_job_commands())
|
||||||
.insert("verify-job", verify_job_commands())
|
.insert("verify-job", verify_job_commands())
|
||||||
|
.insert("prune-job", prune_job_commands())
|
||||||
.insert("task", task_mgmt_cli())
|
.insert("task", task_mgmt_cli())
|
||||||
.insert(
|
.insert(
|
||||||
"pull",
|
"pull",
|
||||||
@ -452,6 +453,9 @@ async fn run() -> Result<(), Error> {
|
|||||||
.insert("versions", CliCommand::new(&API_METHOD_GET_VERSIONS));
|
.insert("versions", CliCommand::new(&API_METHOD_GET_VERSIONS));
|
||||||
|
|
||||||
let args: Vec<String> = std::env::args().take(2).collect();
|
let args: Vec<String> = std::env::args().take(2).collect();
|
||||||
|
if args.len() >= 2 && args[1] == "update-to-prune-jobs-config" {
|
||||||
|
return update_to_prune_jobs_config();
|
||||||
|
}
|
||||||
let avoid_init = args.len() >= 2 && (args[1] == "bashcomplete" || args[1] == "printdoc");
|
let avoid_init = args.len() >= 2 && (args[1] == "bashcomplete" || args[1] == "printdoc");
|
||||||
|
|
||||||
if !avoid_init {
|
if !avoid_init {
|
||||||
@ -459,6 +463,7 @@ async fn run() -> Result<(), Error> {
|
|||||||
let file_opts = CreateOptions::new()
|
let file_opts = CreateOptions::new()
|
||||||
.owner(backup_user.uid)
|
.owner(backup_user.uid)
|
||||||
.group(backup_user.gid);
|
.group(backup_user.gid);
|
||||||
|
|
||||||
proxmox_rest_server::init_worker_tasks(
|
proxmox_rest_server::init_worker_tasks(
|
||||||
pbs_buildcfg::PROXMOX_BACKUP_LOG_DIR_M!().into(),
|
pbs_buildcfg::PROXMOX_BACKUP_LOG_DIR_M!().into(),
|
||||||
file_opts,
|
file_opts,
|
||||||
|
@ -47,7 +47,7 @@ use pbs_buildcfg::configdir;
|
|||||||
use proxmox_time::CalendarEvent;
|
use proxmox_time::CalendarEvent;
|
||||||
|
|
||||||
use pbs_api_types::{
|
use pbs_api_types::{
|
||||||
Authid, DataStoreConfig, Operation, PruneOptions, SyncJobConfig, TapeBackupJobConfig,
|
Authid, DataStoreConfig, Operation, PruneJobConfig, SyncJobConfig, TapeBackupJobConfig,
|
||||||
VerificationJobConfig,
|
VerificationJobConfig,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -557,7 +557,7 @@ async fn run_task_scheduler() {
|
|||||||
|
|
||||||
async fn schedule_tasks() -> Result<(), Error> {
|
async fn schedule_tasks() -> Result<(), Error> {
|
||||||
schedule_datastore_garbage_collection().await;
|
schedule_datastore_garbage_collection().await;
|
||||||
schedule_datastore_prune().await;
|
schedule_datastore_prune_jobs().await;
|
||||||
schedule_datastore_sync_jobs().await;
|
schedule_datastore_sync_jobs().await;
|
||||||
schedule_datastore_verify_jobs().await;
|
schedule_datastore_verify_jobs().await;
|
||||||
schedule_tape_backup_jobs().await;
|
schedule_tape_backup_jobs().await;
|
||||||
@ -667,55 +667,47 @@ async fn schedule_datastore_garbage_collection() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn schedule_datastore_prune() {
|
async fn schedule_datastore_prune_jobs() {
|
||||||
let config = match pbs_config::datastore::config() {
|
let config = match pbs_config::prune::config() {
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
eprintln!("unable to read datastore config - {}", err);
|
eprintln!("unable to read prune job config - {}", err);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Ok((config, _digest)) => config,
|
Ok((config, _digest)) => config,
|
||||||
};
|
};
|
||||||
|
for (job_id, (_, job_config)) in config.sections {
|
||||||
for (store, (_, store_config)) in config.sections {
|
let job_config: PruneJobConfig = match serde_json::from_value(job_config) {
|
||||||
let store_config: DataStoreConfig = match serde_json::from_value(store_config) {
|
|
||||||
Ok(c) => c,
|
Ok(c) => c,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
eprintln!("datastore '{}' config from_value failed - {}", store, err);
|
eprintln!("prune job config from_value failed - {}", err);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let event_str = match store_config.prune_schedule {
|
if job_config.disable {
|
||||||
Some(event_str) => event_str,
|
|
||||||
None => continue,
|
|
||||||
};
|
|
||||||
|
|
||||||
let prune_options = PruneOptions {
|
|
||||||
keep_last: store_config.keep_last,
|
|
||||||
keep_hourly: store_config.keep_hourly,
|
|
||||||
keep_daily: store_config.keep_daily,
|
|
||||||
keep_weekly: store_config.keep_weekly,
|
|
||||||
keep_monthly: store_config.keep_monthly,
|
|
||||||
keep_yearly: store_config.keep_yearly,
|
|
||||||
};
|
|
||||||
|
|
||||||
if !pbs_datastore::prune::keeps_something(&prune_options) {
|
|
||||||
// no prune settings - keep all
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let worker_type = "prune";
|
if !job_config.options.keeps_something() {
|
||||||
if check_schedule(worker_type, &event_str, &store) {
|
// no 'keep' values set, keep all
|
||||||
let job = match Job::new(worker_type, &store) {
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let worker_type = "prunejob";
|
||||||
|
let auth_id = Authid::root_auth_id().clone();
|
||||||
|
if check_schedule(worker_type, &job_config.schedule, &job_id) {
|
||||||
|
let job = match Job::new(worker_type, &job_id) {
|
||||||
Ok(job) => job,
|
Ok(job) => job,
|
||||||
Err(_) => continue, // could not get lock
|
Err(_) => continue, // could not get lock
|
||||||
};
|
};
|
||||||
|
if let Err(err) = do_prune_job(
|
||||||
let auth_id = Authid::root_auth_id().clone();
|
job,
|
||||||
if let Err(err) =
|
job_config.options,
|
||||||
do_prune_job(job, prune_options, store.clone(), &auth_id, Some(event_str))
|
job_config.store,
|
||||||
{
|
&auth_id,
|
||||||
eprintln!("unable to start datastore prune job {} - {}", &store, err);
|
Some(job_config.schedule),
|
||||||
|
) {
|
||||||
|
eprintln!("unable to start datastore prune job {} - {}", &job_id, err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -845,10 +837,7 @@ async fn schedule_task_log_rotate() {
|
|||||||
if !check_schedule(worker_type, schedule, job_id) {
|
if !check_schedule(worker_type, schedule, job_id) {
|
||||||
// if we never ran the rotation, schedule instantly
|
// if we never ran the rotation, schedule instantly
|
||||||
match jobstate::JobState::load(worker_type, job_id) {
|
match jobstate::JobState::load(worker_type, job_id) {
|
||||||
Ok(state) => match state {
|
Ok(jobstate::JobState::Created { .. }) => {}
|
||||||
jobstate::JobState::Created { .. } => {}
|
|
||||||
_ => return,
|
|
||||||
},
|
|
||||||
_ => return,
|
_ => return,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1012,7 +1001,7 @@ async fn run_stat_generator() {
|
|||||||
async fn generate_host_stats() {
|
async fn generate_host_stats() {
|
||||||
match tokio::task::spawn_blocking(generate_host_stats_sync).await {
|
match tokio::task::spawn_blocking(generate_host_stats_sync).await {
|
||||||
Ok(()) => (),
|
Ok(()) => (),
|
||||||
Err(err) => log::error!("generate_host_stats paniced: {}", err),
|
Err(err) => log::error!("generate_host_stats panicked: {}", err),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1191,10 +1180,6 @@ fn gather_disk_stats(disk_manager: Arc<DiskManage>, path: &Path, rrd_prefix: &st
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Rate Limiter lookup
|
// Rate Limiter lookup
|
||||||
|
|
||||||
// Test WITH
|
|
||||||
// proxmox-backup-client restore vm/201/2021-10-22T09:55:56Z drive-scsi0.img img1.img --repository localhost:store2
|
|
||||||
|
|
||||||
async fn run_traffic_control_updater() {
|
async fn run_traffic_control_updater() {
|
||||||
loop {
|
loop {
|
||||||
let delay_target = Instant::now() + Duration::from_secs(1);
|
let delay_target = Instant::now() + Duration::from_secs(1);
|
||||||
|
@ -292,7 +292,7 @@ async fn load_media_from_slot(mut param: Value) -> Result<(), Error> {
|
|||||||
let client = connect_to_localhost()?;
|
let client = connect_to_localhost()?;
|
||||||
|
|
||||||
let path = format!("api2/json/tape/drive/{}/load-slot", drive);
|
let path = format!("api2/json/tape/drive/{}/load-slot", drive);
|
||||||
client.put(&path, Some(param)).await?;
|
client.post(&path, Some(param)).await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::{stdout, Read, Seek, SeekFrom, Write};
|
use std::io::{Read, Seek, SeekFrom, Write};
|
||||||
use std::panic::{RefUnwindSafe, UnwindSafe};
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use anyhow::{bail, format_err, Error};
|
use anyhow::{bail, format_err, Error};
|
||||||
@ -27,18 +26,6 @@ use pbs_datastore::index::IndexFile;
|
|||||||
use pbs_datastore::DataBlob;
|
use pbs_datastore::DataBlob;
|
||||||
use pbs_tools::crypt_config::CryptConfig;
|
use pbs_tools::crypt_config::CryptConfig;
|
||||||
|
|
||||||
// Returns either a new file, if a path is given, or stdout, if no path is given.
|
|
||||||
fn outfile_or_stdout<P: AsRef<Path>>(
|
|
||||||
path: Option<P>,
|
|
||||||
) -> std::io::Result<Box<dyn Write + Send + Sync + Unpin + RefUnwindSafe + UnwindSafe>> {
|
|
||||||
if let Some(path) = path {
|
|
||||||
let f = File::create(path)?;
|
|
||||||
Ok(Box::new(f) as Box<_>)
|
|
||||||
} else {
|
|
||||||
Ok(Box::new(stdout()) as Box<_>)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Decodes a blob and writes its content either to stdout or into a file
|
/// Decodes a blob and writes its content either to stdout or into a file
|
||||||
fn decode_blob(
|
fn decode_blob(
|
||||||
mut output_path: Option<&Path>,
|
mut output_path: Option<&Path>,
|
||||||
@ -61,7 +48,8 @@ fn decode_blob(
|
|||||||
_ => output_path,
|
_ => output_path,
|
||||||
};
|
};
|
||||||
|
|
||||||
outfile_or_stdout(output_path)?.write_all(blob.decode(crypt_conf_opt, digest)?.as_slice())?;
|
crate::outfile_or_stdout(output_path)?
|
||||||
|
.write_all(blob.decode(crypt_conf_opt, digest)?.as_slice())?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,3 +1,22 @@
|
|||||||
|
use std::{
|
||||||
|
fs::File,
|
||||||
|
io::{stdout, Write},
|
||||||
|
panic::{RefUnwindSafe, UnwindSafe},
|
||||||
|
path::Path,
|
||||||
|
};
|
||||||
|
|
||||||
pub mod api;
|
pub mod api;
|
||||||
pub mod inspect;
|
pub mod inspect;
|
||||||
pub mod recover;
|
pub mod recover;
|
||||||
|
|
||||||
|
// Returns either a new file, if a path is given, or stdout, if no path is given.
|
||||||
|
pub(crate) fn outfile_or_stdout<P: AsRef<Path>>(
|
||||||
|
path: Option<P>,
|
||||||
|
) -> std::io::Result<Box<dyn Write + Send + Sync + Unpin + RefUnwindSafe + UnwindSafe>> {
|
||||||
|
if let Some(path) = path {
|
||||||
|
let f = File::create(path)?;
|
||||||
|
Ok(Box::new(f) as Box<_>)
|
||||||
|
} else {
|
||||||
|
Ok(Box::new(stdout()) as Box<_>)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -3,7 +3,6 @@ use std::io::{Read, Seek, SeekFrom, Write};
|
|||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use anyhow::{bail, format_err, Error};
|
use anyhow::{bail, format_err, Error};
|
||||||
use serde_json::Value;
|
|
||||||
|
|
||||||
use proxmox_router::cli::{CliCommand, CliCommandMap, CommandLineInterface};
|
use proxmox_router::cli::{CliCommand, CliCommandMap, CommandLineInterface};
|
||||||
use proxmox_schema::api;
|
use proxmox_schema::api;
|
||||||
@ -25,7 +24,7 @@ use pbs_tools::crypt_config::CryptConfig;
|
|||||||
type: String,
|
type: String,
|
||||||
},
|
},
|
||||||
chunks: {
|
chunks: {
|
||||||
description: "Path to the directorty that contains the chunks, usually <datastore>/.chunks.",
|
description: "Path to the directory that contains the chunks, usually <datastore>/.chunks.",
|
||||||
type: String,
|
type: String,
|
||||||
},
|
},
|
||||||
"keyfile": {
|
"keyfile": {
|
||||||
@ -38,7 +37,24 @@ use pbs_tools::crypt_config::CryptConfig;
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
optional: true,
|
optional: true,
|
||||||
default: false,
|
default: false,
|
||||||
}
|
},
|
||||||
|
"ignore-missing-chunks": {
|
||||||
|
description: "If a chunk is missing, warn and write 0-bytes instead to attempt partial recovery.",
|
||||||
|
type: Boolean,
|
||||||
|
optional: true,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
"ignore-corrupt-chunks": {
|
||||||
|
description: "If a chunk is corrupt, warn and write 0-bytes instead to attempt partial recovery.",
|
||||||
|
type: Boolean,
|
||||||
|
optional: true,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
"output-path": {
|
||||||
|
type: String,
|
||||||
|
description: "Output file path, defaults to `file` without extension, '-' means STDOUT.",
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)]
|
)]
|
||||||
@ -49,7 +65,9 @@ fn recover_index(
|
|||||||
chunks: String,
|
chunks: String,
|
||||||
keyfile: Option<String>,
|
keyfile: Option<String>,
|
||||||
skip_crc: bool,
|
skip_crc: bool,
|
||||||
_param: Value,
|
ignore_missing_chunks: bool,
|
||||||
|
ignore_corrupt_chunks: bool,
|
||||||
|
output_path: Option<String>,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let file_path = Path::new(&file);
|
let file_path = Path::new(&file);
|
||||||
let chunks_path = Path::new(&chunks);
|
let chunks_path = Path::new(&chunks);
|
||||||
@ -78,9 +96,16 @@ fn recover_index(
|
|||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
let output_filename = file_path.file_stem().unwrap().to_str().unwrap();
|
let output_path = output_path.unwrap_or_else(|| {
|
||||||
let output_path = Path::new(output_filename);
|
let filename = file_path.file_stem().unwrap().to_str().unwrap();
|
||||||
let mut output_file = File::create(output_path)
|
filename.to_string()
|
||||||
|
});
|
||||||
|
|
||||||
|
let output_path = match output_path.as_str() {
|
||||||
|
"-" => None,
|
||||||
|
path => Some(path),
|
||||||
|
};
|
||||||
|
let mut output_file = crate::outfile_or_stdout(output_path)
|
||||||
.map_err(|e| format_err!("could not create output file - {}", e))?;
|
.map_err(|e| format_err!("could not create output file - {}", e))?;
|
||||||
|
|
||||||
let mut data = Vec::with_capacity(4 * 1024 * 1024);
|
let mut data = Vec::with_capacity(4 * 1024 * 1024);
|
||||||
@ -89,22 +114,78 @@ fn recover_index(
|
|||||||
let digest_str = hex::encode(chunk_digest);
|
let digest_str = hex::encode(chunk_digest);
|
||||||
let digest_prefix = &digest_str[0..4];
|
let digest_prefix = &digest_str[0..4];
|
||||||
let chunk_path = chunks_path.join(digest_prefix).join(digest_str);
|
let chunk_path = chunks_path.join(digest_prefix).join(digest_str);
|
||||||
let mut chunk_file = std::fs::File::open(&chunk_path)
|
|
||||||
.map_err(|e| format_err!("could not open chunk file - {}", e))?;
|
|
||||||
|
|
||||||
data.clear();
|
let create_zero_chunk = |msg: String| -> Result<(DataBlob, Option<&[u8; 32]>), Error> {
|
||||||
chunk_file.read_to_end(&mut data)?;
|
let info = index
|
||||||
let chunk_blob = DataBlob::from_raw(data.clone())?;
|
.chunk_info(pos)
|
||||||
|
.ok_or_else(|| format_err!("Couldn't read chunk info from index at {pos}"))?;
|
||||||
|
let size = info.size();
|
||||||
|
|
||||||
if !skip_crc {
|
eprintln!("WARN: chunk {:?} {}", chunk_path, msg);
|
||||||
chunk_blob.verify_crc()?;
|
eprintln!("WARN: replacing output file {:?} with '\\0'", info.range,);
|
||||||
}
|
|
||||||
|
|
||||||
output_file.write_all(
|
Ok((
|
||||||
chunk_blob
|
DataBlob::encode(&vec![0; size as usize], crypt_conf_opt.as_ref(), true)?,
|
||||||
.decode(crypt_conf_opt.as_ref(), Some(chunk_digest))?
|
None,
|
||||||
.as_slice(),
|
))
|
||||||
)?;
|
};
|
||||||
|
|
||||||
|
let (chunk_blob, chunk_digest) = match std::fs::File::open(&chunk_path) {
|
||||||
|
Ok(mut chunk_file) => {
|
||||||
|
data.clear();
|
||||||
|
chunk_file.read_to_end(&mut data)?;
|
||||||
|
|
||||||
|
// first chance for corrupt chunk - handling magic fails
|
||||||
|
DataBlob::from_raw(data.clone())
|
||||||
|
.map(|blob| (blob, Some(chunk_digest)))
|
||||||
|
.or_else(|err| {
|
||||||
|
if ignore_corrupt_chunks {
|
||||||
|
create_zero_chunk(format!("is corrupt - {err}"))
|
||||||
|
} else {
|
||||||
|
bail!("{err}");
|
||||||
|
}
|
||||||
|
})?
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
if ignore_missing_chunks && err.kind() == std::io::ErrorKind::NotFound {
|
||||||
|
create_zero_chunk("is missing".to_string())?
|
||||||
|
} else {
|
||||||
|
bail!("could not open chunk file - {}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// second chance - we need CRC to detect truncated chunks!
|
||||||
|
let crc_res = if skip_crc {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
chunk_blob.verify_crc()
|
||||||
|
};
|
||||||
|
|
||||||
|
let (chunk_blob, chunk_digest) = if let Err(crc_err) = crc_res {
|
||||||
|
if ignore_corrupt_chunks {
|
||||||
|
create_zero_chunk(format!("is corrupt - {crc_err}"))?
|
||||||
|
} else {
|
||||||
|
bail!("Error at chunk {:?} - {crc_err}", chunk_path);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
(chunk_blob, chunk_digest)
|
||||||
|
};
|
||||||
|
|
||||||
|
// third chance - decoding might fail (digest, compression, encryption)
|
||||||
|
let decoded = chunk_blob
|
||||||
|
.decode(crypt_conf_opt.as_ref(), chunk_digest)
|
||||||
|
.or_else(|err| {
|
||||||
|
if ignore_corrupt_chunks {
|
||||||
|
create_zero_chunk(format!("fails to decode - {err}"))?
|
||||||
|
.0
|
||||||
|
.decode(crypt_conf_opt.as_ref(), None)
|
||||||
|
} else {
|
||||||
|
bail!("Failed to decode chunk {:?} = {}", chunk_path, err);
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
|
output_file.write_all(decoded.as_slice())?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -10,6 +10,8 @@ mod dns;
|
|||||||
pub use dns::*;
|
pub use dns::*;
|
||||||
mod network;
|
mod network;
|
||||||
pub use network::*;
|
pub use network::*;
|
||||||
|
mod prune;
|
||||||
|
pub use prune::*;
|
||||||
mod remote;
|
mod remote;
|
||||||
pub use remote::*;
|
pub use remote::*;
|
||||||
mod sync;
|
mod sync;
|
||||||
|
242
src/bin/proxmox_backup_manager/prune.rs
Normal file
242
src/bin/proxmox_backup_manager/prune.rs
Normal file
@ -0,0 +1,242 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use anyhow::Error;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
use proxmox_router::{cli::*, ApiHandler, RpcEnvironment};
|
||||||
|
use proxmox_schema::api;
|
||||||
|
|
||||||
|
use pbs_api_types::{DataStoreConfig, PruneJobConfig, PruneJobOptions, JOB_ID_SCHEMA};
|
||||||
|
use pbs_config::prune;
|
||||||
|
|
||||||
|
use proxmox_backup::api2;
|
||||||
|
|
||||||
|
#[api(
|
||||||
|
input: {
|
||||||
|
properties: {
|
||||||
|
"output-format": {
|
||||||
|
schema: OUTPUT_FORMAT,
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)]
|
||||||
|
/// List all prune jobs
|
||||||
|
fn list_prune_jobs(param: Value, rpcenv: &mut dyn RpcEnvironment) -> Result<Value, Error> {
|
||||||
|
let output_format = get_output_format(¶m);
|
||||||
|
|
||||||
|
let info = &api2::config::prune::API_METHOD_LIST_PRUNE_JOBS;
|
||||||
|
let mut data = match info.handler {
|
||||||
|
ApiHandler::Sync(handler) => (handler)(param, info, rpcenv)?,
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let options = default_table_format_options()
|
||||||
|
.column(ColumnConfig::new("id"))
|
||||||
|
.column(ColumnConfig::new("disable"))
|
||||||
|
.column(ColumnConfig::new("store"))
|
||||||
|
.column(ColumnConfig::new("ns"))
|
||||||
|
.column(ColumnConfig::new("schedule"))
|
||||||
|
.column(ColumnConfig::new("max-depth"))
|
||||||
|
.column(ColumnConfig::new("keep-last"))
|
||||||
|
.column(ColumnConfig::new("keep-hourly"))
|
||||||
|
.column(ColumnConfig::new("keep-daily"))
|
||||||
|
.column(ColumnConfig::new("keep-weekly"))
|
||||||
|
.column(ColumnConfig::new("keep-monthly"))
|
||||||
|
.column(ColumnConfig::new("keep-yearly"));
|
||||||
|
|
||||||
|
format_and_print_result_full(&mut data, &info.returns, &output_format, &options);
|
||||||
|
|
||||||
|
Ok(Value::Null)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[api(
|
||||||
|
input: {
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
schema: JOB_ID_SCHEMA,
|
||||||
|
},
|
||||||
|
"output-format": {
|
||||||
|
schema: OUTPUT_FORMAT,
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)]
|
||||||
|
/// Show prune job configuration
|
||||||
|
fn show_prune_job(param: Value, rpcenv: &mut dyn RpcEnvironment) -> Result<Value, Error> {
|
||||||
|
let output_format = get_output_format(¶m);
|
||||||
|
|
||||||
|
let info = &api2::config::prune::API_METHOD_READ_PRUNE_JOB;
|
||||||
|
let mut data = match info.handler {
|
||||||
|
ApiHandler::Sync(handler) => (handler)(param, info, rpcenv)?,
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let options = default_table_format_options();
|
||||||
|
format_and_print_result_full(&mut data, &info.returns, &output_format, &options);
|
||||||
|
|
||||||
|
Ok(Value::Null)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn prune_job_commands() -> CommandLineInterface {
|
||||||
|
let cmd_def = CliCommandMap::new()
|
||||||
|
.insert("list", CliCommand::new(&API_METHOD_LIST_PRUNE_JOBS))
|
||||||
|
.insert(
|
||||||
|
"show",
|
||||||
|
CliCommand::new(&API_METHOD_SHOW_PRUNE_JOB)
|
||||||
|
.arg_param(&["id"])
|
||||||
|
.completion_cb("id", pbs_config::prune::complete_prune_job_id),
|
||||||
|
)
|
||||||
|
.insert(
|
||||||
|
"create",
|
||||||
|
CliCommand::new(&api2::config::prune::API_METHOD_CREATE_PRUNE_JOB)
|
||||||
|
.arg_param(&["id"])
|
||||||
|
.completion_cb("id", pbs_config::prune::complete_prune_job_id)
|
||||||
|
.completion_cb("schedule", pbs_config::datastore::complete_calendar_event)
|
||||||
|
.completion_cb("store", pbs_config::datastore::complete_datastore_name)
|
||||||
|
.completion_cb("ns", complete_prune_local_datastore_namespace),
|
||||||
|
)
|
||||||
|
.insert(
|
||||||
|
"update",
|
||||||
|
CliCommand::new(&api2::config::prune::API_METHOD_UPDATE_PRUNE_JOB)
|
||||||
|
.arg_param(&["id"])
|
||||||
|
.completion_cb("id", pbs_config::prune::complete_prune_job_id)
|
||||||
|
.completion_cb("schedule", pbs_config::datastore::complete_calendar_event)
|
||||||
|
.completion_cb("store", pbs_config::datastore::complete_datastore_name)
|
||||||
|
.completion_cb("ns", complete_prune_local_datastore_namespace),
|
||||||
|
)
|
||||||
|
.insert(
|
||||||
|
"remove",
|
||||||
|
CliCommand::new(&api2::config::prune::API_METHOD_DELETE_PRUNE_JOB)
|
||||||
|
.arg_param(&["id"])
|
||||||
|
.completion_cb("id", pbs_config::prune::complete_prune_job_id),
|
||||||
|
);
|
||||||
|
|
||||||
|
cmd_def.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
// shell completion helper
|
||||||
|
fn complete_prune_local_datastore_namespace(
|
||||||
|
_arg: &str,
|
||||||
|
param: &HashMap<String, String>,
|
||||||
|
) -> Vec<String> {
|
||||||
|
let mut list = Vec::new();
|
||||||
|
let mut rpcenv = CliEnvironment::new();
|
||||||
|
rpcenv.set_auth_id(Some(String::from("root@pam")));
|
||||||
|
|
||||||
|
let mut job: Option<PruneJobConfig> = None;
|
||||||
|
|
||||||
|
let store = param.get("store").map(|r| r.to_owned()).or_else(|| {
|
||||||
|
if let Some(id) = param.get("id") {
|
||||||
|
job = get_prune_job(id).ok();
|
||||||
|
if let Some(ref job) = job {
|
||||||
|
return Some(job.store.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(store) = store {
|
||||||
|
if let Ok(data) =
|
||||||
|
crate::api2::admin::namespace::list_namespaces(store, None, None, &mut rpcenv)
|
||||||
|
{
|
||||||
|
for item in data {
|
||||||
|
list.push(item.ns.name());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
list
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_prune_job(id: &str) -> Result<PruneJobConfig, Error> {
|
||||||
|
let (config, _digest) = prune::config()?;
|
||||||
|
|
||||||
|
config.lookup("prune", id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn update_to_prune_jobs_config() -> Result<(), Error> {
|
||||||
|
use pbs_config::datastore;
|
||||||
|
|
||||||
|
let _prune_lock = prune::lock_config()?;
|
||||||
|
let _datastore_lock = datastore::lock_config()?;
|
||||||
|
|
||||||
|
let (mut data, _digest) = prune::config()?;
|
||||||
|
let (mut storeconfig, _digest) = datastore::config()?;
|
||||||
|
|
||||||
|
for (store, entry) in storeconfig.sections.iter_mut() {
|
||||||
|
let ty = &entry.0;
|
||||||
|
|
||||||
|
if ty != "datastore" {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut config = match DataStoreConfig::deserialize(&entry.1) {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!("failed to parse config of store {store}: {err}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let options = PruneJobOptions {
|
||||||
|
keep: std::mem::take(&mut config.keep),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let schedule = config.prune_schedule.take();
|
||||||
|
|
||||||
|
entry.1 = serde_json::to_value(config)?;
|
||||||
|
|
||||||
|
let schedule = match schedule {
|
||||||
|
Some(s) => s,
|
||||||
|
None => {
|
||||||
|
if options.keeps_something() {
|
||||||
|
eprintln!(
|
||||||
|
"dropping prune job without schedule from datastore '{store}' in datastore.cfg"
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
eprintln!("ignoring empty prune job of datastore '{store}' in datastore.cfg");
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut id = format!("storeconfig-{store}");
|
||||||
|
id.truncate(32);
|
||||||
|
if data.sections.contains_key(&id) {
|
||||||
|
eprintln!("skipping existing converted prune job for datastore '{store}': {id}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !options.keeps_something() {
|
||||||
|
eprintln!("dropping empty prune job of datastore '{store}' in datastore.cfg");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let prune_config = PruneJobConfig {
|
||||||
|
id: id.clone(),
|
||||||
|
store: store.clone(),
|
||||||
|
disable: false,
|
||||||
|
comment: None,
|
||||||
|
schedule,
|
||||||
|
options,
|
||||||
|
};
|
||||||
|
|
||||||
|
let prune_config = serde_json::to_value(prune_config)?;
|
||||||
|
|
||||||
|
data.sections
|
||||||
|
.insert(id, ("prune".to_string(), prune_config));
|
||||||
|
|
||||||
|
eprintln!(
|
||||||
|
"migrating prune job of datastore '{store}' from datastore.cfg to prune.cfg jobs"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
prune::save_config(&data)?;
|
||||||
|
datastore::save_config(&storeconfig)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
@ -154,7 +154,7 @@ pub fn complete_acme_plugin(_arg: &str, _param: &HashMap<String, String>) -> Vec
|
|||||||
pub fn complete_acme_plugin_type(_arg: &str, _param: &HashMap<String, String>) -> Vec<String> {
|
pub fn complete_acme_plugin_type(_arg: &str, _param: &HashMap<String, String>) -> Vec<String> {
|
||||||
vec![
|
vec![
|
||||||
"dns".to_string(),
|
"dns".to_string(),
|
||||||
//"http".to_string(), // makes currently not realyl sense to create or the like
|
//"http".to_string(), // makes currently not really sense to create or the like
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,7 +60,7 @@ pub fn create_configdir() -> Result<(), Error> {
|
|||||||
|
|
||||||
match nix::unistd::mkdir(cfgdir, Mode::from_bits_truncate(0o700)) {
|
match nix::unistd::mkdir(cfgdir, Mode::from_bits_truncate(0o700)) {
|
||||||
Ok(()) => {}
|
Ok(()) => {}
|
||||||
Err(nix::Error::Sys(nix::errno::Errno::EEXIST)) => {
|
Err(nix::errno::Errno::EEXIST) => {
|
||||||
check_configdir_permissions()?;
|
check_configdir_permissions()?;
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
@ -40,11 +40,7 @@ pub fn do_garbage_collection_job(
|
|||||||
let status = worker.create_state(&result);
|
let status = worker.create_state(&result);
|
||||||
|
|
||||||
if let Err(err) = job.finish(status) {
|
if let Err(err) = job.finish(status) {
|
||||||
eprintln!(
|
eprintln!("could not finish job state for {}: {}", job.jobtype(), err);
|
||||||
"could not finish job state for {}: {}",
|
|
||||||
job.jobtype().to_string(),
|
|
||||||
err
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(email) = email {
|
if let Some(email) = email {
|
||||||
|
@ -112,24 +112,17 @@ where
|
|||||||
pub fn remove_state_file(jobtype: &str, jobname: &str) -> Result<(), Error> {
|
pub fn remove_state_file(jobtype: &str, jobname: &str) -> Result<(), Error> {
|
||||||
let mut path = get_path(jobtype, jobname);
|
let mut path = get_path(jobtype, jobname);
|
||||||
let _lock = get_lock(&path)?;
|
let _lock = get_lock(&path)?;
|
||||||
std::fs::remove_file(&path).map_err(|err| {
|
if let Err(err) = std::fs::remove_file(&path) {
|
||||||
format_err!(
|
if err.kind() != std::io::ErrorKind::NotFound {
|
||||||
"cannot remove statefile for {} - {}: {}",
|
bail!("cannot remove statefile for {jobtype} - {jobname}: {err}");
|
||||||
jobtype,
|
}
|
||||||
jobname,
|
}
|
||||||
err
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
path.set_extension("lck");
|
path.set_extension("lck");
|
||||||
// ignore errors
|
if let Err(err) = std::fs::remove_file(&path) {
|
||||||
let _ = std::fs::remove_file(&path).map_err(|err| {
|
if err.kind() != std::io::ErrorKind::NotFound {
|
||||||
format_err!(
|
bail!("cannot remove lockfile for {jobtype} - {jobname}: {err}");
|
||||||
"cannot remove lockfile for {} - {}: {}",
|
}
|
||||||
jobtype,
|
}
|
||||||
jobname,
|
|
||||||
err
|
|
||||||
)
|
|
||||||
});
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user