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