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