]>
Commit | Line | Data |
---|---|---|
583a68a4 | 1 | use anyhow::{format_err, Error}; |
e92c7581 | 2 | use serde_json::{json, Value}; |
583a68a4 | 3 | |
e6604cf3 DM |
4 | use 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 | ||
18 | use 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 | ||
51 | mod proxmox_tape; | |
52 | use 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. | |
56 | pub 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 |
72 | fn 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 | 119 | async 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(¶m, &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 | 151 | async 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(¶m, &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 | 183 | async 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(¶m, &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 | 216 | async 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(¶m, &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 | 253 | async 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(¶m, &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 | 289 | async 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(¶m, &config)?.into(); | |
297 | ||
298 | let output_format = get_output_format(¶m); | |
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) |
344 | async 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(¶m); |
352 | ||
83abc749 | 353 | let (config, _digest) = config::drive::config()?; |
e92c7581 | 354 | let drive = lookup_drive_name(¶m, &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 | 406 | async 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(¶m, &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) | |
438 | fn move_to_eom(param: Value) -> Result<(), Error> { | |
439 | ||
440 | let (config, _digest) = config::drive::config()?; | |
441 | ||
442 | let drive = lookup_drive_name(¶m, &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 |
464 | fn debug_scan(param: Value) -> Result<(), Error> { |
465 | ||
ac461bd6 DM |
466 | let (config, _digest) = config::drive::config()?; |
467 | ||
468 | let drive = lookup_drive_name(¶m, &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 |
513 | fn 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 | } |