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