forked from proxmox-mirrors/proxmox-backup
Compare commits
38 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4be9fd98f2 | ||
|
|
abcfbac411 | ||
|
|
7016c96b28 | ||
|
|
f42b10bcfb | ||
|
|
3c27db8a53 | ||
|
|
b13f9efc5e | ||
|
|
d90d7ff230 | ||
|
|
7c7a05cf6b | ||
|
|
ca08eba0b3 | ||
|
|
c096925472 | ||
|
|
48c7a01008 | ||
|
|
0a9cb27e68 | ||
|
|
847d39c353 | ||
|
|
2a36f7016c | ||
|
|
e7beb64391 | ||
|
|
f37354cea2 | ||
|
|
aa829571cf | ||
|
|
d543ff8b27 | ||
|
|
d41a83268b | ||
|
|
5327a402a4 | ||
|
|
524e5c09a0 | ||
|
|
cc50ee772a | ||
|
|
cf8a658ea2 | ||
|
|
8ac8330818 | ||
|
|
9bcaa05731 | ||
|
|
c6d5c11639 | ||
|
|
cf9334627c | ||
|
|
f3d48c390b | ||
|
|
b6a34622ee | ||
|
|
6e5862770e | ||
|
|
48f936f1a8 | ||
|
|
d84d10125d | ||
|
|
566121ceb0 | ||
|
|
484f096250 | ||
|
|
241f4acb2a | ||
|
|
3122e40d94 | ||
|
|
c0368c11bf | ||
|
|
6b7ac16ebd |
134
Cargo.toml
134
Cargo.toml
@ -1,5 +1,5 @@
|
||||
[workspace.package]
|
||||
version = "4.2.0"
|
||||
version = "3.4.8"
|
||||
authors = [
|
||||
"Dietmar Maurer <dietmar@proxmox.com>",
|
||||
"Dominik Csapak <d.csapak@proxmox.com>",
|
||||
@ -53,59 +53,50 @@ path = "src/lib.rs"
|
||||
|
||||
[workspace.dependencies]
|
||||
# proxmox workspace
|
||||
proxmox-apt = { version = "0.99.7", features = [ "cache" ] }
|
||||
proxmox-apt-api-types = "2.0.5"
|
||||
proxmox-async = "0.5"
|
||||
proxmox-auth-api = "1.0.5"
|
||||
proxmox-base64 = "1"
|
||||
proxmox-apt = { version = "0.11.9", features = [ "cache" ] }
|
||||
proxmox-apt-api-types = "1.0.4"
|
||||
proxmox-async = "0.4"
|
||||
proxmox-auth-api = "0.4.9"
|
||||
proxmox-borrow = "1"
|
||||
proxmox-compression = "1.0.1"
|
||||
proxmox-config-digest = "1"
|
||||
proxmox-daemon = "1"
|
||||
proxmox-fuse = "3"
|
||||
proxmox-docgen = "1"
|
||||
proxmox-http = { version = "1.0.2", features = [ "client", "http-helpers", "api-types", "websocket" ] } # see below
|
||||
proxmox-human-byte = "1"
|
||||
proxmox-compression = "0.2"
|
||||
proxmox-config-digest = "0.1.0"
|
||||
proxmox-daemon = "0.1.0"
|
||||
proxmox-fuse = "0.1.3"
|
||||
proxmox-http = { version = "0.9.5", features = [ "client", "http-helpers", "websocket" ] } # see below
|
||||
proxmox-human-byte = "0.1"
|
||||
proxmox-io = "1.0.1" # tools and client use "tokio" feature
|
||||
proxmox-lang = "1.1"
|
||||
proxmox-log = "1"
|
||||
proxmox-ldap = "1"
|
||||
proxmox-metrics = "1"
|
||||
proxmox-network-api = "1"
|
||||
proxmox-network-types = "1.0.1"
|
||||
proxmox-notify = "1"
|
||||
proxmox-openid = "1"
|
||||
proxmox-parallel-handler = "1"
|
||||
proxmox-product-config = "1"
|
||||
proxmox-rate-limiter = "1.0.0"
|
||||
proxmox-rest-server = { version = "1.0.5", features = [ "templates" ] }
|
||||
proxmox-log = "0.2.6"
|
||||
proxmox-ldap = "0.2.1"
|
||||
proxmox-metrics = "0.3.1"
|
||||
proxmox-network-api = "0.1.6"
|
||||
proxmox-notify = "0.5.1"
|
||||
proxmox-openid = "0.10.0"
|
||||
proxmox-rest-server = { version = "0.8.10", features = [ "templates" ] }
|
||||
# some use "cli", some use "cli" and "server", pbs-config uses nothing
|
||||
proxmox-router = { version = "3.2.2", default-features = false }
|
||||
proxmox-rrd = "1"
|
||||
proxmox-router = { version = "3.0.0", default-features = false }
|
||||
proxmox-rrd = "0.4"
|
||||
proxmox-rrd-api-types = "1.0.2"
|
||||
proxmox-s3-client = { version = "1.4", features = [ "impl" ] }
|
||||
# everything but pbs-config and pbs-client use "api-macro"
|
||||
proxmox-schema = "5"
|
||||
proxmox-schema = "4"
|
||||
proxmox-section-config = "3"
|
||||
proxmox-serde = "1"
|
||||
proxmox-shared-cache = "1"
|
||||
proxmox-shared-memory = "1"
|
||||
proxmox-sortable-macro = "1"
|
||||
proxmox-subscription = { version = "1", features = [ "api-types" ] }
|
||||
proxmox-sys = "1"
|
||||
proxmox-systemd = "1.0.1"
|
||||
proxmox-tfa = { version = "6.0.3", features = [ "api", "api-types" ] }
|
||||
proxmox-serde = "0.1.1"
|
||||
proxmox-shared-cache = "0.1"
|
||||
proxmox-shared-memory = "0.3.0"
|
||||
proxmox-sortable-macro = "0.1.2"
|
||||
proxmox-subscription = { version = "0.5.0", features = [ "api-types" ] }
|
||||
proxmox-sys = "0.6.7"
|
||||
proxmox-systemd = "0.1"
|
||||
proxmox-tfa = { version = "5", features = [ "api", "api-types" ] }
|
||||
proxmox-time = "2"
|
||||
proxmox-upgrade-checks = "1"
|
||||
proxmox-uuid = { version = "1", features = [ "serde" ] }
|
||||
proxmox-worker-task = "1"
|
||||
pbs-api-types = "1.0.15"
|
||||
proxmox-worker-task = "0.1"
|
||||
pbs-api-types = "0.2.2"
|
||||
|
||||
# other proxmox crates
|
||||
pathpatterns = "1"
|
||||
proxmox-acme = "1.1"
|
||||
proxmox-acme-api = { version = "1.0.2", features = [ "impl" ] }
|
||||
pxar = "1.0.1"
|
||||
pathpatterns = "0.3"
|
||||
proxmox-acme = "0.5.3"
|
||||
pxar = "0.12.1"
|
||||
|
||||
# PBS workspace
|
||||
pbs-buildcfg = { path = "pbs-buildcfg" }
|
||||
@ -122,9 +113,10 @@ pbs-tools = { path = "pbs-tools" }
|
||||
anyhow = "1.0"
|
||||
async-trait = "0.1.56"
|
||||
apt-pkg-native = "0.3.2"
|
||||
base64 = "0.13"
|
||||
bitflags = "2.4"
|
||||
bytes = "1.0"
|
||||
cidr = "0.3"
|
||||
cidr = "0.2.1"
|
||||
crc32fast = "1"
|
||||
const_format = "0.2"
|
||||
crossbeam-channel = "0.5"
|
||||
@ -133,49 +125,46 @@ env_logger = "0.11"
|
||||
flate2 = "1.0"
|
||||
foreign-types = "0.3"
|
||||
futures = "0.3"
|
||||
h2 = { version = "0.4", features = [ "stream" ] }
|
||||
h2 = { version = "0.4", features = [ "legacy", "stream" ] }
|
||||
handlebars = "3.0"
|
||||
hex = "0.4.3"
|
||||
hickory-resolver = { version = "0.24.1", default-features = false, features = [ "system-config", "tokio-runtime" ] }
|
||||
http-body-util = "0.1"
|
||||
hyper-util = "0.1"
|
||||
hyper = { version = "1", features = [ "full" ] }
|
||||
hyper = { version = "0.14", features = [ "backports", "deprecated", "full" ] }
|
||||
libc = "0.2"
|
||||
log = "0.4.17"
|
||||
nix = "0.29"
|
||||
nix = "0.26.1"
|
||||
nom = "7"
|
||||
num-traits = "0.2"
|
||||
once_cell = "1.3.1"
|
||||
openssl = "0.10.40"
|
||||
parking_lot = "0.12"
|
||||
percent-encoding = "2.1"
|
||||
pin-project-lite = "0.2"
|
||||
regex = "1.5.5"
|
||||
rustyline = "14"
|
||||
rustyline = "9"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
siphasher = "0.3"
|
||||
syslog = "6"
|
||||
tar = "0.4"
|
||||
tempfile = "3.15.0"
|
||||
termcolor = "1.1.2"
|
||||
thiserror = "2"
|
||||
thiserror = "1.0"
|
||||
tokio = "1.6"
|
||||
tokio-openssl = "0.6.1"
|
||||
tokio-stream = "0.1.0"
|
||||
tokio-util = { version = "0.7", features = [ "io" ] }
|
||||
tracing = "0.1"
|
||||
tower-service = "0.3.0"
|
||||
udev = "0.9"
|
||||
udev = "0.4"
|
||||
url = "2.1"
|
||||
walkdir = "2"
|
||||
xdg = "2.2"
|
||||
zstd = "0.13"
|
||||
zstd-safe = "7"
|
||||
zstd = { version = "0.12", features = [ "bindgen" ] }
|
||||
zstd-safe = "6.0"
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
async-trait.workspace = true
|
||||
base64.workspace = true
|
||||
bytes.workspace = true
|
||||
cidr.workspace = true
|
||||
const_format.workspace = true
|
||||
@ -185,9 +174,7 @@ endian_trait.workspace = true
|
||||
futures.workspace = true
|
||||
h2.workspace = true
|
||||
hex.workspace = true
|
||||
http-body-util.workspace = true
|
||||
hyper.workspace = true
|
||||
hyper-util = { workspace = true, features = ["server", "server-auto", "server-graceful"] }
|
||||
libc.workspace = true
|
||||
log.workspace = true
|
||||
nix.workspace = true
|
||||
@ -220,28 +207,21 @@ proxmox-apt.workspace = true
|
||||
proxmox-apt-api-types.workspace = true
|
||||
proxmox-async.workspace = true
|
||||
proxmox-auth-api = { workspace = true, features = [ "api", "pam-authenticator" ] }
|
||||
proxmox-base64.workspace = true
|
||||
proxmox-compression.workspace = true
|
||||
proxmox-config-digest.workspace = true
|
||||
proxmox-daemon.workspace = true
|
||||
proxmox-docgen.workspace = true
|
||||
proxmox-http = { workspace = true, features = [ "body", "client-trait", "proxmox-async", "rate-limited-stream" ] } # pbs-client doesn't use these
|
||||
proxmox-http = { workspace = true, features = [ "client-trait", "proxmox-async", "rate-limited-stream" ] } # pbs-client doesn't use these
|
||||
proxmox-human-byte.workspace = true
|
||||
proxmox-io.workspace = true
|
||||
proxmox-lang.workspace = true
|
||||
proxmox-log.workspace = true
|
||||
proxmox-ldap.workspace = true
|
||||
proxmox-metrics.workspace = true
|
||||
proxmox-network-api = { workspace = true, features = [ "impl" ] }
|
||||
proxmox-network-types.workspace = true
|
||||
proxmox-network-api.workspace = true
|
||||
proxmox-notify = { workspace = true, features = [ "pbs-context" ] }
|
||||
proxmox-openid.workspace = true
|
||||
proxmox-product-config.workspace = true
|
||||
proxmox-parallel-handler.workspace = true
|
||||
proxmox-rate-limiter = { workspace = true, features = [ "shared-rate-limiter" ] }
|
||||
proxmox-rest-server = { workspace = true, features = [ "rate-limited-stream" ] }
|
||||
proxmox-router = { workspace = true, features = [ "cli", "server"] }
|
||||
proxmox-s3-client.workspace = true
|
||||
proxmox-schema = { workspace = true, features = [ "api-macro" ] }
|
||||
proxmox-section-config.workspace = true
|
||||
proxmox-serde = { workspace = true, features = [ "serde_json" ] }
|
||||
@ -253,14 +233,12 @@ proxmox-sys = { workspace = true, features = [ "timer" ] }
|
||||
proxmox-systemd.workspace = true
|
||||
proxmox-tfa.workspace = true
|
||||
proxmox-time.workspace = true
|
||||
proxmox-upgrade-checks.workspace = true
|
||||
proxmox-uuid.workspace = true
|
||||
proxmox-worker-task.workspace = true
|
||||
pbs-api-types.workspace = true
|
||||
|
||||
# in their respective repo
|
||||
proxmox-acme.workspace = true
|
||||
proxmox-acme-api.workspace = true
|
||||
pxar.workspace = true
|
||||
|
||||
# proxmox-backup workspace/internal crates
|
||||
@ -279,43 +257,31 @@ proxmox-rrd-api-types.workspace = true
|
||||
[patch.crates-io]
|
||||
#pbs-api-types = { path = "../proxmox/pbs-api-types" }
|
||||
#proxmox-acme = { path = "../proxmox/proxmox-acme" }
|
||||
#proxmox-acme-api = { path = "../proxmox/proxmox-acme-api" }
|
||||
#proxmox-api-macro = { path = "../proxmox/proxmox-api-macro" }
|
||||
#proxmox-apt = { path = "../proxmox/proxmox-apt" }
|
||||
#proxmox-apt-api-types = { path = "../proxmox/proxmox-apt-api-types" }
|
||||
#proxmox-async = { path = "../proxmox/proxmox-async" }
|
||||
#proxmox-auth-api = { path = "../proxmox/proxmox-auth-api" }
|
||||
#proxmox-base64 = { path = "../proxmox/proxmox-base64" }
|
||||
#proxmox-borrow = { path = "../proxmox/proxmox-borrow" }
|
||||
#proxmox-compression = { path = "../proxmox/proxmox-compression" }
|
||||
#proxmox-config-digest = { path = "../proxmox/proxmox-config-digest" }
|
||||
#proxmox-daemon = { path = "../proxmox/proxmox-daemon" }
|
||||
#proxmox-docgen = { path = "../proxmox/proxmox-docgen" }
|
||||
#proxmox-fuse = { path = "../proxmox-fuse" }
|
||||
#proxmox-http = { path = "../proxmox/proxmox-http" }
|
||||
#proxmox-http-error = { path = "../proxmox/proxmox-http-error" }
|
||||
#proxmox-human-byte = { path = "../proxmox/proxmox-human-byte" }
|
||||
#proxmox-io = { path = "../proxmox/proxmox-io" }
|
||||
#proxmox-lang = { path = "../proxmox/proxmox-lang" }
|
||||
#proxmox-ldap = { path = "../proxmox/proxmox-ldap" }
|
||||
#proxmox-log = { path = "../proxmox/proxmox-log" }
|
||||
#proxmox-ldap = { path = "../proxmox/proxmox-ldap" }
|
||||
#proxmox-metrics = { path = "../proxmox/proxmox-metrics" }
|
||||
#proxmox-network-api = { path = "../proxmox/proxmox-network-api" }
|
||||
#proxmox-network-types = { path = "../proxmox/proxmox-network-types" }
|
||||
#proxmox-notify = { path = "../proxmox/proxmox-notify" }
|
||||
#proxmox-openid = { path = "../proxmox/proxmox-openid" }
|
||||
#proxmox-parallel-handler = { path = "../proxmox/proxmox-parallel-handler" }
|
||||
#proxmox-product-config = { path = "../proxmox/proxmox-product-config" }
|
||||
#proxmox-rate-limiter = { path = "../proxmox/proxmox-rate-limiter" }
|
||||
#proxmox-rest-server = { path = "../proxmox/proxmox-rest-server" }
|
||||
#proxmox-router = { path = "../proxmox/proxmox-router" }
|
||||
#proxmox-rrd = { path = "../proxmox/proxmox-rrd" }
|
||||
#proxmox-rrd-api-types = { path = "../proxmox/proxmox-rrd-api-types" }
|
||||
#proxmox-s3-client = { path = "../proxmox/proxmox-s3-client" }
|
||||
#proxmox-schema = { path = "../proxmox/proxmox-schema" }
|
||||
#proxmox-section-config = { path = "../proxmox/proxmox-section-config" }
|
||||
#proxmox-sendmail = { path = "../proxmox/proxmox-sendmail" }
|
||||
#proxmox-serde = { path = "../proxmox/proxmox-serde" }
|
||||
#proxmox-shared-cache = { path = "../proxmox/proxmox-shared-cache" }
|
||||
#proxmox-shared-memory = { path = "../proxmox/proxmox-shared-memory" }
|
||||
#proxmox-sortable-macro = { path = "../proxmox/proxmox-sortable-macro" }
|
||||
#proxmox-subscription = { path = "../proxmox/proxmox-subscription" }
|
||||
@ -323,11 +289,9 @@ proxmox-rrd-api-types.workspace = true
|
||||
#proxmox-systemd = { path = "../proxmox/proxmox-systemd" }
|
||||
#proxmox-tfa = { path = "../proxmox/proxmox-tfa" }
|
||||
#proxmox-time = { path = "../proxmox/proxmox-time" }
|
||||
#proxmox-upgrade-checks = { path = "../proxmox/proxmox-upgrade-checks" }
|
||||
#proxmox-uuid = { path = "../proxmox/proxmox-uuid" }
|
||||
#proxmox-worker-task = { path = "../proxmox/proxmox-worker-task" }
|
||||
|
||||
#proxmox-fuse = {path = "../proxmox-fuse" }
|
||||
#pathpatterns = {path = "../pathpatterns" }
|
||||
#pxar = { path = "../pxar" }
|
||||
|
||||
|
||||
10
Makefile
10
Makefile
@ -195,6 +195,7 @@ $(COMPILED_BINS) $(COMPILEDIR)/dump-catalog-shell-cli $(COMPILEDIR)/docgen &:
|
||||
--bin proxmox-restore-daemon \
|
||||
--package proxmox-backup \
|
||||
--bin docgen \
|
||||
--bin pbs2to3 \
|
||||
--bin pbs3to4 \
|
||||
--bin proxmox-backup-api \
|
||||
--bin proxmox-backup-manager \
|
||||
@ -233,6 +234,7 @@ install: $(COMPILED_BINS) $(STATIC_BINS)
|
||||
$(foreach i,$(USR_SBIN), \
|
||||
install -m755 $(COMPILEDIR)/$(i) $(DESTDIR)$(SBINDIR)/ ; \
|
||||
install -m644 zsh-completions/_$(i) $(DESTDIR)$(ZSH_COMPL_DEST)/ ;)
|
||||
install -m755 $(COMPILEDIR)/pbs2to3 $(DESTDIR)$(SBINDIR)/
|
||||
install -m755 $(COMPILEDIR)/pbs3to4 $(DESTDIR)$(SBINDIR)/
|
||||
install -dm755 $(DESTDIR)$(LIBEXECDIR)/proxmox-backup
|
||||
install -dm755 $(DESTDIR)$(LIBEXECDIR)/proxmox-backup/file-restore
|
||||
@ -254,7 +256,7 @@ upload: $(SERVER_DEB) $(CLIENT_DEB) $(RESTORE_DEB) $(DOC_DEB) $(STATIC_CLIENT_DE
|
||||
# check if working directory is clean
|
||||
git diff --exit-code --stat && git diff --exit-code --stat --staged
|
||||
tar cf - $(SERVER_DEB) $(SERVER_DBG_DEB) $(DOC_DEB) $(CLIENT_DEB) $(CLIENT_DBG_DEB) \
|
||||
| ssh -X repoman@repo.proxmox.com upload --product pbs --dist $(UPLOAD_DIST) --arch $(DEB_HOST_ARCH)
|
||||
tar cf - $(CLIENT_DEB) $(CLIENT_DBG_DEB) | ssh -X repoman@repo.proxmox.com upload --product "pve,pmg,pbs-client" --dist $(UPLOAD_DIST) --arch $(DEB_HOST_ARCH)
|
||||
tar cf - $(STATIC_CLIENT_DEB) $(STATIC_CLIENT_DBG_DEB) | ssh -X repoman@repo.proxmox.com upload --product "pbs-client" --dist $(UPLOAD_DIST) --arch $(DEB_HOST_ARCH)
|
||||
tar cf - $(RESTORE_DEB) $(RESTORE_DBG_DEB) | ssh -X repoman@repo.proxmox.com upload --product "pve" --dist $(UPLOAD_DIST) --arch $(DEB_HOST_ARCH)
|
||||
| ssh -X repoman@repo.proxmox.com upload --product pbs --dist $(UPLOAD_DIST)
|
||||
tar cf - $(CLIENT_DEB) $(CLIENT_DBG_DEB) | ssh -X repoman@repo.proxmox.com upload --product "pve,pmg,pbs-client" --dist $(UPLOAD_DIST)
|
||||
tar cf - $(STATIC_CLIENT_DEB) $(STATIC_CLIENT_DBG_DEB) | ssh -X repoman@repo.proxmox.com upload --product "pbs-client" --dist $(UPLOAD_DIST)
|
||||
tar cf - $(RESTORE_DEB) $(RESTORE_DBG_DEB) | ssh -X repoman@repo.proxmox.com upload --product "pve" --dist $(UPLOAD_DIST)
|
||||
|
||||
19
README.rst
19
README.rst
@ -65,23 +65,6 @@ You are now able to build using the Makefile or cargo itself, e.g.::
|
||||
# # or for a non-package build
|
||||
# cargo build --all --release
|
||||
|
||||
Building the online documentation
|
||||
=================================
|
||||
|
||||
The online documentation can be build in HTML format as follows. First build the
|
||||
required binaries::
|
||||
|
||||
make docs
|
||||
|
||||
The previous step is only necessary once. Then the online documentation can be
|
||||
built or regenerated with::
|
||||
|
||||
make -C docs DEB_HOST_RUST_TYPE=x86_64-unknown-linux-gnu html
|
||||
|
||||
With the `rust target <https://doc.rust-lang.org/rustc/platform-support.html>`__
|
||||
of the host. The resulting documentation will be at ``docs/output/html/`` and
|
||||
can be open with your browser of choice or, e.g. ``xdg-open``.
|
||||
|
||||
Design Notes
|
||||
************
|
||||
|
||||
@ -146,7 +129,7 @@ a backup::
|
||||
|
||||
CHUNK_COUNT = BACKUP_SIZE / ACS
|
||||
|
||||
Here are some staticics from my developer workstation::
|
||||
Here are some staticics from my developer worstation::
|
||||
|
||||
Disk Usage: 65 GB
|
||||
Directories: 58971
|
||||
|
||||
1029
debian/changelog
vendored
1029
debian/changelog
vendored
File diff suppressed because it is too large
Load Diff
182
debian/control
vendored
182
debian/control
vendored
@ -1,11 +1,11 @@
|
||||
Source: rust-proxmox-backup
|
||||
Section: admin
|
||||
Priority: optional
|
||||
Build-Depends: debhelper (>= 12~),
|
||||
Build-Depends: bash-completion,
|
||||
cargo:native (>= 0.65.0~),
|
||||
debhelper (>= 12~),
|
||||
debhelper-compat (= 13),
|
||||
dh-cargo (>= 24),
|
||||
bash-completion,
|
||||
cargo:native (>= 0.65.0~),
|
||||
fonts-dejavu-core <!nodoc>,
|
||||
fonts-lato <!nodoc>,
|
||||
fonts-open-sans <!nodoc>,
|
||||
@ -16,9 +16,10 @@ Build-Depends: debhelper (>= 12~),
|
||||
libfuse3-dev,
|
||||
librust-anyhow-1+default-dev,
|
||||
librust-async-trait-0.1+default-dev (>= 0.1.56-~~),
|
||||
librust-base64-0.13+default-dev,
|
||||
librust-bitflags-2+default-dev (>= 2.4-~~),
|
||||
librust-bytes-1+default-dev,
|
||||
librust-cidr-0.3+default-dev,
|
||||
librust-cidr-0.2+default-dev (>= 0.2.1-~~),
|
||||
librust-const-format-0.2+default-dev,
|
||||
librust-crc32fast-1+default-dev,
|
||||
librust-crossbeam-channel-0.5+default-dev,
|
||||
@ -28,123 +29,99 @@ Build-Depends: debhelper (>= 12~),
|
||||
librust-foreign-types-0.3+default-dev,
|
||||
librust-futures-0.3+default-dev,
|
||||
librust-h2-0.4+default-dev,
|
||||
librust-h2-0.4+legacy-dev,
|
||||
librust-h2-0.4+stream-dev,
|
||||
librust-hex-0.4+default-dev (>= 0.4.3-~~),
|
||||
librust-hex-0.4+serde-dev (>= 0.4.3-~~),
|
||||
librust-hickory-resolver-0.24+system-config-dev (>= 0.24.1-~~),
|
||||
librust-hickory-resolver-0.24+tokio-runtime-dev (>= 0.24.1-~~),
|
||||
librust-http-body-util-0.1+default-dev,
|
||||
librust-hyper-1+default-dev,
|
||||
librust-hyper-1+full-dev,
|
||||
librust-hyper-util-0.1+client-dev,
|
||||
librust-hyper-util-0.1+client-legacy-dev,
|
||||
librust-hyper-util-0.1+default-dev,
|
||||
librust-hyper-util-0.1+http1-dev,
|
||||
librust-hyper-util-0.1+http2-dev,
|
||||
librust-hyper-util-0.1+server-auto-dev,
|
||||
librust-hyper-util-0.1+server-dev,
|
||||
librust-hyper-util-0.1+server-graceful-dev,
|
||||
librust-hyper-util-0.1+service-dev,
|
||||
librust-hyper-util-0.1+tokio-dev,
|
||||
librust-hyper-0.14+backports-dev,
|
||||
librust-hyper-0.14+default-dev,
|
||||
librust-hyper-0.14+deprecated-dev,
|
||||
librust-hyper-0.14+full-dev,
|
||||
librust-libc-0.2+default-dev,
|
||||
librust-log-0.4+default-dev (>= 0.4.17-~~),
|
||||
librust-nix-0.29+default-dev,
|
||||
librust-nix-0.29+feature-dev,
|
||||
librust-nix-0.29+mount-dev,
|
||||
librust-nix-0.29+reboot-dev,
|
||||
librust-nix-0.26+default-dev (>= 0.26.1-~~),
|
||||
librust-nom-7+default-dev,
|
||||
librust-num-traits-0.2+default-dev,
|
||||
librust-once-cell-1+default-dev (>= 1.3.1-~~),
|
||||
librust-openssl-0.10+default-dev (>= 0.10.40-~~),
|
||||
librust-pathpatterns-1+default-dev,
|
||||
librust-pbs-api-types-1+default-dev (>= 1.0.15-~~),
|
||||
librust-pathpatterns-0.3+default-dev,
|
||||
librust-pbs-api-types-0.2+default-dev (>= 0.2.2-~~),
|
||||
librust-percent-encoding-2+default-dev (>= 2.1-~~),
|
||||
librust-pin-project-lite-0.2+default-dev,
|
||||
librust-proxmox-acme-1+default-dev (>= 1.1-~~),
|
||||
librust-proxmox-acme-api-1+default-dev (>= 1.0.2-~~),
|
||||
librust-proxmox-acme-api-1+impl-dev (>= 1.0.2-~~),
|
||||
librust-proxmox-apt-0.99+cache-dev,
|
||||
librust-proxmox-apt-0.99+default-dev (>= 0.99.7-~~),
|
||||
librust-proxmox-apt-api-types-2+default-dev (>= 2.0.5-~~),
|
||||
librust-proxmox-async-0.5+default-dev,
|
||||
librust-proxmox-auth-api-1+api-dev (>= 1.0.5-~~),
|
||||
librust-proxmox-auth-api-1+default-dev (>= 1.0.5-~~),
|
||||
librust-proxmox-auth-api-1+pam-authenticator-dev (>= 1.0.5-~~),
|
||||
librust-proxmox-base64-1+default-dev,
|
||||
librust-proxmox-acme-0.5+default-dev (>= 0.5.3-~~),
|
||||
librust-proxmox-apt-0.11+cache-dev,
|
||||
librust-proxmox-apt-0.11+default-dev (>= 0.11.9-~~),
|
||||
librust-proxmox-apt-api-types-1+default-dev (>= 1.0.4-~~),
|
||||
librust-proxmox-async-0.4+default-dev,
|
||||
librust-proxmox-auth-api-0.4+api-dev (>= 0.4.9-~~),
|
||||
librust-proxmox-auth-api-0.4+default-dev (>= 0.4.9-~~),
|
||||
librust-proxmox-auth-api-0.4+pam-authenticator-dev (>= 0.4.9-~~),
|
||||
librust-proxmox-borrow-1+default-dev,
|
||||
librust-proxmox-compression-1+default-dev (>= 1.0.1-~~),
|
||||
librust-proxmox-config-digest-1+default-dev,
|
||||
librust-proxmox-daemon-1+default-dev,
|
||||
librust-proxmox-docgen-1+default-dev,
|
||||
librust-proxmox-fuse-3+default-dev,
|
||||
librust-proxmox-http-1+body-dev (>= 1.0.2-~~),
|
||||
librust-proxmox-http-1+client-dev (>= 1.0.2-~~),
|
||||
librust-proxmox-http-1+client-trait-dev (>= 1.0.2-~~),
|
||||
librust-proxmox-http-1+default-dev (>= 1.0.2-~~),
|
||||
librust-proxmox-http-1+http-helpers-dev (>= 1.0.2-~~),
|
||||
librust-proxmox-http-1+proxmox-async-dev (>= 1.0.2-~~),
|
||||
librust-proxmox-http-1+rate-limited-stream-dev (>= 1.0.2-~~),
|
||||
librust-proxmox-http-1+websocket-dev (>= 1.0.2-~~),
|
||||
librust-proxmox-human-byte-1+default-dev,
|
||||
librust-proxmox-compression-0.2+default-dev,
|
||||
librust-proxmox-config-digest-0.1+default-dev,
|
||||
librust-proxmox-daemon-0.1+default-dev,
|
||||
librust-proxmox-fuse-0.1+default-dev (>= 0.1.3-~~),
|
||||
librust-proxmox-http-0.9+client-dev (>= 0.9.5-~~),
|
||||
librust-proxmox-http-0.9+client-trait-dev (>= 0.9.5-~~),
|
||||
librust-proxmox-http-0.9+default-dev (>= 0.9.5-~~),
|
||||
librust-proxmox-http-0.9+http-helpers-dev (>= 0.9.5-~~),
|
||||
librust-proxmox-http-0.9+proxmox-async-dev (>= 0.9.5-~~),
|
||||
librust-proxmox-http-0.9+rate-limited-stream-dev (>= 0.9.5-~~),
|
||||
librust-proxmox-http-0.9+rate-limiter-dev (>= 0.9.5-~~),
|
||||
librust-proxmox-http-0.9+websocket-dev (>= 0.9.5-~~),
|
||||
librust-proxmox-human-byte-0.1+default-dev,
|
||||
librust-proxmox-io-1+default-dev (>= 1.0.1-~~),
|
||||
librust-proxmox-io-1+tokio-dev (>= 1.0.1-~~),
|
||||
librust-proxmox-lang-1+default-dev (>= 1.1-~~),
|
||||
librust-proxmox-ldap-1+default-dev,
|
||||
librust-proxmox-log-1+default-dev,
|
||||
librust-proxmox-metrics-1+default-dev,
|
||||
librust-proxmox-network-api-1+default-dev,
|
||||
librust-proxmox-network-api-1+impl-dev,
|
||||
librust-proxmox-network-types-1+default-dev (>= 1.0.1-~~),
|
||||
librust-proxmox-notify-1+default-dev,
|
||||
librust-proxmox-notify-1+pbs-context-dev,
|
||||
librust-proxmox-openid-1+default-dev,
|
||||
librust-proxmox-product-config-1+default-dev,
|
||||
librust-proxmox-rate-limiter-1+default-dev (>= 1.0.0-~~),
|
||||
librust-proxmox-rest-server-1+default-dev (>= 1.0.5-~~),
|
||||
librust-proxmox-rest-server-1+rate-limited-stream-dev (>= 1.0.5-~~),
|
||||
librust-proxmox-rest-server-1+templates-dev (>= 1.0.5-~~),
|
||||
librust-proxmox-router-3+cli-dev (>= 3.2.2-~~),
|
||||
librust-proxmox-router-3+server-dev (>= 3.2.2-~~),
|
||||
librust-proxmox-rrd-1+default-dev,
|
||||
librust-proxmox-ldap-0.2+default-dev (>= 0.2.1-~~),
|
||||
librust-proxmox-log-0.2+default-dev (>= 0.2.6-~~),
|
||||
librust-proxmox-metrics-0.3+default-dev (>= 0.3.1-~~),
|
||||
librust-proxmox-network-api-0.1-dev (>= 0.1.6-~~),
|
||||
librust-proxmox-notify-0.5+default-dev (>= 0.5.1-~~),
|
||||
librust-proxmox-notify-0.5+pbs-context-dev (>= 0.5.1-~~),
|
||||
librust-proxmox-openid-0.10+default-dev,
|
||||
librust-proxmox-rest-server-0.8+default-dev (>= 0.8.10-~~),
|
||||
librust-proxmox-rest-server-0.8+rate-limited-stream-dev (>= 0.8.10-~~),
|
||||
librust-proxmox-rest-server-0.8+templates-dev (>= 0.8.10-~~),
|
||||
librust-proxmox-router-3+cli-dev,
|
||||
librust-proxmox-router-3+server-dev,
|
||||
librust-proxmox-rrd-0.4+default-dev,
|
||||
librust-proxmox-rrd-api-types-1+default-dev (>= 1.0.2-~~),
|
||||
librust-proxmox-parallel-handler-1+default-dev,
|
||||
librust-proxmox-s3-client-1+default-dev (>= 1.4.0-~~),
|
||||
librust-proxmox-s3-client-1+impl-dev (>= 1.4.0-~~),
|
||||
librust-proxmox-schema-5+api-macro-dev,
|
||||
librust-proxmox-schema-5+default-dev,
|
||||
librust-proxmox-schema-4+api-macro-dev,
|
||||
librust-proxmox-schema-4+default-dev,
|
||||
librust-proxmox-section-config-3+default-dev,
|
||||
librust-proxmox-serde-1+default-dev,
|
||||
librust-proxmox-serde-1+serde-json-dev,
|
||||
librust-proxmox-shared-cache-1+default-dev,
|
||||
librust-proxmox-shared-memory-1+default-dev,
|
||||
librust-proxmox-sortable-macro-1+default-dev,
|
||||
librust-proxmox-subscription-1+api-types-dev,
|
||||
librust-proxmox-subscription-1+default-dev,
|
||||
librust-proxmox-sys-1+acl-dev,
|
||||
librust-proxmox-sys-1+crypt-dev,
|
||||
librust-proxmox-sys-1+default-dev,
|
||||
librust-proxmox-sys-1+logrotate-dev,
|
||||
librust-proxmox-sys-1+timer-dev,
|
||||
librust-proxmox-systemd-1+default-dev (>= 1.0.1-~~),
|
||||
librust-proxmox-tfa-6+api-dev (>= 6.0.3-~~),
|
||||
librust-proxmox-tfa-6+api-types-dev (>= 6.0.3-~~),
|
||||
librust-proxmox-tfa-6+default-dev (>= 6.0.3-~~),
|
||||
librust-proxmox-serde-0.1+default-dev (>= 0.1.1-~~),
|
||||
librust-proxmox-serde-0.1+serde-json-dev (>= 0.1.1-~~),
|
||||
librust-proxmox-shared-cache-0.1+default-dev,
|
||||
librust-proxmox-shared-memory-0.3+default-dev,
|
||||
librust-proxmox-sortable-macro-0.1+default-dev (>= 0.1.2-~~),
|
||||
librust-proxmox-subscription-0.5+api-types-dev,
|
||||
librust-proxmox-subscription-0.5+default-dev,
|
||||
librust-proxmox-sys-0.6+acl-dev (>= 0.6.7-~~),
|
||||
librust-proxmox-sys-0.6+crypt-dev (>= 0.6.7-~~),
|
||||
librust-proxmox-sys-0.6+default-dev (>= 0.6.7-~~),
|
||||
librust-proxmox-sys-0.6+logrotate-dev (>= 0.6.7-~~),
|
||||
librust-proxmox-sys-0.6+timer-dev (>= 0.6.7-~~),
|
||||
librust-proxmox-systemd-0.1+default-dev,
|
||||
librust-proxmox-tfa-5+api-dev,
|
||||
librust-proxmox-tfa-5+api-types-dev,
|
||||
librust-proxmox-tfa-5+default-dev,
|
||||
librust-proxmox-time-2+default-dev,
|
||||
librust-proxmox-upgrade-checks-1+default-dev,
|
||||
librust-proxmox-uuid-1+default-dev,
|
||||
librust-proxmox-uuid-1+serde-dev,
|
||||
librust-proxmox-worker-task-1+default-dev,
|
||||
librust-pxar-1+default-dev (>= 1.0.1-~~),
|
||||
librust-proxmox-worker-task-0.1+default-dev,
|
||||
librust-pxar-0.12+default-dev (>= 0.12.1-~~),
|
||||
librust-regex-1+default-dev (>= 1.5.5-~~),
|
||||
librust-rustyline-14+default-dev,
|
||||
librust-rustyline-9+default-dev,
|
||||
librust-serde-1+default-dev,
|
||||
librust-serde-1+derive-dev,
|
||||
librust-serde-json-1+default-dev,
|
||||
librust-syslog-6+default-dev,
|
||||
librust-tar-0.4+default-dev,
|
||||
librust-termcolor-1+default-dev (>= 1.1.2-~~),
|
||||
librust-tempfile-3+default-dev (>= 3.15.0),
|
||||
librust-thiserror-2+default-dev,
|
||||
librust-thiserror-1+default-dev,
|
||||
librust-tokio-1+default-dev (>= 1.6-~~),
|
||||
librust-tokio-1+fs-dev (>= 1.6-~~),
|
||||
librust-tokio-1+io-std-dev (>= 1.6-~~),
|
||||
@ -165,23 +142,24 @@ Build-Depends: debhelper (>= 12~),
|
||||
librust-tokio-util-0.7+io-dev,
|
||||
librust-tower-service-0.3+default-dev,
|
||||
librust-tracing-0.1+default-dev,
|
||||
librust-udev-0.9+default-dev,
|
||||
librust-udev-0.4+default-dev,
|
||||
librust-url-2+default-dev (>= 2.1-~~),
|
||||
librust-walkdir-2+default-dev,
|
||||
librust-xdg-2+default-dev (>= 2.2-~~),
|
||||
librust-zstd-0.13+default-dev,
|
||||
librust-zstd-safe-7+default-dev,
|
||||
librust-zstd-0.12+bindgen-dev,
|
||||
librust-zstd-0.12+default-dev,
|
||||
librust-zstd-safe-6+default-dev,
|
||||
libsgutils2-dev,
|
||||
libstd-rust-dev,
|
||||
libsystemd-dev (>= 246-~~),
|
||||
patchelf,
|
||||
proxmox-biome,
|
||||
proxmox-widget-toolkit-dev <!nodoc>,
|
||||
proxmox-biome,
|
||||
python3-docutils,
|
||||
python3-pygments,
|
||||
python3-sphinx <!nodoc>,
|
||||
rsync,
|
||||
rustc:native (>= 1.81),
|
||||
rustc:native,
|
||||
texlive-fonts-extra <!nodoc>,
|
||||
texlive-fonts-recommended <!nodoc>,
|
||||
texlive-xetex <!nodoc>,
|
||||
@ -196,24 +174,20 @@ Rules-Requires-Root: binary-targets
|
||||
|
||||
Package: proxmox-backup-server
|
||||
Architecture: any
|
||||
Depends: dmidecode,
|
||||
fonts-font-awesome,
|
||||
Depends: fonts-font-awesome,
|
||||
gdisk,
|
||||
iproute2,
|
||||
libjs-extjs (>= 7~),
|
||||
libjs-qrcodejs (>= 1.20201119),
|
||||
libproxmox-acme-plugins,
|
||||
libsgutils2-1.48,
|
||||
libsgutils2-1.46-2,
|
||||
libzstd1 (>= 1.3.8),
|
||||
lvm2,
|
||||
openssh-server,
|
||||
pbs-i18n,
|
||||
pciutils,
|
||||
postfix | mail-transport-agent,
|
||||
proxmox-backup-docs,
|
||||
proxmox-mini-journalreader,
|
||||
proxmox-termproxy (>= 2.0.3),
|
||||
proxmox-widget-toolkit (>= 5.1.1),
|
||||
proxmox-widget-toolkit (>= 4.3.3),
|
||||
pve-xtermjs (>= 4.7.0-1),
|
||||
sg3-utils,
|
||||
smartmontools,
|
||||
|
||||
31
debian/copyright
vendored
31
debian/copyright
vendored
@ -1,19 +1,16 @@
|
||||
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
||||
Copyright (C) 2019 - 2025 Proxmox Server Solutions GmbH
|
||||
|
||||
Files: *
|
||||
Copyright: 2019-2026 Proxmox Server Solutions GmbH <support@proxmox.com>
|
||||
License: AGPL-3+
|
||||
This software is written by Proxmox Server Solutions GmbH <support@proxmox.com>
|
||||
|
||||
License: AGPL-3+
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
.
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
.
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
11
debian/lintian-overrides
vendored
11
debian/lintian-overrides
vendored
@ -1,8 +1,9 @@
|
||||
proxmox-backup-server: elevated-privileges 4755 root/root [usr/lib/*/proxmox-backup/sg-tape-cmd]
|
||||
proxmox-backup-server: mail-transport-agent-dependency-does-not-specify-default-mta
|
||||
proxmox-backup-server: package-installs-apt-sources [etc/apt/sources.list.d/pbs-enterprise.sources]
|
||||
proxmox-backup-server: systemd-service-file-refers-to-unusual-wantedby-target getty.target [usr/lib/systemd/system/proxmox-backup-banner.service]
|
||||
proxmox-backup-server: uses-dpkg-database-directly [usr/lib/*/proxmox-backup/proxmox-backup-api]
|
||||
proxmox-backup-server: uses-dpkg-database-directly [usr/lib/*/proxmox-backup/proxmox-backup-proxy]
|
||||
proxmox-backup-server: package-installs-apt-sources [etc/apt/sources.list.d/pbs-enterprise.list]
|
||||
proxmox-backup-server: elevated-privileges 4755 root/root [usr/lib/x86_64-linux-gnu/proxmox-backup/sg-tape-cmd]
|
||||
proxmox-backup-server: systemd-service-file-refers-to-unusual-wantedby-target getty.target [lib/systemd/system/proxmox-backup-banner.service]
|
||||
proxmox-backup-server: uses-dpkg-database-directly [usr/lib/x86_64-linux-gnu/proxmox-backup/proxmox-backup-api]
|
||||
proxmox-backup-server: uses-dpkg-database-directly [usr/lib/x86_64-linux-gnu/proxmox-backup/proxmox-backup-proxy]
|
||||
proxmox-backup-server: uses-dpkg-database-directly [usr/sbin/pbs2to3]
|
||||
proxmox-backup-server: uses-dpkg-database-directly [usr/sbin/pbs3to4]
|
||||
proxmox-backup-server: uses-dpkg-database-directly [usr/sbin/proxmox-backup-debug]
|
||||
|
||||
60
debian/postinst
vendored
60
debian/postinst
vendored
@ -4,18 +4,19 @@ set -e
|
||||
|
||||
#DEBHELPER#
|
||||
|
||||
update_sync_job() {
|
||||
job="$1"
|
||||
|
||||
echo "Updating sync job '$job' to make old 'remove-vanished' default explicit.."
|
||||
proxmox-backup-manager sync-job update "$job" --remove-vanished true \
|
||||
|| echo "Failed, please check sync.cfg manually!"
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
configure)
|
||||
# need to have user backup in the tape group
|
||||
usermod -a -G tape backup
|
||||
|
||||
# FIXME: remove after beta is over and add hunk to actively remove the repo
|
||||
BETA_SOURCES="/etc/apt/sources.list.d/pbs-test-for-beta.sources"
|
||||
if test -f "$BETA_SOURCES" && dpkg --compare-versions "$2" 'lt' '4.0.11-1' && dpkg --compare-versions "$2" 'gt' '4.0~~'; then
|
||||
printf "\nNOTE: Remove the pbs-test repository, which was added during the beta phase.\nYou can (re-)add repositories on the web UI (Administration -> Repositories)\n\n"
|
||||
rm -v "$BETA_SOURCES" || true
|
||||
fi
|
||||
|
||||
# modeled after dh_systemd_start output
|
||||
systemctl --system daemon-reload >/dev/null || true
|
||||
if [ -n "$2" ]; then
|
||||
@ -32,25 +33,50 @@ 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
|
||||
fi
|
||||
|
||||
# TODO: remove with 4.1+, this was just exposed on internal testing.
|
||||
if dpkg --compare-versions "$2" 'lt' '4.0.5~'; then
|
||||
if [ -e /etc/proxmox-backup/s3.cfg ]; then
|
||||
sed -i 's/^s3client:/s3-endpoint:/' /etc/proxmox-backup/s3.cfg \
|
||||
|| echo "Failed to rename 's3client' config section-type to 's3-endpoint' in /etc/proxmox-backup/s3.cfg."
|
||||
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.3.1~' && test -e /etc/proxmox-backup/.datastore.lck; then
|
||||
lock_user="$(stat --format '%U' /etc/proxmox-backup/.datastore.lck)"
|
||||
if [ "${lock_user}" != "backup" ]; then
|
||||
echo "updating /etc/proxmox-backup/.datastore.lck from wrong user '${lock_user}' to 'backup'"
|
||||
chown backup:backup /etc/proxmox-backup/.datastore.lck \
|
||||
|| printf "Failed to fix datastore.lck user, please retry manually with:\n\n\t%s\n\n" \
|
||||
"chown backup:backup /etc/proxmox-backup/.datastore.lck"
|
||||
fi
|
||||
fi
|
||||
|
||||
# TODO: remove with PBS 5+
|
||||
if dpkg --compare-versions "$2" 'lt' '4.0.2~'; then
|
||||
proxmox-backup-manager migrate-config default-notification-mode \
|
||||
|| echo "Failed migrate tape-job/datastore notification mode, please check manually"
|
||||
if dpkg --compare-versions "$2" 'lt' '2.1.3~' && test -e /etc/proxmox-backup/sync.cfg; then
|
||||
prev_job=""
|
||||
|
||||
# read from HERE doc because POSIX sh limitations
|
||||
while read -r key value; do
|
||||
if test "$key" = "sync:"; then
|
||||
if test -n "$prev_job"; then
|
||||
# previous job doesn't have an explicit value
|
||||
update_sync_job "$prev_job"
|
||||
fi
|
||||
prev_job=$value
|
||||
else
|
||||
prev_job=""
|
||||
fi
|
||||
done <<EOF
|
||||
$(grep -e '^sync:' -e 'remove-vanished' /etc/proxmox-backup/sync.cfg)
|
||||
EOF
|
||||
if test -n "$prev_job"; then
|
||||
# last job doesn't have an explicit value
|
||||
update_sync_job "$prev_job"
|
||||
fi
|
||||
fi
|
||||
|
||||
if dpkg --compare-versions "$2" 'lt' '3.3.5~'; then
|
||||
# ensure old locking is used by the daemon until a reboot happened
|
||||
touch "/run/proxmox-backup/old-locking"
|
||||
fi
|
||||
|
||||
fi
|
||||
;;
|
||||
|
||||
|
||||
@ -1,2 +0,0 @@
|
||||
proxmox-backup-client-static: embedded-library zlib [usr/bin/proxmox-backup-client]
|
||||
proxmox-backup-client-static: embedded-library zlib [usr/bin/pxar]
|
||||
2
debian/proxmox-backup-file-restore.install
vendored
2
debian/proxmox-backup-file-restore.install
vendored
@ -1,4 +1,4 @@
|
||||
usr/bin/proxmox-file-restore
|
||||
usr/lib/${DEB_HOST_MULTIARCH}/proxmox-backup/file-restore/proxmox-restore-daemon
|
||||
usr/lib/x86_64-linux-gnu/proxmox-backup/file-restore/proxmox-restore-daemon
|
||||
usr/share/man/man1/proxmox-file-restore.1
|
||||
usr/share/zsh/vendor-completions/_proxmox-file-restore
|
||||
|
||||
14
debian/proxmox-backup-file-restore.postinst
vendored
14
debian/proxmox-backup-file-restore.postinst
vendored
@ -3,20 +3,8 @@
|
||||
set -e
|
||||
|
||||
update_initramfs() {
|
||||
|
||||
ARCH="${DPKG_MAINTSCRIPT_ARCH:-$(dpkg --print-architecture)}"
|
||||
case "$ARCH" in
|
||||
amd64) HOST_MULTIARCH="x86_64-linux-gnu" ;;
|
||||
arm64) HOST_MULTIARCH="aarch64-linux-gnu" ;;
|
||||
riscv64) HOST_MULTIARCH="riscv64-linux-gnu" ;;
|
||||
*)
|
||||
echo "Error: Unsupported architecture '$ARCH', amd64, arm64, and riscv64 are supported." >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# regenerate initramfs for single file restore VM
|
||||
INST_PATH="/usr/lib/${HOST_MULTIARCH}/proxmox-backup/file-restore"
|
||||
INST_PATH="/usr/lib/x86_64-linux-gnu/proxmox-backup/file-restore"
|
||||
CACHE_PATH="/var/cache/proxmox-backup/file-restore-initramfs.img"
|
||||
CACHE_PATH_DBG="/var/cache/proxmox-backup/file-restore-initramfs-debug.img"
|
||||
|
||||
|
||||
31
debian/proxmox-backup-server.install
vendored
31
debian/proxmox-backup-server.install
vendored
@ -1,20 +1,20 @@
|
||||
etc/pbs-enterprise.sources /etc/apt/sources.list.d/
|
||||
etc/pbs-network-config-commit.service /usr/lib/systemd/system/
|
||||
etc/proxmox-backup-banner.service /usr/lib/systemd/system/
|
||||
etc/proxmox-backup-daily-update.service /usr/lib/systemd/system/
|
||||
etc/proxmox-backup-daily-update.timer /usr/lib/systemd/system/
|
||||
etc/proxmox-backup-proxy.service /usr/lib/systemd/system/
|
||||
etc/proxmox-backup.service /usr/lib/systemd/system/
|
||||
etc/removable-device-attach@.service /usr/lib/systemd/system/
|
||||
etc/run-proxmox\x2dbackup.mount /usr/lib/systemd/system/
|
||||
etc/pbs-enterprise.list /etc/apt/sources.list.d/
|
||||
etc/pbs-network-config-commit.service /lib/systemd/system/
|
||||
etc/proxmox-backup-banner.service /lib/systemd/system/
|
||||
etc/proxmox-backup-daily-update.service /lib/systemd/system/
|
||||
etc/proxmox-backup-daily-update.timer /lib/systemd/system/
|
||||
etc/proxmox-backup-proxy.service /lib/systemd/system/
|
||||
etc/proxmox-backup.service /lib/systemd/system/
|
||||
etc/removable-device-attach@.service /lib/systemd/system/
|
||||
usr/bin/pmt
|
||||
usr/bin/pmtx
|
||||
usr/bin/proxmox-tape
|
||||
usr/lib/${DEB_HOST_MULTIARCH}/proxmox-backup/proxmox-backup-api
|
||||
usr/lib/${DEB_HOST_MULTIARCH}/proxmox-backup/proxmox-backup-banner
|
||||
usr/lib/${DEB_HOST_MULTIARCH}/proxmox-backup/proxmox-backup-proxy
|
||||
usr/lib/${DEB_HOST_MULTIARCH}/proxmox-backup/proxmox-daily-update
|
||||
usr/lib/${DEB_HOST_MULTIARCH}/proxmox-backup/sg-tape-cmd
|
||||
usr/lib/x86_64-linux-gnu/proxmox-backup/proxmox-backup-api
|
||||
usr/lib/x86_64-linux-gnu/proxmox-backup/proxmox-backup-banner
|
||||
usr/lib/x86_64-linux-gnu/proxmox-backup/proxmox-backup-proxy
|
||||
usr/lib/x86_64-linux-gnu/proxmox-backup/proxmox-daily-update
|
||||
usr/lib/x86_64-linux-gnu/proxmox-backup/sg-tape-cmd
|
||||
usr/sbin/pbs2to3
|
||||
usr/sbin/pbs3to4
|
||||
usr/sbin/proxmox-backup-debug
|
||||
usr/sbin/proxmox-backup-manager
|
||||
@ -22,6 +22,7 @@ usr/share/javascript/proxmox-backup/css/ext6-pbs.css
|
||||
usr/share/javascript/proxmox-backup/images
|
||||
usr/share/javascript/proxmox-backup/index.hbs
|
||||
usr/share/javascript/proxmox-backup/js/proxmox-backup-gui.js
|
||||
usr/share/man/man1/pbs2to3.1
|
||||
usr/share/man/man1/pbs3to4.1
|
||||
usr/share/man/man1/pmt.1
|
||||
usr/share/man/man1/pmtx.1
|
||||
@ -67,8 +68,6 @@ usr/share/proxmox-backup/templates/default/tape-load-body.txt.hbs
|
||||
usr/share/proxmox-backup/templates/default/tape-load-subject.txt.hbs
|
||||
usr/share/proxmox-backup/templates/default/test-body.txt.hbs
|
||||
usr/share/proxmox-backup/templates/default/test-subject.txt.hbs
|
||||
usr/share/proxmox-backup/templates/default/thresholds-exceeded-body.txt.hbs
|
||||
usr/share/proxmox-backup/templates/default/thresholds-exceeded-subject.txt.hbs
|
||||
usr/share/proxmox-backup/templates/default/verify-err-body.txt.hbs
|
||||
usr/share/proxmox-backup/templates/default/verify-err-subject.txt.hbs
|
||||
usr/share/proxmox-backup/templates/default/verify-ok-body.txt.hbs
|
||||
|
||||
4
debian/proxmox-backup-server.udev
vendored
4
debian/proxmox-backup-server.udev
vendored
@ -5,7 +5,7 @@
|
||||
ACTION=="remove", GOTO="persistent_storage_tape_end"
|
||||
ENV{UDEV_DISABLE_PERSISTENT_STORAGE_RULES_FLAG}=="1", GOTO="persistent_storage_tape_end"
|
||||
|
||||
# also see: /usr/lib/udev/rules.d/60-persistent-storage-tape.rules
|
||||
# also see: /lib/udev/rules.d/60-persistent-storage-tape.rules
|
||||
|
||||
SUBSYSTEM=="scsi_generic", SUBSYSTEMS=="scsi", ATTRS{type}=="1", IMPORT{program}="scsi_id --sg-version=3 --export --whitelisted -d $devnode", \
|
||||
SYMLINK+="tape/by-id/scsi-$env{ID_SERIAL}-sg"
|
||||
@ -18,4 +18,4 @@ SUBSYSTEM=="scsi_generic", SUBSYSTEMS=="scsi", ATTRS{type}=="1", ENV{ID_SCSI_SER
|
||||
LABEL="persistent_storage_tape_end"
|
||||
|
||||
# triggers the mounting of a removable device
|
||||
ACTION=="add", SUBSYSTEM=="block", ENV{ID_FS_UUID}!="", TAG+="systemd", ENV{SYSTEMD_WANTS}="removable-device-attach@$env{ID_FS_UUID}"
|
||||
ACTION=="add", SUBSYSTEM=="block", ENV{ID_FS_UUID}!="", TAG+="systemd", ENV{SYSTEMD_WANTS}="removable-device-attach@$env{ID_FS_UUID}"
|
||||
@ -25,6 +25,7 @@ GENERATED_SYNOPSIS := \
|
||||
pxar/synopsis.rst \
|
||||
|
||||
MAN1_PAGES := \
|
||||
pbs2to3.1 \
|
||||
pbs3to4.1 \
|
||||
pmt.1 \
|
||||
pmtx.1 \
|
||||
|
||||
@ -88,7 +88,7 @@ class ReflabelMapper(Builder):
|
||||
#pprint(vars(node))
|
||||
|
||||
if hasattr(node, 'expect_referenced_by_id') and len(node['ids']) > 1: # explicit labels
|
||||
filename = str(self.env.doc2path(docname))
|
||||
filename = self.env.doc2path(docname)
|
||||
filename_html = re.sub('.rst', '.html', filename)
|
||||
|
||||
# node['ids'][0] contains a normalized version of the
|
||||
|
||||
@ -6,8 +6,6 @@
|
||||
<title>Proxmox Backup Server API Documentation</title>
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="extjs/theme-crisp/resources/theme-crisp-all.css">
|
||||
<link rel="stylesheet" type="text/css" href="/fontawesome/css/font-awesome.css" />
|
||||
<link rel="stylesheet" type="text/css" href="/widgettoolkit/css/ext6-pmx.css" />
|
||||
<link rel="stylesheet" type="text/css" media="(prefers-color-scheme: dark)" href="/widgettoolkit/themes/theme-proxmox-dark.css" />
|
||||
<script type="text/javascript" src="extjs/ext-all.js"></script>
|
||||
<script type="text/javascript" src="apidoc.js"></script>
|
||||
|
||||
@ -28,50 +28,6 @@ brackets (for example, `[fe80::01]`).
|
||||
You can pass the repository with the ``--repository`` command-line option, or
|
||||
by setting the ``PBS_REPOSITORY`` environment variable.
|
||||
|
||||
Alternatively, you can specify the repository components as separate
|
||||
command-line options:
|
||||
|
||||
``--server <host>``
|
||||
Backup server address (hostname or IP address). Defaults to ``localhost``.
|
||||
Requires ``--datastore`` to be set as well.
|
||||
|
||||
``--port <number>``
|
||||
Backup server port. Defaults to ``8007``.
|
||||
|
||||
``--datastore <name>``
|
||||
Name of the target datastore. Required when using component options instead
|
||||
of ``--repository``.
|
||||
|
||||
``--auth-id <user@realm[!token]>``
|
||||
Authentication identity, either a user (``user@realm``) or an API token
|
||||
(``user@realm!tokenname``). Defaults to ``root@pam``.
|
||||
|
||||
These options are mutually exclusive with ``--repository``. Both forms resolve
|
||||
to the same internal representation, so cached login tickets and other session
|
||||
state are shared between them. For example, logging in with ``--repository``
|
||||
and then running a backup with ``--server``/``--datastore`` (or vice versa)
|
||||
reuses the same ticket, as long as the server address and user match.
|
||||
|
||||
When component options are used on the command line, they are merged with the
|
||||
corresponding ``PBS_*`` environment variables on a per-field basis: CLI options
|
||||
take precedence, while unspecified fields fall back to their environment
|
||||
variable. For example, with ``PBS_SERVER`` and ``PBS_DATASTORE`` set in the
|
||||
environment, passing ``--auth-id 'other@pam'`` on the command line overrides
|
||||
just the identity while inheriting the server and datastore from the
|
||||
environment.
|
||||
|
||||
The component options make it easy to change individual parts of the
|
||||
connection, for example switching to a different datastore or server without
|
||||
having to rewrite the entire repository string:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
# proxmox-backup-client backup root.pxar:/ \
|
||||
--auth-id 'user@pbs!backup' --server pbs.example.com --datastore store1
|
||||
|
||||
.. Note:: Remember to quote API token identifiers on the shell, since the
|
||||
exclamation mark (``!``) is a special character in most shells.
|
||||
|
||||
The web interface provides copyable repository text in the datastore summary
|
||||
with the `Show Connection Information` button.
|
||||
|
||||
@ -79,7 +35,7 @@ Below are some examples of valid repositories and their corresponding real
|
||||
values:
|
||||
|
||||
================================ ================== ================== ===========
|
||||
Example Repository Auth-ID Host:Port Datastore
|
||||
Example User Host:Port Datastore
|
||||
================================ ================== ================== ===========
|
||||
mydatastore ``root@pam`` localhost:8007 mydatastore
|
||||
myhostname:mydatastore ``root@pam`` myhostname:8007 mydatastore
|
||||
@ -114,26 +70,6 @@ Environment Variables
|
||||
``PBS_REPOSITORY``
|
||||
The default backup repository.
|
||||
|
||||
``PBS_SERVER``
|
||||
Backup server address. Provides a default that can be overridden by
|
||||
``--server``. Requires ``PBS_DATASTORE`` to be set as well (unless
|
||||
``--datastore`` is given on the command line). Not used when ``--repository``
|
||||
or ``PBS_REPOSITORY`` is set.
|
||||
|
||||
``PBS_PORT``
|
||||
Backup server port. Defaults to ``8007`` if unset.
|
||||
|
||||
``PBS_DATASTORE``
|
||||
Datastore name. Provides a default that can be overridden by ``--datastore``.
|
||||
Not used when ``--repository`` or ``PBS_REPOSITORY`` is set.
|
||||
|
||||
``PBS_AUTH_ID``
|
||||
Authentication identity (``user@realm`` or ``user@realm!tokenname``).
|
||||
Defaults to ``root@pam`` if unset.
|
||||
|
||||
``PBS_NAMESPACE``
|
||||
Backup namespace. Used as a fallback when ``--ns`` is not given.
|
||||
|
||||
``PBS_PASSWORD``
|
||||
When set, this value is used as the password for the backup server.
|
||||
You can also set this to an API token secret.
|
||||
|
||||
@ -4,7 +4,7 @@ Certificate Management
|
||||
----------------------
|
||||
|
||||
Access to the API and thus the web-based administration interface is always
|
||||
encrypted through ``https``. Each `Proxmox Backup`_ Server host creates by default its
|
||||
encrypted through ``https``. Each `Proxmox Backup`_ host creates by default its
|
||||
own (self-signed) certificate. This certificate is used for encrypted
|
||||
communication with the host’s ``proxmox-backup-proxy`` service, for any API
|
||||
call between a user or backup-client and the web-interface.
|
||||
@ -18,7 +18,7 @@ configuration, or by using certificates, signed by a trusted certificate authori
|
||||
Certificates for the API and SMTP
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Proxmox Backup Server stores its certificate and key in:
|
||||
Proxmox Backup stores its certificate and key in:
|
||||
|
||||
- ``/etc/proxmox-backup/proxy.pem``
|
||||
|
||||
@ -59,9 +59,9 @@ Note that any certificate key files must not be password protected.
|
||||
Trusted certificates via Let’s Encrypt (ACME)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Proxmox Backup Server includes an implementation of the **A**\ utomatic
|
||||
Proxmox Backup includes an implementation of the **A**\ utomatic
|
||||
**C**\ ertificate **M**\ anagement **E**\ nvironment (**ACME**)
|
||||
protocol, allowing Proxmox Backup Server admins to use an ACME provider
|
||||
protocol, allowing Proxmox Backup admins to use an ACME provider
|
||||
like Let’s Encrypt for easy setup of TLS certificates, which are
|
||||
accepted and trusted by modern operating systems and web browsers out of
|
||||
the box.
|
||||
@ -127,7 +127,7 @@ DNS record in the domain’s zone.
|
||||
:align: right
|
||||
:alt: Create ACME Account
|
||||
|
||||
Proxmox Backup Server supports both of those challenge types out of the
|
||||
Proxmox Backup supports both of those challenge types out of the
|
||||
box, you can configure plugins either over the web interface under
|
||||
``Certificates -> ACME Challenges``, or using the
|
||||
``proxmox-backup-manager acme plugin add`` command.
|
||||
@ -178,7 +178,7 @@ with Let’s Encrypts ACME.
|
||||
- There **must** be no other listener on port 80.
|
||||
|
||||
- The requested (sub)domain needs to resolve to a public IP of the
|
||||
Proxmox Backup Server host.
|
||||
Proxmox Backup host.
|
||||
|
||||
.. _sysadmin_certs_acme_dns_challenge:
|
||||
|
||||
@ -195,7 +195,7 @@ allows provisioning of ``TXT`` records via an API.
|
||||
Configuring ACME DNS APIs for validation
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Proxmox Backup Server reuses the DNS plugins developed for the
|
||||
Proxmox Backup re-uses the DNS plugins developed for the
|
||||
``acme.sh`` [1]_ project. Please refer to its documentation for details
|
||||
on configuration of specific APIs.
|
||||
|
||||
@ -229,7 +229,7 @@ domain/DNS server, in case your primary/real DNS does not support
|
||||
provisioning via an API. Manually set up a permanent ``CNAME`` record
|
||||
for ``_acme-challenge.domain1.example`` pointing to
|
||||
``_acme-challenge.domain2.example``, and set the ``alias`` property in
|
||||
the Proxmox Backup Server node configuration file ``/etc/proxmox-backup/node.cfg``
|
||||
the Proxmox Backup node configuration file ``/etc/proxmox-backup/node.cfg``
|
||||
to ``domain2.example`` to allow the DNS server of ``domain2.example`` to
|
||||
validate all challenges for ``domain1.example``.
|
||||
|
||||
@ -282,7 +282,7 @@ Manually Change Certificate over the Command Line
|
||||
If you want to get rid of certificate verification warnings, you have to
|
||||
generate a valid certificate for your server.
|
||||
|
||||
Log in to your Proxmox Backup Server via ssh or use the console:
|
||||
Log in to your Proxmox Backup via ssh or use the console:
|
||||
|
||||
::
|
||||
|
||||
@ -295,7 +295,7 @@ Follow the instructions on the screen, for example:
|
||||
Country Name (2 letter code) [AU]: AT
|
||||
State or Province Name (full name) [Some-State]:Vienna
|
||||
Locality Name (eg, city) []:Vienna
|
||||
Organization Name (eg, company) [Internet Widgets Pty Ltd]: Proxmox GmbH
|
||||
Organization Name (eg, company) [Internet Widgits Pty Ltd]: Proxmox GmbH
|
||||
Organizational Unit Name (eg, section) []:Proxmox Backup
|
||||
Common Name (eg, YOUR name) []: yourproxmox.yourdomain.com
|
||||
Email Address []:support@yourdomain.com
|
||||
|
||||
@ -102,6 +102,7 @@ man_pages = [
|
||||
('pxar/man1', 'pxar', 'Proxmox File Archive CLI Tool', [author], 1),
|
||||
('pmt/man1', 'pmt', 'Control Linux Tape Devices', [author], 1),
|
||||
('pmtx/man1', 'pmtx', 'Control SCSI media changer devices (tape autoloaders)', [author], 1),
|
||||
('pbs2to3/man1', 'pbs2to3', 'Proxmox Backup Server upgrade checker script for 2.4+ to current 3.x major upgrades', [author], 1),
|
||||
('pbs3to4/man1', 'pbs3to4', 'Proxmox Backup Server upgrade checker script for 3.4+ to current 4.x major upgrades', [author], 1),
|
||||
# configs
|
||||
('config/acl/man5', 'acl.cfg', 'Access Control Configuration', [author], 5),
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
Proxmox Backup Version , Debian Version , First Release , Debian EOL , Proxmox Backup EOL
|
||||
Proxmox Backup 4 , Debian 13 (Trixie) , 2025-08 , TBA , TBA
|
||||
Proxmox Backup 3 , Debian 12 (Bookworm) , 2023-06 , 2026-08 , 2026-08
|
||||
Proxmox Backup 3 , Debian 12 (Bookworm) , 2023-06 , TBA , TBA
|
||||
Proxmox Backup 2 , Debian 11 (Bullseye) , 2021-07 , 2024-07 , 2024-07
|
||||
Proxmox Backup 1 , Debian 10 (Buster) , 2020-11 , 2022-08 , 2022-07
|
||||
|
||||
|
@ -37,7 +37,7 @@ How can I upgrade Proxmox Backup Server to the next point release?
|
||||
Minor version upgrades, for example upgrading from Proxmox Backup Server in
|
||||
version 3.1 to 3.2 or 3.3, can be done just like any normal update.
|
||||
But, you should still check the `release notes
|
||||
<https://pbs.proxmox.com/wiki/Roadmap>`_ for any relevant notable,
|
||||
<https://pbs.proxmox.com/wiki/index.php/Roadmap>`_ for any relevant notable,
|
||||
or breaking change.
|
||||
|
||||
For the update itself use either the Web UI *Node -> Updates* panel or
|
||||
@ -66,11 +66,9 @@ or tape, ready.
|
||||
Although the specific upgrade steps depend on your respective setup, we provide
|
||||
general instructions and advice of how a upgrade should be performed:
|
||||
|
||||
* `Upgrade from Proxmox Backup Server 3 to 4 <https://pbs.proxmox.com/wiki/Upgrade_from_3_to_4>`_
|
||||
* `Upgrade from Proxmox Backup Server 2 to 3 <https://pbs.proxmox.com/wiki/index.php/Upgrade_from_2_to_3>`_
|
||||
|
||||
* `Upgrade from Proxmox Backup Server 2 to 3 <https://pbs.proxmox.com/wiki/Upgrade_from_2_to_3>`_
|
||||
|
||||
* `Upgrade from Proxmox Backup Server 1 to 2 <https://pbs.proxmox.com/wiki/Upgrade_from_1.1_to_2.x>`_
|
||||
* `Upgrade from Proxmox Backup Server 1 to 2 <https://pbs.proxmox.com/wiki/index.php/Upgrade_from_1.1_to_2.x>`_
|
||||
|
||||
Can I copy or synchronize my datastore to another location?
|
||||
-----------------------------------------------------------
|
||||
|
||||
@ -44,7 +44,7 @@ All headers are stored as little-endian.
|
||||
- header of ``[u8; 16]`` consisting of type hash and size;
|
||||
marks start
|
||||
* - ``PAYLOAD``
|
||||
- header of ``[u8; 16]`` consisting of type hash and size;
|
||||
- header of ``[u8; 16]`` cosisting of type hash and size;
|
||||
referenced by metadata archive
|
||||
* - Payload
|
||||
- raw regular file payload
|
||||
|
||||
10
docs/gui.rst
10
docs/gui.rst
@ -93,15 +93,9 @@ WebAuthn, and HTTP proxy configuration. It also contains the following
|
||||
subsections:
|
||||
|
||||
* **Access Control**: Add and manage users, API tokens, and the permissions
|
||||
associated with these items.
|
||||
* **Remotes**: Add, edit and remove remotes (see :term:`Remote`).
|
||||
* **S3 Endpoints**: Add, edit and remove S3 endpoints to be used by datastores.
|
||||
* **Traffic Control**: Manage rules for traffic limits on users, networks and
|
||||
timeframes.
|
||||
associated with these items
|
||||
* **Remotes**: Add, edit and remove remotes (see :term:`Remote`)
|
||||
* **Certificates**: Manage ACME accounts and create SSL certificates.
|
||||
* **Encryptioin Keys**: Manage encryption keys for tape backups and sync jobs.
|
||||
* **Notifications**: Configure notification targets and control when
|
||||
notifications should be send by defining match rules.
|
||||
* **Subscription**: Upload a subscription key, view subscription status and
|
||||
access a text-based system report.
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 15 KiB |
@ -195,7 +195,7 @@ With `systemd-boot`:
|
||||
# proxmox-boot-tool init <new ESP>
|
||||
|
||||
.. NOTE:: `ESP` stands for EFI System Partition, which is setup as partition #2 on
|
||||
bootable disks setup by the Proxmox Backup Server installer. For details, see
|
||||
bootable disks setup by the Proxmox Backup installer. For details, see
|
||||
:ref:`Setting up a new partition for use as synced ESP <systembooting-proxmox-boot-setup>`.
|
||||
|
||||
With `grub`:
|
||||
@ -224,7 +224,7 @@ preferred editor. The required setting for email notification is
|
||||
|
||||
ZED_EMAIL_ADDR="root"
|
||||
|
||||
Please note that Proxmox Backup Server forwards mails to `root` to the email address
|
||||
Please note that Proxmox Backup forwards mails to `root` to the email address
|
||||
configured for the root user.
|
||||
|
||||
|
||||
@ -283,9 +283,8 @@ A good value for servers is 10:
|
||||
|
||||
# sysctl -w vm.swappiness=10
|
||||
|
||||
To make the swappiness persistent, create a new file
|
||||
`/etc/sysctl.d/99-swappiness.conf` with an editor of your choice and add the
|
||||
following line:
|
||||
To make the swappiness persistent, open `/etc/sysctl.conf` with
|
||||
an editor of your choice and add the following line:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
|
||||
@ -296,17 +296,8 @@ data is intact. Verification is generally carried out through the creation of
|
||||
verify jobs. These are scheduled tasks that run verification at a given interval
|
||||
(see :ref:`calendar-event-scheduling`). With these, you can also set whether
|
||||
already verified snapshots are ignored, as well as set a time period, after
|
||||
which snapshots are checked again. The number of read and verify threads used
|
||||
for a verification job can be specified via the `read-threads` and `verify-threads`
|
||||
parameters. Possible values range from 1 to 32 threads, defaults being 1 reader
|
||||
and 4 verify threads. The interface for creating verify jobs can be found under the
|
||||
**Verify Jobs** tab of the datastore. Alternatively, you can verify all backups manually.
|
||||
The interface for this can be found under the **Content** tab of the datastore or can
|
||||
be done via the CLI:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
# proxmox-backup-manager verify <datastore> --read-threads 1 --verify-threads 4 --ignore-verified false
|
||||
which snapshots are checked again. The interface for creating verify jobs can be
|
||||
found under the **Verify Jobs** tab of the datastore.
|
||||
|
||||
.. Note:: It is recommended that you reverify all backups at least monthly, even
|
||||
if a previous verification was successful. This is because physical drives
|
||||
@ -349,9 +340,3 @@ Internally Proxmox Backup Server tracks whether each datastore access is a
|
||||
write or read operation, so that it can gracefully enter the respective mode,
|
||||
by allowing conflicting operations that started before enabling the maintenance
|
||||
mode to finish.
|
||||
|
||||
The supported maintenance modes are:
|
||||
|
||||
- ``read-only``: Only read operations are allowed on the datastore.
|
||||
|
||||
- ``offline``: Neither read nor write operations are allowed on the datastore.
|
||||
|
||||
@ -67,8 +67,8 @@ itself. Alternatively, you can manage them with the ``proxmox-backup-manager
|
||||
sync-job`` command. The configuration information for sync jobs is stored at
|
||||
``/etc/proxmox-backup/sync.cfg``. To create a new sync job, click the add button
|
||||
in the GUI, or use the ``create`` subcommand. After creating a sync job, you can
|
||||
start it manually from the GUI, use the ``run`` subcommand, or provide it with
|
||||
a schedule (see :ref:`calendar-event-scheduling`) to run regularly.
|
||||
either start it manually from the GUI or provide it with a schedule (see
|
||||
:ref:`calendar-event-scheduling`) to run regularly.
|
||||
Backup snapshots, groups and namespaces which are no longer available on the
|
||||
**Remote** datastore can be removed from the local datastore as well by setting
|
||||
the ``remove-vanished`` option for the sync job.
|
||||
@ -87,17 +87,6 @@ trusted remote backup server.
|
||||
╞════════════╪═══════╪════════╪══════════════╪═══════════╪═════════╡
|
||||
│ pbs2-local │ local │ pbs2 │ local │ Wed 02:30 │ offsite │
|
||||
└────────────┴───────┴────────┴──────────────┴───────────┴─────────┘
|
||||
# proxmox-backup-manager sync-job run pbs2-local
|
||||
Starting datastore sync job 'pbs2:local:local::pbs2-local'
|
||||
sync datastore 'local' from 'pbs2/local'
|
||||
----
|
||||
Syncing datastore 'local', root namespace into datastore 'local', root namespace
|
||||
found 0 groups to sync (out of 0 total)
|
||||
Finished syncing root namespace, current progress: 0 groups, 0 snapshots
|
||||
Summary: sync job found no new data to pull
|
||||
sync job 'pbs2:local:local::pbs2-local' end
|
||||
queued notification (id=a8ce89b5-330a-4ad7-9906-3a876e7ed174)
|
||||
TASK OK
|
||||
# proxmox-backup-manager sync-job remove pbs2-local
|
||||
|
||||
To set up sync jobs, the configuring user needs the following permissions:
|
||||
@ -156,24 +145,6 @@ job needs to be run before a sync job with 'resync-corrupt' can be carried out.
|
||||
that a 'resync-corrupt'-job needs to check the manifests of all snapshots in a datastore
|
||||
and might take much longer than regular sync jobs.
|
||||
|
||||
If the ``run-on-mount`` flag is set, the sync job will be automatically started whenever a
|
||||
relevant removable datastore is mounted. If mounting a removable datastore would start
|
||||
multiple sync jobs, these jobs will be run sequentially in alphabetical order based on
|
||||
their ID.
|
||||
|
||||
If the ``unmount-on-done`` flag is set, the datastore will be automatically unmounted
|
||||
after the sync job finishes. This option is only available for sync jobs triggered by
|
||||
mounting (``run-on-mount``), enabling fully automated external drive workflows.
|
||||
|
||||
Setting the ``worker-threads`` option to a value greater than ``1`` (up to ``32``,
|
||||
default ``1``) synchronizes multiple backup groups in parallel. This can
|
||||
significantly improve throughput on high-latency connections, where a single
|
||||
HTTP/2 connection to the source is otherwise bottlenecked by head-of-line
|
||||
blocking. Log output from concurrent groups is prefixed with the group and
|
||||
buffered briefly to keep related lines together. Note that the memory and
|
||||
connection count on both endpoints grow roughly linearly with the number of
|
||||
workers, so higher values are not always better.
|
||||
|
||||
Namespace Support
|
||||
^^^^^^^^^^^^^^^^^
|
||||
|
||||
@ -311,57 +282,3 @@ The following permissions are required for a sync job in push direction:
|
||||
|
||||
.. note:: Sync jobs in push direction require namespace support on the remote
|
||||
Proxmox Backup Server instance (minimum version 2.2).
|
||||
|
||||
Server Side Encryption/Decryption During Sync
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Sync job in push direction allow to encrypt unencrypted snapshots when syncing
|
||||
to a less trusted remote Proxmox Backup Server instance. For this, a server side
|
||||
encryption key can be assigned to the sync job. This key will then be used to
|
||||
encrypt the contents before pushing them to the remote, analogous to performing
|
||||
a backup with an encryption key. Already encrypted snapshots are not re-encrypted
|
||||
but rather pushed unmodified. Snapshots containing only partially encrypted
|
||||
contents are skipped for security reasons.
|
||||
|
||||
Therefore, sync jobs using the ``encrypted-only`` flag will never use the
|
||||
``active-encryption-key`` when pushing snapshots, since only already encrypted
|
||||
snapshots are being synced.
|
||||
|
||||
On the other hand, sync jobs in pull direction allow to assign a number of
|
||||
associated keys, which will be used to decrypt snapshot contents if the key
|
||||
fingerprint of one of the listed keys matches the one used to encrypt the
|
||||
backup snapshot. The active encryption key has no effect for sync jobs in pull
|
||||
direction and should not be set.
|
||||
|
||||
In order to configure the sync job, as well as for sync job owner/local user
|
||||
to access the keys during sync, ``System.Modify`` permissions are required on
|
||||
the ``/system/encryption-keys/{key}`` path.
|
||||
|
||||
.. note:: Encryption key handling comes with a few risks, especially with key
|
||||
rotation. Therefore, only active keys can be used to encrypt new snapshot
|
||||
contents during push sync. If an active encryption key is changed, the key is
|
||||
kept back as associated key on the sync job, in order to protect it from
|
||||
accidental removal. Further, any encryption key can be archived, rendering it
|
||||
no longer usable for encryption, only to decrypt pre-existing contents. Any
|
||||
encryption key usable for sync jobs must therefore be marked as archived and
|
||||
disassociated from any sync job still associated to it, before being able to
|
||||
remove it.
|
||||
|
||||
The following command can be used to assign the active encryption key for a sync
|
||||
job.
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
# proxmox-backup-manager sync-job update pbs2-push --active-encryption-key key0
|
||||
|
||||
Setting the associated keys will drop any key not present in the given key list,
|
||||
with exception of the previously assigned active encryption key, if it is updated
|
||||
as well. The previously assigned encryption key (in the example above ``key0``)
|
||||
will always be pushed to the list of associated keys on rotation. For example,
|
||||
since ``key0`` is currently the active encryption key, below command would assign
|
||||
``key1`` as the new active encryption key and result in ``key0,key2,key3`` as
|
||||
associated keys for the sync job.
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
# proxmox-backup-manager sync-job update pbs2-push --active-encryption-key key1 --associated-key key2 --associated-key key3
|
||||
|
||||
@ -97,59 +97,3 @@ of **Configuration** or by using the ``dns`` subcommand of
|
||||
|
||||
|
||||
.. include:: traffic-control.rst
|
||||
|
||||
|
||||
Overriding network device names
|
||||
-------------------------------
|
||||
|
||||
When upgrading kernels, adding PCIe devices or updating your BIOS, automatically
|
||||
generated network interface names can change. To alleviate this issues, Proxmox
|
||||
Backup Server provides a tool for automatically generating `systemd .link`_
|
||||
files for overriding the name of network devices. It also automatically replaces
|
||||
the occurrences of the old interface name in ``/etc/network/interfaces``.
|
||||
|
||||
The generated link files are stored in ``/usr/local/lib/systemd/network``. For
|
||||
the interfaces file a new file will be generated in the same place with a
|
||||
``.new`` suffix. This way you can inspect the changes made to the configuration
|
||||
by using diff (or another diff viewer of your choice):
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
diff -y /etc/network/interfaces /etc/network/interfaces.new
|
||||
|
||||
If you see any problematic changes or want to revert the changes made by the
|
||||
pinning tool **before rebooting**, simply delete all ``.new`` files and the
|
||||
respective link files from ``/usr/local/lib/systemd/network``.
|
||||
|
||||
The following command will generate a ``.link`` file for all physical network
|
||||
interfaces that do not yet have a ``.link`` file and update selected Proxmox VE
|
||||
configuration files (see above). The generated names will use the default prefix
|
||||
``nic``, so the resulting interface names will be ``nic1``, ``nic2``, ...
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
proxmox-network-interface-pinning generate
|
||||
|
||||
You can override the default prefix with the ``--prefix`` flag:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
proxmox-network-interface-pinning generate --prefix myprefix
|
||||
|
||||
It is also possible to pin only a specific interface:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
proxmox-network-interface-pinning generate --interface enp1s0
|
||||
|
||||
When pinning a specific interface, you can specify the exact name that the
|
||||
interface should be pinned to:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
proxmox-network-interface-pinning generate --interface enp1s0 --target-name if42
|
||||
|
||||
In order to apply the changes made by ``proxmox-network-interface-pinning`` to
|
||||
the network configuration, the host needs to be rebooted.
|
||||
|
||||
.. _systemd .link: https://www.freedesktop.org/software/systemd/man/latest/systemd.link.html
|
||||
|
||||
@ -7,9 +7,9 @@ Overview
|
||||
--------
|
||||
|
||||
* Proxmox Backup Server emits :ref:`notification_events` in case of noteworthy
|
||||
events in the system. These events are processed based on the global
|
||||
notification settings. Each notification event includes metadata, such as a
|
||||
timestamp, severity level, type, and additional event-specific fields.
|
||||
events in the system. These events are handled by the notification system. A
|
||||
notification event has metadata, for example a timestamp, a severity level, a
|
||||
type and other metadata fields.
|
||||
* :ref:`notification_matchers` route a notification event to one or more
|
||||
notification targets. A matcher can have match rules to selectively route
|
||||
based on the metadata of a notification event.
|
||||
@ -17,7 +17,12 @@ Overview
|
||||
is routed to by a matcher. There are multiple types of target, mail-based
|
||||
(Sendmail and SMTP) and Gotify.
|
||||
|
||||
Global notification settings can be configured in the GUI under *Configuration →
|
||||
Datastores and tape backup jobs have a configurable :ref:`notification_mode`.
|
||||
It allows you to choose between the notification system and a legacy mode for
|
||||
sending notification emails. The legacy mode is equivalent to the way
|
||||
notifications were handled before Proxmox Backup Server 3.2.
|
||||
|
||||
The notification system can be configured in the GUI under *Configuration →
|
||||
Notifications*. The configuration is stored in :ref:`notifications.cfg` and
|
||||
:ref:`notifications_priv.cfg` - the latter contains sensitive configuration
|
||||
options such as passwords or authentication tokens for notification targets and
|
||||
@ -259,7 +264,7 @@ Notification Events
|
||||
-------------------
|
||||
|
||||
The following table contains a list of all notification events in Proxmox
|
||||
Backup Server, their type, severity and additional metadata fields. ``type`` as
|
||||
Backup server, their type, severity and additional metadata fields. ``type`` as
|
||||
well as any other metadata field may be used in ``match-field`` match rules.
|
||||
|
||||
================================ ==================== ========== ==============================================================
|
||||
@ -299,8 +304,9 @@ Metadata field Description
|
||||
System Mail Forwarding
|
||||
----------------------
|
||||
Certain local system daemons, such as ``smartd``, send notification emails to
|
||||
the local ``root`` user. These mails are converted into notification events
|
||||
with the type ``system-mail`` and with a severity of ``unknown``.
|
||||
the local ``root`` user. Proxmox Backup Server will feed these mails into the
|
||||
notification system as a notification of type ``system-mail`` and with severity
|
||||
``unknown``.
|
||||
|
||||
When the email is forwarded to a sendmail target, the mail's content and
|
||||
headers are forwarded as-is. For all other targets, the system tries to extract
|
||||
@ -321,14 +327,24 @@ Notification Mode
|
||||
Datastores and tape backup/restore job configuration have a
|
||||
``notification-mode`` option which can have one of two values:
|
||||
|
||||
* Send notifications based on the global notification settings (``notification-system``).
|
||||
* ``legacy-sendmail``: Send notification emails via the system's ``sendmail``
|
||||
command. The notification system will be bypassed and any configured
|
||||
targets/matchers will be ignored. This mode is equivalent to the notification
|
||||
behavior for version before Proxmox Backup Server 3.2.
|
||||
|
||||
* Send notification emails via the system's ``sendmail`` command
|
||||
(``legacy-sendmail``). Any targets or matchers from the global notification
|
||||
settings are ignored. This mode is equivalent to the notification behavior
|
||||
for Proxmox Backup Server versions before 3.2. It might be removed in a
|
||||
later release of Proxmox Backup Server.
|
||||
* ``notification-system``: Use the new, flexible notification system.
|
||||
|
||||
If the ``notification-mode`` option is not set, Proxmox Backup Server will
|
||||
default to ``legacy-sendmail``.
|
||||
|
||||
Starting with Proxmox Backup Server 3.2, a datastore created in the UI will
|
||||
automatically opt in to the new notification system. If the datastore is
|
||||
created via the API or the ``proxmox-backup-manager`` CLI, the
|
||||
``notification-mode`` option has to be set explicitly to
|
||||
``notification-system`` if the notification system shall be used.
|
||||
|
||||
The ``legacy-sendmail`` mode might be removed in a later release of
|
||||
Proxmox Backup Server.
|
||||
|
||||
Settings for ``legacy-sendmail`` notification mode
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
@ -348,9 +364,8 @@ task type via the ``notify`` option.
|
||||
|
||||
* Never: do not send any notification at all
|
||||
|
||||
The ``notify-user`` and ``notify`` options are ignored when using the global
|
||||
notification settings (``notification-mode`` is set to
|
||||
``notification-system``).
|
||||
The ``notify-user`` and ``notify`` options are ignored if ``notification-mode``
|
||||
is set to ``notification-system``.
|
||||
|
||||
Overriding Notification Templates
|
||||
---------------------------------
|
||||
@ -371,50 +386,3 @@ The template files follow the naming convention of
|
||||
``gc-err-body.txt.hbs`` contains the template for rendering notifications for
|
||||
garbage collection errors, while ``package-updates-subject.txt.hbs`` is used to
|
||||
render the subject line of notifications for available package updates.
|
||||
|
||||
.. _s3_notification_thresholds:
|
||||
|
||||
Notification Thresholds and Reset Schedule (S3 Datastores Only)
|
||||
---------------------------------------------------------------
|
||||
|
||||
Datastores of type S3 keep track of the number of requests being send to the
|
||||
corresponding S3 endpoint and the amount of data being send. Proxmox Backup
|
||||
Server allows to configure threshold for these request and traffic counters to
|
||||
send out notifications if one of the set threshold values is exceeded.
|
||||
|
||||
The notification threshold value can be set individually by request method or
|
||||
traffic volume. Notifications will be send out only once per threshold when the
|
||||
threshold has been exceeded. A counter reset is required to bring it below the
|
||||
threshold again in order to get further notifications. Therefore, it is possible
|
||||
to define a threshold reset schedule so request and traffic counters get
|
||||
periodically reset.
|
||||
|
||||
Per datastore notification thresholds and their reset schedule are configurable
|
||||
in the :ref:`Datastore Options <datastore_options>` for S3 backed datastores.
|
||||
|
||||
The following counters thresholds are available for configuration:
|
||||
|
||||
==================== ==========================================================
|
||||
Counter Threshold Description and Usage
|
||||
==================== ==========================================================
|
||||
``s3-get`` Number of ``GET`` requests: Mainly used for download of
|
||||
data and metadata among the following operations: restore,
|
||||
verification, S3 refresh, garbage collection.
|
||||
``s3-put`` Number of ``PUT`` requests: Mainly used for upload of data
|
||||
and metadata among the following operations: backup, syncs,
|
||||
metadata changes, content moves.
|
||||
``s3-post`` Number of ``POST`` requests: Mainly used for modification
|
||||
of data and metadata among the following operations: bulk
|
||||
deletion during garbage collection, content moves.
|
||||
``s3-head`` Number of ``HEAD`` requests: Mainly used to check for
|
||||
access and existence among which the following operations:
|
||||
checking bucket access, fetching of metadata.
|
||||
``s3-delete`` Number of ``DELETE`` requests: Mainly used for deleting
|
||||
single objects by the following operations: garbage
|
||||
collection, content moves.
|
||||
``s3-upload`` Amount of bytes uploaded to the S3 endpoint, independent of
|
||||
request method.
|
||||
``s3-download`` Amount of bytes downloaded from the S3 endpoint,
|
||||
independent of request method.
|
||||
==================== ==========================================================
|
||||
|
||||
|
||||
@ -4,62 +4,25 @@ Debian Package Repositories
|
||||
---------------------------
|
||||
|
||||
All Debian based systems use APT_ as a package management tool. The lists of
|
||||
repositories are defined in ``/etc/apt/sources.list`` and the ``.list`` or
|
||||
``.sources`` files found in the ``/etc/apt/sources.d/`` directory. Updates can
|
||||
be installed directly with the ``apt`` command-line tool, or via the GUI.
|
||||
repositories are defined in ``/etc/apt/sources.list`` and the ``.list`` files found
|
||||
in the ``/etc/apt/sources.d/`` directory. Updates can be installed directly
|
||||
with the ``apt`` command-line tool, or via the GUI.
|
||||
|
||||
.. _package_repos_repository_formats:
|
||||
APT_ ``sources.list`` files list one package repository per line, with the most
|
||||
preferred source listed first. Empty lines are ignored and a ``#`` character
|
||||
anywhere on a line marks the remainder of that line as a comment. The
|
||||
information available from the configured sources is acquired by ``apt
|
||||
update``.
|
||||
|
||||
Repository Formats
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
.. code-block:: sources.list
|
||||
:caption: File: ``/etc/apt/sources.list``
|
||||
|
||||
APT_ repositories can be configured in two distinct formats, the old single
|
||||
line format and the newer deb822 format. No matter what format you choose,
|
||||
``apt update`` will fetch the information from all configured sources.
|
||||
deb http://deb.debian.org/debian bookworm main contrib
|
||||
deb http://deb.debian.org/debian bookworm-updates main contrib
|
||||
|
||||
Single Line
|
||||
^^^^^^^^^^^
|
||||
# security updates
|
||||
deb http://security.debian.org/debian-security bookworm-security main contrib
|
||||
|
||||
Single line repositories are defined in ``.list`` files list one package
|
||||
repository per line, with the most preferred source listed first. Empty lines
|
||||
are ignored and a ``#`` character anywhere on a line marks the remainder of
|
||||
that line as a comment.
|
||||
|
||||
deb822 Style
|
||||
^^^^^^^^^^^^
|
||||
|
||||
The newer deb822 multiline format is used in ``.sources`` files. Each
|
||||
repository consists of a stanza with multiple key value pairs. A stanza is
|
||||
simply a group of lines. One file can contain multiple stanzas by separating
|
||||
them with a blank line. You can still use ``#`` to comment out lines.
|
||||
|
||||
.. note:: Modernizing your repositories is recommended under Debian Trixie, as
|
||||
``apt`` will complain about older repository definitions otherwise. You can
|
||||
run the command ``apt modernize-sources`` to modernize your existing
|
||||
repositories automatically.
|
||||
|
||||
.. _package_repos_debian_base_repositories:
|
||||
|
||||
Debian Base Repositories
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
You will need a Debian base repository as a minimum to get updates for all
|
||||
packages provided by Debian itself:
|
||||
|
||||
.. code-block:: debian.sources
|
||||
:caption: File: ``/etc/apt/sources.list.d/debian.sources``
|
||||
|
||||
Types: deb
|
||||
URIs: http://deb.debian.org/debian/
|
||||
Suites: trixie trixie-updates
|
||||
Components: main contrib non-free-firmware
|
||||
Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg
|
||||
|
||||
Types: deb
|
||||
URIs: http://security.debian.org/debian-security/
|
||||
Suites: trixie-security
|
||||
Components: main contrib non-free-firmware
|
||||
Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg
|
||||
|
||||
In addition, you need a package repository from Proxmox to get Proxmox Backup
|
||||
updates.
|
||||
@ -69,6 +32,38 @@ updates.
|
||||
:align: right
|
||||
:alt: APT Repository Management in the Web Interface
|
||||
|
||||
.. _package_repos_secure_apt:
|
||||
|
||||
SecureApt
|
||||
~~~~~~~~~
|
||||
|
||||
The `Release` files in the repositories are signed with GnuPG. APT is using
|
||||
these signatures to verify that all packages are from a trusted source.
|
||||
|
||||
If you install Proxmox Backup Server from an official ISO image, the
|
||||
verification key is already installed.
|
||||
|
||||
If you install Proxmox Backup Server on top of Debian, download and install the
|
||||
key with the following commands:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
# wget https://enterprise.proxmox.com/debian/proxmox-release-bookworm.gpg -O /etc/apt/trusted.gpg.d/proxmox-release-bookworm.gpg
|
||||
|
||||
Verify the SHA512 checksum afterwards with the expected output below:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
# sha512sum /etc/apt/trusted.gpg.d/proxmox-release-bookworm.gpg
|
||||
7da6fe34168adc6e479327ba517796d4702fa2f8b4f0a9833f5ea6e6b48f6507a6da403a274fe201595edc86a84463d50383d07f64bdde2e3658108db7d6dc87 /etc/apt/trusted.gpg.d/proxmox-release-bookworm.gpg
|
||||
|
||||
and the md5sum, with the expected output below:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
# md5sum /etc/apt/trusted.gpg.d/proxmox-release-bookworm.gpg
|
||||
41558dc019ef90bd0f6067644a51cf5b /etc/apt/trusted.gpg.d/proxmox-release-bookworm.gpg
|
||||
|
||||
.. _sysadmin_package_repos_enterprise:
|
||||
|
||||
`Proxmox Backup`_ Enterprise Repository
|
||||
@ -79,14 +74,11 @@ all Proxmox Backup subscription users. It contains the most stable packages,
|
||||
and is suitable for production use. The ``pbs-enterprise`` repository is
|
||||
enabled by default:
|
||||
|
||||
.. code-block:: debian.sources
|
||||
:caption: File: ``/etc/apt/sources.list.d/pbs-enterprise.sources``
|
||||
.. code-block:: sources.list
|
||||
:caption: File: ``/etc/apt/sources.list.d/pbs-enterprise.list``
|
||||
|
||||
deb https://enterprise.proxmox.com/debian/pbs bookworm pbs-enterprise
|
||||
|
||||
Types: deb
|
||||
URIs: https://enterprise.proxmox.com/debian/pbs
|
||||
Suites: trixie
|
||||
Components: pbs-enterprise
|
||||
Signed-By: /usr/share/keyrings/proxmox-archive-keyring.gpg
|
||||
|
||||
To never miss important security fixes, the superuser (``root@pam`` user) is
|
||||
notified via email about new packages as soon as they are available. The
|
||||
@ -96,8 +88,11 @@ Please note that you need a valid subscription key to access this
|
||||
repository. More information regarding subscription levels and pricing can be
|
||||
found at https://www.proxmox.com/en/proxmox-backup-server/pricing
|
||||
|
||||
.. note:: You can disable this repository by adding the line ``Enabled: false``
|
||||
to the stanza.
|
||||
.. note:: You can disable this repository by commenting out the above line
|
||||
using a `#` (at the start of the line). This prevents error messages if you do
|
||||
not have a subscription key. Please configure the ``pbs-no-subscription``
|
||||
repository in that case.
|
||||
|
||||
|
||||
`Proxmox Backup`_ No-Subscription Repository
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
@ -107,17 +102,21 @@ this repository. It can be used for testing and non-production
|
||||
use. It is not recommended to use it on production servers, because these
|
||||
packages are not always heavily tested and validated.
|
||||
|
||||
We recommend to configure this repository in
|
||||
``/etc/apt/sources.list.d/proxmox.sources``.
|
||||
We recommend to configure this repository in ``/etc/apt/sources.list``.
|
||||
|
||||
.. code-block:: debian.sources
|
||||
:caption: File: ``/etc/apt/sources.list.d/proxmox.sources``
|
||||
.. code-block:: sources.list
|
||||
:caption: File: ``/etc/apt/sources.list``
|
||||
|
||||
deb http://deb.debian.org/debian bookworm main contrib
|
||||
deb http://deb.debian.org/debian bookworm-updates main contrib
|
||||
|
||||
# Proxmox Backup Server pbs-no-subscription repository provided by proxmox.com,
|
||||
# NOT recommended for production use
|
||||
deb http://download.proxmox.com/debian/pbs bookworm pbs-no-subscription
|
||||
|
||||
# security updates
|
||||
deb http://security.debian.org/debian-security bookworm-security main contrib
|
||||
|
||||
Types: deb
|
||||
URIs: http://download.proxmox.com/debian/pbs
|
||||
Suites: trixie
|
||||
Components: pbs-no-subscription
|
||||
Signed-By: /usr/share/keyrings/proxmox-archive-keyring.gpg
|
||||
|
||||
`Proxmox Backup`_ Test Repository
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
@ -128,17 +127,13 @@ to test new features.
|
||||
.. .. warning:: the ``pbstest`` repository should (as the name implies)
|
||||
only be used to test new features or bug fixes.
|
||||
|
||||
You can access this repository by adding the following stanza to
|
||||
``/etc/apt/sources.list.d/proxmox.sources``:
|
||||
You can access this repository by adding the following line to
|
||||
``/etc/apt/sources.list``:
|
||||
|
||||
.. code-block:: debian.sources
|
||||
.. code-block:: sources.list
|
||||
:caption: sources.list entry for ``pbstest``
|
||||
|
||||
Types: deb
|
||||
URIs: http://download.proxmox.com/debian/pbs
|
||||
Suites: trixie
|
||||
Components: pbs-test
|
||||
Signed-By: /usr/share/keyrings/proxmox-archive-keyring.gpg
|
||||
deb http://download.proxmox.com/debian/pbs bookworm pbstest
|
||||
|
||||
.. _package_repositories_client_only:
|
||||
|
||||
@ -163,24 +158,6 @@ In order to configure this repository you need to first :ref:`setup the Proxmox
|
||||
release key <package_repos_secure_apt>`. After that, add the repository URL to
|
||||
the APT sources lists.
|
||||
|
||||
**Repositories for Debian 13 (Trixie) based releases**
|
||||
|
||||
This repository is tested with:
|
||||
|
||||
- Debian Trixie
|
||||
|
||||
Edit the file ``/etc/apt/sources.list.d/pbs-client.sources`` and add the following
|
||||
snippet
|
||||
|
||||
.. code-block:: debian.sources
|
||||
:caption: File: ``/etc/apt/sources.list.d/pbs``
|
||||
|
||||
Types: deb
|
||||
URIs: http://download.proxmox.com/debian/pbs-client
|
||||
Suites: trixie
|
||||
Components: main
|
||||
Signed-By: /usr/share/keyrings/proxmox-archive-keyring.gpg
|
||||
|
||||
**Repositories for Debian 12 (Bookworm) based releases**
|
||||
|
||||
This repository is tested with:
|
||||
@ -226,50 +203,6 @@ snippet
|
||||
|
||||
deb http://download.proxmox.com/debian/pbs-client buster main
|
||||
|
||||
.. _package_repos_secure_apt:
|
||||
|
||||
SecureApt
|
||||
~~~~~~~~~
|
||||
|
||||
The `Release` files in the repositories are signed with GnuPG. APT is using
|
||||
these signatures to verify that all packages are from a trusted source.
|
||||
|
||||
If you install Proxmox Backup Server from an official ISO image, the
|
||||
verification key is already installed.
|
||||
|
||||
If you install Proxmox Backup Server on top of Debian, download and install the
|
||||
key with the following commands:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
# wget https://enterprise.proxmox.com/debian/proxmox-archive-keyring-trixie.gpg -O /usr/share/keyrings/proxmox-archive-keyring.gpg
|
||||
|
||||
.. note:: The `wget` command above adds the keyring for Proxmox releases based
|
||||
on Debian Trixie. Once the `proxmox-archive-keyring` package is installed,
|
||||
it will manage this file. At that point, the hashes below may no longer
|
||||
match the hashes of this file, as keys for new Proxmox releases get added or
|
||||
removed. This is intended, `apt` will ensure that only trusted keys are
|
||||
being used. **Modifying this file is discouraged once
|
||||
`proxmox-archive-keyring` is installed.**
|
||||
|
||||
Verify the SHA256 checksum afterwards with the expected output below:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
# sha256sum /usr/share/keyrings/proxmox-archive-keyring.gpg
|
||||
136673be77aba35dcce385b28737689ad64fd785a797e57897589aed08db6e45 /usr/share/keyrings/proxmox-archive-keyring.gpg
|
||||
|
||||
and the md5sum, with the expected output below:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
# md5sum /usr/share/keyrings/proxmox-archive-keyring.gpg
|
||||
77c8b1166d15ce8350102ab1bca2fcbf /usr/share/keyrings/proxmox-archive-keyring.gpg
|
||||
|
||||
.. note:: Make sure that the path that you download the key to, matches the
|
||||
path specified in the ``Signed-By:`` lines in your repository stanzas from
|
||||
above.
|
||||
|
||||
.. _node_options_http_proxy:
|
||||
|
||||
Repository Access Behind HTTP Proxy
|
||||
|
||||
16
docs/pbs2to3/man1.rst
Normal file
16
docs/pbs2to3/man1.rst
Normal file
@ -0,0 +1,16 @@
|
||||
:orphan:
|
||||
|
||||
|
||||
=======
|
||||
pbs2to3
|
||||
=======
|
||||
|
||||
Description
|
||||
===========
|
||||
|
||||
This tool will help you to detect common pitfalls and misconfiguration before,
|
||||
and during the upgrade of a Proxmox Backup Server system. Any failures or
|
||||
warnings must be addressed prior to the upgrade. If you suspect that a message
|
||||
is a false positive, you have to make carefully sure that it really is.
|
||||
|
||||
.. include:: ../pbs-copyright.rst
|
||||
@ -12,7 +12,7 @@ Synopsis
|
||||
Catalog Shell Commands
|
||||
======================
|
||||
|
||||
Those command are available when you start an interactive restore shell:
|
||||
Those command are available when you start an intercative restore shell:
|
||||
|
||||
::
|
||||
|
||||
|
||||
233
docs/storage.rst
233
docs/storage.rst
@ -233,170 +233,6 @@ datastore is not mounted when they are scheduled. Sync jobs start, but fail
|
||||
with an error saying the datastore was not mounted. The reason is that syncs
|
||||
not happening as scheduled should at least be noticeable.
|
||||
|
||||
Setups that rely on ``unmount-on-done`` (see :ref:`syncjobs`) have no natural
|
||||
point to run garbage collection, since the device is detached right after the
|
||||
sync finishes. Enabling the ``gc-on-unmount`` option on the datastore makes
|
||||
the unmount trigger a garbage collection first and wait for it to complete
|
||||
before actually unmounting. If a garbage collection run is already in
|
||||
progress when the unmount fires (for example from an overlapping scheduled
|
||||
GC job), that run is waited on instead.
|
||||
|
||||
.. _datastore_s3_backend:
|
||||
|
||||
Datastores with S3 Backend
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Proxmox Backup Server supports S3 compatible object stores as storage backend for datastores. For
|
||||
this, an S3 endpoint needs to be set-up under "Configuration" > "Remotes" > "S3 Endpoints".
|
||||
|
||||
.. important:: Keep in mind that operating as S3 backed object store might cause additional costs.
|
||||
Providers might charge you for storage space and API requests performed to the buckets, egress
|
||||
and bandwidth fees might be charged as well. Therefore, monitoring of these values and eventual
|
||||
costs is highly recommended.
|
||||
|
||||
In the endpoint configuration, provide the REST API endpoint for the object store. The endpoint
|
||||
is provider dependent and allows for the bucket and region templating. For example, configuring
|
||||
the endpoint as e.g. ``{{bucket}}.s3.{{region}}.amazonaws.com`` will be expanded to
|
||||
``my-pbs-bucket.s3.eu-central-1.amazonaws.com`` with a configured bucket of name ``my-pbs-bucket``
|
||||
located in region ``eu-central-1``.
|
||||
|
||||
The bucket name is part of the datastore backend configuration rather than the endpoint
|
||||
configuration, as the same endpoint might be reused for multiple bucket. Objects placed in the
|
||||
bucket are prefixed by the datastore name, therefore it is possible to create multiple datastores
|
||||
using the same bucket.
|
||||
|
||||
.. note:: Proxmox Backup Server does not handle bucket creation and access control. The bucket used
|
||||
to store the datastore's objects as well as the access key have to be setup beforehand in your S3
|
||||
provider interface. The Proxmox Backup Server acts as client and requires permissions to get, put
|
||||
list and delete objects in the bucket.
|
||||
|
||||
Most providers allow to access buckets either using a vhost style addressing, the bucket name being
|
||||
part of the endpoint address, or via path style addressing, the bucket name being the prefix to
|
||||
the path components of requests. Proxmox Backup Server supports both styles, favoring the vhost
|
||||
style urls over the path style. To use path style addresses, set the corresponding configuration
|
||||
flag.
|
||||
|
||||
Proxmox Backup Server does not support plain text communication with the S3 API, all communication
|
||||
is encrypted using HTTPS in transit. Therefore, for self-hosted S3 object stores using a self-signed
|
||||
certificate, the matching fingerprint has to be provided to the endpoint configuration. Otherwise
|
||||
the client refuses connections to the S3 object store.
|
||||
|
||||
The following example shows the setup of a new s3 endpoint configuration:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
# proxmox-backup-manager s3 endpoint create my-s3-ep --access-key 'my-access-key' --secret-key 'my-secret-key' --endpoint '{{bucket}}.s3.{{region}}.amazonaws.com' --region eu-central-1
|
||||
|
||||
To list your s3 endpoint configuration, run:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
# proxmox-backup-manager s3 endpoint list
|
||||
|
||||
A new datastore with S3 backend can be created using one of the configured S3 endpoints. Although
|
||||
storing all contents on the S3 object store, the datastore requires nevertheless a local persistent
|
||||
cache, used to increase performance and reduce the number of requests to the backend. For this, a
|
||||
local filesystem path has to be provided during datastore creation, just like for regular datastore
|
||||
setup. However, unlike for regular datastores the size of the local cache can be limited, 64 GiB to
|
||||
128 GiB are recommended given that cached datastore contents include also data chunks. Best is to
|
||||
use a dedicated disk, partition or ZFS dataset with quota as local cache. Note however, it is not
|
||||
possible to use a pre-existing regular datastore for this. Further, the use of volatile memory only
|
||||
for the cache is currently not possible.
|
||||
|
||||
To setup a new datastore called ``my-s3-store`` placed in a bucket called ``pbs-s3-bucket``, run:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
# proxmox-backup-manager datastore create my-s3-store /mnt/datastore/my-s3-store-cache --backend type=s3,client=my-s3-ep,bucket=pbs-s3-bucket
|
||||
|
||||
A datastore cannot be shared between multiple Proxmox Backup Server instances, only one instance can
|
||||
operate on the datastore at a time. However, datastore contents used on an instance which is no
|
||||
longer available can be reused on a fresh installation. To recreate the datastore, you must pass the
|
||||
``reuse-datastore`` and ``overwrite-in-use`` flags. Since the datastore name is used as prefix, the
|
||||
same datastore name must be used.
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
# proxmox-backup-manager datastore create my-s3-store /mnt/datastore/my-new-s3-store-cache --backend type=s3,client=my-s3-ep,bucket=pbs-s3-bucket --reuse-datastore true --overwrite-in-use true
|
||||
|
||||
.. note:: If your S3 object store runs out of space while performing write operation to it, most
|
||||
likely cleanup operations such as cleaning up of contents within a snapshot directory will fail
|
||||
as well. The recommended procedure is to cleanup any stray objects corresponding to this snapshot
|
||||
on the S3 object store manually and refresh the contents via an ``S3 refresh``, either via the
|
||||
CLI or UI.
|
||||
|
||||
|
||||
.. _datastore_s3_endpoint_examples:
|
||||
|
||||
S3 Datastore Backend Configuration Examples
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
The following shows example configurations for some typical S3 object store providers as excerpts
|
||||
(data relevant to S3 config only) from ``/etc/proxmox-backup/s3.cfg`` and
|
||||
``/etc/proxmox-backup/datastore.cfg``:
|
||||
|
||||
Self hosted S3 object store with Ceph Rados Gateway using plain IP address, custom port, self-signed
|
||||
certificate and path-style bucket:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
# cat /etc/proxmox-backup/s3.cfg
|
||||
|
||||
s3-endpoint: ceph-s3-rados-gw
|
||||
access-key XXXXXXXXXXXXXXXXXXXX
|
||||
endpoint 172.16.0.200
|
||||
fingerprint XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
|
||||
path-style true
|
||||
port 7480
|
||||
secret-key XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
|
||||
|
||||
|
||||
# cat /etc/proxmox-backup/datastore.cfg
|
||||
|
||||
datastore: ceph-s3-rgw-store
|
||||
backend bucket=pbs-ceph-bucket,client=ceph-s3-rados-gw,type=s3
|
||||
path /mnt/datastore/ceph-s3-rgw-store-local-cache
|
||||
|
||||
AWS S3 with vhost style bucket addressing, using bucket name and region templating for the endpoint
|
||||
url:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
# cat /etc/proxmox-backup/s3.cfg
|
||||
|
||||
s3-endpoint: aws-s3
|
||||
access-key XXXXXXXXXXXXXXXXXXXX
|
||||
endpoint {{bucket}}.s3.{{region}}.amazonaws.com
|
||||
region eu-central-1
|
||||
secret-key XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
|
||||
|
||||
# cat /etc/proxmox-backup/datastore.cfg
|
||||
|
||||
datastore: aws-s3-store
|
||||
backend bucket=pbs-s3-bucket,client=aws-s3,type=s3
|
||||
path /mnt/datastore/aws-s3-store-local-cache
|
||||
|
||||
Cloudflare R2 with path style bucket addressing, note that region must be set to ``auto`` as
|
||||
otherwise request authentication might fail:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
# cat /etc/proxmox-backup/s3.cfg
|
||||
|
||||
s3-endpoint: cloudflare-r2
|
||||
access-key XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
|
||||
endpoint XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX.r2.cloudflarestorage.com
|
||||
path-style true
|
||||
region auto
|
||||
secret-key XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
|
||||
|
||||
# cat /etc/proxmox-backup/datastore.cfg
|
||||
|
||||
datastore: r2-s3-store
|
||||
backend bucket=pbs-r2-bucket,client=cloudflare-r2,type=s3
|
||||
path /mnt/datastore/r2-s3-store-local-cache
|
||||
|
||||
|
||||
Managing Datastores
|
||||
^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
@ -533,67 +369,8 @@ For backup groups, the existing privilege rules still apply. You either need a
|
||||
privileged enough permission or to be the owner of the backup group; nothing
|
||||
changed here.
|
||||
|
||||
.. _storage_move_namespaces_groups:
|
||||
.. todo:: continue
|
||||
|
||||
Moving Namespaces and Groups
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Backup groups can be moved between namespaces within the same datastore.
|
||||
This is useful for reorganizing backup hierarchies without having to
|
||||
re-run backups.
|
||||
|
||||
A single group can be moved with ``group move``. To relocate an entire
|
||||
namespace subtree (including all child namespaces and their groups), use
|
||||
``namespace move``.
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
# proxmox-backup-client group move <type>/<id> --ns <source> --target-ns <target> --repository <repo>
|
||||
# proxmox-backup-client namespace move <source> --target-ns <target> --repository <repo>
|
||||
|
||||
If the target namespace already exists, groups are moved into it. When a
|
||||
group with the same type and ID already exists in the target and
|
||||
``merge-groups`` is enabled, the snapshots are merged into the existing
|
||||
group provided:
|
||||
|
||||
- both groups have the same owner
|
||||
- the oldest source snapshot is newer than the newest target snapshot
|
||||
|
||||
Groups that cannot be merged or locked are skipped and reported in the
|
||||
task log. They remain at the source and can be retried individually with
|
||||
``group move``.
|
||||
|
||||
.. note::
|
||||
|
||||
With defaults, ``namespace move`` merges into existing target groups
|
||||
(``merge-groups=true``) and removes source namespaces once they are empty
|
||||
(``delete-source=true``). Pass ``--merge-groups false`` or
|
||||
``--delete-source false`` to opt out.
|
||||
|
||||
Optional parameters for ``namespace move``:
|
||||
|
||||
``merge-groups``
|
||||
Allow merging snapshots into groups that already exist in the target
|
||||
namespace with the same type and ID. Defaults to true.
|
||||
|
||||
``max-depth``
|
||||
Limits how many levels of child namespaces below the source are
|
||||
included. When not set, the entire subtree is moved.
|
||||
|
||||
``delete-source``
|
||||
Controls whether the source namespace directories are removed after
|
||||
all groups have been moved out. Defaults to true. Set to false to
|
||||
keep the (now empty) source namespace structure.
|
||||
|
||||
Required privileges:
|
||||
|
||||
- ``group move``: ``DATASTORE_PRUNE`` on the source namespace and
|
||||
``DATASTORE_BACKUP`` on the target namespace, plus ownership of the
|
||||
backup group; or ``DATASTORE_MODIFY`` on both.
|
||||
- ``namespace move``: ``DATASTORE_MODIFY`` on the parent of both the
|
||||
source and the target namespace.
|
||||
|
||||
.. _datastore_options:
|
||||
|
||||
Options
|
||||
~~~~~~~
|
||||
@ -607,9 +384,6 @@ There are a few per-datastore options:
|
||||
|
||||
* :ref:`Notification mode and legacy notification settings <notification_mode>`
|
||||
* :ref:`Maintenance Mode <maintenance_mode>`
|
||||
* :ref:`Maintenance Mode <maintenance_mode>`
|
||||
* :ref:`Notification Thresholds (S3 datastores only) <s3_notification_thresholds>`
|
||||
* :ref:`Threshold Reset Schedule (S3 datastores only) <s3_notification_thresholds>`
|
||||
* Verification of incoming backups
|
||||
|
||||
.. _datastore_tuning_options:
|
||||
@ -686,11 +460,6 @@ There are some tuning related options for the datastore that are more advanced:
|
||||
cache slots, 1048576 (= 1024 * 1024) being the default, 8388608 (= 8192 *
|
||||
1024) the maximum value.
|
||||
|
||||
* ``default-verification-workers`` and ``default-verification-readers``:
|
||||
Define the default number of threads used for verification and reading of chunks,
|
||||
respectively. By default, 4 threads are used for verification and 1 thread is used
|
||||
for reading.
|
||||
|
||||
If you want to set multiple tuning options simultaneously, you can separate them
|
||||
with a comma, like this:
|
||||
|
||||
|
||||
@ -3,13 +3,13 @@
|
||||
Host System Administration
|
||||
==========================
|
||||
|
||||
`Proxmox Backup`_ Server is based on the famous Debian_ Linux
|
||||
`Proxmox Backup`_ is based on the famous Debian_ Linux
|
||||
distribution. This means that you have access to the entire range of
|
||||
Debian packages, and that the base system is well documented. The `Debian
|
||||
Administrator's Handbook`_ is available online, and provides a
|
||||
comprehensive introduction to the Debian operating system.
|
||||
|
||||
A standard Proxmox Backup Server installation uses the default
|
||||
A standard Proxmox Backup installation uses the default
|
||||
repositories from Debian, so you get bug fixes and security updates
|
||||
through that channel. In addition, we provide our own package
|
||||
repository to roll out all Proxmox related packages. This includes
|
||||
@ -19,8 +19,8 @@ We also deliver a specially optimized Linux kernel, based on the Ubuntu
|
||||
kernel. This kernel includes drivers for ZFS_.
|
||||
|
||||
The following sections will concentrate on backup related topics. They
|
||||
will explain things which are different on Proxmox Backup Server, or
|
||||
tasks which are commonly used on Proxmox Backup Server. For other topics,
|
||||
will explain things which are different on Proxmox Backup, or
|
||||
tasks which are commonly used on Proxmox Backup. For other topics,
|
||||
please refer to the standard Debian documentation.
|
||||
|
||||
|
||||
|
||||
@ -4,8 +4,8 @@
|
||||
Host Bootloader
|
||||
---------------
|
||||
|
||||
`Proxmox Backup`_ Server currently uses one of two bootloaders, depending on
|
||||
the disk setup selected in the installer.
|
||||
`Proxmox Backup`_ currently uses one of two bootloaders, depending on the disk setup
|
||||
selected in the installer.
|
||||
|
||||
For EFI Systems installed with ZFS as the root filesystem ``systemd-boot`` is
|
||||
used, unless Secure Boot is enabled. All other deployments use the standard
|
||||
@ -18,8 +18,8 @@ on top of Debian).
|
||||
Partitioning Scheme Used by the Installer
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The Proxmox Backup Server installer creates 3 partitions on all disks selected
|
||||
for installation.
|
||||
The Proxmox Backup installer creates 3 partitions on all disks selected for
|
||||
installation.
|
||||
|
||||
The created partitions are:
|
||||
|
||||
@ -98,7 +98,7 @@ For example, to format an empty partition ``/dev/sda2`` as ESP, run the followin
|
||||
# proxmox-boot-tool format /dev/sda2
|
||||
|
||||
To setup an existing, unmounted ESP located on ``/dev/sda2`` for inclusion in
|
||||
Proxmox Backup Server's kernel update synchronization mechanism, use the following:
|
||||
Proxmox Backup's kernel update synchronization mechanism, use the following:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
@ -198,7 +198,7 @@ Determine which Bootloader is Used
|
||||
:alt: Grub boot screen
|
||||
|
||||
The simplest and most reliable way to determine which bootloader is used, is to
|
||||
watch the boot process of the Proxmox Backup Server node.
|
||||
watch the boot process of the Proxmox Backup node.
|
||||
|
||||
|
||||
You will either see the blue box of ``grub`` or the simple black on white
|
||||
@ -359,11 +359,10 @@ would run:
|
||||
# proxmox-boot-tool kernel pin 5.15.30-1-pve
|
||||
|
||||
|
||||
.. TIP:: The pinning functionality works for all Proxmox Backup Server systems,
|
||||
not only those using ``proxmox-boot-tool`` to synchronize the contents of
|
||||
the ESPs, if your system does not use ``proxmox-boot-tool`` for
|
||||
synchronizing, you can also skip the ``proxmox-boot-tool refresh`` call in
|
||||
the end.
|
||||
.. TIP:: The pinning functionality works for all Proxmox Backup systems, not only those using
|
||||
``proxmox-boot-tool`` to synchronize the contents of the ESPs, if your system
|
||||
does not use ``proxmox-boot-tool`` for synchronizing, you can also skip the
|
||||
``proxmox-boot-tool refresh`` call in the end.
|
||||
|
||||
You can also set a kernel version to be booted on the next system boot only.
|
||||
This is useful, for example, to test if an updated kernel has resolved an issue,
|
||||
@ -401,8 +400,8 @@ content and configuration on the ESPs by running the ``refresh`` subcommand.
|
||||
Secure Boot
|
||||
~~~~~~~~~~~
|
||||
|
||||
Since Proxmox Backup Server 3.1, Secure Boot is supported out of the box via
|
||||
signed packages and integration in ``proxmox-boot-tool``.
|
||||
Since Proxmox Backup 3.1, Secure Boot is supported out of the box via signed
|
||||
packages and integration in ``proxmox-boot-tool``.
|
||||
|
||||
The following packages need to be installed for Secure Boot to be enabled:
|
||||
|
||||
@ -412,8 +411,8 @@ The following packages need to be installed for Secure Boot to be enabled:
|
||||
* ``proxmox-kernel-6.X.Y-Z-pve-signed`` (Kernel image, signed by Proxmox)
|
||||
|
||||
Only Grub as bootloader is supported out of the box, since there are no other
|
||||
pre-signed bootloader packages available. Any new installation of Proxmox
|
||||
Backup Server will automatically have all of the above packages included.
|
||||
pre-signed bootloader packages available. Any new installation of Proxmox Backup
|
||||
will automatically have all of the above packages included.
|
||||
|
||||
More details about how Secure Boot works, and how to customize the setup, are
|
||||
available in `our wiki <https://pve.proxmox.com/wiki/Secure_Boot_Setup>`_.
|
||||
@ -427,10 +426,10 @@ Switching an Existing Installation to Secure Boot
|
||||
.. WARNING:: This can lead to an unbootable installation in some cases if not
|
||||
done correctly. Reinstalling the host will setup Secure Boot automatically if
|
||||
available, without any extra interactions. **Make sure you have a working and
|
||||
well-tested backup of your Proxmox Backup Server host!**
|
||||
well-tested backup of your Proxmox Backup host!**
|
||||
|
||||
An existing UEFI installation can be switched over to Secure Boot if desired,
|
||||
without having to reinstall Proxmox Backup Server from scratch.
|
||||
without having to reinstall Proxmox Backup from scratch.
|
||||
|
||||
First, ensure all your system is up-to-date. Next, install all the required
|
||||
pre-signed packages as listed above. Grub automatically creates the needed EFI
|
||||
@ -443,8 +442,8 @@ boot entry for booting via the default shim.
|
||||
|
||||
If ``systemd-boot`` is used as a bootloader (see
|
||||
:ref:`Determine which Bootloader is used <systembooting-determine-bootloader>`),
|
||||
some additional setup is needed. This is only the case if Proxmox Backup Server
|
||||
was installed with ZFS-on-root.
|
||||
some additional setup is needed. This is only the case if Proxmox Backup was
|
||||
installed with ZFS-on-root.
|
||||
|
||||
To check the latter, run:
|
||||
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
System Requirements
|
||||
-------------------
|
||||
|
||||
We recommend using high quality server hardware when running Proxmox Backup
|
||||
Server in production. To further decrease the impact of a failed host, you can
|
||||
set up periodic, efficient, incremental :ref:`datastore synchronization
|
||||
<syncjobs>` from other Proxmox Backup Server instances.
|
||||
We recommend using high quality server hardware when running Proxmox Backup in
|
||||
production. To further decrease the impact of a failed host, you can set up
|
||||
periodic, efficient, incremental :ref:`datastore synchronization <syncjobs>`
|
||||
from other Proxmox Backup Server instances.
|
||||
|
||||
.. _minimum_system_requirements:
|
||||
|
||||
|
||||
@ -624,9 +624,9 @@ GUI, or enter the following command:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
// proxmox-tape pool create <name> [OPTIONS]
|
||||
// proxmox-tape pool create <name> --drive <string> [OPTIONS]
|
||||
|
||||
# proxmox-tape pool create daily
|
||||
# proxmox-tape pool create daily --drive mydrive
|
||||
|
||||
|
||||
Additional options can be set later, using the update command:
|
||||
|
||||
@ -156,10 +156,6 @@ metadata:
|
||||
Similarly, the ``user delete-token`` subcommand can be used to delete a token
|
||||
again.
|
||||
|
||||
.. WARNING:: Direct/manual edits to ``token.shadow`` may take up to 60 seconds (or
|
||||
longer in edge cases) to take effect due to caching. Restart services for
|
||||
immediate effect of manual edits.
|
||||
|
||||
Newly generated API tokens don't have any permissions. Please read the next
|
||||
section to learn how to set access permissions.
|
||||
|
||||
@ -340,8 +336,8 @@ Some examples are:
|
||||
:align: left
|
||||
|
||||
=========================== =========================================================
|
||||
``/datastore`` Access to *all* datastores on a Proxmox Backup Server
|
||||
``/datastore/{store}`` Access to a specific datastore on a Proxmox Backup Server
|
||||
``/datastore`` Access to *all* datastores on a Proxmox Backup server
|
||||
``/datastore/{store}`` Access to a specific datastore on a Proxmox Backup server
|
||||
``/datastore/{store}/{ns}`` Access to a specific namespace on a specific datastore
|
||||
``/remote`` Access to all remote entries
|
||||
``/system/network`` Access to configure the host network
|
||||
@ -536,7 +532,7 @@ WebAuthn
|
||||
For WebAuthn to work, you need to have two things:
|
||||
|
||||
* A trusted HTTPS certificate (for example, by using `Let's Encrypt
|
||||
<https://pbs.proxmox.com/wiki/HTTPS_Certificate_Configuration>`_).
|
||||
<https://pbs.proxmox.com/wiki/index.php/HTTPS_Certificate_Configuration>`_).
|
||||
While it probably works with an untrusted certificate, some browsers may warn
|
||||
or refuse WebAuthn operations if it is not trusted.
|
||||
|
||||
|
||||
@ -11,7 +11,7 @@ DYNAMIC_UNITS := \
|
||||
proxmox-backup.service \
|
||||
proxmox-backup-proxy.service
|
||||
|
||||
all: $(UNITS) $(DYNAMIC_UNITS) pbs-enterprise.sources
|
||||
all: $(UNITS) $(DYNAMIC_UNITS) pbs-enterprise.list
|
||||
|
||||
clean:
|
||||
rm -f $(DYNAMIC_UNITS)
|
||||
|
||||
1
etc/pbs-enterprise.list
Normal file
1
etc/pbs-enterprise.list
Normal file
@ -0,0 +1 @@
|
||||
deb https://enterprise.proxmox.com/debian/pbs bookworm pbs-enterprise
|
||||
@ -1,5 +0,0 @@
|
||||
Types: deb
|
||||
URIs: https://enterprise.proxmox.com/debian/pbs
|
||||
Suites: trixie
|
||||
Components: pbs-enterprise
|
||||
Signed-By: /usr/share/keyrings/proxmox-archive-keyring.gpg
|
||||
@ -1,5 +1,5 @@
|
||||
[Unit]
|
||||
Description=Conditionally mounting device with uuid '%i' if it belongs to a removable datastore
|
||||
Description=Try to mount the removable device of a datastore with uuid '%i'.
|
||||
After=proxmox-backup-proxy.service
|
||||
Requires=proxmox-backup-proxy.service
|
||||
|
||||
|
||||
@ -1,14 +0,0 @@
|
||||
[Unit]
|
||||
Description=Mount tmpfs at /run/proxmox-backup
|
||||
Conflicts=umount.target
|
||||
Before=local-fs.target umount.target
|
||||
|
||||
[Mount]
|
||||
Type=tmpfs
|
||||
What=tmpfs
|
||||
Where=/run/proxmox-backup
|
||||
# Unlimited inodes as this stores per-chunk file locks
|
||||
Options=rw,nosuid,nodev,noexec,relatime,uid=backup,gid=backup,nr_inodes=0,mode=755,inode64
|
||||
|
||||
[Install]
|
||||
WantedBy=local-fs.target
|
||||
@ -3,7 +3,7 @@ use anyhow::Error;
|
||||
// chacha20-poly1305
|
||||
|
||||
fn rate_test(name: &str, bench: &dyn Fn() -> usize) {
|
||||
print!("{name:<20} ");
|
||||
print!("{:<20} ", name);
|
||||
|
||||
let start = std::time::SystemTime::now();
|
||||
let duration = std::time::Duration::new(1, 0);
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
use anyhow::Error;
|
||||
use rustyline::history::MemHistory;
|
||||
|
||||
use proxmox_router::cli::*;
|
||||
use proxmox_schema::*;
|
||||
@ -18,7 +17,7 @@ use proxmox_schema::*;
|
||||
///
|
||||
/// Returns: nothing
|
||||
fn echo_command(text: String) -> Result<(), Error> {
|
||||
println!("{text}");
|
||||
println!("{}", text);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -72,10 +71,7 @@ fn cli_definition() -> CommandLineInterface {
|
||||
fn main() -> Result<(), Error> {
|
||||
let helper = CliHelper::new(cli_definition());
|
||||
|
||||
let mut rl = rustyline::Editor::<CliHelper, MemHistory>::with_history(
|
||||
Default::default(),
|
||||
MemHistory::new(),
|
||||
)?;
|
||||
let mut rl = rustyline::Editor::<CliHelper>::new();
|
||||
rl.set_helper(Some(helper));
|
||||
|
||||
while let Ok(line) = rl.readline("# prompt: ") {
|
||||
@ -86,7 +82,7 @@ fn main() -> Result<(), Error> {
|
||||
let rpcenv = CliEnvironment::new();
|
||||
let _ = handle_command(helper.cmd_def(), "", args, rpcenv, None);
|
||||
|
||||
rl.add_history_entry(line)?;
|
||||
rl.add_history_entry(line);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@ -67,7 +67,7 @@ async fn run() -> Result<(), Error> {
|
||||
|
||||
fn main() {
|
||||
if let Err(err) = proxmox_async::runtime::main(run()) {
|
||||
eprintln!("ERROR: {err}");
|
||||
eprintln!("ERROR: {}", err);
|
||||
}
|
||||
println!("DONE");
|
||||
}
|
||||
|
||||
@ -19,7 +19,7 @@ use anyhow::{bail, Error};
|
||||
// Error: detected shrunk file "./dyntest1/testfile0.dat" (22020096 < 12679380992)
|
||||
|
||||
fn create_large_file(path: PathBuf) {
|
||||
println!("TEST {path:?}");
|
||||
println!("TEST {:?}", path);
|
||||
|
||||
let mut file = std::fs::OpenOptions::new()
|
||||
.write(true)
|
||||
@ -47,7 +47,7 @@ fn main() -> Result<(), Error> {
|
||||
for i in 0..20 {
|
||||
let base = base.clone();
|
||||
handles.push(thread::spawn(move || {
|
||||
create_large_file(base.join(format!("testfile{i}.dat")));
|
||||
create_large_file(base.join(format!("testfile{}.dat", i)));
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
109
examples/h2client.rs
Normal file
109
examples/h2client.rs
Normal file
@ -0,0 +1,109 @@
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
|
||||
use anyhow::Error;
|
||||
use futures::future::TryFutureExt;
|
||||
use futures::stream::Stream;
|
||||
use tokio::net::TcpStream;
|
||||
|
||||
// Simple H2 client to test H2 download speed using h2server.rs
|
||||
|
||||
struct Process {
|
||||
body: h2::legacy::RecvStream,
|
||||
trailers: bool,
|
||||
bytes: usize,
|
||||
}
|
||||
|
||||
impl Future for Process {
|
||||
type Output = Result<usize, Error>;
|
||||
|
||||
fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
|
||||
let this = self.get_mut();
|
||||
|
||||
loop {
|
||||
if this.trailers {
|
||||
match futures::ready!(this.body.poll_trailers(cx)) {
|
||||
Ok(Some(trailers)) => println!("trailers: {:?}", trailers),
|
||||
Ok(None) => (),
|
||||
Err(err) => return Poll::Ready(Err(Error::from(err))),
|
||||
}
|
||||
|
||||
println!("Received {} bytes", this.bytes);
|
||||
|
||||
return Poll::Ready(Ok(this.bytes));
|
||||
} else {
|
||||
match futures::ready!(Pin::new(&mut this.body).poll_next(cx)) {
|
||||
Some(Ok(chunk)) => {
|
||||
this.body.flow_control().release_capacity(chunk.len())?;
|
||||
this.bytes += chunk.len();
|
||||
// println!("GOT FRAME {}", chunk.len());
|
||||
}
|
||||
Some(Err(err)) => return Poll::Ready(Err(Error::from(err))),
|
||||
None => {
|
||||
this.trailers = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn send_request(
|
||||
mut client: h2::legacy::client::SendRequest<bytes::Bytes>,
|
||||
) -> impl Future<Output = Result<usize, Error>> {
|
||||
println!("sending request");
|
||||
|
||||
let request = hyper::http::Request::builder()
|
||||
.uri("http://localhost/")
|
||||
.body(())
|
||||
.unwrap();
|
||||
|
||||
let (response, _stream) = client.send_request(request, true).unwrap();
|
||||
|
||||
response.map_err(Error::from).and_then(|response| Process {
|
||||
body: response.into_body(),
|
||||
trailers: false,
|
||||
bytes: 0,
|
||||
})
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Error> {
|
||||
proxmox_async::runtime::main(run())
|
||||
}
|
||||
|
||||
async fn run() -> Result<(), Error> {
|
||||
let start = std::time::SystemTime::now();
|
||||
|
||||
let conn = TcpStream::connect(std::net::SocketAddr::from(([127, 0, 0, 1], 8008))).await?;
|
||||
conn.set_nodelay(true).unwrap();
|
||||
|
||||
let (client, h2) = h2::legacy::client::Builder::new()
|
||||
.initial_connection_window_size(1024 * 1024 * 1024)
|
||||
.initial_window_size(1024 * 1024 * 1024)
|
||||
.max_frame_size(4 * 1024 * 1024)
|
||||
.handshake(conn)
|
||||
.await?;
|
||||
|
||||
tokio::spawn(async move {
|
||||
if let Err(err) = h2.await {
|
||||
println!("GOT ERR={:?}", err);
|
||||
}
|
||||
});
|
||||
|
||||
let mut bytes = 0;
|
||||
for _ in 0..2000 {
|
||||
bytes += send_request(client.clone()).await?;
|
||||
}
|
||||
|
||||
let elapsed = start.elapsed().unwrap();
|
||||
let elapsed = (elapsed.as_secs() as f64) + (elapsed.subsec_millis() as f64) / 1000.0;
|
||||
|
||||
println!(
|
||||
"Downloaded {} bytes, {} MB/s",
|
||||
bytes,
|
||||
(bytes as f64) / (elapsed * 1024.0 * 1024.0)
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
125
examples/h2s-client.rs
Normal file
125
examples/h2s-client.rs
Normal file
@ -0,0 +1,125 @@
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
|
||||
use anyhow::{format_err, Error};
|
||||
use futures::future::TryFutureExt;
|
||||
use futures::stream::Stream;
|
||||
use tokio::net::TcpStream;
|
||||
|
||||
// Simple H2 client to test H2 download speed using h2s-server.rs
|
||||
|
||||
struct Process {
|
||||
body: h2::legacy::RecvStream,
|
||||
trailers: bool,
|
||||
bytes: usize,
|
||||
}
|
||||
|
||||
impl Future for Process {
|
||||
type Output = Result<usize, Error>;
|
||||
|
||||
fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
|
||||
let this = self.get_mut();
|
||||
|
||||
loop {
|
||||
if this.trailers {
|
||||
match futures::ready!(this.body.poll_trailers(cx)) {
|
||||
Ok(Some(trailers)) => println!("trailers: {:?}", trailers),
|
||||
Ok(None) => (),
|
||||
Err(err) => return Poll::Ready(Err(Error::from(err))),
|
||||
}
|
||||
|
||||
println!("Received {} bytes", this.bytes);
|
||||
|
||||
return Poll::Ready(Ok(this.bytes));
|
||||
} else {
|
||||
match futures::ready!(Pin::new(&mut this.body).poll_next(cx)) {
|
||||
Some(Ok(chunk)) => {
|
||||
this.body.flow_control().release_capacity(chunk.len())?;
|
||||
this.bytes += chunk.len();
|
||||
// println!("GOT FRAME {}", chunk.len());
|
||||
}
|
||||
Some(Err(err)) => return Poll::Ready(Err(Error::from(err))),
|
||||
None => {
|
||||
this.trailers = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn send_request(
|
||||
mut client: h2::legacy::client::SendRequest<bytes::Bytes>,
|
||||
) -> impl Future<Output = Result<usize, Error>> {
|
||||
println!("sending request");
|
||||
|
||||
let request = hyper::http::Request::builder()
|
||||
.uri("http://localhost/")
|
||||
.body(())
|
||||
.unwrap();
|
||||
|
||||
let (response, _stream) = client.send_request(request, true).unwrap();
|
||||
|
||||
response.map_err(Error::from).and_then(|response| Process {
|
||||
body: response.into_body(),
|
||||
trailers: false,
|
||||
bytes: 0,
|
||||
})
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Error> {
|
||||
proxmox_async::runtime::main(run())
|
||||
}
|
||||
|
||||
async fn run() -> Result<(), Error> {
|
||||
let start = std::time::SystemTime::now();
|
||||
|
||||
let conn = TcpStream::connect(std::net::SocketAddr::from(([127, 0, 0, 1], 8008))).await?;
|
||||
conn.set_nodelay(true).unwrap();
|
||||
|
||||
use openssl::ssl::{SslConnector, SslMethod};
|
||||
|
||||
let mut ssl_connector_builder = SslConnector::builder(SslMethod::tls()).unwrap();
|
||||
ssl_connector_builder.set_verify(openssl::ssl::SslVerifyMode::NONE);
|
||||
let ssl = ssl_connector_builder
|
||||
.build()
|
||||
.configure()?
|
||||
.into_ssl("localhost")?;
|
||||
|
||||
let conn = tokio_openssl::SslStream::new(ssl, conn)?;
|
||||
let mut conn = Box::pin(conn);
|
||||
conn.as_mut()
|
||||
.connect()
|
||||
.await
|
||||
.map_err(|err| format_err!("connect failed - {}", err))?;
|
||||
|
||||
let (client, h2) = h2::legacy::client::Builder::new()
|
||||
.initial_connection_window_size(1024 * 1024 * 1024)
|
||||
.initial_window_size(1024 * 1024 * 1024)
|
||||
.max_frame_size(4 * 1024 * 1024)
|
||||
.handshake(conn)
|
||||
.await?;
|
||||
|
||||
tokio::spawn(async move {
|
||||
if let Err(err) = h2.await {
|
||||
println!("GOT ERR={:?}", err);
|
||||
}
|
||||
});
|
||||
|
||||
let mut bytes = 0;
|
||||
for _ in 0..2000 {
|
||||
bytes += send_request(client.clone()).await?;
|
||||
}
|
||||
|
||||
let elapsed = start.elapsed().unwrap();
|
||||
let elapsed = (elapsed.as_secs() as f64) + (elapsed.subsec_millis() as f64) / 1000.0;
|
||||
|
||||
println!(
|
||||
"Downloaded {} bytes, {} MB/s",
|
||||
bytes,
|
||||
(bytes as f64) / (elapsed * 1024.0 * 1024.0)
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
94
examples/h2s-server.rs
Normal file
94
examples/h2s-server.rs
Normal file
@ -0,0 +1,94 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{format_err, Error};
|
||||
use futures::*;
|
||||
use hyper::{Body, Request, Response};
|
||||
use openssl::ssl::{SslAcceptor, SslFiletype, SslMethod};
|
||||
use tokio::net::{TcpListener, TcpStream};
|
||||
|
||||
use pbs_buildcfg::configdir;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct H2SExecutor;
|
||||
|
||||
impl<Fut> hyper::rt::Executor<Fut> for H2SExecutor
|
||||
where
|
||||
Fut: Future + Send + 'static,
|
||||
Fut::Output: Send,
|
||||
{
|
||||
fn execute(&self, fut: Fut) {
|
||||
tokio::spawn(fut);
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Error> {
|
||||
proxmox_async::runtime::main(run())
|
||||
}
|
||||
|
||||
async fn run() -> Result<(), Error> {
|
||||
let key_path = configdir!("/proxy.key");
|
||||
let cert_path = configdir!("/proxy.pem");
|
||||
|
||||
let mut acceptor = SslAcceptor::mozilla_intermediate(SslMethod::tls()).unwrap();
|
||||
acceptor
|
||||
.set_private_key_file(key_path, SslFiletype::PEM)
|
||||
.map_err(|err| format_err!("unable to read proxy key {} - {}", key_path, err))?;
|
||||
acceptor
|
||||
.set_certificate_chain_file(cert_path)
|
||||
.map_err(|err| format_err!("unable to read proxy cert {} - {}", cert_path, err))?;
|
||||
acceptor.check_private_key().unwrap();
|
||||
|
||||
let acceptor = Arc::new(acceptor.build());
|
||||
|
||||
let listener = TcpListener::bind(std::net::SocketAddr::from(([127, 0, 0, 1], 8008))).await?;
|
||||
|
||||
println!("listening on {:?}", listener.local_addr());
|
||||
|
||||
loop {
|
||||
let (socket, _addr) = listener.accept().await?;
|
||||
tokio::spawn(handle_connection(socket, Arc::clone(&acceptor)).map(|res| {
|
||||
if let Err(err) = res {
|
||||
eprintln!("Error: {}", err);
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_connection(socket: TcpStream, acceptor: Arc<SslAcceptor>) -> Result<(), Error> {
|
||||
socket.set_nodelay(true).unwrap();
|
||||
|
||||
let ssl = openssl::ssl::Ssl::new(acceptor.context())?;
|
||||
let stream = tokio_openssl::SslStream::new(ssl, socket)?;
|
||||
let mut stream = Box::pin(stream);
|
||||
|
||||
stream.as_mut().accept().await?;
|
||||
|
||||
let mut http = hyper::server::conn::http2::Builder::new(H2SExecutor);
|
||||
// increase window size: todo - find optiomal size
|
||||
let max_window_size = (1 << 31) - 2;
|
||||
http.initial_stream_window_size(max_window_size);
|
||||
http.initial_connection_window_size(max_window_size);
|
||||
|
||||
let service = hyper::service::service_fn(|_req: Request<Body>| {
|
||||
println!("Got request");
|
||||
let buffer = vec![65u8; 4 * 1024 * 1024]; // nonsense [A,A,A,A...]
|
||||
let body = Body::from(buffer);
|
||||
|
||||
let response = Response::builder()
|
||||
.status(hyper::http::StatusCode::OK)
|
||||
.header(
|
||||
hyper::http::header::CONTENT_TYPE,
|
||||
"application/octet-stream",
|
||||
)
|
||||
.body(body)
|
||||
.unwrap();
|
||||
future::ok::<_, Error>(response)
|
||||
});
|
||||
|
||||
http.serve_connection(stream, service)
|
||||
.map_err(Error::from)
|
||||
.await?;
|
||||
|
||||
println!("H2 connection CLOSE !");
|
||||
Ok(())
|
||||
}
|
||||
72
examples/h2server.rs
Normal file
72
examples/h2server.rs
Normal file
@ -0,0 +1,72 @@
|
||||
use std::future::Future;
|
||||
|
||||
use anyhow::Error;
|
||||
use futures::*;
|
||||
use hyper::{Body, Request, Response};
|
||||
|
||||
use tokio::net::{TcpListener, TcpStream};
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct H2Executor;
|
||||
|
||||
impl<Fut> hyper::rt::Executor<Fut> for H2Executor
|
||||
where
|
||||
Fut: Future + Send + 'static,
|
||||
Fut::Output: Send,
|
||||
{
|
||||
fn execute(&self, fut: Fut) {
|
||||
tokio::spawn(fut);
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Error> {
|
||||
proxmox_async::runtime::main(run())
|
||||
}
|
||||
|
||||
async fn run() -> Result<(), Error> {
|
||||
let listener = TcpListener::bind(std::net::SocketAddr::from(([127, 0, 0, 1], 8008))).await?;
|
||||
|
||||
println!("listening on {:?}", listener.local_addr());
|
||||
|
||||
loop {
|
||||
let (socket, _addr) = listener.accept().await?;
|
||||
tokio::spawn(handle_connection(socket).map(|res| {
|
||||
if let Err(err) = res {
|
||||
eprintln!("Error: {}", err);
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_connection(socket: TcpStream) -> Result<(), Error> {
|
||||
socket.set_nodelay(true).unwrap();
|
||||
|
||||
let mut http = hyper::server::conn::http2::Builder::new(H2Executor);
|
||||
// increase window size: todo - find optiomal size
|
||||
let max_window_size = (1 << 31) - 2;
|
||||
http.initial_stream_window_size(max_window_size);
|
||||
http.initial_connection_window_size(max_window_size);
|
||||
|
||||
let service = hyper::service::service_fn(|_req: Request<Body>| {
|
||||
println!("Got request");
|
||||
let buffer = vec![65u8; 4 * 1024 * 1024]; // nonsense [A,A,A,A...]
|
||||
let body = Body::from(buffer);
|
||||
|
||||
let response = Response::builder()
|
||||
.status(hyper::http::StatusCode::OK)
|
||||
.header(
|
||||
hyper::http::header::CONTENT_TYPE,
|
||||
"application/octet-stream",
|
||||
)
|
||||
.body(body)
|
||||
.unwrap();
|
||||
future::ok::<_, Error>(response)
|
||||
});
|
||||
|
||||
http.serve_connection(socket, service)
|
||||
.map_err(Error::from)
|
||||
.await?;
|
||||
|
||||
println!("H2 connection CLOSE !");
|
||||
Ok(())
|
||||
}
|
||||
@ -15,7 +15,7 @@ use proxmox_human_byte::HumanByte;
|
||||
|
||||
fn main() {
|
||||
if let Err(err) = proxmox_async::runtime::main(run()) {
|
||||
panic!("ERROR: {err}");
|
||||
panic!("ERROR: {}", err);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
use anyhow::Error;
|
||||
|
||||
use pbs_api_types::{Authid, BackupNamespace, BackupType};
|
||||
use pbs_client::{BackupWriter, BackupWriterOptions, HttpClient, HttpClientOptions};
|
||||
use pbs_client::{BackupWriter, HttpClient, HttpClientOptions};
|
||||
|
||||
async fn upload_speed() -> Result<f64, Error> {
|
||||
let host = "localhost";
|
||||
@ -19,15 +19,12 @@ async fn upload_speed() -> Result<f64, Error> {
|
||||
|
||||
let client = BackupWriter::start(
|
||||
&client,
|
||||
BackupWriterOptions {
|
||||
datastore,
|
||||
ns: &BackupNamespace::root(),
|
||||
backup: &(BackupType::Host, "speedtest".to_string(), backup_time).into(),
|
||||
crypt_config: None,
|
||||
debug: false,
|
||||
benchmark: true,
|
||||
no_cache: false,
|
||||
},
|
||||
None,
|
||||
datastore,
|
||||
&BackupNamespace::root(),
|
||||
&(BackupType::Host, "speedtest".to_string(), backup_time).into(),
|
||||
false,
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@ -40,10 +37,10 @@ async fn upload_speed() -> Result<f64, Error> {
|
||||
fn main() {
|
||||
match proxmox_async::runtime::main(upload_speed()) {
|
||||
Ok(mbs) => {
|
||||
println!("average upload speed: {mbs} MB/s");
|
||||
println!("average upload speed: {} MB/s", mbs);
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("ERROR: {err}");
|
||||
eprintln!("ERROR: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,26 +8,10 @@ fn main() {
|
||||
Err(_) => match Command::new("git").args(["rev-parse", "HEAD"]).output() {
|
||||
Ok(output) => String::from_utf8(output.stdout).unwrap(),
|
||||
Err(err) => {
|
||||
panic!("git rev-parse failed: {err}");
|
||||
panic!("git rev-parse failed: {}", err);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
println!("cargo:rustc-env=REPOID={repoid}");
|
||||
|
||||
let multiarch = match env::var("CARGO_CFG_TARGET_ARCH")
|
||||
.as_ref()
|
||||
.map(String::as_ref)
|
||||
{
|
||||
Ok("x86_64") => "x86_64-linux-gnu",
|
||||
Ok("aarch64") => "aarch64-linux-gnu",
|
||||
Ok("riscv64") => "riscv64-linux-gnu",
|
||||
Ok(arch) => {
|
||||
panic!("Unsupported architecture: {arch}");
|
||||
}
|
||||
Err(err) => {
|
||||
panic!("Failed to get architecture from CARGO_CFG_TARGET_ARCH - {err}");
|
||||
}
|
||||
};
|
||||
println!("cargo:rustc-env=DEB_HOST_MULTIARCH={multiarch}");
|
||||
println!("cargo:rustc-env=REPOID={}", repoid);
|
||||
}
|
||||
|
||||
@ -51,17 +51,13 @@ macro_rules! PROXMOX_BACKUP_CACHE_DIR_M {
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! PROXMOX_BACKUP_MULTIARCH_LIB_DIR_M {
|
||||
#[macro_export]
|
||||
macro_rules! PROXMOX_BACKUP_FILE_RESTORE_BIN_DIR_M {
|
||||
() => {
|
||||
concat!("/usr/lib/", env!("DEB_HOST_MULTIARCH"), "/proxmox-backup")
|
||||
"/usr/lib/x86_64-linux-gnu/proxmox-backup/file-restore"
|
||||
};
|
||||
}
|
||||
|
||||
/// The multiarch-namespaced /usr/lib path for Proxmox Backup.
|
||||
///
|
||||
/// E.g., on am64/x86_64 this will be "/usr/lib/x86_64-linux-gnu/proxmox-backup"
|
||||
pub const PROXMOX_BACKUP_MULTIARCH_LIB_DIR: &str = PROXMOX_BACKUP_MULTIARCH_LIB_DIR_M!();
|
||||
|
||||
/// namespaced directory for in-memory (tmpfs) run state
|
||||
pub const PROXMOX_BACKUP_RUN_DIR: &str = PROXMOX_BACKUP_RUN_DIR_M!();
|
||||
|
||||
@ -97,10 +93,8 @@ pub const PROXMOX_BACKUP_INITRAMFS_DBG_FN: &str = concat!(
|
||||
);
|
||||
|
||||
/// filename of the kernel to use for booting single file restore VMs
|
||||
pub const PROXMOX_BACKUP_KERNEL_FN: &str = concat!(
|
||||
PROXMOX_BACKUP_MULTIARCH_LIB_DIR_M!(),
|
||||
"/file-restore/bzImage"
|
||||
);
|
||||
pub const PROXMOX_BACKUP_KERNEL_FN: &str =
|
||||
concat!(PROXMOX_BACKUP_FILE_RESTORE_BIN_DIR_M!(), "/bzImage");
|
||||
|
||||
pub const PROXMOX_BACKUP_SUBSCRIPTION_FN: &str = configdir!("/subscription");
|
||||
|
||||
|
||||
@ -12,8 +12,6 @@ bytes.workspace = true
|
||||
futures.workspace = true
|
||||
h2.workspace = true
|
||||
hex.workspace = true
|
||||
http-body-util.workspace = true
|
||||
hyper-util = { workspace = true, features = ["client", "client-legacy", "http1", "http2", "tokio" ]}
|
||||
hyper.workspace = true
|
||||
libc.workspace = true
|
||||
nix.workspace = true
|
||||
@ -36,11 +34,10 @@ pathpatterns.workspace = true
|
||||
proxmox-async.workspace = true
|
||||
proxmox-auth-api.workspace = true
|
||||
proxmox-compression.workspace = true
|
||||
proxmox-http = { workspace = true, features = [ "body" ] }
|
||||
proxmox-http = { workspace = true, features = [ "rate-limiter" ] }
|
||||
proxmox-human-byte.workspace = true
|
||||
proxmox-io = { workspace = true, features = [ "tokio" ] }
|
||||
proxmox-log = { workspace = true }
|
||||
proxmox-rate-limiter = { workspace = true, features = [ "rate-limiter" ] }
|
||||
proxmox-router = { workspace = true, features = [ "cli", "server" ] }
|
||||
proxmox-schema.workspace = true
|
||||
proxmox-sys.workspace = true
|
||||
|
||||
@ -1,159 +1,8 @@
|
||||
use std::fmt;
|
||||
|
||||
use anyhow::{bail, format_err, Error};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use anyhow::{format_err, Error};
|
||||
|
||||
use proxmox_schema::*;
|
||||
|
||||
use pbs_api_types::{
|
||||
Authid, BackupNamespace, Userid, BACKUP_REPO_URL, BACKUP_REPO_URL_REGEX, DATASTORE_SCHEMA,
|
||||
IP_V6_REGEX,
|
||||
};
|
||||
|
||||
pub const REPO_URL_SCHEMA: Schema =
|
||||
StringSchema::new("Repository URL: [[auth-id@]server[:port]:]datastore")
|
||||
.format(&BACKUP_REPO_URL)
|
||||
.max_length(256)
|
||||
.schema();
|
||||
|
||||
pub const BACKUP_REPO_SERVER_SCHEMA: Schema =
|
||||
StringSchema::new("Backup server address (hostname or IP). Default: localhost")
|
||||
.format(&api_types::DNS_NAME_OR_IP_FORMAT)
|
||||
.max_length(256)
|
||||
.schema();
|
||||
|
||||
pub const BACKUP_REPO_PORT_SCHEMA: Schema = IntegerSchema::new("Backup server port. Default: 8007")
|
||||
.minimum(1)
|
||||
.maximum(65535)
|
||||
.default(8007)
|
||||
.schema();
|
||||
|
||||
#[api(
|
||||
properties: {
|
||||
repository: {
|
||||
schema: REPO_URL_SCHEMA,
|
||||
optional: true,
|
||||
},
|
||||
server: {
|
||||
schema: BACKUP_REPO_SERVER_SCHEMA,
|
||||
optional: true,
|
||||
},
|
||||
port: {
|
||||
schema: BACKUP_REPO_PORT_SCHEMA,
|
||||
optional: true,
|
||||
},
|
||||
datastore: {
|
||||
schema: DATASTORE_SCHEMA,
|
||||
optional: true,
|
||||
},
|
||||
"auth-id": {
|
||||
type: Authid,
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
)]
|
||||
#[derive(Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
/// Backup repository location, specified either as a repository URL or as individual
|
||||
/// components (server, port, datastore, auth-id).
|
||||
pub struct BackupRepositoryArgs {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub repository: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub server: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub port: Option<u16>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub datastore: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub auth_id: Option<Authid>,
|
||||
}
|
||||
|
||||
#[api(
|
||||
properties: {
|
||||
target: {
|
||||
type: BackupRepositoryArgs,
|
||||
flatten: true,
|
||||
},
|
||||
ns: {
|
||||
type: BackupNamespace,
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
)]
|
||||
#[derive(Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
/// Backup target for CLI commands, combining the repository location with an
|
||||
/// optional namespace.
|
||||
pub struct BackupTargetArgs {
|
||||
#[serde(flatten)]
|
||||
pub target: BackupRepositoryArgs,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub ns: Option<BackupNamespace>,
|
||||
}
|
||||
|
||||
impl BackupRepositoryArgs {
|
||||
/// Returns `true` if any atom parameter (server, port, datastore, or auth-id) is set.
|
||||
pub fn has_atoms(&self) -> bool {
|
||||
self.server.is_some()
|
||||
|| self.port.is_some()
|
||||
|| self.datastore.is_some()
|
||||
|| self.auth_id.is_some()
|
||||
}
|
||||
|
||||
/// Check that `--repository` and atom options are not mixed.
|
||||
pub fn check_mutual_exclusion(&self) -> Result<(), Error> {
|
||||
if self.repository.is_some() && self.has_atoms() {
|
||||
bail!("--repository and --server/--port/--datastore/--auth-id are mutually exclusive");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Merge `self` with `fallback`, using values from `self` where present
|
||||
/// and filling in from `fallback` for fields that are `None`.
|
||||
pub fn merge_from(self, fallback: BackupRepositoryArgs) -> Self {
|
||||
Self {
|
||||
repository: self.repository.or(fallback.repository),
|
||||
server: self.server.or(fallback.server),
|
||||
port: self.port.or(fallback.port),
|
||||
datastore: self.datastore.or(fallback.datastore),
|
||||
auth_id: self.auth_id.or(fallback.auth_id),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<BackupRepositoryArgs> for BackupRepository {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
/// Convert explicit CLI arguments into a [`BackupRepository`].
|
||||
///
|
||||
/// * If `repository` and any atom are both set, returns an error.
|
||||
/// * If atoms are present, builds the repository from them (requires `datastore`).
|
||||
/// * If only `repository` is set, parses the repo URL.
|
||||
/// * If nothing is set, returns an error - callers must fall back to environment variables /
|
||||
/// credentials themselves.
|
||||
fn try_from(args: BackupRepositoryArgs) -> Result<Self, Self::Error> {
|
||||
args.check_mutual_exclusion()?;
|
||||
|
||||
if args.has_atoms() {
|
||||
let store = args.datastore.ok_or_else(|| {
|
||||
format_err!("--datastore is required when not using --repository")
|
||||
})?;
|
||||
return Ok(BackupRepository::new(
|
||||
args.auth_id,
|
||||
args.server,
|
||||
args.port,
|
||||
store,
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(url) = args.repository {
|
||||
return url.parse();
|
||||
}
|
||||
|
||||
bail!("no repository specified")
|
||||
}
|
||||
}
|
||||
use pbs_api_types::{Authid, Userid, BACKUP_REPO_URL_REGEX, IP_V6_REGEX};
|
||||
|
||||
/// Reference remote backup locations
|
||||
///
|
||||
@ -178,7 +27,7 @@ impl BackupRepository {
|
||||
store: String,
|
||||
) -> Self {
|
||||
let host = match host {
|
||||
Some(host) if (IP_V6_REGEX.regex_obj)().is_match(&host) => Some(format!("[{host}]")),
|
||||
Some(host) if (IP_V6_REGEX.regex_obj)().is_match(&host) => Some(format!("[{}]", host)),
|
||||
other => other,
|
||||
};
|
||||
Self {
|
||||
@ -266,172 +115,3 @@ impl std::str::FromStr for BackupRepository {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parse_datastore_only() {
|
||||
let repo: BackupRepository = "mystore".parse().unwrap();
|
||||
assert_eq!(repo.store(), "mystore");
|
||||
assert_eq!(repo.host(), "localhost");
|
||||
assert_eq!(repo.port(), 8007);
|
||||
assert_eq!(repo.auth_id().to_string(), "root@pam");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_host_and_datastore() {
|
||||
let repo: BackupRepository = "myhost:mystore".parse().unwrap();
|
||||
assert_eq!(repo.host(), "myhost");
|
||||
assert_eq!(repo.store(), "mystore");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_full_with_port() {
|
||||
let repo: BackupRepository = "admin@pam@backuphost:8008:tank".parse().unwrap();
|
||||
assert_eq!(repo.auth_id().to_string(), "admin@pam");
|
||||
assert_eq!(repo.host(), "backuphost");
|
||||
assert_eq!(repo.port(), 8008);
|
||||
assert_eq!(repo.store(), "tank");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_ipv4_with_port() {
|
||||
let repo: BackupRepository = "192.168.1.1:1234:mystore".parse().unwrap();
|
||||
assert_eq!(repo.host(), "192.168.1.1");
|
||||
assert_eq!(repo.port(), 1234);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_ipv6_with_port() {
|
||||
let repo: BackupRepository = "[ff80::1]:9007:mystore".parse().unwrap();
|
||||
assert_eq!(repo.host(), "[ff80::1]");
|
||||
assert_eq!(repo.port(), 9007);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_api_token() {
|
||||
let repo: BackupRepository = "user@pbs!token@myhost:mystore".parse().unwrap();
|
||||
assert_eq!(repo.auth_id().to_string(), "user@pbs!token");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_invalid_url_errors() {
|
||||
assert!("".parse::<BackupRepository>().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display_round_trip() {
|
||||
for url in [
|
||||
"mystore",
|
||||
"myhost:mystore",
|
||||
"admin@pam@backuphost:8008:tank",
|
||||
] {
|
||||
let repo: BackupRepository = url.parse().unwrap();
|
||||
assert_eq!(repo.to_string(), url, "round-trip failed for '{url}'");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_wraps_bare_ipv6_in_brackets() {
|
||||
let repo = BackupRepository::new(None, Some("ff80::1".into()), None, "s".into());
|
||||
assert_eq!(repo.host(), "[ff80::1]");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_preserves_already_bracketed_ipv6() {
|
||||
let repo = BackupRepository::new(None, Some("[ff80::1]".into()), None, "s".into());
|
||||
assert_eq!(repo.host(), "[ff80::1]");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn has_atoms() {
|
||||
assert!(!BackupRepositoryArgs::default().has_atoms());
|
||||
|
||||
let with_server = BackupRepositoryArgs {
|
||||
server: Some("host".into()),
|
||||
..Default::default()
|
||||
};
|
||||
assert!(with_server.has_atoms());
|
||||
|
||||
let repo_only = BackupRepositoryArgs {
|
||||
repository: Some("myhost:mystore".into()),
|
||||
..Default::default()
|
||||
};
|
||||
assert!(!repo_only.has_atoms());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn try_from_atoms_only() {
|
||||
let args = BackupRepositoryArgs {
|
||||
server: Some("pbs.local".into()),
|
||||
port: Some(9000),
|
||||
datastore: Some("tank".into()),
|
||||
auth_id: Some("backup@pam".parse().unwrap()),
|
||||
..Default::default()
|
||||
};
|
||||
let repo = BackupRepository::try_from(args).unwrap();
|
||||
assert_eq!(repo.host(), "pbs.local");
|
||||
assert_eq!(repo.port(), 9000);
|
||||
assert_eq!(repo.store(), "tank");
|
||||
assert_eq!(repo.auth_id().to_string(), "backup@pam");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn try_from_atoms_datastore_only() {
|
||||
let args = BackupRepositoryArgs {
|
||||
datastore: Some("local".into()),
|
||||
..Default::default()
|
||||
};
|
||||
let repo = BackupRepository::try_from(args).unwrap();
|
||||
assert_eq!(repo.store(), "local");
|
||||
assert_eq!(repo.host(), "localhost");
|
||||
assert_eq!(repo.port(), 8007);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn try_from_url_only() {
|
||||
let args = BackupRepositoryArgs {
|
||||
repository: Some("admin@pam@backuphost:8008:mystore".into()),
|
||||
..Default::default()
|
||||
};
|
||||
let repo = BackupRepository::try_from(args).unwrap();
|
||||
assert_eq!(repo.host(), "backuphost");
|
||||
assert_eq!(repo.port(), 8008);
|
||||
assert_eq!(repo.store(), "mystore");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn try_from_mutual_exclusion_error() {
|
||||
let args = BackupRepositoryArgs {
|
||||
repository: Some("somehost:mystore".into()),
|
||||
server: Some("otherhost".into()),
|
||||
..Default::default()
|
||||
};
|
||||
let err = BackupRepository::try_from(args).unwrap_err();
|
||||
assert!(err.to_string().contains("mutually exclusive"), "got: {err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn try_from_nothing_set_error() {
|
||||
let err = BackupRepository::try_from(BackupRepositoryArgs::default()).unwrap_err();
|
||||
assert!(
|
||||
err.to_string().contains("no repository specified"),
|
||||
"got: {err}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn try_from_atoms_without_datastore_error() {
|
||||
let args = BackupRepositoryArgs {
|
||||
server: Some("pbs.local".into()),
|
||||
..Default::default()
|
||||
};
|
||||
let err = BackupRepository::try_from(args).unwrap_err();
|
||||
assert!(
|
||||
err.to_string().contains("--datastore is required"),
|
||||
"got: {err}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -52,54 +52,17 @@ pub struct UploadOptions {
|
||||
pub previous_manifest: Option<Arc<BackupManifest>>,
|
||||
pub compress: bool,
|
||||
pub encrypt: bool,
|
||||
pub index_type: IndexType,
|
||||
}
|
||||
|
||||
/// Index type for upload options.
|
||||
#[derive(Default, Clone)]
|
||||
pub enum IndexType {
|
||||
/// Dynamic chunking.
|
||||
#[default]
|
||||
Dynamic,
|
||||
/// Fixed size chunking with optional image file size.
|
||||
Fixed(Option<u64>),
|
||||
}
|
||||
|
||||
impl IndexType {
|
||||
fn to_prefix_and_size(&self) -> (&'static str, Option<u64>) {
|
||||
match self {
|
||||
IndexType::Fixed(size) => ("fixed", *size),
|
||||
IndexType::Dynamic => ("dynamic", None),
|
||||
}
|
||||
}
|
||||
pub fixed_size: Option<u64>,
|
||||
}
|
||||
|
||||
struct ChunkUploadResponse {
|
||||
future: h2::client::ResponseFuture,
|
||||
future: h2::legacy::client::ResponseFuture,
|
||||
size: usize,
|
||||
}
|
||||
|
||||
type UploadQueueSender = mpsc::Sender<(MergedChunkInfo, Option<ChunkUploadResponse>)>;
|
||||
type UploadResultReceiver = oneshot::Receiver<Result<(), Error>>;
|
||||
|
||||
/// Additional configuration options for BackupWriter instance
|
||||
pub struct BackupWriterOptions<'a> {
|
||||
/// Target datastore
|
||||
pub datastore: &'a str,
|
||||
/// Target namespace
|
||||
pub ns: &'a BackupNamespace,
|
||||
/// Target snapshot
|
||||
pub backup: &'a BackupDir,
|
||||
/// Crypto configuration
|
||||
pub crypt_config: Option<Arc<CryptConfig>>,
|
||||
/// Run in debug mode
|
||||
pub debug: bool,
|
||||
/// Start benchmark
|
||||
pub benchmark: bool,
|
||||
/// Skip datastore cache
|
||||
pub no_cache: bool,
|
||||
}
|
||||
|
||||
impl BackupWriter {
|
||||
fn new(h2: H2Client, abort: AbortHandle, crypt_config: Option<Arc<CryptConfig>>) -> Arc<Self> {
|
||||
Arc::new(Self {
|
||||
@ -109,25 +72,28 @@ impl BackupWriter {
|
||||
})
|
||||
}
|
||||
|
||||
// FIXME: extract into (flattened) parameter struct?
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn start(
|
||||
client: &HttpClient,
|
||||
writer_options: BackupWriterOptions<'_>,
|
||||
crypt_config: Option<Arc<CryptConfig>>,
|
||||
datastore: &str,
|
||||
ns: &BackupNamespace,
|
||||
backup: &BackupDir,
|
||||
debug: bool,
|
||||
benchmark: bool,
|
||||
) -> Result<Arc<BackupWriter>, Error> {
|
||||
let mut param = json!({
|
||||
"backup-type": writer_options.backup.ty(),
|
||||
"backup-id": writer_options.backup.id(),
|
||||
"backup-time": writer_options.backup.time,
|
||||
"store": writer_options.datastore,
|
||||
"debug": writer_options.debug,
|
||||
"benchmark": writer_options.benchmark,
|
||||
"backup-type": backup.ty(),
|
||||
"backup-id": backup.id(),
|
||||
"backup-time": backup.time,
|
||||
"store": datastore,
|
||||
"debug": debug,
|
||||
"benchmark": benchmark
|
||||
});
|
||||
if writer_options.no_cache {
|
||||
param["no-cache"] = serde_json::to_value(writer_options.no_cache)?;
|
||||
}
|
||||
|
||||
if !writer_options.ns.is_root() {
|
||||
param["ns"] = serde_json::to_value(writer_options.ns)?;
|
||||
if !ns.is_root() {
|
||||
param["ns"] = serde_json::to_value(ns)?;
|
||||
}
|
||||
|
||||
let req = HttpClient::request_builder(
|
||||
@ -143,7 +109,7 @@ impl BackupWriter {
|
||||
.start_h2_connection(req, String::from(PROXMOX_BACKUP_PROTOCOL_ID_V1!()))
|
||||
.await?;
|
||||
|
||||
Ok(BackupWriter::new(h2, abort, writer_options.crypt_config))
|
||||
Ok(BackupWriter::new(h2, abort, crypt_config))
|
||||
}
|
||||
|
||||
pub async fn get(&self, path: &str, param: Option<Value>) -> Result<Value, Error> {
|
||||
@ -177,7 +143,7 @@ impl BackupWriter {
|
||||
param: Option<Value>,
|
||||
content_type: &str,
|
||||
data: Vec<u8>,
|
||||
) -> Result<h2::client::ResponseFuture, Error> {
|
||||
) -> Result<h2::legacy::client::ResponseFuture, Error> {
|
||||
let request =
|
||||
H2Client::request_builder("localhost", method, path, param, Some(content_type))
|
||||
.unwrap();
|
||||
@ -311,10 +277,12 @@ impl BackupWriter {
|
||||
options: UploadOptions,
|
||||
) -> Result<BackupStats, Error> {
|
||||
let mut param = json!({ "archive-name": archive_name });
|
||||
let (prefix, archive_size) = options.index_type.to_prefix_and_size();
|
||||
if let Some(size) = archive_size {
|
||||
let prefix = if let Some(size) = options.fixed_size {
|
||||
param["size"] = size.into();
|
||||
}
|
||||
"fixed"
|
||||
} else {
|
||||
"dynamic"
|
||||
};
|
||||
|
||||
if options.encrypt && self.crypt_config.is_none() {
|
||||
bail!("requested encryption without a crypt config");
|
||||
@ -404,17 +372,19 @@ impl BackupWriter {
|
||||
let known_chunks = Arc::new(Mutex::new(HashSet::new()));
|
||||
|
||||
let mut param = json!({ "archive-name": archive_name });
|
||||
let (prefix, archive_size) = options.index_type.to_prefix_and_size();
|
||||
if let Some(size) = archive_size {
|
||||
let prefix = if let Some(size) = options.fixed_size {
|
||||
param["size"] = size.into();
|
||||
}
|
||||
"fixed"
|
||||
} else {
|
||||
"dynamic"
|
||||
};
|
||||
|
||||
if options.encrypt && self.crypt_config.is_none() {
|
||||
bail!("requested encryption without a crypt config");
|
||||
}
|
||||
|
||||
let index_path = format!("{prefix}_index");
|
||||
let close_path = format!("{prefix}_close");
|
||||
let index_path = format!("{}_index", prefix);
|
||||
let close_path = format!("{}_close", prefix);
|
||||
|
||||
if let Some(manifest) = options.previous_manifest {
|
||||
if !manifest
|
||||
@ -544,7 +514,7 @@ impl BackupWriter {
|
||||
}
|
||||
|
||||
fn response_queue() -> (
|
||||
mpsc::Sender<h2::client::ResponseFuture>,
|
||||
mpsc::Sender<h2::legacy::client::ResponseFuture>,
|
||||
oneshot::Receiver<Result<(), Error>>,
|
||||
) {
|
||||
let (verify_queue_tx, verify_queue_rx) = mpsc::channel(100);
|
||||
@ -567,7 +537,7 @@ impl BackupWriter {
|
||||
tokio::spawn(
|
||||
ReceiverStream::new(verify_queue_rx)
|
||||
.map(Ok::<_, Error>)
|
||||
.try_for_each(move |response: h2::client::ResponseFuture| {
|
||||
.try_for_each(move |response: h2::legacy::client::ResponseFuture| {
|
||||
response
|
||||
.map_err(Error::from)
|
||||
.and_then(H2Client::h2api_response)
|
||||
|
||||
@ -7,7 +7,6 @@ use std::ops::ControlFlow;
|
||||
use std::os::unix::ffi::{OsStrExt, OsStringExt};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::pin::Pin;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use anyhow::{bail, format_err, Error};
|
||||
use nix::dir::Dir;
|
||||
@ -36,7 +35,7 @@ type FileEntry = pxar::accessor::aio::FileEntry<Reader>;
|
||||
|
||||
const MAX_SYMLINK_COUNT: usize = 40;
|
||||
|
||||
static SHELL: Mutex<Option<Shell>> = Mutex::new(None);
|
||||
static mut SHELL: Option<usize> = None;
|
||||
|
||||
/// This list defines all the shell commands and their properties
|
||||
/// using the api schema
|
||||
@ -104,10 +103,8 @@ pub fn catalog_shell_cli() -> CommandLineInterface {
|
||||
}
|
||||
|
||||
fn complete_path(complete_me: &str, _map: &HashMap<String, String>) -> Vec<String> {
|
||||
let result = block_on(Shell::with(async |shell| {
|
||||
shell.complete_path(complete_me).await
|
||||
}));
|
||||
match result {
|
||||
let shell: &mut Shell = unsafe { std::mem::transmute(SHELL.unwrap()) };
|
||||
match shell.complete_path(complete_me) {
|
||||
Ok(list) => list,
|
||||
Err(err) => {
|
||||
error!("error during completion: {}", err);
|
||||
@ -127,7 +124,7 @@ async fn exit() -> Result<(), Error> {
|
||||
#[api(input: { properties: {} })]
|
||||
/// List the current working directory.
|
||||
async fn pwd_command() -> Result<(), Error> {
|
||||
Shell::with(Shell::pwd).await
|
||||
Shell::with(move |shell| shell.pwd()).await
|
||||
}
|
||||
|
||||
#[api(
|
||||
@ -144,7 +141,7 @@ async fn pwd_command() -> Result<(), Error> {
|
||||
/// Change the current working directory to the new directory
|
||||
async fn cd_command(path: Option<String>) -> Result<(), Error> {
|
||||
let path = path.as_ref().map(Path::new);
|
||||
Shell::with(async |shell| shell.cd(path).await).await
|
||||
Shell::with(move |shell| shell.cd(path)).await
|
||||
}
|
||||
|
||||
#[api(
|
||||
@ -161,7 +158,7 @@ async fn cd_command(path: Option<String>) -> Result<(), Error> {
|
||||
/// List the content of working directory or given path.
|
||||
async fn ls_command(path: Option<String>) -> Result<(), Error> {
|
||||
let path = path.as_ref().map(Path::new);
|
||||
Shell::with(async |shell| shell.ls(path).await).await
|
||||
Shell::with(move |shell| shell.ls(path)).await
|
||||
}
|
||||
|
||||
#[api(
|
||||
@ -179,7 +176,7 @@ async fn ls_command(path: Option<String>) -> Result<(), Error> {
|
||||
/// This is expensive because the data has to be read from the pxar archive, which means reading
|
||||
/// over the network.
|
||||
async fn stat_command(path: String) -> Result<(), Error> {
|
||||
Shell::with(async |shell| shell.stat(PathBuf::from(path)).await).await
|
||||
Shell::with(move |shell| shell.stat(PathBuf::from(path))).await
|
||||
}
|
||||
|
||||
#[api(
|
||||
@ -197,7 +194,7 @@ async fn stat_command(path: String) -> Result<(), Error> {
|
||||
/// This will return an error if the entry is already present in the list or
|
||||
/// if an invalid path was provided.
|
||||
async fn select_command(path: String) -> Result<(), Error> {
|
||||
Shell::with(async |shell| shell.select(PathBuf::from(path)).await).await
|
||||
Shell::with(move |shell| shell.select(PathBuf::from(path))).await
|
||||
}
|
||||
|
||||
#[api(
|
||||
@ -215,13 +212,13 @@ async fn select_command(path: String) -> Result<(), Error> {
|
||||
/// This will return an error if the entry was not found in the list of entries
|
||||
/// selected for restore.
|
||||
async fn deselect_command(path: String) -> Result<(), Error> {
|
||||
Shell::with(async |shell| shell.deselect(PathBuf::from(path)).await).await
|
||||
Shell::with(move |shell| shell.deselect(PathBuf::from(path))).await
|
||||
}
|
||||
|
||||
#[api( input: { properties: { } })]
|
||||
/// Clear the list of files selected for restore.
|
||||
async fn clear_selected_command() -> Result<(), Error> {
|
||||
Shell::with(async |shell| shell.deselect_all().await).await
|
||||
Shell::with(move |shell| shell.deselect_all()).await
|
||||
}
|
||||
|
||||
#[api(
|
||||
@ -238,7 +235,7 @@ async fn clear_selected_command() -> Result<(), Error> {
|
||||
)]
|
||||
/// List entries currently selected for restore.
|
||||
async fn list_selected_command(patterns: bool) -> Result<(), Error> {
|
||||
Shell::with(async |shell| shell.list_selected(patterns).await).await
|
||||
Shell::with(move |shell| shell.list_selected(patterns)).await
|
||||
}
|
||||
|
||||
#[api(
|
||||
@ -258,7 +255,7 @@ async fn list_selected_command(patterns: bool) -> Result<(), Error> {
|
||||
)]
|
||||
/// Find entries in the catalog matching the given match pattern.
|
||||
async fn find_command(pattern: String, select: bool) -> Result<(), Error> {
|
||||
Shell::with(async |shell| shell.find(pattern, select).await).await
|
||||
Shell::with(move |shell| shell.find(pattern, select)).await
|
||||
}
|
||||
|
||||
#[api(
|
||||
@ -275,7 +272,7 @@ async fn find_command(pattern: String, select: bool) -> Result<(), Error> {
|
||||
///
|
||||
/// Target must not exist on the clients filesystem.
|
||||
async fn restore_selected_command(target: String) -> Result<(), Error> {
|
||||
Shell::with(async |shell| shell.restore_selected(PathBuf::from(target)).await).await
|
||||
Shell::with(move |shell| shell.restore_selected(PathBuf::from(target))).await
|
||||
}
|
||||
|
||||
#[api(
|
||||
@ -298,7 +295,7 @@ async fn restore_selected_command(target: String) -> Result<(), Error> {
|
||||
/// subset of this sub-archive.
|
||||
/// If pattern is not present or empty, the full archive is restored to target.
|
||||
async fn restore_command(target: String, pattern: Option<String>) -> Result<(), Error> {
|
||||
Shell::with(async |shell| shell.restore(PathBuf::from(target), pattern).await).await
|
||||
Shell::with(move |shell| shell.restore(PathBuf::from(target), pattern)).await
|
||||
}
|
||||
|
||||
/// TODO: Should we use this to fix `step()`? Make path resolution behave more like described in
|
||||
@ -308,6 +305,9 @@ async fn restore_command(target: String, pattern: Option<String>) -> Result<(),
|
||||
/// trailing `Component::CurDir` entries. Since we only support regular paths we'll roll our own
|
||||
/// here:
|
||||
pub struct Shell {
|
||||
/// Readline instance handling input and callbacks
|
||||
rl: rustyline::Editor<CliHelper>,
|
||||
|
||||
/// Interactive prompt.
|
||||
prompt: String,
|
||||
|
||||
@ -351,6 +351,10 @@ impl Shell {
|
||||
archive_name: &str,
|
||||
archive: Accessor,
|
||||
) -> Result<Self, Error> {
|
||||
let cli_helper = CliHelper::new(catalog_shell_cli());
|
||||
let mut rl = rustyline::Editor::<CliHelper>::new();
|
||||
rl.set_helper(Some(cli_helper));
|
||||
|
||||
let mut position = Vec::new();
|
||||
if let Some(catalog) = catalog.as_mut() {
|
||||
let catalog_root = catalog.root()?;
|
||||
@ -378,6 +382,7 @@ impl Shell {
|
||||
}
|
||||
|
||||
let mut this = Self {
|
||||
rl,
|
||||
prompt: String::new(),
|
||||
catalog,
|
||||
selected: HashMap::new(),
|
||||
@ -388,40 +393,28 @@ impl Shell {
|
||||
Ok(this)
|
||||
}
|
||||
|
||||
async fn with<R, F>(call: F) -> Result<R, Error>
|
||||
async fn with<'a, Fut, R, F>(call: F) -> Result<R, Error>
|
||||
where
|
||||
F: AsyncFnOnce(&mut Shell) -> Result<R, Error>,
|
||||
F: FnOnce(&'a mut Shell) -> Fut,
|
||||
Fut: Future<Output = Result<R, Error>>,
|
||||
F: 'a,
|
||||
Fut: 'a,
|
||||
R: 'static,
|
||||
{
|
||||
// Under the assumption that only one shell command is executed at a
|
||||
// time, moving out of a std mutex like this ensures data integrity
|
||||
// after panics (or cancellations) and detects deadlocks from recursion.
|
||||
let mut shell = SHELL
|
||||
.lock()
|
||||
.unwrap()
|
||||
.take()
|
||||
.ok_or_else(|| format_err!("expected SHELL"))?;
|
||||
let result = call(&mut shell).await;
|
||||
*SHELL.lock().unwrap() = Some(shell);
|
||||
result
|
||||
let shell: &mut Shell = unsafe { std::mem::transmute(SHELL.unwrap()) };
|
||||
call(&mut *shell).await
|
||||
}
|
||||
|
||||
pub async fn shell(self) -> Result<(), Error> {
|
||||
let mut prompt = self.prompt.clone();
|
||||
|
||||
*SHELL.lock().unwrap() = Some(self);
|
||||
|
||||
let cli_helper = CliHelper::new(catalog_shell_cli());
|
||||
let mut rl = rustyline::Editor::<CliHelper, _>::with_history(
|
||||
rustyline::Config::default(),
|
||||
rustyline::history::MemHistory::new(),
|
||||
)?;
|
||||
rl.set_helper(Some(cli_helper));
|
||||
|
||||
while let Ok(line) = rl.readline(&prompt) {
|
||||
pub async fn shell(mut self) -> Result<(), Error> {
|
||||
let this = &mut self;
|
||||
unsafe {
|
||||
SHELL = Some(this as *mut Shell as usize);
|
||||
}
|
||||
while let Ok(line) = this.rl.readline(&this.prompt) {
|
||||
if line == "exit" {
|
||||
break;
|
||||
}
|
||||
let helper = rl.helper().unwrap();
|
||||
let helper = this.rl.helper().unwrap();
|
||||
let args = match cli::shellword_split(&line) {
|
||||
Ok(args) => args,
|
||||
Err(err) => {
|
||||
@ -433,16 +426,9 @@ impl Shell {
|
||||
let _ =
|
||||
cli::handle_command_future(helper.cmd_def(), "", args, cli::CliEnvironment::new())
|
||||
.await;
|
||||
let _ = rl.add_history_entry(line);
|
||||
|
||||
if let Some(shell) = &mut *SHELL.lock().unwrap() {
|
||||
shell.update_prompt();
|
||||
prompt = shell.prompt.clone();
|
||||
} else {
|
||||
bail!("SHELL missing at prompt update");
|
||||
}
|
||||
this.rl.add_history_entry(line);
|
||||
this.update_prompt();
|
||||
}
|
||||
*SHELL.lock().unwrap() = None;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -474,7 +460,7 @@ impl Shell {
|
||||
)
|
||||
.await?;
|
||||
let path = Self::format_path_stack(&stack);
|
||||
println!("{path:?}");
|
||||
println!("{:?}", path);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -565,7 +551,7 @@ impl Shell {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn step_nofollow(
|
||||
fn step_nofollow(
|
||||
stack: &mut Vec<PathStackEntry>,
|
||||
catalog: &mut Option<CatalogReader>,
|
||||
component: std::path::Component<'_>,
|
||||
@ -590,8 +576,8 @@ impl Shell {
|
||||
}
|
||||
} else {
|
||||
let pxar_entry = parent_pxar_entry(stack)?;
|
||||
let parent_dir = pxar_entry.enter_directory().await?;
|
||||
match parent_dir.lookup(entry).await? {
|
||||
let parent_dir = block_on(pxar_entry.enter_directory())?;
|
||||
match block_on(parent_dir.lookup(entry))? {
|
||||
Some(entry) => {
|
||||
let entry_attr = DirEntryAttribute::try_from(&entry)?;
|
||||
stack.push(PathStackEntry {
|
||||
@ -625,13 +611,13 @@ impl Shell {
|
||||
}
|
||||
|
||||
/// Non-async version cannot follow symlinks.
|
||||
async fn walk_catalog_nofollow(
|
||||
fn walk_catalog_nofollow(
|
||||
stack: &mut Vec<PathStackEntry>,
|
||||
catalog: &mut Option<CatalogReader>,
|
||||
path: &Path,
|
||||
) -> Result<(), Error> {
|
||||
for c in path.components() {
|
||||
Self::step_nofollow(stack, catalog, c).await?;
|
||||
Self::step_nofollow(stack, catalog, c)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@ -668,7 +654,7 @@ impl Shell {
|
||||
Ok(stack.last().unwrap().pxar.clone().unwrap())
|
||||
}
|
||||
|
||||
async fn complete_path(&mut self, input: &str) -> Result<Vec<String>, Error> {
|
||||
fn complete_path(&mut self, input: &str) -> Result<Vec<String>, Error> {
|
||||
let mut tmp_stack;
|
||||
let (parent, base, part) = match input.rfind('/') {
|
||||
Some(ind) => {
|
||||
@ -679,7 +665,7 @@ impl Shell {
|
||||
} else {
|
||||
tmp_stack = self.position.clone();
|
||||
}
|
||||
Self::walk_catalog_nofollow(&mut tmp_stack, &mut self.catalog, &path).await?;
|
||||
Self::walk_catalog_nofollow(&mut tmp_stack, &mut self.catalog, &path)?;
|
||||
(&tmp_stack.last().unwrap(), base, part)
|
||||
}
|
||||
None => (&self.position.last().unwrap(), "", input),
|
||||
@ -689,12 +675,12 @@ impl Shell {
|
||||
catalog.read_dir(&parent.catalog)?
|
||||
} else {
|
||||
let dir = if let Some(entry) = parent.pxar.as_ref() {
|
||||
entry.enter_directory().await?
|
||||
block_on(entry.enter_directory())?
|
||||
} else {
|
||||
bail!("missing pxar entry for parent");
|
||||
};
|
||||
let mut out = Vec::new();
|
||||
let entries = crate::pxar::tools::pxar_metadata_read_dir(dir).await?;
|
||||
let entries = block_on(crate::pxar::tools::pxar_metadata_read_dir(dir))?;
|
||||
for entry in entries {
|
||||
let mut name = base.to_string();
|
||||
let file_name = entry.file_name().as_bytes();
|
||||
@ -873,9 +859,9 @@ impl Shell {
|
||||
let path = Self::format_path_stack(&stack);
|
||||
let entry = MatchEntry::include(MatchPattern::Literal(path.as_bytes().to_vec()));
|
||||
if self.selected.insert(path.clone(), entry).is_some() {
|
||||
println!("path already selected: {path:?}");
|
||||
println!("path already selected: {:?}", path);
|
||||
} else {
|
||||
println!("added path: {path:?}");
|
||||
println!("added path: {:?}", path);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@ -894,9 +880,9 @@ impl Shell {
|
||||
let path = Self::format_path_stack(&stack);
|
||||
|
||||
if self.selected.remove(&path).is_some() {
|
||||
println!("removed path from selection: {path:?}");
|
||||
println!("removed path from selection: {:?}", path);
|
||||
} else {
|
||||
println!("path not selected: {path:?}");
|
||||
println!("path not selected: {:?}", path);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@ -918,7 +904,7 @@ impl Shell {
|
||||
|
||||
async fn list_selected_patterns(&self) -> Result<(), Error> {
|
||||
for entry in self.selected.keys() {
|
||||
println!("{entry:?}");
|
||||
println!("{:?}", entry);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@ -1091,8 +1077,7 @@ impl Shell {
|
||||
extractor,
|
||||
match_list,
|
||||
&self.accessor,
|
||||
)
|
||||
.await?;
|
||||
)?;
|
||||
|
||||
extractor.extract().await
|
||||
}
|
||||
@ -1131,7 +1116,7 @@ struct ExtractorState<'a> {
|
||||
}
|
||||
|
||||
impl<'a> ExtractorState<'a> {
|
||||
pub async fn new(
|
||||
pub fn new(
|
||||
catalog: &'a mut Option<CatalogReader>,
|
||||
dir_stack: Vec<PathStackEntry>,
|
||||
extractor: crate::pxar::extract::Extractor,
|
||||
@ -1144,8 +1129,8 @@ impl<'a> ExtractorState<'a> {
|
||||
.into_iter()
|
||||
} else {
|
||||
let pxar_entry = parent_pxar_entry(&dir_stack)?;
|
||||
let dir = pxar_entry.enter_directory().await?;
|
||||
let entries = crate::pxar::tools::pxar_metadata_read_dir(dir).await?;
|
||||
let dir = block_on(pxar_entry.enter_directory())?;
|
||||
let entries = block_on(crate::pxar::tools::pxar_metadata_read_dir(dir))?;
|
||||
|
||||
let mut catalog_entries = Vec::with_capacity(entries.len());
|
||||
for entry in entries {
|
||||
@ -1241,7 +1226,7 @@ impl<'a> ExtractorState<'a> {
|
||||
let dir = Shell::walk_pxar_archive(self.accessor, &mut self.dir_stack).await?;
|
||||
self.dir_stack.pop();
|
||||
let dir = dir.enter_directory().await?;
|
||||
let entries = crate::pxar::tools::pxar_metadata_read_dir(dir).await?;
|
||||
let entries = block_on(crate::pxar::tools::pxar_metadata_read_dir(dir))?;
|
||||
entries
|
||||
.into_iter()
|
||||
.map(|entry| {
|
||||
|
||||
@ -34,7 +34,6 @@ impl InjectionData {
|
||||
/// Split input stream into dynamic sized chunks
|
||||
pub struct ChunkStream<S: Unpin> {
|
||||
input: S,
|
||||
input_is_done: bool,
|
||||
chunker: Box<dyn Chunker + Send>,
|
||||
buffer: BytesMut,
|
||||
scan_pos: usize,
|
||||
@ -52,7 +51,6 @@ impl<S: Unpin> ChunkStream<S> {
|
||||
let chunk_size = chunk_size.unwrap_or(4 * 1024 * 1024);
|
||||
Self {
|
||||
input,
|
||||
input_is_done: false,
|
||||
chunker: if let Some(suggested) = suggested_boundaries {
|
||||
Box::new(PayloadChunker::new(chunk_size, suggested))
|
||||
} else {
|
||||
@ -164,16 +162,12 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
if this.input_is_done {
|
||||
return Poll::Ready(None);
|
||||
}
|
||||
match ready!(Pin::new(&mut this.input).try_poll_next(cx)) {
|
||||
Some(Err(err)) => {
|
||||
return Poll::Ready(Some(Err(err.into())));
|
||||
}
|
||||
None => {
|
||||
this.scan_pos = 0;
|
||||
this.input_is_done = true;
|
||||
if !this.buffer.is_empty() {
|
||||
return Poll::Ready(Some(Ok(this.buffer.split())));
|
||||
} else {
|
||||
@ -193,7 +187,6 @@ pub struct FixedChunkStream<S: Unpin> {
|
||||
input: S,
|
||||
chunk_size: usize,
|
||||
buffer: BytesMut,
|
||||
done: bool,
|
||||
}
|
||||
|
||||
impl<S: Unpin> FixedChunkStream<S> {
|
||||
@ -202,7 +195,6 @@ impl<S: Unpin> FixedChunkStream<S> {
|
||||
input,
|
||||
chunk_size,
|
||||
buffer: BytesMut::new(),
|
||||
done: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -221,9 +213,6 @@ where
|
||||
cx: &mut Context,
|
||||
) -> Poll<Option<Result<BytesMut, S::Error>>> {
|
||||
let this = self.get_mut();
|
||||
if this.done {
|
||||
return Poll::Ready(None);
|
||||
}
|
||||
loop {
|
||||
if this.buffer.len() >= this.chunk_size {
|
||||
return Poll::Ready(Some(Ok(this.buffer.split_to(this.chunk_size))));
|
||||
@ -234,9 +223,6 @@ where
|
||||
return Poll::Ready(Some(Err(err)));
|
||||
}
|
||||
None => {
|
||||
// Must not call input.try_poll_next again!
|
||||
this.done = true;
|
||||
|
||||
// last chunk can have any size
|
||||
if !this.buffer.is_empty() {
|
||||
return Poll::Ready(Some(Ok(this.buffer.split())));
|
||||
@ -260,12 +246,11 @@ mod test {
|
||||
|
||||
struct DummyInput {
|
||||
data: Vec<u8>,
|
||||
done: bool,
|
||||
}
|
||||
|
||||
impl DummyInput {
|
||||
fn new(data: Vec<u8>) -> Self {
|
||||
Self { data, done: false }
|
||||
Self { data }
|
||||
}
|
||||
}
|
||||
|
||||
@ -275,11 +260,7 @@ mod test {
|
||||
fn poll_next(self: Pin<&mut Self>, _cx: &mut Context) -> Poll<Option<Self::Item>> {
|
||||
let this = self.get_mut();
|
||||
match this.data.len() {
|
||||
0 => {
|
||||
assert!(!this.done);
|
||||
this.done = true;
|
||||
Poll::Ready(None)
|
||||
}
|
||||
0 => Poll::Ready(None),
|
||||
size if size > 10 => Poll::Ready(Some(Ok(this.data.split_off(10)))),
|
||||
_ => Poll::Ready(Some(Ok(std::mem::take(&mut this.data)))),
|
||||
}
|
||||
|
||||
@ -3,18 +3,14 @@ use std::sync::{Arc, Mutex, RwLock};
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{bail, format_err, Error};
|
||||
use bytes::Bytes;
|
||||
use futures::*;
|
||||
use http_body_util::{BodyDataStream, BodyExt};
|
||||
use hyper::body::Incoming;
|
||||
use hyper::header::SET_COOKIE;
|
||||
#[cfg(not(target_feature = "crt-static"))]
|
||||
use hyper::client::connect::dns::GaiResolver;
|
||||
use hyper::client::{Client, HttpConnector};
|
||||
use hyper::http::header::HeaderValue;
|
||||
use hyper::http::Uri;
|
||||
use hyper::http::{Request, Response};
|
||||
#[cfg(not(target_feature = "crt-static"))]
|
||||
use hyper_util::client::legacy::connect::dns::GaiResolver;
|
||||
use hyper_util::client::legacy::{connect::HttpConnector, Client};
|
||||
use hyper_util::rt::{TokioExecutor, TokioIo};
|
||||
use hyper::{body::HttpBody, Body};
|
||||
use openssl::{
|
||||
ssl::{SslConnector, SslMethod},
|
||||
x509::X509StoreContextRef,
|
||||
@ -30,10 +26,8 @@ use proxmox_sys::linux::tty;
|
||||
use proxmox_async::broadcast_future::BroadcastFuture;
|
||||
use proxmox_http::client::HttpsConnector;
|
||||
use proxmox_http::uri::{build_authority, json_object_to_query};
|
||||
use proxmox_http::Body;
|
||||
use proxmox_http::ProxyConfig;
|
||||
use proxmox_http::{ProxyConfig, RateLimiter};
|
||||
use proxmox_log::{error, info, warn};
|
||||
use proxmox_rate_limiter::RateLimiter;
|
||||
|
||||
use pbs_api_types::percent_encoding::DEFAULT_ENCODE_SET;
|
||||
use pbs_api_types::{Authid, RateLimitConfig, Userid};
|
||||
@ -58,7 +52,7 @@ mod resolver {
|
||||
use hickory_resolver::error::ResolveError;
|
||||
use hickory_resolver::lookup_ip::LookupIpIntoIter;
|
||||
use hickory_resolver::TokioAsyncResolver;
|
||||
use hyper_util::client::legacy::connect::dns::Name;
|
||||
use hyper::client::connect::dns::Name;
|
||||
use tower_service::Service;
|
||||
|
||||
pub(crate) struct SocketAddrIter {
|
||||
@ -113,16 +107,11 @@ mod resolver {
|
||||
/// certain error conditions. Keep it generous, to avoid false-positive under high load.
|
||||
const HTTP_TIMEOUT: Duration = Duration::from_secs(2 * 60);
|
||||
|
||||
const PROXMOX_BACKUP_AUTH_COOKIE: &str = "PBSAuthCookie";
|
||||
const PROXMOX_BACKUP_PREFIXED_AUTH_COOKIE: &str = "__Host-PBSAuthCookie";
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AuthInfo {
|
||||
pub auth_id: Authid,
|
||||
pub ticket: String,
|
||||
pub token: String,
|
||||
// Whether the server uses HttpOnly cookies for authentication
|
||||
pub http_only: bool,
|
||||
}
|
||||
|
||||
pub struct HttpClientOptions {
|
||||
@ -134,7 +123,6 @@ pub struct HttpClientOptions {
|
||||
fingerprint_cache: bool,
|
||||
verify_cert: bool,
|
||||
limit: RateLimitConfig,
|
||||
proxy: Option<ProxyConfig>,
|
||||
}
|
||||
|
||||
impl HttpClientOptions {
|
||||
@ -197,11 +185,6 @@ impl HttpClientOptions {
|
||||
self.limit = rate_limit;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn proxy(mut self, proxy: Option<ProxyConfig>) -> Self {
|
||||
self.proxy = proxy;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for HttpClientOptions {
|
||||
@ -215,14 +198,13 @@ impl Default for HttpClientOptions {
|
||||
fingerprint_cache: false,
|
||||
verify_cert: true,
|
||||
limit: RateLimitConfig::default(), // unlimited
|
||||
proxy: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// HTTP(S) API client
|
||||
pub struct HttpClient {
|
||||
client: Client<HttpsConnector<DnsResolver>, Body>,
|
||||
client: Client<HttpsConnector<DnsResolver>>,
|
||||
server: String,
|
||||
port: u16,
|
||||
fingerprint: Arc<Mutex<Option<String>>>,
|
||||
@ -244,11 +226,7 @@ pub fn delete_ticket_info(prefix: &str, server: &str, username: &Userid) -> Resu
|
||||
let mut data = file_get_json(&path, Some(json!({})))?;
|
||||
|
||||
if let Some(map) = data[server].as_object_mut() {
|
||||
if map.remove(username.as_str()).is_none() {
|
||||
warn!("no ticket found for {username} on server {server}");
|
||||
}
|
||||
} else {
|
||||
warn!("no ticket found for server {server}");
|
||||
map.remove(username.as_str());
|
||||
}
|
||||
|
||||
replace_file(
|
||||
@ -392,8 +370,8 @@ fn build_uri(server: &str, port: u16, path: &str, query: Option<String>) -> Resu
|
||||
.scheme("https")
|
||||
.authority(build_authority(server, port)?)
|
||||
.path_and_query(match query {
|
||||
Some(query) => format!("/{path}?{query}"),
|
||||
None => format!("/{path}"),
|
||||
Some(query) => format!("/{}?{}", path, query),
|
||||
None => format!("/{}", path),
|
||||
})
|
||||
.build()
|
||||
.map_err(|err| format_err!("error building uri - {}", err))
|
||||
@ -437,11 +415,11 @@ impl HttpClient {
|
||||
) {
|
||||
Ok(None) => true,
|
||||
Ok(Some(fingerprint)) => {
|
||||
if fingerprint_cache {
|
||||
if let Some(ref prefix) = prefix {
|
||||
if let Err(err) = store_fingerprint(prefix, &server, &fingerprint) {
|
||||
error!("{}", err);
|
||||
}
|
||||
if fingerprint_cache && prefix.is_some() {
|
||||
if let Err(err) =
|
||||
store_fingerprint(prefix.as_ref().unwrap(), &server, &fingerprint)
|
||||
{
|
||||
error!("{}", err);
|
||||
}
|
||||
}
|
||||
*verified_fingerprint.lock().unwrap() = Some(fingerprint);
|
||||
@ -485,15 +463,13 @@ impl HttpClient {
|
||||
)))));
|
||||
}
|
||||
|
||||
let proxy_config = options.proxy.clone();
|
||||
let proxy_config = proxy_config.or(ProxyConfig::from_proxy_env()?);
|
||||
|
||||
let proxy_config = ProxyConfig::from_proxy_env()?;
|
||||
if let Some(config) = proxy_config {
|
||||
info!("Using proxy connection: {}:{}", config.host, config.port);
|
||||
https.set_proxy(config);
|
||||
}
|
||||
|
||||
let client = Client::builder(TokioExecutor::new())
|
||||
let client = Client::builder()
|
||||
//.http2_initial_stream_window_size( (1 << 31) - 2)
|
||||
//.http2_initial_connection_window_size( (1 << 31) - 2)
|
||||
.build::<_, Body>(https);
|
||||
@ -524,7 +500,6 @@ impl HttpClient {
|
||||
auth_id: auth_id.clone(),
|
||||
ticket: password.clone(),
|
||||
token: "".to_string(),
|
||||
http_only: false,
|
||||
}));
|
||||
|
||||
let server2 = server.to_string();
|
||||
@ -549,18 +524,16 @@ impl HttpClient {
|
||||
.await
|
||||
{
|
||||
Ok(auth) => {
|
||||
if use_ticket_cache {
|
||||
if let Some(ref prefix) = prefix2 {
|
||||
if let Err(err) = store_ticket_info(
|
||||
prefix,
|
||||
&server2,
|
||||
&auth.auth_id.to_string(),
|
||||
&auth.ticket,
|
||||
&auth.token,
|
||||
) {
|
||||
if std::io::stdout().is_terminal() {
|
||||
error!("storing login ticket failed: {}", err);
|
||||
}
|
||||
if use_ticket_cache && prefix2.is_some() {
|
||||
if let Err(err) = store_ticket_info(
|
||||
prefix2.as_ref().unwrap(),
|
||||
&server2,
|
||||
&auth.auth_id.to_string(),
|
||||
&auth.ticket,
|
||||
&auth.token,
|
||||
) {
|
||||
if std::io::stdout().is_terminal() {
|
||||
error!("storing login ticket failed: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -588,22 +561,19 @@ impl HttpClient {
|
||||
let authinfo = auth.clone();
|
||||
|
||||
move |auth| {
|
||||
if use_ticket_cache {
|
||||
if let Some(ref prefix) = prefix {
|
||||
if let Err(err) = store_ticket_info(
|
||||
prefix,
|
||||
&server,
|
||||
&auth.auth_id.to_string(),
|
||||
&auth.ticket,
|
||||
&auth.token,
|
||||
) {
|
||||
if std::io::stdout().is_terminal() {
|
||||
error!("storing login ticket failed: {}", err);
|
||||
}
|
||||
if use_ticket_cache && prefix.is_some() {
|
||||
if let Err(err) = store_ticket_info(
|
||||
prefix.as_ref().unwrap(),
|
||||
&server,
|
||||
&auth.auth_id.to_string(),
|
||||
&auth.ticket,
|
||||
&auth.token,
|
||||
) {
|
||||
if std::io::stdout().is_terminal() {
|
||||
error!("storing login ticket failed: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
*authinfo.write().unwrap() = auth;
|
||||
tokio::spawn(renewal_future);
|
||||
}
|
||||
@ -654,7 +624,7 @@ impl HttpClient {
|
||||
fn get_password(username: &Userid, interactive: bool) -> Result<String, Error> {
|
||||
// If we're on a TTY, query the user for a password
|
||||
if interactive && std::io::stdin().is_terminal() {
|
||||
let msg = format!("Password for \"{username}\": ");
|
||||
let msg = format!("Password for \"{}\": ", username);
|
||||
return Ok(String::from_utf8(tty::read_password(&msg)?)?);
|
||||
}
|
||||
|
||||
@ -751,18 +721,10 @@ impl HttpClient {
|
||||
HeaderValue::from_str(&enc_api_token).unwrap(),
|
||||
);
|
||||
} else {
|
||||
let cookie_name = if auth.http_only {
|
||||
// server has switched to http only flow, provide ticket in properly prefixed cookie
|
||||
PROXMOX_BACKUP_PREFIXED_AUTH_COOKIE
|
||||
} else {
|
||||
PROXMOX_BACKUP_AUTH_COOKIE
|
||||
};
|
||||
|
||||
let enc_ticket = format!(
|
||||
"{cookie_name}={}",
|
||||
"PBSAuthCookie={}",
|
||||
percent_encode(auth.ticket.as_bytes(), DEFAULT_ENCODE_SET)
|
||||
);
|
||||
|
||||
req.headers_mut()
|
||||
.insert("Cookie", HeaderValue::from_str(&enc_ticket).unwrap());
|
||||
req.headers_mut().insert(
|
||||
@ -801,18 +763,10 @@ impl HttpClient {
|
||||
|
||||
let auth = self.login().await?;
|
||||
|
||||
let cookie_name = if auth.http_only {
|
||||
// server has switched to http only flow, provide ticket in properly prefixed cookie
|
||||
PROXMOX_BACKUP_PREFIXED_AUTH_COOKIE
|
||||
} else {
|
||||
PROXMOX_BACKUP_AUTH_COOKIE
|
||||
};
|
||||
|
||||
let enc_ticket = format!(
|
||||
"{cookie_name}={}",
|
||||
"PBSAuthCookie={}",
|
||||
percent_encode(auth.ticket.as_bytes(), DEFAULT_ENCODE_SET)
|
||||
);
|
||||
|
||||
req.headers_mut()
|
||||
.insert("Cookie", HeaderValue::from_str(&enc_ticket).unwrap());
|
||||
|
||||
@ -825,7 +779,7 @@ impl HttpClient {
|
||||
.map(|_| Err(format_err!("unknown error")))
|
||||
.await?
|
||||
} else {
|
||||
futures::TryStreamExt::map_err(BodyDataStream::new(resp.into_body()), Error::from)
|
||||
futures::TryStreamExt::map_err(resp.into_body(), Error::from)
|
||||
.try_fold(output, move |acc, chunk| async move {
|
||||
acc.write_all(&chunk)?;
|
||||
Ok::<_, Error>(acc)
|
||||
@ -878,18 +832,10 @@ impl HttpClient {
|
||||
HeaderValue::from_str(&enc_api_token).unwrap(),
|
||||
);
|
||||
} else {
|
||||
let cookie_name = if auth.http_only {
|
||||
// server has switched to http only flow, provide ticket in properly prefixed cookie
|
||||
PROXMOX_BACKUP_PREFIXED_AUTH_COOKIE
|
||||
} else {
|
||||
PROXMOX_BACKUP_AUTH_COOKIE
|
||||
};
|
||||
|
||||
let enc_ticket = format!(
|
||||
"{cookie_name}={}",
|
||||
"PBSAuthCookie={}",
|
||||
percent_encode(auth.ticket.as_bytes(), DEFAULT_ENCODE_SET)
|
||||
);
|
||||
|
||||
req.headers_mut()
|
||||
.insert("Cookie", HeaderValue::from_str(&enc_ticket).unwrap());
|
||||
req.headers_mut().insert(
|
||||
@ -913,11 +859,11 @@ impl HttpClient {
|
||||
bail!("unknown error");
|
||||
}
|
||||
|
||||
let upgraded = TokioIo::new(hyper::upgrade::on(resp).await?);
|
||||
let upgraded = hyper::upgrade::on(resp).await?;
|
||||
|
||||
let max_window_size = (1 << 31) - 2;
|
||||
|
||||
let (h2, connection) = h2::client::Builder::new()
|
||||
let (h2, connection) = h2::legacy::client::Builder::new()
|
||||
.initial_connection_window_size(max_window_size)
|
||||
.initial_window_size(max_window_size)
|
||||
.max_frame_size(4 * 1024 * 1024)
|
||||
@ -941,7 +887,7 @@ impl HttpClient {
|
||||
}
|
||||
|
||||
async fn credentials(
|
||||
client: Client<HttpsConnector<DnsResolver>, Body>,
|
||||
client: Client<HttpsConnector<DnsResolver>>,
|
||||
server: String,
|
||||
port: u16,
|
||||
username: Userid,
|
||||
@ -955,51 +901,22 @@ impl HttpClient {
|
||||
"/api2/json/access/ticket",
|
||||
Some(data),
|
||||
)?;
|
||||
|
||||
let res = tokio::time::timeout(HTTP_TIMEOUT, client.request(req))
|
||||
.await
|
||||
.map_err(|_| format_err!("http request timed out"))??;
|
||||
|
||||
// check if the headers contain a newer HttpOnly cookie
|
||||
let http_only_ticket = res
|
||||
.headers()
|
||||
.get_all(SET_COOKIE)
|
||||
.iter()
|
||||
.filter_map(|c| c.to_str().ok())
|
||||
.filter_map(|c| match (c.find('='), c.find(';')) {
|
||||
(Some(begin), Some(end))
|
||||
if begin < end && &c[..begin] == PROXMOX_BACKUP_PREFIXED_AUTH_COOKIE =>
|
||||
{
|
||||
Some(c[begin + 1..end].to_string())
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.next();
|
||||
|
||||
// if the headers contained a new HttpOnly cookie, the server switched to providing these
|
||||
// by default. this means that older cookies may no longer be supported, so switch to using
|
||||
// the new cookie name.
|
||||
let http_only = http_only_ticket.is_some();
|
||||
|
||||
let cred = Self::api_response(res).await?;
|
||||
let cred = Self::api_request(client, req).await?;
|
||||
let auth = AuthInfo {
|
||||
auth_id: cred["data"]["username"].as_str().unwrap().parse()?,
|
||||
ticket: http_only_ticket
|
||||
.or(cred["data"]["ticket"].as_str().map(|t| t.to_string()))
|
||||
.unwrap(),
|
||||
ticket: cred["data"]["ticket"].as_str().unwrap().to_owned(),
|
||||
token: cred["data"]["CSRFPreventionToken"]
|
||||
.as_str()
|
||||
.unwrap()
|
||||
.to_owned(),
|
||||
http_only,
|
||||
};
|
||||
|
||||
Ok(auth)
|
||||
}
|
||||
|
||||
async fn api_response(response: Response<Incoming>) -> Result<Value, Error> {
|
||||
async fn api_response(response: Response<Body>) -> Result<Value, Error> {
|
||||
let status = response.status();
|
||||
let data = response.into_body().collect().await?.to_bytes();
|
||||
let data = HttpBody::collect(response.into_body()).await?.to_bytes();
|
||||
|
||||
let text = String::from_utf8(data.to_vec()).unwrap();
|
||||
if status.is_success() {
|
||||
@ -1015,7 +932,7 @@ impl HttpClient {
|
||||
}
|
||||
|
||||
async fn api_request(
|
||||
client: Client<HttpsConnector<DnsResolver>, Body>,
|
||||
client: Client<HttpsConnector<DnsResolver>>,
|
||||
req: Request<Body>,
|
||||
) -> Result<Value, Error> {
|
||||
Self::api_response(
|
||||
@ -1050,7 +967,7 @@ impl HttpClient {
|
||||
.uri(url)
|
||||
.header("User-Agent", "proxmox-backup-client/1.0")
|
||||
.header(hyper::header::CONTENT_TYPE, "application/json")
|
||||
.body(data.to_string().into())?;
|
||||
.body(Body::from(data.to_string()))?;
|
||||
Ok(request)
|
||||
} else {
|
||||
let query = json_object_to_query(data)?;
|
||||
@ -1091,11 +1008,11 @@ impl Drop for HttpClient {
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct H2Client {
|
||||
h2: h2::client::SendRequest<Bytes>,
|
||||
h2: h2::legacy::client::SendRequest<bytes::Bytes>,
|
||||
}
|
||||
|
||||
impl H2Client {
|
||||
pub fn new(h2: h2::client::SendRequest<Bytes>) -> Self {
|
||||
pub fn new(h2: h2::legacy::client::SendRequest<bytes::Bytes>) -> Self {
|
||||
Self { h2 }
|
||||
}
|
||||
|
||||
@ -1175,7 +1092,7 @@ impl H2Client {
|
||||
&self,
|
||||
request: Request<()>,
|
||||
data: Option<bytes::Bytes>,
|
||||
) -> impl Future<Output = Result<h2::client::ResponseFuture, Error>> {
|
||||
) -> impl Future<Output = Result<h2::legacy::client::ResponseFuture, Error>> {
|
||||
self.h2
|
||||
.clone()
|
||||
.ready()
|
||||
@ -1192,7 +1109,9 @@ impl H2Client {
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn h2api_response(response: Response<h2::RecvStream>) -> Result<Value, Error> {
|
||||
pub async fn h2api_response(
|
||||
response: Response<h2::legacy::RecvStream>,
|
||||
) -> Result<Value, Error> {
|
||||
let status = response.status();
|
||||
|
||||
let (_head, mut body) = response.into_parts();
|
||||
|
||||
@ -8,7 +8,7 @@ use std::task::{Context, Poll};
|
||||
use anyhow::{format_err, Error};
|
||||
use bytes::Bytes;
|
||||
use futures::{ready, Future};
|
||||
use h2::SendStream;
|
||||
use h2::legacy::SendStream;
|
||||
|
||||
pub struct PipeToSendStream {
|
||||
body_tx: SendStream<Bytes>,
|
||||
|
||||
@ -188,11 +188,6 @@ struct Archiver {
|
||||
suggested_boundaries: Option<mpsc::Sender<u64>>,
|
||||
previous_payload_index: Option<DynamicIndexReader>,
|
||||
cache: PxarLookaheadCache,
|
||||
// Highest payload offset accepted as reusable so far. Mirrors the encoder's accumulated
|
||||
// strict-monotonic-offset invariant on the read side, so that a previous archive written by
|
||||
// an older client without the encode-time check is detected here and the affected file is
|
||||
// re-encoded instead of reused.
|
||||
last_reusable_offset: Option<u64>,
|
||||
reuse_stats: ReuseStats,
|
||||
split_archive: bool,
|
||||
}
|
||||
@ -303,7 +298,6 @@ where
|
||||
suggested_boundaries,
|
||||
previous_payload_index,
|
||||
cache: PxarLookaheadCache::new(options.max_cache_size),
|
||||
last_reusable_offset: None,
|
||||
reuse_stats: ReuseStats::default(),
|
||||
split_archive,
|
||||
};
|
||||
@ -424,16 +418,6 @@ impl Archiver {
|
||||
.boxed()
|
||||
}
|
||||
|
||||
/// Record an accepted reusable payload offset from the previous metadata archive.
|
||||
///
|
||||
/// Returns `false` if `offset` is not strictly greater than the last accepted offset, which
|
||||
/// signals a duplicate or non-monotonic previous archive. The caller must then treat the file
|
||||
/// as non-reusable so the encoder's strict offset check cannot reject the eventual
|
||||
/// `add_payload_ref`. Tracking is global to catch inversions that span cache ranges.
|
||||
fn try_record_reusable_offset(&mut self, offset: u64) -> bool {
|
||||
try_record_strictly_greater(&mut self.last_reusable_offset, offset)
|
||||
}
|
||||
|
||||
async fn is_reusable_entry(
|
||||
&mut self,
|
||||
previous_metadata_accessor: &Option<Directory<MetadataArchiveReader>>,
|
||||
@ -453,19 +437,6 @@ impl Archiver {
|
||||
if file_size != *size {
|
||||
return Ok(None);
|
||||
}
|
||||
// a previous archive written by an older client version may contain
|
||||
// duplicate or non-monotonic PXAR_PAYLOAD_REF offsets for distinct files.
|
||||
// Re-encoding the affected file lets the chain self-heal and avoids the
|
||||
// pxar strict offset check rejecting the whole backup.
|
||||
if !self.try_record_reusable_offset(*offset) {
|
||||
let last = self.last_reusable_offset.unwrap_or(0);
|
||||
warn!(
|
||||
"re-encode: {file_name:?} previous archive payload offset \
|
||||
{offset} not strictly greater than last seen {last}, \
|
||||
treating as non-reusable"
|
||||
);
|
||||
return Ok(None);
|
||||
}
|
||||
let range =
|
||||
*offset..*offset + size + size_of::<pxar::format::Header>() as u64;
|
||||
debug!(
|
||||
@ -566,20 +537,13 @@ impl Archiver {
|
||||
|
||||
let mut buf;
|
||||
let (line, mode, anchored) = if line[0] == b'/' {
|
||||
buf = Vec::with_capacity(path_bytes.len() + 2 + line.len());
|
||||
// need to anchor the base path if it is not
|
||||
if !path_bytes.is_empty() && !path_bytes.starts_with(b"/") {
|
||||
buf.push(b'/');
|
||||
}
|
||||
buf = Vec::with_capacity(path_bytes.len() + 1 + line.len());
|
||||
buf.extend(path_bytes);
|
||||
buf.extend(line);
|
||||
(&buf[..], MatchType::Exclude, true)
|
||||
} else if line.starts_with(b"!/") {
|
||||
// inverted case with absolute path
|
||||
buf = Vec::with_capacity(path_bytes.len() + 1 + line.len());
|
||||
if !path_bytes.is_empty() && !path_bytes.starts_with(b"/") {
|
||||
buf.push(b'/');
|
||||
}
|
||||
buf = Vec::with_capacity(path_bytes.len() + line.len());
|
||||
buf.extend(path_bytes);
|
||||
buf.extend(&line[1..]); // without the '!'
|
||||
(&buf[..], MatchType::Include, true)
|
||||
@ -589,20 +553,7 @@ impl Archiver {
|
||||
(line, MatchType::Exclude, false)
|
||||
};
|
||||
|
||||
let line = OsStr::from_bytes(line);
|
||||
let line_normalized = crate::pxar::tools::normalize_lexically(line);
|
||||
if line_normalized.as_os_str() != line {
|
||||
warn!(
|
||||
"Sanitized exclude pattern. Exclude patterns are relative to the current \
|
||||
backup root, not the current working directory and should not contain '.' or \
|
||||
'..' as path segments."
|
||||
);
|
||||
}
|
||||
match MatchEntry::parse_pattern(
|
||||
line_normalized.as_os_str().as_bytes(),
|
||||
PatternFlag::PATH_NAME,
|
||||
mode,
|
||||
) {
|
||||
match MatchEntry::parse_pattern(line, PatternFlag::PATH_NAME, mode) {
|
||||
Ok(pattern) => {
|
||||
if anchored {
|
||||
self.patterns.push(pattern.add_flags(MatchFlag::ANCHORED));
|
||||
@ -679,11 +630,7 @@ impl Archiver {
|
||||
let mut stat_results: Option<FileStat> = None;
|
||||
|
||||
let get_file_mode = || {
|
||||
nix::sys::stat::fstatat(
|
||||
Some(dir_fd),
|
||||
file_name,
|
||||
nix::fcntl::AtFlags::AT_SYMLINK_NOFOLLOW,
|
||||
)
|
||||
nix::sys::stat::fstatat(dir_fd, file_name, nix::fcntl::AtFlags::AT_SYMLINK_NOFOLLOW)
|
||||
};
|
||||
|
||||
let match_result = self
|
||||
@ -1063,10 +1010,9 @@ impl Archiver {
|
||||
let mut padding = start_padding + end_padding;
|
||||
let total_size = (range.end - range.start) + padding;
|
||||
|
||||
// must use the same identity predicate as the absorb branch below; otherwise
|
||||
// a dedup collision subtracts an unrelated chunk's bytes from padding.
|
||||
// take into account used bytes of kept back chunk for padding
|
||||
if let (Some(first), Some(last)) = (indices.first(), prev_last_chunk.as_ref()) {
|
||||
if last.same_indexed_chunk_as(first) {
|
||||
if last.digest() == first.digest() {
|
||||
// Update padding used for threshold calculation only
|
||||
let used = last.size() - last.padding;
|
||||
padding -= used;
|
||||
@ -1094,13 +1040,10 @@ impl Archiver {
|
||||
indices.len(),
|
||||
);
|
||||
|
||||
// inject the kept back chunk unless it refers to the same index entry
|
||||
// as the new range's first: digests differ when the previous range ended
|
||||
// on a chunk boundary, and matching digests at different `end_offset`s
|
||||
// are a dedup collision in the previous archive (same content at distinct
|
||||
// index positions). Both cases need an explicit injection.
|
||||
// check for cases where kept back last is not equal first chunk because the range
|
||||
// end aligned with a chunk boundary, and the chunks therefore needs to be injected
|
||||
if let (Some(first), Some(last)) = (indices.first_mut(), prev_last_chunk) {
|
||||
if !last.same_indexed_chunk_as(first) {
|
||||
if last.digest() != first.digest() {
|
||||
// make sure to inject previous last chunk before encoding entries
|
||||
self.inject_chunks_at_current_payload_position(encoder, vec![last])?;
|
||||
} else {
|
||||
@ -1366,7 +1309,7 @@ impl Archiver {
|
||||
file_name: &Path,
|
||||
metadata: &Metadata,
|
||||
) -> Result<(), Error> {
|
||||
let dest = match nix::fcntl::readlinkat(Some(fd.as_raw_fd()), &b""[..]) {
|
||||
let dest = match nix::fcntl::readlinkat(fd.as_raw_fd(), &b""[..]) {
|
||||
Ok(dest) => dest,
|
||||
Err(Errno::ESTALE) => {
|
||||
self.report_stale_file_handle(None);
|
||||
@ -1402,10 +1345,6 @@ pub struct ReusableDynamicEntry {
|
||||
size: u64,
|
||||
padding: u64,
|
||||
digest: [u8; 32],
|
||||
/// Absolute end position of this entry in the previous archive's payload index;
|
||||
/// together with `digest` the canonical identity tuple for an index entry -
|
||||
/// `digest` alone aliases dedup-collided entries at distinct index positions.
|
||||
end_offset: u64,
|
||||
}
|
||||
|
||||
impl ReusableDynamicEntry {
|
||||
@ -1418,28 +1357,6 @@ impl ReusableDynamicEntry {
|
||||
pub fn digest(&self) -> [u8; 32] {
|
||||
self.digest
|
||||
}
|
||||
|
||||
/// Returns whether `self` and `other` reference the same entry in the previous
|
||||
/// archive's payload index. Two distinct entries can share a digest (same chunk
|
||||
/// content via deduplication), so digest equality alone aliases dedup collisions;
|
||||
/// `end_offset` is the per-index-entry identity component that disambiguates them.
|
||||
#[inline]
|
||||
pub fn same_indexed_chunk_as(&self, other: &Self) -> bool {
|
||||
self.digest == other.digest && self.end_offset == other.end_offset
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates `last` to `offset` and returns `true` if `offset` is strictly greater than the current
|
||||
/// value (or `last` is `None`). Returns `false` without modifying `last` for duplicate or
|
||||
/// backwards offsets.
|
||||
fn try_record_strictly_greater(last: &mut Option<u64>, offset: u64) -> bool {
|
||||
if let Some(prev) = *last {
|
||||
if offset <= prev {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
*last = Some(offset);
|
||||
true
|
||||
}
|
||||
|
||||
/// List of dynamic entries containing the data given by an offset range
|
||||
@ -1467,7 +1384,6 @@ fn lookup_dynamic_entries(
|
||||
size: (end - prev_end),
|
||||
padding: 0,
|
||||
digest: dynamic_entry.digest(),
|
||||
end_offset: end,
|
||||
};
|
||||
indices.push(reusable_dynamic_entry);
|
||||
|
||||
@ -2074,7 +1990,6 @@ mod tests {
|
||||
previous_payload_index,
|
||||
suggested_boundaries: Some(suggested_boundaries),
|
||||
cache: PxarLookaheadCache::new(None),
|
||||
last_reusable_offset: None,
|
||||
reuse_stats: ReuseStats::default(),
|
||||
split_archive: true,
|
||||
};
|
||||
@ -2108,93 +2023,4 @@ mod tests {
|
||||
Ok::<(), Error>(())
|
||||
})
|
||||
}
|
||||
|
||||
/// Strictly-greater bookkeeping for [`Archiver::try_record_reusable_offset`]; mirrors the
|
||||
/// encoder's accumulated strict-monotonic-offset invariant on the read side.
|
||||
#[test]
|
||||
fn try_record_strictly_greater_accepts_increasing() {
|
||||
let mut last = None;
|
||||
assert!(super::try_record_strictly_greater(&mut last, 100));
|
||||
assert_eq!(last, Some(100));
|
||||
assert!(super::try_record_strictly_greater(&mut last, 200));
|
||||
assert_eq!(last, Some(200));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn try_record_strictly_greater_rejects_equal_and_backwards() {
|
||||
let mut last = Some(200);
|
||||
// duplicate offset: previous archive recorded two distinct files at the same offset
|
||||
assert!(!super::try_record_strictly_greater(&mut last, 200));
|
||||
assert_eq!(last, Some(200), "rejected offset must not update state");
|
||||
// backwards offset: previous archive recorded the next file at a lower offset
|
||||
assert!(!super::try_record_strictly_greater(&mut last, 150));
|
||||
assert_eq!(last, Some(200));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn try_record_strictly_greater_first_call_accepts_any_value() {
|
||||
let mut last = None;
|
||||
assert!(super::try_record_strictly_greater(&mut last, 0));
|
||||
assert_eq!(last, Some(0));
|
||||
}
|
||||
|
||||
/// Pins `lookup_dynamic_entries`'s `end_offset` population to `dynamic_entry.end()`;
|
||||
/// any range-relative substitute would alias dedup-collided entries and resurrect
|
||||
/// the backwards-`PXAR_PAYLOAD_REF` bug class.
|
||||
#[test]
|
||||
fn lookup_dynamic_entries_pins_chunk_identity() {
|
||||
use pbs_datastore::dynamic_index::{DynamicIndexReader, DynamicIndexWriter};
|
||||
|
||||
// Use ./target/ instead of tempfile / std::env::temp_dir to keep the test
|
||||
// runnable in build environments where /tmp may not be writable.
|
||||
let mut testdir = PathBuf::from("./target/testout");
|
||||
testdir.push(std::module_path!());
|
||||
let _ = std::fs::create_dir_all(&testdir);
|
||||
let path = testdir.join("lookup_dynamic_entries.didx");
|
||||
let _ = std::fs::remove_file(&path);
|
||||
|
||||
let mut writer = DynamicIndexWriter::create(&path).unwrap();
|
||||
let digest_a = [0xAAu8; 32];
|
||||
let digest_b = [0xBBu8; 32];
|
||||
// chunk 0: ends at 1024, digest A
|
||||
writer.add_chunk(1024, &digest_a).unwrap();
|
||||
// chunk 1: ends at 2048, digest B
|
||||
writer.add_chunk(2048, &digest_b).unwrap();
|
||||
// chunk 2: ends at 3072, digest A again - dedup collision (same content,
|
||||
// distinct position). This is the configuration the bug class lives on.
|
||||
writer.add_chunk(3072, &digest_a).unwrap();
|
||||
let _csum = writer.close().unwrap();
|
||||
|
||||
let reader = DynamicIndexReader::open(&path).unwrap();
|
||||
let (indices, _start_padding, _end_padding) =
|
||||
super::lookup_dynamic_entries(&reader, &(0..3072)).unwrap();
|
||||
|
||||
assert_eq!(indices.len(), 3);
|
||||
assert_eq!(indices[0].end_offset, 1024);
|
||||
assert_eq!(indices[1].end_offset, 2048);
|
||||
assert_eq!(indices[2].end_offset, 3072);
|
||||
|
||||
// The dedup collision must NOT compare equal under same_indexed_chunk_as; this
|
||||
// is the predicate flush_cached_reusing_if_below_threshold relies on for the
|
||||
// absorb-vs-inject decision.
|
||||
assert_eq!(indices[0].digest, indices[2].digest);
|
||||
assert!(!indices[0].same_indexed_chunk_as(&indices[2]));
|
||||
assert!(indices[0].same_indexed_chunk_as(&indices[0]));
|
||||
|
||||
let _ = std::fs::remove_file(&path);
|
||||
}
|
||||
|
||||
/// Inversions that span cache ranges must be caught by the global tracking; a per-range
|
||||
/// implementation would have reset on flush and accepted the second offset, after which
|
||||
/// the encoder strict check would still abort the backup.
|
||||
#[test]
|
||||
fn try_record_strictly_greater_persists_across_resets() {
|
||||
let mut last = None;
|
||||
assert!(super::try_record_strictly_greater(&mut last, 1000));
|
||||
// a per-range implementation would have cleared `last` here on cache flush
|
||||
assert!(!super::try_record_strictly_greater(&mut last, 500));
|
||||
assert_eq!(last, Some(1000));
|
||||
assert!(super::try_record_strictly_greater(&mut last, 1500));
|
||||
assert_eq!(last, Some(1500));
|
||||
}
|
||||
}
|
||||
|
||||
@ -36,12 +36,12 @@ impl PxarDir {
|
||||
}
|
||||
|
||||
fn create_dir(
|
||||
&'_ mut self,
|
||||
&mut self,
|
||||
parent: RawFd,
|
||||
allow_existing_dirs: bool,
|
||||
) -> Result<BorrowedFd<'_>, Error> {
|
||||
) -> Result<BorrowedFd, Error> {
|
||||
if let Err(err) = mkdirat(
|
||||
Some(parent),
|
||||
parent,
|
||||
self.file_name.as_os_str(),
|
||||
perms_from_metadata(&self.metadata)?,
|
||||
) {
|
||||
@ -53,9 +53,9 @@ impl PxarDir {
|
||||
self.open_dir(parent)
|
||||
}
|
||||
|
||||
fn open_dir(&'_ mut self, parent: RawFd) -> Result<BorrowedFd<'_>, Error> {
|
||||
fn open_dir(&mut self, parent: RawFd) -> Result<BorrowedFd, Error> {
|
||||
let dir = Dir::openat(
|
||||
Some(parent),
|
||||
parent,
|
||||
self.file_name.as_os_str(),
|
||||
OFlag::O_DIRECTORY | OFlag::O_CLOEXEC,
|
||||
Mode::empty(),
|
||||
@ -68,7 +68,7 @@ impl PxarDir {
|
||||
Ok(fd)
|
||||
}
|
||||
|
||||
pub fn try_as_borrowed_fd(&'_ self) -> Option<BorrowedFd<'_>> {
|
||||
pub fn try_as_borrowed_fd(&self) -> Option<BorrowedFd> {
|
||||
// Once `nix` adds `AsFd` support use `.as_fd()` instead.
|
||||
self.dir
|
||||
.as_ref()
|
||||
@ -120,7 +120,7 @@ impl PxarDirStack {
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
pub fn last_dir_fd(&'_ mut self, allow_existing_dirs: bool) -> Result<BorrowedFd<'_>, Error> {
|
||||
pub fn last_dir_fd(&mut self, allow_existing_dirs: bool) -> Result<BorrowedFd, Error> {
|
||||
// should not be possible given the way we use it:
|
||||
assert!(!self.dirs.is_empty(), "PxarDirStack underrun");
|
||||
|
||||
@ -147,7 +147,7 @@ impl PxarDirStack {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn root_dir_fd(&'_ self) -> Result<BorrowedFd<'_>, Error> {
|
||||
pub fn root_dir_fd(&self) -> Result<BorrowedFd, Error> {
|
||||
// should not be possible given the way we use it:
|
||||
assert!(!self.dirs.is_empty(), "PxarDirStack underrun");
|
||||
|
||||
|
||||
@ -26,7 +26,7 @@ use proxmox_log::{debug, error, info};
|
||||
use proxmox_sys::c_result;
|
||||
use proxmox_sys::fs::{create_path, CreateOptions};
|
||||
|
||||
use proxmox_compression::zip::{FileType, ZipEncoder, ZipEntry};
|
||||
use proxmox_compression::zip::{ZipEncoder, ZipEntry};
|
||||
|
||||
use crate::pxar::dir_stack::PxarDirStack;
|
||||
use crate::pxar::metadata;
|
||||
@ -627,7 +627,7 @@ impl Extractor {
|
||||
target.as_c_str(),
|
||||
Some(parent),
|
||||
file_name,
|
||||
nix::fcntl::AtFlags::empty(),
|
||||
nix::unistd::LinkatFlags::NoSymlinkFollow,
|
||||
)
|
||||
};
|
||||
|
||||
@ -697,13 +697,8 @@ impl Extractor {
|
||||
}
|
||||
let mut file = unsafe {
|
||||
std::fs::File::from_raw_fd(
|
||||
nix::fcntl::openat(
|
||||
Some(parent),
|
||||
file_name,
|
||||
oflags,
|
||||
Mode::from_bits(0o600).unwrap(),
|
||||
)
|
||||
.with_context(|| format!("failed to create file {file_name:?}"))?,
|
||||
nix::fcntl::openat(parent, file_name, oflags, Mode::from_bits(0o600).unwrap())
|
||||
.with_context(|| format!("failed to create file {file_name:?}"))?,
|
||||
)
|
||||
};
|
||||
|
||||
@ -727,7 +722,7 @@ impl Extractor {
|
||||
}
|
||||
|
||||
if result.seeked_last {
|
||||
while match nix::unistd::ftruncate(&file, size as i64) {
|
||||
while match nix::unistd::ftruncate(file.as_raw_fd(), size as i64) {
|
||||
Ok(_) => false,
|
||||
Err(nix::errno::Errno::EINTR) => true,
|
||||
Err(err) => return Err(err).context("error setting file size"),
|
||||
@ -763,13 +758,8 @@ impl Extractor {
|
||||
}
|
||||
let mut file = tokio::fs::File::from_std(unsafe {
|
||||
std::fs::File::from_raw_fd(
|
||||
nix::fcntl::openat(
|
||||
Some(parent),
|
||||
file_name,
|
||||
oflags,
|
||||
Mode::from_bits(0o600).unwrap(),
|
||||
)
|
||||
.with_context(|| format!("failed to create file {file_name:?}"))?,
|
||||
nix::fcntl::openat(parent, file_name, oflags, Mode::from_bits(0o600).unwrap())
|
||||
.with_context(|| format!("failed to create file {file_name:?}"))?,
|
||||
)
|
||||
});
|
||||
|
||||
@ -794,7 +784,7 @@ impl Extractor {
|
||||
}
|
||||
|
||||
if result.seeked_last {
|
||||
while match nix::unistd::ftruncate(&file, size as i64) {
|
||||
while match nix::unistd::ftruncate(file.as_raw_fd(), size as i64) {
|
||||
Ok(_) => false,
|
||||
Err(nix::errno::Errno::EINTR) => true,
|
||||
Err(err) => return Err(err).context("error setting file size"),
|
||||
@ -1034,7 +1024,7 @@ where
|
||||
path,
|
||||
metadata.stat.mtime.secs,
|
||||
metadata.stat.mode as u16,
|
||||
FileType::Directory,
|
||||
false,
|
||||
);
|
||||
zip.add_entry::<FileContents<T>>(entry, None).await?;
|
||||
}
|
||||
@ -1053,7 +1043,7 @@ where
|
||||
path,
|
||||
metadata.stat.mtime.secs,
|
||||
metadata.stat.mode as u16,
|
||||
FileType::Regular,
|
||||
true,
|
||||
);
|
||||
let contents = decoder.contents().await?;
|
||||
zip.add_entry(entry, contents)
|
||||
@ -1064,7 +1054,7 @@ where
|
||||
let entry = root
|
||||
.lookup(&path)
|
||||
.await?
|
||||
.with_context(|| format!("error looking up {path:?}"))?;
|
||||
.with_context(|| format!("error looking up {:?}", path))?;
|
||||
let realfile = accessor.follow_hardlink(&entry).await?;
|
||||
let metadata = realfile.entry().metadata();
|
||||
debug!("adding '{}' to zip", path.display());
|
||||
@ -1072,7 +1062,7 @@ where
|
||||
path,
|
||||
metadata.stat.mtime.secs,
|
||||
metadata.stat.mode as u16,
|
||||
FileType::Regular,
|
||||
true,
|
||||
);
|
||||
let contents = decoder.contents().await?;
|
||||
zip.add_entry(entry, contents)
|
||||
@ -1085,28 +1075,17 @@ where
|
||||
path,
|
||||
metadata.stat.mtime.secs,
|
||||
metadata.stat.mode as u16,
|
||||
FileType::Directory,
|
||||
false,
|
||||
);
|
||||
zip.add_entry::<FileContents<T>>(entry, None).await?;
|
||||
}
|
||||
EntryKind::Symlink(target) => {
|
||||
debug!("adding '{}' to zip", path.display());
|
||||
let entry = ZipEntry::new(
|
||||
path,
|
||||
metadata.stat.mtime.secs,
|
||||
metadata.stat.mode as u16,
|
||||
FileType::Symlink,
|
||||
);
|
||||
let target = io::Cursor::<&[u8]>::new(target.as_ref());
|
||||
zip.add_entry(entry, Some(target)).await?;
|
||||
}
|
||||
_ => {} // ignore all else
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
zip.finish().await.map_err(|err| {
|
||||
eprintln!("error during finishing of zip: {err}");
|
||||
eprintln!("error during finishing of zip: {}", err);
|
||||
err
|
||||
})
|
||||
}
|
||||
@ -1333,11 +1312,16 @@ where
|
||||
}
|
||||
.await
|
||||
{
|
||||
let path_display = match entry.kind() {
|
||||
EntryKind::GoodbyeTable => "<directory>".to_string(),
|
||||
_ => entry.path().display().to_string(),
|
||||
};
|
||||
error!("error extracting {path_display}: {err}");
|
||||
let display_string = entry.path().display().to_string();
|
||||
error!(
|
||||
"error extracting {}: {}",
|
||||
if matches!(entry.kind(), EntryKind::GoodbyeTable) {
|
||||
"<directory>"
|
||||
} else {
|
||||
&display_string
|
||||
},
|
||||
err
|
||||
);
|
||||
}
|
||||
|
||||
if dir_level < 0 {
|
||||
|
||||
@ -105,7 +105,7 @@ pub fn apply(
|
||||
path_info: &Path,
|
||||
on_error: &mut (dyn FnMut(Error) -> Result<(), Error> + Send),
|
||||
) -> Result<(), Error> {
|
||||
let c_proc_path = CString::new(format!("/proc/self/fd/{fd}")).unwrap();
|
||||
let c_proc_path = CString::new(format!("/proc/self/fd/{}", fd)).unwrap();
|
||||
apply_ownership(flags, c_proc_path.as_ptr(), metadata, &mut *on_error)?;
|
||||
|
||||
let mut skip_xattrs = false;
|
||||
|
||||
@ -33,7 +33,7 @@ pub(crate) fn perms_from_metadata(meta: &Metadata) -> Result<Mode, Error> {
|
||||
.context("couldn't narrow permission bits")
|
||||
.and_then(|mode| {
|
||||
Mode::from_bits(mode)
|
||||
.with_context(|| format!("mode contains illegal bits: 0x{mode:x} (0o{mode:o})"))
|
||||
.with_context(|| format!("mode contains illegal bits: 0x{:x} (0o{:o})", mode, mode))
|
||||
})
|
||||
}
|
||||
|
||||
@ -76,35 +76,6 @@ fn assert_single_path_component_do(path: &Path) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn normalize_lexically<S: AsRef<OsStr> + ?Sized>(path: &S) -> PathBuf {
|
||||
// FIXME: Once std::path::normalize_lexically is stabilized we can
|
||||
// switch to that
|
||||
use std::path::Component;
|
||||
|
||||
let path = Path::new(path);
|
||||
let has_trailing_slash = path
|
||||
.as_os_str()
|
||||
.as_encoded_bytes()
|
||||
.ends_with(std::path::MAIN_SEPARATOR_STR.as_bytes());
|
||||
let mut new = PathBuf::new();
|
||||
let iter = path.components();
|
||||
for component in iter {
|
||||
match component {
|
||||
Component::RootDir => new.push(Component::RootDir),
|
||||
Component::Prefix(p) => new.push(Component::Prefix(p)),
|
||||
Component::CurDir => continue,
|
||||
Component::ParentDir => {
|
||||
new.pop();
|
||||
}
|
||||
Component::Normal(n) => new.push(n),
|
||||
};
|
||||
}
|
||||
if has_trailing_slash {
|
||||
new.push("");
|
||||
}
|
||||
new
|
||||
}
|
||||
|
||||
#[rustfmt::skip]
|
||||
fn symbolic_mode(c: u64, special: bool, special_x: u8, special_no_x: u8) -> [u8; 3] {
|
||||
[
|
||||
|
||||
@ -42,9 +42,9 @@ pub enum KeySource {
|
||||
|
||||
pub fn format_key_source(source: &KeySource, key_type: &str) -> String {
|
||||
match source {
|
||||
KeySource::DefaultKey => format!("Using default {key_type} key.."),
|
||||
KeySource::Fd => format!("Using {key_type} key from file descriptor.."),
|
||||
KeySource::Path(path) => format!("Using {key_type} key from '{path}'.."),
|
||||
KeySource::DefaultKey => format!("Using default {} key..", key_type),
|
||||
KeySource::Fd => format!("Using {} key from file descriptor..", key_type),
|
||||
KeySource::Path(path) => format!("Using {} key from '{}'..", key_type, path),
|
||||
}
|
||||
}
|
||||
|
||||
@ -386,9 +386,9 @@ fn test_crypto_parameters_handling() -> Result<(), Error> {
|
||||
|
||||
let testdir = create_testdir("key_source")?;
|
||||
|
||||
let keypath = format!("{testdir}/keyfile.test");
|
||||
let master_keypath = format!("{testdir}/masterkeyfile.test");
|
||||
let invalid_keypath = format!("{testdir}/invalid_keyfile.test");
|
||||
let keypath = format!("{}/keyfile.test", testdir);
|
||||
let master_keypath = format!("{}/masterkeyfile.test", testdir);
|
||||
let invalid_keypath = format!("{}/invalid_keyfile.test", testdir);
|
||||
|
||||
let no_key_res = CryptoParams {
|
||||
enc_key: None,
|
||||
|
||||
@ -17,14 +17,12 @@ use proxmox_router::cli::{complete_file_name, shellword_split};
|
||||
use proxmox_schema::*;
|
||||
use proxmox_sys::fs::file_get_json;
|
||||
|
||||
use pbs_api_types::{Authid, BackupArchiveName, BackupNamespace, RateLimitConfig, UserWithTokens};
|
||||
use pbs_api_types::{
|
||||
Authid, BackupArchiveName, BackupNamespace, RateLimitConfig, UserWithTokens, BACKUP_REPO_URL,
|
||||
};
|
||||
use pbs_datastore::BackupManifest;
|
||||
|
||||
use crate::{BackupRepository, BackupRepositoryArgs, HttpClient, HttpClientOptions};
|
||||
|
||||
// Re-export for backward compatibility; the canonical definition is now in backup_repo alongside
|
||||
// BackupRepositoryArgs.
|
||||
pub use crate::REPO_URL_SCHEMA;
|
||||
use crate::{BackupRepository, HttpClient, HttpClientOptions};
|
||||
|
||||
pub mod key_source;
|
||||
|
||||
@ -32,11 +30,6 @@ const ENV_VAR_PBS_FINGERPRINT: &str = "PBS_FINGERPRINT";
|
||||
const ENV_VAR_PBS_PASSWORD: &str = "PBS_PASSWORD";
|
||||
const ENV_VAR_PBS_ENCRYPTION_PASSWORD: &str = "PBS_ENCRYPTION_PASSWORD";
|
||||
const ENV_VAR_PBS_REPOSITORY: &str = "PBS_REPOSITORY";
|
||||
const ENV_VAR_PBS_SERVER: &str = "PBS_SERVER";
|
||||
const ENV_VAR_PBS_PORT: &str = "PBS_PORT";
|
||||
const ENV_VAR_PBS_DATASTORE: &str = "PBS_DATASTORE";
|
||||
const ENV_VAR_PBS_AUTH_ID: &str = "PBS_AUTH_ID";
|
||||
const ENV_VAR_PBS_NAMESPACE: &str = "PBS_NAMESPACE";
|
||||
|
||||
/// Directory with system [credential]s. See systemd-creds(1).
|
||||
///
|
||||
@ -51,6 +44,11 @@ const CRED_PBS_REPOSITORY: &str = "proxmox-backup-client.repository";
|
||||
/// Credential name of the the fingerprint.
|
||||
const CRED_PBS_FINGERPRINT: &str = "proxmox-backup-client.fingerprint";
|
||||
|
||||
pub const REPO_URL_SCHEMA: Schema = StringSchema::new("Repository URL.")
|
||||
.format(&BACKUP_REPO_URL)
|
||||
.max_length(256)
|
||||
.schema();
|
||||
|
||||
pub const CHUNK_SIZE_SCHEMA: Schema = IntegerSchema::new("Chunk size in KB. Must be a power of 2.")
|
||||
.minimum(64)
|
||||
.maximum(4096)
|
||||
@ -115,7 +113,7 @@ fn get_secret_from_env(base_name: &str) -> Result<Option<String>, Error> {
|
||||
Err(NotPresent) => {}
|
||||
};
|
||||
|
||||
let env_name = format!("{base_name}_FD");
|
||||
let env_name = format!("{}_FD", base_name);
|
||||
match std::env::var(&env_name) {
|
||||
Ok(fd_str) => {
|
||||
let fd: i32 = fd_str.parse().map_err(|err| {
|
||||
@ -132,7 +130,7 @@ fn get_secret_from_env(base_name: &str) -> Result<Option<String>, Error> {
|
||||
Err(NotPresent) => {}
|
||||
}
|
||||
|
||||
let env_name = format!("{base_name}_FILE");
|
||||
let env_name = format!("{}_FILE", base_name);
|
||||
match std::env::var(&env_name) {
|
||||
Ok(filename) => {
|
||||
let mut file = std::fs::File::open(filename)
|
||||
@ -143,7 +141,7 @@ fn get_secret_from_env(base_name: &str) -> Result<Option<String>, Error> {
|
||||
Err(NotPresent) => {}
|
||||
}
|
||||
|
||||
let env_name = format!("{base_name}_CMD");
|
||||
let env_name = format!("{}_CMD", base_name);
|
||||
match std::env::var(&env_name) {
|
||||
Ok(ref command) => {
|
||||
let args = shellword_split(command)?;
|
||||
@ -235,122 +233,41 @@ pub fn get_fingerprint() -> Option<String> {
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Build [`BackupRepositoryArgs`] from the fields in a JSON Value.
|
||||
fn args_from_value(param: &Value) -> BackupRepositoryArgs {
|
||||
BackupRepositoryArgs {
|
||||
repository: param["repository"].as_str().map(String::from),
|
||||
server: param["server"].as_str().map(String::from),
|
||||
port: param["port"].as_u64().map(|p| p as u16),
|
||||
datastore: param["datastore"].as_str().map(String::from),
|
||||
auth_id: param["auth-id"]
|
||||
.as_str()
|
||||
.and_then(|s| s.parse::<Authid>().ok()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Build [`BackupRepositoryArgs`] from `PBS_*` environment variables.
|
||||
fn args_from_env() -> BackupRepositoryArgs {
|
||||
BackupRepositoryArgs {
|
||||
repository: None,
|
||||
server: std::env::var(ENV_VAR_PBS_SERVER).ok(),
|
||||
port: std::env::var(ENV_VAR_PBS_PORT)
|
||||
.ok()
|
||||
.and_then(|p| p.parse::<u16>().ok()),
|
||||
datastore: std::env::var(ENV_VAR_PBS_DATASTORE).ok(),
|
||||
auth_id: std::env::var(ENV_VAR_PBS_AUTH_ID)
|
||||
.ok()
|
||||
.and_then(|s| s.parse::<Authid>().ok()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve a [`BackupRepository`] from explicit CLI arguments with environment variable fallback.
|
||||
///
|
||||
/// Resolution:
|
||||
/// - `--repository` and CLI atoms are mutually exclusive.
|
||||
/// - `--repository` alone is used as-is (env vars ignored).
|
||||
/// - CLI atoms are merged with `PBS_*` env atom vars per-field (CLI wins).
|
||||
/// - If no CLI args are given, falls back to `PBS_REPOSITORY`, then to
|
||||
/// `PBS_*` atom env vars, then errors.
|
||||
fn resolve_repository(cli: BackupRepositoryArgs) -> Result<BackupRepository, Error> {
|
||||
cli.check_mutual_exclusion()?;
|
||||
|
||||
if cli.repository.is_some() {
|
||||
return BackupRepository::try_from(cli);
|
||||
}
|
||||
if cli.has_atoms() {
|
||||
let env = args_from_env();
|
||||
return BackupRepository::try_from(cli.merge_from(env));
|
||||
}
|
||||
|
||||
// No CLI args at all, try environment.
|
||||
if let Some(url) = get_default_repository() {
|
||||
return url.parse();
|
||||
}
|
||||
let env = args_from_env();
|
||||
if env.has_atoms() {
|
||||
return BackupRepository::try_from(env);
|
||||
}
|
||||
bail!("unable to get (default) repository");
|
||||
}
|
||||
|
||||
/// Remove repository-related keys from a JSON Value and return the parsed [`BackupRepository`].
|
||||
///
|
||||
/// This is used by commands that forward the remaining parameters to the server API after stripping
|
||||
/// the repository fields.
|
||||
pub fn remove_repository_from_value(param: &mut Value) -> Result<BackupRepository, Error> {
|
||||
let map = param
|
||||
if let Some(url) = param
|
||||
.as_object_mut()
|
||||
.ok_or_else(|| format_err!("unable to get repository (parameter is not an object)"))?;
|
||||
.ok_or_else(|| format_err!("unable to get repository (parameter is not an object)"))?
|
||||
.remove("repository")
|
||||
{
|
||||
return url
|
||||
.as_str()
|
||||
.ok_or_else(|| format_err!("invalid repository value (must be a string)"))?
|
||||
.parse();
|
||||
}
|
||||
|
||||
let to_string = |v: Value| v.as_str().map(String::from);
|
||||
|
||||
let args = BackupRepositoryArgs {
|
||||
repository: map.remove("repository").and_then(to_string),
|
||||
server: map.remove("server").and_then(to_string),
|
||||
port: map
|
||||
.remove("port")
|
||||
.and_then(|v| v.as_u64())
|
||||
.map(|p| p as u16),
|
||||
datastore: map.remove("datastore").and_then(to_string),
|
||||
auth_id: map
|
||||
.remove("auth-id")
|
||||
.and_then(to_string)
|
||||
.map(|s| s.parse::<Authid>())
|
||||
.transpose()?,
|
||||
};
|
||||
|
||||
resolve_repository(args)
|
||||
get_default_repository()
|
||||
.ok_or_else(|| format_err!("unable to get default repository"))?
|
||||
.parse()
|
||||
}
|
||||
|
||||
/// Extract a [`BackupRepository`] from CLI parameters.
|
||||
pub fn extract_repository_from_value(param: &Value) -> Result<BackupRepository, Error> {
|
||||
resolve_repository(args_from_value(param))
|
||||
let repo_url = param["repository"]
|
||||
.as_str()
|
||||
.map(String::from)
|
||||
.or_else(get_default_repository)
|
||||
.ok_or_else(|| format_err!("unable to get (default) repository"))?;
|
||||
|
||||
let repo: BackupRepository = repo_url.parse()?;
|
||||
|
||||
Ok(repo)
|
||||
}
|
||||
|
||||
/// Extract a [`BackupRepository`] from a parameter map (used for shell completion callbacks).
|
||||
pub fn extract_repository_from_map(param: &HashMap<String, String>) -> Option<BackupRepository> {
|
||||
let cli = BackupRepositoryArgs {
|
||||
repository: param.get("repository").cloned(),
|
||||
server: param.get("server").cloned(),
|
||||
port: param.get("port").and_then(|p| p.parse().ok()),
|
||||
datastore: param.get("datastore").cloned(),
|
||||
auth_id: param.get("auth-id").and_then(|s| s.parse().ok()),
|
||||
};
|
||||
|
||||
resolve_repository(cli).ok()
|
||||
}
|
||||
|
||||
/// Extract a [`BackupNamespace`] from CLI parameters, falling back to PBS_NAMESPACE.
|
||||
pub fn optional_ns_param(param: &Value) -> Result<BackupNamespace, Error> {
|
||||
match param.get("ns") {
|
||||
Some(Value::String(ns)) => return ns.parse(),
|
||||
Some(_) => bail!("invalid namespace parameter"),
|
||||
None => {}
|
||||
}
|
||||
if let Ok(ns) = std::env::var(ENV_VAR_PBS_NAMESPACE) {
|
||||
return ns.parse();
|
||||
}
|
||||
Ok(BackupNamespace::root())
|
||||
param
|
||||
.get("repository")
|
||||
.map(String::from)
|
||||
.or_else(get_default_repository)
|
||||
.and_then(|repo_url| repo_url.parse::<BackupRepository>().ok())
|
||||
}
|
||||
|
||||
pub fn connect(repo: &BackupRepository) -> Result<HttpClient, Error> {
|
||||
@ -428,7 +345,7 @@ pub async fn complete_backup_group_do(param: &HashMap<String, String>) -> Vec<St
|
||||
if let (Some(backup_id), Some(backup_type)) =
|
||||
(item["backup-id"].as_str(), item["backup-type"].as_str())
|
||||
{
|
||||
result.push(format!("{backup_type}/{backup_id}"));
|
||||
result.push(format!("{}/{}", backup_type, backup_id));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -449,7 +366,7 @@ pub async fn complete_group_or_snapshot_do(
|
||||
let mut result = vec![];
|
||||
for group in groups {
|
||||
result.push(group.to_string());
|
||||
result.push(format!("{group}/"));
|
||||
result.push(format!("{}/", group));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@ -730,7 +647,7 @@ pub fn find_xdg_file(
|
||||
let file_name = file_name.as_ref();
|
||||
base_directories()
|
||||
.map(|base| base.find_config_file(file_name))
|
||||
.with_context(|| format!("error searching for {description}"))
|
||||
.with_context(|| format!("error searching for {}", description))
|
||||
}
|
||||
|
||||
pub fn place_xdg_file(
|
||||
@ -740,7 +657,7 @@ pub fn place_xdg_file(
|
||||
let file_name = file_name.as_ref();
|
||||
base_directories()
|
||||
.and_then(|base| base.place_config_file(file_name).map_err(Error::from))
|
||||
.with_context(|| format!("failed to place {description} in xdg home"))
|
||||
.with_context(|| format!("failed to place {} in xdg home", description))
|
||||
}
|
||||
|
||||
pub fn get_pxar_archive_names(
|
||||
@ -840,141 +757,3 @@ pub fn create_tmp_file() -> std::io::Result<std::fs::File> {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
static ENV_MUTEX: std::sync::Mutex<()> = std::sync::Mutex::new(());
|
||||
|
||||
const REPO_ENV_VARS: &[&str] = &[
|
||||
ENV_VAR_PBS_REPOSITORY,
|
||||
ENV_VAR_PBS_SERVER,
|
||||
ENV_VAR_PBS_PORT,
|
||||
ENV_VAR_PBS_DATASTORE,
|
||||
ENV_VAR_PBS_AUTH_ID,
|
||||
ENV_VAR_PBS_NAMESPACE,
|
||||
ENV_VAR_CREDENTIALS_DIRECTORY,
|
||||
];
|
||||
|
||||
fn with_cleared_repo_env(f: impl FnOnce()) {
|
||||
let _guard = ENV_MUTEX.lock().unwrap();
|
||||
for k in REPO_ENV_VARS {
|
||||
std::env::remove_var(k);
|
||||
}
|
||||
f();
|
||||
for k in REPO_ENV_VARS {
|
||||
std::env::remove_var(k);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_repo_from_atoms() {
|
||||
with_cleared_repo_env(|| {
|
||||
let param = json!({"server": "myhost", "datastore": "mystore"});
|
||||
let repo = extract_repository_from_value(¶m).unwrap();
|
||||
assert_eq!(repo.host(), "myhost");
|
||||
assert_eq!(repo.store(), "mystore");
|
||||
assert_eq!(repo.port(), 8007);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_repo_from_url() {
|
||||
with_cleared_repo_env(|| {
|
||||
let param = json!({"repository": "myhost:mystore"});
|
||||
let repo = extract_repository_from_value(¶m).unwrap();
|
||||
assert_eq!(repo.host(), "myhost");
|
||||
assert_eq!(repo.store(), "mystore");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_repo_mutual_exclusion_error() {
|
||||
with_cleared_repo_env(|| {
|
||||
let param = json!({"repository": "myhost:mystore", "auth-id": "user@pam"});
|
||||
let err = extract_repository_from_value(¶m).unwrap_err();
|
||||
assert!(err.to_string().contains("mutually exclusive"), "got: {err}");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_repo_atoms_without_datastore_error() {
|
||||
with_cleared_repo_env(|| {
|
||||
let param = json!({"server": "myhost"});
|
||||
let err = extract_repository_from_value(¶m).unwrap_err();
|
||||
assert!(
|
||||
err.to_string().contains("--datastore is required"),
|
||||
"got: {err}"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_repo_nothing_provided_error() {
|
||||
with_cleared_repo_env(|| {
|
||||
let err = extract_repository_from_value(&json!({})).unwrap_err();
|
||||
assert!(err.to_string().contains("unable to get"), "got: {err}");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_repo_env_fallback() {
|
||||
with_cleared_repo_env(|| {
|
||||
std::env::set_var(ENV_VAR_PBS_SERVER, "envhost");
|
||||
std::env::set_var(ENV_VAR_PBS_DATASTORE, "envstore");
|
||||
let repo = extract_repository_from_value(&json!({})).unwrap();
|
||||
assert_eq!(repo.host(), "envhost");
|
||||
assert_eq!(repo.store(), "envstore");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_repo_pbs_repository_env_takes_precedence() {
|
||||
with_cleared_repo_env(|| {
|
||||
std::env::set_var(ENV_VAR_PBS_REPOSITORY, "repohost:repostore");
|
||||
std::env::set_var(ENV_VAR_PBS_SERVER, "envhost");
|
||||
std::env::set_var(ENV_VAR_PBS_DATASTORE, "envstore");
|
||||
let repo = extract_repository_from_value(&json!({})).unwrap();
|
||||
assert_eq!(repo.host(), "repohost");
|
||||
assert_eq!(repo.store(), "repostore");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_repo_cli_overrides_env() {
|
||||
with_cleared_repo_env(|| {
|
||||
std::env::set_var(ENV_VAR_PBS_REPOSITORY, "envhost:envstore");
|
||||
let param = json!({"server": "clihost", "datastore": "clistore"});
|
||||
let repo = extract_repository_from_value(¶m).unwrap();
|
||||
assert_eq!(repo.host(), "clihost");
|
||||
assert_eq!(repo.store(), "clistore");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_repo_cli_atoms_merge_with_env_atoms() {
|
||||
with_cleared_repo_env(|| {
|
||||
std::env::set_var(ENV_VAR_PBS_SERVER, "envhost");
|
||||
std::env::set_var(ENV_VAR_PBS_DATASTORE, "envstore");
|
||||
let param = json!({"auth-id": "backup@pbs"});
|
||||
let repo = extract_repository_from_value(¶m).unwrap();
|
||||
assert_eq!(repo.host(), "envhost");
|
||||
assert_eq!(repo.store(), "envstore");
|
||||
assert_eq!(repo.auth_id().to_string(), "backup@pbs");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_repo_cli_atom_overrides_same_env_atom() {
|
||||
with_cleared_repo_env(|| {
|
||||
std::env::set_var(ENV_VAR_PBS_SERVER, "envhost");
|
||||
std::env::set_var(ENV_VAR_PBS_DATASTORE, "envstore");
|
||||
let param = json!({"server": "clihost"});
|
||||
let repo = extract_repository_from_value(¶m).unwrap();
|
||||
assert_eq!(repo.host(), "clihost");
|
||||
assert_eq!(repo.store(), "envstore");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,23 +1,19 @@
|
||||
use std::os::fd::{AsRawFd, IntoRawFd};
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
|
||||
use anyhow::{bail, format_err, Error};
|
||||
use futures::*;
|
||||
use http_body_util::{BodyDataStream, BodyExt};
|
||||
use hyper::body::Incoming;
|
||||
use hyper::client::connect::{Connected, Connection};
|
||||
use hyper::client::Client;
|
||||
use hyper::http::Uri;
|
||||
use hyper::http::{Request, Response};
|
||||
use hyper_util::client::legacy::connect::{Connected, Connection};
|
||||
use hyper_util::client::legacy::Client;
|
||||
use hyper_util::rt::{TokioExecutor, TokioIo};
|
||||
use hyper::{body::HttpBody, Body};
|
||||
use pin_project_lite::pin_project;
|
||||
use serde_json::Value;
|
||||
use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt, ReadBuf};
|
||||
use tokio::net::UnixStream;
|
||||
|
||||
use proxmox_http::uri::json_object_to_query;
|
||||
use proxmox_http::Body;
|
||||
use proxmox_router::HttpError;
|
||||
|
||||
pub const DEFAULT_VSOCK_PORT: u16 = 807;
|
||||
@ -34,9 +30,9 @@ pin_project! {
|
||||
}
|
||||
|
||||
impl tower_service::Service<Uri> for VsockConnector {
|
||||
type Response = TokioIo<UnixConnection>;
|
||||
type Response = UnixConnection;
|
||||
type Error = Error;
|
||||
type Future = Pin<Box<dyn Future<Output = Result<TokioIo<UnixConnection>, Error>> + Send>>;
|
||||
type Future = Pin<Box<dyn Future<Output = Result<UnixConnection, Error>> + Send>>;
|
||||
|
||||
fn poll_ready(&mut self, _cx: &mut task::Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
Poll::Ready(Ok(()))
|
||||
@ -77,17 +73,16 @@ impl tower_service::Service<Uri> for VsockConnector {
|
||||
)?;
|
||||
|
||||
let sock_addr = VsockAddr::new(cid, port as u32);
|
||||
connect(sock_fd.as_raw_fd(), &sock_addr)?;
|
||||
connect(sock_fd, &sock_addr)?;
|
||||
|
||||
// connect sync, but set nonblock after (tokio requires it)
|
||||
let std_stream =
|
||||
unsafe { std::os::unix::net::UnixStream::from_raw_fd(sock_fd.into_raw_fd()) };
|
||||
let std_stream = unsafe { std::os::unix::net::UnixStream::from_raw_fd(sock_fd) };
|
||||
std_stream.set_nonblocking(true)?;
|
||||
|
||||
let stream = tokio::net::UnixStream::from_std(std_stream)?;
|
||||
let connection = UnixConnection { stream };
|
||||
|
||||
Ok(TokioIo::new(connection))
|
||||
Ok(connection)
|
||||
})
|
||||
// unravel the thread JoinHandle to a usable future
|
||||
.map(|res| match res {
|
||||
@ -138,7 +133,7 @@ impl AsyncWrite for UnixConnection {
|
||||
|
||||
/// Slimmed down version of HttpClient for virtio-vsock connections (file restore daemon)
|
||||
pub struct VsockClient {
|
||||
client: Client<VsockConnector, Body>,
|
||||
client: Client<VsockConnector>,
|
||||
cid: i32,
|
||||
port: u16,
|
||||
auth: Option<String>,
|
||||
@ -147,7 +142,7 @@ pub struct VsockClient {
|
||||
impl VsockClient {
|
||||
pub fn new(cid: i32, port: u16, auth: Option<String>) -> Self {
|
||||
let conn = VsockConnector {};
|
||||
let client = Client::builder(TokioExecutor::new()).build::<_, Body>(conn);
|
||||
let client = Client::builder().build::<_, Body>(conn);
|
||||
Self {
|
||||
client,
|
||||
cid,
|
||||
@ -184,7 +179,7 @@ impl VsockClient {
|
||||
if !status.is_success() {
|
||||
Self::api_response(resp).await.map(|_| ())?
|
||||
} else {
|
||||
futures::TryStreamExt::map_err(BodyDataStream::new(resp.into_body()), Error::from)
|
||||
futures::TryStreamExt::map_err(resp.into_body(), Error::from)
|
||||
.try_fold(output, move |acc, chunk| async move {
|
||||
acc.write_all(&chunk).await?;
|
||||
Ok::<_, Error>(acc)
|
||||
@ -194,9 +189,9 @@ impl VsockClient {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn api_response(response: Response<Incoming>) -> Result<Value, Error> {
|
||||
async fn api_response(response: Response<Body>) -> Result<Value, Error> {
|
||||
let status = response.status();
|
||||
let data = response.into_body().collect().await?.to_bytes();
|
||||
let data = HttpBody::collect(response.into_body()).await?.to_bytes();
|
||||
|
||||
let text = String::from_utf8(data.to_vec()).unwrap();
|
||||
if status.is_success() {
|
||||
@ -242,7 +237,7 @@ impl VsockClient {
|
||||
if let Some(data) = data {
|
||||
if method == "POST" {
|
||||
let builder = make_builder("application/json", &url);
|
||||
let request = builder.body(data.to_string().into())?;
|
||||
let request = builder.body(Body::from(data.to_string()))?;
|
||||
return Ok(request);
|
||||
} else {
|
||||
let query = json_object_to_query(data)?;
|
||||
|
||||
@ -9,21 +9,17 @@ rust-version.workspace = true
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
const_format.workspace = true
|
||||
hex.workspace = true
|
||||
libc.workspace = true
|
||||
nix.workspace = true
|
||||
once_cell.workspace = true
|
||||
openssl.workspace = true
|
||||
parking_lot.workspace = true
|
||||
regex.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
|
||||
proxmox-http.workspace = true
|
||||
proxmox-lang.workspace = true
|
||||
proxmox-notify.workspace = true
|
||||
proxmox-network-api.workspace = true
|
||||
proxmox-router = { workspace = true, default-features = false }
|
||||
proxmox-s3-client.workspace = true
|
||||
proxmox-schema.workspace = true
|
||||
proxmox-section-config.workspace = true
|
||||
proxmox-shared-memory.workspace = true
|
||||
@ -33,4 +29,3 @@ proxmox-uuid.workspace = true
|
||||
|
||||
pbs-api-types.workspace = true
|
||||
pbs-buildcfg.workspace = true
|
||||
pbs-key-config.workspace = true
|
||||
|
||||
@ -127,12 +127,6 @@ pub fn check_acl_path(path: &str) -> Result<(), Error> {
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
"s3-endpoint" | "encryption-keys" => {
|
||||
// /system/<matched-component>/{id}
|
||||
if components_len <= 3 {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@ -449,7 +443,7 @@ impl AclTree {
|
||||
|
||||
for (auth_id, roles) in &node.users {
|
||||
// no need to save, because root is always 'Administrator'
|
||||
if !auth_id.is_token() && auth_id.user() == Userid::root_userid() {
|
||||
if !auth_id.is_token() && auth_id.user() == "root@pam" {
|
||||
continue;
|
||||
}
|
||||
for (role, propagate) in roles {
|
||||
@ -465,7 +459,7 @@ impl AclTree {
|
||||
|
||||
for (group, roles) in &node.groups {
|
||||
for (role, propagate) in roles {
|
||||
let group = format!("@{group}");
|
||||
let group = format!("@{}", group);
|
||||
if *propagate {
|
||||
role_ug_map1.entry(role).or_default().insert(group);
|
||||
} else {
|
||||
@ -533,7 +527,7 @@ impl AclTree {
|
||||
}
|
||||
|
||||
for (name, child) in node.children.iter() {
|
||||
let child_path = format!("{path}/{name}");
|
||||
let child_path = format!("{}/{}", path, name);
|
||||
Self::write_node_config(child, &child_path, w)?;
|
||||
}
|
||||
|
||||
@ -789,7 +783,8 @@ mod test {
|
||||
|
||||
assert_eq!(
|
||||
roles, expected_roles,
|
||||
"\nat check_roles for '{auth_id}' on '{path}'"
|
||||
"\nat check_roles for '{}' on '{}'",
|
||||
auth_id, path
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -137,7 +137,7 @@ impl CachedUserInfo {
|
||||
}
|
||||
|
||||
pub fn is_superuser(&self, auth_id: &Authid) -> bool {
|
||||
!auth_id.is_token() && auth_id.user() == Userid::root_userid()
|
||||
!auth_id.is_token() && auth_id.user() == "root@pam"
|
||||
}
|
||||
|
||||
pub fn is_group_member(&self, _userid: &Userid, _group: &str) -> bool {
|
||||
@ -208,7 +208,7 @@ impl CachedUserInfo {
|
||||
|
||||
impl UserInformation for CachedUserInfo {
|
||||
fn is_superuser(&self, userid: &str) -> bool {
|
||||
userid == Userid::root_userid().as_str()
|
||||
userid == "root@pam"
|
||||
}
|
||||
|
||||
fn is_group_member(&self, _userid: &str, _group: &str) -> bool {
|
||||
|
||||
@ -26,9 +26,8 @@ struct ConfigVersionCacheDataInner {
|
||||
// Traffic control (traffic-control.cfg) generation/version.
|
||||
traffic_control_generation: AtomicUsize,
|
||||
// datastore (datastore.cfg) generation/version
|
||||
// FIXME: remove with PBS 3.0
|
||||
datastore_generation: AtomicUsize,
|
||||
// Token shadow (token.shadow) generation/version.
|
||||
token_shadow_generation: AtomicUsize,
|
||||
// Add further atomics here
|
||||
}
|
||||
|
||||
@ -146,35 +145,12 @@ impl ConfigVersionCache {
|
||||
.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.
|
||||
// FIXME: remove with PBS 3.0 or make actually useful again in datastore lookup
|
||||
pub fn increase_datastore_generation(&self) -> usize {
|
||||
self.shmem
|
||||
.data()
|
||||
.datastore_generation
|
||||
.fetch_add(1, Ordering::AcqRel)
|
||||
}
|
||||
|
||||
/// Returns the token shadow generation number.
|
||||
pub fn token_shadow_generation(&self) -> usize {
|
||||
self.shmem
|
||||
.data()
|
||||
.token_shadow_generation
|
||||
.load(Ordering::Acquire)
|
||||
}
|
||||
|
||||
/// Increase the token shadow generation number.
|
||||
pub fn increase_token_shadow_generation(&self) -> usize {
|
||||
self.shmem
|
||||
.data()
|
||||
.token_shadow_generation
|
||||
.fetch_add(1, Ordering::AcqRel)
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,7 +6,7 @@ use anyhow::Error;
|
||||
use proxmox_schema::{AllOfSchema, ApiType};
|
||||
use proxmox_section_config::{SectionConfig, SectionConfigData, SectionConfigPlugin};
|
||||
|
||||
use pbs_api_types::{DataStoreConfig, DatastoreBackendConfig, DATASTORE_SCHEMA};
|
||||
use pbs_api_types::{DataStoreConfig, DATASTORE_SCHEMA};
|
||||
|
||||
use crate::{open_backup_lockfile, replace_backup_config, BackupLockGuard, ConfigVersionCache};
|
||||
|
||||
@ -85,7 +85,7 @@ pub fn complete_acl_path(_arg: &str, _param: &HashMap<String, String>) -> Vec<St
|
||||
|
||||
if let Ok((data, _digest)) = config() {
|
||||
for id in data.sections.keys() {
|
||||
list.push(format!("/datastore/{id}"));
|
||||
list.push(format!("/datastore/{}", id));
|
||||
}
|
||||
}
|
||||
|
||||
@ -113,16 +113,3 @@ pub fn complete_calendar_event(_arg: &str, _param: &HashMap<String, String>) ->
|
||||
.map(|s| String::from(*s))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Parse the backend configuration from a datastore config.
|
||||
pub fn parse_backend_config(config: &DataStoreConfig) -> Result<DatastoreBackendConfig, Error> {
|
||||
config.backend.as_deref().unwrap_or("").parse()
|
||||
}
|
||||
|
||||
/// Returns the datastore backend type from its name.
|
||||
pub fn datastore_backend_type(store: &str) -> Result<pbs_api_types::DatastoreBackendType, Error> {
|
||||
let (config, _) = self::config()?;
|
||||
let store_config: DataStoreConfig = config.lookup("datastore", store)?;
|
||||
|
||||
Ok(parse_backend_config(&store_config)?.ty.unwrap_or_default())
|
||||
}
|
||||
|
||||
@ -1,201 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use anyhow::{bail, format_err, Error};
|
||||
use nix::{sys::stat::Mode, unistd::Uid};
|
||||
use serde::Deserialize;
|
||||
|
||||
use pbs_api_types::{CryptKey, KeyInfo, CRYPT_KEY_ID_SCHEMA};
|
||||
use proxmox_schema::ApiType;
|
||||
use proxmox_section_config::{SectionConfig, SectionConfigData, SectionConfigPlugin};
|
||||
use proxmox_sys::fs::CreateOptions;
|
||||
|
||||
use pbs_buildcfg::configdir;
|
||||
use pbs_key_config::KeyConfig;
|
||||
|
||||
use crate::{open_backup_lockfile, replace_backup_config, BackupLockGuard};
|
||||
|
||||
pub static CONFIG: LazyLock<SectionConfig> = LazyLock::new(init);
|
||||
|
||||
fn init() -> SectionConfig {
|
||||
let obj_schema = CryptKey::API_SCHEMA.unwrap_all_of_schema();
|
||||
let plugin = SectionConfigPlugin::new(
|
||||
ENCRYPTION_KEYS_CFG_TYPE_ID.to_string(),
|
||||
Some(String::from("id")),
|
||||
obj_schema,
|
||||
);
|
||||
let mut config = SectionConfig::new(&CRYPT_KEY_ID_SCHEMA);
|
||||
config.register_plugin(plugin);
|
||||
|
||||
config
|
||||
}
|
||||
|
||||
/// Configuration file location for encryption keys.
|
||||
pub const ENCRYPTION_KEYS_CFG_FILENAME: &str = configdir!("/encryption-keys.cfg");
|
||||
/// Configuration lock file used to prevent concurrent configuration update operations.
|
||||
pub const ENCRYPTION_KEYS_CFG_LOCKFILE: &str = configdir!("/.encryption-keys.lck");
|
||||
/// Directory where to store the actual encryption keys
|
||||
pub const ENCRYPTION_KEYS_DIR: &str = configdir!("/encryption-keys/");
|
||||
|
||||
/// Config type for encryption key config entries
|
||||
pub const ENCRYPTION_KEYS_CFG_TYPE_ID: &str = "sync-key";
|
||||
|
||||
/// Get exclusive lock for encryption key configuration update.
|
||||
pub fn lock_config() -> Result<BackupLockGuard, Error> {
|
||||
open_backup_lockfile(ENCRYPTION_KEYS_CFG_LOCKFILE, None, true)
|
||||
}
|
||||
|
||||
/// Load encryption key configuration from file.
|
||||
pub fn config() -> Result<(SectionConfigData, [u8; 32]), Error> {
|
||||
let content = proxmox_sys::fs::file_read_optional_string(ENCRYPTION_KEYS_CFG_FILENAME)?;
|
||||
let content = content.unwrap_or_default();
|
||||
let digest = openssl::sha::sha256(content.as_bytes());
|
||||
let data = CONFIG.parse(ENCRYPTION_KEYS_CFG_FILENAME, &content)?;
|
||||
Ok((data, digest))
|
||||
}
|
||||
|
||||
/// Save given key configuration to file.
|
||||
pub fn save_config(config: &SectionConfigData) -> Result<(), Error> {
|
||||
let raw = CONFIG.write(ENCRYPTION_KEYS_CFG_FILENAME, config)?;
|
||||
replace_backup_config(ENCRYPTION_KEYS_CFG_FILENAME, raw.as_bytes())
|
||||
}
|
||||
|
||||
/// Shell completion helper to complete encryption key id's as found in the config.
|
||||
pub fn complete_encryption_key_id(_arg: &str, _param: &HashMap<String, String>) -> Vec<String> {
|
||||
match config() {
|
||||
Ok((data, _digest)) => data.sections.keys().map(|id| id.to_string()).collect(),
|
||||
Err(_) => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Load the encryption key from file.
|
||||
///
|
||||
/// Looks up the key in the config and tries to load it from the given file.
|
||||
/// Upon loading, the config key fingerprint is compared to the one stored in the key
|
||||
/// file. Fail to load archived keys if flag is set.
|
||||
pub fn load_key_config(id: &str, fail_on_archived: bool) -> Result<KeyConfig, Error> {
|
||||
let _lock = lock_config()?;
|
||||
let (config, _digest) = config()?;
|
||||
|
||||
let key: CryptKey = config.lookup(ENCRYPTION_KEYS_CFG_TYPE_ID, id)?;
|
||||
if fail_on_archived && key.archived_at.is_some() {
|
||||
bail!("cannot load archived encryption key {id}");
|
||||
}
|
||||
let key_config = match &key.info.path {
|
||||
Some(path) => KeyConfig::load(path)?,
|
||||
None => bail!("missing path for encryption key {id}"),
|
||||
};
|
||||
|
||||
let stored_key_info = KeyInfo::from(&key_config);
|
||||
|
||||
if key.info.fingerprint != stored_key_info.fingerprint {
|
||||
bail!("loaded key does not match the config for key {id}");
|
||||
}
|
||||
|
||||
Ok(key_config)
|
||||
}
|
||||
|
||||
/// Store the encryption key to file.
|
||||
///
|
||||
/// Inserts the key in the config and stores it to the given file.
|
||||
pub fn store_key(id: &str, key: &KeyConfig) -> Result<(), Error> {
|
||||
let _lock = lock_config()?;
|
||||
let (mut config, _digest) = config()?;
|
||||
|
||||
if config.sections.contains_key(id) {
|
||||
bail!("key with id '{id}' already exists.");
|
||||
}
|
||||
|
||||
let backup_user = crate::backup_user()?;
|
||||
let dir_options = CreateOptions::new()
|
||||
.perm(Mode::from_bits_truncate(0o0750))
|
||||
.owner(Uid::from_raw(0))
|
||||
.group(backup_user.gid);
|
||||
|
||||
proxmox_sys::fs::ensure_dir_exists(ENCRYPTION_KEYS_DIR, &dir_options, true)?;
|
||||
|
||||
let key_path = format!("{ENCRYPTION_KEYS_DIR}{id}.enc");
|
||||
let key_lock_path = format!("{key_path}.lck");
|
||||
|
||||
// lock to avoid race with key deletion
|
||||
let _lock = open_backup_lockfile(&key_lock_path, None, true)?;
|
||||
|
||||
// assert the key file is empty or does not exist
|
||||
match std::fs::metadata(&key_path) {
|
||||
Ok(metadata) => {
|
||||
if metadata.len() > 0 {
|
||||
bail!("detected pre-existing key file, refusing to overwrite.");
|
||||
}
|
||||
}
|
||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => (),
|
||||
Err(err) => return Err(err.into()),
|
||||
}
|
||||
|
||||
let keyfile_mode = nix::sys::stat::Mode::from_bits_truncate(0o0640);
|
||||
|
||||
key.store_with(
|
||||
&key_path,
|
||||
true,
|
||||
Some(keyfile_mode),
|
||||
Some(Uid::from_raw(0)),
|
||||
Some(backup_user.gid),
|
||||
)?;
|
||||
|
||||
let mut info = KeyInfo::from(key);
|
||||
info.path = Some(key_path.clone());
|
||||
|
||||
let crypt_key = CryptKey {
|
||||
id: id.to_string(),
|
||||
info,
|
||||
archived_at: None,
|
||||
};
|
||||
|
||||
let result = proxmox_lang::try_block!({
|
||||
config.set_data(id, ENCRYPTION_KEYS_CFG_TYPE_ID, crypt_key)?;
|
||||
save_config(&config)
|
||||
});
|
||||
|
||||
if result.is_err() {
|
||||
let _ = std::fs::remove_file(key_path);
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Delete the encryption key from config.
|
||||
///
|
||||
/// Returns true if the key was removed successfully, false if there was no matching key.
|
||||
/// Safety: caller must acquire and hold config lock.
|
||||
pub fn delete_key(id: &str, mut config: SectionConfigData) -> Result<bool, Error> {
|
||||
if let Some((_, key)) = config.sections.remove(id) {
|
||||
let key =
|
||||
CryptKey::deserialize(key).map_err(|_err| format_err!("failed to parse key config"))?;
|
||||
|
||||
if key.archived_at.is_none() {
|
||||
bail!("key still active, deleting is only possible for archived keys");
|
||||
}
|
||||
|
||||
if let Some(key_path) = &key.info.path {
|
||||
let key_lock_path = format!("{key_path}.lck");
|
||||
// Avoid races with key insertion
|
||||
let _lock = open_backup_lockfile(key_lock_path, None, true)?;
|
||||
|
||||
let key_config = KeyConfig::load(key_path)?;
|
||||
let stored_key_info = KeyInfo::from(&key_config);
|
||||
// Check the key is the expected one
|
||||
if key.info.fingerprint != stored_key_info.fingerprint {
|
||||
bail!("unexpected key detected in key file, refuse to delete");
|
||||
}
|
||||
|
||||
let raw = CONFIG.write(ENCRYPTION_KEYS_CFG_FILENAME, &config)?;
|
||||
// drops config lock
|
||||
replace_backup_config(ENCRYPTION_KEYS_CFG_FILENAME, raw.as_bytes())?;
|
||||
|
||||
std::fs::remove_file(key_path)?;
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
bail!("missing key file path for key '{id}'");
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
@ -4,15 +4,12 @@ pub use cached_user_info::CachedUserInfo;
|
||||
pub mod datastore;
|
||||
pub mod domains;
|
||||
pub mod drive;
|
||||
pub mod encryption_keys;
|
||||
pub mod key_value;
|
||||
pub mod media_pool;
|
||||
pub mod metrics;
|
||||
pub mod node;
|
||||
pub mod network;
|
||||
pub mod notifications;
|
||||
pub mod prune;
|
||||
pub mod remote;
|
||||
pub mod s3;
|
||||
pub mod sync;
|
||||
pub mod tape_job;
|
||||
pub mod token_shadow;
|
||||
@ -23,8 +20,7 @@ pub mod verify;
|
||||
mod config_version_cache;
|
||||
pub use config_version_cache::ConfigVersionCache;
|
||||
|
||||
use anyhow::{bail, format_err, Error};
|
||||
use hex::FromHex;
|
||||
use anyhow::{format_err, Error};
|
||||
use nix::unistd::{Gid, Group, Uid, User};
|
||||
use proxmox_sys::fs::DirLockGuard;
|
||||
use std::os::unix::prelude::AsRawFd;
|
||||
@ -51,16 +47,6 @@ pub fn backup_group() -> Result<nix::unistd::Group, Error> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Return User info for root
|
||||
pub fn priv_user() -> Result<nix::unistd::User, Error> {
|
||||
if cfg!(test) {
|
||||
Ok(User::from_uid(Uid::current())?.expect("current user does not exist"))
|
||||
} else {
|
||||
User::from_name("root")?.ok_or_else(|| format_err!("Unable to lookup superuser."))
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use = "lock guard must be used to keep file locked"]
|
||||
pub struct BackupLockGuard {
|
||||
file: Option<std::fs::File>,
|
||||
// TODO: Remove `_legacy_dir` with PBS 5
|
||||
@ -153,19 +139,3 @@ pub fn replace_secret_config<P: AsRef<std::path::Path>>(path: P, data: &[u8]) ->
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Detect modified configuration files
|
||||
///
|
||||
/// This function fails with a reasonable error message if checksums do not match.
|
||||
pub fn detect_modified_configuration_file<T: AsRef<str>>(
|
||||
digest_str: Option<T>,
|
||||
expected_digest: &[u8; 32],
|
||||
) -> Result<(), Error> {
|
||||
if let Some(digest_str) = digest_str {
|
||||
let digest = <[u8; 32]>::from_hex(digest_str.as_ref())?;
|
||||
if &digest != expected_digest {
|
||||
bail!("detected modified configuration - file changed by other user? Try again.");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
227
pbs-config/src/network/helper.rs
Normal file
227
pbs-config/src/network/helper.rs
Normal file
@ -0,0 +1,227 @@
|
||||
use std::collections::HashMap;
|
||||
use std::os::unix::io::{AsRawFd, FromRawFd, OwnedFd};
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use anyhow::{bail, format_err, Error};
|
||||
use const_format::concatcp;
|
||||
use nix::ioctl_read_bad;
|
||||
use nix::sys::socket::{socket, AddressFamily, SockFlag, SockType};
|
||||
use regex::Regex;
|
||||
|
||||
use pbs_api_types::*; // for IP macros
|
||||
|
||||
pub static IPV4_REVERSE_MASK: &[&str] = &[
|
||||
"0.0.0.0",
|
||||
"128.0.0.0",
|
||||
"192.0.0.0",
|
||||
"224.0.0.0",
|
||||
"240.0.0.0",
|
||||
"248.0.0.0",
|
||||
"252.0.0.0",
|
||||
"254.0.0.0",
|
||||
"255.0.0.0",
|
||||
"255.128.0.0",
|
||||
"255.192.0.0",
|
||||
"255.224.0.0",
|
||||
"255.240.0.0",
|
||||
"255.248.0.0",
|
||||
"255.252.0.0",
|
||||
"255.254.0.0",
|
||||
"255.255.0.0",
|
||||
"255.255.128.0",
|
||||
"255.255.192.0",
|
||||
"255.255.224.0",
|
||||
"255.255.240.0",
|
||||
"255.255.248.0",
|
||||
"255.255.252.0",
|
||||
"255.255.254.0",
|
||||
"255.255.255.0",
|
||||
"255.255.255.128",
|
||||
"255.255.255.192",
|
||||
"255.255.255.224",
|
||||
"255.255.255.240",
|
||||
"255.255.255.248",
|
||||
"255.255.255.252",
|
||||
"255.255.255.254",
|
||||
"255.255.255.255",
|
||||
];
|
||||
|
||||
pub static IPV4_MASK_HASH_LOCALNET: LazyLock<HashMap<&'static str, u8>> = LazyLock::new(|| {
|
||||
let mut map = HashMap::new();
|
||||
#[allow(clippy::needless_range_loop)]
|
||||
for i in 0..IPV4_REVERSE_MASK.len() {
|
||||
map.insert(IPV4_REVERSE_MASK[i], i as u8);
|
||||
}
|
||||
map
|
||||
});
|
||||
|
||||
pub fn parse_cidr(cidr: &str) -> Result<(String, u8, bool), Error> {
|
||||
let (address, mask, is_v6) = parse_address_or_cidr(cidr)?;
|
||||
if let Some(mask) = mask {
|
||||
Ok((address, mask, is_v6))
|
||||
} else {
|
||||
bail!("missing netmask in '{}'", cidr);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn check_netmask(mask: u8, is_v6: bool) -> Result<(), Error> {
|
||||
let (ver, min, max) = if is_v6 {
|
||||
("IPv6", 1, 128)
|
||||
} else {
|
||||
("IPv4", 1, 32)
|
||||
};
|
||||
|
||||
if !(mask >= min && mask <= max) {
|
||||
bail!(
|
||||
"{} mask '{}' is out of range ({}..{}).",
|
||||
ver,
|
||||
mask,
|
||||
min,
|
||||
max
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// parse ip address with optional cidr mask
|
||||
pub fn parse_address_or_cidr(cidr: &str) -> Result<(String, Option<u8>, bool), Error> {
|
||||
// NOTE: This is NOT the same regex as in proxmox-schema as this one has capture groups for
|
||||
// the addresses vs cidr portions!
|
||||
pub static CIDR_V4_REGEX: LazyLock<Regex> =
|
||||
LazyLock::new(|| Regex::new(concatcp!(r"^(", IPV4RE_STR, r")(?:/(\d{1,2}))?$")).unwrap());
|
||||
pub static CIDR_V6_REGEX: LazyLock<Regex> =
|
||||
LazyLock::new(|| Regex::new(concatcp!(r"^(", IPV6RE_STR, r")(?:/(\d{1,3}))?$")).unwrap());
|
||||
|
||||
if let Some(caps) = CIDR_V4_REGEX.captures(cidr) {
|
||||
let address = &caps[1];
|
||||
if let Some(mask) = caps.get(2) {
|
||||
let mask: u8 = mask.as_str().parse()?;
|
||||
check_netmask(mask, false)?;
|
||||
Ok((address.to_string(), Some(mask), false))
|
||||
} else {
|
||||
Ok((address.to_string(), None, false))
|
||||
}
|
||||
} else if let Some(caps) = CIDR_V6_REGEX.captures(cidr) {
|
||||
let address = &caps[1];
|
||||
if let Some(mask) = caps.get(2) {
|
||||
let mask: u8 = mask.as_str().parse()?;
|
||||
check_netmask(mask, true)?;
|
||||
Ok((address.to_string(), Some(mask), true))
|
||||
} else {
|
||||
Ok((address.to_string(), None, true))
|
||||
}
|
||||
} else {
|
||||
bail!("invalid address/mask '{}'", cidr);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_network_interfaces() -> Result<HashMap<String, bool>, Error> {
|
||||
const PROC_NET_DEV: &str = "/proc/net/dev";
|
||||
|
||||
#[repr(C)]
|
||||
pub struct ifreq {
|
||||
ifr_name: [libc::c_uchar; libc::IFNAMSIZ],
|
||||
ifru_flags: libc::c_short,
|
||||
}
|
||||
|
||||
ioctl_read_bad!(get_interface_flags, libc::SIOCGIFFLAGS, ifreq);
|
||||
|
||||
static IFACE_LINE_REGEX: LazyLock<Regex> =
|
||||
LazyLock::new(|| Regex::new(r"^\s*([^:\s]+):").unwrap());
|
||||
|
||||
let raw = std::fs::read_to_string(PROC_NET_DEV)
|
||||
.map_err(|err| format_err!("unable to read {} - {}", PROC_NET_DEV, err))?;
|
||||
|
||||
let lines = raw.lines();
|
||||
|
||||
let sock = unsafe {
|
||||
OwnedFd::from_raw_fd(
|
||||
socket(
|
||||
AddressFamily::Inet,
|
||||
SockType::Datagram,
|
||||
SockFlag::empty(),
|
||||
None,
|
||||
)
|
||||
.or_else(|_| {
|
||||
socket(
|
||||
AddressFamily::Inet6,
|
||||
SockType::Datagram,
|
||||
SockFlag::empty(),
|
||||
None,
|
||||
)
|
||||
})?,
|
||||
)
|
||||
};
|
||||
|
||||
let mut interface_list = HashMap::new();
|
||||
|
||||
for line in lines {
|
||||
if let Some(cap) = IFACE_LINE_REGEX.captures(line) {
|
||||
let ifname = &cap[1];
|
||||
|
||||
let mut req = ifreq {
|
||||
ifr_name: *b"0000000000000000",
|
||||
ifru_flags: 0,
|
||||
};
|
||||
for (i, b) in std::ffi::CString::new(ifname)?
|
||||
.as_bytes_with_nul()
|
||||
.iter()
|
||||
.enumerate()
|
||||
{
|
||||
if i < (libc::IFNAMSIZ - 1) {
|
||||
req.ifr_name[i] = *b as libc::c_uchar;
|
||||
}
|
||||
}
|
||||
let res = unsafe { get_interface_flags(sock.as_raw_fd(), &mut req)? };
|
||||
if res != 0 {
|
||||
bail!(
|
||||
"ioctl get_interface_flags for '{}' failed ({})",
|
||||
ifname,
|
||||
res
|
||||
);
|
||||
}
|
||||
let is_up = (req.ifru_flags & (libc::IFF_UP as libc::c_short)) != 0;
|
||||
interface_list.insert(ifname.to_string(), is_up);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(interface_list)
|
||||
}
|
||||
|
||||
pub fn compute_file_diff(filename: &str, shadow: &str) -> Result<String, Error> {
|
||||
let output = Command::new("diff")
|
||||
.arg("-b")
|
||||
.arg("-u")
|
||||
.arg(filename)
|
||||
.arg(shadow)
|
||||
.output()
|
||||
.map_err(|err| format_err!("failed to execute diff - {}", err))?;
|
||||
|
||||
let diff = proxmox_sys::command::command_output_as_string(output, Some(|c| c == 0 || c == 1))
|
||||
.map_err(|err| format_err!("diff failed: {}", err))?;
|
||||
|
||||
Ok(diff)
|
||||
}
|
||||
|
||||
pub fn assert_ifupdown2_installed() -> Result<(), Error> {
|
||||
if !Path::new("/usr/share/ifupdown2").exists() {
|
||||
bail!("ifupdown2 is not installed.");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn network_reload() -> Result<(), Error> {
|
||||
let output = Command::new("ifreload")
|
||||
.arg("-a")
|
||||
.output()
|
||||
.map_err(|err| format_err!("failed to execute 'ifreload' - {}", err))?;
|
||||
|
||||
proxmox_sys::command::command_output(output, None)
|
||||
.map_err(|err| format_err!("ifreload failed: {}", err))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
136
pbs-config/src/network/lexer.rs
Normal file
136
pbs-config/src/network/lexer.rs
Normal file
@ -0,0 +1,136 @@
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
use std::io::BufRead;
|
||||
use std::iter::Iterator;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
|
||||
pub enum Token {
|
||||
Text,
|
||||
Comment,
|
||||
DHCP,
|
||||
Newline,
|
||||
Address,
|
||||
Auto,
|
||||
Gateway,
|
||||
Inet,
|
||||
Inet6,
|
||||
Iface,
|
||||
Loopback,
|
||||
Manual,
|
||||
Netmask,
|
||||
Static,
|
||||
Attribute,
|
||||
MTU,
|
||||
BridgePorts,
|
||||
BridgeVlanAware,
|
||||
VlanId,
|
||||
VlanRawDevice,
|
||||
BondSlaves,
|
||||
BondMode,
|
||||
BondPrimary,
|
||||
BondXmitHashPolicy,
|
||||
EOF,
|
||||
}
|
||||
|
||||
static KEYWORDS: LazyLock<HashMap<&'static str, Token>> = LazyLock::new(|| {
|
||||
let mut map = HashMap::new();
|
||||
map.insert("address", Token::Address);
|
||||
map.insert("auto", Token::Auto);
|
||||
map.insert("dhcp", Token::DHCP);
|
||||
map.insert("gateway", Token::Gateway);
|
||||
map.insert("inet", Token::Inet);
|
||||
map.insert("inet6", Token::Inet6);
|
||||
map.insert("iface", Token::Iface);
|
||||
map.insert("loopback", Token::Loopback);
|
||||
map.insert("manual", Token::Manual);
|
||||
map.insert("netmask", Token::Netmask);
|
||||
map.insert("static", Token::Static);
|
||||
map.insert("mtu", Token::MTU);
|
||||
map.insert("bridge-ports", Token::BridgePorts);
|
||||
map.insert("bridge_ports", Token::BridgePorts);
|
||||
map.insert("bridge-vlan-aware", Token::BridgeVlanAware);
|
||||
map.insert("bridge_vlan_aware", Token::BridgeVlanAware);
|
||||
map.insert("vlan-id", Token::VlanId);
|
||||
map.insert("vlan_id", Token::VlanId);
|
||||
map.insert("vlan-raw-device", Token::VlanRawDevice);
|
||||
map.insert("vlan_raw_device", Token::VlanRawDevice);
|
||||
map.insert("bond-slaves", Token::BondSlaves);
|
||||
map.insert("bond_slaves", Token::BondSlaves);
|
||||
map.insert("bond-mode", Token::BondMode);
|
||||
map.insert("bond-primary", Token::BondPrimary);
|
||||
map.insert("bond_primary", Token::BondPrimary);
|
||||
map.insert("bond_xmit_hash_policy", Token::BondXmitHashPolicy);
|
||||
map.insert("bond-xmit-hash-policy", Token::BondXmitHashPolicy);
|
||||
map
|
||||
});
|
||||
|
||||
pub struct Lexer<R> {
|
||||
input: R,
|
||||
eof_count: usize,
|
||||
cur_line: Option<VecDeque<(Token, String)>>,
|
||||
}
|
||||
|
||||
impl<R: BufRead> Lexer<R> {
|
||||
pub fn new(input: R) -> Self {
|
||||
Self {
|
||||
input,
|
||||
eof_count: 0,
|
||||
cur_line: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn split_line(line: &str) -> VecDeque<(Token, String)> {
|
||||
if let Some(comment) = line.strip_prefix('#') {
|
||||
let mut res = VecDeque::new();
|
||||
res.push_back((Token::Comment, comment.trim().to_string()));
|
||||
return res;
|
||||
}
|
||||
let mut list: VecDeque<(Token, String)> = line
|
||||
.split_ascii_whitespace()
|
||||
.map(|text| {
|
||||
let token = KEYWORDS.get(text).unwrap_or(&Token::Text);
|
||||
(*token, text.to_string())
|
||||
})
|
||||
.collect();
|
||||
|
||||
if line.starts_with(|c: char| c.is_ascii_whitespace() && c != '\n') {
|
||||
list.push_front((Token::Attribute, String::from("\t")));
|
||||
}
|
||||
list
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: BufRead> Iterator for Lexer<R> {
|
||||
type Item = Result<(Token, String), std::io::Error>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.cur_line.is_none() {
|
||||
let mut line = String::new();
|
||||
match self.input.read_line(&mut line) {
|
||||
Err(err) => return Some(Err(err)),
|
||||
Ok(0) => {
|
||||
self.eof_count += 1;
|
||||
if self.eof_count == 1 {
|
||||
return Some(Ok((Token::EOF, String::new())));
|
||||
}
|
||||
return None;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
self.cur_line = Some(Self::split_line(&line));
|
||||
}
|
||||
|
||||
match self.cur_line {
|
||||
Some(ref mut cur_line) => {
|
||||
if cur_line.is_empty() {
|
||||
self.cur_line = None;
|
||||
Some(Ok((Token::Newline, String::from("\n"))))
|
||||
} else {
|
||||
let (token, text) = cur_line.pop_front().unwrap();
|
||||
Some(Ok((token, text)))
|
||||
}
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
687
pbs-config/src/network/mod.rs
Normal file
687
pbs-config/src/network/mod.rs
Normal file
@ -0,0 +1,687 @@
|
||||
use std::collections::{BTreeMap, HashMap, HashSet};
|
||||
use std::io::Write;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use anyhow::{bail, format_err, Error};
|
||||
use regex::Regex;
|
||||
use serde::de::{value, Deserialize, IntoDeserializer};
|
||||
|
||||
use proxmox_sys::{fs::replace_file, fs::CreateOptions};
|
||||
|
||||
mod helper;
|
||||
pub use helper::*;
|
||||
|
||||
mod lexer;
|
||||
pub use lexer::*;
|
||||
|
||||
mod parser;
|
||||
pub use parser::*;
|
||||
|
||||
use proxmox_network_api::{
|
||||
BondXmitHashPolicy, Interface, LinuxBondMode, NetworkConfigMethod, NetworkInterfaceType,
|
||||
};
|
||||
|
||||
use crate::{open_backup_lockfile, BackupLockGuard};
|
||||
|
||||
static PHYSICAL_NIC_REGEX: LazyLock<Regex> =
|
||||
LazyLock::new(|| Regex::new(r"^(?:eth\d+|en[^:.]+|ib\d+)$").unwrap());
|
||||
static VLAN_INTERFACE_REGEX: LazyLock<Regex> = LazyLock::new(|| {
|
||||
Regex::new(r"^(?P<vlan_raw_device>\S+)\.(?P<vlan_id>\d+)|vlan(?P<vlan_id2>\d+)$").unwrap()
|
||||
});
|
||||
|
||||
pub fn is_physical_nic(iface: &str) -> bool {
|
||||
PHYSICAL_NIC_REGEX.is_match(iface)
|
||||
}
|
||||
|
||||
pub fn bond_mode_from_str(s: &str) -> Result<LinuxBondMode, Error> {
|
||||
LinuxBondMode::deserialize(s.into_deserializer())
|
||||
.map_err(|_: value::Error| format_err!("invalid bond_mode '{}'", s))
|
||||
}
|
||||
|
||||
pub fn bond_xmit_hash_policy_from_str(s: &str) -> Result<BondXmitHashPolicy, Error> {
|
||||
BondXmitHashPolicy::deserialize(s.into_deserializer())
|
||||
.map_err(|_: value::Error| format_err!("invalid bond_xmit_hash_policy '{}'", s))
|
||||
}
|
||||
|
||||
pub fn parse_vlan_id_from_name(iface_name: &str) -> Option<u16> {
|
||||
VLAN_INTERFACE_REGEX.captures(iface_name).and_then(|cap| {
|
||||
cap.name("vlan_id")
|
||||
.or(cap.name("vlan_id2"))
|
||||
.and_then(|id| id.as_str().parse::<u16>().ok())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn parse_vlan_raw_device_from_name(iface_name: &str) -> Option<&str> {
|
||||
VLAN_INTERFACE_REGEX
|
||||
.captures(iface_name)
|
||||
.and_then(|cap| cap.name("vlan_raw_device"))
|
||||
.map(Into::into)
|
||||
}
|
||||
|
||||
// Write attributes not depending on address family
|
||||
fn write_iface_attributes(iface: &Interface, w: &mut dyn Write) -> Result<(), Error> {
|
||||
static EMPTY_LIST: Vec<String> = Vec::new();
|
||||
|
||||
match iface.interface_type {
|
||||
NetworkInterfaceType::Bridge => {
|
||||
if let Some(true) = iface.bridge_vlan_aware {
|
||||
writeln!(w, "\tbridge-vlan-aware yes")?;
|
||||
}
|
||||
let ports = iface.bridge_ports.as_ref().unwrap_or(&EMPTY_LIST);
|
||||
if ports.is_empty() {
|
||||
writeln!(w, "\tbridge-ports none")?;
|
||||
} else {
|
||||
writeln!(w, "\tbridge-ports {}", ports.join(" "))?;
|
||||
}
|
||||
}
|
||||
NetworkInterfaceType::Bond => {
|
||||
let mode = iface.bond_mode.unwrap_or(LinuxBondMode::BalanceRr);
|
||||
writeln!(w, "\tbond-mode {mode}")?;
|
||||
if let Some(primary) = &iface.bond_primary {
|
||||
if mode == LinuxBondMode::ActiveBackup {
|
||||
writeln!(w, "\tbond-primary {}", primary)?;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(xmit_policy) = &iface.bond_xmit_hash_policy {
|
||||
if mode == LinuxBondMode::Ieee802_3ad || mode == LinuxBondMode::BalanceXor {
|
||||
writeln!(w, "\tbond_xmit_hash_policy {xmit_policy}")?;
|
||||
}
|
||||
}
|
||||
|
||||
let slaves = iface.slaves.as_ref().unwrap_or(&EMPTY_LIST);
|
||||
if slaves.is_empty() {
|
||||
writeln!(w, "\tbond-slaves none")?;
|
||||
} else {
|
||||
writeln!(w, "\tbond-slaves {}", slaves.join(" "))?;
|
||||
}
|
||||
}
|
||||
NetworkInterfaceType::Vlan => {
|
||||
if let Some(vlan_id) = iface.vlan_id {
|
||||
writeln!(w, "\tvlan-id {vlan_id}")?;
|
||||
}
|
||||
if let Some(vlan_raw_device) = &iface.vlan_raw_device {
|
||||
writeln!(w, "\tvlan-raw-device {vlan_raw_device}")?;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if let Some(mtu) = iface.mtu {
|
||||
writeln!(w, "\tmtu {}", mtu)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Write attributes depending on address family inet (IPv4)
|
||||
fn write_iface_attributes_v4(
|
||||
iface: &Interface,
|
||||
w: &mut dyn Write,
|
||||
method: NetworkConfigMethod,
|
||||
) -> Result<(), Error> {
|
||||
if method == NetworkConfigMethod::Static {
|
||||
if let Some(address) = &iface.cidr {
|
||||
writeln!(w, "\taddress {}", address)?;
|
||||
}
|
||||
if let Some(gateway) = &iface.gateway {
|
||||
writeln!(w, "\tgateway {}", gateway)?;
|
||||
}
|
||||
}
|
||||
|
||||
for option in &iface.options {
|
||||
writeln!(w, "\t{}", option)?;
|
||||
}
|
||||
|
||||
if let Some(ref comments) = iface.comments {
|
||||
for comment in comments.lines() {
|
||||
writeln!(w, "#{}", comment)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Write attributes depending on address family inet6 (IPv6)
|
||||
fn write_iface_attributes_v6(
|
||||
iface: &Interface,
|
||||
w: &mut dyn Write,
|
||||
method: NetworkConfigMethod,
|
||||
) -> Result<(), Error> {
|
||||
if method == NetworkConfigMethod::Static {
|
||||
if let Some(address) = &iface.cidr6 {
|
||||
writeln!(w, "\taddress {}", address)?;
|
||||
}
|
||||
if let Some(gateway) = &iface.gateway6 {
|
||||
writeln!(w, "\tgateway {}", gateway)?;
|
||||
}
|
||||
}
|
||||
|
||||
for option in &iface.options6 {
|
||||
writeln!(w, "\t{}", option)?;
|
||||
}
|
||||
|
||||
if let Some(ref comments) = iface.comments6 {
|
||||
for comment in comments.lines() {
|
||||
writeln!(w, "#{}", comment)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_iface(iface: &Interface, w: &mut dyn Write) -> Result<(), Error> {
|
||||
fn method_to_str(method: NetworkConfigMethod) -> &'static str {
|
||||
match method {
|
||||
NetworkConfigMethod::Static => "static",
|
||||
NetworkConfigMethod::Loopback => "loopback",
|
||||
NetworkConfigMethod::Manual => "manual",
|
||||
NetworkConfigMethod::DHCP => "dhcp",
|
||||
}
|
||||
}
|
||||
|
||||
if iface.method.is_none() && iface.method6.is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if iface.autostart {
|
||||
writeln!(w, "auto {}", iface.name)?;
|
||||
}
|
||||
|
||||
if let Some(method) = iface.method {
|
||||
writeln!(w, "iface {} inet {}", iface.name, method_to_str(method))?;
|
||||
write_iface_attributes_v4(iface, w, method)?;
|
||||
write_iface_attributes(iface, w)?;
|
||||
writeln!(w)?;
|
||||
}
|
||||
|
||||
if let Some(method6) = iface.method6 {
|
||||
let mut skip_v6 = false; // avoid empty inet6 manual entry
|
||||
if iface.method.is_some()
|
||||
&& method6 == NetworkConfigMethod::Manual
|
||||
&& iface.comments6.is_none()
|
||||
&& iface.options6.is_empty()
|
||||
{
|
||||
skip_v6 = true;
|
||||
}
|
||||
|
||||
if !skip_v6 {
|
||||
writeln!(w, "iface {} inet6 {}", iface.name, method_to_str(method6))?;
|
||||
write_iface_attributes_v6(iface, w, method6)?;
|
||||
if iface.method.is_none() {
|
||||
// only write common attributes once
|
||||
write_iface_attributes(iface, w)?;
|
||||
}
|
||||
writeln!(w)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum NetworkOrderEntry {
|
||||
Iface(String),
|
||||
Comment(String),
|
||||
Option(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct NetworkConfig {
|
||||
pub interfaces: BTreeMap<String, Interface>,
|
||||
order: Vec<NetworkOrderEntry>,
|
||||
}
|
||||
|
||||
impl TryFrom<NetworkConfig> for String {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(config: NetworkConfig) -> Result<Self, Self::Error> {
|
||||
let mut output = Vec::new();
|
||||
config.write_config(&mut output)?;
|
||||
let res = String::from_utf8(output)?;
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl NetworkConfig {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
interfaces: BTreeMap::new(),
|
||||
order: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn lookup(&self, name: &str) -> Result<&Interface, Error> {
|
||||
let interface = self
|
||||
.interfaces
|
||||
.get(name)
|
||||
.ok_or_else(|| format_err!("interface '{}' does not exist.", name))?;
|
||||
Ok(interface)
|
||||
}
|
||||
|
||||
pub fn lookup_mut(&mut self, name: &str) -> Result<&mut Interface, Error> {
|
||||
let interface = self
|
||||
.interfaces
|
||||
.get_mut(name)
|
||||
.ok_or_else(|| format_err!("interface '{}' does not exist.", name))?;
|
||||
Ok(interface)
|
||||
}
|
||||
|
||||
/// Check if ports are used only once
|
||||
fn check_port_usage(&self) -> Result<(), Error> {
|
||||
let mut used_ports = HashMap::new();
|
||||
let mut check_port_usage = |iface, ports: &Vec<String>| {
|
||||
for port in ports.iter() {
|
||||
if let Some(prev_iface) = used_ports.get(port) {
|
||||
bail!(
|
||||
"iface '{}' port '{}' is already used on interface '{}'",
|
||||
iface,
|
||||
port,
|
||||
prev_iface
|
||||
);
|
||||
}
|
||||
used_ports.insert(port.to_string(), iface);
|
||||
}
|
||||
Ok(())
|
||||
};
|
||||
|
||||
for (iface, interface) in self.interfaces.iter() {
|
||||
if let Some(ports) = &interface.bridge_ports {
|
||||
check_port_usage(iface, ports)?;
|
||||
}
|
||||
if let Some(slaves) = &interface.slaves {
|
||||
check_port_usage(iface, slaves)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check if child mtu is less or equal than parent mtu
|
||||
fn check_mtu(&self, parent_name: &str, child_name: &str) -> Result<(), Error> {
|
||||
let parent = self
|
||||
.interfaces
|
||||
.get(parent_name)
|
||||
.ok_or_else(|| format_err!("check_mtu - missing parent interface '{}'", parent_name))?;
|
||||
let child = self
|
||||
.interfaces
|
||||
.get(child_name)
|
||||
.ok_or_else(|| format_err!("check_mtu - missing child interface '{}'", child_name))?;
|
||||
|
||||
let child_mtu = match child.mtu {
|
||||
Some(mtu) => mtu,
|
||||
None => return Ok(()),
|
||||
};
|
||||
|
||||
let parent_mtu = match parent.mtu {
|
||||
Some(mtu) => mtu,
|
||||
None => {
|
||||
if parent.interface_type == NetworkInterfaceType::Bond {
|
||||
child_mtu
|
||||
} else {
|
||||
1500
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if parent_mtu < child_mtu {
|
||||
bail!(
|
||||
"interface '{}' - mtu {} is lower than '{}' - mtu {}\n",
|
||||
parent_name,
|
||||
parent_mtu,
|
||||
child_name,
|
||||
child_mtu
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check if bond slaves exists
|
||||
fn check_bond_slaves(&self) -> Result<(), Error> {
|
||||
for (iface, interface) in self.interfaces.iter() {
|
||||
if let Some(slaves) = &interface.slaves {
|
||||
for slave in slaves.iter() {
|
||||
match self.interfaces.get(slave) {
|
||||
Some(entry) => {
|
||||
if entry.interface_type != NetworkInterfaceType::Eth {
|
||||
bail!(
|
||||
"bond '{}' - wrong interface type on slave '{}' ({:?} != {:?})",
|
||||
iface,
|
||||
slave,
|
||||
entry.interface_type,
|
||||
NetworkInterfaceType::Eth
|
||||
);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
bail!("bond '{}' - unable to find slave '{}'", iface, slave);
|
||||
}
|
||||
}
|
||||
self.check_mtu(iface, slave)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check if bridge ports exists
|
||||
fn check_bridge_ports(&self) -> Result<(), Error> {
|
||||
static VLAN_INTERFACE_REGEX: LazyLock<Regex> =
|
||||
LazyLock::new(|| Regex::new(r"^(\S+)\.(\d+)$").unwrap());
|
||||
|
||||
for (iface, interface) in self.interfaces.iter() {
|
||||
if let Some(ports) = &interface.bridge_ports {
|
||||
for port in ports.iter() {
|
||||
let captures = VLAN_INTERFACE_REGEX.captures(port);
|
||||
let port = if let Some(ref caps) = captures {
|
||||
&caps[1]
|
||||
} else {
|
||||
port.as_str()
|
||||
};
|
||||
if !self.interfaces.contains_key(port) {
|
||||
bail!("bridge '{}' - unable to find port '{}'", iface, port);
|
||||
}
|
||||
self.check_mtu(iface, port)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_config(&self, w: &mut dyn Write) -> Result<(), Error> {
|
||||
self.check_port_usage()?;
|
||||
self.check_bond_slaves()?;
|
||||
self.check_bridge_ports()?;
|
||||
|
||||
let mut done = HashSet::new();
|
||||
|
||||
let mut last_entry_was_comment = false;
|
||||
|
||||
for entry in self.order.iter() {
|
||||
match entry {
|
||||
NetworkOrderEntry::Comment(comment) => {
|
||||
writeln!(w, "#{}", comment)?;
|
||||
last_entry_was_comment = true;
|
||||
}
|
||||
NetworkOrderEntry::Option(option) => {
|
||||
if last_entry_was_comment {
|
||||
writeln!(w)?;
|
||||
}
|
||||
last_entry_was_comment = false;
|
||||
writeln!(w, "{}", option)?;
|
||||
writeln!(w)?;
|
||||
}
|
||||
NetworkOrderEntry::Iface(name) => {
|
||||
let interface = match self.interfaces.get(name) {
|
||||
Some(interface) => interface,
|
||||
None => continue,
|
||||
};
|
||||
|
||||
if last_entry_was_comment {
|
||||
writeln!(w)?;
|
||||
}
|
||||
last_entry_was_comment = false;
|
||||
|
||||
if done.contains(name) {
|
||||
continue;
|
||||
}
|
||||
done.insert(name);
|
||||
|
||||
write_iface(interface, w)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (name, interface) in &self.interfaces {
|
||||
if done.contains(name) {
|
||||
continue;
|
||||
}
|
||||
write_iface(interface, w)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub const NETWORK_INTERFACES_FILENAME: &str = "/etc/network/interfaces";
|
||||
pub const NETWORK_INTERFACES_NEW_FILENAME: &str = "/etc/network/interfaces.new";
|
||||
pub const NETWORK_LOCKFILE: &str = "/var/lock/pve-network.lck";
|
||||
|
||||
pub fn lock_config() -> Result<BackupLockGuard, Error> {
|
||||
open_backup_lockfile(NETWORK_LOCKFILE, None, true)
|
||||
}
|
||||
|
||||
pub fn config() -> Result<(NetworkConfig, [u8; 32]), Error> {
|
||||
let content =
|
||||
match proxmox_sys::fs::file_get_optional_contents(NETWORK_INTERFACES_NEW_FILENAME)? {
|
||||
Some(content) => content,
|
||||
None => {
|
||||
let content =
|
||||
proxmox_sys::fs::file_get_optional_contents(NETWORK_INTERFACES_FILENAME)?;
|
||||
content.unwrap_or_default()
|
||||
}
|
||||
};
|
||||
|
||||
let digest = openssl::sha::sha256(&content);
|
||||
|
||||
let existing_interfaces = get_network_interfaces()?;
|
||||
let mut parser = NetworkParser::new(&content[..]);
|
||||
let data = parser.parse_interfaces(Some(&existing_interfaces))?;
|
||||
|
||||
Ok((data, digest))
|
||||
}
|
||||
|
||||
pub fn changes() -> Result<String, Error> {
|
||||
if !std::path::Path::new(NETWORK_INTERFACES_NEW_FILENAME).exists() {
|
||||
return Ok(String::new());
|
||||
}
|
||||
|
||||
compute_file_diff(NETWORK_INTERFACES_FILENAME, NETWORK_INTERFACES_NEW_FILENAME)
|
||||
}
|
||||
|
||||
pub fn save_config(config: &NetworkConfig) -> Result<(), Error> {
|
||||
let mut raw = Vec::new();
|
||||
config.write_config(&mut raw)?;
|
||||
|
||||
let mode = nix::sys::stat::Mode::from_bits_truncate(0o0644);
|
||||
// set the correct owner/group/permissions while saving file
|
||||
// owner(rw) = root, group(r)=root, others(r)
|
||||
let options = CreateOptions::new()
|
||||
.perm(mode)
|
||||
.owner(nix::unistd::ROOT)
|
||||
.group(nix::unistd::Gid::from_raw(0));
|
||||
|
||||
replace_file(NETWORK_INTERFACES_NEW_FILENAME, &raw, options, true)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// shell completion helper
|
||||
pub fn complete_interface_name(_arg: &str, _param: &HashMap<String, String>) -> Vec<String> {
|
||||
match config() {
|
||||
Ok((data, _digest)) => data.interfaces.keys().map(|id| id.to_string()).collect(),
|
||||
Err(_) => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn complete_port_list(arg: &str, _param: &HashMap<String, String>) -> Vec<String> {
|
||||
let mut ports = Vec::new();
|
||||
match config() {
|
||||
Ok((data, _digest)) => {
|
||||
for (iface, interface) in data.interfaces.iter() {
|
||||
if interface.interface_type == NetworkInterfaceType::Eth {
|
||||
ports.push(iface.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => return Vec::new(),
|
||||
};
|
||||
|
||||
let arg = arg.trim();
|
||||
let prefix = if let Some(idx) = arg.rfind(',') {
|
||||
&arg[..idx + 1]
|
||||
} else {
|
||||
""
|
||||
};
|
||||
ports
|
||||
.iter()
|
||||
.map(|port| format!("{}{}", prefix, port))
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use NetworkConfigMethod::*;
|
||||
use NetworkInterfaceType::*;
|
||||
use NetworkOrderEntry::*;
|
||||
|
||||
#[test]
|
||||
fn test_write_network_config_manual() {
|
||||
let iface_name = String::from("enp3s0");
|
||||
let mut iface = Interface::new(iface_name.clone());
|
||||
iface.interface_type = Eth;
|
||||
iface.method = Some(Manual);
|
||||
iface.active = true;
|
||||
|
||||
let nw_config = NetworkConfig {
|
||||
interfaces: BTreeMap::from([(iface_name.clone(), iface)]),
|
||||
order: vec![Iface(iface_name.clone())],
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
String::try_from(nw_config).unwrap().trim(),
|
||||
r#"iface enp3s0 inet manual"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_write_network_config_static() {
|
||||
let iface_name = String::from("enp3s0");
|
||||
let mut iface = Interface::new(iface_name.clone());
|
||||
iface.interface_type = Eth;
|
||||
iface.method = Some(Static);
|
||||
iface.cidr = Some(String::from("10.0.0.100/16"));
|
||||
iface.active = true;
|
||||
|
||||
let nw_config = NetworkConfig {
|
||||
interfaces: BTreeMap::from([(iface_name.clone(), iface)]),
|
||||
order: vec![Iface(iface_name.clone())],
|
||||
};
|
||||
assert_eq!(
|
||||
String::try_from(nw_config).unwrap().trim(),
|
||||
r#"
|
||||
iface enp3s0 inet static
|
||||
address 10.0.0.100/16"#
|
||||
.to_string()
|
||||
.trim()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_write_network_config_static_with_gateway() {
|
||||
let iface_name = String::from("enp3s0");
|
||||
let mut iface = Interface::new(iface_name.clone());
|
||||
iface.interface_type = Eth;
|
||||
iface.method = Some(Static);
|
||||
iface.cidr = Some(String::from("10.0.0.100/16"));
|
||||
iface.gateway = Some(String::from("10.0.0.1"));
|
||||
iface.active = true;
|
||||
|
||||
let nw_config = NetworkConfig {
|
||||
interfaces: BTreeMap::from([(iface_name.clone(), iface)]),
|
||||
order: vec![Iface(iface_name.clone())],
|
||||
};
|
||||
assert_eq!(
|
||||
String::try_from(nw_config).unwrap().trim(),
|
||||
r#"
|
||||
iface enp3s0 inet static
|
||||
address 10.0.0.100/16
|
||||
gateway 10.0.0.1"#
|
||||
.to_string()
|
||||
.trim()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_write_network_config_vlan_id_in_name() {
|
||||
let iface_name = String::from("vmbr0.100");
|
||||
let mut iface = Interface::new(iface_name.clone());
|
||||
iface.interface_type = Vlan;
|
||||
iface.method = Some(Manual);
|
||||
iface.active = true;
|
||||
|
||||
let nw_config = NetworkConfig {
|
||||
interfaces: BTreeMap::from([(iface_name.clone(), iface)]),
|
||||
order: vec![Iface(iface_name.clone())],
|
||||
};
|
||||
assert_eq!(
|
||||
String::try_from(nw_config).unwrap().trim(),
|
||||
"iface vmbr0.100 inet manual"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_write_network_config_vlan_with_raw_device() {
|
||||
let iface_name = String::from("vlan100");
|
||||
let mut iface = Interface::new(iface_name.clone());
|
||||
iface.interface_type = Vlan;
|
||||
iface.vlan_raw_device = Some(String::from("vmbr0"));
|
||||
iface.method = Some(Manual);
|
||||
iface.active = true;
|
||||
|
||||
let nw_config = NetworkConfig {
|
||||
interfaces: BTreeMap::from([(iface_name.clone(), iface)]),
|
||||
order: vec![Iface(iface_name.clone())],
|
||||
};
|
||||
assert_eq!(
|
||||
String::try_from(nw_config).unwrap().trim(),
|
||||
r#"
|
||||
iface vlan100 inet manual
|
||||
vlan-raw-device vmbr0"#
|
||||
.trim()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_write_network_config_vlan_with_individual_name() {
|
||||
let iface_name = String::from("individual_name");
|
||||
let mut iface = Interface::new(iface_name.clone());
|
||||
iface.interface_type = Vlan;
|
||||
iface.vlan_raw_device = Some(String::from("vmbr0"));
|
||||
iface.vlan_id = Some(100);
|
||||
iface.method = Some(Manual);
|
||||
iface.active = true;
|
||||
|
||||
let nw_config = NetworkConfig {
|
||||
interfaces: BTreeMap::from([(iface_name.clone(), iface)]),
|
||||
order: vec![Iface(iface_name.clone())],
|
||||
};
|
||||
assert_eq!(
|
||||
String::try_from(nw_config).unwrap().trim(),
|
||||
r#"
|
||||
iface individual_name inet manual
|
||||
vlan-id 100
|
||||
vlan-raw-device vmbr0"#
|
||||
.trim()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vlan_parse_vlan_id_from_name() {
|
||||
assert_eq!(parse_vlan_id_from_name("vlan100"), Some(100));
|
||||
assert_eq!(parse_vlan_id_from_name("vlan"), None);
|
||||
assert_eq!(parse_vlan_id_from_name("arbitrary"), None);
|
||||
assert_eq!(parse_vlan_id_from_name("vmbr0.100"), Some(100));
|
||||
assert_eq!(parse_vlan_id_from_name("vmbr0"), None);
|
||||
// assert_eq!(parse_vlan_id_from_name("vmbr0.1.400"), Some(400)); // NOTE ifupdown2 does actually support this
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vlan_parse_vlan_raw_device_from_name() {
|
||||
assert_eq!(parse_vlan_raw_device_from_name("vlan100"), None);
|
||||
assert_eq!(parse_vlan_raw_device_from_name("arbitrary"), None);
|
||||
assert_eq!(parse_vlan_raw_device_from_name("vmbr0"), None);
|
||||
assert_eq!(parse_vlan_raw_device_from_name("vmbr0.200"), Some("vmbr0"));
|
||||
}
|
||||
}
|
||||
846
pbs-config/src/network/parser.rs
Normal file
846
pbs-config/src/network/parser.rs
Normal file
@ -0,0 +1,846 @@
|
||||
use crate::network::VLAN_INTERFACE_REGEX;
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::io::BufRead;
|
||||
use std::iter::{Iterator, Peekable};
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use anyhow::{bail, format_err, Error};
|
||||
use regex::Regex;
|
||||
|
||||
use super::helper::*;
|
||||
use super::lexer::*;
|
||||
|
||||
use super::{
|
||||
bond_mode_from_str, bond_xmit_hash_policy_from_str, Interface, NetworkConfig,
|
||||
NetworkConfigMethod, NetworkInterfaceType, NetworkOrderEntry,
|
||||
};
|
||||
|
||||
fn set_method_v4(iface: &mut Interface, method: NetworkConfigMethod) -> Result<(), Error> {
|
||||
if iface.method.is_none() {
|
||||
iface.method = Some(method);
|
||||
} else {
|
||||
bail!("inet configuration method already set.");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_method_v6(iface: &mut Interface, method: NetworkConfigMethod) -> Result<(), Error> {
|
||||
if iface.method6.is_none() {
|
||||
iface.method6 = Some(method);
|
||||
} else {
|
||||
bail!("inet6 configuration method already set.");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_cidr_v4(iface: &mut Interface, address: String) -> Result<(), Error> {
|
||||
if iface.cidr.is_none() {
|
||||
iface.cidr = Some(address);
|
||||
} else {
|
||||
bail!("duplicate IPv4 address.");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_gateway_v4(iface: &mut Interface, gateway: String) -> Result<(), Error> {
|
||||
if iface.gateway.is_none() {
|
||||
iface.gateway = Some(gateway);
|
||||
} else {
|
||||
bail!("duplicate IPv4 gateway.");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_cidr_v6(iface: &mut Interface, address: String) -> Result<(), Error> {
|
||||
if iface.cidr6.is_none() {
|
||||
iface.cidr6 = Some(address);
|
||||
} else {
|
||||
bail!("duplicate IPv6 address.");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_gateway_v6(iface: &mut Interface, gateway: String) -> Result<(), Error> {
|
||||
if iface.gateway6.is_none() {
|
||||
iface.gateway6 = Some(gateway);
|
||||
} else {
|
||||
bail!("duplicate IPv4 gateway.");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_interface_type(
|
||||
iface: &mut Interface,
|
||||
interface_type: NetworkInterfaceType,
|
||||
) -> Result<(), Error> {
|
||||
if iface.interface_type == NetworkInterfaceType::Unknown {
|
||||
iface.interface_type = interface_type;
|
||||
} else if iface.interface_type != interface_type {
|
||||
bail!(
|
||||
"interface type already defined - cannot change from {:?} to {:?}",
|
||||
iface.interface_type,
|
||||
interface_type
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub struct NetworkParser<R: BufRead> {
|
||||
input: Peekable<Lexer<R>>,
|
||||
line_nr: usize,
|
||||
}
|
||||
|
||||
impl<R: BufRead> NetworkParser<R> {
|
||||
pub fn new(reader: R) -> Self {
|
||||
let input = Lexer::new(reader).peekable();
|
||||
Self { input, line_nr: 1 }
|
||||
}
|
||||
|
||||
fn peek(&mut self) -> Result<Token, Error> {
|
||||
match self.input.peek() {
|
||||
Some(Err(err)) => {
|
||||
bail!("input error - {}", err);
|
||||
}
|
||||
Some(Ok((token, _))) => Ok(*token),
|
||||
None => {
|
||||
bail!("got unexpected end of stream (inside peek)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn next(&mut self) -> Result<(Token, String), Error> {
|
||||
match self.input.next() {
|
||||
Some(Err(err)) => {
|
||||
bail!("input error - {}", err);
|
||||
}
|
||||
Some(Ok((token, text))) => {
|
||||
if token == Token::Newline {
|
||||
self.line_nr += 1;
|
||||
}
|
||||
Ok((token, text))
|
||||
}
|
||||
None => {
|
||||
bail!("got unexpected end of stream (inside peek)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn next_text(&mut self) -> Result<String, Error> {
|
||||
match self.next()? {
|
||||
(Token::Text, text) => Ok(text),
|
||||
(unexpected, _) => bail!("got unexpected token {:?} (expecting Text)", unexpected),
|
||||
}
|
||||
}
|
||||
|
||||
fn eat(&mut self, expected: Token) -> Result<String, Error> {
|
||||
let (next, text) = self.next()?;
|
||||
if next != expected {
|
||||
bail!("expected {:?}, got {:?}", expected, next);
|
||||
}
|
||||
Ok(text)
|
||||
}
|
||||
|
||||
fn parse_auto(&mut self, auto_flag: &mut HashSet<String>) -> Result<(), Error> {
|
||||
self.eat(Token::Auto)?;
|
||||
|
||||
loop {
|
||||
match self.next()? {
|
||||
(Token::Text, iface) => {
|
||||
auto_flag.insert(iface.to_string());
|
||||
}
|
||||
(Token::Newline, _) => break,
|
||||
unexpected => {
|
||||
bail!("expected {:?}, got {:?}", Token::Text, unexpected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_netmask(&mut self) -> Result<u8, Error> {
|
||||
self.eat(Token::Netmask)?;
|
||||
let netmask = self.next_text()?;
|
||||
|
||||
let mask = if let Some(mask) = IPV4_MASK_HASH_LOCALNET.get(netmask.as_str()) {
|
||||
*mask
|
||||
} else {
|
||||
match netmask.as_str().parse::<u8>() {
|
||||
Ok(mask) => mask,
|
||||
Err(err) => {
|
||||
bail!("unable to parse netmask '{}' - {}", netmask, err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
self.eat(Token::Newline)?;
|
||||
|
||||
Ok(mask)
|
||||
}
|
||||
|
||||
fn parse_iface_address(&mut self) -> Result<(String, Option<u8>, bool), Error> {
|
||||
self.eat(Token::Address)?;
|
||||
let cidr = self.next_text()?;
|
||||
|
||||
let (_address, mask, ipv6) = parse_address_or_cidr(&cidr)?;
|
||||
|
||||
self.eat(Token::Newline)?;
|
||||
|
||||
Ok((cidr, mask, ipv6))
|
||||
}
|
||||
|
||||
fn parse_iface_gateway(&mut self, interface: &mut Interface) -> Result<(), Error> {
|
||||
self.eat(Token::Gateway)?;
|
||||
let gateway = self.next_text()?;
|
||||
|
||||
if pbs_api_types::IP_REGEX.is_match(&gateway) {
|
||||
if gateway.contains(':') {
|
||||
set_gateway_v6(interface, gateway)?;
|
||||
} else {
|
||||
set_gateway_v4(interface, gateway)?;
|
||||
}
|
||||
} else {
|
||||
bail!("unable to parse gateway address");
|
||||
}
|
||||
|
||||
self.eat(Token::Newline)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_iface_mtu(&mut self) -> Result<u64, Error> {
|
||||
self.eat(Token::MTU)?;
|
||||
|
||||
let mtu = self.next_text()?;
|
||||
let mtu = match mtu.parse::<u64>() {
|
||||
Ok(mtu) => mtu,
|
||||
Err(err) => {
|
||||
bail!("unable to parse mtu value '{}' - {}", mtu, err);
|
||||
}
|
||||
};
|
||||
|
||||
self.eat(Token::Newline)?;
|
||||
|
||||
Ok(mtu)
|
||||
}
|
||||
|
||||
fn parse_yes_no(&mut self) -> Result<bool, Error> {
|
||||
let text = self.next_text()?;
|
||||
let value = match text.to_lowercase().as_str() {
|
||||
"yes" => true,
|
||||
"no" => false,
|
||||
_ => {
|
||||
bail!("unable to bool value '{}' - (expected yes/no)", text);
|
||||
}
|
||||
};
|
||||
|
||||
self.eat(Token::Newline)?;
|
||||
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
fn parse_to_eol(&mut self) -> Result<String, Error> {
|
||||
let mut line = String::new();
|
||||
loop {
|
||||
match self.next()? {
|
||||
(Token::Newline, _) => return Ok(line),
|
||||
(_, text) => {
|
||||
if !line.is_empty() {
|
||||
line.push(' ');
|
||||
}
|
||||
line.push_str(&text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_iface_list(&mut self) -> Result<Vec<String>, Error> {
|
||||
let mut list = Vec::new();
|
||||
|
||||
loop {
|
||||
let (token, text) = self.next()?;
|
||||
match token {
|
||||
Token::Newline => break,
|
||||
Token::Text => {
|
||||
if &text != "none" {
|
||||
list.push(text);
|
||||
}
|
||||
}
|
||||
_ => bail!(
|
||||
"unable to parse interface list - unexpected token '{:?}'",
|
||||
token
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(list)
|
||||
}
|
||||
|
||||
fn parse_iface_attributes(
|
||||
&mut self,
|
||||
interface: &mut Interface,
|
||||
address_family_v4: bool,
|
||||
address_family_v6: bool,
|
||||
) -> Result<(), Error> {
|
||||
let mut netmask = None;
|
||||
let mut address_list = Vec::new();
|
||||
|
||||
loop {
|
||||
match self.peek()? {
|
||||
Token::Attribute => {
|
||||
self.eat(Token::Attribute)?;
|
||||
}
|
||||
Token::Comment => {
|
||||
let comment = self.eat(Token::Comment)?;
|
||||
if !address_family_v4 && address_family_v6 {
|
||||
let mut comments = interface.comments6.take().unwrap_or_default();
|
||||
if !comments.is_empty() {
|
||||
comments.push('\n');
|
||||
}
|
||||
comments.push_str(&comment);
|
||||
interface.comments6 = Some(comments);
|
||||
} else {
|
||||
let mut comments = interface.comments.take().unwrap_or_default();
|
||||
if !comments.is_empty() {
|
||||
comments.push('\n');
|
||||
}
|
||||
comments.push_str(&comment);
|
||||
interface.comments = Some(comments);
|
||||
}
|
||||
self.eat(Token::Newline)?;
|
||||
continue;
|
||||
}
|
||||
_ => break,
|
||||
}
|
||||
|
||||
match self.peek()? {
|
||||
Token::Address => {
|
||||
let (cidr, mask, is_v6) = self.parse_iface_address()?;
|
||||
address_list.push((cidr, mask, is_v6));
|
||||
}
|
||||
Token::Gateway => self.parse_iface_gateway(interface)?,
|
||||
Token::Netmask => {
|
||||
//Note: netmask is deprecated, but we try to do our best
|
||||
netmask = Some(self.parse_netmask()?);
|
||||
}
|
||||
Token::MTU => {
|
||||
let mtu = self.parse_iface_mtu()?;
|
||||
interface.mtu = Some(mtu);
|
||||
}
|
||||
Token::BridgeVlanAware => {
|
||||
self.eat(Token::BridgeVlanAware)?;
|
||||
let bridge_vlan_aware = self.parse_yes_no()?;
|
||||
interface.bridge_vlan_aware = Some(bridge_vlan_aware);
|
||||
}
|
||||
Token::BridgePorts => {
|
||||
self.eat(Token::BridgePorts)?;
|
||||
let ports = self.parse_iface_list()?;
|
||||
interface.bridge_ports = Some(ports);
|
||||
set_interface_type(interface, NetworkInterfaceType::Bridge)?;
|
||||
}
|
||||
Token::BondSlaves => {
|
||||
self.eat(Token::BondSlaves)?;
|
||||
let slaves = self.parse_iface_list()?;
|
||||
interface.slaves = Some(slaves);
|
||||
set_interface_type(interface, NetworkInterfaceType::Bond)?;
|
||||
}
|
||||
Token::BondMode => {
|
||||
self.eat(Token::BondMode)?;
|
||||
let mode = self.next_text()?;
|
||||
interface.bond_mode = Some(bond_mode_from_str(&mode)?);
|
||||
self.eat(Token::Newline)?;
|
||||
}
|
||||
Token::BondPrimary => {
|
||||
self.eat(Token::BondPrimary)?;
|
||||
let primary = self.next_text()?;
|
||||
interface.bond_primary = Some(primary);
|
||||
self.eat(Token::Newline)?;
|
||||
}
|
||||
Token::BondXmitHashPolicy => {
|
||||
self.eat(Token::BondXmitHashPolicy)?;
|
||||
let policy = bond_xmit_hash_policy_from_str(&self.next_text()?)?;
|
||||
interface.bond_xmit_hash_policy = Some(policy);
|
||||
self.eat(Token::Newline)?;
|
||||
}
|
||||
Token::VlanId => {
|
||||
self.eat(Token::VlanId)?;
|
||||
let vlan_id = self.next_text()?.parse()?;
|
||||
interface.vlan_id = Some(vlan_id);
|
||||
set_interface_type(interface, NetworkInterfaceType::Vlan)?;
|
||||
self.eat(Token::Newline)?;
|
||||
}
|
||||
Token::VlanRawDevice => {
|
||||
self.eat(Token::VlanRawDevice)?;
|
||||
let vlan_raw_device = self.next_text()?;
|
||||
interface.vlan_raw_device = Some(vlan_raw_device);
|
||||
set_interface_type(interface, NetworkInterfaceType::Vlan)?;
|
||||
self.eat(Token::Newline)?;
|
||||
}
|
||||
_ => {
|
||||
// parse addon attributes
|
||||
let option = self.parse_to_eol()?;
|
||||
if !option.is_empty() {
|
||||
if !address_family_v4 && address_family_v6 {
|
||||
interface.options6.push(option);
|
||||
} else {
|
||||
interface.options.push(option);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::comparison_chain)]
|
||||
if let Some(netmask) = netmask {
|
||||
if address_list.len() > 1 {
|
||||
bail!("unable to apply netmask to multiple addresses (please use cidr notation)");
|
||||
} else if address_list.len() == 1 {
|
||||
let (mut cidr, mask, is_v6) = address_list.pop().unwrap();
|
||||
if mask.is_some() {
|
||||
// address already has a mask - ignore netmask
|
||||
} else {
|
||||
use std::fmt::Write as _;
|
||||
check_netmask(netmask, is_v6)?;
|
||||
let _ = write!(cidr, "/{}", netmask);
|
||||
}
|
||||
if is_v6 {
|
||||
set_cidr_v6(interface, cidr)?;
|
||||
} else {
|
||||
set_cidr_v4(interface, cidr)?;
|
||||
}
|
||||
} else {
|
||||
// no address - simply ignore useless netmask
|
||||
}
|
||||
} else {
|
||||
for (cidr, mask, is_v6) in address_list {
|
||||
if mask.is_none() {
|
||||
bail!("missing netmask in '{}'", cidr);
|
||||
}
|
||||
if is_v6 {
|
||||
set_cidr_v6(interface, cidr)?;
|
||||
} else {
|
||||
set_cidr_v4(interface, cidr)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_iface(&mut self, config: &mut NetworkConfig) -> Result<(), Error> {
|
||||
self.eat(Token::Iface)?;
|
||||
let iface = self.next_text()?;
|
||||
|
||||
let mut address_family_v4 = false;
|
||||
let mut address_family_v6 = false;
|
||||
let mut config_method = None;
|
||||
|
||||
loop {
|
||||
let (token, text) = self.next()?;
|
||||
match token {
|
||||
Token::Newline => break,
|
||||
Token::Inet => address_family_v4 = true,
|
||||
Token::Inet6 => address_family_v6 = true,
|
||||
Token::Loopback => config_method = Some(NetworkConfigMethod::Loopback),
|
||||
Token::Static => config_method = Some(NetworkConfigMethod::Static),
|
||||
Token::Manual => config_method = Some(NetworkConfigMethod::Manual),
|
||||
Token::DHCP => config_method = Some(NetworkConfigMethod::DHCP),
|
||||
_ => bail!("unknown iface option {}", text),
|
||||
}
|
||||
}
|
||||
|
||||
let config_method = config_method.unwrap_or(NetworkConfigMethod::Static);
|
||||
|
||||
if !(address_family_v4 || address_family_v6) {
|
||||
address_family_v4 = true;
|
||||
address_family_v6 = true;
|
||||
}
|
||||
|
||||
if let Some(interface) = config.interfaces.get_mut(&iface) {
|
||||
if address_family_v4 {
|
||||
set_method_v4(interface, config_method)?;
|
||||
}
|
||||
if address_family_v6 {
|
||||
set_method_v6(interface, config_method)?;
|
||||
}
|
||||
|
||||
self.parse_iface_attributes(interface, address_family_v4, address_family_v6)?;
|
||||
} else {
|
||||
let mut interface = Interface::new(iface.clone());
|
||||
if address_family_v4 {
|
||||
set_method_v4(&mut interface, config_method)?;
|
||||
}
|
||||
if address_family_v6 {
|
||||
set_method_v6(&mut interface, config_method)?;
|
||||
}
|
||||
|
||||
self.parse_iface_attributes(&mut interface, address_family_v4, address_family_v6)?;
|
||||
|
||||
config.interfaces.insert(interface.name.clone(), interface);
|
||||
|
||||
config.order.push(NetworkOrderEntry::Iface(iface));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn parse_interfaces(
|
||||
&mut self,
|
||||
existing_interfaces: Option<&HashMap<String, bool>>,
|
||||
) -> Result<NetworkConfig, Error> {
|
||||
self.do_parse_interfaces(existing_interfaces)
|
||||
.map_err(|err| format_err!("line {}: {}", self.line_nr, err))
|
||||
}
|
||||
|
||||
fn do_parse_interfaces(
|
||||
&mut self,
|
||||
existing_interfaces: Option<&HashMap<String, bool>>,
|
||||
) -> Result<NetworkConfig, Error> {
|
||||
let mut config = NetworkConfig::new();
|
||||
|
||||
let mut auto_flag: HashSet<String> = HashSet::new();
|
||||
|
||||
loop {
|
||||
match self.peek()? {
|
||||
Token::EOF => {
|
||||
break;
|
||||
}
|
||||
Token::Newline => {
|
||||
// skip empty lines
|
||||
self.eat(Token::Newline)?;
|
||||
}
|
||||
Token::Comment => {
|
||||
let (_, text) = self.next()?;
|
||||
config.order.push(NetworkOrderEntry::Comment(text));
|
||||
self.eat(Token::Newline)?;
|
||||
}
|
||||
Token::Auto => {
|
||||
self.parse_auto(&mut auto_flag)?;
|
||||
}
|
||||
Token::Iface => {
|
||||
self.parse_iface(&mut config)?;
|
||||
}
|
||||
_ => {
|
||||
let option = self.parse_to_eol()?;
|
||||
if !option.is_empty() {
|
||||
config.order.push(NetworkOrderEntry::Option(option));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for iface in auto_flag.iter() {
|
||||
if let Some(interface) = config.interfaces.get_mut(iface) {
|
||||
interface.autostart = true;
|
||||
}
|
||||
}
|
||||
|
||||
static INTERFACE_ALIAS_REGEX: LazyLock<Regex> =
|
||||
LazyLock::new(|| Regex::new(r"^\S+:\d+$").unwrap());
|
||||
|
||||
if let Some(existing_interfaces) = existing_interfaces {
|
||||
for (iface, active) in existing_interfaces.iter() {
|
||||
if let Some(interface) = config.interfaces.get_mut(iface) {
|
||||
interface.active = *active;
|
||||
if interface.interface_type == NetworkInterfaceType::Unknown
|
||||
&& super::is_physical_nic(iface)
|
||||
{
|
||||
interface.interface_type = NetworkInterfaceType::Eth;
|
||||
}
|
||||
} else if super::is_physical_nic(iface) {
|
||||
// also add all physical NICs
|
||||
let mut interface = Interface::new(iface.clone());
|
||||
set_method_v4(&mut interface, NetworkConfigMethod::Manual)?;
|
||||
interface.interface_type = NetworkInterfaceType::Eth;
|
||||
interface.active = *active;
|
||||
config.interfaces.insert(interface.name.clone(), interface);
|
||||
config
|
||||
.order
|
||||
.push(NetworkOrderEntry::Iface(iface.to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (name, interface) in config.interfaces.iter_mut() {
|
||||
if interface.interface_type != NetworkInterfaceType::Unknown {
|
||||
continue;
|
||||
}
|
||||
if name == "lo" {
|
||||
interface.interface_type = NetworkInterfaceType::Loopback;
|
||||
continue;
|
||||
}
|
||||
if INTERFACE_ALIAS_REGEX.is_match(name) {
|
||||
interface.interface_type = NetworkInterfaceType::Alias;
|
||||
continue;
|
||||
}
|
||||
if VLAN_INTERFACE_REGEX.is_match(name) {
|
||||
interface.interface_type = NetworkInterfaceType::Vlan;
|
||||
continue;
|
||||
}
|
||||
if super::is_physical_nic(name) {
|
||||
interface.interface_type = NetworkInterfaceType::Eth;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if !config.interfaces.contains_key("lo") {
|
||||
let mut interface = Interface::new(String::from("lo"));
|
||||
set_method_v4(&mut interface, NetworkConfigMethod::Loopback)?;
|
||||
interface.interface_type = NetworkInterfaceType::Loopback;
|
||||
interface.autostart = true;
|
||||
config.interfaces.insert(interface.name.clone(), interface);
|
||||
|
||||
// Note: insert 'lo' as first interface after initial comments
|
||||
let mut new_order = Vec::new();
|
||||
let mut added_lo = false;
|
||||
for entry in config.order {
|
||||
if added_lo {
|
||||
new_order.push(entry);
|
||||
continue;
|
||||
} // copy the rest
|
||||
match entry {
|
||||
NetworkOrderEntry::Comment(_) => {
|
||||
new_order.push(entry);
|
||||
}
|
||||
_ => {
|
||||
new_order.push(NetworkOrderEntry::Iface(String::from("lo")));
|
||||
added_lo = true;
|
||||
new_order.push(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
config.order = new_order;
|
||||
}
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
|
||||
use anyhow::Error;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_network_config_create_lo_1() -> Result<(), Error> {
|
||||
let input = "";
|
||||
|
||||
let mut parser = NetworkParser::new(input.as_bytes());
|
||||
|
||||
let config = parser.parse_interfaces(None)?;
|
||||
|
||||
let output = String::try_from(config)?;
|
||||
|
||||
let expected = "auto lo\niface lo inet loopback\n\n";
|
||||
assert_eq!(output, expected);
|
||||
|
||||
// run again using output as input
|
||||
let mut parser = NetworkParser::new(output.as_bytes());
|
||||
|
||||
let config = parser.parse_interfaces(None)?;
|
||||
|
||||
let output = String::try_from(config)?;
|
||||
|
||||
assert_eq!(output, expected);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_network_config_create_lo_2() -> Result<(), Error> {
|
||||
let input = "#c1\n\n#c2\n\niface test inet manual\n";
|
||||
|
||||
let mut parser = NetworkParser::new(input.as_bytes());
|
||||
|
||||
let config = parser.parse_interfaces(None)?;
|
||||
|
||||
let output = String::try_from(config)?;
|
||||
|
||||
// Note: loopback should be added in front of other interfaces
|
||||
let expected = "#c1\n#c2\n\nauto lo\niface lo inet loopback\n\niface test inet manual\n\n";
|
||||
assert_eq!(output, expected);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_network_config_parser_no_blank_1() -> Result<(), Error> {
|
||||
let input = "auto lo\n\
|
||||
iface lo inet loopback\n\
|
||||
iface lo inet6 loopback\n\
|
||||
auto ens18\n\
|
||||
iface ens18 inet static\n\
|
||||
\taddress 192.168.20.144/20\n\
|
||||
\tgateway 192.168.16.1\n\
|
||||
# comment\n\
|
||||
iface ens20 inet static\n\
|
||||
\taddress 192.168.20.145/20\n\
|
||||
iface ens21 inet manual\n\
|
||||
iface ens22 inet manual\n";
|
||||
|
||||
let mut parser = NetworkParser::new(input.as_bytes());
|
||||
|
||||
let config = parser.parse_interfaces(None)?;
|
||||
|
||||
let output = String::try_from(config)?;
|
||||
|
||||
let expected = "auto lo\n\
|
||||
iface lo inet loopback\n\
|
||||
\n\
|
||||
iface lo inet6 loopback\n\
|
||||
\n\
|
||||
auto ens18\n\
|
||||
iface ens18 inet static\n\
|
||||
\taddress 192.168.20.144/20\n\
|
||||
\tgateway 192.168.16.1\n\
|
||||
#comment\n\
|
||||
\n\
|
||||
iface ens20 inet static\n\
|
||||
\taddress 192.168.20.145/20\n\
|
||||
\n\
|
||||
iface ens21 inet manual\n\
|
||||
\n\
|
||||
iface ens22 inet manual\n\
|
||||
\n";
|
||||
assert_eq!(output, expected);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_network_config_parser_no_blank_2() -> Result<(), Error> {
|
||||
// Adapted from bug 2926
|
||||
let input = "### Hetzner Online GmbH installimage\n\
|
||||
\n\
|
||||
source /etc/network/interfaces.d/*\n\
|
||||
\n\
|
||||
auto lo\n\
|
||||
iface lo inet loopback\n\
|
||||
iface lo inet6 loopback\n\
|
||||
\n\
|
||||
auto enp4s0\n\
|
||||
iface enp4s0 inet static\n\
|
||||
\taddress 10.10.10.10/24\n\
|
||||
\tgateway 10.10.10.1\n\
|
||||
\t# route 10.10.20.10/24 via 10.10.20.1\n\
|
||||
\tup route add -net 10.10.20.10 netmask 255.255.255.0 gw 10.10.20.1 dev enp4s0\n\
|
||||
\n\
|
||||
iface enp4s0 inet6 static\n\
|
||||
\taddress fe80::5496:35ff:fe99:5a6a/64\n\
|
||||
\tgateway fe80::1\n";
|
||||
|
||||
let mut parser = NetworkParser::new(input.as_bytes());
|
||||
|
||||
let config = parser.parse_interfaces(None)?;
|
||||
|
||||
let output = String::try_from(config)?;
|
||||
|
||||
let expected = "### Hetzner Online GmbH installimage\n\
|
||||
\n\
|
||||
source /etc/network/interfaces.d/*\n\
|
||||
\n\
|
||||
auto lo\n\
|
||||
iface lo inet loopback\n\
|
||||
\n\
|
||||
iface lo inet6 loopback\n\
|
||||
\n\
|
||||
auto enp4s0\n\
|
||||
iface enp4s0 inet static\n\
|
||||
\taddress 10.10.10.10/24\n\
|
||||
\tgateway 10.10.10.1\n\
|
||||
\t# route 10.10.20.10/24 via 10.10.20.1\n\
|
||||
\tup route add -net 10.10.20.10 netmask 255.255.255.0 gw 10.10.20.1 dev enp4s0\n\
|
||||
\n\
|
||||
iface enp4s0 inet6 static\n\
|
||||
\taddress fe80::5496:35ff:fe99:5a6a/64\n\
|
||||
\tgateway fe80::1\n\
|
||||
\n";
|
||||
assert_eq!(output, expected);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_network_config_parser_vlan_id_in_name() {
|
||||
let input = "iface vmbr0.100 inet static manual";
|
||||
let mut parser = NetworkParser::new(input.as_bytes());
|
||||
let config = parser.parse_interfaces(None).unwrap();
|
||||
|
||||
let iface = config.interfaces.get("vmbr0.100").unwrap();
|
||||
assert_eq!(iface.interface_type, NetworkInterfaceType::Vlan);
|
||||
assert_eq!(iface.vlan_raw_device, None);
|
||||
assert_eq!(iface.vlan_id, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_network_config_parser_vlan_with_raw_device() {
|
||||
let input = r#"
|
||||
iface vlan100 inet manual
|
||||
vlan-raw-device vmbr0"#;
|
||||
|
||||
let mut parser = NetworkParser::new(input.as_bytes());
|
||||
let config = parser.parse_interfaces(None).unwrap();
|
||||
|
||||
let iface = config.interfaces.get("vlan100").unwrap();
|
||||
assert_eq!(iface.interface_type, NetworkInterfaceType::Vlan);
|
||||
assert_eq!(iface.vlan_raw_device, Some(String::from("vmbr0")));
|
||||
assert_eq!(iface.vlan_id, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_network_config_parser_vlan_with_raw_device_static() {
|
||||
let input = r#"
|
||||
iface vlan100 inet static
|
||||
vlan-raw-device vmbr0
|
||||
address 10.0.0.100/16"#;
|
||||
|
||||
let mut parser = NetworkParser::new(input.as_bytes());
|
||||
let config = parser.parse_interfaces(None).unwrap();
|
||||
|
||||
let iface = config.interfaces.get("vlan100").unwrap();
|
||||
assert_eq!(iface.interface_type, NetworkInterfaceType::Vlan);
|
||||
assert_eq!(iface.vlan_raw_device, Some(String::from("vmbr0")));
|
||||
assert_eq!(iface.vlan_id, None);
|
||||
assert_eq!(iface.method, Some(NetworkConfigMethod::Static));
|
||||
assert_eq!(iface.cidr, Some(String::from("10.0.0.100/16")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_network_config_parser_vlan_individual_name() {
|
||||
let input = r#"
|
||||
iface individual_name inet manual
|
||||
vlan-id 100
|
||||
vlan-raw-device vmbr0"#;
|
||||
|
||||
let mut parser = NetworkParser::new(input.as_bytes());
|
||||
let config = parser.parse_interfaces(None).unwrap();
|
||||
|
||||
let iface = config.interfaces.get("individual_name").unwrap();
|
||||
assert_eq!(iface.interface_type, NetworkInterfaceType::Vlan);
|
||||
assert_eq!(iface.vlan_raw_device, Some(String::from("vmbr0")));
|
||||
assert_eq!(iface.vlan_id, Some(100));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_network_config_parser_vlan_individual_name_static() {
|
||||
let input = r#"
|
||||
iface individual_name inet static
|
||||
vlan-id 100
|
||||
vlan-raw-device vmbr0
|
||||
address 10.0.0.100/16
|
||||
"#;
|
||||
|
||||
let mut parser = NetworkParser::new(input.as_bytes());
|
||||
let config = parser.parse_interfaces(None).unwrap();
|
||||
|
||||
let iface = config.interfaces.get("individual_name").unwrap();
|
||||
assert_eq!(iface.interface_type, NetworkInterfaceType::Vlan);
|
||||
assert_eq!(iface.vlan_raw_device, Some(String::from("vmbr0")));
|
||||
assert_eq!(iface.vlan_id, Some(100));
|
||||
assert_eq!(iface.method, Some(NetworkConfigMethod::Static));
|
||||
assert_eq!(iface.cidr, Some(String::from("10.0.0.100/16")));
|
||||
}
|
||||
}
|
||||
@ -1,55 +0,0 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use anyhow::{bail, Error};
|
||||
use openssl::ssl::{SslAcceptor, SslMethod};
|
||||
|
||||
use pbs_api_types::NodeConfig;
|
||||
use proxmox_http::ProxyConfig;
|
||||
use proxmox_schema::ApiType;
|
||||
|
||||
use pbs_buildcfg::configdir;
|
||||
|
||||
use crate::{open_backup_lockfile, BackupLockGuard};
|
||||
|
||||
const CONF_FILE: &str = configdir!("/node.cfg");
|
||||
const LOCK_FILE: &str = configdir!("/.node.lck");
|
||||
|
||||
pub fn lock() -> Result<BackupLockGuard, Error> {
|
||||
open_backup_lockfile(LOCK_FILE, None, true)
|
||||
}
|
||||
|
||||
/// Read the Node Config.
|
||||
pub fn config() -> Result<(NodeConfig, [u8; 32]), Error> {
|
||||
let content = proxmox_sys::fs::file_read_optional_string(CONF_FILE)?.unwrap_or_default();
|
||||
|
||||
let digest = openssl::sha::sha256(content.as_bytes());
|
||||
let data: NodeConfig = crate::key_value::from_str(&content, &NodeConfig::API_SCHEMA)?;
|
||||
|
||||
Ok((data, digest))
|
||||
}
|
||||
|
||||
/// Write the Node Config, requires the write lock to be held.
|
||||
pub fn save_config(config: &NodeConfig) -> Result<(), Error> {
|
||||
let mut domains = HashSet::new();
|
||||
for domain in config.acme_domains() {
|
||||
let domain = domain?;
|
||||
if !domains.insert(domain.domain.to_lowercase()) {
|
||||
bail!("duplicate domain '{}' in ACME config", domain.domain);
|
||||
}
|
||||
}
|
||||
let mut dummy_acceptor = SslAcceptor::mozilla_intermediate_v5(SslMethod::tls()).unwrap();
|
||||
if let Some(ciphers) = config.ciphers_tls_1_3.as_deref() {
|
||||
dummy_acceptor.set_ciphersuites(ciphers)?;
|
||||
}
|
||||
if let Some(ciphers) = config.ciphers_tls_1_2.as_deref() {
|
||||
dummy_acceptor.set_cipher_list(ciphers)?;
|
||||
}
|
||||
|
||||
let raw = crate::key_value::to_bytes(config, &NodeConfig::API_SCHEMA)?;
|
||||
crate::replace_backup_config(CONF_FILE, &raw)
|
||||
}
|
||||
|
||||
pub fn node_http_proxy_config() -> Result<Option<ProxyConfig>, Error> {
|
||||
let (node_config, _digest) = self::config()?;
|
||||
Ok(node_config.http_proxy())
|
||||
}
|
||||
@ -1,68 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use anyhow::Error;
|
||||
|
||||
use proxmox_s3_client::{S3ClientConf, S3_CLIENT_ID_SCHEMA};
|
||||
use proxmox_schema::*;
|
||||
use proxmox_section_config::{SectionConfig, SectionConfigData, SectionConfigPlugin};
|
||||
|
||||
use pbs_buildcfg::configdir;
|
||||
|
||||
use crate::{open_backup_lockfile, replace_backup_config, BackupLockGuard};
|
||||
|
||||
pub static CONFIG: LazyLock<SectionConfig> = LazyLock::new(init);
|
||||
|
||||
fn init() -> SectionConfig {
|
||||
let obj_schema = S3ClientConf::API_SCHEMA.unwrap_all_of_schema();
|
||||
let plugin = SectionConfigPlugin::new(
|
||||
"s3-endpoint".to_string(),
|
||||
Some(String::from("id")),
|
||||
obj_schema,
|
||||
);
|
||||
let mut config = SectionConfig::new(&S3_CLIENT_ID_SCHEMA);
|
||||
config.register_plugin(plugin);
|
||||
|
||||
config
|
||||
}
|
||||
|
||||
/// Configuration file location for S3 client.
|
||||
pub const S3_CFG_FILENAME: &str = configdir!("/s3.cfg");
|
||||
/// Configuration lock file used to prevent concurrent configuration update operations.
|
||||
pub const S3_CFG_LOCKFILE: &str = configdir!("/.s3.lck");
|
||||
|
||||
/// Config type for s3 client config entries
|
||||
pub const S3_CFG_TYPE_ID: &str = "s3-endpoint";
|
||||
|
||||
/// Get exclusive lock for S3 client configuration update.
|
||||
pub fn lock_config() -> Result<BackupLockGuard, Error> {
|
||||
open_backup_lockfile(S3_CFG_LOCKFILE, None, true)
|
||||
}
|
||||
|
||||
/// Load s3 client configuration from file.
|
||||
pub fn config() -> Result<(SectionConfigData, [u8; 32]), Error> {
|
||||
parse_config(S3_CFG_FILENAME)
|
||||
}
|
||||
|
||||
/// Save given s3 client configuration to file.
|
||||
pub fn save_config(config: &SectionConfigData) -> Result<(), Error> {
|
||||
let raw = CONFIG.write(S3_CFG_FILENAME, config)?;
|
||||
replace_backup_config(S3_CFG_FILENAME, raw.as_bytes())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Shell completion helper to complete s3 client id's as found in the config.
|
||||
pub fn complete_s3_client_id(_arg: &str, _param: &HashMap<String, String>) -> Vec<String> {
|
||||
match config() {
|
||||
Ok((data, _digest)) => data.sections.keys().map(|id| id.to_string()).collect(),
|
||||
Err(_) => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_config(path: &str) -> Result<(SectionConfigData, [u8; 32]), Error> {
|
||||
let content = proxmox_sys::fs::file_read_optional_string(path)?;
|
||||
let content = content.unwrap_or_default();
|
||||
let digest = openssl::sha::sha256(content.as_bytes());
|
||||
let data = CONFIG.parse(path, &content)?;
|
||||
Ok((data, digest))
|
||||
}
|
||||
@ -1,16 +1,10 @@
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::io::ErrorKind;
|
||||
use std::sync::LazyLock;
|
||||
use std::time::SystemTime;
|
||||
|
||||
use anyhow::{bail, format_err, Error};
|
||||
use parking_lot::RwLock;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{from_value, Value};
|
||||
|
||||
use proxmox_sys::fs::CreateOptions;
|
||||
use proxmox_time::epoch_i64;
|
||||
|
||||
use pbs_api_types::Authid;
|
||||
//use crate::auth;
|
||||
@ -19,21 +13,6 @@ use crate::{open_backup_lockfile, BackupLockGuard};
|
||||
const LOCK_FILE: &str = pbs_buildcfg::configdir!("/token.shadow.lock");
|
||||
const CONF_FILE: &str = pbs_buildcfg::configdir!("/token.shadow");
|
||||
|
||||
/// Global in-memory cache for successfully verified API token secrets.
|
||||
/// The cache stores plain text secrets for token Authids that have already been
|
||||
/// verified against the hashed values in `token.shadow`. This allows for cheap
|
||||
/// subsequent authentications for the same token+secret combination, avoiding
|
||||
/// recomputing the password hash on every request.
|
||||
static TOKEN_SECRET_CACHE: LazyLock<RwLock<ApiTokenSecretCache>> = LazyLock::new(|| {
|
||||
RwLock::new(ApiTokenSecretCache {
|
||||
secrets: HashMap::new(),
|
||||
cached_gen: 0,
|
||||
file_info: None,
|
||||
})
|
||||
});
|
||||
/// Max age in seconds of the token secret cache before checking for file changes.
|
||||
const TOKEN_SECRET_CACHE_TTL_SECS: i64 = 60;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
/// ApiToken id / secret pair
|
||||
@ -69,111 +48,15 @@ fn write_file(data: HashMap<Authid, String>) -> Result<(), Error> {
|
||||
proxmox_sys::fs::replace_file(CONF_FILE, &json, options, true)
|
||||
}
|
||||
|
||||
/// Tries to match the given token secret against the cached secret.
|
||||
///
|
||||
/// Verifies the generation/version before doing the constant-time
|
||||
/// comparison to reduce TOCTOU risk. During token rotation or deletion
|
||||
/// tokens for in-flight requests may still validate against the previous
|
||||
/// generation.
|
||||
///
|
||||
/// If the cache file metadata's TTL has expired, will revalidate and invalidate the cache if
|
||||
/// needed.
|
||||
///
|
||||
/// Returns true if secret is cached and cache is still valid
|
||||
fn cached_secret_valid(tokenid: &Authid, secret: &str) -> bool {
|
||||
let now = epoch_i64();
|
||||
|
||||
// Fast path: cache is fresh if generation matches and TTL not expired.
|
||||
if let (Some(cache), Some(read_gen)) =
|
||||
(TOKEN_SECRET_CACHE.try_read(), token_shadow_generation())
|
||||
{
|
||||
if cache.cached_gen == read_gen && cache.shadow_check_within_ttl(now) {
|
||||
return cache.secret_matches(tokenid, secret);
|
||||
}
|
||||
// read lock drops here
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Slow path: best-effort refresh under write lock.
|
||||
let Some(mut cache) = TOKEN_SECRET_CACHE.try_write() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
// Re-read generation after acquiring the lock (may have changed meanwhile).
|
||||
let Some(current_gen) = token_shadow_generation() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
// If another process bumped the generation, we don't know what changed -> clear cache
|
||||
if cache.cached_gen != current_gen {
|
||||
cache.reset_and_set_gen(current_gen);
|
||||
}
|
||||
|
||||
// TTL check again after acquiring the lock
|
||||
let now = epoch_i64();
|
||||
if cache.shadow_check_within_ttl(now) {
|
||||
return cache.secret_matches(tokenid, secret);
|
||||
}
|
||||
|
||||
// Stat the file to detect manual edits.
|
||||
let Ok((new_mtime, new_len)) = shadow_mtime_len() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
// If the file didn't change, only update last_checked
|
||||
if let Some(shadow) = cache.file_info.as_mut() {
|
||||
if shadow.mtime == new_mtime && shadow.len == new_len {
|
||||
shadow.last_checked = now;
|
||||
return cache.secret_matches(tokenid, secret);
|
||||
}
|
||||
}
|
||||
|
||||
cache.secrets.clear();
|
||||
|
||||
let prev = cache.file_info.replace(ShadowFileInfo {
|
||||
mtime: new_mtime,
|
||||
len: new_len,
|
||||
last_checked: now,
|
||||
});
|
||||
|
||||
if prev.is_some() {
|
||||
// Best-effort propagation to other processes if a change was detected
|
||||
if let Some(new_gen) = bump_token_shadow_generation() {
|
||||
cache.cached_gen = new_gen;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
/// Verifies that an entry for given tokenid / API token secret exists
|
||||
pub fn verify_secret(tokenid: &Authid, secret: &str) -> Result<(), Error> {
|
||||
if !tokenid.is_token() {
|
||||
bail!("not an API token ID");
|
||||
}
|
||||
|
||||
// Fast path
|
||||
if cached_secret_valid(tokenid, secret) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Slow path
|
||||
// First, capture the generation before doing the hash verification.
|
||||
let gen_before = token_shadow_generation();
|
||||
|
||||
let data = read_file()?;
|
||||
match data.get(tokenid) {
|
||||
Some(hashed_secret) => {
|
||||
proxmox_sys::crypt::verify_crypt_pw(secret, hashed_secret)?;
|
||||
|
||||
// Try to cache only if nothing changed while verifying the secret.
|
||||
if let Some(gen_before) = gen_before {
|
||||
cache_try_insert_secret(tokenid.clone(), secret.to_owned(), gen_before);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Some(hashed_secret) => proxmox_sys::crypt::verify_crypt_pw(secret, hashed_secret),
|
||||
None => bail!("invalid API token"),
|
||||
}
|
||||
}
|
||||
@ -181,203 +64,38 @@ pub fn verify_secret(tokenid: &Authid, secret: &str) -> Result<(), Error> {
|
||||
/// Generates a new secret for the given tokenid / API token, sets it then returns it.
|
||||
/// The secret is stored as salted hash.
|
||||
pub fn generate_and_set_secret(tokenid: &Authid) -> Result<String, Error> {
|
||||
apply_api_mutation(tokenid, true)?
|
||||
.ok_or_else(|| format_err!("Failed to generate API token secret"))
|
||||
let secret = format!("{:x}", proxmox_uuid::Uuid::generate());
|
||||
set_secret(tokenid, &secret)?;
|
||||
Ok(secret)
|
||||
}
|
||||
|
||||
/// Deletes the entry for the given tokenid.
|
||||
pub fn delete_secret(tokenid: &Authid) -> Result<(), Error> {
|
||||
apply_api_mutation(tokenid, false)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Cached secret.
|
||||
struct CachedSecret {
|
||||
secret: String,
|
||||
}
|
||||
|
||||
struct ApiTokenSecretCache {
|
||||
/// Keys are token Authids, values are the corresponding plain text secrets.
|
||||
/// Entries are added after a successful on-disk verification in
|
||||
/// `verify_secret` or when a new token secret is generated by
|
||||
/// `generate_and_set_secret`. Used to avoid repeated
|
||||
/// password-hash computation on subsequent authentications.
|
||||
secrets: HashMap<Authid, CachedSecret>,
|
||||
/// token.shadow generation of cached secrets.
|
||||
cached_gen: usize,
|
||||
/// Shadow file info to detect changes
|
||||
file_info: Option<ShadowFileInfo>,
|
||||
}
|
||||
|
||||
impl ApiTokenSecretCache {
|
||||
/// Resets all local cache contents and sets/updates the cached generation.
|
||||
fn reset_and_set_gen(&mut self, new_gen: usize) {
|
||||
self.secrets.clear();
|
||||
self.cached_gen = new_gen;
|
||||
self.file_info = None;
|
||||
}
|
||||
|
||||
/// Caches a secret and sets/updates the cache generation.
|
||||
fn insert_and_set_gen(&mut self, tokenid: Authid, secret: CachedSecret, new_gen: usize) {
|
||||
self.secrets.insert(tokenid, secret);
|
||||
self.cached_gen = new_gen;
|
||||
}
|
||||
|
||||
/// Evicts a cached secret and sets/updates the cached generation.
|
||||
fn evict_and_set_gen(&mut self, tokenid: &Authid, new_gen: usize) {
|
||||
self.secrets.remove(tokenid);
|
||||
self.cached_gen = new_gen;
|
||||
}
|
||||
|
||||
/// Returns true if cached token.shadow metadata exists and was checked within the TTL window.
|
||||
fn shadow_check_within_ttl(&self, now: i64) -> bool {
|
||||
self.file_info.as_ref().is_some_and(|cached| {
|
||||
now >= cached.last_checked && (now - cached.last_checked) < TOKEN_SECRET_CACHE_TTL_SECS
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns true if there is a matching cached entry
|
||||
fn secret_matches(&self, tokenid: &Authid, secret: &str) -> bool {
|
||||
let Some(entry) = self.secrets.get(tokenid) else {
|
||||
return false;
|
||||
};
|
||||
let cached_secret_bytes = entry.secret.as_bytes();
|
||||
let secret_bytes = secret.as_bytes();
|
||||
|
||||
cached_secret_bytes.len() == secret_bytes.len()
|
||||
&& openssl::memcmp::eq(cached_secret_bytes, secret_bytes)
|
||||
}
|
||||
}
|
||||
|
||||
/// Shadow file info
|
||||
struct ShadowFileInfo {
|
||||
// shadow file mtime to detect changes
|
||||
mtime: Option<SystemTime>,
|
||||
// shadow file length to detect changes
|
||||
len: Option<u64>,
|
||||
// last time the file metadata was checked
|
||||
last_checked: i64,
|
||||
}
|
||||
|
||||
fn cache_try_insert_secret(tokenid: Authid, secret: String, gen_before: usize) {
|
||||
let Some(mut cache) = TOKEN_SECRET_CACHE.try_write() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(gen_now) = token_shadow_generation() else {
|
||||
return;
|
||||
};
|
||||
|
||||
// If this process missed a generation bump, its cache is stale.
|
||||
if cache.cached_gen != gen_now {
|
||||
cache.reset_and_set_gen(gen_now);
|
||||
}
|
||||
|
||||
// If a mutation happened while we were verifying the secret, do not insert.
|
||||
if gen_now == gen_before {
|
||||
cache.insert_and_set_gen(tokenid, CachedSecret { secret }, gen_now);
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_api_mutation(tokenid: &Authid, generate: bool) -> Result<Option<String>, Error> {
|
||||
/// Adds a new entry for the given tokenid / API token secret. The secret is stored as salted hash.
|
||||
fn set_secret(tokenid: &Authid, secret: &str) -> Result<(), Error> {
|
||||
if !tokenid.is_token() {
|
||||
bail!("not an API token ID");
|
||||
}
|
||||
|
||||
let _guard = lock_config()?;
|
||||
|
||||
// Capture state before we write to detect external edits.
|
||||
let pre_write_meta = shadow_mtime_len().unwrap_or((None, None));
|
||||
|
||||
let mut data = read_file()?;
|
||||
let secret = if generate {
|
||||
let secret = format!("{:x}", proxmox_uuid::Uuid::generate());
|
||||
let hashed_secret = proxmox_sys::crypt::encrypt_pw(&secret)?;
|
||||
data.insert(tokenid.clone(), hashed_secret);
|
||||
Some(secret)
|
||||
} else {
|
||||
data.remove(tokenid);
|
||||
None
|
||||
};
|
||||
let hashed_secret = proxmox_sys::crypt::encrypt_pw(secret)?;
|
||||
data.insert(tokenid.clone(), hashed_secret);
|
||||
write_file(data)?;
|
||||
|
||||
let now = epoch_i64();
|
||||
|
||||
// Signal cache invalidation to other processes (best-effort).
|
||||
let bumped_gen = bump_token_shadow_generation();
|
||||
let mut cache = TOKEN_SECRET_CACHE.write();
|
||||
|
||||
// If we cannot get the current generation, we cannot trust the cache
|
||||
let Some(current_gen) = token_shadow_generation() else {
|
||||
cache.reset_and_set_gen(0);
|
||||
return Ok(secret);
|
||||
};
|
||||
|
||||
// If we cannot bump the generation, or if it changed after
|
||||
// obtaining the cache write lock, we cannot trust the cache
|
||||
if bumped_gen != Some(current_gen) {
|
||||
cache.reset_and_set_gen(current_gen);
|
||||
return Ok(secret);
|
||||
}
|
||||
|
||||
// If our cached file metadata does not match the on-disk state before our write,
|
||||
// we likely missed an external/manual edit. We can no longer trust any cached secrets.
|
||||
if cache
|
||||
.file_info
|
||||
.as_ref()
|
||||
.is_some_and(|s| (s.mtime, s.len) != pre_write_meta)
|
||||
{
|
||||
cache.secrets.clear();
|
||||
}
|
||||
|
||||
// Apply the new mutation.
|
||||
match &secret {
|
||||
Some(secret) => {
|
||||
let cached_secret = CachedSecret {
|
||||
secret: secret.to_owned(),
|
||||
};
|
||||
cache.insert_and_set_gen(tokenid.clone(), cached_secret, current_gen);
|
||||
}
|
||||
None => cache.evict_and_set_gen(tokenid, current_gen),
|
||||
}
|
||||
|
||||
// Update our view of the file metadata to the post-write state (best-effort).
|
||||
// (If this fails, drop local cache so callers fall back to slow path until refreshed.)
|
||||
match shadow_mtime_len() {
|
||||
Ok((mtime, len)) => {
|
||||
cache.file_info = Some(ShadowFileInfo {
|
||||
mtime,
|
||||
len,
|
||||
last_checked: now,
|
||||
});
|
||||
}
|
||||
Err(_) => {
|
||||
// If we cannot validate state, do not trust cache.
|
||||
cache.reset_and_set_gen(current_gen);
|
||||
}
|
||||
}
|
||||
Ok(secret)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the current generation.
|
||||
fn token_shadow_generation() -> Option<usize> {
|
||||
crate::ConfigVersionCache::new()
|
||||
.ok()
|
||||
.map(|cvc| cvc.token_shadow_generation())
|
||||
}
|
||||
|
||||
/// Bump and return the new generation.
|
||||
fn bump_token_shadow_generation() -> Option<usize> {
|
||||
crate::ConfigVersionCache::new()
|
||||
.ok()
|
||||
.map(|cvc| cvc.increase_token_shadow_generation() + 1)
|
||||
}
|
||||
|
||||
fn shadow_mtime_len() -> Result<(Option<SystemTime>, Option<u64>), Error> {
|
||||
match fs::metadata(CONF_FILE) {
|
||||
Ok(meta) => Ok((meta.modified().ok(), Some(meta.len()))),
|
||||
Err(e) if e.kind() == ErrorKind::NotFound => Ok((None, None)),
|
||||
Err(e) => Err(e.into()),
|
||||
/// Deletes the entry for the given tokenid.
|
||||
pub fn delete_secret(tokenid: &Authid) -> Result<(), Error> {
|
||||
if !tokenid.is_token() {
|
||||
bail!("not an API token ID");
|
||||
}
|
||||
|
||||
let _guard = lock_config()?;
|
||||
|
||||
let mut data = read_file()?;
|
||||
data.remove(tokenid);
|
||||
write_file(data)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -85,7 +85,7 @@ mod test {
|
||||
timeframe fri 9:00-12:00
|
||||
";
|
||||
let data = CONFIG.parse(TRAFFIC_CONTROL_CFG_FILENAME, content)?;
|
||||
eprintln!("GOT {data:?}");
|
||||
eprintln!("GOT {:?}", data);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -53,9 +53,8 @@ pub fn config() -> Result<(SectionConfigData, [u8; 32]), Error> {
|
||||
|
||||
let digest = openssl::sha::sha256(content.as_bytes());
|
||||
let mut data = CONFIG.parse(USER_CFG_FILENAME, &content)?;
|
||||
let root_user = Userid::root_userid().as_str();
|
||||
|
||||
if !data.sections.contains_key(root_user) {
|
||||
if !data.sections.contains_key("root@pam") {
|
||||
let user: User = User {
|
||||
userid: Userid::root_userid().clone(),
|
||||
comment: Some("Superuser".to_string()),
|
||||
@ -65,7 +64,7 @@ pub fn config() -> Result<(SectionConfigData, [u8; 32]), Error> {
|
||||
lastname: None,
|
||||
email: None,
|
||||
};
|
||||
data.set_data(root_user, "user", &user).unwrap();
|
||||
data.set_data("root@pam", "user", &user).unwrap();
|
||||
}
|
||||
|
||||
Ok((data, digest))
|
||||
|
||||
@ -8,13 +8,12 @@ rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
base64.workspace = true
|
||||
const_format.workspace = true
|
||||
crc32fast.workspace = true
|
||||
endian_trait.workspace = true
|
||||
futures.workspace = true
|
||||
hex = { workspace = true, features = [ "serde" ] }
|
||||
http-body-util.workspace = true
|
||||
hyper.workspace = true
|
||||
libc.workspace = true
|
||||
log.workspace = true
|
||||
nix.workspace = true
|
||||
@ -31,16 +30,11 @@ zstd-safe.workspace = true
|
||||
pathpatterns.workspace = true
|
||||
pxar.workspace = true
|
||||
|
||||
proxmox-async.workspace = true
|
||||
proxmox-base64.workspace = true
|
||||
proxmox-borrow.workspace = true
|
||||
proxmox-http.workspace = true
|
||||
proxmox-human-byte.workspace = true
|
||||
proxmox-io.workspace = true
|
||||
proxmox-lang.workspace=true
|
||||
proxmox-s3-client = { workspace = true, features = [ "impl" ] }
|
||||
proxmox-schema = { workspace = true, features = [ "api-macro" ] }
|
||||
proxmox-section-config.workspace = true
|
||||
proxmox-serde = { workspace = true, features = [ "serde_json" ] }
|
||||
proxmox-sys.workspace = true
|
||||
proxmox-systemd.workspace = true
|
||||
@ -53,6 +47,3 @@ pbs-buildcfg.workspace = true
|
||||
pbs-config.workspace = true
|
||||
pbs-key-config.workspace = true
|
||||
pbs-tools.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile.workspace = true
|
||||
|
||||
@ -21,7 +21,7 @@ fn run() -> Result<(), Error> {
|
||||
let store = unsafe { DataStore::open_path("", base, None)? };
|
||||
|
||||
for ns in store.recursive_iter_backup_ns_ok(Default::default(), max_depth)? {
|
||||
println!("found namespace store:/{ns}");
|
||||
println!("found namespace store:/{}", ns);
|
||||
|
||||
for group in store.iter_backup_groups(ns)? {
|
||||
let group = group?;
|
||||
@ -41,7 +41,7 @@ fn main() {
|
||||
std::process::exit(match run() {
|
||||
Ok(_) => 0,
|
||||
Err(err) => {
|
||||
eprintln!("error: {err}");
|
||||
eprintln!("error: {}", err);
|
||||
1
|
||||
}
|
||||
});
|
||||
|
||||
@ -8,26 +8,21 @@ use std::time::Duration;
|
||||
|
||||
use anyhow::{bail, format_err, Context, Error};
|
||||
use const_format::concatcp;
|
||||
use tracing::info;
|
||||
|
||||
use proxmox_s3_client::{S3ObjectKey, S3PathPrefix};
|
||||
use proxmox_sys::fs::{lock_dir_noblock, lock_dir_noblock_shared, replace_file, CreateOptions};
|
||||
use proxmox_systemd::escape_unit;
|
||||
|
||||
use pbs_api_types::{
|
||||
ArchiveType, Authid, BackupGroupDeleteStats, BackupNamespace, BackupType, GroupFilter,
|
||||
VerifyState, BACKUP_DATE_REGEX, CLIENT_LOG_BLOB_NAME, MANIFEST_BLOB_NAME,
|
||||
Authid, BackupGroupDeleteStats, BackupNamespace, BackupType, GroupFilter, VerifyState,
|
||||
BACKUP_DATE_REGEX, CLIENT_LOG_BLOB_NAME, MANIFEST_BLOB_NAME,
|
||||
};
|
||||
use pbs_config::{open_backup_lockfile, BackupLockGuard};
|
||||
|
||||
use crate::datastore::{GROUP_NOTES_FILE_NAME, GROUP_OWNER_FILE_NAME};
|
||||
use crate::manifest::{BackupManifest, MANIFEST_LOCK_NAME};
|
||||
use crate::move_journal;
|
||||
use crate::s3::S3_CONTENT_PREFIX;
|
||||
use crate::{DataBlob, DataStore, DatastoreBackend};
|
||||
use crate::{DataBlob, DataStore};
|
||||
|
||||
pub const DATASTORE_LOCKS_DIR: &str = "/run/proxmox-backup/locks";
|
||||
pub const PROTECTED_MARKER_FILENAME: &str = ".protected";
|
||||
const PROTECTED_MARKER_FILENAME: &str = ".protected";
|
||||
|
||||
proxmox_schema::const_regex! {
|
||||
pub BACKUP_FILES_AND_PROTECTED_REGEX = concatcp!(r"^(.*\.([fd]idx|blob)|\", PROTECTED_MARKER_FILENAME, ")$");
|
||||
@ -166,7 +161,7 @@ impl BackupGroup {
|
||||
|
||||
use nix::fcntl::{openat, OFlag};
|
||||
match openat(
|
||||
Some(l2_fd),
|
||||
l2_fd,
|
||||
&manifest_path,
|
||||
OFlag::O_RDONLY | OFlag::O_CLOEXEC,
|
||||
nix::sys::stat::Mode::empty(),
|
||||
@ -219,18 +214,14 @@ impl BackupGroup {
|
||||
crate::ListSnapshots::new(self.clone())
|
||||
}
|
||||
|
||||
/// Destroy the group inclusive all its backup snapshots (BackupDir's).
|
||||
///
|
||||
/// Consumes the group lock. The caller is responsible for acquiring it via
|
||||
/// [`Self::lock`] beforehand.
|
||||
/// Destroy the group inclusive all its backup snapshots (BackupDir's)
|
||||
///
|
||||
/// Returns `BackupGroupDeleteStats`, containing the number of deleted snapshots
|
||||
/// and number of protected snaphsots, which therefore were not removed.
|
||||
pub(crate) fn destroy(
|
||||
&self,
|
||||
_lock_guard: BackupLockGuard,
|
||||
backend: &DatastoreBackend,
|
||||
) -> Result<BackupGroupDeleteStats, Error> {
|
||||
pub fn destroy(&self) -> Result<BackupGroupDeleteStats, Error> {
|
||||
let _guard = self
|
||||
.lock()
|
||||
.with_context(|| format!("while destroying group '{self:?}'"))?;
|
||||
let path = self.full_group_path();
|
||||
|
||||
log::info!("removing backup group {:?}", path);
|
||||
@ -241,31 +232,10 @@ impl BackupGroup {
|
||||
delete_stats.increment_protected_snapshots();
|
||||
continue;
|
||||
}
|
||||
// also for S3 cleanup local only, the actual S3 objects will be removed below,
|
||||
// reducing the number of required API calls.
|
||||
snap.destroy(false, &DatastoreBackend::Filesystem)?;
|
||||
snap.destroy(false)?;
|
||||
delete_stats.increment_removed_snapshots();
|
||||
}
|
||||
|
||||
if let DatastoreBackend::S3(s3_client) = backend {
|
||||
let path = self.relative_group_path();
|
||||
let group_prefix = path
|
||||
.to_str()
|
||||
.ok_or_else(|| format_err!("invalid group path prefix"))?;
|
||||
let prefix = format!("{S3_CONTENT_PREFIX}/{group_prefix}");
|
||||
let delete_objects_errors = proxmox_async::runtime::block_on(
|
||||
s3_client.delete_objects_by_prefix_with_suffix_filter(
|
||||
&S3PathPrefix::Some(prefix),
|
||||
PROTECTED_MARKER_FILENAME,
|
||||
&[GROUP_OWNER_FILE_NAME, GROUP_NOTES_FILE_NAME],
|
||||
),
|
||||
)?;
|
||||
if !delete_objects_errors.is_empty() {
|
||||
crate::s3::log_s3_delete_objects_errors(&delete_objects_errors);
|
||||
bail!("deleting objects failed");
|
||||
}
|
||||
}
|
||||
|
||||
// Note: make sure the old locking mechanism isn't used as `remove_dir_all` is not safe in
|
||||
// that case
|
||||
if delete_stats.all_removed() && !*OLD_LOCKING {
|
||||
@ -276,132 +246,6 @@ impl BackupGroup {
|
||||
Ok(delete_stats)
|
||||
}
|
||||
|
||||
/// Check merge invariants for moving this group's snapshots into `target`.
|
||||
/// Returns an error if ownership differs or snapshot times mismatch.
|
||||
pub(crate) fn check_merge_invariants(&self, target: &BackupGroup) -> Result<(), Error> {
|
||||
let src_owner = self.get_owner()?;
|
||||
let tgt_owner = target.get_owner()?;
|
||||
if src_owner != tgt_owner {
|
||||
bail!(
|
||||
"cannot merge group '{}/{}' from '{}' into '{}': owner mismatch \
|
||||
(source: {src_owner}, target: {tgt_owner})",
|
||||
self.group.ty,
|
||||
self.group.id,
|
||||
self.ns,
|
||||
target.ns,
|
||||
);
|
||||
}
|
||||
|
||||
let (src_oldest, src_oldest_str) = self.iter_snapshots()?.filter_map(Result::ok).fold(
|
||||
(i64::MAX, String::new()),
|
||||
|(min, min_str), s| {
|
||||
let curr = s.backup_time();
|
||||
if curr < min {
|
||||
(curr, s.backup_time_string.clone())
|
||||
} else {
|
||||
(min, min_str)
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
if src_oldest != i64::MAX {
|
||||
// Any target snapshot with time >= src_oldest violates the
|
||||
// "source strictly newer than target" merge invariant. Short-circuit on the first hit.
|
||||
if let Some(overlap) = target
|
||||
.iter_snapshots()?
|
||||
.filter_map(Result::ok)
|
||||
.find_map(|s| {
|
||||
if s.backup_time() >= src_oldest {
|
||||
Some(s.backup_time_string().to_owned())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
{
|
||||
info!("oldest source snapshot: {src_oldest_str}");
|
||||
info!("conflicting target snapshot: {overlap}");
|
||||
bail!(
|
||||
"cannot merge group '{}/{}' from '{}' into '{}': snapshot time mismatch",
|
||||
self.group.ty,
|
||||
self.group.id,
|
||||
self.ns,
|
||||
target.ns,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Move the group notes file (if any) from this group to `target`. Caller must hold
|
||||
/// exclusive group locks on both.
|
||||
///
|
||||
/// Behavior:
|
||||
/// - If the source has no notes, do nothing.
|
||||
/// - If only the source has notes, upload them to the target's S3 notes object first
|
||||
/// (when the backend is S3), then rename the local file. A failure in either step
|
||||
/// aborts the move so the source is left intact for the caller to retry; the S3
|
||||
/// upload uses replace semantics so re-running is idempotent.
|
||||
/// - If both source and target have notes (merge case):
|
||||
/// - identical contents: leave the target unchanged. The source copy will be
|
||||
/// removed by `destroy()`.
|
||||
/// - diverging contents: log a warning and keep the target's notes.
|
||||
pub(crate) fn move_notes_to(
|
||||
&self,
|
||||
target: &BackupGroup,
|
||||
backend: &DatastoreBackend,
|
||||
) -> Result<(), Error> {
|
||||
let src_notes_path = self.store.group_notes_path(&self.ns, &self.group);
|
||||
let dst_notes_path = target.store.group_notes_path(&target.ns, &target.group);
|
||||
|
||||
let src_notes = match std::fs::read(&src_notes_path) {
|
||||
Ok(v) => v,
|
||||
Err(ref err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(()),
|
||||
Err(err) => {
|
||||
bail!("reading source group notes {src_notes_path:?} failed: {err}")
|
||||
}
|
||||
};
|
||||
|
||||
match std::fs::read(&dst_notes_path) {
|
||||
Ok(dst_notes) => {
|
||||
if dst_notes != src_notes {
|
||||
log::warn!(
|
||||
"group notes differ during merge of '{}' from '{}' into '{}' - keeping target's notes",
|
||||
self.group, self.ns, target.ns,
|
||||
);
|
||||
}
|
||||
// Identical or intentionally kept: source copy will be removed by destroy().
|
||||
return Ok(());
|
||||
}
|
||||
Err(ref err) if err.kind() == std::io::ErrorKind::NotFound => {}
|
||||
Err(err) => {
|
||||
bail!("reading target group notes {dst_notes_path:?} failed: {err}")
|
||||
}
|
||||
}
|
||||
|
||||
if let DatastoreBackend::S3(s3_client) = backend {
|
||||
let dst_key = crate::s3::object_key_from_path(
|
||||
&target.relative_group_path(),
|
||||
GROUP_NOTES_FILE_NAME,
|
||||
)
|
||||
.context("invalid target notes object key")?;
|
||||
let data = hyper::body::Bytes::copy_from_slice(&src_notes);
|
||||
proxmox_async::runtime::block_on(s3_client.upload_replace_with_retry(dst_key, data))
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"failed to upload group notes on S3 backend for '{}' in '{}'",
|
||||
target.group, target.ns,
|
||||
)
|
||||
})?;
|
||||
}
|
||||
|
||||
std::fs::rename(&src_notes_path, &dst_notes_path).with_context(|| {
|
||||
format!("failed to move group notes {src_notes_path:?} -> {dst_notes_path:?}")
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Helper function, assumes that no more snapshots are present in the group.
|
||||
fn remove_group_dir(&self) -> Result<(), Error> {
|
||||
let note_path = self.store.group_notes_path(&self.ns, &self.group);
|
||||
@ -413,11 +257,9 @@ impl BackupGroup {
|
||||
|
||||
let owner_path = self.store.owner_path(&self.ns, &self.group);
|
||||
|
||||
if let Err(err) = std::fs::remove_file(&owner_path) {
|
||||
if err.kind() != std::io::ErrorKind::NotFound {
|
||||
bail!("removing the owner file '{owner_path:?}' failed - {err}");
|
||||
}
|
||||
}
|
||||
std::fs::remove_file(&owner_path).map_err(|err| {
|
||||
format_err!("removing the owner file '{owner_path:?}' failed - {err}")
|
||||
})?;
|
||||
|
||||
let path = self.full_group_path();
|
||||
|
||||
@ -741,149 +583,10 @@ impl BackupDir {
|
||||
}
|
||||
}
|
||||
|
||||
/// Move this snapshot into `target`.
|
||||
///
|
||||
/// For the filesystem backend, renames the snapshot directory. For S3, copies all
|
||||
/// objects under the snapshot prefix to the target, renames the local cache directory,
|
||||
/// then deletes the source objects. A copy failure returns an error with the snapshot
|
||||
/// intact at source. A delete failure is logged as a warning.
|
||||
///
|
||||
/// Before the rename, each index file's post-rename path is recorded in the per-datastore
|
||||
/// move journal so a concurrent GC phase-1 drain can still mark their chunks - see
|
||||
/// `move_journal` for the race this closes. If the rename then fails the journal entry
|
||||
/// becomes a ghost that the drain skips on `open_index_reader` returning `None`.
|
||||
///
|
||||
/// The caller must hold an exclusive lock on this snapshot, hold exclusive locks on
|
||||
/// both source and target groups, and ensure the target group directory exists.
|
||||
pub(crate) fn move_to(
|
||||
&self,
|
||||
target: &BackupGroup,
|
||||
backend: &DatastoreBackend,
|
||||
) -> Result<(), Error> {
|
||||
if !Arc::ptr_eq(&self.store, &target.store) {
|
||||
bail!("cannot move snapshot across different datastores");
|
||||
}
|
||||
|
||||
let target_snap = target.backup_dir_with_rfc3339(self.backup_time_string.clone())?;
|
||||
let src_snap_path = self.full_path();
|
||||
let dst_snap_path = target_snap.full_path();
|
||||
|
||||
// Enumerate the index files at source (they are still there until the
|
||||
// rename below) and record their future paths in the move journal so a
|
||||
// concurrent GC phase-1 drain can mark their chunks even if the
|
||||
// hierarchy iteration missed both the source and target.
|
||||
let mut journal_entries: Vec<PathBuf> = Vec::new();
|
||||
for entry in std::fs::read_dir(&src_snap_path)
|
||||
.with_context(|| format!("failed to list source snapshot dir {src_snap_path:?}"))?
|
||||
{
|
||||
let entry =
|
||||
entry.with_context(|| format!("failed to read entry in {src_snap_path:?}"))?;
|
||||
let name = entry.file_name();
|
||||
let Some(name_str) = name.to_str() else {
|
||||
continue;
|
||||
};
|
||||
if matches!(
|
||||
ArchiveType::from_path(name_str),
|
||||
Ok(ArchiveType::FixedIndex) | Ok(ArchiveType::DynamicIndex)
|
||||
) {
|
||||
journal_entries.push(dst_snap_path.join(&name));
|
||||
}
|
||||
}
|
||||
move_journal::append_moved_indices(self.store.name(), &journal_entries)?;
|
||||
|
||||
match backend {
|
||||
DatastoreBackend::Filesystem => {
|
||||
std::fs::rename(&src_snap_path, &dst_snap_path).with_context(|| {
|
||||
format!("failed to move snapshot {src_snap_path:?} to {dst_snap_path:?}")
|
||||
})?;
|
||||
}
|
||||
DatastoreBackend::S3(s3_client) => {
|
||||
let src_rel = self.relative_path();
|
||||
let src_rel_str = src_rel
|
||||
.to_str()
|
||||
.ok_or_else(|| format_err!("invalid source snapshot path"))?;
|
||||
let src_prefix_str = format!("{S3_CONTENT_PREFIX}/{src_rel_str}/");
|
||||
|
||||
let dst_rel = target_snap.relative_path();
|
||||
let dst_rel_str = dst_rel
|
||||
.to_str()
|
||||
.ok_or_else(|| format_err!("invalid target snapshot path"))?;
|
||||
let dst_prefix_str = format!("{S3_CONTENT_PREFIX}/{dst_rel_str}/");
|
||||
|
||||
let store_prefix = format!("{}/", self.store.name());
|
||||
|
||||
// Copy all objects for this snapshot to the target prefix. On failure the
|
||||
// source snapshot remains intact and any partial target copies stay as
|
||||
// leftovers (visible via the API for cleanup).
|
||||
let prefix = S3PathPrefix::Some(src_prefix_str.clone());
|
||||
let mut token: Option<String> = None;
|
||||
let mut src_keys = Vec::new();
|
||||
|
||||
loop {
|
||||
let result = proxmox_async::runtime::block_on(
|
||||
s3_client.list_objects_v2(&prefix, token.as_deref()),
|
||||
)
|
||||
.context("failed to list snapshot objects on S3 backend")?;
|
||||
|
||||
for item in result.contents {
|
||||
let full_key_str: &str = &item.key;
|
||||
let rel_key =
|
||||
full_key_str.strip_prefix(&store_prefix).ok_or_else(|| {
|
||||
format_err!("unexpected key prefix in '{full_key_str}'")
|
||||
})?;
|
||||
let src_key = S3ObjectKey::try_from(rel_key)?;
|
||||
|
||||
let suffix = rel_key
|
||||
.strip_prefix(&src_prefix_str)
|
||||
.ok_or_else(|| format_err!("unexpected key format '{rel_key}'"))?;
|
||||
let dst_key_str = format!("{dst_prefix_str}{suffix}");
|
||||
let dst_key = S3ObjectKey::try_from(dst_key_str.as_str())?;
|
||||
|
||||
proxmox_async::runtime::block_on(
|
||||
s3_client.copy_object(src_key.clone(), dst_key),
|
||||
)
|
||||
.with_context(|| format!("failed to copy S3 object '{rel_key}'"))?;
|
||||
src_keys.push(src_key);
|
||||
}
|
||||
|
||||
if result.is_truncated {
|
||||
token = result.next_continuation_token;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
std::fs::rename(&src_snap_path, &dst_snap_path).with_context(|| {
|
||||
format!("failed to move snapshot cache {src_snap_path:?} to {dst_snap_path:?}")
|
||||
})?;
|
||||
|
||||
// Delete source S3 objects. Treat failures as warnings since the snapshot
|
||||
// is already at the target.
|
||||
for src_key in src_keys {
|
||||
if let Err(err) =
|
||||
proxmox_async::runtime::block_on(s3_client.delete_object(src_key.clone()))
|
||||
{
|
||||
log::warn!(
|
||||
"S3 move: failed to delete source object '{src_key:?}' \
|
||||
(snapshot already at target, orphaned object requires manual removal): \
|
||||
{err:#}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up stale source lock files under /run for this snapshot.
|
||||
let _ = std::fs::remove_file(self.manifest_lock_path());
|
||||
let _ = std::fs::remove_file(self.lock_path());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Destroy the whole snapshot, bails if it's protected
|
||||
///
|
||||
/// Setting `force` to true skips locking and thus ignores if the backup is currently in use.
|
||||
pub(crate) fn destroy(&self, force: bool, backend: &DatastoreBackend) -> Result<(), Error> {
|
||||
pub fn destroy(&self, force: bool) -> Result<(), Error> {
|
||||
let (_guard, _manifest_guard);
|
||||
if !force {
|
||||
_guard = self
|
||||
@ -896,21 +599,6 @@ impl BackupDir {
|
||||
bail!("cannot remove protected snapshot"); // use special error type?
|
||||
}
|
||||
|
||||
if let DatastoreBackend::S3(s3_client) = backend {
|
||||
let path = self.relative_path();
|
||||
let snapshot_prefix = path
|
||||
.to_str()
|
||||
.ok_or_else(|| format_err!("invalid snapshot path"))?;
|
||||
let prefix = format!("{S3_CONTENT_PREFIX}/{snapshot_prefix}");
|
||||
let delete_objects_error = proxmox_async::runtime::block_on(
|
||||
s3_client.delete_objects_by_prefix(&S3PathPrefix::Some(prefix)),
|
||||
)?;
|
||||
if !delete_objects_error.is_empty() {
|
||||
crate::s3::log_s3_delete_objects_errors(&delete_objects_error);
|
||||
bail!("deleting objects failed");
|
||||
}
|
||||
}
|
||||
|
||||
let full_path = self.full_path();
|
||||
log::info!("removing backup snapshot {:?}", full_path);
|
||||
std::fs::remove_dir_all(&full_path).map_err(|err| {
|
||||
@ -940,12 +628,6 @@ impl BackupDir {
|
||||
// do to rectify the situation.
|
||||
if guard.is_ok() && group.list_backups()?.is_empty() && !*OLD_LOCKING {
|
||||
group.remove_group_dir()?;
|
||||
if let DatastoreBackend::S3(s3_client) = backend {
|
||||
let object_key =
|
||||
super::s3::object_key_from_path(&group.relative_group_path(), "owner")
|
||||
.context("invalid owner file object key")?;
|
||||
proxmox_async::runtime::block_on(s3_client.delete_object(object_key))?;
|
||||
}
|
||||
} else if let Err(err) = guard {
|
||||
log::debug!("{err:#}");
|
||||
}
|
||||
@ -982,7 +664,6 @@ impl BackupDir {
|
||||
/// only use this method - anything else may break locking guarantees.
|
||||
pub fn update_manifest(
|
||||
&self,
|
||||
backend: &DatastoreBackend,
|
||||
update_fn: impl FnOnce(&mut BackupManifest),
|
||||
) -> Result<(), Error> {
|
||||
let _guard = self.lock_manifest()?;
|
||||
@ -995,15 +676,6 @@ impl BackupDir {
|
||||
let blob = DataBlob::encode(manifest.as_bytes(), None, true)?;
|
||||
let raw_data = blob.raw_data();
|
||||
|
||||
if let DatastoreBackend::S3(s3_client) = backend {
|
||||
let object_key =
|
||||
super::s3::object_key_from_path(&self.relative_path(), MANIFEST_BLOB_NAME.as_ref())
|
||||
.context("invalid manifest object key")?;
|
||||
let data = hyper::body::Bytes::copy_from_slice(raw_data);
|
||||
proxmox_async::runtime::block_on(s3_client.upload_replace_with_retry(object_key, data))
|
||||
.context("failed to update manifest on s3 backend")?;
|
||||
}
|
||||
|
||||
let mut path = self.full_path();
|
||||
path.push(MANIFEST_BLOB_NAME.as_ref());
|
||||
|
||||
@ -1118,13 +790,13 @@ impl BackupInfo {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn sort_list(list: &mut [BackupInfo], ascending: bool) {
|
||||
if ascending {
|
||||
pub fn sort_list(list: &mut [BackupInfo], ascendending: bool) {
|
||||
if ascendending {
|
||||
// oldest first
|
||||
list.sort_unstable_by_key(|a| a.backup_dir.dir.time);
|
||||
list.sort_unstable_by(|a, b| a.backup_dir.dir.time.cmp(&b.backup_dir.dir.time));
|
||||
} else {
|
||||
// newest first
|
||||
list.sort_unstable_by_key(|b| std::cmp::Reverse(b.backup_dir.dir.time));
|
||||
list.sort_unstable_by(|a, b| b.backup_dir.dir.time.cmp(&a.backup_dir.dir.time));
|
||||
}
|
||||
}
|
||||
|
||||
@ -1209,7 +881,7 @@ fn lock_file_path_helper(ns: &BackupNamespace, path: PathBuf) -> PathBuf {
|
||||
/// deletion.
|
||||
///
|
||||
/// It also creates the base directory for lock files.
|
||||
pub(crate) fn lock_helper<F>(
|
||||
fn lock_helper<F>(
|
||||
store_name: &str,
|
||||
path: &std::path::Path,
|
||||
lock_fn: F,
|
||||
|
||||
@ -81,11 +81,7 @@ impl<I: IndexFile, R: AsyncReadChunk + Send + Sync + 'static> CachedChunkReader<
|
||||
let info = self.index.chunk_info(chunk.0).unwrap();
|
||||
|
||||
// will never be None, see AsyncChunkCacher
|
||||
let data = self
|
||||
.cache
|
||||
.access(info.digest, &self.cacher, |_| Ok(()))
|
||||
.await?
|
||||
.unwrap();
|
||||
let data = self.cache.access(info.digest, &self.cacher).await?.unwrap();
|
||||
|
||||
let want_bytes = ((info.range.end - cur_offset) as usize).min(size - read);
|
||||
let slice = &mut buf[read..(read + want_bytes)];
|
||||
|
||||
@ -203,7 +203,7 @@ impl DirEntry {
|
||||
|
||||
/// Check if DirEntry is a symlink
|
||||
pub fn is_symlink(&self) -> bool {
|
||||
matches!(self.attr, DirEntryAttribute::Symlink)
|
||||
matches!(self.attr, DirEntryAttribute::Symlink { .. })
|
||||
}
|
||||
}
|
||||
|
||||
@ -973,8 +973,8 @@ impl ArchiveEntry {
|
||||
size: Option<u64>,
|
||||
) -> Self {
|
||||
Self {
|
||||
filepath: proxmox_base64::encode(filepath),
|
||||
text: String::from_utf8_lossy(filepath.split(|x| *x == b'/').next_back().unwrap())
|
||||
filepath: base64::encode(filepath),
|
||||
text: String::from_utf8_lossy(filepath.split(|x| *x == b'/').last().unwrap())
|
||||
.to_string(),
|
||||
entry_type: match entry_type {
|
||||
Some(entry_type) => CatalogEntryType::from(entry_type).to_string(),
|
||||
|
||||
@ -51,9 +51,12 @@ impl<R: Read> Read for ChecksumReader<R> {
|
||||
if count > 0 {
|
||||
self.hasher.update(&buf[..count]);
|
||||
if let Some(ref mut signer) = self.signer {
|
||||
signer
|
||||
.update(&buf[..count])
|
||||
.map_err(|err| std::io::Error::other(format!("hmac update failed - {err}")))?;
|
||||
signer.update(&buf[..count]).map_err(|err| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
format!("hmac update failed - {}", err),
|
||||
)
|
||||
})?;
|
||||
}
|
||||
}
|
||||
Ok(count)
|
||||
|
||||
@ -49,9 +49,12 @@ impl<W: Write> Write for ChecksumWriter<W> {
|
||||
fn write(&mut self, buf: &[u8]) -> Result<usize, std::io::Error> {
|
||||
self.hasher.update(buf);
|
||||
if let Some(ref mut signer) = self.signer {
|
||||
signer
|
||||
.update(buf)
|
||||
.map_err(|err| std::io::Error::other(format!("hmac update failed - {err}")))?;
|
||||
signer.update(buf).map_err(|err| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
format!("hmac update failed - {}", err),
|
||||
)
|
||||
})?;
|
||||
}
|
||||
self.writer.write(buf)
|
||||
}
|
||||
|
||||
@ -5,27 +5,21 @@ use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{bail, format_err, Context, Error};
|
||||
use hex::FromHex;
|
||||
use tracing::{info, warn};
|
||||
|
||||
use pbs_api_types::{DatastoreFSyncLevel, GarbageCollectionStatus};
|
||||
use pbs_config::BackupLockGuard;
|
||||
use proxmox_io::ReadExt;
|
||||
use proxmox_s3_client::S3Client;
|
||||
use proxmox_sys::fs::{create_dir, create_path, file_type_from_file_stat, CreateOptions};
|
||||
use proxmox_sys::process_locker::{
|
||||
ProcessLockExclusiveGuard, ProcessLockSharedGuard, ProcessLocker,
|
||||
};
|
||||
use proxmox_worker_task::WorkerTaskContext;
|
||||
|
||||
use crate::backup_info::DATASTORE_LOCKS_DIR;
|
||||
use crate::data_blob::DataChunkBuilder;
|
||||
use crate::file_formats::{
|
||||
COMPRESSED_BLOB_MAGIC_1_0, ENCRYPTED_BLOB_MAGIC_1_0, UNCOMPRESSED_BLOB_MAGIC_1_0,
|
||||
};
|
||||
use crate::{DataBlob, LocalDatastoreLruCache};
|
||||
|
||||
const USING_MARKER_FILENAME_EXT: &str = "using";
|
||||
use crate::DataBlob;
|
||||
|
||||
/// File system based chunk store
|
||||
pub struct ChunkStore {
|
||||
@ -140,7 +134,7 @@ impl ChunkStore {
|
||||
|
||||
for i in 0..64 * 1024 {
|
||||
let mut l1path = chunk_dir.clone();
|
||||
l1path.push(format!("{i:04x}"));
|
||||
l1path.push(format!("{:04x}", i));
|
||||
if let Err(err) = create_dir(&l1path, options) {
|
||||
bail!(
|
||||
"unable to create chunk store '{}' subdir {:?} - {}",
|
||||
@ -209,32 +203,15 @@ impl ChunkStore {
|
||||
})
|
||||
}
|
||||
|
||||
fn touch_chunk_no_lock(&self, digest: &[u8; 32]) -> Result<(), Error> {
|
||||
pub fn touch_chunk(&self, digest: &[u8; 32]) -> Result<(), Error> {
|
||||
// unwrap: only `None` in unit tests
|
||||
assert!(self.locker.is_some());
|
||||
|
||||
self.cond_touch_chunk_no_lock(digest, true)?;
|
||||
self.cond_touch_chunk(digest, true)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Update the chunk files atime if it exists.
|
||||
///
|
||||
/// If the chunk file does not exist, return with error if assert_exists is true, with
|
||||
/// Ok(false) otherwise.
|
||||
pub(super) fn cond_touch_chunk(
|
||||
&self,
|
||||
digest: &[u8; 32],
|
||||
assert_exists: bool,
|
||||
) -> Result<bool, Error> {
|
||||
let _lock = self.mutex.lock();
|
||||
self.cond_touch_chunk_no_lock(digest, assert_exists)
|
||||
}
|
||||
|
||||
fn cond_touch_chunk_no_lock(
|
||||
&self,
|
||||
digest: &[u8; 32],
|
||||
assert_exists: bool,
|
||||
) -> Result<bool, Error> {
|
||||
pub fn cond_touch_chunk(&self, digest: &[u8; 32], assert_exists: bool) -> Result<bool, Error> {
|
||||
// unwrap: only `None` in unit tests
|
||||
assert!(self.locker.is_some());
|
||||
|
||||
@ -242,7 +219,7 @@ impl ChunkStore {
|
||||
self.cond_touch_path(&chunk_path, assert_exists)
|
||||
}
|
||||
|
||||
fn cond_touch_path(&self, path: &Path, assert_exists: bool) -> Result<bool, Error> {
|
||||
pub fn cond_touch_path(&self, path: &Path, assert_exists: bool) -> Result<bool, Error> {
|
||||
// unwrap: only `None` in unit tests
|
||||
assert!(self.locker.is_some());
|
||||
|
||||
@ -276,32 +253,11 @@ impl ChunkStore {
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// Update access timestamp on all bad chunks for given digest
|
||||
///
|
||||
/// Gets exclusive access by acquiring the chunk store mutex guard.
|
||||
pub(super) fn cond_touch_bad_chunks(&self, digest: &[u8; 32]) -> Result<bool, Error> {
|
||||
let _lock = self.mutex.lock();
|
||||
|
||||
let (mut chunk_path, digest_str) = self.chunk_path(digest);
|
||||
let mut is_bad = false;
|
||||
for i in 0..=9 {
|
||||
chunk_path.set_file_name(ChunkExt::bad_chunk_filename(&digest_str, i));
|
||||
if self.cond_touch_path(&chunk_path, false)? {
|
||||
is_bad = true;
|
||||
}
|
||||
}
|
||||
Ok(is_bad)
|
||||
}
|
||||
|
||||
fn get_chunk_store_iterator(
|
||||
pub fn get_chunk_iterator(
|
||||
&self,
|
||||
) -> Result<
|
||||
impl std::iter::FusedIterator<
|
||||
Item = (
|
||||
Result<proxmox_sys::fs::ReadDirEntry, Error>,
|
||||
usize,
|
||||
ChunkExt,
|
||||
),
|
||||
Item = (Result<proxmox_sys::fs::ReadDirEntry, Error>, usize, bool),
|
||||
>,
|
||||
Error,
|
||||
> {
|
||||
@ -336,47 +292,21 @@ impl ChunkStore {
|
||||
Some(Ok(entry)) => {
|
||||
// skip files if they're not a hash
|
||||
let bytes = entry.file_name().to_bytes();
|
||||
|
||||
if bytes.len() < 64 {
|
||||
if bytes.len() != 64 && bytes.len() != 64 + ".0.bad".len() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if !bytes.iter().take(64).all(u8::is_ascii_hexdigit) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// regular chunk
|
||||
if bytes.len() == 64 {
|
||||
return Some((Ok(entry), percentage, ChunkExt::None));
|
||||
}
|
||||
|
||||
// i-th bad chunk
|
||||
if bytes.len() == 64 + ".i.bad".len()
|
||||
&& bytes[64] == b'.'
|
||||
&& bytes[65] >= b'0'
|
||||
&& bytes[65] <= b'9'
|
||||
&& bytes[66] == b'.'
|
||||
&& bytes.ends_with(b"bad")
|
||||
{
|
||||
return Some((Ok(entry), percentage, ChunkExt::Bad));
|
||||
}
|
||||
|
||||
// chunk marker file
|
||||
let marker_ext_bytes = USING_MARKER_FILENAME_EXT.as_bytes();
|
||||
if bytes.len() == 64 + 1 + marker_ext_bytes.len()
|
||||
&& bytes[64] == b'.'
|
||||
&& bytes.ends_with(marker_ext_bytes)
|
||||
{
|
||||
return Some((Ok(entry), percentage, ChunkExt::UsedMarker));
|
||||
}
|
||||
|
||||
continue;
|
||||
let bad = bytes.ends_with(b".bad");
|
||||
return Some((Ok(entry), percentage, bad));
|
||||
}
|
||||
Some(Err(err)) => {
|
||||
// stop after first error
|
||||
done = true;
|
||||
// and pass the error through:
|
||||
return Some((Err(err), percentage, ChunkExt::None));
|
||||
return Some((Err(err), percentage, false));
|
||||
}
|
||||
None => (), // open next directory
|
||||
}
|
||||
@ -389,7 +319,7 @@ impl ChunkStore {
|
||||
return None;
|
||||
}
|
||||
|
||||
let subdir: &str = &format!("{at:04x}");
|
||||
let subdir: &str = &format!("{:04x}", at);
|
||||
percentage = (at * 100) / 0x10000;
|
||||
at += 1;
|
||||
match proxmox_sys::fs::read_subdir(base_handle.as_raw_fd(), subdir) {
|
||||
@ -409,7 +339,7 @@ impl ChunkStore {
|
||||
return Some((
|
||||
Err(format_err!("unable to read subdir '{subdir}' - {err}")),
|
||||
percentage,
|
||||
ChunkExt::None,
|
||||
false,
|
||||
));
|
||||
}
|
||||
}
|
||||
@ -423,17 +353,12 @@ impl ChunkStore {
|
||||
ProcessLocker::oldest_shared_lock(self.locker.clone().unwrap())
|
||||
}
|
||||
|
||||
pub fn mutex(&self) -> &std::sync::Mutex<()> {
|
||||
&self.mutex
|
||||
}
|
||||
|
||||
pub fn sweep_unused_chunks(
|
||||
&self,
|
||||
oldest_writer: i64,
|
||||
min_atime: i64,
|
||||
status: &mut GarbageCollectionStatus,
|
||||
worker: &dyn WorkerTaskContext,
|
||||
cache: Option<&LocalDatastoreLruCache>,
|
||||
) -> Result<(), Error> {
|
||||
// unwrap: only `None` in unit tests
|
||||
assert!(self.locker.is_some());
|
||||
@ -444,8 +369,7 @@ impl ChunkStore {
|
||||
let mut last_percentage = 0;
|
||||
let mut chunk_count = 0;
|
||||
|
||||
for (entry, percentage, chunk_ext) in self.get_chunk_store_iterator()? {
|
||||
let bad = chunk_ext == ChunkExt::Bad;
|
||||
for (entry, percentage, bad) in self.get_chunk_iterator()? {
|
||||
if last_percentage != percentage {
|
||||
last_percentage = percentage;
|
||||
info!("processed {percentage}% ({chunk_count} chunks)");
|
||||
@ -466,63 +390,45 @@ impl ChunkStore {
|
||||
|
||||
let lock = self.mutex.lock();
|
||||
|
||||
if let Ok(stat) = fstatat(
|
||||
Some(dirfd),
|
||||
filename,
|
||||
nix::fcntl::AtFlags::AT_SYMLINK_NOFOLLOW,
|
||||
) {
|
||||
if let Ok(stat) = fstatat(dirfd, filename, nix::fcntl::AtFlags::AT_SYMLINK_NOFOLLOW) {
|
||||
let file_type = file_type_from_file_stat(&stat);
|
||||
if file_type != Some(nix::dir::Type::File) {
|
||||
drop(lock);
|
||||
continue;
|
||||
}
|
||||
if chunk_ext == ChunkExt::UsedMarker {
|
||||
unlinkat(Some(dirfd), filename, UnlinkatFlags::NoRemoveDir).map_err(|err| {
|
||||
format_err!("unlinking chunk using marker {filename:?} failed - {err}")
|
||||
})?;
|
||||
drop(lock);
|
||||
continue;
|
||||
}
|
||||
|
||||
chunk_count += 1;
|
||||
|
||||
unsafe {
|
||||
self.cond_sweep_chunk(
|
||||
stat.st_atime,
|
||||
min_atime,
|
||||
oldest_writer,
|
||||
stat.st_size as u64,
|
||||
bad,
|
||||
status,
|
||||
|| {
|
||||
// non-bad S3 chunks need to be removed via cache
|
||||
if let Some(cache) = cache {
|
||||
if !bad {
|
||||
let digest = <[u8; 32]>::from_hex(filename.to_bytes())?;
|
||||
|
||||
// unless there is a concurrent upload pending,
|
||||
// must never block due to required locking order
|
||||
if let Ok(_guard) =
|
||||
self.lock_chunk(&digest, Duration::from_secs(0))
|
||||
{
|
||||
cache.remove(&digest)?;
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
// bad or local chunks
|
||||
unlinkat(Some(dirfd), filename, UnlinkatFlags::NoRemoveDir).map_err(
|
||||
|err| {
|
||||
format_err!(
|
||||
"unlinking chunk {filename:?} failed on store '{}' - {err}",
|
||||
self.name,
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
)?;
|
||||
if stat.st_atime < min_atime {
|
||||
//let age = now - stat.st_atime;
|
||||
//println!("UNLINK {} {:?}", age/(3600*24), filename);
|
||||
if let Err(err) = unlinkat(Some(dirfd), filename, UnlinkatFlags::NoRemoveDir) {
|
||||
if bad {
|
||||
status.still_bad += 1;
|
||||
}
|
||||
bail!(
|
||||
"unlinking chunk {filename:?} failed on store '{}' - {err}",
|
||||
self.name,
|
||||
);
|
||||
}
|
||||
if bad {
|
||||
status.removed_bad += 1;
|
||||
} else {
|
||||
status.removed_chunks += 1;
|
||||
}
|
||||
status.removed_bytes += stat.st_size as u64;
|
||||
} else if stat.st_atime < oldest_writer {
|
||||
if bad {
|
||||
status.still_bad += 1;
|
||||
} else {
|
||||
status.pending_chunks += 1;
|
||||
}
|
||||
status.pending_bytes += stat.st_size as u64;
|
||||
} else {
|
||||
if !bad {
|
||||
status.disk_chunks += 1;
|
||||
}
|
||||
status.disk_bytes += stat.st_size as u64;
|
||||
}
|
||||
}
|
||||
drop(lock);
|
||||
@ -531,53 +437,6 @@ impl ChunkStore {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check within what range the provided chunks atime falls and update the garbage collection
|
||||
/// status accordingly.
|
||||
///
|
||||
/// If the chunk should be removed, the [`remove_callback`] is executed.
|
||||
///
|
||||
/// Unsafe: requires locking and GC checks to be called
|
||||
/// FIXME: make this internal with further refactoring
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(super) unsafe fn cond_sweep_chunk<T: FnOnce() -> Result<(), Error>>(
|
||||
&self,
|
||||
atime: i64,
|
||||
min_atime: i64,
|
||||
oldest_writer: i64,
|
||||
size: u64,
|
||||
bad: bool,
|
||||
gc_status: &mut GarbageCollectionStatus,
|
||||
remove_callback: T,
|
||||
) -> Result<(), Error> {
|
||||
if atime < min_atime {
|
||||
if let Err(err) = remove_callback() {
|
||||
if bad {
|
||||
gc_status.still_bad += 1;
|
||||
}
|
||||
return Err(err);
|
||||
}
|
||||
if bad {
|
||||
gc_status.removed_bad += 1;
|
||||
} else {
|
||||
gc_status.removed_chunks += 1;
|
||||
}
|
||||
gc_status.removed_bytes += size;
|
||||
} else if atime < oldest_writer {
|
||||
if bad {
|
||||
gc_status.still_bad += 1;
|
||||
} else {
|
||||
gc_status.pending_chunks += 1;
|
||||
}
|
||||
gc_status.pending_bytes += size;
|
||||
} else {
|
||||
if !bad {
|
||||
gc_status.disk_chunks += 1;
|
||||
}
|
||||
gc_status.disk_bytes += size;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check if atime updates are honored by the filesystem backing the chunk store.
|
||||
///
|
||||
/// Checks if the atime is always updated by utimensat taking into consideration the Linux
|
||||
@ -587,28 +446,10 @@ impl ChunkStore {
|
||||
/// Uses a 4 MiB fixed size, compressed but unencrypted chunk to test. The chunk is inserted in
|
||||
/// the chunk store if not yet present.
|
||||
/// Returns with error if the check could not be performed.
|
||||
pub fn check_fs_atime_updates(
|
||||
&self,
|
||||
retry_on_file_changed: bool,
|
||||
s3_client: Option<Arc<S3Client>>,
|
||||
) -> Result<(), Error> {
|
||||
pub fn check_fs_atime_updates(&self, retry_on_file_changed: bool) -> Result<(), Error> {
|
||||
let (zero_chunk, digest) = DataChunkBuilder::build_zero_chunk(None, 4096 * 1024, true)?;
|
||||
let (path, _digest) = self.chunk_path(&digest);
|
||||
|
||||
if let Some(ref s3_client) = s3_client {
|
||||
if let Err(err) = std::fs::metadata(&path) {
|
||||
if err.kind() == std::io::ErrorKind::NotFound {
|
||||
let object_key = crate::s3::object_key_from_digest(&digest)?;
|
||||
proxmox_async::runtime::block_on(s3_client.upload_no_replace_with_retry(
|
||||
object_key,
|
||||
zero_chunk.raw_data().to_vec().into(),
|
||||
))
|
||||
.context("failed to upload chunk to s3 backend")?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let (pre_existing, _) = self.insert_chunk(&zero_chunk, &digest)?;
|
||||
let (path, _digest) = self.chunk_path(&digest);
|
||||
|
||||
// Take into account timestamp update granularity in the kernel
|
||||
// Blocking the thread is fine here since this runs in a worker.
|
||||
@ -629,7 +470,7 @@ impl ChunkStore {
|
||||
// two metadata calls, try to check once again on changed file
|
||||
if metadata_before.ino() != metadata_now.ino() {
|
||||
if retry_on_file_changed {
|
||||
return self.check_fs_atime_updates(false, s3_client);
|
||||
return self.check_fs_atime_updates(false);
|
||||
}
|
||||
bail!("chunk {path:?} changed twice during access time safety check, cannot proceed.");
|
||||
}
|
||||
@ -665,24 +506,10 @@ impl ChunkStore {
|
||||
|
||||
//println!("DIGEST {}", hex::encode(digest));
|
||||
|
||||
let _lock = self.mutex.lock();
|
||||
|
||||
// Safety: lock acquired above
|
||||
unsafe { self.insert_chunk_nolock(chunk, digest, true) }
|
||||
}
|
||||
|
||||
/// Safety: requires holding the chunk store mutex!
|
||||
pub(crate) unsafe fn insert_chunk_nolock(
|
||||
&self,
|
||||
chunk: &DataBlob,
|
||||
digest: &[u8; 32],
|
||||
warn_on_overwrite_empty: bool,
|
||||
) -> Result<(bool, u64), Error> {
|
||||
// unwrap: only `None` in unit tests
|
||||
assert!(self.locker.is_some());
|
||||
|
||||
let (chunk_path, digest_str) = self.chunk_path(digest);
|
||||
|
||||
let lock = self.mutex.lock();
|
||||
|
||||
let raw_data = chunk.raw_data();
|
||||
let encoded_size = raw_data.len() as u64;
|
||||
|
||||
@ -694,12 +521,10 @@ impl ChunkStore {
|
||||
}
|
||||
let old_size = metadata.len();
|
||||
if encoded_size == old_size {
|
||||
self.touch_chunk_no_lock(digest)?;
|
||||
self.touch_chunk(digest)?;
|
||||
return Ok((true, old_size));
|
||||
} else if old_size == 0 {
|
||||
if warn_on_overwrite_empty {
|
||||
log::warn!("found empty chunk '{digest_str}' in store {name}, overwriting");
|
||||
}
|
||||
log::warn!("found empty chunk '{digest_str}' in store {name}, overwriting");
|
||||
} else if chunk.is_encrypted() {
|
||||
// incoming chunk is encrypted, possible attack or hash collision!
|
||||
let mut existing_file = std::fs::File::open(&chunk_path)?;
|
||||
@ -721,11 +546,11 @@ impl ChunkStore {
|
||||
// compressed, the size mismatch could be caused by different zstd versions
|
||||
// so let's keep the one that was uploaded first, bit-rot is hopefully detected by
|
||||
// verification at some point..
|
||||
self.touch_chunk_no_lock(digest)?;
|
||||
self.touch_chunk(digest)?;
|
||||
return Ok((true, old_size));
|
||||
} else if old_size < encoded_size {
|
||||
log::debug!("Got another copy of chunk with digest '{digest_str}', existing chunk is smaller, discarding uploaded one.");
|
||||
self.touch_chunk_no_lock(digest)?;
|
||||
self.touch_chunk(digest)?;
|
||||
return Ok((true, old_size));
|
||||
} else {
|
||||
log::debug!("Got another copy of chunk with digest '{digest_str}', existing chunk is bigger, replacing with uploaded one.");
|
||||
@ -759,6 +584,8 @@ impl ChunkStore {
|
||||
.map_err(|err| format_err!("fsync failed: {err}"))?;
|
||||
}
|
||||
|
||||
drop(lock);
|
||||
|
||||
Ok((false, encoded_size))
|
||||
}
|
||||
|
||||
@ -774,82 +601,6 @@ impl ChunkStore {
|
||||
(chunk_path, digest_str)
|
||||
}
|
||||
|
||||
/// Replace a chunk file with a zero size file in the chunk store.
|
||||
///
|
||||
/// Used to evict chunks from the local datastore cache, while keeping them as in-use markers
|
||||
/// for garbage collection. Returns with success also if chunk file is not pre-existing,
|
||||
/// also creating the marker file in that case.
|
||||
///
|
||||
/// Safety: chunk store mutex must be held!
|
||||
pub(crate) unsafe fn replace_chunk_with_marker_or_create_marker(
|
||||
&self,
|
||||
digest: &[u8; 32],
|
||||
) -> Result<(), Error> {
|
||||
let (chunk_path, digest_str) = self.chunk_path(digest);
|
||||
Self::create_marker_file(&chunk_path)
|
||||
.map_err(|err| format_err!("clear chunk failed for {digest_str} - {err}"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Helper to generate new empty marker file
|
||||
fn create_marker_file(path: &Path) -> Result<(), Error> {
|
||||
let mut create_options = CreateOptions::new();
|
||||
if nix::unistd::Uid::effective().is_root() {
|
||||
let uid = pbs_config::backup_user()?.uid;
|
||||
let gid = pbs_config::backup_group()?.gid;
|
||||
create_options = create_options.owner(uid).group(gid);
|
||||
}
|
||||
proxmox_sys::fs::replace_file(path, &[], create_options, false)
|
||||
}
|
||||
|
||||
/// Mark chunk as expected to be present by writing a file the chunk store.
|
||||
///
|
||||
/// Used to mark chunks which are found in index files during phase 1 of garbage collection
|
||||
/// for s3 datastores, but the marker file is not present and it is seemingly not a bad chunk.
|
||||
/// This might happen if the local store cache is empty after datastore re-creation.
|
||||
pub(crate) fn mark_chunk_as_expected(&self, digest: &[u8; 32]) -> Result<(), Error> {
|
||||
let marker_path = self.chunk_expected_marker_path(digest);
|
||||
Self::create_marker_file(&marker_path)
|
||||
.map_err(|err| format_err!("mark chunk failed for {} - {err}", hex::encode(digest)))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Remove the chunk-expected marker file from the chunk store.
|
||||
///
|
||||
/// Used to remove the chunk-expected marker file during phase 2 of garbage collection. This
|
||||
/// marker file is created during phase 1 of garbage collection in case the chunk or its zero
|
||||
/// size marker file is not found in the chunk store, but still referenced by an index file,
|
||||
/// flagging it as still required.
|
||||
///
|
||||
/// Returns true if the file was present and removed, false if the file did not exist.
|
||||
pub(crate) fn clear_chunk_expected_mark(&self, digest: &[u8; 32]) -> Result<bool, Error> {
|
||||
let marker_path = self.chunk_expected_marker_path(digest);
|
||||
if let Err(err) = std::fs::remove_file(marker_path) {
|
||||
if err.kind() != std::io::ErrorKind::NotFound {
|
||||
return Err(err.into());
|
||||
} else {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// Helper to generate marker file path for expected chunks
|
||||
fn chunk_expected_marker_path(&self, digest: &[u8; 32]) -> PathBuf {
|
||||
let (mut path, _digest_str) = self.chunk_path(digest);
|
||||
path.set_extension(USING_MARKER_FILENAME_EXT);
|
||||
path
|
||||
}
|
||||
|
||||
/// Removes a chunk marker file from the `LocalDatastoreLruCache`s chunk store.
|
||||
///
|
||||
/// Callers must hold the per-chunk file lock in order to avoid races with renaming of corrupt
|
||||
/// chunks by verifications and chunk inserts by backups.
|
||||
pub(crate) fn remove_chunk_marker(&self, digest: &[u8; 32]) -> Result<(), Error> {
|
||||
let (chunk_path, _digest_str) = self.chunk_path(digest);
|
||||
std::fs::remove_file(chunk_path).map_err(Error::from)
|
||||
}
|
||||
|
||||
pub fn relative_path(&self, path: &Path) -> PathBuf {
|
||||
// unwrap: only `None` in unit tests
|
||||
assert!(self.locker.is_some());
|
||||
@ -917,7 +668,7 @@ impl ChunkStore {
|
||||
// Check all .chunks subdirectories
|
||||
for i in 0..64 * 1024 {
|
||||
let mut l1path = chunk_dir.clone();
|
||||
l1path.push(format!("{i:04x}"));
|
||||
l1path.push(format!("{:04x}", i));
|
||||
ChunkStore::check_permissions(&l1path, 0o750)?;
|
||||
}
|
||||
|
||||
@ -926,76 +677,23 @@ impl ChunkStore {
|
||||
ChunkStore::check_permissions(lockfile_path, 0o644)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Generates the path to the chunks lock file
|
||||
pub(crate) fn chunk_lock_path(&self, digest: &[u8]) -> PathBuf {
|
||||
let mut lock_path = Path::new(DATASTORE_LOCKS_DIR).join(self.name.clone());
|
||||
let digest_str = hex::encode(digest);
|
||||
lock_path.push(".chunks");
|
||||
let prefix = digest_to_prefix(digest);
|
||||
lock_path.push(&prefix);
|
||||
lock_path.push(&digest_str);
|
||||
lock_path
|
||||
}
|
||||
|
||||
/// Get an exclusive lock on the chunks lock file
|
||||
pub(crate) fn lock_chunk(
|
||||
&self,
|
||||
digest: &[u8],
|
||||
timeout: Duration,
|
||||
) -> Result<BackupLockGuard, Error> {
|
||||
let lock_path = self.chunk_lock_path(digest);
|
||||
let guard = crate::backup_info::lock_helper(self.name(), &lock_path, |path| {
|
||||
pbs_config::open_backup_lockfile(path, Some(timeout), true)
|
||||
})?;
|
||||
Ok(guard)
|
||||
}
|
||||
|
||||
/// Generate the next bad chunk file path for given digest. Returns the path as well as the bad
|
||||
/// chunk counter.
|
||||
pub(crate) fn next_bad_chunk_path(&self, digest: &[u8; 32]) -> (PathBuf, usize) {
|
||||
let (mut chunk_path, digest_str) = self.chunk_path(digest);
|
||||
let mut counter = 0;
|
||||
loop {
|
||||
chunk_path.set_file_name(ChunkExt::bad_chunk_filename(&digest_str, counter));
|
||||
if chunk_path.exists() && counter < 9 {
|
||||
counter += 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
(chunk_path, counter)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
/// Chunk iterator directory entry filename extension
|
||||
enum ChunkExt {
|
||||
None,
|
||||
Bad,
|
||||
UsedMarker,
|
||||
}
|
||||
|
||||
impl ChunkExt {
|
||||
fn bad_chunk_filename(digest_str: &str, counter: usize) -> String {
|
||||
format!("{digest_str}.{counter}.bad")
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_chunk_store1() {
|
||||
use tempfile::TempDir;
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let path = temp_dir.path();
|
||||
let mut path = std::fs::canonicalize(".").unwrap(); // we need absolute path
|
||||
path.push(".testdir");
|
||||
|
||||
let chunk_store = ChunkStore::open("test", path, DatastoreFSyncLevel::None);
|
||||
if let Err(_e) = std::fs::remove_dir_all(".testdir") { /* ignore */ }
|
||||
|
||||
let chunk_store = ChunkStore::open("test", &path, DatastoreFSyncLevel::None);
|
||||
assert!(chunk_store.is_err());
|
||||
|
||||
let user = nix::unistd::User::from_uid(nix::unistd::Uid::current())
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let chunk_store =
|
||||
ChunkStore::create("test", path, user.uid, user.gid, DatastoreFSyncLevel::None).unwrap();
|
||||
ChunkStore::create("test", &path, user.uid, user.gid, DatastoreFSyncLevel::None).unwrap();
|
||||
|
||||
let (chunk, digest) = crate::data_blob::DataChunkBuilder::new(&[0u8, 1u8])
|
||||
.build()
|
||||
@ -1008,8 +706,8 @@ fn test_chunk_store1() {
|
||||
assert!(exists);
|
||||
|
||||
let chunk_store =
|
||||
ChunkStore::create("test", path, user.uid, user.gid, DatastoreFSyncLevel::None);
|
||||
ChunkStore::create("test", &path, user.uid, user.gid, DatastoreFSyncLevel::None);
|
||||
assert!(chunk_store.is_err());
|
||||
|
||||
temp_dir.close().unwrap();
|
||||
if let Err(_e) = std::fs::remove_dir_all(".testdir") { /* ignore */ }
|
||||
}
|
||||
|
||||
@ -363,13 +363,13 @@ fn test_chunker1() {
|
||||
for (_offset, len) in &chunks1 {
|
||||
size1 += len;
|
||||
}
|
||||
println!("Chunks1:{size1}\n{chunks1:?}\n");
|
||||
println!("Chunks1:{}\n{:?}\n", size1, chunks1);
|
||||
|
||||
let mut size2 = 0;
|
||||
for (_offset, len) in &chunks2 {
|
||||
size2 += len;
|
||||
}
|
||||
println!("Chunks2:{size2}\n{chunks2:?}\n");
|
||||
println!("Chunks2:{}\n{:?}\n", size2, chunks2);
|
||||
|
||||
if size1 != 256 * 4 * 1024 {
|
||||
panic!("wrong size for chunks1");
|
||||
|
||||
@ -54,7 +54,12 @@ impl<W: Write> Write for CryptWriter<W> {
|
||||
let count = self
|
||||
.crypter
|
||||
.update(&buf[..write_size], self.encr_buf.as_mut())
|
||||
.map_err(|err| std::io::Error::other(format!("crypter update failed - {err}")))?;
|
||||
.map_err(|err| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
format!("crypter update failed - {}", err),
|
||||
)
|
||||
})?;
|
||||
|
||||
self.writer.write_all(&self.encr_buf[..count])?;
|
||||
|
||||
|
||||
@ -2,7 +2,6 @@ use std::io::Write;
|
||||
|
||||
use anyhow::{bail, Error};
|
||||
use openssl::symm::{decrypt_aead, Mode};
|
||||
use tokio::io::{AsyncRead, AsyncReadExt};
|
||||
|
||||
use proxmox_io::{ReadExt, WriteExt};
|
||||
|
||||
@ -239,26 +238,15 @@ impl DataBlob {
|
||||
}
|
||||
}
|
||||
|
||||
/// Load data blob via given sync ``reader`` and verify its CRC
|
||||
/// Load blob from ``reader``, verify CRC
|
||||
pub fn load_from_reader(reader: &mut dyn std::io::Read) -> Result<Self, Error> {
|
||||
let mut data = Vec::with_capacity(1024 * 1024);
|
||||
reader.read_to_end(&mut data)?;
|
||||
Self::from_raw_with_crc_check(data)
|
||||
}
|
||||
|
||||
/// Load data blob via given async ``reader`` and verify its CRC
|
||||
pub async fn load_from_async_reader(
|
||||
reader: &mut (dyn AsyncRead + Unpin + Send),
|
||||
) -> Result<Self, Error> {
|
||||
let mut data = Vec::with_capacity(1024 * 1024);
|
||||
reader.read_to_end(&mut data).await?;
|
||||
Self::from_raw_with_crc_check(data)
|
||||
}
|
||||
let blob = Self::from_raw(data)?;
|
||||
|
||||
/// Generates a data blob from raw input data and checks for matching CRC in header
|
||||
fn from_raw_with_crc_check(raw_data: Vec<u8>) -> Result<Self, Error> {
|
||||
let blob = Self::from_raw(raw_data)?;
|
||||
blob.verify_crc()?;
|
||||
|
||||
Ok(blob)
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -11,14 +11,19 @@ use anyhow::{bail, format_err, Error};
|
||||
|
||||
use proxmox_io::ReadExt;
|
||||
use proxmox_sys::mmap::Mmap;
|
||||
use proxmox_sys::process_locker::ProcessLockSharedGuard;
|
||||
use proxmox_uuid::Uuid;
|
||||
use pxar::accessor::{MaybeReady, ReadAt, ReadAtOperation};
|
||||
|
||||
use pbs_tools::lru_cache::LruCache;
|
||||
|
||||
use crate::chunk_stat::ChunkStat;
|
||||
use crate::chunk_store::ChunkStore;
|
||||
use crate::data_blob::{DataBlob, DataChunkBuilder};
|
||||
use crate::file_formats;
|
||||
use crate::index::{ChunkReadInfo, IndexFile};
|
||||
use crate::read_chunk::ReadChunk;
|
||||
use crate::{Chunker, ChunkerImpl};
|
||||
|
||||
/// Header format definition for dynamic index files (`.dixd`)
|
||||
#[repr(C)]
|
||||
@ -119,6 +124,8 @@ impl DynamicIndexReader {
|
||||
bail!("got unknown magic number");
|
||||
}
|
||||
|
||||
let ctime = proxmox_time::epoch_i64();
|
||||
|
||||
let index_size = stat.st_size as usize - header_size;
|
||||
let index_count = index_size / 40;
|
||||
if index_count * 40 != index_size {
|
||||
@ -127,7 +134,7 @@ impl DynamicIndexReader {
|
||||
|
||||
let index = unsafe {
|
||||
Mmap::map_fd(
|
||||
&file,
|
||||
rawfd,
|
||||
header_size as u64,
|
||||
index_count,
|
||||
nix::sys::mman::ProtFlags::PROT_READ,
|
||||
@ -139,13 +146,14 @@ impl DynamicIndexReader {
|
||||
_file: file,
|
||||
size,
|
||||
index,
|
||||
ctime: i64::from_le(header.ctime),
|
||||
ctime,
|
||||
uuid: header.uuid,
|
||||
index_csum: header.index_csum,
|
||||
})
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[allow(clippy::cast_ptr_alignment)]
|
||||
pub fn chunk_end(&self, pos: usize) -> u64 {
|
||||
if pos >= self.index.len() {
|
||||
panic!("chunk index out of range");
|
||||
@ -196,7 +204,7 @@ impl IndexFile for DynamicIndexReader {
|
||||
if pos >= self.index.len() {
|
||||
None
|
||||
} else {
|
||||
Some(self.chunk_digest(pos))
|
||||
Some(unsafe { &*(self.chunk_digest(pos).as_ptr() as *const [u8; 32]) })
|
||||
}
|
||||
}
|
||||
|
||||
@ -268,6 +276,8 @@ impl IndexFile for DynamicIndexReader {
|
||||
|
||||
/// Create dynamic index files (`.dixd`)
|
||||
pub struct DynamicIndexWriter {
|
||||
store: Arc<ChunkStore>,
|
||||
_lock: ProcessLockSharedGuard,
|
||||
writer: BufWriter<File>,
|
||||
closed: bool,
|
||||
filename: PathBuf,
|
||||
@ -284,9 +294,10 @@ impl Drop for DynamicIndexWriter {
|
||||
}
|
||||
|
||||
impl DynamicIndexWriter {
|
||||
// Requires obtaining a shared chunk store lock beforehand
|
||||
pub fn create(full_path: impl Into<PathBuf>) -> Result<Self, Error> {
|
||||
let full_path = full_path.into();
|
||||
pub fn create(store: Arc<ChunkStore>, path: &Path) -> Result<Self, Error> {
|
||||
let shared_lock = store.try_shared_lock()?;
|
||||
|
||||
let full_path = store.relative_path(path);
|
||||
let mut tmp_path = full_path.clone();
|
||||
tmp_path.set_extension("tmp_didx");
|
||||
|
||||
@ -313,6 +324,8 @@ impl DynamicIndexWriter {
|
||||
let csum = Some(openssl::sha::Sha256::new());
|
||||
|
||||
Ok(Self {
|
||||
store,
|
||||
_lock: shared_lock,
|
||||
writer,
|
||||
closed: false,
|
||||
filename: full_path,
|
||||
@ -323,6 +336,11 @@ impl DynamicIndexWriter {
|
||||
})
|
||||
}
|
||||
|
||||
// fixme: use add_chunk instead?
|
||||
pub fn insert_chunk(&self, chunk: &DataBlob, digest: &[u8; 32]) -> Result<(bool, u64), Error> {
|
||||
self.store.insert_chunk(chunk, digest)
|
||||
}
|
||||
|
||||
pub fn close(&mut self) -> Result<[u8; 32], Error> {
|
||||
if self.closed {
|
||||
bail!(
|
||||
@ -373,6 +391,138 @@ impl DynamicIndexWriter {
|
||||
}
|
||||
}
|
||||
|
||||
/// Writer which splits a binary stream into dynamic sized chunks
|
||||
///
|
||||
/// And store the resulting chunk list into the index file.
|
||||
pub struct DynamicChunkWriter {
|
||||
index: DynamicIndexWriter,
|
||||
closed: bool,
|
||||
chunker: ChunkerImpl,
|
||||
stat: ChunkStat,
|
||||
chunk_offset: usize,
|
||||
last_chunk: usize,
|
||||
chunk_buffer: Vec<u8>,
|
||||
}
|
||||
|
||||
impl DynamicChunkWriter {
|
||||
pub fn new(index: DynamicIndexWriter, chunk_size: usize) -> Self {
|
||||
Self {
|
||||
index,
|
||||
closed: false,
|
||||
chunker: ChunkerImpl::new(chunk_size),
|
||||
stat: ChunkStat::new(0),
|
||||
chunk_offset: 0,
|
||||
last_chunk: 0,
|
||||
chunk_buffer: Vec::with_capacity(chunk_size * 4),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stat(&self) -> &ChunkStat {
|
||||
&self.stat
|
||||
}
|
||||
|
||||
pub fn close(&mut self) -> Result<(), Error> {
|
||||
if self.closed {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.closed = true;
|
||||
|
||||
self.write_chunk_buffer()?;
|
||||
|
||||
self.index.close()?;
|
||||
|
||||
self.stat.size = self.chunk_offset as u64;
|
||||
|
||||
// add size of index file
|
||||
self.stat.size +=
|
||||
(self.stat.chunk_count * 40 + std::mem::size_of::<DynamicIndexHeader>()) as u64;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_chunk_buffer(&mut self) -> Result<(), Error> {
|
||||
let chunk_size = self.chunk_buffer.len();
|
||||
|
||||
if chunk_size == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let expected_chunk_size = self.chunk_offset - self.last_chunk;
|
||||
if expected_chunk_size != self.chunk_buffer.len() {
|
||||
bail!("wrong chunk size {} != {}", expected_chunk_size, chunk_size);
|
||||
}
|
||||
|
||||
self.stat.chunk_count += 1;
|
||||
|
||||
self.last_chunk = self.chunk_offset;
|
||||
|
||||
let (chunk, digest) = DataChunkBuilder::new(&self.chunk_buffer)
|
||||
.compress(true)
|
||||
.build()?;
|
||||
|
||||
match self.index.insert_chunk(&chunk, &digest) {
|
||||
Ok((is_duplicate, compressed_size)) => {
|
||||
self.stat.compressed_size += compressed_size;
|
||||
if is_duplicate {
|
||||
self.stat.duplicate_chunks += 1;
|
||||
} else {
|
||||
self.stat.disk_size += compressed_size;
|
||||
}
|
||||
|
||||
log::info!(
|
||||
"ADD CHUNK {:016x} {} {}% {} {}",
|
||||
self.chunk_offset,
|
||||
chunk_size,
|
||||
(compressed_size * 100) / (chunk_size as u64),
|
||||
is_duplicate,
|
||||
hex::encode(digest)
|
||||
);
|
||||
self.index.add_chunk(self.chunk_offset as u64, &digest)?;
|
||||
self.chunk_buffer.truncate(0);
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
self.chunk_buffer.truncate(0);
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Write for DynamicChunkWriter {
|
||||
fn write(&mut self, data: &[u8]) -> std::result::Result<usize, std::io::Error> {
|
||||
let chunker = &mut self.chunker;
|
||||
|
||||
let ctx = crate::chunker::Context::default();
|
||||
let pos = chunker.scan(data, &ctx);
|
||||
|
||||
if pos > 0 {
|
||||
self.chunk_buffer.extend_from_slice(&data[0..pos]);
|
||||
self.chunk_offset += pos;
|
||||
|
||||
if let Err(err) = self.write_chunk_buffer() {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
err.to_string(),
|
||||
));
|
||||
}
|
||||
Ok(pos)
|
||||
} else {
|
||||
self.chunk_offset += data.len();
|
||||
self.chunk_buffer.extend_from_slice(data);
|
||||
Ok(data.len())
|
||||
}
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> std::result::Result<(), std::io::Error> {
|
||||
Err(std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
"please use close() instead of flush()",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
struct CachedChunk {
|
||||
range: Range<u64>,
|
||||
data: Vec<u8>,
|
||||
@ -449,7 +599,6 @@ impl<S: ReadChunk> BufferedDynamicReader<S> {
|
||||
store: &mut self.store,
|
||||
index: &self.index,
|
||||
},
|
||||
|_| Ok(()),
|
||||
)?
|
||||
.ok_or_else(|| format_err!("chunk not found by cacher"))?;
|
||||
|
||||
@ -503,11 +652,11 @@ impl<S: ReadChunk> BufferedDynamicReader<S> {
|
||||
|
||||
impl<S: ReadChunk> std::io::Read for BufferedDynamicReader<S> {
|
||||
fn read(&mut self, buf: &mut [u8]) -> Result<usize, std::io::Error> {
|
||||
use std::io::Error;
|
||||
use std::io::{Error, ErrorKind};
|
||||
|
||||
let data = match self.buffered_read(self.read_offset) {
|
||||
Ok(v) => v,
|
||||
Err(err) => return Err(Error::other(err.to_string())),
|
||||
Err(err) => return Err(Error::new(ErrorKind::Other, err.to_string())),
|
||||
};
|
||||
|
||||
let n = if data.len() > buf.len() {
|
||||
@ -532,12 +681,15 @@ impl<S: ReadChunk> std::io::Seek for BufferedDynamicReader<S> {
|
||||
SeekFrom::Current(offset) => (self.read_offset as i64) + offset,
|
||||
};
|
||||
|
||||
use std::io::Error;
|
||||
use std::io::{Error, ErrorKind};
|
||||
if (new_offset < 0) || (new_offset > (self.archive_size as i64)) {
|
||||
return Err(Error::other(format!(
|
||||
"seek is out of range {} ([0..{}])",
|
||||
new_offset, self.archive_size
|
||||
)));
|
||||
return Err(Error::new(
|
||||
ErrorKind::Other,
|
||||
format!(
|
||||
"seek is out of range {} ([0..{}])",
|
||||
new_offset, self.archive_size
|
||||
),
|
||||
));
|
||||
}
|
||||
self.read_offset = new_offset as u64;
|
||||
|
||||
|
||||
@ -1,14 +1,19 @@
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::io::{Seek, SeekFrom};
|
||||
use std::os::unix::io::AsRawFd;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::ptr::NonNull;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{bail, format_err, Context, Error};
|
||||
use anyhow::{bail, format_err, Error};
|
||||
|
||||
use proxmox_io::ReadExt;
|
||||
use proxmox_sys::process_locker::ProcessLockSharedGuard;
|
||||
use proxmox_uuid::Uuid;
|
||||
|
||||
use crate::chunk_stat::ChunkStat;
|
||||
use crate::chunk_store::ChunkStore;
|
||||
use crate::data_blob::ChunkInfo;
|
||||
use crate::file_formats;
|
||||
use crate::index::{ChunkReadInfo, IndexFile};
|
||||
|
||||
@ -85,10 +90,6 @@ impl FixedIndexReader {
|
||||
let ctime = i64::from_le(header.ctime);
|
||||
let chunk_size = u64::from_le(header.chunk_size);
|
||||
|
||||
if !chunk_size.is_power_of_two() {
|
||||
bail!("got non-power-of-two chunk size: {chunk_size}");
|
||||
}
|
||||
|
||||
let index_length = size.div_ceil(chunk_size) as usize;
|
||||
let index_size = index_length * 32;
|
||||
|
||||
@ -101,8 +102,6 @@ impl FixedIndexReader {
|
||||
);
|
||||
}
|
||||
|
||||
let chunk_size = usize::try_from(chunk_size)?;
|
||||
|
||||
let data = unsafe {
|
||||
nix::sys::mman::mmap(
|
||||
None,
|
||||
@ -110,16 +109,14 @@ impl FixedIndexReader {
|
||||
.ok_or_else(|| format_err!("invalid index size"))?,
|
||||
nix::sys::mman::ProtFlags::PROT_READ,
|
||||
nix::sys::mman::MapFlags::MAP_PRIVATE,
|
||||
&file,
|
||||
file.as_raw_fd(),
|
||||
header_size as i64,
|
||||
)
|
||||
}?
|
||||
.as_ptr()
|
||||
.cast::<u8>();
|
||||
}? as *mut u8;
|
||||
|
||||
Ok(Self {
|
||||
_file: file,
|
||||
chunk_size,
|
||||
chunk_size: chunk_size as usize,
|
||||
size,
|
||||
index_length,
|
||||
index: data,
|
||||
@ -130,13 +127,15 @@ impl FixedIndexReader {
|
||||
}
|
||||
|
||||
fn unmap(&mut self) -> Result<(), Error> {
|
||||
let Some(index) = NonNull::new(self.index as *mut std::ffi::c_void) else {
|
||||
if self.index.is_null() {
|
||||
return Ok(());
|
||||
};
|
||||
}
|
||||
|
||||
let index_size = self.index_length * 32;
|
||||
|
||||
if let Err(err) = unsafe { nix::sys::mman::munmap(index, index_size) } {
|
||||
if let Err(err) =
|
||||
unsafe { nix::sys::mman::munmap(self.index as *mut std::ffi::c_void, index_size) }
|
||||
{
|
||||
bail!("unmap file failed - {}", err);
|
||||
}
|
||||
|
||||
@ -215,32 +214,18 @@ impl IndexFile for FixedIndexReader {
|
||||
}
|
||||
}
|
||||
|
||||
struct MmapPtr(NonNull<std::ffi::c_void>);
|
||||
|
||||
impl MmapPtr {
|
||||
fn header(&self) -> NonNull<FixedIndexHeader> {
|
||||
self.0.cast::<FixedIndexHeader>()
|
||||
}
|
||||
|
||||
fn index(&self) -> NonNull<u8> {
|
||||
unsafe { self.0.byte_add(size_of::<FixedIndexHeader>()).cast::<u8>() }
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FixedIndexWriter {
|
||||
store: Arc<ChunkStore>,
|
||||
file: File,
|
||||
_lock: ProcessLockSharedGuard,
|
||||
filename: PathBuf,
|
||||
tmp_filename: PathBuf,
|
||||
/// Most places use u32 because values are just a few MiB, but here
|
||||
/// u64 is sightly more convenient for calculations involving size.
|
||||
chunk_size: u64,
|
||||
size: u64,
|
||||
chunk_size: usize,
|
||||
size: usize,
|
||||
index_length: usize,
|
||||
index_capacity: usize,
|
||||
memory: Option<MmapPtr>,
|
||||
index: *mut u8,
|
||||
pub uuid: [u8; 16],
|
||||
pub ctime: i64,
|
||||
growable_size: bool,
|
||||
}
|
||||
|
||||
// `index` is mmap()ed which cannot be thread-local so should be sendable
|
||||
@ -256,27 +241,20 @@ impl Drop for FixedIndexWriter {
|
||||
}
|
||||
|
||||
impl FixedIndexWriter {
|
||||
/// The initial capacity, if the total size is unknown.
|
||||
///
|
||||
/// This capacity takes up the same amount of space as the header
|
||||
/// and can refer to 128 Blocks * 4 MiB/Block = 512 MiB of content.
|
||||
///
|
||||
/// On systems with 4 KiB page size this value ensures that the
|
||||
/// mapped length is a multiple of the page size, but this is not
|
||||
/// strictly necessary.
|
||||
const INITIAL_CAPACITY: usize = 4096 / 32;
|
||||
|
||||
// Requires obtaining a shared chunk store lock beforehand
|
||||
#[allow(clippy::cast_ptr_alignment)]
|
||||
pub fn create(
|
||||
full_path: impl Into<PathBuf>,
|
||||
known_size: Option<u64>,
|
||||
chunk_size: u32,
|
||||
store: Arc<ChunkStore>,
|
||||
path: &Path,
|
||||
size: usize,
|
||||
chunk_size: usize,
|
||||
) -> Result<Self, Error> {
|
||||
let full_path = full_path.into();
|
||||
let shared_lock = store.try_shared_lock()?;
|
||||
|
||||
let full_path = store.relative_path(path);
|
||||
let mut tmp_path = full_path.clone();
|
||||
tmp_path.set_extension("tmp_fidx");
|
||||
|
||||
let file = std::fs::OpenOptions::new()
|
||||
let mut file = std::fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.read(true)
|
||||
@ -290,196 +268,91 @@ impl FixedIndexWriter {
|
||||
panic!("got unexpected header size");
|
||||
}
|
||||
|
||||
let chunk_size = u64::from(chunk_size);
|
||||
if !chunk_size.is_power_of_two() {
|
||||
bail!("got non-power-of-two chunk size: {chunk_size}");
|
||||
}
|
||||
|
||||
let ctime = proxmox_time::epoch_i64();
|
||||
let size = known_size.unwrap_or(0);
|
||||
|
||||
let uuid = Uuid::generate();
|
||||
|
||||
let (index_length, index_capacity) = match known_size {
|
||||
Some(s) => {
|
||||
let len = s.div_ceil(chunk_size).try_into()?;
|
||||
(len, len)
|
||||
}
|
||||
None => (0, Self::INITIAL_CAPACITY),
|
||||
};
|
||||
let buffer = vec![0u8; header_size];
|
||||
let header = unsafe { &mut *(buffer.as_ptr() as *mut FixedIndexHeader) };
|
||||
|
||||
let file_size = Self::file_size(index_capacity)?;
|
||||
nix::unistd::ftruncate(&file, file_size)?;
|
||||
|
||||
let memory = MmapPtr(unsafe {
|
||||
nix::sys::mman::mmap(
|
||||
None,
|
||||
std::num::NonZeroUsize::new(file_size as usize)
|
||||
.ok_or_else(|| format_err!("index file size cannot be zero"))?,
|
||||
nix::sys::mman::ProtFlags::PROT_READ | nix::sys::mman::ProtFlags::PROT_WRITE,
|
||||
nix::sys::mman::MapFlags::MAP_SHARED,
|
||||
&file,
|
||||
0,
|
||||
)
|
||||
}?);
|
||||
|
||||
let header = unsafe { memory.header().as_mut() };
|
||||
header.magic = file_formats::FIXED_SIZED_CHUNK_INDEX_1_0;
|
||||
header.ctime = i64::to_le(ctime);
|
||||
header.chunk_size = u64::to_le(chunk_size);
|
||||
header.size = u64::to_le(size as u64);
|
||||
header.chunk_size = u64::to_le(chunk_size as u64);
|
||||
header.uuid = *uuid.as_bytes();
|
||||
|
||||
header.index_csum = [0u8; 32];
|
||||
|
||||
file.write_all(&buffer)?;
|
||||
|
||||
let index_length = size.div_ceil(chunk_size);
|
||||
let index_size = index_length * 32;
|
||||
nix::unistd::ftruncate(file.as_raw_fd(), (header_size + index_size) as i64)?;
|
||||
|
||||
let data = unsafe {
|
||||
nix::sys::mman::mmap(
|
||||
None,
|
||||
std::num::NonZeroUsize::new(index_size)
|
||||
.ok_or_else(|| format_err!("invalid index size"))?,
|
||||
nix::sys::mman::ProtFlags::PROT_READ | nix::sys::mman::ProtFlags::PROT_WRITE,
|
||||
nix::sys::mman::MapFlags::MAP_SHARED,
|
||||
file.as_raw_fd(),
|
||||
header_size as i64,
|
||||
)
|
||||
}? as *mut u8;
|
||||
|
||||
Ok(Self {
|
||||
store,
|
||||
file,
|
||||
_lock: shared_lock,
|
||||
filename: full_path,
|
||||
tmp_filename: tmp_path,
|
||||
chunk_size,
|
||||
size,
|
||||
index_length,
|
||||
index_capacity,
|
||||
memory: Some(memory),
|
||||
index: data,
|
||||
ctime,
|
||||
uuid: *uuid.as_bytes(),
|
||||
growable_size: known_size.is_none(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Computes the size of a fidx file containing `index_length`
|
||||
/// chunk digests.
|
||||
///
|
||||
/// Guarantees that the size fits into usize, isize and i64.
|
||||
fn file_size(index_length: usize) -> Result<i64, Error> {
|
||||
if index_length == 0 {
|
||||
bail!("fidx file must have at least one chunk");
|
||||
}
|
||||
index_length
|
||||
.checked_mul(32)
|
||||
.and_then(|s| s.checked_add(size_of::<FixedIndexHeader>()))
|
||||
.filter(|s| *s <= isize::MAX as usize)
|
||||
.and_then(|s| i64::try_from(s).ok())
|
||||
.ok_or_else(|| format_err!("fidx file size overflow for {index_length} chunks"))
|
||||
}
|
||||
|
||||
/// If this returns an error, the sizes may be out of sync,
|
||||
/// which is especially bad if the capacity was reduced.
|
||||
fn set_index_capacity(&mut self, new_capacity: usize) -> Result<(), Error> {
|
||||
if new_capacity == self.index_capacity {
|
||||
return Ok(());
|
||||
}
|
||||
let old_size = Self::file_size(self.index_capacity)?;
|
||||
let new_size = Self::file_size(new_capacity)?;
|
||||
|
||||
let Some(MmapPtr(index_addr)) = self.memory else {
|
||||
bail!("Can't resize unmapped FixedIndexWriter");
|
||||
};
|
||||
|
||||
nix::unistd::ftruncate(&self.file, new_size)?;
|
||||
|
||||
let new_index = unsafe {
|
||||
nix::sys::mman::mremap(
|
||||
index_addr,
|
||||
old_size as usize,
|
||||
new_size as usize,
|
||||
nix::sys::mman::MRemapFlags::MREMAP_MAYMOVE,
|
||||
None,
|
||||
)
|
||||
}?;
|
||||
|
||||
self.memory = Some(MmapPtr(new_index));
|
||||
self.index_capacity = new_capacity;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Unmapping ensures future add and close operations fail.
|
||||
fn set_index_capacity_or_unmap(&mut self, new_capacity: usize) -> Result<(), Error> {
|
||||
self.set_index_capacity(new_capacity).map_err(|e| {
|
||||
let unmap_result = self.unmap();
|
||||
let message = format!(
|
||||
"failed to resize index capacity from {} to {new_capacity} with backing file: {:?}",
|
||||
self.index_capacity, self.tmp_filename
|
||||
);
|
||||
if let Err(unmap_err) = unmap_result {
|
||||
e.context(message).context(unmap_err)
|
||||
} else {
|
||||
e.context(message)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Increase the content size to be at least `requested_size` and
|
||||
/// ensure there is enough capacity.
|
||||
///
|
||||
/// Only writers that were created without a known size can grow.
|
||||
/// The size also becomes fixed as soon as it is no longer divisible
|
||||
/// by the block size, to ensure that only the last block can be
|
||||
/// smaller.
|
||||
pub fn grow_to_size(&mut self, requested_size: u64) -> Result<(), Error> {
|
||||
if self.size < requested_size {
|
||||
if !self.growable_size {
|
||||
bail!("refusing to resize from {} to {requested_size}", self.size);
|
||||
}
|
||||
let new_len = requested_size.div_ceil(self.chunk_size).try_into()?;
|
||||
if new_len as u64 * self.chunk_size != requested_size {
|
||||
// not a full chunk, so this must be the last one
|
||||
self.growable_size = false;
|
||||
self.set_index_capacity_or_unmap(new_len)?;
|
||||
} else if new_len > self.index_capacity {
|
||||
let new_capacity = new_len
|
||||
.checked_next_power_of_two()
|
||||
.ok_or_else(|| format_err!("capacity overflow"))?;
|
||||
self.set_index_capacity_or_unmap(new_capacity)?;
|
||||
}
|
||||
assert!(new_len <= self.index_capacity);
|
||||
self.index_length = new_len;
|
||||
self.size = requested_size;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The current length of the index.
|
||||
pub fn index_length(&self) -> usize {
|
||||
self.index_length
|
||||
}
|
||||
|
||||
/// The current total size of the referenced content.
|
||||
pub fn size(&self) -> u64 {
|
||||
self.size
|
||||
}
|
||||
|
||||
fn unmap(&mut self) -> Result<(), Error> {
|
||||
if let Some(ptr) = self.memory.take() {
|
||||
let len = Self::file_size(self.index_capacity).context(
|
||||
"calculation of index file size for unmapping failed - this should never happen!",
|
||||
)?;
|
||||
if let Err(err) = unsafe { nix::sys::mman::munmap(ptr.0, len as usize) } {
|
||||
bail!("unmap file {:?} failed - {}", self.tmp_filename, err);
|
||||
}
|
||||
if self.index.is_null() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let index_size = self.index_length * 32;
|
||||
|
||||
if let Err(err) =
|
||||
unsafe { nix::sys::mman::munmap(self.index as *mut std::ffi::c_void, index_size) }
|
||||
{
|
||||
bail!("unmap file {:?} failed - {}", self.tmp_filename, err);
|
||||
}
|
||||
|
||||
self.index = std::ptr::null_mut();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn close(&mut self) -> Result<[u8; 32], Error> {
|
||||
let Some(ptr) = &self.memory else {
|
||||
if self.index.is_null() {
|
||||
bail!("cannot close already closed index file.");
|
||||
};
|
||||
}
|
||||
|
||||
let index_size = self.index_length * 32;
|
||||
let data = unsafe { std::slice::from_raw_parts(ptr.index().as_ptr(), index_size) };
|
||||
let data = unsafe { std::slice::from_raw_parts(self.index, index_size) };
|
||||
let index_csum = openssl::sha::sha256(data);
|
||||
|
||||
{
|
||||
let header = unsafe { ptr.header().as_mut() };
|
||||
header.index_csum = index_csum;
|
||||
header.size = self.size.to_le();
|
||||
}
|
||||
|
||||
self.unmap()?;
|
||||
|
||||
if self.index_length < self.index_capacity {
|
||||
let file_size = Self::file_size(self.index_length)?;
|
||||
nix::unistd::ftruncate(&self.file, file_size)?;
|
||||
self.index_capacity = self.index_length;
|
||||
}
|
||||
let csum_offset = std::mem::offset_of!(FixedIndexHeader, index_csum);
|
||||
self.file.seek(SeekFrom::Start(csum_offset as u64))?;
|
||||
self.file.write_all(&index_csum)?;
|
||||
self.file.flush()?;
|
||||
|
||||
if let Err(err) = std::fs::rename(&self.tmp_filename, &self.filename) {
|
||||
bail!("Atomic rename file {:?} failed - {}", self.filename, err);
|
||||
@ -488,10 +361,12 @@ impl FixedIndexWriter {
|
||||
Ok(index_csum)
|
||||
}
|
||||
|
||||
fn check_chunk_alignment(&self, offset: u64, chunk_len: u64) -> Result<usize, Error> {
|
||||
let Some(pos) = offset.checked_sub(chunk_len) else {
|
||||
pub fn check_chunk_alignment(&self, offset: usize, chunk_len: usize) -> Result<usize, Error> {
|
||||
if offset < chunk_len {
|
||||
bail!("got chunk with small offset ({} < {}", offset, chunk_len);
|
||||
};
|
||||
}
|
||||
|
||||
let pos = offset - chunk_len;
|
||||
|
||||
if offset > self.size {
|
||||
bail!("chunk data exceeds size ({} >= {})", offset, self.size);
|
||||
@ -513,10 +388,44 @@ impl FixedIndexWriter {
|
||||
bail!("got unaligned chunk (pos = {})", pos);
|
||||
}
|
||||
|
||||
Ok((pos / self.chunk_size) as usize)
|
||||
Ok(pos / self.chunk_size)
|
||||
}
|
||||
|
||||
fn add_digest(&mut self, index: usize, digest: &[u8; 32]) -> Result<(), Error> {
|
||||
// Note: We want to add data out of order, so do not assume any order here.
|
||||
pub fn add_chunk(&mut self, chunk_info: &ChunkInfo, stat: &mut ChunkStat) -> Result<(), Error> {
|
||||
let chunk_len = chunk_info.chunk_len as usize;
|
||||
let offset = chunk_info.offset as usize; // end of chunk
|
||||
|
||||
let idx = self.check_chunk_alignment(offset, chunk_len)?;
|
||||
|
||||
let (is_duplicate, compressed_size) = self
|
||||
.store
|
||||
.insert_chunk(&chunk_info.chunk, &chunk_info.digest)?;
|
||||
|
||||
stat.chunk_count += 1;
|
||||
stat.compressed_size += compressed_size;
|
||||
|
||||
let digest = &chunk_info.digest;
|
||||
|
||||
log::info!(
|
||||
"ADD CHUNK {} {} {}% {} {}",
|
||||
idx,
|
||||
chunk_len,
|
||||
(compressed_size * 100) / (chunk_len as u64),
|
||||
is_duplicate,
|
||||
hex::encode(digest)
|
||||
);
|
||||
|
||||
if is_duplicate {
|
||||
stat.duplicate_chunks += 1;
|
||||
} else {
|
||||
stat.disk_size += compressed_size;
|
||||
}
|
||||
|
||||
self.add_digest(idx, digest)
|
||||
}
|
||||
|
||||
pub fn add_digest(&mut self, index: usize, digest: &[u8; 32]) -> Result<(), Error> {
|
||||
if index >= self.index_length {
|
||||
bail!(
|
||||
"add digest failed - index out of range ({} >= {})",
|
||||
@ -524,282 +433,29 @@ impl FixedIndexWriter {
|
||||
self.index_length
|
||||
);
|
||||
}
|
||||
self.add_digest_unchecked(index, digest)
|
||||
}
|
||||
|
||||
fn add_digest_unchecked(&mut self, index: usize, digest: &[u8; 32]) -> Result<(), Error> {
|
||||
let Some(ptr) = &self.memory else {
|
||||
if self.index.is_null() {
|
||||
bail!("cannot write to closed index file.");
|
||||
};
|
||||
}
|
||||
|
||||
let index_pos = index * 32;
|
||||
unsafe {
|
||||
let dst = ptr.index().as_ptr().add(index_pos);
|
||||
let dst = self.index.add(index_pos);
|
||||
dst.copy_from_nonoverlapping(digest.as_ptr(), 32);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Write the digest of a chunk into this index file.
|
||||
///
|
||||
/// The `start` and `size` parameters encode the range of
|
||||
/// content that is backed up. It is verified that `start` is
|
||||
/// aligned and that only the last chunk may be smaller.
|
||||
///
|
||||
/// If this writer has been created without a fixed size, the
|
||||
/// index capacity and content size are increased automatically
|
||||
/// until an incomplete chunk is encountered.
|
||||
pub fn add_chunk(&mut self, start: u64, size: u32, digest: &[u8; 32]) -> Result<(), Error> {
|
||||
let size = u64::from(size);
|
||||
let Some(end) = start.checked_add(size) else {
|
||||
bail!("add_chunk: start and size are too large: {start}+{size}");
|
||||
};
|
||||
self.grow_to_size(end)?;
|
||||
let idx = self.check_chunk_alignment(end, size)?;
|
||||
self.add_digest(idx, digest)
|
||||
}
|
||||
|
||||
/// Copy the chunk hashes from a Reader to the start of this Writer.
|
||||
///
|
||||
/// If this writer is resizable the capacity may increase,
|
||||
/// but the size and length stay the same.
|
||||
pub fn clone_data_from(&mut self, reader: &FixedIndexReader) -> Result<(), Error> {
|
||||
if self.chunk_size != reader.chunk_size as u64 {
|
||||
bail!("can't reuse file with different chunk size");
|
||||
if self.index_length != reader.index_count() {
|
||||
bail!("clone_data_from failed - index sizes not equal");
|
||||
}
|
||||
|
||||
let count = reader.index_count();
|
||||
if self.growable_size && self.index_capacity < count {
|
||||
self.set_index_capacity_or_unmap(count)?;
|
||||
for i in 0..self.index_length {
|
||||
self.add_digest(i, reader.index_digest(i).unwrap())?;
|
||||
}
|
||||
|
||||
for i in 0..count.min(self.index_capacity) {
|
||||
self.add_digest_unchecked(i, reader.index_digest(i).unwrap())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::fs;
|
||||
use tempfile::TempDir;
|
||||
|
||||
use super::*;
|
||||
|
||||
const CS: u32 = 4096;
|
||||
|
||||
#[test]
|
||||
fn test_empty() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let path = dir.path().join("test_empty");
|
||||
let mut w = FixedIndexWriter::create(&path, None, CS).unwrap();
|
||||
|
||||
assert!(w.add_digest(0, &[1u8; 32]).is_err(), "out of bounds");
|
||||
|
||||
assert_eq!(0, w.size);
|
||||
assert_eq!(0, w.index_length(), "returns length, not capacity");
|
||||
assert_eq!(FixedIndexWriter::INITIAL_CAPACITY, w.index_capacity);
|
||||
|
||||
assert!(w.close().is_err(), "should refuse to create empty file");
|
||||
|
||||
drop(w);
|
||||
assert!(!fs::exists(path).unwrap());
|
||||
|
||||
dir.close().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_single_partial_chunk() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let path = dir.path().join("test_single_partial_chunk");
|
||||
let mut w = FixedIndexWriter::create(&path, None, CS).unwrap();
|
||||
|
||||
let size = CS as u64 - 1;
|
||||
let expected = test_data(size);
|
||||
w.grow_to_size(size).unwrap();
|
||||
expected[0].add_to(&mut w);
|
||||
|
||||
w.close().unwrap();
|
||||
drop(w);
|
||||
|
||||
check_with_reader(&path, size, &expected);
|
||||
compare_to_known_size_writer(&path, size, &expected);
|
||||
|
||||
dir.close().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_grow_to_multiples_of_chunk_size() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let path = dir.path().join("test_grow_to_multiples_of_chunk_size");
|
||||
let mut w = FixedIndexWriter::create(&path, None, CS).unwrap();
|
||||
|
||||
let initial = FixedIndexWriter::INITIAL_CAPACITY;
|
||||
let steps = [1, 2, initial, initial + 1, 5 * initial, 10 * initial + 1];
|
||||
let expected = test_data(*steps.last().unwrap() as u64 * CS as u64);
|
||||
|
||||
let mut begin = 0;
|
||||
for chunk_count in steps {
|
||||
let last = &expected[chunk_count - 1];
|
||||
w.grow_to_size(last.end).unwrap();
|
||||
assert_eq!(last.index + 1, w.index_length());
|
||||
assert!(w.add_digest(last.index + 1, &[1u8; 32]).is_err());
|
||||
|
||||
for c in expected[begin..chunk_count].iter().rev() {
|
||||
c.add_to(&mut w);
|
||||
}
|
||||
begin = chunk_count;
|
||||
}
|
||||
w.close().unwrap();
|
||||
drop(w);
|
||||
|
||||
let size = expected.len() as u64 * CS as u64;
|
||||
check_with_reader(&path, size, &expected);
|
||||
compare_to_known_size_writer(&path, size, &expected);
|
||||
|
||||
dir.close().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_grow_to_misaligned_size() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let path = dir.path().join("test_grow_to_misaligned_size");
|
||||
let mut w = FixedIndexWriter::create(&path, None, CS).unwrap();
|
||||
|
||||
let size = (FixedIndexWriter::INITIAL_CAPACITY as u64 + 42) * CS as u64 - 1; // last is not full
|
||||
let expected = test_data(size);
|
||||
|
||||
w.grow_to_size(size).unwrap();
|
||||
assert!(w.grow_to_size(size + 1).is_err(), "size must be fixed now");
|
||||
assert_eq!(expected.len(), w.index_length());
|
||||
assert!(w.add_digest(expected.len(), &[1u8; 32]).is_err());
|
||||
|
||||
for c in expected.iter().rev() {
|
||||
c.add_to(&mut w);
|
||||
}
|
||||
|
||||
w.close().unwrap();
|
||||
drop(w);
|
||||
|
||||
check_with_reader(&path, size, &expected);
|
||||
compare_to_known_size_writer(&path, size, &expected);
|
||||
|
||||
dir.close().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_clone_data_from() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let size = (FixedIndexWriter::INITIAL_CAPACITY as u64 + 3) * CS as u64;
|
||||
let mut expected = test_data(size);
|
||||
|
||||
let reused = dir.path().join("reused");
|
||||
let mut w = FixedIndexWriter::create(&reused, Some(size), CS).unwrap();
|
||||
for c in expected.iter() {
|
||||
c.add_to(&mut w);
|
||||
}
|
||||
w.close().unwrap();
|
||||
drop(w);
|
||||
|
||||
let reused = FixedIndexReader::open(&reused).unwrap();
|
||||
|
||||
let truncated = dir.path().join("truncated");
|
||||
let size = size - CS as u64;
|
||||
expected.pop();
|
||||
let mut w = FixedIndexWriter::create(&truncated, Some(size), CS).unwrap();
|
||||
w.clone_data_from(&reused).unwrap();
|
||||
w.close().unwrap();
|
||||
drop(w);
|
||||
check_with_reader(&truncated, size, &expected);
|
||||
compare_to_known_size_writer(&truncated, size, &expected);
|
||||
|
||||
let modified = dir.path().join("modified");
|
||||
let mut w = FixedIndexWriter::create(&modified, None, CS).unwrap();
|
||||
w.clone_data_from(&reused).unwrap();
|
||||
{
|
||||
let i = expected.len() / 2;
|
||||
expected[i].digest[1] += 1;
|
||||
let chunk = &expected[i];
|
||||
let chunk_pos = chunk.end - chunk.size as u64;
|
||||
w.add_chunk(chunk_pos, chunk.size, &chunk.digest).unwrap();
|
||||
}
|
||||
w.grow_to_size(size).unwrap();
|
||||
w.close().unwrap();
|
||||
drop(w);
|
||||
check_with_reader(&modified, size, &expected);
|
||||
compare_to_known_size_writer(&modified, size, &expected);
|
||||
|
||||
dir.close().unwrap();
|
||||
}
|
||||
|
||||
struct TestChunk {
|
||||
digest: [u8; 32],
|
||||
index: usize,
|
||||
size: u32,
|
||||
end: u64,
|
||||
}
|
||||
|
||||
impl TestChunk {
|
||||
fn add_to(&self, w: &mut FixedIndexWriter) {
|
||||
assert_eq!(
|
||||
self.index,
|
||||
w.check_chunk_alignment(self.end, self.size as u64).unwrap()
|
||||
);
|
||||
w.add_digest(self.index, &self.digest).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn test_data(size: u64) -> Vec<TestChunk> {
|
||||
(0..size.div_ceil(CS as u64))
|
||||
.map(|index| {
|
||||
let mut digest = [0u8; 32];
|
||||
let i = &index.to_le_bytes();
|
||||
for c in digest.chunks_mut(i.len()) {
|
||||
c.copy_from_slice(i);
|
||||
}
|
||||
let size = if ((index + 1) * CS as u64) <= size {
|
||||
CS
|
||||
} else {
|
||||
(size % CS as u64) as u32
|
||||
};
|
||||
TestChunk {
|
||||
digest,
|
||||
index: index as usize,
|
||||
size,
|
||||
end: index * CS as u64 + size as u64,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn check_with_reader(path: &Path, size: u64, chunks: &[TestChunk]) {
|
||||
let reader = FixedIndexReader::open(path).unwrap();
|
||||
assert_eq!(size, reader.index_bytes());
|
||||
assert_eq!(chunks.len(), reader.index_count());
|
||||
for c in chunks {
|
||||
assert_eq!(&c.digest, reader.index_digest(c.index).unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
fn compare_to_known_size_writer(file: &Path, size: u64, chunks: &[TestChunk]) {
|
||||
let mut path = file.to_path_buf();
|
||||
path.set_extension("reference");
|
||||
let mut w = FixedIndexWriter::create(&path, Some(size), CS).unwrap();
|
||||
for c in chunks {
|
||||
c.add_to(&mut w);
|
||||
}
|
||||
w.close().unwrap();
|
||||
drop(w);
|
||||
|
||||
let mut reference = fs::read(file).unwrap();
|
||||
let mut tested = fs::read(path).unwrap();
|
||||
|
||||
// ignore uuid and ctime
|
||||
reference[8..32].fill(0);
|
||||
tested[8..32].fill(0);
|
||||
|
||||
assert_eq!(reference, tested);
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user