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