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