Coverage for drivers/iscsilib.py : 36%

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