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