]> git.proxmox.com Git - proxmox-backup.git/blame - src/bin/proxmox-tape.rs
adaptions for proxmox 0.9 and proxmox-api-macro 0.3
[proxmox-backup.git] / src / bin / proxmox-tape.rs
CommitLineData
583a68a4 1use anyhow::{format_err, Error};
e92c7581 2use serde_json::{json, Value};
583a68a4 3
e6604cf3
DM
4use proxmox::{
5 api::{
583a68a4 6 api,
e6604cf3 7 cli::*,
583a68a4 8 ApiHandler,
e6604cf3 9 RpcEnvironment,
583a68a4
DM
10 section_config::SectionConfigData,
11 },
3f803af0 12 tools::{
3f803af0
DM
13 time::strftime_local,
14 io::ReadExt,
15 },
583a68a4
DM
16};
17
18use proxmox_backup::{
3f803af0
DM
19 tools::format::{
20 HumanByte,
21 render_epoch,
22 },
6dbad5b4
DM
23 server::{
24 UPID,
25 worker_is_active_local,
26 },
583a68a4
DM
27 api2::{
28 self,
29 types::{
88356646 30 DATASTORE_SCHEMA,
49c965a4 31 DRIVE_NAME_SCHEMA,
e49f0c03 32 MEDIA_LABEL_SCHEMA,
7bb720cb 33 MEDIA_POOL_NAME_SCHEMA,
583a68a4
DM
34 },
35 },
36 config::{
37 self,
88356646 38 datastore::complete_datastore_name,
583a68a4 39 drive::complete_drive_name,
7bb720cb 40 media_pool::complete_pool_name,
e6604cf3 41 },
e49f0c03 42 tape::{
ac461bd6 43 open_drive,
e49f0c03 44 complete_media_changer_id,
ac461bd6
DM
45 file_formats::{
46 PROXMOX_BACKUP_CONTENT_HEADER_MAGIC_1_0,
47 PROXMOX_BACKUP_CONTENT_NAME,
48 MediaContentHeader,
49 },
e49f0c03 50 },
e6604cf3
DM
51};
52
53mod proxmox_tape;
54use proxmox_tape::*;
55
6dbad5b4
DM
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.
58pub 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
583a68a4
DM
74fn 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: {
49c965a4 108 schema: DRIVE_NAME_SCHEMA,
583a68a4
DM
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
663ef859 121async fn erase_media(
583a68a4
DM
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
663ef859 132 let result = match info.handler {
583a68a4
DM
133 ApiHandler::Sync(handler) => (handler)(param, info, rpcenv)?,
134 _ => unreachable!(),
135 };
136
663ef859
DM
137 wait_for_local_worker(result.as_str().unwrap()).await?;
138
583a68a4
DM
139 Ok(())
140}
141
5fb694e8
DM
142#[api(
143 input: {
144 properties: {
145 drive: {
49c965a4 146 schema: DRIVE_NAME_SCHEMA,
5fb694e8
DM
147 optional: true,
148 },
149 },
150 },
151)]
152/// Rewind tape
663ef859 153async fn rewind(
5fb694e8
DM
154 mut param: Value,
155 rpcenv: &mut dyn RpcEnvironment,
156) -> Result<(), Error> {
0098b712 157
5fb694e8
DM
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
663ef859 164 let result = match info.handler {
5fb694e8
DM
165 ApiHandler::Sync(handler) => (handler)(param, info, rpcenv)?,
166 _ => unreachable!(),
167 };
168
663ef859
DM
169 wait_for_local_worker(result.as_str().unwrap()).await?;
170
5fb694e8
DM
171 Ok(())
172}
173
0098b712
DM
174#[api(
175 input: {
176 properties: {
177 drive: {
49c965a4 178 schema: DRIVE_NAME_SCHEMA,
0098b712
DM
179 optional: true,
180 },
181 },
182 },
183)]
184/// Eject/Unload drive media
6fe9aedd 185async fn eject_media(
0098b712
DM
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 {
6fe9aedd 197 ApiHandler::Async(handler) => (handler)(param, info, rpcenv).await?,
0098b712
DM
198 _ => unreachable!(),
199 };
200
201 Ok(())
202}
203
e49f0c03
DM
204#[api(
205 input: {
206 properties: {
207 drive: {
49c965a4 208 schema: DRIVE_NAME_SCHEMA,
e49f0c03
DM
209 optional: true,
210 },
211 "changer-id": {
212 schema: MEDIA_LABEL_SCHEMA,
213 },
214 },
215 },
216)]
217/// Load media
6fe9aedd 218async fn load_media(
e49f0c03
DM
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 {
6fe9aedd 230 ApiHandler::Async(handler) => (handler)(param, info, rpcenv).await?,
e49f0c03
DM
231 _ => unreachable!(),
232 };
233
234 Ok(())
235}
236
7bb720cb
DM
237#[api(
238 input: {
239 properties: {
240 pool: {
241 schema: MEDIA_POOL_NAME_SCHEMA,
242 optional: true,
243 },
244 drive: {
49c965a4 245 schema: DRIVE_NAME_SCHEMA,
7bb720cb
DM
246 optional: true,
247 },
248 "changer-id": {
249 schema: MEDIA_LABEL_SCHEMA,
250 },
251 },
252 },
253)]
254/// Label media
6dbad5b4 255async fn label_media(
7bb720cb
DM
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
6dbad5b4 266 let result = match info.handler {
7bb720cb
DM
267 ApiHandler::Sync(handler) => (handler)(param, info, rpcenv)?,
268 _ => unreachable!(),
269 };
270
6dbad5b4
DM
271 wait_for_local_worker(result.as_str().unwrap()).await?;
272
7bb720cb
DM
273 Ok(())
274}
275
4606f343
DM
276#[api(
277 input: {
278 properties: {
279 drive: {
49c965a4 280 schema: DRIVE_NAME_SCHEMA,
4606f343
DM
281 optional: true,
282 },
283 "output-format": {
284 schema: OUTPUT_FORMAT,
285 optional: true,
286 },
287 },
288 },
289)]
290/// Read media label
6fe9aedd 291async fn read_label(
4606f343
DM
292 mut param: Value,
293 rpcenv: &mut dyn RpcEnvironment,
294) -> Result<(), Error> {
83abc749
DM
295
296 let (config, _digest) = config::drive::config()?;
4606f343
DM
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 {
6fe9aedd 303 ApiHandler::Async(handler) => (handler)(param, info, rpcenv).await?,
4606f343
DM
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
b2362a12 316 format_and_print_result_full(&mut data, &info.returns, &output_format, &options);
4606f343
DM
317
318 Ok(())
83abc749
DM
319}
320
321#[api(
322 input: {
323 properties: {
324 "output-format": {
325 schema: OUTPUT_FORMAT,
326 optional: true,
327 },
328 drive: {
49c965a4 329 schema: DRIVE_NAME_SCHEMA,
83abc749
DM
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)]
e92c7581
DM
345/// List (and update) media labels (Changer Inventory)
346async fn inventory(
347 read_labels: Option<bool>,
348 read_all_labels: Option<bool>,
349 param: Value,
83abc749
DM
350 rpcenv: &mut dyn RpcEnvironment,
351) -> Result<(), Error> {
352
e92c7581
DM
353 let output_format = get_output_format(&param);
354
83abc749 355 let (config, _digest) = config::drive::config()?;
e92c7581 356 let drive = lookup_drive_name(&param, &config)?;
83abc749 357
e92c7581
DM
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 }
83abc749 374
83abc749 375 let info = &api2::tape::drive::API_METHOD_INVENTORY;
e92c7581
DM
376
377 let param = json!({ "drive": &drive });
83abc749 378 let mut data = match info.handler {
6fe9aedd 379 ApiHandler::Async(handler) => (handler)(param, info, rpcenv).await?,
83abc749
DM
380 _ => unreachable!(),
381 };
4606f343 382
83abc749
DM
383 let options = default_table_format_options()
384 .column(ColumnConfig::new("changer-id"))
385 .column(ColumnConfig::new("uuid"))
386 ;
387
b2362a12 388 format_and_print_result_full(&mut data, &info.returns, &output_format, &options);
83abc749
DM
389
390 Ok(())
4606f343 391}
83abc749 392
bff7e3f3
DM
393#[api(
394 input: {
395 properties: {
396 pool: {
397 schema: MEDIA_POOL_NAME_SCHEMA,
398 optional: true,
399 },
400 drive: {
49c965a4 401 schema: DRIVE_NAME_SCHEMA,
bff7e3f3
DM
402 optional: true,
403 },
404 },
405 },
406)]
407/// Label media with barcodes from changer device
6dbad5b4 408async fn barcode_label_media(
bff7e3f3
DM
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
6dbad5b4 419 let result = match info.handler {
bff7e3f3
DM
420 ApiHandler::Sync(handler) => (handler)(param, info, rpcenv)?,
421 _ => unreachable!(),
422 };
423
6dbad5b4
DM
424 wait_for_local_worker(result.as_str().unwrap()).await?;
425
bff7e3f3
DM
426 Ok(())
427}
428
ce955e16
DM
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)
440fn 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
ac461bd6
DM
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
3f803af0
DM
463///
464/// Note: This reads unless the driver returns an IO Error, so this
465/// method is expected to fails when we reach EOT.
ac461bd6
DM
466fn debug_scan(param: Value) -> Result<(), Error> {
467
ac461bd6
DM
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);
af07ec8f 495 println!(" uuid: {}", header.content_uuid());
3f803af0
DM
496 println!(" ctime: {}", strftime_local("%c", header.ctime)?);
497 println!(" hsize: {}", HumanByte::from(header.size as usize));
498 println!(" part: {}", header.part_number);
ac461bd6
DM
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()?;
3f803af0 509 println!("skipped {}", HumanByte::from(bytes));
ac461bd6
DM
510 }
511 }
512 }
513}
514
1e20f819
DM
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)
530fn 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
88356646
DM
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
570async 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
e6604cf3
DM
587fn main() {
588
589 let cmd_def = CliCommandMap::new()
88356646
DM
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 )
bff7e3f3
DM
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 )
5fb694e8
DM
603 .insert(
604 "rewind",
605 CliCommand::new(&API_METHOD_REWIND)
606 .completion_cb("drive", complete_drive_name)
607 )
ac461bd6
DM
608 .insert(
609 "scan",
610 CliCommand::new(&API_METHOD_DEBUG_SCAN)
611 .completion_cb("drive", complete_drive_name)
612 )
ce955e16
DM
613 .insert(
614 "eod",
615 CliCommand::new(&API_METHOD_MOVE_TO_EOM)
616 .completion_cb("drive", complete_drive_name)
617 )
583a68a4
DM
618 .insert(
619 "erase",
620 CliCommand::new(&API_METHOD_ERASE_MEDIA)
621 .completion_cb("drive", complete_drive_name)
622 )
0098b712
DM
623 .insert(
624 "eject",
625 CliCommand::new(&API_METHOD_EJECT_MEDIA)
626 .completion_cb("drive", complete_drive_name)
83abc749
DM
627 )
628 .insert(
629 "inventory",
630 CliCommand::new(&API_METHOD_INVENTORY)
631 .completion_cb("drive", complete_drive_name)
0098b712 632 )
4606f343
DM
633 .insert(
634 "read-label",
635 CliCommand::new(&API_METHOD_READ_LABEL)
636 .completion_cb("drive", complete_drive_name)
637 )
1e20f819
DM
638 .insert(
639 "mam",
640 CliCommand::new(&API_METHOD_MAM_ATTRIBUTES)
641 .completion_cb("drive", complete_drive_name)
642 )
7bb720cb
DM
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 )
e6604cf3
DM
650 .insert("changer", changer_commands())
651 .insert("drive", drive_commands())
9700d537 652 .insert("pool", pool_commands())
fba0b774 653 .insert("media", media_commands())
e49f0c03
DM
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 )
e6604cf3
DM
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}