3 use std
::convert
::TryFrom
;
4 use std
::io
::{Seek, SeekFrom}
;
6 use anyhow
::{bail, format_err, Error}
;
16 section_config
::SectionConfigData
,
32 tools
::compute_file_csum
,
42 cached_user_info
::CachedUserInfo
,
44 PRIV_DATASTORE_BACKUP
,
45 PRIV_DATASTORE_MODIFY
,
75 PROXMOX_BACKUP_MEDIA_LABEL_MAGIC_1_0
,
76 PROXMOX_BACKUP_SNAPSHOT_ARCHIVE_MAGIC_1_0
,
77 PROXMOX_BACKUP_SNAPSHOT_ARCHIVE_MAGIC_1_1
,
78 PROXMOX_BACKUP_MEDIA_SET_LABEL_MAGIC_1_0
,
79 PROXMOX_BACKUP_CONTENT_HEADER_MAGIC_1_0
,
80 PROXMOX_BACKUP_CHUNK_ARCHIVE_MAGIC_1_0
,
81 PROXMOX_BACKUP_CHUNK_ARCHIVE_MAGIC_1_1
,
82 PROXMOX_BACKUP_CATALOG_ARCHIVE_MAGIC_1_0
,
86 SnapshotArchiveHeader
,
91 request_and_load_media
,
93 set_tape_device_state
,
98 pub const ROUTER
: Router
= Router
::new()
99 .post(&API_METHOD_RESTORE
);
105 schema
: DATASTORE_SCHEMA
,
108 schema
: DRIVE_NAME_SCHEMA
,
111 description
: "Media set UUID.",
128 // Note: parameters are no uri parameter, so we need to test inside function body
129 description
: "The user needs Tape.Read privilege on /tape/pool/{pool} \
130 and /tape/drive/{drive}, Datastore.Backup privilege on /datastore/{store}.",
131 permission
: &Permission
::Anybody
,
134 /// Restore data from media-set
139 notify_user
: Option
<Userid
>,
140 owner
: Option
<Authid
>,
141 rpcenv
: &mut dyn RpcEnvironment
,
142 ) -> Result
<Value
, Error
> {
144 let auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
145 let user_info
= CachedUserInfo
::new()?
;
147 let privs
= user_info
.lookup_privs(&auth_id
, &["datastore", &store
]);
148 if (privs
& PRIV_DATASTORE_BACKUP
) == 0 {
149 bail
!("no permissions on /datastore/{}", store
);
152 if let Some(ref owner
) = owner
{
153 let correct_owner
= owner
== &auth_id
155 && !auth_id
.is_token()
156 && owner
.user() == auth_id
.user());
158 // same permission as changing ownership after syncing
159 if !correct_owner
&& privs
& PRIV_DATASTORE_MODIFY
== 0 {
160 bail
!("no permission to restore as '{}'", owner
);
164 let privs
= user_info
.lookup_privs(&auth_id
, &["tape", "drive", &drive
]);
165 if (privs
& PRIV_TAPE_READ
) == 0 {
166 bail
!("no permissions on /tape/drive/{}", drive
);
169 let media_set_uuid
= media_set
.parse()?
;
171 let status_path
= Path
::new(TAPE_STATUS_DIR
);
173 let _lock
= lock_media_set(status_path
, &media_set_uuid
, None
)?
;
175 let inventory
= Inventory
::load(status_path
)?
;
177 let pool
= inventory
.lookup_media_set_pool(&media_set_uuid
)?
;
179 let privs
= user_info
.lookup_privs(&auth_id
, &["tape", "pool", &pool
]);
180 if (privs
& PRIV_TAPE_READ
) == 0 {
181 bail
!("no permissions on /tape/pool/{}", pool
);
184 let datastore
= DataStore
::lookup_datastore(&store
)?
;
186 let (drive_config
, _digest
) = config
::drive
::config()?
;
188 // early check/lock before starting worker
189 let drive_lock
= lock_tape_device(&drive_config
, &drive
)?
;
191 let to_stdout
= rpcenv
.env_type() == RpcEnvironmentType
::CLI
;
193 let upid_str
= WorkerTask
::new_thread(
199 let _drive_lock
= drive_lock
; // keep lock guard
201 set_tape_device_state(&drive
, &worker
.upid().to_string())?
;
203 let members
= inventory
.compute_media_set_members(&media_set_uuid
)?
;
205 let media_list
= members
.media_list();
207 let mut media_id_list
= Vec
::new();
209 let mut encryption_key_fingerprint
= None
;
211 for (seq_nr
, media_uuid
) in media_list
.iter().enumerate() {
214 bail
!("media set {} is incomplete (missing member {}).", media_set_uuid
, seq_nr
);
216 Some(media_uuid
) => {
217 let media_id
= inventory
.lookup_media(media_uuid
).unwrap();
218 if let Some(ref set
) = media_id
.media_set_label
{ // always true here
219 if encryption_key_fingerprint
.is_none() && set
.encryption_key_fingerprint
.is_some() {
220 encryption_key_fingerprint
= set
.encryption_key_fingerprint
.clone();
223 media_id_list
.push(media_id
);
228 task_log
!(worker
, "Restore mediaset '{}'", media_set
);
229 if let Some(fingerprint
) = encryption_key_fingerprint
{
230 task_log
!(worker
, "Encryption key fingerprint: {}", fingerprint
);
232 task_log
!(worker
, "Pool: {}", pool
);
233 task_log
!(worker
, "Datastore: {}", store
);
234 task_log
!(worker
, "Drive: {}", drive
);
237 "Required media list: {}",
239 .map(|media_id
| media_id
.label
.label_text
.as_str())
240 .collect
::<Vec
<&str>>()
244 for media_id
in media_id_list
.iter() {
245 request_and_restore_media(
257 task_log
!(worker
, "Restore mediaset '{}' done", media_set
);
259 if let Err(err
) = set_tape_device_state(&drive
, "") {
262 "could not unset drive state for {}: {}",
275 /// Request and restore complete media without using existing catalog (create catalog instead)
276 pub fn request_and_restore_media(
279 drive_config
: &SectionConfigData
,
281 datastore
: &DataStore
,
283 notify_user
: &Option
<Userid
>,
284 owner
: &Option
<Authid
>,
285 ) -> Result
<(), Error
> {
287 let media_set_uuid
= match media_id
.media_set_label
{
288 None
=> bail
!("restore_media: no media set - internal error"),
289 Some(ref set
) => &set
.uuid
,
292 let email
= notify_user
294 .and_then(|userid
| lookup_user_email(userid
))
295 .or_else(|| lookup_user_email(&authid
.clone().into()));
297 let (mut drive
, info
) = request_and_load_media(worker
, &drive_config
, &drive_name
, &media_id
.label
, &email
)?
;
299 match info
.media_set_label
{
301 bail
!("missing media set label on media {} ({})",
302 media_id
.label
.label_text
, media_id
.label
.uuid
);
305 if &set
.uuid
!= media_set_uuid
{
306 bail
!("wrong media set label on media {} ({} != {})",
307 media_id
.label
.label_text
, media_id
.label
.uuid
,
310 let encrypt_fingerprint
= set
.encryption_key_fingerprint
.clone()
311 .map(|fp
| (fp
, set
.uuid
.clone()));
313 drive
.set_encryption(encrypt_fingerprint
)?
;
317 let restore_owner
= owner
.as_ref().unwrap_or(authid
);
319 restore_media(worker
, &mut drive
, &info
, Some((datastore
, restore_owner
)), false)
322 /// Restore complete media content and catalog
324 /// Only create the catalog if target is None.
325 pub fn restore_media(
327 drive
: &mut Box
<dyn TapeDriver
>,
329 target
: Option
<(&DataStore
, &Authid
)>,
331 ) -> Result
<(), Error
> {
333 let status_path
= Path
::new(TAPE_STATUS_DIR
);
334 let mut catalog
= MediaCatalog
::create_temporary_database(status_path
, media_id
, false)?
;
337 let current_file_number
= drive
.current_file_number()?
;
338 let reader
= match drive
.read_next_file()?
{
340 task_log
!(worker
, "detected EOT after {} files", current_file_number
);
343 Some(reader
) => reader
,
346 restore_archive(worker
, reader
, current_file_number
, target
, &mut catalog
, verbose
)?
;
349 MediaCatalog
::finish_temporary_database(status_path
, &media_id
.label
.uuid
, true)?
;
354 fn restore_archive
<'a
>(
356 mut reader
: Box
<dyn 'a
+ TapeRead
>,
357 current_file_number
: u64,
358 target
: Option
<(&DataStore
, &Authid
)>,
359 catalog
: &mut MediaCatalog
,
361 ) -> Result
<(), Error
> {
363 let header
: MediaContentHeader
= unsafe { reader.read_le_value()? }
;
364 if header
.magic
!= PROXMOX_BACKUP_CONTENT_HEADER_MAGIC_1_0
{
365 bail
!("missing MediaContentHeader");
368 //println!("Found MediaContentHeader: {:?}", header);
370 match header
.content_magic
{
371 PROXMOX_BACKUP_MEDIA_LABEL_MAGIC_1_0
| PROXMOX_BACKUP_MEDIA_SET_LABEL_MAGIC_1_0
=> {
372 bail
!("unexpected content magic (label)");
374 PROXMOX_BACKUP_SNAPSHOT_ARCHIVE_MAGIC_1_0
=> {
375 bail
!("unexpected snapshot archive version (v1.0)");
377 PROXMOX_BACKUP_SNAPSHOT_ARCHIVE_MAGIC_1_1
=> {
378 let header_data
= reader
.read_exact_allocated(header
.size
as usize)?
;
380 let archive_header
: SnapshotArchiveHeader
= serde_json
::from_slice(&header_data
)
381 .map_err(|err
| format_err
!("unable to parse snapshot archive header - {}", err
))?
;
383 let datastore_name
= archive_header
.store
;
384 let snapshot
= archive_header
.snapshot
;
386 task_log
!(worker
, "File {}: snapshot archive {}:{}", current_file_number
, datastore_name
, snapshot
);
388 let backup_dir
: BackupDir
= snapshot
.parse()?
;
390 if let Some((datastore
, authid
)) = target
.as_ref() {
392 let (owner
, _group_lock
) = datastore
.create_locked_backup_group(backup_dir
.group(), authid
)?
;
393 if *authid
!= &owner
{ // only the owner is allowed to create additional snapshots
394 bail
!("restore '{}' failed - owner check failed ({} != {})", snapshot
, authid
, owner
);
397 let (rel_path
, is_new
, _snap_lock
) = datastore
.create_locked_backup_dir(&backup_dir
)?
;
398 let mut path
= datastore
.base_path();
402 task_log
!(worker
, "restore snapshot {}", backup_dir
);
404 match restore_snapshot_archive(worker
, reader
, &path
) {
406 std
::fs
::remove_dir_all(&path
)?
;
407 bail
!("restore snapshot {} failed - {}", backup_dir
, err
);
410 std
::fs
::remove_dir_all(&path
)?
;
411 task_log
!(worker
, "skip incomplete snapshot {}", backup_dir
);
414 catalog
.register_snapshot(Uuid
::from(header
.uuid
), current_file_number
, &datastore_name
, &snapshot
)?
;
415 catalog
.commit_if_large()?
;
422 reader
.skip_to_end()?
; // read all data
423 if let Ok(false) = reader
.is_incomplete() {
424 catalog
.register_snapshot(Uuid
::from(header
.uuid
), current_file_number
, &datastore_name
, &snapshot
)?
;
425 catalog
.commit_if_large()?
;
428 PROXMOX_BACKUP_CHUNK_ARCHIVE_MAGIC_1_0
=> {
429 bail
!("unexpected chunk archive version (v1.0)");
431 PROXMOX_BACKUP_CHUNK_ARCHIVE_MAGIC_1_1
=> {
432 let header_data
= reader
.read_exact_allocated(header
.size
as usize)?
;
434 let archive_header
: ChunkArchiveHeader
= serde_json
::from_slice(&header_data
)
435 .map_err(|err
| format_err
!("unable to parse chunk archive header - {}", err
))?
;
437 let source_datastore
= archive_header
.store
;
439 task_log
!(worker
, "File {}: chunk archive for datastore '{}'", current_file_number
, source_datastore
);
440 let datastore
= target
.as_ref().map(|t
| t
.0);
442 if let Some(chunks
) = restore_chunk_archive(worker
, reader
, datastore
, verbose
)?
{
443 catalog
.start_chunk_archive(Uuid
::from(header
.uuid
), current_file_number
, &source_datastore
)?
;
444 for digest
in chunks
.iter() {
445 catalog
.register_chunk(&digest
)?
;
447 task_log
!(worker
, "register {} chunks", chunks
.len());
448 catalog
.end_chunk_archive()?
;
449 catalog
.commit_if_large()?
;
452 PROXMOX_BACKUP_CATALOG_ARCHIVE_MAGIC_1_0
=> {
453 let header_data
= reader
.read_exact_allocated(header
.size
as usize)?
;
455 let archive_header
: CatalogArchiveHeader
= serde_json
::from_slice(&header_data
)
456 .map_err(|err
| format_err
!("unable to parse catalog archive header - {}", err
))?
;
458 task_log
!(worker
, "File {}: skip catalog '{}'", current_file_number
, archive_header
.uuid
);
460 reader
.skip_to_end()?
; // read all data
462 _
=> bail
!("unknown content magic {:?}", header
.content_magic
),
470 fn restore_chunk_archive
<'a
>(
472 reader
: Box
<dyn 'a
+ TapeRead
>,
473 datastore
: Option
<&DataStore
>,
475 ) -> Result
<Option
<Vec
<[u8;32]>>, Error
> {
477 let mut chunks
= Vec
::new();
479 let mut decoder
= ChunkArchiveDecoder
::new(reader
);
481 let result
: Result
<_
, Error
> = proxmox
::try_block
!({
482 while let Some((digest
, blob
)) = decoder
.next_chunk()?
{
484 worker
.check_abort()?
;
486 if let Some(datastore
) = datastore
{
487 let chunk_exists
= datastore
.cond_touch_chunk(&digest
, false)?
;
491 if blob
.crypt_mode()?
== CryptMode
::None
{
492 blob
.decode(None
, Some(&digest
))?
; // verify digest
495 task_log
!(worker
, "Insert chunk: {}", proxmox
::tools
::digest_to_hex(&digest
));
497 datastore
.insert_chunk(&blob
, &digest
)?
;
499 task_log
!(worker
, "Found existing chunk: {}", proxmox
::tools
::digest_to_hex(&digest
));
502 task_log
!(worker
, "Found chunk: {}", proxmox
::tools
::digest_to_hex(&digest
));
510 Ok(()) => Ok(Some(chunks
)),
512 let reader
= decoder
.reader();
514 // check if this stream is marked incomplete
515 if let Ok(true) = reader
.is_incomplete() {
516 return Ok(Some(chunks
));
519 // check if this is an aborted stream without end marker
520 if let Ok(false) = reader
.has_end_marker() {
521 worker
.log("missing stream end marker".to_string());
525 // else the archive is corrupt
531 fn restore_snapshot_archive
<'a
>(
533 reader
: Box
<dyn 'a
+ TapeRead
>,
534 snapshot_path
: &Path
,
535 ) -> Result
<bool
, Error
> {
537 let mut decoder
= pxar
::decoder
::sync
::Decoder
::from_std(reader
)?
;
538 match try_restore_snapshot_archive(worker
, &mut decoder
, snapshot_path
) {
541 let reader
= decoder
.input();
543 // check if this stream is marked incomplete
544 if let Ok(true) = reader
.is_incomplete() {
548 // check if this is an aborted stream without end marker
549 if let Ok(false) = reader
.has_end_marker() {
553 // else the archive is corrupt
559 fn try_restore_snapshot_archive
<R
: pxar
::decoder
::SeqRead
>(
561 decoder
: &mut pxar
::decoder
::sync
::Decoder
<R
>,
562 snapshot_path
: &Path
,
563 ) -> Result
<(), Error
> {
565 let _root
= match decoder
.next() {
566 None
=> bail
!("missing root entry"),
570 pxar
::EntryKind
::Directory
=> { /* Ok */ }
571 _
=> bail
!("wrong root entry type"),
577 let root_path
= Path
::new("/");
578 let manifest_file_name
= OsStr
::new(MANIFEST_BLOB_NAME
);
580 let mut manifest
= None
;
583 worker
.check_abort()?
;
585 let entry
= match decoder
.next() {
587 Some(entry
) => entry?
,
589 let entry_path
= entry
.path();
592 pxar
::EntryKind
::File { .. }
=> { /* Ok */ }
593 _
=> bail
!("wrong entry type for {:?}", entry_path
),
595 match entry_path
.parent() {
596 None
=> bail
!("wrong parent for {:?}", entry_path
),
599 bail
!("wrong parent for {:?}", entry_path
);
604 let filename
= entry
.file_name();
605 let mut contents
= match decoder
.contents() {
606 None
=> bail
!("missing file content"),
607 Some(contents
) => contents
,
610 let mut archive_path
= snapshot_path
.to_owned();
611 archive_path
.push(&filename
);
613 let mut tmp_path
= archive_path
.clone();
614 tmp_path
.set_extension("tmp");
616 if filename
== manifest_file_name
{
618 let blob
= DataBlob
::load_from_reader(&mut contents
)?
;
619 let options
= CreateOptions
::new();
620 replace_file(&tmp_path
, blob
.raw_data(), options
)?
;
622 manifest
= Some(BackupManifest
::try_from(blob
)?
);
624 let mut tmpfile
= std
::fs
::OpenOptions
::new()
629 .map_err(|err
| format_err
!("restore {:?} failed - {}", tmp_path
, err
))?
;
631 std
::io
::copy(&mut contents
, &mut tmpfile
)?
;
633 if let Err(err
) = std
::fs
::rename(&tmp_path
, &archive_path
) {
634 bail
!("Atomic rename file {:?} failed - {}", archive_path
, err
);
639 let manifest
= match manifest
{
640 None
=> bail
!("missing manifest"),
641 Some(manifest
) => manifest
,
644 for item
in manifest
.files() {
645 let mut archive_path
= snapshot_path
.to_owned();
646 archive_path
.push(&item
.filename
);
648 match archive_type(&item
.filename
)?
{
649 ArchiveType
::DynamicIndex
=> {
650 let index
= DynamicIndexReader
::open(&archive_path
)?
;
651 let (csum
, size
) = index
.compute_csum();
652 manifest
.verify_file(&item
.filename
, &csum
, size
)?
;
654 ArchiveType
::FixedIndex
=> {
655 let index
= FixedIndexReader
::open(&archive_path
)?
;
656 let (csum
, size
) = index
.compute_csum();
657 manifest
.verify_file(&item
.filename
, &csum
, size
)?
;
659 ArchiveType
::Blob
=> {
660 let mut tmpfile
= std
::fs
::File
::open(&archive_path
)?
;
661 let (csum
, size
) = compute_file_csum(&mut tmpfile
)?
;
662 manifest
.verify_file(&item
.filename
, &csum
, size
)?
;
668 let mut manifest_path
= snapshot_path
.to_owned();
669 manifest_path
.push(MANIFEST_BLOB_NAME
);
670 let mut tmp_manifest_path
= manifest_path
.clone();
671 tmp_manifest_path
.set_extension("tmp");
673 if let Err(err
) = std
::fs
::rename(&tmp_manifest_path
, &manifest_path
) {
674 bail
!("Atomic rename manifest {:?} failed - {}", manifest_path
, err
);
680 /// Try to restore media catalogs (form catalog_archives)
681 pub fn fast_catalog_restore(
683 drive
: &mut Box
<dyn TapeDriver
>,
684 media_set
: &MediaSet
,
685 uuid
: &Uuid
, // current media Uuid
686 ) -> Result
<bool
, Error
> {
688 let status_path
= Path
::new(TAPE_STATUS_DIR
);
690 let current_file_number
= drive
.current_file_number()?
;
691 if current_file_number
!= 2 {
692 bail
!("fast_catalog_restore: wrong media position - internal error");
695 let mut found_catalog
= false;
697 let mut moved_to_eom
= false;
700 let current_file_number
= drive
.current_file_number()?
;
702 { // limit reader scope
703 let mut reader
= match drive
.read_next_file()?
{
705 task_log
!(worker
, "detected EOT after {} files", current_file_number
);
708 Some(reader
) => reader
,
711 let header
: MediaContentHeader
= unsafe { reader.read_le_value()? }
;
712 if header
.magic
!= PROXMOX_BACKUP_CONTENT_HEADER_MAGIC_1_0
{
713 bail
!("missing MediaContentHeader");
716 if header
.content_magic
== PROXMOX_BACKUP_CATALOG_ARCHIVE_MAGIC_1_0
{
717 task_log
!(worker
, "found catalog at pos {}", current_file_number
);
719 let header_data
= reader
.read_exact_allocated(header
.size
as usize)?
;
721 let archive_header
: CatalogArchiveHeader
= serde_json
::from_slice(&header_data
)
722 .map_err(|err
| format_err
!("unable to parse catalog archive header - {}", err
))?
;
724 if &archive_header
.media_set_uuid
!= media_set
.uuid() {
725 task_log
!(worker
, "skipping unrelated catalog at pos {}", current_file_number
);
726 reader
.skip_to_end()?
; // read all data
730 let catalog_uuid
= &archive_header
.uuid
;
732 let wanted
= media_set
738 Some(uuid
) => uuid
== catalog_uuid
,
744 task_log
!(worker
, "skip catalog because media '{}' not inventarized", catalog_uuid
);
745 reader
.skip_to_end()?
; // read all data
749 if catalog_uuid
== uuid
{
750 // always restore and overwrite catalog
752 // only restore if catalog does not exist
753 if MediaCatalog
::exists(status_path
, catalog_uuid
) {
754 task_log
!(worker
, "catalog for media '{}' already exists", catalog_uuid
);
755 reader
.skip_to_end()?
; // read all data
760 let mut file
= MediaCatalog
::create_temporary_database_file(status_path
, catalog_uuid
)?
;
762 std
::io
::copy(&mut reader
, &mut file
)?
;
764 file
.seek(SeekFrom
::Start(0))?
;
766 match MediaCatalog
::parse_catalog_header(&mut file
)?
{
767 (true, Some(media_uuid
), Some(media_set_uuid
)) => {
768 if &media_uuid
!= catalog_uuid
{
769 task_log
!(worker
, "catalog uuid missmatch at pos {}", current_file_number
);
772 if media_set_uuid
!= archive_header
.media_set_uuid
{
773 task_log
!(worker
, "catalog media_set missmatch at pos {}", current_file_number
);
777 MediaCatalog
::finish_temporary_database(status_path
, &media_uuid
, true)?
;
779 if catalog_uuid
== uuid
{
780 task_log
!(worker
, "successfully restored catalog");
783 task_log
!(worker
, "successfully restored related catalog {}", media_uuid
);
787 task_warn
!(worker
, "got incomplete catalog header - skip file");
797 break; // already done - stop
801 task_log
!(worker
, "searching for catalog at EOT (moving to EOT)");
802 drive
.move_to_last_file()?
;
804 let new_file_number
= drive
.current_file_number()?
;
806 if new_file_number
< (current_file_number
+ 1) {
807 break; // no new content - stop