]> git.proxmox.com Git - proxmox-backup.git/blob - src/api2/tape/drive.rs
tape: remove MediaLabelInfo, use MediaId instead
[proxmox-backup.git] / src / api2 / tape / drive.rs
1 use std::path::Path;
2 use std::sync::Arc;
3
4 use anyhow::{bail, Error};
5 use serde_json::Value;
6
7 use proxmox::{
8 sortable,
9 identity,
10 list_subdirs_api_method,
11 tools::Uuid,
12 sys::error::SysError,
13 api::{
14 api,
15 RpcEnvironment,
16 Router,
17 SubdirMap,
18 },
19 };
20
21 use crate::{
22 config::{
23 self,
24 drive::check_drive_exists,
25 },
26 api2::types::{
27 UPID_SCHEMA,
28 DRIVE_NAME_SCHEMA,
29 MEDIA_LABEL_SCHEMA,
30 MEDIA_POOL_NAME_SCHEMA,
31 Authid,
32 LinuxTapeDrive,
33 ScsiTapeChanger,
34 TapeDeviceInfo,
35 MediaIdFlat,
36 LabelUuidMap,
37 },
38 server::WorkerTask,
39 tape::{
40 TAPE_STATUS_DIR,
41 TapeDriver,
42 MediaChange,
43 Inventory,
44 MediaStateDatabase,
45 MediaId,
46 mtx_load,
47 mtx_unload,
48 linux_tape_device_list,
49 open_drive,
50 media_changer,
51 update_changer_online_status,
52 file_formats::{
53 MediaLabel,
54 MediaSetLabel,
55 },
56 },
57 };
58
59 #[api(
60 input: {
61 properties: {
62 drive: {
63 schema: DRIVE_NAME_SCHEMA,
64 },
65 slot: {
66 description: "Source slot number",
67 minimum: 1,
68 },
69 },
70 },
71 )]
72 /// Load media via changer from slot
73 pub async fn load_slot(
74 drive: String,
75 slot: u64,
76 _param: Value,
77 ) -> Result<(), Error> {
78
79 let (config, _digest) = config::drive::config()?;
80
81 let drive_config: LinuxTapeDrive = config.lookup("linux", &drive)?;
82
83 let changer: ScsiTapeChanger = match drive_config.changer {
84 Some(ref changer) => config.lookup("changer", changer)?,
85 None => bail!("drive '{}' has no associated changer", drive),
86 };
87
88 tokio::task::spawn_blocking(move || {
89 let drivenum = drive_config.changer_drive_id.unwrap_or(0);
90 mtx_load(&changer.path, slot, drivenum)
91 }).await?
92 }
93
94 #[api(
95 input: {
96 properties: {
97 drive: {
98 schema: DRIVE_NAME_SCHEMA,
99 },
100 "changer-id": {
101 schema: MEDIA_LABEL_SCHEMA,
102 },
103 },
104 },
105 )]
106 /// Load media with specified label
107 ///
108 /// Issue a media load request to the associated changer device.
109 pub async fn load_media(drive: String, changer_id: String) -> Result<(), Error> {
110
111 let (config, _digest) = config::drive::config()?;
112
113 tokio::task::spawn_blocking(move || {
114 let (mut changer, _) = media_changer(&config, &drive, false)?;
115 changer.load_media(&changer_id)
116 }).await?
117 }
118
119 #[api(
120 input: {
121 properties: {
122 drive: {
123 schema: DRIVE_NAME_SCHEMA,
124 },
125 slot: {
126 description: "Target slot number. If omitted, defaults to the slot that the drive was loaded from.",
127 minimum: 1,
128 optional: true,
129 },
130 },
131 },
132 )]
133 /// Unload media via changer
134 pub async fn unload(
135 drive: String,
136 slot: Option<u64>,
137 _param: Value,
138 ) -> Result<(), Error> {
139
140 let (config, _digest) = config::drive::config()?;
141
142 let mut drive_config: LinuxTapeDrive = config.lookup("linux", &drive)?;
143
144 let changer: ScsiTapeChanger = match drive_config.changer {
145 Some(ref changer) => config.lookup("changer", changer)?,
146 None => bail!("drive '{}' has no associated changer", drive),
147 };
148
149 let drivenum = drive_config.changer_drive_id.unwrap_or(0);
150
151 tokio::task::spawn_blocking(move || {
152 if let Some(slot) = slot {
153 mtx_unload(&changer.path, slot, drivenum)
154 } else {
155 drive_config.unload_media()
156 }
157 }).await?
158 }
159
160 #[api(
161 input: {
162 properties: {},
163 },
164 returns: {
165 description: "The list of autodetected tape drives.",
166 type: Array,
167 items: {
168 type: TapeDeviceInfo,
169 },
170 },
171 )]
172 /// Scan tape drives
173 pub fn scan_drives(_param: Value) -> Result<Vec<TapeDeviceInfo>, Error> {
174
175 let list = linux_tape_device_list();
176
177 Ok(list)
178 }
179
180 #[api(
181 input: {
182 properties: {
183 drive: {
184 schema: DRIVE_NAME_SCHEMA,
185 },
186 fast: {
187 description: "Use fast erase.",
188 type: bool,
189 optional: true,
190 default: true,
191 },
192 },
193 },
194 returns: {
195 schema: UPID_SCHEMA,
196 },
197 )]
198 /// Erase media
199 pub fn erase_media(
200 drive: String,
201 fast: Option<bool>,
202 rpcenv: &mut dyn RpcEnvironment,
203 ) -> Result<Value, Error> {
204
205 let (config, _digest) = config::drive::config()?;
206
207 check_drive_exists(&config, &drive)?; // early check before starting worker
208
209 let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
210
211 let upid_str = WorkerTask::new_thread(
212 "erase-media",
213 Some(drive.clone()),
214 auth_id,
215 true,
216 move |_worker| {
217 let mut drive = open_drive(&config, &drive)?;
218 drive.erase_media(fast.unwrap_or(true))?;
219 Ok(())
220 }
221 )?;
222
223 Ok(upid_str.into())
224 }
225
226 #[api(
227 input: {
228 properties: {
229 drive: {
230 schema: DRIVE_NAME_SCHEMA,
231 },
232 },
233 },
234 returns: {
235 schema: UPID_SCHEMA,
236 },
237 )]
238 /// Rewind tape
239 pub fn rewind(
240 drive: String,
241 rpcenv: &mut dyn RpcEnvironment,
242 ) -> Result<Value, Error> {
243
244 let (config, _digest) = config::drive::config()?;
245
246 check_drive_exists(&config, &drive)?; // early check before starting worker
247
248 let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
249
250 let upid_str = WorkerTask::new_thread(
251 "rewind-media",
252 Some(drive.clone()),
253 auth_id,
254 true,
255 move |_worker| {
256 let mut drive = open_drive(&config, &drive)?;
257 drive.rewind()?;
258 Ok(())
259 }
260 )?;
261
262 Ok(upid_str.into())
263 }
264
265 #[api(
266 input: {
267 properties: {
268 drive: {
269 schema: DRIVE_NAME_SCHEMA,
270 },
271 },
272 },
273 )]
274 /// Eject/Unload drive media
275 pub async fn eject_media(drive: String) -> Result<(), Error> {
276
277 let (config, _digest) = config::drive::config()?;
278
279 tokio::task::spawn_blocking(move || {
280 let (mut changer, _) = media_changer(&config, &drive, false)?;
281
282 if !changer.eject_on_unload() {
283 let mut drive = open_drive(&config, &drive)?;
284 drive.eject_media()?;
285 }
286
287 changer.unload_media()
288 }).await?
289 }
290
291 #[api(
292 input: {
293 properties: {
294 drive: {
295 schema: DRIVE_NAME_SCHEMA,
296 },
297 "changer-id": {
298 schema: MEDIA_LABEL_SCHEMA,
299 },
300 pool: {
301 schema: MEDIA_POOL_NAME_SCHEMA,
302 optional: true,
303 },
304 },
305 },
306 returns: {
307 schema: UPID_SCHEMA,
308 },
309 )]
310 /// Label media
311 ///
312 /// Write a new media label to the media in 'drive'. The media is
313 /// assigned to the specified 'pool', or else to the free media pool.
314 ///
315 /// Note: The media need to be empty (you may want to erase it first).
316 pub fn label_media(
317 drive: String,
318 pool: Option<String>,
319 changer_id: String,
320 rpcenv: &mut dyn RpcEnvironment,
321 ) -> Result<Value, Error> {
322
323 let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
324
325 if let Some(ref pool) = pool {
326 let (pool_config, _digest) = config::media_pool::config()?;
327
328 if pool_config.sections.get(pool).is_none() {
329 bail!("no such pool ('{}')", pool);
330 }
331 }
332
333 let (config, _digest) = config::drive::config()?;
334
335 let upid_str = WorkerTask::new_thread(
336 "label-media",
337 Some(drive.clone()),
338 auth_id,
339 true,
340 move |worker| {
341
342 let mut drive = open_drive(&config, &drive)?;
343
344 drive.rewind()?;
345
346 match drive.read_next_file() {
347 Ok(Some(_file)) => bail!("media is not empty (erase first)"),
348 Ok(None) => { /* EOF mark at BOT, assume tape is empty */ },
349 Err(err) => {
350 if err.is_errno(nix::errno::Errno::ENOSPC) || err.is_errno(nix::errno::Errno::EIO) {
351 /* assume tape is empty */
352 } else {
353 bail!("media read error - {}", err);
354 }
355 }
356 }
357
358 let ctime = proxmox::tools::time::epoch_i64();
359 let label = MediaLabel {
360 changer_id: changer_id.to_string(),
361 uuid: Uuid::generate(),
362 ctime,
363 };
364
365 write_media_label(worker, &mut drive, label, pool)
366 }
367 )?;
368
369 Ok(upid_str.into())
370 }
371
372 fn write_media_label(
373 worker: Arc<WorkerTask>,
374 drive: &mut Box<dyn TapeDriver>,
375 label: MediaLabel,
376 pool: Option<String>,
377 ) -> Result<(), Error> {
378
379 drive.label_tape(&label)?;
380
381 let mut media_set_label = None;
382
383 if let Some(ref pool) = pool {
384 // assign media to pool by writing special media set label
385 worker.log(format!("Label media '{}' for pool '{}'", label.changer_id, pool));
386 let set = MediaSetLabel::with_data(&pool, [0u8; 16].into(), 0, label.ctime);
387
388 drive.write_media_set_label(&set)?;
389 media_set_label = Some(set);
390 } else {
391 worker.log(format!("Label media '{}' (no pool assignment)", label.changer_id));
392 }
393
394 let media_id = MediaId { label, media_set_label };
395
396 let mut inventory = Inventory::load(Path::new(TAPE_STATUS_DIR))?;
397 inventory.store(media_id.clone())?;
398
399 drive.rewind()?;
400
401 match drive.read_label() {
402 Ok(Some(info)) => {
403 if info.label.uuid != media_id.label.uuid {
404 bail!("verify label failed - got wrong label uuid");
405 }
406 if let Some(ref pool) = pool {
407 match info.media_set_label {
408 Some(set) => {
409 if set.uuid != [0u8; 16].into() {
410 bail!("verify media set label failed - got wrong set uuid");
411 }
412 if &set.pool != pool {
413 bail!("verify media set label failed - got wrong pool");
414 }
415 }
416 None => {
417 bail!("verify media set label failed (missing set label)");
418 }
419 }
420 }
421 },
422 Ok(None) => bail!("verify label failed (got empty media)"),
423 Err(err) => bail!("verify label failed - {}", err),
424 };
425
426 drive.rewind()?;
427
428 Ok(())
429 }
430
431 #[api(
432 input: {
433 properties: {
434 drive: {
435 schema: DRIVE_NAME_SCHEMA,
436 },
437 },
438 },
439 returns: {
440 type: MediaIdFlat,
441 },
442 )]
443 /// Read media label
444 pub async fn read_label(drive: String) -> Result<MediaIdFlat, Error> {
445
446 let (config, _digest) = config::drive::config()?;
447
448 tokio::task::spawn_blocking(move || {
449 let mut drive = open_drive(&config, &drive)?;
450
451 let media_id = drive.read_label()?;
452
453 let media_id = match media_id {
454 Some(media_id) => {
455 let mut flat = MediaIdFlat {
456 uuid: media_id.label.uuid.to_string(),
457 changer_id: media_id.label.changer_id.clone(),
458 ctime: media_id.label.ctime,
459 media_set_ctime: None,
460 media_set_uuid: None,
461 pool: None,
462 seq_nr: None,
463 };
464 if let Some(set) = media_id.media_set_label {
465 flat.pool = Some(set.pool.clone());
466 flat.seq_nr = Some(set.seq_nr);
467 flat.media_set_uuid = Some(set.uuid.to_string());
468 flat.media_set_ctime = Some(set.ctime);
469 }
470 flat
471 }
472 None => {
473 bail!("Media is empty (no label).");
474 }
475 };
476
477 Ok(media_id)
478 }).await?
479 }
480
481 #[api(
482 input: {
483 properties: {
484 drive: {
485 schema: DRIVE_NAME_SCHEMA,
486 },
487 },
488 },
489 returns: {
490 description: "The list of media labels with associated media Uuid (if any).",
491 type: Array,
492 items: {
493 type: LabelUuidMap,
494 },
495 },
496 )]
497 /// List known media labels (Changer Inventory)
498 ///
499 /// Note: Only useful for drives with associated changer device.
500 ///
501 /// This method queries the changer to get a list of media labels.
502 ///
503 /// Note: This updates the media online status.
504 pub async fn inventory(
505 drive: String,
506 ) -> Result<Vec<LabelUuidMap>, Error> {
507
508 let (config, _digest) = config::drive::config()?;
509
510 tokio::task::spawn_blocking(move || {
511 let (changer, changer_name) = media_changer(&config, &drive, false)?;
512
513 let changer_id_list = changer.list_media_changer_ids()?;
514
515 let state_path = Path::new(TAPE_STATUS_DIR);
516
517 let mut inventory = Inventory::load(state_path)?;
518 let mut state_db = MediaStateDatabase::load(state_path)?;
519
520 update_changer_online_status(
521 &config,
522 &mut inventory,
523 &mut state_db,
524 &changer_name,
525 &changer_id_list,
526 )?;
527
528 let mut list = Vec::new();
529
530 for changer_id in changer_id_list.iter() {
531 if changer_id.starts_with("CLN") {
532 // skip cleaning unit
533 continue;
534 }
535
536 let changer_id = changer_id.to_string();
537
538 if let Some(media_id) = inventory.find_media_by_changer_id(&changer_id) {
539 list.push(LabelUuidMap { changer_id, uuid: Some(media_id.label.uuid.to_string()) });
540 } else {
541 list.push(LabelUuidMap { changer_id, uuid: None });
542 }
543 }
544
545 Ok(list)
546 }).await?
547 }
548
549 #[api(
550 input: {
551 properties: {
552 drive: {
553 schema: DRIVE_NAME_SCHEMA,
554 },
555 "read-all-labels": {
556 description: "Load all tapes and try read labels (even if already inventoried)",
557 type: bool,
558 optional: true,
559 },
560 },
561 },
562 returns: {
563 schema: UPID_SCHEMA,
564 },
565 )]
566 /// Update inventory
567 ///
568 /// Note: Only useful for drives with associated changer device.
569 ///
570 /// This method queries the changer to get a list of media labels. It
571 /// then loads any unknown media into the drive, reads the label, and
572 /// store the result to the media database.
573 ///
574 /// Note: This updates the media online status.
575 pub fn update_inventory(
576 drive: String,
577 read_all_labels: Option<bool>,
578 rpcenv: &mut dyn RpcEnvironment,
579 ) -> Result<Value, Error> {
580
581 let (config, _digest) = config::drive::config()?;
582
583 check_drive_exists(&config, &drive)?; // early check before starting worker
584
585 let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
586
587 let upid_str = WorkerTask::new_thread(
588 "inventory-update",
589 Some(drive.clone()),
590 auth_id,
591 true,
592 move |worker| {
593
594 let (mut changer, changer_name) = media_changer(&config, &drive, false)?;
595
596 let changer_id_list = changer.list_media_changer_ids()?;
597 if changer_id_list.is_empty() {
598 worker.log(format!("changer device does not list any media labels"));
599 }
600
601 let state_path = Path::new(TAPE_STATUS_DIR);
602
603 let mut inventory = Inventory::load(state_path)?;
604 let mut state_db = MediaStateDatabase::load(state_path)?;
605
606 update_changer_online_status(&config, &mut inventory, &mut state_db, &changer_name, &changer_id_list)?;
607
608 for changer_id in changer_id_list.iter() {
609 if changer_id.starts_with("CLN") {
610 worker.log(format!("skip cleaning unit '{}'", changer_id));
611 continue;
612 }
613
614 let changer_id = changer_id.to_string();
615
616 if !read_all_labels.unwrap_or(false) {
617 if let Some(_) = inventory.find_media_by_changer_id(&changer_id) {
618 worker.log(format!("media '{}' already inventoried", changer_id));
619 continue;
620 }
621 }
622
623 if let Err(err) = changer.load_media(&changer_id) {
624 worker.warn(format!("unable to load media '{}' - {}", changer_id, err));
625 continue;
626 }
627
628 let mut drive = open_drive(&config, &drive)?;
629 match drive.read_label() {
630 Err(err) => {
631 worker.warn(format!("unable to read label form media '{}' - {}", changer_id, err));
632 }
633 Ok(None) => {
634 worker.log(format!("media '{}' is empty", changer_id));
635 }
636 Ok(Some(media_id)) => {
637 if changer_id != media_id.label.changer_id {
638 worker.warn(format!("label changer ID missmatch ({} != {})", changer_id, media_id.label.changer_id));
639 continue;
640 }
641 worker.log(format!("inventorize media '{}' with uuid '{}'", changer_id, media_id.label.uuid));
642 inventory.store(media_id)?;
643 }
644 }
645 }
646 Ok(())
647 }
648 )?;
649
650 Ok(upid_str.into())
651 }
652
653
654 #[api(
655 input: {
656 properties: {
657 drive: {
658 schema: DRIVE_NAME_SCHEMA,
659 },
660 pool: {
661 schema: MEDIA_POOL_NAME_SCHEMA,
662 optional: true,
663 },
664 },
665 },
666 returns: {
667 schema: UPID_SCHEMA,
668 },
669 )]
670 /// Label media with barcodes from changer device
671 pub fn barcode_label_media(
672 drive: String,
673 pool: Option<String>,
674 rpcenv: &mut dyn RpcEnvironment,
675 ) -> Result<Value, Error> {
676
677 if let Some(ref pool) = pool {
678 let (pool_config, _digest) = config::media_pool::config()?;
679
680 if pool_config.sections.get(pool).is_none() {
681 bail!("no such pool ('{}')", pool);
682 }
683 }
684
685 let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
686
687 let upid_str = WorkerTask::new_thread(
688 "barcode-label-media",
689 Some(drive.clone()),
690 auth_id,
691 true,
692 move |worker| {
693 barcode_label_media_worker(worker, drive, pool)
694 }
695 )?;
696
697 Ok(upid_str.into())
698 }
699
700 fn barcode_label_media_worker(
701 worker: Arc<WorkerTask>,
702 drive: String,
703 pool: Option<String>,
704 ) -> Result<(), Error> {
705
706 let (config, _digest) = config::drive::config()?;
707
708 let (mut changer, changer_name) = media_changer(&config, &drive, false)?;
709
710 let changer_id_list = changer.list_media_changer_ids()?;
711
712 let state_path = Path::new(TAPE_STATUS_DIR);
713
714 let mut inventory = Inventory::load(state_path)?;
715 let mut state_db = MediaStateDatabase::load(state_path)?;
716
717 update_changer_online_status(&config, &mut inventory, &mut state_db, &changer_name, &changer_id_list)?;
718
719 if changer_id_list.is_empty() {
720 bail!("changer device does not list any media labels");
721 }
722
723 for changer_id in changer_id_list {
724 if changer_id.starts_with("CLN") { continue; }
725
726 inventory.reload()?;
727 if inventory.find_media_by_changer_id(&changer_id).is_some() {
728 worker.log(format!("media '{}' already inventoried (already labeled)", changer_id));
729 continue;
730 }
731
732 worker.log(format!("checking/loading media '{}'", changer_id));
733
734 if let Err(err) = changer.load_media(&changer_id) {
735 worker.warn(format!("unable to load media '{}' - {}", changer_id, err));
736 continue;
737 }
738
739 let mut drive = open_drive(&config, &drive)?;
740 drive.rewind()?;
741
742 match drive.read_next_file() {
743 Ok(Some(_file)) => {
744 worker.log(format!("media '{}' is not empty (erase first)", changer_id));
745 continue;
746 }
747 Ok(None) => { /* EOF mark at BOT, assume tape is empty */ },
748 Err(err) => {
749 if err.is_errno(nix::errno::Errno::ENOSPC) || err.is_errno(nix::errno::Errno::EIO) {
750 /* assume tape is empty */
751 } else {
752 worker.warn(format!("media '{}' read error (maybe not empty - erase first)", changer_id));
753 continue;
754 }
755 }
756 }
757
758 let ctime = proxmox::tools::time::epoch_i64();
759 let label = MediaLabel {
760 changer_id: changer_id.to_string(),
761 uuid: Uuid::generate(),
762 ctime,
763 };
764
765 write_media_label(worker.clone(), &mut drive, label, pool.clone())?
766 }
767
768 Ok(())
769 }
770
771 #[sortable]
772 pub const SUBDIRS: SubdirMap = &sorted!([
773 (
774 "barcode-label-media",
775 &Router::new()
776 .put(&API_METHOD_BARCODE_LABEL_MEDIA)
777 ),
778 (
779 "eject-media",
780 &Router::new()
781 .put(&API_METHOD_EJECT_MEDIA)
782 ),
783 (
784 "erase-media",
785 &Router::new()
786 .put(&API_METHOD_ERASE_MEDIA)
787 ),
788 (
789 "inventory",
790 &Router::new()
791 .get(&API_METHOD_INVENTORY)
792 .put(&API_METHOD_UPDATE_INVENTORY)
793 ),
794 (
795 "label-media",
796 &Router::new()
797 .put(&API_METHOD_LABEL_MEDIA)
798 ),
799 (
800 "load-slot",
801 &Router::new()
802 .put(&API_METHOD_LOAD_SLOT)
803 ),
804 (
805 "read-label",
806 &Router::new()
807 .get(&API_METHOD_READ_LABEL)
808 ),
809 (
810 "rewind",
811 &Router::new()
812 .put(&API_METHOD_REWIND)
813 ),
814 (
815 "scan",
816 &Router::new()
817 .get(&API_METHOD_SCAN_DRIVES)
818 ),
819 (
820 "unload",
821 &Router::new()
822 .put(&API_METHOD_UNLOAD)
823 ),
824 ]);
825
826 pub const ROUTER: Router = Router::new()
827 .get(&list_subdirs_api_method!(SUBDIRS))
828 .subdirs(SUBDIRS);