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