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