1 #!@PYTHON@ -tt
2
3 import sys
4 import stat
5 import re
6 import os
7 import time
8 import logging
9 import atexit
10 import hashlib
11 import ctypes
12 sys.path.append("@FENCEAGENTSLIBDIR@")
13 from fencing import fail_usage, run_command, atexit_handler, check_input, process_input, show_docs, fence_action, all_opt
14 from fencing import run_delay
15
16 STORE_PATH = "@STORE_PATH@"
17
18
19 def get_status(conn, options):
20 del conn
21 status = "off"
22 for dev in options["devices"]:
23 is_block_device(dev)
24 reset_dev(options, dev)
25 if options["--key"] in get_registration_keys(options, dev):
26 status = "on"
27 else:
28 logging.debug("No registration for key "\
29 + options["--key"] + " on device " + dev + "\n")
30 if options["--action"] == "on":
31 status = "off"
32 break
33 return status
34
35
36 def set_status(conn, options):
37 del conn
38 count = 0
39 if options["--action"] == "on":
40 set_key(options)
41 for dev in options["devices"]:
42 is_block_device(dev)
43
44 register_dev(options, dev, options["--key"])
45 if options["--key"] not in get_registration_keys(options, dev):
46 count += 1
47 logging.debug("Failed to register key "\
48 + options["--key"] + "on device " + dev + "\n")
49 continue
50 dev_write(dev, options)
51
52 if get_reservation_key(options, dev) is None \
53 and not reserve_dev(options, dev) \
54 and get_reservation_key(options, dev) is None:
55 count += 1
56 logging.debug("Failed to create reservation (key="\
57 + options["--key"] + ", device=" + dev + ")\n")
58
59 else:
60 host_key = get_key()
61 if host_key == options["--key"].lower():
62 fail_usage("Failed: keys cannot be same. You can not fence yourself.")
63 for dev in options["devices"]:
64 is_block_device(dev)
65 register_dev(options, dev, host_key)
66 if options["--key"] in get_registration_keys(options, dev):
67 preempt_abort(options, host_key, dev)
68
69 for dev in options["devices"]:
70 if options["--key"] in get_registration_keys(options, dev):
71 count += 1
72 logging.debug("Failed to remove key "\
73 + options["--key"] + " on device " + dev + "\n")
74 continue
75
76 if not get_reservation_key(options, dev):
77 count += 1
78 logging.debug("No reservation exists on device " + dev + "\n")
79 if count:
80 logging.error("Failed to verify " + str(count) + " device(s)")
81 sys.exit(1)
82
83
84 # check if host is ready to execute actions
85 def do_action_monitor(options):
86 # Check if required binaries are installed
87 if bool(run_cmd(options, options["--sg_persist-path"] + " -V")["rc"]):
88 logging.error("Unable to run " + options["--sg_persist-path"])
89 return 1
90 elif bool(run_cmd(options, options["--sg_turs-path"] + " -V")["rc"]):
91 logging.error("Unable to run " + options["--sg_turs-path"])
92 return 1
93 elif ("--devices" not in options and
94 bool(run_cmd(options, options["--vgs-path"] + " --version")["rc"])):
95 logging.error("Unable to run " + options["--vgs-path"])
96 return 1
97
98 # Keys have to be present in order to fence/unfence
99 get_key()
100 dev_read()
101
102 return 0
103
104
105 # run command, returns dict, ret["rc"] = exit code; ret["out"] = output;
106 # ret["err"] = error
107 def run_cmd(options, cmd):
108 ret = {}
109 (ret["rc"], ret["out"], ret["err"]) = run_command(options, cmd)
110 ret["out"] = "".join([i for i in ret["out"] if i is not None])
111 ret["err"] = "".join([i for i in ret["err"] if i is not None])
112 return ret
113
114
115 # check if device exist and is block device
116 def is_block_device(dev):
117 if not os.path.exists(dev):
118 fail_usage("Failed: device \"" + dev + "\" does not exist")
119 if not stat.S_ISBLK(os.stat(dev).st_mode):
120 fail_usage("Failed: device \"" + dev + "\" is not a block device")
121
122
123 # cancel registration
124 def preempt_abort(options, host, dev):
125 reset_dev(options,dev)
126 cmd = options["--sg_persist-path"] + " -n -o -A -T 5 -K " + host + " -S " + options["--key"] + " -d " + dev
127 return not bool(run_cmd(options, cmd)["rc"])
128
129
130 def reset_dev(options, dev):
131 return run_cmd(options, options["--sg_turs-path"] + " " + dev)["rc"]
132
133
134 def register_dev(options, dev, key, do_preempt=True):
135 dev = os.path.realpath(dev)
136 if options.get("--mpath-register-method") == "multi" and re.search(r"^dm", dev[5:]):
137 devices = get_mpath_slaves(dev)
138 register_dev(options, devices[0], key)
139 for device in devices[1:]:
140 register_dev(options, device, key, False)
141 return True
142
143 # Check if any registration exists for the key already. We track this in
144 # order to decide whether the existing registration needs to be cleared.
145 # This is needed since the previous registration could be for a
146 # different I_T nexus (different ISID).
147 registration_key_exists = False
148 if key in get_registration_keys(options, dev):
149 logging.debug("Registration key exists for device " + dev)
150 registration_key_exists = True
151 if not register_helper(options, dev, key):
152 return False
153
154 if registration_key_exists:
155 # If key matches, make sure it matches with the connection that
156 # exists right now. To do this, we can issue a preempt with same key
157 # which should replace the old invalid entries from the target.
158 if do_preempt and not preempt(options, key, dev, key):
159 return False
160
161 # If there was no reservation, we need to issue another registration
162 # since the previous preempt would clear registration made above.
163 if get_reservation_key(options, dev, False) != key:
164 return register_helper(options, dev, key)
165 return True
166
167 # helper function to preempt host with 'key' using 'host_key' without aborting tasks
168 def preempt(options, host_key, dev, key):
169 reset_dev(options,dev)
170 cmd = options["--sg_persist-path"] + " -n -o -P -T 5 -K " + host_key + " -S " + key + " -d " + dev
171 return not bool(run_cmd(options, cmd)["rc"])
172
173 # helper function to send the register command
174 def register_helper(options, dev, key):
175 reset_dev(options, dev)
176 cmd = options["--sg_persist-path"] + " -n -o -I -S " + key + " -d " + dev
177 cmd += " -Z" if "--aptpl" in options else ""
178 return not bool(run_cmd(options, cmd)["rc"])
179
180
181 def reserve_dev(options, dev):
182 reset_dev(options,dev)
183 cmd = options["--sg_persist-path"] + " -n -o -R -T 5 -K " + options["--key"] + " -d " + dev
184 return not bool(run_cmd(options, cmd)["rc"])
185
186
187 def get_reservation_key(options, dev, fail=True):
188 reset_dev(options,dev)
189 opts = ""
190 if "--readonly" in options:
191 opts = "-y "
192 cmd = options["--sg_persist-path"] + " -n -i " + opts + "-r -d " + dev
193 out = run_cmd(options, cmd)
194 if out["rc"] and fail:
195 fail_usage('Cannot get reservation key on device "' + dev
196 + '": ' + out["err"])
197 match = re.search(r"\s+key=0x(\S+)\s+", out["out"], re.IGNORECASE)
198 return match.group(1) if match else None
199
200
201 def get_registration_keys(options, dev, fail=True):
202 reset_dev(options,dev)
203 keys = []
204 opts = ""
205 if "--readonly" in options:
206 opts = "-y "
207 cmd = options["--sg_persist-path"] + " -n -i " + opts + "-k -d " + dev
208 out = run_cmd(options, cmd)
209 if out["rc"]:
210 fail_usage('Cannot get registration keys on device "' + dev
211 + '": ' + out["err"], fail)
212 if not fail:
213 return []
214 for line in out["out"].split("\n"):
215 match = re.search(r"\s+0x(\S+)\s*", line)
216 if match:
217 keys.append(match.group(1))
218 return keys
219
220
221 def get_cluster_id(options):
222 cmd = options["--corosync-cmap-path"] + " totem.cluster_name"
223
224 match = re.search(r"\(str\) = (\S+)\n", run_cmd(options, cmd)["out"])
225
226 if not match:
227 fail_usage("Failed: cannot get cluster name")
228
229 try:
|
(1) Event Sigma main event: |
This application uses a weak algorithm to generate hash digests in security context, which may lead to collision attacks. |
|
(2) Event remediation: |
Use strong algorithms such as `sha256` to prevent collision attacks or explicitly set `usedforsecurity` parameter to `false` to make it clear that it is not being used in a security context. |
230 return hashlib.md5(match.group(1).encode('ascii')).hexdigest()
231 except ValueError:
232 # FIPS requires usedforsecurity=False and might not be
233 # available on all distros: https://bugs.python.org/issue9216
234 return hashlib.md5(match.group(1).encode('ascii'), usedforsecurity=False).hexdigest()
235
236
237 def get_node_id(options):
238 cmd = options["--corosync-cmap-path"] + " nodelist"
239 out = run_cmd(options, cmd)["out"]
240
241 match = re.search(r".(\d+).name \(str\) = " + options["--plug"] + r"\n", out)
242
243 # try old format before failing
244 if not match:
245 match = re.search(r".(\d+).ring._addr \(str\) = " + options["--plug"] + r"\n", out)
246
247 return match.group(1) if match else fail_usage("Failed: unable to parse output of corosync-cmapctl or node does not exist")
248
249 def get_node_hash(options):
250 try:
251 return hashlib.md5(options["--plug"].encode('ascii')).hexdigest()
252 except ValueError:
253 # FIPS requires usedforsecurity=False and might not be
254 # available on all distros: https://bugs.python.org/issue9216
255 return hashlib.md5(options["--plug"].encode('ascii'), usedforsecurity=False).hexdigest()
256
257
258 def generate_key(options):
259 if options["--key-value"] == "hash":
260 return "%.4s%.4s" % (get_cluster_id(options), get_node_hash(options))
261 else:
262 return "%.4s%.4d" % (get_cluster_id(options), int(get_node_id(options)))
263
264
265 # save node key to file
266 def set_key(options):
267 file_path = options["store_path"] + ".key"
268 if not os.path.isdir(os.path.dirname(options["store_path"])):
269 os.makedirs(os.path.dirname(options["store_path"]))
270 try:
271 f = open(file_path, "w")
272 except IOError:
273 fail_usage("Failed: Cannot open file \""+ file_path + "\"")
274 f.write(options["--key"].lower() + "\n")
275 f.close()
276
277
278 # read node key from file
279 def get_key(fail=True):
280 file_path = STORE_PATH + ".key"
281 try:
282 f = open(file_path, "r")
283 except IOError:
284 fail_usage("Failed: Cannot open file \""+ file_path + "\"", fail)
285 if not fail:
286 return None
287 return f.readline().strip().lower()
288
289
290 def dev_write(dev, options):
291 file_path = options["store_path"] + ".dev"
292 if not os.path.isdir(os.path.dirname(options["store_path"])):
293 os.makedirs(os.path.dirname(options["store_path"]))
294 try:
295 f = open(file_path, "a+")
296 except IOError:
297 fail_usage("Failed: Cannot open file \""+ file_path + "\"")
298 f.seek(0)
299 out = f.read()
300 if not re.search(r"^" + dev + r"\s+", out, flags=re.MULTILINE):
301 f.write(dev + "\n")
302 f.close()
303
304
305 def dev_read(fail=True, opt=None):
306 file_path = STORE_PATH + ".dev"
307 try:
308 f = open(file_path, "r")
309 except IOError:
310 if "--suppress-errors" not in opt:
311 fail_usage("Failed: Cannot open file \"" + file_path + "\"", fail)
312 if not fail:
313 return None
314 # get not empty lines from file
315 devs = [line.strip() for line in f if line.strip()]
316 f.close()
317 return devs
318
319
320 def get_shared_devices(options):
321 devs = []
322 cmd = options["--vgs-path"] + " " +\
323 "--noheadings " +\
324 "--separator : " +\
325 "--sort pv_uuid " +\
326 "--options vg_attr,pv_name "+\
327 "--config 'global { locking_type = 0 } devices { preferred_names = [ \"^/dev/dm\" ] }'"
328 out = run_cmd(options, cmd)
329 if out["rc"]:
330 fail_usage("Failed: Cannot get shared devices")
331 for line in out["out"].splitlines():
332 vg_attr, pv_name = line.strip().split(":")
333 if vg_attr[5] in "cs":
334 devs.append(pv_name)
335 return devs
336
337
338 def get_mpath_slaves(dev):
339 if dev[:5] == "/dev/":
340 dev = dev[5:]
341 slaves = [i for i in os.listdir("/sys/block/" + dev + "/slaves/") if i[:1] != "."]
342 if slaves[0][:2] == "dm":
343 slaves = get_mpath_slaves(slaves[0])
344 else:
345 slaves = ["/dev/" + x for x in slaves]
346 return slaves
347
348
349 def define_new_opts():
350 all_opt["devices"] = {
351 "getopt" : "d:",
352 "longopt" : "devices",
353 "help" : "-d, --devices=[devices] List of devices to use for current operation",
354 "required" : "0",
355 "shortdesc" : "List of devices to use for current operation. Devices can \
356 be comma or space separated list of raw devices (eg. /dev/sdc). Each device must support SCSI-3 \
357 persistent reservations. Optional if cluster is configured with clvm or lvmlockd.",
358 "order": 1
359 }
360 all_opt["nodename"] = {
361 "getopt" : ":",
362 "longopt" : "nodename",
363 "help" : "",
364 "required" : "0",
365 "shortdesc" : "",
366 "order": 1
367 }
368 all_opt["key"] = {
369 "getopt" : "k:",
370 "longopt" : "key",
371 "help" : "-k, --key=[key] Key to use for the current operation",
372 "required" : "0",
373 "shortdesc" : "Key to use for the current operation. This key should be \
374 unique to a node. For the \"on\" action, the key specifies the key use to \
375 register the local node. For the \"off\" action, this key specifies the key to \
376 be removed from the device(s).",
377 "order": 1
378 }
379 all_opt["aptpl"] = {
380 "getopt" : "a",
381 "longopt" : "aptpl",
382 "help" : "-a, --aptpl Use the APTPL flag for registrations",
383 "required" : "0",
384 "shortdesc" : "Use the APTPL flag for registrations. This option is only used for the 'on' action.",
385 "order": 1
386 }
387 all_opt["mpath_register_method"] = {
388 "getopt" : ":",
389 "longopt" : "mpath-register-method",
390 "help" : "--mpath-register-method=[multi|single] Register key to all multipath sub devices (multi), or directly to the main multipath device (single).",
391 "required" : "0",
392 "shortdesc" : "Multipath register method (multi/single).",
393 "default": "multi",
394 "order": 3
395 }
396 all_opt["readonly"] = {
397 "getopt" : "",
398 "longopt" : "readonly",
399 "help" : "--readonly Open DEVICE read-only. May be useful with PRIN commands if there are unwanted side effects with the default read-write open.",
400 "required" : "0",
401 "shortdesc" : "Open DEVICE read-only.",
402 "order": 4
403 }
404 all_opt["suppress-errors"] = {
405 "getopt" : "",
406 "longopt" : "suppress-errors",
407 "help" : "--suppress-errors Suppress error log. Suppresses error logging when run from the watchdog service before pacemaker starts.",
408 "required" : "0",
409 "shortdesc" : "Error log suppression.",
410 "order": 5
411 }
412 all_opt["logfile"] = {
413 "getopt" : ":",
414 "longopt" : "logfile",
415 "help" : "-f, --logfile Log output (stdout and stderr) to file",
416 "required" : "0",
417 "shortdesc" : "Log output (stdout and stderr) to file",
418 "order": 6
419 }
420 all_opt["corosync_cmap_path"] = {
421 "getopt" : ":",
422 "longopt" : "corosync-cmap-path",
423 "help" : "--corosync-cmap-path=[path] Path to corosync-cmapctl binary",
424 "required" : "0",
425 "shortdesc" : "Path to corosync-cmapctl binary",
426 "default" : "@COROSYNC_CMAPCTL_PATH@",
427 "order": 300
428 }
429 all_opt["sg_persist_path"] = {
430 "getopt" : ":",
431 "longopt" : "sg_persist-path",
432 "help" : "--sg_persist-path=[path] Path to sg_persist binary",
433 "required" : "0",
434 "shortdesc" : "Path to sg_persist binary",
435 "default" : "@SG_PERSIST_PATH@",
436 "order": 300
437 }
438 all_opt["sg_turs_path"] = {
439 "getopt" : ":",
440 "longopt" : "sg_turs-path",
441 "help" : "--sg_turs-path=[path] Path to sg_turs binary",
442 "required" : "0",
443 "shortdesc" : "Path to sg_turs binary",
444 "default" : "@SG_TURS_PATH@",
445 "order": 300
446 }
447 all_opt["vgs_path"] = {
448 "getopt" : ":",
449 "longopt" : "vgs-path",
450 "help" : "--vgs-path=[path] Path to vgs binary",
451 "required" : "0",
452 "shortdesc" : "Path to vgs binary",
453 "default" : "@VGS_PATH@",
454 "order": 300
455 }
456 all_opt["key_value"] = {
457 "getopt" : ":",
458 "longopt" : "key-value",
459 "help" : "--key-value=<id|hash> SCSI key node generation method",
460 "required" : "0",
461 "shortdesc" : "Method used to generate the SCSI key. \"id\" (default) \
462 uses the positional ID from \"corosync-cmactl nodelist\" output which can get inconsistent \
463 when nodes are removed from cluster without full cluster restart. \"hash\" uses part of hash \
464 made out of node names which is not affected over time but there is theoretical chance that \
465 hashes can collide as size of SCSI key is quite limited.",
466 "default" : "id",
467 "order": 300
468 }
469
470
471 def scsi_check_get_options(options):
472 try:
473 f = open("/etc/sysconfig/stonith", "r")
474 except IOError:
475 return options
476
477 match = re.findall(r"^\s*(\S*)\s*=\s*(\S*)\s*", "".join(f.readlines()), re.MULTILINE)
478
479 for m in match:
480 options[m[0].lower()] = m[1].lower()
481
482 f.close()
483
484 return options
485
486
487 def scsi_check(hardreboot=False):
488 if len(sys.argv) >= 3 and sys.argv[1] == "repair":
489 return int(sys.argv[2])
490 options = {}
491 options["--sg_turs-path"] = "@SG_TURS_PATH@"
492 options["--sg_persist-path"] = "@SG_PERSIST_PATH@"
493 options["--power-timeout"] = "5"
494 options["retry"] = "0"
495 options["retry-sleep"] = "1"
496 options = scsi_check_get_options(options)
497 if "verbose" in options and options["verbose"] == "yes":
498 logging.getLogger().setLevel(logging.DEBUG)
499 devs = dev_read(fail=False,opt=options)
500 if not devs:
501 if "--suppress-errors" not in options:
502 logging.error("No devices found")
503 return 0
504 key = get_key(fail=False)
505 if not key:
506 logging.error("Key not found")
507 return 0
508 for dev in devs:
509 for n in range(int(options["retry"]) + 1):
510 if n > 0:
511 logging.debug("retry: " + str(n) + " of " + options["retry"])
512 if key in get_registration_keys(options, dev, fail=False):
513 logging.debug("key " + key + " registered with device " + dev)
514 return 0
515 else:
516 logging.debug("key " + key + " not registered with device " + dev)
517
518 if n < int(options["retry"]):
519 time.sleep(float(options["retry-sleep"]))
520
521 logging.debug("key " + key + " registered with any devices")
522
523 if hardreboot == True:
524 libc = ctypes.cdll['libc.so.6']
525 libc.reboot(0x1234567)
526 return 2
527
528
529 def main():
530
531 atexit.register(atexit_handler)
532
533 device_opt = ["no_login", "no_password", "devices", "nodename", "port",\
534 "no_port", "key", "aptpl", "fabric_fencing", "on_target", "corosync_cmap_path",\
535 "sg_persist_path", "sg_turs_path", "mpath_register_method", "readonly", \
536 "suppress-errors", "logfile", "vgs_path","force_on", "key_value"]
537
538 define_new_opts()
539
540 all_opt["delay"]["getopt"] = "H:"
541
542 all_opt["port"]["help"] = "-n, --plug=[nodename] Name of the node to be fenced"
543 all_opt["port"]["shortdesc"] = "Name of the node to be fenced. The node name is used to \
544 generate the key value used for the current operation. This option will be \
545 ignored when used with the -k option."
546
547 #fence_scsi_check
548 if os.path.basename(sys.argv[0]) == "fence_scsi_check":
549 sys.exit(scsi_check())
550 elif os.path.basename(sys.argv[0]) == "fence_scsi_check_hardreboot":
551 sys.exit(scsi_check(True))
552
553 options = check_input(device_opt, process_input(device_opt), other_conditions=True)
554
555 # hack to remove list/list-status actions which are not supported
556 options["device_opt"] = [ o for o in options["device_opt"] if o != "separator" ]
557
558 docs = {}
559 docs["shortdesc"] = "Fence agent for SCSI persistent reservation"
560 docs["longdesc"] = "fence_scsi is an I/O Fencing agent that uses SCSI-3 \
561 persistent reservations to control access to shared storage devices. These \
562 devices must support SCSI-3 persistent reservations (SPC-3 or greater) as \
563 well as the \"preempt-and-abort\" subcommand.\nThe fence_scsi agent works by \
564 having each node in the cluster register a unique key with the SCSI \
565 device(s). Reservation key is generated from \"node id\" (default) or from \
566 \"node name hash\" (RECOMMENDED) by adjusting \"key_value\" option. \
567 Using hash is recommended to prevent issues when removing nodes \
568 from cluster without full cluster restart. \
569 Once registered, a single node will become the reservation holder \
570 by creating a \"write exclusive, registrants only\" reservation on the \
571 device(s). The result is that only registered nodes may write to the \
572 device(s). When a node failure occurs, the fence_scsi agent will remove the \
573 key belonging to the failed node from the device(s). The failed node will no \
574 longer be able to write to the device(s). A manual reboot is required.\
575 \n.P\n\
576 When used as a watchdog device you can define e.g. retry=1, retry-sleep=2 and \
577 verbose=yes parameters in /etc/sysconfig/stonith if you have issues with it \
578 failing."
579 docs["vendorurl"] = ""
580 show_docs(options, docs)
581
582 run_delay(options)
583
584 # backward compatibility layer BEGIN
585 if "--logfile" in options:
586 try:
587 logfile = open(options["--logfile"], 'w')
588 sys.stderr = logfile
589 sys.stdout = logfile
590 except IOError:
591 fail_usage("Failed: Unable to create file " + options["--logfile"])
592 # backward compatibility layer END
593
594 options["store_path"] = STORE_PATH
595
596 # Input control BEGIN
597 stop_after_error = False if options["--action"] == "validate-all" else True
598
599 if options["--action"] == "monitor":
600 sys.exit(do_action_monitor(options))
601
602 # workaround to avoid regressions
603 if "--nodename" in options and options["--nodename"]:
604 options["--plug"] = options["--nodename"]
605 del options["--nodename"]
606
607 if not (("--plug" in options and options["--plug"])\
608 or ("--key" in options and options["--key"])):
609 fail_usage("Failed: nodename or key is required", stop_after_error)
610
611 if options["--action"] != "validate-all":
612 if not ("--key" in options and options["--key"]):
613 options["--key"] = generate_key(options)
614
615 if options["--key"] == "0" or not options["--key"]:
616 fail_usage("Failed: key cannot be 0", stop_after_error)
617
618 if "--key-value" in options\
619 and (options["--key-value"] != "id" and options["--key-value"] != "hash"):
620 fail_usage("Failed: key-value has to be 'id' or 'hash'", stop_after_error)
621
622 if options["--action"] == "validate-all":
623 sys.exit(0)
624
625 options["--key"] = options["--key"].lstrip('0')
626
627 if not ("--devices" in options and [d for d in re.split(r"\s*,\s*|\s+", options["--devices"].strip()) if d]):
628 options["devices"] = get_shared_devices(options)
629 else:
630 options["devices"] = [d for d in re.split(r"\s*,\s*|\s+", options["--devices"].strip()) if d]
631
632 if not options["devices"]:
633 fail_usage("Failed: No devices found")
634 # Input control END
635
636 result = fence_action(None, options, set_status, get_status)
637 sys.exit(result)
638
639 if __name__ == "__main__":
640 main()
641