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