]> git.proxmox.com Git - proxmox-backup.git/blob - src/api2/tape/drive.rs
tape: generate random encryptions keys and store key_config on media
[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 = if rpcenv.env_type() == RpcEnvironmentType::CLI { true } else { false };
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 = if rpcenv.env_type() == RpcEnvironmentType::CLI { true } else { false };
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 = if rpcenv.env_type() == RpcEnvironmentType::CLI { true } else { false };
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 = if rpcenv.env_type() == RpcEnvironmentType::CLI { true } else { false };
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 = if rpcenv.env_type() == RpcEnvironmentType::CLI { true } else { false };
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) {
751 if let Some(_) = inventory.find_media_by_label_text(&label_text) {
752 worker.log(format!("media '{}' already inventoried", label_text));
753 continue;
754 }
755 }
756
757 if let Err(err) = changer.load_media(&label_text) {
758 worker.warn(format!("unable to load media '{}' - {}", label_text, err));
759 continue;
760 }
761
762 let mut drive = open_drive(&config, &drive)?;
763 match drive.read_label() {
764 Err(err) => {
765 worker.warn(format!("unable to read label form media '{}' - {}", label_text, err));
766 }
767 Ok((None, _)) => {
768 worker.log(format!("media '{}' is empty", label_text));
769 }
770 Ok((Some(media_id), _key_config)) => {
771 if label_text != media_id.label.label_text {
772 worker.warn(format!("label text missmatch ({} != {})", label_text, media_id.label.label_text));
773 continue;
774 }
775 worker.log(format!("inventorize media '{}' with uuid '{}'", label_text, media_id.label.uuid));
776 inventory.store(media_id, false)?;
777 }
778 }
779 changer.unload_media(None)?;
780 }
781 Ok(())
782 }
783 )?;
784
785 Ok(upid_str.into())
786 }
787
788
789 #[api(
790 input: {
791 properties: {
792 drive: {
793 schema: DRIVE_NAME_SCHEMA,
794 },
795 pool: {
796 schema: MEDIA_POOL_NAME_SCHEMA,
797 optional: true,
798 },
799 },
800 },
801 returns: {
802 schema: UPID_SCHEMA,
803 },
804 )]
805 /// Label media with barcodes from changer device
806 pub fn barcode_label_media(
807 drive: String,
808 pool: Option<String>,
809 rpcenv: &mut dyn RpcEnvironment,
810 ) -> Result<Value, Error> {
811
812 if let Some(ref pool) = pool {
813 let (pool_config, _digest) = config::media_pool::config()?;
814
815 if pool_config.sections.get(pool).is_none() {
816 bail!("no such pool ('{}')", pool);
817 }
818 }
819
820 let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
821
822 let to_stdout = if rpcenv.env_type() == RpcEnvironmentType::CLI { true } else { false };
823
824 let upid_str = WorkerTask::new_thread(
825 "barcode-label-media",
826 Some(drive.clone()),
827 auth_id,
828 to_stdout,
829 move |worker| {
830 barcode_label_media_worker(worker, drive, pool)
831 }
832 )?;
833
834 Ok(upid_str.into())
835 }
836
837 fn barcode_label_media_worker(
838 worker: Arc<WorkerTask>,
839 drive: String,
840 pool: Option<String>,
841 ) -> Result<(), Error> {
842
843 let (config, _digest) = config::drive::config()?;
844
845 let (mut changer, changer_name) = required_media_changer(&config, &drive)?;
846
847 let label_text_list = changer.online_media_label_texts()?;
848
849 let state_path = Path::new(TAPE_STATUS_DIR);
850
851 let mut inventory = Inventory::load(state_path)?;
852
853 update_changer_online_status(&config, &mut inventory, &changer_name, &label_text_list)?;
854
855 if label_text_list.is_empty() {
856 bail!("changer device does not list any media labels");
857 }
858
859 for label_text in label_text_list {
860 if label_text.starts_with("CLN") { continue; }
861
862 inventory.reload()?;
863 if inventory.find_media_by_label_text(&label_text).is_some() {
864 worker.log(format!("media '{}' already inventoried (already labeled)", label_text));
865 continue;
866 }
867
868 worker.log(format!("checking/loading media '{}'", label_text));
869
870 if let Err(err) = changer.load_media(&label_text) {
871 worker.warn(format!("unable to load media '{}' - {}", label_text, err));
872 continue;
873 }
874
875 let mut drive = open_drive(&config, &drive)?;
876 drive.rewind()?;
877
878 match drive.read_next_file() {
879 Ok(Some(_file)) => {
880 worker.log(format!("media '{}' is not empty (erase first)", label_text));
881 continue;
882 }
883 Ok(None) => { /* EOF mark at BOT, assume tape is empty */ },
884 Err(err) => {
885 if err.is_errno(nix::errno::Errno::ENOSPC) || err.is_errno(nix::errno::Errno::EIO) {
886 /* assume tape is empty */
887 } else {
888 worker.warn(format!("media '{}' read error (maybe not empty - erase first)", label_text));
889 continue;
890 }
891 }
892 }
893
894 let ctime = proxmox::tools::time::epoch_i64();
895 let label = MediaLabel {
896 label_text: label_text.to_string(),
897 uuid: Uuid::generate(),
898 ctime,
899 };
900
901 write_media_label(worker.clone(), &mut drive, label, pool.clone())?
902 }
903
904 Ok(())
905 }
906
907 #[api(
908 input: {
909 properties: {
910 drive: {
911 schema: DRIVE_NAME_SCHEMA,
912 },
913 },
914 },
915 returns: {
916 description: "A List of medium auxiliary memory attributes.",
917 type: Array,
918 items: {
919 type: MamAttribute,
920 },
921 },
922 )]
923 /// Read Cartridge Memory (Medium auxiliary memory attributes)
924 pub fn cartridge_memory(drive: String) -> Result<Vec<MamAttribute>, Error> {
925
926 let (config, _digest) = config::drive::config()?;
927
928 let drive_config: LinuxTapeDrive = config.lookup("linux", &drive)?;
929 let mut handle = drive_config.open()?;
930
931 handle.cartridge_memory()
932 }
933
934 #[api(
935 input: {
936 properties: {
937 drive: {
938 schema: DRIVE_NAME_SCHEMA,
939 },
940 },
941 },
942 returns: {
943 type: LinuxDriveAndMediaStatus,
944 },
945 )]
946 /// Get drive/media status
947 pub fn status(drive: String) -> Result<LinuxDriveAndMediaStatus, Error> {
948
949 let (config, _digest) = config::drive::config()?;
950
951 let drive_config: LinuxTapeDrive = config.lookup("linux", &drive)?;
952
953 // Note: use open_linux_tape_device, because this also works if no medium loaded
954 let file = open_linux_tape_device(&drive_config.path)?;
955
956 let mut handle = LinuxTapeHandle::new(file);
957
958 handle.get_drive_and_media_status()
959 }
960
961 #[api(
962 input: {
963 properties: {
964 drive: {
965 schema: DRIVE_NAME_SCHEMA,
966 },
967 force: {
968 description: "Force overriding existing index.",
969 type: bool,
970 optional: true,
971 },
972 verbose: {
973 description: "Verbose mode - log all found chunks.",
974 type: bool,
975 optional: true,
976 },
977 },
978 },
979 returns: {
980 schema: UPID_SCHEMA,
981 },
982 )]
983 /// Scan media and record content
984 pub fn catalog_media(
985 drive: String,
986 force: Option<bool>,
987 verbose: Option<bool>,
988 rpcenv: &mut dyn RpcEnvironment,
989 ) -> Result<Value, Error> {
990
991 let verbose = verbose.unwrap_or(false);
992 let force = force.unwrap_or(false);
993
994 let (config, _digest) = config::drive::config()?;
995
996 check_drive_exists(&config, &drive)?; // early check before starting worker
997
998 let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
999
1000 let to_stdout = if rpcenv.env_type() == RpcEnvironmentType::CLI { true } else { false };
1001
1002 let upid_str = WorkerTask::new_thread(
1003 "catalog-media",
1004 Some(drive.clone()),
1005 auth_id,
1006 to_stdout,
1007 move |worker| {
1008
1009 let mut drive = open_drive(&config, &drive)?;
1010
1011 drive.rewind()?;
1012
1013 let media_id = match drive.read_label()? {
1014 (Some(media_id), key_config) => {
1015 worker.log(format!(
1016 "found media label: {}",
1017 serde_json::to_string_pretty(&serde_json::to_value(&media_id)?)?
1018 ));
1019 if key_config.is_some() {
1020 worker.log(format!(
1021 "encryption key config: {}",
1022 serde_json::to_string_pretty(&serde_json::to_value(&key_config)?)?
1023 ));
1024 }
1025 media_id
1026 },
1027 (None, _) => bail!("media is empty (no media label found)"),
1028 };
1029
1030 let status_path = Path::new(TAPE_STATUS_DIR);
1031
1032 let mut inventory = Inventory::load(status_path)?;
1033 inventory.store(media_id.clone(), false)?;
1034
1035 let pool = match media_id.media_set_label {
1036 None => {
1037 worker.log("media is empty");
1038 MediaCatalog::destroy(status_path, &media_id.label.uuid)?;
1039 return Ok(());
1040 }
1041 Some(ref set) => {
1042 if set.uuid.as_ref() == [0u8;16] { // media is empty
1043 worker.log("media is empty");
1044 MediaCatalog::destroy(status_path, &media_id.label.uuid)?;
1045 return Ok(());
1046 }
1047 let encrypt_fingerprint = set.encryption_key_fingerprint.clone();
1048 drive.set_encryption(encrypt_fingerprint)?;
1049
1050 set.pool.clone()
1051 }
1052 };
1053
1054 let _lock = MediaPool::lock(status_path, &pool)?;
1055
1056 if MediaCatalog::exists(status_path, &media_id.label.uuid) {
1057 if !force {
1058 bail!("media catalog exists (please use --force to overwrite)");
1059 }
1060 }
1061
1062 restore_media(&worker, &mut drive, &media_id, None, verbose)?;
1063
1064 Ok(())
1065
1066 }
1067 )?;
1068
1069 Ok(upid_str.into())
1070 }
1071
1072 #[sortable]
1073 pub const SUBDIRS: SubdirMap = &sorted!([
1074 (
1075 "barcode-label-media",
1076 &Router::new()
1077 .put(&API_METHOD_BARCODE_LABEL_MEDIA)
1078 ),
1079 (
1080 "catalog",
1081 &Router::new()
1082 .put(&API_METHOD_CATALOG_MEDIA)
1083 ),
1084 (
1085 "clean",
1086 &Router::new()
1087 .put(&API_METHOD_CLEAN_DRIVE)
1088 ),
1089 (
1090 "eject-media",
1091 &Router::new()
1092 .put(&API_METHOD_EJECT_MEDIA)
1093 ),
1094 (
1095 "erase-media",
1096 &Router::new()
1097 .put(&API_METHOD_ERASE_MEDIA)
1098 ),
1099 (
1100 "inventory",
1101 &Router::new()
1102 .get(&API_METHOD_INVENTORY)
1103 .put(&API_METHOD_UPDATE_INVENTORY)
1104 ),
1105 (
1106 "label-media",
1107 &Router::new()
1108 .put(&API_METHOD_LABEL_MEDIA)
1109 ),
1110 (
1111 "load-slot",
1112 &Router::new()
1113 .put(&API_METHOD_LOAD_SLOT)
1114 ),
1115 (
1116 "cartridge-memory",
1117 &Router::new()
1118 .put(&API_METHOD_CARTRIDGE_MEMORY)
1119 ),
1120 (
1121 "read-label",
1122 &Router::new()
1123 .get(&API_METHOD_READ_LABEL)
1124 ),
1125 (
1126 "rewind",
1127 &Router::new()
1128 .put(&API_METHOD_REWIND)
1129 ),
1130 (
1131 "scan",
1132 &Router::new()
1133 .get(&API_METHOD_SCAN_DRIVES)
1134 ),
1135 (
1136 "status",
1137 &Router::new()
1138 .get(&API_METHOD_STATUS)
1139 ),
1140 (
1141 "unload",
1142 &Router::new()
1143 .put(&API_METHOD_UNLOAD)
1144 ),
1145 ]);
1146
1147 pub const ROUTER: Router = Router::new()
1148 .get(&list_subdirs_api_method!(SUBDIRS))
1149 .subdirs(SUBDIRS);