2021-05-11 10:50:04 +00:00
|
|
|
use std::path::{Path, PathBuf};
|
2020-12-30 08:48:18 +00:00
|
|
|
use std::ffi::OsStr;
|
2021-05-11 10:50:04 +00:00
|
|
|
use std::collections::{HashMap, HashSet, BTreeMap};
|
2020-12-30 08:48:18 +00:00
|
|
|
use std::convert::TryFrom;
|
2021-03-23 12:39:33 +00:00
|
|
|
use std::io::{Seek, SeekFrom};
|
2021-03-24 13:10:14 +00:00
|
|
|
use std::sync::Arc;
|
2020-12-30 08:48:18 +00:00
|
|
|
|
|
|
|
use anyhow::{bail, format_err, Error};
|
2020-12-31 09:26:48 +00:00
|
|
|
use serde_json::Value;
|
2020-12-30 08:48:18 +00:00
|
|
|
|
|
|
|
use proxmox::{
|
2020-12-31 09:26:48 +00:00
|
|
|
api::{
|
|
|
|
api,
|
|
|
|
RpcEnvironment,
|
|
|
|
RpcEnvironmentType,
|
|
|
|
Router,
|
2021-03-05 10:40:52 +00:00
|
|
|
Permission,
|
2021-03-24 13:10:14 +00:00
|
|
|
schema::parse_property_string,
|
2020-12-31 09:26:48 +00:00
|
|
|
section_config::SectionConfigData,
|
|
|
|
},
|
2020-12-30 08:48:18 +00:00
|
|
|
tools::{
|
|
|
|
Uuid,
|
|
|
|
io::ReadExt,
|
|
|
|
fs::{
|
|
|
|
replace_file,
|
|
|
|
CreateOptions,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
2021-09-09 08:32:44 +00:00
|
|
|
use pbs_api_types::{
|
|
|
|
Authid, Userid, CryptMode,
|
|
|
|
DATASTORE_MAP_ARRAY_SCHEMA, DATASTORE_MAP_LIST_SCHEMA, DRIVE_NAME_SCHEMA,
|
|
|
|
UPID_SCHEMA, TAPE_RESTORE_SNAPSHOT_SCHEMA,
|
|
|
|
PRIV_DATASTORE_BACKUP, PRIV_DATASTORE_MODIFY, PRIV_TAPE_READ,
|
|
|
|
};
|
2021-08-30 09:49:22 +00:00
|
|
|
use pbs_datastore::{task_log, task_warn, DataBlob};
|
|
|
|
use pbs_datastore::backup_info::BackupDir;
|
|
|
|
use pbs_datastore::dynamic_index::DynamicIndexReader;
|
|
|
|
use pbs_datastore::fixed_index::FixedIndexReader;
|
|
|
|
use pbs_datastore::index::IndexFile;
|
|
|
|
use pbs_datastore::manifest::{archive_type, ArchiveType, BackupManifest, MANIFEST_BLOB_NAME};
|
2021-07-07 12:37:47 +00:00
|
|
|
use pbs_datastore::task::TaskState;
|
2021-09-10 04:53:53 +00:00
|
|
|
use pbs_config::CachedUserInfo;
|
2021-09-13 09:54:24 +00:00
|
|
|
use pbs_tape::{
|
|
|
|
TapeRead, BlockReadError, MediaContentHeader,
|
|
|
|
PROXMOX_BACKUP_CONTENT_HEADER_MAGIC_1_0,
|
|
|
|
};
|
2021-07-07 12:37:47 +00:00
|
|
|
|
2020-12-30 08:48:18 +00:00
|
|
|
use crate::{
|
2021-05-03 10:42:49 +00:00
|
|
|
tools::ParallelHandler,
|
2021-08-30 09:49:22 +00:00
|
|
|
backup::DataStore,
|
2021-03-05 13:10:19 +00:00
|
|
|
server::{
|
|
|
|
lookup_user_email,
|
|
|
|
WorkerTask,
|
|
|
|
},
|
2020-12-30 08:48:18 +00:00
|
|
|
tape::{
|
|
|
|
TAPE_STATUS_DIR,
|
|
|
|
MediaId,
|
2021-03-23 12:39:33 +00:00
|
|
|
MediaSet,
|
2020-12-30 08:48:18 +00:00
|
|
|
MediaCatalog,
|
2021-05-11 10:50:04 +00:00
|
|
|
MediaSetCatalog,
|
2020-12-31 09:26:48 +00:00
|
|
|
Inventory,
|
2021-03-22 05:32:18 +00:00
|
|
|
lock_media_set,
|
2020-12-30 08:48:18 +00:00
|
|
|
file_formats::{
|
|
|
|
PROXMOX_BACKUP_MEDIA_LABEL_MAGIC_1_0,
|
|
|
|
PROXMOX_BACKUP_SNAPSHOT_ARCHIVE_MAGIC_1_0,
|
2021-03-16 11:52:49 +00:00
|
|
|
PROXMOX_BACKUP_SNAPSHOT_ARCHIVE_MAGIC_1_1,
|
2020-12-30 08:48:18 +00:00
|
|
|
PROXMOX_BACKUP_MEDIA_SET_LABEL_MAGIC_1_0,
|
|
|
|
PROXMOX_BACKUP_CHUNK_ARCHIVE_MAGIC_1_0,
|
2021-03-16 11:52:49 +00:00
|
|
|
PROXMOX_BACKUP_CHUNK_ARCHIVE_MAGIC_1_1,
|
2021-03-23 12:39:33 +00:00
|
|
|
PROXMOX_BACKUP_CATALOG_ARCHIVE_MAGIC_1_0,
|
2021-03-16 11:52:49 +00:00
|
|
|
ChunkArchiveHeader,
|
2021-02-04 06:58:34 +00:00
|
|
|
ChunkArchiveDecoder,
|
2021-03-16 11:52:49 +00:00
|
|
|
SnapshotArchiveHeader,
|
2021-03-23 12:39:33 +00:00
|
|
|
CatalogArchiveHeader,
|
2020-12-30 08:48:18 +00:00
|
|
|
},
|
2021-01-21 16:12:01 +00:00
|
|
|
drive::{
|
|
|
|
TapeDriver,
|
|
|
|
request_and_load_media,
|
2021-02-05 09:50:21 +00:00
|
|
|
lock_tape_device,
|
2021-02-18 14:40:28 +00:00
|
|
|
set_tape_device_state,
|
2021-02-05 09:50:21 +00:00
|
|
|
},
|
2020-12-30 08:48:18 +00:00
|
|
|
},
|
|
|
|
};
|
|
|
|
|
2021-05-11 10:50:04 +00:00
|
|
|
const RESTORE_TMP_DIR: &str = "/var/tmp/proxmox-backup";
|
|
|
|
|
2021-03-24 13:10:14 +00:00
|
|
|
pub struct DataStoreMap {
|
|
|
|
map: HashMap<String, Arc<DataStore>>,
|
|
|
|
default: Option<Arc<DataStore>>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl TryFrom<String> for DataStoreMap {
|
|
|
|
type Error = Error;
|
|
|
|
|
|
|
|
fn try_from(value: String) -> Result<Self, Error> {
|
|
|
|
let value = parse_property_string(&value, &DATASTORE_MAP_ARRAY_SCHEMA)?;
|
|
|
|
let mut mapping: Vec<String> = value
|
|
|
|
.as_array()
|
|
|
|
.unwrap()
|
|
|
|
.iter()
|
|
|
|
.map(|v| v.as_str().unwrap().to_string())
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
let mut map = HashMap::new();
|
|
|
|
let mut default = None;
|
|
|
|
while let Some(mut store) = mapping.pop() {
|
|
|
|
if let Some(index) = store.find('=') {
|
|
|
|
let mut target = store.split_off(index);
|
|
|
|
target.remove(0); // remove '='
|
|
|
|
let datastore = DataStore::lookup_datastore(&target)?;
|
|
|
|
map.insert(store, datastore);
|
|
|
|
} else if default.is_none() {
|
|
|
|
default = Some(DataStore::lookup_datastore(&store)?);
|
|
|
|
} else {
|
|
|
|
bail!("multiple default stores given");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(Self { map, default })
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl DataStoreMap {
|
|
|
|
fn used_datastores<'a>(&self) -> HashSet<&str> {
|
|
|
|
let mut set = HashSet::new();
|
|
|
|
for store in self.map.values() {
|
|
|
|
set.insert(store.name());
|
|
|
|
}
|
|
|
|
|
|
|
|
if let Some(ref store) = self.default {
|
|
|
|
set.insert(store.name());
|
|
|
|
}
|
|
|
|
|
|
|
|
set
|
|
|
|
}
|
|
|
|
|
2021-04-30 10:09:50 +00:00
|
|
|
fn get_datastore(&self, source: &str) -> Option<Arc<DataStore>> {
|
2021-03-24 13:10:14 +00:00
|
|
|
if let Some(store) = self.map.get(source) {
|
2021-04-30 10:09:50 +00:00
|
|
|
return Some(Arc::clone(store));
|
2021-03-24 13:10:14 +00:00
|
|
|
}
|
|
|
|
if let Some(ref store) = self.default {
|
2021-04-30 10:09:50 +00:00
|
|
|
return Some(Arc::clone(store));
|
2021-03-24 13:10:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return None;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-05-05 10:09:13 +00:00
|
|
|
fn check_datastore_privs(
|
|
|
|
user_info: &CachedUserInfo,
|
|
|
|
store: &str,
|
|
|
|
auth_id: &Authid,
|
|
|
|
owner: &Option<Authid>,
|
|
|
|
) -> Result<(), Error> {
|
|
|
|
let privs = user_info.lookup_privs(&auth_id, &["datastore", &store]);
|
|
|
|
if (privs & PRIV_DATASTORE_BACKUP) == 0 {
|
|
|
|
bail!("no permissions on /datastore/{}", store);
|
|
|
|
}
|
|
|
|
|
|
|
|
if let Some(ref owner) = owner {
|
|
|
|
let correct_owner = owner == auth_id
|
|
|
|
|| (owner.is_token() && !auth_id.is_token() && owner.user() == auth_id.user());
|
|
|
|
|
|
|
|
// same permission as changing ownership after syncing
|
|
|
|
if !correct_owner && privs & PRIV_DATASTORE_MODIFY == 0 {
|
|
|
|
bail!("no permission to restore as '{}'", owner);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2021-03-24 13:10:14 +00:00
|
|
|
pub const ROUTER: Router = Router::new().post(&API_METHOD_RESTORE);
|
2020-12-31 09:26:48 +00:00
|
|
|
|
|
|
|
#[api(
|
|
|
|
input: {
|
|
|
|
properties: {
|
|
|
|
store: {
|
2021-03-24 13:10:14 +00:00
|
|
|
schema: DATASTORE_MAP_LIST_SCHEMA,
|
2020-12-31 09:26:48 +00:00
|
|
|
},
|
2021-02-01 08:14:28 +00:00
|
|
|
drive: {
|
|
|
|
schema: DRIVE_NAME_SCHEMA,
|
|
|
|
},
|
2020-12-31 09:26:48 +00:00
|
|
|
"media-set": {
|
|
|
|
description: "Media set UUID.",
|
|
|
|
type: String,
|
|
|
|
},
|
2021-03-05 13:10:19 +00:00
|
|
|
"notify-user": {
|
|
|
|
type: Userid,
|
|
|
|
optional: true,
|
|
|
|
},
|
2021-05-11 10:50:04 +00:00
|
|
|
"snapshots": {
|
|
|
|
description: "List of snapshots.",
|
|
|
|
type: Array,
|
|
|
|
optional: true,
|
|
|
|
items: {
|
|
|
|
schema: TAPE_RESTORE_SNAPSHOT_SCHEMA,
|
|
|
|
},
|
|
|
|
},
|
2021-03-12 10:28:41 +00:00
|
|
|
owner: {
|
|
|
|
type: Authid,
|
|
|
|
optional: true,
|
|
|
|
},
|
2020-12-31 09:26:48 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
returns: {
|
|
|
|
schema: UPID_SCHEMA,
|
|
|
|
},
|
2021-03-05 10:40:52 +00:00
|
|
|
access: {
|
|
|
|
// Note: parameters are no uri parameter, so we need to test inside function body
|
|
|
|
description: "The user needs Tape.Read privilege on /tape/pool/{pool} \
|
|
|
|
and /tape/drive/{drive}, Datastore.Backup privilege on /datastore/{store}.",
|
|
|
|
permission: &Permission::Anybody,
|
|
|
|
},
|
2020-12-31 09:26:48 +00:00
|
|
|
)]
|
|
|
|
/// Restore data from media-set
|
|
|
|
pub fn restore(
|
|
|
|
store: String,
|
2021-02-01 08:14:28 +00:00
|
|
|
drive: String,
|
2020-12-31 09:26:48 +00:00
|
|
|
media_set: String,
|
2021-03-05 13:10:19 +00:00
|
|
|
notify_user: Option<Userid>,
|
2021-05-11 10:50:04 +00:00
|
|
|
snapshots: Option<Vec<String>>,
|
2021-03-12 10:28:41 +00:00
|
|
|
owner: Option<Authid>,
|
2020-12-31 09:26:48 +00:00
|
|
|
rpcenv: &mut dyn RpcEnvironment,
|
|
|
|
) -> Result<Value, Error> {
|
|
|
|
let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
|
2021-03-05 10:40:52 +00:00
|
|
|
let user_info = CachedUserInfo::new()?;
|
|
|
|
|
2021-03-24 13:10:14 +00:00
|
|
|
let store_map = DataStoreMap::try_from(store)
|
|
|
|
.map_err(|err| format_err!("cannot parse store mapping: {}", err))?;
|
|
|
|
let used_datastores = store_map.used_datastores();
|
|
|
|
if used_datastores.len() == 0 {
|
|
|
|
bail!("no datastores given");
|
2021-03-05 10:40:52 +00:00
|
|
|
}
|
|
|
|
|
2021-03-24 13:10:14 +00:00
|
|
|
for store in used_datastores.iter() {
|
2021-05-05 10:09:13 +00:00
|
|
|
check_datastore_privs(&user_info, &store, &auth_id, &owner)?;
|
2021-03-12 10:28:41 +00:00
|
|
|
}
|
|
|
|
|
2021-03-05 10:40:52 +00:00
|
|
|
let privs = user_info.lookup_privs(&auth_id, &["tape", "drive", &drive]);
|
|
|
|
if (privs & PRIV_TAPE_READ) == 0 {
|
|
|
|
bail!("no permissions on /tape/drive/{}", drive);
|
|
|
|
}
|
2020-12-31 09:26:48 +00:00
|
|
|
|
2021-03-22 05:32:18 +00:00
|
|
|
let media_set_uuid = media_set.parse()?;
|
|
|
|
|
2020-12-31 09:26:48 +00:00
|
|
|
let status_path = Path::new(TAPE_STATUS_DIR);
|
|
|
|
|
2021-03-22 05:32:18 +00:00
|
|
|
let _lock = lock_media_set(status_path, &media_set_uuid, None)?;
|
|
|
|
|
|
|
|
let inventory = Inventory::load(status_path)?;
|
2020-12-31 09:26:48 +00:00
|
|
|
|
|
|
|
let pool = inventory.lookup_media_set_pool(&media_set_uuid)?;
|
|
|
|
|
2021-03-05 10:40:52 +00:00
|
|
|
let privs = user_info.lookup_privs(&auth_id, &["tape", "pool", &pool]);
|
|
|
|
if (privs & PRIV_TAPE_READ) == 0 {
|
|
|
|
bail!("no permissions on /tape/pool/{}", pool);
|
|
|
|
}
|
|
|
|
|
2021-09-03 07:10:18 +00:00
|
|
|
let (drive_config, _digest) = pbs_config::drive::config()?;
|
2021-02-05 09:50:21 +00:00
|
|
|
|
|
|
|
// early check/lock before starting worker
|
|
|
|
let drive_lock = lock_tape_device(&drive_config, &drive)?;
|
2020-12-31 09:26:48 +00:00
|
|
|
|
2021-01-18 13:12:27 +00:00
|
|
|
let to_stdout = rpcenv.env_type() == RpcEnvironmentType::CLI;
|
2020-12-31 09:26:48 +00:00
|
|
|
|
2021-03-24 13:10:14 +00:00
|
|
|
let taskid = used_datastores
|
|
|
|
.iter()
|
|
|
|
.map(|s| s.to_string())
|
|
|
|
.collect::<Vec<String>>()
|
|
|
|
.join(", ");
|
2021-05-11 10:50:04 +00:00
|
|
|
|
2020-12-31 09:26:48 +00:00
|
|
|
let upid_str = WorkerTask::new_thread(
|
|
|
|
"tape-restore",
|
2021-03-24 13:10:14 +00:00
|
|
|
Some(taskid),
|
2020-12-31 09:26:48 +00:00
|
|
|
auth_id.clone(),
|
|
|
|
to_stdout,
|
|
|
|
move |worker| {
|
2021-02-05 09:50:21 +00:00
|
|
|
let _drive_lock = drive_lock; // keep lock guard
|
2020-12-31 09:26:48 +00:00
|
|
|
|
2021-02-18 14:40:28 +00:00
|
|
|
set_tape_device_state(&drive, &worker.upid().to_string())?;
|
|
|
|
|
2021-05-11 10:50:03 +00:00
|
|
|
let restore_owner = owner.as_ref().unwrap_or(&auth_id);
|
2020-12-31 09:26:48 +00:00
|
|
|
|
2021-05-11 10:50:03 +00:00
|
|
|
let email = notify_user
|
|
|
|
.as_ref()
|
|
|
|
.and_then(|userid| lookup_user_email(userid))
|
|
|
|
.or_else(|| lookup_user_email(&auth_id.clone().into()));
|
2020-12-31 09:26:48 +00:00
|
|
|
|
2021-05-11 10:50:04 +00:00
|
|
|
task_log!(worker, "Mediaset '{}'", media_set);
|
2021-02-04 05:55:18 +00:00
|
|
|
task_log!(worker, "Pool: {}", pool);
|
2021-05-11 10:50:04 +00:00
|
|
|
|
|
|
|
let res = if let Some(snapshots) = snapshots {
|
|
|
|
restore_list_worker(
|
|
|
|
worker.clone(),
|
|
|
|
snapshots,
|
|
|
|
inventory,
|
|
|
|
media_set_uuid,
|
|
|
|
drive_config,
|
|
|
|
&drive,
|
|
|
|
store_map,
|
|
|
|
restore_owner,
|
|
|
|
email,
|
|
|
|
)
|
|
|
|
} else {
|
|
|
|
restore_full_worker(
|
|
|
|
worker.clone(),
|
|
|
|
inventory,
|
|
|
|
media_set_uuid,
|
|
|
|
drive_config,
|
|
|
|
&drive,
|
|
|
|
store_map,
|
|
|
|
restore_owner,
|
|
|
|
email,
|
|
|
|
)
|
|
|
|
};
|
|
|
|
|
|
|
|
if res.is_ok() {
|
|
|
|
task_log!(worker, "Restore mediaset '{}' done", media_set);
|
|
|
|
}
|
2021-02-18 14:40:28 +00:00
|
|
|
|
|
|
|
if let Err(err) = set_tape_device_state(&drive, "") {
|
|
|
|
task_log!(
|
|
|
|
worker,
|
|
|
|
"could not unset drive state for {}: {}",
|
|
|
|
drive,
|
|
|
|
err
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2021-05-11 10:50:03 +00:00
|
|
|
res
|
2020-12-31 09:26:48 +00:00
|
|
|
}
|
|
|
|
)?;
|
|
|
|
|
|
|
|
Ok(upid_str.into())
|
|
|
|
}
|
|
|
|
|
2021-05-11 10:50:04 +00:00
|
|
|
fn restore_full_worker(
|
2021-05-11 10:50:03 +00:00
|
|
|
worker: Arc<WorkerTask>,
|
|
|
|
inventory: Inventory,
|
|
|
|
media_set_uuid: Uuid,
|
|
|
|
drive_config: SectionConfigData,
|
|
|
|
drive_name: &str,
|
|
|
|
store_map: DataStoreMap,
|
|
|
|
restore_owner: &Authid,
|
|
|
|
email: Option<String>,
|
|
|
|
) -> Result<(), Error> {
|
|
|
|
let members = inventory.compute_media_set_members(&media_set_uuid)?;
|
|
|
|
|
|
|
|
let media_list = members.media_list();
|
|
|
|
|
|
|
|
let mut media_id_list = Vec::new();
|
|
|
|
|
|
|
|
let mut encryption_key_fingerprint = None;
|
|
|
|
|
|
|
|
for (seq_nr, media_uuid) in media_list.iter().enumerate() {
|
|
|
|
match media_uuid {
|
|
|
|
None => {
|
|
|
|
bail!("media set {} is incomplete (missing member {}).", media_set_uuid, seq_nr);
|
|
|
|
}
|
|
|
|
Some(media_uuid) => {
|
|
|
|
let media_id = inventory.lookup_media(media_uuid).unwrap();
|
|
|
|
if let Some(ref set) = media_id.media_set_label { // always true here
|
|
|
|
if encryption_key_fingerprint.is_none() && set.encryption_key_fingerprint.is_some() {
|
|
|
|
encryption_key_fingerprint = set.encryption_key_fingerprint.clone();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
media_id_list.push(media_id);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if let Some(fingerprint) = encryption_key_fingerprint {
|
|
|
|
task_log!(worker, "Encryption key fingerprint: {}", fingerprint);
|
|
|
|
}
|
|
|
|
|
|
|
|
task_log!(
|
|
|
|
worker,
|
|
|
|
"Datastore(s): {}",
|
|
|
|
store_map
|
|
|
|
.used_datastores()
|
|
|
|
.into_iter()
|
|
|
|
.map(String::from)
|
|
|
|
.collect::<Vec<String>>()
|
|
|
|
.join(", "),
|
|
|
|
);
|
|
|
|
|
|
|
|
task_log!(worker, "Drive: {}", drive_name);
|
|
|
|
task_log!(
|
|
|
|
worker,
|
|
|
|
"Required media list: {}",
|
|
|
|
media_id_list.iter()
|
|
|
|
.map(|media_id| media_id.label.label_text.as_str())
|
|
|
|
.collect::<Vec<&str>>()
|
|
|
|
.join(";")
|
|
|
|
);
|
|
|
|
|
|
|
|
let mut datastore_locks = Vec::new();
|
|
|
|
for store_name in store_map.used_datastores() {
|
|
|
|
// explicit create shared lock to prevent GC on newly created chunks
|
|
|
|
if let Some(store) = store_map.get_datastore(store_name) {
|
|
|
|
let shared_store_lock = store.try_shared_chunk_store_lock()?;
|
|
|
|
datastore_locks.push(shared_store_lock);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
let mut checked_chunks_map = HashMap::new();
|
|
|
|
|
|
|
|
for media_id in media_id_list.iter() {
|
|
|
|
request_and_restore_media(
|
|
|
|
worker.clone(),
|
|
|
|
media_id,
|
|
|
|
&drive_config,
|
|
|
|
drive_name,
|
|
|
|
&store_map,
|
|
|
|
&mut checked_chunks_map,
|
|
|
|
restore_owner,
|
|
|
|
&email,
|
|
|
|
)?;
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2021-05-11 10:50:04 +00:00
|
|
|
fn restore_list_worker(
|
|
|
|
worker: Arc<WorkerTask>,
|
|
|
|
snapshots: Vec<String>,
|
|
|
|
inventory: Inventory,
|
|
|
|
media_set_uuid: Uuid,
|
|
|
|
drive_config: SectionConfigData,
|
|
|
|
drive_name: &str,
|
|
|
|
store_map: DataStoreMap,
|
|
|
|
restore_owner: &Authid,
|
|
|
|
email: Option<String>,
|
|
|
|
) -> Result<(), Error> {
|
|
|
|
let base_path: PathBuf = format!("{}/{}", RESTORE_TMP_DIR, media_set_uuid).into();
|
|
|
|
std::fs::create_dir_all(&base_path)?;
|
|
|
|
|
|
|
|
let catalog = get_media_set_catalog(&inventory, &media_set_uuid)?;
|
|
|
|
|
|
|
|
let mut datastore_locks = Vec::new();
|
|
|
|
let mut snapshot_file_hash: BTreeMap<Uuid, Vec<u64>> = BTreeMap::new();
|
|
|
|
let mut snapshot_locks = HashMap::new();
|
|
|
|
|
|
|
|
let res = proxmox::try_block!({
|
|
|
|
// assemble snapshot files/locks
|
|
|
|
for store_snapshot in snapshots.iter() {
|
|
|
|
let mut split = store_snapshot.splitn(2, ':');
|
|
|
|
let source_datastore = split
|
|
|
|
.next()
|
|
|
|
.ok_or_else(|| format_err!("invalid snapshot: {}", store_snapshot))?;
|
|
|
|
let snapshot = split
|
|
|
|
.next()
|
|
|
|
.ok_or_else(|| format_err!("invalid snapshot:{}", store_snapshot))?;
|
|
|
|
let backup_dir: BackupDir = snapshot.parse()?;
|
|
|
|
|
|
|
|
let datastore = store_map.get_datastore(source_datastore).ok_or_else(|| {
|
|
|
|
format_err!(
|
|
|
|
"could not find mapping for source datastore: {}",
|
|
|
|
source_datastore
|
|
|
|
)
|
|
|
|
})?;
|
|
|
|
|
|
|
|
let (owner, _group_lock) =
|
|
|
|
datastore.create_locked_backup_group(backup_dir.group(), &restore_owner)?;
|
|
|
|
if restore_owner != &owner {
|
|
|
|
// only the owner is allowed to create additional snapshots
|
|
|
|
bail!(
|
|
|
|
"restore '{}' failed - owner check failed ({} != {})",
|
|
|
|
snapshot,
|
|
|
|
restore_owner,
|
|
|
|
owner
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
let (media_id, file_num) = if let Some((media_uuid, file_num)) =
|
|
|
|
catalog.lookup_snapshot(&source_datastore, &snapshot)
|
|
|
|
{
|
|
|
|
let media_id = inventory.lookup_media(media_uuid).unwrap();
|
|
|
|
(media_id, file_num)
|
|
|
|
} else {
|
|
|
|
task_warn!(
|
|
|
|
worker,
|
|
|
|
"did not find snapshot '{}' in media set {}",
|
|
|
|
snapshot,
|
|
|
|
media_set_uuid
|
|
|
|
);
|
|
|
|
continue;
|
|
|
|
};
|
|
|
|
|
|
|
|
let (_rel_path, is_new, snap_lock) = datastore.create_locked_backup_dir(&backup_dir)?;
|
|
|
|
|
|
|
|
if !is_new {
|
|
|
|
task_log!(
|
|
|
|
worker,
|
|
|
|
"found snapshot {} on target datastore, skipping...",
|
|
|
|
snapshot
|
|
|
|
);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
snapshot_locks.insert(store_snapshot.to_string(), snap_lock);
|
|
|
|
|
|
|
|
let shared_store_lock = datastore.try_shared_chunk_store_lock()?;
|
|
|
|
datastore_locks.push(shared_store_lock);
|
|
|
|
|
|
|
|
let file_list = snapshot_file_hash
|
|
|
|
.entry(media_id.label.uuid.clone())
|
|
|
|
.or_insert_with(Vec::new);
|
|
|
|
file_list.push(file_num);
|
|
|
|
|
|
|
|
task_log!(
|
|
|
|
worker,
|
|
|
|
"found snapshot {} on {}: file {}",
|
|
|
|
snapshot,
|
|
|
|
media_id.label.label_text,
|
|
|
|
file_num
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
if snapshot_file_hash.is_empty() {
|
|
|
|
task_log!(worker, "nothing to restore, skipping remaining phases...");
|
|
|
|
return Ok(());
|
|
|
|
}
|
|
|
|
|
|
|
|
task_log!(worker, "Phase 1: temporarily restore snapshots to temp dir");
|
|
|
|
let mut datastore_chunk_map: HashMap<String, HashSet<[u8; 32]>> = HashMap::new();
|
|
|
|
for (media_uuid, file_list) in snapshot_file_hash.iter_mut() {
|
|
|
|
let media_id = inventory.lookup_media(media_uuid).unwrap();
|
|
|
|
let (drive, info) = request_and_load_media(
|
|
|
|
&worker,
|
|
|
|
&drive_config,
|
|
|
|
&drive_name,
|
|
|
|
&media_id.label,
|
|
|
|
&email,
|
|
|
|
)?;
|
|
|
|
file_list.sort_unstable();
|
|
|
|
restore_snapshots_to_tmpdir(
|
|
|
|
worker.clone(),
|
|
|
|
&base_path,
|
|
|
|
file_list,
|
|
|
|
drive,
|
|
|
|
&info,
|
|
|
|
&media_set_uuid,
|
|
|
|
&mut datastore_chunk_map,
|
|
|
|
).map_err(|err| format_err!("could not restore snapshots to tmpdir: {}", err))?;
|
|
|
|
}
|
|
|
|
|
|
|
|
// sorted media_uuid => (sorted file_num => (set of digests)))
|
|
|
|
let mut media_file_chunk_map: BTreeMap<Uuid, BTreeMap<u64, HashSet<[u8; 32]>>> = BTreeMap::new();
|
|
|
|
|
|
|
|
for (source_datastore, chunks) in datastore_chunk_map.into_iter() {
|
|
|
|
let datastore = store_map.get_datastore(&source_datastore).ok_or_else(|| {
|
|
|
|
format_err!(
|
|
|
|
"could not find mapping for source datastore: {}",
|
|
|
|
source_datastore
|
|
|
|
)
|
|
|
|
})?;
|
|
|
|
for digest in chunks.into_iter() {
|
|
|
|
// we only want to restore chunks that we do not have yet
|
|
|
|
if !datastore.cond_touch_chunk(&digest, false)? {
|
|
|
|
if let Some((uuid, nr)) = catalog.lookup_chunk(&source_datastore, &digest) {
|
|
|
|
let file = media_file_chunk_map.entry(uuid.clone()).or_insert_with(BTreeMap::new);
|
|
|
|
let chunks = file.entry(nr).or_insert_with(HashSet::new);
|
|
|
|
chunks.insert(digest);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// we do not need it anymore, saves memory
|
|
|
|
drop(catalog);
|
|
|
|
|
|
|
|
if !media_file_chunk_map.is_empty() {
|
|
|
|
task_log!(worker, "Phase 2: restore chunks to datastores");
|
|
|
|
} else {
|
|
|
|
task_log!(worker, "all chunks exist already, skipping phase 2...");
|
|
|
|
}
|
|
|
|
|
|
|
|
for (media_uuid, file_chunk_map) in media_file_chunk_map.iter_mut() {
|
|
|
|
let media_id = inventory.lookup_media(media_uuid).unwrap();
|
|
|
|
let (mut drive, _info) = request_and_load_media(
|
|
|
|
&worker,
|
|
|
|
&drive_config,
|
|
|
|
&drive_name,
|
|
|
|
&media_id.label,
|
|
|
|
&email,
|
|
|
|
)?;
|
|
|
|
restore_file_chunk_map(worker.clone(), &mut drive, &store_map, file_chunk_map)?;
|
|
|
|
}
|
|
|
|
|
|
|
|
task_log!(
|
|
|
|
worker,
|
|
|
|
"Phase 3: copy snapshots from temp dir to datastores"
|
|
|
|
);
|
|
|
|
for (store_snapshot, _lock) in snapshot_locks.into_iter() {
|
|
|
|
proxmox::try_block!({
|
|
|
|
let mut split = store_snapshot.splitn(2, ':');
|
|
|
|
let source_datastore = split
|
|
|
|
.next()
|
|
|
|
.ok_or_else(|| format_err!("invalid snapshot: {}", store_snapshot))?;
|
|
|
|
let snapshot = split
|
|
|
|
.next()
|
|
|
|
.ok_or_else(|| format_err!("invalid snapshot:{}", store_snapshot))?;
|
|
|
|
let backup_dir: BackupDir = snapshot.parse()?;
|
|
|
|
|
|
|
|
let datastore = store_map
|
|
|
|
.get_datastore(&source_datastore)
|
|
|
|
.ok_or_else(|| format_err!("unexpected source datastore: {}", source_datastore))?;
|
|
|
|
|
|
|
|
let mut tmp_path = base_path.clone();
|
|
|
|
tmp_path.push(&source_datastore);
|
|
|
|
tmp_path.push(snapshot);
|
|
|
|
|
|
|
|
let path = datastore.snapshot_path(&backup_dir);
|
|
|
|
|
|
|
|
for entry in std::fs::read_dir(tmp_path)? {
|
|
|
|
let entry = entry?;
|
|
|
|
let mut new_path = path.clone();
|
|
|
|
new_path.push(entry.file_name());
|
|
|
|
std::fs::copy(entry.path(), new_path)?;
|
|
|
|
}
|
|
|
|
task_log!(worker, "Restore snapshot '{}' done", snapshot);
|
|
|
|
Ok(())
|
|
|
|
}).map_err(|err: Error| format_err!("could not copy {}: {}", store_snapshot, err))?;
|
|
|
|
}
|
|
|
|
Ok(())
|
|
|
|
});
|
|
|
|
|
2021-07-13 09:11:25 +00:00
|
|
|
if res.is_err() {
|
|
|
|
task_warn!(worker, "Error during restore, partially restored snapshots will NOT be cleaned up");
|
|
|
|
}
|
|
|
|
|
2021-05-11 10:50:04 +00:00
|
|
|
match std::fs::remove_dir_all(&base_path) {
|
|
|
|
Ok(()) => {}
|
|
|
|
Err(err) => task_warn!(worker, "error cleaning up: {}", err),
|
|
|
|
}
|
|
|
|
|
|
|
|
res
|
|
|
|
}
|
|
|
|
|
|
|
|
fn get_media_set_catalog(
|
|
|
|
inventory: &Inventory,
|
|
|
|
media_set_uuid: &Uuid,
|
|
|
|
) -> Result<MediaSetCatalog, Error> {
|
|
|
|
let status_path = Path::new(TAPE_STATUS_DIR);
|
|
|
|
|
|
|
|
let members = inventory.compute_media_set_members(media_set_uuid)?;
|
|
|
|
let media_list = members.media_list();
|
|
|
|
let mut catalog = MediaSetCatalog::new();
|
|
|
|
|
|
|
|
for (seq_nr, media_uuid) in media_list.iter().enumerate() {
|
|
|
|
match media_uuid {
|
|
|
|
None => {
|
|
|
|
bail!(
|
|
|
|
"media set {} is incomplete (missing member {}).",
|
|
|
|
media_set_uuid,
|
|
|
|
seq_nr
|
|
|
|
);
|
|
|
|
}
|
|
|
|
Some(media_uuid) => {
|
|
|
|
let media_id = inventory.lookup_media(media_uuid).unwrap();
|
|
|
|
let media_catalog = MediaCatalog::open(status_path, &media_id, false, false)?;
|
|
|
|
catalog.append_catalog(media_catalog)?;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(catalog)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn restore_snapshots_to_tmpdir(
|
|
|
|
worker: Arc<WorkerTask>,
|
|
|
|
path: &PathBuf,
|
|
|
|
file_list: &[u64],
|
|
|
|
mut drive: Box<dyn TapeDriver>,
|
|
|
|
media_id: &MediaId,
|
|
|
|
media_set_uuid: &Uuid,
|
|
|
|
chunks_list: &mut HashMap<String, HashSet<[u8; 32]>>,
|
|
|
|
) -> Result<(), Error> {
|
|
|
|
match media_id.media_set_label {
|
|
|
|
None => {
|
|
|
|
bail!(
|
|
|
|
"missing media set label on media {} ({})",
|
|
|
|
media_id.label.label_text,
|
|
|
|
media_id.label.uuid
|
|
|
|
);
|
|
|
|
}
|
|
|
|
Some(ref set) => {
|
|
|
|
if set.uuid != *media_set_uuid {
|
|
|
|
bail!(
|
|
|
|
"wrong media set label on media {} ({} != {})",
|
|
|
|
media_id.label.label_text,
|
|
|
|
media_id.label.uuid,
|
|
|
|
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)?;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
for file_num in file_list {
|
|
|
|
let current_file_number = drive.current_file_number()?;
|
|
|
|
if current_file_number != *file_num {
|
|
|
|
task_log!(worker, "was at file {}, moving to {}", current_file_number, file_num);
|
|
|
|
drive.move_to_file(*file_num)?;
|
|
|
|
let current_file_number = drive.current_file_number()?;
|
|
|
|
task_log!(worker, "now at file {}", current_file_number);
|
|
|
|
}
|
|
|
|
let mut reader = drive.read_next_file()?;
|
|
|
|
|
|
|
|
let header: MediaContentHeader = unsafe { reader.read_le_value()? };
|
|
|
|
if header.magic != PROXMOX_BACKUP_CONTENT_HEADER_MAGIC_1_0 {
|
|
|
|
bail!("missing MediaContentHeader");
|
|
|
|
}
|
|
|
|
|
|
|
|
match header.content_magic {
|
|
|
|
PROXMOX_BACKUP_SNAPSHOT_ARCHIVE_MAGIC_1_1 => {
|
|
|
|
let header_data = reader.read_exact_allocated(header.size as usize)?;
|
|
|
|
|
|
|
|
let archive_header: SnapshotArchiveHeader = serde_json::from_slice(&header_data)
|
|
|
|
.map_err(|err| {
|
|
|
|
format_err!("unable to parse snapshot archive header - {}", err)
|
|
|
|
})?;
|
|
|
|
|
|
|
|
let source_datastore = archive_header.store;
|
|
|
|
let snapshot = archive_header.snapshot;
|
|
|
|
|
|
|
|
task_log!(
|
|
|
|
worker,
|
|
|
|
"File {}: snapshot archive {}:{}",
|
|
|
|
file_num,
|
|
|
|
source_datastore,
|
|
|
|
snapshot
|
|
|
|
);
|
|
|
|
|
|
|
|
let mut decoder = pxar::decoder::sync::Decoder::from_std(reader)?;
|
|
|
|
|
|
|
|
let mut tmp_path = path.clone();
|
|
|
|
tmp_path.push(&source_datastore);
|
|
|
|
tmp_path.push(snapshot);
|
|
|
|
std::fs::create_dir_all(&tmp_path)?;
|
|
|
|
|
|
|
|
let chunks = chunks_list
|
|
|
|
.entry(source_datastore)
|
|
|
|
.or_insert_with(HashSet::new);
|
|
|
|
let manifest = try_restore_snapshot_archive(worker.clone(), &mut decoder, &tmp_path)?;
|
|
|
|
for item in manifest.files() {
|
|
|
|
let mut archive_path = tmp_path.to_owned();
|
|
|
|
archive_path.push(&item.filename);
|
|
|
|
|
|
|
|
let index: Box<dyn IndexFile> = match archive_type(&item.filename)? {
|
|
|
|
ArchiveType::DynamicIndex => {
|
|
|
|
Box::new(DynamicIndexReader::open(&archive_path)?)
|
|
|
|
}
|
|
|
|
ArchiveType::FixedIndex => {
|
|
|
|
Box::new(FixedIndexReader::open(&archive_path)?)
|
|
|
|
}
|
|
|
|
ArchiveType::Blob => continue,
|
|
|
|
};
|
|
|
|
for i in 0..index.index_count() {
|
|
|
|
if let Some(digest) = index.index_digest(i) {
|
|
|
|
chunks.insert(*digest);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
other => bail!("unexpected file type: {:?}", other),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
fn restore_file_chunk_map(
|
|
|
|
worker: Arc<WorkerTask>,
|
|
|
|
drive: &mut Box<dyn TapeDriver>,
|
|
|
|
store_map: &DataStoreMap,
|
|
|
|
file_chunk_map: &mut BTreeMap<u64, HashSet<[u8; 32]>>,
|
|
|
|
) -> Result<(), Error> {
|
|
|
|
for (nr, chunk_map) in file_chunk_map.iter_mut() {
|
|
|
|
let current_file_number = drive.current_file_number()?;
|
|
|
|
if current_file_number != *nr {
|
|
|
|
task_log!(worker, "was at file {}, moving to {}", current_file_number, nr);
|
|
|
|
drive.move_to_file(*nr)?;
|
|
|
|
let current_file_number = drive.current_file_number()?;
|
|
|
|
task_log!(worker, "now at file {}", current_file_number);
|
|
|
|
}
|
|
|
|
let mut reader = drive.read_next_file()?;
|
|
|
|
let header: MediaContentHeader = unsafe { reader.read_le_value()? };
|
|
|
|
if header.magic != PROXMOX_BACKUP_CONTENT_HEADER_MAGIC_1_0 {
|
|
|
|
bail!("missing MediaContentHeader");
|
|
|
|
}
|
|
|
|
|
|
|
|
match header.content_magic {
|
|
|
|
PROXMOX_BACKUP_CHUNK_ARCHIVE_MAGIC_1_1 => {
|
|
|
|
let header_data = reader.read_exact_allocated(header.size as usize)?;
|
|
|
|
|
|
|
|
let archive_header: ChunkArchiveHeader = serde_json::from_slice(&header_data)
|
|
|
|
.map_err(|err| format_err!("unable to parse chunk archive header - {}", err))?;
|
|
|
|
|
|
|
|
let source_datastore = archive_header.store;
|
|
|
|
|
|
|
|
task_log!(
|
|
|
|
worker,
|
|
|
|
"File {}: chunk archive for datastore '{}'",
|
|
|
|
nr,
|
|
|
|
source_datastore
|
|
|
|
);
|
|
|
|
|
|
|
|
let datastore = store_map.get_datastore(&source_datastore).ok_or_else(|| {
|
|
|
|
format_err!("unexpected chunk archive for store: {}", source_datastore)
|
|
|
|
})?;
|
|
|
|
|
|
|
|
let count = restore_partial_chunk_archive(worker.clone(), reader, datastore.clone(), chunk_map)?;
|
|
|
|
task_log!(worker, "restored {} chunks", count);
|
|
|
|
}
|
|
|
|
_ => bail!("unexpected content magic {:?}", header.content_magic),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
fn restore_partial_chunk_archive<'a>(
|
|
|
|
worker: Arc<WorkerTask>,
|
|
|
|
reader: Box<dyn 'a + TapeRead>,
|
|
|
|
datastore: Arc<DataStore>,
|
|
|
|
chunk_list: &mut HashSet<[u8; 32]>,
|
|
|
|
) -> Result<usize, Error> {
|
|
|
|
let mut decoder = ChunkArchiveDecoder::new(reader);
|
|
|
|
|
|
|
|
let mut count = 0;
|
|
|
|
|
|
|
|
let start_time = std::time::SystemTime::now();
|
|
|
|
let bytes = Arc::new(std::sync::atomic::AtomicU64::new(0));
|
|
|
|
let bytes2 = bytes.clone();
|
|
|
|
|
|
|
|
let writer_pool = ParallelHandler::new(
|
|
|
|
"tape restore chunk writer",
|
|
|
|
4,
|
|
|
|
move |(chunk, digest): (DataBlob, [u8; 32])| {
|
|
|
|
if !datastore.cond_touch_chunk(&digest, false)? {
|
|
|
|
bytes2.fetch_add(chunk.raw_size(), std::sync::atomic::Ordering::SeqCst);
|
|
|
|
chunk.verify_crc()?;
|
|
|
|
if chunk.crypt_mode()? == CryptMode::None {
|
|
|
|
chunk.decode(None, Some(&digest))?; // verify digest
|
|
|
|
}
|
|
|
|
|
|
|
|
datastore.insert_chunk(&chunk, &digest)?;
|
|
|
|
}
|
|
|
|
Ok(())
|
|
|
|
},
|
|
|
|
);
|
|
|
|
|
|
|
|
let verify_and_write_channel = writer_pool.channel();
|
|
|
|
|
|
|
|
loop {
|
|
|
|
let (digest, blob) = match decoder.next_chunk()? {
|
|
|
|
Some((digest, blob)) => (digest, blob),
|
|
|
|
None => break,
|
|
|
|
};
|
|
|
|
|
|
|
|
worker.check_abort()?;
|
|
|
|
|
|
|
|
if chunk_list.remove(&digest) {
|
|
|
|
verify_and_write_channel.send((blob, digest.clone()))?;
|
|
|
|
count += 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
if chunk_list.is_empty() {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
drop(verify_and_write_channel);
|
|
|
|
|
|
|
|
writer_pool.complete()?;
|
|
|
|
|
|
|
|
let elapsed = start_time.elapsed()?.as_secs_f64();
|
|
|
|
|
|
|
|
let bytes = bytes.load(std::sync::atomic::Ordering::SeqCst);
|
|
|
|
|
|
|
|
task_log!(
|
|
|
|
worker,
|
|
|
|
"restored {} bytes ({:.2} MB/s)",
|
|
|
|
bytes,
|
|
|
|
(bytes as f64) / (1_000_000.0 * elapsed)
|
|
|
|
);
|
|
|
|
|
|
|
|
Ok(count)
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2020-12-30 08:48:18 +00:00
|
|
|
/// Request and restore complete media without using existing catalog (create catalog instead)
|
|
|
|
pub fn request_and_restore_media(
|
2021-05-04 10:21:47 +00:00
|
|
|
worker: Arc<WorkerTask>,
|
2020-12-30 08:48:18 +00:00
|
|
|
media_id: &MediaId,
|
|
|
|
drive_config: &SectionConfigData,
|
|
|
|
drive_name: &str,
|
2021-03-24 13:10:14 +00:00
|
|
|
store_map: &DataStoreMap,
|
2021-04-16 11:17:17 +00:00
|
|
|
checked_chunks_map: &mut HashMap<String, HashSet<[u8;32]>>,
|
2021-05-11 10:50:03 +00:00
|
|
|
restore_owner: &Authid,
|
|
|
|
email: &Option<String>,
|
2020-12-30 08:48:18 +00:00
|
|
|
) -> Result<(), Error> {
|
|
|
|
let media_set_uuid = match media_id.media_set_label {
|
|
|
|
None => bail!("restore_media: no media set - internal error"),
|
|
|
|
Some(ref set) => &set.uuid,
|
|
|
|
};
|
|
|
|
|
2021-05-11 10:50:03 +00:00
|
|
|
let (mut drive, info) = request_and_load_media(&worker, &drive_config, &drive_name, &media_id.label, email)?;
|
2020-12-30 08:48:18 +00:00
|
|
|
|
|
|
|
match info.media_set_label {
|
|
|
|
None => {
|
|
|
|
bail!("missing media set label on media {} ({})",
|
2021-01-13 12:26:59 +00:00
|
|
|
media_id.label.label_text, media_id.label.uuid);
|
2020-12-30 08:48:18 +00:00
|
|
|
}
|
|
|
|
Some(ref set) => {
|
|
|
|
if &set.uuid != media_set_uuid {
|
|
|
|
bail!("wrong media set label on media {} ({} != {})",
|
2021-01-13 12:26:59 +00:00
|
|
|
media_id.label.label_text, media_id.label.uuid,
|
2020-12-30 08:48:18 +00:00
|
|
|
media_set_uuid);
|
|
|
|
}
|
2021-01-22 06:26:42 +00:00
|
|
|
let encrypt_fingerprint = set.encryption_key_fingerprint.clone()
|
|
|
|
.map(|fp| (fp, set.uuid.clone()));
|
|
|
|
|
|
|
|
drive.set_encryption(encrypt_fingerprint)?;
|
2020-12-30 08:48:18 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-24 13:10:14 +00:00
|
|
|
restore_media(
|
|
|
|
worker,
|
|
|
|
&mut drive,
|
|
|
|
&info,
|
|
|
|
Some((&store_map, restore_owner)),
|
2021-04-16 11:17:17 +00:00
|
|
|
checked_chunks_map,
|
2021-03-24 13:10:14 +00:00
|
|
|
false,
|
|
|
|
)
|
2020-12-30 08:48:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Restore complete media content and catalog
|
|
|
|
///
|
|
|
|
/// Only create the catalog if target is None.
|
|
|
|
pub fn restore_media(
|
2021-05-04 10:21:47 +00:00
|
|
|
worker: Arc<WorkerTask>,
|
2020-12-30 08:48:18 +00:00
|
|
|
drive: &mut Box<dyn TapeDriver>,
|
|
|
|
media_id: &MediaId,
|
2021-03-24 13:10:14 +00:00
|
|
|
target: Option<(&DataStoreMap, &Authid)>,
|
2021-04-16 11:17:17 +00:00
|
|
|
checked_chunks_map: &mut HashMap<String, HashSet<[u8;32]>>,
|
2020-12-30 08:48:18 +00:00
|
|
|
verbose: bool,
|
|
|
|
) -> Result<(), Error> {
|
|
|
|
|
|
|
|
let status_path = Path::new(TAPE_STATUS_DIR);
|
|
|
|
let mut catalog = MediaCatalog::create_temporary_database(status_path, media_id, false)?;
|
|
|
|
|
|
|
|
loop {
|
|
|
|
let current_file_number = drive.current_file_number()?;
|
2021-04-12 09:25:40 +00:00
|
|
|
let reader = match drive.read_next_file() {
|
|
|
|
Err(BlockReadError::EndOfFile) => {
|
|
|
|
task_log!(worker, "skip unexpected filemark at pos {}", current_file_number);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
Err(BlockReadError::EndOfStream) => {
|
2021-02-04 05:55:18 +00:00
|
|
|
task_log!(worker, "detected EOT after {} files", current_file_number);
|
2020-12-30 08:48:18 +00:00
|
|
|
break;
|
|
|
|
}
|
2021-04-12 09:25:40 +00:00
|
|
|
Err(BlockReadError::Error(err)) => {
|
|
|
|
return Err(err.into());
|
|
|
|
}
|
|
|
|
Ok(reader) => reader,
|
2020-12-30 08:48:18 +00:00
|
|
|
};
|
|
|
|
|
2021-05-04 10:21:47 +00:00
|
|
|
restore_archive(worker.clone(), reader, current_file_number, target, &mut catalog, checked_chunks_map, verbose)?;
|
2020-12-30 08:48:18 +00:00
|
|
|
}
|
|
|
|
|
2021-07-20 08:55:11 +00:00
|
|
|
catalog.commit()?;
|
|
|
|
|
2020-12-30 08:48:18 +00:00
|
|
|
MediaCatalog::finish_temporary_database(status_path, &media_id.label.uuid, true)?;
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
fn restore_archive<'a>(
|
2021-05-04 10:21:47 +00:00
|
|
|
worker: Arc<WorkerTask>,
|
2020-12-30 08:48:18 +00:00
|
|
|
mut reader: Box<dyn 'a + TapeRead>,
|
|
|
|
current_file_number: u64,
|
2021-03-24 13:10:14 +00:00
|
|
|
target: Option<(&DataStoreMap, &Authid)>,
|
2020-12-30 08:48:18 +00:00
|
|
|
catalog: &mut MediaCatalog,
|
2021-04-16 11:17:17 +00:00
|
|
|
checked_chunks_map: &mut HashMap<String, HashSet<[u8;32]>>,
|
2020-12-30 08:48:18 +00:00
|
|
|
verbose: bool,
|
|
|
|
) -> Result<(), Error> {
|
|
|
|
let header: MediaContentHeader = unsafe { reader.read_le_value()? };
|
|
|
|
if header.magic != PROXMOX_BACKUP_CONTENT_HEADER_MAGIC_1_0 {
|
|
|
|
bail!("missing MediaContentHeader");
|
|
|
|
}
|
|
|
|
|
|
|
|
//println!("Found MediaContentHeader: {:?}", header);
|
|
|
|
|
|
|
|
match header.content_magic {
|
|
|
|
PROXMOX_BACKUP_MEDIA_LABEL_MAGIC_1_0 | PROXMOX_BACKUP_MEDIA_SET_LABEL_MAGIC_1_0 => {
|
|
|
|
bail!("unexpected content magic (label)");
|
|
|
|
}
|
|
|
|
PROXMOX_BACKUP_SNAPSHOT_ARCHIVE_MAGIC_1_0 => {
|
2021-03-16 11:52:49 +00:00
|
|
|
bail!("unexpected snapshot archive version (v1.0)");
|
|
|
|
}
|
|
|
|
PROXMOX_BACKUP_SNAPSHOT_ARCHIVE_MAGIC_1_1 => {
|
|
|
|
let header_data = reader.read_exact_allocated(header.size as usize)?;
|
|
|
|
|
|
|
|
let archive_header: SnapshotArchiveHeader = serde_json::from_slice(&header_data)
|
|
|
|
.map_err(|err| format_err!("unable to parse snapshot archive header - {}", err))?;
|
|
|
|
|
|
|
|
let datastore_name = archive_header.store;
|
|
|
|
let snapshot = archive_header.snapshot;
|
|
|
|
|
|
|
|
task_log!(worker, "File {}: snapshot archive {}:{}", current_file_number, datastore_name, snapshot);
|
2020-12-30 08:48:18 +00:00
|
|
|
|
|
|
|
let backup_dir: BackupDir = snapshot.parse()?;
|
|
|
|
|
2021-03-24 13:10:14 +00:00
|
|
|
if let Some((store_map, authid)) = target.as_ref() {
|
|
|
|
if let Some(datastore) = store_map.get_datastore(&datastore_name) {
|
|
|
|
let (owner, _group_lock) =
|
|
|
|
datastore.create_locked_backup_group(backup_dir.group(), authid)?;
|
|
|
|
if *authid != &owner {
|
|
|
|
// only the owner is allowed to create additional snapshots
|
|
|
|
bail!(
|
|
|
|
"restore '{}' failed - owner check failed ({} != {})",
|
|
|
|
snapshot,
|
|
|
|
authid,
|
|
|
|
owner
|
|
|
|
);
|
|
|
|
}
|
2020-12-30 08:48:18 +00:00
|
|
|
|
2021-03-24 13:10:14 +00:00
|
|
|
let (rel_path, is_new, _snap_lock) =
|
|
|
|
datastore.create_locked_backup_dir(&backup_dir)?;
|
|
|
|
let mut path = datastore.base_path();
|
|
|
|
path.push(rel_path);
|
2020-12-30 08:48:18 +00:00
|
|
|
|
2021-03-24 13:10:14 +00:00
|
|
|
if is_new {
|
|
|
|
task_log!(worker, "restore snapshot {}", backup_dir);
|
2020-12-30 08:48:18 +00:00
|
|
|
|
2021-05-05 10:09:14 +00:00
|
|
|
match restore_snapshot_archive(worker.clone(), reader, &path) {
|
2021-03-24 13:10:14 +00:00
|
|
|
Err(err) => {
|
|
|
|
std::fs::remove_dir_all(&path)?;
|
|
|
|
bail!("restore snapshot {} failed - {}", backup_dir, err);
|
|
|
|
}
|
|
|
|
Ok(false) => {
|
|
|
|
std::fs::remove_dir_all(&path)?;
|
|
|
|
task_log!(worker, "skip incomplete snapshot {}", backup_dir);
|
|
|
|
}
|
|
|
|
Ok(true) => {
|
|
|
|
catalog.register_snapshot(
|
|
|
|
Uuid::from(header.uuid),
|
|
|
|
current_file_number,
|
|
|
|
&datastore_name,
|
|
|
|
&snapshot,
|
|
|
|
)?;
|
|
|
|
catalog.commit_if_large()?;
|
|
|
|
}
|
2020-12-30 08:48:18 +00:00
|
|
|
}
|
2021-03-24 13:10:14 +00:00
|
|
|
return Ok(());
|
2020-12-30 08:48:18 +00:00
|
|
|
}
|
2021-03-24 13:10:14 +00:00
|
|
|
} else {
|
|
|
|
task_log!(worker, "skipping...");
|
2020-12-30 08:48:18 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-13 11:11:44 +00:00
|
|
|
reader.skip_data()?; // read all data
|
2020-12-30 08:48:18 +00:00
|
|
|
if let Ok(false) = reader.is_incomplete() {
|
2021-03-16 11:52:49 +00:00
|
|
|
catalog.register_snapshot(Uuid::from(header.uuid), current_file_number, &datastore_name, &snapshot)?;
|
2020-12-30 08:48:18 +00:00
|
|
|
catalog.commit_if_large()?;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
PROXMOX_BACKUP_CHUNK_ARCHIVE_MAGIC_1_0 => {
|
2021-03-16 11:52:49 +00:00
|
|
|
bail!("unexpected chunk archive version (v1.0)");
|
|
|
|
}
|
|
|
|
PROXMOX_BACKUP_CHUNK_ARCHIVE_MAGIC_1_1 => {
|
|
|
|
let header_data = reader.read_exact_allocated(header.size as usize)?;
|
|
|
|
|
|
|
|
let archive_header: ChunkArchiveHeader = serde_json::from_slice(&header_data)
|
|
|
|
.map_err(|err| format_err!("unable to parse chunk archive header - {}", err))?;
|
|
|
|
|
|
|
|
let source_datastore = archive_header.store;
|
2020-12-30 08:48:18 +00:00
|
|
|
|
2021-03-16 11:52:49 +00:00
|
|
|
task_log!(worker, "File {}: chunk archive for datastore '{}'", current_file_number, source_datastore);
|
2021-03-24 13:10:14 +00:00
|
|
|
let datastore = target
|
|
|
|
.as_ref()
|
|
|
|
.and_then(|t| t.0.get_datastore(&source_datastore));
|
|
|
|
|
|
|
|
if datastore.is_some() || target.is_none() {
|
2021-04-16 11:17:17 +00:00
|
|
|
let checked_chunks = checked_chunks_map
|
2021-04-30 10:09:50 +00:00
|
|
|
.entry(datastore.as_ref().map(|d| d.name()).unwrap_or("_unused_").to_string())
|
2021-04-16 11:17:17 +00:00
|
|
|
.or_insert(HashSet::new());
|
|
|
|
|
2021-04-30 10:09:50 +00:00
|
|
|
let chunks = if let Some(datastore) = datastore {
|
2021-05-04 10:21:47 +00:00
|
|
|
restore_chunk_archive(worker.clone(), reader, datastore, checked_chunks, verbose)?
|
2021-04-30 10:09:50 +00:00
|
|
|
} else {
|
2021-05-04 10:21:47 +00:00
|
|
|
scan_chunk_archive(worker.clone(), reader, verbose)?
|
2021-04-30 10:09:50 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
if let Some(chunks) = chunks {
|
2021-07-22 13:41:00 +00:00
|
|
|
catalog.register_chunk_archive(
|
2021-03-24 13:10:14 +00:00
|
|
|
Uuid::from(header.uuid),
|
|
|
|
current_file_number,
|
|
|
|
&source_datastore,
|
2021-07-22 13:41:00 +00:00
|
|
|
&chunks[..],
|
2021-03-24 13:10:14 +00:00
|
|
|
)?;
|
|
|
|
task_log!(worker, "register {} chunks", chunks.len());
|
|
|
|
catalog.commit_if_large()?;
|
2020-12-30 08:48:18 +00:00
|
|
|
}
|
2021-03-24 13:10:14 +00:00
|
|
|
return Ok(());
|
|
|
|
} else if target.is_some() {
|
|
|
|
task_log!(worker, "skipping...");
|
2020-12-30 08:48:18 +00:00
|
|
|
}
|
2021-03-24 13:10:14 +00:00
|
|
|
|
2021-04-13 11:11:44 +00:00
|
|
|
reader.skip_data()?; // read all data
|
2020-12-30 08:48:18 +00:00
|
|
|
}
|
2021-03-24 08:33:39 +00:00
|
|
|
PROXMOX_BACKUP_CATALOG_ARCHIVE_MAGIC_1_0 => {
|
|
|
|
let header_data = reader.read_exact_allocated(header.size as usize)?;
|
|
|
|
|
|
|
|
let archive_header: CatalogArchiveHeader = serde_json::from_slice(&header_data)
|
|
|
|
.map_err(|err| format_err!("unable to parse catalog archive header - {}", err))?;
|
|
|
|
|
|
|
|
task_log!(worker, "File {}: skip catalog '{}'", current_file_number, archive_header.uuid);
|
|
|
|
|
2021-04-13 11:11:44 +00:00
|
|
|
reader.skip_data()?; // read all data
|
2021-03-24 08:33:39 +00:00
|
|
|
}
|
|
|
|
_ => bail!("unknown content magic {:?}", header.content_magic),
|
2020-12-30 08:48:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2021-04-30 10:09:50 +00:00
|
|
|
// Read chunk archive without restoring data - just record contained chunks
|
|
|
|
fn scan_chunk_archive<'a>(
|
2021-05-04 10:21:47 +00:00
|
|
|
worker: Arc<WorkerTask>,
|
2021-04-30 10:09:50 +00:00
|
|
|
reader: Box<dyn 'a + TapeRead>,
|
|
|
|
verbose: bool,
|
|
|
|
) -> Result<Option<Vec<[u8;32]>>, Error> {
|
|
|
|
|
|
|
|
let mut chunks = Vec::new();
|
|
|
|
|
|
|
|
let mut decoder = ChunkArchiveDecoder::new(reader);
|
|
|
|
|
|
|
|
loop {
|
|
|
|
let digest = match decoder.next_chunk() {
|
|
|
|
Ok(Some((digest, _blob))) => digest,
|
|
|
|
Ok(None) => break,
|
|
|
|
Err(err) => {
|
|
|
|
let reader = decoder.reader();
|
|
|
|
|
|
|
|
// check if this stream is marked incomplete
|
|
|
|
if let Ok(true) = reader.is_incomplete() {
|
|
|
|
return Ok(Some(chunks));
|
|
|
|
}
|
|
|
|
|
|
|
|
// check if this is an aborted stream without end marker
|
|
|
|
if let Ok(false) = reader.has_end_marker() {
|
2021-05-04 10:21:47 +00:00
|
|
|
task_log!(worker, "missing stream end marker");
|
2021-04-30 10:09:50 +00:00
|
|
|
return Ok(None);
|
|
|
|
}
|
|
|
|
|
|
|
|
// else the archive is corrupt
|
|
|
|
return Err(err);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
worker.check_abort()?;
|
|
|
|
|
|
|
|
if verbose {
|
|
|
|
task_log!(worker, "Found chunk: {}", proxmox::tools::digest_to_hex(&digest));
|
|
|
|
}
|
|
|
|
|
|
|
|
chunks.push(digest);
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(Some(chunks))
|
|
|
|
}
|
|
|
|
|
2020-12-30 08:48:18 +00:00
|
|
|
fn restore_chunk_archive<'a>(
|
2021-05-04 10:21:47 +00:00
|
|
|
worker: Arc<WorkerTask>,
|
2020-12-30 08:48:18 +00:00
|
|
|
reader: Box<dyn 'a + TapeRead>,
|
2021-04-30 10:09:50 +00:00
|
|
|
datastore: Arc<DataStore>,
|
2021-04-16 11:17:17 +00:00
|
|
|
checked_chunks: &mut HashSet<[u8;32]>,
|
2020-12-30 08:48:18 +00:00
|
|
|
verbose: bool,
|
|
|
|
) -> Result<Option<Vec<[u8;32]>>, Error> {
|
|
|
|
|
2021-04-16 11:17:17 +00:00
|
|
|
let mut chunks = Vec::new();
|
2020-12-30 08:48:18 +00:00
|
|
|
|
|
|
|
let mut decoder = ChunkArchiveDecoder::new(reader);
|
|
|
|
|
2021-04-30 10:35:11 +00:00
|
|
|
let datastore2 = datastore.clone();
|
2021-05-04 10:21:47 +00:00
|
|
|
let start_time = std::time::SystemTime::now();
|
|
|
|
let bytes = Arc::new(std::sync::atomic::AtomicU64::new(0));
|
|
|
|
let bytes2 = bytes.clone();
|
|
|
|
|
|
|
|
let worker2 = worker.clone();
|
|
|
|
|
2021-04-30 10:35:11 +00:00
|
|
|
let writer_pool = ParallelHandler::new(
|
|
|
|
"tape restore chunk writer",
|
|
|
|
4,
|
|
|
|
move |(chunk, digest): (DataBlob, [u8; 32])| {
|
2021-05-04 10:21:47 +00:00
|
|
|
let chunk_exists = datastore2.cond_touch_chunk(&digest, false)?;
|
|
|
|
if !chunk_exists {
|
|
|
|
if verbose {
|
|
|
|
task_log!(worker2, "Insert chunk: {}", proxmox::tools::digest_to_hex(&digest));
|
|
|
|
}
|
|
|
|
bytes2.fetch_add(chunk.raw_size(), std::sync::atomic::Ordering::SeqCst);
|
|
|
|
// println!("verify and write {}", proxmox::tools::digest_to_hex(&digest));
|
|
|
|
chunk.verify_crc()?;
|
|
|
|
if chunk.crypt_mode()? == CryptMode::None {
|
|
|
|
chunk.decode(None, Some(&digest))?; // verify digest
|
|
|
|
}
|
2021-04-30 10:35:11 +00:00
|
|
|
|
2021-05-04 10:21:47 +00:00
|
|
|
datastore2.insert_chunk(&chunk, &digest)?;
|
|
|
|
} else if verbose {
|
|
|
|
task_log!(worker2, "Found existing chunk: {}", proxmox::tools::digest_to_hex(&digest));
|
|
|
|
}
|
2021-04-30 10:35:11 +00:00
|
|
|
Ok(())
|
|
|
|
},
|
|
|
|
);
|
|
|
|
|
|
|
|
let verify_and_write_channel = writer_pool.channel();
|
|
|
|
|
2021-04-30 11:04:55 +00:00
|
|
|
|
2021-04-13 10:58:58 +00:00
|
|
|
loop {
|
|
|
|
let (digest, blob) = match decoder.next_chunk() {
|
|
|
|
Ok(Some((digest, blob))) => (digest, blob),
|
|
|
|
Ok(None) => break,
|
|
|
|
Err(err) => {
|
|
|
|
let reader = decoder.reader();
|
|
|
|
|
|
|
|
// check if this stream is marked incomplete
|
|
|
|
if let Ok(true) = reader.is_incomplete() {
|
|
|
|
return Ok(Some(chunks));
|
|
|
|
}
|
2021-01-19 13:52:20 +00:00
|
|
|
|
2021-04-13 10:58:58 +00:00
|
|
|
// check if this is an aborted stream without end marker
|
|
|
|
if let Ok(false) = reader.has_end_marker() {
|
2021-05-04 10:21:47 +00:00
|
|
|
task_log!(worker, "missing stream end marker");
|
2021-04-13 10:58:58 +00:00
|
|
|
return Ok(None);
|
2020-12-30 08:48:18 +00:00
|
|
|
}
|
2021-04-13 10:58:58 +00:00
|
|
|
|
|
|
|
// else the archive is corrupt
|
|
|
|
return Err(err);
|
2020-12-30 08:48:18 +00:00
|
|
|
}
|
2021-04-13 10:58:58 +00:00
|
|
|
};
|
2020-12-30 08:48:18 +00:00
|
|
|
|
2021-04-13 10:58:58 +00:00
|
|
|
worker.check_abort()?;
|
2020-12-30 08:48:18 +00:00
|
|
|
|
2021-05-04 10:21:47 +00:00
|
|
|
if !checked_chunks.contains(&digest) {
|
2021-04-30 10:35:11 +00:00
|
|
|
verify_and_write_channel.send((blob, digest.clone()))?;
|
2021-05-04 10:21:47 +00:00
|
|
|
checked_chunks.insert(digest.clone());
|
2020-12-30 08:48:18 +00:00
|
|
|
}
|
2021-04-13 10:58:58 +00:00
|
|
|
chunks.push(digest);
|
2020-12-30 08:48:18 +00:00
|
|
|
}
|
2021-04-13 10:58:58 +00:00
|
|
|
|
2021-04-30 10:35:11 +00:00
|
|
|
drop(verify_and_write_channel);
|
|
|
|
|
|
|
|
writer_pool.complete()?;
|
|
|
|
|
2021-04-30 11:04:55 +00:00
|
|
|
let elapsed = start_time.elapsed()?.as_secs_f64();
|
|
|
|
|
2021-05-04 10:21:47 +00:00
|
|
|
let bytes = bytes.load(std::sync::atomic::Ordering::SeqCst);
|
|
|
|
|
2021-04-30 11:04:55 +00:00
|
|
|
task_log!(
|
|
|
|
worker,
|
|
|
|
"restored {} bytes ({:.2} MB/s)",
|
|
|
|
bytes,
|
|
|
|
(bytes as f64) / (1_000_000.0 * elapsed)
|
|
|
|
);
|
|
|
|
|
2021-04-13 10:58:58 +00:00
|
|
|
Ok(Some(chunks))
|
2020-12-30 08:48:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
fn restore_snapshot_archive<'a>(
|
2021-05-04 10:21:47 +00:00
|
|
|
worker: Arc<WorkerTask>,
|
2020-12-30 08:48:18 +00:00
|
|
|
reader: Box<dyn 'a + TapeRead>,
|
|
|
|
snapshot_path: &Path,
|
|
|
|
) -> Result<bool, Error> {
|
|
|
|
|
|
|
|
let mut decoder = pxar::decoder::sync::Decoder::from_std(reader)?;
|
2021-05-05 10:09:14 +00:00
|
|
|
match try_restore_snapshot_archive(worker, &mut decoder, snapshot_path) {
|
|
|
|
Ok(_) => Ok(true),
|
2020-12-30 08:48:18 +00:00
|
|
|
Err(err) => {
|
|
|
|
let reader = decoder.input();
|
|
|
|
|
|
|
|
// check if this stream is marked incomplete
|
|
|
|
if let Ok(true) = reader.is_incomplete() {
|
|
|
|
return Ok(false);
|
|
|
|
}
|
|
|
|
|
|
|
|
// check if this is an aborted stream without end marker
|
|
|
|
if let Ok(false) = reader.has_end_marker() {
|
|
|
|
return Ok(false);
|
|
|
|
}
|
|
|
|
|
|
|
|
// else the archive is corrupt
|
2021-01-19 09:50:42 +00:00
|
|
|
Err(err)
|
2020-12-30 08:48:18 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn try_restore_snapshot_archive<R: pxar::decoder::SeqRead>(
|
2021-05-04 10:21:47 +00:00
|
|
|
worker: Arc<WorkerTask>,
|
2020-12-30 08:48:18 +00:00
|
|
|
decoder: &mut pxar::decoder::sync::Decoder<R>,
|
|
|
|
snapshot_path: &Path,
|
2021-05-06 12:20:02 +00:00
|
|
|
) -> Result<BackupManifest, Error> {
|
2020-12-30 08:48:18 +00:00
|
|
|
|
|
|
|
let _root = match decoder.next() {
|
|
|
|
None => bail!("missing root entry"),
|
|
|
|
Some(root) => {
|
|
|
|
let root = root?;
|
|
|
|
match root.kind() {
|
|
|
|
pxar::EntryKind::Directory => { /* Ok */ }
|
|
|
|
_ => bail!("wrong root entry type"),
|
|
|
|
}
|
|
|
|
root
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
let root_path = Path::new("/");
|
|
|
|
let manifest_file_name = OsStr::new(MANIFEST_BLOB_NAME);
|
|
|
|
|
|
|
|
let mut manifest = None;
|
|
|
|
|
|
|
|
loop {
|
2021-02-04 06:05:43 +00:00
|
|
|
worker.check_abort()?;
|
|
|
|
|
2020-12-30 08:48:18 +00:00
|
|
|
let entry = match decoder.next() {
|
|
|
|
None => break,
|
|
|
|
Some(entry) => entry?,
|
|
|
|
};
|
|
|
|
let entry_path = entry.path();
|
|
|
|
|
|
|
|
match entry.kind() {
|
|
|
|
pxar::EntryKind::File { .. } => { /* Ok */ }
|
|
|
|
_ => bail!("wrong entry type for {:?}", entry_path),
|
|
|
|
}
|
|
|
|
match entry_path.parent() {
|
|
|
|
None => bail!("wrong parent for {:?}", entry_path),
|
|
|
|
Some(p) => {
|
|
|
|
if p != root_path {
|
|
|
|
bail!("wrong parent for {:?}", entry_path);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
let filename = entry.file_name();
|
|
|
|
let mut contents = match decoder.contents() {
|
|
|
|
None => bail!("missing file content"),
|
|
|
|
Some(contents) => contents,
|
|
|
|
};
|
|
|
|
|
|
|
|
let mut archive_path = snapshot_path.to_owned();
|
|
|
|
archive_path.push(&filename);
|
|
|
|
|
|
|
|
let mut tmp_path = archive_path.clone();
|
|
|
|
tmp_path.set_extension("tmp");
|
|
|
|
|
|
|
|
if filename == manifest_file_name {
|
|
|
|
|
|
|
|
let blob = DataBlob::load_from_reader(&mut contents)?;
|
2021-05-03 10:42:49 +00:00
|
|
|
let mut old_manifest = BackupManifest::try_from(blob)?;
|
|
|
|
|
|
|
|
// Remove verify_state to indicate that this snapshot is not verified
|
|
|
|
old_manifest.unprotected
|
|
|
|
.as_object_mut()
|
|
|
|
.map(|m| m.remove("verify_state"));
|
|
|
|
|
|
|
|
let old_manifest = serde_json::to_string_pretty(&old_manifest)?;
|
|
|
|
let blob = DataBlob::encode(old_manifest.as_bytes(), None, true)?;
|
|
|
|
|
2020-12-30 08:48:18 +00:00
|
|
|
let options = CreateOptions::new();
|
|
|
|
replace_file(&tmp_path, blob.raw_data(), options)?;
|
|
|
|
|
|
|
|
manifest = Some(BackupManifest::try_from(blob)?);
|
|
|
|
} else {
|
|
|
|
let mut tmpfile = std::fs::OpenOptions::new()
|
|
|
|
.write(true)
|
|
|
|
.create(true)
|
|
|
|
.read(true)
|
|
|
|
.open(&tmp_path)
|
|
|
|
.map_err(|err| format_err!("restore {:?} failed - {}", tmp_path, err))?;
|
|
|
|
|
|
|
|
std::io::copy(&mut contents, &mut tmpfile)?;
|
|
|
|
|
|
|
|
if let Err(err) = std::fs::rename(&tmp_path, &archive_path) {
|
|
|
|
bail!("Atomic rename file {:?} failed - {}", archive_path, err);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-05-06 12:20:02 +00:00
|
|
|
let manifest = match manifest {
|
|
|
|
None => bail!("missing manifest"),
|
|
|
|
Some(manifest) => manifest,
|
|
|
|
};
|
2020-12-30 08:48:18 +00:00
|
|
|
|
2021-05-03 10:42:49 +00:00
|
|
|
// Do not verify anything here, because this would be to slow (causes tape stops).
|
|
|
|
|
2020-12-30 08:48:18 +00:00
|
|
|
// commit manifest
|
|
|
|
let mut manifest_path = snapshot_path.to_owned();
|
|
|
|
manifest_path.push(MANIFEST_BLOB_NAME);
|
|
|
|
let mut tmp_manifest_path = manifest_path.clone();
|
|
|
|
tmp_manifest_path.set_extension("tmp");
|
|
|
|
|
|
|
|
if let Err(err) = std::fs::rename(&tmp_manifest_path, &manifest_path) {
|
|
|
|
bail!("Atomic rename manifest {:?} failed - {}", manifest_path, err);
|
|
|
|
}
|
|
|
|
|
2021-05-06 12:20:02 +00:00
|
|
|
Ok(manifest)
|
2020-12-30 08:48:18 +00:00
|
|
|
}
|
2021-03-23 12:39:33 +00:00
|
|
|
|
|
|
|
/// Try to restore media catalogs (form catalog_archives)
|
|
|
|
pub fn fast_catalog_restore(
|
|
|
|
worker: &WorkerTask,
|
|
|
|
drive: &mut Box<dyn TapeDriver>,
|
|
|
|
media_set: &MediaSet,
|
|
|
|
uuid: &Uuid, // current media Uuid
|
|
|
|
) -> Result<bool, Error> {
|
|
|
|
|
|
|
|
let status_path = Path::new(TAPE_STATUS_DIR);
|
|
|
|
|
|
|
|
let current_file_number = drive.current_file_number()?;
|
|
|
|
if current_file_number != 2 {
|
|
|
|
bail!("fast_catalog_restore: wrong media position - internal error");
|
|
|
|
}
|
|
|
|
|
|
|
|
let mut found_catalog = false;
|
|
|
|
|
|
|
|
let mut moved_to_eom = false;
|
|
|
|
|
|
|
|
loop {
|
|
|
|
let current_file_number = drive.current_file_number()?;
|
|
|
|
|
|
|
|
{ // limit reader scope
|
2021-04-12 09:25:40 +00:00
|
|
|
let mut reader = match drive.read_next_file() {
|
|
|
|
Err(BlockReadError::EndOfFile) => {
|
|
|
|
task_log!(worker, "skip unexpected filemark at pos {}", current_file_number);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
Err(BlockReadError::EndOfStream) => {
|
2021-03-23 12:39:33 +00:00
|
|
|
task_log!(worker, "detected EOT after {} files", current_file_number);
|
|
|
|
break;
|
|
|
|
}
|
2021-04-12 09:25:40 +00:00
|
|
|
Err(BlockReadError::Error(err)) => {
|
|
|
|
return Err(err.into());
|
|
|
|
}
|
|
|
|
Ok(reader) => reader,
|
2021-03-23 12:39:33 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
let header: MediaContentHeader = unsafe { reader.read_le_value()? };
|
|
|
|
if header.magic != PROXMOX_BACKUP_CONTENT_HEADER_MAGIC_1_0 {
|
|
|
|
bail!("missing MediaContentHeader");
|
|
|
|
}
|
|
|
|
|
|
|
|
if header.content_magic == PROXMOX_BACKUP_CATALOG_ARCHIVE_MAGIC_1_0 {
|
|
|
|
task_log!(worker, "found catalog at pos {}", current_file_number);
|
|
|
|
|
|
|
|
let header_data = reader.read_exact_allocated(header.size as usize)?;
|
|
|
|
|
|
|
|
let archive_header: CatalogArchiveHeader = serde_json::from_slice(&header_data)
|
|
|
|
.map_err(|err| format_err!("unable to parse catalog archive header - {}", err))?;
|
|
|
|
|
|
|
|
if &archive_header.media_set_uuid != media_set.uuid() {
|
|
|
|
task_log!(worker, "skipping unrelated catalog at pos {}", current_file_number);
|
2021-04-13 11:11:44 +00:00
|
|
|
reader.skip_data()?; // read all data
|
2021-03-23 12:39:33 +00:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
let catalog_uuid = &archive_header.uuid;
|
|
|
|
|
|
|
|
let wanted = media_set
|
|
|
|
.media_list()
|
|
|
|
.iter()
|
|
|
|
.find(|e| {
|
|
|
|
match e {
|
|
|
|
None => false,
|
|
|
|
Some(uuid) => uuid == catalog_uuid,
|
|
|
|
}
|
|
|
|
})
|
|
|
|
.is_some();
|
|
|
|
|
|
|
|
if !wanted {
|
|
|
|
task_log!(worker, "skip catalog because media '{}' not inventarized", catalog_uuid);
|
2021-04-13 11:11:44 +00:00
|
|
|
reader.skip_data()?; // read all data
|
2021-03-23 12:39:33 +00:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
if catalog_uuid == uuid {
|
|
|
|
// always restore and overwrite catalog
|
|
|
|
} else {
|
|
|
|
// only restore if catalog does not exist
|
|
|
|
if MediaCatalog::exists(status_path, catalog_uuid) {
|
|
|
|
task_log!(worker, "catalog for media '{}' already exists", catalog_uuid);
|
2021-04-13 11:11:44 +00:00
|
|
|
reader.skip_data()?; // read all data
|
2021-03-23 12:39:33 +00:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
let mut file = MediaCatalog::create_temporary_database_file(status_path, catalog_uuid)?;
|
|
|
|
|
|
|
|
std::io::copy(&mut reader, &mut file)?;
|
|
|
|
|
|
|
|
file.seek(SeekFrom::Start(0))?;
|
|
|
|
|
|
|
|
match MediaCatalog::parse_catalog_header(&mut file)? {
|
|
|
|
(true, Some(media_uuid), Some(media_set_uuid)) => {
|
|
|
|
if &media_uuid != catalog_uuid {
|
|
|
|
task_log!(worker, "catalog uuid missmatch at pos {}", current_file_number);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if media_set_uuid != archive_header.media_set_uuid {
|
|
|
|
task_log!(worker, "catalog media_set missmatch at pos {}", current_file_number);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
MediaCatalog::finish_temporary_database(status_path, &media_uuid, true)?;
|
|
|
|
|
|
|
|
if catalog_uuid == uuid {
|
|
|
|
task_log!(worker, "successfully restored catalog");
|
|
|
|
found_catalog = true
|
|
|
|
} else {
|
|
|
|
task_log!(worker, "successfully restored related catalog {}", media_uuid);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
_ => {
|
|
|
|
task_warn!(worker, "got incomplete catalog header - skip file");
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if moved_to_eom {
|
|
|
|
break; // already done - stop
|
|
|
|
}
|
|
|
|
moved_to_eom = true;
|
|
|
|
|
|
|
|
task_log!(worker, "searching for catalog at EOT (moving to EOT)");
|
|
|
|
drive.move_to_last_file()?;
|
|
|
|
|
|
|
|
let new_file_number = drive.current_file_number()?;
|
|
|
|
|
|
|
|
if new_file_number < (current_file_number + 1) {
|
|
|
|
break; // no new content - stop
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(found_catalog)
|
|
|
|
}
|