]> git.proxmox.com Git - proxmox-backup.git/blob - src/api2/tape/drive.rs
cleanup KeyConfig
[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, None);
410
411 drive.write_media_set_label(&set, None)?;
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 protected: true,
461 input: {
462 properties: {
463 drive: {
464 schema: DRIVE_NAME_SCHEMA,
465 },
466 password: {
467 description: "Encryption key password.",
468 },
469 },
470 },
471 )]
472 /// Try to restore a tape encryption key
473 pub async fn restore_key(
474 drive: String,
475 password: String,
476 ) -> Result<(), Error> {
477
478 let (config, _digest) = config::drive::config()?;
479
480 tokio::task::spawn_blocking(move || {
481 let mut drive = open_drive(&config, &drive)?;
482
483 let (_media_id, key_config) = drive.read_label()?;
484
485 if let Some(key_config) = key_config {
486 let password_fn = || { Ok(password.as_bytes().to_vec()) };
487 let key = match key_config.decrypt(&password_fn) {
488 Ok((key, ..)) => key,
489 Err(_) => {
490 match key_config.hint {
491 Some(hint) => {
492 bail!("decrypt key failed (password hint: {})", hint);
493 }
494 None => {
495 bail!("decrypt key failed (wrong password)");
496 }
497 }
498 }
499 };
500 config::tape_encryption_keys::insert_key(key, key_config)?;
501 } else {
502 bail!("media does not contain any encryption key configuration");
503 }
504
505 Ok(())
506 }).await?
507 }
508
509 #[api(
510 input: {
511 properties: {
512 drive: {
513 schema: DRIVE_NAME_SCHEMA,
514 },
515 inventorize: {
516 description: "Inventorize media",
517 optional: true,
518 },
519 },
520 },
521 returns: {
522 type: MediaIdFlat,
523 },
524 )]
525 /// Read media label (optionally inventorize media)
526 pub async fn read_label(
527 drive: String,
528 inventorize: Option<bool>,
529 ) -> Result<MediaIdFlat, Error> {
530
531 let (config, _digest) = config::drive::config()?;
532
533 tokio::task::spawn_blocking(move || {
534 let mut drive = open_drive(&config, &drive)?;
535
536 let (media_id, _key_config) = drive.read_label()?;
537
538 let media_id = match media_id {
539 Some(media_id) => {
540 let mut flat = MediaIdFlat {
541 uuid: media_id.label.uuid.to_string(),
542 label_text: media_id.label.label_text.clone(),
543 ctime: media_id.label.ctime,
544 media_set_ctime: None,
545 media_set_uuid: None,
546 encryption_key_fingerprint: None,
547 pool: None,
548 seq_nr: None,
549 };
550 if let Some(ref set) = media_id.media_set_label {
551 flat.pool = Some(set.pool.clone());
552 flat.seq_nr = Some(set.seq_nr);
553 flat.media_set_uuid = Some(set.uuid.to_string());
554 flat.media_set_ctime = Some(set.ctime);
555 flat.encryption_key_fingerprint = set
556 .encryption_key_fingerprint
557 .as_ref()
558 .map(|fp| crate::tools::format::as_fingerprint(fp.bytes()));
559 }
560
561 if let Some(true) = inventorize {
562 let state_path = Path::new(TAPE_STATUS_DIR);
563 let mut inventory = Inventory::load(state_path)?;
564 inventory.store(media_id, false)?;
565 }
566
567 flat
568 }
569 None => {
570 bail!("Media is empty (no label).");
571 }
572 };
573
574 Ok(media_id)
575 }).await?
576 }
577
578 #[api(
579 input: {
580 properties: {
581 drive: {
582 schema: DRIVE_NAME_SCHEMA,
583 },
584 },
585 },
586 returns: {
587 schema: UPID_SCHEMA,
588 },
589 )]
590 /// Clean drive
591 pub fn clean_drive(
592 drive: String,
593 rpcenv: &mut dyn RpcEnvironment,
594 ) -> Result<Value, Error> {
595
596 let (config, _digest) = config::drive::config()?;
597
598 check_drive_exists(&config, &drive)?; // early check before starting worker
599
600 let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
601
602 let to_stdout = if rpcenv.env_type() == RpcEnvironmentType::CLI { true } else { false };
603
604 let upid_str = WorkerTask::new_thread(
605 "clean-drive",
606 Some(drive.clone()),
607 auth_id,
608 to_stdout,
609 move |worker| {
610
611 let (mut changer, _changer_name) = required_media_changer(&config, &drive)?;
612
613 worker.log("Starting drive clean");
614
615 changer.clean_drive()?;
616
617 worker.log("Drive cleaned sucessfully");
618
619 Ok(())
620 })?;
621
622 Ok(upid_str.into())
623 }
624
625 #[api(
626 input: {
627 properties: {
628 drive: {
629 schema: DRIVE_NAME_SCHEMA,
630 },
631 },
632 },
633 returns: {
634 description: "The list of media labels with associated media Uuid (if any).",
635 type: Array,
636 items: {
637 type: LabelUuidMap,
638 },
639 },
640 )]
641 /// List known media labels (Changer Inventory)
642 ///
643 /// Note: Only useful for drives with associated changer device.
644 ///
645 /// This method queries the changer to get a list of media labels.
646 ///
647 /// Note: This updates the media online status.
648 pub async fn inventory(
649 drive: String,
650 ) -> Result<Vec<LabelUuidMap>, Error> {
651
652 let (config, _digest) = config::drive::config()?;
653
654 tokio::task::spawn_blocking(move || {
655 let (mut changer, changer_name) = required_media_changer(&config, &drive)?;
656
657 let label_text_list = changer.online_media_label_texts()?;
658
659 let state_path = Path::new(TAPE_STATUS_DIR);
660
661 let mut inventory = Inventory::load(state_path)?;
662
663 update_changer_online_status(
664 &config,
665 &mut inventory,
666 &changer_name,
667 &label_text_list,
668 )?;
669
670 let mut list = Vec::new();
671
672 for label_text in label_text_list.iter() {
673 if label_text.starts_with("CLN") {
674 // skip cleaning unit
675 continue;
676 }
677
678 let label_text = label_text.to_string();
679
680 if let Some(media_id) = inventory.find_media_by_label_text(&label_text) {
681 list.push(LabelUuidMap { label_text, uuid: Some(media_id.label.uuid.to_string()) });
682 } else {
683 list.push(LabelUuidMap { label_text, uuid: None });
684 }
685 }
686
687 Ok(list)
688 }).await?
689 }
690
691 #[api(
692 input: {
693 properties: {
694 drive: {
695 schema: DRIVE_NAME_SCHEMA,
696 },
697 "read-all-labels": {
698 description: "Load all tapes and try read labels (even if already inventoried)",
699 type: bool,
700 optional: true,
701 },
702 },
703 },
704 returns: {
705 schema: UPID_SCHEMA,
706 },
707 )]
708 /// Update inventory
709 ///
710 /// Note: Only useful for drives with associated changer device.
711 ///
712 /// This method queries the changer to get a list of media labels. It
713 /// then loads any unknown media into the drive, reads the label, and
714 /// store the result to the media database.
715 ///
716 /// Note: This updates the media online status.
717 pub fn update_inventory(
718 drive: String,
719 read_all_labels: Option<bool>,
720 rpcenv: &mut dyn RpcEnvironment,
721 ) -> Result<Value, Error> {
722
723 let (config, _digest) = config::drive::config()?;
724
725 check_drive_exists(&config, &drive)?; // early check before starting worker
726
727 let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
728
729 let to_stdout = if rpcenv.env_type() == RpcEnvironmentType::CLI { true } else { false };
730
731 let upid_str = WorkerTask::new_thread(
732 "inventory-update",
733 Some(drive.clone()),
734 auth_id,
735 to_stdout,
736 move |worker| {
737
738 let (mut changer, changer_name) = required_media_changer(&config, &drive)?;
739
740 let label_text_list = changer.online_media_label_texts()?;
741 if label_text_list.is_empty() {
742 worker.log(format!("changer device does not list any media labels"));
743 }
744
745 let state_path = Path::new(TAPE_STATUS_DIR);
746
747 let mut inventory = Inventory::load(state_path)?;
748
749 update_changer_online_status(&config, &mut inventory, &changer_name, &label_text_list)?;
750
751 for label_text in label_text_list.iter() {
752 if label_text.starts_with("CLN") {
753 worker.log(format!("skip cleaning unit '{}'", label_text));
754 continue;
755 }
756
757 let label_text = label_text.to_string();
758
759 if !read_all_labels.unwrap_or(false) {
760 if let Some(_) = inventory.find_media_by_label_text(&label_text) {
761 worker.log(format!("media '{}' already inventoried", label_text));
762 continue;
763 }
764 }
765
766 if let Err(err) = changer.load_media(&label_text) {
767 worker.warn(format!("unable to load media '{}' - {}", label_text, err));
768 continue;
769 }
770
771 let mut drive = open_drive(&config, &drive)?;
772 match drive.read_label() {
773 Err(err) => {
774 worker.warn(format!("unable to read label form media '{}' - {}", label_text, err));
775 }
776 Ok((None, _)) => {
777 worker.log(format!("media '{}' is empty", label_text));
778 }
779 Ok((Some(media_id), _key_config)) => {
780 if label_text != media_id.label.label_text {
781 worker.warn(format!("label text missmatch ({} != {})", label_text, media_id.label.label_text));
782 continue;
783 }
784 worker.log(format!("inventorize media '{}' with uuid '{}'", label_text, media_id.label.uuid));
785 inventory.store(media_id, false)?;
786 }
787 }
788 changer.unload_media(None)?;
789 }
790 Ok(())
791 }
792 )?;
793
794 Ok(upid_str.into())
795 }
796
797
798 #[api(
799 input: {
800 properties: {
801 drive: {
802 schema: DRIVE_NAME_SCHEMA,
803 },
804 pool: {
805 schema: MEDIA_POOL_NAME_SCHEMA,
806 optional: true,
807 },
808 },
809 },
810 returns: {
811 schema: UPID_SCHEMA,
812 },
813 )]
814 /// Label media with barcodes from changer device
815 pub fn barcode_label_media(
816 drive: String,
817 pool: Option<String>,
818 rpcenv: &mut dyn RpcEnvironment,
819 ) -> Result<Value, Error> {
820
821 if let Some(ref pool) = pool {
822 let (pool_config, _digest) = config::media_pool::config()?;
823
824 if pool_config.sections.get(pool).is_none() {
825 bail!("no such pool ('{}')", pool);
826 }
827 }
828
829 let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
830
831 let to_stdout = if rpcenv.env_type() == RpcEnvironmentType::CLI { true } else { false };
832
833 let upid_str = WorkerTask::new_thread(
834 "barcode-label-media",
835 Some(drive.clone()),
836 auth_id,
837 to_stdout,
838 move |worker| {
839 barcode_label_media_worker(worker, drive, pool)
840 }
841 )?;
842
843 Ok(upid_str.into())
844 }
845
846 fn barcode_label_media_worker(
847 worker: Arc<WorkerTask>,
848 drive: String,
849 pool: Option<String>,
850 ) -> Result<(), Error> {
851
852 let (config, _digest) = config::drive::config()?;
853
854 let (mut changer, changer_name) = required_media_changer(&config, &drive)?;
855
856 let label_text_list = changer.online_media_label_texts()?;
857
858 let state_path = Path::new(TAPE_STATUS_DIR);
859
860 let mut inventory = Inventory::load(state_path)?;
861
862 update_changer_online_status(&config, &mut inventory, &changer_name, &label_text_list)?;
863
864 if label_text_list.is_empty() {
865 bail!("changer device does not list any media labels");
866 }
867
868 for label_text in label_text_list {
869 if label_text.starts_with("CLN") { continue; }
870
871 inventory.reload()?;
872 if inventory.find_media_by_label_text(&label_text).is_some() {
873 worker.log(format!("media '{}' already inventoried (already labeled)", label_text));
874 continue;
875 }
876
877 worker.log(format!("checking/loading media '{}'", label_text));
878
879 if let Err(err) = changer.load_media(&label_text) {
880 worker.warn(format!("unable to load media '{}' - {}", label_text, err));
881 continue;
882 }
883
884 let mut drive = open_drive(&config, &drive)?;
885 drive.rewind()?;
886
887 match drive.read_next_file() {
888 Ok(Some(_file)) => {
889 worker.log(format!("media '{}' is not empty (erase first)", label_text));
890 continue;
891 }
892 Ok(None) => { /* EOF mark at BOT, assume tape is empty */ },
893 Err(err) => {
894 if err.is_errno(nix::errno::Errno::ENOSPC) || err.is_errno(nix::errno::Errno::EIO) {
895 /* assume tape is empty */
896 } else {
897 worker.warn(format!("media '{}' read error (maybe not empty - erase first)", label_text));
898 continue;
899 }
900 }
901 }
902
903 let ctime = proxmox::tools::time::epoch_i64();
904 let label = MediaLabel {
905 label_text: label_text.to_string(),
906 uuid: Uuid::generate(),
907 ctime,
908 };
909
910 write_media_label(worker.clone(), &mut drive, label, pool.clone())?
911 }
912
913 Ok(())
914 }
915
916 #[api(
917 input: {
918 properties: {
919 drive: {
920 schema: DRIVE_NAME_SCHEMA,
921 },
922 },
923 },
924 returns: {
925 description: "A List of medium auxiliary memory attributes.",
926 type: Array,
927 items: {
928 type: MamAttribute,
929 },
930 },
931 )]
932 /// Read Cartridge Memory (Medium auxiliary memory attributes)
933 pub fn cartridge_memory(drive: String) -> Result<Vec<MamAttribute>, Error> {
934
935 let (config, _digest) = config::drive::config()?;
936
937 let drive_config: LinuxTapeDrive = config.lookup("linux", &drive)?;
938 let mut handle = drive_config.open()?;
939
940 handle.cartridge_memory()
941 }
942
943 #[api(
944 input: {
945 properties: {
946 drive: {
947 schema: DRIVE_NAME_SCHEMA,
948 },
949 },
950 },
951 returns: {
952 type: LinuxDriveAndMediaStatus,
953 },
954 )]
955 /// Get drive/media status
956 pub fn status(drive: String) -> Result<LinuxDriveAndMediaStatus, Error> {
957
958 let (config, _digest) = config::drive::config()?;
959
960 let drive_config: LinuxTapeDrive = config.lookup("linux", &drive)?;
961
962 // Note: use open_linux_tape_device, because this also works if no medium loaded
963 let file = open_linux_tape_device(&drive_config.path)?;
964
965 let mut handle = LinuxTapeHandle::new(file);
966
967 handle.get_drive_and_media_status()
968 }
969
970 #[api(
971 input: {
972 properties: {
973 drive: {
974 schema: DRIVE_NAME_SCHEMA,
975 },
976 force: {
977 description: "Force overriding existing index.",
978 type: bool,
979 optional: true,
980 },
981 verbose: {
982 description: "Verbose mode - log all found chunks.",
983 type: bool,
984 optional: true,
985 },
986 },
987 },
988 returns: {
989 schema: UPID_SCHEMA,
990 },
991 )]
992 /// Scan media and record content
993 pub fn catalog_media(
994 drive: String,
995 force: Option<bool>,
996 verbose: Option<bool>,
997 rpcenv: &mut dyn RpcEnvironment,
998 ) -> Result<Value, Error> {
999
1000 let verbose = verbose.unwrap_or(false);
1001 let force = force.unwrap_or(false);
1002
1003 let (config, _digest) = config::drive::config()?;
1004
1005 check_drive_exists(&config, &drive)?; // early check before starting worker
1006
1007 let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
1008
1009 let to_stdout = if rpcenv.env_type() == RpcEnvironmentType::CLI { true } else { false };
1010
1011 let upid_str = WorkerTask::new_thread(
1012 "catalog-media",
1013 Some(drive.clone()),
1014 auth_id,
1015 to_stdout,
1016 move |worker| {
1017
1018 let mut drive = open_drive(&config, &drive)?;
1019
1020 drive.rewind()?;
1021
1022 let media_id = match drive.read_label()? {
1023 (Some(media_id), key_config) => {
1024 worker.log(format!(
1025 "found media label: {}",
1026 serde_json::to_string_pretty(&serde_json::to_value(&media_id)?)?
1027 ));
1028 if key_config.is_some() {
1029 worker.log(format!(
1030 "encryption key config: {}",
1031 serde_json::to_string_pretty(&serde_json::to_value(&key_config)?)?
1032 ));
1033 }
1034 media_id
1035 },
1036 (None, _) => bail!("media is empty (no media label found)"),
1037 };
1038
1039 let status_path = Path::new(TAPE_STATUS_DIR);
1040
1041 let mut inventory = Inventory::load(status_path)?;
1042 inventory.store(media_id.clone(), false)?;
1043
1044 let pool = match media_id.media_set_label {
1045 None => {
1046 worker.log("media is empty");
1047 MediaCatalog::destroy(status_path, &media_id.label.uuid)?;
1048 return Ok(());
1049 }
1050 Some(ref set) => {
1051 if set.uuid.as_ref() == [0u8;16] { // media is empty
1052 worker.log("media is empty");
1053 MediaCatalog::destroy(status_path, &media_id.label.uuid)?;
1054 return Ok(());
1055 }
1056 let encrypt_fingerprint = set.encryption_key_fingerprint.clone();
1057 drive.set_encryption(encrypt_fingerprint)?;
1058
1059 set.pool.clone()
1060 }
1061 };
1062
1063 let _lock = MediaPool::lock(status_path, &pool)?;
1064
1065 if MediaCatalog::exists(status_path, &media_id.label.uuid) {
1066 if !force {
1067 bail!("media catalog exists (please use --force to overwrite)");
1068 }
1069 }
1070
1071 restore_media(&worker, &mut drive, &media_id, None, verbose)?;
1072
1073 Ok(())
1074
1075 }
1076 )?;
1077
1078 Ok(upid_str.into())
1079 }
1080
1081 #[sortable]
1082 pub const SUBDIRS: SubdirMap = &sorted!([
1083 (
1084 "barcode-label-media",
1085 &Router::new()
1086 .put(&API_METHOD_BARCODE_LABEL_MEDIA)
1087 ),
1088 (
1089 "catalog",
1090 &Router::new()
1091 .put(&API_METHOD_CATALOG_MEDIA)
1092 ),
1093 (
1094 "clean",
1095 &Router::new()
1096 .put(&API_METHOD_CLEAN_DRIVE)
1097 ),
1098 (
1099 "eject-media",
1100 &Router::new()
1101 .put(&API_METHOD_EJECT_MEDIA)
1102 ),
1103 (
1104 "erase-media",
1105 &Router::new()
1106 .put(&API_METHOD_ERASE_MEDIA)
1107 ),
1108 (
1109 "inventory",
1110 &Router::new()
1111 .get(&API_METHOD_INVENTORY)
1112 .put(&API_METHOD_UPDATE_INVENTORY)
1113 ),
1114 (
1115 "label-media",
1116 &Router::new()
1117 .put(&API_METHOD_LABEL_MEDIA)
1118 ),
1119 (
1120 "load-slot",
1121 &Router::new()
1122 .put(&API_METHOD_LOAD_SLOT)
1123 ),
1124 (
1125 "cartridge-memory",
1126 &Router::new()
1127 .put(&API_METHOD_CARTRIDGE_MEMORY)
1128 ),
1129 (
1130 "read-label",
1131 &Router::new()
1132 .get(&API_METHOD_READ_LABEL)
1133 ),
1134 (
1135 "rewind",
1136 &Router::new()
1137 .put(&API_METHOD_REWIND)
1138 ),
1139 (
1140 "scan",
1141 &Router::new()
1142 .get(&API_METHOD_SCAN_DRIVES)
1143 ),
1144 (
1145 "status",
1146 &Router::new()
1147 .get(&API_METHOD_STATUS)
1148 ),
1149 (
1150 "unload",
1151 &Router::new()
1152 .put(&API_METHOD_UNLOAD)
1153 ),
1154 ]);
1155
1156 pub const ROUTER: Router = Router::new()
1157 .get(&list_subdirs_api_method!(SUBDIRS))
1158 .subdirs(SUBDIRS);