]>
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 | ||
6ef1b649 WB |
20 | use proxmox_schema::api; |
21 | use proxmox_router::cli::*; | |
22 | use proxmox_router::RpcEnvironment; | |
c3747b93 | 23 | |
1ce8e905 | 24 | use pbs_config::drive::complete_changer_name; |
6227654a DM |
25 | use pbs_api_types::{ |
26 | SCSI_CHANGER_PATH_SCHEMA, CHANGER_NAME_SCHEMA, ScsiTapeChanger, LtoTapeDrive, | |
27 | }; | |
048b43af DM |
28 | use 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 | ||
35 | fn 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 | |
86 | fn inquiry( | |
87 | param: Value, | |
88 | ) -> Result<(), Error> { | |
89 | ||
90 | let output_format = get_output_format(¶m); | |
91 | ||
6ef1b649 | 92 | let result: Result<_, Error> = proxmox_lang::try_block!({ |
c3747b93 DM |
93 | let mut file = get_changer_handle(¶m)?; |
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 | |
139 | fn inventory( | |
140 | param: Value, | |
141 | ) -> Result<(), Error> { | |
142 | ||
143 | let mut file = get_changer_handle(¶m)?; | |
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 | |
173 | fn load( | |
174 | param: Value, | |
175 | slot: u64, | |
176 | drivenum: Option<u64>, | |
177 | ) -> Result<(), Error> { | |
178 | ||
179 | let mut file = get_changer_handle(¶m)?; | |
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 | |
213 | fn unload( | |
214 | param: Value, | |
215 | slot: Option<u64>, | |
216 | drivenum: Option<u64>, | |
217 | ) -> Result<(), Error> { | |
218 | ||
219 | let mut file = get_changer_handle(¶m)?; | |
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 | |
274 | fn status( | |
275 | param: Value, | |
276 | ) -> Result<(), Error> { | |
277 | ||
278 | let output_format = get_output_format(¶m); | |
279 | ||
6ef1b649 | 280 | let result: Result<_, Error> = proxmox_lang::try_block!({ |
c3747b93 DM |
281 | let mut file = get_changer_handle(¶m)?; |
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 | |
358 | fn transfer( | |
359 | param: Value, | |
360 | from: u64, | |
361 | to: u64, | |
362 | ) -> Result<(), Error> { | |
363 | ||
364 | let mut file = get_changer_handle(¶m)?; | |
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 | |
382 | fn scan(param: Value) -> Result<(), Error> { | |
383 | ||
384 | let output_format = get_output_format(¶m); | |
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 |
409 | fn 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 | } |