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