Coverage for drivers/LVHDoISCSISR.py : 16%

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# LVHDoISCSISR: LVHD over ISCSI software initiator SR driver
19#
21import SR
22import LVHDSR
23import BaseISCSI
24import SRCommand
25import util
26import scsiutil
27import lvutil
28import time
29import os
30import sys
31import xs_errors
32import xmlrpc.client
33import mpath_cli
34import iscsilib
35import glob
36import copy
37import scsiutil
38import xml.dom.minidom
40CAPABILITIES = ["SR_PROBE", "SR_UPDATE", "SR_METADATA", "SR_TRIM",
41 "VDI_CREATE", "VDI_DELETE", "VDI_ATTACH", "VDI_DETACH",
42 "VDI_GENERATE_CONFIG", "VDI_CLONE", "VDI_SNAPSHOT",
43 "VDI_RESIZE", "ATOMIC_PAUSE", "VDI_RESET_ON_BOOT/2",
44 "VDI_UPDATE", "VDI_MIRROR", "VDI_CONFIG_CBT",
45 "VDI_ACTIVATE", "VDI_DEACTIVATE"]
47CONFIGURATION = [['SCSIid', 'The scsi_id of the destination LUN'], \
48 ['target', 'IP address or hostname of the iSCSI target'], \
49 ['targetIQN', 'The IQN of the target LUN group to be attached'], \
50 ['chapuser', 'The username to be used during CHAP authentication'], \
51 ['chappassword', 'The password to be used during CHAP authentication'], \
52 ['incoming_chapuser', 'The incoming username to be used during bi-directional CHAP authentication (optional)'], \
53 ['incoming_chappassword', 'The incoming password to be used during bi-directional CHAP authentication (optional)'], \
54 ['port', 'The network port number on which to query the target'], \
55 ['multihomed', 'Enable multi-homing to this target, true or false (optional, defaults to same value as host.other_config:multipathing)'], \
56 ['usediscoverynumber', 'The specific iscsi record index to use. (optional)'], \
57 ['allocation', 'Valid values are thick or thin (optional, defaults to thick)']]
59DRIVER_INFO = {
60 'name': 'LVHD over iSCSI',
61 'description': 'SR plugin which represents disks as Logical Volumes within a Volume Group created on an iSCSI LUN',
62 'vendor': 'Citrix Systems Inc',
63 'copyright': '(C) 2008 Citrix Systems Inc',
64 'driver_version': '1.0',
65 'required_api_version': '1.0',
66 'capabilities': CAPABILITIES,
67 'configuration': CONFIGURATION
68 }
71class LVHDoISCSISR(LVHDSR.LVHDSR):
72 """LVHD over ISCSI storage repository"""
74 def handles(type):
75 if __name__ == '__main__':
76 name = sys.argv[0]
77 else:
78 name = __name__
79 if name.endswith("LVMoISCSISR"):
80 return type == "lvmoiscsi"
81 if type == "lvhdoiscsi":
82 return True
83 return False
84 handles = staticmethod(handles)
86 def load(self, sr_uuid):
87 if not sr_uuid: 87 ↛ 89line 87 didn't jump to line 89, because the condition on line 87 was never true
88 # This is a probe call, generate a temp sr_uuid
89 sr_uuid = util.gen_uuid()
91 # If this is a vdi command, don't initialise SR
92 if util.isVDICommand(self.original_srcmd.cmd): 92 ↛ 93line 92 didn't jump to line 93, because the condition on line 92 was never true
93 self.SCSIid = self.dconf['SCSIid']
94 else:
95 if 'target' in self.original_srcmd.dconf: 95 ↛ 97line 95 didn't jump to line 97, because the condition on line 95 was never false
96 self.original_srcmd.dconf['targetlist'] = self.original_srcmd.dconf['target']
97 iscsi = BaseISCSI.BaseISCSISR(self.original_srcmd, sr_uuid)
98 self.iscsiSRs = []
99 self.iscsiSRs.append(iscsi)
101 saved_exc = None
102 if self.dconf['target'].find(',') == 0 or self.dconf['targetIQN'] == "*": 102 ↛ 173line 102 didn't jump to line 173, because the condition on line 102 was never false
103 # Instantiate multiple sessions
104 self.iscsiSRs = []
105 if self.dconf['targetIQN'] == "*": 105 ↛ 108line 105 didn't jump to line 108, because the condition on line 105 was never false
106 IQN = "any"
107 else:
108 IQN = self.dconf['targetIQN']
109 dict = {}
110 IQNstring = ""
111 IQNs = []
112 try:
113 if 'multiSession' in self.dconf: 113 ↛ 114line 113 didn't jump to line 114, because the condition on line 113 was never true
114 IQNs = self.dconf['multiSession'].split("|")
115 for IQN in IQNs:
116 if IQN:
117 dict[IQN] = ""
118 else:
119 try:
120 IQNs.remove(IQN)
121 except:
122 # Exceptions are not expected but just in case
123 pass
124 # Order in multiSession must be preserved. It is important for dual-controllers.
125 # IQNstring cannot be built with a dictionary iteration because of this
126 IQNstring = self.dconf['multiSession']
127 else:
128 for tgt in self.dconf['target'].split(','): 128 ↛ 147line 128 didn't jump to line 147, because the loop on line 128 didn't complete
129 try:
130 tgt_ip = util._convertDNS(tgt)
131 except:
132 raise xs_errors.XenError('DNSError')
133 iscsilib.ensure_daemon_running_ok(iscsi.localIQN)
134 map = iscsilib.discovery(tgt_ip, iscsi.port, iscsi.chapuser, iscsi.chappassword, targetIQN=IQN)
135 util.SMlog("Discovery for IP %s returned %s" % (tgt, map))
136 for i in range(0, len(map)):
137 (portal, tpgt, iqn) = map[i]
138 (ipaddr, port) = iscsilib.parse_IP_port(portal)
139 try:
140 util._testHost(ipaddr, int(port), 'ISCSITarget')
141 except:
142 util.SMlog("Target Not reachable: (%s:%s)" % (ipaddr, port))
143 continue
144 key = "%s,%s,%s" % (ipaddr, port, iqn)
145 dict[key] = ""
146 # Again, do not mess up with IQNs order. Dual controllers will benefit from that
147 if IQNstring == "":
148 # Compose the IQNstring first
149 for key in dict.keys():
150 IQNstring += "%s|" % key
151 # Reinitialize and store iterator
152 key_iterator = iter(dict.keys())
153 else:
154 key_iterator = IQNs
156 # Now load the individual iSCSI base classes
157 for key in key_iterator:
158 (ipaddr, port, iqn) = key.split(',')
159 srcmd_copy = copy.deepcopy(self.original_srcmd)
160 srcmd_copy.dconf['target'] = ipaddr
161 srcmd_copy.dconf['targetIQN'] = iqn
162 srcmd_copy.dconf['multiSession'] = IQNstring
163 util.SMlog("Setting targetlist: %s" % srcmd_copy.dconf['targetlist'])
164 self.iscsiSRs.append(BaseISCSI.BaseISCSISR(srcmd_copy, sr_uuid))
165 pbd = util.find_my_pbd(self.session, self.host_ref, self.sr_ref)
166 if pbd is not None and 'multiSession' not in self.dconf:
167 dconf = self.session.xenapi.PBD.get_device_config(pbd)
168 dconf['multiSession'] = IQNstring
169 self.session.xenapi.PBD.set_device_config(pbd, dconf)
170 except Exception as exc:
171 util.logException("LVHDoISCSISR.load")
172 saved_exc = exc
173 try:
174 self.iscsi = self.iscsiSRs[0]
175 except IndexError as exc:
176 if isinstance(saved_exc, SR.SROSError):
177 raise saved_exc # pylint: disable-msg=E0702
178 elif isinstance(saved_exc, Exception): 178 ↛ 181line 178 didn't jump to line 181, because the condition on line 178 was never false
179 raise xs_errors.XenError('SMGeneral', str(saved_exc))
180 else:
181 raise xs_errors.XenError('SMGeneral', str(exc))
183 # Be extremely careful not to throw exceptions here since this function
184 # is the main one used by all operations including probing and creating
185 pbd = None
186 try:
187 pbd = util.find_my_pbd(self.session, self.host_ref, self.sr_ref)
188 except:
189 pass
191 # Apart from the upgrade case, user must specify a SCSIid
192 if 'SCSIid' not in self.dconf:
193 # Dual controller issue
194 self.LUNs = {} # Dict for LUNs from all the iscsi objects
195 for ii in range(0, len(self.iscsiSRs)):
196 self.iscsi = self.iscsiSRs[ii]
197 self._LUNprint(sr_uuid)
198 for key in self.iscsi.LUNs:
199 self.LUNs[key] = self.iscsi.LUNs[key]
200 self.print_LUNs_XML()
201 self.iscsi = self.iscsiSRs[0] # back to original value
202 raise xs_errors.XenError('ConfigSCSIid')
204 self.SCSIid = self.dconf['SCSIid']
206 # This block checks if the first iscsi target contains the right SCSIid.
207 # If not it scans the other iscsi targets because chances are that more
208 # than one controller is present
209 dev_match = False
210 forced_login = False
211 # No need to check if only one iscsi target is present
212 if len(self.iscsiSRs) == 1:
213 pass
214 else:
215 target_success = False
216 attempt_discovery = False
217 for iii in range(0, len(self.iscsiSRs)):
218 # Check we didn't leave any iscsi session open
219 # If exceptions happened before, the cleanup function has worked on the right target.
220 if forced_login == True:
221 try:
222 iscsilib.ensure_daemon_running_ok(self.iscsi.localIQN)
223 iscsilib.logout(self.iscsi.target, self.iscsi.targetIQN)
224 forced_login = False
225 except:
226 raise xs_errors.XenError('ISCSILogout')
227 self.iscsi = self.iscsiSRs[iii]
228 util.SMlog("path %s" % self.iscsi.path)
229 util.SMlog("iscsci data: targetIQN %s, portal %s" % (self.iscsi.targetIQN, self.iscsi.target))
230 iscsilib.ensure_daemon_running_ok(self.iscsi.localIQN)
231 if not iscsilib._checkTGT(self.iscsi.targetIQN):
232 attempt_discovery = True
233 try:
234 # Ensure iscsi db has been populated
235 map = iscsilib.discovery(
236 self.iscsi.target,
237 self.iscsi.port,
238 self.iscsi.chapuser,
239 self.iscsi.chappassword,
240 targetIQN=self.iscsi.targetIQN)
241 if len(map) == 0:
242 util.SMlog("Discovery for iscsi data targetIQN %s,"
243 " portal %s returned empty list"
244 " Trying another path if available" %
245 (self.iscsi.targetIQN,
246 self.iscsi.target))
247 continue
248 except:
249 util.SMlog("Discovery failed for iscsi data targetIQN"
250 " %s, portal %s. Trying another path if"
251 " available" %
252 (self.iscsi.targetIQN, self.iscsi.target))
253 continue
254 try:
255 iscsilib.login(self.iscsi.target,
256 self.iscsi.targetIQN,
257 self.iscsi.chapuser,
258 self.iscsi.chappassword,
259 self.iscsi.incoming_chapuser,
260 self.iscsi.incoming_chappassword,
261 self.mpath == "true")
262 except:
263 util.SMlog("Login failed for iscsi data targetIQN %s,"
264 " portal %s. Trying another path"
265 " if available" %
266 (self.iscsi.targetIQN, self.iscsi.target))
267 continue
268 target_success = True
269 forced_login = True
270 # A session should be active.
271 if not util.wait_for_path(self.iscsi.path, BaseISCSI.MAX_TIMEOUT):
272 util.SMlog("%s has no associated LUNs" % self.iscsi.targetIQN)
273 continue
274 scsiid_path = "/dev/disk/by-id/scsi-" + self.SCSIid
275 if not util.wait_for_path(scsiid_path, BaseISCSI.MAX_TIMEOUT):
276 util.SMlog("%s not found" % scsiid_path)
277 continue
278 for file in filter(self.iscsi.match_lun, util.listdir(self.iscsi.path)):
279 lun_path = os.path.join(self.iscsi.path, file)
280 lun_dev = scsiutil.getdev(lun_path)
281 try:
282 lun_scsiid = scsiutil.getSCSIid(lun_dev)
283 except:
284 util.SMlog("getSCSIid failed on %s in iscsi %s: LUN"
285 " offline or iscsi path down" %
286 (lun_dev, self.iscsi.path))
287 continue
288 util.SMlog("dev from lun %s %s" % (lun_dev, lun_scsiid))
289 if lun_scsiid == self.SCSIid:
290 util.SMlog("lun match in %s" % self.iscsi.path)
291 dev_match = True
292 # No more need to raise ISCSITarget exception.
293 # Resetting attempt_discovery
294 attempt_discovery = False
295 break
296 if dev_match:
297 if iii == 0:
298 break
299 util.SMlog("IQN reordering needed")
300 new_iscsiSRs = []
301 IQNs = {}
302 IQNstring = ""
303 # iscsiSRs can be seen as a circular buffer: the head now is the matching one
304 for kkk in list(range(iii, len(self.iscsiSRs))) + list(range(0, iii)):
305 new_iscsiSRs.append(self.iscsiSRs[kkk])
306 ipaddr = self.iscsiSRs[kkk].target
307 port = self.iscsiSRs[kkk].port
308 iqn = self.iscsiSRs[kkk].targetIQN
309 key = "%s,%s,%s" % (ipaddr, port, iqn)
310 # The final string must preserve the order without repetition
311 if key not in IQNs:
312 IQNs[key] = ""
313 IQNstring += "%s|" % key
314 util.SMlog("IQNstring is now %s" % IQNstring)
315 self.iscsiSRs = new_iscsiSRs
316 util.SMlog("iqn %s is leading now" % self.iscsiSRs[0].targetIQN)
317 # Updating pbd entry, if any
318 try:
319 pbd = util.find_my_pbd(self.session, self.host_ref, self.sr_ref)
320 if pbd is not None and 'multiSession' in self.dconf:
321 util.SMlog("Updating multiSession in PBD")
322 dconf = self.session.xenapi.PBD.get_device_config(pbd)
323 dconf['multiSession'] = IQNstring
324 self.session.xenapi.PBD.set_device_config(pbd, dconf)
325 except:
326 pass
327 break
328 if not target_success and attempt_discovery:
329 raise xs_errors.XenError('ISCSITarget')
331 LVHDSR.LVHDSR.load(self, sr_uuid)
333 def print_LUNs_XML(self):
334 dom = xml.dom.minidom.Document()
335 element = dom.createElement("iscsi-target")
336 dom.appendChild(element)
337 for uuid in self.LUNs:
338 val = self.LUNs[uuid]
339 entry = dom.createElement('LUN')
340 element.appendChild(entry)
342 for attr in ('vendor', 'serial', 'LUNid', \
343 'size', 'SCSIid'):
344 try:
345 aval = getattr(val, attr)
346 except AttributeError:
347 continue
349 if aval:
350 subentry = dom.createElement(attr)
351 entry.appendChild(subentry)
352 textnode = dom.createTextNode(str(aval))
353 subentry.appendChild(textnode)
355 print(dom.toprettyxml(), file=sys.stderr)
357 def _getSCSIid_from_LUN(self, sr_uuid):
358 was_attached = True
359 self.iscsi.attach(sr_uuid)
360 dev = self.dconf['LUNid'].split(',')
361 if len(dev) > 1:
362 raise xs_errors.XenError('LVMOneLUN')
363 path = os.path.join(self.iscsi.path, "LUN%s" % dev[0])
364 if not util.wait_for_path(path, BaseISCSI.MAX_TIMEOUT):
365 util.SMlog("Unable to detect LUN attached to host [%s]" % path)
366 try:
367 SCSIid = scsiutil.getSCSIid(path)
368 except:
369 raise xs_errors.XenError('InvalidDev')
370 self.iscsi.detach(sr_uuid)
371 return SCSIid
373 def _LUNprint(self, sr_uuid):
374 if self.iscsi.attached:
375 # Force a rescan on the bus.
376 self.iscsi.refresh()
377# time.sleep(5)
378# Now call attach (handles the refcounting + session activa)
379 self.iscsi.attach(sr_uuid)
381 util.SMlog("LUNprint: waiting for path: %s" % self.iscsi.path)
382 if util.wait_for_path("%s/LUN*" % self.iscsi.path, BaseISCSI.MAX_TIMEOUT):
383 try:
384 adapter = self.iscsi.adapter[self.iscsi.address]
385 util.SMlog("adapter=%s" % adapter)
387 # find a scsi device on which to issue a report luns command:
388 devs = glob.glob("%s/LUN*" % self.iscsi.path)
389 sgdevs = []
390 for i in devs:
391 sgdevs.append(int(i.split("LUN")[1]))
392 sgdevs.sort()
393 sgdev = "%s/LUN%d" % (self.iscsi.path, sgdevs[0])
395 # issue a report luns:
396 luns = util.pread2(["/usr/bin/sg_luns", "-q", sgdev]).split('\n')
397 nluns = len(luns) - 1 # remove the line relating to the final \n
399 # make sure we've got that many sg devices present
400 for i in range(0, 30):
401 luns = scsiutil._dosgscan()
402 sgdevs = [r for r in luns if r[1] == adapter]
403 if len(sgdevs) >= nluns:
404 util.SMlog("Got all %d sg devices" % nluns)
405 break
406 else:
407 util.SMlog("Got %d sg devices - expecting %d" % (len(sgdevs), nluns))
408 time.sleep(1)
410 if os.path.exists("/sbin/udevsettle"):
411 util.pread2(["/sbin/udevsettle"])
412 else:
413 util.pread2(["/sbin/udevadm", "settle"])
414 except:
415 util.SMlog("Generic exception caught. Pass")
416 pass # Make sure we don't break the probe...
418 self.iscsi.print_LUNs()
419 self.iscsi.detach(sr_uuid)
421 def create(self, sr_uuid, size):
422 # Check SCSIid not already in use by other PBDs
423 if util.test_SCSIid(self.session, sr_uuid, self.SCSIid):
424 raise xs_errors.XenError('SRInUse')
426 self.iscsi.attach(sr_uuid)
427 try:
428 self.iscsi._attach_LUN_bySCSIid(self.SCSIid)
429 self._pathrefresh(LVHDoISCSISR)
430 LVHDSR.LVHDSR.create(self, sr_uuid, size)
431 except Exception as inst:
432 self.iscsi.detach(sr_uuid)
433 raise xs_errors.XenError("SRUnavailable", opterr=inst)
434 self.iscsi.detach(sr_uuid)
436 def delete(self, sr_uuid):
437 self._pathrefresh(LVHDoISCSISR)
438 LVHDSR.LVHDSR.delete(self, sr_uuid)
439 for i in self.iscsiSRs:
440 i.detach(sr_uuid)
442 def attach(self, sr_uuid):
443 try:
444 connected = False
445 stored_exception = None
446 for i in self.iscsiSRs:
447 try:
448 i.attach(sr_uuid)
449 except SR.SROSError as inst:
450 # Some iscsi objects can fail login/discovery but not all. Storing exception
451 if inst.errno in [141, 83]:
452 util.SMlog("Connection failed for target %s, continuing.." % i.target)
453 stored_exception = inst
454 continue
455 else:
456 raise
457 else:
458 connected = True
460 i._attach_LUN_bySCSIid(self.SCSIid)
462 # Check if at least one iscsi succeeded
463 if not connected and stored_exception:
464 # pylint: disable=raising-bad-type
465 raise stored_exception
467 if 'multiSession' in self.dconf:
468 # Force a manual bus refresh
469 for a in self.iscsi.adapter:
470 scsiutil.rescan([self.iscsi.adapter[a]])
472 self._pathrefresh(LVHDoISCSISR)
473 LVHDSR.LVHDSR.attach(self, sr_uuid)
474 except Exception as inst:
475 for i in self.iscsiSRs:
476 i.detach(sr_uuid)
477 raise xs_errors.XenError("SRUnavailable", opterr=inst)
478 self._setMultipathableFlag(SCSIid=self.SCSIid)
480 def detach(self, sr_uuid):
481 LVHDSR.LVHDSR.detach(self, sr_uuid)
482 for i in self.iscsiSRs:
483 i.detach(sr_uuid)
485 def scan(self, sr_uuid):
486 self._pathrefresh(LVHDoISCSISR)
487 if self.mpath == "true":
488 for i in self.iscsiSRs:
489 try:
490 i.attach(sr_uuid)
491 except SR.SROSError:
492 util.SMlog("Connection failed for target %s, continuing.." % i.target)
493 LVHDSR.LVHDSR.scan(self, sr_uuid)
495 def probe(self):
496 self.uuid = util.gen_uuid()
498 # When multipathing is enabled, since we don't refcount the multipath maps,
499 # we should not attempt to do the iscsi.attach/detach when the map is already present,
500 # as this will remove it (which may well be in use).
501 if self.mpath == 'true' and 'SCSIid' in self.dconf:
502 maps = []
503 try:
504 maps = mpath_cli.list_maps()
505 except:
506 pass
508 if self.dconf['SCSIid'] in maps:
509 raise xs_errors.XenError('SRInUse')
511 self.iscsi.attach(self.uuid)
512 self.iscsi._attach_LUN_bySCSIid(self.SCSIid)
513 self._pathrefresh(LVHDoISCSISR)
514 out = LVHDSR.LVHDSR.probe(self)
515 self.iscsi.detach(self.uuid)
516 return out
518 def vdi(self, uuid):
519 return LVHDoISCSIVDI(self, uuid)
522class LVHDoISCSIVDI(LVHDSR.LVHDVDI):
523 def generate_config(self, sr_uuid, vdi_uuid):
524 util.SMlog("LVHDoISCSIVDI.generate_config")
525 if not lvutil._checkLV(self.path):
526 raise xs_errors.XenError('VDIUnavailable')
527 dict = {}
528 self.sr.dconf['localIQN'] = self.sr.iscsi.localIQN
529 self.sr.dconf['multipathing'] = self.sr.mpath
530 self.sr.dconf['multipathhandle'] = self.sr.mpathhandle
531 dict['device_config'] = self.sr.dconf
532 if 'chappassword_secret' in dict['device_config']:
533 s = util.get_secret(self.session, dict['device_config']['chappassword_secret'])
534 del dict['device_config']['chappassword_secret']
535 dict['device_config']['chappassword'] = s
536 dict['sr_uuid'] = sr_uuid
537 dict['vdi_uuid'] = vdi_uuid
538 dict['command'] = 'vdi_attach_from_config'
539 # Return the 'config' encoded within a normal XMLRPC response so that
540 # we can use the regular response/error parsing code.
541 config = xmlrpc.client.dumps(tuple([dict]), "vdi_attach_from_config")
542 return xmlrpc.client.dumps((config, ), "", True)
544 def attach_from_config(self, sr_uuid, vdi_uuid):
545 util.SMlog("LVHDoISCSIVDI.attach_from_config")
546 try:
547 self.sr.iscsi.attach(sr_uuid)
548 self.sr.iscsi._attach_LUN_bySCSIid(self.sr.SCSIid)
549 return LVHDSR.LVHDVDI.attach(self, sr_uuid, vdi_uuid)
550 except:
551 util.logException("LVHDoISCSIVDI.attach_from_config")
552 raise xs_errors.XenError('SRUnavailable', \
553 opterr='Unable to attach the heartbeat disk')
556if __name__ == '__main__': 556 ↛ 557line 556 didn't jump to line 557, because the condition on line 556 was never true
557 SRCommand.run(LVHDoISCSISR, DRIVER_INFO)
558else:
559 SR.registerSR(LVHDoISCSISR)