Manuel Amador (Rudd-O) 05c020e1f6 Source code committed
2010-08-11 09:13:29 -07:00

671 lines
26 KiB
Python
Executable File

#!/usr/bin/python
# Copyright (C) 2006-2007 XenSource Ltd.
# Copyright (C) 2008-2009 Citrix Ltd.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation; version 2.1 only.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# ISCSISR: ISCSI software initiator SR driver
#
import SR, VDI, SRCommand, util
import statvfs, time, LUNperVDI
import os, socket, sys, re, glob
import xml.dom.minidom
import shutil, xmlrpclib
import scsiutil, iscsilib
import xs_errors, errno
CAPABILITIES = ["SR_PROBE","VDI_CREATE","VDI_DELETE","VDI_ATTACH",
"VDI_DETACH", "VDI_INTRODUCE"]
CONFIGURATION = [ [ 'target', 'IP address or hostname of the iSCSI target (required)' ], \
[ 'targetIQN', 'The IQN of the target LUN group to be attached (required)' ], \
[ 'chapuser', 'The username to be used during CHAP authentication (optional)' ], \
[ 'chappassword', 'The password to be used during CHAP authentication (optional)' ], \
[ 'port', 'The network port number on which to query the target (optional)' ], \
[ 'multihomed', 'Enable multi-homing to this target, true or false (optional, defaults to same value as host.other_config:multipathing)' ] ]
DRIVER_INFO = {
'name': 'iSCSI',
'description': 'Base ISCSI SR driver, provides a LUN-per-VDI. Does not support creation of VDIs but accesses existing LUNs on a target.',
'vendor': 'Citrix Systems Inc',
'copyright': '(C) 2008 Citrix Systems Inc',
'driver_version': '1.0',
'required_api_version': '1.0',
'capabilities': CAPABILITIES,
'configuration': CONFIGURATION
}
INITIATORNAME_FILE = '/etc/iscsi/initiatorname.iscsi'
SECTOR_SHIFT = 9
DEFAULT_PORT = 3260
# 2^16 Max port number value
MAXPORT = 65535
MAX_TIMEOUT = 15
MAX_LUNID_TIMEOUT = 60
ISCSI_PROCNAME = "iscsi_tcp"
class ISCSISR(SR.SR):
"""ISCSI storage repository"""
def handles(type):
if type == "iscsi":
return True
return False
handles = staticmethod(handles)
def _synchroniseAddrList(self, addrlist):
if not self.multihomed:
return
change = False
if not self.dconf.has_key('multihomelist'):
change = True
self.mlist = []
mstr = ""
else:
self.mlist = self.dconf['multihomelist'].split(',')
mstr = self.dconf['multihomelist']
for val in addrlist:
if not val in self.mlist:
self.mlist.append(val)
if len(mstr):
mstr += ","
mstr += val
change = True
if change:
pbd = None
try:
pbd = util.find_my_pbd(self.session, self.host_ref, self.sr_ref)
if pbd <> None:
device_config = self.session.xenapi.PBD.get_device_config(pbd)
device_config['multihomelist'] = mstr
self.session.xenapi.PBD.set_device_config(pbd, device_config)
except:
pass
@util.transformpasswords
def load(self, sr_uuid):
self.sr_vditype = 'phy'
self.discoverentry = 0
self.default_vdi_visibility = False
# Required parameters
if not self.dconf.has_key('target') or not self.dconf['target']:
raise xs_errors.XenError('ConfigTargetMissing')
# we are no longer putting hconf in the xml.
# Instead we pass a session and host ref and let the SM backend query XAPI itself
try:
if not self.dconf.has_key('localIQN'):
self.localIQN = self.session.xenapi.host.get_other_config(self.host_ref)['iscsi_iqn']
else:
self.localIQN = self.dconf['localIQN']
except:
raise xs_errors.XenError('ConfigISCSIIQNMissing')
# Check for empty string
if not self.localIQN:
raise xs_errors.XenError('ConfigISCSIIQNMissing')
try:
self.target = util._convertDNS(self.dconf['target'].split(',')[0])
except:
raise xs_errors.XenError('DNSError')
self.targetlist = self.target
if self.dconf.has_key('targetlist'):
self.targetlist = self.dconf['targetlist']
# Optional parameters
self.chapuser = ""
self.chappassword = ""
if self.dconf.has_key('chapuser') \
and (self.dconf.has_key('chappassword') or self.dconf.has_key('chappassword_secret')):
self.chapuser = self.dconf['chapuser']
if self.dconf.has_key('chappassword_secret'):
self.chappassword = util.get_secret(self.session, self.dconf['chappassword_secret'])
else:
self.chappassword = self.dconf['chappassword']
self.port = DEFAULT_PORT
if self.dconf.has_key('port') and self.dconf['port']:
try:
self.port = long(self.dconf['port'])
except:
raise xs_errors.XenError('ISCSIPort')
if self.port > MAXPORT or self.port < 1:
raise xs_errors.XenError('ISCSIPort')
# For backwards compatibility
if self.dconf.has_key('usediscoverynumber'):
self.discoverentry = self.dconf['usediscoverynumber']
self.multihomed = False
if self.dconf.has_key('multihomed'):
if self.dconf['multihomed'] == "true":
self.multihomed = True
elif self.mpath == 'true':
self.multihomed = True
if not self.dconf.has_key('targetIQN') or not self.dconf['targetIQN']:
self._scan_IQNs()
raise xs_errors.XenError('ConfigTargetIQNMissing')
self.targetIQN = self.dconf['targetIQN']
self.attached = False
try:
self.attached = iscsilib._checkTGT(self.targetIQN)
except:
pass
self._initPaths()
def _initPaths(self):
self._init_adapters()
# Generate a list of all possible paths
self.pathdict = {}
addrlist = []
rec = {}
key = "%s:%d" % (self.target,self.port)
rec['ipaddr'] = self.target
rec['port'] = self.port
rec['path'] = os.path.join("/dev/iscsi",self.targetIQN,\
key)
self.pathdict[key] = rec
util.SMlog("PATHDICT: key %s: %s" % (key,rec))
self.tgtidx = key
addrlist.append(key)
self.path = rec['path']
self.address = self.tgtidx
if not self.attached:
return
if self.multihomed:
map = iscsilib.get_node_records(targetIQN=self.targetIQN)
for i in range(0,len(map)):
(portal,tpgt,iqn) = map[i]
(ipaddr,port) = portal.split(',')[0].split(':')
if self.target != ipaddr:
key = "%s:%s" % (ipaddr,port)
rec = {}
rec['ipaddr'] = ipaddr
rec['port'] = long(port)
rec['path'] = os.path.join("/dev/iscsi",self.targetIQN,\
key)
self.pathdict[key] = rec
util.SMlog("PATHDICT: key %s: %s" % (key,rec))
addrlist.append(key)
# Try to detect an active path in order of priority
for key in self.pathdict:
if self.adapter.has_key(key):
self.tgtidx = key
self.path = self.pathdict[self.tgtidx]['path']
if os.path.exists(self.path):
util.SMlog("Path found: %s" % self.path)
break
self.address = self.tgtidx
self._synchroniseAddrList(addrlist)
def _init_adapters(self):
# Generate a list of active adapters
ids = scsiutil._genHostList(ISCSI_PROCNAME)
util.SMlog(ids)
self.adapter = {}
for host in ids:
try:
targetIQN = util.get_single_entry(glob.glob(\
'/sys/class/iscsi_host/host%s/device/session*/iscsi_session*/targetname' % host)[0])
if targetIQN != self.targetIQN:
continue
addr = util.get_single_entry(glob.glob(\
'/sys/class/iscsi_host/host%s/device/session*/connection*/iscsi_connection*/persistent_address' % host)[0])
port = util.get_single_entry(glob.glob(\
'/sys/class/iscsi_host/host%s/device/session*/connection*/iscsi_connection*/persistent_port' % host)[0])
entry = "%s:%s" % (addr,port)
self.adapter[entry] = host
except:
pass
self.devs = scsiutil.cacheSCSIidentifiers()
def attach(self, sr_uuid):
self._mpathHandle()
npaths=0
if not self.attached:
# Verify iSCSI target and port
if self.dconf.has_key('multihomelist') and not self.dconf.has_key('multiSession'):
targetlist = self.dconf['multihomelist'].split(',')
else:
targetlist = ['%s:%d' % (self.target,self.port)]
conn = False
for val in targetlist:
(target,port) = val.split(':')
try:
util._testHost(target, long(port), 'ISCSITarget')
self.target = target
self.port = long(port)
conn = True
break
except:
pass
if not conn:
raise xs_errors.XenError('ISCSITarget')
# Test and set the initiatorname file
iscsilib.ensure_daemon_running_ok(self.localIQN)
# Check to see if auto attach was set
if not iscsilib._checkTGT(self.targetIQN):
try:
map = iscsilib.discovery(self.target, self.port, self.chapuser, \
self.chappassword, targetIQN=self.targetIQN)
iqn = ''
if len(map) == 0:
self._scan_IQNs()
raise xs_errors.XenError('ISCSIDiscovery',
opterr='check target settings')
for i in range(0,len(map)):
(portal,tpgt,iqn) = map[i]
try:
(ipaddr,port) = portal.split(',')[0].split(':')
if not self.multihomed and ipaddr != self.target:
continue
util._testHost(ipaddr, long(port), 'ISCSITarget')
util.SMlog("Logging in to [%s:%s]" % (ipaddr,port))
iscsilib.login(portal, iqn, self.chapuser, self.chappassword)
npaths = npaths + 1
except:
pass
if not iscsilib._checkTGT(self.targetIQN):
raise xs_errors.XenError('ISCSIDevice', \
opterr='during login')
# Allow the devices to settle
time.sleep(5)
except util.CommandException, inst:
raise xs_errors.XenError('ISCSILogin', \
opterr='code is %d' % inst.code)
self.attached = True
self._initPaths()
util._incr_iscsiSR_refcount(self.targetIQN, sr_uuid)
IQNs = []
if self.dconf.has_key("multiSession"):
IQNs = ""
for iqn in self.dconf['multiSession'].split("|"):
if len(iqn): IQNs += iqn.split(',')[2]
else:
IQNs.append(self.targetIQN)
sessions = 0
paths = glob.glob(\
'/sys/class/iscsi_host/host*/device/session*/iscsi_session*/targetname')
for path in paths:
try:
if util.get_single_entry(path) in IQNs:
sessions += 1
util.SMlog("IQN match. Incrementing sessions to %d" % sessions)
except:
util.SMlog("Failed to read targetname path," \
+ "iscsi_sessions value may be incorrect")
try:
pbdref = util.find_my_pbd(self.session, self.host_ref, self.sr_ref)
if pbdref <> None:
other_conf = self.session.xenapi.PBD.get_other_config(pbdref)
other_conf['iscsi_sessions'] = str(sessions)
self.session.xenapi.PBD.set_other_config(pbdref, other_conf)
except:
pass
if self.mpath == 'true' and self.dconf.has_key('SCSIid'):
self.mpathmodule.refresh(self.dconf['SCSIid'],npaths)
def detach(self, sr_uuid):
keys = []
pbdref = None
try:
pbdref = util.find_my_pbd(self.session, self.host_ref, self.sr_ref)
except:
pass
if self.dconf.has_key('SCSIid'):
self.mpathmodule.reset(self.dconf['SCSIid'], True) # explicitly unmap
keys.append("mpath-" + self.dconf['SCSIid'])
# Remove iscsi_sessions and multipathed keys
if pbdref <> None:
if self.cmd == 'sr_detach':
keys += ["multipathed", "iscsi_sessions"]
for key in keys:
try:
self.session.xenapi.PBD.remove_from_other_config(pbdref, key)
except:
pass
if util._decr_iscsiSR_refcount(self.targetIQN, sr_uuid) != 0:
return
if self.direct and util._containsVDIinuse(self):
return
if iscsilib._checkTGT(self.targetIQN):
try:
iscsilib.logout(self.target, self.targetIQN, all=True)
except util.CommandException, inst:
raise xs_errors.XenError('ISCSIQueryDaemon', \
opterr='error is %d' % inst.code)
if iscsilib._checkTGT(self.targetIQN):
raise xs_errors.XenError('ISCSIQueryDaemon', \
opterr='Failed to logout from target')
self.attached = False
def create(self, sr_uuid, size):
# Check whether an SR already exists
SRs = self.session.xenapi.SR.get_all_records()
for sr in SRs:
record = SRs[sr]
sm_config = record["sm_config"]
if sm_config.has_key('targetIQN') and \
sm_config['targetIQN'] == self.targetIQN:
raise xs_errors.XenError('SRInUse')
self.attach(sr_uuid)
# Wait up to MAX_TIMEOUT for devices to appear
util.wait_for_path(self.path, MAX_TIMEOUT)
if self._loadvdis() > 0:
scanrecord = SR.ScanRecord(self)
scanrecord.synchronise()
try:
self.detach(sr_uuid)
except:
pass
self.sm_config = self.session.xenapi.SR.get_sm_config(self.sr_ref)
self.sm_config['disktype'] = 'Raw'
self.sm_config['datatype'] = 'ISCSI'
self.sm_config['target'] = self.target
self.sm_config['targetIQN'] = self.targetIQN
self.sm_config['multipathable'] = 'true'
self.session.xenapi.SR.set_sm_config(self.sr_ref, self.sm_config)
return
def delete(self, sr_uuid):
self.detach(sr_uuid)
return
def probe(self):
SRs = self.session.xenapi.SR.get_all_records()
Recs = {}
for sr in SRs:
record = SRs[sr]
sm_config = record["sm_config"]
if sm_config.has_key('targetIQN') and \
sm_config['targetIQN'] == self.targetIQN:
Recs[record["uuid"]] = sm_config
return self.srlist_toxml(Recs)
def scan(self, sr_uuid):
if not self.passthrough:
if not self.attached:
raise xs_errors.XenError('SRUnavailable')
self.refresh()
time.sleep(2) # it seems impossible to tell when a scan's finished
self._loadvdis()
self.physical_utilisation = self.physical_size
for uuid, vdi in self.vdis.iteritems():
if vdi.managed:
self.physical_utilisation += vdi.size
self.virtual_allocation = self.physical_utilisation
return super(ISCSISR, self).scan(sr_uuid)
def vdi(self, uuid):
return LUNperVDI.RAWVDI(self, uuid)
def _scan_IQNs(self):
# Verify iSCSI target and port
util._testHost(self.target, self.port, 'ISCSITarget')
# Test and set the initiatorname file
iscsilib.ensure_daemon_running_ok(self.localIQN)
map = iscsilib.discovery(self.target, self.port, self.chapuser, self.chappassword)
map.append(("%s:%d" % (self.targetlist,self.port),"0","*"))
self.print_entries(map)
def _attach_LUN_bylunid(self, lunid):
if not self.attached:
raise xs_errors.XenError('SRUnavailable')
connected = []
for val in self.adapter:
if not self.pathdict.has_key(val):
continue
rec = self.pathdict[val]
path = os.path.join(rec['path'],"LUN%s" % lunid)
util.SMlog("path: %s" % path)
realpath = os.path.realpath(path)
util.SMlog("realpath: %s" % realpath)
host = self.adapter[val]
if not self.devs.has_key(realpath):
l = [realpath, host, 0, 0, lunid]
scsiutil.scsi_dev_ctrl(l,"add")
if not util.wait_for_path(path, MAX_LUNID_TIMEOUT):
util.SMlog("Unable to detect LUN attached to host on path [%s]" % path)
continue
else:
# Verify that we are not seeing a stale LUN map
try:
real_SCSIid = scsiutil.getSCSIid(realpath)
cur_scsibuspath = glob.glob('/dev/disk/by-scsibus/*-%s:0:0:%s' % (host,lunid))
cur_SCSIid = os.path.basename(cur_scsibuspath[0]).split("-")[0]
assert(cur_SCSIid == real_SCSIid)
except:
scsiutil.rescan([host])
if not os.path.exists('/dev/disk/by-scsibus/%s-%s:0:0:%s' % \
(real_SCSIid,host,lunid)):
util.SMlog("Unable to detect LUN attached to host after bus re-probe")
continue
connected.append(path)
return connected
def _attach_LUN_byserialid(self, serialid):
if not self.attached:
raise xs_errors.XenError('SRUnavailable')
connected = []
for val in self.adapter:
if not self.pathdict.has_key(val):
continue
rec = self.pathdict[val]
path = os.path.join(rec['path'],"SERIAL-%s" % serialid)
realpath = os.path.realpath(path)
if not self.devs.has_key(realpath):
if not util.wait_for_path(path, 5):
util.SMlog("Unable to detect LUN attached to host on serial path [%s]" % path)
continue
connected.append(path)
return connected
def _detach_LUN_bylunid(self, lunid, SCSIid):
if not self.attached:
raise xs_errors.XenError('SRUnavailable')
if self.mpath == 'true' and len(SCSIid):
self.mpathmodule.reset(SCSIid, True)
util.remove_mpathcount_field(self.session, self.host_ref, self.sr_ref, SCSIid)
for val in self.adapter:
if not self.pathdict.has_key(val):
continue
rec = self.pathdict[val]
path = os.path.join(rec['path'],"LUN%s" % lunid)
realpath = os.path.realpath(path)
if self.devs.has_key(realpath):
util.SMlog("Found key: %s" % realpath)
scsiutil.scsi_dev_ctrl(self.devs[realpath], 'remove')
# Wait for device to disappear
if not util.wait_for_nopath(realpath, MAX_LUNID_TIMEOUT):
util.SMlog("Device has not disappeared after %d seconds" % \
MAX_LUNID_TIMEOUT)
else:
util.SMlog("Device [%s,%s] disappeared" % (realpath,path))
def _attach_LUN_bySCSIid(self, SCSIid):
if not self.attached:
raise xs_errors.XenError('SRUnavailable')
path = self.mpathmodule.path(SCSIid)
if not util.pathexists(path):
self.refresh()
if not util.wait_for_path(path, MAX_TIMEOUT):
util.SMlog("Unable to detect LUN attached to host [%s]" \
% path)
return False
return True
# This function queries the session for the attached LUNs
def _loadvdis(self):
count = 0
if not os.path.exists(self.path):
return 0
for file in filter(self.match_lun, util.listdir(self.path)):
vdi_path = os.path.join(self.path,file)
LUNid = file.replace("LUN","")
uuid = scsiutil.gen_uuid_from_string(scsiutil.getuniqueserial(vdi_path))
obj = self.vdi(uuid)
obj._query(vdi_path, LUNid)
self.vdis[uuid] = obj
self.physical_size += obj.size
count += 1
return count
def refresh(self):
for val in self.adapter:
util.SMlog("Rescanning host adapter %s" % self.adapter[val])
scsiutil.rescan([self.adapter[val]])
# Helper function for LUN-per-VDI VDI.introduce
def _getLUNbySMconfig(self, sm_config):
if not sm_config.has_key('LUNid'):
raise xs_errors.XenError('VDIUnavailable')
LUNid = long(sm_config['LUNid'])
if not len(self._attach_LUN_bylunid(LUNid)):
raise xs_errors.XenError('VDIUnavailable')
return os.path.join(self.path,"LUN%d" % LUNid)
def print_LUNs(self):
self.LUNs = {}
if os.path.exists(self.path):
for file in util.listdir(self.path):
if file.find("LUN") != -1 and file.find("_") == -1:
vdi_path = os.path.join(self.path,file)
LUNid = file.replace("LUN","")
obj = self.vdi(self.uuid)
obj._query(vdi_path, LUNid)
self.LUNs[obj.uuid] = obj
dom = xml.dom.minidom.Document()
element = dom.createElement("iscsi-target")
dom.appendChild(element)
for uuid in self.LUNs:
val = self.LUNs[uuid]
entry = dom.createElement('LUN')
element.appendChild(entry)
for attr in ('vendor', 'serial', 'LUNid', \
'size', 'SCSIid'):
try:
aval = getattr(val, attr)
except AttributeError:
continue
if aval:
subentry = dom.createElement(attr)
entry.appendChild(subentry)
textnode = dom.createTextNode(str(aval))
subentry.appendChild(textnode)
print >>sys.stderr,dom.toprettyxml()
def print_entries(self, map):
dom = xml.dom.minidom.Document()
element = dom.createElement("iscsi-target-iqns")
dom.appendChild(element)
count = 0
for address,tpgt,iqn in map:
entry = dom.createElement('TGT')
element.appendChild(entry)
subentry = dom.createElement('Index')
entry.appendChild(subentry)
textnode = dom.createTextNode(str(count))
subentry.appendChild(textnode)
try:
addr,port = address.split(":")
except:
addr = address
port = DEFAULT_PORT
subentry = dom.createElement('IPAddress')
entry.appendChild(subentry)
textnode = dom.createTextNode(str(addr))
subentry.appendChild(textnode)
if int(port) != DEFAULT_PORT:
subentry = dom.createElement('Port')
entry.appendChild(subentry)
textnode = dom.createTextNode(str(port))
subentry.appendChild(textnode)
subentry = dom.createElement('TargetIQN')
entry.appendChild(subentry)
textnode = dom.createTextNode(str(iqn))
subentry.appendChild(textnode)
count += 1
print >>sys.stderr,dom.toprettyxml()
def srlist_toxml(self, SRs):
dom = xml.dom.minidom.Document()
element = dom.createElement("SRlist")
dom.appendChild(element)
for val in SRs:
record = SRs[val]
entry = dom.createElement('SR')
element.appendChild(entry)
subentry = dom.createElement("UUID")
entry.appendChild(subentry)
textnode = dom.createTextNode(val)
subentry.appendChild(textnode)
subentry = dom.createElement("Target")
entry.appendChild(subentry)
textnode = dom.createTextNode(record['target'])
subentry.appendChild(textnode)
subentry = dom.createElement("TargetIQN")
entry.appendChild(subentry)
textnode = dom.createTextNode(record['targetIQN'])
subentry.appendChild(textnode)
return dom.toprettyxml()
def match_lun(self, s):
regex = re.compile("_")
if regex.search(s,0):
return False
regex = re.compile("LUN")
return regex.search(s, 0)
if __name__ == '__main__':
SRCommand.run(ISCSISR, DRIVER_INFO)
else:
SR.registerSR(ISCSISR)