]> git.proxmox.com Git - proxmox-backup.git/blob - src/bin/pmtx.rs
move drive config to pbs_config workspace
[proxmox-backup.git] / src / bin / pmtx.rs
1 /// SCSI changer command implemented using scsi-generic raw commands
2 ///
3 /// This is a Rust implementation, meant to replace the 'mtx' command
4 /// line tool.
5 ///
6 /// Features:
7 ///
8 /// - written in Rust
9 ///
10 /// - json output
11 ///
12 /// - list serial number for attached drives, so that it is possible
13 /// to associate drive numbers with drives.
14
15 use std::fs::File;
16
17 use anyhow::{bail, Error};
18 use serde_json::Value;
19
20 use proxmox::{
21 api::{
22 api,
23 cli::*,
24 RpcEnvironment,
25 },
26 };
27
28 use pbs_config::drive::complete_changer_name;
29
30 use proxmox_backup::{
31 tools::sgutils2::{
32 scsi_inquiry,
33 },
34 api2::types::{
35 SCSI_CHANGER_PATH_SCHEMA,
36 CHANGER_NAME_SCHEMA,
37 ScsiTapeChanger,
38 LtoTapeDrive,
39 },
40 tape::{
41 linux_tape_changer_list,
42 complete_changer_path,
43 changer::{
44 ElementStatus,
45 sg_pt_changer,
46 },
47 },
48 };
49
50 fn get_changer_handle(param: &Value) -> Result<File, Error> {
51
52 if let Some(name) = param["changer"].as_str() {
53 let (config, _digest) = pbs_config::drive::config()?;
54 let changer_config: ScsiTapeChanger = config.lookup("changer", &name)?;
55 eprintln!("using device {}", changer_config.path);
56 return sg_pt_changer::open(&changer_config.path);
57 }
58
59 if let Some(device) = param["device"].as_str() {
60 eprintln!("using device {}", device);
61 return sg_pt_changer::open(device);
62 }
63
64 if let Ok(name) = std::env::var("PROXMOX_TAPE_DRIVE") {
65 let (config, _digest) = pbs_config::drive::config()?;
66 let drive: LtoTapeDrive = config.lookup("lto", &name)?;
67 if let Some(changer) = drive.changer {
68 let changer_config: ScsiTapeChanger = config.lookup("changer", &changer)?;
69 eprintln!("using device {}", changer_config.path);
70 return sg_pt_changer::open(&changer_config.path);
71 }
72 }
73
74 if let Ok(device) = std::env::var("CHANGER") {
75 eprintln!("using device {}", device);
76 return sg_pt_changer::open(device);
77 }
78
79 bail!("no changer device specified");
80 }
81
82 #[api(
83 input: {
84 properties: {
85 changer: {
86 schema: CHANGER_NAME_SCHEMA,
87 optional: true,
88 },
89 device: {
90 schema: SCSI_CHANGER_PATH_SCHEMA,
91 optional: true,
92 },
93 "output-format": {
94 schema: OUTPUT_FORMAT,
95 optional: true,
96 },
97 },
98 },
99 )]
100 /// Inquiry
101 fn inquiry(
102 param: Value,
103 ) -> Result<(), Error> {
104
105 let output_format = get_output_format(&param);
106
107 let result: Result<_, Error> = proxmox::try_block!({
108 let mut file = get_changer_handle(&param)?;
109 let info = scsi_inquiry(&mut file)?;
110 Ok(info)
111 });
112
113 if output_format == "json-pretty" {
114 let result = result.map_err(|err: Error| err.to_string());
115 println!("{}", serde_json::to_string_pretty(&result)?);
116 return Ok(());
117 }
118
119 if output_format == "json" {
120 let result = result.map_err(|err: Error| err.to_string());
121 println!("{}", serde_json::to_string(&result)?);
122 return Ok(());
123 }
124
125 if output_format != "text" {
126 bail!("unknown output format '{}'", output_format);
127 }
128
129 let info = result?;
130
131 println!("Type: {} ({})", info.peripheral_type_text, info.peripheral_type);
132 println!("Vendor: {}", info.vendor);
133 println!("Product: {}", info.product);
134 println!("Revision: {}", info.revision);
135
136 Ok(())
137 }
138
139 #[api(
140 input: {
141 properties: {
142 changer: {
143 schema: CHANGER_NAME_SCHEMA,
144 optional: true,
145 },
146 device: {
147 schema: SCSI_CHANGER_PATH_SCHEMA,
148 optional: true,
149 },
150 },
151 },
152 )]
153 /// Inventory
154 fn inventory(
155 param: Value,
156 ) -> Result<(), Error> {
157
158 let mut file = get_changer_handle(&param)?;
159 sg_pt_changer::initialize_element_status(&mut file)?;
160
161 Ok(())
162 }
163
164 #[api(
165 input: {
166 properties: {
167 changer: {
168 schema: CHANGER_NAME_SCHEMA,
169 optional: true,
170 },
171 device: {
172 schema: SCSI_CHANGER_PATH_SCHEMA,
173 optional: true,
174 },
175 slot: {
176 description: "Storage slot number (source).",
177 type: u64,
178 },
179 drivenum: {
180 description: "Target drive number (defaults to Drive 0)",
181 type: u64,
182 optional: true,
183 },
184 },
185 },
186 )]
187 /// Load
188 fn load(
189 param: Value,
190 slot: u64,
191 drivenum: Option<u64>,
192 ) -> Result<(), Error> {
193
194 let mut file = get_changer_handle(&param)?;
195
196 let drivenum = drivenum.unwrap_or(0);
197
198 sg_pt_changer::load_slot(&mut file, slot, drivenum)?;
199
200 Ok(())
201 }
202
203 #[api(
204 input: {
205 properties: {
206 changer: {
207 schema: CHANGER_NAME_SCHEMA,
208 optional: true,
209 },
210 device: {
211 schema: SCSI_CHANGER_PATH_SCHEMA,
212 optional: true,
213 },
214 slot: {
215 description: "Storage slot number (target). If omitted, defaults to the slot that the drive was loaded from.",
216 type: u64,
217 optional: true,
218 },
219 drivenum: {
220 description: "Target drive number (defaults to Drive 0)",
221 type: u64,
222 optional: true,
223 },
224 },
225 },
226 )]
227 /// Unload
228 fn unload(
229 param: Value,
230 slot: Option<u64>,
231 drivenum: Option<u64>,
232 ) -> Result<(), Error> {
233
234 let mut file = get_changer_handle(&param)?;
235
236 let drivenum = drivenum.unwrap_or(0);
237
238 if let Some(to_slot) = slot {
239 sg_pt_changer::unload(&mut file, to_slot, drivenum)?;
240 return Ok(());
241 }
242
243 let status = sg_pt_changer::read_element_status(&mut file)?;
244
245 if let Some(info) = status.drives.get(drivenum as usize) {
246 if let ElementStatus::Empty = info.status {
247 bail!("Drive {} is empty.", drivenum);
248 }
249 if let Some(to_slot) = info.loaded_slot {
250 // check if original slot is empty/usable
251 if let Some(slot_info) = status.slots.get(to_slot as usize - 1) {
252 if let ElementStatus::Empty = slot_info.status {
253 sg_pt_changer::unload(&mut file, to_slot, drivenum)?;
254 return Ok(());
255 }
256 }
257 }
258
259 if let Some(to_slot) = status.find_free_slot(false) {
260 sg_pt_changer::unload(&mut file, to_slot, drivenum)?;
261 return Ok(());
262 } else {
263 bail!("Drive '{}' unload failure - no free slot", drivenum);
264 }
265 } else {
266 bail!("Drive {} does not exist.", drivenum);
267 }
268 }
269
270 #[api(
271 input: {
272 properties: {
273 changer: {
274 schema: CHANGER_NAME_SCHEMA,
275 optional: true,
276 },
277 device: {
278 schema: SCSI_CHANGER_PATH_SCHEMA,
279 optional: true,
280 },
281 "output-format": {
282 schema: OUTPUT_FORMAT,
283 optional: true,
284 },
285 },
286 },
287 )]
288 /// Changer Status
289 fn status(
290 param: Value,
291 ) -> Result<(), Error> {
292
293 let output_format = get_output_format(&param);
294
295 let result: Result<_, Error> = proxmox::try_block!({
296 let mut file = get_changer_handle(&param)?;
297 let status = sg_pt_changer::read_element_status(&mut file)?;
298 Ok(status)
299 });
300
301 if output_format == "json-pretty" {
302 let result = result.map_err(|err: Error| err.to_string());
303 println!("{}", serde_json::to_string_pretty(&result)?);
304 return Ok(());
305 }
306
307 if output_format == "json" {
308 let result = result.map_err(|err: Error| err.to_string());
309 println!("{}", serde_json::to_string(&result)?);
310 return Ok(());
311 }
312
313 if output_format != "text" {
314 bail!("unknown output format '{}'", output_format);
315 }
316
317 let status = result?;
318
319 for (i, transport) in status.transports.iter().enumerate() {
320 println!("Transport Element (Griper) {:>3}: {:?}",i, transport.status);
321 }
322
323 for (i, drive) in status.drives.iter().enumerate() {
324 let loaded_txt = match drive.loaded_slot {
325 Some(slot) => format!(", Source: {}", slot),
326 None => String::new(),
327 };
328 let serial_txt = match drive.drive_serial_number {
329 Some(ref serial) => format!(", Serial: {}", serial),
330 None => String::new(),
331 };
332
333 println!(
334 "Data Transfer Element (Drive) {:>3}: {:?}{}{}",
335 i, drive.status, loaded_txt, serial_txt,
336 );
337 }
338
339 for (i, slot) in status.slots.iter().enumerate() {
340 if slot.import_export {
341 println!(" Import/Export {:>3}: {:?}", i+1, slot.status);
342 } else {
343 println!(" Storage Element {:>3}: {:?}", i+1, slot.status);
344 }
345 }
346
347 Ok(())
348 }
349
350 #[api(
351 input: {
352 properties: {
353 changer: {
354 schema: CHANGER_NAME_SCHEMA,
355 optional: true,
356 },
357 device: {
358 schema: SCSI_CHANGER_PATH_SCHEMA,
359 optional: true,
360 },
361 from: {
362 description: "Source storage slot number.",
363 type: u64,
364 },
365 to: {
366 description: "Target storage slot number.",
367 type: u64,
368 },
369 },
370 },
371 )]
372 /// Transfer
373 fn transfer(
374 param: Value,
375 from: u64,
376 to: u64,
377 ) -> Result<(), Error> {
378
379 let mut file = get_changer_handle(&param)?;
380
381 sg_pt_changer::transfer_medium(&mut file, from, to)?;
382
383 Ok(())
384 }
385
386 #[api(
387 input: {
388 properties: {
389 "output-format": {
390 schema: OUTPUT_FORMAT,
391 optional: true,
392 },
393 },
394 },
395 )]
396 /// Scan for existing tape changer devices
397 fn scan(param: Value) -> Result<(), Error> {
398
399 let output_format = get_output_format(&param);
400
401 let list = linux_tape_changer_list();
402
403 if output_format == "json-pretty" {
404 println!("{}", serde_json::to_string_pretty(&list)?);
405 return Ok(());
406 }
407
408 if output_format == "json" {
409 println!("{}", serde_json::to_string(&list)?);
410 return Ok(());
411 }
412
413 if output_format != "text" {
414 bail!("unknown output format '{}'", output_format);
415 }
416
417 for item in list.iter() {
418 println!("{} ({}/{}/{})", item.path, item.vendor, item.model, item.serial);
419 }
420
421 Ok(())
422 }
423
424 fn main() -> Result<(), Error> {
425
426 let uid = nix::unistd::Uid::current();
427
428 let username = match nix::unistd::User::from_uid(uid)? {
429 Some(user) => user.name,
430 None => bail!("unable to get user name"),
431 };
432
433
434 let cmd_def = CliCommandMap::new()
435 .usage_skip_options(&["device", "changer", "output-format"])
436 .insert(
437 "inquiry",
438 CliCommand::new(&API_METHOD_INQUIRY)
439 .completion_cb("changer", complete_changer_name)
440 .completion_cb("device", complete_changer_path)
441 )
442 .insert(
443 "inventory",
444 CliCommand::new(&API_METHOD_INVENTORY)
445 .completion_cb("changer", complete_changer_name)
446 .completion_cb("device", complete_changer_path)
447 )
448 .insert(
449 "load",
450 CliCommand::new(&API_METHOD_LOAD)
451 .arg_param(&["slot"])
452 .completion_cb("changer", complete_changer_name)
453 .completion_cb("device", complete_changer_path)
454 )
455 .insert(
456 "unload",
457 CliCommand::new(&API_METHOD_UNLOAD)
458 .completion_cb("changer", complete_changer_name)
459 .completion_cb("device", complete_changer_path)
460 )
461 .insert("scan", CliCommand::new(&API_METHOD_SCAN))
462 .insert(
463 "status",
464 CliCommand::new(&API_METHOD_STATUS)
465 .completion_cb("changer", complete_changer_name)
466 .completion_cb("device", complete_changer_path)
467 )
468 .insert(
469 "transfer",
470 CliCommand::new(&API_METHOD_TRANSFER)
471 .arg_param(&["from", "to"])
472 .completion_cb("changer", complete_changer_name)
473 .completion_cb("device", complete_changer_path)
474 )
475 ;
476
477 let mut rpcenv = CliEnvironment::new();
478 rpcenv.set_auth_id(Some(format!("{}@pam", username)));
479
480 run_cli_command(cmd_def, rpcenv, None);
481
482 Ok(())
483 }