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