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