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