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