]> git.proxmox.com Git - proxmox-backup.git/blob - src/tape/changer/sg_pt_changer.rs
tape/changer: refactor marking of import/export slots from config
[proxmox-backup.git] / src / tape / changer / sg_pt_changer.rs
1 //! SCSI changer implementation using libsgutil2
2
3 use std::os::unix::prelude::AsRawFd;
4 use std::io::Read;
5 use std::collections::HashMap;
6 use std::path::Path;
7 use std::fs::{OpenOptions, File};
8
9 use anyhow::{bail, format_err, Error};
10 use endian_trait::Endian;
11
12 use proxmox::tools::io::ReadExt;
13
14 use crate::{
15 tape::{
16 changer::{
17 DriveStatus,
18 ElementStatus,
19 StorageElementStatus,
20 TransportElementStatus,
21 MtxStatus,
22 },
23 },
24 tools::sgutils2::{
25 SgRaw,
26 SENSE_KEY_NO_SENSE,
27 SENSE_KEY_RECOVERED_ERROR,
28 SENSE_KEY_UNIT_ATTENTION,
29 SENSE_KEY_NOT_READY,
30 InquiryInfo,
31 scsi_ascii_to_string,
32 scsi_inquiry,
33 },
34 api2::types::ScsiTapeChanger,
35 };
36
37 const SCSI_CHANGER_DEFAULT_TIMEOUT: usize = 60*5; // 5 minutes
38
39 /// Initialize element status (Inventory)
40 pub fn initialize_element_status<F: AsRawFd>(file: &mut F) -> Result<(), Error> {
41
42 let mut sg_raw = SgRaw::new(file, 64)?;
43
44 // like mtx(1), set a very long timeout (30 minutes)
45 sg_raw.set_timeout(30*60);
46
47 let mut cmd = Vec::new();
48 cmd.extend(&[0x07, 0, 0, 0, 0, 0]); // INITIALIZE ELEMENT STATUS (07h)
49
50 sg_raw.do_command(&cmd)
51 .map_err(|err| format_err!("initializte element status (07h) failed - {}", err))?;
52
53 Ok(())
54 }
55
56 #[repr(C, packed)]
57 #[derive(Endian)]
58 struct AddressAssignmentPage {
59 data_len: u8,
60 reserved1: u8,
61 reserved2: u8,
62 block_descriptor_len: u8,
63
64 page_code: u8,
65 additional_page_len: u8,
66 first_transport_element_address: u16,
67 transport_element_count: u16,
68 first_storage_element_address: u16,
69 storage_element_count: u16,
70 first_import_export_element_address: u16,
71 import_export_element_count: u16,
72 first_tranfer_element_address: u16,
73 transfer_element_count: u16,
74 reserved22: u8,
75 reserved23: u8,
76 }
77
78 /// Execute scsi commands, optionally repeat the command until
79 /// successful (sleep 1 second between invovations)
80 ///
81 /// Any Sense key other than NO_SENSE, RECOVERED_ERROR, NOT_READY and
82 /// UNIT_ATTENTION aborts the loop and returns an error. If the device
83 /// reports "Not Ready - becoming ready", we wait up to 5 minutes.
84 ///
85 /// Skipped errors are printed on stderr.
86 fn execute_scsi_command<F: AsRawFd>(
87 sg_raw: &mut SgRaw<F>,
88 cmd: &[u8],
89 error_prefix: &str,
90 retry: bool,
91 ) -> Result<Vec<u8>, Error> {
92
93 let start = std::time::SystemTime::now();
94
95 let mut last_msg: Option<String> = None;
96
97 let mut timeout = std::time::Duration::new(5, 0); // short timeout by default
98
99 loop {
100 match sg_raw.do_command(&cmd) {
101 Ok(data) => return Ok(data.to_vec()),
102 Err(err) => {
103 if !retry {
104 bail!("{} failed: {}", error_prefix, err);
105 }
106 if let Some(ref sense) = err.sense {
107
108 if sense.sense_key == SENSE_KEY_NO_SENSE ||
109 sense.sense_key == SENSE_KEY_RECOVERED_ERROR ||
110 sense.sense_key == SENSE_KEY_UNIT_ATTENTION ||
111 sense.sense_key == SENSE_KEY_NOT_READY
112 {
113 let msg = err.to_string();
114 if let Some(ref last) = last_msg {
115 if &msg != last {
116 eprintln!("{}", err);
117 last_msg = Some(msg);
118 }
119 } else {
120 eprintln!("{}", err);
121 last_msg = Some(msg);
122 }
123
124 // Not Ready - becoming ready
125 if sense.sense_key == SENSE_KEY_NOT_READY && sense.asc == 0x04 && sense.ascq == 1 {
126 // wait up to 5 minutes, long enough to finish inventorize
127 timeout = std::time::Duration::new(5*60, 0);
128 }
129
130 if start.elapsed()? > timeout {
131 bail!("{} failed: {}", error_prefix, err);
132 }
133
134 std::thread::sleep(std::time::Duration::new(1, 0));
135 continue; // try again
136 }
137 }
138 }
139 }
140 }
141 }
142
143
144 fn read_element_address_assignment<F: AsRawFd>(
145 file: &mut F,
146 ) -> Result<AddressAssignmentPage, Error> {
147
148 let allocation_len: u8 = u8::MAX;
149 let mut sg_raw = SgRaw::new(file, allocation_len as usize)?;
150 sg_raw.set_timeout(SCSI_CHANGER_DEFAULT_TIMEOUT);
151
152 let mut cmd = Vec::new();
153 cmd.push(0x1A); // MODE SENSE6 (1Ah)
154 cmd.push(0x08); // DBD=1 (The Disable Block Descriptors)
155 cmd.push(0x1D); // Element Address Assignment Page
156 cmd.push(0);
157 cmd.push(allocation_len); // allocation len
158 cmd.push(0); //control
159
160 let data = execute_scsi_command(&mut sg_raw, &cmd, "read element address assignment", true)?;
161
162 proxmox::try_block!({
163 let mut reader = &data[..];
164 let page: AddressAssignmentPage = unsafe { reader.read_be_value()? };
165
166 if page.data_len != 23 {
167 bail!("got unexpected page len ({} != 23)", page.data_len);
168 }
169
170 Ok(page)
171 }).map_err(|err: Error| format_err!("decode element address assignment page failed - {}", err))
172 }
173
174 fn scsi_move_medium_cdb(
175 medium_transport_address: u16,
176 source_element_address: u16,
177 destination_element_address: u16,
178 ) -> Vec<u8> {
179
180 let mut cmd = Vec::new();
181 cmd.push(0xA5); // MOVE MEDIUM (A5h)
182 cmd.push(0); // reserved
183 cmd.extend(&medium_transport_address.to_be_bytes());
184 cmd.extend(&source_element_address.to_be_bytes());
185 cmd.extend(&destination_element_address.to_be_bytes());
186 cmd.push(0); // reserved
187 cmd.push(0); // reserved
188 cmd.push(0); // Invert=0
189 cmd.push(0); // control
190
191 cmd
192 }
193
194 /// Load media from storage slot into drive
195 pub fn load_slot(
196 file: &mut File,
197 from_slot: u64,
198 drivenum: u64,
199 ) -> Result<(), Error> {
200 let status = read_element_status(file)?;
201
202 let transport_address = status.transport_address();
203 let source_element_address = status.slot_address(from_slot)?;
204 let drive_element_address = status.drive_address(drivenum)?;
205
206 let cmd = scsi_move_medium_cdb(
207 transport_address,
208 source_element_address,
209 drive_element_address,
210 );
211
212 let mut sg_raw = SgRaw::new(file, 64)?;
213 sg_raw.set_timeout(SCSI_CHANGER_DEFAULT_TIMEOUT);
214
215 sg_raw.do_command(&cmd)
216 .map_err(|err| format_err!("load drive failed - {}", err))?;
217
218 Ok(())
219 }
220
221 /// Unload media from drive into a storage slot
222 pub fn unload(
223 file: &mut File,
224 to_slot: u64,
225 drivenum: u64,
226 ) -> Result<(), Error> {
227
228 let status = read_element_status(file)?;
229
230 let transport_address = status.transport_address();
231 let target_element_address = status.slot_address(to_slot)?;
232 let drive_element_address = status.drive_address(drivenum)?;
233
234 let cmd = scsi_move_medium_cdb(
235 transport_address,
236 drive_element_address,
237 target_element_address,
238 );
239
240 let mut sg_raw = SgRaw::new(file, 64)?;
241 sg_raw.set_timeout(SCSI_CHANGER_DEFAULT_TIMEOUT);
242
243 sg_raw.do_command(&cmd)
244 .map_err(|err| format_err!("unload drive failed - {}", err))?;
245
246 Ok(())
247 }
248
249 /// Tranfer medium from one storage slot to another
250 pub fn transfer_medium<F: AsRawFd>(
251 file: &mut F,
252 from_slot: u64,
253 to_slot: u64,
254 ) -> Result<(), Error> {
255
256 let status = read_element_status(file)?;
257
258 let transport_address = status.transport_address();
259 let source_element_address = status.slot_address(from_slot)?;
260 let target_element_address = status.slot_address(to_slot)?;
261
262 let cmd = scsi_move_medium_cdb(
263 transport_address,
264 source_element_address,
265 target_element_address,
266 );
267
268 let mut sg_raw = SgRaw::new(file, 64)?;
269 sg_raw.set_timeout(SCSI_CHANGER_DEFAULT_TIMEOUT);
270
271 sg_raw.do_command(&cmd)
272 .map_err(|err| {
273 format_err!("transfer medium from slot {} to slot {} failed - {}",
274 from_slot, to_slot, err)
275 })?;
276
277 Ok(())
278 }
279
280 fn scsi_read_element_status_cdb(
281 start_element_address: u16,
282 allocation_len: u32,
283 ) -> Vec<u8> {
284
285 let mut cmd = Vec::new();
286 cmd.push(0xB8); // READ ELEMENT STATUS (B8h)
287 cmd.push(1u8<<4); // report all types and volume tags
288 cmd.extend(&start_element_address.to_be_bytes());
289
290 let number_of_elements: u16 = 0xffff;
291 cmd.extend(&number_of_elements.to_be_bytes());
292 cmd.push(0b001); // Mixed=0,CurData=0,DVCID=1
293 cmd.extend(&allocation_len.to_be_bytes()[1..4]);
294 cmd.push(0);
295 cmd.push(0);
296
297 cmd
298 }
299
300 /// Read element status.
301 pub fn read_element_status<F: AsRawFd>(file: &mut F) -> Result<MtxStatus, Error> {
302
303 let inquiry = scsi_inquiry(file)?;
304
305 if inquiry.peripheral_type != 8 {
306 bail!("wrong device type (not a scsi changer device)");
307 }
308
309 // first, request address assignment (used for sanity checks)
310 let setup = read_element_address_assignment(file)?;
311
312 let allocation_len: u32 = 0x10000;
313
314 let mut sg_raw = SgRaw::new(file, allocation_len as usize)?;
315 sg_raw.set_timeout(SCSI_CHANGER_DEFAULT_TIMEOUT);
316
317 let mut start_element_address = 0;
318
319 let mut drives = Vec::new();
320 let mut storage_slots = Vec::new();
321 let mut import_export_slots = Vec::new();
322 let mut transports = Vec::new();
323
324 let mut retry = true;
325
326 loop {
327 let cmd = scsi_read_element_status_cdb(start_element_address, allocation_len);
328
329 let data = execute_scsi_command(&mut sg_raw, &cmd, "read element status (B8h)", retry)?;
330
331 let page = decode_element_status_page(&inquiry, &data, start_element_address)?;
332
333 retry = false; // only retry the first command
334
335 transports.extend(page.transports);
336 drives.extend(page.drives);
337 storage_slots.extend(page.storage_slots);
338 import_export_slots.extend(page.import_export_slots);
339
340 if data.len() < (allocation_len as usize) {
341 break;
342 }
343
344 if let Some(last_element_address) = page.last_element_address {
345 if last_element_address >= start_element_address {
346 start_element_address = last_element_address + 1;
347 } else {
348 bail!("got strange element address");
349 }
350 } else {
351 break;
352 }
353 }
354
355 if (setup.transport_element_count as usize) != transports.len() {
356 bail!("got wrong number of transport elements");
357 }
358 if (setup.storage_element_count as usize) != storage_slots.len() {
359 bail!("got wrong number of storage elements");
360 }
361 if (setup.import_export_element_count as usize) != import_export_slots.len() {
362 bail!("got wrong number of import/export elements");
363 }
364 if (setup.transfer_element_count as usize) != drives.len() {
365 bail!("got wrong number of tranfer elements");
366 }
367
368 // create same virtual slot order as mtx(1)
369 // - storage slots first
370 // - import export slots at the end
371 let mut slots = storage_slots;
372 slots.extend(import_export_slots);
373
374 let mut status = MtxStatus { transports, drives, slots };
375
376 // sanity checks
377 if status.drives.is_empty() {
378 bail!("no data transfer elements reported");
379 }
380 if status.slots.is_empty() {
381 bail!("no storage elements reported");
382 }
383
384 // compute virtual storage slot to element_address map
385 let mut slot_map = HashMap::new();
386 for (i, slot) in status.slots.iter().enumerate() {
387 slot_map.insert(slot.element_address, (i + 1) as u64);
388 }
389
390 // translate element addresses in loaded_lot
391 for drive in status.drives.iter_mut() {
392 if let Some(source_address) = drive.loaded_slot {
393 let source_address = source_address as u16;
394 drive.loaded_slot = slot_map.get(&source_address).map(|v| *v);
395 }
396 }
397
398 Ok(status)
399 }
400
401 /// Read status and map import-export slots from config
402 pub fn status(config: &ScsiTapeChanger) -> Result<MtxStatus, Error> {
403 let path = &config.path;
404
405 let mut file = open(path)
406 .map_err(|err| format_err!("error opening '{}': {}", path, err))?;
407 let mut status = read_element_status(&mut file)
408 .map_err(|err| format_err!("error reading element status: {}", err))?;
409
410 status.mark_import_export_slots(&config)?;
411
412 Ok(status)
413 }
414
415
416 #[repr(C, packed)]
417 #[derive(Endian)]
418 struct ElementStatusHeader {
419 first_element_address_reported: u16,
420 number_of_elements_available: u16,
421 reserved: u8,
422 byte_count_of_report_available: [u8;3],
423 }
424
425 #[repr(C, packed)]
426 #[derive(Endian)]
427 struct SubHeader {
428 element_type_code: u8,
429 flags: u8,
430 descriptor_length: u16,
431 reseved: u8,
432 byte_count_of_descriptor_data_available: [u8;3],
433 }
434
435 impl SubHeader {
436
437 fn parse_optional_volume_tag<R: Read>(
438 &self,
439 reader: &mut R,
440 full: bool,
441 ) -> Result<Option<String>, Error> {
442
443 if (self.flags & 128) != 0 { // has PVolTag
444 let tmp = reader.read_exact_allocated(36)?;
445 if full {
446 let volume_tag = scsi_ascii_to_string(&tmp);
447 return Ok(Some(volume_tag));
448 }
449 }
450 Ok(None)
451 }
452
453 // AFAIK, tape changer do not use AlternateVolumeTag
454 // but parse anyways, just to be sure
455 fn skip_alternate_volume_tag<R: Read>(
456 &self,
457 reader: &mut R,
458 ) -> Result<Option<String>, Error> {
459
460 if (self.flags & 64) != 0 { // has AVolTag
461 let _tmp = reader.read_exact_allocated(36)?;
462 }
463
464 Ok(None)
465 }
466 }
467
468 #[repr(C, packed)]
469 #[derive(Endian)]
470 struct TrasnsportDescriptor { // Robot/Griper
471 element_address: u16,
472 flags1: u8,
473 reserved_3: u8,
474 additional_sense_code: u8,
475 additional_sense_code_qualifier: u8,
476 reserved_6: [u8;3],
477 flags2: u8,
478 source_storage_element_address: u16,
479 // volume tag and Mixed media descriptor follows (depends on flags)
480 }
481
482 #[repr(C, packed)]
483 #[derive(Endian)]
484 struct TransferDescriptor { // Tape drive
485 element_address: u16,
486 flags1: u8,
487 reserved_3: u8,
488 additional_sense_code: u8,
489 additional_sense_code_qualifier: u8,
490 id_valid: u8,
491 scsi_bus_address: u8,
492 reserved_8: u8,
493 flags2: u8,
494 source_storage_element_address: u16,
495 // volume tag, drive identifier and Mixed media descriptor follows
496 // (depends on flags)
497 }
498
499 #[repr(C, packed)]
500 #[derive(Endian)]
501 struct DvcidHead { // Drive Identifier Header
502 code_set: u8,
503 identifier_type: u8,
504 reserved: u8,
505 identifier_len: u8,
506 // Identifier follows
507 }
508
509 #[repr(C, packed)]
510 #[derive(Endian)]
511 struct StorageDescriptor { // Mail Slot
512 element_address: u16,
513 flags1: u8,
514 reserved_3: u8,
515 additional_sense_code: u8,
516 additional_sense_code_qualifier: u8,
517 reserved_6: [u8;3],
518 flags2: u8,
519 source_storage_element_address: u16,
520 // volume tag and Mixed media descriptor follows (depends on flags)
521 }
522
523 struct DecodedStatusPage {
524 last_element_address: Option<u16>,
525 transports: Vec<TransportElementStatus>,
526 drives: Vec<DriveStatus>,
527 storage_slots: Vec<StorageElementStatus>,
528 import_export_slots: Vec<StorageElementStatus>,
529 }
530
531 fn create_element_status(full: bool, volume_tag: Option<String>) -> ElementStatus {
532 if full {
533 if let Some(volume_tag) = volume_tag {
534 ElementStatus::VolumeTag(volume_tag)
535 } else {
536 ElementStatus::Full
537 }
538 } else {
539 ElementStatus::Empty
540 }
541 }
542
543 fn decode_element_status_page(
544 _info: &InquiryInfo,
545 data: &[u8],
546 start_element_address: u16,
547 ) -> Result<DecodedStatusPage, Error> {
548
549 proxmox::try_block!({
550
551 let mut result = DecodedStatusPage {
552 last_element_address: None,
553 transports: Vec::new(),
554 drives: Vec::new(),
555 storage_slots: Vec::new(),
556 import_export_slots: Vec::new(),
557 };
558
559 let mut reader = &data[..];
560
561 let head: ElementStatusHeader = unsafe { reader.read_be_value()? };
562
563 if head.number_of_elements_available == 0 {
564 return Ok(result);
565 }
566
567 if head.first_element_address_reported < start_element_address {
568 bail!("got wrong first_element_address_reported"); // sanity check
569 }
570
571 loop {
572 if reader.is_empty() {
573 break;
574 }
575
576 let subhead: SubHeader = unsafe { reader.read_be_value()? };
577
578 let len = subhead.byte_count_of_descriptor_data_available;
579 let mut len = ((len[0] as usize) << 16) + ((len[1] as usize) << 8) + (len[2] as usize);
580 if len > reader.len() {
581 len = reader.len();
582 }
583
584 let descr_data = reader.read_exact_allocated(len)?;
585 let mut reader = &descr_data[..];
586
587 loop {
588 if reader.is_empty() {
589 break;
590 }
591 if reader.len() < (subhead.descriptor_length as usize) {
592 break;
593 }
594
595 match subhead.element_type_code {
596 1 => {
597 let desc: TrasnsportDescriptor = unsafe { reader.read_be_value()? };
598
599 let full = (desc.flags1 & 1) != 0;
600 let volume_tag = subhead.parse_optional_volume_tag(&mut reader, full)?;
601
602 subhead.skip_alternate_volume_tag(&mut reader)?;
603
604 let mut reserved = [0u8; 4];
605 reader.read_exact(&mut reserved)?;
606
607 result.last_element_address = Some(desc.element_address);
608
609 let status = TransportElementStatus {
610 status: create_element_status(full, volume_tag),
611 element_address: desc.element_address,
612 };
613 result.transports.push(status);
614 }
615 2 | 3 => {
616 let desc: StorageDescriptor = unsafe { reader.read_be_value()? };
617
618 let full = (desc.flags1 & 1) != 0;
619 let volume_tag = subhead.parse_optional_volume_tag(&mut reader, full)?;
620
621 subhead.skip_alternate_volume_tag(&mut reader)?;
622
623 let mut reserved = [0u8; 4];
624 reader.read_exact(&mut reserved)?;
625
626 result.last_element_address = Some(desc.element_address);
627
628 if subhead.element_type_code == 3 {
629 let status = StorageElementStatus {
630 import_export: true,
631 status: create_element_status(full, volume_tag),
632 element_address: desc.element_address,
633 };
634 result.import_export_slots.push(status);
635 } else {
636 let status = StorageElementStatus {
637 import_export: false,
638 status: create_element_status(full, volume_tag),
639 element_address: desc.element_address,
640 };
641 result.storage_slots.push(status);
642 }
643 }
644 4 => {
645 let desc: TransferDescriptor = unsafe { reader.read_be_value()? };
646
647 let loaded_slot = if (desc.flags2 & 128) != 0 { // SValid
648 Some(desc.source_storage_element_address as u64)
649 } else {
650 None
651 };
652
653 let full = (desc.flags1 & 1) != 0;
654 let volume_tag = subhead.parse_optional_volume_tag(&mut reader, full)?;
655
656 subhead.skip_alternate_volume_tag(&mut reader)?;
657
658 let dvcid: DvcidHead = unsafe { reader.read_be_value()? };
659
660 let (drive_serial_number, vendor, model) = match (dvcid.code_set, dvcid.identifier_type) {
661 (2, 0) => { // Serial number only (Quantum Superloader3 uses this)
662 let serial = reader.read_exact_allocated(dvcid.identifier_len as usize)?;
663 let serial = scsi_ascii_to_string(&serial);
664 (Some(serial), None, None)
665 }
666 (2, 1) => {
667 if dvcid.identifier_len != 34 {
668 bail!("got wrong DVCID length");
669 }
670 let vendor = reader.read_exact_allocated(8)?;
671 let vendor = scsi_ascii_to_string(&vendor);
672 let model = reader.read_exact_allocated(16)?;
673 let model = scsi_ascii_to_string(&model);
674 let serial = reader.read_exact_allocated(10)?;
675 let serial = scsi_ascii_to_string(&serial);
676 (Some(serial), Some(vendor), Some(model))
677 }
678 _ => (None, None, None),
679 };
680
681 result.last_element_address = Some(desc.element_address);
682
683 let drive = DriveStatus {
684 loaded_slot,
685 status: create_element_status(full, volume_tag),
686 drive_serial_number,
687 vendor,
688 model,
689 element_address: desc.element_address,
690 };
691 result.drives.push(drive);
692 }
693 code => bail!("got unknown element type code {}", code),
694 }
695 }
696 }
697
698 Ok(result)
699 }).map_err(|err: Error| format_err!("decode element status failed - {}", err))
700 }
701
702 /// Open the device for read/write, returns the file handle
703 pub fn open<P: AsRef<Path>>(path: P) -> Result<File, Error> {
704 let file = OpenOptions::new()
705 .read(true)
706 .write(true)
707 .open(path)?;
708
709 Ok(file)
710 }