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