]>
Commit | Line | Data |
---|---|---|
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 | ||
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 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 | ||
53 | fn 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 | |
104 | fn inquiry( | |
105 | param: Value, | |
106 | ) -> Result<(), Error> { | |
107 | ||
108 | let output_format = get_output_format(¶m); | |
109 | ||
110 | let result: Result<_, Error> = proxmox::try_block!({ | |
111 | let mut file = get_changer_handle(¶m)?; | |
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 | |
157 | fn inventory( | |
158 | param: Value, | |
159 | ) -> Result<(), Error> { | |
160 | ||
161 | let mut file = get_changer_handle(¶m)?; | |
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 | |
191 | fn load( | |
192 | param: Value, | |
193 | slot: u64, | |
194 | drivenum: Option<u64>, | |
195 | ) -> Result<(), Error> { | |
196 | ||
197 | let mut file = get_changer_handle(¶m)?; | |
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 | |
231 | fn unload( | |
232 | param: Value, | |
233 | slot: Option<u64>, | |
234 | drivenum: Option<u64>, | |
235 | ) -> Result<(), Error> { | |
236 | ||
237 | let mut file = get_changer_handle(¶m)?; | |
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 | |
292 | fn status( | |
293 | param: Value, | |
294 | ) -> Result<(), Error> { | |
295 | ||
296 | let output_format = get_output_format(¶m); | |
297 | ||
298 | let result: Result<_, Error> = proxmox::try_block!({ | |
299 | let mut file = get_changer_handle(¶m)?; | |
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 | |
376 | fn transfer( | |
377 | param: Value, | |
378 | from: u64, | |
379 | to: u64, | |
380 | ) -> Result<(), Error> { | |
381 | ||
382 | let mut file = get_changer_handle(¶m)?; | |
383 | ||
384 | sg_pt_changer::transfer_medium(&mut file, from, to)?; | |
385 | ||
386 | Ok(()) | |
387 | } | |
388 | ||
389 | fn 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 | } |