]> git.proxmox.com Git - proxmox-backup.git/blob - src/api2/tape/drive.rs
cleanup worker task logging
[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, task_warn};
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 task_log!(worker, "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 task_log!(worker, "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 task_log!(worker, "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 task_log!(worker, "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 task_log!(worker, "Volume mounts: {}", volume_stats.volume_mounts);
795 let wearout = volume_stats.volume_mounts * 2; // (*100.0/50.0);
796 task_log!(worker, "Cleaning tape wearout: {}%", wearout);
797 }
798 }
799
800 task_log!(worker, "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 task_log!(worker, "changer device does not list any media labels");
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 task_log!(worker, "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 task_log!(worker, "media '{}' already inventoried", label_text);
943 continue;
944 }
945
946 if let Err(err) = changer.load_media(&label_text) {
947 task_warn!(worker, "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 task_warn!(worker, "unable to read label form media '{}' - {}", label_text, err);
955 }
956 Ok((None, _)) => {
957 task_log!(worker, "media '{}' is empty", label_text);
958 }
959 Ok((Some(media_id), _key_config)) => {
960 if label_text != media_id.label.label_text {
961 task_warn!(worker, "label text mismatch ({} != {})", label_text, media_id.label.label_text);
962 continue;
963 }
964 task_log!(worker, "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 task_log!(worker, "media '{}' already inventoried (already labeled)", label_text);
1061 continue;
1062 }
1063
1064 task_log!(worker, "checking/loading media '{}'", label_text);
1065
1066 if let Err(err) = changer.load_media(&label_text) {
1067 task_warn!(worker, "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 task_log!(worker, "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 task_warn!(worker, "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 task_log!(
1253 worker,
1254 "found media label: {}",
1255 serde_json::to_string_pretty(&serde_json::to_value(&media_id)?)?
1256 );
1257 if key_config.is_some() {
1258 task_log!(
1259 worker,
1260 "encryption key config: {}",
1261 serde_json::to_string_pretty(&serde_json::to_value(&key_config)?)?
1262 );
1263 }
1264 media_id
1265 },
1266 (None, _) => bail!("media is empty (no media label found)"),
1267 };
1268
1269 let status_path = Path::new(TAPE_STATUS_DIR);
1270
1271 let mut inventory = Inventory::new(status_path);
1272
1273 let (_media_set_lock, media_set_uuid) = match media_id.media_set_label {
1274 None => {
1275 task_log!(worker, "media is empty");
1276 let _lock = lock_unassigned_media_pool(status_path)?;
1277 MediaCatalog::destroy(status_path, &media_id.label.uuid)?;
1278 inventory.store(media_id.clone(), false)?;
1279 return Ok(());
1280 }
1281 Some(ref set) => {
1282 if set.uuid.as_ref() == [0u8;16] { // media is empty
1283 task_log!(worker, "media is empty");
1284 let _lock = lock_unassigned_media_pool(status_path)?;
1285 MediaCatalog::destroy(status_path, &media_id.label.uuid)?;
1286 inventory.store(media_id.clone(), false)?;
1287 return Ok(());
1288 }
1289 let encrypt_fingerprint = set.encryption_key_fingerprint.clone()
1290 .map(|fp| (fp, set.uuid.clone()));
1291
1292 drive.set_encryption(encrypt_fingerprint)?;
1293
1294 let _pool_lock = lock_media_pool(status_path, &set.pool)?;
1295 let media_set_lock = lock_media_set(status_path, &set.uuid, None)?;
1296
1297 MediaCatalog::destroy_unrelated_catalog(status_path, &media_id)?;
1298
1299 inventory.store(media_id.clone(), false)?;
1300
1301 (media_set_lock, &set.uuid)
1302 }
1303 };
1304
1305 if MediaCatalog::exists(status_path, &media_id.label.uuid) && !force {
1306 bail!("media catalog exists (please use --force to overwrite)");
1307 }
1308
1309 if !scan {
1310 let media_set = inventory.compute_media_set_members(media_set_uuid)?;
1311
1312 if fast_catalog_restore(&worker, &mut drive, &media_set, &media_id.label.uuid)? {
1313 return Ok(())
1314 }
1315
1316 task_log!(worker, "no catalog found");
1317 }
1318
1319 task_log!(worker, "scanning entire media to reconstruct catalog");
1320
1321 drive.rewind()?;
1322 drive.read_label()?; // skip over labels - we already read them above
1323
1324 let mut checked_chunks = HashMap::new();
1325 restore_media(worker, &mut drive, &media_id, None, &mut checked_chunks, verbose)?;
1326
1327 Ok(())
1328 },
1329 )?;
1330
1331 Ok(upid_str.into())
1332 }
1333
1334 #[api(
1335 input: {
1336 properties: {
1337 changer: {
1338 schema: CHANGER_NAME_SCHEMA,
1339 optional: true,
1340 },
1341 },
1342 },
1343 returns: {
1344 description: "The list of configured drives with model information.",
1345 type: Array,
1346 items: {
1347 type: DriveListEntry,
1348 },
1349 },
1350 access: {
1351 description: "List configured tape drives filtered by Tape.Audit privileges",
1352 permission: &Permission::Anybody,
1353 },
1354 )]
1355 /// List drives
1356 pub fn list_drives(
1357 changer: Option<String>,
1358 _param: Value,
1359 rpcenv: &mut dyn RpcEnvironment,
1360 ) -> Result<Vec<DriveListEntry>, Error> {
1361 let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
1362 let user_info = CachedUserInfo::new()?;
1363
1364 let (config, _) = pbs_config::drive::config()?;
1365
1366 let lto_drives = lto_tape_device_list();
1367
1368 let drive_list: Vec<LtoTapeDrive> = config.convert_to_typed_array("lto")?;
1369
1370 let mut list = Vec::new();
1371
1372 for drive in drive_list {
1373 if changer.is_some() && drive.changer != changer {
1374 continue;
1375 }
1376
1377 let privs = user_info.lookup_privs(&auth_id, &["tape", "drive", &drive.name]);
1378 if (privs & PRIV_TAPE_AUDIT) == 0 {
1379 continue;
1380 }
1381
1382 let info = lookup_device_identification(&lto_drives, &drive.path);
1383 let state = get_tape_device_state(&config, &drive.name)?;
1384 let entry = DriveListEntry { config: drive, info, state };
1385 list.push(entry);
1386 }
1387
1388 Ok(list)
1389 }
1390
1391 #[sortable]
1392 pub const SUBDIRS: SubdirMap = &sorted!([
1393 (
1394 "barcode-label-media",
1395 &Router::new()
1396 .post(&API_METHOD_BARCODE_LABEL_MEDIA)
1397 ),
1398 (
1399 "catalog",
1400 &Router::new()
1401 .post(&API_METHOD_CATALOG_MEDIA)
1402 ),
1403 (
1404 "clean",
1405 &Router::new()
1406 .put(&API_METHOD_CLEAN_DRIVE)
1407 ),
1408 (
1409 "eject-media",
1410 &Router::new()
1411 .post(&API_METHOD_EJECT_MEDIA)
1412 ),
1413 (
1414 "format-media",
1415 &Router::new()
1416 .post(&API_METHOD_FORMAT_MEDIA)
1417 ),
1418 (
1419 "export-media",
1420 &Router::new()
1421 .put(&API_METHOD_EXPORT_MEDIA)
1422 ),
1423 (
1424 "inventory",
1425 &Router::new()
1426 .get(&API_METHOD_INVENTORY)
1427 .put(&API_METHOD_UPDATE_INVENTORY)
1428 ),
1429 (
1430 "label-media",
1431 &Router::new()
1432 .post(&API_METHOD_LABEL_MEDIA)
1433 ),
1434 (
1435 "load-media",
1436 &Router::new()
1437 .post(&API_METHOD_LOAD_MEDIA)
1438 ),
1439 (
1440 "load-slot",
1441 &Router::new()
1442 .post(&API_METHOD_LOAD_SLOT)
1443 ),
1444 (
1445 "cartridge-memory",
1446 &Router::new()
1447 .get(&API_METHOD_CARTRIDGE_MEMORY)
1448 ),
1449 (
1450 "volume-statistics",
1451 &Router::new()
1452 .get(&API_METHOD_VOLUME_STATISTICS)
1453 ),
1454 (
1455 "read-label",
1456 &Router::new()
1457 .get(&API_METHOD_READ_LABEL)
1458 ),
1459 (
1460 "restore-key",
1461 &Router::new()
1462 .post(&API_METHOD_RESTORE_KEY)
1463 ),
1464 (
1465 "rewind",
1466 &Router::new()
1467 .post(&API_METHOD_REWIND)
1468 ),
1469 (
1470 "status",
1471 &Router::new()
1472 .get(&API_METHOD_STATUS)
1473 ),
1474 (
1475 "unload",
1476 &Router::new()
1477 .post(&API_METHOD_UNLOAD)
1478 ),
1479 ]);
1480
1481 const ITEM_ROUTER: Router = Router::new()
1482 .get(&list_subdirs_api_method!(SUBDIRS))
1483 .subdirs(&SUBDIRS);
1484
1485 pub const ROUTER: Router = Router::new()
1486 .get(&API_METHOD_LIST_DRIVES)
1487 .match_all("drive", &ITEM_ROUTER);