forked from proxmox-mirrors/proxmox-backup
Compare commits
66 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
0936e669f9 | ||
![]() |
e16ce82985 | ||
![]() |
dbc9d2c223 | ||
![]() |
3d9e0b6627 | ||
![]() |
6751d3787e | ||
![]() |
80e9b5076c | ||
![]() |
9b099edbcd | ||
![]() |
fc126d7fc1 | ||
![]() |
01d885f987 | ||
![]() |
326f5e09dd | ||
![]() |
9796c20e25 | ||
![]() |
6439200564 | ||
![]() |
53b80f64b9 | ||
![]() |
16afcb7f14 | ||
![]() |
7072e8120c | ||
![]() |
7f7a367445 | ||
![]() |
687c8fb9b1 | ||
![]() |
e88c5584bf | ||
![]() |
6cbf3e4958 | ||
![]() |
0fbc7a85e1 | ||
![]() |
d75cf3d629 | ||
![]() |
de086eda25 | ||
![]() |
dfce0d384e | ||
![]() |
2287e64ae6 | ||
![]() |
e16f063b41 | ||
![]() |
95c1673f6c | ||
![]() |
a3344b8c65 | ||
![]() |
c3fcfbbda8 | ||
![]() |
b63da904ff | ||
![]() |
386f447a42 | ||
![]() |
462b01c3ac | ||
![]() |
d56b17479d | ||
![]() |
e2d129f9de | ||
![]() |
dc051f2e30 | ||
![]() |
ccf5519a03 | ||
![]() |
892d781dbc | ||
![]() |
fa506db92d | ||
![]() |
d8320229f1 | ||
![]() |
ff14832915 | ||
![]() |
bf0290a766 | ||
![]() |
6cff4c6bf6 | ||
![]() |
7a7d973f6d | ||
![]() |
6a031a4ec8 | ||
![]() |
24734478e1 | ||
![]() |
6cc67d99a6 | ||
![]() |
b7a43abb50 | ||
![]() |
520566200a | ||
![]() |
0590aee637 | ||
![]() |
1ebab6a43f | ||
![]() |
7b7ceb0c68 | ||
![]() |
6a9a5db597 | ||
![]() |
ef8b0c2528 | ||
![]() |
6c47db412f | ||
![]() |
af63d6f9b5 | ||
![]() |
682bb42edd | ||
![]() |
5f7852f5a1 | ||
![]() |
2997cff520 | ||
![]() |
690d061d76 | ||
![]() |
7feefff6c5 | ||
![]() |
ab1f559f1e | ||
![]() |
e2cdac6e70 | ||
![]() |
6733f1283b | ||
![]() |
9eaded8572 | ||
![]() |
891f6a6fe4 | ||
![]() |
93088816c7 | ||
![]() |
f4c54e9917 |
@ -1,5 +1,5 @@
|
||||
[workspace.package]
|
||||
version = "2.4.2"
|
||||
version = "2.4.7"
|
||||
authors = [
|
||||
"Dietmar Maurer <dietmar@proxmox.com>",
|
||||
"Dominik Csapak <d.csapak@proxmox.com>",
|
||||
@ -82,9 +82,9 @@ proxmox-uuid = "1"
|
||||
# other proxmox crates
|
||||
pathpatterns = "0.1.2"
|
||||
proxmox-acme-rs = "0.4"
|
||||
proxmox-apt = "0.9.0"
|
||||
proxmox-apt = "0.9.4"
|
||||
proxmox-openid = "0.9.9"
|
||||
pxar = "0.10.2"
|
||||
pxar = "0.10.3"
|
||||
|
||||
# PBS workspace
|
||||
pbs-api-types = { path = "pbs-api-types" }
|
||||
|
3
Makefile
3
Makefile
@ -33,7 +33,7 @@ RESTORE_BIN := \
|
||||
|
||||
SUBCRATES != cargo metadata --no-deps --format-version=1 \
|
||||
| jq -r .workspace_members'[]' \
|
||||
| awk '!/^proxmox-backup\s/ { printf "%s ", $$1 }'
|
||||
| awk '!/^proxmox-backup[[:space:]]/ { printf "%s ", $$1 }'
|
||||
|
||||
ifeq ($(BUILD_MODE), release)
|
||||
CARGO_BUILD_ARGS += --release
|
||||
@ -195,6 +195,7 @@ install: $(COMPILED_BINS)
|
||||
$(foreach i,$(USR_SBIN), \
|
||||
install -m755 $(COMPILEDIR)/$(i) $(DESTDIR)$(SBINDIR)/ ; \
|
||||
install -m644 zsh-completions/_$(i) $(DESTDIR)$(ZSH_COMPL_DEST)/ ;)
|
||||
install -m755 $(COMPILEDIR)/pbs2to3 $(DESTDIR)$(SBINDIR)/
|
||||
install -dm755 $(DESTDIR)$(LIBEXECDIR)/proxmox-backup
|
||||
install -dm755 $(DESTDIR)$(LIBEXECDIR)/proxmox-backup/file-restore
|
||||
$(foreach i,$(RESTORE_BIN), \
|
||||
|
78
debian/changelog
vendored
78
debian/changelog
vendored
@ -1,3 +1,81 @@
|
||||
rust-proxmox-backup (2.4.7-1) bullseye; urgency=medium
|
||||
|
||||
* rebuild with pxar 0.10.3 for nicer error messages on pxar v2 archives
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Thu, 06 Jun 2024 13:47:23 +0200
|
||||
|
||||
rust-proxmox-backup (2.4.6-2) bullseye; urgency=medium
|
||||
|
||||
* ui: add notice for the nearing PBS 2.4 End-of-Life on 2024-07-31
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Tue, 16 Apr 2024 13:36:58 +0200
|
||||
|
||||
rust-proxmox-backup (2.4.6-1) bullseye; urgency=medium
|
||||
|
||||
* tape: fix regression in restoring an encryption key from medium, avoid
|
||||
trying to load the key to the drive, which cannot work in this special
|
||||
case.
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Thu, 01 Feb 2024 16:32:22 +0100
|
||||
|
||||
rust-proxmox-backup (2.4.5-1) bullseye; urgency=medium
|
||||
|
||||
* pbs2to3: add check for dkms modules
|
||||
|
||||
* pbs2to3: check for proper grub meta-package for bootmode
|
||||
|
||||
* system report: switch to markdown-like output syntax to make it easier to
|
||||
digest
|
||||
|
||||
* system report: add information about block devices, basic uptime, usage
|
||||
and process info, all apt repo files, proxmox-boot-tool status output, and
|
||||
ldap and oidc realm list and prune configuration
|
||||
|
||||
* tape: rework on-drive encryption key handling and ensure this key does not
|
||||
gets unloaded to early
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Wed, 24 Jan 2024 13:32:38 +0100
|
||||
|
||||
rust-proxmox-backup (2.4.4-1) bullseye; urgency=medium
|
||||
|
||||
* pbs2to3: fix boot-mode detection
|
||||
|
||||
* fix #4823: datastore: ignore vanished files when walking directory
|
||||
|
||||
* fix #4380: stat() is run when file is executed
|
||||
|
||||
* fix #4779: client: add missing "Connection" header for HTTP2 upgrade
|
||||
|
||||
* fix #4971: client: Improve output on successful snapshot deletion
|
||||
|
||||
* fix #4638: proxmox-backup-client: status: guard against div by zero
|
||||
|
||||
* 6cc67d99 ui: tape restore: fix default namespace mapping
|
||||
|
||||
* fix #4895: scheduled jobs: ignore task-log not found error
|
||||
|
||||
* ui: tape: mark incomplete media-sets as such
|
||||
|
||||
* fix #4977: ui: tape: restore: rework snapshot selection logic
|
||||
|
||||
* docs: update FAQ release support table, add PBS 2.x EOL date
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Fri, 10 Nov 2023 16:05:35 +0100
|
||||
|
||||
rust-proxmox-backup (2.4.3-1) bullseye; urgency=medium
|
||||
|
||||
* add pbs2to3 major-upgrade checker binary
|
||||
|
||||
* cargo: bump proxmox-apt to 0.9.4 to improve repository API during upgrade
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Wed, 28 Jun 2023 18:55:03 +0200
|
||||
|
||||
rust-proxmox-backup (2.4.2-2) bullseye; urgency=medium
|
||||
|
||||
* ui: tape: fix restore datastore mapping parameter construction
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Tue, 06 Jun 2023 13:16:41 +0200
|
||||
|
||||
rust-proxmox-backup (2.4.2-1) bullseye; urgency=medium
|
||||
|
||||
* docs: dark mode: adapt background for bottom links in mobile view
|
||||
|
4
debian/control
vendored
4
debian/control
vendored
@ -42,7 +42,7 @@ Build-Depends: debhelper (>= 12),
|
||||
librust-percent-encoding-2+default-dev (>= 2.1-~~),
|
||||
librust-pin-project-lite-0.2+default-dev,
|
||||
librust-proxmox-acme-rs-0.4+default-dev,
|
||||
librust-proxmox-apt-0.9+default-dev,
|
||||
librust-proxmox-apt-0.9+default-dev (>= 0.9.4),
|
||||
librust-proxmox-async-0.4+default-dev,
|
||||
librust-proxmox-auth-api-0.1+api-dev,
|
||||
librust-proxmox-auth-api-0.1+api-types-dev,
|
||||
@ -91,7 +91,7 @@ Build-Depends: debhelper (>= 12),
|
||||
librust-proxmox-time-1+default-dev (>= 1.1.2-~~),
|
||||
librust-proxmox-uuid-1+default-dev,
|
||||
librust-proxmox-uuid-1+serde-dev,
|
||||
librust-pxar-0.10+default-dev (>= 0.10.2-~~),
|
||||
librust-pxar-0.10+default-dev (>= 0.10.3-~~),
|
||||
librust-regex-1+default-dev (>= 1.5.5-~~),
|
||||
librust-rustyline-9+default-dev,
|
||||
librust-serde-1+default-dev,
|
||||
|
2
debian/proxmox-backup-server.install
vendored
2
debian/proxmox-backup-server.install
vendored
@ -11,6 +11,7 @@ usr/lib/x86_64-linux-gnu/proxmox-backup/proxmox-daily-update
|
||||
usr/lib/x86_64-linux-gnu/proxmox-backup/sg-tape-cmd
|
||||
usr/sbin/proxmox-backup-debug
|
||||
usr/sbin/proxmox-backup-manager
|
||||
usr/sbin/pbs2to3
|
||||
usr/bin/pmtx
|
||||
usr/bin/pmt
|
||||
usr/bin/proxmox-tape
|
||||
@ -24,6 +25,7 @@ usr/share/man/man1/proxmox-backup-proxy.1
|
||||
usr/share/man/man1/proxmox-tape.1
|
||||
usr/share/man/man1/pmtx.1
|
||||
usr/share/man/man1/pmt.1
|
||||
usr/share/man/man1/pbs2to3.1
|
||||
usr/share/man/man5/acl.cfg.5
|
||||
usr/share/man/man5/datastore.cfg.5
|
||||
usr/share/man/man5/domains.cfg.5
|
||||
|
@ -30,7 +30,8 @@ MAN1_PAGES := \
|
||||
proxmox-backup-client.1 \
|
||||
proxmox-backup-manager.1 \
|
||||
proxmox-file-restore.1 \
|
||||
proxmox-backup-debug.1
|
||||
proxmox-backup-debug.1 \
|
||||
pbs2to3.1 \
|
||||
|
||||
MAN5_PAGES := \
|
||||
media-pool.cfg.5 \
|
||||
|
@ -102,6 +102,7 @@ man_pages = [
|
||||
('pxar/man1', 'pxar', 'Proxmox File Archive CLI Tool', [author], 1),
|
||||
('pmt/man1', 'pmt', 'Control Linux Tape Devices', [author], 1),
|
||||
('pmtx/man1', 'pmtx', 'Control SCSI media changer devices (tape autoloaders)', [author], 1),
|
||||
('pbs2to3/man1', 'pbs2to3', 'Proxmox Backup Server upgrade checker script for 2.4+ to current 3.x major upgrades', [author], 1),
|
||||
# configs
|
||||
('config/acl/man5', 'acl.cfg', 'Access Control Configuration', [author], 5),
|
||||
('config/datastore/man5', 'datastore.cfg', 'Datastore Configuration', [author], 5),
|
||||
|
4
docs/faq-release-support-table.csv
Normal file
4
docs/faq-release-support-table.csv
Normal file
@ -0,0 +1,4 @@
|
||||
Proxmox Backup Version , Debian Version , First Release , Debian EOL , Proxmox Backup EOL
|
||||
Proxmox Backup 3 , Debian 12 (Bookworm) , 2023-06 , TBA , TBA
|
||||
Proxmox Backup 2 , Debian 11 (Bullseye) , 2021-07 , 2024-07 , 2024-07
|
||||
Proxmox Backup 1 , Debian 10 (Buster) , 2020-11 , 2022-08 , 2022-07
|
|
51
docs/faq.rst
51
docs/faq.rst
@ -21,17 +21,54 @@ Proxmox Backup Server only supports 64-bit CPUs (AMD or Intel). There are no
|
||||
future plans to support 32-bit processors.
|
||||
|
||||
|
||||
.. _faq-support-table:
|
||||
|
||||
How long will my Proxmox Backup Server version be supported?
|
||||
------------------------------------------------------------
|
||||
|
||||
+-----------------------+----------------------+---------------+------------+--------------------+
|
||||
|Proxmox Backup Version | Debian Version | First Release | Debian EOL | Proxmox Backup EOL |
|
||||
+=======================+======================+===============+============+====================+
|
||||
|Proxmox Backup 2.x | Debian 11 (Bullseye) | 2021-07 | tba | tba |
|
||||
+-----------------------+----------------------+---------------+------------+--------------------+
|
||||
|Proxmox Backup 1.x | Debian 10 (Buster) | 2020-11 | 2022-08 | 2022-07 |
|
||||
+-----------------------+----------------------+---------------+------------+--------------------+
|
||||
.. csv-table:: Table Title
|
||||
:file: faq-release-support-table.csv
|
||||
:widths: 30 26 13 13 18
|
||||
:header-rows: 1
|
||||
|
||||
How can I upgrade Proxmox Backup Server to the next point release?
|
||||
------------------------------------------------------------------
|
||||
|
||||
Minor version upgrades, for example upgrading from Proxmox Backup Server in
|
||||
version 3.1 to 3.2 or 3.3, can be done just like any normal update.
|
||||
But, you should still check the `release notes
|
||||
<https://pbs.proxmox.com/wiki/index.php/Roadmap>`_ for any relevant noteable,
|
||||
or breaking change.
|
||||
|
||||
For the update itself use either the Web UI *Node -> Updates* panel or
|
||||
through the CLI with:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
apt update
|
||||
apt full-upgrade
|
||||
|
||||
.. note:: Always ensure you correctly setup the
|
||||
:ref:`package repositories <sysadmin_package_repositories>` and only
|
||||
continue with the actual upgrade if `apt update` did not hit any error.
|
||||
|
||||
.. _faq-upgrade-major:
|
||||
|
||||
How can I upgrade Proxmox Backup Server to the next major release?
|
||||
------------------------------------------------------------------
|
||||
|
||||
Major version upgrades, for example going from Proxmox Backup Server 2.4 to
|
||||
3.1, are also supported.
|
||||
They must be carefully planned and tested and should **never** be started
|
||||
without having an off-site copy of the important backups, e.g., via remote sync
|
||||
or tape, ready.
|
||||
|
||||
Although the specific upgrade steps depend on your respective setup, we provide
|
||||
general instructions and advice of how a upgrade should be performed:
|
||||
|
||||
* `Upgrade from Proxmox Backup Server 2 to 3 <https://pbs.proxmox.com/wiki/index.php/Upgrade_from_2_to_3>`_
|
||||
|
||||
* `Upgrade from Proxmox Backup Server 1 to 2 <https://pbs.proxmox.com/wiki/index.php/Upgrade_from_1.1_to_2.x>`_
|
||||
|
||||
Can I copy or synchronize my datastore to another location?
|
||||
-----------------------------------------------------------
|
||||
|
14
docs/pbs2to3/man1.rst
Normal file
14
docs/pbs2to3/man1.rst
Normal file
@ -0,0 +1,14 @@
|
||||
|
||||
=======
|
||||
pbs2to3
|
||||
=======
|
||||
|
||||
Description
|
||||
===========
|
||||
|
||||
This tool will help you to detect common pitfalls and misconfguration before,
|
||||
and during the upgrade of a Proxmox VE system Any failure must be addressed
|
||||
before the upgrade, and any waring must be addressed, or at least carefully
|
||||
evaluated, if a false-positive is suspected
|
||||
|
||||
.. include:: ../pbs-copyright.rst
|
@ -764,6 +764,8 @@ impl HttpClient {
|
||||
);
|
||||
}
|
||||
|
||||
req.headers_mut()
|
||||
.insert("Connection", HeaderValue::from_str("upgrade").unwrap());
|
||||
req.headers_mut()
|
||||
.insert("UPGRADE", HeaderValue::from_str(&protocol_name).unwrap());
|
||||
|
||||
|
@ -434,6 +434,15 @@ impl Archiver {
|
||||
assert_single_path_component(os_file_name)?;
|
||||
let full_path = self.path.join(os_file_name);
|
||||
|
||||
let match_path = PathBuf::from("/").join(full_path.clone());
|
||||
if self
|
||||
.patterns
|
||||
.matches(match_path.as_os_str().as_bytes(), None)
|
||||
== Some(MatchType::Exclude)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let stat = match nix::sys::stat::fstatat(
|
||||
dir_fd,
|
||||
file_name.as_c_str(),
|
||||
@ -444,15 +453,6 @@ impl Archiver {
|
||||
Err(err) => bail!("stat failed on {:?}: {}", full_path, err),
|
||||
};
|
||||
|
||||
let match_path = PathBuf::from("/").join(full_path.clone());
|
||||
if self
|
||||
.patterns
|
||||
.matches(match_path.as_os_str().as_bytes(), Some(stat.st_mode))
|
||||
== Some(MatchType::Exclude)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
self.entry_counter += 1;
|
||||
if self.entry_counter > self.entry_limit {
|
||||
bail!(
|
||||
|
@ -866,26 +866,26 @@ impl DataStore {
|
||||
.unwrap_or(false)
|
||||
}
|
||||
let handle_entry_err = |err: walkdir::Error| {
|
||||
if let Some(inner) = err.io_error() {
|
||||
if let Some(path) = err.path() {
|
||||
if inner.kind() == io::ErrorKind::PermissionDenied {
|
||||
// only allow to skip ext4 fsck directory, avoid GC if, for example,
|
||||
// a user got file permissions wrong on datastore rsync to new server
|
||||
if err.depth() > 1 || !path.ends_with("lost+found") {
|
||||
bail!("cannot continue garbage-collection safely, permission denied on: {:?}", path)
|
||||
}
|
||||
} else {
|
||||
bail!(
|
||||
"unexpected error on datastore traversal: {} - {:?}",
|
||||
inner,
|
||||
path
|
||||
)
|
||||
}
|
||||
} else {
|
||||
bail!("unexpected error on datastore traversal: {}", inner)
|
||||
// first, extract the actual IO error and the affected path
|
||||
let (inner, path) = match (err.io_error(), err.path()) {
|
||||
(None, _) => return Ok(()), // not an IO-error
|
||||
(Some(inner), Some(path)) => (inner, path),
|
||||
(Some(inner), None) => bail!("unexpected error on datastore traversal: {inner}"),
|
||||
};
|
||||
if inner.kind() == io::ErrorKind::PermissionDenied {
|
||||
if err.depth() <= 1 && path.ends_with("lost+found") {
|
||||
// allow skipping of (root-only) ext4 fsck-directory on EPERM ..
|
||||
return Ok(());
|
||||
}
|
||||
// .. but do not ignore EPERM in general, otherwise we might prune too many chunks.
|
||||
// E.g., if users messed up with owner/perms on a rsync
|
||||
bail!("cannot continue garbage-collection safely, permission denied on: {path:?}");
|
||||
} else if inner.kind() == io::ErrorKind::NotFound {
|
||||
log::info!("ignoring vanished file: {path:?}");
|
||||
return Ok(());
|
||||
} else {
|
||||
bail!("unexpected error on datastore traversal: {inner} - {path:?}");
|
||||
}
|
||||
Ok(())
|
||||
};
|
||||
for entry in walker.filter_entry(|e| !is_hidden(e)) {
|
||||
let path = match entry {
|
||||
|
@ -14,6 +14,7 @@ lazy_static.workspace = true
|
||||
libc.workspace = true
|
||||
log.workspace = true
|
||||
nix.workspace = true
|
||||
openssl.workspace = true
|
||||
regex.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
|
@ -9,9 +9,10 @@ use endian_trait::Endian;
|
||||
use nix::fcntl::{fcntl, FcntlArg, OFlag};
|
||||
|
||||
mod encryption;
|
||||
pub use encryption::*;
|
||||
pub use encryption::{drive_set_encryption, drive_get_encryption};
|
||||
|
||||
mod volume_statistics;
|
||||
use proxmox_uuid::Uuid;
|
||||
pub use volume_statistics::*;
|
||||
|
||||
mod tape_alert_flags;
|
||||
@ -26,8 +27,9 @@ pub use report_density::*;
|
||||
use proxmox_io::{ReadExt, WriteExt};
|
||||
use proxmox_sys::error::SysResult;
|
||||
|
||||
use pbs_api_types::{Lp17VolumeStatistics, LtoDriveAndMediaStatus, MamAttribute};
|
||||
use pbs_api_types::{Lp17VolumeStatistics, LtoDriveAndMediaStatus, LtoTapeDrive, MamAttribute};
|
||||
|
||||
use crate::linux_list_drives::open_lto_tape_device;
|
||||
use crate::{
|
||||
sgutils2::{
|
||||
alloc_page_aligned_buffer, scsi_cmd_mode_select10, scsi_cmd_mode_select6, scsi_inquiry,
|
||||
@ -102,7 +104,6 @@ pub struct SgTape {
|
||||
file: File,
|
||||
locate_offset: Option<i64>,
|
||||
info: InquiryInfo,
|
||||
encryption_key_loaded: bool,
|
||||
}
|
||||
|
||||
impl SgTape {
|
||||
@ -124,11 +125,47 @@ impl SgTape {
|
||||
Ok(Self {
|
||||
file,
|
||||
info,
|
||||
encryption_key_loaded: false,
|
||||
locate_offset: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Open a tape device
|
||||
///
|
||||
/// This does additional checks:
|
||||
///
|
||||
/// - check if it is a non-rewinding tape device
|
||||
/// - check if drive is ready (tape loaded)
|
||||
/// - check block size
|
||||
/// - for autoloader only, try to reload ejected tapes
|
||||
pub fn open_lto_drive(config: &LtoTapeDrive) -> Result<Self, Error> {
|
||||
proxmox_lang::try_block!({
|
||||
let file = open_lto_tape_device(&config.path)?;
|
||||
|
||||
let mut handle = SgTape::new(file)?;
|
||||
|
||||
if handle.test_unit_ready().is_err() {
|
||||
// for autoloader only, try to reload ejected tapes
|
||||
if config.changer.is_some() {
|
||||
let _ = handle.load(); // just try, ignore error
|
||||
}
|
||||
}
|
||||
|
||||
handle.wait_until_ready()?;
|
||||
|
||||
handle.set_default_options()?;
|
||||
|
||||
Ok(handle)
|
||||
})
|
||||
.map_err(|err: Error| {
|
||||
format_err!(
|
||||
"open drive '{}' ({}) failed - {}",
|
||||
config.name,
|
||||
config.path,
|
||||
err
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Access to file descriptor - useful for testing
|
||||
pub fn file_mut(&mut self) -> &mut File {
|
||||
&mut self.file
|
||||
@ -603,10 +640,28 @@ impl SgTape {
|
||||
read_volume_statistics(&mut self.file)
|
||||
}
|
||||
|
||||
pub fn set_encryption(&mut self, key: Option<[u8; 32]>) -> Result<(), Error> {
|
||||
self.encryption_key_loaded = key.is_some();
|
||||
pub fn set_encryption(&mut self, key_data: Option<([u8; 32], Uuid)>) -> Result<(), Error> {
|
||||
let key = if let Some((ref key, ref uuid)) = key_data {
|
||||
// derive specialized key for each media-set
|
||||
|
||||
set_encryption(&mut self.file, key)
|
||||
let mut tape_key = [0u8; 32];
|
||||
|
||||
let uuid_bytes: [u8; 16] = *uuid.as_bytes();
|
||||
|
||||
openssl::pkcs5::pbkdf2_hmac(
|
||||
key,
|
||||
&uuid_bytes,
|
||||
10,
|
||||
openssl::hash::MessageDigest::sha256(),
|
||||
&mut tape_key,
|
||||
)?;
|
||||
|
||||
Some(tape_key)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
drive_set_encryption(&mut self.file, key)
|
||||
}
|
||||
|
||||
// Note: use alloc_page_aligned_buffer to alloc data transfer buffer
|
||||
@ -960,15 +1015,6 @@ impl SgTape {
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for SgTape {
|
||||
fn drop(&mut self) {
|
||||
// For security reasons, clear the encryption key
|
||||
if self.encryption_key_loaded {
|
||||
let _ = self.set_encryption(None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SgTapeReader<'a> {
|
||||
sg_tape: &'a mut SgTape,
|
||||
end_of_file: bool,
|
||||
|
@ -8,21 +8,10 @@ use proxmox_io::{ReadExt, WriteExt};
|
||||
|
||||
use crate::sgutils2::{alloc_page_aligned_buffer, SgRaw};
|
||||
|
||||
/// Test if drive supports hardware encryption
|
||||
///
|
||||
/// We search for AES_GCM algorithm with 256bits key.
|
||||
pub fn has_encryption<F: AsRawFd>(file: &mut F) -> bool {
|
||||
let data = match sg_spin_data_encryption_caps(file) {
|
||||
Ok(data) => data,
|
||||
Err(_) => return false,
|
||||
};
|
||||
decode_spin_data_encryption_caps(&data).is_ok()
|
||||
}
|
||||
|
||||
/// Set or clear encryption key
|
||||
///
|
||||
/// We always use mixed mode,
|
||||
pub fn set_encryption<F: AsRawFd>(file: &mut F, key: Option<[u8; 32]>) -> Result<(), Error> {
|
||||
pub fn drive_set_encryption<F: AsRawFd>(file: &mut F, key: Option<[u8; 32]>) -> Result<(), Error> {
|
||||
let data = match sg_spin_data_encryption_caps(file) {
|
||||
Ok(data) => data,
|
||||
Err(_) if key.is_none() => {
|
||||
@ -57,6 +46,27 @@ pub fn set_encryption<F: AsRawFd>(file: &mut F, key: Option<[u8; 32]>) -> Result
|
||||
bail!("got unexpected encryption mode {:?}", status.mode);
|
||||
}
|
||||
|
||||
/// Returns if encryption is enabled on the drive
|
||||
pub fn drive_get_encryption<F: AsRawFd>(file: &mut F) -> Result<bool, Error> {
|
||||
let data = match sg_spin_data_encryption_status(file) {
|
||||
Ok(data) => data,
|
||||
Err(_) => {
|
||||
// Assume device does not support HW encryption
|
||||
return Ok(false);
|
||||
}
|
||||
};
|
||||
let status = decode_spin_data_encryption_status(&data)?;
|
||||
match status.mode {
|
||||
// these three below have all encryption enabled, and only differ in how decryption is
|
||||
// handled
|
||||
DataEncryptionMode::On => Ok(true),
|
||||
DataEncryptionMode::Mixed => Ok(true),
|
||||
DataEncryptionMode::RawRead => Ok(true),
|
||||
// currently, the mode below is the only one that has encryption actually disabled
|
||||
DataEncryptionMode::Off => Ok(false),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Endian)]
|
||||
#[repr(C, packed)]
|
||||
struct SspSetDataEncryptionPage {
|
||||
@ -187,7 +197,7 @@ fn sg_spin_data_encryption_caps<F: AsRawFd>(file: &mut F) -> Result<Vec<u8>, Err
|
||||
.map(|v| v.to_vec())
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
enum DataEncryptionMode {
|
||||
On,
|
||||
Mixed,
|
||||
|
@ -1590,9 +1590,12 @@ async fn status(param: Value) -> Result<Value, Error> {
|
||||
let v = v.as_u64().unwrap();
|
||||
let total = record["total"].as_u64().unwrap();
|
||||
let roundup = total / 200;
|
||||
let per = ((v + roundup) * 100) / total;
|
||||
let info = format!(" ({} %)", per);
|
||||
Ok(format!("{} {:>8}", v, info))
|
||||
if let Some(per) = ((v + roundup) * 100).checked_div(total) {
|
||||
let info = format!(" ({} %)", per);
|
||||
Ok(format!("{} {:>8}", v, info))
|
||||
} else {
|
||||
bail!("Cannot render total percentage: denominator is zero");
|
||||
}
|
||||
};
|
||||
|
||||
let options = default_table_format_options()
|
||||
|
@ -188,13 +188,13 @@ async fn forget_snapshots(param: Value) -> Result<Value, Error> {
|
||||
|
||||
let path = format!("api2/json/admin/datastore/{}/snapshots", repo.store());
|
||||
|
||||
let result = client
|
||||
client
|
||||
.delete(&path, Some(snapshot_args(&backup_ns, &snapshot)?))
|
||||
.await?;
|
||||
|
||||
record_repository(&repo);
|
||||
|
||||
Ok(result)
|
||||
Ok(Value::Null)
|
||||
}
|
||||
|
||||
#[api(
|
||||
|
@ -36,8 +36,7 @@ use crate::{
|
||||
changer::update_changer_online_status,
|
||||
drive::{
|
||||
get_tape_device_state, lock_tape_device, media_changer, open_drive,
|
||||
open_lto_tape_drive, required_media_changer, set_tape_device_state, LtoTapeHandle,
|
||||
TapeDriver,
|
||||
required_media_changer, set_tape_device_state, LtoTapeHandle, TapeDriver,
|
||||
},
|
||||
encryption_keys::insert_key,
|
||||
file_formats::{MediaLabel, MediaSetLabel},
|
||||
@ -610,7 +609,7 @@ pub async fn restore_key(drive: String, password: String) -> Result<(), Error> {
|
||||
run_drive_blocking_task(drive.clone(), "restore key".to_string(), move |config| {
|
||||
let mut drive = open_drive(&config, &drive)?;
|
||||
|
||||
let (_media_id, key_config) = drive.read_label()?;
|
||||
let (_media_id, key_config) = drive.read_label_without_loading_key()?;
|
||||
|
||||
if let Some(key_config) = key_config {
|
||||
let password_fn = || Ok(password.as_bytes().to_vec());
|
||||
@ -657,9 +656,6 @@ pub async fn read_label(drive: String, inventorize: Option<bool>) -> Result<Medi
|
||||
let label = if let Some(ref set) = media_id.media_set_label {
|
||||
let key = &set.encryption_key_fingerprint;
|
||||
|
||||
if let Err(err) = drive.set_encryption(key.clone().map(|fp| (fp, set.uuid.clone()))) {
|
||||
eprintln!("unable to load encryption key: {}", err); // best-effort only
|
||||
}
|
||||
MediaIdFlat {
|
||||
ctime: media_id.label.ctime,
|
||||
encryption_key_fingerprint: key.as_ref().map(|fp| fp.signature()),
|
||||
@ -1144,7 +1140,7 @@ pub async fn cartridge_memory(drive: String) -> Result<Vec<MamAttribute>, Error>
|
||||
"reading cartridge memory".to_string(),
|
||||
move |config| {
|
||||
let drive_config: LtoTapeDrive = config.lookup("lto", &drive)?;
|
||||
let mut handle = open_lto_tape_drive(&drive_config)?;
|
||||
let mut handle = LtoTapeHandle::open_lto_drive(&drive_config)?;
|
||||
|
||||
handle.cartridge_memory()
|
||||
},
|
||||
@ -1174,7 +1170,7 @@ pub async fn volume_statistics(drive: String) -> Result<Lp17VolumeStatistics, Er
|
||||
"reading volume statistics".to_string(),
|
||||
move |config| {
|
||||
let drive_config: LtoTapeDrive = config.lookup("lto", &drive)?;
|
||||
let mut handle = open_lto_tape_drive(&drive_config)?;
|
||||
let mut handle = LtoTapeHandle::open_lto_drive(&drive_config)?;
|
||||
|
||||
handle.volume_statistics()
|
||||
},
|
||||
@ -1311,12 +1307,6 @@ pub fn catalog_media(
|
||||
inventory.store(media_id.clone(), false)?;
|
||||
return Ok(());
|
||||
}
|
||||
let encrypt_fingerprint = set
|
||||
.encryption_key_fingerprint
|
||||
.clone()
|
||||
.map(|fp| (fp, set.uuid.clone()));
|
||||
|
||||
drive.set_encryption(encrypt_fingerprint)?;
|
||||
|
||||
let _pool_lock = lock_media_pool(TAPE_STATUS_DIR, &set.pool)?;
|
||||
let media_set_lock = lock_media_set(TAPE_STATUS_DIR, &set.uuid, None)?;
|
||||
|
@ -1029,12 +1029,6 @@ fn restore_snapshots_to_tmpdir(
|
||||
media_set_uuid
|
||||
);
|
||||
}
|
||||
let encrypt_fingerprint = set.encryption_key_fingerprint.clone().map(|fp| {
|
||||
task_log!(worker, "Encryption key fingerprint: {}", fp);
|
||||
(fp, set.uuid.clone())
|
||||
});
|
||||
|
||||
drive.set_encryption(encrypt_fingerprint)?;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1279,12 +1273,6 @@ pub fn request_and_restore_media(
|
||||
media_set_uuid
|
||||
);
|
||||
}
|
||||
let encrypt_fingerprint = set
|
||||
.encryption_key_fingerprint
|
||||
.clone()
|
||||
.map(|fp| (fp, set.uuid.clone()));
|
||||
|
||||
drive.set_encryption(encrypt_fingerprint)?;
|
||||
}
|
||||
}
|
||||
|
||||
|
633
src/bin/pbs2to3.rs
Normal file
633
src/bin/pbs2to3.rs
Normal file
@ -0,0 +1,633 @@
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::{format_err, Error};
|
||||
use regex::Regex;
|
||||
use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor};
|
||||
|
||||
use proxmox_apt::repositories::{self, APTRepositoryFile, APTRepositoryPackageType};
|
||||
use proxmox_backup::api2::node::apt;
|
||||
|
||||
const OLD_SUITE: &str = "bullseye";
|
||||
const NEW_SUITE: &str = "bookworm";
|
||||
const PROXMOX_BACKUP_META: &str = "proxmox-backup";
|
||||
const MIN_PBS_MAJOR: u8 = 2;
|
||||
const MIN_PBS_MINOR: u8 = 4;
|
||||
const MIN_PBS_PKGREL: u8 = 1;
|
||||
|
||||
fn main() -> Result<(), Error> {
|
||||
let mut checker = Checker::new();
|
||||
checker.check_pbs_packages()?;
|
||||
checker.check_misc()?;
|
||||
checker.summary()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct Checker {
|
||||
output: ConsoleOutput,
|
||||
upgraded: bool,
|
||||
}
|
||||
|
||||
impl Checker {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
output: ConsoleOutput::new(),
|
||||
upgraded: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn check_pbs_packages(&mut self) -> Result<(), Error> {
|
||||
self.output
|
||||
.print_header("CHECKING VERSION INFORMATION FOR PBS PACKAGES")?;
|
||||
|
||||
self.check_upgradable_packages()?;
|
||||
let pkg_versions = apt::get_versions()?;
|
||||
self.check_meta_package_version(&pkg_versions)?;
|
||||
self.check_kernel_compat(&pkg_versions)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn check_upgradable_packages(&mut self) -> Result<(), Error> {
|
||||
self.output.log_info("Checking for package updates..")?;
|
||||
|
||||
let result = Self::get_upgradable_packages();
|
||||
match result {
|
||||
Err(err) => {
|
||||
self.output.log_warn(format!("{err}"))?;
|
||||
self.output
|
||||
.log_fail("unable to retrieve list of package updates!")?;
|
||||
}
|
||||
Ok(cache) => {
|
||||
if cache.package_status.is_empty() {
|
||||
self.output.log_pass("all packages up-to-date")?;
|
||||
} else {
|
||||
let pkgs = cache
|
||||
.package_status
|
||||
.iter()
|
||||
.map(|pkg| pkg.package.clone())
|
||||
.collect::<Vec<String>>()
|
||||
.join(", ");
|
||||
self.output.log_warn(format!(
|
||||
"updates for the following packages are available:\n {pkgs}",
|
||||
))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn check_meta_package_version(
|
||||
&mut self,
|
||||
pkg_versions: &[pbs_api_types::APTUpdateInfo],
|
||||
) -> Result<(), Error> {
|
||||
self.output
|
||||
.log_info("Checking proxmox backup server package version..")?;
|
||||
|
||||
let pbs_meta_pkg = pkg_versions
|
||||
.iter()
|
||||
.find(|pkg| pkg.package.as_str() == PROXMOX_BACKUP_META);
|
||||
|
||||
if let Some(pbs_meta_pkg) = pbs_meta_pkg {
|
||||
let pkg_version = Regex::new(r"^(\d+)\.(\d+)[.-](\d+)")?;
|
||||
let captures = pkg_version.captures(&pbs_meta_pkg.old_version);
|
||||
if let Some(captures) = captures {
|
||||
let maj = Self::extract_version_from_captures(1, &captures)?;
|
||||
let min = Self::extract_version_from_captures(2, &captures)?;
|
||||
let pkgrel = Self::extract_version_from_captures(3, &captures)?;
|
||||
|
||||
if maj > MIN_PBS_MAJOR {
|
||||
self.output
|
||||
.log_pass(format!("Already upgraded to Proxmox Backup Server {maj}"))?;
|
||||
self.upgraded = true;
|
||||
} else if maj >= MIN_PBS_MAJOR && min >= MIN_PBS_MINOR && pkgrel >= MIN_PBS_PKGREL {
|
||||
self.output.log_pass(format!(
|
||||
"'{}' has version >= {}.{}-{}",
|
||||
PROXMOX_BACKUP_META, MIN_PBS_MAJOR, MIN_PBS_MINOR, MIN_PBS_PKGREL,
|
||||
))?;
|
||||
} else {
|
||||
self.output.log_fail(format!(
|
||||
"'{}' package is too old, please upgrade to >= {}.{}-{}",
|
||||
PROXMOX_BACKUP_META, MIN_PBS_MAJOR, MIN_PBS_MINOR, MIN_PBS_PKGREL,
|
||||
))?;
|
||||
}
|
||||
} else {
|
||||
self.output.log_fail(format!(
|
||||
"could not match the '{PROXMOX_BACKUP_META}' package version, \
|
||||
is it installed?",
|
||||
))?;
|
||||
}
|
||||
} else {
|
||||
self.output
|
||||
.log_fail(format!("'{PROXMOX_BACKUP_META}' package not found!"))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn check_kernel_compat(
|
||||
&mut self,
|
||||
pkg_versions: &[pbs_api_types::APTUpdateInfo],
|
||||
) -> Result<(), Error> {
|
||||
self.output.log_info("Check running kernel version..")?;
|
||||
let (krunning, kinstalled) = if self.upgraded {
|
||||
(
|
||||
Regex::new(r"^6\.(?:2\.(?:[2-9]\d+|1[6-8]|1\d\d+)|5)[^~]*$")?,
|
||||
"pve-kernel-6.2",
|
||||
)
|
||||
} else {
|
||||
(Regex::new(r"^(?:5\.(?:13|15)|6\.2)")?, "pve-kernel-5.15")
|
||||
};
|
||||
|
||||
let output = std::process::Command::new("uname").arg("-r").output();
|
||||
match output {
|
||||
Err(_err) => self
|
||||
.output
|
||||
.log_fail("unable to determine running kernel version.")?,
|
||||
Ok(ret) => {
|
||||
let running_version = std::str::from_utf8(&ret.stdout[..ret.stdout.len() - 1])?;
|
||||
if krunning.is_match(running_version) {
|
||||
if self.upgraded {
|
||||
self.output.log_pass(format!(
|
||||
"running new kernel '{running_version}' after upgrade."
|
||||
))?;
|
||||
} else {
|
||||
self.output.log_pass(format!(
|
||||
"running kernel '{running_version}' is considered suitable for \
|
||||
upgrade."
|
||||
))?;
|
||||
}
|
||||
} else {
|
||||
let installed_kernel = pkg_versions
|
||||
.iter()
|
||||
.find(|pkg| pkg.package.as_str() == kinstalled);
|
||||
if installed_kernel.is_some() {
|
||||
self.output.log_warn(format!(
|
||||
"a suitable kernel '{kinstalled}' is installed, but an \
|
||||
unsuitable '{running_version}' is booted, missing reboot?!",
|
||||
))?;
|
||||
} else {
|
||||
self.output.log_warn(format!(
|
||||
"unexpected running and installed kernel '{running_version}'.",
|
||||
))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn extract_version_from_captures(
|
||||
index: usize,
|
||||
captures: ®ex::Captures,
|
||||
) -> Result<u8, Error> {
|
||||
if let Some(capture) = captures.get(index) {
|
||||
let val = capture.as_str().parse::<u8>()?;
|
||||
Ok(val)
|
||||
} else {
|
||||
Ok(0)
|
||||
}
|
||||
}
|
||||
|
||||
fn check_bootloader(&mut self) -> Result<(), Error> {
|
||||
self.output
|
||||
.log_info("Checking bootloader configuration...")?;
|
||||
|
||||
if !Path::new("/sys/firmware/efi").is_dir() {
|
||||
self.output
|
||||
.log_skip("System booted in legacy-mode - no need for systemd-boot")?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if Path::new("/etc/kernel/proxmox-boot-uuids").is_file() {
|
||||
// PBS packages version check needs to be run before
|
||||
if !self.upgraded {
|
||||
self.output
|
||||
.log_skip("not yet upgraded, no need to check the presence of systemd-boot")?;
|
||||
return Ok(());
|
||||
}
|
||||
if Path::new("/usr/share/doc/systemd-boot/changelog.Debian.gz").is_file() {
|
||||
self.output.log_pass("bootloader packages installed correctly")?;
|
||||
return Ok(());
|
||||
}
|
||||
self.output.log_warn(
|
||||
"proxmox-boot-tool is used for bootloader configuration in uefi mode \
|
||||
but the separate systemd-boot package, is not installed.\n\
|
||||
initializing new ESPs will not work unitl the package is installed.",
|
||||
)?;
|
||||
return Ok(());
|
||||
} else if !Path::new("/usr/share/doc/grub-efi-amd64/changelog.Debian.gz").is_file() {
|
||||
self.output.log_warn(
|
||||
"System booted in uefi mode but grub-efi-amd64 meta-package not installed, \
|
||||
new grub versions will not be installed to /boot/efi!
|
||||
Install grub-efi-amd64."
|
||||
)?;
|
||||
return Ok(());
|
||||
} else {
|
||||
self.output.log_pass("bootloader packages installed correctly")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn check_apt_repos(&mut self) -> Result<(), Error> {
|
||||
self.output
|
||||
.log_info("Checking for package repository suite mismatches..")?;
|
||||
|
||||
let mut strange_suite = false;
|
||||
let mut mismatches = Vec::new();
|
||||
let mut found_suite: Option<(String, String)> = None;
|
||||
|
||||
let (repo_files, _repo_errors, _digest) = repositories::repositories()?;
|
||||
for repo_file in repo_files {
|
||||
self.check_repo_file(
|
||||
&mut found_suite,
|
||||
&mut mismatches,
|
||||
&mut strange_suite,
|
||||
repo_file,
|
||||
)?;
|
||||
}
|
||||
|
||||
match (mismatches.is_empty(), strange_suite) {
|
||||
(true, false) => self.output.log_pass("found no suite mismatch")?,
|
||||
(true, true) => self
|
||||
.output
|
||||
.log_notice("found no suite mismatches, but found at least one strange suite")?,
|
||||
(false, _) => {
|
||||
let mut message = String::from(
|
||||
"Found mixed old and new packages repository suites, fix before upgrading!\
|
||||
\n Mismatches:",
|
||||
);
|
||||
for (suite, location) in mismatches.iter() {
|
||||
message.push_str(
|
||||
format!("\n found suite '{suite}' at '{location}'").as_str(),
|
||||
);
|
||||
}
|
||||
message.push('\n');
|
||||
self.output.log_fail(message)?
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn check_dkms_modules(&mut self) -> Result<(), Error> {
|
||||
let kver = std::process::Command::new("uname")
|
||||
.arg("-r")
|
||||
.output()
|
||||
.map_err(|err| format_err!("failed to retrieve running kernel version - {err}"))?;
|
||||
|
||||
let output = std::process::Command::new("dkms")
|
||||
.arg("status")
|
||||
.arg("-k")
|
||||
.arg(std::str::from_utf8(&kver.stdout)?)
|
||||
.output();
|
||||
match output {
|
||||
Err(_err) => self.output.log_skip("could not get dkms status")?,
|
||||
Ok(ret) => {
|
||||
let num_dkms_modules = std::str::from_utf8(&ret.stdout)?.lines().count();
|
||||
if num_dkms_modules == 0 {
|
||||
self.output.log_pass("no dkms modules found")?;
|
||||
} else {
|
||||
self.output.log_warn("dkms modules found, this might cause issues during upgrade.")?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn check_misc(&mut self) -> Result<(), Error> {
|
||||
self.output.print_header("MISCELLANEOUS CHECKS")?;
|
||||
self.check_pbs_services()?;
|
||||
self.check_time_sync()?;
|
||||
self.check_apt_repos()?;
|
||||
self.check_bootloader()?;
|
||||
self.check_dkms_modules()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn summary(&mut self) -> Result<(), Error> {
|
||||
self.output.print_summary()
|
||||
}
|
||||
|
||||
fn check_repo_file(
|
||||
&mut self,
|
||||
found_suite: &mut Option<(String, String)>,
|
||||
mismatches: &mut Vec<(String, String)>,
|
||||
strange_suite: &mut bool,
|
||||
repo_file: APTRepositoryFile,
|
||||
) -> Result<(), Error> {
|
||||
for repo in repo_file.repositories {
|
||||
if !repo.enabled || repo.types == [APTRepositoryPackageType::DebSrc] {
|
||||
continue;
|
||||
}
|
||||
for suite in &repo.suites {
|
||||
let suite = match suite.find(&['-', '/'][..]) {
|
||||
Some(n) => &suite[0..n],
|
||||
None => suite,
|
||||
};
|
||||
|
||||
if suite != OLD_SUITE && suite != NEW_SUITE {
|
||||
let location = repo_file.path.clone().unwrap_or_default();
|
||||
self.output.log_notice(format!(
|
||||
"found unusual suite '{suite}', neither old '{OLD_SUITE}' nor new \
|
||||
'{NEW_SUITE}'..\n Affected file {location}\n Please \
|
||||
assure this is shipping compatible packages for the upgrade!"
|
||||
))?;
|
||||
*strange_suite = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some((ref current_suite, ref current_location)) = found_suite {
|
||||
let location = repo_file.path.clone().unwrap_or_default();
|
||||
if suite != current_suite {
|
||||
if mismatches.is_empty() {
|
||||
mismatches.push((current_suite.clone(), current_location.clone()));
|
||||
mismatches.push((suite.to_string(), location));
|
||||
} else {
|
||||
mismatches.push((suite.to_string(), location));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let location = repo_file.path.clone().unwrap_or_default();
|
||||
*found_suite = Some((suite.to_string(), location));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_systemd_unit_state(
|
||||
&mut self,
|
||||
unit: &str,
|
||||
) -> Result<(SystemdUnitState, SystemdUnitState), Error> {
|
||||
let output = std::process::Command::new("systemctl")
|
||||
.arg("is-enabled")
|
||||
.arg(unit)
|
||||
.output()
|
||||
.map_err(|err| format_err!("failed to execute - {err}"))?;
|
||||
|
||||
let enabled_state = match output.stdout.as_slice() {
|
||||
b"enabled\n" => SystemdUnitState::Enabled,
|
||||
b"disabled\n" => SystemdUnitState::Disabled,
|
||||
_ => SystemdUnitState::Unknown,
|
||||
};
|
||||
|
||||
let output = std::process::Command::new("systemctl")
|
||||
.arg("is-active")
|
||||
.arg(unit)
|
||||
.output()
|
||||
.map_err(|err| format_err!("failed to execute - {err}"))?;
|
||||
|
||||
let active_state = match output.stdout.as_slice() {
|
||||
b"active\n" => SystemdUnitState::Active,
|
||||
b"inactive\n" => SystemdUnitState::Inactive,
|
||||
b"failed\n" => SystemdUnitState::Failed,
|
||||
_ => SystemdUnitState::Unknown,
|
||||
};
|
||||
Ok((enabled_state, active_state))
|
||||
}
|
||||
|
||||
fn check_pbs_services(&mut self) -> Result<(), Error> {
|
||||
self.output.log_info("Checking PBS daemon services..")?;
|
||||
|
||||
for service in ["proxmox-backup.service", "proxmox-backup-proxy.service"] {
|
||||
match self.get_systemd_unit_state(service)? {
|
||||
(_, SystemdUnitState::Active) => {
|
||||
self.output
|
||||
.log_pass(format!("systemd unit '{service}' is in state 'active'"))?;
|
||||
}
|
||||
(_, SystemdUnitState::Inactive) => {
|
||||
self.output.log_fail(format!(
|
||||
"systemd unit '{service}' is in state 'inactive'\
|
||||
\n Please check the service for errors and start it.",
|
||||
))?;
|
||||
}
|
||||
(_, SystemdUnitState::Failed) => {
|
||||
self.output.log_fail(format!(
|
||||
"systemd unit '{service}' is in state 'failed'\
|
||||
\n Please check the service for errors and start it.",
|
||||
))?;
|
||||
}
|
||||
(_, _) => {
|
||||
self.output.log_fail(format!(
|
||||
"systemd unit '{service}' is not in state 'active'\
|
||||
\n Please check the service for errors and start it.",
|
||||
))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn check_time_sync(&mut self) -> Result<(), Error> {
|
||||
self.output
|
||||
.log_info("Checking for supported & active NTP service..")?;
|
||||
if self.get_systemd_unit_state("systemd-timesyncd.service")?.1 == SystemdUnitState::Active {
|
||||
self.output.log_warn(
|
||||
"systemd-timesyncd is not the best choice for time-keeping on servers, due to only \
|
||||
applying updates on boot.\
|
||||
\n While not necessary for the upgrade it's recommended to use one of:\
|
||||
\n * chrony (Default in new Proxmox Backup Server installations)\
|
||||
\n * ntpsec\
|
||||
\n * openntpd"
|
||||
)?;
|
||||
} else if self.get_systemd_unit_state("ntp.service")?.1 == SystemdUnitState::Active {
|
||||
self.output.log_info(
|
||||
"Debian deprecated and removed the ntp package for Bookworm, but the system \
|
||||
will automatically migrate to the 'ntpsec' replacement package on upgrade.",
|
||||
)?;
|
||||
} else if self.get_systemd_unit_state("chrony.service")?.1 == SystemdUnitState::Active
|
||||
|| self.get_systemd_unit_state("openntpd.service")?.1 == SystemdUnitState::Active
|
||||
|| self.get_systemd_unit_state("ntpsec.service")?.1 == SystemdUnitState::Active
|
||||
{
|
||||
self.output
|
||||
.log_pass("Detected active time synchronisation unit")?;
|
||||
} else {
|
||||
self.output.log_warn(
|
||||
"No (active) time synchronisation daemon (NTP) detected, but synchronized systems \
|
||||
are important!",
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_upgradable_packages() -> Result<proxmox_backup::tools::apt::PkgState, Error> {
|
||||
let cache = if let Ok(false) = proxmox_backup::tools::apt::pkg_cache_expired() {
|
||||
if let Ok(Some(cache)) = proxmox_backup::tools::apt::read_pkg_state() {
|
||||
cache
|
||||
} else {
|
||||
proxmox_backup::tools::apt::update_cache()?
|
||||
}
|
||||
} else {
|
||||
proxmox_backup::tools::apt::update_cache()?
|
||||
};
|
||||
|
||||
Ok(cache)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
enum SystemdUnitState {
|
||||
Active,
|
||||
Enabled,
|
||||
Disabled,
|
||||
Failed,
|
||||
Inactive,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Counters {
|
||||
pass: u64,
|
||||
skip: u64,
|
||||
notice: u64,
|
||||
warn: u64,
|
||||
fail: u64,
|
||||
}
|
||||
|
||||
enum LogLevel {
|
||||
Pass,
|
||||
Info,
|
||||
Skip,
|
||||
Notice,
|
||||
Warn,
|
||||
Fail,
|
||||
}
|
||||
|
||||
struct ConsoleOutput {
|
||||
stream: StandardStream,
|
||||
first_header: bool,
|
||||
counters: Counters,
|
||||
}
|
||||
|
||||
impl ConsoleOutput {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
stream: StandardStream::stdout(ColorChoice::Always),
|
||||
first_header: true,
|
||||
counters: Counters::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn print_header(&mut self, message: &str) -> Result<(), Error> {
|
||||
if !self.first_header {
|
||||
writeln!(&mut self.stream)?;
|
||||
}
|
||||
self.first_header = false;
|
||||
writeln!(&mut self.stream, "= {message} =\n")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_color(&mut self, color: Color, bold: bool) -> Result<(), Error> {
|
||||
self.stream
|
||||
.set_color(ColorSpec::new().set_fg(Some(color)).set_bold(bold))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn reset(&mut self) -> Result<(), std::io::Error> {
|
||||
self.stream.reset()
|
||||
}
|
||||
|
||||
pub fn log_line(&mut self, level: LogLevel, message: &str) -> Result<(), Error> {
|
||||
match level {
|
||||
LogLevel::Pass => {
|
||||
self.counters.pass += 1;
|
||||
self.set_color(Color::Green, false)?;
|
||||
writeln!(&mut self.stream, "PASS: {}", message)?;
|
||||
}
|
||||
LogLevel::Info => {
|
||||
writeln!(&mut self.stream, "INFO: {}", message)?;
|
||||
}
|
||||
LogLevel::Skip => {
|
||||
self.counters.skip += 1;
|
||||
writeln!(&mut self.stream, "SKIP: {}", message)?;
|
||||
}
|
||||
LogLevel::Notice => {
|
||||
self.counters.notice += 1;
|
||||
self.set_color(Color::White, true)?;
|
||||
writeln!(&mut self.stream, "NOTICE: {}", message)?;
|
||||
}
|
||||
LogLevel::Warn => {
|
||||
self.counters.warn += 1;
|
||||
self.set_color(Color::Yellow, false)?;
|
||||
writeln!(&mut self.stream, "WARN: {}", message)?;
|
||||
}
|
||||
LogLevel::Fail => {
|
||||
self.counters.fail += 1;
|
||||
self.set_color(Color::Red, true)?;
|
||||
writeln!(&mut self.stream, "FAIL: {}", message)?;
|
||||
}
|
||||
}
|
||||
self.reset()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn log_pass<T: AsRef<str>>(&mut self, message: T) -> Result<(), Error> {
|
||||
self.log_line(LogLevel::Pass, message.as_ref())
|
||||
}
|
||||
|
||||
pub fn log_info<T: AsRef<str>>(&mut self, message: T) -> Result<(), Error> {
|
||||
self.log_line(LogLevel::Info, message.as_ref())
|
||||
}
|
||||
|
||||
pub fn log_skip<T: AsRef<str>>(&mut self, message: T) -> Result<(), Error> {
|
||||
self.log_line(LogLevel::Skip, message.as_ref())
|
||||
}
|
||||
|
||||
pub fn log_notice<T: AsRef<str>>(&mut self, message: T) -> Result<(), Error> {
|
||||
self.log_line(LogLevel::Notice, message.as_ref())
|
||||
}
|
||||
|
||||
pub fn log_warn<T: AsRef<str>>(&mut self, message: T) -> Result<(), Error> {
|
||||
self.log_line(LogLevel::Warn, message.as_ref())
|
||||
}
|
||||
|
||||
pub fn log_fail<T: AsRef<str>>(&mut self, message: T) -> Result<(), Error> {
|
||||
self.log_line(LogLevel::Fail, message.as_ref())
|
||||
}
|
||||
|
||||
pub fn print_summary(&mut self) -> Result<(), Error> {
|
||||
self.print_header("SUMMARY")?;
|
||||
|
||||
let total = self.counters.fail
|
||||
+ self.counters.pass
|
||||
+ self.counters.notice
|
||||
+ self.counters.skip
|
||||
+ self.counters.warn;
|
||||
|
||||
writeln!(&mut self.stream, "TOTAL: {total}")?;
|
||||
self.set_color(Color::Green, false)?;
|
||||
writeln!(&mut self.stream, "PASSED: {}", self.counters.pass)?;
|
||||
self.reset()?;
|
||||
writeln!(&mut self.stream, "SKIPPED: {}", self.counters.skip)?;
|
||||
writeln!(&mut self.stream, "NOTICE: {}", self.counters.notice)?;
|
||||
if self.counters.warn > 0 {
|
||||
self.set_color(Color::Yellow, false)?;
|
||||
writeln!(&mut self.stream, "WARNINGS: {}", self.counters.warn)?;
|
||||
}
|
||||
if self.counters.fail > 0 {
|
||||
self.set_color(Color::Red, true)?;
|
||||
writeln!(&mut self.stream, "FAILURES: {}", self.counters.fail)?;
|
||||
}
|
||||
if self.counters.warn > 0 || self.counters.fail > 0 {
|
||||
let (color, bold) = if self.counters.fail > 0 {
|
||||
(Color::Red, true)
|
||||
} else {
|
||||
(Color::Yellow, false)
|
||||
};
|
||||
|
||||
self.set_color(color, bold)?;
|
||||
writeln!(
|
||||
&mut self.stream,
|
||||
"\nATTENTION: Please check the output for detailed information!",
|
||||
)?;
|
||||
if self.counters.fail > 0 {
|
||||
writeln!(
|
||||
&mut self.stream,
|
||||
"Try to solve the problems one at a time and rerun this checklist tool again.",
|
||||
)?;
|
||||
}
|
||||
}
|
||||
self.reset()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -6,6 +6,8 @@ use std::fs::File;
|
||||
use std::os::unix::io::{AsRawFd, FromRawFd};
|
||||
|
||||
use anyhow::{bail, Error};
|
||||
use pbs_tape::sg_tape::SgTape;
|
||||
use proxmox_backup::tape::encryption_keys::load_key;
|
||||
use serde_json::Value;
|
||||
|
||||
use proxmox_router::{cli::*, RpcEnvironment};
|
||||
@ -19,28 +21,26 @@ use pbs_api_types::{
|
||||
|
||||
use pbs_tape::linux_list_drives::{check_tape_is_lto_tape_device, open_lto_tape_device};
|
||||
|
||||
use proxmox_backup::tape::drive::{open_lto_tape_drive, LtoTapeHandle, TapeDriver};
|
||||
|
||||
fn get_tape_handle(param: &Value) -> Result<LtoTapeHandle, Error> {
|
||||
fn get_tape_handle(param: &Value) -> Result<SgTape, Error> {
|
||||
let handle = if let Some(name) = param["drive"].as_str() {
|
||||
let (config, _digest) = pbs_config::drive::config()?;
|
||||
let drive: LtoTapeDrive = config.lookup("lto", name)?;
|
||||
log::info!("using device {}", drive.path);
|
||||
open_lto_tape_drive(&drive)?
|
||||
SgTape::open_lto_drive(&drive)?
|
||||
} else if let Some(device) = param["device"].as_str() {
|
||||
log::info!("using device {}", device);
|
||||
LtoTapeHandle::new(open_lto_tape_device(device)?)?
|
||||
SgTape::new(open_lto_tape_device(device)?)?
|
||||
} else if let Some(true) = param["stdin"].as_bool() {
|
||||
log::info!("using stdin");
|
||||
let fd = std::io::stdin().as_raw_fd();
|
||||
let file = unsafe { File::from_raw_fd(fd) };
|
||||
check_tape_is_lto_tape_device(&file)?;
|
||||
LtoTapeHandle::new(file)?
|
||||
SgTape::new(file)?
|
||||
} else if let Ok(name) = std::env::var("PROXMOX_TAPE_DRIVE") {
|
||||
let (config, _digest) = pbs_config::drive::config()?;
|
||||
let drive: LtoTapeDrive = config.lookup("lto", &name)?;
|
||||
log::info!("using device {}", drive.path);
|
||||
open_lto_tape_drive(&drive)?
|
||||
SgTape::open_lto_drive(&drive)?
|
||||
} else {
|
||||
let (config, _digest) = pbs_config::drive::config()?;
|
||||
|
||||
@ -56,7 +56,7 @@ fn get_tape_handle(param: &Value) -> Result<LtoTapeHandle, Error> {
|
||||
let name = drive_names[0];
|
||||
let drive: LtoTapeDrive = config.lookup("lto", name)?;
|
||||
log::info!("using device {}", drive.path);
|
||||
open_lto_tape_drive(&drive)?
|
||||
SgTape::open_lto_drive(&drive)?
|
||||
} else {
|
||||
bail!("no drive/device specified");
|
||||
}
|
||||
@ -103,7 +103,8 @@ fn set_encryption(
|
||||
|
||||
match (fingerprint, uuid) {
|
||||
(Some(fingerprint), Some(uuid)) => {
|
||||
handle.set_encryption(Some((fingerprint, uuid)))?;
|
||||
let key = load_key(&fingerprint)?;
|
||||
handle.set_encryption(Some((key, uuid)))?;
|
||||
}
|
||||
(Some(_), None) => {
|
||||
bail!("missing media set uuid");
|
||||
|
@ -198,8 +198,9 @@ impl JobState {
|
||||
.map_err(|err| format_err!("error parsing upid: {err}"))?;
|
||||
|
||||
if !worker_is_active_local(&parsed) {
|
||||
let state = upid_read_status(&parsed)
|
||||
.map_err(|err| format_err!("error reading upid log status: {err}"))?;
|
||||
let state = upid_read_status(&parsed).unwrap_or(TaskState::Unknown {
|
||||
endtime: parsed.starttime,
|
||||
});
|
||||
|
||||
Ok(JobState::Finished {
|
||||
upid,
|
||||
|
@ -1,20 +1,67 @@
|
||||
use std::fmt::Write;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
|
||||
fn files() -> Vec<&'static str> {
|
||||
fn get_top_processes() -> String {
|
||||
let (exe, args) = ("top", vec!["-b", "-c", "-w512", "-n", "1", "-o", "TIME"]);
|
||||
let output = Command::new(exe)
|
||||
.args(&args)
|
||||
.output();
|
||||
let output = match output {
|
||||
Ok(output) => String::from_utf8_lossy(&output.stdout).to_string(),
|
||||
Err(err) => err.to_string(),
|
||||
};
|
||||
let output = output.lines().take(30).collect::<Vec<&str>>().join("\n");
|
||||
format!("$ `{exe} {}`\n```\n{output}\n```", args.join(" "))
|
||||
}
|
||||
|
||||
fn files() -> Vec<(&'static str, Vec<&'static str>)> {
|
||||
vec![
|
||||
"/etc/hostname",
|
||||
"/etc/hosts",
|
||||
"/etc/network/interfaces",
|
||||
"/etc/proxmox-backup/datastore.cfg",
|
||||
"/etc/proxmox-backup/user.cfg",
|
||||
"/etc/proxmox-backup/acl.cfg",
|
||||
"/etc/proxmox-backup/remote.cfg",
|
||||
"/etc/proxmox-backup/sync.cfg",
|
||||
"/etc/proxmox-backup/verification.cfg",
|
||||
"/etc/proxmox-backup/tape.cfg",
|
||||
"/etc/proxmox-backup/media-pool.cfg",
|
||||
"/etc/proxmox-backup/traffic-control.cfg",
|
||||
(
|
||||
"General System Info",
|
||||
vec![
|
||||
"/etc/hostname",
|
||||
"/etc/hosts",
|
||||
"/etc/network/interfaces",
|
||||
"/etc/apt/sources.list",
|
||||
"/etc/apt/sources.list.d/",
|
||||
"/proc/pressure/",
|
||||
],
|
||||
),
|
||||
(
|
||||
"Datastores & Remotes",
|
||||
vec!["/etc/proxmox-backup/datastore.cfg"],
|
||||
),
|
||||
(
|
||||
"User & Access",
|
||||
vec![
|
||||
"/etc/proxmox-backup/user.cfg",
|
||||
"/etc/proxmox-backup/acl.cfg",
|
||||
],
|
||||
),
|
||||
("Remotes", vec!["/etc/proxmox-backup/remote.cfg"]),
|
||||
(
|
||||
"Jobs",
|
||||
vec![
|
||||
"/etc/proxmox-backup/sync.cfg",
|
||||
"/etc/proxmox-backup/prune.cfg",
|
||||
"/etc/proxmox-backup/verification.cfg",
|
||||
],
|
||||
),
|
||||
(
|
||||
"Tape",
|
||||
vec![
|
||||
"/etc/proxmox-backup/tape.cfg",
|
||||
"/etc/proxmox-backup/media-pool.cfg",
|
||||
],
|
||||
),
|
||||
(
|
||||
"Others",
|
||||
vec![
|
||||
"/etc/proxmox-backup/node.cfg",
|
||||
"/etc/proxmox-backup/traffic-control.cfg",
|
||||
],
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
@ -24,8 +71,19 @@ fn commands() -> Vec<(&'static str, Vec<&'static str>)> {
|
||||
("date", vec!["-R"]),
|
||||
("proxmox-backup-manager", vec!["versions", "--verbose"]),
|
||||
("proxmox-backup-manager", vec!["subscription", "get"]),
|
||||
("proxmox-backup-manager", vec!["ldap", "list"]),
|
||||
("proxmox-backup-manager", vec!["openid", "list"]),
|
||||
("proxmox-boot-tool", vec!["status"]),
|
||||
("df", vec!["-h"]),
|
||||
("lsblk", vec!["--ascii"]),
|
||||
(
|
||||
"lsblk",
|
||||
vec![
|
||||
"--ascii",
|
||||
"-M",
|
||||
"-o",
|
||||
"+HOTPLUG,ROTA,PHY-SEC,FSTYPE,MODEL,TRAN",
|
||||
],
|
||||
),
|
||||
("ls", vec!["-l", "/dev/disk/by-id", "/dev/disk/by-path"]),
|
||||
("zpool", vec!["status"]),
|
||||
("zfs", vec!["list"]),
|
||||
@ -37,60 +95,132 @@ fn commands() -> Vec<(&'static str, Vec<&'static str>)> {
|
||||
type FunctionMapping = (&'static str, fn() -> String);
|
||||
|
||||
fn function_calls() -> Vec<FunctionMapping> {
|
||||
vec![("Datastores", || {
|
||||
let config = match pbs_config::datastore::config() {
|
||||
Ok((config, _digest)) => config,
|
||||
_ => return String::from("could not read datastore config"),
|
||||
};
|
||||
vec![
|
||||
("Datastores", || {
|
||||
let config = match pbs_config::datastore::config() {
|
||||
Ok((config, _digest)) => config,
|
||||
_ => return String::from("could not read datastore config"),
|
||||
};
|
||||
|
||||
let mut list = Vec::new();
|
||||
for store in config.sections.keys() {
|
||||
list.push(store.as_str());
|
||||
let mut list = Vec::new();
|
||||
for store in config.sections.keys() {
|
||||
list.push(store.as_str());
|
||||
}
|
||||
format!("```\n{}\n```", list.join(", "))
|
||||
}),
|
||||
("System Load & Uptime", get_top_processes),
|
||||
]
|
||||
}
|
||||
|
||||
fn get_file_content(file: impl AsRef<Path>) -> String {
|
||||
use proxmox_sys::fs::file_read_optional_string;
|
||||
let content = match file_read_optional_string(&file) {
|
||||
Ok(Some(content)) => content,
|
||||
Ok(None) => String::from("# file does not exist"),
|
||||
Err(err) => err.to_string(),
|
||||
};
|
||||
let file_name = file.as_ref().display();
|
||||
format!("`$ cat '{file_name}'`\n```\n{}\n```", content.trim_end())
|
||||
}
|
||||
|
||||
fn get_directory_content(path: impl AsRef<Path>) -> String {
|
||||
let read_dir_iter = match std::fs::read_dir(&path) {
|
||||
Ok(iter) => iter,
|
||||
Err(err) => {
|
||||
return format!(
|
||||
"`$ cat '{}*'`\n```\n# read dir failed - {}\n```",
|
||||
path.as_ref().display(),
|
||||
err.to_string(),
|
||||
);
|
||||
}
|
||||
list.join(", ")
|
||||
})]
|
||||
};
|
||||
let mut out = String::new();
|
||||
let mut first = true;
|
||||
for entry in read_dir_iter {
|
||||
let entry = match entry {
|
||||
Ok(entry) => entry,
|
||||
Err(err) => {
|
||||
let _ = writeln!(out, "error during read-dir - {}", err.to_string());
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let path = entry.path();
|
||||
if path.is_file() {
|
||||
if first {
|
||||
let _ = writeln!(out, "{}", get_file_content(path));
|
||||
first = false;
|
||||
} else {
|
||||
let _ = writeln!(out, "\n{}", get_file_content(path));
|
||||
}
|
||||
} else {
|
||||
let _ = writeln!(out, "skipping sub-directory `{}`", path.display());
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn get_command_output(exe: &str, args: &Vec<&str>) -> String {
|
||||
let output = Command::new(exe)
|
||||
.env("PROXMOX_OUTPUT_NO_BORDER", "1")
|
||||
.args(args)
|
||||
.output();
|
||||
let output = match output {
|
||||
Ok(output) => {
|
||||
let mut out = String::from_utf8_lossy(&output.stdout)
|
||||
.trim_end()
|
||||
.to_string();
|
||||
let stderr = String::from_utf8_lossy(&output.stderr)
|
||||
.trim_end()
|
||||
.to_string();
|
||||
if !stderr.is_empty() {
|
||||
let _ = writeln!(out, "\n```\nSTDERR:\n```\n{stderr}");
|
||||
}
|
||||
out
|
||||
}
|
||||
Err(err) => err.to_string(),
|
||||
};
|
||||
format!("$ `{exe} {}`\n```\n{output}\n```", args.join(" "))
|
||||
}
|
||||
|
||||
pub fn generate_report() -> String {
|
||||
use proxmox_sys::fs::file_read_optional_string;
|
||||
|
||||
let file_contents = files()
|
||||
.iter()
|
||||
.map(|file_name| {
|
||||
let content = match file_read_optional_string(Path::new(file_name)) {
|
||||
Ok(Some(content)) => content,
|
||||
Ok(None) => String::from("# file does not exist"),
|
||||
Err(err) => err.to_string(),
|
||||
};
|
||||
format!("$ cat '{}'\n{}", file_name, content)
|
||||
.map(|group| {
|
||||
let (group, files) = group;
|
||||
let group_content = files
|
||||
.iter()
|
||||
.map(|file_name| {
|
||||
let path = Path::new(file_name);
|
||||
if path.is_dir() {
|
||||
get_directory_content(&path)
|
||||
} else {
|
||||
get_file_content(file_name)
|
||||
}
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n\n");
|
||||
|
||||
format!("### {group}\n\n{group_content}")
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n\n");
|
||||
|
||||
let command_outputs = commands()
|
||||
.iter()
|
||||
.map(|(command, args)| {
|
||||
let output = Command::new(command)
|
||||
.env("PROXMOX_OUTPUT_NO_BORDER", "1")
|
||||
.args(args)
|
||||
.output();
|
||||
let output = match output {
|
||||
Ok(output) => String::from_utf8_lossy(&output.stdout).to_string(),
|
||||
Err(err) => err.to_string(),
|
||||
};
|
||||
format!("$ `{} {}`\n{}", command, args.join(" "), output)
|
||||
})
|
||||
.map(|(command, args)| get_command_output(command, args))
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n\n");
|
||||
|
||||
let function_outputs = function_calls()
|
||||
.iter()
|
||||
.map(|(desc, function)| format!("$ {}\n{}", desc, function()))
|
||||
.map(|(desc, function)| {
|
||||
let output = function();
|
||||
format!("#### {desc}\n{}\n", output.trim_end())
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n\n");
|
||||
|
||||
format!(
|
||||
"= FILES =\n\n{}\n= COMMANDS =\n\n{}\n= FUNCTIONS =\n\n{}\n",
|
||||
file_contents, command_outputs, function_outputs
|
||||
"## FILES\n\n{file_contents}\n## COMMANDS\n\n{command_outputs}\n## FUNCTIONS\n\n{function_outputs}\n"
|
||||
)
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ use std::os::unix::io::{AsRawFd, FromRawFd, RawFd};
|
||||
|
||||
use anyhow::{bail, format_err, Error};
|
||||
|
||||
use pbs_tape::sg_tape::drive_get_encryption;
|
||||
use proxmox_uuid::Uuid;
|
||||
|
||||
use pbs_api_types::{
|
||||
@ -23,7 +24,6 @@ use pbs_api_types::{
|
||||
};
|
||||
use pbs_key_config::KeyConfig;
|
||||
use pbs_tape::{
|
||||
linux_list_drives::open_lto_tape_device,
|
||||
sg_tape::{SgTape, TapeAlertFlags},
|
||||
BlockReadError, MediaContentHeader, TapeRead, TapeWrite,
|
||||
};
|
||||
@ -34,75 +34,47 @@ use crate::tape::{
|
||||
file_formats::{MediaSetLabel, PROXMOX_BACKUP_MEDIA_SET_LABEL_MAGIC_1_0},
|
||||
};
|
||||
|
||||
/// Open a tape device
|
||||
///
|
||||
/// This does additional checks:
|
||||
///
|
||||
/// - check if it is a non-rewinding tape device
|
||||
/// - check if drive is ready (tape loaded)
|
||||
/// - check block size
|
||||
/// - for autoloader only, try to reload ejected tapes
|
||||
pub fn open_lto_tape_drive(config: &LtoTapeDrive) -> Result<LtoTapeHandle, Error> {
|
||||
proxmox_lang::try_block!({
|
||||
let file = open_lto_tape_device(&config.path)?;
|
||||
|
||||
let mut handle = LtoTapeHandle::new(file)?;
|
||||
|
||||
if handle.sg_tape.test_unit_ready().is_err() {
|
||||
// for autoloader only, try to reload ejected tapes
|
||||
if config.changer.is_some() {
|
||||
let _ = handle.sg_tape.load(); // just try, ignore error
|
||||
impl Drop for LtoTapeHandle {
|
||||
fn drop(&mut self) {
|
||||
// always unload the encryption key when the handle is dropped for security
|
||||
// but only log an error if we set one in the first place
|
||||
if let Err(err) = self.set_encryption(None) {
|
||||
if self.encryption_key_loaded {
|
||||
log::error!("could not unload encryption key from drive: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
handle.sg_tape.wait_until_ready()?;
|
||||
|
||||
handle.set_default_options()?;
|
||||
|
||||
Ok(handle)
|
||||
})
|
||||
.map_err(|err: Error| {
|
||||
format_err!(
|
||||
"open drive '{}' ({}) failed - {}",
|
||||
config.name,
|
||||
config.path,
|
||||
err
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Lto Tape device handle
|
||||
pub struct LtoTapeHandle {
|
||||
sg_tape: SgTape,
|
||||
encryption_key_loaded: bool,
|
||||
}
|
||||
|
||||
impl LtoTapeHandle {
|
||||
/// Creates a new instance
|
||||
pub fn new(file: File) -> Result<Self, Error> {
|
||||
let sg_tape = SgTape::new(file)?;
|
||||
Ok(Self { sg_tape })
|
||||
Ok(Self {
|
||||
sg_tape,
|
||||
encryption_key_loaded: false,
|
||||
})
|
||||
}
|
||||
|
||||
/// Set all options we need/want
|
||||
pub fn set_default_options(&mut self) -> Result<(), Error> {
|
||||
self.sg_tape.set_default_options()?;
|
||||
Ok(())
|
||||
}
|
||||
/// Open a tape device
|
||||
///
|
||||
/// since this calls [SgTape::open_lto_drive], it does some internal checks.
|
||||
/// See [SgTape] docs for details.
|
||||
pub fn open_lto_drive(config: &LtoTapeDrive) -> Result<Self, Error> {
|
||||
let sg_tape = SgTape::open_lto_drive(config)?;
|
||||
|
||||
/// Set driver options
|
||||
pub fn set_drive_options(
|
||||
&mut self,
|
||||
compression: Option<bool>,
|
||||
block_length: Option<u32>,
|
||||
buffer_mode: Option<bool>,
|
||||
) -> Result<(), Error> {
|
||||
self.sg_tape
|
||||
.set_drive_options(compression, block_length, buffer_mode)
|
||||
}
|
||||
let handle = Self {
|
||||
sg_tape,
|
||||
encryption_key_loaded: false,
|
||||
};
|
||||
|
||||
/// Write a single EOF mark without flushing buffers
|
||||
pub fn write_filemarks(&mut self, count: usize) -> Result<(), std::io::Error> {
|
||||
self.sg_tape.write_filemarks(count, false)
|
||||
Ok(handle)
|
||||
}
|
||||
|
||||
/// Get Tape and Media status
|
||||
@ -118,27 +90,11 @@ impl LtoTapeHandle {
|
||||
self.sg_tape.space_filemarks(-count.try_into()?)
|
||||
}
|
||||
|
||||
pub fn forward_space_count_records(&mut self, count: usize) -> Result<(), Error> {
|
||||
self.sg_tape.space_blocks(count.try_into()?)
|
||||
}
|
||||
|
||||
pub fn backward_space_count_records(&mut self, count: usize) -> Result<(), Error> {
|
||||
self.sg_tape.space_blocks(-count.try_into()?)
|
||||
}
|
||||
|
||||
/// Position the tape after filemark count. Count 0 means BOT.
|
||||
pub fn locate_file(&mut self, position: u64) -> Result<(), Error> {
|
||||
self.sg_tape.locate_file(position)
|
||||
}
|
||||
|
||||
pub fn erase_media(&mut self, fast: bool) -> Result<(), Error> {
|
||||
self.sg_tape.erase_media(fast)
|
||||
}
|
||||
|
||||
pub fn load(&mut self) -> Result<(), Error> {
|
||||
self.sg_tape.load()
|
||||
}
|
||||
|
||||
/// Read Cartridge Memory (MAM Attributes)
|
||||
pub fn cartridge_memory(&mut self) -> Result<Vec<MamAttribute>, Error> {
|
||||
self.sg_tape.cartridge_memory()
|
||||
@ -148,20 +104,6 @@ impl LtoTapeHandle {
|
||||
pub fn volume_statistics(&mut self) -> Result<Lp17VolumeStatistics, Error> {
|
||||
self.sg_tape.volume_statistics()
|
||||
}
|
||||
|
||||
/// Lock the drive door
|
||||
pub fn lock(&mut self) -> Result<(), Error> {
|
||||
self.sg_tape
|
||||
.set_medium_removal(false)
|
||||
.map_err(|err| format_err!("lock door failed - {}", err))
|
||||
}
|
||||
|
||||
/// Unlock the drive door
|
||||
pub fn unlock(&mut self) -> Result<(), Error> {
|
||||
self.sg_tape
|
||||
.set_medium_removal(true)
|
||||
.map_err(|err| format_err!("unlock door failed - {}", err))
|
||||
}
|
||||
}
|
||||
|
||||
impl TapeDriver for LtoTapeHandle {
|
||||
@ -271,6 +213,13 @@ impl TapeDriver for LtoTapeHandle {
|
||||
|
||||
self.sync()?; // sync data to tape
|
||||
|
||||
let encrypt_fingerprint = media_set_label
|
||||
.encryption_key_fingerprint
|
||||
.clone()
|
||||
.map(|fp| (fp, media_set_label.uuid.clone()));
|
||||
|
||||
self.set_encryption(encrypt_fingerprint)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -292,46 +241,27 @@ impl TapeDriver for LtoTapeHandle {
|
||||
&mut self,
|
||||
key_fingerprint: Option<(Fingerprint, Uuid)>,
|
||||
) -> Result<(), Error> {
|
||||
if nix::unistd::Uid::effective().is_root() {
|
||||
if let Some((ref key_fingerprint, ref uuid)) = key_fingerprint {
|
||||
let (key_map, _digest) = crate::tape::encryption_keys::load_keys()?;
|
||||
match key_map.get(key_fingerprint) {
|
||||
Some(item) => {
|
||||
// derive specialized key for each media-set
|
||||
|
||||
let mut tape_key = [0u8; 32];
|
||||
|
||||
let uuid_bytes: [u8; 16] = *uuid.as_bytes();
|
||||
|
||||
openssl::pkcs5::pbkdf2_hmac(
|
||||
&item.key,
|
||||
&uuid_bytes,
|
||||
10,
|
||||
openssl::hash::MessageDigest::sha256(),
|
||||
&mut tape_key,
|
||||
)?;
|
||||
|
||||
return self.sg_tape.set_encryption(Some(tape_key));
|
||||
}
|
||||
None => bail!("unknown tape encryption key '{}'", key_fingerprint),
|
||||
}
|
||||
} else {
|
||||
return self.sg_tape.set_encryption(None);
|
||||
}
|
||||
}
|
||||
|
||||
let output = if let Some((fingerprint, uuid)) = key_fingerprint {
|
||||
if let Some((fingerprint, uuid)) = key_fingerprint {
|
||||
let fingerprint = fingerprint.signature();
|
||||
run_sg_tape_cmd(
|
||||
let output = run_sg_tape_cmd(
|
||||
"encryption",
|
||||
&["--fingerprint", &fingerprint, "--uuid", &uuid.to_string()],
|
||||
self.sg_tape.file_mut().as_raw_fd(),
|
||||
)?
|
||||
)?;
|
||||
self.encryption_key_loaded = true;
|
||||
let result: Result<(), String> = serde_json::from_str(&output)?;
|
||||
result.map_err(|err| format_err!("{}", err))
|
||||
} else {
|
||||
run_sg_tape_cmd("encryption", &[], self.sg_tape.file_mut().as_raw_fd())?
|
||||
};
|
||||
let result: Result<(), String> = serde_json::from_str(&output)?;
|
||||
result.map_err(|err| format_err!("{}", err))
|
||||
self.sg_tape.set_encryption(None)
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_encryption_mode(&mut self, encryption_wanted: bool) -> Result<(), Error> {
|
||||
let encryption_set = drive_get_encryption(self.sg_tape.file_mut())?;
|
||||
if encryption_wanted != encryption_set {
|
||||
bail!("Set encryption mode not what was desired (set: {encryption_set}, wanted: {encryption_wanted})");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -105,11 +105,13 @@ pub trait TapeDriver {
|
||||
key_config: Option<&KeyConfig>,
|
||||
) -> Result<(), Error>;
|
||||
|
||||
/// Read the media label
|
||||
/// Read the media label without setting the encryption key
|
||||
///
|
||||
/// This tries to read both media labels (label and
|
||||
/// media_set_label). Also returns the optional encryption key configuration.
|
||||
fn read_label(&mut self) -> Result<(Option<MediaId>, Option<KeyConfig>), Error> {
|
||||
/// This is used internally by 'read_label' and when restoring the encryption
|
||||
/// key from the drive. Should not be used or overwritten otherwise!
|
||||
fn read_label_without_loading_key(
|
||||
&mut self,
|
||||
) -> Result<(Option<MediaId>, Option<KeyConfig>), Error> {
|
||||
self.rewind()?;
|
||||
|
||||
let label = {
|
||||
@ -187,6 +189,22 @@ pub trait TapeDriver {
|
||||
Ok((Some(media_id), key_config))
|
||||
}
|
||||
|
||||
/// Read the media label
|
||||
///
|
||||
/// This tries to read both media labels (label and
|
||||
/// media_set_label). Also returns the optional encryption key configuration.
|
||||
///
|
||||
/// Automatically sets the encryption key on the drive
|
||||
fn read_label(&mut self) -> Result<(Option<MediaId>, Option<KeyConfig>), Error> {
|
||||
let (media_id, key_config) = self.read_label_without_loading_key()?;
|
||||
|
||||
let encrypt_fingerprint = media_id.as_ref().and_then(|id| id.get_encryption_fp());
|
||||
|
||||
self.set_encryption(encrypt_fingerprint)?;
|
||||
|
||||
Ok((media_id, key_config))
|
||||
}
|
||||
|
||||
/// Eject media
|
||||
fn eject_media(&mut self) -> Result<(), Error>;
|
||||
|
||||
@ -203,6 +221,9 @@ pub trait TapeDriver {
|
||||
/// We use the media_set_uuid to XOR the secret key with the
|
||||
/// uuid (first 16 bytes), so that each media set uses an unique
|
||||
/// key for encryption.
|
||||
///
|
||||
/// Should be called as part of write_media_set_label or read_label,
|
||||
/// so this should not be called manually.
|
||||
fn set_encryption(
|
||||
&mut self,
|
||||
key_fingerprint: Option<(Fingerprint, Uuid)>,
|
||||
@ -212,6 +233,14 @@ pub trait TapeDriver {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Asserts that the encryption mode is set to the given value
|
||||
fn assert_encryption_mode(&mut self, encryption_wanted: bool) -> Result<(), Error> {
|
||||
if encryption_wanted {
|
||||
bail!("drive does not support encryption");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// A boxed implementor of [`MediaChange`].
|
||||
@ -280,7 +309,7 @@ pub fn open_drive(config: &SectionConfigData, drive: &str) -> Result<Box<dyn Tap
|
||||
}
|
||||
"lto" => {
|
||||
let tape = LtoTapeDrive::deserialize(config)?;
|
||||
let handle = open_lto_tape_drive(&tape)?;
|
||||
let handle = LtoTapeHandle::open_lto_drive(&tape)?;
|
||||
Ok(Box::new(handle))
|
||||
}
|
||||
ty => bail!("unknown drive type '{}' - internal error", ty),
|
||||
@ -449,7 +478,7 @@ pub fn request_and_load_media(
|
||||
}
|
||||
}
|
||||
|
||||
let mut handle = match open_lto_tape_drive(&drive_config) {
|
||||
let mut handle = match LtoTapeHandle::open_lto_drive(&drive_config) {
|
||||
Ok(handle) => handle,
|
||||
Err(err) => {
|
||||
update_and_log_request_error(
|
||||
|
@ -12,7 +12,7 @@
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use anyhow::{bail, Error};
|
||||
use anyhow::{bail, format_err, Error};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use proxmox_sys::fs::file_read_optional_string;
|
||||
@ -92,6 +92,14 @@ pub fn load_keys() -> Result<(HashMap<Fingerprint, EncryptionKeyInfo>, [u8; 32])
|
||||
Ok((map, digest))
|
||||
}
|
||||
|
||||
pub fn load_key(fingerprint: &Fingerprint) -> Result<[u8; 32], Error> {
|
||||
let (key_map, _digest) = crate::tape::encryption_keys::load_keys()?;
|
||||
key_map
|
||||
.get(fingerprint)
|
||||
.map(|data| data.key)
|
||||
.ok_or_else(|| format_err!("unknown tape encryption key '{fingerprint}'"))
|
||||
}
|
||||
|
||||
/// Load tape encryption key configurations (password protected keys)
|
||||
pub fn load_key_configs() -> Result<(HashMap<Fingerprint, KeyConfig>, [u8; 32]), Error> {
|
||||
let content = file_read_optional_string(TAPE_KEY_CONFIG_FILENAME)?;
|
||||
|
@ -33,7 +33,7 @@ use serde_json::json;
|
||||
use proxmox_sys::fs::{file_get_json, replace_file, CreateOptions};
|
||||
use proxmox_uuid::Uuid;
|
||||
|
||||
use pbs_api_types::{MediaLocation, MediaSetPolicy, MediaStatus, RetentionPolicy};
|
||||
use pbs_api_types::{Fingerprint, MediaLocation, MediaSetPolicy, MediaStatus, RetentionPolicy};
|
||||
use pbs_config::BackupLockGuard;
|
||||
|
||||
#[cfg(not(test))]
|
||||
@ -71,6 +71,10 @@ impl MediaId {
|
||||
}
|
||||
self.label.pool.to_owned()
|
||||
}
|
||||
pub(crate) fn get_encryption_fp(&self) -> Option<(Fingerprint, Uuid)> {
|
||||
let label = self.clone().media_set_label?;
|
||||
label.encryption_key_fingerprint.map(|fp| (fp, label.uuid))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
|
@ -272,12 +272,7 @@ impl PoolWriter {
|
||||
|
||||
let media_set = media.media_set_label().unwrap();
|
||||
|
||||
let encrypt_fingerprint = media_set
|
||||
.encryption_key_fingerprint
|
||||
.clone()
|
||||
.map(|fp| (fp, media_set.uuid.clone()));
|
||||
|
||||
drive.set_encryption(encrypt_fingerprint)?;
|
||||
drive.assert_encryption_mode(media_set.encryption_key_fingerprint.is_some())?;
|
||||
|
||||
self.status = Some(PoolWriterState {
|
||||
drive,
|
||||
|
@ -218,6 +218,16 @@ Ext.define('PBS.MainView', {
|
||||
flex: 1,
|
||||
baseCls: 'x-plain',
|
||||
},
|
||||
{
|
||||
xtype: 'proxmoxEOLNotice',
|
||||
product: 'Proxmox Backup Server',
|
||||
version: '2',
|
||||
eolDate: '2024-07-31',
|
||||
href: 'pbs.proxmox.com/docs/faq.html#faq-support-table',
|
||||
},
|
||||
{
|
||||
flex: 1,
|
||||
},
|
||||
{
|
||||
xtype: 'button',
|
||||
baseCls: 'x-btn',
|
||||
|
@ -35,6 +35,14 @@ const proxmoxOnlineHelpInfo = {
|
||||
"link": "/docs/configuration-files.html#domains-cfg",
|
||||
"title": "``domains.cfg``"
|
||||
},
|
||||
"faq-support-table": {
|
||||
"link": "/docs/faq.html#faq-support-table",
|
||||
"title": "How long will my Proxmox Backup Server version be supported?"
|
||||
},
|
||||
"faq-upgrade-major": {
|
||||
"link": "/docs/faq.html#faq-upgrade-major",
|
||||
"title": "How can I upgrade Proxmox Backup Server to the next major release?"
|
||||
},
|
||||
"pxar-format": {
|
||||
"link": "/docs/file-formats.html#pxar-format",
|
||||
"title": "Proxmox File Archive Format (``.pxar``)"
|
||||
|
@ -55,17 +55,24 @@ Ext.define('PBS.TapeManagement.BackupOverview', {
|
||||
data[pool] = {};
|
||||
}
|
||||
|
||||
let seq_nr = entry['seq-nr'];
|
||||
|
||||
if (data[pool][media_set] === undefined) {
|
||||
data[pool][media_set] = entry;
|
||||
data[pool][media_set].text = media_set;
|
||||
data[pool][media_set].restore = true;
|
||||
data[pool][media_set].tapes = 1;
|
||||
data[pool][media_set]['seq-nr'] = undefined;
|
||||
data[pool][media_set]['max-seq-nr'] = seq_nr;
|
||||
data[pool][media_set].is_media_set = true;
|
||||
data[pool][media_set].typeText = 'media-set';
|
||||
} else {
|
||||
data[pool][media_set].tapes++;
|
||||
}
|
||||
|
||||
if (data[pool][media_set]['max-seq-nr'] < seq_nr) {
|
||||
data[pool][media_set]['max-seq-nr'] = seq_nr;
|
||||
}
|
||||
}
|
||||
|
||||
let list = [];
|
||||
@ -309,11 +316,33 @@ Ext.define('PBS.TapeManagement.BackupOverview', {
|
||||
},
|
||||
],
|
||||
|
||||
viewConfig: {
|
||||
getRowClass: function(rec) {
|
||||
let tapeCount = (rec.get('max-seq-nr') ?? 0) + 1;
|
||||
let actualTapeCount = rec.get('tapes') ?? 1;
|
||||
|
||||
if (tapeCount !== actualTapeCount) {
|
||||
return 'proxmox-warning-row';
|
||||
}
|
||||
|
||||
return '';
|
||||
},
|
||||
},
|
||||
|
||||
columns: [
|
||||
{
|
||||
xtype: 'treecolumn',
|
||||
text: gettext('Pool/Media-Set/Snapshot'),
|
||||
dataIndex: 'text',
|
||||
renderer: function(value, mD, rec) {
|
||||
let tapeCount = (rec.get('max-seq-nr') ?? 0) + 1;
|
||||
let actualTapeCount = rec.get('tapes') ?? 1;
|
||||
|
||||
if (tapeCount !== actualTapeCount) {
|
||||
return `${value} (${gettext('Incomplete')})`;
|
||||
}
|
||||
return value;
|
||||
},
|
||||
sortable: false,
|
||||
flex: 3,
|
||||
},
|
||||
|
@ -179,9 +179,6 @@ Ext.define('PBS.TapeManagement.TapeRestoreWindow', {
|
||||
|
||||
updateDatastores: function(grid, values) {
|
||||
let me = this;
|
||||
if (values === 'all') {
|
||||
values = [];
|
||||
}
|
||||
let datastores = {};
|
||||
values.forEach((snapshotOrDatastore) => {
|
||||
let datastore = snapshotOrDatastore;
|
||||
@ -297,14 +294,12 @@ Ext.define('PBS.TapeManagement.TapeRestoreWindow', {
|
||||
onGetValues: function(values) {
|
||||
let me = this;
|
||||
|
||||
if (values !== "all" &&
|
||||
Ext.isString(values.snapshots) &&
|
||||
values.snapshots &&
|
||||
values.snapshots.indexOf(':') !== -1
|
||||
) {
|
||||
values.snapshots = values.snapshots.split(',');
|
||||
} else {
|
||||
delete values.snapshots;
|
||||
// cannot use the string serialized one from onGetValues, so gather manually
|
||||
delete values.snapshots;
|
||||
let snapshots = me.down('pbsTapeSnapshotGrid').getValue();
|
||||
|
||||
if (snapshots.length > 0 && snapshots[0].indexOf(':') !== -1) {
|
||||
values.snapshots = snapshots;
|
||||
}
|
||||
|
||||
return values;
|
||||
@ -378,13 +373,35 @@ Ext.define('PBS.TapeManagement.TapeRestoreWindow', {
|
||||
let vm = controller.getViewModel();
|
||||
let datastores = [];
|
||||
if (values.store.toString() !== "") {
|
||||
if (vm.get('singleDatastore')) {
|
||||
let source = controller.lookup('snapshotGrid').getValue();
|
||||
datastores.push(`${source}=${values.store}`);
|
||||
} else {
|
||||
datastores.push(values.store);
|
||||
}
|
||||
let target = values.store.toString();
|
||||
delete values.store;
|
||||
|
||||
let source = [];
|
||||
if (vm.get('singleDatastore')) {
|
||||
// can be '[]' (for all), a list of datastores, or a list of snapshots
|
||||
source = controller.lookup('snapshotGrid').getValue();
|
||||
if (source.length > 0) {
|
||||
if (source[0].indexOf(':') !== -1) {
|
||||
// one or multiple snapshots are selected
|
||||
// extract datastore from first
|
||||
source = source[0].split(':')[0];
|
||||
} else {
|
||||
// one whole datstore is selected
|
||||
source = source[0];
|
||||
}
|
||||
} else {
|
||||
// must be [] (all snapshots) so we use it as a default target
|
||||
}
|
||||
} else {
|
||||
// there is more than one datastore to be restored, so this is just
|
||||
// the default fallback
|
||||
}
|
||||
|
||||
if (Ext.isString(source)) {
|
||||
datastores.push(`${source}=${target}`);
|
||||
} else {
|
||||
datastores.push(target);
|
||||
}
|
||||
}
|
||||
|
||||
let defaultNs = values.defaultNs;
|
||||
@ -524,6 +541,8 @@ Ext.define('PBS.TapeManagement.DataStoreMappingGrid', {
|
||||
let ns = targetns || defaultNs;
|
||||
if (ns) {
|
||||
namespaces.push(`store=${source},target=${ns}`);
|
||||
} else {
|
||||
namespaces.push(`store=${source}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -708,7 +727,7 @@ Ext.define('PBS.TapeManagement.SnapshotGrid', {
|
||||
let me = this;
|
||||
let snapshots = [];
|
||||
|
||||
let storeCounts = {};
|
||||
let selectedStoreCounts = {};
|
||||
|
||||
me.getSelection().forEach((rec) => {
|
||||
let id = rec.get('id');
|
||||
@ -717,10 +736,10 @@ Ext.define('PBS.TapeManagement.SnapshotGrid', {
|
||||
// only add if not filtered
|
||||
if (me.store.findExact('id', id) !== -1) {
|
||||
snapshots.push(`${store}:${snap}`);
|
||||
if (storeCounts[store] === undefined) {
|
||||
storeCounts[store] = 0;
|
||||
if (selectedStoreCounts[store] === undefined) {
|
||||
selectedStoreCounts[store] = 0;
|
||||
}
|
||||
storeCounts[store]++;
|
||||
selectedStoreCounts[store]++;
|
||||
}
|
||||
});
|
||||
|
||||
@ -728,21 +747,21 @@ Ext.define('PBS.TapeManagement.SnapshotGrid', {
|
||||
let originalData = me.store.getData().getSource() || me.store.getData();
|
||||
|
||||
if (snapshots.length === originalData.length) {
|
||||
return "all";
|
||||
return [];
|
||||
}
|
||||
|
||||
let wholeStores = [];
|
||||
let wholeStoresSelected = true;
|
||||
for (const [store, count] of Object.entries(storeCounts)) {
|
||||
if (me.storeCounts[store] === count) {
|
||||
let onlyWholeStoresSelected = true;
|
||||
for (const [store, selectedCount] of Object.entries(selectedStoreCounts)) {
|
||||
if (me.storeCounts[store] === selectedCount) {
|
||||
wholeStores.push(store);
|
||||
} else {
|
||||
wholeStoresSelected = false;
|
||||
onlyWholeStoresSelected = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (wholeStoresSelected) {
|
||||
if (onlyWholeStoresSelected) {
|
||||
return wholeStores;
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user