Coverage for drivers/scsiutil.py : 29%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1#!/usr/bin/python3
2#
3# Copyright (C) Citrix Systems Inc.
4#
5# This program is free software; you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License as published
7# by the Free Software Foundation; version 2.1 only.
8#
9# This program is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with this program; if not, write to the Free Software Foundation, Inc.,
16# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17#
18# Miscellaneous scsi utility functions
19#
21import util
22import os
23import re
24import xs_errors
25import base64
26import time
27import errno
28import glob
29import mpath_cli
30import traceback
32PREFIX_LEN = 4
33SUFFIX_LEN = 12
34SECTOR_SHIFT = 9
35SCSI_ID_BIN = '/usr/lib/udev/scsi_id'
38def gen_hash(st, len):
39 hs = 0
40 for i in st:
41 hs = ord(i) + (hs << 6) + (hs << 16) - hs
42 return str(hs)[0:len]
45def gen_uuid_from_serial(iqn, serial):
46 if len(serial) < SUFFIX_LEN:
47 raise util.CommandException(1)
48 prefix = gen_hash(iqn, PREFIX_LEN)
49 suffix = gen_hash(serial, SUFFIX_LEN)
50 str = prefix.encode("hex") + suffix.encode("hex")
51 return str[0:8] + '-' + str[8:12] + '-' + str[12:16] + '-' + str[16:20] + '-' + str[20:32]
54def gen_serial_from_uuid(iqn, uuid):
55 str = uuid.replace('-', '')
56 prefix = gen_hash(iqn, PREFIX_LEN)
57 if str[0:(PREFIX_LEN * 2)].decode("hex") != prefix:
58 raise util.CommandException(1)
59 return str[(PREFIX_LEN * 2):].decode("hex")
62def getsize(path):
63 dev = getdev(path)
64 sysfs = os.path.join('/sys/block', dev, 'size')
65 size = 0
66 if os.path.exists(sysfs):
67 try:
68 f = open(sysfs, 'r')
69 size = (int(f.readline()) << SECTOR_SHIFT)
70 f.close()
71 except:
72 pass
73 return size
76def getuniqueserial(path):
77 dev = getdev(path)
78 try:
79 cmd = ["md5sum"]
80 txt = util.pread3(cmd, getSCSIid(path))
81 return txt.split(' ')[0]
82 except:
83 return ''
86def gen_uuid_from_string(src_string):
87 if len(src_string) < (PREFIX_LEN + SUFFIX_LEN):
88 raise util.CommandException(1)
89 return (src_string[0:8] + '-' +
90 src_string[8:12] + '-' +
91 src_string[12:16] + '-' +
92 src_string[16:20] + '-' +
93 src_string[20:32])
96def SCSIid_sanitise(str):
97 text = str.strip()
98 return re.sub(r"\s+", "_", text)
101def getSCSIid(path):
102 """Get the SCSI id of a block device
104 Input:
105 path -- (str) path to block device; can be symlink
107 Return:
108 scsi_id -- (str) the device's SCSI id
110 Raise:
111 util.CommandException
112 """
114 if not path.startswith('/dev/'): 114 ↛ 115line 114 didn't jump to line 115, because the condition on line 114 was never true
115 util.SMlog("getSCSIid: fixing invalid input {}".format(path),
116 priority=util.LOG_WARNING)
117 path = '/dev/' + path.lstrip('/')
119 stdout = util.pread2([SCSI_ID_BIN, '-g', '--device', path])
121 return SCSIid_sanitise(stdout)
124def compareSCSIid_2_6_18(SCSIid, path):
125 serial = getserial(path)
126 len_serial = len(serial)
127 if (len_serial == 0) or (len_serial > (len(SCSIid) - 1)):
128 return False
129 list_SCSIid = list(SCSIid)
130 list_serial = list_SCSIid[1:(len_serial + 1)]
131 serial_2_6_18 = ''.join(list_serial)
132 if (serial == serial_2_6_18):
133 return True
134 else:
135 return False
138def getserial(path):
139 dev = os.path.join('/dev', getdev(path))
140 try:
141 cmd = ["sginfo", "-s", dev]
142 text = re.sub(r"\s+", "", util.pread2(cmd))
143 except:
144 raise xs_errors.XenError('EIO', \
145 opterr='An error occured querying device serial number [%s]' \
146 % dev)
147 try:
148 return text.split("'")[1]
149 except:
150 return ''
153def getmanufacturer(path):
154 cmd = ["sginfo", "-M", path]
155 try:
156 for line in filter(match_vendor, util.pread2(cmd).split('\n')):
157 return line.replace(' ', '').split(':')[-1]
158 except:
159 return ''
162def cacheSCSIidentifiers():
163 SCSI = {}
164 SYS_PATH = "/dev/disk/by-scsibus/*"
165 for node in glob.glob(SYS_PATH):
166 if not re.match(r'.*-\d+:\d+:\d+:\d+$', node): 166 ↛ 167line 166 didn't jump to line 167, because the condition on line 166 was never true
167 continue
168 dev = os.path.realpath(node)
169 HBTL = os.path.basename(node).split("-")[-1].split(":")
170 line = "NONE %s %s %s %s 0 %s" % \
171 (HBTL[0], HBTL[1], HBTL[2], HBTL[3], dev)
172 ids = line.split()
173 SCSI[ids[6]] = ids
174 return SCSI
177def scsi_dev_ctrl(ids, cmd):
178 f = None
179 for i in range(0, 10):
180 try:
181 str = "scsi %s-single-device %s %s %s %s" % \
182 (cmd, ids[1], ids[2], ids[3], ids[4])
183 util.SMlog(str)
184 f = open('/proc/scsi/scsi', 'w')
185 print(str, file=f)
186 f.close()
187 return
188 except IOError as e:
189 util.SMlog("SCSI_DEV_CTRL: Failure, %s [%d]" % (e.strerror, e.errno))
190 if f is not None:
191 f.close()
192 f = None
193 if e.errno == errno.ENXIO:
194 util.SMlog("Device has disappeared already")
195 return
196 time.sleep(6)
197 continue
198 raise xs_errors.XenError('EIO', \
199 opterr='An error occured during the scsi operation')
202def getdev(path):
203 realpath = os.path.realpath(path)
204 if match_dm(realpath): 204 ↛ 205line 204 didn't jump to line 205, because the condition on line 204 was never true
205 newpath = realpath.replace("/dev/mapper/", "/dev/disk/by-id/scsi-")
206 else:
207 newpath = path
208 return os.path.realpath(newpath).split('/')[-1]
211def get_devices_by_SCSIid(SCSIid):
212 devices = os.listdir(os.path.join('/dev/disk/by-scsid', SCSIid))
213 if 'mapper' in devices:
214 devices.remove('mapper')
215 return devices
218def rawdev(dev):
219 device = getdev(dev)
220 if device.startswith('dm-') and device[3:].isdigit():
221 return device
223 return re.sub('[0-9]*$', '', device)
226def getSessionID(path):
227 for line in filter(match_session, util.listdir(path)):
228 return line.split('-')[-1]
231def match_session(s):
232 regex = re.compile("^SESSIONID-")
233 return regex.search(s, 0)
236def match_vendor(s):
237 regex = re.compile("^Vendor:")
238 return regex.search(s, 0)
241def match_dm(s):
242 regex = re.compile("mapper/")
243 return regex.search(s, 0)
246def match_sd(s):
247 regex = re.compile("/dev/sd")
248 return regex.search(s, 0)
251def _isSCSIdev(dev):
252 if match_dm(dev):
253 path = dev.replace("/dev/mapper/", "/dev/disk/by-id/scsi-")
254 else:
255 path = dev
256 return match_sd(os.path.realpath(path))
259def add_serial_record(session, sr_ref, devstring):
260 try:
261 conf = session.xenapi.SR.get_sm_config(sr_ref)
262 conf['devserial'] = devstring
263 session.xenapi.SR.set_sm_config(sr_ref, conf)
264 except:
265 pass
268def get_serial_record(session, sr_ref):
269 try:
270 conf = session.xenapi.SR.get_sm_config(sr_ref)
271 return conf['devserial']
272 except:
273 return ""
276def devlist_to_serialstring(devlist):
277 serial = ''
278 for dev in devlist:
279 try:
280 devserial = "scsi-%s" % getSCSIid(dev)
281 if not len(devserial) > 0:
282 continue
283 if len(serial):
284 serial += ','
285 serial += devserial
286 except:
287 pass
289 return serial
292def gen_synthetic_page_data(uuid):
293 # For generating synthetic page data for non-raw LUNs
294 # we set the vendor ID to XENSRC
295 # Note that the Page 80 serial number must be limited
296 # to 16 characters
297 page80 = ""
298 page80 += "\x00\x80"
299 page80 += "\x00\x12"
300 page80 += uuid[0:16]
301 page80 += " "
303 page83 = ""
304 page83 += "\x00\x83"
305 page83 += "\x00\x31"
306 page83 += "\x02\x01\x00\x2d"
307 page83 += "XENSRC "
308 page83 += uuid
309 page83 += " "
310 return ["", base64.b64encode(str.encode(page80)).decode(),
311 base64.b64encode(str.encode(page83)).decode()]
314def gen_raw_page_data(path):
315 default = ""
316 page80 = ""
317 page83 = ""
318 try:
319 cmd = ["sg_inq", "-r", path]
320 text = util.pread2(cmd)
321 default = base64.b64encode(text)
323 cmd = ["sg_inq", "--page=0x80", "-r", path]
324 text = util.pread2(cmd)
325 page80 = base64.b64encode(text)
327 cmd = ["sg_inq", "--page=0x83", "-r", path]
328 text = util.pread2(cmd)
329 page83 = base64.b64encode(text)
330 except:
331 pass
332 return [default, page80, page83]
335def update_XS_SCSIdata(vdi_uuid, data):
336 # XXX: PR-1255: passing through SCSI data doesn't make sense when
337 # it will change over storage migration. It also doesn't make sense
338 # to preserve one array's identity and copy it when a VM moves to
339 # a new array because the drivers in the VM may attempt to contact
340 # the original array, fail and bluescreen.
342 xenstore_data = {}
343 xenstore_data["vdi-uuid"] = vdi_uuid
344 if len(data[0]): 344 ↛ 345line 344 didn't jump to line 345, because the condition on line 344 was never true
345 xenstore_data["scsi/0x12/default"] = data[0]
347 if len(data[1]): 347 ↛ 350line 347 didn't jump to line 350, because the condition on line 347 was never false
348 xenstore_data["scsi/0x12/0x80"] = data[1]
350 if len(data[2]): 350 ↛ 353line 350 didn't jump to line 353, because the condition on line 350 was never false
351 xenstore_data["scsi/0x12/0x83"] = data[2]
353 return xenstore_data
356def rescan(ids, fullrescan=True):
357 for id in ids:
358 refresh_HostID(id, fullrescan)
361def _genHostList(procname):
362 # loop through and check all adapters
363 ids = []
364 try:
365 for dir in util.listdir('/sys/class/scsi_host'):
366 filename = os.path.join('/sys/class/scsi_host', dir, 'proc_name')
367 if os.path.exists(filename):
368 f = open(filename, 'r')
369 if f.readline().find(procname) != -1:
370 ids.append(dir.replace("host", ""))
371 f.close()
372 except:
373 pass
374 return ids
377def _genReverseSCSIidmap(SCSIid, pathname="scsibus"):
378 util.SMlog("map_by_scsibus: sid=%s" % SCSIid)
380 devices = []
381 for link in glob.glob('/dev/disk/by-%s/%s-*' % (pathname, SCSIid)):
382 realpath = os.path.realpath(link)
383 if os.path.exists(realpath):
384 devices.append(realpath)
385 return devices
388def _genReverseSCSidtoLUNidmap(SCSIid):
389 devices = []
390 for link in glob.glob('/dev/disk/by-scsibus/%s-*' % SCSIid):
391 devices.append(link.split('-')[-1])
392 return devices
395def _dosgscan():
396 regex = re.compile(r"([^:]*):\s+scsi([0-9]+)\s+channel=([0-9]+)\s+id=([0-9]+)\s+lun=([0-9]+)")
397 scan = util.pread2(["/usr/bin/sg_scan"]).split('\n')
398 sgs = []
399 for line in scan:
400 m = regex.match(line)
401 if m:
402 device = m.group(1)
403 host = m.group(2)
404 channel = m.group(3)
405 sid = m.group(4)
406 lun = m.group(5)
407 sgs.append([device, host, channel, sid, lun])
408 return sgs
411def refresh_HostID(HostID, fullrescan):
412 LUNs = glob.glob('/sys/class/scsi_disk/%s*' % HostID)
413 li = []
414 for l in LUNs:
415 chan = re.sub(":[0-9]*$", '', os.path.basename(l))
416 if chan not in li: 416 ↛ 414line 416 didn't jump to line 414, because the condition on line 416 was never false
417 li.append(chan)
419 if len(li) and not fullrescan: 419 ↛ 420line 419 didn't jump to line 420, because the condition on line 419 was never true
420 for c in li:
421 if not refresh_scsi_channel(c):
422 fullrescan = True
424 if fullrescan: 424 ↛ exitline 424 didn't return from function 'refresh_HostID', because the condition on line 424 was never false
425 util.SMlog("Full rescan of HostID %s" % HostID)
426 path = '/sys/class/scsi_host/host%s/scan' % HostID
427 if os.path.exists(path): 427 ↛ 428line 427 didn't jump to line 428, because the condition on line 427 was never true
428 try:
429 scanstring = "- - -"
430 f = open(path, 'w')
431 f.write('%s\n' % scanstring)
432 f.close()
433 if len(li):
434 # Channels already exist, allow some time for
435 # undiscovered LUNs/channels to appear
436 time.sleep(2)
437 except:
438 pass
439 # Host Bus scan issued, now try to detect channels
440 if util.wait_for_path("/sys/class/scsi_disk/%s*" % HostID, 5): 440 ↛ exitline 440 didn't return from function 'refresh_HostID', because the condition on line 440 was never false
441 # At least one LUN is mapped
442 LUNs = glob.glob('/sys/class/scsi_disk/%s*' % HostID)
443 li = []
444 for l in LUNs:
445 chan = re.sub(":[0-9]*$", '', os.path.basename(l))
446 if chan not in li: 446 ↛ 444line 446 didn't jump to line 444, because the condition on line 446 was never false
447 li.append(chan)
448 for c in li:
449 refresh_scsi_channel(c)
452def refresh_scsi_channel(channel):
453 DEV_WAIT = 5
454 util.SMlog("Refreshing channel %s" % channel)
455 util.wait_for_path('/dev/disk/by-scsibus/*-%s*' % channel, DEV_WAIT)
456 LUNs = glob.glob('/dev/disk/by-scsibus/*-%s*' % channel)
457 try:
458 rootdevs = util.dom0_disks()
459 except:
460 util.SMlog("Failed to query root disk, failing operation")
461 return False
463 # a) Find a LUN to issue a Query LUNs command
464 li = []
465 Query = False
466 for lun in LUNs:
467 try:
468 hbtl = lun.split('-')[-1]
469 h = hbtl.split(':')
470 l = util.pread2(["/usr/bin/sg_luns", "-q", lun]).split('\n')
471 li = []
472 for i in l:
473 if len(i):
474 li.append(int(i[0:4], 16))
475 util.SMlog("sg_luns query returned %s" % li)
476 Query = True
477 break
478 except:
479 pass
480 if not Query:
481 util.SMlog("Failed to detect or query LUN on Channel %s" % channel)
482 return False
484 # b) Remove stale LUNs
485 current = glob.glob('/dev/disk/by-scsibus/*-%s:%s:%s*' % (h[0], h[1], h[2]))
486 for cur in current:
487 lunID = int(cur.split(':')[-1])
488 newhbtl = ['', h[0], h[1], h[2], str(lunID)]
489 if os.path.realpath(cur) in rootdevs:
490 # Don't touch the rootdev
491 if lunID in li:
492 li.remove(lunID)
493 continue
495 # Check if LUN is stale, and remove it
496 if not lunID in li:
497 util.SMlog("Stale LUN detected. Removing HBTL: %s" % newhbtl)
498 scsi_dev_ctrl(newhbtl, "remove")
499 util.wait_for_nopath(cur, DEV_WAIT)
500 continue
501 else:
502 li.remove(lunID)
504 # Check if the device is still present
505 if not os.path.exists(cur):
506 continue
508 # Query SCSIid, check it matches, if not, re-probe
509 cur_SCSIid = os.path.basename(cur).split("-%s:%s:%s" % (h[0], h[1], h[2]))[0]
510 real_SCSIid = getSCSIid(cur)
511 if cur_SCSIid != real_SCSIid:
512 util.SMlog("HBTL %s does not match, re-probing" % newhbtl)
513 scsi_dev_ctrl(newhbtl, "remove")
514 util.wait_for_nopath(cur, DEV_WAIT)
515 scsi_dev_ctrl(newhbtl, "add")
516 util.wait_for_path('/dev/disk/by-scsibus/%s-%s' % (real_SCSIid, hbtl), DEV_WAIT)
517 pass
519 # c) Probe for any LUNs that are not present in the system
520 for l in li:
521 newhbtl = ['', h[0], h[1], h[2], str(l)]
522 newhbtlstr = "%s:%s:%s:%s" % (h[0], h[1], h[2], str(l))
523 util.SMlog("Probing new HBTL: %s" % newhbtl)
524 scsi_dev_ctrl(newhbtl, "add")
525 util.wait_for_path('/dev/disk/by-scsibus/*-%s' % newhbtlstr, DEV_WAIT)
527 return True
530def refreshdev(pathlist):
531 """
532 Refresh block devices given a path list
533 """
534 for path in pathlist:
535 dev = getdev(path)
536 sysfs = os.path.join('/sys/block', dev, 'device/rescan')
537 if os.path.exists(sysfs):
538 try:
539 with open(sysfs, 'w') as f:
540 f.write('1')
541 except:
542 pass
545def refresh_lun_size_by_SCSIid(SCSIid):
546 """
547 Refresh all devices for the SCSIid.
548 Returns True if all known devices and the mapper device are up to date.
549 """
551 def get_primary_device(SCSIid):
552 mapperdevice = os.path.join('/dev/mapper', SCSIid)
553 if os.path.exists(mapperdevice):
554 return mapperdevice
555 else:
556 devices = get_devices_by_SCSIid(SCSIid)
557 if devices:
558 return devices[0]
559 else:
560 return None
562 def get_outdated_size_devices(currentcapacity, devices):
563 devicesthatneedrefresh = []
564 for device in devices:
565 if getsize(device) != currentcapacity:
566 devicesthatneedrefresh.append(device)
567 return devicesthatneedrefresh
569 def refresh_devices_if_needed(primarydevice, SCSIid, currentcapacity):
570 devices = get_devices_by_SCSIid(SCSIid)
571 if "/dev/mapper/" in primarydevice:
572 devices = set(devices + mpath_cli.list_paths(SCSIid))
573 devicesthatneedrefresh = get_outdated_size_devices(currentcapacity,
574 devices)
575 if devicesthatneedrefresh:
576 # timing out avoids waiting for min(dev_loss_tmo, fast_io_fail_tmo)
577 # if one or multiple devices don't answer
578 util.timeout_call(10, refreshdev, devicesthatneedrefresh)
579 if get_outdated_size_devices(currentcapacity,
580 devicesthatneedrefresh):
581 # in this state we shouldn't force resizing the mapper dev
582 raise util.SMException("Failed to get %s to agree on the "
583 "current capacity."
584 % devicesthatneedrefresh)
586 def refresh_mapper_if_needed(primarydevice, SCSIid, currentcapacity):
587 if "/dev/mapper/" in primarydevice \
588 and get_outdated_size_devices(currentcapacity, [primarydevice]):
589 mpath_cli.resize_map(SCSIid)
590 if get_outdated_size_devices(currentcapacity, [primarydevice]):
591 raise util.SMException("Failed to get the mapper dev to agree "
592 "on the current capacity.")
594 try:
595 primarydevice = get_primary_device(SCSIid)
596 if primarydevice:
597 currentcapacity = sg_readcap(primarydevice)
598 refresh_devices_if_needed(primarydevice, SCSIid, currentcapacity)
599 refresh_mapper_if_needed(primarydevice, SCSIid, currentcapacity)
600 else:
601 util.SMlog("scsiutil.refresh_lun_size_by_SCSIid(%s) could not "
602 "find any devices for the SCSIid." % SCSIid)
603 return True
604 except:
605 util.logException(f"Error in scsiutil.refresh_lun_size_by_SCSIid({SCSIid}: {traceback.format_exc()})")
606 return False
609def refresh_lun_size_by_SCSIid_on_slaves(session, SCSIid):
610 for slave in util.get_all_slaves(session):
611 util.SMlog("Calling on-slave.refresh_lun_size_by_SCSIid(%s) on %s."
612 % (SCSIid, slave))
613 resulttext = session.xenapi.host.call_plugin(
614 slave,
615 "on-slave",
616 "refresh_lun_size_by_SCSIid",
617 {'SCSIid': SCSIid})
618 if "True" == resulttext:
619 util.SMlog("Calling on-slave.refresh_lun_size_by_SCSIid(%s) on"
620 " %s succeeded." % (SCSIid, slave))
621 else:
622 message = ("Failed in on-slave.refresh_lun_size_by_SCSIid(%s) "
623 "on %s." % (SCSIid, slave))
624 raise util.SMException("Slave %s failed in on-slave.refresh_lun_"
625 "size_by_SCSIid(%s) " % (slave, SCSIid))
628def remove_stale_luns(hostids, lunid, expectedPath, mpath):
629 try:
630 for hostid in hostids:
631 # get all LUNs of the format hostid:x:y:lunid
632 luns = glob.glob('/dev/disk/by-scsibus/*-%s:*:*:%s' % (hostid, lunid))
634 # try to get the scsiid for each of these luns
635 for lun in luns:
636 try:
637 getSCSIid(lun)
638 # if it works, we have a problem as this device should not
639 # be present and be valid on this system
640 util.SMlog("Warning! The lun %s should not be present and" \
641 " be valid on this system." % lun)
642 except:
643 # Now do the rest.
644 pass
646 # get the HBTL
647 basename = os.path.basename(lun)
648 hbtl_list = basename.split(':')
649 hbtl = basename.split('-')[1]
651 # the first one in scsiid-hostid
652 hbtl_list[0] = hbtl_list[0].split('-')[1]
654 expectedPath = expectedPath + '*' + hbtl
655 if not os.path.exists(expectedPath):
656 # wait for sometime and check if the expected path exists
657 # check if a rescan was done outside of this process
658 time.sleep(2)
660 if os.path.exists(expectedPath):
661 # do not remove device, this might be dangerous
662 util.SMlog("Path %s appeared before checking for " \
663 "stale LUNs, ignore this LUN %s." % (expectedPath, lun))
664 continue
666 # remove the scsi device
667 l = [os.path.realpath(lun), hbtl_list[0], hbtl_list[1], \
668 hbtl_list[2], hbtl_list[3]]
669 scsi_dev_ctrl(l, 'remove')
671 # if multipath is enabled, do a best effort cleanup
672 if mpath:
673 try:
674 path = os.path.basename(os.path.realpath(lun))
675 mpath_cli.remove_path(path)
676 except Exception as e:
677 util.SMlog("Failed to remove path %s, ignoring " \
678 "exception as path may not be present." % path)
679 except Exception as e:
680 util.SMlog("Exception removing stale LUNs, new devices may not come" \
681 " up properly! Error: %s" % str(e))
684def sg_readcap(device):
685 device = os.path.join('/dev', getdev(device))
686 readcapcommand = ['/usr/bin/sg_readcap', '-b', device]
687 (rc, stdout, stderr) = util.doexec(readcapcommand)
688 if rc == 6:
689 # retry one time for "Capacity data has changed"
690 (rc, stdout, stderr) = util.doexec(readcapcommand)
691 if rc != 0: 691 ↛ 692line 691 didn't jump to line 692, because the condition on line 691 was never true
692 raise util.SMException("scsiutil.sg_readcap(%s) failed" % (device))
693 match = re.search('(^|.*\n)(0x[0-9a-fA-F]+) (0x[0-9a-fA-F]+)\n$', stdout)
694 if not match: 694 ↛ 695line 694 didn't jump to line 695, because the condition on line 694 was never true
695 raise util.SMException("scsiutil.sg_readcap(%s) failed to parse: %s"
696 % (device, stdout))
697 (blockcount, blocksize) = match.group(2, 3)
698 return (int(blockcount, 0) * int(blocksize, 0))