]> git.proxmox.com Git - proxmox-backup.git/blob - src/api2/tape/drive.rs
proxmox-tape: api: restore_key-code moved to tape-encryption-keys
[proxmox-backup.git] / src / api2 / tape / drive.rs
1 use std::collections::HashMap;
2 use std::panic::UnwindSafe;
3 use std::path::Path;
4 use std::sync::Arc;
5
6 use anyhow::{bail, format_err, Error};
7 use serde_json::Value;
8
9 use proxmox_router::{
10 list_subdirs_api_method, Permission, Router, RpcEnvironment, RpcEnvironmentType, SubdirMap,
11 };
12 use proxmox_schema::api;
13 use proxmox_section_config::SectionConfigData;
14 use proxmox_sys::sortable;
15 use proxmox_sys::{task_log, task_warn};
16 use proxmox_uuid::Uuid;
17
18 use pbs_api_types::{
19 Authid, DriveListEntry, LabelUuidMap, Lp17VolumeStatistics, LtoDriveAndMediaStatus,
20 LtoTapeDrive, MamAttribute, MediaIdFlat, CHANGER_NAME_SCHEMA, DRIVE_NAME_SCHEMA,
21 MEDIA_LABEL_SCHEMA, MEDIA_POOL_NAME_SCHEMA, UPID_SCHEMA,
22 };
23
24 use pbs_api_types::{PRIV_TAPE_AUDIT, PRIV_TAPE_READ, PRIV_TAPE_WRITE};
25
26 use pbs_config::tape_encryption_keys::insert_key;
27 use pbs_config::CachedUserInfo;
28 use pbs_tape::{
29 linux_list_drives::{lookup_device_identification, lto_tape_device_list, open_lto_tape_device},
30 sg_tape::tape_alert_flags_critical,
31 BlockReadError,
32 };
33 use proxmox_rest_server::WorkerTask;
34
35 use crate::{
36 api2::tape::restore::{fast_catalog_restore, restore_media},
37 tape::{
38 changer::update_changer_online_status,
39 drive::{
40 get_tape_device_state, lock_tape_device, media_changer, open_drive,
41 open_lto_tape_drive, required_media_changer, set_tape_device_state, LtoTapeHandle,
42 TapeDriver,
43 },
44 file_formats::{MediaLabel, MediaSetLabel},
45 lock_media_pool, lock_media_set, lock_unassigned_media_pool, Inventory, MediaCatalog,
46 MediaId, TAPE_STATUS_DIR,
47 },
48 };
49
50 fn run_drive_worker<F>(
51 rpcenv: &dyn RpcEnvironment,
52 drive: String,
53 worker_type: &str,
54 job_id: Option<String>,
55 f: F,
56 ) -> Result<String, Error>
57 where
58 F: Send
59 + UnwindSafe
60 + 'static
61 + FnOnce(Arc<WorkerTask>, SectionConfigData) -> Result<(), Error>,
62 {
63 // early check/lock before starting worker
64 let (config, _digest) = pbs_config::drive::config()?;
65 let lock_guard = lock_tape_device(&config, &drive)?;
66
67 let auth_id = rpcenv.get_auth_id().unwrap();
68 let to_stdout = rpcenv.env_type() == RpcEnvironmentType::CLI;
69
70 WorkerTask::new_thread(worker_type, job_id, auth_id, to_stdout, move |worker| {
71 let _lock_guard = lock_guard;
72 set_tape_device_state(&drive, &worker.upid().to_string())
73 .map_err(|err| format_err!("could not set tape device state: {}", err))?;
74
75 let result = f(worker, config);
76 set_tape_device_state(&drive, "")
77 .map_err(|err| format_err!("could not unset tape device state: {}", err))?;
78 result
79 })
80 }
81
82 async fn run_drive_blocking_task<F, R>(drive: String, state: String, f: F) -> Result<R, Error>
83 where
84 F: Send + 'static + FnOnce(SectionConfigData) -> Result<R, Error>,
85 R: Send + 'static,
86 {
87 // early check/lock before starting worker
88 let (config, _digest) = pbs_config::drive::config()?;
89 let lock_guard = lock_tape_device(&config, &drive)?;
90 tokio::task::spawn_blocking(move || {
91 let _lock_guard = lock_guard;
92 set_tape_device_state(&drive, &state)
93 .map_err(|err| format_err!("could not set tape device state: {}", err))?;
94 let result = f(config);
95 set_tape_device_state(&drive, "")
96 .map_err(|err| format_err!("could not unset tape device state: {}", err))?;
97 result
98 })
99 .await?
100 }
101
102 #[api(
103 input: {
104 properties: {
105 drive: {
106 schema: DRIVE_NAME_SCHEMA,
107 },
108 "label-text": {
109 schema: MEDIA_LABEL_SCHEMA,
110 },
111 },
112 },
113 returns: {
114 schema: UPID_SCHEMA,
115 },
116 access: {
117 permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_READ, false),
118 },
119 )]
120 /// Load media with specified label
121 ///
122 /// Issue a media load request to the associated changer device.
123 pub fn load_media(
124 drive: String,
125 label_text: String,
126 rpcenv: &mut dyn RpcEnvironment,
127 ) -> Result<Value, Error> {
128 let job_id = format!("{}:{}", drive, label_text);
129
130 let upid_str = run_drive_worker(
131 rpcenv,
132 drive.clone(),
133 "load-media",
134 Some(job_id),
135 move |worker, config| {
136 task_log!(
137 worker,
138 "loading media '{}' into drive '{}'",
139 label_text,
140 drive
141 );
142 let (mut changer, _) = required_media_changer(&config, &drive)?;
143 changer.load_media(&label_text)?;
144 Ok(())
145 },
146 )?;
147
148 Ok(upid_str.into())
149 }
150
151 #[api(
152 input: {
153 properties: {
154 drive: {
155 schema: DRIVE_NAME_SCHEMA,
156 },
157 "source-slot": {
158 description: "Source slot number.",
159 minimum: 1,
160 },
161 },
162 },
163 access: {
164 permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_READ, false),
165 },
166 )]
167 /// Load media from the specified slot
168 ///
169 /// Issue a media load request to the associated changer device.
170 pub async fn load_slot(drive: String, source_slot: u64) -> Result<(), Error> {
171 run_drive_blocking_task(
172 drive.clone(),
173 format!("load from slot {}", source_slot),
174 move |config| {
175 let (mut changer, _) = required_media_changer(&config, &drive)?;
176 changer.load_media_from_slot(source_slot)?;
177 Ok(())
178 },
179 )
180 .await
181 }
182
183 #[api(
184 input: {
185 properties: {
186 drive: {
187 schema: DRIVE_NAME_SCHEMA,
188 },
189 "label-text": {
190 schema: MEDIA_LABEL_SCHEMA,
191 },
192 },
193 },
194 returns: {
195 description: "The import-export slot number the media was transferred to.",
196 type: u64,
197 minimum: 1,
198 },
199 access: {
200 permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_READ, false),
201 },
202 )]
203 /// Export media with specified label
204 pub async fn export_media(drive: String, label_text: String) -> Result<u64, Error> {
205 run_drive_blocking_task(
206 drive.clone(),
207 format!("export media {}", label_text),
208 move |config| {
209 let (mut changer, changer_name) = required_media_changer(&config, &drive)?;
210 match changer.export_media(&label_text)? {
211 Some(slot) => Ok(slot),
212 None => bail!(
213 "media '{}' is not online (via changer '{}')",
214 label_text,
215 changer_name
216 ),
217 }
218 },
219 )
220 .await
221 }
222
223 #[api(
224 input: {
225 properties: {
226 drive: {
227 schema: DRIVE_NAME_SCHEMA,
228 },
229 "target-slot": {
230 description: "Target slot number. If omitted, defaults to the slot that the drive was loaded from.",
231 minimum: 1,
232 optional: true,
233 },
234 },
235 },
236 returns: {
237 schema: UPID_SCHEMA,
238 },
239 access: {
240 permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_READ, false),
241 },
242 )]
243 /// Unload media via changer
244 pub fn unload(
245 drive: String,
246 target_slot: Option<u64>,
247 rpcenv: &mut dyn RpcEnvironment,
248 ) -> Result<Value, Error> {
249 let upid_str = run_drive_worker(
250 rpcenv,
251 drive.clone(),
252 "unload-media",
253 Some(drive.clone()),
254 move |worker, config| {
255 task_log!(worker, "unloading media from drive '{}'", drive);
256
257 let (mut changer, _) = required_media_changer(&config, &drive)?;
258 changer.unload_media(target_slot)?;
259 Ok(())
260 },
261 )?;
262
263 Ok(upid_str.into())
264 }
265
266 #[api(
267 input: {
268 properties: {
269 drive: {
270 schema: DRIVE_NAME_SCHEMA,
271 },
272 fast: {
273 description: "Use fast erase.",
274 type: bool,
275 optional: true,
276 default: true,
277 },
278 "label-text": {
279 schema: MEDIA_LABEL_SCHEMA,
280 optional: true,
281 },
282 },
283 },
284 returns: {
285 schema: UPID_SCHEMA,
286 },
287 access: {
288 permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_WRITE, false),
289 },
290 )]
291 /// Format media. Check for label-text if given (cancels if wrong media).
292 pub fn format_media(
293 drive: String,
294 fast: Option<bool>,
295 label_text: Option<String>,
296 rpcenv: &mut dyn RpcEnvironment,
297 ) -> Result<Value, Error> {
298 let upid_str = run_drive_worker(
299 rpcenv,
300 drive.clone(),
301 "format-media",
302 Some(drive.clone()),
303 move |worker, config| {
304 if let Some(ref label) = label_text {
305 task_log!(worker, "try to load media '{}'", label);
306 if let Some((mut changer, _)) = media_changer(&config, &drive)? {
307 changer.load_media(label)?;
308 }
309 }
310
311 let mut handle = open_drive(&config, &drive)?;
312
313 match handle.read_label() {
314 Err(err) => {
315 if let Some(label) = label_text {
316 bail!("expected label '{}', found unrelated data", label);
317 }
318 /* assume drive contains no or unrelated data */
319 task_log!(worker, "unable to read media label: {}", err);
320 task_log!(worker, "format anyways");
321 handle.format_media(fast.unwrap_or(true))?;
322 }
323 Ok((None, _)) => {
324 if let Some(label) = label_text {
325 bail!("expected label '{}', found empty tape", label);
326 }
327 task_log!(worker, "found empty media - format anyways");
328 handle.format_media(fast.unwrap_or(true))?;
329 }
330 Ok((Some(media_id), _key_config)) => {
331 if let Some(label_text) = label_text {
332 if media_id.label.label_text != label_text {
333 bail!(
334 "expected label '{}', found '{}', aborting",
335 label_text,
336 media_id.label.label_text
337 );
338 }
339 }
340
341 task_log!(
342 worker,
343 "found media '{}' with uuid '{}'",
344 media_id.label.label_text,
345 media_id.label.uuid,
346 );
347
348 let status_path = Path::new(TAPE_STATUS_DIR);
349 let mut inventory = Inventory::new(status_path);
350
351 if let Some(MediaSetLabel {
352 ref pool, ref uuid, ..
353 }) = media_id.media_set_label
354 {
355 let _pool_lock = lock_media_pool(status_path, pool)?;
356 let _media_set_lock = lock_media_set(status_path, uuid, None)?;
357 MediaCatalog::destroy(status_path, &media_id.label.uuid)?;
358 inventory.remove_media(&media_id.label.uuid)?;
359 } else {
360 let _lock = lock_unassigned_media_pool(status_path)?;
361 MediaCatalog::destroy(status_path, &media_id.label.uuid)?;
362 inventory.remove_media(&media_id.label.uuid)?;
363 };
364
365 handle.format_media(fast.unwrap_or(true))?;
366 }
367 }
368
369 Ok(())
370 },
371 )?;
372
373 Ok(upid_str.into())
374 }
375
376 #[api(
377 input: {
378 properties: {
379 drive: {
380 schema: DRIVE_NAME_SCHEMA,
381 },
382 },
383 },
384 returns: {
385 schema: UPID_SCHEMA,
386 },
387 access: {
388 permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_READ, false),
389 },
390 )]
391 /// Rewind tape
392 pub fn rewind(drive: String, rpcenv: &mut dyn RpcEnvironment) -> Result<Value, Error> {
393 let upid_str = run_drive_worker(
394 rpcenv,
395 drive.clone(),
396 "rewind-media",
397 Some(drive.clone()),
398 move |_worker, config| {
399 let mut drive = open_drive(&config, &drive)?;
400 drive.rewind()?;
401 Ok(())
402 },
403 )?;
404
405 Ok(upid_str.into())
406 }
407
408 #[api(
409 input: {
410 properties: {
411 drive: {
412 schema: DRIVE_NAME_SCHEMA,
413 },
414 },
415 },
416 returns: {
417 schema: UPID_SCHEMA,
418 },
419 access: {
420 permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_READ, false),
421 },
422 )]
423 /// Eject/Unload drive media
424 pub fn eject_media(drive: String, rpcenv: &mut dyn RpcEnvironment) -> Result<Value, Error> {
425 let upid_str = run_drive_worker(
426 rpcenv,
427 drive.clone(),
428 "eject-media",
429 Some(drive.clone()),
430 move |_worker, config| {
431 if let Some((mut changer, _)) = media_changer(&config, &drive)? {
432 changer.unload_media(None)?;
433 } else {
434 let mut drive = open_drive(&config, &drive)?;
435 drive.eject_media()?;
436 }
437 Ok(())
438 },
439 )?;
440
441 Ok(upid_str.into())
442 }
443
444 #[api(
445 input: {
446 properties: {
447 drive: {
448 schema: DRIVE_NAME_SCHEMA,
449 },
450 "label-text": {
451 schema: MEDIA_LABEL_SCHEMA,
452 },
453 pool: {
454 schema: MEDIA_POOL_NAME_SCHEMA,
455 optional: true,
456 },
457 },
458 },
459 returns: {
460 schema: UPID_SCHEMA,
461 },
462 access: {
463 permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_WRITE, false),
464 },
465 )]
466 /// Label media
467 ///
468 /// Write a new media label to the media in 'drive'. The media is
469 /// assigned to the specified 'pool', or else to the free media pool.
470 ///
471 /// Note: The media need to be empty (you may want to format it first).
472 pub fn label_media(
473 drive: String,
474 pool: Option<String>,
475 label_text: String,
476 rpcenv: &mut dyn RpcEnvironment,
477 ) -> Result<Value, Error> {
478 if let Some(ref pool) = pool {
479 let (pool_config, _digest) = pbs_config::media_pool::config()?;
480
481 if pool_config.sections.get(pool).is_none() {
482 bail!("no such pool ('{}')", pool);
483 }
484 }
485 let upid_str = run_drive_worker(
486 rpcenv,
487 drive.clone(),
488 "label-media",
489 Some(drive.clone()),
490 move |worker, config| {
491 let mut drive = open_drive(&config, &drive)?;
492
493 drive.rewind()?;
494
495 match drive.read_next_file() {
496 Ok(_reader) => bail!("media is not empty (format it first)"),
497 Err(BlockReadError::EndOfFile) => { /* EOF mark at BOT, assume tape is empty */ }
498 Err(BlockReadError::EndOfStream) => { /* tape is empty */ }
499 Err(err) => {
500 bail!("media read error - {}", err);
501 }
502 }
503
504 let ctime = proxmox_time::epoch_i64();
505 let label = MediaLabel {
506 label_text: label_text.to_string(),
507 uuid: Uuid::generate(),
508 ctime,
509 };
510
511 write_media_label(worker, &mut drive, label, pool)
512 },
513 )?;
514
515 Ok(upid_str.into())
516 }
517
518 fn write_media_label(
519 worker: Arc<WorkerTask>,
520 drive: &mut Box<dyn TapeDriver>,
521 label: MediaLabel,
522 pool: Option<String>,
523 ) -> Result<(), Error> {
524 drive.label_tape(&label)?;
525
526 let status_path = Path::new(TAPE_STATUS_DIR);
527
528 let media_id = if let Some(ref pool) = pool {
529 // assign media to pool by writing special media set label
530 task_log!(
531 worker,
532 "Label media '{}' for pool '{}'",
533 label.label_text,
534 pool
535 );
536 let set = MediaSetLabel::with_data(pool, [0u8; 16].into(), 0, label.ctime, None);
537
538 drive.write_media_set_label(&set, None)?;
539
540 let media_id = MediaId {
541 label,
542 media_set_label: Some(set),
543 };
544
545 // Create the media catalog
546 MediaCatalog::overwrite(status_path, &media_id, false)?;
547
548 let mut inventory = Inventory::new(status_path);
549 inventory.store(media_id.clone(), false)?;
550
551 media_id
552 } else {
553 task_log!(
554 worker,
555 "Label media '{}' (no pool assignment)",
556 label.label_text
557 );
558
559 let media_id = MediaId {
560 label,
561 media_set_label: None,
562 };
563
564 // Create the media catalog
565 MediaCatalog::overwrite(status_path, &media_id, false)?;
566
567 let mut inventory = Inventory::new(status_path);
568 inventory.store(media_id.clone(), false)?;
569
570 media_id
571 };
572
573 drive.rewind()?;
574
575 match drive.read_label() {
576 Ok((Some(info), _)) => {
577 if info.label.uuid != media_id.label.uuid {
578 bail!("verify label failed - got wrong label uuid");
579 }
580 if let Some(ref pool) = pool {
581 match info.media_set_label {
582 Some(set) => {
583 if set.uuid != [0u8; 16].into() {
584 bail!("verify media set label failed - got wrong set uuid");
585 }
586 if &set.pool != pool {
587 bail!("verify media set label failed - got wrong pool");
588 }
589 }
590 None => {
591 bail!("verify media set label failed (missing set label)");
592 }
593 }
594 }
595 }
596 Ok((None, _)) => bail!("verify label failed (got empty media)"),
597 Err(err) => bail!("verify label failed - {}", err),
598 };
599
600 drive.rewind()?;
601
602 Ok(())
603 }
604
605 #[api(
606 protected: true,
607 input: {
608 properties: {
609 drive: {
610 schema: DRIVE_NAME_SCHEMA,
611 //description: "Restore the key from this drive the (encrypted) key was saved on.",
612 },
613 password: {
614 description: "The password the key was encrypted with.",
615 },
616 },
617 },
618 access: {
619 permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_READ, false),
620 },
621 )]
622 /// Try to restore a tape encryption key
623 pub async fn restore_key(
624 drive: String,
625 password: String,
626 ) -> Result<(), Error> {
627
628 run_drive_blocking_task(drive.clone(), "restore key".to_string(), move |config| {
629 let mut drive = open_drive(&config, &drive)?;
630
631 let (_media_id, key_config) = drive.read_label()?;
632
633 if let Some(key_config) = key_config {
634 let password_fn = || Ok(password.as_bytes().to_vec());
635 let (key, ..) = key_config.decrypt(&password_fn)?;
636 insert_key(key, key_config, true)?;
637 } else {
638 bail!("media does not contain any encryption key configuration");
639 }
640
641 Ok(())
642 })
643 .await?;
644
645 Ok(())
646 }
647
648 #[api(
649 input: {
650 properties: {
651 drive: {
652 schema: DRIVE_NAME_SCHEMA,
653 },
654 inventorize: {
655 description: "Inventorize media",
656 optional: true,
657 },
658 },
659 },
660 returns: {
661 type: MediaIdFlat,
662 },
663 access: {
664 permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_READ, false),
665 },
666 )]
667 /// Read media label (optionally inventorize media)
668 pub async fn read_label(drive: String, inventorize: Option<bool>) -> Result<MediaIdFlat, Error> {
669 run_drive_blocking_task(drive.clone(), "reading label".to_string(), move |config| {
670 let mut drive = open_drive(&config, &drive)?;
671
672 let (media_id, _key_config) = drive.read_label()?;
673 let media_id = media_id.ok_or(format_err!("Media is empty (no label)."))?;
674
675 let label = if let Some(ref set) = media_id.media_set_label {
676 let key = &set.encryption_key_fingerprint;
677
678 if let Err(err) = drive.set_encryption(key.clone().map(|fp| (fp, set.uuid.clone()))) {
679 eprintln!("unable to load encryption key: {}", err); // best-effort only
680 }
681 MediaIdFlat {
682 ctime: media_id.label.ctime,
683 encryption_key_fingerprint: key.as_ref().map(|fp| fp.signature()),
684 label_text: media_id.label.label_text.clone(),
685 media_set_ctime: Some(set.ctime),
686 media_set_uuid: Some(set.uuid.clone()),
687 pool: Some(set.pool.clone()),
688 seq_nr: Some(set.seq_nr),
689 uuid: media_id.label.uuid.clone(),
690 }
691 } else {
692 MediaIdFlat {
693 ctime: media_id.label.ctime,
694 encryption_key_fingerprint: None,
695 label_text: media_id.label.label_text.clone(),
696 media_set_ctime: None,
697 media_set_uuid: None,
698 pool: None,
699 seq_nr: None,
700 uuid: media_id.label.uuid.clone(),
701 }
702 };
703
704 if let Some(true) = inventorize {
705 let state_path = Path::new(TAPE_STATUS_DIR);
706 let mut inventory = Inventory::new(state_path);
707
708 if let Some(MediaSetLabel {
709 ref pool, ref uuid, ..
710 }) = media_id.media_set_label
711 {
712 let _pool_lock = lock_media_pool(state_path, pool)?;
713 let _lock = lock_media_set(state_path, uuid, None)?;
714 MediaCatalog::destroy_unrelated_catalog(state_path, &media_id)?;
715 inventory.store(media_id, false)?;
716 } else {
717 let _lock = lock_unassigned_media_pool(state_path)?;
718 MediaCatalog::destroy(state_path, &media_id.label.uuid)?;
719 inventory.store(media_id, false)?;
720 };
721 }
722
723 Ok(label)
724 })
725 .await
726 }
727
728 #[api(
729 input: {
730 properties: {
731 drive: {
732 schema: DRIVE_NAME_SCHEMA,
733 },
734 },
735 },
736 returns: {
737 schema: UPID_SCHEMA,
738 },
739 access: {
740 permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_READ, false),
741 },
742 )]
743 /// Clean drive
744 pub fn clean_drive(drive: String, rpcenv: &mut dyn RpcEnvironment) -> Result<Value, Error> {
745 let upid_str = run_drive_worker(
746 rpcenv,
747 drive.clone(),
748 "clean-drive",
749 Some(drive.clone()),
750 move |worker, config| {
751 let (mut changer, _changer_name) = required_media_changer(&config, &drive)?;
752
753 task_log!(worker, "Starting drive clean");
754
755 changer.clean_drive()?;
756
757 if let Ok(drive_config) = config.lookup::<LtoTapeDrive>("lto", &drive) {
758 // Note: clean_drive unloads the cleaning media, so we cannot use drive_config.open
759 let mut handle = LtoTapeHandle::new(open_lto_tape_device(&drive_config.path)?)?;
760
761 // test for critical tape alert flags
762 if let Ok(alert_flags) = handle.tape_alert_flags() {
763 if !alert_flags.is_empty() {
764 task_log!(worker, "TapeAlertFlags: {:?}", alert_flags);
765 if tape_alert_flags_critical(alert_flags) {
766 bail!("found critical tape alert flags: {:?}", alert_flags);
767 }
768 }
769 }
770
771 // test wearout (max. 50 mounts)
772 if let Ok(volume_stats) = handle.volume_statistics() {
773 task_log!(worker, "Volume mounts: {}", volume_stats.volume_mounts);
774 let wearout = volume_stats.volume_mounts * 2; // (*100.0/50.0);
775 task_log!(worker, "Cleaning tape wearout: {}%", wearout);
776 }
777 }
778
779 task_log!(worker, "Drive cleaned successfully");
780
781 Ok(())
782 },
783 )?;
784
785 Ok(upid_str.into())
786 }
787
788 #[api(
789 input: {
790 properties: {
791 drive: {
792 schema: DRIVE_NAME_SCHEMA,
793 },
794 },
795 },
796 returns: {
797 description: "The list of media labels with associated media Uuid (if any).",
798 type: Array,
799 items: {
800 type: LabelUuidMap,
801 },
802 },
803 access: {
804 permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_READ, false),
805 },
806 )]
807 /// List known media labels (Changer Inventory)
808 ///
809 /// Note: Only useful for drives with associated changer device.
810 ///
811 /// This method queries the changer to get a list of media labels.
812 ///
813 /// Note: This updates the media online status.
814 pub async fn inventory(drive: String) -> Result<Vec<LabelUuidMap>, Error> {
815 run_drive_blocking_task(drive.clone(), "inventorize".to_string(), move |config| {
816 let (mut changer, changer_name) = required_media_changer(&config, &drive)?;
817
818 let label_text_list = changer.online_media_label_texts()?;
819
820 let state_path = Path::new(TAPE_STATUS_DIR);
821
822 let mut inventory = Inventory::load(state_path)?;
823
824 update_changer_online_status(&config, &mut inventory, &changer_name, &label_text_list)?;
825
826 let mut list = Vec::new();
827
828 for label_text in label_text_list.iter() {
829 if label_text.starts_with("CLN") {
830 // skip cleaning unit
831 continue;
832 }
833
834 let label_text = label_text.to_string();
835
836 if let Some(media_id) = inventory.find_media_by_label_text(&label_text) {
837 list.push(LabelUuidMap {
838 label_text,
839 uuid: Some(media_id.label.uuid.clone()),
840 });
841 } else {
842 list.push(LabelUuidMap {
843 label_text,
844 uuid: None,
845 });
846 }
847 }
848
849 Ok(list)
850 })
851 .await
852 }
853
854 #[api(
855 input: {
856 properties: {
857 drive: {
858 schema: DRIVE_NAME_SCHEMA,
859 },
860 "read-all-labels": {
861 description: "Load all tapes and try read labels (even if already inventoried)",
862 type: bool,
863 optional: true,
864 },
865 },
866 },
867 returns: {
868 schema: UPID_SCHEMA,
869 },
870 access: {
871 permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_READ, false),
872 },
873 )]
874 /// Update inventory
875 ///
876 /// Note: Only useful for drives with associated changer device.
877 ///
878 /// This method queries the changer to get a list of media labels. It
879 /// then loads any unknown media into the drive, reads the label, and
880 /// store the result to the media database.
881 ///
882 /// Note: This updates the media online status.
883 pub fn update_inventory(
884 drive: String,
885 read_all_labels: Option<bool>,
886 rpcenv: &mut dyn RpcEnvironment,
887 ) -> Result<Value, Error> {
888 let upid_str = run_drive_worker(
889 rpcenv,
890 drive.clone(),
891 "inventory-update",
892 Some(drive.clone()),
893 move |worker, config| {
894 let (mut changer, changer_name) = required_media_changer(&config, &drive)?;
895
896 let label_text_list = changer.online_media_label_texts()?;
897 if label_text_list.is_empty() {
898 task_log!(worker, "changer device does not list any media labels");
899 }
900
901 let state_path = Path::new(TAPE_STATUS_DIR);
902
903 let mut inventory = Inventory::load(state_path)?;
904
905 update_changer_online_status(&config, &mut inventory, &changer_name, &label_text_list)?;
906
907 for label_text in label_text_list.iter() {
908 if label_text.starts_with("CLN") {
909 task_log!(worker, "skip cleaning unit '{}'", label_text);
910 continue;
911 }
912
913 let label_text = label_text.to_string();
914
915 if !read_all_labels.unwrap_or(false)
916 && inventory.find_media_by_label_text(&label_text).is_some()
917 {
918 task_log!(worker, "media '{}' already inventoried", label_text);
919 continue;
920 }
921
922 if let Err(err) = changer.load_media(&label_text) {
923 task_warn!(worker, "unable to load media '{}' - {}", label_text, err);
924 continue;
925 }
926
927 let mut drive = open_drive(&config, &drive)?;
928 match drive.read_label() {
929 Err(err) => {
930 task_warn!(
931 worker,
932 "unable to read label form media '{}' - {}",
933 label_text,
934 err
935 );
936 }
937 Ok((None, _)) => {
938 task_log!(worker, "media '{}' is empty", label_text);
939 }
940 Ok((Some(media_id), _key_config)) => {
941 if label_text != media_id.label.label_text {
942 task_warn!(
943 worker,
944 "label text mismatch ({} != {})",
945 label_text,
946 media_id.label.label_text
947 );
948 continue;
949 }
950 task_log!(
951 worker,
952 "inventorize media '{}' with uuid '{}'",
953 label_text,
954 media_id.label.uuid
955 );
956
957 if let Some(MediaSetLabel {
958 ref pool, ref uuid, ..
959 }) = media_id.media_set_label
960 {
961 let _pool_lock = lock_media_pool(state_path, pool)?;
962 let _lock = lock_media_set(state_path, uuid, None)?;
963 MediaCatalog::destroy_unrelated_catalog(state_path, &media_id)?;
964 inventory.store(media_id, false)?;
965 } else {
966 let _lock = lock_unassigned_media_pool(state_path)?;
967 MediaCatalog::destroy(state_path, &media_id.label.uuid)?;
968 inventory.store(media_id, false)?;
969 };
970 }
971 }
972 changer.unload_media(None)?;
973 }
974 Ok(())
975 },
976 )?;
977
978 Ok(upid_str.into())
979 }
980
981 #[api(
982 input: {
983 properties: {
984 drive: {
985 schema: DRIVE_NAME_SCHEMA,
986 },
987 pool: {
988 schema: MEDIA_POOL_NAME_SCHEMA,
989 optional: true,
990 },
991 },
992 },
993 returns: {
994 schema: UPID_SCHEMA,
995 },
996 access: {
997 permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_WRITE, false),
998 },
999 )]
1000 /// Label media with barcodes from changer device
1001 pub fn barcode_label_media(
1002 drive: String,
1003 pool: Option<String>,
1004 rpcenv: &mut dyn RpcEnvironment,
1005 ) -> Result<Value, Error> {
1006 if let Some(ref pool) = pool {
1007 let (pool_config, _digest) = pbs_config::media_pool::config()?;
1008
1009 if pool_config.sections.get(pool).is_none() {
1010 bail!("no such pool ('{}')", pool);
1011 }
1012 }
1013
1014 let upid_str = run_drive_worker(
1015 rpcenv,
1016 drive.clone(),
1017 "barcode-label-media",
1018 Some(drive.clone()),
1019 move |worker, config| barcode_label_media_worker(worker, drive, &config, pool),
1020 )?;
1021
1022 Ok(upid_str.into())
1023 }
1024
1025 fn barcode_label_media_worker(
1026 worker: Arc<WorkerTask>,
1027 drive: String,
1028 drive_config: &SectionConfigData,
1029 pool: Option<String>,
1030 ) -> Result<(), Error> {
1031 let (mut changer, changer_name) = required_media_changer(drive_config, &drive)?;
1032
1033 let mut label_text_list = changer.online_media_label_texts()?;
1034
1035 // make sure we label them in the right order
1036 label_text_list.sort();
1037
1038 let state_path = Path::new(TAPE_STATUS_DIR);
1039
1040 let mut inventory = Inventory::load(state_path)?;
1041
1042 update_changer_online_status(
1043 drive_config,
1044 &mut inventory,
1045 &changer_name,
1046 &label_text_list,
1047 )?;
1048
1049 if label_text_list.is_empty() {
1050 bail!("changer device does not list any media labels");
1051 }
1052
1053 for label_text in label_text_list {
1054 if label_text.starts_with("CLN") {
1055 continue;
1056 }
1057
1058 inventory.reload()?;
1059 if inventory.find_media_by_label_text(&label_text).is_some() {
1060 task_log!(
1061 worker,
1062 "media '{}' already inventoried (already labeled)",
1063 label_text
1064 );
1065 continue;
1066 }
1067
1068 task_log!(worker, "checking/loading media '{}'", label_text);
1069
1070 if let Err(err) = changer.load_media(&label_text) {
1071 task_warn!(worker, "unable to load media '{}' - {}", label_text, err);
1072 continue;
1073 }
1074
1075 let mut drive = open_drive(drive_config, &drive)?;
1076 drive.rewind()?;
1077
1078 match drive.read_next_file() {
1079 Ok(_reader) => {
1080 task_log!(
1081 worker,
1082 "media '{}' is not empty (format it first)",
1083 label_text
1084 );
1085 continue;
1086 }
1087 Err(BlockReadError::EndOfFile) => { /* EOF mark at BOT, assume tape is empty */ }
1088 Err(BlockReadError::EndOfStream) => { /* tape is empty */ }
1089 Err(_err) => {
1090 task_warn!(
1091 worker,
1092 "media '{}' read error (maybe not empty - format it first)",
1093 label_text
1094 );
1095 continue;
1096 }
1097 }
1098
1099 let ctime = proxmox_time::epoch_i64();
1100 let label = MediaLabel {
1101 label_text: label_text.to_string(),
1102 uuid: Uuid::generate(),
1103 ctime,
1104 };
1105
1106 write_media_label(worker.clone(), &mut drive, label, pool.clone())?
1107 }
1108
1109 Ok(())
1110 }
1111
1112 #[api(
1113 input: {
1114 properties: {
1115 drive: {
1116 schema: DRIVE_NAME_SCHEMA,
1117 },
1118 },
1119 },
1120 returns: {
1121 description: "A List of medium auxiliary memory attributes.",
1122 type: Array,
1123 items: {
1124 type: MamAttribute,
1125 },
1126 },
1127 access: {
1128 permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_AUDIT, false),
1129 },
1130 )]
1131 /// Read Cartridge Memory (Medium auxiliary memory attributes)
1132 pub async fn cartridge_memory(drive: String) -> Result<Vec<MamAttribute>, Error> {
1133 run_drive_blocking_task(
1134 drive.clone(),
1135 "reading cartridge memory".to_string(),
1136 move |config| {
1137 let drive_config: LtoTapeDrive = config.lookup("lto", &drive)?;
1138 let mut handle = open_lto_tape_drive(&drive_config)?;
1139
1140 handle.cartridge_memory()
1141 },
1142 )
1143 .await
1144 }
1145
1146 #[api(
1147 input: {
1148 properties: {
1149 drive: {
1150 schema: DRIVE_NAME_SCHEMA,
1151 },
1152 },
1153 },
1154 returns: {
1155 type: Lp17VolumeStatistics,
1156 },
1157 access: {
1158 permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_AUDIT, false),
1159 },
1160 )]
1161 /// Read Volume Statistics (SCSI log page 17h)
1162 pub async fn volume_statistics(drive: String) -> Result<Lp17VolumeStatistics, Error> {
1163 run_drive_blocking_task(
1164 drive.clone(),
1165 "reading volume statistics".to_string(),
1166 move |config| {
1167 let drive_config: LtoTapeDrive = config.lookup("lto", &drive)?;
1168 let mut handle = open_lto_tape_drive(&drive_config)?;
1169
1170 handle.volume_statistics()
1171 },
1172 )
1173 .await
1174 }
1175
1176 #[api(
1177 input: {
1178 properties: {
1179 drive: {
1180 schema: DRIVE_NAME_SCHEMA,
1181 },
1182 },
1183 },
1184 returns: {
1185 type: LtoDriveAndMediaStatus,
1186 },
1187 access: {
1188 permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_AUDIT, false),
1189 },
1190 )]
1191 /// Get drive/media status
1192 pub async fn status(drive: String) -> Result<LtoDriveAndMediaStatus, Error> {
1193 run_drive_blocking_task(
1194 drive.clone(),
1195 "reading drive status".to_string(),
1196 move |config| {
1197 let drive_config: LtoTapeDrive = config.lookup("lto", &drive)?;
1198
1199 // Note: use open_lto_tape_device, because this also works if no medium loaded
1200 let file = open_lto_tape_device(&drive_config.path)?;
1201
1202 let mut handle = LtoTapeHandle::new(file)?;
1203
1204 handle.get_drive_and_media_status()
1205 },
1206 )
1207 .await
1208 }
1209
1210 #[api(
1211 input: {
1212 properties: {
1213 drive: {
1214 schema: DRIVE_NAME_SCHEMA,
1215 },
1216 force: {
1217 description: "Force overriding existing index.",
1218 type: bool,
1219 optional: true,
1220 },
1221 scan: {
1222 description: "Re-read the whole tape to reconstruct the catalog instead of restoring saved versions.",
1223 type: bool,
1224 optional: true,
1225 },
1226 verbose: {
1227 description: "Verbose mode - log all found chunks.",
1228 type: bool,
1229 optional: true,
1230 },
1231 },
1232 },
1233 returns: {
1234 schema: UPID_SCHEMA,
1235 },
1236 access: {
1237 permission: &Permission::Privilege(&["tape", "device", "{drive}"], PRIV_TAPE_READ, false),
1238 },
1239 )]
1240 /// Scan media and record content
1241 pub fn catalog_media(
1242 drive: String,
1243 force: Option<bool>,
1244 scan: Option<bool>,
1245 verbose: Option<bool>,
1246 rpcenv: &mut dyn RpcEnvironment,
1247 ) -> Result<Value, Error> {
1248 let verbose = verbose.unwrap_or(false);
1249 let force = force.unwrap_or(false);
1250 let scan = scan.unwrap_or(false);
1251
1252 let upid_str = run_drive_worker(
1253 rpcenv,
1254 drive.clone(),
1255 "catalog-media",
1256 Some(drive.clone()),
1257 move |worker, config| {
1258 let mut drive = open_drive(&config, &drive)?;
1259
1260 drive.rewind()?;
1261
1262 let media_id = match drive.read_label()? {
1263 (Some(media_id), key_config) => {
1264 task_log!(
1265 worker,
1266 "found media label: {}",
1267 serde_json::to_string_pretty(&serde_json::to_value(&media_id)?)?
1268 );
1269 if key_config.is_some() {
1270 task_log!(
1271 worker,
1272 "encryption key config: {}",
1273 serde_json::to_string_pretty(&serde_json::to_value(&key_config)?)?
1274 );
1275 }
1276 media_id
1277 }
1278 (None, _) => bail!("media is empty (no media label found)"),
1279 };
1280
1281 let status_path = Path::new(TAPE_STATUS_DIR);
1282
1283 let mut inventory = Inventory::new(status_path);
1284
1285 let (_media_set_lock, media_set_uuid) = match media_id.media_set_label {
1286 None => {
1287 task_log!(worker, "media is empty");
1288 let _lock = lock_unassigned_media_pool(status_path)?;
1289 MediaCatalog::destroy(status_path, &media_id.label.uuid)?;
1290 inventory.store(media_id.clone(), false)?;
1291 return Ok(());
1292 }
1293 Some(ref set) => {
1294 if set.uuid.as_ref() == [0u8; 16] {
1295 // media is empty
1296 task_log!(worker, "media is empty");
1297 let _lock = lock_unassigned_media_pool(status_path)?;
1298 MediaCatalog::destroy(status_path, &media_id.label.uuid)?;
1299 inventory.store(media_id.clone(), false)?;
1300 return Ok(());
1301 }
1302 let encrypt_fingerprint = set
1303 .encryption_key_fingerprint
1304 .clone()
1305 .map(|fp| (fp, set.uuid.clone()));
1306
1307 drive.set_encryption(encrypt_fingerprint)?;
1308
1309 let _pool_lock = lock_media_pool(status_path, &set.pool)?;
1310 let media_set_lock = lock_media_set(status_path, &set.uuid, None)?;
1311
1312 MediaCatalog::destroy_unrelated_catalog(status_path, &media_id)?;
1313
1314 inventory.store(media_id.clone(), false)?;
1315
1316 (media_set_lock, &set.uuid)
1317 }
1318 };
1319
1320 if MediaCatalog::exists(status_path, &media_id.label.uuid) && !force {
1321 bail!("media catalog exists (please use --force to overwrite)");
1322 }
1323
1324 if !scan {
1325 let media_set = inventory.compute_media_set_members(media_set_uuid)?;
1326
1327 if fast_catalog_restore(&worker, &mut drive, &media_set, &media_id.label.uuid)? {
1328 return Ok(());
1329 }
1330
1331 task_log!(worker, "no catalog found");
1332 }
1333
1334 task_log!(worker, "scanning entire media to reconstruct catalog");
1335
1336 drive.rewind()?;
1337 drive.read_label()?; // skip over labels - we already read them above
1338
1339 let mut checked_chunks = HashMap::new();
1340 restore_media(
1341 worker,
1342 &mut drive,
1343 &media_id,
1344 None,
1345 &mut checked_chunks,
1346 verbose,
1347 )?;
1348
1349 Ok(())
1350 },
1351 )?;
1352
1353 Ok(upid_str.into())
1354 }
1355
1356 #[api(
1357 input: {
1358 properties: {
1359 changer: {
1360 schema: CHANGER_NAME_SCHEMA,
1361 optional: true,
1362 },
1363 },
1364 },
1365 returns: {
1366 description: "The list of configured drives with model information.",
1367 type: Array,
1368 items: {
1369 type: DriveListEntry,
1370 },
1371 },
1372 access: {
1373 description: "List configured tape drives filtered by Tape.Audit privileges",
1374 permission: &Permission::Anybody,
1375 },
1376 )]
1377 /// List drives
1378 pub fn list_drives(
1379 changer: Option<String>,
1380 _param: Value,
1381 rpcenv: &mut dyn RpcEnvironment,
1382 ) -> Result<Vec<DriveListEntry>, Error> {
1383 let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
1384 let user_info = CachedUserInfo::new()?;
1385
1386 let (config, _) = pbs_config::drive::config()?;
1387
1388 let lto_drives = lto_tape_device_list();
1389
1390 let drive_list: Vec<LtoTapeDrive> = config.convert_to_typed_array("lto")?;
1391
1392 let mut list = Vec::new();
1393
1394 for drive in drive_list {
1395 if changer.is_some() && drive.changer != changer {
1396 continue;
1397 }
1398
1399 let privs = user_info.lookup_privs(&auth_id, &["tape", "drive", &drive.name]);
1400 if (privs & PRIV_TAPE_AUDIT) == 0 {
1401 continue;
1402 }
1403
1404 let info = lookup_device_identification(&lto_drives, &drive.path);
1405 let state = get_tape_device_state(&config, &drive.name)?;
1406 let entry = DriveListEntry {
1407 config: drive,
1408 info,
1409 state,
1410 };
1411 list.push(entry);
1412 }
1413
1414 Ok(list)
1415 }
1416
1417 #[sortable]
1418 pub const SUBDIRS: SubdirMap = &sorted!([
1419 (
1420 "barcode-label-media",
1421 &Router::new().post(&API_METHOD_BARCODE_LABEL_MEDIA)
1422 ),
1423 ("catalog", &Router::new().post(&API_METHOD_CATALOG_MEDIA)),
1424 ("clean", &Router::new().put(&API_METHOD_CLEAN_DRIVE)),
1425 ("eject-media", &Router::new().post(&API_METHOD_EJECT_MEDIA)),
1426 (
1427 "format-media",
1428 &Router::new().post(&API_METHOD_FORMAT_MEDIA)
1429 ),
1430 ("export-media", &Router::new().put(&API_METHOD_EXPORT_MEDIA)),
1431 (
1432 "inventory",
1433 &Router::new()
1434 .get(&API_METHOD_INVENTORY)
1435 .put(&API_METHOD_UPDATE_INVENTORY)
1436 ),
1437 ("label-media", &Router::new().post(&API_METHOD_LABEL_MEDIA)),
1438 ("load-media", &Router::new().post(&API_METHOD_LOAD_MEDIA)),
1439 ("load-slot", &Router::new().post(&API_METHOD_LOAD_SLOT)),
1440 (
1441 "cartridge-memory",
1442 &Router::new().get(&API_METHOD_CARTRIDGE_MEMORY)
1443 ),
1444 (
1445 "volume-statistics",
1446 &Router::new().get(&API_METHOD_VOLUME_STATISTICS)
1447 ),
1448 ("read-label", &Router::new().get(&API_METHOD_READ_LABEL)),
1449 ("restore-key", &Router::new().post(&API_METHOD_RESTORE_KEY)),
1450 ("rewind", &Router::new().post(&API_METHOD_REWIND)),
1451 ("status", &Router::new().get(&API_METHOD_STATUS)),
1452 ("unload", &Router::new().post(&API_METHOD_UNLOAD)),
1453 ]);
1454
1455 const ITEM_ROUTER: Router = Router::new()
1456 .get(&list_subdirs_api_method!(SUBDIRS))
1457 .subdirs(SUBDIRS);
1458
1459 pub const ROUTER: Router = Router::new()
1460 .get(&API_METHOD_LIST_DRIVES)
1461 .match_all("drive", &ITEM_ROUTER);