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