Compare commits

...

66 Commits

Author SHA1 Message Date
Fabian Grünbichler
0936e669f9 bump version to 2.4.7-1
Signed-off-by: Fabian Grünbichler <f.gruenbichler@proxmox.com>
2024-06-06 13:47:35 +02:00
Fabian Grünbichler
e16ce82985 bump pxar to 0.10.3
for backported format bailing support

Signed-off-by: Fabian Grünbichler <f.gruenbichler@proxmox.com>
2024-06-06 13:45:08 +02:00
Thomas Lamprecht
dbc9d2c223 bump version to 2.4.6-2
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2024-04-16 13:37:26 +02:00
Thomas Lamprecht
3d9e0b6627 ui: add notice for nearing PBS 2.4 End-of-Life
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2024-04-16 10:40:29 +02:00
Thomas Lamprecht
6751d3787e bump version to 2.4.6-1
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2024-02-01 16:32:36 +01:00
Thomas Lamprecht
80e9b5076c tape: factor getting encryption fingerprint tuple out
makes it a bit more readable as there's less "noise" in the read_label
function and as the separate new fn allows us to nicely use ? to early
return as it has an option in the return signature avoiding 5 lines of
code while not really getting more terse.

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
(cherry picked from commit a7a42de63b)
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2024-02-01 16:31:29 +01:00
Dominik Csapak
9b099edbcd tape: fix regression in restoring key from medium
Since commit 1343dcaf we automatically try to load the key into the
drive after reading the media-set label, this cannot work for the case
where we actually restore the key from the tape itself.

To address this special case while preserving the automatic key
loading, everything except the setup of the key has been separated
from the 'read_label' method into a new function named
'read_label_without_loading_key'. Consequently, the 'restore-key' API
endpoint can be switched to utilize this new method, thereby avoiding
the issue.

Fixes: 1343dcaf ("tape: move 'set_encryption' calls to the TapeDriver")
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
 [ TL: reword and shorten commit message ]
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
(cherry picked from commit a33d795741)
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2024-02-01 16:31:29 +01:00
Thomas Lamprecht
fc126d7fc1 bump version to 2.4.5-1
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2024-01-24 13:33:11 +01:00
Thomas Lamprecht
01d885f987 report: change output contract of functions
let them manage it completely themselves, as we cannot really say if a
code-block fits for the whole output, like it was the case for the
function that returned a limited output of a 'top' process status
command.

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
(cherry picked from commit c597473080)
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2024-01-23 17:07:54 +01:00
Stefan Lendl
326f5e09dd report: add prune.cfg to report
prune.cfg stores prune job configurations

Signed-off-by: Stefan Lendl <s.lendl@proxmox.com>
(cherry picked from commit 6b0d7f3d32)
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2024-01-23 17:07:54 +01:00
Thomas Lamprecht
9796c20e25 report: add extra newline between files of directory output
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
(cherry picked from commit 460c3d1619)
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2024-01-23 17:07:54 +01:00
Thomas Lamprecht
6439200564 report: factor out getting first 30 lines of top output
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
(cherry picked from commit 5736fa917c)
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2024-01-23 17:07:54 +01:00
Thomas Lamprecht
53b80f64b9 system report: fix warning about checking result
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
(cherry picked from commit 164f96a5a6)
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2024-01-23 17:07:54 +01:00
Thomas Lamprecht
16afcb7f14 system report: record stderr output too
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
(cherry picked from commit 122179414c)
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2024-01-23 17:07:54 +01:00
Thomas Lamprecht
7072e8120c system report: factor out getting command output into helper fn
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
(cherry picked from commit c55884d1a7)
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2024-01-23 17:07:54 +01:00
Thomas Lamprecht
7f7a367445 system report: add proxmox-boot-tool status output
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
(cherry picked from commit 3b5cb8fd33)
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2024-01-23 17:07:54 +01:00
Thomas Lamprecht
687c8fb9b1 system report: add basic uptime, usage and process info
Command is copied over from Proxmox VE

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
(cherry picked from commit 49d465c72a)
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2024-01-23 17:07:54 +01:00
Thomas Lamprecht
e88c5584bf system report: add all apt repo files
note, we do not filter by *.list or *.source, so one might get also
files that apt won't read, like .dpkg-dist files, but also those with
typos, and thus possibly helpful when debugging things.

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
(cherry picked from commit 2f7b3e2ecc)
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2024-01-23 17:07:54 +01:00
Thomas Lamprecht
6cbf3e4958 system report: support outputting all files in a directory
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
(cherry picked from commit 0b9614d5a4)
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2024-01-23 17:07:54 +01:00
Thomas Lamprecht
0fbc7a85e1 system report: group files by categories
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
(cherry picked from commit eb32373e3c)
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2024-01-23 17:07:54 +01:00
Thomas Lamprecht
d75cf3d629 system report: switch to markdown-like output
allows one to render this via any of the thousands markdown viewers to
get a better formatting.

We can switch our web ui widget to (optionally) render this as html
when a user is viewing it from the UI too.

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
(cherry picked from commit 20875de2ec)
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2024-01-23 17:07:54 +01:00
Thomas Lamprecht
de086eda25 system report: output ldap and oidc realm list
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
(cherry picked from commit 11c4632bcf)
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2024-01-23 17:07:54 +01:00
Thomas Lamprecht
dfce0d384e system report: add more information to lsblk
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
(cherry picked from commit 84fb190ee2)
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2024-01-23 17:07:54 +01:00
Thomas Lamprecht
2287e64ae6 tape: drop unused has_encryption helper
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
(cherry picked from commit 320b72437c)
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2024-01-23 12:11:46 +01:00
Dominik Csapak
e16f063b41 tape: remove unused methods of LtoTapeHandle
in preparation of making it not public anymore

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
(cherry picked from commit 645f81339f)
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2024-01-23 12:11:46 +01:00
Dominik Csapak
95c1673f6c tape: assert encryption mode when using the PoolWriter
by introducing an 'assert_encryption_mode' that checks the desired
state, and bails out if it's different, called directly where we
previously set the encryption mode (which is now done automatically)

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
 [ TL: add drive_ prefix and fleece in comment ]
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
(cherry picked from commit 3579d724a3)
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2024-01-23 12:11:46 +01:00
Dominik Csapak
a3344b8c65 tape: move 'set_encryption' calls to the TapeDriver (and implementation)
namely everytime we know what the key for the tape has to be:
* after we write the MediaSetLabel
* after reading the MediaSetLabel

When handling data on tape, we always have to have the MediaSetLabel, so
we should always trigger one of these. Because of that, we should not be
able to forget to set the encryption mode.

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
(cherry picked from commit 1343dcaf01)
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2024-01-23 12:11:46 +01:00
Dominik Csapak
c3fcfbbda8 tape: fix wrongly unloading encryption key
For security, we want to automatically unload the encryption key from
the drive when we're done, so there was a Drop handler for SgTape that
handles that. Sadly, our tool we use to set it in the first place, also
invoked the Drop handler, thus unloading the keys again immediately

To fix that, move the Drop handler one logical level higher to the
LtoTapeHandle, which is not used by the 'sg-tape-cmd'.

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
(cherry picked from commit 1dc0df339b)
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2024-01-23 12:11:46 +01:00
Dominik Csapak
b63da904ff tape: don't call sg-tape-cmd for unloading encryption keys
since sg-tape-cmd is only necessary if we want to load the key, we don't
have to call it when we don't have one.

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
(cherry picked from commit 8ee5a5d302)
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2024-01-23 12:11:46 +01:00
Dominik Csapak
386f447a42 tape: use SgTape in sg-tape-cmd
instead of LtoTapeHandle. This way, we can simply always call the binary
from LtoTapeHandle, and don't have to concern ourselves with the sg_tape
calling.

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
(cherry picked from commit f1467d23f3)
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2024-01-23 12:11:46 +01:00
Dominik Csapak
462b01c3ac tape: move key deriving into SgTape
makes the boundary a bit clearer, introduce 'load_key' to load a single
key from the key config

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
(cherry picked from commit 4d17ed496a)
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2024-01-23 12:11:46 +01:00
Dominik Csapak
d56b17479d tape: provide 'open_lto_drive' methods for LtoTapeHandle and SgTape
Prepares for the use in sg-tape-cmd, since we want to use the SgTape
directly instead of LtoTapeHandle.

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
(cherry picked from commit b5f8626706)
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2024-01-23 12:11:42 +01:00
Dominik Csapak
e2d129f9de tape: improve function naming
rename the inner 'set_encryption' in sg_tape to drive_set_encryption,
so that it's a bit clearer where it comes from.

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
(cherry picked from commit 8cc8aa510f)
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2024-01-23 12:05:16 +01:00
Stoiko Ivanov
dc051f2e30 pbs2to3: check for proper grub meta-package for bootmode
ported over from pve-manager:
'pve7to8: check for proper grub meta-package for bootmode'
`67c655b9333714f31d5115de80961a2abc4b6506`

Signed-off-by: Stoiko Ivanov <s.ivanov@proxmox.com>
2023-11-29 15:26:07 +01:00
Stoiko Ivanov
ccf5519a03 pbs2to3: add check for dkms modules
ported over from pve-manager: 'pve7to8: Add check for dkms modules'
`0329876ccf1d78b848897718bb0c2337c6a55fbb`

Signed-off-by: Stoiko Ivanov <s.ivanov@proxmox.com>
2023-11-29 15:26:07 +01:00
Thomas Lamprecht
892d781dbc bump version to 2.4.4-1
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2023-11-10 16:05:43 +01:00
Thomas Lamprecht
fa506db92d docs: faq: move support table to CSV
as the current table format isn't really a recommended way to encode
tables for reStructuredText, and breaks various editor integrations
(and possibly parsing in the future).

From the two supported options, i.e., csv-table and list-table, the
first one seems to be easier to maintain in the long-run, so go for
that.

https://docutils.sourceforge.io/docs/ref/rst/directives.html#csv-table-1

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
(cherry picked from commit 31b121f4b3)
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2023-11-10 16:05:15 +01:00
Thomas Lamprecht
d8320229f1 docs: faq: add reference to support table
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
(cherry picked from commit 4650abb46e)
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2023-11-10 16:05:15 +01:00
Thomas Lamprecht
ff14832915 docs: faq: add entries for how to do minor and major upgrades
Adapted from Proxmox VE's FAQ

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
(cherry picked from commit 5d4f981308)
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2023-11-10 16:05:15 +01:00
Thomas Lamprecht
bf0290a766 docs: update FAQ release support table, add PBS 2.x EOL date
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
(cherry picked from commit c2fa67b417)
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2023-11-10 13:11:13 +01:00
Dominik Csapak
6cff4c6bf6 fix #4977: ui: tape: restore: rework snapshot selection logic
previously, the snapshot grid returned one of three possible types of
values:
* a list of snapshots
* a list of datastores (if only whole datastores were selected)
* the string 'all' (when all snapshots were selected)

this led to some confusing and wrong code, especially the part:
```
  if (source === 'all') {
      source = values.store;
  }
```

which basically set the selected *target* store as a source.  (meaning
it tried restoring a datastore with the selected target name,
regardless if it existed or not)

This fell through in testing, since we most often only restored to the
same datastore anyway were the target and source name were the same.

Rework the return value to return the empty array in case all
snapshots are selected, since selecting none is not a valid anyway.

This means we always get an array back, which makes the code a bit
cleaner overall.

At the same time, we now differentiate correctly the 'all selected'
case, by setting the selected target as a default target.

So instead of previously having `target=target` as datastore
parameter, we now have `target` which is the correct behavior when we
want to restore the whole media set anyway.

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
Tested-by: Mira Limbeck <m.limbeck@proxmox.com>
(cherry picked from commit 3429304733)
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2023-11-10 13:09:13 +01:00
Dominik Csapak
7a7d973f6d ui: tape: restore: improve variable names
some of the variable names did not really tell the full story, so
extend them a bit. This makes the intention much clearer.

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
Tested-by: Mira Limbeck <m.limbeck@proxmox.com>
(cherry picked from commit 4206d6fadb)
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2023-11-10 13:07:04 +01:00
Dominik Csapak
6a031a4ec8 ui: tape: mark incomplete media-sets as such
by counting the returned tapes and compare it to the sequence number.
If the tape count is lower than the highest sequence number plus one,
there must be a tape missing.

Mark it in the text and add the proxmox-warning-row class.

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
(cherry picked from commit be79c2bb6e)
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2023-11-10 13:07:04 +01:00
Gabriel Goller
24734478e1 fix #4895: scheduled jobs: ignore task-log not found error
Use the job start-time as end-time when it's stuck in the
`JobState::Starting` state, no active working is running and the task
log of the last run doesn't exists.

A user experienced a power loss, which left a GC job in the `Started`
state, but the task log did not exist. This breaks the schedule and no
following GC runs. Now, the error is simply ignored and a new gc job
is started on the next occurrence.

Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
(cherry picked from commit 48fbce07df)
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2023-11-10 13:06:14 +01:00
Dominik Csapak
6cc67d99a6 ui: tape restore: fix default namespace mapping
the ui shows the default 'root' namespace as target, but this only
worked when no namespace was selected. as soon as one source datastore
had a target namespace selected, the others datastores would be
skipped as there was no namespace mapping for them. To fix that, we
simply send a default namespace mapping for each source datastore
without a target (no target means 'root')

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
Tested-by: Mira Limbeck <m.limbeck@proxmox.com>
(cherry picked from commit 344db2d6f6)
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2023-11-10 13:05:04 +01:00
Maximiliano Sandoval
b7a43abb50 fix #4638: proxmox-backup-client: status: guard against div by zero
We throw an error if the value for total is zero.

Signed-off-by: Maximiliano Sandoval <m.sandoval@proxmox.com>
(cherry picked from commit dae0b67f1f)
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2023-11-10 12:24:59 +01:00
Philipp Hufnagl
520566200a fix #4971: client: Improve output on successful snapshot deletion
When a snapshot gets deleted (forgotten), the proxmox backup client
currently returns returns
"Result: {
  "data": null
}"

This feedback may confuse users therefore this patch removes the output.

Signed-off-by: Philipp Hufnagl <p.hufnagl@proxmox.com>
2023-11-10 12:21:40 +01:00
Max Carrara
0590aee637 fix #4779: client: add missing "Connection" header for HTTP2 upgrade
This commit adds the missing "Connection: upgrade" HTTP header [1]
when requesting an upgrade to HTTP 2.
Doing so is mandated in the HTTP Semantics RFC [2], and without this,
(reverse) proxies that strictly follow the standard could potentially
break.

[1]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Upgrade
[2]: RFC 9110, 7.8. Upgrade: “[...] sender of Upgrade MUST also send
     an "Upgrade" connection option in the Connection header [...]”

Reported-By: McTwist <rajb89@hotmail.com>
Signed-off-by: Max Carrara <m.carrara@proxmox.com>
 [ TL: added RFC reference and use case to commit message ]
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2023-11-10 12:21:32 +01:00
Gabriel Goller
1ebab6a43f fix #4380: stat() is run when file is executed
When executing `proxmox-backup-client backup ...
--exclude "test/test.txt"` it still executed stat() on "test.txt",
which won't work when the current user doesn't have access to the
file or the parent folder. Now we check if the file is excluded,
and if it is not, then we execute stat().

Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
2023-11-10 12:21:13 +01:00
Gabriel Goller
7b7ceb0c68 fix #4823: datastore: ignore vanished files when walking directory
When walking through a datastore on a GC run, it can
happen that the snapshot is deleted, and then walked over.
For example:
- read dir entry for group
- walk entries (snapshots)
- snapshot X is removed/pruned
- walking reaches snapshot X, but ENOENT
Previously we bailed here, now we just ignore it.
Backups that are just created (and a atomic rename from
tmpdir happens, which might triggers a ENOENT error) are
not a problem here, the GC handles them separately.

Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
(cherry picked from commit 9d1ba51de7)
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2023-11-10 12:19:46 +01:00
Fabian Grünbichler
6a9a5db597 gc: improve lost+found comment
this sounded like we need to skip lost+found to avoid pruning too many chunks,
while the opposite is true - it's safe to skip lost+found on EPERM without
pruning too many chunks, but this is not the case for all EPERM situations..

Signed-off-by: Fabian Grünbichler <f.gruenbichler@proxmox.com>
(cherry picked from commit 8fcd709cf2)
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2023-11-10 12:19:46 +01:00
Hannes Laimer
ef8b0c2528 gc: fix ignoring if lost+found can't be accessed
Signed-off-by: Hannes Laimer <h.laimer@proxmox.com>

Fixes: e2516f61a860eae59395a3a990d9abe4c445ce8c
Signed-off-by: Fabian Grünbichler <f.gruenbichler@proxmox.com>
(cherry picked from commit 0d69dcb497)
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2023-11-10 12:19:46 +01:00
Thomas Lamprecht
6c47db412f datastore: list images: reduce indentation depth on error checking
Simply pull out the inner IO error and the affected path first.

Clean up style-wise a bit while touching this anyway, but no semantic
change intended.

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
(cherry picked from commit 5b16dffcf2)
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2023-11-10 12:19:46 +01:00
Stoiko Ivanov
af63d6f9b5 pbs2to3: fix boot-mode detection
/sys/firmware/efi is a directory and std::path::Path seems to detect
only regular files with is_file [0].

Reported in our Enterprise support portal.

Quickly tested the fix on a VM.

https://doc.rust-lang.org/stable/std/path/struct.Path.html#method.is_file

Signed-off-by: Stoiko Ivanov <s.ivanov@proxmox.com>
2023-08-11 09:22:23 +02:00
Thomas Lamprecht
682bb42edd bump version to 2.4.3-1
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2023-06-29 15:43:46 +02:00
Thomas Lamprecht
5f7852f5a1 pbs2to3: do not log warnings in bold
for consistency with both, the summary but also PVE and PMG checker
scripts

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2023-06-29 15:43:46 +02:00
Thomas Lamprecht
2997cff520 pbs2to3: add manual page
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2023-06-29 14:09:39 +02:00
Thomas Lamprecht
690d061d76 pbs2to3: install manually to avoid empty shell-completion script
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2023-06-29 14:09:39 +02:00
Christian Ebner
7feefff6c5 pbs2to3: set failure messages to bold
... and reset instead of setting color to clear settings

Signed-off-by: Christian Ebner <c.ebner@proxmox.com>
2023-06-29 14:09:39 +02:00
Christian Ebner
ab1f559f1e pbs2to3: fix whitespaces
Signed-off-by: Christian Ebner <c.ebner@proxmox.com>
2023-06-29 14:09:39 +02:00
Christian Ebner
e2cdac6e70 pbs2to3: fix typo s/neighter/neither/
Signed-off-by: Christian Ebner <c.ebner@proxmox.com>
2023-06-29 14:09:39 +02:00
Thomas Lamprecht
6733f1283b cargo: bump proxmox-apt to 0.9.4
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2023-06-28 17:34:35 +02:00
Christian Ebner
9eaded8572 pbs2to3: add upgrade checker binary
Adds the pbs2to3 upgrade checker with some basic checks.

Signed-off-by: Christian Ebner <c.ebner@proxmox.com>
Reviewed-by: Fiona Ebner <f.ebner@proxmox.com>
Tested-by: Fiona Ebner <f.ebner@proxmox.com>
2023-06-28 15:44:51 +02:00
Thomas Lamprecht
891f6a6fe4 bump version to 2.4.2-2
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2023-06-06 13:16:44 +02:00
Stefan Sterz
93088816c7 buildsys: switch from '\s' as a whitespace matcher to [[:space:]]
previously the build process was broken for some versions of `awk`
(most notably `mawk`) as they did not understand the shorthand `\s`
notation for matching a whitspace. use the more universal and more
explicit `[[:space:]]` instead.

Signed-off-by: Stefan Sterz <s.sterz@proxmox.com>
(cherry picked from commit 7075496424)
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2023-06-06 13:16:07 +02:00
Dominik Csapak
f4c54e9917 ui: tape: fix restore datastore mapping parameter construction
We recently took into account the selected datastore when restoring
from tape, but the snapshot grids value may not only be a single
datastore, it can also be a list of snapshots, datastores or 'all'.

Handle these cases and extract the source datastore correctly.

This fixes tape restoration when not a whole datastore is selected.

Reported in the forum:
https://forum.proxmox.com/threads/restore-from-lto-parameter-verification-errors-store.128445

Fixes: df881ed0 ("ui: tape: fix restoring a single datastore")
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
(cherry picked from commit 73bd988c42)
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2023-06-06 13:15:01 +02:00
33 changed files with 1293 additions and 318 deletions

View File

@ -1,5 +1,5 @@
[workspace.package]
version = "2.4.2"
version = "2.4.7"
authors = [
"Dietmar Maurer <dietmar@proxmox.com>",
"Dominik Csapak <d.csapak@proxmox.com>",
@ -82,9 +82,9 @@ proxmox-uuid = "1"
# other proxmox crates
pathpatterns = "0.1.2"
proxmox-acme-rs = "0.4"
proxmox-apt = "0.9.0"
proxmox-apt = "0.9.4"
proxmox-openid = "0.9.9"
pxar = "0.10.2"
pxar = "0.10.3"
# PBS workspace
pbs-api-types = { path = "pbs-api-types" }

View File

@ -33,7 +33,7 @@ RESTORE_BIN := \
SUBCRATES != cargo metadata --no-deps --format-version=1 \
| jq -r .workspace_members'[]' \
| awk '!/^proxmox-backup\s/ { printf "%s ", $$1 }'
| awk '!/^proxmox-backup[[:space:]]/ { printf "%s ", $$1 }'
ifeq ($(BUILD_MODE), release)
CARGO_BUILD_ARGS += --release
@ -195,6 +195,7 @@ install: $(COMPILED_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 -dm755 $(DESTDIR)$(LIBEXECDIR)/proxmox-backup
install -dm755 $(DESTDIR)$(LIBEXECDIR)/proxmox-backup/file-restore
$(foreach i,$(RESTORE_BIN), \

78
debian/changelog vendored
View File

@ -1,3 +1,81 @@
rust-proxmox-backup (2.4.7-1) bullseye; urgency=medium
* rebuild with pxar 0.10.3 for nicer error messages on pxar v2 archives
-- Proxmox Support Team <support@proxmox.com> Thu, 06 Jun 2024 13:47:23 +0200
rust-proxmox-backup (2.4.6-2) bullseye; urgency=medium
* ui: add notice for the nearing PBS 2.4 End-of-Life on 2024-07-31
-- Proxmox Support Team <support@proxmox.com> Tue, 16 Apr 2024 13:36:58 +0200
rust-proxmox-backup (2.4.6-1) bullseye; urgency=medium
* tape: fix regression in restoring an encryption key from medium, avoid
trying to load the key to the drive, which cannot work in this special
case.
-- Proxmox Support Team <support@proxmox.com> Thu, 01 Feb 2024 16:32:22 +0100
rust-proxmox-backup (2.4.5-1) bullseye; urgency=medium
* pbs2to3: add check for dkms modules
* pbs2to3: check for proper grub meta-package for bootmode
* system report: switch to markdown-like output syntax to make it easier to
digest
* system report: add information about block devices, basic uptime, usage
and process info, all apt repo files, proxmox-boot-tool status output, and
ldap and oidc realm list and prune configuration
* tape: rework on-drive encryption key handling and ensure this key does not
gets unloaded to early
-- Proxmox Support Team <support@proxmox.com> Wed, 24 Jan 2024 13:32:38 +0100
rust-proxmox-backup (2.4.4-1) bullseye; urgency=medium
* pbs2to3: fix boot-mode detection
* fix #4823: datastore: ignore vanished files when walking directory
* fix #4380: stat() is run when file is executed
* fix #4779: client: add missing "Connection" header for HTTP2 upgrade
* fix #4971: client: Improve output on successful snapshot deletion
* fix #4638: proxmox-backup-client: status: guard against div by zero
* 6cc67d99 ui: tape restore: fix default namespace mapping
* fix #4895: scheduled jobs: ignore task-log not found error
* ui: tape: mark incomplete media-sets as such
* fix #4977: ui: tape: restore: rework snapshot selection logic
* docs: update FAQ release support table, add PBS 2.x EOL date
-- Proxmox Support Team <support@proxmox.com> Fri, 10 Nov 2023 16:05:35 +0100
rust-proxmox-backup (2.4.3-1) bullseye; urgency=medium
* add pbs2to3 major-upgrade checker binary
* cargo: bump proxmox-apt to 0.9.4 to improve repository API during upgrade
-- Proxmox Support Team <support@proxmox.com> Wed, 28 Jun 2023 18:55:03 +0200
rust-proxmox-backup (2.4.2-2) bullseye; urgency=medium
* ui: tape: fix restore datastore mapping parameter construction
-- Proxmox Support Team <support@proxmox.com> Tue, 06 Jun 2023 13:16:41 +0200
rust-proxmox-backup (2.4.2-1) bullseye; urgency=medium
* docs: dark mode: adapt background for bottom links in mobile view

4
debian/control vendored
View File

@ -42,7 +42,7 @@ Build-Depends: debhelper (>= 12),
librust-percent-encoding-2+default-dev (>= 2.1-~~),
librust-pin-project-lite-0.2+default-dev,
librust-proxmox-acme-rs-0.4+default-dev,
librust-proxmox-apt-0.9+default-dev,
librust-proxmox-apt-0.9+default-dev (>= 0.9.4),
librust-proxmox-async-0.4+default-dev,
librust-proxmox-auth-api-0.1+api-dev,
librust-proxmox-auth-api-0.1+api-types-dev,
@ -91,7 +91,7 @@ Build-Depends: debhelper (>= 12),
librust-proxmox-time-1+default-dev (>= 1.1.2-~~),
librust-proxmox-uuid-1+default-dev,
librust-proxmox-uuid-1+serde-dev,
librust-pxar-0.10+default-dev (>= 0.10.2-~~),
librust-pxar-0.10+default-dev (>= 0.10.3-~~),
librust-regex-1+default-dev (>= 1.5.5-~~),
librust-rustyline-9+default-dev,
librust-serde-1+default-dev,

View File

@ -11,6 +11,7 @@ usr/lib/x86_64-linux-gnu/proxmox-backup/proxmox-daily-update
usr/lib/x86_64-linux-gnu/proxmox-backup/sg-tape-cmd
usr/sbin/proxmox-backup-debug
usr/sbin/proxmox-backup-manager
usr/sbin/pbs2to3
usr/bin/pmtx
usr/bin/pmt
usr/bin/proxmox-tape
@ -24,6 +25,7 @@ usr/share/man/man1/proxmox-backup-proxy.1
usr/share/man/man1/proxmox-tape.1
usr/share/man/man1/pmtx.1
usr/share/man/man1/pmt.1
usr/share/man/man1/pbs2to3.1
usr/share/man/man5/acl.cfg.5
usr/share/man/man5/datastore.cfg.5
usr/share/man/man5/domains.cfg.5

View File

@ -30,7 +30,8 @@ MAN1_PAGES := \
proxmox-backup-client.1 \
proxmox-backup-manager.1 \
proxmox-file-restore.1 \
proxmox-backup-debug.1
proxmox-backup-debug.1 \
pbs2to3.1 \
MAN5_PAGES := \
media-pool.cfg.5 \

View File

@ -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),
# configs
('config/acl/man5', 'acl.cfg', 'Access Control Configuration', [author], 5),
('config/datastore/man5', 'datastore.cfg', 'Datastore Configuration', [author], 5),

View File

@ -0,0 +1,4 @@
Proxmox Backup Version , Debian Version , First Release , Debian EOL , Proxmox Backup EOL
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
1 Proxmox Backup Version Debian Version First Release Debian EOL Proxmox Backup EOL
2 Proxmox Backup 3 Debian 12 (Bookworm) 2023-06 TBA TBA
3 Proxmox Backup 2 Debian 11 (Bullseye) 2021-07 2024-07 2024-07
4 Proxmox Backup 1 Debian 10 (Buster) 2020-11 2022-08 2022-07

View File

@ -21,17 +21,54 @@ Proxmox Backup Server only supports 64-bit CPUs (AMD or Intel). There are no
future plans to support 32-bit processors.
.. _faq-support-table:
How long will my Proxmox Backup Server version be supported?
------------------------------------------------------------
+-----------------------+----------------------+---------------+------------+--------------------+
|Proxmox Backup Version | Debian Version | First Release | Debian EOL | Proxmox Backup EOL |
+=======================+======================+===============+============+====================+
|Proxmox Backup 2.x | Debian 11 (Bullseye) | 2021-07 | tba | tba |
+-----------------------+----------------------+---------------+------------+--------------------+
|Proxmox Backup 1.x | Debian 10 (Buster) | 2020-11 | 2022-08 | 2022-07 |
+-----------------------+----------------------+---------------+------------+--------------------+
.. csv-table:: Table Title
:file: faq-release-support-table.csv
:widths: 30 26 13 13 18
:header-rows: 1
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/index.php/Roadmap>`_ for any relevant noteable,
or breaking change.
For the update itself use either the Web UI *Node -> Updates* panel or
through the CLI with:
.. code-block:: console
apt update
apt full-upgrade
.. note:: Always ensure you correctly setup the
:ref:`package repositories <sysadmin_package_repositories>` and only
continue with the actual upgrade if `apt update` did not hit any error.
.. _faq-upgrade-major:
How can I upgrade Proxmox Backup Server to the next major release?
------------------------------------------------------------------
Major version upgrades, for example going from Proxmox Backup Server 2.4 to
3.1, are also supported.
They must be carefully planned and tested and should **never** be started
without having an off-site copy of the important backups, e.g., via remote sync
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 2 to 3 <https://pbs.proxmox.com/wiki/index.php/Upgrade_from_2_to_3>`_
* `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?
-----------------------------------------------------------

14
docs/pbs2to3/man1.rst Normal file
View File

@ -0,0 +1,14 @@
=======
pbs2to3
=======
Description
===========
This tool will help you to detect common pitfalls and misconfguration before,
and during the upgrade of a Proxmox VE system Any failure must be addressed
before the upgrade, and any waring must be addressed, or at least carefully
evaluated, if a false-positive is suspected
.. include:: ../pbs-copyright.rst

View File

@ -764,6 +764,8 @@ impl HttpClient {
);
}
req.headers_mut()
.insert("Connection", HeaderValue::from_str("upgrade").unwrap());
req.headers_mut()
.insert("UPGRADE", HeaderValue::from_str(&protocol_name).unwrap());

View File

@ -434,6 +434,15 @@ impl Archiver {
assert_single_path_component(os_file_name)?;
let full_path = self.path.join(os_file_name);
let match_path = PathBuf::from("/").join(full_path.clone());
if self
.patterns
.matches(match_path.as_os_str().as_bytes(), None)
== Some(MatchType::Exclude)
{
continue;
}
let stat = match nix::sys::stat::fstatat(
dir_fd,
file_name.as_c_str(),
@ -444,15 +453,6 @@ impl Archiver {
Err(err) => bail!("stat failed on {:?}: {}", full_path, err),
};
let match_path = PathBuf::from("/").join(full_path.clone());
if self
.patterns
.matches(match_path.as_os_str().as_bytes(), Some(stat.st_mode))
== Some(MatchType::Exclude)
{
continue;
}
self.entry_counter += 1;
if self.entry_counter > self.entry_limit {
bail!(

View File

@ -866,26 +866,26 @@ impl DataStore {
.unwrap_or(false)
}
let handle_entry_err = |err: walkdir::Error| {
if let Some(inner) = err.io_error() {
if let Some(path) = err.path() {
if inner.kind() == io::ErrorKind::PermissionDenied {
// only allow to skip ext4 fsck directory, avoid GC if, for example,
// a user got file permissions wrong on datastore rsync to new server
if err.depth() > 1 || !path.ends_with("lost+found") {
bail!("cannot continue garbage-collection safely, permission denied on: {:?}", path)
}
} else {
bail!(
"unexpected error on datastore traversal: {} - {:?}",
inner,
path
)
}
} else {
bail!("unexpected error on datastore traversal: {}", inner)
// first, extract the actual IO error and the affected path
let (inner, path) = match (err.io_error(), err.path()) {
(None, _) => return Ok(()), // not an IO-error
(Some(inner), Some(path)) => (inner, path),
(Some(inner), None) => bail!("unexpected error on datastore traversal: {inner}"),
};
if inner.kind() == io::ErrorKind::PermissionDenied {
if err.depth() <= 1 && path.ends_with("lost+found") {
// allow skipping of (root-only) ext4 fsck-directory on EPERM ..
return Ok(());
}
// .. but do not ignore EPERM in general, otherwise we might prune too many chunks.
// E.g., if users messed up with owner/perms on a rsync
bail!("cannot continue garbage-collection safely, permission denied on: {path:?}");
} else if inner.kind() == io::ErrorKind::NotFound {
log::info!("ignoring vanished file: {path:?}");
return Ok(());
} else {
bail!("unexpected error on datastore traversal: {inner} - {path:?}");
}
Ok(())
};
for entry in walker.filter_entry(|e| !is_hidden(e)) {
let path = match entry {

View File

@ -14,6 +14,7 @@ lazy_static.workspace = true
libc.workspace = true
log.workspace = true
nix.workspace = true
openssl.workspace = true
regex.workspace = true
serde.workspace = true
serde_json.workspace = true

View File

@ -9,9 +9,10 @@ use endian_trait::Endian;
use nix::fcntl::{fcntl, FcntlArg, OFlag};
mod encryption;
pub use encryption::*;
pub use encryption::{drive_set_encryption, drive_get_encryption};
mod volume_statistics;
use proxmox_uuid::Uuid;
pub use volume_statistics::*;
mod tape_alert_flags;
@ -26,8 +27,9 @@ pub use report_density::*;
use proxmox_io::{ReadExt, WriteExt};
use proxmox_sys::error::SysResult;
use pbs_api_types::{Lp17VolumeStatistics, LtoDriveAndMediaStatus, MamAttribute};
use pbs_api_types::{Lp17VolumeStatistics, LtoDriveAndMediaStatus, LtoTapeDrive, MamAttribute};
use crate::linux_list_drives::open_lto_tape_device;
use crate::{
sgutils2::{
alloc_page_aligned_buffer, scsi_cmd_mode_select10, scsi_cmd_mode_select6, scsi_inquiry,
@ -102,7 +104,6 @@ pub struct SgTape {
file: File,
locate_offset: Option<i64>,
info: InquiryInfo,
encryption_key_loaded: bool,
}
impl SgTape {
@ -124,11 +125,47 @@ impl SgTape {
Ok(Self {
file,
info,
encryption_key_loaded: false,
locate_offset: None,
})
}
/// Open a tape device
///
/// This does additional checks:
///
/// - check if it is a non-rewinding tape device
/// - check if drive is ready (tape loaded)
/// - check block size
/// - for autoloader only, try to reload ejected tapes
pub fn open_lto_drive(config: &LtoTapeDrive) -> Result<Self, Error> {
proxmox_lang::try_block!({
let file = open_lto_tape_device(&config.path)?;
let mut handle = SgTape::new(file)?;
if handle.test_unit_ready().is_err() {
// for autoloader only, try to reload ejected tapes
if config.changer.is_some() {
let _ = handle.load(); // just try, ignore error
}
}
handle.wait_until_ready()?;
handle.set_default_options()?;
Ok(handle)
})
.map_err(|err: Error| {
format_err!(
"open drive '{}' ({}) failed - {}",
config.name,
config.path,
err
)
})
}
/// Access to file descriptor - useful for testing
pub fn file_mut(&mut self) -> &mut File {
&mut self.file
@ -603,10 +640,28 @@ impl SgTape {
read_volume_statistics(&mut self.file)
}
pub fn set_encryption(&mut self, key: Option<[u8; 32]>) -> Result<(), Error> {
self.encryption_key_loaded = key.is_some();
pub fn set_encryption(&mut self, key_data: Option<([u8; 32], Uuid)>) -> Result<(), Error> {
let key = if let Some((ref key, ref uuid)) = key_data {
// derive specialized key for each media-set
set_encryption(&mut self.file, key)
let mut tape_key = [0u8; 32];
let uuid_bytes: [u8; 16] = *uuid.as_bytes();
openssl::pkcs5::pbkdf2_hmac(
key,
&uuid_bytes,
10,
openssl::hash::MessageDigest::sha256(),
&mut tape_key,
)?;
Some(tape_key)
} else {
None
};
drive_set_encryption(&mut self.file, key)
}
// Note: use alloc_page_aligned_buffer to alloc data transfer buffer
@ -960,15 +1015,6 @@ impl SgTape {
}
}
impl Drop for SgTape {
fn drop(&mut self) {
// For security reasons, clear the encryption key
if self.encryption_key_loaded {
let _ = self.set_encryption(None);
}
}
}
pub struct SgTapeReader<'a> {
sg_tape: &'a mut SgTape,
end_of_file: bool,

View File

@ -8,21 +8,10 @@ use proxmox_io::{ReadExt, WriteExt};
use crate::sgutils2::{alloc_page_aligned_buffer, SgRaw};
/// Test if drive supports hardware encryption
///
/// We search for AES_GCM algorithm with 256bits key.
pub fn has_encryption<F: AsRawFd>(file: &mut F) -> bool {
let data = match sg_spin_data_encryption_caps(file) {
Ok(data) => data,
Err(_) => return false,
};
decode_spin_data_encryption_caps(&data).is_ok()
}
/// Set or clear encryption key
///
/// We always use mixed mode,
pub fn set_encryption<F: AsRawFd>(file: &mut F, key: Option<[u8; 32]>) -> Result<(), Error> {
pub fn drive_set_encryption<F: AsRawFd>(file: &mut F, key: Option<[u8; 32]>) -> Result<(), Error> {
let data = match sg_spin_data_encryption_caps(file) {
Ok(data) => data,
Err(_) if key.is_none() => {
@ -57,6 +46,27 @@ pub fn set_encryption<F: AsRawFd>(file: &mut F, key: Option<[u8; 32]>) -> Result
bail!("got unexpected encryption mode {:?}", status.mode);
}
/// Returns if encryption is enabled on the drive
pub fn drive_get_encryption<F: AsRawFd>(file: &mut F) -> Result<bool, Error> {
let data = match sg_spin_data_encryption_status(file) {
Ok(data) => data,
Err(_) => {
// Assume device does not support HW encryption
return Ok(false);
}
};
let status = decode_spin_data_encryption_status(&data)?;
match status.mode {
// these three below have all encryption enabled, and only differ in how decryption is
// handled
DataEncryptionMode::On => Ok(true),
DataEncryptionMode::Mixed => Ok(true),
DataEncryptionMode::RawRead => Ok(true),
// currently, the mode below is the only one that has encryption actually disabled
DataEncryptionMode::Off => Ok(false),
}
}
#[derive(Endian)]
#[repr(C, packed)]
struct SspSetDataEncryptionPage {
@ -187,7 +197,7 @@ fn sg_spin_data_encryption_caps<F: AsRawFd>(file: &mut F) -> Result<Vec<u8>, Err
.map(|v| v.to_vec())
}
#[derive(Debug)]
#[derive(Debug, PartialEq, Eq)]
enum DataEncryptionMode {
On,
Mixed,

View File

@ -1590,9 +1590,12 @@ async fn status(param: Value) -> Result<Value, Error> {
let v = v.as_u64().unwrap();
let total = record["total"].as_u64().unwrap();
let roundup = total / 200;
let per = ((v + roundup) * 100) / total;
let info = format!(" ({} %)", per);
Ok(format!("{} {:>8}", v, info))
if let Some(per) = ((v + roundup) * 100).checked_div(total) {
let info = format!(" ({} %)", per);
Ok(format!("{} {:>8}", v, info))
} else {
bail!("Cannot render total percentage: denominator is zero");
}
};
let options = default_table_format_options()

View File

@ -188,13 +188,13 @@ async fn forget_snapshots(param: Value) -> Result<Value, Error> {
let path = format!("api2/json/admin/datastore/{}/snapshots", repo.store());
let result = client
client
.delete(&path, Some(snapshot_args(&backup_ns, &snapshot)?))
.await?;
record_repository(&repo);
Ok(result)
Ok(Value::Null)
}
#[api(

View File

@ -36,8 +36,7 @@ use crate::{
changer::update_changer_online_status,
drive::{
get_tape_device_state, lock_tape_device, media_changer, open_drive,
open_lto_tape_drive, required_media_changer, set_tape_device_state, LtoTapeHandle,
TapeDriver,
required_media_changer, set_tape_device_state, LtoTapeHandle, TapeDriver,
},
encryption_keys::insert_key,
file_formats::{MediaLabel, MediaSetLabel},
@ -610,7 +609,7 @@ pub async fn restore_key(drive: String, password: String) -> Result<(), Error> {
run_drive_blocking_task(drive.clone(), "restore key".to_string(), move |config| {
let mut drive = open_drive(&config, &drive)?;
let (_media_id, key_config) = drive.read_label()?;
let (_media_id, key_config) = drive.read_label_without_loading_key()?;
if let Some(key_config) = key_config {
let password_fn = || Ok(password.as_bytes().to_vec());
@ -657,9 +656,6 @@ pub async fn read_label(drive: String, inventorize: Option<bool>) -> Result<Medi
let label = if let Some(ref set) = media_id.media_set_label {
let key = &set.encryption_key_fingerprint;
if let Err(err) = drive.set_encryption(key.clone().map(|fp| (fp, set.uuid.clone()))) {
eprintln!("unable to load encryption key: {}", err); // best-effort only
}
MediaIdFlat {
ctime: media_id.label.ctime,
encryption_key_fingerprint: key.as_ref().map(|fp| fp.signature()),
@ -1144,7 +1140,7 @@ pub async fn cartridge_memory(drive: String) -> Result<Vec<MamAttribute>, Error>
"reading cartridge memory".to_string(),
move |config| {
let drive_config: LtoTapeDrive = config.lookup("lto", &drive)?;
let mut handle = open_lto_tape_drive(&drive_config)?;
let mut handle = LtoTapeHandle::open_lto_drive(&drive_config)?;
handle.cartridge_memory()
},
@ -1174,7 +1170,7 @@ pub async fn volume_statistics(drive: String) -> Result<Lp17VolumeStatistics, Er
"reading volume statistics".to_string(),
move |config| {
let drive_config: LtoTapeDrive = config.lookup("lto", &drive)?;
let mut handle = open_lto_tape_drive(&drive_config)?;
let mut handle = LtoTapeHandle::open_lto_drive(&drive_config)?;
handle.volume_statistics()
},
@ -1311,12 +1307,6 @@ pub fn catalog_media(
inventory.store(media_id.clone(), false)?;
return Ok(());
}
let encrypt_fingerprint = set
.encryption_key_fingerprint
.clone()
.map(|fp| (fp, set.uuid.clone()));
drive.set_encryption(encrypt_fingerprint)?;
let _pool_lock = lock_media_pool(TAPE_STATUS_DIR, &set.pool)?;
let media_set_lock = lock_media_set(TAPE_STATUS_DIR, &set.uuid, None)?;

View File

@ -1029,12 +1029,6 @@ fn restore_snapshots_to_tmpdir(
media_set_uuid
);
}
let encrypt_fingerprint = set.encryption_key_fingerprint.clone().map(|fp| {
task_log!(worker, "Encryption key fingerprint: {}", fp);
(fp, set.uuid.clone())
});
drive.set_encryption(encrypt_fingerprint)?;
}
}
@ -1279,12 +1273,6 @@ pub fn request_and_restore_media(
media_set_uuid
);
}
let encrypt_fingerprint = set
.encryption_key_fingerprint
.clone()
.map(|fp| (fp, set.uuid.clone()));
drive.set_encryption(encrypt_fingerprint)?;
}
}

633
src/bin/pbs2to3.rs Normal file
View File

@ -0,0 +1,633 @@
use std::io::Write;
use std::path::Path;
use anyhow::{format_err, Error};
use regex::Regex;
use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor};
use proxmox_apt::repositories::{self, APTRepositoryFile, APTRepositoryPackageType};
use proxmox_backup::api2::node::apt;
const OLD_SUITE: &str = "bullseye";
const NEW_SUITE: &str = "bookworm";
const PROXMOX_BACKUP_META: &str = "proxmox-backup";
const MIN_PBS_MAJOR: u8 = 2;
const MIN_PBS_MINOR: u8 = 4;
const MIN_PBS_PKGREL: u8 = 1;
fn main() -> Result<(), Error> {
let mut checker = Checker::new();
checker.check_pbs_packages()?;
checker.check_misc()?;
checker.summary()?;
Ok(())
}
struct Checker {
output: ConsoleOutput,
upgraded: bool,
}
impl Checker {
pub fn new() -> Self {
Self {
output: ConsoleOutput::new(),
upgraded: false,
}
}
pub fn check_pbs_packages(&mut self) -> Result<(), Error> {
self.output
.print_header("CHECKING VERSION INFORMATION FOR PBS PACKAGES")?;
self.check_upgradable_packages()?;
let pkg_versions = apt::get_versions()?;
self.check_meta_package_version(&pkg_versions)?;
self.check_kernel_compat(&pkg_versions)?;
Ok(())
}
fn check_upgradable_packages(&mut self) -> Result<(), Error> {
self.output.log_info("Checking for package updates..")?;
let result = Self::get_upgradable_packages();
match result {
Err(err) => {
self.output.log_warn(format!("{err}"))?;
self.output
.log_fail("unable to retrieve list of package updates!")?;
}
Ok(cache) => {
if cache.package_status.is_empty() {
self.output.log_pass("all packages up-to-date")?;
} else {
let pkgs = cache
.package_status
.iter()
.map(|pkg| pkg.package.clone())
.collect::<Vec<String>>()
.join(", ");
self.output.log_warn(format!(
"updates for the following packages are available:\n {pkgs}",
))?;
}
}
}
Ok(())
}
fn check_meta_package_version(
&mut self,
pkg_versions: &[pbs_api_types::APTUpdateInfo],
) -> Result<(), Error> {
self.output
.log_info("Checking proxmox backup server package version..")?;
let pbs_meta_pkg = pkg_versions
.iter()
.find(|pkg| pkg.package.as_str() == PROXMOX_BACKUP_META);
if let Some(pbs_meta_pkg) = pbs_meta_pkg {
let pkg_version = Regex::new(r"^(\d+)\.(\d+)[.-](\d+)")?;
let captures = pkg_version.captures(&pbs_meta_pkg.old_version);
if let Some(captures) = captures {
let maj = Self::extract_version_from_captures(1, &captures)?;
let min = Self::extract_version_from_captures(2, &captures)?;
let pkgrel = Self::extract_version_from_captures(3, &captures)?;
if maj > MIN_PBS_MAJOR {
self.output
.log_pass(format!("Already upgraded to Proxmox Backup Server {maj}"))?;
self.upgraded = true;
} else if maj >= MIN_PBS_MAJOR && min >= MIN_PBS_MINOR && pkgrel >= MIN_PBS_PKGREL {
self.output.log_pass(format!(
"'{}' has version >= {}.{}-{}",
PROXMOX_BACKUP_META, MIN_PBS_MAJOR, MIN_PBS_MINOR, MIN_PBS_PKGREL,
))?;
} else {
self.output.log_fail(format!(
"'{}' package is too old, please upgrade to >= {}.{}-{}",
PROXMOX_BACKUP_META, MIN_PBS_MAJOR, MIN_PBS_MINOR, MIN_PBS_PKGREL,
))?;
}
} else {
self.output.log_fail(format!(
"could not match the '{PROXMOX_BACKUP_META}' package version, \
is it installed?",
))?;
}
} else {
self.output
.log_fail(format!("'{PROXMOX_BACKUP_META}' package not found!"))?;
}
Ok(())
}
fn check_kernel_compat(
&mut self,
pkg_versions: &[pbs_api_types::APTUpdateInfo],
) -> Result<(), Error> {
self.output.log_info("Check running kernel version..")?;
let (krunning, kinstalled) = if self.upgraded {
(
Regex::new(r"^6\.(?:2\.(?:[2-9]\d+|1[6-8]|1\d\d+)|5)[^~]*$")?,
"pve-kernel-6.2",
)
} else {
(Regex::new(r"^(?:5\.(?:13|15)|6\.2)")?, "pve-kernel-5.15")
};
let output = std::process::Command::new("uname").arg("-r").output();
match output {
Err(_err) => self
.output
.log_fail("unable to determine running kernel version.")?,
Ok(ret) => {
let running_version = std::str::from_utf8(&ret.stdout[..ret.stdout.len() - 1])?;
if krunning.is_match(running_version) {
if self.upgraded {
self.output.log_pass(format!(
"running new kernel '{running_version}' after upgrade."
))?;
} else {
self.output.log_pass(format!(
"running kernel '{running_version}' is considered suitable for \
upgrade."
))?;
}
} else {
let installed_kernel = pkg_versions
.iter()
.find(|pkg| pkg.package.as_str() == kinstalled);
if installed_kernel.is_some() {
self.output.log_warn(format!(
"a suitable kernel '{kinstalled}' is installed, but an \
unsuitable '{running_version}' is booted, missing reboot?!",
))?;
} else {
self.output.log_warn(format!(
"unexpected running and installed kernel '{running_version}'.",
))?;
}
}
}
}
Ok(())
}
fn extract_version_from_captures(
index: usize,
captures: &regex::Captures,
) -> Result<u8, Error> {
if let Some(capture) = captures.get(index) {
let val = capture.as_str().parse::<u8>()?;
Ok(val)
} else {
Ok(0)
}
}
fn check_bootloader(&mut self) -> Result<(), Error> {
self.output
.log_info("Checking bootloader configuration...")?;
if !Path::new("/sys/firmware/efi").is_dir() {
self.output
.log_skip("System booted in legacy-mode - no need for systemd-boot")?;
return Ok(());
}
if Path::new("/etc/kernel/proxmox-boot-uuids").is_file() {
// PBS packages version check needs to be run before
if !self.upgraded {
self.output
.log_skip("not yet upgraded, no need to check the presence of systemd-boot")?;
return Ok(());
}
if Path::new("/usr/share/doc/systemd-boot/changelog.Debian.gz").is_file() {
self.output.log_pass("bootloader packages installed correctly")?;
return Ok(());
}
self.output.log_warn(
"proxmox-boot-tool is used for bootloader configuration in uefi mode \
but the separate systemd-boot package, is not installed.\n\
initializing new ESPs will not work unitl the package is installed.",
)?;
return Ok(());
} else if !Path::new("/usr/share/doc/grub-efi-amd64/changelog.Debian.gz").is_file() {
self.output.log_warn(
"System booted in uefi mode but grub-efi-amd64 meta-package not installed, \
new grub versions will not be installed to /boot/efi!
Install grub-efi-amd64."
)?;
return Ok(());
} else {
self.output.log_pass("bootloader packages installed correctly")?;
}
Ok(())
}
fn check_apt_repos(&mut self) -> Result<(), Error> {
self.output
.log_info("Checking for package repository suite mismatches..")?;
let mut strange_suite = false;
let mut mismatches = Vec::new();
let mut found_suite: Option<(String, String)> = None;
let (repo_files, _repo_errors, _digest) = repositories::repositories()?;
for repo_file in repo_files {
self.check_repo_file(
&mut found_suite,
&mut mismatches,
&mut strange_suite,
repo_file,
)?;
}
match (mismatches.is_empty(), strange_suite) {
(true, false) => self.output.log_pass("found no suite mismatch")?,
(true, true) => self
.output
.log_notice("found no suite mismatches, but found at least one strange suite")?,
(false, _) => {
let mut message = String::from(
"Found mixed old and new packages repository suites, fix before upgrading!\
\n Mismatches:",
);
for (suite, location) in mismatches.iter() {
message.push_str(
format!("\n found suite '{suite}' at '{location}'").as_str(),
);
}
message.push('\n');
self.output.log_fail(message)?
}
}
Ok(())
}
fn check_dkms_modules(&mut self) -> Result<(), Error> {
let kver = std::process::Command::new("uname")
.arg("-r")
.output()
.map_err(|err| format_err!("failed to retrieve running kernel version - {err}"))?;
let output = std::process::Command::new("dkms")
.arg("status")
.arg("-k")
.arg(std::str::from_utf8(&kver.stdout)?)
.output();
match output {
Err(_err) => self.output.log_skip("could not get dkms status")?,
Ok(ret) => {
let num_dkms_modules = std::str::from_utf8(&ret.stdout)?.lines().count();
if num_dkms_modules == 0 {
self.output.log_pass("no dkms modules found")?;
} else {
self.output.log_warn("dkms modules found, this might cause issues during upgrade.")?;
}
}
}
Ok(())
}
pub fn check_misc(&mut self) -> Result<(), Error> {
self.output.print_header("MISCELLANEOUS CHECKS")?;
self.check_pbs_services()?;
self.check_time_sync()?;
self.check_apt_repos()?;
self.check_bootloader()?;
self.check_dkms_modules()?;
Ok(())
}
pub fn summary(&mut self) -> Result<(), Error> {
self.output.print_summary()
}
fn check_repo_file(
&mut self,
found_suite: &mut Option<(String, String)>,
mismatches: &mut Vec<(String, String)>,
strange_suite: &mut bool,
repo_file: APTRepositoryFile,
) -> Result<(), Error> {
for repo in repo_file.repositories {
if !repo.enabled || repo.types == [APTRepositoryPackageType::DebSrc] {
continue;
}
for suite in &repo.suites {
let suite = match suite.find(&['-', '/'][..]) {
Some(n) => &suite[0..n],
None => suite,
};
if suite != OLD_SUITE && suite != NEW_SUITE {
let location = repo_file.path.clone().unwrap_or_default();
self.output.log_notice(format!(
"found unusual suite '{suite}', neither old '{OLD_SUITE}' nor new \
'{NEW_SUITE}'..\n Affected file {location}\n Please \
assure this is shipping compatible packages for the upgrade!"
))?;
*strange_suite = true;
continue;
}
if let Some((ref current_suite, ref current_location)) = found_suite {
let location = repo_file.path.clone().unwrap_or_default();
if suite != current_suite {
if mismatches.is_empty() {
mismatches.push((current_suite.clone(), current_location.clone()));
mismatches.push((suite.to_string(), location));
} else {
mismatches.push((suite.to_string(), location));
}
}
} else {
let location = repo_file.path.clone().unwrap_or_default();
*found_suite = Some((suite.to_string(), location));
}
}
}
Ok(())
}
fn get_systemd_unit_state(
&mut self,
unit: &str,
) -> Result<(SystemdUnitState, SystemdUnitState), Error> {
let output = std::process::Command::new("systemctl")
.arg("is-enabled")
.arg(unit)
.output()
.map_err(|err| format_err!("failed to execute - {err}"))?;
let enabled_state = match output.stdout.as_slice() {
b"enabled\n" => SystemdUnitState::Enabled,
b"disabled\n" => SystemdUnitState::Disabled,
_ => SystemdUnitState::Unknown,
};
let output = std::process::Command::new("systemctl")
.arg("is-active")
.arg(unit)
.output()
.map_err(|err| format_err!("failed to execute - {err}"))?;
let active_state = match output.stdout.as_slice() {
b"active\n" => SystemdUnitState::Active,
b"inactive\n" => SystemdUnitState::Inactive,
b"failed\n" => SystemdUnitState::Failed,
_ => SystemdUnitState::Unknown,
};
Ok((enabled_state, active_state))
}
fn check_pbs_services(&mut self) -> Result<(), Error> {
self.output.log_info("Checking PBS daemon services..")?;
for service in ["proxmox-backup.service", "proxmox-backup-proxy.service"] {
match self.get_systemd_unit_state(service)? {
(_, SystemdUnitState::Active) => {
self.output
.log_pass(format!("systemd unit '{service}' is in state 'active'"))?;
}
(_, SystemdUnitState::Inactive) => {
self.output.log_fail(format!(
"systemd unit '{service}' is in state 'inactive'\
\n Please check the service for errors and start it.",
))?;
}
(_, SystemdUnitState::Failed) => {
self.output.log_fail(format!(
"systemd unit '{service}' is in state 'failed'\
\n Please check the service for errors and start it.",
))?;
}
(_, _) => {
self.output.log_fail(format!(
"systemd unit '{service}' is not in state 'active'\
\n Please check the service for errors and start it.",
))?;
}
}
}
Ok(())
}
fn check_time_sync(&mut self) -> Result<(), Error> {
self.output
.log_info("Checking for supported & active NTP service..")?;
if self.get_systemd_unit_state("systemd-timesyncd.service")?.1 == SystemdUnitState::Active {
self.output.log_warn(
"systemd-timesyncd is not the best choice for time-keeping on servers, due to only \
applying updates on boot.\
\n While not necessary for the upgrade it's recommended to use one of:\
\n * chrony (Default in new Proxmox Backup Server installations)\
\n * ntpsec\
\n * openntpd"
)?;
} else if self.get_systemd_unit_state("ntp.service")?.1 == SystemdUnitState::Active {
self.output.log_info(
"Debian deprecated and removed the ntp package for Bookworm, but the system \
will automatically migrate to the 'ntpsec' replacement package on upgrade.",
)?;
} else if self.get_systemd_unit_state("chrony.service")?.1 == SystemdUnitState::Active
|| self.get_systemd_unit_state("openntpd.service")?.1 == SystemdUnitState::Active
|| self.get_systemd_unit_state("ntpsec.service")?.1 == SystemdUnitState::Active
{
self.output
.log_pass("Detected active time synchronisation unit")?;
} else {
self.output.log_warn(
"No (active) time synchronisation daemon (NTP) detected, but synchronized systems \
are important!",
)?;
}
Ok(())
}
fn get_upgradable_packages() -> Result<proxmox_backup::tools::apt::PkgState, Error> {
let cache = if let Ok(false) = proxmox_backup::tools::apt::pkg_cache_expired() {
if let Ok(Some(cache)) = proxmox_backup::tools::apt::read_pkg_state() {
cache
} else {
proxmox_backup::tools::apt::update_cache()?
}
} else {
proxmox_backup::tools::apt::update_cache()?
};
Ok(cache)
}
}
#[derive(PartialEq)]
enum SystemdUnitState {
Active,
Enabled,
Disabled,
Failed,
Inactive,
Unknown,
}
#[derive(Default)]
struct Counters {
pass: u64,
skip: u64,
notice: u64,
warn: u64,
fail: u64,
}
enum LogLevel {
Pass,
Info,
Skip,
Notice,
Warn,
Fail,
}
struct ConsoleOutput {
stream: StandardStream,
first_header: bool,
counters: Counters,
}
impl ConsoleOutput {
pub fn new() -> Self {
Self {
stream: StandardStream::stdout(ColorChoice::Always),
first_header: true,
counters: Counters::default(),
}
}
pub fn print_header(&mut self, message: &str) -> Result<(), Error> {
if !self.first_header {
writeln!(&mut self.stream)?;
}
self.first_header = false;
writeln!(&mut self.stream, "= {message} =\n")?;
Ok(())
}
pub fn set_color(&mut self, color: Color, bold: bool) -> Result<(), Error> {
self.stream
.set_color(ColorSpec::new().set_fg(Some(color)).set_bold(bold))?;
Ok(())
}
pub fn reset(&mut self) -> Result<(), std::io::Error> {
self.stream.reset()
}
pub fn log_line(&mut self, level: LogLevel, message: &str) -> Result<(), Error> {
match level {
LogLevel::Pass => {
self.counters.pass += 1;
self.set_color(Color::Green, false)?;
writeln!(&mut self.stream, "PASS: {}", message)?;
}
LogLevel::Info => {
writeln!(&mut self.stream, "INFO: {}", message)?;
}
LogLevel::Skip => {
self.counters.skip += 1;
writeln!(&mut self.stream, "SKIP: {}", message)?;
}
LogLevel::Notice => {
self.counters.notice += 1;
self.set_color(Color::White, true)?;
writeln!(&mut self.stream, "NOTICE: {}", message)?;
}
LogLevel::Warn => {
self.counters.warn += 1;
self.set_color(Color::Yellow, false)?;
writeln!(&mut self.stream, "WARN: {}", message)?;
}
LogLevel::Fail => {
self.counters.fail += 1;
self.set_color(Color::Red, true)?;
writeln!(&mut self.stream, "FAIL: {}", message)?;
}
}
self.reset()?;
Ok(())
}
pub fn log_pass<T: AsRef<str>>(&mut self, message: T) -> Result<(), Error> {
self.log_line(LogLevel::Pass, message.as_ref())
}
pub fn log_info<T: AsRef<str>>(&mut self, message: T) -> Result<(), Error> {
self.log_line(LogLevel::Info, message.as_ref())
}
pub fn log_skip<T: AsRef<str>>(&mut self, message: T) -> Result<(), Error> {
self.log_line(LogLevel::Skip, message.as_ref())
}
pub fn log_notice<T: AsRef<str>>(&mut self, message: T) -> Result<(), Error> {
self.log_line(LogLevel::Notice, message.as_ref())
}
pub fn log_warn<T: AsRef<str>>(&mut self, message: T) -> Result<(), Error> {
self.log_line(LogLevel::Warn, message.as_ref())
}
pub fn log_fail<T: AsRef<str>>(&mut self, message: T) -> Result<(), Error> {
self.log_line(LogLevel::Fail, message.as_ref())
}
pub fn print_summary(&mut self) -> Result<(), Error> {
self.print_header("SUMMARY")?;
let total = self.counters.fail
+ self.counters.pass
+ self.counters.notice
+ self.counters.skip
+ self.counters.warn;
writeln!(&mut self.stream, "TOTAL: {total}")?;
self.set_color(Color::Green, false)?;
writeln!(&mut self.stream, "PASSED: {}", self.counters.pass)?;
self.reset()?;
writeln!(&mut self.stream, "SKIPPED: {}", self.counters.skip)?;
writeln!(&mut self.stream, "NOTICE: {}", self.counters.notice)?;
if self.counters.warn > 0 {
self.set_color(Color::Yellow, false)?;
writeln!(&mut self.stream, "WARNINGS: {}", self.counters.warn)?;
}
if self.counters.fail > 0 {
self.set_color(Color::Red, true)?;
writeln!(&mut self.stream, "FAILURES: {}", self.counters.fail)?;
}
if self.counters.warn > 0 || self.counters.fail > 0 {
let (color, bold) = if self.counters.fail > 0 {
(Color::Red, true)
} else {
(Color::Yellow, false)
};
self.set_color(color, bold)?;
writeln!(
&mut self.stream,
"\nATTENTION: Please check the output for detailed information!",
)?;
if self.counters.fail > 0 {
writeln!(
&mut self.stream,
"Try to solve the problems one at a time and rerun this checklist tool again.",
)?;
}
}
self.reset()?;
Ok(())
}
}

View File

@ -6,6 +6,8 @@ use std::fs::File;
use std::os::unix::io::{AsRawFd, FromRawFd};
use anyhow::{bail, Error};
use pbs_tape::sg_tape::SgTape;
use proxmox_backup::tape::encryption_keys::load_key;
use serde_json::Value;
use proxmox_router::{cli::*, RpcEnvironment};
@ -19,28 +21,26 @@ use pbs_api_types::{
use pbs_tape::linux_list_drives::{check_tape_is_lto_tape_device, open_lto_tape_device};
use proxmox_backup::tape::drive::{open_lto_tape_drive, LtoTapeHandle, TapeDriver};
fn get_tape_handle(param: &Value) -> Result<LtoTapeHandle, Error> {
fn get_tape_handle(param: &Value) -> Result<SgTape, Error> {
let handle = if let Some(name) = param["drive"].as_str() {
let (config, _digest) = pbs_config::drive::config()?;
let drive: LtoTapeDrive = config.lookup("lto", name)?;
log::info!("using device {}", drive.path);
open_lto_tape_drive(&drive)?
SgTape::open_lto_drive(&drive)?
} else if let Some(device) = param["device"].as_str() {
log::info!("using device {}", device);
LtoTapeHandle::new(open_lto_tape_device(device)?)?
SgTape::new(open_lto_tape_device(device)?)?
} else if let Some(true) = param["stdin"].as_bool() {
log::info!("using stdin");
let fd = std::io::stdin().as_raw_fd();
let file = unsafe { File::from_raw_fd(fd) };
check_tape_is_lto_tape_device(&file)?;
LtoTapeHandle::new(file)?
SgTape::new(file)?
} else if let Ok(name) = std::env::var("PROXMOX_TAPE_DRIVE") {
let (config, _digest) = pbs_config::drive::config()?;
let drive: LtoTapeDrive = config.lookup("lto", &name)?;
log::info!("using device {}", drive.path);
open_lto_tape_drive(&drive)?
SgTape::open_lto_drive(&drive)?
} else {
let (config, _digest) = pbs_config::drive::config()?;
@ -56,7 +56,7 @@ fn get_tape_handle(param: &Value) -> Result<LtoTapeHandle, Error> {
let name = drive_names[0];
let drive: LtoTapeDrive = config.lookup("lto", name)?;
log::info!("using device {}", drive.path);
open_lto_tape_drive(&drive)?
SgTape::open_lto_drive(&drive)?
} else {
bail!("no drive/device specified");
}
@ -103,7 +103,8 @@ fn set_encryption(
match (fingerprint, uuid) {
(Some(fingerprint), Some(uuid)) => {
handle.set_encryption(Some((fingerprint, uuid)))?;
let key = load_key(&fingerprint)?;
handle.set_encryption(Some((key, uuid)))?;
}
(Some(_), None) => {
bail!("missing media set uuid");

View File

@ -198,8 +198,9 @@ impl JobState {
.map_err(|err| format_err!("error parsing upid: {err}"))?;
if !worker_is_active_local(&parsed) {
let state = upid_read_status(&parsed)
.map_err(|err| format_err!("error reading upid log status: {err}"))?;
let state = upid_read_status(&parsed).unwrap_or(TaskState::Unknown {
endtime: parsed.starttime,
});
Ok(JobState::Finished {
upid,

View File

@ -1,20 +1,67 @@
use std::fmt::Write;
use std::path::Path;
use std::process::Command;
fn files() -> Vec<&'static str> {
fn get_top_processes() -> String {
let (exe, args) = ("top", vec!["-b", "-c", "-w512", "-n", "1", "-o", "TIME"]);
let output = Command::new(exe)
.args(&args)
.output();
let output = match output {
Ok(output) => String::from_utf8_lossy(&output.stdout).to_string(),
Err(err) => err.to_string(),
};
let output = output.lines().take(30).collect::<Vec<&str>>().join("\n");
format!("$ `{exe} {}`\n```\n{output}\n```", args.join(" "))
}
fn files() -> Vec<(&'static str, Vec<&'static str>)> {
vec![
"/etc/hostname",
"/etc/hosts",
"/etc/network/interfaces",
"/etc/proxmox-backup/datastore.cfg",
"/etc/proxmox-backup/user.cfg",
"/etc/proxmox-backup/acl.cfg",
"/etc/proxmox-backup/remote.cfg",
"/etc/proxmox-backup/sync.cfg",
"/etc/proxmox-backup/verification.cfg",
"/etc/proxmox-backup/tape.cfg",
"/etc/proxmox-backup/media-pool.cfg",
"/etc/proxmox-backup/traffic-control.cfg",
(
"General System Info",
vec![
"/etc/hostname",
"/etc/hosts",
"/etc/network/interfaces",
"/etc/apt/sources.list",
"/etc/apt/sources.list.d/",
"/proc/pressure/",
],
),
(
"Datastores & Remotes",
vec!["/etc/proxmox-backup/datastore.cfg"],
),
(
"User & Access",
vec![
"/etc/proxmox-backup/user.cfg",
"/etc/proxmox-backup/acl.cfg",
],
),
("Remotes", vec!["/etc/proxmox-backup/remote.cfg"]),
(
"Jobs",
vec![
"/etc/proxmox-backup/sync.cfg",
"/etc/proxmox-backup/prune.cfg",
"/etc/proxmox-backup/verification.cfg",
],
),
(
"Tape",
vec![
"/etc/proxmox-backup/tape.cfg",
"/etc/proxmox-backup/media-pool.cfg",
],
),
(
"Others",
vec![
"/etc/proxmox-backup/node.cfg",
"/etc/proxmox-backup/traffic-control.cfg",
],
),
]
}
@ -24,8 +71,19 @@ fn commands() -> Vec<(&'static str, Vec<&'static str>)> {
("date", vec!["-R"]),
("proxmox-backup-manager", vec!["versions", "--verbose"]),
("proxmox-backup-manager", vec!["subscription", "get"]),
("proxmox-backup-manager", vec!["ldap", "list"]),
("proxmox-backup-manager", vec!["openid", "list"]),
("proxmox-boot-tool", vec!["status"]),
("df", vec!["-h"]),
("lsblk", vec!["--ascii"]),
(
"lsblk",
vec![
"--ascii",
"-M",
"-o",
"+HOTPLUG,ROTA,PHY-SEC,FSTYPE,MODEL,TRAN",
],
),
("ls", vec!["-l", "/dev/disk/by-id", "/dev/disk/by-path"]),
("zpool", vec!["status"]),
("zfs", vec!["list"]),
@ -37,60 +95,132 @@ fn commands() -> Vec<(&'static str, Vec<&'static str>)> {
type FunctionMapping = (&'static str, fn() -> String);
fn function_calls() -> Vec<FunctionMapping> {
vec![("Datastores", || {
let config = match pbs_config::datastore::config() {
Ok((config, _digest)) => config,
_ => return String::from("could not read datastore config"),
};
vec![
("Datastores", || {
let config = match pbs_config::datastore::config() {
Ok((config, _digest)) => config,
_ => return String::from("could not read datastore config"),
};
let mut list = Vec::new();
for store in config.sections.keys() {
list.push(store.as_str());
let mut list = Vec::new();
for store in config.sections.keys() {
list.push(store.as_str());
}
format!("```\n{}\n```", list.join(", "))
}),
("System Load & Uptime", get_top_processes),
]
}
fn get_file_content(file: impl AsRef<Path>) -> String {
use proxmox_sys::fs::file_read_optional_string;
let content = match file_read_optional_string(&file) {
Ok(Some(content)) => content,
Ok(None) => String::from("# file does not exist"),
Err(err) => err.to_string(),
};
let file_name = file.as_ref().display();
format!("`$ cat '{file_name}'`\n```\n{}\n```", content.trim_end())
}
fn get_directory_content(path: impl AsRef<Path>) -> String {
let read_dir_iter = match std::fs::read_dir(&path) {
Ok(iter) => iter,
Err(err) => {
return format!(
"`$ cat '{}*'`\n```\n# read dir failed - {}\n```",
path.as_ref().display(),
err.to_string(),
);
}
list.join(", ")
})]
};
let mut out = String::new();
let mut first = true;
for entry in read_dir_iter {
let entry = match entry {
Ok(entry) => entry,
Err(err) => {
let _ = writeln!(out, "error during read-dir - {}", err.to_string());
continue;
}
};
let path = entry.path();
if path.is_file() {
if first {
let _ = writeln!(out, "{}", get_file_content(path));
first = false;
} else {
let _ = writeln!(out, "\n{}", get_file_content(path));
}
} else {
let _ = writeln!(out, "skipping sub-directory `{}`", path.display());
}
}
out
}
fn get_command_output(exe: &str, args: &Vec<&str>) -> String {
let output = Command::new(exe)
.env("PROXMOX_OUTPUT_NO_BORDER", "1")
.args(args)
.output();
let output = match output {
Ok(output) => {
let mut out = String::from_utf8_lossy(&output.stdout)
.trim_end()
.to_string();
let stderr = String::from_utf8_lossy(&output.stderr)
.trim_end()
.to_string();
if !stderr.is_empty() {
let _ = writeln!(out, "\n```\nSTDERR:\n```\n{stderr}");
}
out
}
Err(err) => err.to_string(),
};
format!("$ `{exe} {}`\n```\n{output}\n```", args.join(" "))
}
pub fn generate_report() -> String {
use proxmox_sys::fs::file_read_optional_string;
let file_contents = files()
.iter()
.map(|file_name| {
let content = match file_read_optional_string(Path::new(file_name)) {
Ok(Some(content)) => content,
Ok(None) => String::from("# file does not exist"),
Err(err) => err.to_string(),
};
format!("$ cat '{}'\n{}", file_name, content)
.map(|group| {
let (group, files) = group;
let group_content = files
.iter()
.map(|file_name| {
let path = Path::new(file_name);
if path.is_dir() {
get_directory_content(&path)
} else {
get_file_content(file_name)
}
})
.collect::<Vec<String>>()
.join("\n\n");
format!("### {group}\n\n{group_content}")
})
.collect::<Vec<String>>()
.join("\n\n");
let command_outputs = commands()
.iter()
.map(|(command, args)| {
let output = Command::new(command)
.env("PROXMOX_OUTPUT_NO_BORDER", "1")
.args(args)
.output();
let output = match output {
Ok(output) => String::from_utf8_lossy(&output.stdout).to_string(),
Err(err) => err.to_string(),
};
format!("$ `{} {}`\n{}", command, args.join(" "), output)
})
.map(|(command, args)| get_command_output(command, args))
.collect::<Vec<String>>()
.join("\n\n");
let function_outputs = function_calls()
.iter()
.map(|(desc, function)| format!("$ {}\n{}", desc, function()))
.map(|(desc, function)| {
let output = function();
format!("#### {desc}\n{}\n", output.trim_end())
})
.collect::<Vec<String>>()
.join("\n\n");
format!(
"= FILES =\n\n{}\n= COMMANDS =\n\n{}\n= FUNCTIONS =\n\n{}\n",
file_contents, command_outputs, function_outputs
"## FILES\n\n{file_contents}\n## COMMANDS\n\n{command_outputs}\n## FUNCTIONS\n\n{function_outputs}\n"
)
}

View File

@ -16,6 +16,7 @@ use std::os::unix::io::{AsRawFd, FromRawFd, RawFd};
use anyhow::{bail, format_err, Error};
use pbs_tape::sg_tape::drive_get_encryption;
use proxmox_uuid::Uuid;
use pbs_api_types::{
@ -23,7 +24,6 @@ use pbs_api_types::{
};
use pbs_key_config::KeyConfig;
use pbs_tape::{
linux_list_drives::open_lto_tape_device,
sg_tape::{SgTape, TapeAlertFlags},
BlockReadError, MediaContentHeader, TapeRead, TapeWrite,
};
@ -34,75 +34,47 @@ use crate::tape::{
file_formats::{MediaSetLabel, PROXMOX_BACKUP_MEDIA_SET_LABEL_MAGIC_1_0},
};
/// Open a tape device
///
/// This does additional checks:
///
/// - check if it is a non-rewinding tape device
/// - check if drive is ready (tape loaded)
/// - check block size
/// - for autoloader only, try to reload ejected tapes
pub fn open_lto_tape_drive(config: &LtoTapeDrive) -> Result<LtoTapeHandle, Error> {
proxmox_lang::try_block!({
let file = open_lto_tape_device(&config.path)?;
let mut handle = LtoTapeHandle::new(file)?;
if handle.sg_tape.test_unit_ready().is_err() {
// for autoloader only, try to reload ejected tapes
if config.changer.is_some() {
let _ = handle.sg_tape.load(); // just try, ignore error
impl Drop for LtoTapeHandle {
fn drop(&mut self) {
// always unload the encryption key when the handle is dropped for security
// but only log an error if we set one in the first place
if let Err(err) = self.set_encryption(None) {
if self.encryption_key_loaded {
log::error!("could not unload encryption key from drive: {err}");
}
}
handle.sg_tape.wait_until_ready()?;
handle.set_default_options()?;
Ok(handle)
})
.map_err(|err: Error| {
format_err!(
"open drive '{}' ({}) failed - {}",
config.name,
config.path,
err
)
})
}
}
/// Lto Tape device handle
pub struct LtoTapeHandle {
sg_tape: SgTape,
encryption_key_loaded: bool,
}
impl LtoTapeHandle {
/// Creates a new instance
pub fn new(file: File) -> Result<Self, Error> {
let sg_tape = SgTape::new(file)?;
Ok(Self { sg_tape })
Ok(Self {
sg_tape,
encryption_key_loaded: false,
})
}
/// Set all options we need/want
pub fn set_default_options(&mut self) -> Result<(), Error> {
self.sg_tape.set_default_options()?;
Ok(())
}
/// Open a tape device
///
/// since this calls [SgTape::open_lto_drive], it does some internal checks.
/// See [SgTape] docs for details.
pub fn open_lto_drive(config: &LtoTapeDrive) -> Result<Self, Error> {
let sg_tape = SgTape::open_lto_drive(config)?;
/// Set driver options
pub fn set_drive_options(
&mut self,
compression: Option<bool>,
block_length: Option<u32>,
buffer_mode: Option<bool>,
) -> Result<(), Error> {
self.sg_tape
.set_drive_options(compression, block_length, buffer_mode)
}
let handle = Self {
sg_tape,
encryption_key_loaded: false,
};
/// Write a single EOF mark without flushing buffers
pub fn write_filemarks(&mut self, count: usize) -> Result<(), std::io::Error> {
self.sg_tape.write_filemarks(count, false)
Ok(handle)
}
/// Get Tape and Media status
@ -118,27 +90,11 @@ impl LtoTapeHandle {
self.sg_tape.space_filemarks(-count.try_into()?)
}
pub fn forward_space_count_records(&mut self, count: usize) -> Result<(), Error> {
self.sg_tape.space_blocks(count.try_into()?)
}
pub fn backward_space_count_records(&mut self, count: usize) -> Result<(), Error> {
self.sg_tape.space_blocks(-count.try_into()?)
}
/// Position the tape after filemark count. Count 0 means BOT.
pub fn locate_file(&mut self, position: u64) -> Result<(), Error> {
self.sg_tape.locate_file(position)
}
pub fn erase_media(&mut self, fast: bool) -> Result<(), Error> {
self.sg_tape.erase_media(fast)
}
pub fn load(&mut self) -> Result<(), Error> {
self.sg_tape.load()
}
/// Read Cartridge Memory (MAM Attributes)
pub fn cartridge_memory(&mut self) -> Result<Vec<MamAttribute>, Error> {
self.sg_tape.cartridge_memory()
@ -148,20 +104,6 @@ impl LtoTapeHandle {
pub fn volume_statistics(&mut self) -> Result<Lp17VolumeStatistics, Error> {
self.sg_tape.volume_statistics()
}
/// Lock the drive door
pub fn lock(&mut self) -> Result<(), Error> {
self.sg_tape
.set_medium_removal(false)
.map_err(|err| format_err!("lock door failed - {}", err))
}
/// Unlock the drive door
pub fn unlock(&mut self) -> Result<(), Error> {
self.sg_tape
.set_medium_removal(true)
.map_err(|err| format_err!("unlock door failed - {}", err))
}
}
impl TapeDriver for LtoTapeHandle {
@ -271,6 +213,13 @@ impl TapeDriver for LtoTapeHandle {
self.sync()?; // sync data to tape
let encrypt_fingerprint = media_set_label
.encryption_key_fingerprint
.clone()
.map(|fp| (fp, media_set_label.uuid.clone()));
self.set_encryption(encrypt_fingerprint)?;
Ok(())
}
@ -292,46 +241,27 @@ impl TapeDriver for LtoTapeHandle {
&mut self,
key_fingerprint: Option<(Fingerprint, Uuid)>,
) -> Result<(), Error> {
if nix::unistd::Uid::effective().is_root() {
if let Some((ref key_fingerprint, ref uuid)) = key_fingerprint {
let (key_map, _digest) = crate::tape::encryption_keys::load_keys()?;
match key_map.get(key_fingerprint) {
Some(item) => {
// derive specialized key for each media-set
let mut tape_key = [0u8; 32];
let uuid_bytes: [u8; 16] = *uuid.as_bytes();
openssl::pkcs5::pbkdf2_hmac(
&item.key,
&uuid_bytes,
10,
openssl::hash::MessageDigest::sha256(),
&mut tape_key,
)?;
return self.sg_tape.set_encryption(Some(tape_key));
}
None => bail!("unknown tape encryption key '{}'", key_fingerprint),
}
} else {
return self.sg_tape.set_encryption(None);
}
}
let output = if let Some((fingerprint, uuid)) = key_fingerprint {
if let Some((fingerprint, uuid)) = key_fingerprint {
let fingerprint = fingerprint.signature();
run_sg_tape_cmd(
let output = run_sg_tape_cmd(
"encryption",
&["--fingerprint", &fingerprint, "--uuid", &uuid.to_string()],
self.sg_tape.file_mut().as_raw_fd(),
)?
)?;
self.encryption_key_loaded = true;
let result: Result<(), String> = serde_json::from_str(&output)?;
result.map_err(|err| format_err!("{}", err))
} else {
run_sg_tape_cmd("encryption", &[], self.sg_tape.file_mut().as_raw_fd())?
};
let result: Result<(), String> = serde_json::from_str(&output)?;
result.map_err(|err| format_err!("{}", err))
self.sg_tape.set_encryption(None)
}
}
fn assert_encryption_mode(&mut self, encryption_wanted: bool) -> Result<(), Error> {
let encryption_set = drive_get_encryption(self.sg_tape.file_mut())?;
if encryption_wanted != encryption_set {
bail!("Set encryption mode not what was desired (set: {encryption_set}, wanted: {encryption_wanted})");
}
Ok(())
}
}

View File

@ -105,11 +105,13 @@ pub trait TapeDriver {
key_config: Option<&KeyConfig>,
) -> Result<(), Error>;
/// Read the media label
/// Read the media label without setting the encryption key
///
/// This tries to read both media labels (label and
/// media_set_label). Also returns the optional encryption key configuration.
fn read_label(&mut self) -> Result<(Option<MediaId>, Option<KeyConfig>), Error> {
/// This is used internally by 'read_label' and when restoring the encryption
/// key from the drive. Should not be used or overwritten otherwise!
fn read_label_without_loading_key(
&mut self,
) -> Result<(Option<MediaId>, Option<KeyConfig>), Error> {
self.rewind()?;
let label = {
@ -187,6 +189,22 @@ pub trait TapeDriver {
Ok((Some(media_id), key_config))
}
/// Read the media label
///
/// This tries to read both media labels (label and
/// media_set_label). Also returns the optional encryption key configuration.
///
/// Automatically sets the encryption key on the drive
fn read_label(&mut self) -> Result<(Option<MediaId>, Option<KeyConfig>), Error> {
let (media_id, key_config) = self.read_label_without_loading_key()?;
let encrypt_fingerprint = media_id.as_ref().and_then(|id| id.get_encryption_fp());
self.set_encryption(encrypt_fingerprint)?;
Ok((media_id, key_config))
}
/// Eject media
fn eject_media(&mut self) -> Result<(), Error>;
@ -203,6 +221,9 @@ pub trait TapeDriver {
/// We use the media_set_uuid to XOR the secret key with the
/// uuid (first 16 bytes), so that each media set uses an unique
/// key for encryption.
///
/// Should be called as part of write_media_set_label or read_label,
/// so this should not be called manually.
fn set_encryption(
&mut self,
key_fingerprint: Option<(Fingerprint, Uuid)>,
@ -212,6 +233,14 @@ pub trait TapeDriver {
}
Ok(())
}
/// Asserts that the encryption mode is set to the given value
fn assert_encryption_mode(&mut self, encryption_wanted: bool) -> Result<(), Error> {
if encryption_wanted {
bail!("drive does not support encryption");
}
Ok(())
}
}
/// A boxed implementor of [`MediaChange`].
@ -280,7 +309,7 @@ pub fn open_drive(config: &SectionConfigData, drive: &str) -> Result<Box<dyn Tap
}
"lto" => {
let tape = LtoTapeDrive::deserialize(config)?;
let handle = open_lto_tape_drive(&tape)?;
let handle = LtoTapeHandle::open_lto_drive(&tape)?;
Ok(Box::new(handle))
}
ty => bail!("unknown drive type '{}' - internal error", ty),
@ -449,7 +478,7 @@ pub fn request_and_load_media(
}
}
let mut handle = match open_lto_tape_drive(&drive_config) {
let mut handle = match LtoTapeHandle::open_lto_drive(&drive_config) {
Ok(handle) => handle,
Err(err) => {
update_and_log_request_error(

View File

@ -12,7 +12,7 @@
use std::collections::HashMap;
use anyhow::{bail, Error};
use anyhow::{bail, format_err, Error};
use serde::{Deserialize, Serialize};
use proxmox_sys::fs::file_read_optional_string;
@ -92,6 +92,14 @@ pub fn load_keys() -> Result<(HashMap<Fingerprint, EncryptionKeyInfo>, [u8; 32])
Ok((map, digest))
}
pub fn load_key(fingerprint: &Fingerprint) -> Result<[u8; 32], Error> {
let (key_map, _digest) = crate::tape::encryption_keys::load_keys()?;
key_map
.get(fingerprint)
.map(|data| data.key)
.ok_or_else(|| format_err!("unknown tape encryption key '{fingerprint}'"))
}
/// Load tape encryption key configurations (password protected keys)
pub fn load_key_configs() -> Result<(HashMap<Fingerprint, KeyConfig>, [u8; 32]), Error> {
let content = file_read_optional_string(TAPE_KEY_CONFIG_FILENAME)?;

View File

@ -33,7 +33,7 @@ use serde_json::json;
use proxmox_sys::fs::{file_get_json, replace_file, CreateOptions};
use proxmox_uuid::Uuid;
use pbs_api_types::{MediaLocation, MediaSetPolicy, MediaStatus, RetentionPolicy};
use pbs_api_types::{Fingerprint, MediaLocation, MediaSetPolicy, MediaStatus, RetentionPolicy};
use pbs_config::BackupLockGuard;
#[cfg(not(test))]
@ -71,6 +71,10 @@ impl MediaId {
}
self.label.pool.to_owned()
}
pub(crate) fn get_encryption_fp(&self) -> Option<(Fingerprint, Uuid)> {
let label = self.clone().media_set_label?;
label.encryption_key_fingerprint.map(|fp| (fp, label.uuid))
}
}
#[derive(Serialize, Deserialize)]

View File

@ -272,12 +272,7 @@ impl PoolWriter {
let media_set = media.media_set_label().unwrap();
let encrypt_fingerprint = media_set
.encryption_key_fingerprint
.clone()
.map(|fp| (fp, media_set.uuid.clone()));
drive.set_encryption(encrypt_fingerprint)?;
drive.assert_encryption_mode(media_set.encryption_key_fingerprint.is_some())?;
self.status = Some(PoolWriterState {
drive,

View File

@ -218,6 +218,16 @@ Ext.define('PBS.MainView', {
flex: 1,
baseCls: 'x-plain',
},
{
xtype: 'proxmoxEOLNotice',
product: 'Proxmox Backup Server',
version: '2',
eolDate: '2024-07-31',
href: 'pbs.proxmox.com/docs/faq.html#faq-support-table',
},
{
flex: 1,
},
{
xtype: 'button',
baseCls: 'x-btn',

View File

@ -35,6 +35,14 @@ const proxmoxOnlineHelpInfo = {
"link": "/docs/configuration-files.html#domains-cfg",
"title": "``domains.cfg``"
},
"faq-support-table": {
"link": "/docs/faq.html#faq-support-table",
"title": "How long will my Proxmox Backup Server version be supported?"
},
"faq-upgrade-major": {
"link": "/docs/faq.html#faq-upgrade-major",
"title": "How can I upgrade Proxmox Backup Server to the next major release?"
},
"pxar-format": {
"link": "/docs/file-formats.html#pxar-format",
"title": "Proxmox File Archive Format (``.pxar``)"

View File

@ -55,17 +55,24 @@ Ext.define('PBS.TapeManagement.BackupOverview', {
data[pool] = {};
}
let seq_nr = entry['seq-nr'];
if (data[pool][media_set] === undefined) {
data[pool][media_set] = entry;
data[pool][media_set].text = media_set;
data[pool][media_set].restore = true;
data[pool][media_set].tapes = 1;
data[pool][media_set]['seq-nr'] = undefined;
data[pool][media_set]['max-seq-nr'] = seq_nr;
data[pool][media_set].is_media_set = true;
data[pool][media_set].typeText = 'media-set';
} else {
data[pool][media_set].tapes++;
}
if (data[pool][media_set]['max-seq-nr'] < seq_nr) {
data[pool][media_set]['max-seq-nr'] = seq_nr;
}
}
let list = [];
@ -309,11 +316,33 @@ Ext.define('PBS.TapeManagement.BackupOverview', {
},
],
viewConfig: {
getRowClass: function(rec) {
let tapeCount = (rec.get('max-seq-nr') ?? 0) + 1;
let actualTapeCount = rec.get('tapes') ?? 1;
if (tapeCount !== actualTapeCount) {
return 'proxmox-warning-row';
}
return '';
},
},
columns: [
{
xtype: 'treecolumn',
text: gettext('Pool/Media-Set/Snapshot'),
dataIndex: 'text',
renderer: function(value, mD, rec) {
let tapeCount = (rec.get('max-seq-nr') ?? 0) + 1;
let actualTapeCount = rec.get('tapes') ?? 1;
if (tapeCount !== actualTapeCount) {
return `${value} (${gettext('Incomplete')})`;
}
return value;
},
sortable: false,
flex: 3,
},

View File

@ -179,9 +179,6 @@ Ext.define('PBS.TapeManagement.TapeRestoreWindow', {
updateDatastores: function(grid, values) {
let me = this;
if (values === 'all') {
values = [];
}
let datastores = {};
values.forEach((snapshotOrDatastore) => {
let datastore = snapshotOrDatastore;
@ -297,14 +294,12 @@ Ext.define('PBS.TapeManagement.TapeRestoreWindow', {
onGetValues: function(values) {
let me = this;
if (values !== "all" &&
Ext.isString(values.snapshots) &&
values.snapshots &&
values.snapshots.indexOf(':') !== -1
) {
values.snapshots = values.snapshots.split(',');
} else {
delete values.snapshots;
// cannot use the string serialized one from onGetValues, so gather manually
delete values.snapshots;
let snapshots = me.down('pbsTapeSnapshotGrid').getValue();
if (snapshots.length > 0 && snapshots[0].indexOf(':') !== -1) {
values.snapshots = snapshots;
}
return values;
@ -378,13 +373,35 @@ Ext.define('PBS.TapeManagement.TapeRestoreWindow', {
let vm = controller.getViewModel();
let datastores = [];
if (values.store.toString() !== "") {
if (vm.get('singleDatastore')) {
let source = controller.lookup('snapshotGrid').getValue();
datastores.push(`${source}=${values.store}`);
} else {
datastores.push(values.store);
}
let target = values.store.toString();
delete values.store;
let source = [];
if (vm.get('singleDatastore')) {
// can be '[]' (for all), a list of datastores, or a list of snapshots
source = controller.lookup('snapshotGrid').getValue();
if (source.length > 0) {
if (source[0].indexOf(':') !== -1) {
// one or multiple snapshots are selected
// extract datastore from first
source = source[0].split(':')[0];
} else {
// one whole datstore is selected
source = source[0];
}
} else {
// must be [] (all snapshots) so we use it as a default target
}
} else {
// there is more than one datastore to be restored, so this is just
// the default fallback
}
if (Ext.isString(source)) {
datastores.push(`${source}=${target}`);
} else {
datastores.push(target);
}
}
let defaultNs = values.defaultNs;
@ -524,6 +541,8 @@ Ext.define('PBS.TapeManagement.DataStoreMappingGrid', {
let ns = targetns || defaultNs;
if (ns) {
namespaces.push(`store=${source},target=${ns}`);
} else {
namespaces.push(`store=${source}`);
}
}
});
@ -708,7 +727,7 @@ Ext.define('PBS.TapeManagement.SnapshotGrid', {
let me = this;
let snapshots = [];
let storeCounts = {};
let selectedStoreCounts = {};
me.getSelection().forEach((rec) => {
let id = rec.get('id');
@ -717,10 +736,10 @@ Ext.define('PBS.TapeManagement.SnapshotGrid', {
// only add if not filtered
if (me.store.findExact('id', id) !== -1) {
snapshots.push(`${store}:${snap}`);
if (storeCounts[store] === undefined) {
storeCounts[store] = 0;
if (selectedStoreCounts[store] === undefined) {
selectedStoreCounts[store] = 0;
}
storeCounts[store]++;
selectedStoreCounts[store]++;
}
});
@ -728,21 +747,21 @@ Ext.define('PBS.TapeManagement.SnapshotGrid', {
let originalData = me.store.getData().getSource() || me.store.getData();
if (snapshots.length === originalData.length) {
return "all";
return [];
}
let wholeStores = [];
let wholeStoresSelected = true;
for (const [store, count] of Object.entries(storeCounts)) {
if (me.storeCounts[store] === count) {
let onlyWholeStoresSelected = true;
for (const [store, selectedCount] of Object.entries(selectedStoreCounts)) {
if (me.storeCounts[store] === selectedCount) {
wholeStores.push(store);
} else {
wholeStoresSelected = false;
onlyWholeStoresSelected = false;
break;
}
}
if (wholeStoresSelected) {
if (onlyWholeStoresSelected) {
return wholeStores;
}