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