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