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