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