Compare commits

...

127 Commits

Author SHA1 Message Date
5547f90ba7 bump version to 1.1.2-1
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2021-04-15 13:26:59 +02:00
2e1b63fb25 backup verify: do not check every loop iteration for abort/shutdown
only check every 1024'th, which is cheaper to do than a modulo, as we
can just mask the 10 least-significant-bits and check if the result
is zero.

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2021-04-15 13:21:36 +02:00
7b2d3a5fe9 backup verify: unify check if chunk can be skipped
This also re-checks the corrupt chunk list before actually loading a
chunk.

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2021-04-15 13:21:07 +02:00
0216f56241 config: tfa: drop now unused schema::Updatable
was used in a macro expansion, now handled otherwise

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2021-04-15 12:35:11 +02:00
80acdd71fa tape: do not try to backup unfinished backups 2021-04-15 10:24:14 +02:00
26af61debc backup verify: re-check if we can skip a chunk in the actual verify loop
Fixes a non-negligible performance regression from commit
7f394c807b

While we skip known-verified chunks in the stat-and-inode-sort loop,
those are only the ones from previous indexes. If there's a repeated
chunk in one index they would get re-verified more often as required.

So, add the check again explicitly to the read+verify loop.

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2021-04-15 10:00:06 +02:00
e7f94010d3 cargo toml: update proxmox version
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2021-04-15 09:56:45 +02:00
a4e871f52c api2/access/user: remove password for @pbs users on removal
so that their password entry is not left in the shadow.json

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2021-04-15 08:33:20 +02:00
bc3072ef7a bump version to 1.1.1-1
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2021-04-14 14:50:41 +02:00
f4bb2510b9 docs: tape: replace changer overview screenshot
Replace previous screenshot with one that shows a more realistic amount
of drives.

Signed-off-by: Dylan Whyte <d.whyte@proxmox.com>
2021-04-14 14:41:00 +02:00
2ab12cd0cb verify: add comment for inode sorting
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2021-04-14 14:39:24 +02:00
c894909e17 verify: partially rust fmt
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2021-04-14 14:39:24 +02:00
7f394c807b backup/verify: improve speed by sorting chunks by inode
before reading the chunks from disk in the order of the index file,
stat them first and sort them by inode number.

this can have a very positive impact on read speed on spinning disks,
even with the additional stat'ing of the chunks.

memory footprint should be tolerable, for 1_000_000 chunks
we need about ~16MiB of memory (Vec of 64bit position + 64bit inode)
(assuming 4MiB Chunks, such an index would reference 4TiB of data)

two small benchmarks (single spinner, ext4) here showed an improvement from
~430 seconds to ~330 seconds for a 32GiB fixed index
and from
~160 seconds to ~120 seconds for a 10GiB dynamic index

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2021-04-14 14:39:24 +02:00
7afb98a912 docs: fix tape.cfg format description 2021-04-14 14:30:20 +02:00
3847008e1b docs: pmt - remove old linux driver options 2021-04-14 14:26:39 +02:00
f6ed2eff47 docs: move tape config/syntax to appendix 2021-04-14 14:25:10 +02:00
23eed6755a ui: tape/ChangerStatus: hide drive-slots without assigned drives
if a user has not configured a drive for a specified driveslot of the
changer, simply hide that slot

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2021-04-14 14:24:07 +02:00
384a2b4d4f docs: faq: fix encryption link
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2021-04-14 14:18:05 +02:00
910177a388 docs: pve-integration: mention gui integration
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2021-04-14 14:18:05 +02:00
54311a38c6 docs: update proxmox-tape status output 2021-04-14 14:03:45 +02:00
983edbc54a ui: tape/ChangerStatus: add Format button to drivegrid
so that the user can also format an already inserted tape directly

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2021-04-14 13:53:16 +02:00
10439718e2 docs: use defininition list for tape Terminology
To avoid those strange line breaks in Field list labels.
2021-04-14 13:25:29 +02:00
ebddccef5f docs: gui: add short tape backup section
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2021-04-14 12:47:05 +02:00
9cfe0ff350 docs: include tape backup in TOC
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2021-04-14 12:47:05 +02:00
295bae14b7 docs: tape: add a bit of text to changers for better image flow
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2021-04-14 12:47:05 +02:00
53939bb438 docs: tape screenshots
Add tape screenshots throughout the tape docs with some references to the GUI

Signed-off-by: Dylan Whyte <d.whyte@proxmox.com>
2021-04-14 12:47:05 +02:00
329c2cbe66 docs: reorder maintenance & network after client usage/tools
The idea is that people first need to make actual backups before they
need to do maintenance tasks.
Network is already setup when installing with the ISO or on-top of
Debian, so that is not a priority either.

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2021-04-14 12:47:05 +02:00
55334cf45a docs: use "Backup Storage" heading
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2021-04-14 12:47:05 +02:00
a2e30cd51d ui: tape: rename erase to format
erase is a different action, so correctly call it 'format'

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2021-04-14 12:25:53 +02:00
4bf2ab1109 cleanup: remove debug println 2021-04-14 10:39:29 +02:00
1dd1c9eb5c api2/tape/restore: restore_chunk_archive: only ignore tape related errors
when we get an error from the tape, we possibly want to ignore it,
i.e. when the file was incomplete, but we still want to error
out if the error came from e.g, the datastore, so we have to move
the error checking code to the 'next_chunk' call

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2021-04-14 10:38:26 +02:00
6dde015f8c bump version to 1.1.0-1
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2021-04-13 14:42:50 +02:00
5f3b2330c8 docs: typo fixes
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2021-04-13 14:42:50 +02:00
4ba5d3b3dd docs: mention client repository and rework client installation
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2021-04-13 14:42:50 +02:00
e7e3d7360a docs: get help: actually link customer portal
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2021-04-13 14:42:50 +02:00
fd8b00aed7 docs: client: add "Backup" to "Repository Locations" heading to avoid confusion
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2021-04-13 14:42:50 +02:00
2631e57d20 fix regression tests 2021-04-13 14:02:37 +02:00
90461b76fb TapeRead: add skip_data() 2021-04-13 13:32:45 +02:00
629103d60c d/rules: update binary path for proxmox-backup-file-restore
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2021-04-13 12:18:29 +02:00
dc232b8946 d/control: update to track thiserror build dependency
was in Cargo.toml since commit
64ad7b706148b289d4ca67b87630cdc60f464cc

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2021-04-13 12:16:42 +02:00
6fed819dc2 proxmox-backup-file-restore: fix various postinst bugs/bad-behavior
1. The exit was never called as `test ... || echo "foo" || exit 1`
   can never come to the exit, as echo will not fail

2. The echo was meant to be redirected to stderr (FD #2) but it was
   actually redirected to a file named '2'

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2021-04-13 12:06:10 +02:00
646fc7f086 ui: tape/DriveStatus: show that no tape is loaded in grid title
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2021-04-13 11:48:45 +02:00
ecc5602c88 ui: tape/DriveStatus: remove buffer-mode
since this will almost always be set to '1', it has no real
value to show to the user

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2021-04-13 11:47:37 +02:00
6a15cce540 tape: SgTapeReader::read_block - disable reading beyond EOF 2021-04-13 11:46:30 +02:00
f281b8d3a9 tape: cleanup MediaCatalog on tape reuse 2021-04-13 11:46:30 +02:00
4465b76812 d/control: bump versioned dependency for proxmox-widget-toolkit
to ensure we have the moved out file browser widget available

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2021-04-13 09:14:11 +02:00
61df02cda1 ui: file browser: adapt to abi changes
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2021-04-13 09:06:30 +02:00
3b0321365b use FileBrowser from proxmox-widget-toolkit
Signed-off-by: Stefan Reiter <s.reiter@proxmox.com>
2021-04-13 08:44:48 +02:00
0dfce17a43 api/datastore: allow pxar file download of entire archive
Treat filepaths like "/root.pxar.didx" without a trailing slash as
wanting to download the entire archive content instead of erroring. The
zip-creation code already works fine for this scenario.

Signed-off-by: Stefan Reiter <s.reiter@proxmox.com>
2021-04-13 08:26:41 +02:00
a38dccf0e8 ui: index: drop enableTapeUI
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2021-04-12 15:55:58 +02:00
f05085ab22 ui: nav tree: code cleanup
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2021-04-12 15:55:58 +02:00
bc42bb3c6e ui: nav tree: make datastore-add button less special cased
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2021-04-12 15:55:58 +02:00
94b7f56e65 ui: nav tree: code cleanup and unification between datastore and tapes
datastore and tape entries are very similar but differ in some points
in such a way that a nice unification is not really that helpful, but
making similar key parts the same is still nice when reading the code

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2021-04-12 15:55:58 +02:00
0417e9af1b tools/async_io: do not error on Accept for StaticIncoming
in proxmox-backup-proxy, we log and discard any errors on 'accept',
so that we can continue to server requests

in proxmox-backup-api, we just have the StaticIncoming that accepts,
which will forward any errors from the underlying TcpListener

this patch also logs and discards the errors, like in the proxy.
Otherwise it could happen that if the api-daemon has more files open
than the proxy, it will shut itself down because of a
'too many open files' error if there are many open connections

(the service should also restart on exit i think, but this is
a separate issue)

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2021-04-12 15:43:13 +02:00
ce5327badc tape: fix regression tests 2021-04-12 14:08:05 +02:00
368f4c5416 fix gathering io stats for zpools
if a datastore or root is not used directly on the pool dir
(e.g. the installer creates 2 sub datasets ROOT/pbs-1), info in
/proc/self/mountinfo returns not the pool, but the path to the
dataset, which has no iostats itself in /proc/spl/kstat/zfs/
but only the pool itself

so instead of not gathering data at all, gather the info from the
underlying pool instead. if one has multiple datastores on the same
pool those rrd stats will be the same for all those datastores now
(instead of empty) similar to 'normal' directories

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2021-04-12 13:35:38 +02:00
318b310638 tape: improve EOT error handling 2021-04-12 13:27:34 +02:00
164ad7b706 sgutils2: use thiserror to derive Error 2021-04-12 13:27:34 +02:00
a5322f3c50 buildsys: fix restore package names
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2021-04-12 12:52:19 +02:00
fa29d7eb49 ui: improve code-readability s/tapestore/tapeStore/
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2021-04-12 12:34:26 +02:00
a21f9852fd enable tape backup by default
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2021-04-12 12:31:56 +02:00
79e2473c63 d/control: file restore: only recommend proxmox-backup-restore-image
should not be a hard dependency, as one can use the file-restore tool
for pxar archives without it too

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2021-04-12 07:56:31 +02:00
375b1f6150 d/control: rename proxmox-file-restore to proxmox-backup-file-restore
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2021-04-12 07:56:31 +02:00
109ccd300f cleanup: move tape SCSI code to src/tape/drive/lto/sg_tape/ 2021-04-09 11:34:45 +02:00
c287b28725 buildsys: dinstall: only install server/client debs
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2021-04-09 10:09:15 +02:00
c560cfddca tape: read_drive_status - ignore media changed sense info 2021-04-09 09:46:19 +02:00
44f6bb019c sgutils2: implement scsi_request_sense() 2021-04-09 09:46:19 +02:00
d6d42702d1 bump version to 1.0.14-1
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2021-04-08 18:59:07 +02:00
3fafd0e2a1 d/postinst: check for old tape.cfg
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2021-04-08 18:59:07 +02:00
59648eac3d avoid extra separate upload to pbs repo
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2021-04-08 18:45:30 +02:00
5b6b5bba68 upload file restore package only to PVE for now
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2021-04-08 18:45:30 +02:00
b13089cdf5 file-restore: add 'extract' command for VM file restore
The data on the restore daemon is either encoded into a pxar archive, to
provide the most accurate data for local restore, or encoded directly
into a zip file (or written out unprocessed for files), depending on the
'pxar' argument to the 'extract' API call.

Signed-off-by: Stefan Reiter <s.reiter@proxmox.com>
2021-04-08 14:43:41 +02:00
1f03196c0b tools/zip: add zip_directory helper
Encodes an entire local directory into an AsyncWrite recursively.

Signed-off-by: Stefan Reiter <s.reiter@proxmox.com>
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2021-04-08 14:32:03 +02:00
edf0940649 pxar/extract: add sequential variant of extract_sub_dir
extract_sub_dir_seq, together with seq_files_extractor, allow extracting
files from a pxar Decoder, along with the existing option for an
Accessor. To facilitate code re-use, some helper functions are extracted
in the process.

Signed-off-by: Stefan Reiter <s.reiter@proxmox.com>
2021-04-08 14:24:23 +02:00
801ec1dbf9 file-restore(-daemon): implement list API
Allows listing files and directories on a block device snapshot.
Hierarchy displayed is:

/archive.img.fidx/bucket/component/<path>
e.g.
/drive-scsi0.img.fidx/part/2/etc/passwd
(corresponding to /etc/passwd on the second partition of drive-scsi0)

Signed-off-by: Stefan Reiter <s.reiter@proxmox.com>
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2021-04-08 14:24:14 +02:00
34ac5cd889 debian/client: add postinst hook to rebuild file-restore initramfs
This will be triggered on updating proxmox-file-restore (via configure,
necessary since the daemon binary might change) and
proxmox-backup-restore-image (via 'activate-noawait', necessary since
the base image might change).

Signed-off-by: Stefan Reiter <s.reiter@proxmox.com>
2021-04-08 14:20:05 +02:00
58421ec112 file-restore: add basic VM/block device support
Includes methods to start, stop and list QEMU file-restore VMs, as well
as CLI commands do the latter two (start is implicit).

The implementation is abstracted behind the concept of a
"BlockRestoreDriver", so other methods can be implemented later (e.g.
mapping directly to loop devices on the host, using other hypervisors
then QEMU, etc...).

Starting VMs is currently unused but will be needed for further changes.

The design for the QEMU driver uses a locked 'map' file
(/run/proxmox-backup/$UID/restore-vm-map.json) containing a JSON
encoding of currently running VMs. VMs are addressed by a 'name', which
is a systemd-unit encoded combination of repository and snapshot string,
thus uniquely identifying it.

Note that currently you need to run proxmox-file-restore as root to use
this method of restoring.

Signed-off-by: Stefan Reiter <s.reiter@proxmox.com>
2021-04-08 14:11:02 +02:00
a5bdc987dc add tools/cpio encoding module
Signed-off-by: Stefan Reiter <s.reiter@proxmox.com>
2021-04-08 14:10:45 +02:00
d32a8652bd file-restore-daemon: add disk module
Includes functionality for scanning and referring to partitions on
attached disks (i.e. snapshot images).

Fairly modular structure, so adding ZFS/LVM/etc... support in the future
should be easy.

The path is encoded as "/disk/bucket/component/path/to/file", e.g.
"/drive-scsi0/part/0/etc/passwd". See the comments for further
explanations on the design.

Signed-off-by: Stefan Reiter <s.reiter@proxmox.com>
2021-04-08 14:03:54 +02:00
a26ebad5f9 file-restore-daemon: add watchdog module
Add a watchdog that will automatically shut down the VM after 10
minutes, if no API call is received.

Signed-off-by: Stefan Reiter <s.reiter@proxmox.com>
2021-04-08 13:58:29 +02:00
dd9cef56fc file-restore-daemon: add binary with virtio-vsock API server
Implements the base of a small daemon to run within a file-restore VM.

The binary spawns an API server on a virtio-vsock socket, listening for
connections from the host. This happens mostly manually via the standard
Unix socket API, since tokio/hyper do not have support for vsock built
in. Once we have the accept'ed file descriptor, we can create a
UnixStream and use our tower service implementation for that.

The binary is deliberately not installed in the usual $PATH location,
since it shouldn't be executed on the host by a user anyway.

For now, only the API calls 'status' and 'stop' are implemented, to
demonstrate and test proxmox::api functionality.

Authorization is provided via a custom ApiAuth only checking a header
value against a static /ticket file.

Since the REST server implementation uses the log!() macro, we can
redirect its output to stdout by registering env_logger as the logging
target. env_logger is already in our dependency tree via zstd/bindgen.

Signed-off-by: Stefan Reiter <s.reiter@proxmox.com>
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2021-04-08 13:57:57 +02:00
26858dba84 server/rest: add ApiAuth trait to make user auth generic
This allows switching the base user identification/authentication method
in the rest server. Will initially be used for single file restore VMs,
where authentication is based on a ticket file, not the PBS user
backend (PAM/local).

To avoid putting generic types into the RestServer type for this, we
merge the two calls "extract_auth_data" and "check_auth" into a single
one, which can use whatever type it wants internally.

Signed-off-by: Stefan Reiter <s.reiter@proxmox.com>
2021-04-08 13:57:57 +02:00
9fe3358ce6 file-restore: allow specifying output-format
Makes CLI use more comfortable by not just printing JSON to the
terminal.

Signed-off-by: Stefan Reiter <s.reiter@proxmox.com>
2021-04-08 13:57:57 +02:00
76425d84b3 file-restore: add binary and basic commands
For now it only supports 'list' and 'extract' commands for 'pxar.didx'
files. This should be the foundation for a general file-restore
interface that is shared with block-level snapshots.

This is packaged as a seperate .deb file, since for block level restore
it will need to depend on pve-qemu-kvm, which we want to seperate from
proxmox-backup-client.

[original code for proxmox-file-restore.rs]
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>

[code cleanups/clippy, use helpers::list_dir_content/ArchiveEntry, no
/block subdir for .fidx files, seperate binary and package]
Signed-off-by: Stefan Reiter <s.reiter@proxmox.com>
2021-04-08 13:57:57 +02:00
42355b11a4 update d/control
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2021-04-08 13:57:57 +02:00
511e4f6987 ui: tape/DriveStatus: improve status grid a bit
by using format_boolean for compression/write protect,
combining file/block posiition into one (saves a line)

and adding the missing alert-flags

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2021-04-08 13:56:46 +02:00
3f0e344bc1 ui: tape/ChangerStatus: hide selector for single drives in barcode-label
it is rather pointless to let the user select something were there
is no choice. We have to keep the window though, since the user may
want to choose a pool

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2021-04-08 13:56:46 +02:00
a316178768 ui: tape/ChangerStatus: shortcut Inventory for single drives
like 'load-media'

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2021-04-08 13:56:46 +02:00
dff8ea92aa ui: tape/ChangerStatus: shortcut 'load-media' for single drive
if a changer only has a single drive, there is no point in showing
a window with a DriveSelector, just do want the user wanted.

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2021-04-08 13:56:46 +02:00
88e1f7997c ui: tape/ChangerStatus: rework EraseWindow
to make it more like a 'dangerous' remove window
also works in the singleDrive logic to hide/show the driveselector

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2021-04-08 13:56:46 +02:00
4c3eabeaf3 ui: tape/ChangerStatus: save assigned drives
so that we can shortcut later if we only have one

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2021-04-08 13:56:46 +02:00
4c7be5f59d ui: tape/ChangerStatus: add missing property
it will actually not fail, but we declare it nonetheless to indicate
that it exists

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2021-04-08 13:56:46 +02:00
6d4fbbc3ea ui: dashobard/DataStoreStatistics: add 'Available' column
for some storages, it is valuable information, e.g. if one has datastores
on separate datasets of the same zpool

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2021-04-08 13:27:22 +02:00
1a23132262 tape: add TapeDensity::Unknown 2021-04-08 12:23:54 +02:00
48c4193f7c ui: update tape DriveStatus for new driver 2021-04-08 12:04:14 +02:00
8204d9b095 tape: avoid unneccessary SCSI request in Drop 2021-04-08 11:26:08 +02:00
fad95a334a tape: clear encryption key after backup (for security reasons) 2021-04-08 10:37:49 +02:00
973e985d73 cleanup: remove unused linux tape driver code 2021-04-08 10:15:52 +02:00
e5a13382b2 ui: tape/TapeRestore: use correct value check for store & mapping
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2021-04-08 10:02:31 +02:00
81c0b90447 ui: tape/TapeRestore: fix restoring without mapping
we have to delete the 'mapping' variable in any case since it's not
a valid api parameter

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2021-04-08 10:02:17 +02:00
ee9fa953de docs: Mention our new user space tape driver, adopt device path names 2021-04-08 09:50:09 +02:00
09acf0a70d do not depend on mt-st and mtx
We now use our own driver, so those tools are no longer required.
2021-04-08 09:48:47 +02:00
15d1435789 tape: add vendor, product and revision to LtoDriveAndMediaStatus 2021-04-08 08:34:46 +02:00
80ea23e1b9 tape: pmt - implement options command 2021-04-08 08:34:45 +02:00
5d6379f8db tape: implement locate_file without LOCATE(10) 2021-04-08 08:34:45 +02:00
566b946f9b tape: pmt - re-implement lock/unlock command 2021-04-08 07:28:30 +02:00
7f7459677d tape: pmt - re-implement fsr/bsr 2021-04-08 07:28:30 +02:00
0892a512bc tape: correctly set/display drive option 2021-04-08 07:28:30 +02:00
b717871d2a sgutils2: add scsi_mode_sense helper 2021-04-08 07:28:30 +02:00
7b11a8098d tape: make sure there is a filemark at the end of the tape 2021-04-08 07:28:30 +02:00
8b2c6f5dbc tape: make fsf/bsf driver specific
Because the virtual tape driver behaves different than LTO drives.
2021-04-08 07:28:30 +02:00
d26985a600 tape: fix LEOM handling 2021-04-08 07:28:30 +02:00
e29f456efc tape: implement format/erase 2021-04-08 07:28:30 +02:00
a79082a0dd tape: implement LTO userspace driver 2021-04-08 07:28:30 +02:00
1336ae8249 tape: introduce trait BlockWrite 2021-04-08 07:28:30 +02:00
0db5712493 tape: introduce trait BlockRead 2021-04-08 07:28:30 +02:00
c47609fedb server: rest: collapse nested if for less indentation
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2021-04-07 17:57:46 +02:00
b84e8aaee9 server: rest: switch from fastest to default deflate compression level
I made some comparision with bombardier[0], the one listed here are
30s looped requests with two concurrent clients:

[ static download of ext-all.js ]:
  lvl                              avg /   stdev  / max
 none        1.98 MiB  100 %    5.17ms /  1.30ms / 32.38ms
 fastest   813.14 KiB   42 %   20.53ms /  2.85ms / 58.71ms
 default   626.35 KiB   30 %   39.70ms /  3.98ms / 85.47ms

[ deterministic (pre-defined data), but real API call ]:
  lvl                              avg /   stdev  / max
 none      129.09 KiB  100 %    2.70ms / 471.58us / 26.93ms
 fastest    42.12 KiB   33 %    3.47ms / 606.46us / 32.42ms
 default    34.82 KiB   27 %    4.28ms / 737.99us / 33.75ms

The reduction is quite better with default, but it's also slower, but
only when testing over unconstrained network. For real world
scenarios where compression actually matters, e.g., when using a
spotty train connection, we will be faster again with better
compression.

A GPRS limited connection (Firefox developer console) requires the
following load (until the DOMContentLoaded event triggered) times:
  lvl        t      x faster
 none      9m 18.6s   x 1.0
 fastest   3m 20.0s   x 2.8
 default   2m 30.0s   x 3.7

So for worst case using sligthly more CPU time on the server has a
tremendous effect on the client load time.

Using a more realistical example and limiting for "Good 2G" gives:

 none      1m  1.8s   x 1.0
 fastest      22.6s   x 2.7
 default      16.6s   x 3.7

16s is somewhat OK, >1m just isn't...

So, use default level to ensure we get bearable load times on
clients, and if we want to improve transmission size AND speed then
we could always use a in-memory cache, only a few MiB would be
required for the compressable static files we server.

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2021-04-07 17:57:42 +02:00
d84e4073af tools/zip: compress zips with deflate
by using our DeflateEncoder

for this to work, we have to create wrapper reader that generates the crc32
checksum while reading.

also we need to put the target writer in an Option, so that we can take
it out of self and move it into the DeflateEncoder while writing
compressed

we can drop the internal buffer then, since that is managed by the
deflate encoder now

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2021-04-07 12:34:31 +02:00
e8656da70d tools/zip: run rustfmt
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2021-04-07 12:34:31 +02:00
59477ad252 server/rest: compress static files
compress them on the fly
and refactor the size limit for chunking files

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2021-04-07 12:34:31 +02:00
2f29f1c765 server/rest: compress api calls
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2021-04-07 12:34:31 +02:00
4d84e869bf server/rest: add helper to extract compression headers
for now we only extract 'deflate'

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2021-04-07 12:34:31 +02:00
79d841014e tools/compression: add DeflateEncoder and helpers
implements a deflate encoder that can compress anything that implements
AsyncRead + Unpin into a file with the helper 'compress'

if the inner type is a Stream, it implements Stream itself, this way
some streaming data can be streamed compressed

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2021-04-07 12:34:31 +02:00
ea62611d8e tools: add compression module
only contains a basic enum for the different compresssion methods

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2021-04-07 12:34:31 +02:00
f3c867a034 docs: fix MathJax inclusion in build
Else it does not gets picked up on release builds...

Also the mathjax path option affects HTML not EPUB so move it to the
correct section in conf.py

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2021-04-05 11:59:45 +02:00
aae5db916e docs: fix heading
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2021-04-05 11:59:18 +02:00
131 changed files with 6569 additions and 3016 deletions

View File

@ -1,6 +1,6 @@
[package]
name = "proxmox-backup"
version = "1.0.13"
version = "1.1.2"
authors = [
"Dietmar Maurer <dietmar@proxmox.com>",
"Dominik Csapak <d.csapak@proxmox.com>",
@ -29,7 +29,10 @@ bitflags = "1.2.1"
bytes = "1.0"
crc32fast = "1"
endian_trait = { version = "0.6", features = ["arrays"] }
env_logger = "0.7"
flate2 = "1.0"
anyhow = "1.0"
thiserror = "1.0"
futures = "0.3"
h2 = { version = "0.3", features = [ "stream" ] }
handlebars = "3.0"
@ -48,7 +51,7 @@ percent-encoding = "2.1"
pin-utils = "0.1.0"
pin-project = "1.0"
pathpatterns = "0.1.2"
proxmox = { version = "0.11.0", features = [ "sortable-macro", "api-macro", "websocket" ] }
proxmox = { version = "0.11.1", features = [ "sortable-macro", "api-macro", "websocket" ] }
#proxmox = { git = "git://git.proxmox.com/git/proxmox", version = "0.1.2", features = [ "sortable-macro", "api-macro" ] }
#proxmox = { path = "../proxmox/proxmox", features = [ "sortable-macro", "api-macro", "websocket" ] }
proxmox-fuse = "0.1.1"
@ -60,10 +63,10 @@ serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
siphasher = "0.3"
syslog = "4.0"
tokio = { version = "1.0", features = [ "fs", "io-util", "macros", "net", "parking_lot", "process", "rt", "rt-multi-thread", "signal", "time" ] }
tokio = { version = "1.0", features = [ "fs", "io-util", "io-std", "macros", "net", "parking_lot", "process", "rt", "rt-multi-thread", "signal", "time" ] }
tokio-openssl = "0.6.1"
tokio-stream = "0.1.0"
tokio-util = { version = "0.6", features = [ "codec" ] }
tokio-util = { version = "0.6", features = [ "codec", "io" ] }
tower-service = "0.3.0"
udev = ">= 0.3, <0.5"
url = "2.1"

View File

@ -9,6 +9,7 @@ SUBDIRS := etc www docs
# Binaries usable by users
USR_BIN := \
proxmox-backup-client \
proxmox-file-restore \
pxar \
proxmox-tape \
pmtx \
@ -25,6 +26,10 @@ SERVICE_BIN := \
proxmox-backup-proxy \
proxmox-daily-update
# Single file restore daemon
RESTORE_BIN := \
proxmox-restore-daemon
ifeq ($(BUILD_MODE), release)
CARGO_BUILD_ARGS += --release
COMPILEDIR := target/release
@ -39,7 +44,7 @@ endif
CARGO ?= cargo
COMPILED_BINS := \
$(addprefix $(COMPILEDIR)/,$(USR_BIN) $(USR_SBIN) $(SERVICE_BIN))
$(addprefix $(COMPILEDIR)/,$(USR_BIN) $(USR_SBIN) $(SERVICE_BIN) $(RESTORE_BIN))
export DEB_VERSION DEB_VERSION_UPSTREAM
@ -47,9 +52,12 @@ SERVER_DEB=${PACKAGE}-server_${DEB_VERSION}_${ARCH}.deb
SERVER_DBG_DEB=${PACKAGE}-server-dbgsym_${DEB_VERSION}_${ARCH}.deb
CLIENT_DEB=${PACKAGE}-client_${DEB_VERSION}_${ARCH}.deb
CLIENT_DBG_DEB=${PACKAGE}-client-dbgsym_${DEB_VERSION}_${ARCH}.deb
RESTORE_DEB=proxmox-backup-file-restore_${DEB_VERSION}_${ARCH}.deb
RESTORE_DBG_DEB=proxmox-backup-file-restore-dbgsym_${DEB_VERSION}_${ARCH}.deb
DOC_DEB=${PACKAGE}-docs_${DEB_VERSION}_all.deb
DEBS=${SERVER_DEB} ${SERVER_DBG_DEB} ${CLIENT_DEB} ${CLIENT_DBG_DEB}
DEBS=${SERVER_DEB} ${SERVER_DBG_DEB} ${CLIENT_DEB} ${CLIENT_DBG_DEB} \
${RESTORE_DEB} ${RESTORE_DBG_DEB}
DSC = rust-${PACKAGE}_${DEB_VERSION}.dsc
@ -117,8 +125,8 @@ clean:
find . -name '*~' -exec rm {} ';'
.PHONY: dinstall
dinstall: ${DEBS}
dpkg -i ${DEBS}
dinstall: ${SERVER_DEB} ${SERVER_DBG_DEB} ${CLIENT_DEB} ${CLIENT_DBG_DEB}
dpkg -i $^
# make sure we build binaries before docs
docs: cargo-build
@ -144,6 +152,9 @@ install: $(COMPILED_BINS)
install -m755 $(COMPILEDIR)/$(i) $(DESTDIR)$(SBINDIR)/ ; \
install -m644 zsh-completions/_$(i) $(DESTDIR)$(ZSH_COMPL_DEST)/ ;)
install -dm755 $(DESTDIR)$(LIBEXECDIR)/proxmox-backup
install -dm755 $(DESTDIR)$(LIBEXECDIR)/proxmox-backup/file-restore
$(foreach i,$(RESTORE_BIN), \
install -m755 $(COMPILEDIR)/$(i) $(DESTDIR)$(LIBEXECDIR)/proxmox-backup/file-restore/ ;)
# install sg-tape-cmd as setuid binary
install -m4755 -o root -g root $(COMPILEDIR)/sg-tape-cmd $(DESTDIR)$(LIBEXECDIR)/proxmox-backup/sg-tape-cmd
$(foreach i,$(SERVICE_BIN), \
@ -152,8 +163,10 @@ install: $(COMPILED_BINS)
$(MAKE) -C docs install
.PHONY: upload
upload: ${SERVER_DEB} ${CLIENT_DEB} ${DOC_DEB}
upload: ${SERVER_DEB} ${CLIENT_DEB} ${RESTORE_DEB} ${DOC_DEB}
# 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} | ssh -X repoman@repo.proxmox.com upload --product pbs --dist buster
tar cf - ${CLIENT_DEB} ${CLIENT_DBG_DEB} | ssh -X repoman@repo.proxmox.com upload --product "pbs,pve,pmg" --dist buster
tar cf - ${SERVER_DEB} ${SERVER_DBG_DEB} ${DOC_DEB} ${CLIENT_DEB} ${CLIENT_DBG_DEB} | \
ssh -X repoman@repo.proxmox.com upload --product pbs --dist buster
tar cf - ${CLIENT_DEB} ${CLIENT_DBG_DEB} | ssh -X repoman@repo.proxmox.com upload --product "pve,pmg" --dist buster
tar cf - ${RESTORE_DEB} ${RESTORE_DBG_DEB} | ssh -X repoman@repo.proxmox.com upload --product "pve" --dist buster

65
debian/changelog vendored
View File

@ -1,3 +1,68 @@
rust-proxmox-backup (1.1.2-1) unstable; urgency=medium
* backup verify: always re-check if we can skip a chunk in the actual verify
loop.
* tape: do not try to backup unfinished backups
-- Proxmox Support Team <support@proxmox.com> Thu, 15 Apr 2021 13:26:52 +0200
rust-proxmox-backup (1.1.1-1) unstable; urgency=medium
* docs: include tape in table of contents
* docs: tape: improve definition-list format and add screenshots
* docs: reorder maintenance and network chapters after client-usage/tools
chapters
* ui: tape changer status: add Format button to drive grid
* backup/verify: improve speed on disks with slow random-IO (spinners) by
iterating over chunks sorted by inode
-- Proxmox Support Team <support@proxmox.com> Wed, 14 Apr 2021 14:50:29 +0200
rust-proxmox-backup (1.1.0-1) unstable; urgency=medium
* enable tape backup as technology preview by default
* tape: read drive status: clear deferred error or media changed events.
* tape: improve end-of-tape (EOT) error handling
* tape: cleanup media catalog on tape reuse
* zfs: re-use underlying pool wide IO stats for datasets
* api daemon: only log error from accepting new connections to avoid opening
to many file descriptors
* api/datastore: allow downloading the entire archive as ZIP archive, not
only sub-paths
-- Proxmox Support Team <support@proxmox.com> Tue, 13 Apr 2021 14:42:18 +0200
rust-proxmox-backup (1.0.14-1) unstable; urgency=medium
* server: compress API call response and static files if client accepts that
* compress generated ZIP archives with deflate
* tape: implement LTO userspace driver
* docs: mention new user space tape driver, adopt device path names
* tape: always clear encryption key after backup (for security reasons)
* ui: improve changer status view
* add proxmox-file-restore package, providing a central file-restore binary
with preparations for restoring files also from block level backups using
QEMU for a safe encapsulation.
-- Proxmox Support Team <support@proxmox.com> Thu, 08 Apr 2021 16:35:11 +0200
rust-proxmox-backup (1.0.13-1) unstable; urgency=medium
* pxar: improve handling ACL entries on create and restore

28
debian/control vendored
View File

@ -15,6 +15,8 @@ Build-Depends: debhelper (>= 11),
librust-crossbeam-channel-0.5+default-dev,
librust-endian-trait-0.6+arrays-dev,
librust-endian-trait-0.6+default-dev,
librust-env-logger-0.7+default-dev,
librust-flate2-1+default-dev,
librust-futures-0.3+default-dev,
librust-h2-0.3+default-dev,
librust-h2-0.3+stream-dev,
@ -36,10 +38,10 @@ Build-Depends: debhelper (>= 11),
librust-percent-encoding-2+default-dev (>= 2.1-~~),
librust-pin-project-1+default-dev,
librust-pin-utils-0.1+default-dev,
librust-proxmox-0.11+api-macro-dev,
librust-proxmox-0.11+default-dev,
librust-proxmox-0.11+sortable-macro-dev,
librust-proxmox-0.11+websocket-dev,
librust-proxmox-0.11+api-macro-dev (>= 0.11.1-~~),
librust-proxmox-0.11+default-dev (>= 0.11.1-~~),
librust-proxmox-0.11+sortable-macro-dev (>= 0.11.1-~~),
librust-proxmox-0.11+websocket-dev (>= 0.11.1-~~),
librust-proxmox-fuse-0.1+default-dev (>= 0.1.1-~~),
librust-pxar-0.10+default-dev (>= 0.10.1-~~),
librust-pxar-0.10+tokio-io-dev (>= 0.10.1-~~),
@ -50,8 +52,10 @@ Build-Depends: debhelper (>= 11),
librust-serde-json-1+default-dev,
librust-siphasher-0.3+default-dev,
librust-syslog-4+default-dev,
librust-thiserror-1+default-dev,
librust-tokio-1+default-dev,
librust-tokio-1+fs-dev,
librust-tokio-1+io-std-dev,
librust-tokio-1+io-util-dev,
librust-tokio-1+macros-dev,
librust-tokio-1+net-dev,
@ -65,6 +69,7 @@ Build-Depends: debhelper (>= 11),
librust-tokio-stream-0.1+default-dev,
librust-tokio-util-0.6+codec-dev,
librust-tokio-util-0.6+default-dev,
librust-tokio-util-0.6+io-dev,
librust-tower-service-0.3+default-dev,
librust-udev-0.4+default-dev | librust-udev-0.3+default-dev,
librust-url-2+default-dev (>= 2.1-~~),
@ -109,14 +114,12 @@ Depends: fonts-font-awesome,
libsgutils2-2,
libzstd1 (>= 1.3.8),
lvm2,
mt-st,
mtx,
openssh-server,
pbs-i18n,
postfix | mail-transport-agent,
proxmox-backup-docs,
proxmox-mini-journalreader,
proxmox-widget-toolkit (>= 2.3-6),
proxmox-widget-toolkit (>= 2.5-1),
pve-xtermjs (>= 4.7.0-1),
sg3-utils,
smartmontools,
@ -146,3 +149,14 @@ Depends: libjs-extjs,
Architecture: all
Description: Proxmox Backup Documentation
This package contains the Proxmox Backup Documentation files.
Package: proxmox-backup-file-restore
Architecture: any
Depends: ${misc:Depends},
${shlibs:Depends},
Recommends: pve-qemu-kvm (>= 5.0.0-9),
proxmox-backup-restore-image,
Description: Proxmox Backup single file restore tools for pxar and block device backups
This package contains the Proxmox Backup single file restore client for
restoring individual files and folders from both host/container and VM/block
device backups. It includes a block device restore driver using QEMU.

15
debian/control.in vendored
View File

@ -6,14 +6,12 @@ Depends: fonts-font-awesome,
libsgutils2-2,
libzstd1 (>= 1.3.8),
lvm2,
mt-st,
mtx,
openssh-server,
pbs-i18n,
postfix | mail-transport-agent,
proxmox-backup-docs,
proxmox-mini-journalreader,
proxmox-widget-toolkit (>= 2.3-6),
proxmox-widget-toolkit (>= 2.5-1),
pve-xtermjs (>= 4.7.0-1),
sg3-utils,
smartmontools,
@ -43,3 +41,14 @@ Depends: libjs-extjs,
Architecture: all
Description: Proxmox Backup Documentation
This package contains the Proxmox Backup Documentation files.
Package: proxmox-backup-file-restore
Architecture: any
Depends: ${misc:Depends},
${shlibs:Depends},
Recommends: pve-qemu-kvm (>= 5.0.0-9),
proxmox-backup-restore-image,
Description: Proxmox Backup single file restore tools for pxar and block device backups
This package contains the Proxmox Backup single file restore client for
restoring individual files and folders from both host/container and VM/block
device backups. It includes a block device restore driver using QEMU.

10
debian/postinst vendored
View File

@ -48,6 +48,16 @@ case "$1" in
/etc/proxmox-backup/remote.cfg || true
fi
fi
if dpkg --compare-versions "$2" 'le' '1.0.14-1'; then
# FIXME: Remove with 2.0
if grep -s -q -P -e '^linux:' /etc/proxmox-backup/tape.cfg; then
echo "========="
echo "= NOTE: You have now unsupported 'linux' tape drives configured."
echo "= * Execute 'udevadm control --reload-rules && udevadm trigger' to update /dev"
echo "= * Edit '/etc/proxmox-backup/tape.cfg', remove 'linux' entries and re-add over CLI/GUI"
echo "========="
fi
fi
# FIXME: remove with 2.0
if [ -d "/var/lib/proxmox-backup/tape" ] &&
[ "$(stat --printf '%a' '/var/lib/proxmox-backup/tape')" != "750" ]; then

View File

@ -0,0 +1 @@
debian/proxmox-file-restore.bc proxmox-file-restore

8
debian/proxmox-backup-file-restore.bc vendored Normal file
View File

@ -0,0 +1,8 @@
# proxmox-file-restore bash completion
# see http://tiswww.case.edu/php/chet/bash/FAQ
# and __ltrim_colon_completions() in /usr/share/bash-completion/bash_completion
# this modifies global var, but I found no better way
COMP_WORDBREAKS=${COMP_WORDBREAKS//:}
complete -C 'proxmox-file-restore bashcomplete' proxmox-file-restore

View File

@ -0,0 +1,4 @@
usr/bin/proxmox-file-restore
usr/share/man/man1/proxmox-file-restore.1
usr/share/zsh/vendor-completions/_proxmox-file-restore
usr/lib/x86_64-linux-gnu/proxmox-backup/file-restore/proxmox-restore-daemon

64
debian/proxmox-backup-file-restore.postinst vendored Executable file
View File

@ -0,0 +1,64 @@
#!/bin/sh
set -e
update_initramfs() {
# regenerate initramfs for single file restore VM
INST_PATH="/usr/lib/x86_64-linux-gnu/proxmox-backup/file-restore"
CACHE_PATH="/var/cache/proxmox-backup/file-restore-initramfs.img"
# cleanup first, in case proxmox-file-restore was uninstalled since we do
# not want an unuseable image lying around
rm -f "$CACHE_PATH"
if [ ! -f "$INST_PATH/initramfs.img" ]; then
echo "proxmox-backup-restore-image is not installed correctly, skipping update" >&2
exit 0
fi
echo "Updating file-restore initramfs..."
# avoid leftover temp file
cleanup() {
rm -f "$CACHE_PATH.tmp"
}
trap cleanup EXIT
mkdir -p "/var/cache/proxmox-backup"
cp "$INST_PATH/initramfs.img" "$CACHE_PATH.tmp"
# cpio uses passed in path as offset inside the archive as well, so we need
# to be in the same dir as the daemon binary to ensure it's placed in /
( cd "$INST_PATH"; \
printf "./proxmox-restore-daemon" \
| cpio -o --format=newc -A -F "$CACHE_PATH.tmp" )
mv -f "$CACHE_PATH.tmp" "$CACHE_PATH"
trap - EXIT
}
case "$1" in
configure)
# in case restore daemon was updated
update_initramfs
;;
triggered)
if [ "$2" = "proxmox-backup-restore-image-update" ]; then
# in case base-image was updated
update_initramfs
else
echo "postinst called with unknown trigger name: \`$2'" >&2
fi
;;
abort-upgrade|abort-remove|abort-deconfigure)
;;
*)
echo "postinst called with unknown argument \`$1'" >&2
exit 1
;;
esac
exit 0

View File

@ -0,0 +1 @@
interest-noawait proxmox-backup-restore-image-update

18
debian/proxmox-backup-server.udev vendored Normal file
View File

@ -0,0 +1,18 @@
# do not edit this file, it will be overwritten on update
# persistent storage links: /dev/tape/{by-id,by-path}
ACTION=="remove", GOTO="persistent_storage_tape_end"
ENV{UDEV_DISABLE_PERSISTENT_STORAGE_RULES_FLAG}=="1", GOTO="persistent_storage_tape_end"
# 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"
# iSCSI devices from the same host have all the same ID_SERIAL,
# but additionally a property named ID_SCSI_SERIAL.
SUBSYSTEM=="scsi_generic", SUBSYSTEMS=="scsi", ATTRS{type}=="1", ENV{ID_SCSI_SERIAL}=="?*", \
SYMLINK+="tape/by-id/scsi-$env{ID_SCSI_SERIAL}-sg"
LABEL="persistent_storage_tape_end"

7
debian/rules vendored
View File

@ -52,8 +52,11 @@ override_dh_dwz:
override_dh_strip:
dh_strip
for exe in $$(find debian/proxmox-backup-client/usr \
debian/proxmox-backup-server/usr -executable -type f); do \
for exe in $$(find \
debian/proxmox-backup-client/usr \
debian/proxmox-backup-server/usr \
debian/proxmox-backup-file-restore \
-executable -type f); do \
debian/scripts/elf-strip-unused-dependencies.sh "$$exe" || true; \
done

View File

@ -5,6 +5,7 @@ GENERATED_SYNOPSIS := \
proxmox-backup-client/synopsis.rst \
proxmox-backup-client/catalog-shell-synopsis.rst \
proxmox-backup-manager/synopsis.rst \
proxmox-file-restore/synopsis.rst \
pxar/synopsis.rst \
pmtx/synopsis.rst \
pmt/synopsis.rst \
@ -25,7 +26,8 @@ MAN1_PAGES := \
proxmox-tape.1 \
proxmox-backup-proxy.1 \
proxmox-backup-client.1 \
proxmox-backup-manager.1
proxmox-backup-manager.1 \
proxmox-file-restore.1
MAN5_PAGES := \
media-pool.cfg.5 \
@ -179,6 +181,12 @@ proxmox-backup-manager.1: proxmox-backup-manager/man1.rst proxmox-backup-manage
proxmox-backup-proxy.1: proxmox-backup-proxy/man1.rst proxmox-backup-proxy/description.rst
rst2man $< >$@
proxmox-file-restore/synopsis.rst: ${COMPILEDIR}/proxmox-file-restore
${COMPILEDIR}/proxmox-file-restore printdoc > proxmox-file-restore/synopsis.rst
proxmox-file-restore.1: proxmox-file-restore/man1.rst proxmox-file-restore/description.rst proxmox-file-restore/synopsis.rst
rst2man $< >$@
.PHONY: onlinehelpinfo
onlinehelpinfo:
@echo "Generating OnlineHelpInfo.js..."

View File

@ -143,7 +143,7 @@ Ext.onReady(function() {
permhtml += "</div></div>";
} else {
//console.log(permission);
permhtml += "Unknown systax!";
permhtml += "Unknown syntax!";
}
return permhtml;

View File

@ -5,8 +5,8 @@ The command line client is called :command:`proxmox-backup-client`.
.. _client_repository:
Repository Locations
--------------------
Backup Repository Locations
---------------------------
The client uses the following notation to specify a datastore repository
on the backup server.
@ -472,7 +472,7 @@ located in ``/etc``, you could do the following:
pxar:/ > restore target/ --pattern etc/**/*.conf
...
The above will scan trough all the directories below ``/etc`` and restore all
The above will scan through all the directories below ``/etc`` and restore all
files ending in ``.conf``.
.. todo:: Explain interactive restore in more detail

View File

@ -6,6 +6,11 @@ Command Line Tools
.. include:: proxmox-backup-client/description.rst
``proxmox-file-restore``
~~~~~~~~~~~~~~~~~~~~~~~~~
.. include:: proxmox-file-restore/description.rst
``proxmox-backup-manager``
~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -26,6 +26,27 @@ Those command are available when you start an interactive restore shell:
.. include:: proxmox-backup-manager/synopsis.rst
``proxmox-tape``
----------------
.. include:: proxmox-tape/synopsis.rst
``pmt``
-------
.. include:: pmt/options.rst
....
.. include:: pmt/synopsis.rst
``pmtx``
--------
.. include:: pmtx/synopsis.rst
``pxar``
--------

View File

@ -49,7 +49,7 @@ PygmentsBridge.latex_formatter = CustomLatexFormatter
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = ["sphinx.ext.graphviz", "sphinx.ext.todo", "proxmox-scanrefs"]
extensions = ["sphinx.ext.graphviz", 'sphinx.ext.mathjax', "sphinx.ext.todo", "proxmox-scanrefs"]
todo_link_only = True
@ -307,6 +307,9 @@ html_show_sourcelink = False
# Output file base name for HTML help builder.
htmlhelp_basename = 'ProxmoxBackupdoc'
# use local mathjax package, symlink comes from debian/proxmox-backup-docs.links
mathjax_path = "mathjax/MathJax.js?config=TeX-AMS-MML_HTMLorMML"
# -- Options for LaTeX output ---------------------------------------------
latex_engine = 'xelatex'
@ -464,6 +467,3 @@ epub_exclude_files = ['search.html']
# If false, no index is generated.
#
# epub_use_index = True
# use local mathjax package, symlink comes from debian/proxmox-backup-docs.links
mathjax_path = "mathjax/MathJax.js?config=TeX-AMS-MML_HTMLorMML"

View File

@ -1,4 +1,4 @@
Each drive configuration section starts with a header ``linux: <name>``,
Each LTO drive configuration section starts with a header ``lto: <name>``,
followed by the drive configuration options.
Tape changer configurations starts with ``changer: <name>``,
@ -6,7 +6,7 @@ followed by the changer configuration options.
::
linux: hh8
lto: hh8
changer sl3
path /dev/tape/by-id/scsi-10WT065325-nst

View File

@ -37,8 +37,53 @@ Options
.. include:: config/datastore/config.rst
``media-pool.cfg``
~~~~~~~~~~~~~~~~~~
File Format
^^^^^^^^^^^
.. include:: config/media-pool/format.rst
Options
^^^^^^^
.. include:: config/media-pool/config.rst
``tape.cfg``
~~~~~~~~~~~~
File Format
^^^^^^^^^^^
.. include:: config/tape/format.rst
Options
^^^^^^^
.. include:: config/tape/config.rst
``tape-job.cfg``
~~~~~~~~~~~~~~~~
File Format
^^^^^^^^^^^
.. include:: config/tape-job/format.rst
Options
^^^^^^^
.. include:: config/tape-job/config.rst
``user.cfg``
~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~
File Format
^^^^^^^^^^^

View File

@ -61,9 +61,7 @@ attacker gains access to the server or any point of the network, they will not
be able to read the data.
.. note:: Encryption is not enabled by default. To set up encryption, see the
`Encryption
<https://pbs.proxmox.com/docs/administration-guide.html#encryption>`_ section
of the Proxmox Backup Server Administration Guide.
:ref:`backup client encryption section <client_encryption>`.
Is the backup incremental/deduplicated?

View File

@ -112,6 +112,18 @@ The administration menu item also contains a disk management subsection:
* **Directory**: Create and view information on *ext4* and *xfs* disks
* **ZFS**: Create and view information on *ZFS* disks
Tape Backup
^^^^^^^^^^^
.. image:: images/screenshots/pbs-gui-tape-changer-overview.png
:align: right
:alt: Tape Backup: Tape changer overview
The `Tape Backup`_ section contains a top panel, managing tape media sets,
inventories, drives, changers and the tape backup jobs itself.
It also contains a subsection per standalone drive and per changer, with a
status and management view for those devices.
Datastore
^^^^^^^^^

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

View File

@ -25,14 +25,15 @@ in the section entitled "GNU Free Documentation License".
terminology.rst
gui.rst
storage.rst
network-management.rst
user-management.rst
managing-remotes.rst
maintenance.rst
backup-client.rst
pve-integration.rst
pxar-tool.rst
tape-backup.rst
managing-remotes.rst
maintenance.rst
sysadmin.rst
network-management.rst
technical-overview.rst
faq.rst

View File

@ -113,9 +113,9 @@ Client Installation
Install `Proxmox Backup`_ Client on Debian
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Proxmox ships as a set of Debian packages to be installed on
top of a standard Debian installation. After configuring the
:ref:`sysadmin_package_repositories`, you need to run:
Proxmox ships as a set of Debian packages to be installed on top of a standard
Debian installation. After configuring the :ref:`package_repositories_client_only_apt`,
you need to run:
.. code-block:: console
@ -123,12 +123,6 @@ top of a standard Debian installation. After configuring the
# apt-get install proxmox-backup-client
Installing from source
~~~~~~~~~~~~~~~~~~~~~~
.. note:: The client-only repository should be usable by most recent Debian and
Ubuntu derivatives.
.. todo:: Add section "Installing from source"
Installing statically linked binary
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. todo:: Add section "Installing statically linked binary"

View File

@ -76,7 +76,7 @@ Main Features
:Open Source: No secrets. Proxmox Backup Server is free and open-source
software. The source code is licensed under AGPL, v3.
:No Limits: Proxmox Backup Server has no artifical limits for backup storage or
:No Limits: Proxmox Backup Server has no artificial limits for backup storage or
backup-clients.
:Enterprise Support: Proxmox Server Solutions GmbH offers enterprise support in
@ -149,8 +149,8 @@ Enterprise Support
Users with a `Proxmox Backup Server Basic, Standard or Premium Subscription Plan
<https://www.proxmox.com/en/proxmox-backup-server/pricing>`_ have access to the
Proxmox Customer Portal. The Customer Portal provides support with guaranteed
response times from the Proxmox developers.
`Proxmox Customer Portal <https://my.proxmox.com>`_. The customer portal
provides support with guaranteed response times from the Proxmox developers.
For more information or for volume discounts, please contact office@proxmox.com.
Community Support Forum

View File

@ -148,7 +148,7 @@ are checked again. The interface for creating verify jobs can be found under the
**Verify Jobs** tab of the datastore.
.. Note:: It is recommended that you reverify all backups at least monthly, even
if a previous verification was successful. This is becuase physical drives
if a previous verification was successful. This is because physical drives
are susceptible to damage over time, which can cause an old, working backup
to become corrupted in a process known as `bit rot/data degradation
<https://en.wikipedia.org/wiki/Data_degradation>`_. It is good practice to

View File

@ -29,6 +29,8 @@ update``.
In addition, you need a package repository from Proxmox to get Proxmox Backup
updates.
.. _package_repos_secure_apt:
SecureApt
~~~~~~~~~
@ -139,3 +141,40 @@ You can access this repository by adding the following line to
:caption: sources.list entry for ``pbstest``
deb http://download.proxmox.com/debian/pbs buster pbstest
.. _package_repositories_client_only:
Proxmox Backup Client-only Repository
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If you want to :ref:`use the the Proxmox Backup Client <client_creating_backups>`
on systems using a Linux distribution not based on Proxmox projects, you can
use the client-only repository.
Currently there's only a client-repository for APT based systems.
.. _package_repositories_client_only_apt:
APT-based Proxmox Backup Client Repository
++++++++++++++++++++++++++++++++++++++++++
For modern Linux distributions using `apt` as package manager, like all Debian
and Ubuntu Derivative do, you may be able to use the APT-based repository.
This repository is tested with:
- Debian Buster
- Ubuntu 20.04 LTS
It may work with older, and should work with more recent released versions.
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.
Edit the file ``/etc/apt/sources.list.d/pbs-client.list`` and add the following
snipped
.. code-block:: sources.list
:caption: File: ``/etc/apt/sources.list``
deb http://download.proxmox.com/debian/pbs-client buster main

View File

@ -13,39 +13,3 @@ parameter. It accepts the following values:
:``json``: JSON (single line).
:``json-pretty``: JSON (multiple lines, nicely formatted).
Device driver options can be specified as integer numbers (see
``/usr/include/linux/mtio.h``), or using symbolic names:
:``buffer-writes``: Enable buffered writes
:``async-writes``: Enable async writes
:``read-ahead``: Use read-ahead for fixed block size
:``debugging``: Enable debugging if compiled into the driver
:``two-fm``: Write two file marks when closing the file
:``fast-mteom``: Space directly to eod (and lose file number)
:``auto-lock``: Automatically lock/unlock drive door
:``def-writes``: Defaults are meant only for writes
:``can-bsr``: Indicates that the drive can space backwards
:``no-blklims``: Drive does not support read block limits
:``can-partitions``: Drive can handle partitioned tapes
:``scsi2locical``: Seek and tell use SCSI-2 logical block addresses
:``sysv``: Enable the System V semantics
:``nowait``: Do not wait for rewind, etc. to complete
:``sili``: Enables setting the SILI bit in SCSI commands when reading
in variable block mode to enhance performance when reading blocks
shorter than the byte count

View File

@ -0,0 +1,3 @@
Command line tool for restoring files and directories from PBS archives. In contrast to
proxmox-backup-client, this supports both container/host and VM backups.

View File

@ -0,0 +1,28 @@
==========================
proxmox-file-restore
==========================
.. include:: ../epilog.rst
-----------------------------------------------------------------------
Command line tool for restoring files and directories from PBS archives
-----------------------------------------------------------------------
:Author: |AUTHOR|
:Version: Version |VERSION|
:Manual section: 1
Synopsis
==========
.. include:: synopsis.rst
Description
============
.. include:: description.rst
.. include:: ../pbs-copyright.rst

View File

@ -3,6 +3,26 @@
`Proxmox VE`_ Integration
-------------------------
A Proxmox Backup Server can be integrated into a Proxmox VE setup by adding the
former as a storage in a Proxmox VE standalone or cluster setup.
See also the `Proxmox VE Storage - Proxmox Backup Server
<https://pve.proxmox.com/pve-docs/pve-admin-guide.html#storage_pbs>`_ section
of the Proxmox VE Administration Guide for Proxmox VE specific documentation.
Using the Proxmox VE Web-Interface
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Proxmox VE has native API and web-interface integration of Proxmox Backup
Server since the `Proxmox VE 6.3 release
<https://pve.proxmox.com/wiki/Roadmap#Proxmox_VE_6.3>`_.
A Proxmox Backup Server can be added under ``Datacenter -> Storage``.
Using the Proxmox VE Command-Line
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
You need to define a new storage with type 'pbs' on your `Proxmox VE`_
node. The following example uses ``store2`` as storage name, and
assumes the server address is ``localhost``, and you want to connect
@ -41,9 +61,9 @@ After that you should be able to see storage status with:
Name Type Status Total Used Available %
store2 pbs active 3905109820 1336687816 2568422004 34.23%
Having added the PBS datastore to `Proxmox VE`_, you can backup VMs and
containers in the same way you would for any other storage device within the
environment (see `PVE Admin Guide: Backup and Restore
Having added the Proxmox Backup Server datastore to `Proxmox VE`_, you can
backup VMs and containers in the same way you would for any other storage
device within the environment (see `Proxmox VE Admin Guide: Backup and Restore
<https://pve.proxmox.com/pve-docs/pve-admin-guide.html#chapter_vzdump>`_.

View File

@ -1,5 +1,5 @@
Storage
=======
Backup Storage
==============
.. _storage_disk_management:

View File

@ -4,12 +4,11 @@ Tape Backup
===========
.. CAUTION:: Tape Backup is a technical preview feature, not meant for
production use. To enable it in the GUI, you need to issue the
following command (as root user on the console):
production use.
.. code-block:: console
# touch /etc/proxmox-backup/tape.cfg
.. image:: images/screenshots/pbs-gui-tape-changer-overview.png
:align: right
:alt: Tape Backup: Tape changer overview
Proxmox tape backup provides an easy way to store datastore content
onto magnetic tapes. This increases data safety because you get:
@ -59,7 +58,7 @@ In general, LTO tapes offer the following advantages:
- Cold Media
- Movable (storable inside vault)
- Multiple vendors (for both media and drives)
- Build in AES-GCM Encryption engine
- Built in AES-GCM Encryption engine
Note that `Proxmox Backup Server` already stores compressed data, so using the
tape compression feature has no advantage.
@ -69,12 +68,16 @@ Supported Hardware
------------------
Proxmox Backup Server supports `Linear Tape-Open`_ generation 4 (LTO-4)
or later. In general, all SCSI-2 tape drives supported by the Linux
kernel should work, but features like hardware encryption need LTO-4
or later.
Tape changing is carried out using the Linux 'mtx' command line
tool, so any changer device supported by this tool should work.
Tape changing is carried out using the SCSI Medium Changer protocol,
so all modern tape libraries should work.
.. Note:: We use a custom user space tape driver written in Rust_. This
driver directly communicates with the tape drive using the SCSI
generic interface. This may have negative side effects when used with the old
Linux kernel tape driver, so you should not use that driver with
Proxmox tape backup.
Drive Performance
@ -84,7 +87,7 @@ Current LTO-8 tapes provide read/write speeds of up to 360 MB/s. This means,
that it still takes a minimum of 9 hours to completely write or
read a single tape (even at maximum speed).
The only way to speed up that data rate is to use more than one
The only way to speed that data rate up is to use more than one
drive. That way, you can run several backup jobs in parallel, or run
restore jobs while the other dives are used for backups.
@ -93,15 +96,16 @@ Also consider that you first need to read data from your datastore
rate. We measured a maximum rate of about 60MB/s to 100MB/s in practice,
so it takes 33 hours to read the 12TB needed to fill up an LTO-8 tape. If you want
to write to your tape at full speed, please make sure that the source
datastore is able to deliver that performance (e.g, by using SSDs).
datastore is able to deliver that performance (for example, by using SSDs).
Terminology
-----------
:Tape Labels: are used to uniquely identify a tape. You would normally apply a
sticky paper label to the front of the cartridge. We additionally store the
label text magnetically on the tape (first file on tape).
**Tape Labels:**
are used to uniquely identify a tape. You would normally apply a
sticky paper label to the front of the cartridge. We additionally
store the label text magnetically on the tape (first file on tape).
.. _Code 39: https://en.wikipedia.org/wiki/Code_39
@ -109,7 +113,8 @@ Terminology
.. _LTO Barcode Generator: lto-barcode/index.html
:Barcodes: are a special form of tape labels, which are electronically
**Barcodes:**
are a special form of tape labels, which are electronically
readable. Most LTO tape robots use an 8 character string encoded as
`Code 39`_, as defined in the `LTO Ultrium Cartridge Label
Specification`_.
@ -122,38 +127,45 @@ Terminology
environmental performance to match or exceed the environmental
specifications of the cartridge to which it is applied.
:Media Pools: A media pool is a logical container for tapes. A backup
job targets one media pool, so a job only uses tapes from that
pool. The pool additionally defines how long a backup job can
append data to tapes (allocation policy) and how long you want to
keep the data (retention policy).
**Media Pools:**
A media pool is a logical container for tapes. A backup job targets
one media pool, so a job only uses tapes from that pool. The pool
additionally defines how long a backup job can append data to tapes
(allocation policy) and how long you want to keep the data
(retention policy).
:Media Set: A group of continuously written tapes (all from the same
media pool).
**Media Set:**
A group of continuously written tapes (all from the same media pool).
:Tape drive: The device used to read and write data to the tape. There
are standalone drives, but drives are usually shipped within tape libraries.
**Tape drive:**
The device used to read and write data to the tape. There are
standalone drives, but drives are usually shipped within tape
libraries.
:Tape changer: A device which can change the tapes inside a tape drive
(tape robot). They are usually part of a tape library.
**Tape changer:**
A device which can change the tapes inside a tape drive (tape
robot). They are usually part of a tape library.
.. _Tape Library: https://en.wikipedia.org/wiki/Tape_library
:`Tape library`_: A storage device that contains one or more tape drives,
a number of slots to hold tape cartridges, a barcode reader to
identify tape cartridges, and an automated method for loading tapes
(a robot).
`Tape library`_:
A storage device that contains one or more tape drives, a number of
slots to hold tape cartridges, a barcode reader to identify tape
cartridges, and an automated method for loading tapes (a robot).
This is also commonly known as an 'autoloader', 'tape robot' or 'tape jukebox'.
This is also commonly known as an 'autoloader', 'tape robot' or
'tape jukebox'.
:Inventory: The inventory stores the list of known tapes (with
additional status information).
**Inventory:**
The inventory stores the list of known tapes (with additional status
information).
:Catalog: A media catalog stores information about the media content.
**Catalog:**
A media catalog stores information about the media content.
Tape Quick Start
---------------
----------------
1. Configure your tape hardware (drives and changers)
@ -176,8 +188,15 @@ same configuration.
Tape changers
~~~~~~~~~~~~~
Tape changers (robots) are part of a `Tape Library`_. You can skip
this step if you are using a standalone drive.
.. image:: images/screenshots/pbs-gui-tape-changers.png
:align: right
:alt: Tape Backup: Tape Changers
Tape changers (robots) are part of a `Tape Library`_. They contain a number of
slots to hold tape cartridges, a barcode reader to identify tape cartridges and
an automated method for loading tapes.
You can skip this step if you are using a standalone drive.
Linux is able to auto detect these devices, and you can get a list
of available devices using:
@ -204,6 +223,13 @@ Where ``sl3`` is an arbitrary name you can choose.
``/dev/tape/by-id/``. Names like ``/dev/sg0`` may point to a
different device after reboot, and that is not what you want.
.. image:: images/screenshots/pbs-gui-tape-changers-add.png
:align: right
:alt: Tape Backup: Add a new tape changer
This operation can also be carried out from the GUI, by navigating to the
**Changers** tab of **Tape Backup** and clicking **Add**.
You can display the final configuration with:
.. code-block:: console
@ -217,7 +243,8 @@ You can display the final configuration with:
│ path │ /dev/tape/by-id/scsi-CC2C52 │
└──────┴─────────────────────────────┘
Or simply list all configured changer devices:
Or simply list all configured changer devices (as seen in the **Changers** tab
of the GUI):
.. code-block:: console
@ -228,7 +255,7 @@ Or simply list all configured changer devices:
│ sl3 │ /dev/tape/by-id/scsi-CC2C52 │ Quantum │ Superloader3 │ CC2C52 │
└──────┴─────────────────────────────┴─────────┴──────────────┴────────────┘
The Vendor, Model and Serial number are auto detected, but only shown
The Vendor, Model and Serial number are auto-detected, but only shown
if the device is online.
To test your setup, please query the status of the changer device with:
@ -261,12 +288,12 @@ It's worth noting that some of the smaller tape libraries don't have
such slots. While they have something called a "Mail Slot", that slot
is just a way to grab the tape from the gripper. They are unable
to hold media while the robot does other things. They also do not
expose that "Mail Slot" over the SCSI interface, so you wont see them in
expose that "Mail Slot" over the SCSI interface, so you won't see them in
the status output.
As a workaround, you can mark some of the normal slots as export
slot. The software treats those slots like real ``import-export``
slots, and the media inside those slots is considered to be 'offline'
slots, and the media inside those slots are considered to be 'offline'
(not available for backup):
.. code-block:: console
@ -302,6 +329,10 @@ the status output:
Tape drives
~~~~~~~~~~~
.. image:: images/screenshots/pbs-gui-tape-drives.png
:align: right
:alt: Tape Backup: Drive list
Linux is able to auto detect tape drives, and you can get a list
of available tape drives using:
@ -311,18 +342,23 @@ of available tape drives using:
┌────────────────────────────────┬────────┬─────────────┬────────┐
│ path │ vendor │ model │ serial │
╞════════════════════════════════╪════════╪═════════════╪════════╡
│ /dev/tape/by-id/scsi-12345-nst │ IBM │ ULT3580-TD4 │ 12345 │
│ /dev/tape/by-id/scsi-12345-sg │ IBM │ ULT3580-TD4 │ 12345 │
└────────────────────────────────┴────────┴─────────────┴────────┘
.. image:: images/screenshots/pbs-gui-tape-drives-add.png
:align: right
:alt: Tape Backup: Add a tape drive
In order to use that drive with Proxmox, you need to create a
configuration entry:
configuration entry. This can be done through **Tape Backup -> Drives** in the
GUI or by using the command below:
.. code-block:: console
# proxmox-tape drive create mydrive --path /dev/tape/by-id/scsi-12345-nst
# proxmox-tape drive create mydrive --path /dev/tape/by-id/scsi-12345-sg
.. Note:: Please use the persistent device path names from inside
``/dev/tape/by-id/``. Names like ``/dev/nst0`` may point to a
``/dev/tape/by-id/``. Names like ``/dev/sg0`` may point to a
different device after reboot, and that is not what you want.
If you have a tape library, you also need to set the associated
@ -346,7 +382,7 @@ You can display the final configuration with:
╞═════════╪════════════════════════════════╡
│ name │ mydrive │
├─────────┼────────────────────────────────┤
│ path │ /dev/tape/by-id/scsi-12345-nst
│ path │ /dev/tape/by-id/scsi-12345-sg
├─────────┼────────────────────────────────┤
│ changer │ sl3 │
└─────────┴────────────────────────────────┘
@ -362,10 +398,10 @@ To list all configured drives use:
┌──────────┬────────────────────────────────┬─────────┬────────┬─────────────┬────────┐
│ name │ path │ changer │ vendor │ model │ serial │
╞══════════╪════════════════════════════════╪═════════╪════════╪═════════════╪════════╡
│ mydrive │ /dev/tape/by-id/scsi-12345-nst │ sl3 │ IBM │ ULT3580-TD4 │ 12345 │
│ mydrive │ /dev/tape/by-id/scsi-12345-sg │ sl3 │ IBM │ ULT3580-TD4 │ 12345 │
└──────────┴────────────────────────────────┴─────────┴────────┴─────────────┴────────┘
The Vendor, Model and Serial number are auto detected, but only shown
The Vendor, Model and Serial number are auto detected and only shown
if the device is online.
For testing, you can simply query the drive status with:
@ -373,13 +409,35 @@ For testing, you can simply query the drive status with:
.. code-block:: console
# proxmox-tape status --drive mydrive
┌───────────────────────────────────┐
┌────────────────┬──────────────────────────┐
│ Name │ Value │
╞═══════════════════════════════════╡
╞════════════════╪══════════════════════════╡
│ blocksize │ 0 │
├───────────────────────────────────┤
status │ DRIVE_OPEN | IM_REP_EN
───────────────────────────────────
├────────────────┼──────────────────────────┤
density │ LTO4
├────────────────┼──────────────────────────
│ compression │ 1 │
├────────────────┼──────────────────────────┤
│ buffer-mode │ 1 │
├────────────────┼──────────────────────────┤
│ alert-flags │ (empty) │
├────────────────┼──────────────────────────┤
│ file-number │ 0 │
├────────────────┼──────────────────────────┤
│ block-number │ 0 │
├────────────────┼──────────────────────────┤
│ manufactured │ Fri Dec 13 01:00:00 2019 │
├────────────────┼──────────────────────────┤
│ bytes-written │ 501.80 GiB │
├────────────────┼──────────────────────────┤
│ bytes-read │ 4.00 MiB │
├────────────────┼──────────────────────────┤
│ medium-passes │ 20 │
├────────────────┼──────────────────────────┤
│ medium-wearout │ 0.12% │
├────────────────┼──────────────────────────┤
│ volume-mounts │ 2 │
└────────────────┴──────────────────────────┘
.. NOTE:: Blocksize should always be 0 (variable block size
mode). This is the default anyway.
@ -390,8 +448,12 @@ For testing, you can simply query the drive status with:
Media Pools
~~~~~~~~~~~
.. image:: images/screenshots/pbs-gui-tape-pools.png
:align: right
:alt: Tape Backup: Media Pools
A media pool is a logical container for tapes. A backup job targets
one media pool, so a job only uses tapes from that pool.
a single media pool, so a job only uses tapes from that pool.
.. topic:: Media Set
@ -510,8 +572,12 @@ one media pool, so a job only uses tapes from that pool.
if the sources are from different namespaces with conflicting names
(for example, if the sources are from different Proxmox VE clusters).
.. image:: images/screenshots/pbs-gui-tape-pools-add.png
:align: right
:alt: Tape Backup: Add a media pool
The following command creates a new media pool:
To create a new media pool, add one from **Tape Backup -> Media Pools** in the
GUI, or enter the following command:
.. code-block:: console
@ -520,7 +586,7 @@ The following command creates a new media pool:
# proxmox-tape pool create daily --drive mydrive
Additional option can be set later, using the update command:
Additional options can be set later, using the update command:
.. code-block:: console
@ -543,6 +609,10 @@ To list all configured pools use:
Tape Backup Jobs
~~~~~~~~~~~~~~~~
.. image:: images/screenshots/pbs-gui-tape-backup-jobs.png
:align: right
:alt: Tape Backup: Tape Backup Jobs
To automate tape backup, you can configure tape backup jobs which
write datastore content to a media pool, based on a specific time schedule.
The required settings are:
@ -618,6 +688,14 @@ To remove a job, please use:
# proxmox-tape backup-job remove job2
.. image:: images/screenshots/pbs-gui-tape-backup-jobs-add.png
:align: right
:alt: Tape Backup: Add a backup job
This same functionality also exists in the GUI, under the **Backup Jobs** tab of
**Tape Backup**, where *Local Datastore* relates to the datastore you want to
backup and *Media Pool* is the pool to back up to.
Administration
--------------
@ -633,7 +711,7 @@ variable:
You can then omit the ``--drive`` parameter from the command. If the
drive has an associated changer device, you may also omit the changer
parameter from commands that needs a changer device, for example:
parameter from commands that need a changer device, for example:
.. code-block:: console
@ -707,7 +785,7 @@ can then label all unlabeled tapes with a single command:
Run Tape Backups
~~~~~~~~~~~~~~~~
To manually run a backup job use:
To manually run a backup job click *Run Now* in the GUI or use the command:
.. code-block:: console
@ -772,7 +850,14 @@ Restore Catalog
Encryption Key Management
~~~~~~~~~~~~~~~~~~~~~~~~~
Creating a new encryption key:
.. image:: images/screenshots/pbs-gui-tape-crypt-keys.png
:align: right
:alt: Tape Backup: Encryption Keys
Proxmox Backup Server also provides an interface for handling encryption keys on
the backup server. Encryption keys can be managed from the **Tape Backup ->
Encryption Keys** section of the GUI or through the ``proxmox-tape key`` command
line tool. To create a new encryption key from the command line:
.. code-block:: console
@ -883,78 +968,3 @@ This command does the following:
- run drive cleaning operation
- unload the cleaning tape (to slot 3)
Configuration Files
-------------------
``media-pool.cfg``
~~~~~~~~~~~~~~~~~~
File Format
^^^^^^^^^^^
.. include:: config/media-pool/format.rst
Options
^^^^^^^
.. include:: config/media-pool/config.rst
``tape.cfg``
~~~~~~~~~~~~
File Format
^^^^^^^^^^^
.. include:: config/tape/format.rst
Options
^^^^^^^
.. include:: config/tape/config.rst
``tape-job.cfg``
~~~~~~~~~~~~~~~~
File Format
^^^^^^^^^^^
.. include:: config/tape-job/format.rst
Options
^^^^^^^
.. include:: config/tape-job/config.rst
Command Syntax
--------------
``proxmox-tape``
----------------
.. include:: proxmox-tape/synopsis.rst
``pmt``
-------
.. include:: pmt/options.rst
....
.. include:: pmt/synopsis.rst
``pmtx``
--------
.. include:: pmtx/synopsis.rst

View File

@ -100,7 +100,7 @@ can be encrypted, and they are handled in a slightly different manner than
normal chunks.
The hashes of encrypted chunks are calculated not with the actual (encrypted)
chunk content, but with the plaintext content concatenated with the encryption
chunk content, but with the plain-text content concatenated with the encryption
key. This way, two chunks of the same data encrypted with different keys
generate two different checksums and no collisions occur for multiple
encryption keys.
@ -138,7 +138,7 @@ will see that the probability of a collision in that scenario is:
For context, in a lottery game of guessing 6 out of 45, the chance to correctly
guess all 6 numbers is only :math:`1.2277 * 10^{-7}`, that means the chance of
collission is about the same as winning 13 such lotto games *in a row*.
a collision is about the same as winning 13 such lotto games *in a row*.
In conclusion, it is extremely unlikely that such a collision would occur by
accident in a normal datastore.

View File

@ -12,7 +12,7 @@ pub mod version;
pub mod ping;
pub mod pull;
pub mod tape;
mod helpers;
pub mod helpers;
use proxmox::api::router::SubdirMap;
use proxmox::api::Router;

View File

@ -477,6 +477,17 @@ pub fn delete_user(userid: Userid, digest: Option<String>) -> Result<(), Error>
user::save_config(&config)?;
let authenticator = crate::auth::lookup_authenticator(userid.realm())?;
match authenticator.remove_password(userid.name()) {
Ok(()) => {},
Err(err) => {
eprintln!(
"error removing password after deleting user {:?}: {}",
userid, err
);
}
}
match crate::config::tfa::read().and_then(|mut cfg| {
let _: bool = cfg.remove_user(&userid);
crate::config::tfa::write(&cfg)

View File

@ -1385,7 +1385,7 @@ pub fn pxar_file_download(
let mut split = components.splitn(2, |c| *c == b'/');
let pxar_name = std::str::from_utf8(split.next().unwrap())?;
let file_path = split.next().ok_or_else(|| format_err!("filepath looks strange '{}'", filepath))?;
let file_path = split.next().unwrap_or(b"/");
let (manifest, files) = read_backup_index(&datastore, &backup_dir)?;
for file in files {
if file.filename == pxar_name && file.crypt_mode == Some(CryptMode::Encrypt) {

View File

@ -27,7 +27,7 @@ use crate::{
SLOT_ARRAY_SCHEMA,
EXPORT_SLOT_LIST_SCHEMA,
ScsiTapeChanger,
LinuxTapeDrive,
LtoTapeDrive,
},
tape::{
linux_tape_changer_list,
@ -303,7 +303,7 @@ pub fn delete_changer(name: String, _param: Value) -> Result<(), Error> {
None => bail!("Delete changer '{}' failed - no such entry", name),
}
let drive_list: Vec<LinuxTapeDrive> = config.convert_to_typed_array("linux")?;
let drive_list: Vec<LtoTapeDrive> = config.convert_to_typed_array("lto")?;
for drive in drive_list {
if let Some(changer) = drive.changer {
if changer == name {

View File

@ -19,12 +19,12 @@ use crate::{
DRIVE_NAME_SCHEMA,
CHANGER_NAME_SCHEMA,
CHANGER_DRIVENUM_SCHEMA,
LINUX_DRIVE_PATH_SCHEMA,
LinuxTapeDrive,
LTO_DRIVE_PATH_SCHEMA,
LtoTapeDrive,
ScsiTapeChanger,
},
tape::{
linux_tape_device_list,
lto_tape_device_list,
check_drive_path,
},
};
@ -37,7 +37,7 @@ use crate::{
schema: DRIVE_NAME_SCHEMA,
},
path: {
schema: LINUX_DRIVE_PATH_SCHEMA,
schema: LTO_DRIVE_PATH_SCHEMA,
},
changer: {
schema: CHANGER_NAME_SCHEMA,
@ -60,13 +60,13 @@ pub fn create_drive(param: Value) -> Result<(), Error> {
let (mut config, _digest) = config::drive::config()?;
let item: LinuxTapeDrive = serde_json::from_value(param)?;
let item: LtoTapeDrive = serde_json::from_value(param)?;
let linux_drives = linux_tape_device_list();
let lto_drives = lto_tape_device_list();
check_drive_path(&linux_drives, &item.path)?;
check_drive_path(&lto_drives, &item.path)?;
let existing: Vec<LinuxTapeDrive> = config.convert_to_typed_array("linux")?;
let existing: Vec<LtoTapeDrive> = config.convert_to_typed_array("lto")?;
for drive in existing {
if drive.name == item.name {
@ -77,7 +77,7 @@ pub fn create_drive(param: Value) -> Result<(), Error> {
}
}
config.set_data(&item.name, "linux", &item)?;
config.set_data(&item.name, "lto", &item)?;
config::drive::save_config(&config)?;
@ -93,7 +93,7 @@ pub fn create_drive(param: Value) -> Result<(), Error> {
},
},
returns: {
type: LinuxTapeDrive,
type: LtoTapeDrive,
},
access: {
permission: &Permission::Privilege(&["tape", "device", "{name}"], PRIV_TAPE_AUDIT, false),
@ -104,11 +104,11 @@ pub fn get_config(
name: String,
_param: Value,
mut rpcenv: &mut dyn RpcEnvironment,
) -> Result<LinuxTapeDrive, Error> {
) -> Result<LtoTapeDrive, Error> {
let (config, digest) = config::drive::config()?;
let data: LinuxTapeDrive = config.lookup("linux", &name)?;
let data: LtoTapeDrive = config.lookup("lto", &name)?;
rpcenv["digest"] = proxmox::tools::digest_to_hex(&digest).into();
@ -123,7 +123,7 @@ pub fn get_config(
description: "The list of configured drives (with config digest).",
type: Array,
items: {
type: LinuxTapeDrive,
type: LtoTapeDrive,
},
},
access: {
@ -135,13 +135,13 @@ pub fn get_config(
pub fn list_drives(
_param: Value,
mut rpcenv: &mut dyn RpcEnvironment,
) -> Result<Vec<LinuxTapeDrive>, Error> {
) -> Result<Vec<LtoTapeDrive>, Error> {
let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
let user_info = CachedUserInfo::new()?;
let (config, digest) = config::drive::config()?;
let drive_list: Vec<LinuxTapeDrive> = config.convert_to_typed_array("linux")?;
let drive_list: Vec<LtoTapeDrive> = config.convert_to_typed_array("lto")?;
let drive_list = drive_list
.into_iter()
@ -176,7 +176,7 @@ pub enum DeletableProperty {
schema: DRIVE_NAME_SCHEMA,
},
path: {
schema: LINUX_DRIVE_PATH_SCHEMA,
schema: LTO_DRIVE_PATH_SCHEMA,
optional: true,
},
changer: {
@ -225,7 +225,7 @@ pub fn update_drive(
crate::tools::detect_modified_configuration_file(&digest, &expected_digest)?;
}
let mut data: LinuxTapeDrive = config.lookup("linux", &name)?;
let mut data: LtoTapeDrive = config.lookup("lto", &name)?;
if let Some(delete) = delete {
for delete_prop in delete {
@ -240,8 +240,8 @@ pub fn update_drive(
}
if let Some(path) = path {
let linux_drives = linux_tape_device_list();
check_drive_path(&linux_drives, &path)?;
let lto_drives = lto_tape_device_list();
check_drive_path(&lto_drives, &path)?;
data.path = path;
}
@ -261,7 +261,7 @@ pub fn update_drive(
}
}
config.set_data(&name, "linux", &data)?;
config.set_data(&name, "lto", &data)?;
config::drive::save_config(&config)?;
@ -290,8 +290,8 @@ pub fn delete_drive(name: String, _param: Value) -> Result<(), Error> {
match config.sections.get(&name) {
Some((section_type, _)) => {
if section_type != "linux" {
bail!("Entry '{}' exists, but is not a linux tape drive", name);
if section_type != "lto" {
bail!("Entry '{}' exists, but is not a lto tape drive", name);
}
config.sections.remove(&name);
},

View File

@ -442,7 +442,13 @@ fn backup_worker(
progress.done_snapshots = 0;
progress.group_snapshots = 0;
let mut snapshot_list = group.list_backups(&datastore.base_path())?;
let snapshot_list = group.list_backups(&datastore.base_path())?;
// filter out unfinished backups
let mut snapshot_list = snapshot_list
.into_iter()
.filter(|item| item.is_finished())
.collect();
BackupInfo::sort_list(&mut snapshot_list, true); // oldest first

View File

@ -20,7 +20,7 @@ use crate::{
Authid,
CHANGER_NAME_SCHEMA,
ChangerListEntry,
LinuxTapeDrive,
LtoTapeDrive,
MtxEntryKind,
MtxStatusEntry,
ScsiTapeChanger,
@ -88,7 +88,7 @@ pub async fn get_status(
inventory.update_online_status(&map)?;
let drive_list: Vec<LinuxTapeDrive> = config.convert_to_typed_array("linux")?;
let drive_list: Vec<LtoTapeDrive> = config.convert_to_typed_array("lto")?;
let mut drive_map: HashMap<u64, String> = HashMap::new();
for drive in drive_list {

View File

@ -10,7 +10,6 @@ use proxmox::{
identity,
list_subdirs_api_method,
tools::Uuid,
sys::error::SysError,
api::{
api,
section_config::SectionConfigData,
@ -42,11 +41,12 @@ use crate::{
MEDIA_POOL_NAME_SCHEMA,
Authid,
DriveListEntry,
LinuxTapeDrive,
LtoTapeDrive,
MediaIdFlat,
LabelUuidMap,
MamAttribute,
LinuxDriveAndMediaStatus,
LtoDriveAndMediaStatus,
Lp17VolumeStatistics,
},
tape::restore::{
fast_catalog_restore,
@ -59,10 +59,11 @@ use crate::{
Inventory,
MediaCatalog,
MediaId,
BlockReadError,
lock_media_set,
lock_media_pool,
lock_unassigned_media_pool,
linux_tape_device_list,
lto_tape_device_list,
lookup_device_identification,
file_formats::{
MediaLabel,
@ -70,9 +71,8 @@ use crate::{
},
drive::{
TapeDriver,
LinuxTapeHandle,
Lp17VolumeStatistics,
open_linux_tape_device,
LtoTapeHandle,
open_lto_tape_device,
media_changer,
required_media_changer,
open_drive,
@ -321,8 +321,8 @@ pub fn unload(
permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_WRITE, false),
},
)]
/// Erase media. Check for label-text if given (cancels if wrong media).
pub fn erase_media(
/// Format media. Check for label-text if given (cancels if wrong media).
pub fn format_media(
drive: String,
fast: Option<bool>,
label_text: Option<String>,
@ -331,7 +331,7 @@ pub fn erase_media(
let upid_str = run_drive_worker(
rpcenv,
drive.clone(),
"erase-media",
"format-media",
Some(drive.clone()),
move |worker, config| {
if let Some(ref label) = label_text {
@ -350,15 +350,15 @@ pub fn erase_media(
}
/* assume drive contains no or unrelated data */
task_log!(worker, "unable to read media label: {}", err);
task_log!(worker, "erase anyways");
handle.erase_media(fast.unwrap_or(true))?;
task_log!(worker, "format anyways");
handle.format_media(fast.unwrap_or(true))?;
}
Ok((None, _)) => {
if let Some(label) = label_text {
bail!("expected label '{}', found empty tape", label);
}
task_log!(worker, "found empty media - erase anyways");
handle.erase_media(fast.unwrap_or(true))?;
task_log!(worker, "found empty media - format anyways");
handle.format_media(fast.unwrap_or(true))?;
}
Ok((Some(media_id), _key_config)) => {
if let Some(label_text) = label_text {
@ -391,7 +391,7 @@ pub fn erase_media(
inventory.remove_media(&media_id.label.uuid)?;
};
handle.erase_media(fast.unwrap_or(true))?;
handle.format_media(fast.unwrap_or(true))?;
}
}
@ -503,7 +503,7 @@ pub fn eject_media(
/// Write a new media label to the media in 'drive'. The media is
/// assigned to the specified 'pool', or else to the free media pool.
///
/// Note: The media need to be empty (you may want to erase it first).
/// Note: The media need to be empty (you may want to format it first).
pub fn label_media(
drive: String,
pool: Option<String>,
@ -528,16 +528,13 @@ pub fn label_media(
drive.rewind()?;
match drive.read_next_file() {
Ok(Some(_file)) => bail!("media is not empty (erase first)"),
Ok(None) => { /* EOF mark at BOT, assume tape is empty */ },
Ok(_reader) => bail!("media is not empty (format it first)"),
Err(BlockReadError::EndOfFile) => { /* EOF mark at BOT, assume tape is empty */ },
Err(BlockReadError::EndOfStream) => { /* tape is empty */ },
Err(err) => {
if err.is_errno(nix::errno::Errno::ENOSPC) || err.is_errno(nix::errno::Errno::EIO) {
/* assume tape is empty */
} else {
bail!("media read error - {}", err);
}
}
}
let ctime = proxmox::tools::time::epoch_i64();
let label = MediaLabel {
@ -793,9 +790,9 @@ pub fn clean_drive(
changer.clean_drive()?;
if let Ok(drive_config) = config.lookup::<LinuxTapeDrive>("linux", &drive) {
if let Ok(drive_config) = config.lookup::<LtoTapeDrive>("lto", &drive) {
// Note: clean_drive unloads the cleaning media, so we cannot use drive_config.open
let mut handle = LinuxTapeHandle::new(open_linux_tape_device(&drive_config.path)?);
let mut handle = LtoTapeHandle::new(open_lto_tape_device(&drive_config.path)?)?;
// test for critical tape alert flags
if let Ok(alert_flags) = handle.tape_alert_flags() {
@ -1090,20 +1087,17 @@ fn barcode_label_media_worker(
drive.rewind()?;
match drive.read_next_file() {
Ok(Some(_file)) => {
worker.log(format!("media '{}' is not empty (erase first)", label_text));
Ok(_reader) => {
worker.log(format!("media '{}' is not empty (format it first)", label_text));
continue;
}
Ok(None) => { /* EOF mark at BOT, assume tape is empty */ },
Err(err) => {
if err.is_errno(nix::errno::Errno::ENOSPC) || err.is_errno(nix::errno::Errno::EIO) {
/* assume tape is empty */
} else {
worker.warn(format!("media '{}' read error (maybe not empty - erase first)", label_text));
Err(BlockReadError::EndOfFile) => { /* EOF mark at BOT, assume tape is empty */ },
Err(BlockReadError::EndOfStream) => { /* tape is empty */ },
Err(_err) => {
worker.warn(format!("media '{}' read error (maybe not empty - format it first)", label_text));
continue;
}
}
}
let ctime = proxmox::tools::time::epoch_i64();
let label = MediaLabel {
@ -1143,7 +1137,7 @@ pub async fn cartridge_memory(drive: String) -> Result<Vec<MamAttribute>, Error>
drive.clone(),
"reading cartridge memory".to_string(),
move |config| {
let drive_config: LinuxTapeDrive = config.lookup("linux", &drive)?;
let drive_config: LtoTapeDrive = config.lookup("lto", &drive)?;
let mut handle = drive_config.open()?;
handle.cartridge_memory()
@ -1173,7 +1167,7 @@ pub async fn volume_statistics(drive: String) -> Result<Lp17VolumeStatistics, Er
drive.clone(),
"reading volume statistics".to_string(),
move |config| {
let drive_config: LinuxTapeDrive = config.lookup("linux", &drive)?;
let drive_config: LtoTapeDrive = config.lookup("lto", &drive)?;
let mut handle = drive_config.open()?;
handle.volume_statistics()
@ -1191,24 +1185,24 @@ pub async fn volume_statistics(drive: String) -> Result<Lp17VolumeStatistics, Er
},
},
returns: {
type: LinuxDriveAndMediaStatus,
type: LtoDriveAndMediaStatus,
},
access: {
permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_AUDIT, false),
},
)]
/// Get drive/media status
pub async fn status(drive: String) -> Result<LinuxDriveAndMediaStatus, Error> {
pub async fn status(drive: String) -> Result<LtoDriveAndMediaStatus, Error> {
run_drive_blocking_task(
drive.clone(),
"reading drive status".to_string(),
move |config| {
let drive_config: LinuxTapeDrive = config.lookup("linux", &drive)?;
let drive_config: LtoTapeDrive = config.lookup("lto", &drive)?;
// Note: use open_linux_tape_device, because this also works if no medium loaded
let file = open_linux_tape_device(&drive_config.path)?;
// Note: use open_lto_tape_device, because this also works if no medium loaded
let file = open_lto_tape_device(&drive_config.path)?;
let mut handle = LinuxTapeHandle::new(file);
let mut handle = LtoTapeHandle::new(file)?;
handle.get_drive_and_media_status()
}
@ -1381,9 +1375,9 @@ pub fn list_drives(
let (config, _) = config::drive::config()?;
let linux_drives = linux_tape_device_list();
let lto_drives = lto_tape_device_list();
let drive_list: Vec<LinuxTapeDrive> = config.convert_to_typed_array("linux")?;
let drive_list: Vec<LtoTapeDrive> = config.convert_to_typed_array("lto")?;
let mut list = Vec::new();
@ -1397,7 +1391,7 @@ pub fn list_drives(
continue;
}
let info = lookup_device_identification(&linux_drives, &drive.path);
let info = lookup_device_identification(&lto_drives, &drive.path);
let state = get_tape_device_state(&config, &drive.name)?;
let entry = DriveListEntry { config: drive, info, state };
list.push(entry);
@ -1429,9 +1423,9 @@ pub const SUBDIRS: SubdirMap = &sorted!([
.post(&API_METHOD_EJECT_MEDIA)
),
(
"erase-media",
"format-media",
&Router::new()
.post(&API_METHOD_ERASE_MEDIA)
.post(&API_METHOD_FORMAT_MEDIA)
),
(
"export-media",

View File

@ -15,7 +15,7 @@ use proxmox::{
use crate::{
api2::types::TapeDeviceInfo,
tape::{
linux_tape_device_list,
lto_tape_device_list,
linux_tape_changer_list,
},
};
@ -41,7 +41,7 @@ pub mod restore;
/// Scan tape drives
pub fn scan_drives(_param: Value) -> Result<Vec<TapeDeviceInfo>, Error> {
let list = linux_tape_device_list();
let list = lto_tape_device_list();
Ok(list)
}

View File

@ -70,6 +70,7 @@ use crate::{
tape::{
TAPE_STATUS_DIR,
TapeRead,
BlockReadError,
MediaId,
MediaSet,
MediaCatalog,
@ -418,12 +419,19 @@ pub fn restore_media(
loop {
let current_file_number = drive.current_file_number()?;
let reader = match drive.read_next_file()? {
None => {
let reader = match drive.read_next_file() {
Err(BlockReadError::EndOfFile) => {
task_log!(worker, "skip unexpected filemark at pos {}", current_file_number);
continue;
}
Err(BlockReadError::EndOfStream) => {
task_log!(worker, "detected EOT after {} files", current_file_number);
break;
}
Some(reader) => reader,
Err(BlockReadError::Error(err)) => {
return Err(err.into());
}
Ok(reader) => reader,
};
restore_archive(worker, reader, current_file_number, target, &mut catalog, verbose)?;
@ -517,7 +525,7 @@ fn restore_archive<'a>(
}
}
reader.skip_to_end()?; // read all data
reader.skip_data()?; // read all data
if let Ok(false) = reader.is_incomplete() {
catalog.register_snapshot(Uuid::from(header.uuid), current_file_number, &datastore_name, &snapshot)?;
catalog.commit_if_large()?;
@ -558,7 +566,7 @@ fn restore_archive<'a>(
task_log!(worker, "skipping...");
}
reader.skip_to_end()?; // read all data
reader.skip_data()?; // read all data
}
PROXMOX_BACKUP_CATALOG_ARCHIVE_MAGIC_1_0 => {
let header_data = reader.read_exact_allocated(header.size as usize)?;
@ -568,7 +576,7 @@ fn restore_archive<'a>(
task_log!(worker, "File {}: skip catalog '{}'", current_file_number, archive_header.uuid);
reader.skip_to_end()?; // read all data
reader.skip_data()?; // read all data
}
_ => bail!("unknown content magic {:?}", header.content_magic),
}
@ -589,8 +597,28 @@ fn restore_chunk_archive<'a>(
let mut decoder = ChunkArchiveDecoder::new(reader);
let result: Result<_, Error> = proxmox::try_block!({
while let Some((digest, blob)) = decoder.next_chunk()? {
loop {
let (digest, blob) = match decoder.next_chunk() {
Ok(Some((digest, blob))) => (digest, blob),
Ok(None) => break,
Err(err) => {
let reader = decoder.reader();
// check if this stream is marked incomplete
if let Ok(true) = reader.is_incomplete() {
return Ok(Some(chunks));
}
// check if this is an aborted stream without end marker
if let Ok(false) = reader.has_end_marker() {
worker.log("missing stream end marker".to_string());
return Ok(None);
}
// else the archive is corrupt
return Err(err);
}
};
worker.check_abort()?;
@ -614,29 +642,8 @@ fn restore_chunk_archive<'a>(
}
chunks.push(digest);
}
Ok(())
});
match result {
Ok(()) => Ok(Some(chunks)),
Err(err) => {
let reader = decoder.reader();
// check if this stream is marked incomplete
if let Ok(true) = reader.is_incomplete() {
return Ok(Some(chunks));
}
// check if this is an aborted stream without end marker
if let Ok(false) = reader.has_end_marker() {
worker.log("missing stream end marker".to_string());
return Ok(None);
}
// else the archive is corrupt
Err(err)
}
}
Ok(Some(chunks))
}
fn restore_snapshot_archive<'a>(
@ -811,12 +818,19 @@ pub fn fast_catalog_restore(
let current_file_number = drive.current_file_number()?;
{ // limit reader scope
let mut reader = match drive.read_next_file()? {
None => {
let mut reader = match drive.read_next_file() {
Err(BlockReadError::EndOfFile) => {
task_log!(worker, "skip unexpected filemark at pos {}", current_file_number);
continue;
}
Err(BlockReadError::EndOfStream) => {
task_log!(worker, "detected EOT after {} files", current_file_number);
break;
}
Some(reader) => reader,
Err(BlockReadError::Error(err)) => {
return Err(err.into());
}
Ok(reader) => reader,
};
let header: MediaContentHeader = unsafe { reader.read_le_value()? };
@ -834,7 +848,7 @@ pub fn fast_catalog_restore(
if &archive_header.media_set_uuid != media_set.uuid() {
task_log!(worker, "skipping unrelated catalog at pos {}", current_file_number);
reader.skip_to_end()?; // read all data
reader.skip_data()?; // read all data
continue;
}
@ -853,7 +867,7 @@ pub fn fast_catalog_restore(
if !wanted {
task_log!(worker, "skip catalog because media '{}' not inventarized", catalog_uuid);
reader.skip_to_end()?; // read all data
reader.skip_data()?; // read all data
continue;
}
@ -863,7 +877,7 @@ pub fn fast_catalog_restore(
// only restore if catalog does not exist
if MediaCatalog::exists(status_path, catalog_uuid) {
task_log!(worker, "catalog for media '{}' already exists", catalog_uuid);
reader.skip_to_end()?; // read all data
reader.skip_data()?; // read all data
continue;
}
}

View File

@ -0,0 +1,15 @@
use serde::{Deserialize, Serialize};
use proxmox::api::api;
#[api()]
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
/// General status information about a running VM file-restore daemon
pub struct RestoreDaemonStatus {
/// VM uptime in seconds
pub uptime: i64,
/// time left until auto-shutdown, keep in mind that this is useless when 'keep-timeout' is
/// not set, as then the status call will have reset the timer before returning the value
pub timeout: i64,
}

View File

@ -34,6 +34,9 @@ pub use userid::{PROXMOX_TOKEN_ID_SCHEMA, PROXMOX_TOKEN_NAME_SCHEMA, PROXMOX_GRO
mod tape;
pub use tape::*;
mod file_restore;
pub use file_restore::*;
// File names: may not contain slashes, may not start with "."
pub const FILENAME_FORMAT: ApiStringFormat = ApiStringFormat::VerifyFn(|name| {
if name.starts_with('.') {

View File

@ -21,8 +21,8 @@ pub const DRIVE_NAME_SCHEMA: Schema = StringSchema::new("Drive Identifier.")
.max_length(32)
.schema();
pub const LINUX_DRIVE_PATH_SCHEMA: Schema = StringSchema::new(
"The path to a LINUX non-rewinding SCSI tape device (i.e. '/dev/nst0')")
pub const LTO_DRIVE_PATH_SCHEMA: Schema = StringSchema::new(
"The path to a LTO SCSI-generic tape device (i.e. '/dev/sg0')")
.schema();
pub const CHANGER_DRIVENUM_SCHEMA: Schema = IntegerSchema::new(
@ -57,7 +57,7 @@ pub struct VirtualTapeDrive {
schema: DRIVE_NAME_SCHEMA,
},
path: {
schema: LINUX_DRIVE_PATH_SCHEMA,
schema: LTO_DRIVE_PATH_SCHEMA,
},
changer: {
schema: CHANGER_NAME_SCHEMA,
@ -71,8 +71,8 @@ pub struct VirtualTapeDrive {
)]
#[derive(Serialize,Deserialize)]
#[serde(rename_all = "kebab-case")]
/// Linux SCSI tape driver
pub struct LinuxTapeDrive {
/// Lto SCSI tape driver
pub struct LtoTapeDrive {
pub name: String,
pub path: String,
#[serde(skip_serializing_if="Option::is_none")]
@ -84,7 +84,7 @@ pub struct LinuxTapeDrive {
#[api(
properties: {
config: {
type: LinuxTapeDrive,
type: LtoTapeDrive,
},
info: {
type: OptionalDeviceIdentification,
@ -96,7 +96,7 @@ pub struct LinuxTapeDrive {
/// Drive list entry
pub struct DriveListEntry {
#[serde(flatten)]
pub config: LinuxTapeDrive,
pub config: LtoTapeDrive,
#[serde(flatten)]
pub info: OptionalDeviceIdentification,
/// the state of the drive if locked
@ -119,6 +119,8 @@ pub struct MamAttribute {
#[api()]
#[derive(Serialize,Deserialize,Copy,Clone,Debug)]
pub enum TapeDensity {
/// Unknown (no media loaded)
Unknown,
/// LTO1
LTO1,
/// LTO2
@ -144,6 +146,7 @@ impl TryFrom<u8> for TapeDensity {
fn try_from(value: u8) -> Result<Self, Self::Error> {
let density = match value {
0x00 => TapeDensity::Unknown,
0x40 => TapeDensity::LTO1,
0x42 => TapeDensity::LTO2,
0x44 => TapeDensity::LTO3,
@ -169,29 +172,37 @@ impl TryFrom<u8> for TapeDensity {
)]
#[derive(Serialize,Deserialize)]
#[serde(rename_all = "kebab-case")]
/// Drive/Media status for Linux SCSI drives.
/// Drive/Media status for Lto SCSI drives.
///
/// Media related data is optional - only set if there is a medium
/// loaded.
pub struct LinuxDriveAndMediaStatus {
pub struct LtoDriveAndMediaStatus {
/// Vendor
pub vendor: String,
/// Product
pub product: String,
/// Revision
pub revision: String,
/// Block size (0 is variable size)
pub blocksize: u32,
/// Compression enabled
pub compression: bool,
/// Drive buffer mode
pub buffer_mode: u8,
/// Tape density
pub density: TapeDensity,
/// Media is write protected
#[serde(skip_serializing_if="Option::is_none")]
pub density: Option<TapeDensity>,
/// Status flags
pub status: String,
/// Linux Driver Options
pub options: String,
pub write_protect: Option<bool>,
/// Tape Alert Flags
#[serde(skip_serializing_if="Option::is_none")]
pub alert_flags: Option<String>,
/// Current file number
#[serde(skip_serializing_if="Option::is_none")]
pub file_number: Option<u32>,
pub file_number: Option<u64>,
/// Current block number
#[serde(skip_serializing_if="Option::is_none")]
pub block_number: Option<u32>,
pub block_number: Option<u64>,
/// Medium Manufacture Date (epoch)
#[serde(skip_serializing_if="Option::is_none")]
pub manufactured: Option<i64>,
@ -212,3 +223,62 @@ pub struct LinuxDriveAndMediaStatus {
#[serde(skip_serializing_if="Option::is_none")]
pub medium_wearout: Option<f64>,
}
#[api()]
/// Volume statistics from SCSI log page 17h
#[derive(Default, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct Lp17VolumeStatistics {
/// Volume mounts (thread count)
pub volume_mounts: u64,
/// Total data sets written
pub volume_datasets_written: u64,
/// Write retries
pub volume_recovered_write_data_errors: u64,
/// Total unrecovered write errors
pub volume_unrecovered_write_data_errors: u64,
/// Total suspended writes
pub volume_write_servo_errors: u64,
/// Total fatal suspended writes
pub volume_unrecovered_write_servo_errors: u64,
/// Total datasets read
pub volume_datasets_read: u64,
/// Total read retries
pub volume_recovered_read_errors: u64,
/// Total unrecovered read errors
pub volume_unrecovered_read_errors: u64,
/// Last mount unrecovered write errors
pub last_mount_unrecovered_write_errors: u64,
/// Last mount unrecovered read errors
pub last_mount_unrecovered_read_errors: u64,
/// Last mount bytes written
pub last_mount_bytes_written: u64,
/// Last mount bytes read
pub last_mount_bytes_read: u64,
/// Lifetime bytes written
pub lifetime_bytes_written: u64,
/// Lifetime bytes read
pub lifetime_bytes_read: u64,
/// Last load write compression ratio
pub last_load_write_compression_ratio: u64,
/// Last load read compression ratio
pub last_load_read_compression_ratio: u64,
/// Medium mount time
pub medium_mount_time: u64,
/// Medium ready time
pub medium_ready_time: u64,
/// Total native capacity
pub total_native_capacity: u64,
/// Total used native capacity
pub total_used_native_capacity: u64,
/// Write protect
pub write_protect: bool,
/// Volume is WORM
pub worm: bool,
/// Beginning of medium passes
pub beginning_of_medium_passes: u64,
/// Middle of medium passes
pub middle_of_tape_passes: u64,
/// Volume serial number
pub serial: String,
}

View File

@ -14,6 +14,7 @@ use crate::api2::types::{Userid, UsernameRef, RealmRef};
pub trait ProxmoxAuthenticator {
fn authenticate_user(&self, username: &UsernameRef, password: &str) -> Result<(), Error>;
fn store_password(&self, username: &UsernameRef, password: &str) -> Result<(), Error>;
fn remove_password(&self, username: &UsernameRef) -> Result<(), Error>;
}
pub struct PAM();
@ -60,6 +61,11 @@ impl ProxmoxAuthenticator for PAM {
Ok(())
}
// do not remove password for pam users
fn remove_password(&self, _username: &UsernameRef) -> Result<(), Error> {
Ok(())
}
}
pub struct PBS();
@ -132,6 +138,24 @@ impl ProxmoxAuthenticator for PBS {
Ok(())
}
fn remove_password(&self, username: &UsernameRef) -> Result<(), Error> {
let mut data = proxmox::tools::fs::file_get_json(SHADOW_CONFIG_FILENAME, Some(json!({})))?;
if let Some(map) = data.as_object_mut() {
map.remove(username.as_str());
}
let mode = nix::sys::stat::Mode::from_bits_truncate(0o0600);
let options = proxmox::tools::fs::CreateOptions::new()
.perm(mode)
.owner(nix::unistd::ROOT)
.group(nix::unistd::Gid::from_raw(0));
let data = serde_json::to_vec_pretty(&data)?;
proxmox::tools::fs::replace_file(SHADOW_CONFIG_FILENAME, &data, options)?;
Ok(())
}
}
/// Lookup the autenticator for the specified realm

View File

@ -686,6 +686,11 @@ impl DataStore {
}
pub fn stat_chunk(&self, digest: &[u8; 32]) -> Result<std::fs::Metadata, Error> {
let (chunk_path, _digest_str) = self.chunk_store.chunk_path(digest);
std::fs::metadata(chunk_path).map_err(Error::from)
}
pub fn load_chunk(&self, digest: &[u8; 32]) -> Result<DataBlob, Error> {
let (chunk_path, digest_str) = self.chunk_store.chunk_path(digest);

View File

@ -1,8 +1,8 @@
use std::collections::HashSet;
use std::sync::{Arc, Mutex};
use std::sync::atomic::{Ordering, AtomicUsize};
use std::time::Instant;
use nix::dir::Dir;
use std::collections::HashSet;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use std::time::Instant;
use anyhow::{bail, format_err, Error};
@ -25,8 +25,8 @@ use crate::{
server::UPID,
task::TaskState,
task_log,
tools::ParallelHandler,
tools::fs::lock_dir_noblock_shared,
tools::ParallelHandler,
};
/// A VerifyWorker encapsulates a task worker, datastore and information about which chunks have
@ -52,8 +52,11 @@ impl VerifyWorker {
}
}
fn verify_blob(datastore: Arc<DataStore>, backup_dir: &BackupDir, info: &FileInfo) -> Result<(), Error> {
fn verify_blob(
datastore: Arc<DataStore>,
backup_dir: &BackupDir,
info: &FileInfo,
) -> Result<(), Error> {
let blob = datastore.load_blob(backup_dir, &info.filename)?;
let raw_size = blob.raw_size();
@ -88,7 +91,11 @@ fn rename_corrupted_chunk(
let mut new_path = path.clone();
loop {
new_path.set_file_name(format!("{}.{}.bad", digest_str, counter));
if new_path.exists() && counter < 9 { counter += 1; } else { break; }
if new_path.exists() && counter < 9 {
counter += 1;
} else {
break;
}
}
match std::fs::rename(&path, &new_path) {
@ -109,7 +116,6 @@ fn verify_index_chunks(
index: Box<dyn IndexFile + Send>,
crypt_mode: CryptMode,
) -> Result<(), Error> {
let errors = Arc::new(AtomicUsize::new(0));
let start_time = Instant::now();
@ -124,7 +130,8 @@ fn verify_index_chunks(
let errors2 = Arc::clone(&errors);
let decoder_pool = ParallelHandler::new(
"verify chunk decoder", 4,
"verify chunk decoder",
4,
move |(chunk, digest, size): (DataBlob, [u8; 32], u64)| {
let chunk_crypt_mode = match chunk.crypt_mode() {
Err(err) => {
@ -159,23 +166,65 @@ fn verify_index_chunks(
}
);
for pos in 0..index.index_count() {
let skip_chunk = |digest: &[u8; 32]| -> bool {
if verify_worker.verified_chunks.lock().unwrap().contains(digest) {
true
} else if verify_worker.corrupt_chunks.lock().unwrap().contains(digest) {
let digest_str = proxmox::tools::digest_to_hex(digest);
task_log!(verify_worker.worker, "chunk {} was marked as corrupt", digest_str);
errors.fetch_add(1, Ordering::SeqCst);
true
} else {
false
}
};
let index_count = index.index_count();
let mut chunk_list = Vec::with_capacity(index_count);
use std::os::unix::fs::MetadataExt;
for pos in 0..index_count {
if pos & 1023 == 0 {
verify_worker.worker.check_abort()?;
crate::tools::fail_on_shutdown()?;
}
let info = index.chunk_info(pos).unwrap();
if skip_chunk(&info.digest) {
continue; // already verified or marked corrupt
}
match verify_worker.datastore.stat_chunk(&info.digest) {
Err(err) => {
verify_worker.corrupt_chunks.lock().unwrap().insert(info.digest);
task_log!(verify_worker.worker, "can't verify chunk, stat failed - {}", err);
errors.fetch_add(1, Ordering::SeqCst);
rename_corrupted_chunk(
verify_worker.datastore.clone(),
&info.digest,
&verify_worker.worker,
);
}
Ok(metadata) => {
chunk_list.push((pos, metadata.ino()));
}
}
}
// sorting by inode improves data locality, which makes it lots faster on spinners
chunk_list.sort_unstable_by(|(_, ino_a), (_, ino_b)| ino_a.cmp(&ino_b));
for (pos, _) in chunk_list {
verify_worker.worker.check_abort()?;
crate::tools::fail_on_shutdown()?;
let info = index.chunk_info(pos).unwrap();
let size = info.size();
if verify_worker.verified_chunks.lock().unwrap().contains(&info.digest) {
continue; // already verified
}
if verify_worker.corrupt_chunks.lock().unwrap().contains(&info.digest) {
let digest_str = proxmox::tools::digest_to_hex(&info.digest);
task_log!(verify_worker.worker, "chunk {} was marked as corrupt", digest_str);
errors.fetch_add(1, Ordering::SeqCst);
continue;
// we must always recheck this here, the parallel worker below alter it!
if skip_chunk(&info.digest) {
continue; // already verified or marked corrupt
}
match verify_worker.datastore.load_chunk(&info.digest) {
@ -183,10 +232,14 @@ fn verify_index_chunks(
verify_worker.corrupt_chunks.lock().unwrap().insert(info.digest);
task_log!(verify_worker.worker, "can't verify chunk, load failed - {}", err);
errors.fetch_add(1, Ordering::SeqCst);
rename_corrupted_chunk(verify_worker.datastore.clone(), &info.digest, &verify_worker.worker);
continue;
rename_corrupted_chunk(
verify_worker.datastore.clone(),
&info.digest,
&verify_worker.worker,
);
}
Ok(chunk) => {
let size = info.size();
read_bytes += chunk.raw_size();
decoder_pool.send((chunk, info.digest, size))?;
decoded_bytes += size;
@ -229,7 +282,6 @@ fn verify_fixed_index(
backup_dir: &BackupDir,
info: &FileInfo,
) -> Result<(), Error> {
let mut path = backup_dir.relative_path();
path.push(&info.filename);
@ -244,11 +296,7 @@ fn verify_fixed_index(
bail!("wrong index checksum");
}
verify_index_chunks(
verify_worker,
Box::new(index),
info.chunk_crypt_mode(),
)
verify_index_chunks(verify_worker, Box::new(index), info.chunk_crypt_mode())
}
fn verify_dynamic_index(
@ -256,7 +304,6 @@ fn verify_dynamic_index(
backup_dir: &BackupDir,
info: &FileInfo,
) -> Result<(), Error> {
let mut path = backup_dir.relative_path();
path.push(&info.filename);
@ -271,11 +318,7 @@ fn verify_dynamic_index(
bail!("wrong index checksum");
}
verify_index_chunks(
verify_worker,
Box::new(index),
info.chunk_crypt_mode(),
)
verify_index_chunks(verify_worker, Box::new(index), info.chunk_crypt_mode())
}
/// Verify a single backup snapshot
@ -296,15 +339,12 @@ pub fn verify_backup_dir(
let snap_lock = lock_dir_noblock_shared(
&verify_worker.datastore.snapshot_path(&backup_dir),
"snapshot",
"locked by another operation");
"locked by another operation",
);
match snap_lock {
Ok(snap_lock) => verify_backup_dir_with_lock(
verify_worker,
backup_dir,
upid,
filter,
snap_lock
),
Ok(snap_lock) => {
verify_backup_dir_with_lock(verify_worker, backup_dir, upid, filter, snap_lock)
}
Err(err) => {
task_log!(
verify_worker.worker,
@ -361,19 +401,11 @@ pub fn verify_backup_dir_with_lock(
let result = proxmox::try_block!({
task_log!(verify_worker.worker, " check {}", info.filename);
match archive_type(&info.filename)? {
ArchiveType::FixedIndex =>
verify_fixed_index(
verify_worker,
&backup_dir,
info,
),
ArchiveType::DynamicIndex =>
verify_dynamic_index(
verify_worker,
&backup_dir,
info,
),
ArchiveType::Blob => verify_blob(verify_worker.datastore.clone(), &backup_dir, info),
ArchiveType::FixedIndex => verify_fixed_index(verify_worker, &backup_dir, info),
ArchiveType::DynamicIndex => verify_dynamic_index(verify_worker, &backup_dir, info),
ArchiveType::Blob => {
verify_blob(verify_worker.datastore.clone(), &backup_dir, info)
}
}
});
@ -392,7 +424,6 @@ pub fn verify_backup_dir_with_lock(
error_count += 1;
verify_result = VerifyState::Failed;
}
}
let verify_state = SnapshotVerifyState {
@ -400,9 +431,12 @@ pub fn verify_backup_dir_with_lock(
upid,
};
let verify_state = serde_json::to_value(verify_state)?;
verify_worker.datastore.update_manifest(&backup_dir, |manifest| {
verify_worker
.datastore
.update_manifest(&backup_dir, |manifest| {
manifest.unprotected["verify_state"] = verify_state;
}).map_err(|err| format_err!("unable to update manifest blob - {}", err))?;
})
.map_err(|err| format_err!("unable to update manifest blob - {}", err))?;
Ok(error_count == 0)
}
@ -421,7 +455,6 @@ pub fn verify_backup_group(
upid: &UPID,
filter: Option<&dyn Fn(&BackupManifest) -> bool>,
) -> Result<Vec<String>, Error> {
let mut errors = Vec::new();
let mut list = match group.list_backups(&verify_worker.datastore.base_path()) {
Ok(list) => list,
@ -438,26 +471,23 @@ pub fn verify_backup_group(
};
let snapshot_count = list.len();
task_log!(verify_worker.worker, "verify group {}:{} ({} snapshots)", verify_worker.datastore.name(), group, snapshot_count);
task_log!(
verify_worker.worker,
"verify group {}:{} ({} snapshots)",
verify_worker.datastore.name(),
group,
snapshot_count
);
progress.group_snapshots = snapshot_count as u64;
BackupInfo::sort_list(&mut list, false); // newest first
for (pos, info) in list.into_iter().enumerate() {
if !verify_backup_dir(
verify_worker,
&info.backup_dir,
upid.clone(),
filter,
)? {
if !verify_backup_dir(verify_worker, &info.backup_dir, upid.clone(), filter)? {
errors.push(info.backup_dir.to_string());
}
progress.done_snapshots = pos as u64 + 1;
task_log!(
verify_worker.worker,
"percentage done: {}",
progress
);
task_log!(verify_worker.worker, "percentage done: {}", progress);
}
Ok(errors)
@ -521,11 +551,7 @@ pub fn verify_all_backups(
.filter(filter_by_owner)
.collect::<Vec<BackupGroup>>(),
Err(err) => {
task_log!(
worker,
"unable to list backups: {}",
err,
);
task_log!(worker, "unable to list backups: {}", err,);
return Ok(errors);
}
};
@ -542,13 +568,8 @@ pub fn verify_all_backups(
progress.done_snapshots = 0;
progress.group_snapshots = 0;
let mut group_errors = verify_backup_group(
verify_worker,
&group,
&mut progress,
upid,
filter,
)?;
let mut group_errors =
verify_backup_group(verify_worker, &group, &mut progress, upid, filter)?;
errors.append(&mut group_errors);
}

View File

@ -1,18 +1,18 @@
/// Control magnetic tape drive operation
///
/// This is a Rust implementation, meant to replace the 'mt' command
/// line tool.
/// This is a Rust implementation, using the Proxmox userspace tape
/// driver. This is meant as replacement fot the 'mt' command line
/// tool.
///
/// Features:
///
/// - written in Rust
/// - use Proxmox userspace driver (using SG_IO)
/// - optional json output format
/// - support tape alert flags
/// - support volume statistics
/// - read cartridge memory
use std::collections::HashMap;
use anyhow::{bail, Error};
use serde_json::Value;
@ -36,6 +36,12 @@ pub const FILE_MARK_COUNT_SCHEMA: Schema =
.maximum(i32::MAX as isize)
.schema();
pub const FILE_MARK_POSITION_SCHEMA: Schema =
IntegerSchema::new("File mark position (0 is BOT).")
.minimum(0)
.maximum(i32::MAX as isize)
.schema();
pub const RECORD_COUNT_SCHEMA: Schema =
IntegerSchema::new("Record count.")
.minimum(1)
@ -43,7 +49,7 @@ pub const RECORD_COUNT_SCHEMA: Schema =
.schema();
pub const DRIVE_OPTION_SCHEMA: Schema = StringSchema::new(
"Linux Tape Driver Option, either numeric value or option name.")
"Lto Tape Driver Option, either numeric value or option name.")
.schema();
pub const DRIVE_OPTION_LIST_SCHEMA: Schema =
@ -57,103 +63,60 @@ use proxmox_backup::{
drive::complete_drive_name,
},
api2::types::{
LINUX_DRIVE_PATH_SCHEMA,
LTO_DRIVE_PATH_SCHEMA,
DRIVE_NAME_SCHEMA,
LinuxTapeDrive,
LtoTapeDrive,
},
tape::{
complete_drive_path,
linux_tape_device_list,
lto_tape_device_list,
drive::{
linux_mtio::{MTCmd, SetDrvBufferOptions},
TapeDriver,
LinuxTapeHandle,
open_linux_tape_device,
LtoTapeHandle,
open_lto_tape_device,
},
},
};
lazy_static::lazy_static!{
static ref DRIVE_OPTIONS: HashMap<String, SetDrvBufferOptions> = {
let mut map = HashMap::new();
for i in 0..31 {
let bit: i32 = 1 << i;
let flag = SetDrvBufferOptions::from_bits_truncate(bit);
if flag.bits() == 0 { continue; }
let name = format!("{:?}", flag)
.to_lowercase()
.replace("_", "-");
map.insert(name, flag);
}
map
};
}
fn parse_drive_options(options: Vec<String>) -> Result<SetDrvBufferOptions, Error> {
let mut value = SetDrvBufferOptions::empty();
for option in options.iter() {
if let Ok::<i32,_>(v) = option.parse() {
value |= SetDrvBufferOptions::from_bits_truncate(v);
} else if let Some(v) = DRIVE_OPTIONS.get(option) {
value |= *v;
} else {
let option = option.to_lowercase().replace("_", "-");
if let Some(v) = DRIVE_OPTIONS.get(&option) {
value |= *v;
} else {
bail!("unknown drive option {}", option);
}
}
}
Ok(value)
}
fn get_tape_handle(param: &Value) -> Result<LinuxTapeHandle, Error> {
fn get_tape_handle(param: &Value) -> Result<LtoTapeHandle, Error> {
if let Some(name) = param["drive"].as_str() {
let (config, _digest) = config::drive::config()?;
let drive: LinuxTapeDrive = config.lookup("linux", &name)?;
let drive: LtoTapeDrive = config.lookup("lto", &name)?;
eprintln!("using device {}", drive.path);
return Ok(LinuxTapeHandle::new(open_linux_tape_device(&drive.path)?))
return LtoTapeHandle::new(open_lto_tape_device(&drive.path)?);
}
if let Some(device) = param["device"].as_str() {
eprintln!("using device {}", device);
return Ok(LinuxTapeHandle::new(open_linux_tape_device(&device)?))
return LtoTapeHandle::new(open_lto_tape_device(&device)?);
}
if let Ok(name) = std::env::var("PROXMOX_TAPE_DRIVE") {
let (config, _digest) = config::drive::config()?;
let drive: LinuxTapeDrive = config.lookup("linux", &name)?;
let drive: LtoTapeDrive = config.lookup("lto", &name)?;
eprintln!("using device {}", drive.path);
return Ok(LinuxTapeHandle::new(open_linux_tape_device(&drive.path)?))
return LtoTapeHandle::new(open_lto_tape_device(&drive.path)?);
}
if let Ok(device) = std::env::var("TAPE") {
eprintln!("using device {}", device);
return Ok(LinuxTapeHandle::new(open_linux_tape_device(&device)?))
return LtoTapeHandle::new(open_lto_tape_device(&device)?);
}
let (config, _digest) = config::drive::config()?;
let mut drive_names = Vec::new();
for (name, (section_type, _)) in config.sections.iter() {
if section_type != "linux" { continue; }
if section_type != "lto" { continue; }
drive_names.push(name);
}
if drive_names.len() == 1 {
let name = drive_names[0];
let drive: LinuxTapeDrive = config.lookup("linux", &name)?;
let drive: LtoTapeDrive = config.lookup("lto", &name)?;
eprintln!("using device {}", drive.path);
return Ok(LinuxTapeHandle::new(open_linux_tape_device(&drive.path)?))
return LtoTapeHandle::new(open_lto_tape_device(&drive.path)?);
}
bail!("no drive/device specified");
@ -167,26 +130,22 @@ fn get_tape_handle(param: &Value) -> Result<LinuxTapeHandle, Error> {
optional: true,
},
device: {
schema: LINUX_DRIVE_PATH_SCHEMA,
schema: LTO_DRIVE_PATH_SCHEMA,
optional: true,
},
count: {
schema: FILE_MARK_COUNT_SCHEMA,
schema: FILE_MARK_POSITION_SCHEMA,
},
},
},
)]
/// Position the tape at the beginning of the count file.
///
/// Positioning is done by first rewinding the tape and then spacing
/// forward over count file marks.
fn asf(count: usize, param: Value) -> Result<(), Error> {
/// Position the tape at the beginning of the count file (after
/// filemark count)
fn asf(count: u64, param: Value) -> Result<(), Error> {
let mut handle = get_tape_handle(&param)?;
handle.rewind()?;
handle.forward_space_count_files(count)?;
handle.locate_file(count)?;
Ok(())
}
@ -200,7 +159,7 @@ fn asf(count: usize, param: Value) -> Result<(), Error> {
optional: true,
},
device: {
schema: LINUX_DRIVE_PATH_SCHEMA,
schema: LTO_DRIVE_PATH_SCHEMA,
optional: true,
},
count: {
@ -230,7 +189,7 @@ fn bsf(count: usize, param: Value) -> Result<(), Error> {
optional: true,
},
device: {
schema: LINUX_DRIVE_PATH_SCHEMA,
schema: LTO_DRIVE_PATH_SCHEMA,
optional: true,
},
count: {
@ -243,11 +202,12 @@ fn bsf(count: usize, param: Value) -> Result<(), Error> {
///
/// This leaves the tape positioned at the first block of the file
/// that is count - 1 files before the current file.
fn bsfm(count: i32, param: Value) -> Result<(), Error> {
fn bsfm(count: usize, param: Value) -> Result<(), Error> {
let mut handle = get_tape_handle(&param)?;
handle.mtop(MTCmd::MTBSFM, count, "bsfm")?;
handle.backward_space_count_files(count)?;
handle.forward_space_count_files(1)?;
Ok(())
}
@ -261,7 +221,7 @@ fn bsfm(count: i32, param: Value) -> Result<(), Error> {
optional: true,
},
device: {
schema: LINUX_DRIVE_PATH_SCHEMA,
schema: LTO_DRIVE_PATH_SCHEMA,
optional: true,
},
count: {
@ -271,11 +231,11 @@ fn bsfm(count: i32, param: Value) -> Result<(), Error> {
},
)]
/// Backward space records.
fn bsr(count: i32, param: Value) -> Result<(), Error> {
fn bsr(count: usize, param: Value) -> Result<(), Error> {
let mut handle = get_tape_handle(&param)?;
handle.mtop(MTCmd::MTBSR, count, "backward space records")?;
handle.backward_space_count_records(count)?;
Ok(())
}
@ -289,7 +249,7 @@ fn bsr(count: i32, param: Value) -> Result<(), Error> {
optional: true,
},
device: {
schema: LINUX_DRIVE_PATH_SCHEMA,
schema: LTO_DRIVE_PATH_SCHEMA,
optional: true,
},
"output-format": {
@ -340,7 +300,7 @@ fn cartridge_memory(param: Value) -> Result<(), Error> {
optional: true,
},
device: {
schema: LINUX_DRIVE_PATH_SCHEMA,
schema: LTO_DRIVE_PATH_SCHEMA,
optional: true,
},
"output-format": {
@ -389,7 +349,7 @@ fn tape_alert_flags(param: Value) -> Result<(), Error> {
optional: true,
},
device: {
schema: LINUX_DRIVE_PATH_SCHEMA,
schema: LTO_DRIVE_PATH_SCHEMA,
optional: true,
},
},
@ -413,7 +373,7 @@ fn eject(param: Value) -> Result<(), Error> {
optional: true,
},
device: {
schema: LINUX_DRIVE_PATH_SCHEMA,
schema: LTO_DRIVE_PATH_SCHEMA,
optional: true,
},
},
@ -423,7 +383,7 @@ fn eject(param: Value) -> Result<(), Error> {
fn eod(param: Value) -> Result<(), Error> {
let mut handle = get_tape_handle(&param)?;
handle.move_to_eom()?;
handle.move_to_eom(false)?;
Ok(())
}
@ -437,7 +397,7 @@ fn eod(param: Value) -> Result<(), Error> {
optional: true,
},
device: {
schema: LINUX_DRIVE_PATH_SCHEMA,
schema: LTO_DRIVE_PATH_SCHEMA,
optional: true,
},
fast: {
@ -449,7 +409,7 @@ fn eod(param: Value) -> Result<(), Error> {
},
},
)]
/// Erase media
/// Erase media (from current position)
fn erase(fast: Option<bool>, param: Value) -> Result<(), Error> {
let mut handle = get_tape_handle(&param)?;
@ -466,7 +426,36 @@ fn erase(fast: Option<bool>, param: Value) -> Result<(), Error> {
optional: true,
},
device: {
schema: LINUX_DRIVE_PATH_SCHEMA,
schema: LTO_DRIVE_PATH_SCHEMA,
optional: true,
},
fast: {
description: "Use fast erase.",
type: bool,
optional: true,
default: true,
},
},
},
)]
/// Format media, single partition
fn format(fast: Option<bool>, param: Value) -> Result<(), Error> {
let mut handle = get_tape_handle(&param)?;
handle.format_media(fast.unwrap_or(true))?;
Ok(())
}
#[api(
input: {
properties: {
drive: {
schema: DRIVE_NAME_SCHEMA,
optional: true,
},
device: {
schema: LTO_DRIVE_PATH_SCHEMA,
optional: true,
},
count: {
@ -495,7 +484,7 @@ fn fsf(count: usize, param: Value) -> Result<(), Error> {
optional: true,
},
device: {
schema: LINUX_DRIVE_PATH_SCHEMA,
schema: LTO_DRIVE_PATH_SCHEMA,
optional: true,
},
count: {
@ -508,11 +497,12 @@ fn fsf(count: usize, param: Value) -> Result<(), Error> {
///
/// This leaves the tape positioned at the last block of the file that
/// is count - 1 files past the current file.
fn fsfm(count: i32, param: Value) -> Result<(), Error> {
fn fsfm(count: usize, param: Value) -> Result<(), Error> {
let mut handle = get_tape_handle(&param)?;
handle.mtop(MTCmd::MTFSFM, count, "fsfm")?;
handle.forward_space_count_files(count)?;
handle.backward_space_count_files(1)?;
Ok(())
}
@ -526,7 +516,7 @@ fn fsfm(count: i32, param: Value) -> Result<(), Error> {
optional: true,
},
device: {
schema: LINUX_DRIVE_PATH_SCHEMA,
schema: LTO_DRIVE_PATH_SCHEMA,
optional: true,
},
count: {
@ -536,11 +526,11 @@ fn fsfm(count: i32, param: Value) -> Result<(), Error> {
},
)]
/// Forward space records.
fn fsr(count: i32, param: Value) -> Result<(), Error> {
fn fsr(count: usize, param: Value) -> Result<(), Error> {
let mut handle = get_tape_handle(&param)?;
handle.mtop(MTCmd::MTFSR, count, "forward space records")?;
handle.forward_space_count_records(count)?;
Ok(())
}
@ -554,7 +544,7 @@ fn fsr(count: i32, param: Value) -> Result<(), Error> {
optional: true,
},
device: {
schema: LINUX_DRIVE_PATH_SCHEMA,
schema: LTO_DRIVE_PATH_SCHEMA,
optional: true,
},
},
@ -564,7 +554,7 @@ fn fsr(count: i32, param: Value) -> Result<(), Error> {
fn load(param: Value) -> Result<(), Error> {
let mut handle = get_tape_handle(&param)?;
handle.mtload()?;
handle.load()?;
Ok(())
}
@ -578,7 +568,7 @@ fn load(param: Value) -> Result<(), Error> {
optional: true,
},
device: {
schema: LINUX_DRIVE_PATH_SCHEMA,
schema: LTO_DRIVE_PATH_SCHEMA,
optional: true,
},
},
@ -589,7 +579,7 @@ fn lock(param: Value) -> Result<(), Error> {
let mut handle = get_tape_handle(&param)?;
handle.mtop(MTCmd::MTLOCK, 1, "lock tape drive door")?;
handle.lock()?;
Ok(())
}
@ -603,7 +593,7 @@ fn lock(param: Value) -> Result<(), Error> {
optional: true,
},
device: {
schema: LINUX_DRIVE_PATH_SCHEMA,
schema: LTO_DRIVE_PATH_SCHEMA,
optional: true,
},
},
@ -634,7 +624,7 @@ fn scan(param: Value) -> Result<(), Error> {
let output_format = get_output_format(&param);
let list = linux_tape_device_list();
let list = lto_tape_device_list();
if output_format == "json-pretty" {
println!("{}", serde_json::to_string_pretty(&list)?);
@ -657,7 +647,6 @@ fn scan(param: Value) -> Result<(), Error> {
Ok(())
}
#[api(
input: {
properties: {
@ -666,36 +655,7 @@ fn scan(param: Value) -> Result<(), Error> {
optional: true,
},
device: {
schema: LINUX_DRIVE_PATH_SCHEMA,
optional: true,
},
size: {
description: "Block size in bytes.",
minimum: 0,
},
},
},
)]
/// Set the block size of the drive
fn setblk(size: i32, param: Value) -> Result<(), Error> {
let mut handle = get_tape_handle(&param)?;
handle.mtop(MTCmd::MTSETBLK, size, "set block size")?;
Ok(())
}
#[api(
input: {
properties: {
drive: {
schema: DRIVE_NAME_SCHEMA,
optional: true,
},
device: {
schema: LINUX_DRIVE_PATH_SCHEMA,
schema: LTO_DRIVE_PATH_SCHEMA,
optional: true,
},
"output-format": {
@ -745,123 +705,7 @@ fn status(param: Value) -> Result<(), Error> {
optional: true,
},
device: {
schema: LINUX_DRIVE_PATH_SCHEMA,
optional: true,
},
options: {
schema: DRIVE_OPTION_LIST_SCHEMA,
optional: true,
},
defaults: {
description: "Set default options (buffer-writes async-writes read-ahead can-bsr).",
type: bool,
optional: true,
},
},
},
)]
/// Set device driver options (root only)
fn st_options(
options: Option<Vec<String>>,
defaults: Option<bool>,
param: Value) -> Result<(), Error> {
let handle = get_tape_handle(&param)?;
let options = match defaults {
Some(true) => {
if options.is_some() {
bail!("option --defaults conflicts with specified options");
}
let mut list = Vec::new();
list.push(String::from("buffer-writes"));
list.push(String::from("async-writes"));
list.push(String::from("read-ahead"));
list.push(String::from("can-bsr"));
list
}
Some(false) | None => {
options.unwrap_or_else(|| Vec::new())
}
};
let value = parse_drive_options(options)?;
handle.set_drive_buffer_options(value)?;
Ok(())
}
#[api(
input: {
properties: {
drive: {
schema: DRIVE_NAME_SCHEMA,
optional: true,
},
device: {
schema: LINUX_DRIVE_PATH_SCHEMA,
optional: true,
},
options: {
schema: DRIVE_OPTION_LIST_SCHEMA,
},
},
},
)]
/// Set selected device driver options bits (root only)
fn st_set_options(options: Vec<String>, param: Value) -> Result<(), Error> {
let handle = get_tape_handle(&param)?;
let value = parse_drive_options(options)?;
handle.drive_buffer_set_options(value)?;
Ok(())
}
#[api(
input: {
properties: {
drive: {
schema: DRIVE_NAME_SCHEMA,
optional: true,
},
device: {
schema: LINUX_DRIVE_PATH_SCHEMA,
optional: true,
},
options: {
schema: DRIVE_OPTION_LIST_SCHEMA,
},
},
},
)]
/// Clear selected device driver options bits (root only)
fn st_clear_options(options: Vec<String>, param: Value) -> Result<(), Error> {
let handle = get_tape_handle(&param)?;
let value = parse_drive_options(options)?;
handle.drive_buffer_clear_options(value)?;
Ok(())
}
#[api(
input: {
properties: {
drive: {
schema: DRIVE_NAME_SCHEMA,
optional: true,
},
device: {
schema: LINUX_DRIVE_PATH_SCHEMA,
schema: LTO_DRIVE_PATH_SCHEMA,
optional: true,
},
},
@ -872,7 +716,7 @@ fn unlock(param: Value) -> Result<(), Error> {
let mut handle = get_tape_handle(&param)?;
handle.mtop(MTCmd::MTUNLOCK, 1, "unlock tape drive door")?;
handle.unlock()?;
Ok(())
}
@ -886,7 +730,7 @@ fn unlock(param: Value) -> Result<(), Error> {
optional: true,
},
device: {
schema: LINUX_DRIVE_PATH_SCHEMA,
schema: LTO_DRIVE_PATH_SCHEMA,
optional: true,
},
"output-format": {
@ -935,7 +779,7 @@ fn volume_statistics(param: Value) -> Result<(), Error> {
optional: true,
},
device: {
schema: LINUX_DRIVE_PATH_SCHEMA,
schema: LTO_DRIVE_PATH_SCHEMA,
optional: true,
},
count: {
@ -946,10 +790,68 @@ fn volume_statistics(param: Value) -> Result<(), Error> {
},
)]
/// Write count (default 1) EOF marks at current position.
fn weof(count: Option<i32>, param: Value) -> Result<(), Error> {
fn weof(count: Option<usize>, param: Value) -> Result<(), Error> {
let count = count.unwrap_or(1);
let mut handle = get_tape_handle(&param)?;
handle.mtop(MTCmd::MTWEOF, count.unwrap_or(1), "write EOF mark")?;
handle.write_filemarks(count)?;
Ok(())
}
#[api(
input: {
properties: {
drive: {
schema: DRIVE_NAME_SCHEMA,
optional: true,
},
device: {
schema: LTO_DRIVE_PATH_SCHEMA,
optional: true,
},
compression: {
description: "Enable/disable compression.",
type: bool,
optional: true,
},
blocksize: {
description: "Set tape drive block_length (0 is variable length).",
type: u32,
minimum: 0,
maximum: 0x80_00_00,
optional: true,
},
buffer_mode: {
description: "Use drive buffer.",
type: bool,
optional: true,
},
defaults: {
description: "Set default options",
type: bool,
optional: true,
},
},
},
)]
/// Set varios drive options
fn options(
compression: Option<bool>,
blocksize: Option<u32>,
buffer_mode: Option<bool>,
defaults: Option<bool>,
param: Value,
) -> Result<(), Error> {
let mut handle = get_tape_handle(&param)?;
if let Some(true) = defaults {
handle.set_default_options()?;
}
handle.set_drive_options(compression, blocksize, buffer_mode)?;
Ok(())
}
@ -967,7 +869,6 @@ fn main() -> Result<(), Error> {
CliCommand::new(method)
.completion_cb("drive", complete_drive_name)
.completion_cb("device", complete_drive_path)
.completion_cb("options", complete_option_name)
};
let cmd_def = CliCommandMap::new()
@ -980,18 +881,16 @@ fn main() -> Result<(), Error> {
.insert("eject", std_cmd(&API_METHOD_EJECT))
.insert("eod", std_cmd(&API_METHOD_EOD))
.insert("erase", std_cmd(&API_METHOD_ERASE))
.insert("format", std_cmd(&API_METHOD_FORMAT))
.insert("fsf", std_cmd(&API_METHOD_FSF).arg_param(&["count"]))
.insert("fsfm", std_cmd(&API_METHOD_FSFM).arg_param(&["count"]))
.insert("fsr", std_cmd(&API_METHOD_FSR).arg_param(&["count"]))
.insert("load", std_cmd(&API_METHOD_LOAD))
.insert("lock", std_cmd(&API_METHOD_LOCK))
.insert("options", std_cmd(&API_METHOD_OPTIONS))
.insert("rewind", std_cmd(&API_METHOD_REWIND))
.insert("scan", CliCommand::new(&API_METHOD_SCAN))
.insert("setblk", CliCommand::new(&API_METHOD_SETBLK).arg_param(&["size"]))
.insert("status", std_cmd(&API_METHOD_STATUS))
.insert("stoptions", std_cmd(&API_METHOD_ST_OPTIONS).arg_param(&["options"]))
.insert("stsetoptions", std_cmd(&API_METHOD_ST_SET_OPTIONS).arg_param(&["options"]))
.insert("stclearoptions", std_cmd(&API_METHOD_ST_CLEAR_OPTIONS).arg_param(&["options"]))
.insert("tape-alert-flags", std_cmd(&API_METHOD_TAPE_ALERT_FLAGS))
.insert("unlock", std_cmd(&API_METHOD_UNLOCK))
.insert("volume-statistics", std_cmd(&API_METHOD_VOLUME_STATISTICS))
@ -1005,11 +904,3 @@ fn main() -> Result<(), Error> {
Ok(())
}
// Completion helpers
pub fn complete_option_name(_arg: &str, _param: &HashMap<String, String>) -> Vec<String> {
DRIVE_OPTIONS
.keys()
.map(String::from)
.collect()
}

View File

@ -33,7 +33,7 @@ use proxmox_backup::{
SCSI_CHANGER_PATH_SCHEMA,
CHANGER_NAME_SCHEMA,
ScsiTapeChanger,
LinuxTapeDrive,
LtoTapeDrive,
},
tape::{
linux_tape_changer_list,
@ -67,7 +67,7 @@ fn get_changer_handle(param: &Value) -> Result<File, Error> {
if let Ok(name) = std::env::var("PROXMOX_TAPE_DRIVE") {
let (config, _digest) = config::drive::config()?;
let drive: LinuxTapeDrive = config.lookup("linux", &name)?;
let drive: LtoTapeDrive = config.lookup("lto", &name)?;
if let Some(changer) = drive.changer {
let changer_config: ScsiTapeChanger = config.lookup("changer", &changer)?;
eprintln!("using device {}", changer_config.path);

View File

@ -6,8 +6,11 @@ use proxmox::api::RpcEnvironmentType;
//use proxmox_backup::tools;
//use proxmox_backup::api_schema::config::*;
use proxmox_backup::server::rest::*;
use proxmox_backup::server;
use proxmox_backup::server::{
self,
auth::default_api_auth,
rest::*,
};
use proxmox_backup::tools::daemon;
use proxmox_backup::auth_helpers::*;
use proxmox_backup::config;
@ -53,7 +56,11 @@ async fn run() -> Result<(), Error> {
let _ = csrf_secret(); // load with lazy_static
let mut config = server::ApiConfig::new(
buildcfg::JS_DIR, &proxmox_backup::api2::ROUTER, RpcEnvironmentType::PRIVILEGED)?;
buildcfg::JS_DIR,
&proxmox_backup::api2::ROUTER,
RpcEnvironmentType::PRIVILEGED,
default_api_auth(),
)?;
let mut commando_sock = server::CommandoSocket::new(server::our_ctrl_sock());

View File

@ -14,6 +14,7 @@ use proxmox::api::RpcEnvironmentType;
use proxmox_backup::{
backup::DataStore,
server::{
auth::default_api_auth,
WorkerTask,
ApiConfig,
rest::*,
@ -40,6 +41,7 @@ use proxmox_backup::tools::{
disks::{
DiskManage,
zfs_pool_stats,
get_pool_from_dataset,
},
logrotate::LogRotate,
socket::{
@ -84,12 +86,11 @@ async fn run() -> Result<(), Error> {
let _ = csrf_secret(); // load with lazy_static
let mut config = ApiConfig::new(
buildcfg::JS_DIR, &proxmox_backup::api2::ROUTER, RpcEnvironmentType::PUBLIC)?;
// Enable experimental tape UI if tape.cfg exists
if Path::new("/etc/proxmox-backup/tape.cfg").exists() {
config.enable_tape_ui = true;
}
buildcfg::JS_DIR,
&proxmox_backup::api2::ROUTER,
RpcEnvironmentType::PUBLIC,
default_api_auth(),
)?;
config.add_alias("novnc", "/usr/share/novnc-pve");
config.add_alias("extjs", "/usr/share/javascript/extjs");
@ -865,8 +866,9 @@ fn gather_disk_stats(disk_manager: Arc<DiskManage>, path: &Path, rrd_prefix: &st
let mut device_stat = None;
match fs_type.as_str() {
"zfs" => {
if let Some(pool) = source {
match zfs_pool_stats(&pool) {
if let Some(source) = source {
let pool = get_pool_from_dataset(&source).unwrap_or(&source);
match zfs_pool_stats(pool) {
Ok(stat) => device_stat = stat,
Err(err) => eprintln!("zfs_pool_stats({:?}) failed - {}", pool, err),
}

View File

@ -0,0 +1,456 @@
use std::ffi::OsStr;
use std::os::unix::ffi::OsStrExt;
use std::path::PathBuf;
use std::sync::Arc;
use anyhow::{bail, format_err, Error};
use serde_json::{json, Value};
use proxmox::api::{
api,
cli::{
default_table_format_options, format_and_print_result_full, get_output_format,
run_cli_command, CliCommand, CliCommandMap, CliEnvironment, ColumnConfig, OUTPUT_FORMAT,
},
};
use pxar::accessor::aio::Accessor;
use pxar::decoder::aio::Decoder;
use proxmox_backup::api2::{helpers, types::ArchiveEntry};
use proxmox_backup::backup::{
decrypt_key, BackupDir, BufferedDynamicReader, CatalogReader, CryptConfig, CryptMode,
DirEntryAttribute, IndexFile, LocalDynamicReadAt, CATALOG_NAME,
};
use proxmox_backup::client::{BackupReader, RemoteChunkReader};
use proxmox_backup::pxar::{create_zip, extract_sub_dir, extract_sub_dir_seq};
use proxmox_backup::tools;
// use "pub" so rust doesn't complain about "unused" functions in the module
pub mod proxmox_client_tools;
use proxmox_client_tools::{
complete_group_or_snapshot, complete_repository, connect, extract_repository_from_value,
key_source::{
crypto_parameters, format_key_source, get_encryption_key_password, KEYFD_SCHEMA,
KEYFILE_SCHEMA,
},
REPO_URL_SCHEMA,
};
mod proxmox_file_restore;
use proxmox_file_restore::*;
enum ExtractPath {
ListArchives,
Pxar(String, Vec<u8>),
VM(String, Vec<u8>),
}
fn parse_path(path: String, base64: bool) -> Result<ExtractPath, Error> {
let mut bytes = if base64 {
base64::decode(path)?
} else {
path.into_bytes()
};
if bytes == b"/" {
return Ok(ExtractPath::ListArchives);
}
while bytes.len() > 0 && bytes[0] == b'/' {
bytes.remove(0);
}
let (file, path) = {
let slash_pos = bytes.iter().position(|c| *c == b'/').unwrap_or(bytes.len());
let path = bytes.split_off(slash_pos);
let file = String::from_utf8(bytes)?;
(file, path)
};
if file.ends_with(".pxar.didx") {
Ok(ExtractPath::Pxar(file, path))
} else if file.ends_with(".img.fidx") {
Ok(ExtractPath::VM(file, path))
} else {
bail!("'{}' is not supported for file-restore", file);
}
}
#[api(
input: {
properties: {
repository: {
schema: REPO_URL_SCHEMA,
optional: true,
},
snapshot: {
type: String,
description: "Group/Snapshot path.",
},
"path": {
description: "Path to restore. Directories will be restored as .zip files.",
type: String,
},
"base64": {
type: Boolean,
description: "If set, 'path' will be interpreted as base64 encoded.",
optional: true,
default: false,
},
keyfile: {
schema: KEYFILE_SCHEMA,
optional: true,
},
"keyfd": {
schema: KEYFD_SCHEMA,
optional: true,
},
"crypt-mode": {
type: CryptMode,
optional: true,
},
"driver": {
type: BlockDriverType,
optional: true,
},
"output-format": {
schema: OUTPUT_FORMAT,
optional: true,
},
}
},
returns: {
description: "A list of elements under the given path",
type: Array,
items: {
type: ArchiveEntry,
}
}
)]
/// List a directory from a backup snapshot.
async fn list(
snapshot: String,
path: String,
base64: bool,
param: Value,
) -> Result<(), Error> {
let repo = extract_repository_from_value(&param)?;
let snapshot: BackupDir = snapshot.parse()?;
let path = parse_path(path, base64)?;
let crypto = crypto_parameters(&param)?;
let crypt_config = match crypto.enc_key {
None => None,
Some(ref key) => {
let (key, _, _) =
decrypt_key(&key.key, &get_encryption_key_password).map_err(|err| {
eprintln!("{}", format_key_source(&key.source, "encryption"));
err
})?;
Some(Arc::new(CryptConfig::new(key)?))
}
};
let client = connect(&repo)?;
let client = BackupReader::start(
client,
crypt_config.clone(),
repo.store(),
&snapshot.group().backup_type(),
&snapshot.group().backup_id(),
snapshot.backup_time(),
true,
)
.await?;
let (manifest, _) = client.download_manifest().await?;
manifest.check_fingerprint(crypt_config.as_ref().map(Arc::as_ref))?;
let result = match path {
ExtractPath::ListArchives => {
let mut entries = vec![];
for file in manifest.files() {
match file.filename.rsplitn(2, '.').next().unwrap() {
"didx" => {}
"fidx" => {}
_ => continue, // ignore all non fidx/didx
}
let path = format!("/{}", file.filename);
let attr = DirEntryAttribute::Directory { start: 0 };
entries.push(ArchiveEntry::new(path.as_bytes(), &attr));
}
Ok(entries)
}
ExtractPath::Pxar(file, mut path) => {
let index = client
.download_dynamic_index(&manifest, CATALOG_NAME)
.await?;
let most_used = index.find_most_used_chunks(8);
let file_info = manifest.lookup_file_info(&CATALOG_NAME)?;
let chunk_reader = RemoteChunkReader::new(
client.clone(),
crypt_config,
file_info.chunk_crypt_mode(),
most_used,
);
let reader = BufferedDynamicReader::new(index, chunk_reader);
let mut catalog_reader = CatalogReader::new(reader);
let mut fullpath = file.into_bytes();
fullpath.append(&mut path);
helpers::list_dir_content(&mut catalog_reader, &fullpath)
}
ExtractPath::VM(file, path) => {
let details = SnapRestoreDetails {
manifest,
repo,
snapshot,
};
let driver: Option<BlockDriverType> = match param.get("driver") {
Some(drv) => Some(serde_json::from_value(drv.clone())?),
None => None,
};
data_list(driver, details, file, path).await
}
}?;
let options = default_table_format_options()
.sortby("type", false)
.sortby("text", false)
.column(ColumnConfig::new("type"))
.column(ColumnConfig::new("text").header("name"))
.column(ColumnConfig::new("mtime").header("last modified"))
.column(ColumnConfig::new("size"));
let output_format = get_output_format(&param);
format_and_print_result_full(
&mut json!(result),
&API_METHOD_LIST.returns,
&output_format,
&options,
);
Ok(())
}
#[api(
input: {
properties: {
repository: {
schema: REPO_URL_SCHEMA,
optional: true,
},
snapshot: {
type: String,
description: "Group/Snapshot path.",
},
"path": {
description: "Path to restore. Directories will be restored as .zip files if extracted to stdout.",
type: String,
},
"base64": {
type: Boolean,
description: "If set, 'path' will be interpreted as base64 encoded.",
optional: true,
default: false,
},
target: {
type: String,
optional: true,
description: "Target directory path. Use '-' to write to standard output.",
},
keyfile: {
schema: KEYFILE_SCHEMA,
optional: true,
},
"keyfd": {
schema: KEYFD_SCHEMA,
optional: true,
},
"crypt-mode": {
type: CryptMode,
optional: true,
},
verbose: {
type: Boolean,
description: "Print verbose information",
optional: true,
default: false,
},
"driver": {
type: BlockDriverType,
optional: true,
},
}
}
)]
/// Restore files from a backup snapshot.
async fn extract(
snapshot: String,
path: String,
base64: bool,
target: Option<String>,
verbose: bool,
param: Value,
) -> Result<(), Error> {
let repo = extract_repository_from_value(&param)?;
let snapshot: BackupDir = snapshot.parse()?;
let orig_path = path;
let path = parse_path(orig_path.clone(), base64)?;
let target = match target {
Some(target) if target == "-" => None,
Some(target) => Some(PathBuf::from(target)),
None => Some(std::env::current_dir()?),
};
let crypto = crypto_parameters(&param)?;
let crypt_config = match crypto.enc_key {
None => None,
Some(ref key) => {
let (key, _, _) =
decrypt_key(&key.key, &get_encryption_key_password).map_err(|err| {
eprintln!("{}", format_key_source(&key.source, "encryption"));
err
})?;
Some(Arc::new(CryptConfig::new(key)?))
}
};
let client = connect(&repo)?;
let client = BackupReader::start(
client,
crypt_config.clone(),
repo.store(),
&snapshot.group().backup_type(),
&snapshot.group().backup_id(),
snapshot.backup_time(),
true,
)
.await?;
let (manifest, _) = client.download_manifest().await?;
match path {
ExtractPath::Pxar(archive_name, path) => {
let file_info = manifest.lookup_file_info(&archive_name)?;
let index = client
.download_dynamic_index(&manifest, &archive_name)
.await?;
let most_used = index.find_most_used_chunks(8);
let chunk_reader = RemoteChunkReader::new(
client.clone(),
crypt_config,
file_info.chunk_crypt_mode(),
most_used,
);
let reader = BufferedDynamicReader::new(index, chunk_reader);
let archive_size = reader.archive_size();
let reader = LocalDynamicReadAt::new(reader);
let decoder = Accessor::new(reader, archive_size).await?;
extract_to_target(decoder, &path, target, verbose).await?;
}
ExtractPath::VM(file, path) => {
let details = SnapRestoreDetails {
manifest,
repo,
snapshot,
};
let driver: Option<BlockDriverType> = match param.get("driver") {
Some(drv) => Some(serde_json::from_value(drv.clone())?),
None => None,
};
if let Some(mut target) = target {
let reader = data_extract(driver, details, file, path.clone(), true).await?;
let decoder = Decoder::from_tokio(reader).await?;
extract_sub_dir_seq(&target, decoder, verbose).await?;
// we extracted a .pxarexclude-cli file auto-generated by the VM when encoding the
// archive, this file is of no use for the user, so try to remove it
target.push(".pxarexclude-cli");
std::fs::remove_file(target).map_err(|e| {
format_err!("unable to remove temporary .pxarexclude-cli file - {}", e)
})?;
} else {
let mut reader = data_extract(driver, details, file, path.clone(), false).await?;
tokio::io::copy(&mut reader, &mut tokio::io::stdout()).await?;
}
}
_ => {
bail!("cannot extract '{}'", orig_path);
}
}
Ok(())
}
async fn extract_to_target<T>(
decoder: Accessor<T>,
path: &[u8],
target: Option<PathBuf>,
verbose: bool,
) -> Result<(), Error>
where
T: pxar::accessor::ReadAt + Clone + Send + Sync + Unpin + 'static,
{
let root = decoder.open_root().await?;
let file = root
.lookup(OsStr::from_bytes(&path))
.await?
.ok_or_else(|| format_err!("error opening '{:?}'", path))?;
if let Some(target) = target {
extract_sub_dir(target, decoder, OsStr::from_bytes(&path), verbose).await?;
} else {
match file.kind() {
pxar::EntryKind::File { .. } => {
tokio::io::copy(&mut file.contents().await?, &mut tokio::io::stdout()).await?;
}
_ => {
create_zip(
tokio::io::stdout(),
decoder,
OsStr::from_bytes(&path),
verbose,
)
.await?;
}
}
}
Ok(())
}
fn main() {
let list_cmd_def = CliCommand::new(&API_METHOD_LIST)
.arg_param(&["snapshot", "path"])
.completion_cb("repository", complete_repository)
.completion_cb("snapshot", complete_group_or_snapshot);
let restore_cmd_def = CliCommand::new(&API_METHOD_EXTRACT)
.arg_param(&["snapshot", "path", "target"])
.completion_cb("repository", complete_repository)
.completion_cb("snapshot", complete_group_or_snapshot)
.completion_cb("target", tools::complete_file_name);
let status_cmd_def = CliCommand::new(&API_METHOD_STATUS);
let stop_cmd_def = CliCommand::new(&API_METHOD_STOP)
.arg_param(&["name"])
.completion_cb("name", complete_block_driver_ids);
let cmd_def = CliCommandMap::new()
.insert("list", list_cmd_def)
.insert("extract", restore_cmd_def)
.insert("status", status_cmd_def)
.insert("stop", stop_cmd_def);
let rpcenv = CliEnvironment::new();
run_cli_command(
cmd_def,
rpcenv,
Some(|future| proxmox_backup::tools::runtime::main(future)),
);
}

View File

@ -0,0 +1,124 @@
///! Daemon binary to run inside a micro-VM for secure single file restore of disk images
use anyhow::{bail, format_err, Error};
use log::error;
use lazy_static::lazy_static;
use std::os::unix::{
io::{FromRawFd, RawFd},
net,
};
use std::path::Path;
use std::sync::{Arc, Mutex};
use tokio::sync::mpsc;
use tokio_stream::wrappers::ReceiverStream;
use proxmox::api::RpcEnvironmentType;
use proxmox_backup::client::DEFAULT_VSOCK_PORT;
use proxmox_backup::server::{rest::*, ApiConfig};
mod proxmox_restore_daemon;
use proxmox_restore_daemon::*;
/// Maximum amount of pending requests. If saturated, virtio-vsock returns ETIMEDOUT immediately.
/// We should never have more than a few requests in queue, so use a low number.
pub const MAX_PENDING: usize = 32;
/// Will be present in base initramfs
pub const VM_DETECT_FILE: &str = "/restore-vm-marker";
lazy_static! {
/// The current disks state. Use for accessing data on the attached snapshots.
pub static ref DISK_STATE: Arc<Mutex<DiskState>> = {
Arc::new(Mutex::new(DiskState::scan().unwrap()))
};
}
/// This is expected to be run by 'proxmox-file-restore' within a mini-VM
fn main() -> Result<(), Error> {
if !Path::new(VM_DETECT_FILE).exists() {
bail!(concat!(
"This binary is not supposed to be run manually. ",
"Please use 'proxmox-file-restore' instead."
));
}
// don't have a real syslog (and no persistance), so use env_logger to print to a log file (via
// stdout to a serial terminal attached by QEMU)
env_logger::from_env(env_logger::Env::default().default_filter_or("info"))
.write_style(env_logger::WriteStyle::Never)
.init();
// scan all attached disks now, before starting the API
// this will panic and stop the VM if anything goes wrong
{
let _disk_state = DISK_STATE.lock().unwrap();
}
proxmox_backup::tools::runtime::main(run())
}
async fn run() -> Result<(), Error> {
watchdog_init();
let auth_config = Arc::new(
auth::ticket_auth().map_err(|err| format_err!("reading ticket file failed: {}", err))?,
);
let config = ApiConfig::new("", &ROUTER, RpcEnvironmentType::PUBLIC, auth_config)?;
let rest_server = RestServer::new(config);
let vsock_fd = get_vsock_fd()?;
let connections = accept_vsock_connections(vsock_fd);
let receiver_stream = ReceiverStream::new(connections);
let acceptor = hyper::server::accept::from_stream(receiver_stream);
hyper::Server::builder(acceptor).serve(rest_server).await?;
bail!("hyper server exited");
}
fn accept_vsock_connections(
vsock_fd: RawFd,
) -> mpsc::Receiver<Result<tokio::net::UnixStream, Error>> {
use nix::sys::socket::*;
let (sender, receiver) = mpsc::channel(MAX_PENDING);
tokio::spawn(async move {
loop {
let stream: Result<tokio::net::UnixStream, Error> = tokio::task::block_in_place(|| {
// we need to accept manually, as UnixListener aborts if socket type != AF_UNIX ...
let client_fd = accept(vsock_fd)?;
let stream = unsafe { net::UnixStream::from_raw_fd(client_fd) };
stream.set_nonblocking(true)?;
tokio::net::UnixStream::from_std(stream).map_err(|err| err.into())
});
match stream {
Ok(stream) => {
if sender.send(Ok(stream)).await.is_err() {
error!("connection accept channel was closed");
}
}
Err(err) => {
error!("error accepting vsock connetion: {}", err);
}
}
}
});
receiver
}
fn get_vsock_fd() -> Result<RawFd, Error> {
use nix::sys::socket::*;
let sock_fd = socket(
AddressFamily::Vsock,
SockType::Stream,
SockFlag::empty(),
None,
)?;
let sock_addr = VsockAddr::new(libc::VMADDR_CID_ANY, DEFAULT_VSOCK_PORT as u32);
bind(sock_fd, &SockAddr::Vsock(sock_addr))?;
listen(sock_fd, MAX_PENDING)?;
Ok(sock_fd)
}

View File

@ -43,6 +43,7 @@ use proxmox_backup::{
media_pool::complete_pool_name,
},
tape::{
BlockReadError,
drive::{
open_drive,
lock_tape_device,
@ -115,8 +116,8 @@ pub fn extract_drive_name(
},
},
)]
/// Erase media
async fn erase_media(mut param: Value) -> Result<(), Error> {
/// Format media
async fn format_media(mut param: Value) -> Result<(), Error> {
let output_format = get_output_format(&param);
@ -126,7 +127,7 @@ async fn erase_media(mut param: Value) -> Result<(), Error> {
let mut client = connect_to_localhost()?;
let path = format!("api2/json/tape/drive/{}/erase-media", drive);
let path = format!("api2/json/tape/drive/{}/format-media", drive);
let result = client.post(&path, Some(param)).await?;
view_task_result(&mut client, result, &output_format).await?;
@ -551,7 +552,7 @@ fn move_to_eom(mut param: Value) -> Result<(), Error> {
let mut drive = open_drive(&config, &drive)?;
drive.move_to_eom()?;
drive.move_to_eom(false)?;
Ok(())
}
@ -587,12 +588,19 @@ fn debug_scan(mut param: Value) -> Result<(), Error> {
loop {
let file_number = drive.current_file_number()?;
match drive.read_next_file()? {
None => {
println!("EOD");
match drive.read_next_file() {
Err(BlockReadError::EndOfFile) => {
println!("filemark number {}", file_number);
continue;
},
Some(mut reader) => {
}
Err(BlockReadError::EndOfStream) => {
println!("got EOT");
return Ok(());
}
Err(BlockReadError::Error(err)) => {
return Err(err.into());
}
Ok(mut reader) => {
println!("got file number {}", file_number);
let header: Result<MediaContentHeader, _> = unsafe { reader.read_le_value() };
@ -614,8 +622,15 @@ fn debug_scan(mut param: Value) -> Result<(), Error> {
println!("unable to read content header - {}", err);
}
}
let bytes = reader.skip_to_end()?;
let bytes = reader.skip_data()?;
println!("skipped {}", HumanByte::from(bytes));
if let Ok(true) = reader.has_end_marker() {
if reader.is_incomplete()? {
println!("WARNING: file is incomplete");
}
} else {
println!("WARNING: file without end marker");
}
}
}
}
@ -741,8 +756,9 @@ async fn status(mut param: Value) -> Result<(), Error> {
let options = default_table_format_options()
.column(ColumnConfig::new("blocksize"))
.column(ColumnConfig::new("density"))
.column(ColumnConfig::new("status"))
.column(ColumnConfig::new("options"))
.column(ColumnConfig::new("compression"))
.column(ColumnConfig::new("buffer-mode"))
.column(ColumnConfig::new("write-protect"))
.column(ColumnConfig::new("alert-flags"))
.column(ColumnConfig::new("file-number"))
.column(ColumnConfig::new("block-number"))
@ -992,8 +1008,8 @@ fn main() {
.completion_cb("drive", complete_drive_name)
)
.insert(
"erase",
CliCommand::new(&API_METHOD_ERASE_MEDIA)
"format",
CliCommand::new(&API_METHOD_FORMAT_MEDIA)
.completion_cb("drive", complete_drive_name)
)
.insert(

View File

@ -13,6 +13,7 @@ use proxmox::{
use proxmox_backup::api2::access::user::UserWithTokens;
use proxmox_backup::api2::types::*;
use proxmox_backup::backup::BackupDir;
use proxmox_backup::buildcfg;
use proxmox_backup::client::*;
use proxmox_backup::tools;
@ -372,3 +373,15 @@ pub fn place_xdg_file(
.and_then(|base| base.place_config_file(file_name).map_err(Error::from))
.with_context(|| format!("failed to place {} in xdg home", description))
}
/// Returns a runtime dir owned by the current user.
/// Note that XDG_RUNTIME_DIR is not always available, especially for non-login users like
/// "www-data", so we use a custom one in /run/proxmox-backup/<uid> instead.
pub fn get_user_run_dir() -> Result<std::path::PathBuf, Error> {
let uid = nix::unistd::Uid::current();
let mut path: std::path::PathBuf = buildcfg::PROXMOX_BACKUP_RUN_DIR.into();
path.push(uid.to_string());
tools::create_run_dir()?;
std::fs::create_dir_all(&path)?;
Ok(path)
}

View File

@ -0,0 +1,206 @@
//! Abstraction layer over different methods of accessing a block backup
use anyhow::{bail, Error};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use std::collections::HashMap;
use std::future::Future;
use std::hash::BuildHasher;
use std::pin::Pin;
use proxmox_backup::backup::{BackupDir, BackupManifest};
use proxmox_backup::api2::types::ArchiveEntry;
use proxmox_backup::client::BackupRepository;
use proxmox::api::{api, cli::*};
use super::block_driver_qemu::QemuBlockDriver;
/// Contains details about a snapshot that is to be accessed by block file restore
pub struct SnapRestoreDetails {
pub repo: BackupRepository,
pub snapshot: BackupDir,
pub manifest: BackupManifest,
}
/// Return value of a BlockRestoreDriver.status() call, 'id' must be valid for .stop(id)
pub struct DriverStatus {
pub id: String,
pub data: Value,
}
pub type Async<R> = Pin<Box<dyn Future<Output = R> + Send>>;
/// An abstract implementation for retrieving data out of a block file backup
pub trait BlockRestoreDriver {
/// List ArchiveEntrys for the given image file and path
fn data_list(
&self,
details: SnapRestoreDetails,
img_file: String,
path: Vec<u8>,
) -> Async<Result<Vec<ArchiveEntry>, Error>>;
/// pxar=true:
/// Attempt to create a pxar archive of the given file path and return a reader instance for it
/// pxar=false:
/// Attempt to read the file or folder at the given path and return the file content or a zip
/// file as a stream
fn data_extract(
&self,
details: SnapRestoreDetails,
img_file: String,
path: Vec<u8>,
pxar: bool,
) -> Async<Result<Box<dyn tokio::io::AsyncRead + Unpin + Send>, Error>>;
/// Return status of all running/mapped images, result value is (id, extra data), where id must
/// match with the ones returned from list()
fn status(&self) -> Async<Result<Vec<DriverStatus>, Error>>;
/// Stop/Close a running restore method
fn stop(&self, id: String) -> Async<Result<(), Error>>;
/// Returned ids must be prefixed with driver type so that they cannot collide between drivers,
/// the returned values must be passable to stop()
fn list(&self) -> Vec<String>;
}
#[api()]
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Copy)]
pub enum BlockDriverType {
/// Uses a small QEMU/KVM virtual machine to map images securely. Requires PVE-patched QEMU.
Qemu,
}
impl BlockDriverType {
fn resolve(&self) -> impl BlockRestoreDriver {
match self {
BlockDriverType::Qemu => QemuBlockDriver {},
}
}
}
const DEFAULT_DRIVER: BlockDriverType = BlockDriverType::Qemu;
const ALL_DRIVERS: &[BlockDriverType] = &[BlockDriverType::Qemu];
pub async fn data_list(
driver: Option<BlockDriverType>,
details: SnapRestoreDetails,
img_file: String,
path: Vec<u8>,
) -> Result<Vec<ArchiveEntry>, Error> {
let driver = driver.unwrap_or(DEFAULT_DRIVER).resolve();
driver.data_list(details, img_file, path).await
}
pub async fn data_extract(
driver: Option<BlockDriverType>,
details: SnapRestoreDetails,
img_file: String,
path: Vec<u8>,
pxar: bool,
) -> Result<Box<dyn tokio::io::AsyncRead + Send + Unpin>, Error> {
let driver = driver.unwrap_or(DEFAULT_DRIVER).resolve();
driver.data_extract(details, img_file, path, pxar).await
}
#[api(
input: {
properties: {
"driver": {
type: BlockDriverType,
optional: true,
},
"output-format": {
schema: OUTPUT_FORMAT,
optional: true,
},
},
},
)]
/// Retrieve status information about currently running/mapped restore images
pub async fn status(driver: Option<BlockDriverType>, param: Value) -> Result<(), Error> {
let output_format = get_output_format(&param);
let text = output_format == "text";
let mut ret = json!({});
for dt in ALL_DRIVERS {
if driver.is_some() && &driver.unwrap() != dt {
continue;
}
let drv_name = format!("{:?}", dt);
let drv = dt.resolve();
match drv.status().await {
Ok(data) if data.is_empty() => {
if text {
println!("{}: no mappings", drv_name);
} else {
ret[drv_name] = json!({});
}
}
Ok(data) => {
if text {
println!("{}:", &drv_name);
}
ret[&drv_name]["ids"] = json!({});
for status in data {
if text {
println!("{} \t({})", status.id, status.data);
} else {
ret[&drv_name]["ids"][status.id] = status.data;
}
}
}
Err(err) => {
if text {
eprintln!("error getting status from driver '{}' - {}", drv_name, err);
} else {
ret[drv_name] = json!({ "error": format!("{}", err) });
}
}
}
}
if !text {
format_and_print_result(&ret, &output_format);
}
Ok(())
}
#[api(
input: {
properties: {
"name": {
type: String,
description: "The name of the VM to stop.",
},
},
},
)]
/// Immediately stop/unmap a given image. Not typically necessary, as VMs will stop themselves
/// after a timer anyway.
pub async fn stop(name: String) -> Result<(), Error> {
for drv in ALL_DRIVERS.iter().map(BlockDriverType::resolve) {
if drv.list().contains(&name) {
return drv.stop(name).await;
}
}
bail!("no mapping with name '{}' found", name);
}
/// Autocompletion handler for block mappings
pub fn complete_block_driver_ids<S: BuildHasher>(
_arg: &str,
_param: &HashMap<String, String, S>,
) -> Vec<String> {
ALL_DRIVERS
.iter()
.map(BlockDriverType::resolve)
.map(|d| d.list())
.flatten()
.collect()
}

View File

@ -0,0 +1,330 @@
//! Block file access via a small QEMU restore VM using the PBS block driver in QEMU
use anyhow::{bail, Error};
use futures::FutureExt;
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::collections::HashMap;
use std::fs::{File, OpenOptions};
use std::io::{prelude::*, SeekFrom};
use proxmox::tools::fs::lock_file;
use proxmox_backup::api2::types::ArchiveEntry;
use proxmox_backup::backup::BackupDir;
use proxmox_backup::client::*;
use proxmox_backup::tools;
use super::block_driver::*;
use crate::proxmox_client_tools::get_user_run_dir;
const RESTORE_VM_MAP: &str = "restore-vm-map.json";
pub struct QemuBlockDriver {}
#[derive(Clone, Hash, Serialize, Deserialize)]
struct VMState {
pid: i32,
cid: i32,
ticket: String,
}
struct VMStateMap {
map: HashMap<String, VMState>,
file: File,
}
impl VMStateMap {
fn open_file_raw(write: bool) -> Result<File, Error> {
use std::os::unix::fs::OpenOptionsExt;
let mut path = get_user_run_dir()?;
path.push(RESTORE_VM_MAP);
OpenOptions::new()
.read(true)
.write(write)
.create(write)
.mode(0o600)
.open(path)
.map_err(Error::from)
}
/// Acquire a lock on the state map and retrieve a deserialized version
fn load() -> Result<Self, Error> {
let mut file = Self::open_file_raw(true)?;
lock_file(&mut file, true, Some(std::time::Duration::from_secs(5)))?;
let map = serde_json::from_reader(&file).unwrap_or_default();
Ok(Self { map, file })
}
/// Load a read-only copy of the current VM map. Only use for informational purposes, like
/// shell auto-completion, for anything requiring consistency use load() !
fn load_read_only() -> Result<HashMap<String, VMState>, Error> {
let file = Self::open_file_raw(false)?;
Ok(serde_json::from_reader(&file).unwrap_or_default())
}
/// Write back a potentially modified state map, consuming the held lock
fn write(mut self) -> Result<(), Error> {
self.file.seek(SeekFrom::Start(0))?;
self.file.set_len(0)?;
serde_json::to_writer(self.file, &self.map)?;
// drop ourselves including file lock
Ok(())
}
/// Return the map, but drop the lock immediately
fn read_only(self) -> HashMap<String, VMState> {
self.map
}
}
fn make_name(repo: &BackupRepository, snap: &BackupDir) -> String {
let full = format!("qemu_{}/{}", repo, snap);
tools::systemd::escape_unit(&full, false)
}
/// remove non-responsive VMs from given map, returns 'true' if map was modified
async fn cleanup_map(map: &mut HashMap<String, VMState>) -> bool {
let mut to_remove = Vec::new();
for (name, state) in map.iter() {
let client = VsockClient::new(state.cid, DEFAULT_VSOCK_PORT, Some(state.ticket.clone()));
let res = client
.get("api2/json/status", Some(json!({"keep-timeout": true})))
.await;
if res.is_err() {
// VM is not reachable, remove from map and inform user
to_remove.push(name.clone());
println!(
"VM '{}' (pid: {}, cid: {}) was not reachable, removing from map",
name, state.pid, state.cid
);
}
}
for tr in &to_remove {
map.remove(tr);
}
!to_remove.is_empty()
}
fn new_ticket() -> String {
proxmox::tools::Uuid::generate().to_string()
}
async fn ensure_running(details: &SnapRestoreDetails) -> Result<VsockClient, Error> {
let name = make_name(&details.repo, &details.snapshot);
let mut state = VMStateMap::load()?;
cleanup_map(&mut state.map).await;
let new_cid;
let vms = match state.map.get(&name) {
Some(vm) => {
let client = VsockClient::new(vm.cid, DEFAULT_VSOCK_PORT, Some(vm.ticket.clone()));
let res = client.get("api2/json/status", None).await;
match res {
Ok(_) => {
// VM is running and we just reset its timeout, nothing to do
return Ok(client);
}
Err(err) => {
println!("stale VM detected, restarting ({})", err);
// VM is dead, restart
let vms = start_vm(vm.cid, details).await?;
new_cid = vms.cid;
state.map.insert(name, vms.clone());
vms
}
}
}
None => {
let mut cid = state
.map
.iter()
.map(|v| v.1.cid)
.max()
.unwrap_or(0)
.wrapping_add(1);
// offset cid by user id, to avoid unneccessary retries
let running_uid = nix::unistd::Uid::current();
cid = cid.wrapping_add(running_uid.as_raw() as i32);
// some low CIDs have special meaning, start at 10 to avoid them
cid = cid.max(10);
let vms = start_vm(cid, details).await?;
new_cid = vms.cid;
state.map.insert(name, vms.clone());
vms
}
};
state.write()?;
Ok(VsockClient::new(
new_cid,
DEFAULT_VSOCK_PORT,
Some(vms.ticket.clone()),
))
}
async fn start_vm(cid_request: i32, details: &SnapRestoreDetails) -> Result<VMState, Error> {
let ticket = new_ticket();
let files = details
.manifest
.files()
.iter()
.map(|file| file.filename.clone())
.filter(|name| name.ends_with(".img.fidx"));
let (pid, cid) =
super::qemu_helper::start_vm((cid_request.abs() & 0xFFFF) as u16, details, files, &ticket)
.await?;
Ok(VMState { pid, cid, ticket })
}
impl BlockRestoreDriver for QemuBlockDriver {
fn data_list(
&self,
details: SnapRestoreDetails,
img_file: String,
mut path: Vec<u8>,
) -> Async<Result<Vec<ArchiveEntry>, Error>> {
async move {
let client = ensure_running(&details).await?;
if !path.is_empty() && path[0] != b'/' {
path.insert(0, b'/');
}
let path = base64::encode(img_file.bytes().chain(path).collect::<Vec<u8>>());
let mut result = client
.get("api2/json/list", Some(json!({ "path": path })))
.await?;
serde_json::from_value(result["data"].take()).map_err(|err| err.into())
}
.boxed()
}
fn data_extract(
&self,
details: SnapRestoreDetails,
img_file: String,
mut path: Vec<u8>,
pxar: bool,
) -> Async<Result<Box<dyn tokio::io::AsyncRead + Unpin + Send>, Error>> {
async move {
let client = ensure_running(&details).await?;
if !path.is_empty() && path[0] != b'/' {
path.insert(0, b'/');
}
let path = base64::encode(img_file.bytes().chain(path).collect::<Vec<u8>>());
let (mut tx, rx) = tokio::io::duplex(1024 * 4096);
tokio::spawn(async move {
if let Err(err) = client
.download(
"api2/json/extract",
Some(json!({ "path": path, "pxar": pxar })),
&mut tx,
)
.await
{
eprintln!("reading file extraction stream failed - {}", err);
}
});
Ok(Box::new(rx) as Box<dyn tokio::io::AsyncRead + Unpin + Send>)
}
.boxed()
}
fn status(&self) -> Async<Result<Vec<DriverStatus>, Error>> {
async move {
let mut state_map = VMStateMap::load()?;
let modified = cleanup_map(&mut state_map.map).await;
let map = if modified {
let m = state_map.map.clone();
state_map.write()?;
m
} else {
state_map.read_only()
};
let mut result = Vec::new();
for (n, s) in map.iter() {
let client = VsockClient::new(s.cid, DEFAULT_VSOCK_PORT, Some(s.ticket.clone()));
let resp = client
.get("api2/json/status", Some(json!({"keep-timeout": true})))
.await;
let name = tools::systemd::unescape_unit(n)
.unwrap_or_else(|_| "<invalid name>".to_owned());
let mut extra = json!({"pid": s.pid, "cid": s.cid});
match resp {
Ok(status) => match status["data"].as_object() {
Some(map) => {
for (k, v) in map.iter() {
extra[k] = v.clone();
}
}
None => {
let err = format!(
"invalid JSON received from /status call: {}",
status.to_string()
);
extra["error"] = json!(err);
}
},
Err(err) => {
let err = format!("error during /status API call: {}", err);
extra["error"] = json!(err);
}
}
result.push(DriverStatus {
id: name,
data: extra,
});
}
Ok(result)
}
.boxed()
}
fn stop(&self, id: String) -> Async<Result<(), Error>> {
async move {
let name = tools::systemd::escape_unit(&id, false);
let mut map = VMStateMap::load()?;
let map_mod = cleanup_map(&mut map.map).await;
match map.map.get(&name) {
Some(state) => {
let client =
VsockClient::new(state.cid, DEFAULT_VSOCK_PORT, Some(state.ticket.clone()));
// ignore errors, this either fails because:
// * the VM is unreachable/dead, in which case we don't want it in the map
// * the call was successful and the connection reset when the VM stopped
let _ = client.get("api2/json/stop", None).await;
map.map.remove(&name);
map.write()?;
}
None => {
if map_mod {
map.write()?;
}
bail!("VM with name '{}' not found", name);
}
}
Ok(())
}
.boxed()
}
fn list(&self) -> Vec<String> {
match VMStateMap::load_read_only() {
Ok(state) => state
.iter()
.filter_map(|(name, _)| tools::systemd::unescape_unit(&name).ok())
.collect(),
Err(_) => Vec::new(),
}
}
}

View File

@ -0,0 +1,6 @@
//! Block device drivers and tools for single file restore
pub mod block_driver;
pub use block_driver::*;
mod qemu_helper;
mod block_driver_qemu;

View File

@ -0,0 +1,274 @@
//! Helper to start a QEMU VM for single file restore.
use std::fs::{File, OpenOptions};
use std::io::prelude::*;
use std::os::unix::io::{AsRawFd, FromRawFd};
use std::path::PathBuf;
use std::time::Duration;
use anyhow::{bail, format_err, Error};
use tokio::time;
use nix::sys::signal::{kill, Signal};
use nix::unistd::Pid;
use proxmox::tools::{
fd::Fd,
fs::{create_path, file_read_string, make_tmp_file, CreateOptions},
};
use proxmox_backup::backup::backup_user;
use proxmox_backup::client::{VsockClient, DEFAULT_VSOCK_PORT};
use proxmox_backup::{buildcfg, tools};
use super::SnapRestoreDetails;
const PBS_VM_NAME: &str = "pbs-restore-vm";
const MAX_CID_TRIES: u64 = 32;
fn create_restore_log_dir() -> Result<String, Error> {
let logpath = format!("{}/file-restore", buildcfg::PROXMOX_BACKUP_LOG_DIR);
proxmox::try_block!({
let backup_user = backup_user()?;
let opts = CreateOptions::new()
.owner(backup_user.uid)
.group(backup_user.gid);
let opts_root = CreateOptions::new()
.owner(nix::unistd::ROOT)
.group(nix::unistd::Gid::from_raw(0));
create_path(buildcfg::PROXMOX_BACKUP_LOG_DIR, None, Some(opts))?;
create_path(&logpath, None, Some(opts_root))?;
Ok(())
})
.map_err(|err: Error| format_err!("unable to create file-restore log dir - {}", err))?;
Ok(logpath)
}
fn validate_img_existance() -> Result<(), Error> {
let kernel = PathBuf::from(buildcfg::PROXMOX_BACKUP_KERNEL_FN);
let initramfs = PathBuf::from(buildcfg::PROXMOX_BACKUP_INITRAMFS_FN);
if !kernel.exists() || !initramfs.exists() {
bail!("cannot run file-restore VM: package 'proxmox-file-restore' is not (correctly) installed");
}
Ok(())
}
fn try_kill_vm(pid: i32) -> Result<(), Error> {
let pid = Pid::from_raw(pid);
if let Ok(()) = kill(pid, None) {
// process is running (and we could kill it), check if it is actually ours
// (if it errors assume we raced with the process's death and ignore it)
if let Ok(cmdline) = file_read_string(format!("/proc/{}/cmdline", pid)) {
if cmdline.split('\0').any(|a| a == PBS_VM_NAME) {
// yes, it's ours, kill it brutally with SIGKILL, no reason to take
// any chances - in this state it's most likely broken anyway
if let Err(err) = kill(pid, Signal::SIGKILL) {
bail!(
"reaping broken VM (pid {}) with SIGKILL failed: {}",
pid,
err
);
}
}
}
}
Ok(())
}
async fn create_temp_initramfs(ticket: &str) -> Result<(Fd, String), Error> {
use std::ffi::CString;
use tokio::fs::File;
let (tmp_fd, tmp_path) =
make_tmp_file("/tmp/file-restore-qemu.initramfs.tmp", CreateOptions::new())?;
nix::unistd::unlink(&tmp_path)?;
tools::fd_change_cloexec(tmp_fd.0, false)?;
let mut f = File::from_std(unsafe { std::fs::File::from_raw_fd(tmp_fd.0) });
let mut base = File::open(buildcfg::PROXMOX_BACKUP_INITRAMFS_FN).await?;
tokio::io::copy(&mut base, &mut f).await?;
let name = CString::new("ticket").unwrap();
tools::cpio::append_file(
&mut f,
ticket.as_bytes(),
&name,
0,
(libc::S_IFREG | 0o400) as u16,
0,
0,
0,
ticket.len() as u32,
)
.await?;
tools::cpio::append_trailer(&mut f).await?;
// forget the tokio file, we close the file descriptor via the returned Fd
std::mem::forget(f);
let path = format!("/dev/fd/{}", &tmp_fd.0);
Ok((tmp_fd, path))
}
pub async fn start_vm(
// u16 so we can do wrapping_add without going too high
mut cid: u16,
details: &SnapRestoreDetails,
files: impl Iterator<Item = String>,
ticket: &str,
) -> Result<(i32, i32), Error> {
validate_img_existance()?;
if let Err(_) = std::env::var("PBS_PASSWORD") {
bail!("environment variable PBS_PASSWORD has to be set for QEMU VM restore");
}
if let Err(_) = std::env::var("PBS_FINGERPRINT") {
bail!("environment variable PBS_FINGERPRINT has to be set for QEMU VM restore");
}
let pid;
let (pid_fd, pid_path) = make_tmp_file("/tmp/file-restore-qemu.pid.tmp", CreateOptions::new())?;
nix::unistd::unlink(&pid_path)?;
tools::fd_change_cloexec(pid_fd.0, false)?;
let (_ramfs_pid, ramfs_path) = create_temp_initramfs(ticket).await?;
let logpath = create_restore_log_dir()?;
let logfile = &format!("{}/qemu.log", logpath);
let mut logrotate = tools::logrotate::LogRotate::new(logfile, false)
.ok_or_else(|| format_err!("could not get QEMU log file names"))?;
if let Err(err) = logrotate.do_rotate(CreateOptions::default(), Some(16)) {
eprintln!("warning: logrotate for QEMU log file failed - {}", err);
}
let mut logfd = OpenOptions::new()
.append(true)
.create_new(true)
.open(logfile)?;
tools::fd_change_cloexec(logfd.as_raw_fd(), false)?;
// preface log file with start timestamp so one can see how long QEMU took to start
writeln!(logfd, "[{}] PBS file restore VM log", {
let now = proxmox::tools::time::epoch_i64();
proxmox::tools::time::epoch_to_rfc3339(now)?
},)?;
let base_args = [
"-chardev",
&format!(
"file,id=log,path=/dev/null,logfile=/dev/fd/{},logappend=on",
logfd.as_raw_fd()
),
"-serial",
"chardev:log",
"-vnc",
"none",
"-enable-kvm",
"-m",
"512",
"-kernel",
buildcfg::PROXMOX_BACKUP_KERNEL_FN,
"-initrd",
&ramfs_path,
"-append",
"quiet",
"-daemonize",
"-pidfile",
&format!("/dev/fd/{}", pid_fd.as_raw_fd()),
"-name",
PBS_VM_NAME,
];
// Generate drive arguments for all fidx files in backup snapshot
let mut drives = Vec::new();
let mut id = 0;
for file in files {
if !file.ends_with(".img.fidx") {
continue;
}
drives.push("-drive".to_owned());
drives.push(format!(
"file=pbs:repository={},,snapshot={},,archive={},read-only=on,if=none,id=drive{}",
details.repo, details.snapshot, file, id
));
drives.push("-device".to_owned());
// drive serial is used by VM to map .fidx files to /dev paths
drives.push(format!("virtio-blk-pci,drive=drive{},serial={}", id, file));
id += 1;
}
// Try starting QEMU in a loop to retry if we fail because of a bad 'cid' value
let mut attempts = 0;
loop {
let mut qemu_cmd = std::process::Command::new("qemu-system-x86_64");
qemu_cmd.args(base_args.iter());
qemu_cmd.args(&drives);
qemu_cmd.arg("-device");
qemu_cmd.arg(format!(
"vhost-vsock-pci,guest-cid={},disable-legacy=on",
cid
));
qemu_cmd.stdout(std::process::Stdio::null());
qemu_cmd.stderr(std::process::Stdio::piped());
let res = tokio::task::block_in_place(|| qemu_cmd.spawn()?.wait_with_output())?;
if res.status.success() {
// at this point QEMU is already daemonized and running, so if anything fails we
// technically leave behind a zombie-VM... this shouldn't matter, as it will stop
// itself soon enough (timer), and the following operations are unlikely to fail
let mut pid_file = unsafe { File::from_raw_fd(pid_fd.as_raw_fd()) };
std::mem::forget(pid_fd); // FD ownership is now in pid_fd/File
let mut pidstr = String::new();
pid_file.read_to_string(&mut pidstr)?;
pid = pidstr.trim_end().parse().map_err(|err| {
format_err!("cannot parse PID returned by QEMU ('{}'): {}", &pidstr, err)
})?;
break;
} else {
let out = String::from_utf8_lossy(&res.stderr);
if out.contains("unable to set guest cid: Address already in use") {
attempts += 1;
if attempts >= MAX_CID_TRIES {
bail!("CID '{}' in use, but max attempts reached, aborting", cid);
}
// CID in use, try next higher one
eprintln!("CID '{}' in use by other VM, attempting next one", cid);
// skip special-meaning low values
cid = cid.wrapping_add(1).max(10);
} else {
eprint!("{}", out);
bail!("Starting VM failed. See output above for more information.");
}
}
}
// QEMU has started successfully, now wait for virtio socket to become ready
let pid_t = Pid::from_raw(pid);
for _ in 0..60 {
let client = VsockClient::new(cid as i32, DEFAULT_VSOCK_PORT, Some(ticket.to_owned()));
if let Ok(Ok(_)) =
time::timeout(Duration::from_secs(2), client.get("api2/json/status", None)).await
{
return Ok((pid, cid as i32));
}
if kill(pid_t, None).is_err() {
// QEMU exited
bail!("VM exited before connection could be established");
}
time::sleep(Duration::from_millis(200)).await;
}
// start failed
if let Err(err) = try_kill_vm(pid) {
eprintln!("killing failed VM failed: {}", err);
}
bail!("starting VM timed out");
}

View File

@ -0,0 +1,369 @@
///! File-restore API running inside the restore VM
use anyhow::{bail, Error};
use futures::FutureExt;
use hyper::http::request::Parts;
use hyper::{header, Body, Response, StatusCode};
use log::error;
use pathpatterns::{MatchEntry, MatchPattern, MatchType, Pattern};
use serde_json::Value;
use std::ffi::OsStr;
use std::fs;
use std::os::unix::ffi::OsStrExt;
use std::path::{Path, PathBuf};
use proxmox::api::{
api, schema::*, ApiHandler, ApiMethod, ApiResponseFuture, Permission, Router, RpcEnvironment,
SubdirMap,
};
use proxmox::{identity, list_subdirs_api_method, sortable};
use proxmox_backup::api2::types::*;
use proxmox_backup::backup::DirEntryAttribute;
use proxmox_backup::pxar::{create_archive, Flags, PxarCreateOptions, ENCODER_MAX_ENTRIES};
use proxmox_backup::tools::{self, fs::read_subdir, zip::zip_directory};
use pxar::encoder::aio::TokioWriter;
use super::{disk::ResolveResult, watchdog_remaining, watchdog_ping};
// NOTE: All API endpoints must have Permission::Superuser, as the configs for authentication do
// not exist within the restore VM. Safety is guaranteed by checking a ticket via a custom ApiAuth.
const SUBDIRS: SubdirMap = &[
("extract", &Router::new().get(&API_METHOD_EXTRACT)),
("list", &Router::new().get(&API_METHOD_LIST)),
("status", &Router::new().get(&API_METHOD_STATUS)),
("stop", &Router::new().get(&API_METHOD_STOP)),
];
pub const ROUTER: Router = Router::new()
.get(&list_subdirs_api_method!(SUBDIRS))
.subdirs(SUBDIRS);
fn read_uptime() -> Result<f32, Error> {
let uptime = fs::read_to_string("/proc/uptime")?;
// unwrap the Option, if /proc/uptime is empty we have bigger problems
Ok(uptime.split_ascii_whitespace().next().unwrap().parse()?)
}
#[api(
input: {
properties: {
"keep-timeout": {
type: bool,
description: "If true, do not reset the watchdog timer on this API call.",
default: false,
optional: true,
},
},
},
access: {
description: "Permissions are handled outside restore VM. This call can be made without a ticket, but keep-timeout is always assumed 'true' then.",
permission: &Permission::World,
},
returns: {
type: RestoreDaemonStatus,
}
)]
/// General status information
fn status(rpcenv: &mut dyn RpcEnvironment, keep_timeout: bool) -> Result<RestoreDaemonStatus, Error> {
if !keep_timeout && rpcenv.get_auth_id().is_some() {
watchdog_ping();
}
Ok(RestoreDaemonStatus {
uptime: read_uptime()? as i64,
timeout: watchdog_remaining(),
})
}
#[api(
access: {
description: "Permissions are handled outside restore VM.",
permission: &Permission::Superuser,
},
)]
/// Stop the restore VM immediately, this will never return if successful
fn stop() {
use nix::sys::reboot;
println!("/stop called, shutting down");
let err = reboot::reboot(reboot::RebootMode::RB_POWER_OFF).unwrap_err();
println!("'reboot' syscall failed: {}", err);
std::process::exit(1);
}
fn get_dir_entry(path: &Path) -> Result<DirEntryAttribute, Error> {
use nix::sys::stat;
let stat = stat::stat(path)?;
Ok(match stat.st_mode & libc::S_IFMT {
libc::S_IFREG => DirEntryAttribute::File {
size: stat.st_size as u64,
mtime: stat.st_mtime,
},
libc::S_IFDIR => DirEntryAttribute::Directory { start: 0 },
_ => bail!("unsupported file type: {}", stat.st_mode),
})
}
#[api(
input: {
properties: {
"path": {
type: String,
description: "base64-encoded path to list files and directories under",
},
},
},
access: {
description: "Permissions are handled outside restore VM.",
permission: &Permission::Superuser,
},
)]
/// List file details for given file or a list of files and directories under the given path if it
/// points to a directory.
fn list(
path: String,
_info: &ApiMethod,
_rpcenv: &mut dyn RpcEnvironment,
) -> Result<Vec<ArchiveEntry>, Error> {
watchdog_ping();
let mut res = Vec::new();
let param_path = base64::decode(path)?;
let mut path = param_path.clone();
if let Some(b'/') = path.last() {
path.pop();
}
let path_str = OsStr::from_bytes(&path[..]);
let param_path_buf = Path::new(path_str);
let mut disk_state = crate::DISK_STATE.lock().unwrap();
let query_result = disk_state.resolve(&param_path_buf)?;
match query_result {
ResolveResult::Path(vm_path) => {
let root_entry = get_dir_entry(&vm_path)?;
match root_entry {
DirEntryAttribute::File { .. } => {
// list on file, return details
res.push(ArchiveEntry::new(&param_path, &root_entry));
}
DirEntryAttribute::Directory { .. } => {
// list on directory, return all contained files/dirs
for f in read_subdir(libc::AT_FDCWD, &vm_path)? {
if let Ok(f) = f {
let name = f.file_name().to_bytes();
let path = &Path::new(OsStr::from_bytes(name));
if path.components().count() == 1 {
// ignore '.' and '..'
match path.components().next().unwrap() {
std::path::Component::CurDir
| std::path::Component::ParentDir => continue,
_ => {}
}
}
let mut full_vm_path = PathBuf::new();
full_vm_path.push(&vm_path);
full_vm_path.push(path);
let mut full_path = PathBuf::new();
full_path.push(param_path_buf);
full_path.push(path);
let entry = get_dir_entry(&full_vm_path);
if let Ok(entry) = entry {
res.push(ArchiveEntry::new(
full_path.as_os_str().as_bytes(),
&entry,
));
}
}
}
}
_ => unreachable!(),
}
}
ResolveResult::BucketTypes(types) => {
for t in types {
let mut t_path = path.clone();
t_path.push(b'/');
t_path.extend(t.as_bytes());
res.push(ArchiveEntry::new(
&t_path[..],
&DirEntryAttribute::Directory { start: 0 },
));
}
}
ResolveResult::BucketComponents(comps) => {
for c in comps {
let mut c_path = path.clone();
c_path.push(b'/');
c_path.extend(c.as_bytes());
res.push(ArchiveEntry::new(
&c_path[..],
&DirEntryAttribute::Directory { start: 0 },
));
}
}
}
Ok(res)
}
#[sortable]
pub const API_METHOD_EXTRACT: ApiMethod = ApiMethod::new(
&ApiHandler::AsyncHttp(&extract),
&ObjectSchema::new(
"Extract a file or directory from the VM as a pxar archive.",
&sorted!([
(
"path",
false,
&StringSchema::new("base64-encoded path to list files and directories under")
.schema()
),
(
"pxar",
true,
&BooleanSchema::new(concat!(
"if true, return a pxar archive, otherwise either the ",
"file content or the directory as a zip file"
))
.default(true)
.schema()
)
]),
),
)
.access(None, &Permission::Superuser);
fn extract(
_parts: Parts,
_req_body: Body,
param: Value,
_info: &ApiMethod,
_rpcenv: Box<dyn RpcEnvironment>,
) -> ApiResponseFuture {
watchdog_ping();
async move {
let path = tools::required_string_param(&param, "path")?;
let mut path = base64::decode(path)?;
if let Some(b'/') = path.last() {
path.pop();
}
let path = Path::new(OsStr::from_bytes(&path[..]));
let pxar = param["pxar"].as_bool().unwrap_or(true);
let query_result = {
let mut disk_state = crate::DISK_STATE.lock().unwrap();
disk_state.resolve(&path)?
};
let vm_path = match query_result {
ResolveResult::Path(vm_path) => vm_path,
_ => bail!("invalid path, cannot restore meta-directory: {:?}", path),
};
// check here so we can return a real error message, failing in the async task will stop
// the transfer, but not return a useful message
if !vm_path.exists() {
bail!("file or directory {:?} does not exist", path);
}
let (mut writer, reader) = tokio::io::duplex(1024 * 64);
if pxar {
tokio::spawn(async move {
let result = async move {
// pxar always expects a directory as it's root, so to accommodate files as
// well we encode the parent dir with a filter only matching the target instead
let mut patterns = vec![MatchEntry::new(
MatchPattern::Pattern(Pattern::path(b"*").unwrap()),
MatchType::Exclude,
)];
let name = match vm_path.file_name() {
Some(name) => name,
None => bail!("no file name found for path: {:?}", vm_path),
};
if vm_path.is_dir() {
let mut pat = name.as_bytes().to_vec();
patterns.push(MatchEntry::new(
MatchPattern::Pattern(Pattern::path(pat.clone())?),
MatchType::Include,
));
pat.extend(b"/**/*".iter());
patterns.push(MatchEntry::new(
MatchPattern::Pattern(Pattern::path(pat)?),
MatchType::Include,
));
} else {
patterns.push(MatchEntry::new(
MatchPattern::Literal(name.as_bytes().to_vec()),
MatchType::Include,
));
}
let dir_path = vm_path.parent().unwrap_or_else(|| Path::new("/"));
let dir = nix::dir::Dir::open(
dir_path,
nix::fcntl::OFlag::O_NOFOLLOW,
nix::sys::stat::Mode::empty(),
)?;
let options = PxarCreateOptions {
entries_max: ENCODER_MAX_ENTRIES,
device_set: None,
patterns,
verbose: false,
skip_lost_and_found: false,
};
let pxar_writer = TokioWriter::new(writer);
create_archive(dir, pxar_writer, Flags::DEFAULT, |_| Ok(()), None, options)
.await
}
.await;
if let Err(err) = result {
error!("pxar streaming task failed - {}", err);
}
});
} else {
tokio::spawn(async move {
let result = async move {
if vm_path.is_dir() {
zip_directory(&mut writer, &vm_path).await?;
Ok(())
} else if vm_path.is_file() {
let mut file = tokio::fs::OpenOptions::new()
.read(true)
.open(vm_path)
.await?;
tokio::io::copy(&mut file, &mut writer).await?;
Ok(())
} else {
bail!("invalid entry type for path: {:?}", vm_path);
}
}
.await;
if let Err(err) = result {
error!("file or dir streaming task failed - {}", err);
}
});
}
let stream = tokio_util::io::ReaderStream::new(reader);
let body = Body::wrap_stream(stream);
Ok(Response::builder()
.status(StatusCode::OK)
.header(header::CONTENT_TYPE, "application/octet-stream")
.body(body)
.unwrap())
}
.boxed()
}

View File

@ -0,0 +1,45 @@
//! Authentication via a static ticket file
use anyhow::{bail, format_err, Error};
use std::fs::File;
use std::io::prelude::*;
use proxmox_backup::api2::types::Authid;
use proxmox_backup::config::cached_user_info::CachedUserInfo;
use proxmox_backup::server::auth::{ApiAuth, AuthError};
const TICKET_FILE: &str = "/ticket";
pub struct StaticAuth {
ticket: String,
}
impl ApiAuth for StaticAuth {
fn check_auth(
&self,
headers: &http::HeaderMap,
_method: &hyper::Method,
_user_info: &CachedUserInfo,
) -> Result<Authid, AuthError> {
match headers.get(hyper::header::AUTHORIZATION) {
Some(header) if header.to_str().unwrap_or("") == &self.ticket => {
Ok(Authid::root_auth_id().to_owned())
}
_ => {
return Err(AuthError::Generic(format_err!(
"invalid file restore ticket provided"
)));
}
}
}
}
pub fn ticket_auth() -> Result<StaticAuth, Error> {
let mut ticket_file = File::open(TICKET_FILE)?;
let mut ticket = String::new();
let len = ticket_file.read_to_string(&mut ticket)?;
if len <= 0 {
bail!("invalid ticket: cannot be empty");
}
Ok(StaticAuth { ticket })
}

View File

@ -0,0 +1,329 @@
//! Low-level disk (image) access functions for file restore VMs.
use anyhow::{bail, format_err, Error};
use lazy_static::lazy_static;
use log::{info, warn};
use std::collections::HashMap;
use std::fs::{create_dir_all, File};
use std::io::{BufRead, BufReader};
use std::path::{Component, Path, PathBuf};
use proxmox::const_regex;
use proxmox::tools::fs;
use proxmox_backup::api2::types::BLOCKDEVICE_NAME_REGEX;
const_regex! {
VIRTIO_PART_REGEX = r"^vd[a-z]+(\d+)$";
}
lazy_static! {
static ref FS_OPT_MAP: HashMap<&'static str, &'static str> = {
let mut m = HashMap::new();
// otherwise ext complains about mounting read-only
m.insert("ext2", "noload");
m.insert("ext3", "noload");
m.insert("ext4", "noload");
// ufs2 is used as default since FreeBSD 5.0 released in 2003, so let's assume that
// whatever the user is trying to restore is not using anything older...
m.insert("ufs", "ufstype=ufs2");
m
};
}
pub enum ResolveResult {
Path(PathBuf),
BucketTypes(Vec<&'static str>),
BucketComponents(Vec<String>),
}
struct PartitionBucketData {
dev_node: String,
number: i32,
mountpoint: Option<PathBuf>,
}
/// A "Bucket" represents a mapping found on a disk, e.g. a partition, a zfs dataset or an LV. A
/// uniquely identifying path to a file then consists of four components:
/// "/disk/bucket/component/path"
/// where
/// disk: fidx file name
/// bucket: bucket type
/// component: identifier of the specific bucket
/// path: relative path of the file on the filesystem indicated by the other parts, may contain
/// more subdirectories
/// e.g.: "/drive-scsi0/part/0/etc/passwd"
enum Bucket {
Partition(PartitionBucketData),
}
impl Bucket {
fn filter_mut<'a, A: AsRef<str>, B: AsRef<str>>(
haystack: &'a mut Vec<Bucket>,
ty: A,
comp: B,
) -> Option<&'a mut Bucket> {
let ty = ty.as_ref();
let comp = comp.as_ref();
haystack.iter_mut().find(|b| match b {
Bucket::Partition(data) => ty == "part" && comp.parse::<i32>().unwrap() == data.number,
})
}
fn type_string(&self) -> &'static str {
match self {
Bucket::Partition(_) => "part",
}
}
fn component_string(&self) -> String {
match self {
Bucket::Partition(data) => data.number.to_string(),
}
}
}
/// Functions related to the local filesystem. This mostly exists so we can use 'supported_fs' in
/// try_mount while a Bucket is still mutably borrowed from DiskState.
struct Filesystems {
supported_fs: Vec<String>,
}
impl Filesystems {
fn scan() -> Result<Self, Error> {
// detect kernel supported filesystems
let mut supported_fs = Vec::new();
for f in BufReader::new(File::open("/proc/filesystems")?)
.lines()
.filter_map(Result::ok)
{
// ZFS is treated specially, don't attempt to do a regular mount with it
let f = f.trim();
if !f.starts_with("nodev") && f != "zfs" {
supported_fs.push(f.to_owned());
}
}
Ok(Self { supported_fs })
}
fn ensure_mounted(&self, bucket: &mut Bucket) -> Result<PathBuf, Error> {
match bucket {
Bucket::Partition(data) => {
// regular data partition à la "/dev/vdxN"
if let Some(mp) = &data.mountpoint {
return Ok(mp.clone());
}
let mp = format!("/mnt{}/", data.dev_node);
self.try_mount(&data.dev_node, &mp)?;
let mp = PathBuf::from(mp);
data.mountpoint = Some(mp.clone());
Ok(mp)
}
}
}
fn try_mount(&self, source: &str, target: &str) -> Result<(), Error> {
use nix::mount::*;
create_dir_all(target)?;
// try all supported fs until one works - this is the way Busybox's 'mount' does it too:
// https://git.busybox.net/busybox/tree/util-linux/mount.c?id=808d93c0eca49e0b22056e23d965f0d967433fbb#n2152
// note that ZFS is intentionally left out (see scan())
let flags =
MsFlags::MS_RDONLY | MsFlags::MS_NOEXEC | MsFlags::MS_NOSUID | MsFlags::MS_NODEV;
for fs in &self.supported_fs {
let fs: &str = fs.as_ref();
let opts = FS_OPT_MAP.get(fs).copied();
match mount(Some(source), target, Some(fs), flags, opts) {
Ok(()) => {
info!("mounting '{}' succeeded, fstype: '{}'", source, fs);
return Ok(());
}
Err(err) => {
warn!("mount error on '{}' ({}) - {}", source, fs, err);
}
}
}
bail!("all mounts failed or no supported file system")
}
}
pub struct DiskState {
filesystems: Filesystems,
disk_map: HashMap<String, Vec<Bucket>>,
}
impl DiskState {
/// Scan all disks for supported buckets.
pub fn scan() -> Result<Self, Error> {
// create mapping for virtio drives and .fidx files (via serial description)
// note: disks::DiskManager relies on udev, which we don't have
let mut disk_map = HashMap::new();
for entry in proxmox_backup::tools::fs::scan_subdir(
libc::AT_FDCWD,
"/sys/block",
&BLOCKDEVICE_NAME_REGEX,
)?
.filter_map(Result::ok)
{
let name = unsafe { entry.file_name_utf8_unchecked() };
if !name.starts_with("vd") {
continue;
}
let sys_path: &str = &format!("/sys/block/{}", name);
let serial = fs::file_read_string(&format!("{}/serial", sys_path));
let fidx = match serial {
Ok(serial) => serial,
Err(err) => {
warn!("disk '{}': could not read serial file - {}", name, err);
continue;
}
};
let mut parts = Vec::new();
for entry in proxmox_backup::tools::fs::scan_subdir(
libc::AT_FDCWD,
sys_path,
&VIRTIO_PART_REGEX,
)?
.filter_map(Result::ok)
{
let part_name = unsafe { entry.file_name_utf8_unchecked() };
let devnode = format!("/dev/{}", part_name);
let part_path = format!("/sys/block/{}/{}", name, part_name);
// create partition device node for further use
let dev_num_str = fs::file_read_firstline(&format!("{}/dev", part_path))?;
let (major, minor) = dev_num_str.split_at(dev_num_str.find(':').unwrap());
Self::mknod_blk(&devnode, major.parse()?, minor[1..].trim_end().parse()?)?;
let number = fs::file_read_firstline(&format!("{}/partition", part_path))?
.trim()
.parse::<i32>()?;
info!(
"drive '{}' ('{}'): found partition '{}' ({})",
name, fidx, devnode, number
);
let bucket = Bucket::Partition(PartitionBucketData {
dev_node: devnode,
mountpoint: None,
number,
});
parts.push(bucket);
}
disk_map.insert(fidx.to_owned(), parts);
}
Ok(Self {
filesystems: Filesystems::scan()?,
disk_map,
})
}
/// Given a path like "/drive-scsi0.img.fidx/part/0/etc/passwd", this will mount the first
/// partition of 'drive-scsi0' on-demand (i.e. if not already mounted) and return a path
/// pointing to the requested file locally, e.g. "/mnt/vda1/etc/passwd", which can be used to
/// read the file. Given a partial path, i.e. only "/drive-scsi0.img.fidx" or
/// "/drive-scsi0.img.fidx/part", it will return a list of available bucket types or bucket
/// components respectively
pub fn resolve(&mut self, path: &Path) -> Result<ResolveResult, Error> {
let mut cmp = path.components().peekable();
match cmp.peek() {
Some(Component::RootDir) | Some(Component::CurDir) => {
cmp.next();
}
None => bail!("empty path cannot be resolved to file location"),
_ => {}
}
let req_fidx = match cmp.next() {
Some(Component::Normal(x)) => x.to_string_lossy(),
_ => bail!("no or invalid image in path"),
};
let buckets = match self.disk_map.get_mut(req_fidx.as_ref()) {
Some(x) => x,
None => bail!("given image '{}' not found", req_fidx),
};
let bucket_type = match cmp.next() {
Some(Component::Normal(x)) => x.to_string_lossy(),
Some(c) => bail!("invalid bucket in path: {:?}", c),
None => {
// list bucket types available
let mut types = buckets
.iter()
.map(|b| b.type_string())
.collect::<Vec<&'static str>>();
// dedup requires duplicates to be consecutive, which is the case - see scan()
types.dedup();
return Ok(ResolveResult::BucketTypes(types));
}
};
let component = match cmp.next() {
Some(Component::Normal(x)) => x.to_string_lossy(),
Some(c) => bail!("invalid bucket component in path: {:?}", c),
None => {
// list bucket components available
let comps = buckets
.iter()
.filter(|b| b.type_string() == bucket_type)
.map(Bucket::component_string)
.collect();
return Ok(ResolveResult::BucketComponents(comps));
}
};
let mut bucket = match Bucket::filter_mut(buckets, &bucket_type, &component) {
Some(bucket) => bucket,
None => bail!(
"bucket/component path not found: {}/{}/{}",
req_fidx,
bucket_type,
component
),
};
// bucket found, check mount
let mountpoint = self
.filesystems
.ensure_mounted(&mut bucket)
.map_err(|err| {
format_err!(
"mounting '{}/{}/{}' failed: {}",
req_fidx,
bucket_type,
component,
err
)
})?;
let mut local_path = PathBuf::new();
local_path.push(mountpoint);
for rem in cmp {
local_path.push(rem);
}
Ok(ResolveResult::Path(local_path))
}
fn mknod_blk(path: &str, maj: u64, min: u64) -> Result<(), Error> {
use nix::sys::stat;
let dev = stat::makedev(maj, min);
stat::mknod(path, stat::SFlag::S_IFBLK, stat::Mode::S_IRWXU, dev)?;
Ok(())
}
}

View File

@ -0,0 +1,11 @@
///! File restore VM related functionality
mod api;
pub use api::*;
pub mod auth;
mod watchdog;
pub use watchdog::*;
mod disk;
pub use disk::*;

View File

@ -0,0 +1,41 @@
//! Tokio-based watchdog that shuts down the VM if not pinged for TIMEOUT
use std::sync::atomic::{AtomicI64, Ordering};
use proxmox::tools::time::epoch_i64;
const TIMEOUT: i64 = 600; // seconds
static TRIGGERED: AtomicI64 = AtomicI64::new(0);
fn handle_expired() -> ! {
use nix::sys::reboot;
println!("watchdog expired, shutting down");
let err = reboot::reboot(reboot::RebootMode::RB_POWER_OFF).unwrap_err();
println!("'reboot' syscall failed: {}", err);
std::process::exit(1);
}
async fn watchdog_loop() {
use tokio::time::{sleep, Duration};
loop {
let remaining = watchdog_remaining();
if remaining <= 0 {
handle_expired();
}
sleep(Duration::from_secs(remaining as u64)).await;
}
}
/// Initialize watchdog
pub fn watchdog_init() {
watchdog_ping();
tokio::spawn(watchdog_loop());
}
/// Trigger watchdog keepalive
pub fn watchdog_ping() {
TRIGGERED.fetch_max(epoch_i64(), Ordering::AcqRel);
}
/// Returns the remaining time before watchdog expiry in seconds
pub fn watchdog_remaining() -> i64 {
TIMEOUT - (epoch_i64() - TRIGGERED.load(Ordering::Acquire))
}

View File

@ -21,7 +21,7 @@ use proxmox_backup::{
config::drive::{
complete_drive_name,
complete_changer_name,
complete_linux_drive_name,
complete_lto_drive_name,
},
};
@ -33,13 +33,13 @@ pub fn drive_commands() -> CommandLineInterface {
.insert("config",
CliCommand::new(&API_METHOD_GET_CONFIG)
.arg_param(&["name"])
.completion_cb("name", complete_linux_drive_name)
.completion_cb("name", complete_lto_drive_name)
)
.insert(
"remove",
CliCommand::new(&api2::config::drive::API_METHOD_DELETE_DRIVE)
.arg_param(&["name"])
.completion_cb("name", complete_linux_drive_name)
.completion_cb("name", complete_lto_drive_name)
)
.insert(
"create",
@ -53,7 +53,7 @@ pub fn drive_commands() -> CommandLineInterface {
"update",
CliCommand::new(&api2::config::drive::API_METHOD_UPDATE_DRIVE)
.arg_param(&["name"])
.completion_cb("name", complete_linux_drive_name)
.completion_cb("name", complete_lto_drive_name)
.completion_cb("path", complete_drive_path)
.completion_cb("changer", complete_changer_name)
)

View File

@ -1,7 +1,5 @@
/// Tape command implemented using scsi-generic raw commands
///
/// SCSI-generic command needs root privileges, so this binary need
/// to be setuid root.
/// Helper to run tape commands as root. Currently only required
/// to read and set the encryption key.
///
/// This command can use STDIN as tape device handle.
@ -24,41 +22,41 @@ use proxmox_backup::{
config,
backup::Fingerprint,
api2::types::{
LINUX_DRIVE_PATH_SCHEMA,
LTO_DRIVE_PATH_SCHEMA,
DRIVE_NAME_SCHEMA,
TAPE_ENCRYPTION_KEY_FINGERPRINT_SCHEMA,
MEDIA_SET_UUID_SCHEMA,
LinuxTapeDrive,
LtoTapeDrive,
},
tape::{
drive::{
TapeDriver,
LinuxTapeHandle,
open_linux_tape_device,
check_tape_is_linux_tape_device,
LtoTapeHandle,
open_lto_tape_device,
check_tape_is_lto_tape_device,
},
},
};
fn get_tape_handle(param: &Value) -> Result<LinuxTapeHandle, Error> {
fn get_tape_handle(param: &Value) -> Result<LtoTapeHandle, Error> {
let handle = if let Some(name) = param["drive"].as_str() {
let (config, _digest) = config::drive::config()?;
let drive: LinuxTapeDrive = config.lookup("linux", &name)?;
let drive: LtoTapeDrive = config.lookup("lto", &name)?;
eprintln!("using device {}", drive.path);
drive.open()?
} else if let Some(device) = param["device"].as_str() {
eprintln!("using device {}", device);
LinuxTapeHandle::new(open_linux_tape_device(&device)?)
LtoTapeHandle::new(open_lto_tape_device(&device)?)?
} else if let Some(true) = param["stdin"].as_bool() {
eprintln!("using stdin");
let fd = std::io::stdin().as_raw_fd();
let file = unsafe { File::from_raw_fd(fd) };
check_tape_is_linux_tape_device(&file)?;
LinuxTapeHandle::new(file)
check_tape_is_lto_tape_device(&file)?;
LtoTapeHandle::new(file)?
} else if let Ok(name) = std::env::var("PROXMOX_TAPE_DRIVE") {
let (config, _digest) = config::drive::config()?;
let drive: LinuxTapeDrive = config.lookup("linux", &name)?;
let drive: LtoTapeDrive = config.lookup("lto", &name)?;
eprintln!("using device {}", drive.path);
drive.open()?
} else {
@ -66,13 +64,13 @@ fn get_tape_handle(param: &Value) -> Result<LinuxTapeHandle, Error> {
let mut drive_names = Vec::new();
for (name, (section_type, _)) in config.sections.iter() {
if section_type != "linux" { continue; }
if section_type != "lto" { continue; }
drive_names.push(name);
}
if drive_names.len() == 1 {
let name = drive_names[0];
let drive: LinuxTapeDrive = config.lookup("linux", &name)?;
let drive: LtoTapeDrive = config.lookup("lto", &name)?;
eprintln!("using device {}", drive.path);
drive.open()?
} else {
@ -83,111 +81,6 @@ fn get_tape_handle(param: &Value) -> Result<LinuxTapeHandle, Error> {
Ok(handle)
}
#[api(
input: {
properties: {
drive: {
schema: DRIVE_NAME_SCHEMA,
optional: true,
},
device: {
schema: LINUX_DRIVE_PATH_SCHEMA,
optional: true,
},
stdin: {
description: "Use standard input as device handle.",
type: bool,
optional: true,
},
},
},
)]
/// Tape/Media Status
fn status(
param: Value,
) -> Result<(), Error> {
let result = proxmox::try_block!({
let mut handle = get_tape_handle(&param)?;
handle.get_drive_and_media_status()
}).map_err(|err: Error| err.to_string());
println!("{}", serde_json::to_string_pretty(&result)?);
Ok(())
}
#[api(
input: {
properties: {
drive: {
schema: DRIVE_NAME_SCHEMA,
optional: true,
},
device: {
schema: LINUX_DRIVE_PATH_SCHEMA,
optional: true,
},
stdin: {
description: "Use standard input as device handle.",
type: bool,
optional: true,
},
},
},
)]
/// Read Cartridge Memory (Medium auxiliary memory attributes)
fn cartridge_memory(
param: Value,
) -> Result<(), Error> {
let result = proxmox::try_block!({
let mut handle = get_tape_handle(&param)?;
handle.cartridge_memory()
}).map_err(|err| err.to_string());
println!("{}", serde_json::to_string_pretty(&result)?);
Ok(())
}
#[api(
input: {
properties: {
drive: {
schema: DRIVE_NAME_SCHEMA,
optional: true,
},
device: {
schema: LINUX_DRIVE_PATH_SCHEMA,
optional: true,
},
stdin: {
description: "Use standard input as device handle.",
type: bool,
optional: true,
},
},
},
)]
/// Read Tape Alert Flags
fn tape_alert_flags(
param: Value,
) -> Result<(), Error> {
let result = proxmox::try_block!({
let mut handle = get_tape_handle(&param)?;
let flags = handle.tape_alert_flags()?;
Ok(flags.bits())
}).map_err(|err: Error| err.to_string());
println!("{}", serde_json::to_string_pretty(&result)?);
Ok(())
}
#[api(
input: {
properties: {
@ -204,7 +97,7 @@ fn tape_alert_flags(
optional: true,
},
device: {
schema: LINUX_DRIVE_PATH_SCHEMA,
schema: LTO_DRIVE_PATH_SCHEMA,
optional: true,
},
stdin: {
@ -245,40 +138,6 @@ fn set_encryption(
Ok(())
}
#[api(
input: {
properties: {
drive: {
schema: DRIVE_NAME_SCHEMA,
optional: true,
},
device: {
schema: LINUX_DRIVE_PATH_SCHEMA,
optional: true,
},
stdin: {
description: "Use standard input as device handle.",
type: bool,
optional: true,
},
},
},
)]
/// Read volume statistics
fn volume_statistics(
param: Value,
) -> Result<(), Error> {
let result = proxmox::try_block!({
let mut handle = get_tape_handle(&param)?;
handle.volume_statistics()
}).map_err(|err: Error| err.to_string());
println!("{}", serde_json::to_string_pretty(&result)?);
Ok(())
}
fn main() -> Result<(), Error> {
// check if we are user root or backup
@ -300,22 +159,6 @@ fn main() -> Result<(), Error> {
}
let cmd_def = CliCommandMap::new()
.insert(
"status",
CliCommand::new(&API_METHOD_STATUS)
)
.insert(
"cartridge-memory",
CliCommand::new(&API_METHOD_CARTRIDGE_MEMORY)
)
.insert(
"tape-alert-flags",
CliCommand::new(&API_METHOD_TAPE_ALERT_FLAGS)
)
.insert(
"volume-statistics",
CliCommand::new(&API_METHOD_VOLUME_STATISTICS)
)
.insert(
"encryption",
CliCommand::new(&API_METHOD_SET_ENCRYPTION)

View File

@ -10,6 +10,14 @@ macro_rules! PROXMOX_BACKUP_RUN_DIR_M { () => ("/run/proxmox-backup") }
#[macro_export]
macro_rules! PROXMOX_BACKUP_LOG_DIR_M { () => ("/var/log/proxmox-backup") }
#[macro_export]
macro_rules! PROXMOX_BACKUP_CACHE_DIR_M { () => ("/var/cache/proxmox-backup") }
#[macro_export]
macro_rules! PROXMOX_BACKUP_FILE_RESTORE_BIN_DIR_M {
() => ("/usr/lib/x86_64-linux-gnu/proxmox-backup/file-restore")
}
/// namespaced directory for in-memory (tmpfs) run state
pub const PROXMOX_BACKUP_RUN_DIR: &str = PROXMOX_BACKUP_RUN_DIR_M!();
@ -30,6 +38,15 @@ pub const PROXMOX_BACKUP_PROXY_PID_FN: &str = concat!(PROXMOX_BACKUP_RUN_DIR_M!(
/// the PID filename for the privileged api daemon
pub const PROXMOX_BACKUP_API_PID_FN: &str = concat!(PROXMOX_BACKUP_RUN_DIR_M!(), "/api.pid");
/// filename of the cached initramfs to use for booting single file restore VMs, this file is
/// automatically created by APT hooks
pub const PROXMOX_BACKUP_INITRAMFS_FN: &str =
concat!(PROXMOX_BACKUP_CACHE_DIR_M!(), "/file-restore-initramfs.img");
/// filename of the kernel to use for booting single file restore VMs
pub const PROXMOX_BACKUP_KERNEL_FN: &str =
concat!(PROXMOX_BACKUP_FILE_RESTORE_BIN_DIR_M!(), "/bzImage");
/// Prepend configuration directory to a file name
///
/// This is a simply way to get the full path for configuration files.

View File

@ -1,12 +1,12 @@
//! Tape drive/changer configuration
//!
//! This configuration module is based on [`SectionConfig`], and
//! provides a type safe interface to store [`LinuxTapeDrive`],
//! provides a type safe interface to store [`LtoTapeDrive`],
//! [`VirtualTapeDrive`] and [`ScsiTapeChanger`] configurations.
//!
//! Drive type [`VirtualTapeDrive`] is only useful for debugging.
//!
//! [LinuxTapeDrive]: crate::api2::types::LinuxTapeDrive
//! [LtoTapeDrive]: crate::api2::types::LtoTapeDrive
//! [VirtualTapeDrive]: crate::api2::types::VirtualTapeDrive
//! [ScsiTapeChanger]: crate::api2::types::ScsiTapeChanger
//! [SectionConfig]: proxmox::api::section_config::SectionConfig
@ -36,7 +36,7 @@ use crate::{
api2::types::{
DRIVE_NAME_SCHEMA,
VirtualTapeDrive,
LinuxTapeDrive,
LtoTapeDrive,
ScsiTapeChanger,
},
};
@ -57,11 +57,11 @@ fn init() -> SectionConfig {
let plugin = SectionConfigPlugin::new("virtual".to_string(), Some("name".to_string()), obj_schema);
config.register_plugin(plugin);
let obj_schema = match LinuxTapeDrive::API_SCHEMA {
let obj_schema = match LtoTapeDrive::API_SCHEMA {
Schema::Object(ref obj_schema) => obj_schema,
_ => unreachable!(),
};
let plugin = SectionConfigPlugin::new("linux".to_string(), Some("name".to_string()), obj_schema);
let plugin = SectionConfigPlugin::new("lto".to_string(), Some("name".to_string()), obj_schema);
config.register_plugin(plugin);
let obj_schema = match ScsiTapeChanger::API_SCHEMA {
@ -116,7 +116,7 @@ pub fn save_config(config: &SectionConfigData) -> Result<(), Error> {
pub fn check_drive_exists(config: &SectionConfigData, drive: &str) -> Result<(), Error> {
match config.sections.get(drive) {
Some((section_type, _)) => {
if !(section_type == "linux" || section_type == "virtual") {
if !(section_type == "lto" || section_type == "virtual") {
bail!("Entry '{}' exists, but is not a tape drive", drive);
}
}
@ -138,12 +138,12 @@ pub fn complete_drive_name(_arg: &str, _param: &HashMap<String, String>) -> Vec<
}
}
/// List Linux tape drives
pub fn complete_linux_drive_name(_arg: &str, _param: &HashMap<String, String>) -> Vec<String> {
/// List Lto tape drives
pub fn complete_lto_drive_name(_arg: &str, _param: &HashMap<String, String>) -> Vec<String> {
match config() {
Ok((data, _digest)) => data.sections.iter()
.filter(|(_id, (section_type, _))| {
section_type == "linux"
section_type == "lto"
})
.map(|(id, _)| id.to_string())
.collect(),

View File

@ -18,7 +18,7 @@ use webauthn_rs::{proto::UserVerificationPolicy, Webauthn};
use webauthn_rs::proto::Credential as WebauthnCredential;
use proxmox::api::api;
use proxmox::api::schema::{Updatable, Updater};
use proxmox::api::schema::Updater;
use proxmox::sys::error::SysError;
use proxmox::tools::fs::CreateOptions;
use proxmox::tools::tfa::totp::Totp;

View File

@ -16,9 +16,10 @@ use nix::fcntl::OFlag;
use nix::sys::stat::Mode;
use pathpatterns::{MatchEntry, MatchList, MatchType};
use pxar::format::Device;
use pxar::Metadata;
use pxar::accessor::aio::{Accessor, FileContents, FileEntry};
use pxar::decoder::aio::Decoder;
use pxar::format::Device;
use pxar::{Entry, EntryKind, Metadata};
use proxmox::c_result;
use proxmox::tools::{
@ -93,8 +94,6 @@ where
let mut err_path_stack = vec![OsString::from("/")];
let mut current_match = options.extract_match_default;
while let Some(entry) = decoder.next() {
use pxar::EntryKind;
let entry = entry.map_err(|err| format_err!("error reading pxar archive: {}", err))?;
let file_name_os = entry.file_name();
@ -556,7 +555,6 @@ where
T: Clone + pxar::accessor::ReadAt + Unpin + Send + Sync + 'static,
W: tokio::io::AsyncWrite + Unpin + Send + 'static,
{
use pxar::EntryKind;
Box::pin(async move {
let metadata = file.entry().metadata();
let path = file.entry().path().strip_prefix(&prefix)?.to_path_buf();
@ -616,10 +614,42 @@ where
})
}
fn get_extractor<DEST>(destination: DEST, metadata: Metadata) -> Result<Extractor, Error>
where
DEST: AsRef<Path>,
{
create_path(
&destination,
None,
Some(CreateOptions::new().perm(Mode::from_bits_truncate(0o700))),
)
.map_err(|err| {
format_err!(
"error creating directory {:?}: {}",
destination.as_ref(),
err
)
})?;
let dir = Dir::open(
destination.as_ref(),
OFlag::O_DIRECTORY | OFlag::O_CLOEXEC,
Mode::empty(),
)
.map_err(|err| {
format_err!(
"unable to open target directory {:?}: {}",
destination.as_ref(),
err,
)
})?;
Ok(Extractor::new(dir, metadata, false, Flags::DEFAULT))
}
pub async fn extract_sub_dir<T, DEST, PATH>(
destination: DEST,
mut decoder: Accessor<T>,
decoder: Accessor<T>,
path: PATH,
verbose: bool,
) -> Result<(), Error>
@ -630,47 +660,79 @@ where
{
let root = decoder.open_root().await?;
create_path(
&destination,
None,
Some(CreateOptions::new().perm(Mode::from_bits_truncate(0o700))),
)
.map_err(|err| format_err!("error creating directory {:?}: {}", destination.as_ref(), err))?;
let dir = Dir::open(
destination.as_ref(),
OFlag::O_DIRECTORY | OFlag::O_CLOEXEC,
Mode::empty(),
)
.map_err(|err| format_err!("unable to open target directory {:?}: {}", destination.as_ref(), err,))?;
let mut extractor = Extractor::new(
dir,
let mut extractor = get_extractor(
destination,
root.lookup_self().await?.entry().metadata().clone(),
false,
Flags::DEFAULT,
);
)?;
let file = root
.lookup(&path).await?
.lookup(&path)
.await?
.ok_or(format_err!("error opening '{:?}'", path.as_ref()))?;
recurse_files_extractor(&mut extractor, &mut decoder, file, verbose).await
recurse_files_extractor(&mut extractor, file, verbose).await
}
fn recurse_files_extractor<'a, T>(
extractor: &'a mut Extractor,
decoder: &'a mut Accessor<T>,
file: FileEntry<T>,
pub async fn extract_sub_dir_seq<S, DEST>(
destination: DEST,
mut decoder: Decoder<S>,
verbose: bool,
) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'a>>
) -> Result<(), Error>
where
T: Clone + pxar::accessor::ReadAt + Unpin + Send + Sync + 'static,
S: pxar::decoder::SeqRead + Unpin + Send + 'static,
DEST: AsRef<Path>,
{
use pxar::EntryKind;
Box::pin(async move {
let metadata = file.entry().metadata();
let file_name_os = file.file_name();
decoder.enable_goodbye_entries(true);
let root = match decoder.next().await {
Some(Ok(root)) => root,
Some(Err(err)) => bail!("error getting root entry from pxar: {}", err),
None => bail!("cannot extract empty archive"),
};
let mut extractor = get_extractor(destination, root.metadata().clone())?;
if let Err(err) = seq_files_extractor(&mut extractor, decoder, verbose).await {
eprintln!("error extracting pxar archive: {}", err);
}
Ok(())
}
fn extract_special(
extractor: &mut Extractor,
entry: &Entry,
file_name: &CStr,
) -> Result<(), Error> {
let metadata = entry.metadata();
match entry.kind() {
EntryKind::Symlink(link) => {
extractor.extract_symlink(file_name, metadata, link.as_ref())?;
}
EntryKind::Hardlink(link) => {
extractor.extract_hardlink(file_name, link.as_os_str())?;
}
EntryKind::Device(dev) => {
if extractor.contains_flags(Flags::WITH_DEVICE_NODES) {
extractor.extract_device(file_name, metadata, dev)?;
}
}
EntryKind::Fifo => {
if extractor.contains_flags(Flags::WITH_FIFOS) {
extractor.extract_special(file_name, metadata, 0)?;
}
}
EntryKind::Socket => {
if extractor.contains_flags(Flags::WITH_SOCKETS) {
extractor.extract_special(file_name, metadata, 0)?;
}
}
_ => bail!("extract_special used with unsupported entry kind"),
}
Ok(())
}
fn get_filename(entry: &Entry) -> Result<(OsString, CString), Error> {
let file_name_os = entry.file_name().to_owned();
// safety check: a file entry in an archive must never contain slashes:
if file_name_os.as_bytes().contains(&b'/') {
@ -680,6 +742,21 @@ where
let file_name = CString::new(file_name_os.as_bytes())
.map_err(|_| format_err!("encountered file name with null-bytes"))?;
Ok((file_name_os, file_name))
}
async fn recurse_files_extractor<'a, T>(
extractor: &'a mut Extractor,
file: FileEntry<T>,
verbose: bool,
) -> Result<(), Error>
where
T: Clone + pxar::accessor::ReadAt + Unpin + Send + Sync + 'static,
{
let entry = file.entry();
let metadata = entry.metadata();
let (file_name_os, file_name) = get_filename(entry)?;
if verbose {
eprintln!("extracting: {}", file.path().display());
}
@ -691,50 +768,97 @@ where
.map_err(|err| format_err!("error at entry {:?}: {}", file_name_os, err))?;
let dir = file.enter_directory().await?;
let mut readdir = dir.read_dir();
while let Some(entry) = readdir.next().await {
let entry = entry?.decode_entry().await?;
let filename = entry.path().to_path_buf();
// log errors and continue
if let Err(err) = recurse_files_extractor(extractor, decoder, entry, verbose).await {
eprintln!("error extracting {:?}: {}", filename.display(), err);
}
}
let mut seq_decoder = dir.decode_full().await?;
seq_decoder.enable_goodbye_entries(true);
seq_files_extractor(extractor, seq_decoder, verbose).await?;
extractor.leave_directory()?;
}
EntryKind::Symlink(link) => {
extractor.extract_symlink(&file_name, metadata, link.as_ref())?;
}
EntryKind::Hardlink(link) => {
extractor.extract_hardlink(&file_name, link.as_os_str())?;
}
EntryKind::Device(dev) => {
if extractor.contains_flags(Flags::WITH_DEVICE_NODES) {
extractor.extract_device(&file_name, metadata, dev)?;
}
}
EntryKind::Fifo => {
if extractor.contains_flags(Flags::WITH_FIFOS) {
extractor.extract_special(&file_name, metadata, 0)?;
}
}
EntryKind::Socket => {
if extractor.contains_flags(Flags::WITH_SOCKETS) {
extractor.extract_special(&file_name, metadata, 0)?;
}
}
EntryKind::File { size, .. } => extractor.async_extract_file(
EntryKind::File { size, .. } => {
extractor
.async_extract_file(
&file_name,
metadata,
*size,
&mut file.contents().await.map_err(|_| {
format_err!("found regular file entry without contents in archive")
})?,
).await?,
EntryKind::GoodbyeTable => {}, // ignore
)
.await?
}
EntryKind::GoodbyeTable => {} // ignore
_ => extract_special(extractor, entry, &file_name)?,
}
Ok(())
})
}
async fn seq_files_extractor<'a, T>(
extractor: &'a mut Extractor,
mut decoder: pxar::decoder::aio::Decoder<T>,
verbose: bool,
) -> Result<(), Error>
where
T: pxar::decoder::SeqRead,
{
let mut dir_level = 0;
loop {
let entry = match decoder.next().await {
Some(entry) => entry?,
None => return Ok(()),
};
let metadata = entry.metadata();
let (file_name_os, file_name) = get_filename(&entry)?;
if verbose && !matches!(entry.kind(), EntryKind::GoodbyeTable) {
eprintln!("extracting: {}", entry.path().display());
}
if let Err(err) = async {
match entry.kind() {
EntryKind::Directory => {
dir_level += 1;
extractor
.enter_directory(file_name_os.to_owned(), metadata.clone(), true)
.map_err(|err| format_err!("error at entry {:?}: {}", file_name_os, err))?;
}
EntryKind::File { size, .. } => {
extractor
.async_extract_file(
&file_name,
metadata,
*size,
&mut decoder.contents().ok_or_else(|| {
format_err!("found regular file entry without contents in archive")
})?,
)
.await?
}
EntryKind::GoodbyeTable => {
dir_level -= 1;
extractor.leave_directory()?;
}
_ => extract_special(extractor, &entry, &file_name)?,
}
Ok(()) as Result<(), Error>
}
.await
{
let display = entry.path().display().to_string();
eprintln!(
"error extracting {}: {}",
if matches!(entry.kind(), EntryKind::GoodbyeTable) {
"<directory>"
} else {
&display
},
err
);
}
if dir_level < 0 {
// we've encountered one Goodbye more then Directory, meaning we've left the dir we
// started in - exit early, otherwise the extractor might panic
return Ok(());
}
}
}

View File

@ -59,7 +59,10 @@ mod flags;
pub use flags::Flags;
pub use create::{create_archive, PxarCreateOptions};
pub use extract::{create_zip, extract_archive, extract_sub_dir, ErrorHandler, PxarExtractOptions};
pub use extract::{
create_zip, extract_archive, extract_sub_dir, extract_sub_dir_seq, ErrorHandler,
PxarExtractOptions,
};
/// The format requires to build sorted directory lookup tables in
/// memory, so we restrict the number of allowed entries to limit

View File

@ -1,26 +1,54 @@
//! Provides authentication primitives for the HTTP server
use anyhow::{bail, format_err, Error};
use anyhow::{format_err, Error};
use std::sync::Arc;
use crate::tools::ticket::Ticket;
use crate::auth_helpers::*;
use crate::tools;
use crate::config::cached_user_info::CachedUserInfo;
use crate::api2::types::{Authid, Userid};
use crate::auth_helpers::*;
use crate::config::cached_user_info::CachedUserInfo;
use crate::tools;
use crate::tools::ticket::Ticket;
use hyper::header;
use percent_encoding::percent_decode_str;
pub struct UserAuthData {
pub enum AuthError {
Generic(Error),
NoData,
}
impl From<Error> for AuthError {
fn from(err: Error) -> Self {
AuthError::Generic(err)
}
}
pub trait ApiAuth {
fn check_auth(
&self,
headers: &http::HeaderMap,
method: &hyper::Method,
user_info: &CachedUserInfo,
) -> Result<Authid, AuthError>;
}
struct UserAuthData {
ticket: String,
csrf_token: Option<String>,
}
pub enum AuthData {
enum AuthData {
User(UserAuthData),
ApiToken(String),
}
pub fn extract_auth_data(headers: &http::HeaderMap) -> Option<AuthData> {
pub struct UserApiAuth {}
pub fn default_api_auth() -> Arc<UserApiAuth> {
Arc::new(UserApiAuth {})
}
impl UserApiAuth {
fn extract_auth_data(headers: &http::HeaderMap) -> Option<AuthData> {
if let Some(raw_cookie) = headers.get(header::COOKIE) {
if let Ok(cookie) = raw_cookie.to_str() {
if let Some(ticket) = tools::extract_cookie(cookie, "PBSAuthCookie") {
@ -28,10 +56,7 @@ pub fn extract_auth_data(headers: &http::HeaderMap) -> Option<AuthData> {
Some(Ok(v)) => Some(v.to_owned()),
_ => None,
};
return Some(AuthData::User(UserAuthData {
ticket,
csrf_token,
}));
return Some(AuthData::User(UserAuthData { ticket, csrf_token }));
}
}
}
@ -43,18 +68,22 @@ pub fn extract_auth_data(headers: &http::HeaderMap) -> Option<AuthData> {
} else {
None
}
},
}
_ => None,
}
}
}
pub fn check_auth(
impl ApiAuth for UserApiAuth {
fn check_auth(
&self,
headers: &http::HeaderMap,
method: &hyper::Method,
auth_data: &AuthData,
user_info: &CachedUserInfo,
) -> Result<Authid, Error> {
) -> Result<Authid, AuthError> {
let auth_data = Self::extract_auth_data(headers);
match auth_data {
AuthData::User(user_auth_data) => {
Some(AuthData::User(user_auth_data)) => {
let ticket = user_auth_data.ticket.clone();
let ticket_lifetime = tools::ticket::TICKET_LIFETIME;
@ -64,30 +93,38 @@ pub fn check_auth(
let auth_id = Authid::from(userid.clone());
if !user_info.is_active_auth_id(&auth_id) {
bail!("user account disabled or expired.");
return Err(format_err!("user account disabled or expired.").into());
}
if method != hyper::Method::GET {
if let Some(csrf_token) = &user_auth_data.csrf_token {
verify_csrf_prevention_token(csrf_secret(), &userid, &csrf_token, -300, ticket_lifetime)?;
verify_csrf_prevention_token(
csrf_secret(),
&userid,
&csrf_token,
-300,
ticket_lifetime,
)?;
} else {
bail!("missing CSRF prevention token");
return Err(format_err!("missing CSRF prevention token").into());
}
}
Ok(auth_id)
},
AuthData::ApiToken(api_token) => {
}
Some(AuthData::ApiToken(api_token)) => {
let mut parts = api_token.splitn(2, ':');
let tokenid = parts.next()
let tokenid = parts
.next()
.ok_or_else(|| format_err!("failed to split API token header"))?;
let tokenid: Authid = tokenid.parse()?;
if !user_info.is_active_auth_id(&tokenid) {
bail!("user account or token disabled or expired.");
return Err(format_err!("user account or token disabled or expired.").into());
}
let tokensecret = parts.next()
let tokensecret = parts
.next()
.ok_or_else(|| format_err!("failed to split API token header"))?;
let tokensecret = percent_decode_str(tokensecret)
.decode_utf8()
@ -97,5 +134,7 @@ pub fn check_auth(
Ok(tokenid)
}
None => Err(AuthError::NoData),
}
}
}

View File

@ -13,6 +13,7 @@ use proxmox::api::{ApiMethod, Router, RpcEnvironmentType};
use proxmox::tools::fs::{create_path, CreateOptions};
use crate::tools::{FileLogger, FileLogOptions};
use super::auth::ApiAuth;
pub struct ApiConfig {
basedir: PathBuf,
@ -22,12 +23,16 @@ pub struct ApiConfig {
templates: RwLock<Handlebars<'static>>,
template_files: RwLock<HashMap<String, (SystemTime, PathBuf)>>,
request_log: Option<Arc<Mutex<FileLogger>>>,
pub enable_tape_ui: bool,
pub api_auth: Arc<dyn ApiAuth + Send + Sync>,
}
impl ApiConfig {
pub fn new<B: Into<PathBuf>>(basedir: B, router: &'static Router, env_type: RpcEnvironmentType) -> Result<Self, Error> {
pub fn new<B: Into<PathBuf>>(
basedir: B,
router: &'static Router,
env_type: RpcEnvironmentType,
api_auth: Arc<dyn ApiAuth + Send + Sync>,
) -> Result<Self, Error> {
Ok(Self {
basedir: basedir.into(),
router,
@ -36,7 +41,7 @@ impl ApiConfig {
templates: RwLock::new(Handlebars::new()),
template_files: RwLock::new(HashMap::new()),
request_log: None,
enable_tape_ui: false,
api_auth,
})
}

View File

@ -30,15 +30,17 @@ use proxmox::api::{
};
use proxmox::http_err;
use super::auth::AuthError;
use super::environment::RestEnvironment;
use super::formatter::*;
use super::ApiConfig;
use super::auth::{check_auth, extract_auth_data};
use crate::api2::types::{Authid, Userid};
use crate::auth_helpers::*;
use crate::config::cached_user_info::CachedUserInfo;
use crate::tools;
use crate::tools::compression::{CompressionMethod, DeflateEncoder, Level};
use crate::tools::AsyncReaderStream;
use crate::tools::FileLogger;
extern "C" {
@ -50,6 +52,7 @@ pub struct RestServer {
}
const MAX_URI_QUERY_LENGTH: usize = 3072;
const CHUNK_SIZE_LIMIT: u64 = 32 * 1024;
impl RestServer {
pub fn new(api_config: ApiConfig) -> Self {
@ -396,6 +399,7 @@ pub async fn handle_api_request<Env: RpcEnvironment, S: 'static + BuildHasher +
uri_param: HashMap<String, String, S>,
) -> Result<Response<Body>, Error> {
let delay_unauth_time = std::time::Instant::now() + std::time::Duration::from_millis(3000);
let compression = extract_compression_method(&parts.headers);
let result = match info.handler {
ApiHandler::AsyncHttp(handler) => {
@ -416,7 +420,7 @@ pub async fn handle_api_request<Env: RpcEnvironment, S: 'static + BuildHasher +
}
};
let resp = match result {
let mut resp = match result {
Ok(resp) => resp,
Err(err) => {
if let Some(httperr) = err.downcast_ref::<HttpError>() {
@ -428,6 +432,24 @@ pub async fn handle_api_request<Env: RpcEnvironment, S: 'static + BuildHasher +
}
};
let resp = match compression {
Some(CompressionMethod::Deflate) => {
resp.headers_mut().insert(
header::CONTENT_ENCODING,
CompressionMethod::Deflate.content_encoding(),
);
resp.map(|body| {
Body::wrap_stream(DeflateEncoder::with_quality(
body.map_err(|err| {
proxmox::io_format_err!("error during compression: {}", err)
}),
Level::Default,
))
})
}
None => resp,
};
if info.reload_timezone {
unsafe {
tzset();
@ -475,7 +497,6 @@ fn get_index(
"CSRFPreventionToken": csrf_token,
"language": lang,
"debug": debug,
"enableTapeUI": api.enable_tape_ui,
});
let (ct, index) = match api.render_template(template_file, &data) {
@ -524,9 +545,11 @@ fn extension_to_content_type(filename: &Path) -> (&'static str, bool) {
("application/octet-stream", false)
}
async fn simple_static_file_download(filename: PathBuf) -> Result<Response<Body>, Error> {
let (content_type, _nocomp) = extension_to_content_type(&filename);
async fn simple_static_file_download(
filename: PathBuf,
content_type: &'static str,
compression: Option<CompressionMethod>,
) -> Result<Response<Body>, Error> {
use tokio::io::AsyncReadExt;
let mut file = File::open(filename)
@ -534,56 +557,98 @@ async fn simple_static_file_download(filename: PathBuf) -> Result<Response<Body>
.map_err(|err| http_err!(BAD_REQUEST, "File open failed: {}", err))?;
let mut data: Vec<u8> = Vec::new();
let mut response = match compression {
Some(CompressionMethod::Deflate) => {
let mut enc = DeflateEncoder::with_quality(data, Level::Default);
enc.compress_vec(&mut file, CHUNK_SIZE_LIMIT as usize).await?;
let mut response = Response::new(enc.into_inner().into());
response.headers_mut().insert(
header::CONTENT_ENCODING,
CompressionMethod::Deflate.content_encoding(),
);
response
}
None => {
file.read_to_end(&mut data)
.await
.map_err(|err| http_err!(BAD_REQUEST, "File read failed: {}", err))?;
Response::new(data.into())
}
};
let mut response = Response::new(data.into());
response.headers_mut().insert(
header::CONTENT_TYPE,
header::HeaderValue::from_static(content_type),
);
Ok(response)
}
async fn chuncked_static_file_download(filename: PathBuf) -> Result<Response<Body>, Error> {
let (content_type, _nocomp) = extension_to_content_type(&filename);
async fn chuncked_static_file_download(
filename: PathBuf,
content_type: &'static str,
compression: Option<CompressionMethod>,
) -> Result<Response<Body>, Error> {
let mut resp = Response::builder()
.status(StatusCode::OK)
.header(header::CONTENT_TYPE, content_type);
let file = File::open(filename)
.await
.map_err(|err| http_err!(BAD_REQUEST, "File open failed: {}", err))?;
let payload = tokio_util::codec::FramedRead::new(file, tokio_util::codec::BytesCodec::new())
.map_ok(|bytes| bytes.freeze());
let body = Body::wrap_stream(payload);
let body = match compression {
Some(CompressionMethod::Deflate) => {
resp = resp.header(
header::CONTENT_ENCODING,
CompressionMethod::Deflate.content_encoding(),
);
Body::wrap_stream(DeflateEncoder::with_quality(
AsyncReaderStream::new(file),
Level::Default,
))
}
None => Body::wrap_stream(AsyncReaderStream::new(file)),
};
// FIXME: set other headers ?
Ok(Response::builder()
.status(StatusCode::OK)
.header(header::CONTENT_TYPE, content_type)
.body(body)
.unwrap())
Ok(resp.body(body).unwrap())
}
async fn handle_static_file_download(filename: PathBuf) -> Result<Response<Body>, Error> {
async fn handle_static_file_download(
filename: PathBuf,
compression: Option<CompressionMethod>,
) -> Result<Response<Body>, Error> {
let metadata = tokio::fs::metadata(filename.clone())
.map_err(|err| http_err!(BAD_REQUEST, "File access problems: {}", err))
.await?;
if metadata.len() < 1024 * 32 {
simple_static_file_download(filename).await
let (content_type, nocomp) = extension_to_content_type(&filename);
let compression = if nocomp { None } else { compression };
if metadata.len() < CHUNK_SIZE_LIMIT {
simple_static_file_download(filename, content_type, compression).await
} else {
chuncked_static_file_download(filename).await
chuncked_static_file_download(filename, content_type, compression).await
}
}
fn extract_lang_header(headers: &http::HeaderMap) -> Option<String> {
if let Some(raw_cookie) = headers.get("COOKIE") {
if let Ok(cookie) = raw_cookie.to_str() {
if let Some(Ok(cookie)) = headers.get("COOKIE").map(|v| v.to_str()) {
return tools::extract_cookie(cookie, "PBSLangCookie");
}
None
}
// FIXME: support handling multiple compression methods
fn extract_compression_method(headers: &http::HeaderMap) -> Option<CompressionMethod> {
if let Some(Ok(encodings)) = headers.get(header::ACCEPT_ENCODING).map(|v| v.to_str()) {
for encoding in encodings.split(&[',', ' '][..]) {
if let Ok(method) = encoding.parse() {
return Some(method);
}
}
}
None
}
@ -612,6 +677,7 @@ async fn handle_request(
rpcenv.set_client_ip(Some(*peer));
let user_info = CachedUserInfo::new()?;
let auth = &api.api_auth;
let delay_unauth_time = std::time::Instant::now() + std::time::Duration::from_millis(3000);
let access_forbidden_time = std::time::Instant::now() + std::time::Duration::from_millis(500);
@ -637,13 +703,15 @@ async fn handle_request(
}
if auth_required {
let auth_result = match extract_auth_data(&parts.headers) {
Some(auth_data) => check_auth(&method, &auth_data, &user_info),
None => Err(format_err!("no authentication credentials provided.")),
};
match auth_result {
match auth.check_auth(&parts.headers, &method, &user_info) {
Ok(authid) => rpcenv.set_auth_id(Some(authid.to_string())),
Err(err) => {
Err(auth_err) => {
let err = match auth_err {
AuthError::Generic(err) => err,
AuthError::NoData => {
format_err!("no authentication credentials provided.")
}
};
let peer = peer.ip();
auth_logger()?.log(format!(
"authentication failure; rhost={} msg={}",
@ -706,9 +774,9 @@ async fn handle_request(
if comp_len == 0 {
let language = extract_lang_header(&parts.headers);
if let Some(auth_data) = extract_auth_data(&parts.headers) {
match check_auth(&method, &auth_data, &user_info) {
Ok(auth_id) if !auth_id.is_token() => {
match auth.check_auth(&parts.headers, &method, &user_info) {
Ok(auth_id) => {
if !auth_id.is_token() {
let userid = auth_id.user();
let new_csrf_token = assemble_csrf_prevention_token(csrf_secret(), userid);
return Ok(get_index(
@ -719,17 +787,17 @@ async fn handle_request(
parts,
));
}
_ => {
}
Err(AuthError::Generic(_)) => {
tokio::time::sleep_until(Instant::from_std(delay_unauth_time)).await;
}
Err(AuthError::NoData) => {}
}
return Ok(get_index(None, None, language, &api, parts));
}
}
} else {
return Ok(get_index(None, None, language, &api, parts));
}
} else {
let filename = api.find_alias(&components);
return handle_static_file_download(filename).await;
let compression = extract_compression_method(&parts.headers);
return handle_static_file_download(filename, compression).await;
}
}

View File

@ -26,7 +26,7 @@ use proxmox::{
use crate::api2::types::{
SLOT_ARRAY_SCHEMA,
ScsiTapeChanger,
LinuxTapeDrive,
LtoTapeDrive,
};
/// Changer element status.
@ -523,7 +523,7 @@ pub struct MtxMediaChanger {
impl MtxMediaChanger {
pub fn with_drive_config(drive_config: &LinuxTapeDrive) -> Result<Self, Error> {
pub fn with_drive_config(drive_config: &LtoTapeDrive) -> Result<Self, Error> {
let (config, _digest) = crate::config::drive::config()?;
let changer_config: ScsiTapeChanger = match drive_config.changer {
Some(ref changer) => config.lookup("changer", changer)?,

View File

@ -1,153 +0,0 @@
//! Linux Magnetic Tape Driver ioctl definitions
//!
//! from: /usr/include/x86_64-linux-gnu/sys/mtio.h
//!
//! also see: man 4 st
#[repr(C)]
pub struct mtop {
pub mt_op: MTCmd, /* Operations defined below. */
pub mt_count: libc::c_int, /* How many of them. */
}
#[repr(i16)]
#[allow(dead_code)] // do not warn about unused command
pub enum MTCmd {
MTRESET = 0, /* +reset drive in case of problems */
MTFSF = 1, /* forward space over FileMark,
* position at first record of next file
*/
MTBSF = 2, /* backward space FileMark (position before FM) */
MTFSR = 3, /* forward space record */
MTBSR = 4, /* backward space record */
MTWEOF = 5, /* write an end-of-file record (mark) */
MTREW = 6, /* rewind */
MTOFFL = 7, /* rewind and put the drive offline (eject?) */
MTNOP = 8, /* no op, set status only (read with MTIOCGET) */
MTRETEN = 9, /* retension tape */
MTBSFM = 10, /* +backward space FileMark, position at FM */
MTFSFM = 11, /* +forward space FileMark, position at FM */
MTEOM = 12, /* goto end of recorded media (for appending files).
* MTEOM positions after the last FM, ready for
* appending another file.
*/
MTERASE = 13, /* erase tape -- be careful! */
MTRAS1 = 14, /* run self test 1 (nondestructive) */
MTRAS2 = 15, /* run self test 2 (destructive) */
MTRAS3 = 16, /* reserved for self test 3 */
MTSETBLK = 20, /* set block length (SCSI) */
MTSETDENSITY = 21, /* set tape density (SCSI) */
MTSEEK = 22, /* seek to block (Tandberg, etc.) */
MTTELL = 23, /* tell block (Tandberg, etc.) */
MTSETDRVBUFFER = 24,/* set the drive buffering according to SCSI-2 */
/* ordinary buffered operation with code 1 */
MTFSS = 25, /* space forward over setmarks */
MTBSS = 26, /* space backward over setmarks */
MTWSM = 27, /* write setmarks */
MTLOCK = 28, /* lock the drive door */
MTUNLOCK = 29, /* unlock the drive door */
MTLOAD = 30, /* execute the SCSI load command */
MTUNLOAD = 31, /* execute the SCSI unload command */
MTCOMPRESSION = 32, /* control compression with SCSI mode page 15 */
MTSETPART = 33, /* Change the active tape partition */
MTMKPART = 34, /* Format the tape with one or two partitions */
MTWEOFI = 35, /* write an end-of-file record (mark) in immediate mode */
}
//#define MTIOCTOP _IOW('m', 1, struct mtop) /* Do a mag tape op. */
nix::ioctl_write_ptr!(mtioctop, b'm', 1, mtop);
// from: /usr/include/x86_64-linux-gnu/sys/mtio.h
#[derive(Default, Debug)]
#[repr(C)]
pub struct mtget {
pub mt_type: libc::c_long, /* Type of magtape device. */
pub mt_resid: libc::c_long, /* Residual count: (not sure)
number of bytes ignored, or
number of files not skipped, or
number of records not skipped. */
/* The following registers are device dependent. */
pub mt_dsreg: libc::c_long, /* Status register. */
pub mt_gstat: libc::c_long, /* Generic (device independent) status. */
pub mt_erreg: libc::c_long, /* Error register. */
/* The next two fields are not always used. */
pub mt_fileno: i32 , /* Number of current file on tape. */
pub mt_blkno: i32, /* Current block number. */
}
//#define MTIOCGET _IOR('m', 2, struct mtget) /* Get tape status. */
nix::ioctl_read!(mtiocget, b'm', 2, mtget);
#[repr(C)]
#[allow(dead_code)]
pub struct mtpos {
pub mt_blkno: libc::c_long, /* current block number */
}
//#define MTIOCPOS _IOR('m', 3, struct mtpos) /* Get tape position.*/
nix::ioctl_read!(mtiocpos, b'm', 3, mtpos);
pub const MT_ST_BLKSIZE_MASK: libc::c_long = 0x0ffffff;
pub const MT_ST_BLKSIZE_SHIFT: usize = 0;
pub const MT_ST_DENSITY_MASK: libc::c_long = 0xff000000;
pub const MT_ST_DENSITY_SHIFT: usize = 24;
pub const MT_TYPE_ISSCSI1: libc::c_long = 0x71; /* Generic ANSI SCSI-1 tape unit. */
pub const MT_TYPE_ISSCSI2: libc::c_long = 0x72; /* Generic ANSI SCSI-2 tape unit. */
// Generic Mag Tape (device independent) status macros for examining mt_gstat -- HP-UX compatible
// from: /usr/include/x86_64-linux-gnu/sys/mtio.h
bitflags::bitflags!{
pub struct GMTStatusFlags: libc::c_long {
const EOF = 0x80000000;
const BOT = 0x40000000;
const EOT = 0x20000000;
const SM = 0x10000000; /* DDS setmark */
const EOD = 0x08000000; /* DDS EOD */
const WR_PROT = 0x04000000;
const ONLINE = 0x01000000;
const D_6250 = 0x00800000;
const D_1600 = 0x00400000;
const D_800 = 0x00200000;
const DRIVE_OPEN = 0x00040000; /* Door open (no tape). */
const IM_REP_EN = 0x00010000; /* Immediate report mode.*/
const END_OF_STREAM = 0b00000001;
}
}
#[repr(i32)]
#[allow(non_camel_case_types, dead_code)]
pub enum SetDrvBufferCmd {
MT_ST_BOOLEANS = 0x10000000,
MT_ST_SETBOOLEANS = 0x30000000,
MT_ST_CLEARBOOLEANS = 0x40000000,
MT_ST_WRITE_THRESHOLD = 0x20000000,
MT_ST_DEF_BLKSIZE = 0x50000000,
MT_ST_DEF_OPTIONS = 0x60000000,
MT_ST_SET_TIMEOUT = 0x70000000,
MT_ST_SET_LONG_TIMEOUT = 0x70100000,
MT_ST_SET_CLN = 0x80000000u32 as i32,
}
bitflags::bitflags!{
pub struct SetDrvBufferOptions: i32 {
const BUFFER_WRITES = 0x1;
const ASYNC_WRITES = 0x2;
const READ_AHEAD = 0x4;
const DEBUGGING = 0x8;
const TWO_FM = 0x10;
const FAST_MTEOM = 0x20;
const AUTO_LOCK = 0x40;
const DEF_WRITES = 0x80;
const CAN_BSR = 0x100;
const NO_BLKLIMS = 0x200;
const CAN_PARTITIONS = 0x400;
const SCSI2LOGICAL = 0x800;
const SYSV = 0x1000;
const NOWAIT = 0x2000;
const SILI = 0x4000;
}
}

View File

@ -1,768 +0,0 @@
//! Driver for Linux SCSI tapes
use std::fs::{OpenOptions, File};
use std::os::unix::fs::OpenOptionsExt;
use std::os::unix::io::{AsRawFd, FromRawFd, RawFd};
use std::convert::TryFrom;
use anyhow::{bail, format_err, Error};
use nix::fcntl::{fcntl, FcntlArg, OFlag};
use proxmox::sys::error::SysResult;
use proxmox::tools::Uuid;
use crate::{
config,
backup::{
Fingerprint,
KeyConfig,
},
tools::run_command,
api2::types::{
TapeDensity,
MamAttribute,
LinuxDriveAndMediaStatus,
},
tape::{
TapeRead,
TapeWrite,
drive::{
linux_mtio::*,
LinuxTapeDrive,
TapeDriver,
TapeAlertFlags,
Lp17VolumeStatistics,
read_mam_attributes,
mam_extract_media_usage,
read_tape_alert_flags,
read_volume_statistics,
set_encryption,
},
file_formats::{
PROXMOX_TAPE_BLOCK_SIZE,
PROXMOX_BACKUP_MEDIA_SET_LABEL_MAGIC_1_0,
MediaSetLabel,
MediaContentHeader,
BlockedReader,
BlockedWriter,
},
},
};
fn run_sg_tape_cmd(subcmd: &str, args: &[&str], fd: RawFd) -> Result<String, Error> {
let mut command = std::process::Command::new(
"/usr/lib/x86_64-linux-gnu/proxmox-backup/sg-tape-cmd");
command.args(&[subcmd]);
command.args(&["--stdin"]);
command.args(args);
let device_fd = nix::unistd::dup(fd)?;
command.stdin(unsafe { std::process::Stdio::from_raw_fd(device_fd)});
run_command(command, None)
}
/// Linux tape drive status
#[derive(Debug)]
pub struct LinuxDriveStatus {
/// Size 0 is variable block size mode (default)
pub blocksize: u32,
/// Drive status flags
pub status: GMTStatusFlags,
/// Tape densitiy code (if drive media loaded)
pub density: Option<TapeDensity>,
/// Current file position if known (or -1)
pub file_number: Option<u32>,
/// Current block number if known (or -1)
pub block_number: Option<u32>,
}
impl LinuxDriveStatus {
pub fn tape_is_ready(&self) -> bool {
self.status.contains(GMTStatusFlags::ONLINE) &&
!self.status.contains(GMTStatusFlags::DRIVE_OPEN)
}
}
impl LinuxTapeDrive {
/// 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(&self) -> Result<LinuxTapeHandle, Error> {
proxmox::try_block!({
let file = open_linux_tape_device(&self.path)?;
let mut handle = LinuxTapeHandle::new(file);
let mut drive_status = handle.get_drive_status()?;
if !drive_status.tape_is_ready() {
// for autoloader only, try to reload ejected tapes
if self.changer.is_some() {
let _ = handle.mtload(); // just try, ignore error
drive_status = handle.get_drive_status()?;
}
}
if !drive_status.tape_is_ready() {
bail!("tape not ready (no tape loaded)");
}
if drive_status.blocksize == 0 {
// device is variable block size - OK
} else if drive_status.blocksize != PROXMOX_TAPE_BLOCK_SIZE as u32 {
eprintln!("device is in fixed block size mode with wrong size ({} bytes)", drive_status.blocksize);
eprintln!("trying to set variable block size mode...");
if handle.set_block_size(0).is_err() {
bail!("set variable block size mod failed - device uses wrong blocksize.");
}
} else {
// device is in fixed block size mode with correct block size
}
// Only root can set driver options, so we cannot
// handle.set_default_options()?;
Ok(handle)
}).map_err(|err| format_err!("open drive '{}' ({}) failed - {}", self.name, self.path, err))
}
}
/// Linux Tape device handle
pub struct LinuxTapeHandle {
file: File,
//_lock: File,
}
impl LinuxTapeHandle {
/// Creates a new instance
pub fn new(file: File) -> Self {
Self { file }
}
/// Set all options we need/want
pub fn set_default_options(&self) -> Result<(), Error> {
let mut opts = SetDrvBufferOptions::empty();
// fixme: ? man st(4) claims we need to clear this for reliable multivolume
opts.set(SetDrvBufferOptions::BUFFER_WRITES, true);
// fixme: ?man st(4) claims we need to clear this for reliable multivolume
opts.set(SetDrvBufferOptions::ASYNC_WRITES, true);
opts.set(SetDrvBufferOptions::READ_AHEAD, true);
self.set_drive_buffer_options(opts)
}
/// call MTSETDRVBUFFER to set boolean options
///
/// Note: this uses MT_ST_BOOLEANS, so missing options are cleared!
pub fn set_drive_buffer_options(&self, opts: SetDrvBufferOptions) -> Result<(), Error> {
let cmd = mtop {
mt_op: MTCmd::MTSETDRVBUFFER,
mt_count: (SetDrvBufferCmd::MT_ST_BOOLEANS as i32) | opts.bits(),
};
unsafe {
mtioctop(self.file.as_raw_fd(), &cmd)
}.map_err(|err| format_err!("MTSETDRVBUFFER options failed - {}", err))?;
Ok(())
}
/// call MTSETDRVBUFFER to set boolean options
///
/// Note: this uses MT_ST_SETBOOLEANS
pub fn drive_buffer_set_options(&self, opts: SetDrvBufferOptions) -> Result<(), Error> {
let cmd = mtop {
mt_op: MTCmd::MTSETDRVBUFFER,
mt_count: (SetDrvBufferCmd::MT_ST_SETBOOLEANS as i32) | opts.bits(),
};
unsafe {
mtioctop(self.file.as_raw_fd(), &cmd)
}.map_err(|err| format_err!("MTSETDRVBUFFER options failed - {}", err))?;
Ok(())
}
/// call MTSETDRVBUFFER to clear boolean options
pub fn drive_buffer_clear_options(&self, opts: SetDrvBufferOptions) -> Result<(), Error> {
let cmd = mtop {
mt_op: MTCmd::MTSETDRVBUFFER,
mt_count: (SetDrvBufferCmd::MT_ST_CLEARBOOLEANS as i32) | opts.bits(),
};
unsafe {
mtioctop(self.file.as_raw_fd(), &cmd)
}.map_err(|err| format_err!("MTSETDRVBUFFER options failed - {}", err))?;
Ok(())
}
/// This flushes the driver's buffer as a side effect. Should be
/// used before reading status with MTIOCGET.
fn mtnop(&self) -> Result<(), Error> {
let cmd = mtop { mt_op: MTCmd::MTNOP, mt_count: 1, };
unsafe {
mtioctop(self.file.as_raw_fd(), &cmd)
}.map_err(|err| format_err!("MTNOP failed - {}", err))?;
Ok(())
}
pub fn mtop(&mut self, mt_op: MTCmd, mt_count: i32, msg: &str) -> Result<(), Error> {
let cmd = mtop { mt_op, mt_count };
unsafe {
mtioctop(self.file.as_raw_fd(), &cmd)
}.map_err(|err| format_err!("{} failed (count {}) - {}", msg, mt_count, err))?;
Ok(())
}
pub fn mtload(&mut self) -> Result<(), Error> {
let cmd = mtop { mt_op: MTCmd::MTLOAD, mt_count: 1, };
unsafe {
mtioctop(self.file.as_raw_fd(), &cmd)
}.map_err(|err| format_err!("MTLOAD failed - {}", err))?;
Ok(())
}
/// Set tape compression feature
pub fn set_compression(&self, on: bool) -> Result<(), Error> {
let cmd = mtop { mt_op: MTCmd::MTCOMPRESSION, mt_count: if on { 1 } else { 0 } };
unsafe {
mtioctop(self.file.as_raw_fd(), &cmd)
}.map_err(|err| format_err!("set compression to {} failed - {}", on, err))?;
Ok(())
}
/// Write a single EOF mark
pub fn write_eof_mark(&self) -> Result<(), Error> {
tape_write_eof_mark(&self.file)?;
Ok(())
}
/// Set the drive's block length to the value specified.
///
/// A block length of zero sets the drive to variable block
/// size mode.
pub fn set_block_size(&self, block_length: usize) -> Result<(), Error> {
if block_length > 256*1024*1024 {
bail!("block_length too large (> max linux scsii block length)");
}
let cmd = mtop { mt_op: MTCmd::MTSETBLK, mt_count: block_length as i32 };
unsafe {
mtioctop(self.file.as_raw_fd(), &cmd)
}.map_err(|err| format_err!("MTSETBLK failed - {}", err))?;
Ok(())
}
/// Get Tape and Media status
pub fn get_drive_and_media_status(&mut self) -> Result<LinuxDriveAndMediaStatus, Error> {
let drive_status = self.get_drive_status()?;
let options = read_tapedev_options(&self.file)?;
let alert_flags = self.tape_alert_flags()
.map(|flags| format!("{:?}", flags))
.ok();
let mut status = LinuxDriveAndMediaStatus {
blocksize: drive_status.blocksize,
density: drive_status.density,
status: format!("{:?}", drive_status.status),
options: format!("{:?}", options),
alert_flags,
file_number: drive_status.file_number,
block_number: drive_status.block_number,
manufactured: None,
bytes_read: None,
bytes_written: None,
medium_passes: None,
medium_wearout: None,
volume_mounts: None,
};
if drive_status.tape_is_ready() {
if let Ok(mam) = self.cartridge_memory() {
let usage = mam_extract_media_usage(&mam)?;
status.manufactured = Some(usage.manufactured);
status.bytes_read = Some(usage.bytes_read);
status.bytes_written = Some(usage.bytes_written);
if let Ok(volume_stats) = self.volume_statistics() {
let passes = std::cmp::max(
volume_stats.beginning_of_medium_passes,
volume_stats.middle_of_tape_passes,
);
// assume max. 16000 medium passes
// see: https://en.wikipedia.org/wiki/Linear_Tape-Open
let wearout: f64 = (passes as f64)/(16000.0 as f64);
status.medium_passes = Some(passes);
status.medium_wearout = Some(wearout);
status.volume_mounts = Some(volume_stats.volume_mounts);
}
}
}
Ok(status)
}
/// Get Tape status/configuration with MTIOCGET ioctl
pub fn get_drive_status(&mut self) -> Result<LinuxDriveStatus, Error> {
let _ = self.mtnop(); // ignore errors (i.e. no tape loaded)
let mut status = mtget::default();
if let Err(err) = unsafe { mtiocget(self.file.as_raw_fd(), &mut status) } {
bail!("MTIOCGET failed - {}", err);
}
let gmt = GMTStatusFlags::from_bits_truncate(status.mt_gstat);
let blocksize;
if status.mt_type == MT_TYPE_ISSCSI1 || status.mt_type == MT_TYPE_ISSCSI2 {
blocksize = ((status.mt_dsreg & MT_ST_BLKSIZE_MASK) >> MT_ST_BLKSIZE_SHIFT) as u32;
} else {
bail!("got unsupported tape type {}", status.mt_type);
}
let density = ((status.mt_dsreg & MT_ST_DENSITY_MASK) >> MT_ST_DENSITY_SHIFT) as u8;
Ok(LinuxDriveStatus {
blocksize,
status: gmt,
density: if density != 0 {
Some(TapeDensity::try_from(density)?)
} else {
None
},
file_number: if status.mt_fileno > 0 {
Some(status.mt_fileno as u32)
} else {
None
},
block_number: if status.mt_blkno > 0 {
Some(status.mt_blkno as u32)
} else {
None
},
})
}
/// Read Cartridge Memory (MAM Attributes)
///
/// Note: Only 'root' user may run RAW SG commands, so we need to
/// spawn setuid binary 'sg-tape-cmd'.
pub fn cartridge_memory(&mut self) -> Result<Vec<MamAttribute>, Error> {
if nix::unistd::Uid::effective().is_root() {
return read_mam_attributes(&mut self.file);
}
let output = run_sg_tape_cmd("cartridge-memory", &[], self.file.as_raw_fd())?;
let result: Result<Vec<MamAttribute>, String> = serde_json::from_str(&output)?;
result.map_err(|err| format_err!("{}", err))
}
/// Read Volume Statistics
///
/// Note: Only 'root' user may run RAW SG commands, so we need to
/// spawn setuid binary 'sg-tape-cmd'.
pub fn volume_statistics(&mut self) -> Result<Lp17VolumeStatistics, Error> {
if nix::unistd::Uid::effective().is_root() {
return read_volume_statistics(&mut self.file);
}
let output = run_sg_tape_cmd("volume-statistics", &[], self.file.as_raw_fd())?;
let result: Result<Lp17VolumeStatistics, String> = serde_json::from_str(&output)?;
result.map_err(|err| format_err!("{}", err))
}
}
impl TapeDriver for LinuxTapeHandle {
fn sync(&mut self) -> Result<(), Error> {
// MTWEOF with count 0 => flush
let cmd = mtop { mt_op: MTCmd::MTWEOF, mt_count: 0 };
unsafe {
mtioctop(self.file.as_raw_fd(), &cmd)
}.map_err(|err| proxmox::io_format_err!("MT sync failed - {}", err))?;
Ok(())
}
/// Go to the end of the recorded media (for appending files).
fn move_to_eom(&mut self) -> Result<(), Error> {
let cmd = mtop { mt_op: MTCmd::MTEOM, mt_count: 1, };
unsafe {
mtioctop(self.file.as_raw_fd(), &cmd)
}.map_err(|err| format_err!("MTEOM failed - {}", err))?;
Ok(())
}
fn forward_space_count_files(&mut self, count: usize) -> Result<(), Error> {
let cmd = mtop { mt_op: MTCmd::MTFSF, mt_count: i32::try_from(count)? };
unsafe {
mtioctop(self.file.as_raw_fd(), &cmd)
}.map_err(|err| {
format_err!("forward space {} files failed - {}", count, err)
})?;
Ok(())
}
fn backward_space_count_files(&mut self, count: usize) -> Result<(), Error> {
let cmd = mtop { mt_op: MTCmd::MTBSF, mt_count: i32::try_from(count)? };
unsafe {
mtioctop(self.file.as_raw_fd(), &cmd)
}.map_err(|err| {
format_err!("backward space {} files failed - {}", count, err)
})?;
Ok(())
}
fn rewind(&mut self) -> Result<(), Error> {
let cmd = mtop { mt_op: MTCmd::MTREW, mt_count: 1, };
unsafe {
mtioctop(self.file.as_raw_fd(), &cmd)
}.map_err(|err| format_err!("tape rewind failed - {}", err))?;
Ok(())
}
fn current_file_number(&mut self) -> Result<u64, Error> {
let mut status = mtget::default();
self.mtnop()?;
if let Err(err) = unsafe { mtiocget(self.file.as_raw_fd(), &mut status) } {
bail!("current_file_number MTIOCGET failed - {}", err);
}
if status.mt_fileno < 0 {
bail!("current_file_number failed (got {})", status.mt_fileno);
}
Ok(status.mt_fileno as u64)
}
fn erase_media(&mut self, fast: bool) -> Result<(), Error> {
self.rewind()?; // important - erase from BOT
let cmd = mtop { mt_op: MTCmd::MTERASE, mt_count: if fast { 0 } else { 1 } };
unsafe {
mtioctop(self.file.as_raw_fd(), &cmd)
}.map_err(|err| format_err!("MTERASE failed - {}", err))?;
Ok(())
}
fn read_next_file<'a>(&'a mut self) -> Result<Option<Box<dyn TapeRead + 'a>>, std::io::Error> {
match BlockedReader::open(&mut self.file)? {
Some(reader) => Ok(Some(Box::new(reader))),
None => Ok(None),
}
}
fn write_file<'a>(&'a mut self) -> Result<Box<dyn TapeWrite + 'a>, std::io::Error> {
let handle = TapeWriterHandle {
writer: BlockedWriter::new(&mut self.file),
};
Ok(Box::new(handle))
}
fn write_media_set_label(
&mut self,
media_set_label: &MediaSetLabel,
key_config: Option<&KeyConfig>,
) -> Result<(), Error> {
let file_number = self.current_file_number()?;
if file_number != 1 {
self.rewind()?;
self.forward_space_count_files(1)?; // skip label
}
let file_number = self.current_file_number()?;
if file_number != 1 {
bail!("write_media_set_label failed - got wrong file number ({} != 1)", file_number);
}
self.set_encryption(None)?;
let mut handle = TapeWriterHandle {
writer: BlockedWriter::new(&mut self.file),
};
let mut value = serde_json::to_value(media_set_label)?;
if media_set_label.encryption_key_fingerprint.is_some() {
match key_config {
Some(key_config) => {
value["key-config"] = serde_json::to_value(key_config)?;
}
None => {
bail!("missing encryption key config");
}
}
}
let raw = serde_json::to_string_pretty(&value)?;
let header = MediaContentHeader::new(PROXMOX_BACKUP_MEDIA_SET_LABEL_MAGIC_1_0, raw.len() as u32);
handle.write_header(&header, raw.as_bytes())?;
handle.finish(false)?;
self.sync()?; // sync data to tape
Ok(())
}
/// Rewind and put the drive off line (Eject media).
fn eject_media(&mut self) -> Result<(), Error> {
let cmd = mtop { mt_op: MTCmd::MTOFFL, mt_count: 1 };
unsafe {
mtioctop(self.file.as_raw_fd(), &cmd)
}.map_err(|err| format_err!("MTOFFL failed - {}", err))?;
Ok(())
}
/// Read Tape Alert Flags
///
/// Note: Only 'root' user may run RAW SG commands, so we need to
/// spawn setuid binary 'sg-tape-cmd'.
fn tape_alert_flags(&mut self) -> Result<TapeAlertFlags, Error> {
if nix::unistd::Uid::effective().is_root() {
return read_tape_alert_flags(&mut self.file);
}
let output = run_sg_tape_cmd("tape-alert-flags", &[], self.file.as_raw_fd())?;
let result: Result<u64, String> = serde_json::from_str(&output)?;
result
.map_err(|err| format_err!("{}", err))
.map(TapeAlertFlags::from_bits_truncate)
}
/// Set or clear encryption key
///
/// Note: Only 'root' user may run RAW SG commands, so we need to
/// spawn setuid binary 'sg-tape-cmd'. Also, encryption key file
/// is only readable by root.
fn set_encryption(
&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) = config::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().clone();
openssl::pkcs5::pbkdf2_hmac(
&item.key,
&uuid_bytes,
10,
openssl::hash::MessageDigest::sha256(),
&mut tape_key)?;
return set_encryption(&mut self.file, Some(tape_key));
}
None => bail!("unknown tape encryption key '{}'", key_fingerprint),
}
} else {
return set_encryption(&mut self.file, None);
}
}
let output = if let Some((fingerprint, uuid)) = key_fingerprint {
let fingerprint = crate::tools::format::as_fingerprint(fingerprint.bytes());
run_sg_tape_cmd("encryption", &[
"--fingerprint", &fingerprint,
"--uuid", &uuid.to_string(),
], self.file.as_raw_fd())?
} else {
run_sg_tape_cmd("encryption", &[], self.file.as_raw_fd())?
};
let result: Result<(), String> = serde_json::from_str(&output)?;
result.map_err(|err| format_err!("{}", err))
}
}
/// Write a single EOF mark without flushing buffers
fn tape_write_eof_mark(file: &File) -> Result<(), std::io::Error> {
let cmd = mtop { mt_op: MTCmd::MTWEOFI, mt_count: 1 };
unsafe {
mtioctop(file.as_raw_fd(), &cmd)
}.map_err(|err| proxmox::io_format_err!("MTWEOFI failed - {}", err))?;
Ok(())
}
/// Check for correct Major/Minor numbers
pub fn check_tape_is_linux_tape_device(file: &File) -> Result<(), Error> {
let stat = nix::sys::stat::fstat(file.as_raw_fd())?;
let devnum = stat.st_rdev;
let major = unsafe { libc::major(devnum) };
let minor = unsafe { libc::minor(devnum) };
if major != 9 {
bail!("not a tape device");
}
if (minor & 128) == 0 {
bail!("Detected rewinding tape. Please use non-rewinding tape devices (/dev/nstX).");
}
Ok(())
}
/// Opens a Linux tape device
///
/// The open call use O_NONBLOCK, but that flag is cleard after open
/// succeeded. This also checks if the device is a non-rewinding tape
/// device.
pub fn open_linux_tape_device(
path: &str,
) -> Result<File, Error> {
let file = OpenOptions::new()
.read(true)
.write(true)
.custom_flags(libc::O_NONBLOCK)
.open(path)?;
// clear O_NONBLOCK from now on.
let flags = fcntl(file.as_raw_fd(), FcntlArg::F_GETFL)
.into_io_result()?;
let mut flags = OFlag::from_bits_truncate(flags);
flags.remove(OFlag::O_NONBLOCK);
fcntl(file.as_raw_fd(), FcntlArg::F_SETFL(flags))
.into_io_result()?;
check_tape_is_linux_tape_device(&file)
.map_err(|err| format_err!("device type check {:?} failed - {}", path, err))?;
Ok(file)
}
/// Read Linux tape device options from /sys
pub fn read_tapedev_options(file: &File) -> Result<SetDrvBufferOptions, Error> {
let stat = nix::sys::stat::fstat(file.as_raw_fd())?;
let devnum = stat.st_rdev;
let major = unsafe { libc::major(devnum) };
let minor = unsafe { libc::minor(devnum) };
let path = format!("/sys/dev/char/{}:{}/options", major, minor);
let options = proxmox::tools::fs::file_read_firstline(&path)?;
let options = options.trim();
let options = match options.strip_prefix("0x") {
Some(rest) => rest,
None => bail!("unable to parse '{}'", path),
};
let options = i32::from_str_radix(&options, 16)?;
Ok(SetDrvBufferOptions::from_bits_truncate(options))
}
/// like BlockedWriter, but writes EOF mark on finish
pub struct TapeWriterHandle<'a> {
writer: BlockedWriter<&'a mut File>,
}
impl TapeWrite for TapeWriterHandle<'_> {
fn write_all(&mut self, data: &[u8]) -> Result<bool, std::io::Error> {
self.writer.write_all(data)
}
fn bytes_written(&self) -> usize {
self.writer.bytes_written()
}
fn finish(&mut self, incomplete: bool) -> Result<bool, std::io::Error> {
let leof = self.writer.finish(incomplete)?;
tape_write_eof_mark(self.writer.writer_ref_mut())?;
Ok(leof)
}
fn logical_end_of_media(&self) -> bool {
self.writer.logical_end_of_media()
}
}

506
src/tape/drive/lto/mod.rs Normal file
View File

@ -0,0 +1,506 @@
//! Driver for LTO SCSI tapes
//!
//! This is a userspace drive implementation using SG_IO.
//!
//! Why we do not use the Linux tape driver:
//!
//! - missing features (MAM, Encryption, ...)
//!
//! - strange permission handling - only root (or CAP_SYS_RAWIO) can
//! do SG_IO (SYS_RAW_IO)
//!
//! - unability to detect EOT (you just get EIO)
mod sg_tape;
pub use sg_tape::*;
use std::fs::{OpenOptions, File};
use std::os::unix::fs::OpenOptionsExt;
use std::os::unix::io::{AsRawFd, FromRawFd, RawFd};
use std::convert::TryInto;
use anyhow::{bail, format_err, Error};
use nix::fcntl::{fcntl, FcntlArg, OFlag};
use proxmox::{
tools::Uuid,
sys::error::SysResult,
};
use crate::{
config,
tools::run_command,
backup::{
Fingerprint,
KeyConfig,
},
api2::types::{
MamAttribute,
LtoDriveAndMediaStatus,
LtoTapeDrive,
Lp17VolumeStatistics,
},
tape::{
TapeRead,
TapeWrite,
BlockReadError,
drive::{
TapeDriver,
},
file_formats::{
PROXMOX_BACKUP_MEDIA_SET_LABEL_MAGIC_1_0,
MediaSetLabel,
MediaContentHeader,
},
},
};
impl LtoTapeDrive {
/// 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(&self) -> Result<LtoTapeHandle, Error> {
proxmox::try_block!({
let file = open_lto_tape_device(&self.path)?;
let mut handle = LtoTapeHandle::new(file)?;
if !handle.sg_tape.test_unit_ready().is_ok() {
// for autoloader only, try to reload ejected tapes
if self.changer.is_some() {
let _ = handle.sg_tape.load(); // just try, ignore error
}
}
handle.sg_tape.wait_until_ready()?;
handle.set_default_options()?;
Ok(handle)
}).map_err(|err: Error| format_err!("open drive '{}' ({}) failed - {}", self.name, self.path, err))
}
}
/// Lto Tape device handle
pub struct LtoTapeHandle {
sg_tape: SgTape,
}
impl LtoTapeHandle {
/// Creates a new instance
pub fn new(file: File) -> Result<Self, Error> {
let sg_tape = SgTape::new(file)?;
Ok(Self { sg_tape })
}
/// Set all options we need/want
pub fn set_default_options(&mut self) -> Result<(), Error> {
let compression = Some(true);
let block_length = Some(0); // variable length mode
let buffer_mode = Some(true); // Always use drive buffer
self.set_drive_options(compression, block_length, buffer_mode)?;
Ok(())
}
/// 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)
}
/// 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)
}
/// Get Tape and Media status
pub fn get_drive_and_media_status(&mut self) -> Result<LtoDriveAndMediaStatus, Error> {
let drive_status = self.sg_tape.read_drive_status()?;
let alert_flags = self.tape_alert_flags()
.map(|flags| format!("{:?}", flags))
.ok();
let mut status = LtoDriveAndMediaStatus {
vendor: self.sg_tape.info().vendor.clone(),
product: self.sg_tape.info().product.clone(),
revision: self.sg_tape.info().revision.clone(),
blocksize: drive_status.block_length,
compression: drive_status.compression,
buffer_mode: drive_status.buffer_mode,
density: drive_status.density_code.try_into()?,
alert_flags,
write_protect: None,
file_number: None,
block_number: None,
manufactured: None,
bytes_read: None,
bytes_written: None,
medium_passes: None,
medium_wearout: None,
volume_mounts: None,
};
if self.sg_tape.test_unit_ready().is_ok() {
if drive_status.write_protect {
status.write_protect = Some(drive_status.write_protect);
}
let position = self.sg_tape.position()?;
status.file_number = Some(position.logical_file_id);
status.block_number = Some(position.logical_object_number);
if let Ok(mam) = self.cartridge_memory() {
let usage = mam_extract_media_usage(&mam)?;
status.manufactured = Some(usage.manufactured);
status.bytes_read = Some(usage.bytes_read);
status.bytes_written = Some(usage.bytes_written);
if let Ok(volume_stats) = self.volume_statistics() {
let passes = std::cmp::max(
volume_stats.beginning_of_medium_passes,
volume_stats.middle_of_tape_passes,
);
// assume max. 16000 medium passes
// see: https://en.wikipedia.org/wiki/Linear_Tape-Open
let wearout: f64 = (passes as f64)/(16000.0 as f64);
status.medium_passes = Some(passes);
status.medium_wearout = Some(wearout);
status.volume_mounts = Some(volume_stats.volume_mounts);
}
}
}
Ok(status)
}
pub fn forward_space_count_files(&mut self, count: usize) -> Result<(), Error> {
self.sg_tape.space_filemarks(count.try_into()?)
}
pub fn backward_space_count_files(&mut self, count: usize) -> Result<(), Error> {
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.
///
/// Note: we dont use LOCATE(10), because that needs LTO5
pub fn locate_file(&mut self, position: u64) -> Result<(), Error> {
if position == 0 {
return self.rewind();
}
let current_position = self.current_file_number()?;
if current_position == position {
// make sure we are immediated afer the filemark
self.sg_tape.space_filemarks(-1)?;
self.sg_tape.space_filemarks(1)?;
} else if current_position < position {
let diff = position - current_position;
self.sg_tape.space_filemarks(diff.try_into()?)?;
} else {
let diff = current_position - position + 1;
self.sg_tape.space_filemarks(-diff.try_into()?)?;
// move to EOT side of filemark
self.sg_tape.space_filemarks(1)?;
}
Ok(())
}
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()
}
/// Read Volume Statistics
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 {
fn sync(&mut self) -> Result<(), Error> {
self.sg_tape.sync()?;
Ok(())
}
/// Go to the end of the recorded media (for appending files).
fn move_to_eom(&mut self, write_missing_eof: bool) -> Result<(), Error> {
self.sg_tape.move_to_eom(write_missing_eof)
}
fn move_to_last_file(&mut self) -> Result<(), Error> {
self.move_to_eom(false)?;
self.sg_tape.check_filemark()?;
let pos = self.current_file_number()?;
if pos == 0 {
bail!("move_to_last_file failed - media contains no data");
}
if pos == 1 {
self.rewind()?;
return Ok(());
}
self.backward_space_count_files(2)?;
self.forward_space_count_files(1)?;
Ok(())
}
fn rewind(&mut self) -> Result<(), Error> {
self.sg_tape.rewind()
}
fn current_file_number(&mut self) -> Result<u64, Error> {
self.sg_tape.current_file_number()
}
fn format_media(&mut self, fast: bool) -> Result<(), Error> {
self.sg_tape.format_media(fast)
}
fn read_next_file<'a>(&'a mut self) -> Result<Box<dyn TapeRead + 'a>, BlockReadError> {
let reader = self.sg_tape.open_reader()?;
let handle: Box<dyn TapeRead> = Box::new(reader);
Ok(handle)
}
fn write_file<'a>(&'a mut self) -> Result<Box<dyn TapeWrite + 'a>, std::io::Error> {
let handle = self.sg_tape.open_writer();
Ok(Box::new(handle))
}
fn write_media_set_label(
&mut self,
media_set_label: &MediaSetLabel,
key_config: Option<&KeyConfig>,
) -> Result<(), Error> {
let file_number = self.current_file_number()?;
if file_number != 1 {
self.rewind()?;
self.forward_space_count_files(1)?; // skip label
}
let file_number = self.current_file_number()?;
if file_number != 1 {
bail!("write_media_set_label failed - got wrong file number ({} != 1)", file_number);
}
self.set_encryption(None)?;
{ // limit handle scope
let mut handle = self.write_file()?;
let mut value = serde_json::to_value(media_set_label)?;
if media_set_label.encryption_key_fingerprint.is_some() {
match key_config {
Some(key_config) => {
value["key-config"] = serde_json::to_value(key_config)?;
}
None => {
bail!("missing encryption key config");
}
}
}
let raw = serde_json::to_string_pretty(&value)?;
let header = MediaContentHeader::new(PROXMOX_BACKUP_MEDIA_SET_LABEL_MAGIC_1_0, raw.len() as u32);
handle.write_header(&header, raw.as_bytes())?;
handle.finish(false)?;
}
self.sync()?; // sync data to tape
Ok(())
}
/// Rewind and put the drive off line (Eject media).
fn eject_media(&mut self) -> Result<(), Error> {
self.sg_tape.eject()
}
/// Read Tape Alert Flags
fn tape_alert_flags(&mut self) -> Result<TapeAlertFlags, Error> {
self.sg_tape.tape_alert_flags()
}
/// Set or clear encryption key
///
/// Note: Only 'root' can read secret encryption keys, so we need
/// to spawn setuid binary 'sg-tape-cmd'.
fn set_encryption(
&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) = config::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().clone();
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 {
let fingerprint = crate::tools::format::as_fingerprint(fingerprint.bytes());
run_sg_tape_cmd("encryption", &[
"--fingerprint", &fingerprint,
"--uuid", &uuid.to_string(),
], self.sg_tape.file_mut().as_raw_fd())?
} 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))
}
}
/// Check for correct Major/Minor numbers
pub fn check_tape_is_lto_tape_device(file: &File) -> Result<(), Error> {
let stat = nix::sys::stat::fstat(file.as_raw_fd())?;
let devnum = stat.st_rdev;
let major = unsafe { libc::major(devnum) };
let _minor = unsafe { libc::minor(devnum) };
if major == 9 {
bail!("not a scsi-generic tape device (cannot use linux tape devices)");
}
if major != 21 {
bail!("not a scsi-generic tape device");
}
Ok(())
}
/// Opens a Lto tape device
///
/// The open call use O_NONBLOCK, but that flag is cleard after open
/// succeeded. This also checks if the device is a non-rewinding tape
/// device.
pub fn open_lto_tape_device(
path: &str,
) -> Result<File, Error> {
let file = OpenOptions::new()
.read(true)
.write(true)
.custom_flags(libc::O_NONBLOCK)
.open(path)?;
// clear O_NONBLOCK from now on.
let flags = fcntl(file.as_raw_fd(), FcntlArg::F_GETFL)
.into_io_result()?;
let mut flags = OFlag::from_bits_truncate(flags);
flags.remove(OFlag::O_NONBLOCK);
fcntl(file.as_raw_fd(), FcntlArg::F_SETFL(flags))
.into_io_result()?;
check_tape_is_lto_tape_device(&file)
.map_err(|err| format_err!("device type check {:?} failed - {}", path, err))?;
Ok(file)
}
fn run_sg_tape_cmd(subcmd: &str, args: &[&str], fd: RawFd) -> Result<String, Error> {
let mut command = std::process::Command::new(
"/usr/lib/x86_64-linux-gnu/proxmox-backup/sg-tape-cmd");
command.args(&[subcmd]);
command.args(&["--stdin"]);
command.args(args);
let device_fd = nix::unistd::dup(fd)?;
command.stdin(unsafe { std::process::Stdio::from_raw_fd(device_fd)});
run_command(command, None)
}

View File

@ -0,0 +1,745 @@
use std::time::SystemTime;
use std::fs::{File, OpenOptions};
use std::os::unix::fs::OpenOptionsExt;
use std::os::unix::io::AsRawFd;
use std::path::Path;
use anyhow::{bail, format_err, Error};
use endian_trait::Endian;
use nix::fcntl::{fcntl, FcntlArg, OFlag};
mod encryption;
pub use encryption::*;
mod volume_statistics;
pub use volume_statistics::*;
mod tape_alert_flags;
pub use tape_alert_flags::*;
mod mam;
pub use mam::*;
use proxmox::{
sys::error::SysResult,
tools::io::{ReadExt, WriteExt},
};
use crate::{
api2::types::{
MamAttribute,
Lp17VolumeStatistics,
},
tape::{
BlockRead,
BlockReadError,
BlockWrite,
file_formats::{
BlockedWriter,
BlockedReader,
},
},
tools::sgutils2::{
SgRaw,
SenseInfo,
ScsiError,
InquiryInfo,
ModeParameterHeader,
ModeBlockDescriptor,
alloc_page_aligned_buffer,
scsi_inquiry,
scsi_mode_sense,
scsi_request_sense,
},
};
#[repr(C, packed)]
#[derive(Endian, Debug, Copy, Clone)]
pub struct ReadPositionLongPage {
flags: u8,
reserved: [u8;3],
partition_number: u32,
pub logical_object_number: u64,
pub logical_file_id: u64,
obsolete: [u8;8],
}
#[repr(C, packed)]
#[derive(Endian, Debug, Copy, Clone)]
struct DataCompressionModePage {
page_code: u8, // 0x0f
page_length: u8, // 0x0e
flags2: u8,
flags3: u8,
compression_algorithm: u32,
decompression_algorithm: u32,
reserved: [u8;4],
}
impl DataCompressionModePage {
pub fn set_compression(&mut self, enable: bool) {
if enable {
self.flags2 |= 128;
} else {
self.flags2 = self.flags2 & 127;
}
}
pub fn compression_enabled(&self) -> bool {
(self.flags2 & 0b1000_0000) != 0
}
}
#[derive(Debug)]
pub struct LtoTapeStatus {
pub block_length: u32,
pub density_code: u8,
pub buffer_mode: u8,
pub write_protect: bool,
pub compression: bool,
}
pub struct SgTape {
file: File,
info: InquiryInfo,
encryption_key_loaded: bool,
}
impl SgTape {
const SCSI_TAPE_DEFAULT_TIMEOUT: usize = 60*2; // 2 minutes
/// Create a new instance
///
/// Uses scsi_inquiry to check the device type.
pub fn new(mut file: File) -> Result<Self, Error> {
let info = scsi_inquiry(&mut file)?;
if info.peripheral_type != 1 {
bail!("not a tape device (peripheral_type = {})", info.peripheral_type);
}
Ok(Self {
file,
info,
encryption_key_loaded: false,
})
}
/// Access to file descriptor - useful for testing
pub fn file_mut(&mut self) -> &mut File {
&mut self.file
}
pub fn info(&self) -> &InquiryInfo {
&self.info
}
pub fn open<P: AsRef<Path>>(path: P) -> Result<SgTape, Error> {
// do not wait for media, use O_NONBLOCK
let file = OpenOptions::new()
.read(true)
.write(true)
.custom_flags(libc::O_NONBLOCK)
.open(path)?;
// then clear O_NONBLOCK
let flags = fcntl(file.as_raw_fd(), FcntlArg::F_GETFL)
.into_io_result()?;
let mut flags = OFlag::from_bits_truncate(flags);
flags.remove(OFlag::O_NONBLOCK);
fcntl(file.as_raw_fd(), FcntlArg::F_SETFL(flags))
.into_io_result()?;
Self::new(file)
}
pub fn inquiry(&mut self) -> Result<InquiryInfo, Error> {
scsi_inquiry(&mut self.file)
}
/// Erase medium.
///
/// EOD is written at the current position, which marks it as end
/// of data. After the command is successfully completed, the
/// drive is positioned immediately before End Of Data (not End Of
/// Tape).
pub fn erase_media(&mut self, fast: bool) -> Result<(), Error> {
let mut sg_raw = SgRaw::new(&mut self.file, 16)?;
sg_raw.set_timeout(Self::SCSI_TAPE_DEFAULT_TIMEOUT);
let mut cmd = Vec::new();
cmd.push(0x19);
if fast {
cmd.push(0); // LONG=0
} else {
cmd.push(1); // LONG=1
}
cmd.extend(&[0, 0, 0, 0]);
sg_raw.do_command(&cmd)
.map_err(|err| format_err!("erase failed - {}", err))?;
Ok(())
}
/// Format media, single partition
pub fn format_media(&mut self, fast: bool) -> Result<(), Error> {
self.rewind()?;
let mut sg_raw = SgRaw::new(&mut self.file, 16)?;
sg_raw.set_timeout(Self::SCSI_TAPE_DEFAULT_TIMEOUT);
let mut cmd = Vec::new();
cmd.extend(&[0x04, 0, 0, 0, 0, 0]);
sg_raw.do_command(&cmd)
.map_err(|err| format_err!("erase failed - {}", err))?;
if !fast {
self.erase_media(false)?; // overwrite everything
}
Ok(())
}
/// Lock/Unlock drive door
pub fn set_medium_removal(&mut self, allow: bool) -> Result<(), ScsiError> {
let mut sg_raw = SgRaw::new(&mut self.file, 16)?;
sg_raw.set_timeout(Self::SCSI_TAPE_DEFAULT_TIMEOUT);
let mut cmd = Vec::new();
cmd.extend(&[0x1E, 0, 0, 0]);
if allow {
cmd.push(0);
} else {
cmd.push(1);
}
cmd.push(0); // control
sg_raw.do_command(&cmd)?;
Ok(())
}
pub fn rewind(&mut self) -> Result<(), Error> {
let mut sg_raw = SgRaw::new(&mut self.file, 16)?;
sg_raw.set_timeout(Self::SCSI_TAPE_DEFAULT_TIMEOUT);
let mut cmd = Vec::new();
cmd.extend(&[0x01, 0, 0, 0, 0, 0]); // REWIND
sg_raw.do_command(&cmd)
.map_err(|err| format_err!("rewind failed - {}", err))?;
Ok(())
}
pub fn position(&mut self) -> Result<ReadPositionLongPage, Error> {
let expected_size = std::mem::size_of::<ReadPositionLongPage>();
let mut sg_raw = SgRaw::new(&mut self.file, 32)?;
sg_raw.set_timeout(30); // use short timeout
let mut cmd = Vec::new();
cmd.extend(&[0x34, 0x06, 0, 0, 0, 0, 0, 0, 0, 0]); // READ POSITION LONG FORM
let data = sg_raw.do_command(&cmd)
.map_err(|err| format_err!("read position failed - {}", err))?;
let page = proxmox::try_block!({
if data.len() != expected_size {
bail!("got unexpected data len ({} != {}", data.len(), expected_size);
}
let mut reader = &data[..];
let page: ReadPositionLongPage = unsafe { reader.read_be_value()? };
Ok(page)
}).map_err(|err: Error| format_err!("decode position page failed - {}", err))?;
if page.partition_number != 0 {
bail!("detecthed partitioned tape - not supported");
}
Ok(page)
}
pub fn current_file_number(&mut self) -> Result<u64, Error> {
let position = self.position()?;
Ok(position.logical_file_id)
}
/// Check if we are positioned after a filemark (or BOT)
pub fn check_filemark(&mut self) -> Result<bool, Error> {
let pos = self.position()?;
if pos.logical_object_number == 0 {
// at BOT, Ok (no filemark required)
return Ok(true);
}
// Note: SPACE blocks returns Err at filemark
match self.space(-1, true) {
Ok(_) => {
self.space(1, true) // move back to end
.map_err(|err| format_err!("check_filemark failed (space forward) - {}", err))?;
Ok(false)
}
Err(ScsiError::Sense(SenseInfo { sense_key: 0, asc: 0, ascq: 1 })) => {
// Filemark detected - good
self.space(1, false) // move to EOT side of filemark
.map_err(|err| format_err!("check_filemark failed (move to EOT side of filemark) - {}", err))?;
Ok(true)
}
Err(err) => {
bail!("check_filemark failed - {:?}", err);
}
}
}
pub fn move_to_eom(&mut self, write_missing_eof: bool) -> Result<(), Error> {
let mut sg_raw = SgRaw::new(&mut self.file, 16)?;
sg_raw.set_timeout(Self::SCSI_TAPE_DEFAULT_TIMEOUT);
let mut cmd = Vec::new();
cmd.extend(&[0x11, 0x03, 0, 0, 0, 0]); // SPACE(6) move to EOD
sg_raw.do_command(&cmd)
.map_err(|err| format_err!("move to EOD failed - {}", err))?;
if write_missing_eof {
if !self.check_filemark()? {
self.write_filemarks(1, false)?;
}
}
Ok(())
}
fn space(&mut self, count: isize, blocks: bool) -> Result<(), ScsiError> {
let mut sg_raw = SgRaw::new(&mut self.file, 16)?;
sg_raw.set_timeout(Self::SCSI_TAPE_DEFAULT_TIMEOUT);
let mut cmd = Vec::new();
// Use short command if possible (supported by all drives)
if (count <= 0x7fffff) && (count > -0x7fffff) {
cmd.push(0x11); // SPACE(6)
if blocks {
cmd.push(0); // blocks
} else {
cmd.push(1); // filemarks
}
cmd.push(((count >> 16) & 0xff) as u8);
cmd.push(((count >> 8) & 0xff) as u8);
cmd.push((count & 0xff) as u8);
cmd.push(0); //control byte
} else {
cmd.push(0x91); // SPACE(16)
if blocks {
cmd.push(0); // blocks
} else {
cmd.push(1); // filemarks
}
cmd.extend(&[0, 0]); // reserved
let count: i64 = count as i64;
cmd.extend(&count.to_be_bytes());
cmd.extend(&[0, 0, 0, 0]); // reserved
}
sg_raw.do_command(&cmd)?;
Ok(())
}
pub fn space_filemarks(&mut self, count: isize) -> Result<(), Error> {
self.space(count, false)
.map_err(|err| format_err!("space filemarks failed - {}", err))
}
pub fn space_blocks(&mut self, count: isize) -> Result<(), Error> {
self.space(count, true)
.map_err(|err| format_err!("space blocks failed - {}", err))
}
pub fn eject(&mut self) -> Result<(), Error> {
let mut sg_raw = SgRaw::new(&mut self.file, 16)?;
sg_raw.set_timeout(Self::SCSI_TAPE_DEFAULT_TIMEOUT);
let mut cmd = Vec::new();
cmd.extend(&[0x1B, 0, 0, 0, 0, 0]); // LODA/UNLOAD HOLD=0, LOAD=0
sg_raw.do_command(&cmd)
.map_err(|err| format_err!("eject failed - {}", err))?;
Ok(())
}
pub fn load(&mut self) -> Result<(), Error> {
let mut sg_raw = SgRaw::new(&mut self.file, 16)?;
sg_raw.set_timeout(Self::SCSI_TAPE_DEFAULT_TIMEOUT);
let mut cmd = Vec::new();
cmd.extend(&[0x1B, 0, 0, 0, 0b0000_0001, 0]); // LODA/UNLOAD HOLD=0, LOAD=1
sg_raw.do_command(&cmd)
.map_err(|err| format_err!("load media failed - {}", err))?;
Ok(())
}
pub fn write_filemarks(
&mut self,
count: usize,
immediate: bool,
) -> Result<(), std::io::Error> {
if count > 255 {
proxmox::io_bail!("write_filemarks failed: got strange count '{}'", count);
}
let mut sg_raw = SgRaw::new(&mut self.file, 16)
.map_err(|err| proxmox::io_format_err!("write_filemarks failed (alloc) - {}", err))?;
sg_raw.set_timeout(Self::SCSI_TAPE_DEFAULT_TIMEOUT);
let mut cmd = Vec::new();
cmd.push(0x10);
if immediate {
cmd.push(1); // IMMED=1
} else {
cmd.push(0); // IMMED=0
}
cmd.extend(&[0, 0, count as u8]); // COUNT
cmd.push(0); // control byte
match sg_raw.do_command(&cmd) {
Ok(_) => { /* OK */ }
Err(ScsiError::Sense(SenseInfo { sense_key: 0, asc: 0, ascq: 2 })) => {
/* LEOM - ignore */
}
Err(err) => {
proxmox::io_bail!("write filemark failed - {}", err);
}
}
Ok(())
}
// Flush tape buffers (WEOF with count 0 => flush)
pub fn sync(&mut self) -> Result<(), std::io::Error> {
self.write_filemarks(0, false)?;
Ok(())
}
pub fn test_unit_ready(&mut self) -> Result<(), Error> {
let mut sg_raw = SgRaw::new(&mut self.file, 16)?;
sg_raw.set_timeout(30); // use short timeout
let mut cmd = Vec::new();
cmd.extend(&[0x00, 0, 0, 0, 0, 0]); // TEST UNIT READY
match sg_raw.do_command(&cmd) {
Ok(_) => Ok(()),
Err(err) => {
bail!("test_unit_ready failed - {}", err);
}
}
}
pub fn wait_until_ready(&mut self) -> Result<(), Error> {
let start = SystemTime::now();
let max_wait = std::time::Duration::new(Self::SCSI_TAPE_DEFAULT_TIMEOUT as u64, 0);
loop {
match self.test_unit_ready() {
Ok(()) => return Ok(()),
_ => {
std::thread::sleep(std::time::Duration::new(1, 0));
if start.elapsed()? > max_wait {
bail!("wait_until_ready failed - got timeout");
}
}
}
}
}
/// Read Tape Alert Flags
pub fn tape_alert_flags(&mut self) -> Result<TapeAlertFlags, Error> {
read_tape_alert_flags(&mut self.file)
}
/// Read Cartridge Memory (MAM Attributes)
pub fn cartridge_memory(&mut self) -> Result<Vec<MamAttribute>, Error> {
read_mam_attributes(&mut self.file)
}
/// Read Volume Statistics
pub fn volume_statistics(&mut self) -> Result<Lp17VolumeStatistics, Error> {
return 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();
set_encryption(&mut self.file, key)
}
// Note: use alloc_page_aligned_buffer to alloc data transfer buffer
//
// Returns true if the drive reached the Logical End Of Media (early warning)
fn write_block(&mut self, data: &[u8]) -> Result<bool, std::io::Error> {
let transfer_len = data.len();
if transfer_len > 0x800000 {
proxmox::io_bail!("write failed - data too large");
}
let mut sg_raw = SgRaw::new(&mut self.file, 0)
.unwrap(); // cannot fail with size 0
sg_raw.set_timeout(Self::SCSI_TAPE_DEFAULT_TIMEOUT);
let mut cmd = Vec::new();
cmd.push(0x0A); // WRITE
cmd.push(0x00); // VARIABLE SIZED BLOCKS
cmd.push(((transfer_len >> 16) & 0xff) as u8);
cmd.push(((transfer_len >> 8) & 0xff) as u8);
cmd.push((transfer_len & 0xff) as u8);
cmd.push(0); // control byte
//println!("WRITE {:?}", cmd);
//println!("WRITE {:?}", data);
match sg_raw.do_out_command(&cmd, data) {
Ok(()) => { return Ok(false) }
Err(ScsiError::Sense(SenseInfo { sense_key: 0, asc: 0, ascq: 2 })) => {
return Ok(true); // LEOM
}
Err(err) => {
proxmox::io_bail!("write failed - {}", err);
}
}
}
fn read_block(&mut self, buffer: &mut [u8]) -> Result<usize, BlockReadError> {
let transfer_len = buffer.len();
if transfer_len > 0xFFFFFF {
return Err(BlockReadError::Error(
proxmox::io_format_err!("read failed - buffer too large")
));
}
let mut sg_raw = SgRaw::new(&mut self.file, 0)
.unwrap(); // cannot fail with size 0
sg_raw.set_timeout(Self::SCSI_TAPE_DEFAULT_TIMEOUT);
let mut cmd = Vec::new();
cmd.push(0x08); // READ
cmd.push(0x02); // VARIABLE SIZED BLOCKS, SILI=1
//cmd.push(0x00); // VARIABLE SIZED BLOCKS, SILI=0
cmd.push(((transfer_len >> 16) & 0xff) as u8);
cmd.push(((transfer_len >> 8) & 0xff) as u8);
cmd.push((transfer_len & 0xff) as u8);
cmd.push(0); // control byte
let data = match sg_raw.do_in_command(&cmd, buffer) {
Ok(data) => data,
Err(ScsiError::Sense(SenseInfo { sense_key: 0, asc: 0, ascq: 1 })) => {
return Err(BlockReadError::EndOfFile);
}
Err(ScsiError::Sense(SenseInfo { sense_key: 8, asc: 0, ascq: 5 })) => {
return Err(BlockReadError::EndOfStream);
}
Err(err) => {
return Err(BlockReadError::Error(
proxmox::io_format_err!("read failed - {}", err)
));
}
};
if data.len() != transfer_len {
return Err(BlockReadError::Error(
proxmox::io_format_err!("read failed - unexpected block len ({} != {})", data.len(), buffer.len())
));
}
Ok(transfer_len)
}
pub fn open_writer(&mut self) -> BlockedWriter<SgTapeWriter> {
let writer = SgTapeWriter::new(self);
BlockedWriter::new(writer)
}
pub fn open_reader(&mut self) -> Result<BlockedReader<SgTapeReader>, BlockReadError> {
let reader = SgTapeReader::new(self);
BlockedReader::open(reader)
}
/// Set important drive options
pub fn set_drive_options(
&mut self,
compression: Option<bool>,
block_length: Option<u32>,
buffer_mode: Option<bool>,
) -> Result<(), Error> {
// Note: Read/Modify/Write
let (mut head, mut block_descriptor, mut page) = self.read_compression_page()?;
let mut sg_raw = SgRaw::new(&mut self.file, 0)?;
sg_raw.set_timeout(Self::SCSI_TAPE_DEFAULT_TIMEOUT);
head.mode_data_len = 0; // need to b e zero
if let Some(compression) = compression {
page.set_compression(compression);
}
if let Some(block_length) = block_length {
block_descriptor.set_block_length(block_length)?;
}
if let Some(buffer_mode) = buffer_mode {
let mut mode = head.flags3 & 0b1_000_1111;
if buffer_mode {
mode |= 0b0_001_0000;
}
head.flags3 = mode;
}
let mut data = Vec::new();
unsafe {
data.write_be_value(head)?;
data.write_be_value(block_descriptor)?;
data.write_be_value(page)?;
}
let mut cmd = Vec::new();
cmd.push(0x55); // MODE SELECT(10)
cmd.push(0b0001_0000); // PF=1
cmd.extend(&[0,0,0,0,0]); //reserved
let param_list_len: u16 = data.len() as u16;
cmd.extend(&param_list_len.to_be_bytes());
cmd.push(0); // control
let mut buffer = alloc_page_aligned_buffer(4096)?;
buffer[..data.len()].copy_from_slice(&data[..]);
sg_raw.do_out_command(&cmd, &buffer[..data.len()])
.map_err(|err| format_err!("set drive options failed - {}", err))?;
Ok(())
}
fn read_compression_page(
&mut self,
) -> Result<(ModeParameterHeader, ModeBlockDescriptor, DataCompressionModePage), Error> {
let (head, block_descriptor, page): (_,_, DataCompressionModePage)
= scsi_mode_sense(&mut self.file, false, 0x0f, 0)?;
if !(page.page_code == 0x0f && page.page_length == 0x0e) {
bail!("read_compression_page: got strange page code/length");
}
let block_descriptor = match block_descriptor {
Some(block_descriptor) => block_descriptor,
None => bail!("read_compression_page failed: missing block descriptor"),
};
Ok((head, block_descriptor, page))
}
/// Read drive options/status
///
/// We read the drive compression page, including the
/// block_descriptor. This is all information we need for now.
pub fn read_drive_status(&mut self) -> Result<LtoTapeStatus, Error> {
// We do a Request Sense, but ignore the result.
// This clears deferred error or media changed events.
let _ = scsi_request_sense(&mut self.file);
let (head, block_descriptor, page) = self.read_compression_page()?;
Ok(LtoTapeStatus {
block_length: block_descriptor.block_length(),
write_protect: (head.flags3 & 0b1000_0000) != 0,
buffer_mode: (head.flags3 & 0b0111_0000) >> 4,
compression: page.compression_enabled(),
density_code: block_descriptor.density_code,
})
}
}
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,
}
impl <'a> SgTapeReader<'a> {
pub fn new(sg_tape: &'a mut SgTape) -> Self {
Self { sg_tape, end_of_file: false, }
}
}
impl <'a> BlockRead for SgTapeReader<'a> {
fn read_block(&mut self, buffer: &mut [u8]) -> Result<usize, BlockReadError> {
if self.end_of_file {
return Err(BlockReadError::Error(proxmox::io_format_err!("detected read after EOF!")));
}
match self.sg_tape.read_block(buffer) {
Ok(usize) => Ok(usize),
Err(BlockReadError::EndOfFile) => {
self.end_of_file = true;
Err(BlockReadError::EndOfFile)
},
Err(err) => Err(err),
}
}
}
pub struct SgTapeWriter<'a> {
sg_tape: &'a mut SgTape,
_leom_sent: bool,
}
impl <'a> SgTapeWriter<'a> {
pub fn new(sg_tape: &'a mut SgTape) -> Self {
Self { sg_tape, _leom_sent: false }
}
}
impl <'a> BlockWrite for SgTapeWriter<'a> {
fn write_block(&mut self, buffer: &[u8]) -> Result<bool, std::io::Error> {
self.sg_tape.write_block(buffer)
}
fn write_filemark(&mut self) -> Result<(), std::io::Error> {
self.sg_tape.write_filemarks(1, true)
}
}

View File

@ -11,7 +11,7 @@ use crate::{
api2::types::MamAttribute,
tools::sgutils2::SgRaw,
tape::{
drive::TapeAlertFlags,
drive::lto::TapeAlertFlags,
},
};

View File

@ -2,15 +2,14 @@ use std::io::Read;
use std::os::unix::io::AsRawFd;
use anyhow::{bail, format_err, Error};
use serde::{Serialize, Deserialize};
use endian_trait::Endian;
use proxmox::{
api::api,
tools::io::ReadExt,
};
use proxmox::tools::io::ReadExt;
use crate::tools::sgutils2::SgRaw;
use crate::{
api2::types::Lp17VolumeStatistics,
tools::sgutils2::SgRaw,
};
/// SCSI command to query volume statistics
///
@ -54,66 +53,6 @@ struct LpParameterHeader {
parameter_len: u8,
}
#[api()]
/// Volume statistics from SCSI log page 17h
#[derive(Default, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct Lp17VolumeStatistics {
/// Volume mounts (thread count)
pub volume_mounts: u64,
/// Total data sets written
pub volume_datasets_written: u64,
/// Write retries
pub volume_recovered_write_data_errors: u64,
/// Total unrecovered write errors
pub volume_unrecovered_write_data_errors: u64,
/// Total suspended writes
pub volume_write_servo_errors: u64,
/// Total fatal suspended writes
pub volume_unrecovered_write_servo_errors: u64,
/// Total datasets read
pub volume_datasets_read: u64,
/// Total read retries
pub volume_recovered_read_errors: u64,
/// Total unrecovered read errors
pub volume_unrecovered_read_errors: u64,
/// Last mount unrecovered write errors
pub last_mount_unrecovered_write_errors: u64,
/// Last mount unrecovered read errors
pub last_mount_unrecovered_read_errors: u64,
/// Last mount bytes written
pub last_mount_bytes_written: u64,
/// Last mount bytes read
pub last_mount_bytes_read: u64,
/// Lifetime bytes written
pub lifetime_bytes_written: u64,
/// Lifetime bytes read
pub lifetime_bytes_read: u64,
/// Last load write compression ratio
pub last_load_write_compression_ratio: u64,
/// Last load read compression ratio
pub last_load_read_compression_ratio: u64,
/// Medium mount time
pub medium_mount_time: u64,
/// Medium ready time
pub medium_ready_time: u64,
/// Total native capacity
pub total_native_capacity: u64,
/// Total used native capacity
pub total_used_native_capacity: u64,
/// Write protect
pub write_protect: bool,
/// Volume is WORM
pub worm: bool,
/// Beginning of medium passes
pub beginning_of_medium_passes: u64,
/// Middle of medium passes
pub middle_of_tape_passes: u64,
/// Volume serial number
pub serial: String,
}
fn decode_volume_statistics(data: &[u8]) -> Result<Lp17VolumeStatistics, Error> {

View File

@ -2,22 +2,8 @@
mod virtual_tape;
pub mod linux_mtio;
mod tape_alert_flags;
pub use tape_alert_flags::*;
mod volume_statistics;
pub use volume_statistics::*;
mod encryption;
pub use encryption::*;
mod linux_tape;
pub use linux_tape::*;
mod mam;
pub use mam::*;
mod lto;
pub use lto::*;
use std::os::unix::io::AsRawFd;
use std::path::PathBuf;
@ -49,7 +35,7 @@ use crate::{
},
api2::types::{
VirtualTapeDrive,
LinuxTapeDrive,
LtoTapeDrive,
},
server::{
send_load_media_email,
@ -58,7 +44,9 @@ use crate::{
tape::{
TapeWrite,
TapeRead,
BlockReadError,
MediaId,
drive::lto::TapeAlertFlags,
file_formats::{
PROXMOX_BACKUP_MEDIA_LABEL_MAGIC_1_0,
PROXMOX_BACKUP_MEDIA_SET_LABEL_MAGIC_1_0,
@ -84,37 +72,22 @@ pub trait TapeDriver {
/// Move to end of recorded data
///
/// We assume this flushes the tape write buffer.
fn move_to_eom(&mut self) -> Result<(), Error>;
/// We assume this flushes the tape write buffer. if
/// write_missing_eof is true, we verify that there is a filemark
/// at the end. If not, we write one.
fn move_to_eom(&mut self, write_missing_eof: bool) -> Result<(), Error>;
/// Move to last file
fn move_to_last_file(&mut self) -> Result<(), Error> {
self.move_to_eom()?;
if self.current_file_number()? == 0 {
bail!("move_to_last_file failed - media contains no data");
}
self.backward_space_count_files(2)?;
Ok(())
}
/// Forward space count files. The tape is positioned on the first block of the next file.
fn forward_space_count_files(&mut self, count: usize) -> Result<(), Error>;
/// Backward space count files. The tape is positioned on the last block of the previous file.
fn backward_space_count_files(&mut self, count: usize) -> Result<(), Error>;
fn move_to_last_file(&mut self) -> Result<(), Error>;
/// Current file number
fn current_file_number(&mut self) -> Result<u64, Error>;
/// Completely erase the media
fn erase_media(&mut self, fast: bool) -> Result<(), Error>;
fn format_media(&mut self, fast: bool) -> Result<(), Error>;
/// Read/Open the next file
fn read_next_file<'a>(&'a mut self) -> Result<Option<Box<dyn TapeRead + 'a>>, std::io::Error>;
fn read_next_file<'a>(&'a mut self) -> Result<Box<dyn TapeRead + 'a>, BlockReadError>;
/// Write/Append a new file
fn write_file<'a>(&'a mut self) -> Result<Box<dyn TapeWrite + 'a>, std::io::Error>;
@ -122,11 +95,9 @@ pub trait TapeDriver {
/// Write label to tape (erase tape content)
fn label_tape(&mut self, label: &MediaLabel) -> Result<(), Error> {
self.rewind()?;
self.set_encryption(None)?;
self.erase_media(true)?;
self.format_media(true)?; // this rewinds the tape
let raw = serde_json::to_string_pretty(&serde_json::to_value(&label)?)?;
@ -162,9 +133,17 @@ pub trait TapeDriver {
self.rewind()?;
let label = {
let mut reader = match self.read_next_file()? {
None => return Ok((None, None)), // tape is empty
Some(reader) => reader,
let mut reader = match self.read_next_file() {
Err(BlockReadError::EndOfStream) => {
return Ok((None, None)); // tape is empty
}
Err(BlockReadError::EndOfFile) => {
bail!("got unexpected filemark at BOT");
}
Err(BlockReadError::Error(err)) => {
return Err(err.into());
}
Ok(reader) => reader,
};
let header: MediaContentHeader = unsafe { reader.read_le_value()? };
@ -185,9 +164,17 @@ pub trait TapeDriver {
let mut media_id = MediaId { label, media_set_label: None };
// try to read MediaSet label
let mut reader = match self.read_next_file()? {
None => return Ok((Some(media_id), None)),
Some(reader) => reader,
let mut reader = match self.read_next_file() {
Err(BlockReadError::EndOfStream) => {
return Ok((Some(media_id), None));
}
Err(BlockReadError::EndOfFile) => {
bail!("got unexpected filemark after label");
}
Err(BlockReadError::Error(err)) => {
return Err(err.into());
}
Ok(reader) => reader,
};
let header: MediaContentHeader = unsafe { reader.read_le_value()? };
@ -263,8 +250,8 @@ pub fn media_changer(
let tape = VirtualTapeDrive::deserialize(config)?;
Ok(Some((Box::new(tape), drive.to_string())))
}
"linux" => {
let drive_config = LinuxTapeDrive::deserialize(config)?;
"lto" => {
let drive_config = LtoTapeDrive::deserialize(config)?;
match drive_config.changer {
Some(ref changer_name) => {
let changer = MtxMediaChanger::with_drive_config(&drive_config)?;
@ -317,8 +304,8 @@ pub fn open_drive(
let handle = tape.open()?;
Ok(Box::new(handle))
}
"linux" => {
let tape = LinuxTapeDrive::deserialize(config)?;
"lto" => {
let tape = LtoTapeDrive::deserialize(config)?;
let handle = tape.open()?;
Ok(Box::new(handle))
}
@ -379,8 +366,8 @@ pub fn request_and_load_media(
Ok((handle, media_id))
}
"linux" => {
let drive_config = LinuxTapeDrive::deserialize(config)?;
"lto" => {
let drive_config = LtoTapeDrive::deserialize(config)?;
let label_text = label.label_text.clone();
@ -546,8 +533,8 @@ fn tape_device_path(
"virtual" => {
VirtualTapeDrive::deserialize(config)?.path
}
"linux" => {
LinuxTapeDrive::deserialize(config)?.path
"lto" => {
LtoTapeDrive::deserialize(config)?.path
}
_ => bail!("unknown drive type '{}' - internal error"),
};

View File

@ -15,6 +15,7 @@ use crate::{
tape::{
TapeWrite,
TapeRead,
BlockReadError,
changer::{
MediaChange,
MtxStatus,
@ -181,6 +182,53 @@ impl VirtualTapeHandle {
Ok(list)
}
#[allow(dead_code)]
fn forward_space_count_files(&mut self, count: usize) -> Result<(), Error> {
let mut status = self.load_status()?;
match status.current_tape {
Some(VirtualTapeStatus { ref name, ref mut pos }) => {
let index = self.load_tape_index(name)
.map_err(|err| io::Error::new(io::ErrorKind::Other, err.to_string()))?;
let new_pos = *pos + count;
if new_pos <= index.files {
*pos = new_pos;
} else {
bail!("forward_space_count_files failed: move beyond EOT");
}
self.store_status(&status)
.map_err(|err| io::Error::new(io::ErrorKind::Other, err.to_string()))?;
Ok(())
}
None => bail!("drive is empty (no tape loaded)."),
}
}
// Note: behavior differs from LTO, because we always position at
// EOT side.
fn backward_space_count_files(&mut self, count: usize) -> Result<(), Error> {
let mut status = self.load_status()?;
match status.current_tape {
Some(VirtualTapeStatus { ref mut pos, .. }) => {
if count <= *pos {
*pos = *pos - count;
} else {
bail!("backward_space_count_files failed: move before BOT");
}
self.store_status(&status)
.map_err(|err| io::Error::new(io::ErrorKind::Other, err.to_string()))?;
Ok(())
}
None => bail!("drive is empty (no tape loaded)."),
}
}
}
impl TapeDriver for VirtualTapeHandle {
@ -199,18 +247,33 @@ impl TapeDriver for VirtualTapeHandle {
}
}
fn read_next_file(&mut self) -> Result<Option<Box<dyn TapeRead>>, io::Error> {
/// Move to last file
fn move_to_last_file(&mut self) -> Result<(), Error> {
self.move_to_eom(false)?;
if self.current_file_number()? == 0 {
bail!("move_to_last_file failed - media contains no data");
}
self.backward_space_count_files(1)?;
Ok(())
}
fn read_next_file(&mut self) -> Result<Box<dyn TapeRead>, BlockReadError> {
let mut status = self.load_status()
.map_err(|err| io::Error::new(io::ErrorKind::Other, err.to_string()))?;
.map_err(|err| BlockReadError::Error(io::Error::new(io::ErrorKind::Other, err.to_string())))?;
match status.current_tape {
Some(VirtualTapeStatus { ref name, ref mut pos }) => {
let index = self.load_tape_index(name)
.map_err(|err| io::Error::new(io::ErrorKind::Other, err.to_string()))?;
.map_err(|err| BlockReadError::Error(io::Error::new(io::ErrorKind::Other, err.to_string())))?;
if *pos >= index.files {
return Ok(None); // EOM
return Err(BlockReadError::EndOfStream);
}
let path = self.tape_file_path(name, *pos);
@ -220,17 +283,15 @@ impl TapeDriver for VirtualTapeHandle {
*pos += 1;
self.store_status(&status)
.map_err(|err| io::Error::new(io::ErrorKind::Other, err.to_string()))?;
.map_err(|err| BlockReadError::Error(io::Error::new(io::ErrorKind::Other, err.to_string())))?;
let reader = Box::new(file);
let reader = Box::new(EmulateTapeReader::new(reader));
match BlockedReader::open(reader)? {
Some(reader) => Ok(Some(Box::new(reader))),
None => Ok(None),
let reader = EmulateTapeReader::new(file);
let reader = BlockedReader::open(reader)?;
Ok(Box::new(reader))
}
None => {
return Err(BlockReadError::Error(proxmox::io_format_err!("drive is empty (no tape loaded).")));
}
None => proxmox::io_bail!("drive is empty (no tape loaded)."),
}
}
@ -277,8 +338,7 @@ impl TapeDriver for VirtualTapeHandle {
free_space = self.max_size - used_space;
}
let writer = Box::new(file);
let writer = Box::new(EmulateTapeWriter::new(writer, free_space));
let writer = EmulateTapeWriter::new(file, free_space);
let writer = Box::new(BlockedWriter::new(writer));
Ok(writer)
@ -287,7 +347,7 @@ impl TapeDriver for VirtualTapeHandle {
}
}
fn move_to_eom(&mut self) -> Result<(), Error> {
fn move_to_eom(&mut self, _write_missing_eof: bool) -> Result<(), Error> {
let mut status = self.load_status()?;
match status.current_tape {
Some(VirtualTapeStatus { ref name, ref mut pos }) => {
@ -306,50 +366,6 @@ impl TapeDriver for VirtualTapeHandle {
}
}
fn forward_space_count_files(&mut self, count: usize) -> Result<(), Error> {
let mut status = self.load_status()?;
match status.current_tape {
Some(VirtualTapeStatus { ref name, ref mut pos }) => {
let index = self.load_tape_index(name)
.map_err(|err| io::Error::new(io::ErrorKind::Other, err.to_string()))?;
let new_pos = *pos + count;
if new_pos <= index.files {
*pos = new_pos;
} else {
bail!("forward_space_count_files failed: move beyond EOT");
}
self.store_status(&status)
.map_err(|err| io::Error::new(io::ErrorKind::Other, err.to_string()))?;
Ok(())
}
None => bail!("drive is empty (no tape loaded)."),
}
}
fn backward_space_count_files(&mut self, count: usize) -> Result<(), Error> {
let mut status = self.load_status()?;
match status.current_tape {
Some(VirtualTapeStatus { ref mut pos, .. }) => {
if count <= *pos {
*pos = *pos - count;
} else {
bail!("backward_space_count_files failed: move before BOT");
}
self.store_status(&status)
.map_err(|err| io::Error::new(io::ErrorKind::Other, err.to_string()))?;
Ok(())
}
None => bail!("drive is empty (no tape loaded)."),
}
}
fn rewind(&mut self) -> Result<(), Error> {
let mut status = self.load_status()?;
match status.current_tape {
@ -362,7 +378,7 @@ impl TapeDriver for VirtualTapeHandle {
}
}
fn erase_media(&mut self, _fast: bool) -> Result<(), Error> {
fn format_media(&mut self, _fast: bool) -> Result<(), Error> {
let mut status = self.load_status()?;
match status.current_tape {
Some(VirtualTapeStatus { ref name, ref mut pos }) => {

View File

@ -2,7 +2,8 @@ use std::io::Read;
use crate::tape::{
TapeRead,
tape_device_read_block,
BlockRead,
BlockReadError,
file_formats::{
PROXMOX_TAPE_BLOCK_HEADER_MAGIC_1_0,
BlockHeader,
@ -32,19 +33,17 @@ pub struct BlockedReader<R> {
read_pos: usize,
}
impl <R: Read> BlockedReader<R> {
impl <R: BlockRead> BlockedReader<R> {
/// Create a new BlockedReader instance.
///
/// This tries to read the first block, and returns None if we are
/// at EOT.
pub fn open(mut reader: R) -> Result<Option<Self>, std::io::Error> {
/// This tries to read the first block. Please inspect the error
/// to detect EOF and EOT.
pub fn open(mut reader: R) -> Result<Self, BlockReadError> {
let mut buffer = BlockHeader::new();
if !Self::read_block_frame(&mut buffer, &mut reader)? {
return Ok(None);
}
Self::read_block_frame(&mut buffer, &mut reader)?;
let (_size, found_end_marker) = Self::check_buffer(&buffer, 0)?;
@ -57,7 +56,7 @@ impl <R: Read> BlockedReader<R> {
got_eod = true;
}
Ok(Some(Self {
Ok(Self {
reader,
buffer,
found_end_marker,
@ -66,7 +65,7 @@ impl <R: Read> BlockedReader<R> {
seq_nr: 1,
read_error: false,
read_pos: 0,
}))
})
}
fn check_buffer(buffer: &BlockHeader, seq_nr: u32) -> Result<(usize, bool), std::io::Error> {
@ -94,7 +93,7 @@ impl <R: Read> BlockedReader<R> {
Ok((size, found_end_marker))
}
fn read_block_frame(buffer: &mut BlockHeader, reader: &mut R) -> Result<bool, std::io::Error> {
fn read_block_frame(buffer: &mut BlockHeader, reader: &mut R) -> Result<(), BlockReadError> {
let data = unsafe {
std::slice::from_raw_parts_mut(
@ -103,27 +102,52 @@ impl <R: Read> BlockedReader<R> {
)
};
tape_device_read_block(reader, data)
let bytes = reader.read_block(data)?;
if bytes != BlockHeader::SIZE {
return Err(proxmox::io_format_err!("got wrong block size").into());
}
Ok(())
}
fn consume_eof_marker(reader: &mut R) -> Result<(), std::io::Error> {
let mut tmp_buf = [0u8; 512]; // use a small buffer for testing EOF
if tape_device_read_block(reader, &mut tmp_buf)? {
proxmox::io_bail!("detected tape block after stream end marker");
match reader.read_block(&mut tmp_buf) {
Ok(_) => {
proxmox::io_bail!("detected tape block after block-stream end marker");
}
Err(BlockReadError::EndOfFile) => {
return Ok(());
}
Err(BlockReadError::EndOfStream) => {
proxmox::io_bail!("got unexpected end of tape");
}
Err(BlockReadError::Error(err)) => {
return Err(err);
}
}
Ok(())
}
fn read_block(&mut self) -> Result<usize, std::io::Error> {
fn read_block(&mut self, check_end_marker: bool) -> Result<usize, std::io::Error> {
if !Self::read_block_frame(&mut self.buffer, &mut self.reader)? {
match Self::read_block_frame(&mut self.buffer, &mut self.reader) {
Ok(()) => { /* ok */ }
Err(BlockReadError::EndOfFile) => {
self.got_eod = true;
self.read_pos = self.buffer.payload.len();
if !self.found_end_marker {
if !self.found_end_marker && check_end_marker {
proxmox::io_bail!("detected tape stream without end marker");
}
return Ok(0); // EOD
}
Err(BlockReadError::EndOfStream) => {
proxmox::io_bail!("got unexpected end of tape");
}
Err(BlockReadError::Error(err)) => {
return Err(err);
}
}
let (size, found_end_marker) = Self::check_buffer(&self.buffer, self.seq_nr)?;
self.seq_nr += 1;
@ -141,7 +165,7 @@ impl <R: Read> BlockedReader<R> {
}
}
impl <R: Read> TapeRead for BlockedReader<R> {
impl <R: BlockRead> TapeRead for BlockedReader<R> {
fn is_incomplete(&self) -> Result<bool, std::io::Error> {
if !self.got_eod {
@ -161,9 +185,26 @@ impl <R: Read> TapeRead for BlockedReader<R> {
Ok(self.found_end_marker)
}
// like ReadExt::skip_to_end(), but does not raise an error if the
// stream has no end marker.
fn skip_data(&mut self) -> Result<usize, std::io::Error> {
let mut bytes = 0;
let buffer_size = self.buffer.size();
let rest = (buffer_size as isize) - (self.read_pos as isize);
if rest > 0 {
bytes = rest as usize;
}
loop {
if self.got_eod {
return Ok(bytes);
}
bytes += self.read_block(false)?;
}
}
}
impl <R: Read> Read for BlockedReader<R> {
impl <R: BlockRead> Read for BlockedReader<R> {
fn read(&mut self, buffer: &mut [u8]) -> Result<usize, std::io::Error> {
@ -175,7 +216,7 @@ impl <R: Read> Read for BlockedReader<R> {
let mut rest = (buffer_size as isize) - (self.read_pos as isize);
if rest <= 0 && !self.got_eod { // try to refill buffer
buffer_size = match self.read_block() {
buffer_size = match self.read_block(true) {
Ok(len) => len,
err => {
self.read_error = true;
@ -204,9 +245,11 @@ impl <R: Read> Read for BlockedReader<R> {
#[cfg(test)]
mod test {
use std::io::Read;
use anyhow::Error;
use anyhow::{bail, Error};
use crate::tape::{
TapeWrite,
BlockReadError,
helpers::{EmulateTapeReader, EmulateTapeWriter},
file_formats::{
PROXMOX_TAPE_BLOCK_SIZE,
BlockedReader,
@ -218,11 +261,14 @@ mod test {
let mut tape_data = Vec::new();
let mut writer = BlockedWriter::new(&mut tape_data);
{
let writer = EmulateTapeWriter::new(&mut tape_data, 1024*1024*10);
let mut writer = BlockedWriter::new(writer);
writer.write_all(data)?;
writer.finish(false)?;
}
assert_eq!(
tape_data.len(),
@ -231,7 +277,8 @@ mod test {
);
let reader = &mut &tape_data[..];
let mut reader = BlockedReader::open(reader)?.unwrap();
let reader = EmulateTapeReader::new(reader);
let mut reader = BlockedReader::open(reader)?;
let mut read_data = Vec::with_capacity(PROXMOX_TAPE_BLOCK_SIZE);
reader.read_to_end(&mut read_data)?;
@ -263,8 +310,11 @@ mod test {
fn no_data() -> Result<(), Error> {
let tape_data = Vec::new();
let reader = &mut &tape_data[..];
let reader = BlockedReader::open(reader)?;
assert!(reader.is_none());
let reader = EmulateTapeReader::new(reader);
match BlockedReader::open(reader) {
Err(BlockReadError::EndOfFile) => { /* OK */ },
_ => bail!("expected EOF"),
}
Ok(())
}
@ -273,14 +323,17 @@ mod test {
fn no_end_marker() -> Result<(), Error> {
let mut tape_data = Vec::new();
{
let mut writer = BlockedWriter::new(&mut tape_data);
let writer = EmulateTapeWriter::new(&mut tape_data, 1024*1024);
let mut writer = BlockedWriter::new(writer);
// write at least one block
let data = proxmox::sys::linux::random_data(PROXMOX_TAPE_BLOCK_SIZE)?;
writer.write_all(&data)?;
// but do not call finish here
}
let reader = &mut &tape_data[..];
let mut reader = BlockedReader::open(reader)?.unwrap();
let reader = EmulateTapeReader::new(reader);
let mut reader = BlockedReader::open(reader)?;
let mut data = Vec::with_capacity(PROXMOX_TAPE_BLOCK_SIZE);
assert!(reader.read_to_end(&mut data).is_err());
@ -292,14 +345,18 @@ mod test {
fn small_read_buffer() -> Result<(), Error> {
let mut tape_data = Vec::new();
let mut writer = BlockedWriter::new(&mut tape_data);
{
let writer = EmulateTapeWriter::new(&mut tape_data, 1024*1024);
let mut writer = BlockedWriter::new(writer);
writer.write_all(b"ABC")?;
writer.finish(false)?;
}
let reader = &mut &tape_data[..];
let mut reader = BlockedReader::open(reader)?.unwrap();
let reader = EmulateTapeReader::new(reader);
let mut reader = BlockedReader::open(reader)?;
let mut buf = [0u8; 1];
assert_eq!(reader.read(&mut buf)?, 1, "wrong byte count");

Some files were not shown because too many files have changed in this diff Show More