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