Compare commits
18 Commits
Author | SHA1 | Date | |
---|---|---|---|
7397f4a390 | |||
8317873c06 | |||
deef63699e | |||
c6e07769e9 | |||
423df9b1f4 | |||
c879e5af11 | |||
63d9aca96f | |||
c3b1da9e41 | |||
46388e6aef | |||
484d439a7c | |||
ab6615134c | |||
b1149ebb36 | |||
1bfdae7933 | |||
4f09d31085 | |||
58d73ddb1d | |||
6b809ff59b | |||
afe08d2755 | |||
a7bc5d4eaf |
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "proxmox-backup"
|
||||
version = "0.8.13"
|
||||
version = "0.8.14"
|
||||
authors = ["Dietmar Maurer <dietmar@proxmox.com>"]
|
||||
edition = "2018"
|
||||
license = "AGPL-3"
|
||||
@ -39,7 +39,7 @@ pam-sys = "0.5"
|
||||
percent-encoding = "2.1"
|
||||
pin-utils = "0.1.0"
|
||||
pathpatterns = "0.1.2"
|
||||
proxmox = { version = "0.3.3", features = [ "sortable-macro", "api-macro", "websocket" ] }
|
||||
proxmox = { version = "0.3.4", features = [ "sortable-macro", "api-macro", "websocket" ] }
|
||||
#proxmox = { git = "ssh://gitolite3@proxdev.maurer-it.com/rust/proxmox", version = "0.1.2", features = [ "sortable-macro", "api-macro" ] }
|
||||
#proxmox = { path = "../proxmox/proxmox", features = [ "sortable-macro", "api-macro", "websocket" ] }
|
||||
proxmox-fuse = "0.1.0"
|
||||
|
14
debian/changelog
vendored
14
debian/changelog
vendored
@ -1,3 +1,17 @@
|
||||
rust-proxmox-backup (0.8.14-1) unstable; urgency=medium
|
||||
|
||||
* verify speed up: use separate IO thread, use datastore-wide cache (instead
|
||||
of per group)
|
||||
|
||||
* ui: datastore content: improve encrypted column
|
||||
|
||||
* ui: datastore content: show more granular verify state, especially for
|
||||
backup group rows
|
||||
|
||||
* verify: log progress in percent
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Wed, 02 Sep 2020 09:36:47 +0200
|
||||
|
||||
rust-proxmox-backup (0.8.13-1) unstable; urgency=medium
|
||||
|
||||
* improve and add to documentation
|
||||
|
8
debian/control
vendored
8
debian/control
vendored
@ -34,10 +34,10 @@ Build-Depends: debhelper (>= 11),
|
||||
librust-pathpatterns-0.1+default-dev (>= 0.1.2-~~),
|
||||
librust-percent-encoding-2+default-dev (>= 2.1-~~),
|
||||
librust-pin-utils-0.1+default-dev,
|
||||
librust-proxmox-0.3+api-macro-dev (>= 0.3.3-~~),
|
||||
librust-proxmox-0.3+default-dev (>= 0.3.3-~~),
|
||||
librust-proxmox-0.3+sortable-macro-dev (>= 0.3.3-~~),
|
||||
librust-proxmox-0.3+websocket-dev (>= 0.3.3-~~),
|
||||
librust-proxmox-0.3+api-macro-dev (>= 0.3.4-~~),
|
||||
librust-proxmox-0.3+default-dev (>= 0.3.4-~~),
|
||||
librust-proxmox-0.3+sortable-macro-dev (>= 0.3.4-~~),
|
||||
librust-proxmox-0.3+websocket-dev (>= 0.3.4-~~),
|
||||
librust-proxmox-fuse-0.1+default-dev,
|
||||
librust-pxar-0.6+default-dev,
|
||||
librust-pxar-0.6+futures-io-dev,
|
||||
|
9
debian/postinst
vendored
9
debian/postinst
vendored
@ -15,11 +15,10 @@ case "$1" in
|
||||
fi
|
||||
deb-systemd-invoke $_dh_action proxmox-backup.service proxmox-backup-proxy.service >/dev/null || true
|
||||
|
||||
if test -n "$2"; then
|
||||
if dpkg --compare-versions "$2" 'le' '0.8.10-1'; then
|
||||
echo "Fixing up termproxy user id in task log..."
|
||||
flock -w 30 /var/log/proxmox-backup/tasks/active.lock sed -i 's/:termproxy::root: /:termproxy::root@pam: /' /var/log/proxmox-backup/tasks/active
|
||||
fi
|
||||
# FIXME: Remove in future version once we're sure no broken entries remain in anyone's files
|
||||
if grep -q -e ':termproxy::[^@]\+: ' /var/log/proxmox-backup/tasks/active; then
|
||||
echo "Fixing up termproxy user id in task log..."
|
||||
flock -w 30 /var/log/proxmox-backup/tasks/active.lock sed -i 's/:termproxy::\([^@]\+\): /:termproxy::\1@pam: /' /var/log/proxmox-backup/tasks/active
|
||||
fi
|
||||
;;
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
use std::collections::{HashSet, HashMap};
|
||||
use std::ffi::OsStr;
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use anyhow::{bail, format_err, Error};
|
||||
use futures::*;
|
||||
@ -512,18 +513,27 @@ pub fn verify(
|
||||
userid,
|
||||
to_stdout,
|
||||
move |worker| {
|
||||
let verified_chunks = Arc::new(Mutex::new(HashSet::with_capacity(1024*16)));
|
||||
let corrupt_chunks = Arc::new(Mutex::new(HashSet::with_capacity(64)));
|
||||
|
||||
let failed_dirs = if let Some(backup_dir) = backup_dir {
|
||||
let mut verified_chunks = HashSet::with_capacity(1024*16);
|
||||
let mut corrupt_chunks = HashSet::with_capacity(64);
|
||||
let mut res = Vec::new();
|
||||
if !verify_backup_dir(&datastore, &backup_dir, &mut verified_chunks, &mut corrupt_chunks, &worker)? {
|
||||
if !verify_backup_dir(datastore, &backup_dir, verified_chunks, corrupt_chunks, worker.clone())? {
|
||||
res.push(backup_dir.to_string());
|
||||
}
|
||||
res
|
||||
} else if let Some(backup_group) = backup_group {
|
||||
verify_backup_group(&datastore, &backup_group, &worker)?
|
||||
let (_count, failed_dirs) = verify_backup_group(
|
||||
datastore,
|
||||
&backup_group,
|
||||
verified_chunks,
|
||||
corrupt_chunks,
|
||||
None,
|
||||
worker.clone(),
|
||||
)?;
|
||||
failed_dirs
|
||||
} else {
|
||||
verify_all_backups(&datastore, &worker)?
|
||||
verify_all_backups(datastore, worker.clone())?
|
||||
};
|
||||
if failed_dirs.len() > 0 {
|
||||
worker.log("Failed to verify following snapshots:");
|
||||
|
@ -295,7 +295,7 @@ impl ChunkStore {
|
||||
for (entry, percentage) in self.get_chunk_iterator()? {
|
||||
if last_percentage != percentage {
|
||||
last_percentage = percentage;
|
||||
worker.log(format!("{}%, processed {} chunks", percentage, chunk_count));
|
||||
worker.log(format!("percentage done: phase2 {}% (processed {} chunks)", percentage, chunk_count));
|
||||
}
|
||||
|
||||
worker.fail_on_abort()?;
|
||||
|
@ -304,7 +304,7 @@ impl DataBlob {
|
||||
|
||||
let digest = match config {
|
||||
Some(config) => config.compute_digest(data),
|
||||
None => openssl::sha::sha256(&data),
|
||||
None => openssl::sha::sha256(data),
|
||||
};
|
||||
if &digest != expected_digest {
|
||||
bail!("detected chunk with wrong digest.");
|
||||
|
@ -430,6 +430,12 @@ impl DataStore {
|
||||
|
||||
let image_list = self.list_images()?;
|
||||
|
||||
let image_count = image_list.len();
|
||||
|
||||
let mut done = 0;
|
||||
|
||||
let mut last_percentage: usize = 0;
|
||||
|
||||
for path in image_list {
|
||||
|
||||
worker.fail_on_abort()?;
|
||||
@ -444,6 +450,14 @@ impl DataStore {
|
||||
self.index_mark_used_chunks(index, &path, status, worker)?;
|
||||
}
|
||||
}
|
||||
done += 1;
|
||||
|
||||
let percentage = done*100/image_count;
|
||||
if percentage > last_percentage {
|
||||
worker.log(format!("percentage done: phase1 {}% ({} of {} index files)",
|
||||
percentage, done, image_count));
|
||||
last_percentage = percentage;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
@ -1,4 +1,7 @@
|
||||
use std::collections::HashSet;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::sync::atomic::{Ordering, AtomicUsize};
|
||||
use std::time::Instant;
|
||||
|
||||
use anyhow::{bail, format_err, Error};
|
||||
|
||||
@ -6,12 +9,12 @@ use crate::server::WorkerTask;
|
||||
use crate::api2::types::*;
|
||||
|
||||
use super::{
|
||||
DataStore, BackupGroup, BackupDir, BackupInfo, IndexFile,
|
||||
DataStore, DataBlob, BackupGroup, BackupDir, BackupInfo, IndexFile,
|
||||
CryptMode,
|
||||
FileInfo, ArchiveType, archive_type,
|
||||
};
|
||||
|
||||
fn verify_blob(datastore: &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)?;
|
||||
|
||||
@ -36,48 +39,97 @@ fn verify_blob(datastore: &DataStore, backup_dir: &BackupDir, info: &FileInfo) -
|
||||
}
|
||||
}
|
||||
|
||||
// We use a separate thread to read/load chunks, so that we can do
|
||||
// load and verify in parallel to increase performance.
|
||||
fn chunk_reader_thread(
|
||||
datastore: Arc<DataStore>,
|
||||
index: Box<dyn IndexFile + Send>,
|
||||
verified_chunks: Arc<Mutex<HashSet<[u8;32]>>>,
|
||||
corrupt_chunks: Arc<Mutex<HashSet<[u8;32]>>>,
|
||||
errors: Arc<AtomicUsize>,
|
||||
worker: Arc<WorkerTask>,
|
||||
) -> std::sync::mpsc::Receiver<(DataBlob, [u8;32], u64)> {
|
||||
|
||||
let (sender, receiver) = std::sync::mpsc::sync_channel(3); // buffer up to 3 chunks
|
||||
|
||||
std::thread::spawn(move|| {
|
||||
for pos in 0..index.index_count() {
|
||||
let info = index.chunk_info(pos).unwrap();
|
||||
let size = info.range.end - info.range.start;
|
||||
|
||||
if verified_chunks.lock().unwrap().contains(&info.digest) {
|
||||
continue; // already verified
|
||||
}
|
||||
|
||||
if corrupt_chunks.lock().unwrap().contains(&info.digest) {
|
||||
let digest_str = proxmox::tools::digest_to_hex(&info.digest);
|
||||
worker.log(format!("chunk {} was marked as corrupt", digest_str));
|
||||
errors.fetch_add(1, Ordering::SeqCst);
|
||||
continue;
|
||||
}
|
||||
|
||||
match datastore.load_chunk(&info.digest) {
|
||||
Err(err) => {
|
||||
corrupt_chunks.lock().unwrap().insert(info.digest);
|
||||
worker.log(format!("can't verify chunk, load failed - {}", err));
|
||||
errors.fetch_add(1, Ordering::SeqCst);
|
||||
continue;
|
||||
}
|
||||
Ok(chunk) => {
|
||||
if sender.send((chunk, info.digest, size)).is_err() {
|
||||
break; // receiver gone - simply stop
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
receiver
|
||||
}
|
||||
|
||||
fn verify_index_chunks(
|
||||
datastore: &DataStore,
|
||||
index: Box<dyn IndexFile>,
|
||||
verified_chunks: &mut HashSet<[u8;32]>,
|
||||
corrupt_chunks: &mut HashSet<[u8; 32]>,
|
||||
datastore: Arc<DataStore>,
|
||||
index: Box<dyn IndexFile + Send>,
|
||||
verified_chunks: Arc<Mutex<HashSet<[u8;32]>>>,
|
||||
corrupt_chunks: Arc<Mutex<HashSet<[u8; 32]>>>,
|
||||
crypt_mode: CryptMode,
|
||||
worker: &WorkerTask,
|
||||
worker: Arc<WorkerTask>,
|
||||
) -> Result<(), Error> {
|
||||
|
||||
let mut errors = 0;
|
||||
for pos in 0..index.index_count() {
|
||||
let errors = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
let start_time = Instant::now();
|
||||
|
||||
let chunk_channel = chunk_reader_thread(
|
||||
datastore,
|
||||
index,
|
||||
verified_chunks.clone(),
|
||||
corrupt_chunks.clone(),
|
||||
errors.clone(),
|
||||
worker.clone(),
|
||||
);
|
||||
|
||||
let mut read_bytes = 0;
|
||||
let mut decoded_bytes = 0;
|
||||
|
||||
loop {
|
||||
|
||||
worker.fail_on_abort()?;
|
||||
crate::tools::fail_on_shutdown()?;
|
||||
|
||||
let info = index.chunk_info(pos).unwrap();
|
||||
|
||||
if verified_chunks.contains(&info.digest) {
|
||||
continue; // already verified
|
||||
}
|
||||
|
||||
if corrupt_chunks.contains(&info.digest) {
|
||||
let digest_str = proxmox::tools::digest_to_hex(&info.digest);
|
||||
worker.log(format!("chunk {} was marked as corrupt", digest_str));
|
||||
errors += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
let chunk = match datastore.load_chunk(&info.digest) {
|
||||
Err(err) => {
|
||||
corrupt_chunks.insert(info.digest);
|
||||
worker.log(format!("can't verify chunk, load failed - {}", err));
|
||||
errors += 1;
|
||||
continue;
|
||||
},
|
||||
Ok(chunk) => chunk,
|
||||
let (chunk, digest, size) = match chunk_channel.recv() {
|
||||
Ok(tuple) => tuple,
|
||||
Err(std::sync::mpsc::RecvError) => break,
|
||||
};
|
||||
|
||||
read_bytes += chunk.raw_size();
|
||||
decoded_bytes += size;
|
||||
|
||||
let chunk_crypt_mode = match chunk.crypt_mode() {
|
||||
Err(err) => {
|
||||
corrupt_chunks.insert(info.digest);
|
||||
corrupt_chunks.lock().unwrap().insert(digest);
|
||||
worker.log(format!("can't verify chunk, unknown CryptMode - {}", err));
|
||||
errors += 1;
|
||||
errors.fetch_add(1, Ordering::SeqCst);
|
||||
continue;
|
||||
},
|
||||
Ok(mode) => mode,
|
||||
@ -89,21 +141,32 @@ fn verify_index_chunks(
|
||||
chunk_crypt_mode,
|
||||
crypt_mode
|
||||
));
|
||||
errors += 1;
|
||||
errors.fetch_add(1, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
let size = info.range.end - info.range.start;
|
||||
|
||||
if let Err(err) = chunk.verify_unencrypted(size as usize, &info.digest) {
|
||||
corrupt_chunks.insert(info.digest);
|
||||
if let Err(err) = chunk.verify_unencrypted(size as usize, &digest) {
|
||||
corrupt_chunks.lock().unwrap().insert(digest);
|
||||
worker.log(format!("{}", err));
|
||||
errors += 1;
|
||||
errors.fetch_add(1, Ordering::SeqCst);
|
||||
} else {
|
||||
verified_chunks.insert(info.digest);
|
||||
verified_chunks.lock().unwrap().insert(digest);
|
||||
}
|
||||
}
|
||||
|
||||
if errors > 0 {
|
||||
let elapsed = start_time.elapsed().as_secs_f64();
|
||||
|
||||
let read_bytes_mib = (read_bytes as f64)/(1024.0*1024.0);
|
||||
let decoded_bytes_mib = (decoded_bytes as f64)/(1024.0*1024.0);
|
||||
|
||||
let read_speed = read_bytes_mib/elapsed;
|
||||
let decode_speed = decoded_bytes_mib/elapsed;
|
||||
|
||||
let error_count = errors.load(Ordering::SeqCst);
|
||||
|
||||
worker.log(format!(" verified {:.2}/{:.2} Mib in {:.2} seconds, speed {:.2}/{:.2} Mib/s ({} errors)",
|
||||
read_bytes_mib, decoded_bytes_mib, elapsed, read_speed, decode_speed, error_count));
|
||||
|
||||
if errors.load(Ordering::SeqCst) > 0 {
|
||||
bail!("chunks could not be verified");
|
||||
}
|
||||
|
||||
@ -111,12 +174,12 @@ fn verify_index_chunks(
|
||||
}
|
||||
|
||||
fn verify_fixed_index(
|
||||
datastore: &DataStore,
|
||||
datastore: Arc<DataStore>,
|
||||
backup_dir: &BackupDir,
|
||||
info: &FileInfo,
|
||||
verified_chunks: &mut HashSet<[u8;32]>,
|
||||
corrupt_chunks: &mut HashSet<[u8;32]>,
|
||||
worker: &WorkerTask,
|
||||
verified_chunks: Arc<Mutex<HashSet<[u8;32]>>>,
|
||||
corrupt_chunks: Arc<Mutex<HashSet<[u8;32]>>>,
|
||||
worker: Arc<WorkerTask>,
|
||||
) -> Result<(), Error> {
|
||||
|
||||
let mut path = backup_dir.relative_path();
|
||||
@ -137,12 +200,12 @@ fn verify_fixed_index(
|
||||
}
|
||||
|
||||
fn verify_dynamic_index(
|
||||
datastore: &DataStore,
|
||||
datastore: Arc<DataStore>,
|
||||
backup_dir: &BackupDir,
|
||||
info: &FileInfo,
|
||||
verified_chunks: &mut HashSet<[u8;32]>,
|
||||
corrupt_chunks: &mut HashSet<[u8;32]>,
|
||||
worker: &WorkerTask,
|
||||
verified_chunks: Arc<Mutex<HashSet<[u8;32]>>>,
|
||||
corrupt_chunks: Arc<Mutex<HashSet<[u8;32]>>>,
|
||||
worker: Arc<WorkerTask>,
|
||||
) -> Result<(), Error> {
|
||||
|
||||
let mut path = backup_dir.relative_path();
|
||||
@ -172,11 +235,11 @@ fn verify_dynamic_index(
|
||||
/// - Ok(false) if there were verification errors
|
||||
/// - Err(_) if task was aborted
|
||||
pub fn verify_backup_dir(
|
||||
datastore: &DataStore,
|
||||
datastore: Arc<DataStore>,
|
||||
backup_dir: &BackupDir,
|
||||
verified_chunks: &mut HashSet<[u8;32]>,
|
||||
corrupt_chunks: &mut HashSet<[u8;32]>,
|
||||
worker: &WorkerTask
|
||||
verified_chunks: Arc<Mutex<HashSet<[u8;32]>>>,
|
||||
corrupt_chunks: Arc<Mutex<HashSet<[u8;32]>>>,
|
||||
worker: Arc<WorkerTask>
|
||||
) -> Result<bool, Error> {
|
||||
|
||||
let mut manifest = match datastore.load_manifest(&backup_dir) {
|
||||
@ -198,27 +261,28 @@ pub fn verify_backup_dir(
|
||||
match archive_type(&info.filename)? {
|
||||
ArchiveType::FixedIndex =>
|
||||
verify_fixed_index(
|
||||
&datastore,
|
||||
datastore.clone(),
|
||||
&backup_dir,
|
||||
info,
|
||||
verified_chunks,
|
||||
corrupt_chunks,
|
||||
worker
|
||||
verified_chunks.clone(),
|
||||
corrupt_chunks.clone(),
|
||||
worker.clone(),
|
||||
),
|
||||
ArchiveType::DynamicIndex =>
|
||||
verify_dynamic_index(
|
||||
&datastore,
|
||||
datastore.clone(),
|
||||
&backup_dir,
|
||||
info,
|
||||
verified_chunks,
|
||||
corrupt_chunks,
|
||||
worker
|
||||
verified_chunks.clone(),
|
||||
corrupt_chunks.clone(),
|
||||
worker.clone(),
|
||||
),
|
||||
ArchiveType::Blob => verify_blob(&datastore, &backup_dir, info),
|
||||
ArchiveType::Blob => verify_blob(datastore.clone(), &backup_dir, info),
|
||||
}
|
||||
});
|
||||
|
||||
worker.fail_on_abort()?;
|
||||
crate::tools::fail_on_shutdown()?;
|
||||
|
||||
if let Err(err) = result {
|
||||
worker.log(format!("verify {}:{}/{} failed: {}", datastore.name(), backup_dir, info.filename, err));
|
||||
@ -245,32 +309,45 @@ pub fn verify_backup_dir(
|
||||
/// Errors are logged to the worker log.
|
||||
///
|
||||
/// Returns
|
||||
/// - Ok(failed_dirs) where failed_dirs had verification errors
|
||||
/// - Ok((count, failed_dirs)) where failed_dirs had verification errors
|
||||
/// - Err(_) if task was aborted
|
||||
pub fn verify_backup_group(datastore: &DataStore, group: &BackupGroup, worker: &WorkerTask) -> Result<Vec<String>, Error> {
|
||||
pub fn verify_backup_group(
|
||||
datastore: Arc<DataStore>,
|
||||
group: &BackupGroup,
|
||||
verified_chunks: Arc<Mutex<HashSet<[u8;32]>>>,
|
||||
corrupt_chunks: Arc<Mutex<HashSet<[u8;32]>>>,
|
||||
progress: Option<(usize, usize)>, // (done, snapshot_count)
|
||||
worker: Arc<WorkerTask>,
|
||||
) -> Result<(usize, Vec<String>), Error> {
|
||||
|
||||
let mut errors = Vec::new();
|
||||
let mut list = match group.list_backups(&datastore.base_path()) {
|
||||
Ok(list) => list,
|
||||
Err(err) => {
|
||||
worker.log(format!("verify group {}:{} - unable to list backups: {}", datastore.name(), group, err));
|
||||
return Ok(errors);
|
||||
return Ok((0, errors));
|
||||
}
|
||||
};
|
||||
|
||||
worker.log(format!("verify group {}:{}", datastore.name(), group));
|
||||
|
||||
let mut verified_chunks = HashSet::with_capacity(1024*16); // start with 16384 chunks (up to 65GB)
|
||||
let mut corrupt_chunks = HashSet::with_capacity(64); // start with 64 chunks since we assume there are few corrupt ones
|
||||
let (done, snapshot_count) = progress.unwrap_or((0, list.len()));
|
||||
|
||||
let mut count = 0;
|
||||
BackupInfo::sort_list(&mut list, false); // newest first
|
||||
for info in list {
|
||||
if !verify_backup_dir(datastore, &info.backup_dir, &mut verified_chunks, &mut corrupt_chunks, worker)?{
|
||||
count += 1;
|
||||
if !verify_backup_dir(datastore.clone(), &info.backup_dir, verified_chunks.clone(), corrupt_chunks.clone(), worker.clone())?{
|
||||
errors.push(info.backup_dir.to_string());
|
||||
}
|
||||
if snapshot_count != 0 {
|
||||
let pos = done + count;
|
||||
let percentage = ((pos as f64) * 100.0)/(snapshot_count as f64);
|
||||
worker.log(format!("percentage done: {:.2}% ({} of {} snapshots)", percentage, pos, snapshot_count));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(errors)
|
||||
Ok((count, errors))
|
||||
}
|
||||
|
||||
/// Verify all backups inside a datastore
|
||||
@ -280,7 +357,7 @@ pub fn verify_backup_group(datastore: &DataStore, group: &BackupGroup, worker: &
|
||||
/// Returns
|
||||
/// - Ok(failed_dirs) where failed_dirs had verification errors
|
||||
/// - Err(_) if task was aborted
|
||||
pub fn verify_all_backups(datastore: &DataStore, worker: &WorkerTask) -> Result<Vec<String>, Error> {
|
||||
pub fn verify_all_backups(datastore: Arc<DataStore>, worker: Arc<WorkerTask>) -> Result<Vec<String>, Error> {
|
||||
|
||||
let mut errors = Vec::new();
|
||||
|
||||
@ -294,11 +371,32 @@ pub fn verify_all_backups(datastore: &DataStore, worker: &WorkerTask) -> Result<
|
||||
|
||||
list.sort_unstable();
|
||||
|
||||
worker.log(format!("verify datastore {}", datastore.name()));
|
||||
let mut snapshot_count = 0;
|
||||
for group in list.iter() {
|
||||
snapshot_count += group.list_backups(&datastore.base_path())?.len();
|
||||
}
|
||||
|
||||
// start with 16384 chunks (up to 65GB)
|
||||
let verified_chunks = Arc::new(Mutex::new(HashSet::with_capacity(1024*16)));
|
||||
|
||||
// start with 64 chunks since we assume there are few corrupt ones
|
||||
let corrupt_chunks = Arc::new(Mutex::new(HashSet::with_capacity(64)));
|
||||
|
||||
worker.log(format!("verify datastore {} ({} snapshots)", datastore.name(), snapshot_count));
|
||||
|
||||
let mut done = 0;
|
||||
for group in list {
|
||||
let mut group_errors = verify_backup_group(datastore, &group, worker)?;
|
||||
let (count, mut group_errors) = verify_backup_group(
|
||||
datastore.clone(),
|
||||
&group,
|
||||
verified_chunks.clone(),
|
||||
corrupt_chunks.clone(),
|
||||
Some((done, snapshot_count)),
|
||||
worker.clone(),
|
||||
)?;
|
||||
errors.append(&mut group_errors);
|
||||
|
||||
done += count;
|
||||
}
|
||||
|
||||
Ok(errors)
|
||||
|
@ -6,17 +6,16 @@ Ext.define('pbs-data-store-snapshots', {
|
||||
{
|
||||
name: 'backup-time',
|
||||
type: 'date',
|
||||
dateFormat: 'timestamp'
|
||||
dateFormat: 'timestamp',
|
||||
},
|
||||
'files',
|
||||
'owner',
|
||||
'verification',
|
||||
{ name: 'size', type: 'int', allowNull: true, },
|
||||
{ name: 'size', type: 'int', allowNull: true },
|
||||
{
|
||||
name: 'crypt-mode',
|
||||
type: 'boolean',
|
||||
calculate: function(data) {
|
||||
let encrypted = 0;
|
||||
let crypt = {
|
||||
none: 0,
|
||||
mixed: 0,
|
||||
@ -24,25 +23,24 @@ Ext.define('pbs-data-store-snapshots', {
|
||||
encrypt: 0,
|
||||
count: 0,
|
||||
};
|
||||
let signed = 0;
|
||||
data.files.forEach(file => {
|
||||
if (file.filename === 'index.json.blob') return; // is never encrypted
|
||||
let mode = PBS.Utils.cryptmap.indexOf(file['crypt-mode']);
|
||||
if (mode !== -1) {
|
||||
crypt[file['crypt-mode']]++;
|
||||
crypt.count++;
|
||||
}
|
||||
crypt.count++;
|
||||
});
|
||||
|
||||
return PBS.Utils.calculateCryptMode(crypt);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'matchesFilter',
|
||||
type: 'boolean',
|
||||
defaultValue: true,
|
||||
},
|
||||
]
|
||||
],
|
||||
});
|
||||
|
||||
Ext.define('PBS.DataStoreContent', {
|
||||
@ -70,7 +68,7 @@ Ext.define('PBS.DataStoreContent', {
|
||||
view.getStore().setSorters([
|
||||
'backup-group',
|
||||
'text',
|
||||
'backup-time'
|
||||
'backup-time',
|
||||
]);
|
||||
Proxmox.Utils.monStoreErrors(view, this.store);
|
||||
this.reload(); // initial load
|
||||
@ -88,7 +86,7 @@ Ext.define('PBS.DataStoreContent', {
|
||||
this.store.setProxy({
|
||||
type: 'proxmox',
|
||||
timeout: 300*1000, // 5 minutes, we should make that api call faster
|
||||
url: url
|
||||
url: url,
|
||||
});
|
||||
|
||||
this.store.load();
|
||||
@ -124,7 +122,7 @@ Ext.define('PBS.DataStoreContent', {
|
||||
expanded: false,
|
||||
backup_type: item.data["backup-type"],
|
||||
backup_id: item.data["backup-id"],
|
||||
children: []
|
||||
children: [],
|
||||
};
|
||||
}
|
||||
|
||||
@ -163,7 +161,7 @@ Ext.define('PBS.DataStoreContent', {
|
||||
}
|
||||
return false;
|
||||
},
|
||||
after: () => {},
|
||||
after: Ext.emptyFn,
|
||||
});
|
||||
|
||||
for (const item of records) {
|
||||
@ -181,7 +179,7 @@ Ext.define('PBS.DataStoreContent', {
|
||||
|
||||
data.children = [];
|
||||
for (const file of data.files) {
|
||||
file.text = file.filename,
|
||||
file.text = file.filename;
|
||||
file['crypt-mode'] = PBS.Utils.cryptmap.indexOf(file['crypt-mode']);
|
||||
file.leaf = true;
|
||||
file.matchesFilter = true;
|
||||
@ -192,6 +190,7 @@ Ext.define('PBS.DataStoreContent', {
|
||||
children.push(data);
|
||||
}
|
||||
|
||||
let nowSeconds = Date.now() / 1000;
|
||||
let children = [];
|
||||
for (const [name, group] of Object.entries(groups)) {
|
||||
let last_backup = 0;
|
||||
@ -201,7 +200,13 @@ Ext.define('PBS.DataStoreContent', {
|
||||
'sign-only': 0,
|
||||
encrypt: 0,
|
||||
};
|
||||
for (const item of group.children) {
|
||||
let verify = {
|
||||
outdated: 0,
|
||||
none: 0,
|
||||
failed: 0,
|
||||
ok: 0,
|
||||
};
|
||||
for (let item of group.children) {
|
||||
crypt[PBS.Utils.cryptmap[item['crypt-mode']]]++;
|
||||
if (item["backup-time"] > last_backup && item.size !== null) {
|
||||
last_backup = item["backup-time"];
|
||||
@ -209,13 +214,24 @@ Ext.define('PBS.DataStoreContent', {
|
||||
group.files = item.files;
|
||||
group.size = item.size;
|
||||
group.owner = item.owner;
|
||||
verify.lastFailed = item.verification && item.verification.state !== 'ok';
|
||||
}
|
||||
if (item.verification &&
|
||||
(!group.verification || group.verification.state !== 'failed')) {
|
||||
group.verification = item.verification;
|
||||
if (!item.verification) {
|
||||
verify.none++;
|
||||
} else {
|
||||
if (item.verification.state === 'ok') {
|
||||
verify.ok++;
|
||||
} else {
|
||||
verify.failed++;
|
||||
}
|
||||
let task = Proxmox.Utils.parse_task_upid(item.verification.upid);
|
||||
item.verification.lastTime = task.starttime;
|
||||
if (nowSeconds - task.starttime > 30 * 24 * 60 * 60) {
|
||||
verify.outdated++;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
group.verification = verify;
|
||||
group.count = group.children.length;
|
||||
group.matchesFilter = true;
|
||||
crypt.count = group.count;
|
||||
@ -226,7 +242,7 @@ Ext.define('PBS.DataStoreContent', {
|
||||
|
||||
view.setRootNode({
|
||||
expanded: true,
|
||||
children: children
|
||||
children: children,
|
||||
});
|
||||
|
||||
if (selected !== undefined) {
|
||||
@ -246,13 +262,13 @@ Ext.define('PBS.DataStoreContent', {
|
||||
Proxmox.Utils.setErrorMask(view, false);
|
||||
if (view.getStore().getFilters().length > 0) {
|
||||
let searchBox = me.lookup("searchbox");
|
||||
let searchvalue = searchBox.getValue();;
|
||||
let searchvalue = searchBox.getValue();
|
||||
me.search(searchBox, searchvalue);
|
||||
}
|
||||
},
|
||||
|
||||
onPrune: function(view, rI, cI, item, e, rec) {
|
||||
var view = this.getView();
|
||||
view = this.getView();
|
||||
|
||||
if (!(rec && rec.data)) return;
|
||||
let data = rec.data;
|
||||
@ -270,7 +286,8 @@ Ext.define('PBS.DataStoreContent', {
|
||||
},
|
||||
|
||||
onVerify: function(view, rI, cI, item, e, rec) {
|
||||
var view = this.getView();
|
||||
let me = this;
|
||||
view = me.getView();
|
||||
|
||||
if (!view.datastore) return;
|
||||
|
||||
@ -302,6 +319,7 @@ Ext.define('PBS.DataStoreContent', {
|
||||
success: function(response, options) {
|
||||
Ext.create('Proxmox.window.TaskViewer', {
|
||||
upid: response.result.data,
|
||||
taskDone: () => me.reload(),
|
||||
}).show();
|
||||
},
|
||||
});
|
||||
@ -309,7 +327,7 @@ Ext.define('PBS.DataStoreContent', {
|
||||
|
||||
onForget: function(view, rI, cI, item, e, rec) {
|
||||
let me = this;
|
||||
var view = this.getView();
|
||||
view = this.getView();
|
||||
|
||||
if (!(rec && rec.data)) return;
|
||||
let data = rec.data;
|
||||
@ -364,7 +382,8 @@ Ext.define('PBS.DataStoreContent', {
|
||||
let atag = document.createElement('a');
|
||||
params['file-name'] = file;
|
||||
atag.download = filename;
|
||||
let url = new URL(`/api2/json/admin/datastore/${view.datastore}/download-decoded`, window.location.origin);
|
||||
let url = new URL(`/api2/json/admin/datastore/${view.datastore}/download-decoded`,
|
||||
window.location.origin);
|
||||
for (const [key, value] of Object.entries(params)) {
|
||||
url.searchParams.append(key, value);
|
||||
}
|
||||
@ -427,7 +446,7 @@ Ext.define('PBS.DataStoreContent', {
|
||||
store.beginUpdate();
|
||||
store.getRoot().cascadeBy({
|
||||
before: function(item) {
|
||||
if(me.filter(item, value)) {
|
||||
if (me.filter(item, value)) {
|
||||
item.set('matchesFilter', true);
|
||||
if (item.parentNode && item.parentNode.id !== 'root') {
|
||||
item.parentNode.childmatches = true;
|
||||
@ -459,12 +478,22 @@ Ext.define('PBS.DataStoreContent', {
|
||||
},
|
||||
},
|
||||
|
||||
viewConfig: {
|
||||
getRowClass: function(record, index) {
|
||||
let verify = record.get('verification');
|
||||
if (verify && verify.lastFailed) {
|
||||
return 'proxmox-invalid-row';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
},
|
||||
|
||||
columns: [
|
||||
{
|
||||
xtype: 'treecolumn',
|
||||
header: gettext("Backup Group"),
|
||||
dataIndex: 'text',
|
||||
flex: 1
|
||||
flex: 1,
|
||||
},
|
||||
{
|
||||
header: gettext('Actions'),
|
||||
@ -511,9 +540,9 @@ Ext.define('PBS.DataStoreContent', {
|
||||
data.filename &&
|
||||
data.filename.endsWith('pxar.didx') &&
|
||||
data['crypt-mode'] < 3);
|
||||
}
|
||||
},
|
||||
},
|
||||
]
|
||||
],
|
||||
},
|
||||
{
|
||||
xtype: 'datecolumn',
|
||||
@ -521,7 +550,7 @@ Ext.define('PBS.DataStoreContent', {
|
||||
sortable: true,
|
||||
dataIndex: 'backup-time',
|
||||
format: 'Y-m-d H:i:s',
|
||||
width: 150
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
header: gettext("Size"),
|
||||
@ -543,6 +572,8 @@ Ext.define('PBS.DataStoreContent', {
|
||||
format: '0',
|
||||
header: gettext("Count"),
|
||||
sortable: true,
|
||||
width: 75,
|
||||
align: 'right',
|
||||
dataIndex: 'count',
|
||||
},
|
||||
{
|
||||
@ -565,29 +596,66 @@ Ext.define('PBS.DataStoreContent', {
|
||||
if (iconCls) {
|
||||
iconTxt = `<i class="fa fa-fw fa-${iconCls}"></i> `;
|
||||
}
|
||||
return (iconTxt + PBS.Utils.cryptText[v]) || Proxmox.Utils.unknownText
|
||||
}
|
||||
return (iconTxt + PBS.Utils.cryptText[v]) || Proxmox.Utils.unknownText;
|
||||
},
|
||||
},
|
||||
{
|
||||
header: gettext('Verify State'),
|
||||
sortable: true,
|
||||
dataIndex: 'verification',
|
||||
width: 120,
|
||||
renderer: (v, meta, record) => {
|
||||
if (v === undefined || v === null || !v.state) {
|
||||
//meta.tdCls = "x-grid-row-loading";
|
||||
return record.data.leaf ? '' : gettext('None');
|
||||
let i = (cls, txt) => `<i class="fa fa-fw fa-${cls}"></i> ${txt}`;
|
||||
if (v === undefined || v === null) {
|
||||
return record.data.leaf ? '' : i('question-circle-o warning', gettext('None'));
|
||||
}
|
||||
let task = Proxmox.Utils.parse_task_upid(v.upid);
|
||||
let verify_time = Proxmox.Utils.render_timestamp(task.starttime);
|
||||
let iconCls = v.state === 'ok' ? 'check good' : 'times critical';
|
||||
let tip = `Verify task started on ${verify_time}`;
|
||||
let tip, iconCls, txt;
|
||||
if (record.parentNode.id === 'root') {
|
||||
tip = v.state === 'ok'
|
||||
? 'All verification OK in backup group'
|
||||
: 'At least one failed verification in backup group!';
|
||||
if (v.failed === 0) {
|
||||
if (v.none === 0) {
|
||||
if (v.outdated > 0) {
|
||||
tip = 'All OK, but some snapshots were not verified in last 30 days';
|
||||
iconCls = 'check warning';
|
||||
txt = gettext('All OK (old)');
|
||||
} else {
|
||||
tip = 'All snapshots verified at least once in last 30 days';
|
||||
iconCls = 'check good';
|
||||
txt = gettext('All OK');
|
||||
}
|
||||
} else if (v.ok === 0) {
|
||||
tip = `${v.none} not verified yet`;
|
||||
iconCls = 'question-circle-o warning';
|
||||
txt = gettext('None');
|
||||
} else {
|
||||
tip = `${v.ok} OK, ${v.none} not verified yet`;
|
||||
iconCls = 'check faded';
|
||||
txt = `${v.ok} OK`;
|
||||
}
|
||||
} else {
|
||||
tip = `${v.ok} OK, ${v.failed} failed, ${v.none} not verified yet`;
|
||||
iconCls = 'times critical';
|
||||
txt = v.ok === 0 && v.none === 0
|
||||
? gettext('All failed')
|
||||
: `${v.failed} failed`;
|
||||
}
|
||||
} else if (!v.state) {
|
||||
return record.data.leaf ? '' : gettext('None');
|
||||
} else {
|
||||
let verify_time = Proxmox.Utils.render_timestamp(v.lastTime);
|
||||
tip = `Last verify task started on ${verify_time}`;
|
||||
txt = v.state;
|
||||
iconCls = 'times critical';
|
||||
if (v.state === 'ok') {
|
||||
iconCls = 'check good';
|
||||
let now = Date.now() / 1000;
|
||||
if (now - v.lastTime > 30 * 24 * 60 * 60) {
|
||||
tip = `Last verify task over 30 days ago: ${verify_time}`;
|
||||
iconCls = 'check warning';
|
||||
}
|
||||
}
|
||||
}
|
||||
return `<span data-qtip="${tip}">
|
||||
<i class="fa fa-fw fa-${iconCls}"></i> ${v.state}
|
||||
<i class="fa fa-fw fa-${iconCls}"></i> ${txt}
|
||||
</span>`;
|
||||
},
|
||||
listeners: {
|
||||
@ -619,6 +687,7 @@ Ext.define('PBS.DataStoreContent', {
|
||||
{
|
||||
xtype: 'textfield',
|
||||
reference: 'searchbox',
|
||||
emptyText: gettext('group, date or owner'),
|
||||
triggers: {
|
||||
clear: {
|
||||
cls: 'pmx-clear-trigger',
|
||||
@ -628,7 +697,7 @@ Ext.define('PBS.DataStoreContent', {
|
||||
this.triggers.clear.setVisible(false);
|
||||
this.setValue('');
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
listeners: {
|
||||
change: {
|
||||
@ -636,6 +705,6 @@ Ext.define('PBS.DataStoreContent', {
|
||||
buffer: 500,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
],
|
||||
});
|
||||
|
Reference in New Issue
Block a user