Coverage for drivers/iscsilib.py : 31%

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# Copyright (C) Citrix Systems Inc.
2#
3# This program is free software; you can redistribute it and/or modify
4# it under the terms of the GNU Lesser General Public License as published
5# by the Free Software Foundation; version 2.1 only.
6#
7# This program is distributed in the hope that it will be useful,
8# but WITHOUT ANY WARRANTY; without even the implied warranty of
9# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10# GNU Lesser General Public License for more details.
11#
12# You should have received a copy of the GNU Lesser General Public License
13# along with this program; if not, write to the Free Software Foundation, Inc.,
14# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16INITIATORNAME_FILE = '/etc/iscsi/initiatorname.iscsi'
18import util
19import os
20import scsiutil
21import time
22import socket
23import re
24import shutil
25import xs_errors
26import lock
27import glob
28import tempfile
29from cleanup import LOCK_TYPE_RUNNING
30from configparser import RawConfigParser
31import io
33# The 3.x kernel brings with it some iSCSI path changes in sysfs
34_KERNEL_VERSION = os.uname()[2]
35if _KERNEL_VERSION.startswith('2.6'): 35 ↛ 36line 35 didn't jump to line 36, because the condition on line 35 was never true
36 _GENERIC_SESSION_PATH = ('/sys/class/iscsi_host/host%s/device/session*/' +
37 'iscsi_session*/')
38 _GENERIC_CONNECTION_PATH = ('/sys/class/iscsi_host/host%s/device/' +
39 'session*/connection*/iscsi_connection*/')
40else:
41 _GENERIC_SESSION_PATH = ('/sys/class/iscsi_host/host%s/device/session*/' +
42 'iscsi_session/session*/')
43 _GENERIC_CONNECTION_PATH = ('/sys/class/iscsi_host/host%s/device/' +
44 'session*/connection*/iscsi_connection/connection*/')
46_REPLACEMENT_TMO_MPATH = 15
47_REPLACEMENT_TMO_DEFAULT = 144
48_REPLACEMENT_TMO_STANDARD = 120
50_ISCSI_DB_PATH = '/var/lib/iscsi'
53def doexec_locked(cmd):
54 """Executes via util.doexec the command specified whilst holding lock"""
55 _lock = None
56 if os.path.basename(cmd[0]) == 'iscsiadm':
57 _lock = lock.Lock(LOCK_TYPE_RUNNING, 'iscsiadm')
58 _lock.acquire()
59 # util.SMlog("%s" % cmd)
60 (rc, stdout, stderr) = util.doexec(cmd)
61 if _lock is not None and _lock.held():
62 _lock.release()
63 return (rc, stdout, stderr)
66def noexn_on_failure(cmd):
67 """Executes via util.doexec the command specified as best effort."""
68 (rc, stdout, stderr) = doexec_locked(cmd)
69 return (stdout, stderr)
72def exn_on_failure(cmd, message):
73 """Executes via util.doexec the command specified. If the return code is
74 non-zero, raises an ISCSIError with the given message"""
75 (rc, stdout, stderr) = doexec_locked(cmd)
76 if rc == 0:
77 return (stdout, stderr)
78 else:
79 msg = 'rc: %d, stdout: %s, stderr: %s' % (rc, stdout, stderr)
80 raise xs_errors.XenError('SMGeneral', opterr=msg)
83def parse_node_output(text, targetIQN):
84 """helper function - parses the output of iscsiadm for discovery and
85 get_node_records"""
87 def dotrans(x):
88 (rec, iqn) = x.split()
89 (portal, tpgt) = rec.split(',')
90 return (portal, tpgt, iqn)
91 unfiltered_map = [dotrans(x) for x in text.split('\n') if
92 match_targetIQN(targetIQN, x)]
93 # We need to filter duplicates orignating from doing the discovery using
94 # multiple interfaces
95 filtered_map = []
96 for input_value in unfiltered_map:
97 if input_value not in filtered_map: 97 ↛ 96line 97 didn't jump to line 96, because the condition on line 97 was never false
98 filtered_map.append(input_value)
99 return filtered_map
102def parse_IP_port(portal):
103 """Extract IP address and port number from portal information.
105 Input: String encoding the IP address and port of form:
106 - x.x.x.x:p (IPv4)
107 or
108 - [xxxx:xxxx:...:xxxx]:p (IPv6)
110 Return tuple of IP and port (without square brackets in case of IPv6):
111 """
112 (ipaddr, port) = portal.split(',')[0].rsplit(':', 1)
113 if ipaddr[0] == '[': 113 ↛ 115line 113 didn't jump to line 115, because the condition on line 113 was never true
114 # This is IPv6, strip off [ ] surround
115 ipaddr = ipaddr[1:-1]
116 return (ipaddr, port)
119def save_rootdisk_nodes(tmpdirname):
120 root_iqns = get_rootdisk_IQNs()
121 if root_iqns:
122 srcdirs = [os.path.join(_ISCSI_DB_PATH, 'nodes', iqn) for iqn in root_iqns]
123 util.doexec(['/bin/cp', '-a'] + srcdirs + [tmpdirname])
126def restore_rootdisk_nodes(tmpdirname):
127 root_iqns = get_rootdisk_IQNs()
128 if root_iqns:
129 srcdirs = [os.path.join(tmpdirname, iqn) for iqn in root_iqns]
130 util.doexec(['/bin/cp', '-a'] + srcdirs +
131 [os.path.join(_ISCSI_DB_PATH, 'nodes')])
134def discovery(target, port, chapuser, chappass, targetIQN="any",
135 interfaceArray=["default"]):
136 """Run iscsiadm in discovery mode to obtain a list of the
137 TargetIQNs available on the specified target and port. Returns
138 a list of triples - the portal (ip:port), the tpgt (target portal
139 group tag) and the target name"""
141 # Save configuration of root LUN nodes and restore after discovery
142 # otherwise when we do a discovery on the same filer as is hosting
143 # our root disk we'll reset the config of the root LUNs
145 # FIXME: Replace this with TemporaryDirectory when moving to Python3
146 tmpdirname = tempfile.mkdtemp()
147 try:
148 save_rootdisk_nodes(tmpdirname)
150 if ':' in target: 150 ↛ 151line 150 didn't jump to line 151, because the condition on line 150 was never true
151 targetstring = "[%s]:%s" % (target, str(port))
152 else:
153 targetstring = "%s:%s" % (target, str(port))
154 cmd_base = ["-t", "st", "-p", targetstring]
155 for interface in interfaceArray:
156 cmd_base.append("-I")
157 cmd_base.append(interface)
158 cmd_disc = ["iscsiadm", "-m", "discovery"] + cmd_base
159 cmd_discdb = ["iscsiadm", "-m", "discoverydb"] + cmd_base
160 auth_args = ["-n", "discovery.sendtargets.auth.authmethod", "-v", "CHAP",
161 "-n", "discovery.sendtargets.auth.username", "-v", chapuser,
162 "-n", "discovery.sendtargets.auth.password", "-v", chappass]
163 fail_msg = "Discovery failed. Check target settings and " \
164 "username/password (if applicable)"
165 try:
166 if chapuser != "" and chappass != "":
167 exn_on_failure(cmd_discdb + ["-o", "new"], fail_msg)
168 exn_on_failure(cmd_discdb + ["-o", "update"] + auth_args, fail_msg)
169 cmd = cmd_discdb + ["--discover"]
170 else:
171 cmd = cmd_disc
172 (stdout, stderr) = exn_on_failure(cmd, fail_msg)
173 except:
174 raise xs_errors.XenError('ISCSILogin')
175 finally:
176 restore_rootdisk_nodes(tmpdirname)
177 finally:
178 shutil.rmtree(tmpdirname) 178 ↛ exitline 178 didn't except from function 'discovery', because the raise on line 174 wasn't executed
180 return parse_node_output(stdout, targetIQN)
183def get_node_records(targetIQN="any"):
184 """Return the node records that the iscsi daemon already knows about"""
185 cmd = ["iscsiadm", "-m", "node"]
186 failuremessage = "Failed to obtain node records from iscsi daemon"
187 (stdout, stderr) = exn_on_failure(cmd, failuremessage)
188 return parse_node_output(stdout, targetIQN)
191def set_chap_settings (portal, targetIQN, username, password, username_in, password_in):
192 """Sets the username and password on the session identified by the
193 portal/targetIQN combination"""
194 failuremessage = "Failed to set CHAP settings"
195 cmd = ["iscsiadm", "-m", "node", "-p", portal, "-T", targetIQN, "--op",
196 "update", "-n", "node.session.auth.authmethod", "-v", "CHAP"]
197 (stdout, stderr) = exn_on_failure(cmd, failuremessage)
199 cmd = ["iscsiadm", "-m", "node", "-p", portal, "-T", targetIQN, "--op",
200 "update", "-n", "node.session.auth.username", "-v",
201 username]
202 (stdout, stderr) = exn_on_failure(cmd, failuremessage)
204 cmd = ["iscsiadm", "-m", "node", "-p", portal, "-T", targetIQN, "--op",
205 "update", "-n", "node.session.auth.password", "-v",
206 password]
207 (stdout, stderr) = exn_on_failure(cmd, failuremessage)
209 if (username_in != ""):
210 cmd = ["iscsiadm", "-m", "node", "-p", portal, "-T", targetIQN, "--op",
211 "update", "-n", "node.session.auth.username_in", "-v",
212 username_in]
213 (stdout, stderr) = exn_on_failure(cmd, failuremessage)
215 cmd = ["iscsiadm", "-m", "node", "-p", portal, "-T", targetIQN, "--op",
216 "update", "-n", "node.session.auth.password_in", "-v",
217 password_in]
218 (stdout, stderr) = exn_on_failure(cmd, failuremessage)
221def remove_chap_settings(portal, targetIQN):
222 cmd = ["iscsiadm", "-m", "node", "-p", portal, "-T", targetIQN, "--op",
223 "update", "-n", "node.session.auth.authmethod", "-v", "None"]
224 (stdout, stderr) = noexn_on_failure(cmd)
226 cmd = ["iscsiadm", "-m", "node", "-p", portal, "-T", targetIQN, "--op",
227 "update", "-n", "node.session.auth.username", "-v", ""]
228 (stdout, stderr) = noexn_on_failure(cmd)
230 cmd = ["iscsiadm", "-m", "node", "-p", portal, "-T", targetIQN, "--op",
231 "update", "-n", "node.session.auth.password", "-v", ""]
232 (stdout, stderr) = noexn_on_failure(cmd)
234 cmd = ["iscsiadm", "-m", "node", "-p", portal, "-T", targetIQN, "--op",
235 "update", "-n", "node.session.auth.username_in", "-v", ""]
236 (stdout, stderr) = noexn_on_failure(cmd)
238 cmd = ["iscsiadm", "-m", "node", "-p", portal, "-T", targetIQN, "--op",
239 "update", "-n", "node.session.auth.password_in", "-v", ""]
240 (stdout, stderr) = noexn_on_failure(cmd)
243def get_node_config (portal, targetIQN):
244 """ Using iscsadm to get the current configuration of a iscsi node.
245 The output is parsed in ini format, and returned as a dictionary."""
246 failuremessage = "Failed to get node configurations"
247 cmd = ["iscsiadm", "-m", "node", "-p", portal, "-T", targetIQN, "-S"]
248 (stdout, stderr) = exn_on_failure(cmd, failuremessage)
249 ini_sec = "root"
250 str_fp = io.StringIO("[%s]\n%s" % (ini_sec, stdout))
251 parser = RawConfigParser()
252 parser.readfp(str_fp)
253 str_fp.close()
254 return dict(parser.items(ini_sec))
257def set_replacement_tmo (portal, targetIQN, mpath):
258 key = "node.session.timeo.replacement_timeout"
259 try:
260 current_tmo = int((get_node_config(portal, targetIQN))[key])
261 except:
262 # Assume a standard TMO setting if get_node_config fails
263 current_tmo = _REPLACEMENT_TMO_STANDARD
264 # deliberately leave the "-p portal" arguments out, so that all the portals
265 # always share the same config (esp. in corner case when switching from
266 # mpath -> non-mpath, where we are only going to operate on one path). The
267 # parameter could be useful if we want further flexibility in the future.
268 cmd = ["iscsiadm", "-m", "node", "-T", targetIQN, # "-p", portal,
269 "--op", "update", "-n", key, "-v"]
270 fail_msg = "Failed to set replacement timeout"
271 if mpath:
272 # Only switch if the current config is a well-known non-mpath setting
273 if current_tmo in [_REPLACEMENT_TMO_DEFAULT, _REPLACEMENT_TMO_STANDARD]:
274 cmd.append(str(_REPLACEMENT_TMO_MPATH))
275 (stdout, stderr) = exn_on_failure(cmd, fail_msg)
276 else:
277 # the current_tmo is a customized value, no change
278 util.SMlog("Keep the current replacement_timout value: %d." % current_tmo)
279 else:
280 # Only switch if the current config is a well-known mpath setting
281 if current_tmo in [_REPLACEMENT_TMO_MPATH, _REPLACEMENT_TMO_STANDARD]:
282 cmd.append(str(_REPLACEMENT_TMO_DEFAULT))
283 (stdout, stderr) = exn_on_failure(cmd, fail_msg)
284 else:
285 # the current_tmo is a customized value, no change
286 util.SMlog("Keep the current replacement_timout value: %d." % current_tmo)
289def get_current_initiator_name():
290 """Looks in the config file to see if we've already got a initiator name,
291 returning it if so, or else returning None"""
292 if os.path.exists(INITIATORNAME_FILE):
293 try:
294 f = open(INITIATORNAME_FILE, 'r')
295 for line in f.readlines():
296 if line.strip().startswith("#"):
297 continue
298 if "InitiatorName" in line:
299 IQN = line.split("=")[1]
300 currentIQN = IQN.strip()
301 f.close()
302 return currentIQN
303 f.close()
304 except IOError as e:
305 return None
306 return None
309def get_system_alias():
310 return socket.gethostname()
313def set_current_initiator_name(localIQN):
314 """Sets the initiator name in the config file. Raises an xs_error on error"""
315 try:
316 alias = get_system_alias()
317 # MD3000i alias bug workaround
318 if len(alias) > 30:
319 alias = alias[0:30]
320 f = open(INITIATORNAME_FILE, 'w')
321 f.write('InitiatorName=%s\n' % localIQN)
322 f.write('InitiatorAlias=%s\n' % alias)
323 f.close()
324 except IOError as e:
325 raise xs_errors.XenError('ISCSIInitiator', \
326 opterr='Could not set initator name')
329def login(portal, target, username, password, username_in="", password_in="",
330 multipath=False):
331 if username != "" and password != "":
332 set_chap_settings(portal, target, username, password, username_in, password_in)
333 else:
334 remove_chap_settings(portal, target)
336 set_replacement_tmo(portal, target, multipath)
337 cmd = ["iscsiadm", "-m", "node", "-p", portal, "-T", target, "-l"]
338 failuremessage = "Failed to login to target."
339 try:
340 (stdout, stderr) = exn_on_failure(cmd, failuremessage)
341 except:
342 raise xs_errors.XenError('ISCSILogin')
345def logout(portal, target, all=False):
346 if all:
347 cmd = ["iscsiadm", "-m", "node", "-T", target, "-u"]
348 else:
349 cmd = ["iscsiadm", "-m", "node", "-p", portal, "-T", target, "-u"]
350 failuremessage = "Failed to log out of target"
351 try:
352 (stdout, stderr) = exn_on_failure(cmd, failuremessage)
353 except:
354 raise xs_errors.XenError('ISCSILogout')
357def delete(target):
358 cmd = ["iscsiadm", "-m", "node", "-o", "delete", "-T", target]
359 failuremessage = "Failed to delete target"
360 try:
361 (stdout, stderr) = exn_on_failure(cmd, failuremessage)
362 except:
363 raise xs_errors.XenError('ISCSIDelete')
366def get_luns(targetIQN, portal):
367 refresh_luns(targetIQN, portal)
368 luns = []
369 path = os.path.join("/dev/iscsi", targetIQN, portal)
370 try:
371 for file in util.listdir(path):
372 if file.find("LUN") == 0 and file.find("_") == -1:
373 lun = file.replace("LUN", "")
374 luns.append(lun)
375 return luns
376 except util.CommandException as inst:
377 raise xs_errors.XenError('ISCSIDevice', opterr='Failed to find any LUNs')
380def is_iscsi_daemon_running():
381 cmd = ["/sbin/pidof", "-s", "/sbin/iscsid"]
382 (rc, stdout, stderr) = util.doexec(cmd)
383 return (rc == 0)
386def stop_daemon():
387 if is_iscsi_daemon_running():
388 cmd = ["service", "iscsid", "stop"]
389 failuremessage = "Failed to stop iscsi daemon"
390 exn_on_failure(cmd, failuremessage)
393def restart_daemon():
394 stop_daemon()
395 if os.path.exists(os.path.join(_ISCSI_DB_PATH, 'nodes')): 395 ↛ 404line 395 didn't jump to line 404, because the condition on line 395 was never false
396 try:
397 shutil.rmtree(os.path.join(_ISCSI_DB_PATH, 'nodes'))
398 except:
399 pass
400 try:
401 shutil.rmtree(os.path.join(_ISCSI_DB_PATH, 'send_targets'))
402 except:
403 pass
404 cmd = ["service", "iscsid", "start"]
405 failuremessage = "Failed to start iscsi daemon"
406 exn_on_failure(cmd, failuremessage)
409def wait_for_devs(targetIQN, portal):
410 path = os.path.join("/dev/iscsi", targetIQN, portal)
411 for i in range(0, 15):
412 if os.path.exists(path):
413 return True
414 time.sleep(1)
415 return False
418def refresh_luns(targetIQN, portal):
419 wait_for_devs(targetIQN, portal)
420 try:
421 path = os.path.join("/dev/iscsi", targetIQN, portal)
422 id = scsiutil.getSessionID(path)
423 f = open('/sys/class/scsi_host/host%s/scan' % id, 'w')
424 f.write('- - -\n')
425 f.close()
426 time.sleep(2) # FIXME
427 except:
428 pass
431def get_IQN_paths():
432 """Return the list of iSCSI session directories"""
433 return glob.glob(_GENERIC_SESSION_PATH % '*')
436def get_targetIQN(iscsi_host):
437 """Get target IQN from sysfs for given iSCSI host number"""
438 iqn_file = os.path.join(_GENERIC_SESSION_PATH % iscsi_host, 'targetname')
439 targetIQN = util.get_single_entry(glob.glob(iqn_file)[0])
440 return targetIQN
443def get_targetIP_and_port(iscsi_host):
444 """Get target IP address and port for given iSCSI host number"""
445 connection_dir = _GENERIC_CONNECTION_PATH % iscsi_host
446 ip = util.get_single_entry(glob.glob(os.path.join(
447 connection_dir, 'persistent_address'))[0])
448 port = util.get_single_entry(glob.glob(os.path.join(
449 connection_dir, 'persistent_port'))[0])
450 return (ip, port)
453def get_path(targetIQN, portal, lun):
454 """Gets the path of a specified LUN - this should be e.g. '1' or '5'"""
455 path = os.path.join("/dev/iscsi", targetIQN, portal)
456 return os.path.join(path, "LUN" + lun)
459def get_path_safe(targetIQN, portal, lun):
460 """Gets the path of a specified LUN, and ensures that it exists.
461 Raises an exception if it hasn't appeared after the timeout"""
462 path = get_path(targetIQN, portal, lun)
463 for i in range(0, 15):
464 if os.path.exists(path):
465 return path
466 time.sleep(1)
467 raise xs_errors.XenError('ISCSIDevice', \
468 opterr='LUN failed to appear at path %s' % path)
471def match_target(tgt, s):
472 regex = re.compile(tgt)
473 return regex.search(s, 0)
476def match_targetIQN(tgtIQN, s):
477 if not len(s): 477 ↛ 478line 477 didn't jump to line 478, because the condition on line 477 was never true
478 return False
479 if tgtIQN == "any": 479 ↛ 483line 479 didn't jump to line 483, because the condition on line 479 was never false
480 return True
481 # Extract IQN from iscsiadm -m session
482 # Ex: tcp: [17] 10.220.98.9:3260,1 iqn.2009-01.xenrt.test:iscsi4181a93e
483 siqn = s.split(",")[1].split()[1]
484 return (siqn == tgtIQN)
487def match_session(s):
488 regex = re.compile("^tcp:")
489 return regex.search(s, 0)
492def _checkTGT(tgtIQN, tgt=''):
493 if not is_iscsi_daemon_running():
494 return False
495 failuremessage = "Failure occured querying iscsi daemon"
496 cmd = ["iscsiadm", "-m", "session"]
497 try:
498 (stdout, stderr) = exn_on_failure(cmd, failuremessage)
499 # Recent versions of iscsiadm return error it this list is empty.
500 # Quick and dirty handling
501 except Exception as e:
502 util.SMlog("%s failed with %s" % (cmd, e.args))
503 stdout = ""
504 for line in stdout.split('\n'):
505 if match_targetIQN(tgtIQN, line) and match_session(line):
506 if len(tgt):
507 if match_target(tgt, line):
508 return True
509 else:
510 return True
511 return False
514def get_rootdisk_IQNs():
515 """Return the list of IQNs for targets required by root filesystem"""
516 if not os.path.isdir('/sys/firmware/ibft/'): 516 ↛ 518line 516 didn't jump to line 518, because the condition on line 516 was never false
517 return []
518 dirs = [x for x in os.listdir('/sys/firmware/ibft/') if x.startswith('target')]
519 return [open('/sys/firmware/ibft/%s/target-name' % d).read().strip() for d in dirs]
522def _checkAnyTGT():
523 if not is_iscsi_daemon_running():
524 return False
525 rootIQNs = get_rootdisk_IQNs()
526 failuremessage = "Failure occured querying iscsi daemon"
527 cmd = ["iscsiadm", "-m", "session"]
528 try:
529 (stdout, stderr) = exn_on_failure(cmd, failuremessage)
530 # Recent versions of iscsiadm return error it this list is empty.
531 # Quick and dirty handling
532 except Exception as e:
533 util.SMlog("%s failed with %s" % (cmd, e.args))
534 stdout = ""
535 for e in filter(match_session, stdout.split('\n')):
536 iqn = e.split()[-1]
537 if not iqn in rootIQNs:
538 return True
539 return False
542def ensure_daemon_running_ok(localiqn):
543 """Check that the daemon is running and the initiator name is correct"""
544 if not is_iscsi_daemon_running():
545 set_current_initiator_name(localiqn)
546 restart_daemon()
547 else:
548 currentiqn = get_current_initiator_name()
549 if currentiqn != localiqn:
550 if _checkAnyTGT():
551 raise xs_errors.XenError('ISCSIInitiator', \
552 opterr='Daemon already running with ' \
553 + 'target(s) attached using ' \
554 + 'different IQN')
555 set_current_initiator_name(localiqn)
556 restart_daemon()
559def get_iscsi_interfaces():
560 result = []
561 try:
562 # Get all configured iscsiadm interfaces
563 cmd = ["iscsiadm", "-m", "iface"]
564 (stdout, stderr) = exn_on_failure(cmd,
565 "Failure occured querying iscsi daemon")
566 # Get the interface (first column) from a line such as default
567 # tcp,<empty>,<empty>,<empty>,<empty>
568 for line in stdout.split("\n"):
569 line_element = line.split(" ")
570 interface_name = line_element[0]
571 # ignore interfaces which aren't marked as starting with
572 # c_.
573 if len(line_element) == 2 and interface_name[:2] == "c_":
574 result.append(interface_name)
575 except:
576 # Ignore exception from exn on failure, still return the default
577 # interface
578 pass
579 # In case there are no configured interfaces, still add the default
580 # interface
581 if len(result) == 0:
582 result.append("default")
583 return result