]> git.proxmox.com Git - proxmox-backup.git/blob - pbs-tape/src/bin/pmt.rs
update to first proxmox crate split
[proxmox-backup.git] / pbs-tape / src / bin / pmt.rs
1 /// Control magnetic tape drive operation
2 ///
3 /// This is a Rust implementation, using the Proxmox userspace tape
4 /// driver. This is meant as replacement fot the 'mt' command line
5 /// tool.
6 ///
7 /// Features:
8 ///
9 /// - written in Rust
10 /// - use Proxmox userspace driver (using SG_IO)
11 /// - optional json output format
12 /// - support tape alert flags
13 /// - support volume statistics
14 /// - read cartridge memory
15
16 use std::convert::TryInto;
17
18 use anyhow::{bail, Error};
19 use serde_json::Value;
20
21 use proxmox_schema::{api, ArraySchema, IntegerSchema, Schema, StringSchema};
22 use proxmox_router::cli::*;
23 use proxmox_router::RpcEnvironment;
24
25 use pbs_api_types::{
26 LTO_DRIVE_PATH_SCHEMA, DRIVE_NAME_SCHEMA, LtoTapeDrive,
27 };
28 use pbs_config::drive::complete_drive_name;
29 use pbs_tape::{
30 sg_tape::SgTape,
31 linux_list_drives::{complete_drive_path, lto_tape_device_list, open_lto_tape_device},
32 };
33
34 pub const FILE_MARK_COUNT_SCHEMA: Schema =
35 IntegerSchema::new("File mark count.")
36 .minimum(1)
37 .maximum(i32::MAX as isize)
38 .schema();
39
40 pub const FILE_MARK_POSITION_SCHEMA: Schema =
41 IntegerSchema::new("File mark position (0 is BOT).")
42 .minimum(0)
43 .maximum(i32::MAX as isize)
44 .schema();
45
46 pub const RECORD_COUNT_SCHEMA: Schema =
47 IntegerSchema::new("Record count.")
48 .minimum(1)
49 .maximum(i32::MAX as isize)
50 .schema();
51
52 pub const DRIVE_OPTION_SCHEMA: Schema = StringSchema::new(
53 "Lto Tape Driver Option, either numeric value or option name.")
54 .schema();
55
56 pub const DRIVE_OPTION_LIST_SCHEMA: Schema =
57 ArraySchema::new("Drive Option List.", &DRIVE_OPTION_SCHEMA)
58 .min_length(1)
59 .schema();
60
61 fn get_tape_handle(param: &Value) -> Result<SgTape, Error> {
62
63 if let Some(name) = param["drive"].as_str() {
64 let (config, _digest) = pbs_config::drive::config()?;
65 let drive: LtoTapeDrive = config.lookup("lto", &name)?;
66 eprintln!("using device {}", drive.path);
67 return SgTape::new(open_lto_tape_device(&drive.path)?);
68 }
69
70 if let Some(device) = param["device"].as_str() {
71 eprintln!("using device {}", device);
72 return SgTape::new(open_lto_tape_device(&device)?);
73 }
74
75 if let Ok(name) = std::env::var("PROXMOX_TAPE_DRIVE") {
76 let (config, _digest) = pbs_config::drive::config()?;
77 let drive: LtoTapeDrive = config.lookup("lto", &name)?;
78 eprintln!("using device {}", drive.path);
79 return SgTape::new(open_lto_tape_device(&drive.path)?);
80 }
81
82 if let Ok(device) = std::env::var("TAPE") {
83 eprintln!("using device {}", device);
84 return SgTape::new(open_lto_tape_device(&device)?);
85 }
86
87 let (config, _digest) = pbs_config::drive::config()?;
88
89 let mut drive_names = Vec::new();
90 for (name, (section_type, _)) in config.sections.iter() {
91 if section_type != "lto" { continue; }
92 drive_names.push(name);
93 }
94
95 if drive_names.len() == 1 {
96 let name = drive_names[0];
97 let drive: LtoTapeDrive = config.lookup("lto", &name)?;
98 eprintln!("using device {}", drive.path);
99 return SgTape::new(open_lto_tape_device(&drive.path)?);
100 }
101
102 bail!("no drive/device specified");
103 }
104
105 #[api(
106 input: {
107 properties: {
108 drive: {
109 schema: DRIVE_NAME_SCHEMA,
110 optional: true,
111 },
112 device: {
113 schema: LTO_DRIVE_PATH_SCHEMA,
114 optional: true,
115 },
116 count: {
117 schema: FILE_MARK_POSITION_SCHEMA,
118 },
119 },
120 },
121 )]
122 /// Position the tape at the beginning of the count file (after
123 /// filemark count)
124 fn asf(count: u64, param: Value) -> Result<(), Error> {
125
126 let mut handle = get_tape_handle(&param)?;
127
128 handle.locate_file(count)?;
129
130 Ok(())
131 }
132
133
134 #[api(
135 input: {
136 properties: {
137 drive: {
138 schema: DRIVE_NAME_SCHEMA,
139 optional: true,
140 },
141 device: {
142 schema: LTO_DRIVE_PATH_SCHEMA,
143 optional: true,
144 },
145 count: {
146 schema: FILE_MARK_COUNT_SCHEMA,
147 },
148 },
149 },
150 )]
151 /// Backward space count files (position before file mark).
152 ///
153 /// The tape is positioned on the last block of the previous file.
154 fn bsf(count: usize, param: Value) -> Result<(), Error> {
155
156 let mut handle = get_tape_handle(&param)?;
157
158 handle.space_filemarks(-count.try_into()?)?;
159
160 Ok(())
161 }
162
163
164 #[api(
165 input: {
166 properties: {
167 drive: {
168 schema: DRIVE_NAME_SCHEMA,
169 optional: true,
170 },
171 device: {
172 schema: LTO_DRIVE_PATH_SCHEMA,
173 optional: true,
174 },
175 count: {
176 schema: FILE_MARK_COUNT_SCHEMA,
177 },
178 },
179 },
180 )]
181 /// Backward space count files, then forward space one record (position after file mark).
182 ///
183 /// This leaves the tape positioned at the first block of the file
184 /// that is count - 1 files before the current file.
185 fn bsfm(count: usize, param: Value) -> Result<(), Error> {
186
187 let mut handle = get_tape_handle(&param)?;
188
189 handle.space_filemarks(-count.try_into()?)?;
190 handle.space_filemarks(1)?;
191
192 Ok(())
193 }
194
195
196 #[api(
197 input: {
198 properties: {
199 drive: {
200 schema: DRIVE_NAME_SCHEMA,
201 optional: true,
202 },
203 device: {
204 schema: LTO_DRIVE_PATH_SCHEMA,
205 optional: true,
206 },
207 count: {
208 schema: RECORD_COUNT_SCHEMA,
209 },
210 },
211 },
212 )]
213 /// Backward space records.
214 fn bsr(count: usize, param: Value) -> Result<(), Error> {
215
216 let mut handle = get_tape_handle(&param)?;
217
218 handle.space_blocks(-count.try_into()?)?;
219
220 Ok(())
221 }
222
223
224 #[api(
225 input: {
226 properties: {
227 drive: {
228 schema: DRIVE_NAME_SCHEMA,
229 optional: true,
230 },
231 device: {
232 schema: LTO_DRIVE_PATH_SCHEMA,
233 optional: true,
234 },
235 "output-format": {
236 schema: OUTPUT_FORMAT,
237 optional: true,
238 },
239 },
240 },
241 )]
242 /// Read Cartridge Memory
243 fn cartridge_memory(param: Value) -> Result<(), Error> {
244
245 let output_format = get_output_format(&param);
246
247 let mut handle = get_tape_handle(&param)?;
248 let result = handle.cartridge_memory();
249
250 if output_format == "json-pretty" {
251 let result = result.map_err(|err: Error| err.to_string());
252 println!("{}", serde_json::to_string_pretty(&result)?);
253 return Ok(());
254 }
255
256 if output_format == "json" {
257 let result = result.map_err(|err: Error| err.to_string());
258 println!("{}", serde_json::to_string(&result)?);
259 return Ok(());
260 }
261
262 if output_format != "text" {
263 bail!("unknown output format '{}'", output_format);
264 }
265
266 let list = result?;
267
268 for item in list {
269 println!("{}|{}|{}", item.id, item.name, item.value);
270 }
271
272 Ok(())
273 }
274
275 #[api(
276 input: {
277 properties: {
278 drive: {
279 schema: DRIVE_NAME_SCHEMA,
280 optional: true,
281 },
282 device: {
283 schema: LTO_DRIVE_PATH_SCHEMA,
284 optional: true,
285 },
286 "output-format": {
287 schema: OUTPUT_FORMAT,
288 optional: true,
289 },
290 },
291 },
292 )]
293 /// Read Tape Alert Flags
294 fn tape_alert_flags(param: Value) -> Result<(), Error> {
295
296 let output_format = get_output_format(&param);
297
298 let mut handle = get_tape_handle(&param)?;
299 let result = handle.tape_alert_flags()
300 .map(|flags| format!("{:?}", flags));
301
302 if output_format == "json-pretty" {
303 let result = result.map_err(|err: Error| err.to_string());
304 println!("{}", serde_json::to_string_pretty(&result)?);
305 return Ok(());
306 }
307
308 if output_format == "json" {
309 let result = result.map_err(|err: Error| err.to_string());
310 println!("{}", serde_json::to_string(&result)?);
311 return Ok(());
312 }
313
314 if output_format != "text" {
315 bail!("unknown output format '{}'", output_format);
316 }
317
318 let flags = result?;
319 println!("Tape Alert Flags: {}", flags);
320
321 Ok(())
322 }
323
324 #[api(
325 input: {
326 properties: {
327 drive: {
328 schema: DRIVE_NAME_SCHEMA,
329 optional: true,
330 },
331 device: {
332 schema: LTO_DRIVE_PATH_SCHEMA,
333 optional: true,
334 },
335 },
336 },
337 )]
338 /// Eject drive media
339 fn eject(param: Value) -> Result<(), Error> {
340
341 let mut handle = get_tape_handle(&param)?;
342 handle.eject()?;
343
344 Ok(())
345 }
346
347
348 #[api(
349 input: {
350 properties: {
351 drive: {
352 schema: DRIVE_NAME_SCHEMA,
353 optional: true,
354 },
355 device: {
356 schema: LTO_DRIVE_PATH_SCHEMA,
357 optional: true,
358 },
359 },
360 },
361 )]
362 /// Move to end of media
363 fn eod(param: Value) -> Result<(), Error> {
364
365 let mut handle = get_tape_handle(&param)?;
366 handle.move_to_eom(false)?;
367
368 Ok(())
369 }
370
371
372 #[api(
373 input: {
374 properties: {
375 drive: {
376 schema: DRIVE_NAME_SCHEMA,
377 optional: true,
378 },
379 device: {
380 schema: LTO_DRIVE_PATH_SCHEMA,
381 optional: true,
382 },
383 fast: {
384 description: "Use fast erase.",
385 type: bool,
386 optional: true,
387 default: true,
388 },
389 },
390 },
391 )]
392 /// Erase media (from current position)
393 fn erase(fast: Option<bool>, param: Value) -> Result<(), Error> {
394
395 let mut handle = get_tape_handle(&param)?;
396 handle.erase_media(fast.unwrap_or(true))?;
397
398 Ok(())
399 }
400
401 #[api(
402 input: {
403 properties: {
404 drive: {
405 schema: DRIVE_NAME_SCHEMA,
406 optional: true,
407 },
408 device: {
409 schema: LTO_DRIVE_PATH_SCHEMA,
410 optional: true,
411 },
412 fast: {
413 description: "Use fast erase.",
414 type: bool,
415 optional: true,
416 default: true,
417 },
418 },
419 },
420 )]
421 /// Format media, single partition
422 fn format(fast: Option<bool>, param: Value) -> Result<(), Error> {
423
424 let mut handle = get_tape_handle(&param)?;
425 handle.format_media(fast.unwrap_or(true))?;
426
427 Ok(())
428 }
429
430 #[api(
431 input: {
432 properties: {
433 drive: {
434 schema: DRIVE_NAME_SCHEMA,
435 optional: true,
436 },
437 device: {
438 schema: LTO_DRIVE_PATH_SCHEMA,
439 optional: true,
440 },
441 count: {
442 schema: FILE_MARK_COUNT_SCHEMA,
443 },
444 },
445 },
446 )]
447 /// Forward space count files (position after file mark).
448 ///
449 /// The tape is positioned on the first block of the next file.
450 fn fsf(count: usize, param: Value) -> Result<(), Error> {
451
452 let mut handle = get_tape_handle(&param)?;
453
454 handle.space_filemarks(count.try_into()?)?;
455
456 Ok(())
457 }
458
459 #[api(
460 input: {
461 properties: {
462 drive: {
463 schema: DRIVE_NAME_SCHEMA,
464 optional: true,
465 },
466 device: {
467 schema: LTO_DRIVE_PATH_SCHEMA,
468 optional: true,
469 },
470 count: {
471 schema: FILE_MARK_COUNT_SCHEMA,
472 },
473 },
474 },
475 )]
476 /// Forward space count files, then backward space one record (position before file mark).
477 ///
478 /// This leaves the tape positioned at the last block of the file that
479 /// is count - 1 files past the current file.
480 fn fsfm(count: usize, param: Value) -> Result<(), Error> {
481
482 let mut handle = get_tape_handle(&param)?;
483
484 handle.space_filemarks(count.try_into()?)?;
485 handle.space_filemarks(-1)?;
486
487 Ok(())
488 }
489
490
491 #[api(
492 input: {
493 properties: {
494 drive: {
495 schema: DRIVE_NAME_SCHEMA,
496 optional: true,
497 },
498 device: {
499 schema: LTO_DRIVE_PATH_SCHEMA,
500 optional: true,
501 },
502 count: {
503 schema: RECORD_COUNT_SCHEMA,
504 },
505 },
506 },
507 )]
508 /// Forward space records.
509 fn fsr(count: usize, param: Value) -> Result<(), Error> {
510
511 let mut handle = get_tape_handle(&param)?;
512
513 handle.space_blocks(count.try_into()?)?;
514
515 Ok(())
516 }
517
518
519 #[api(
520 input: {
521 properties: {
522 drive: {
523 schema: DRIVE_NAME_SCHEMA,
524 optional: true,
525 },
526 device: {
527 schema: LTO_DRIVE_PATH_SCHEMA,
528 optional: true,
529 },
530 },
531 },
532 )]
533 /// Load media
534 fn load(param: Value) -> Result<(), Error> {
535
536 let mut handle = get_tape_handle(&param)?;
537 handle.load()?;
538
539 Ok(())
540 }
541
542
543 #[api(
544 input: {
545 properties: {
546 drive: {
547 schema: DRIVE_NAME_SCHEMA,
548 optional: true,
549 },
550 device: {
551 schema: LTO_DRIVE_PATH_SCHEMA,
552 optional: true,
553 },
554 },
555 },
556 )]
557 /// Lock the tape drive door
558 fn lock(param: Value) -> Result<(), Error> {
559
560 let mut handle = get_tape_handle(&param)?;
561
562 handle.set_medium_removal(false)?;
563
564 Ok(())
565 }
566
567
568 #[api(
569 input: {
570 properties: {
571 drive: {
572 schema: DRIVE_NAME_SCHEMA,
573 optional: true,
574 },
575 device: {
576 schema: LTO_DRIVE_PATH_SCHEMA,
577 optional: true,
578 },
579 },
580 },
581 )]
582 /// Rewind the tape
583 fn rewind(param: Value) -> Result<(), Error> {
584
585 let mut handle = get_tape_handle(&param)?;
586 handle.rewind()?;
587
588 Ok(())
589 }
590
591
592 #[api(
593 input: {
594 properties: {
595 "output-format": {
596 schema: OUTPUT_FORMAT,
597 optional: true,
598 },
599 },
600 },
601 )]
602 /// Scan for existing tape changer devices
603 fn scan(param: Value) -> Result<(), Error> {
604
605 let output_format = get_output_format(&param);
606
607 let list = lto_tape_device_list();
608
609 if output_format == "json-pretty" {
610 println!("{}", serde_json::to_string_pretty(&list)?);
611 return Ok(());
612 }
613
614 if output_format == "json" {
615 println!("{}", serde_json::to_string(&list)?);
616 return Ok(());
617 }
618
619 if output_format != "text" {
620 bail!("unknown output format '{}'", output_format);
621 }
622
623 for item in list.iter() {
624 println!("{} ({}/{}/{})", item.path, item.vendor, item.model, item.serial);
625 }
626
627 Ok(())
628 }
629
630 #[api(
631 input: {
632 properties: {
633 drive: {
634 schema: DRIVE_NAME_SCHEMA,
635 optional: true,
636 },
637 device: {
638 schema: LTO_DRIVE_PATH_SCHEMA,
639 optional: true,
640 },
641 "output-format": {
642 schema: OUTPUT_FORMAT,
643 optional: true,
644 },
645 },
646 },
647 )]
648 /// Drive Status
649 fn status(param: Value) -> Result<(), Error> {
650
651 let output_format = get_output_format(&param);
652
653 let mut handle = get_tape_handle(&param)?;
654
655 let result = handle.get_drive_and_media_status();
656
657 if output_format == "json-pretty" {
658 let result = result.map_err(|err: Error| err.to_string());
659 println!("{}", serde_json::to_string_pretty(&result)?);
660 return Ok(());
661 }
662
663 if output_format == "json" {
664 let result = result.map_err(|err: Error| err.to_string());
665 println!("{}", serde_json::to_string(&result)?);
666 return Ok(());
667 }
668
669 if output_format != "text" {
670 bail!("unknown output format '{}'", output_format);
671 }
672
673 let status = result?;
674
675 println!("{}", serde_json::to_string_pretty(&status)?);
676
677 Ok(())
678 }
679
680
681 #[api(
682 input: {
683 properties: {
684 drive: {
685 schema: DRIVE_NAME_SCHEMA,
686 optional: true,
687 },
688 device: {
689 schema: LTO_DRIVE_PATH_SCHEMA,
690 optional: true,
691 },
692 },
693 },
694 )]
695 /// Unlock the tape drive door
696 fn unlock(param: Value) -> Result<(), Error> {
697
698 let mut handle = get_tape_handle(&param)?;
699
700 handle.set_medium_removal(true)?;
701
702 Ok(())
703 }
704
705
706 #[api(
707 input: {
708 properties: {
709 drive: {
710 schema: DRIVE_NAME_SCHEMA,
711 optional: true,
712 },
713 device: {
714 schema: LTO_DRIVE_PATH_SCHEMA,
715 optional: true,
716 },
717 "output-format": {
718 schema: OUTPUT_FORMAT,
719 optional: true,
720 },
721 },
722 },
723 )]
724 /// Volume Statistics
725 fn volume_statistics(param: Value) -> Result<(), Error> {
726
727 let output_format = get_output_format(&param);
728
729 let mut handle = get_tape_handle(&param)?;
730 let result = handle.volume_statistics();
731
732 if output_format == "json-pretty" {
733 let result = result.map_err(|err: Error| err.to_string());
734 println!("{}", serde_json::to_string_pretty(&result)?);
735 return Ok(());
736 }
737
738 if output_format == "json" {
739 let result = result.map_err(|err: Error| err.to_string());
740 println!("{}", serde_json::to_string(&result)?);
741 return Ok(());
742 }
743
744 if output_format != "text" {
745 bail!("unknown output format '{}'", output_format);
746 }
747
748 let data = result?;
749
750 println!("{}", serde_json::to_string_pretty(&data)?);
751
752 Ok(())
753 }
754
755 #[api(
756 input: {
757 properties: {
758 drive: {
759 schema: DRIVE_NAME_SCHEMA,
760 optional: true,
761 },
762 device: {
763 schema: LTO_DRIVE_PATH_SCHEMA,
764 optional: true,
765 },
766 count: {
767 schema: FILE_MARK_COUNT_SCHEMA,
768 optional: true,
769 },
770 },
771 },
772 )]
773 /// Write count (default 1) EOF marks at current position.
774 fn weof(count: Option<usize>, param: Value) -> Result<(), Error> {
775
776 let count = count.unwrap_or(1);
777
778 let mut handle = get_tape_handle(&param)?;
779
780 handle.write_filemarks(count, false)?;
781
782 Ok(())
783 }
784 #[api(
785 input: {
786 properties: {
787 drive: {
788 schema: DRIVE_NAME_SCHEMA,
789 optional: true,
790 },
791 device: {
792 schema: LTO_DRIVE_PATH_SCHEMA,
793 optional: true,
794 },
795 compression: {
796 description: "Enable/disable compression.",
797 type: bool,
798 optional: true,
799 },
800 blocksize: {
801 description: "Set tape drive block_length (0 is variable length).",
802 type: u32,
803 minimum: 0,
804 maximum: 0x80_00_00,
805 optional: true,
806 },
807 buffer_mode: {
808 description: "Use drive buffer.",
809 type: bool,
810 optional: true,
811 },
812 defaults: {
813 description: "Set default options",
814 type: bool,
815 optional: true,
816 },
817 },
818 },
819 )]
820 /// Set varios drive options
821 fn options(
822 compression: Option<bool>,
823 blocksize: Option<u32>,
824 buffer_mode: Option<bool>,
825 defaults: Option<bool>,
826 param: Value,
827 ) -> Result<(), Error> {
828
829 let mut handle = get_tape_handle(&param)?;
830
831 if let Some(true) = defaults {
832 handle.set_default_options()?;
833 }
834
835 handle.set_drive_options(compression, blocksize, buffer_mode)?;
836
837 Ok(())
838 }
839
840 fn main() -> Result<(), Error> {
841
842 let uid = nix::unistd::Uid::current();
843
844 let username = match nix::unistd::User::from_uid(uid)? {
845 Some(user) => user.name,
846 None => bail!("unable to get user name"),
847 };
848
849 let std_cmd = |method| {
850 CliCommand::new(method)
851 .completion_cb("drive", complete_drive_name)
852 .completion_cb("device", complete_drive_path)
853 };
854
855 let cmd_def = CliCommandMap::new()
856 .usage_skip_options(&["device", "drive", "output-format"])
857 .insert("asf", std_cmd(&API_METHOD_ASF).arg_param(&["count"]))
858 .insert("bsf", std_cmd(&API_METHOD_BSF).arg_param(&["count"]))
859 .insert("bsfm", std_cmd(&API_METHOD_BSFM).arg_param(&["count"]))
860 .insert("bsr", std_cmd(&API_METHOD_BSR).arg_param(&["count"]))
861 .insert("cartridge-memory", std_cmd(&API_METHOD_CARTRIDGE_MEMORY))
862 .insert("eject", std_cmd(&API_METHOD_EJECT))
863 .insert("eod", std_cmd(&API_METHOD_EOD))
864 .insert("erase", std_cmd(&API_METHOD_ERASE))
865 .insert("format", std_cmd(&API_METHOD_FORMAT))
866 .insert("fsf", std_cmd(&API_METHOD_FSF).arg_param(&["count"]))
867 .insert("fsfm", std_cmd(&API_METHOD_FSFM).arg_param(&["count"]))
868 .insert("fsr", std_cmd(&API_METHOD_FSR).arg_param(&["count"]))
869 .insert("load", std_cmd(&API_METHOD_LOAD))
870 .insert("lock", std_cmd(&API_METHOD_LOCK))
871 .insert("options", std_cmd(&API_METHOD_OPTIONS))
872 .insert("rewind", std_cmd(&API_METHOD_REWIND))
873 .insert("scan", CliCommand::new(&API_METHOD_SCAN))
874 .insert("status", std_cmd(&API_METHOD_STATUS))
875 .insert("tape-alert-flags", std_cmd(&API_METHOD_TAPE_ALERT_FLAGS))
876 .insert("unlock", std_cmd(&API_METHOD_UNLOCK))
877 .insert("volume-statistics", std_cmd(&API_METHOD_VOLUME_STATISTICS))
878 .insert("weof", std_cmd(&API_METHOD_WEOF).arg_param(&["count"]))
879 ;
880
881 let mut rpcenv = CliEnvironment::new();
882 rpcenv.set_auth_id(Some(format!("{}@pam", username)));
883
884 run_cli_command(cmd_def, rpcenv, None);
885
886 Ok(())
887 }