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