1 |
# |
2 |
# iscsi.py - iscsi class |
3 |
# |
4 |
# Copyright (C) 2005, 2006 IBM, Inc. All rights reserved. |
5 |
# Copyright (C) 2006 Red Hat, Inc. All rights reserved. |
6 |
# |
7 |
# This program is free software; you can redistribute it and/or modify |
8 |
# it under the terms of the GNU General Public License as published by |
9 |
# the Free Software Foundation; either version 2 of the License, or |
10 |
# (at your option) any later version. |
11 |
# |
12 |
# This program is distributed in the hope that it will be useful, |
13 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
14 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
15 |
# GNU General Public License for more details. |
16 |
# |
17 |
# You should have received a copy of the GNU General Public License |
18 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
19 |
# |
20 |
|
21 |
from constants import * |
22 |
from udev import * |
23 |
import os |
24 |
import iutil |
25 |
from flags import flags |
26 |
import logging |
27 |
import shutil |
28 |
import time |
29 |
import hashlib |
30 |
import random |
31 |
import itertools |
32 |
log = logging.getLogger("anaconda") |
33 |
|
34 |
import gettext |
35 |
_ = lambda x: gettext.ldgettext("anaconda", x) |
36 |
|
37 |
has_libiscsi = True |
38 |
try: |
39 |
import libiscsi |
40 |
except ImportError: |
41 |
has_libiscsi = False |
42 |
|
43 |
# Note that stage2 copies all files under /sbin to /usr/sbin |
44 |
ISCSID="" |
45 |
INITIATOR_FILE="/etc/iscsi/initiatorname.iscsi" |
46 |
|
47 |
ISCSI_MODULES=['cxgb3i', 'bnx2i', 'be2iscsi'] |
48 |
|
49 |
def find_iscsi_files(): |
50 |
global ISCSID |
51 |
if ISCSID == "": |
52 |
for dir in ("/usr/sbin", "/tmp/updates", "/mnt/source/RHupdates"): |
53 |
path="%s/iscsid" % (dir,) |
54 |
if os.access(path, os.X_OK): |
55 |
ISCSID=path |
56 |
|
57 |
def has_iscsi(): |
58 |
find_iscsi_files() |
59 |
if ISCSID == "" or not has_libiscsi: |
60 |
return False |
61 |
|
62 |
log.info("ISCSID is %s" % (ISCSID,)) |
63 |
|
64 |
# make sure the module is loaded |
65 |
if not os.access("/sys/module/iscsi_tcp", os.X_OK): |
66 |
return False |
67 |
return True |
68 |
|
69 |
def randomIname(): |
70 |
"""Generate a random initiator name the same way as iscsi-iname""" |
71 |
|
72 |
s = "iqn.1994-05.com.domain:01." |
73 |
m = hashlib.md5() |
74 |
u = os.uname() |
75 |
for i in u: |
76 |
m.update(i) |
77 |
dig = m.hexdigest() |
78 |
|
79 |
for i in range(0, 6): |
80 |
s += dig[random.randrange(0, 32)] |
81 |
return s |
82 |
|
83 |
class iscsi(object): |
84 |
""" iSCSI utility class. |
85 |
|
86 |
This class will automatically discover and login to iBFT (or |
87 |
other firmware) configured iscsi devices when the startup() method |
88 |
gets called. It can also be used to manually configure iscsi devices |
89 |
through the addTarget() method. |
90 |
|
91 |
As this class needs to make sure certain things like starting iscsid |
92 |
and logging in to firmware discovered disks only happens once |
93 |
and as it keeps a global list of all iSCSI devices it is implemented as |
94 |
a Singleton. |
95 |
""" |
96 |
|
97 |
def __init__(self): |
98 |
# Dictionary of discovered targets containing list of (node, |
99 |
# logged_in) tuples. |
100 |
self.discovered_targets = {} |
101 |
# This list contains nodes discovered through iBFT (or other firmware) |
102 |
self.ibftNodes = [] |
103 |
self._initiator = "" |
104 |
self.initiatorSet = False |
105 |
self.started = False |
106 |
self.ifaces = {} |
107 |
|
108 |
if flags.ibft: |
109 |
try: |
110 |
initiatorname = libiscsi.get_firmware_initiator_name() |
111 |
self._initiator = initiatorname |
112 |
self.initiatorSet = True |
113 |
except: |
114 |
pass |
115 |
|
116 |
# So that users can write iscsi() to get the singleton instance |
117 |
def __call__(self): |
118 |
return self |
119 |
|
120 |
def _getInitiator(self): |
121 |
if self._initiator != "": |
122 |
return self._initiator |
123 |
|
124 |
return randomIname() |
125 |
|
126 |
def _setInitiator(self, val): |
127 |
if self.initiatorSet and val != self._initiator: |
128 |
raise ValueError, _("Unable to change iSCSI initiator name once set") |
129 |
if len(val) == 0: |
130 |
raise ValueError, _("Must provide an iSCSI initiator name") |
131 |
self._initiator = val |
132 |
|
133 |
initiator = property(_getInitiator, _setInitiator) |
134 |
|
135 |
def active_nodes(self, target=None): |
136 |
"""Nodes logged in to""" |
137 |
if target: |
138 |
return [node for (node, logged_in) in |
139 |
self.discovered_targets.get(target, []) |
140 |
if logged_in] |
141 |
else: |
142 |
return [node for (node, logged_in) in |
143 |
itertools.chain(*self.discovered_targets.values()) |
144 |
if logged_in] + self.ibftNodes |
145 |
|
146 |
def _getMode(self): |
147 |
if not self.active_nodes(): |
148 |
return "none" |
149 |
if self.ifaces: |
150 |
return "bind" |
151 |
else: |
152 |
return "default" |
153 |
|
154 |
mode = property(_getMode) |
155 |
|
156 |
def _mark_node_active(self, node, active=True): |
157 |
"""Mark node as one logged in to |
158 |
|
159 |
Returns False if not found |
160 |
""" |
161 |
for target_nodes in self.discovered_targets.values(): |
162 |
for nodeinfo in target_nodes: |
163 |
if nodeinfo[0] is node: |
164 |
nodeinfo[1] = active |
165 |
return True |
166 |
return False |
167 |
|
168 |
def _startIBFT(self, intf = None): |
169 |
if not flags.ibft: |
170 |
return |
171 |
|
172 |
try: |
173 |
found_nodes = libiscsi.discover_firmware() |
174 |
except: |
175 |
log.info("iscsi: No IBFT info found."); |
176 |
return |
177 |
|
178 |
for node in found_nodes: |
179 |
try: |
180 |
node.login() |
181 |
log.info("iscsi IBFT: logged into %s at %s:%s through %s" % ( |
182 |
node.name, node.address, node.port, node.iface)) |
183 |
self.ibftNodes.append(node) |
184 |
except IOError, e: |
185 |
log.error("Could not log into ibft iscsi target %s: %s" % |
186 |
(node.name, str(e))) |
187 |
pass |
188 |
|
189 |
self.stabilize(intf) |
190 |
|
191 |
def stabilize(self, intf = None): |
192 |
# Wait for udev to create the devices for the just added disks |
193 |
if intf: |
194 |
w = intf.waitWindow(_("Scanning iSCSI nodes"), |
195 |
_("Scanning iSCSI nodes")) |
196 |
# It is possible when we get here the events for the new devices |
197 |
# are not send yet, so sleep to make sure the events are fired |
198 |
time.sleep(2) |
199 |
udev_settle() |
200 |
if intf: |
201 |
w.pop() |
202 |
|
203 |
def create_interfaces(self, ifaces): |
204 |
for iface in ifaces: |
205 |
iscsi_iface_name = "iface%d" % len(self.ifaces) |
206 |
#iscsiadm -m iface -I iface0 --op=new |
207 |
iutil.execWithRedirect("iscsiadm", |
208 |
["-m", "iface", "-I", iscsi_iface_name, "--op=new"], |
209 |
stdout="/dev/tty5", |
210 |
stderr="/dev/tty5") |
211 |
#iscsiadm -m iface -I iface0 --op=update -n iface.net_ifacename -v eth0 |
212 |
iutil.execWithRedirect("iscsiadm", |
213 |
["-m", "iface", "-I", iscsi_iface_name, |
214 |
"--op=update", "-n", |
215 |
"iface.net_ifacename", "-v", iface], |
216 |
stdout="/dev/tty5", |
217 |
stderr="/dev/tty5") |
218 |
|
219 |
self.ifaces[iscsi_iface_name] = iface |
220 |
log.debug("created_interface %s:%s" % (iscsi_iface_name, iface)) |
221 |
|
222 |
def delete_interfaces(self): |
223 |
if not self.ifaces: |
224 |
return None |
225 |
for iscsi_iface_name in self.ifaces: |
226 |
#iscsiadm -m iface -I iface0 --op=delete |
227 |
iutil.execWithRedirect("iscsiadm", |
228 |
["-m", "iface", "-I", iscsi_iface_name, |
229 |
"--op=delete"], |
230 |
stdout="/dev/tty5", |
231 |
stderr="/dev/tty5") |
232 |
self.ifaces = {} |
233 |
|
234 |
def startup(self, intf = None): |
235 |
if self.started: |
236 |
return |
237 |
|
238 |
if not has_iscsi(): |
239 |
return |
240 |
|
241 |
if self._initiator == "": |
242 |
log.info("no initiator set") |
243 |
return |
244 |
|
245 |
if intf: |
246 |
w = intf.waitWindow(_("Initializing iSCSI initiator"), |
247 |
_("Initializing iSCSI initiator")) |
248 |
|
249 |
log.debug("Setting up %s" % (INITIATOR_FILE, )) |
250 |
log.info("iSCSI initiator name %s", self.initiator) |
251 |
if os.path.exists(INITIATOR_FILE): |
252 |
os.unlink(INITIATOR_FILE) |
253 |
if not os.path.isdir("/etc/iscsi"): |
254 |
os.makedirs("/etc/iscsi", 0755) |
255 |
fd = os.open(INITIATOR_FILE, os.O_RDWR | os.O_CREAT) |
256 |
os.write(fd, "InitiatorName=%s\n" %(self.initiator)) |
257 |
os.close(fd) |
258 |
self.initiatorSet = True |
259 |
|
260 |
for dir in ['ifaces','isns','nodes','send_targets','slp','static']: |
261 |
fulldir = "/var/lib/iscsi/%s" % (dir,) |
262 |
if not os.path.isdir(fulldir): |
263 |
os.makedirs(fulldir, 0755) |
264 |
|
265 |
log.info("iSCSI startup") |
266 |
iutil.execWithRedirect('modprobe', ['-a'] + ISCSI_MODULES, |
267 |
stdout="/dev/tty5", stderr="/dev/tty5") |
268 |
# this is needed by Broadcom offload cards (bnx2i) |
269 |
iscsiuio = iutil.find_program_in_path('iscsiuio', |
270 |
raise_on_error=True) |
271 |
log.debug("iscsi: iscsiuio is at %s" % iscsiuio) |
272 |
iutil.execWithRedirect(iscsiuio, [], |
273 |
stdout="/dev/tty5", stderr="/dev/tty5") |
274 |
# run the daemon |
275 |
iutil.execWithRedirect(ISCSID, [], |
276 |
stdout="/dev/tty5", stderr="/dev/tty5") |
277 |
time.sleep(1) |
278 |
|
279 |
if intf: |
280 |
w.pop() |
281 |
|
282 |
self._startIBFT(intf) |
283 |
self.started = True |
284 |
|
285 |
def discover(self, ipaddr, port="3260", username=None, password=None, |
286 |
r_username=None, r_password=None, intf=None): |
287 |
""" |
288 |
Discover iSCSI nodes on the target available for login. |
289 |
|
290 |
If we are logged in a node discovered for specified target |
291 |
do not do the discovery again as it can corrupt credentials |
292 |
stored for the node (setAuth and getAuth are using database |
293 |
in /var/lib/iscsi/nodes which is filled by discovery). Just |
294 |
return nodes obtained and stored in the first discovery |
295 |
instead. |
296 |
|
297 |
Returns list of nodes user can log in. |
298 |
""" |
299 |
authinfo = None |
300 |
|
301 |
if not has_iscsi(): |
302 |
raise IOError, _("iSCSI not available") |
303 |
if self._initiator == "": |
304 |
raise ValueError, _("No initiator name set") |
305 |
|
306 |
if self.active_nodes((ipaddr, port)): |
307 |
log.debug("iSCSI: skipping discovery of %s:%s due to active nodes" % |
308 |
(ipaddr, port)) |
309 |
else: |
310 |
if username or password or r_username or r_password: |
311 |
# Note may raise a ValueError |
312 |
authinfo = libiscsi.chapAuthInfo(username=username, |
313 |
password=password, |
314 |
reverse_username=r_username, |
315 |
reverse_password=r_password) |
316 |
self.startup(intf) |
317 |
|
318 |
# Note may raise an IOError |
319 |
found_nodes = libiscsi.discover_sendtargets(address=ipaddr, |
320 |
port=int(port), |
321 |
authinfo=authinfo) |
322 |
if found_nodes is None: |
323 |
return None |
324 |
self.discovered_targets[(ipaddr, port)] = [] |
325 |
for node in found_nodes: |
326 |
self.discovered_targets[(ipaddr, port)].append([node, False]) |
327 |
log.debug("discovered iSCSI node: %s" % node.name) |
328 |
|
329 |
# only return the nodes we are not logged into yet |
330 |
return [node for (node, logged_in) in |
331 |
self.discovered_targets[(ipaddr, port)] |
332 |
if not logged_in] |
333 |
|
334 |
def log_into_node(self, node, username=None, password=None, |
335 |
r_username=None, r_password=None, intf=None): |
336 |
""" |
337 |
Raises IOError. |
338 |
""" |
339 |
rc = False # assume failure |
340 |
msg = "" |
341 |
|
342 |
if intf: |
343 |
w = intf.waitWindow(_("Logging in to iSCSI node"), |
344 |
_("Logging in to iSCSI node %s") % node.name) |
345 |
try: |
346 |
authinfo = None |
347 |
if username or password or r_username or r_password: |
348 |
# may raise a ValueError |
349 |
authinfo = libiscsi.chapAuthInfo(username=username, |
350 |
password=password, |
351 |
reverse_username=r_username, |
352 |
reverse_password=r_password) |
353 |
node.setAuth(authinfo) |
354 |
node.login() |
355 |
rc = True |
356 |
log.info("iSCSI: logged into %s at %s:%s through %s" % ( |
357 |
node.name, node.address, node.port, node.iface)) |
358 |
if not self._mark_node_active(node): |
359 |
log.error("iSCSI: node not found among discovered") |
360 |
except (IOError, ValueError) as e: |
361 |
msg = str(e) |
362 |
log.warning("iSCSI: could not log into %s: %s" % (node.name, msg)) |
363 |
if intf: |
364 |
w.pop() |
365 |
|
366 |
return (rc, msg) |
367 |
|
368 |
# NOTE: the same credentials are used for discovery and login |
369 |
# (unlike in UI) |
370 |
def addTarget(self, ipaddr, port="3260", user=None, pw=None, |
371 |
user_in=None, pw_in=None, intf=None, target=None, iface=None): |
372 |
found = 0 |
373 |
logged_in = 0 |
374 |
|
375 |
found_nodes = self.discover(ipaddr, port, user, pw, user_in, pw_in, |
376 |
intf) |
377 |
if found_nodes == None: |
378 |
raise IOError, _("No iSCSI nodes discovered") |
379 |
|
380 |
for node in found_nodes: |
381 |
if target and target != node.name: |
382 |
log.debug("iscsi: skipping logging to iscsi node '%s'" % |
383 |
node.name) |
384 |
continue |
385 |
if iface: |
386 |
node_net_iface = self.ifaces.get(node.iface, node.iface) |
387 |
if iface != node_net_iface: |
388 |
log.debug("iscsi: skipping logging to iscsi node '%s' via %s" % |
389 |
(node.name, node_net_iface)) |
390 |
continue |
391 |
|
392 |
found = found + 1 |
393 |
|
394 |
(rc, msg) = self.log_into_node(node, user, pw, user_in, pw_in, |
395 |
intf) |
396 |
if rc: |
397 |
logged_in = logged_in +1 |
398 |
|
399 |
if found == 0: |
400 |
raise IOError, _("No new iSCSI nodes discovered") |
401 |
|
402 |
if logged_in == 0: |
403 |
raise IOError, _("Could not log in to any of the discovered nodes") |
404 |
|
405 |
self.stabilize(intf) |
406 |
|
407 |
def writeKS(self, f): |
408 |
|
409 |
if not self.initiatorSet: |
410 |
return |
411 |
|
412 |
nodes = "" |
413 |
for n in self.active_nodes(): |
414 |
if n in self.ibftNodes: |
415 |
continue |
416 |
nodes += "iscsi --ipaddr %s --port %s --target %s" % (n.address, n.port, n.name) |
417 |
if n.iface != "default": |
418 |
nodes += " --iface %s" % self.ifaces[n.iface] |
419 |
auth = n.getAuth() |
420 |
if auth: |
421 |
nodes += " --user %s" % auth.username |
422 |
nodes += " --password %s" % auth.password |
423 |
if len(auth.reverse_username): |
424 |
nodes += " --reverse-user %s" % auth.reverse_username |
425 |
if len(auth.reverse_password): |
426 |
nodes += " --reverse-password %s" % auth.reverse_password |
427 |
nodes += "\n" |
428 |
|
429 |
if nodes: |
430 |
f.write("iscsiname %s\n" %(self.initiator,)) |
431 |
f.write("%s" % nodes) |
432 |
|
433 |
def write(self, instPath, anaconda): |
434 |
if not self.initiatorSet: |
435 |
return |
436 |
|
437 |
# set iscsi nodes to autostart |
438 |
root = anaconda.id.storage.rootDevice |
439 |
for node in self.active_nodes(): |
440 |
autostart = True |
441 |
disks = self.getNodeDisks(node, anaconda.id.storage) |
442 |
for disk in disks: |
443 |
# nodes used for root get started by the initrd |
444 |
if root.dependsOn(disk): |
445 |
autostart = False |
446 |
|
447 |
if autostart: |
448 |
node.setParameter("node.startup", "automatic") |
449 |
|
450 |
if not os.path.isdir(instPath + "/etc/iscsi"): |
451 |
os.makedirs(instPath + "/etc/iscsi", 0755) |
452 |
fd = os.open(instPath + INITIATOR_FILE, os.O_RDWR | os.O_CREAT) |
453 |
os.write(fd, "InitiatorName=%s\n" %(self.initiator)) |
454 |
os.close(fd) |
455 |
|
456 |
# copy "db" files. *sigh* |
457 |
if os.path.isdir(instPath + "/var/lib/iscsi"): |
458 |
shutil.rmtree(instPath + "/var/lib/iscsi") |
459 |
if os.path.isdir("/var/lib/iscsi"): |
460 |
shutil.copytree("/var/lib/iscsi", instPath + "/var/lib/iscsi", |
461 |
symlinks=True) |
462 |
|
463 |
def getNode(self, name, address, port, iface): |
464 |
for node in self.active_nodes(): |
465 |
if node.name == name and node.address == address and \ |
466 |
node.port == int(port) and node.iface == iface: |
467 |
return node |
468 |
|
469 |
return None |
470 |
|
471 |
def getNodeDisks(self, node, storage): |
472 |
nodeDisks = [] |
473 |
iscsiDisks = storage.devicetree.getDevicesByType("iscsi") |
474 |
for disk in iscsiDisks: |
475 |
if disk.node == node: |
476 |
nodeDisks.append(disk) |
477 |
|
478 |
return nodeDisks |
479 |
|
480 |
# Create iscsi singleton |
481 |
iscsi = iscsi() |
482 |
|
483 |
# vim:tw=78:ts=4:et:sw=4 |