]> git.proxmox.com Git - proxmox-backup.git/blob - src/bin/proxmox-tape.rs
tape: add volume_mounts and medium_passes to LinuxDriveAndMediaStatus
[proxmox-backup.git] / src / bin / proxmox-tape.rs
1 use anyhow::{format_err, Error};
2 use serde_json::{json, Value};
3
4 use proxmox::{
5 api::{
6 api,
7 cli::*,
8 ApiHandler,
9 RpcEnvironment,
10 section_config::SectionConfigData,
11 },
12 tools::{
13 time::strftime_local,
14 io::ReadExt,
15 },
16 };
17
18 use proxmox_backup::{
19 tools::format::{
20 HumanByte,
21 render_epoch,
22 render_bytes_human_readable,
23 },
24 server::{
25 UPID,
26 worker_is_active_local,
27 },
28 api2::{
29 self,
30 types::{
31 DATASTORE_SCHEMA,
32 DRIVE_NAME_SCHEMA,
33 MEDIA_LABEL_SCHEMA,
34 MEDIA_POOL_NAME_SCHEMA,
35 },
36 },
37 config::{
38 self,
39 datastore::complete_datastore_name,
40 drive::complete_drive_name,
41 media_pool::complete_pool_name,
42 },
43 tape::{
44 open_drive,
45 complete_media_changer_id,
46 complete_media_set_uuid,
47 file_formats::{
48 PROXMOX_BACKUP_CONTENT_HEADER_MAGIC_1_0,
49 PROXMOX_BACKUP_CONTENT_NAME,
50 MediaContentHeader,
51 },
52 },
53 };
54
55 mod proxmox_tape;
56 use proxmox_tape::*;
57
58 // Note: local workers should print logs to stdout, so there is no need
59 // to fetch/display logs. We just wait for the worker to finish.
60 pub async fn wait_for_local_worker(upid_str: &str) -> Result<(), Error> {
61
62 let upid: UPID = upid_str.parse()?;
63
64 let sleep_duration = core::time::Duration::new(0, 100_000_000);
65
66 loop {
67 if worker_is_active_local(&upid) {
68 tokio::time::delay_for(sleep_duration).await;
69 } else {
70 break;
71 }
72 }
73 Ok(())
74 }
75
76 fn lookup_drive_name(
77 param: &Value,
78 config: &SectionConfigData,
79 ) -> Result<String, Error> {
80
81 let drive = param["drive"]
82 .as_str()
83 .map(String::from)
84 .or_else(|| std::env::var("PROXMOX_TAPE_DRIVE").ok())
85 .or_else(|| {
86
87 let mut drive_names = Vec::new();
88
89 for (name, (section_type, _)) in config.sections.iter() {
90
91 if !(section_type == "linux" || section_type == "virtual") { continue; }
92 drive_names.push(name);
93 }
94
95 if drive_names.len() == 1 {
96 Some(drive_names[0].to_owned())
97 } else {
98 None
99 }
100 })
101 .ok_or_else(|| format_err!("unable to get (default) drive name"))?;
102
103 Ok(drive)
104 }
105
106 #[api(
107 input: {
108 properties: {
109 drive: {
110 schema: DRIVE_NAME_SCHEMA,
111 optional: true,
112 },
113 fast: {
114 description: "Use fast erase.",
115 type: bool,
116 optional: true,
117 default: true,
118 },
119 },
120 },
121 )]
122 /// Erase media
123 async fn erase_media(
124 mut param: Value,
125 rpcenv: &mut dyn RpcEnvironment,
126 ) -> Result<(), Error> {
127
128 let (config, _digest) = config::drive::config()?;
129
130 param["drive"] = lookup_drive_name(&param, &config)?.into();
131
132 let info = &api2::tape::drive::API_METHOD_ERASE_MEDIA;
133
134 let result = match info.handler {
135 ApiHandler::Sync(handler) => (handler)(param, info, rpcenv)?,
136 _ => unreachable!(),
137 };
138
139 wait_for_local_worker(result.as_str().unwrap()).await?;
140
141 Ok(())
142 }
143
144 #[api(
145 input: {
146 properties: {
147 drive: {
148 schema: DRIVE_NAME_SCHEMA,
149 optional: true,
150 },
151 },
152 },
153 )]
154 /// Rewind tape
155 async fn rewind(
156 mut param: Value,
157 rpcenv: &mut dyn RpcEnvironment,
158 ) -> Result<(), Error> {
159
160 let (config, _digest) = config::drive::config()?;
161
162 param["drive"] = lookup_drive_name(&param, &config)?.into();
163
164 let info = &api2::tape::drive::API_METHOD_REWIND;
165
166 let result = match info.handler {
167 ApiHandler::Sync(handler) => (handler)(param, info, rpcenv)?,
168 _ => unreachable!(),
169 };
170
171 wait_for_local_worker(result.as_str().unwrap()).await?;
172
173 Ok(())
174 }
175
176 #[api(
177 input: {
178 properties: {
179 drive: {
180 schema: DRIVE_NAME_SCHEMA,
181 optional: true,
182 },
183 },
184 },
185 )]
186 /// Eject/Unload drive media
187 async fn eject_media(
188 mut param: Value,
189 rpcenv: &mut dyn RpcEnvironment,
190 ) -> Result<(), Error> {
191
192 let (config, _digest) = config::drive::config()?;
193
194 param["drive"] = lookup_drive_name(&param, &config)?.into();
195
196 let info = &api2::tape::drive::API_METHOD_EJECT_MEDIA;
197
198 match info.handler {
199 ApiHandler::Async(handler) => (handler)(param, info, rpcenv).await?,
200 _ => unreachable!(),
201 };
202
203 Ok(())
204 }
205
206 #[api(
207 input: {
208 properties: {
209 drive: {
210 schema: DRIVE_NAME_SCHEMA,
211 optional: true,
212 },
213 "changer-id": {
214 schema: MEDIA_LABEL_SCHEMA,
215 },
216 },
217 },
218 )]
219 /// Load media
220 async fn load_media(
221 mut param: Value,
222 rpcenv: &mut dyn RpcEnvironment,
223 ) -> Result<(), Error> {
224
225 let (config, _digest) = config::drive::config()?;
226
227 param["drive"] = lookup_drive_name(&param, &config)?.into();
228
229 let info = &api2::tape::drive::API_METHOD_LOAD_MEDIA;
230
231 match info.handler {
232 ApiHandler::Async(handler) => (handler)(param, info, rpcenv).await?,
233 _ => unreachable!(),
234 };
235
236 Ok(())
237 }
238
239 #[api(
240 input: {
241 properties: {
242 pool: {
243 schema: MEDIA_POOL_NAME_SCHEMA,
244 optional: true,
245 },
246 drive: {
247 schema: DRIVE_NAME_SCHEMA,
248 optional: true,
249 },
250 "changer-id": {
251 schema: MEDIA_LABEL_SCHEMA,
252 },
253 },
254 },
255 )]
256 /// Label media
257 async fn label_media(
258 mut param: Value,
259 rpcenv: &mut dyn RpcEnvironment,
260 ) -> Result<(), Error> {
261
262 let (config, _digest) = config::drive::config()?;
263
264 param["drive"] = lookup_drive_name(&param, &config)?.into();
265
266 let info = &api2::tape::drive::API_METHOD_LABEL_MEDIA;
267
268 let result = match info.handler {
269 ApiHandler::Sync(handler) => (handler)(param, info, rpcenv)?,
270 _ => unreachable!(),
271 };
272
273 wait_for_local_worker(result.as_str().unwrap()).await?;
274
275 Ok(())
276 }
277
278 #[api(
279 input: {
280 properties: {
281 drive: {
282 schema: DRIVE_NAME_SCHEMA,
283 optional: true,
284 },
285 "output-format": {
286 schema: OUTPUT_FORMAT,
287 optional: true,
288 },
289 },
290 },
291 )]
292 /// Read media label
293 async fn read_label(
294 mut param: Value,
295 rpcenv: &mut dyn RpcEnvironment,
296 ) -> Result<(), Error> {
297
298 let (config, _digest) = config::drive::config()?;
299
300 param["drive"] = lookup_drive_name(&param, &config)?.into();
301
302 let output_format = get_output_format(&param);
303 let info = &api2::tape::drive::API_METHOD_READ_LABEL;
304 let mut data = match info.handler {
305 ApiHandler::Async(handler) => (handler)(param, info, rpcenv).await?,
306 _ => unreachable!(),
307 };
308
309 let options = default_table_format_options()
310 .column(ColumnConfig::new("changer-id"))
311 .column(ColumnConfig::new("uuid"))
312 .column(ColumnConfig::new("ctime").renderer(render_epoch))
313 .column(ColumnConfig::new("pool"))
314 .column(ColumnConfig::new("media-set-uuid"))
315 .column(ColumnConfig::new("media-set-ctime").renderer(render_epoch))
316 ;
317
318 format_and_print_result_full(&mut data, &info.returns, &output_format, &options);
319
320 Ok(())
321 }
322
323 #[api(
324 input: {
325 properties: {
326 "output-format": {
327 schema: OUTPUT_FORMAT,
328 optional: true,
329 },
330 drive: {
331 schema: DRIVE_NAME_SCHEMA,
332 optional: true,
333 },
334 "read-labels": {
335 description: "Load unknown tapes and try read labels",
336 type: bool,
337 optional: true,
338 },
339 "read-all-labels": {
340 description: "Load all tapes and try read labels (even if already inventoried)",
341 type: bool,
342 optional: true,
343 },
344 },
345 },
346 )]
347 /// List (and update) media labels (Changer Inventory)
348 async fn inventory(
349 read_labels: Option<bool>,
350 read_all_labels: Option<bool>,
351 param: Value,
352 rpcenv: &mut dyn RpcEnvironment,
353 ) -> Result<(), Error> {
354
355 let output_format = get_output_format(&param);
356
357 let (config, _digest) = config::drive::config()?;
358 let drive = lookup_drive_name(&param, &config)?;
359
360 let do_read = read_labels.unwrap_or(false) || read_all_labels.unwrap_or(false);
361
362 if do_read {
363 let mut param = json!({
364 "drive": &drive,
365 });
366 if let Some(true) = read_all_labels {
367 param["read-all-labels"] = true.into();
368 }
369 let info = &api2::tape::drive::API_METHOD_UPDATE_INVENTORY;
370 let result = match info.handler {
371 ApiHandler::Sync(handler) => (handler)(param, info, rpcenv)?,
372 _ => unreachable!(),
373 };
374 wait_for_local_worker(result.as_str().unwrap()).await?;
375 }
376
377 let info = &api2::tape::drive::API_METHOD_INVENTORY;
378
379 let param = json!({ "drive": &drive });
380 let mut data = match info.handler {
381 ApiHandler::Async(handler) => (handler)(param, info, rpcenv).await?,
382 _ => unreachable!(),
383 };
384
385 let options = default_table_format_options()
386 .column(ColumnConfig::new("changer-id"))
387 .column(ColumnConfig::new("uuid"))
388 ;
389
390 format_and_print_result_full(&mut data, &info.returns, &output_format, &options);
391
392 Ok(())
393 }
394
395 #[api(
396 input: {
397 properties: {
398 pool: {
399 schema: MEDIA_POOL_NAME_SCHEMA,
400 optional: true,
401 },
402 drive: {
403 schema: DRIVE_NAME_SCHEMA,
404 optional: true,
405 },
406 },
407 },
408 )]
409 /// Label media with barcodes from changer device
410 async fn barcode_label_media(
411 mut param: Value,
412 rpcenv: &mut dyn RpcEnvironment,
413 ) -> Result<(), Error> {
414
415 let (config, _digest) = config::drive::config()?;
416
417 param["drive"] = lookup_drive_name(&param, &config)?.into();
418
419 let info = &api2::tape::drive::API_METHOD_BARCODE_LABEL_MEDIA;
420
421 let result = match info.handler {
422 ApiHandler::Sync(handler) => (handler)(param, info, rpcenv)?,
423 _ => unreachable!(),
424 };
425
426 wait_for_local_worker(result.as_str().unwrap()).await?;
427
428 Ok(())
429 }
430
431 #[api(
432 input: {
433 properties: {
434 drive: {
435 schema: DRIVE_NAME_SCHEMA,
436 optional: true,
437 },
438 },
439 },
440 )]
441 /// Move to end of media (MTEOM, used to debug)
442 fn move_to_eom(param: Value) -> Result<(), Error> {
443
444 let (config, _digest) = config::drive::config()?;
445
446 let drive = lookup_drive_name(&param, &config)?;
447 let mut drive = open_drive(&config, &drive)?;
448
449 drive.move_to_eom()?;
450
451 Ok(())
452 }
453
454 #[api(
455 input: {
456 properties: {
457 drive: {
458 schema: DRIVE_NAME_SCHEMA,
459 optional: true,
460 },
461 },
462 },
463 )]
464 /// Rewind, then read media contents and print debug info
465 ///
466 /// Note: This reads unless the driver returns an IO Error, so this
467 /// method is expected to fails when we reach EOT.
468 fn debug_scan(param: Value) -> Result<(), Error> {
469
470 let (config, _digest) = config::drive::config()?;
471
472 let drive = lookup_drive_name(&param, &config)?;
473 let mut drive = open_drive(&config, &drive)?;
474
475 println!("rewinding tape");
476 drive.rewind()?;
477
478 loop {
479 let file_number = drive.current_file_number()?;
480
481 match drive.read_next_file()? {
482 None => {
483 println!("EOD");
484 continue;
485 },
486 Some(mut reader) => {
487 println!("got file number {}", file_number);
488
489 let header: Result<MediaContentHeader, _> = unsafe { reader.read_le_value() };
490 match header {
491 Ok(header) => {
492 if header.magic != PROXMOX_BACKUP_CONTENT_HEADER_MAGIC_1_0 {
493 println!("got MediaContentHeader with wrong magic: {:?}", header.magic);
494 } else {
495 if let Some(name) = PROXMOX_BACKUP_CONTENT_NAME.get(&header.content_magic) {
496 println!("got content header: {}", name);
497 println!(" uuid: {}", header.content_uuid());
498 println!(" ctime: {}", strftime_local("%c", header.ctime)?);
499 println!(" hsize: {}", HumanByte::from(header.size as usize));
500 println!(" part: {}", header.part_number);
501 } else {
502 println!("got unknown content header: {:?}", header.content_magic);
503 }
504 }
505 }
506 Err(err) => {
507 println!("unable to read content header - {}", err);
508 }
509 }
510 let bytes = reader.skip_to_end()?;
511 println!("skipped {}", HumanByte::from(bytes));
512 }
513 }
514 }
515 }
516
517 #[api(
518 input: {
519 properties: {
520 drive: {
521 schema: DRIVE_NAME_SCHEMA,
522 optional: true,
523 },
524 "output-format": {
525 schema: OUTPUT_FORMAT,
526 optional: true,
527 },
528 },
529 },
530 )]
531 /// Read Cartridge Memory (Medium auxiliary memory attributes)
532 fn cartridge_memory(
533 mut param: Value,
534 rpcenv: &mut dyn RpcEnvironment,
535 ) -> Result<(), Error> {
536
537 let (config, _digest) = config::drive::config()?;
538
539 param["drive"] = lookup_drive_name(&param, &config)?.into();
540
541 let output_format = get_output_format(&param);
542 let info = &api2::tape::drive::API_METHOD_CARTRIDGE_MEMORY;
543
544 let mut data = match info.handler {
545 ApiHandler::Sync(handler) => (handler)(param, info, rpcenv)?,
546 _ => unreachable!(),
547 };
548
549 let options = default_table_format_options()
550 .column(ColumnConfig::new("id"))
551 .column(ColumnConfig::new("name"))
552 .column(ColumnConfig::new("value"))
553 ;
554
555 format_and_print_result_full(&mut data, &info.returns, &output_format, &options);
556 Ok(())
557 }
558
559 #[api(
560 input: {
561 properties: {
562 drive: {
563 schema: DRIVE_NAME_SCHEMA,
564 optional: true,
565 },
566 "output-format": {
567 schema: OUTPUT_FORMAT,
568 optional: true,
569 },
570 },
571 },
572 )]
573 /// Get drive/media status
574 fn status(
575 mut param: Value,
576 rpcenv: &mut dyn RpcEnvironment,
577 ) -> Result<(), Error> {
578
579 let (config, _digest) = config::drive::config()?;
580
581 param["drive"] = lookup_drive_name(&param, &config)?.into();
582
583 let output_format = get_output_format(&param);
584 let info = &api2::tape::drive::API_METHOD_STATUS;
585
586 let mut data = match info.handler {
587 ApiHandler::Sync(handler) => (handler)(param, info, rpcenv)?,
588 _ => unreachable!(),
589 };
590
591 let options = default_table_format_options()
592 .column(ColumnConfig::new("blocksize"))
593 .column(ColumnConfig::new("density"))
594 .column(ColumnConfig::new("status"))
595 .column(ColumnConfig::new("alert-flags"))
596 .column(ColumnConfig::new("file-number"))
597 .column(ColumnConfig::new("block-number"))
598 .column(ColumnConfig::new("manufactured").renderer(render_epoch))
599 .column(ColumnConfig::new("bytes-written").renderer(render_bytes_human_readable))
600 .column(ColumnConfig::new("bytes-read").renderer(render_bytes_human_readable))
601 .column(ColumnConfig::new("medium-passes"))
602 .column(ColumnConfig::new("volume-mounts"))
603 ;
604
605 format_and_print_result_full(&mut data, &info.returns, &output_format, &options);
606 Ok(())
607 }
608
609 #[api(
610 input: {
611 properties: {
612 store: {
613 schema: DATASTORE_SCHEMA,
614 },
615 pool: {
616 schema: MEDIA_POOL_NAME_SCHEMA,
617 },
618 },
619 },
620 )]
621 /// Backup datastore to tape media pool
622 async fn backup(
623 param: Value,
624 rpcenv: &mut dyn RpcEnvironment,
625 ) -> Result<(), Error> {
626
627 let info = &api2::tape::backup::API_METHOD_BACKUP;
628
629 let result = match info.handler {
630 ApiHandler::Sync(handler) => (handler)(param, info, rpcenv)?,
631 _ => unreachable!(),
632 };
633
634 wait_for_local_worker(result.as_str().unwrap()).await?;
635
636 Ok(())
637 }
638 #[api(
639 input: {
640 properties: {
641 store: {
642 schema: DATASTORE_SCHEMA,
643 },
644 "media-set": {
645 description: "Media set UUID.",
646 type: String,
647 },
648 },
649 },
650 )]
651 /// Restore data from media-set
652 async fn restore(
653 param: Value,
654 rpcenv: &mut dyn RpcEnvironment,
655 ) -> Result<(), Error> {
656
657 let info = &api2::tape::restore::API_METHOD_RESTORE;
658
659 let result = match info.handler {
660 ApiHandler::Sync(handler) => (handler)(param, info, rpcenv)?,
661 _ => unreachable!(),
662 };
663
664 wait_for_local_worker(result.as_str().unwrap()).await?;
665
666 Ok(())
667 }
668
669 #[api(
670 input: {
671 properties: {
672 drive: {
673 schema: DRIVE_NAME_SCHEMA,
674 optional: true,
675 },
676 force: {
677 description: "Force overriding existing index.",
678 type: bool,
679 optional: true,
680 },
681 verbose: {
682 description: "Verbose mode - log all found chunks.",
683 type: bool,
684 optional: true,
685 },
686 "output-format": {
687 schema: OUTPUT_FORMAT,
688 optional: true,
689 },
690 },
691 },
692 )]
693 /// Scan media and record content
694 async fn catalog_media(
695 mut param: Value,
696 rpcenv: &mut dyn RpcEnvironment,
697 ) -> Result<(), Error> {
698
699 let (config, _digest) = config::drive::config()?;
700
701 param["drive"] = lookup_drive_name(&param, &config)?.into();
702
703 let info = &api2::tape::drive::API_METHOD_CATALOG_MEDIA;
704
705 let result = match info.handler {
706 ApiHandler::Sync(handler) => (handler)(param, info, rpcenv)?,
707 _ => unreachable!(),
708 };
709
710 wait_for_local_worker(result.as_str().unwrap()).await?;
711
712 Ok(())
713 }
714
715 fn main() {
716
717 let cmd_def = CliCommandMap::new()
718 .insert(
719 "backup",
720 CliCommand::new(&API_METHOD_BACKUP)
721 .arg_param(&["store", "pool"])
722 .completion_cb("store", complete_datastore_name)
723 .completion_cb("pool", complete_pool_name)
724 )
725 .insert(
726 "restore",
727 CliCommand::new(&API_METHOD_RESTORE)
728 .arg_param(&["media-set", "store"])
729 .completion_cb("store", complete_datastore_name)
730 .completion_cb("media-set", complete_media_set_uuid)
731 )
732 .insert(
733 "barcode-label",
734 CliCommand::new(&API_METHOD_BARCODE_LABEL_MEDIA)
735 .completion_cb("drive", complete_drive_name)
736 .completion_cb("pool", complete_pool_name)
737 )
738 .insert(
739 "rewind",
740 CliCommand::new(&API_METHOD_REWIND)
741 .completion_cb("drive", complete_drive_name)
742 )
743 .insert(
744 "scan",
745 CliCommand::new(&API_METHOD_DEBUG_SCAN)
746 .completion_cb("drive", complete_drive_name)
747 )
748 .insert(
749 "status",
750 CliCommand::new(&API_METHOD_STATUS)
751 .completion_cb("drive", complete_drive_name)
752 )
753 .insert(
754 "eod",
755 CliCommand::new(&API_METHOD_MOVE_TO_EOM)
756 .completion_cb("drive", complete_drive_name)
757 )
758 .insert(
759 "erase",
760 CliCommand::new(&API_METHOD_ERASE_MEDIA)
761 .completion_cb("drive", complete_drive_name)
762 )
763 .insert(
764 "eject",
765 CliCommand::new(&API_METHOD_EJECT_MEDIA)
766 .completion_cb("drive", complete_drive_name)
767 )
768 .insert(
769 "inventory",
770 CliCommand::new(&API_METHOD_INVENTORY)
771 .completion_cb("drive", complete_drive_name)
772 )
773 .insert(
774 "read-label",
775 CliCommand::new(&API_METHOD_READ_LABEL)
776 .completion_cb("drive", complete_drive_name)
777 )
778 .insert(
779 "catalog",
780 CliCommand::new(&API_METHOD_CATALOG_MEDIA)
781 .completion_cb("drive", complete_drive_name)
782 )
783 .insert(
784 "cartridge-memory",
785 CliCommand::new(&API_METHOD_CARTRIDGE_MEMORY)
786 .completion_cb("drive", complete_drive_name)
787 )
788 .insert(
789 "label",
790 CliCommand::new(&API_METHOD_LABEL_MEDIA)
791 .completion_cb("drive", complete_drive_name)
792 .completion_cb("pool", complete_pool_name)
793
794 )
795 .insert("changer", changer_commands())
796 .insert("drive", drive_commands())
797 .insert("pool", pool_commands())
798 .insert("media", media_commands())
799 .insert(
800 "load-media",
801 CliCommand::new(&API_METHOD_LOAD_MEDIA)
802 .arg_param(&["changer-id"])
803 .completion_cb("drive", complete_drive_name)
804 .completion_cb("changer-id", complete_media_changer_id)
805 )
806 ;
807
808 let mut rpcenv = CliEnvironment::new();
809 rpcenv.set_auth_id(Some(String::from("root@pam")));
810
811 proxmox_backup::tools::runtime::main(run_async_cli_command(cmd_def, rpcenv));
812 }