1# Copyright (C) 2008-2011 Dejan Muhamedagic <dmuhamedagic@suse.de> 2# See COPYING for license information. 3 4import os 5import sys 6from tempfile import mkstemp 7import subprocess 8import re 9import glob 10import time 11import datetime 12import shutil 13import shlex 14import bz2 15import fnmatch 16import gc 17import ipaddress 18import argparse 19import random 20import string 21from pathlib import Path 22from contextlib import contextmanager, closing 23from stat import S_ISBLK 24from . import config 25from . import userdir 26from . import constants 27from . import options 28from . import term 29from . import parallax 30from .msg import common_warn, common_info, common_debug, common_err, err_buf 31from .constants import SSH_OPTION 32 33 34class TerminateSubCommand(Exception): 35 """ 36 This is an exception to jump out of subcommand when meeting errors while staying interactive shell 37 """ 38 39 40def to_ascii(input_str): 41 """Convert the bytes string to a ASCII string 42 Usefull to remove accent (diacritics)""" 43 if input_str is None: 44 return input_str 45 if isinstance(input_str, str): 46 return input_str 47 try: 48 return str(input_str, 'utf-8') 49 except UnicodeDecodeError: 50 if config.core.debug or options.regression_tests: 51 import traceback 52 traceback.print_exc() 53 return input_str.decode('utf-8', errors='ignore') 54 55 56def filter_keys(key_list, args, sign="="): 57 """Return list item which not be completed yet""" 58 return [s+sign for s in key_list if any_startswith(args, s+sign) is None] 59 60 61def any_startswith(iterable, prefix): 62 """Return first element in iterable which startswith prefix, or None.""" 63 for element in iterable: 64 if element.startswith(prefix): 65 return element 66 return None 67 68 69def rindex(iterable, value): 70 return len(iterable) - iterable[::-1].index(value) - 1 71 72 73def memoize(function): 74 "Decorator to invoke a function once only for any argument" 75 memoized = {} 76 77 def inner(*args): 78 if args in memoized: 79 return memoized[args] 80 r = function(*args) 81 memoized[args] = r 82 return r 83 return inner 84 85 86@contextmanager 87def nogc(): 88 gc.disable() 89 try: 90 yield 91 finally: 92 gc.enable() 93 94 95getuser = userdir.getuser 96gethomedir = userdir.gethomedir 97 98 99@memoize 100def this_node(): 101 'returns name of this node (hostname)' 102 return os.uname()[1] 103 104 105_cib_shadow = 'CIB_shadow' 106_cib_in_use = '' 107 108 109def set_cib_in_use(name): 110 os.putenv(_cib_shadow, name) 111 global _cib_in_use 112 _cib_in_use = name 113 114 115def clear_cib_in_use(): 116 os.unsetenv(_cib_shadow) 117 global _cib_in_use 118 _cib_in_use = '' 119 120 121def get_cib_in_use(): 122 return _cib_in_use 123 124 125def get_tempdir(): 126 return os.getenv("TMPDIR") or "/tmp" 127 128 129def is_program(prog): 130 """Is this program available?""" 131 def isexec(filename): 132 return os.path.isfile(filename) and os.access(filename, os.X_OK) 133 for p in os.getenv("PATH").split(os.pathsep): 134 f = os.path.join(p, prog) 135 if isexec(f): 136 return f 137 return None 138 139 140def pacemaker_20_daemon(new, old): 141 "helper to discover renamed pacemaker daemons" 142 if is_program(new): 143 return new 144 return old 145 146 147@memoize 148def pacemaker_attrd(): 149 return pacemaker_20_daemon("pacemaker-attrd", "attrd") 150 151 152@memoize 153def pacemaker_based(): 154 return pacemaker_20_daemon("pacemaker-based", "cib") 155 156 157@memoize 158def pacemaker_controld(): 159 return pacemaker_20_daemon("pacemaker-controld", "crmd") 160 161 162@memoize 163def pacemaker_execd(): 164 return pacemaker_20_daemon("pacemaker-execd", "lrmd") 165 166 167@memoize 168def pacemaker_fenced(): 169 return pacemaker_20_daemon("pacemaker-fenced", "stonithd") 170 171 172@memoize 173def pacemaker_remoted(): 174 return pacemaker_20_daemon("pacemaker-remoted", "pacemaker_remoted") 175 176 177@memoize 178def pacemaker_schedulerd(): 179 return pacemaker_20_daemon("pacemaker-schedulerd", "pengine") 180 181 182def pacemaker_daemon(name): 183 if name == "attrd" or name == "pacemaker-attrd": 184 return pacemaker_attrd() 185 if name == "cib" or name == "pacemaker-based": 186 return pacemaker_based() 187 if name == "crmd" or name == "pacemaker-controld": 188 return pacemaker_controld() 189 if name == "lrmd" or name == "pacemaker-execd": 190 return pacemaker_execd() 191 if name == "stonithd" or name == "pacemaker-fenced": 192 return pacemaker_fenced() 193 if name == "pacemaker_remoted" or name == "pacemeaker-remoted": 194 return pacemaker_remoted() 195 if name == "pengine" or name == "pacemaker-schedulerd": 196 return pacemaker_schedulerd() 197 raise ValueError("Not a Pacemaker daemon name: {}".format(name)) 198 199 200def can_ask(): 201 """ 202 Is user-interactivity possible? 203 Checks if connected to a TTY. 204 """ 205 return (not options.ask_no) and sys.stdin.isatty() 206 207 208def ask(msg): 209 """ 210 Ask for user confirmation. 211 If core.force is true, always return true. 212 If not interactive and core.force is false, always return false. 213 """ 214 if config.core.force: 215 common_info("%s [YES]" % (msg)) 216 return True 217 if not can_ask(): 218 return False 219 220 msg += ' ' 221 if msg.endswith('? '): 222 msg = msg[:-2] + ' (y/n)? ' 223 224 while True: 225 try: 226 ans = input(msg) 227 except EOFError: 228 ans = 'n' 229 if ans: 230 ans = ans[0].lower() 231 if ans in 'yn': 232 return ans == 'y' 233 234 235# holds part of line before \ split 236# for a multi-line input 237_LINE_BUFFER = '' 238 239 240def get_line_buffer(): 241 return _LINE_BUFFER 242 243 244def multi_input(prompt=''): 245 """ 246 Get input from user 247 Allow multiple lines using a continuation character 248 """ 249 global _LINE_BUFFER 250 line = [] 251 _LINE_BUFFER = '' 252 while True: 253 try: 254 text = input(prompt) 255 except EOFError: 256 return None 257 err_buf.incr_lineno() 258 if options.regression_tests: 259 print(".INP:", text) 260 sys.stdout.flush() 261 sys.stderr.flush() 262 stripped = text.strip() 263 if stripped.endswith('\\'): 264 stripped = stripped.rstrip('\\') 265 line.append(stripped) 266 _LINE_BUFFER += stripped 267 if prompt: 268 prompt = ' > ' 269 else: 270 line.append(stripped) 271 break 272 return ''.join(line) 273 274 275def verify_boolean(opt): 276 return opt.lower() in ("yes", "true", "on", "1") or \ 277 opt.lower() in ("no", "false", "off", "0") 278 279 280def is_boolean_true(opt): 281 if opt in (None, False): 282 return False 283 if opt is True: 284 return True 285 return opt.lower() in ("yes", "true", "on", "1") 286 287 288def is_boolean_false(opt): 289 if opt in (None, False): 290 return True 291 if opt is True: 292 return False 293 return opt.lower() in ("no", "false", "off", "0") 294 295 296def get_boolean(opt, dflt=False): 297 if not opt: 298 return dflt 299 return is_boolean_true(opt) 300 301 302def canonical_boolean(opt): 303 return 'true' if is_boolean_true(opt) else 'false' 304 305 306def keyword_cmp(string1, string2): 307 return string1.lower() == string2.lower() 308 309 310class olist(list): 311 """ 312 Implements the 'in' operator 313 in a case-insensitive manner, 314 allowing "if x in olist(...)" 315 """ 316 def __init__(self, keys): 317 super(olist, self).__init__([k.lower() for k in keys]) 318 319 def __contains__(self, key): 320 return super(olist, self).__contains__(key.lower()) 321 322 def append(self, key): 323 super(olist, self).append(key.lower()) 324 325 326def os_types_list(path): 327 l = [] 328 for f in glob.glob(path): 329 if os.access(f, os.X_OK) and os.path.isfile(f): 330 a = f.split("/") 331 l.append(a[-1]) 332 return l 333 334 335def listtemplates(): 336 l = [] 337 templates_dir = os.path.join(config.path.sharedir, 'templates') 338 for f in os.listdir(templates_dir): 339 if os.path.isfile("%s/%s" % (templates_dir, f)): 340 l.append(f) 341 return l 342 343 344def listconfigs(): 345 l = [] 346 for f in os.listdir(userdir.CRMCONF_DIR): 347 if os.path.isfile("%s/%s" % (userdir.CRMCONF_DIR, f)): 348 l.append(f) 349 return l 350 351 352def add_sudo(cmd): 353 if config.core.user: 354 return "sudo -E -u %s %s" % (config.core.user, cmd) 355 return cmd 356 357 358def add_su(cmd, user): 359 """ 360 Wrapped cmd with su -c "<cmd>" <user> 361 """ 362 if user == "root": 363 return cmd 364 return "su -c \"{}\" {}".format(cmd, user) 365 366 367def chown(path, user, group): 368 if isinstance(user, int): 369 uid = user 370 else: 371 import pwd 372 uid = pwd.getpwnam(user).pw_uid 373 if isinstance(group, int): 374 gid = group 375 else: 376 import grp 377 gid = grp.getgrnam(group).gr_gid 378 os.chown(path, uid, gid) 379 380 381def ensure_sudo_readable(f): 382 # make sure the tempfile is readable to crm_diff (bsc#999683) 383 if config.core.user: 384 from pwd import getpwnam 385 uid = getpwnam(config.core.user).pw_uid 386 try: 387 os.chown(f, uid, -1) 388 except os.error as err: 389 common_err('Failed setting temporary file permissions: %s' % (err)) 390 return False 391 return True 392 393 394def pipe_string(cmd, s): 395 rc = -1 # command failed 396 cmd = add_sudo(cmd) 397 common_debug("piping string to %s" % cmd) 398 if options.regression_tests: 399 print(".EXT", cmd) 400 p = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE) 401 try: 402 # communicate() expects encoded bytes 403 if isinstance(s, str): 404 s = s.encode('utf-8') 405 p.communicate(s) 406 p.wait() 407 rc = p.returncode 408 except IOError as msg: 409 if "Broken pipe" not in str(msg): 410 common_err(msg) 411 return rc 412 413 414def filter_string(cmd, s, stderr_on=True, shell=True): 415 rc = -1 # command failed 416 outp = '' 417 if stderr_on is True: 418 stderr = None 419 else: 420 stderr = subprocess.PIPE 421 cmd = add_sudo(cmd) 422 common_debug("pipe through %s" % cmd) 423 if options.regression_tests: 424 print(".EXT", cmd) 425 p = subprocess.Popen(cmd, 426 shell=shell, 427 stdin=subprocess.PIPE, 428 stdout=subprocess.PIPE, 429 stderr=stderr) 430 try: 431 # bytes expected here 432 if isinstance(s, str): 433 s = s.encode('utf-8') 434 ret = p.communicate(s) 435 if stderr_on == 'stdout': 436 outp = b"\n".join(ret) 437 else: 438 outp = ret[0] 439 p.wait() 440 rc = p.returncode 441 except OSError as err: 442 if err.errno != os.errno.EPIPE: 443 common_err(err.strerror) 444 common_info("from: %s" % cmd) 445 except Exception as msg: 446 common_err(msg) 447 common_info("from: %s" % cmd) 448 return rc, to_ascii(outp) 449 450 451def str2tmp(_str, suffix=".pcmk"): 452 ''' 453 Write the given string to a temporary file. Return the name 454 of the file. 455 ''' 456 s = to_ascii(_str) 457 fd, tmp = mkstemp(suffix=suffix) 458 try: 459 f = os.fdopen(fd, "w") 460 except IOError as msg: 461 common_err(msg) 462 return 463 f.write(s) 464 if not s.endswith('\n'): 465 f.write("\n") 466 f.close() 467 return tmp 468 469 470@contextmanager 471def create_tempfile(suffix='', dir=None): 472 """ Context for temporary file. 473 474 Will find a free temporary filename upon entering 475 and will try to delete the file on leaving, even in case of an exception. 476 477 Parameters 478 ---------- 479 suffix : string 480 optional file suffix 481 dir : string 482 optional directory to save temporary file in 483 484 (from http://stackoverflow.com/a/29491523) 485 """ 486 import tempfile 487 tf = tempfile.NamedTemporaryFile(delete=False, suffix=suffix, dir=dir) 488 tf.file.close() 489 try: 490 yield tf.name 491 finally: 492 try: 493 os.remove(tf.name) 494 except OSError as e: 495 if e.errno == 2: 496 pass 497 else: 498 raise 499 500 501@contextmanager 502def open_atomic(filepath, mode="r", buffering=-1, fsync=False, encoding=None): 503 """ Open temporary file object that atomically moves to destination upon 504 exiting. 505 506 Allows reading and writing to and from the same filename. 507 508 The file will not be moved to destination in case of an exception. 509 510 Parameters 511 ---------- 512 filepath : string 513 the file path to be opened 514 fsync : bool 515 whether to force write the file to disk 516 517 (from http://stackoverflow.com/a/29491523) 518 """ 519 520 with create_tempfile(dir=os.path.dirname(os.path.abspath(filepath))) as tmppath: 521 with open(tmppath, mode, buffering, encoding=encoding) as file: 522 try: 523 yield file 524 finally: 525 if fsync: 526 file.flush() 527 os.fsync(file.fileno()) 528 os.rename(tmppath, filepath) 529 530 531def str2file(s, fname, mod=0o644): 532 ''' 533 Write a string to a file. 534 ''' 535 try: 536 with open_atomic(fname, 'w', encoding='utf-8', fsync=True) as dst: 537 dst.write(to_ascii(s)) 538 os.chmod(fname, mod) 539 except IOError as msg: 540 common_err(msg) 541 return False 542 return True 543 544 545def file2str(fname, noerr=True): 546 ''' 547 Read a one line file into a string, strip whitespace around. 548 ''' 549 try: 550 f = open(fname, "r") 551 except IOError as msg: 552 if not noerr: 553 common_err(msg) 554 return None 555 s = f.readline() 556 f.close() 557 return s.strip() 558 559 560def file2list(fname): 561 ''' 562 Read a file into a list (newlines dropped). 563 ''' 564 try: 565 return open(fname).read().split('\n') 566 except IOError as msg: 567 common_err(msg) 568 return None 569 570 571def safe_open_w(fname): 572 if fname == "-": 573 f = sys.stdout 574 else: 575 if not options.batch and os.access(fname, os.F_OK): 576 if not ask("File %s exists. Do you want to overwrite it?" % fname): 577 return None 578 try: 579 f = open(fname, "w") 580 except IOError as msg: 581 common_err(msg) 582 return None 583 return f 584 585 586def safe_close_w(f): 587 if f and f != sys.stdout: 588 f.close() 589 590 591def is_path_sane(name): 592 if re.search(r"['`#*?$\[\];]", name): 593 common_err("%s: bad path" % name) 594 return False 595 return True 596 597 598def is_filename_sane(name): 599 if re.search(r"['`/#*?$\[\];]", name): 600 common_err("%s: bad filename" % name) 601 return False 602 return True 603 604 605def is_name_sane(name): 606 if re.search("[']", name): 607 common_err("%s: bad name" % name) 608 return False 609 return True 610 611 612def show_dot_graph(dotfile, keep_file=False, desc="transition graph"): 613 cmd = "%s %s" % (config.core.dotty, dotfile) 614 if not keep_file: 615 cmd = "(%s; rm -f %s)" % (cmd, dotfile) 616 if options.regression_tests: 617 print(".EXT", cmd) 618 subprocess.Popen(cmd, shell=True, bufsize=0, 619 stdin=None, stdout=None, stderr=None, close_fds=True) 620 common_info("starting %s to show %s" % (config.core.dotty, desc)) 621 622 623def ext_cmd(cmd, shell=True): 624 cmd = add_sudo(cmd) 625 if options.regression_tests: 626 print(".EXT", cmd) 627 common_debug("invoke: %s" % cmd) 628 return subprocess.call(cmd, shell=shell) 629 630 631def ext_cmd_nosudo(cmd, shell=True): 632 if options.regression_tests: 633 print(".EXT", cmd) 634 return subprocess.call(cmd, shell=shell) 635 636 637def rmdir_r(d): 638 # TODO: Make sure we're not deleting something we shouldn't! 639 if d and os.path.isdir(d): 640 shutil.rmtree(d) 641 642 643def nvpairs2dict(pairs): 644 ''' 645 takes a list of string of form ['a=b', 'c=d'] 646 and returns {'a':'b', 'c':'d'} 647 ''' 648 data = [] 649 for var in pairs: 650 if '=' in var: 651 data.append(var.split('=', 1)) 652 else: 653 data.append([var, None]) 654 return dict(data) 655 656 657def is_check_always(): 658 ''' 659 Even though the frequency may be set to always, it doesn't 660 make sense to do that with non-interactive sessions. 661 ''' 662 return options.interactive and config.core.check_frequency == "always" 663 664 665def get_check_rc(): 666 ''' 667 If the check mode is set to strict, then on errors we 668 return 2 which is the code for error. Otherwise, we 669 pretend that errors are warnings. 670 ''' 671 return 2 if config.core.check_mode == "strict" else 1 672 673 674_LOCKDIR = ".lockdir" 675_PIDF = "pid" 676 677 678def check_locker(lockdir): 679 if not os.path.isdir(os.path.join(lockdir, _LOCKDIR)): 680 return 681 s = file2str(os.path.join(lockdir, _LOCKDIR, _PIDF)) 682 pid = convert2ints(s) 683 if not isinstance(pid, int): 684 common_warn("history: removing malformed lock") 685 rmdir_r(os.path.join(lockdir, _LOCKDIR)) 686 return 687 try: 688 os.kill(pid, 0) 689 except OSError as err: 690 if err.errno == os.errno.ESRCH: 691 common_info("history: removing stale lock") 692 rmdir_r(os.path.join(lockdir, _LOCKDIR)) 693 else: 694 common_err("%s: %s" % (_LOCKDIR, err.strerror)) 695 696 697@contextmanager 698def lock(lockdir): 699 """ 700 Ensure that the lock is released properly 701 even in the face of an exception between 702 acquire and release. 703 """ 704 def acquire_lock(): 705 check_locker(lockdir) 706 while True: 707 try: 708 os.makedirs(os.path.join(lockdir, _LOCKDIR)) 709 str2file("%d" % os.getpid(), os.path.join(lockdir, _LOCKDIR, _PIDF)) 710 return True 711 except OSError as err: 712 if err.errno != os.errno.EEXIST: 713 common_err("Failed to acquire lock to %s: %s" % (lockdir, err.strerror)) 714 return False 715 time.sleep(0.1) 716 continue 717 else: 718 return False 719 720 has_lock = acquire_lock() 721 try: 722 yield 723 finally: 724 if has_lock: 725 rmdir_r(os.path.join(lockdir, _LOCKDIR)) 726 727 728def mkdirp(directory, mode=0o777, parents=True, exist_ok=True): 729 """ 730 Same behavior as the POSIX mkdir -p command 731 """ 732 Path(directory).mkdir(mode, parents, exist_ok) 733 734 735def pipe_cmd_nosudo(cmd): 736 if options.regression_tests: 737 print(".EXT", cmd) 738 proc = subprocess.Popen(cmd, 739 shell=True, 740 stdout=subprocess.PIPE, 741 stderr=subprocess.PIPE) 742 (outp, err_outp) = proc.communicate() 743 proc.wait() 744 rc = proc.returncode 745 if rc != 0: 746 print(outp) 747 print(err_outp) 748 return rc 749 750 751def run_cmd_on_remote(cmd, remote_addr, prompt_msg=None): 752 """ 753 Run a cmd on remote node 754 return (rc, stdout, err_msg) 755 """ 756 rc = 1 757 out_data = None 758 err_data = None 759 760 need_pw = check_ssh_passwd_need(remote_addr) 761 if need_pw and prompt_msg: 762 print(prompt_msg) 763 try: 764 result = parallax.parallax_call([remote_addr], cmd, need_pw) 765 rc, out_data, _ = result[0][1] 766 except ValueError as err: 767 err_match = re.search("Exited with error code ([0-9]+), Error output: (.*)", str(err)) 768 if err_match: 769 rc, err_data = err_match.groups() 770 finally: 771 return int(rc), to_ascii(out_data), err_data 772 773 774def get_stdout(cmd, input_s=None, stderr_on=True, shell=True, raw=False): 775 ''' 776 Run a cmd, return stdout output. 777 Optional input string "input_s". 778 stderr_on controls whether to show output which comes on stderr. 779 ''' 780 if stderr_on: 781 stderr = None 782 else: 783 stderr = subprocess.PIPE 784 if options.regression_tests: 785 print(".EXT", cmd) 786 proc = subprocess.Popen(cmd, 787 shell=shell, 788 stdin=subprocess.PIPE, 789 stdout=subprocess.PIPE, 790 stderr=stderr) 791 stdout_data, stderr_data = proc.communicate(input_s) 792 if raw: 793 return proc.returncode, stdout_data 794 return proc.returncode, to_ascii(stdout_data).strip() 795 796 797def get_stdout_stderr(cmd, input_s=None, shell=True, raw=False): 798 ''' 799 Run a cmd, return (rc, stdout, stderr) 800 ''' 801 if options.regression_tests: 802 print(".EXT", cmd) 803 proc = subprocess.Popen(cmd, 804 shell=shell, 805 stdin=input_s and subprocess.PIPE or None, 806 stdout=subprocess.PIPE, 807 stderr=subprocess.PIPE) 808 stdout_data, stderr_data = proc.communicate(input_s) 809 if raw: 810 return proc.returncode, stdout_data, stderr_data 811 return proc.returncode, to_ascii(stdout_data).strip(), to_ascii(stderr_data).strip() 812 813 814def stdout2list(cmd, stderr_on=True, shell=True): 815 ''' 816 Run a cmd, fetch output, return it as a list of lines. 817 stderr_on controls whether to show output which comes on stderr. 818 ''' 819 rc, s = get_stdout(add_sudo(cmd), stderr_on=stderr_on, shell=shell) 820 if not s: 821 return rc, [] 822 return rc, s.split('\n') 823 824 825def append_file(dest, src): 826 'Append src to dest' 827 try: 828 open(dest, "a").write(open(src).read()) 829 return True 830 except IOError as msg: 831 common_err("append %s to %s: %s" % (src, dest, msg)) 832 return False 833 834 835def get_dc(): 836 cmd = "crmadmin -D" 837 rc, s = get_stdout(add_sudo(cmd)) 838 if rc != 0: 839 return None 840 if not s.startswith("Designated"): 841 return None 842 return s.split()[-1] 843 844 845def wait4dc(what="", show_progress=True): 846 ''' 847 Wait for the DC to get into the S_IDLE state. This should be 848 invoked only after a CIB modification which would exercise 849 the PE. Parameter "what" is whatever the caller wants to be 850 printed if showing progress. 851 852 It is assumed that the DC is already in a different state, 853 usually it should be either PENGINE or TRANSITION. This 854 assumption may not be true, but there's a high chance that it 855 is since crmd should be faster to move through states than 856 this shell. 857 858 Further, it may also be that crmd already calculated the new 859 graph, did transition, and went back to the idle state. This 860 may in particular be the case if the transition turned out to 861 be empty. 862 863 Tricky. Though in practice it shouldn't be an issue. 864 865 There's no timeout, as we expect the DC to eventually becomes 866 idle. 867 ''' 868 dc = get_dc() 869 if not dc: 870 common_warn("can't find DC") 871 return False 872 cmd = "crm_attribute -Gq -t crm_config -n crmd-transition-delay 2> /dev/null" 873 delay = get_stdout(add_sudo(cmd))[1] 874 if delay: 875 delaymsec = crm_msec(delay) 876 if delaymsec > 0: 877 common_info("The crmd-transition-delay is configured. Waiting %d msec before check DC status." % delaymsec) 878 time.sleep(delaymsec // 1000) 879 cnt = 0 880 output_started = 0 881 init_sleep = 0.25 882 max_sleep = 1.00 883 sleep_time = init_sleep 884 while True: 885 dc = get_dc() 886 if not dc: 887 common_warn("DC lost during wait") 888 return False 889 cmd = "crmadmin -S %s" % dc 890 rc, s = get_stdout(add_sudo(cmd)) 891 if not s.startswith("Status"): 892 common_warn("%s unexpected output: %s (exit code: %d)" % 893 (cmd, s, rc)) 894 return False 895 try: 896 dc_status = s.split()[-2] 897 except: 898 common_warn("%s unexpected output: %s" % (cmd, s)) 899 return False 900 if dc_status == "S_IDLE": 901 if output_started: 902 sys.stderr.write(" done\n") 903 return True 904 time.sleep(sleep_time) 905 if sleep_time < max_sleep: 906 sleep_time *= 2 907 if show_progress: 908 if not output_started: 909 output_started = 1 910 sys.stderr.write("waiting for %s to finish ." % what) 911 cnt += 1 912 if cnt % 5 == 0: 913 sys.stderr.write(".") 914 915 916def run_ptest(graph_s, nograph, scores, utilization, actions, verbosity): 917 ''' 918 Pipe graph_s thru ptest(8). Show graph using dotty if requested. 919 ''' 920 actions_filter = "grep LogActions: | grep -vw Leave" 921 ptest = "2>&1 %s -x -" % config.core.ptest 922 if re.search("simulate", ptest) and \ 923 not re.search("-[RS]", ptest): 924 ptest = "%s -S" % ptest 925 if verbosity: 926 if actions: 927 verbosity = 'v' * max(3, len(verbosity)) 928 ptest = "%s -%s" % (ptest, verbosity.upper()) 929 if scores: 930 ptest = "%s -s" % ptest 931 if utilization: 932 ptest = "%s -U" % ptest 933 if config.core.dotty and not nograph: 934 fd, dotfile = mkstemp() 935 ptest = "%s -D %s" % (ptest, dotfile) 936 else: 937 dotfile = None 938 # ptest prints to stderr 939 if actions: 940 ptest = "%s | %s" % (ptest, actions_filter) 941 if options.regression_tests: 942 ptest = ">/dev/null %s" % ptest 943 common_debug("invoke: %s" % ptest) 944 rc, s = get_stdout(ptest, input_s=graph_s) 945 if rc != 0: 946 common_debug("'%s' exited with (rc=%d)" % (ptest, rc)) 947 if actions and rc == 1: 948 common_warn("No actions found.") 949 else: 950 common_warn("Simulation was unsuccessful (RC=%d)." % (rc)) 951 if dotfile: 952 if os.path.getsize(dotfile) > 0: 953 show_dot_graph(dotfile) 954 else: 955 common_warn("ptest produced empty dot file") 956 else: 957 if not nograph: 958 common_info("install graphviz to see a transition graph") 959 if s: 960 page_string(s) 961 return True 962 963 964def is_id_valid(ident): 965 """ 966 Verify that the id follows the definition: 967 http://www.w3.org/TR/1999/REC-xml-names-19990114/#ns-qualnames 968 """ 969 if not ident: 970 return False 971 id_re = r"^[A-Za-z_][\w._-]*$" 972 return re.match(id_re, ident) 973 974 975def check_range(a): 976 """ 977 Verify that the integer range in list a is valid. 978 """ 979 if len(a) != 2: 980 return False 981 if not isinstance(a[0], int) or not isinstance(a[1], int): 982 return False 983 return int(a[0]) <= int(a[1]) 984 985 986def crm_msec(t): 987 ''' 988 See lib/common/utils.c:crm_get_msec(). 989 ''' 990 convtab = { 991 'ms': (1, 1), 992 'msec': (1, 1), 993 'us': (1, 1000), 994 'usec': (1, 1000), 995 '': (1000, 1), 996 's': (1000, 1), 997 'sec': (1000, 1), 998 'm': (60*1000, 1), 999 'min': (60*1000, 1), 1000 'h': (60*60*1000, 1), 1001 'hr': (60*60*1000, 1), 1002 } 1003 if not t: 1004 return -1 1005 r = re.match(r"\s*(\d+)\s*([a-zA-Z]+)?", t) 1006 if not r: 1007 return -1 1008 if not r.group(2): 1009 q = '' 1010 else: 1011 q = r.group(2).lower() 1012 try: 1013 mult, div = convtab[q] 1014 except KeyError: 1015 return -1 1016 return (int(r.group(1))*mult) // div 1017 1018 1019def crm_time_cmp(a, b): 1020 return crm_msec(a) - crm_msec(b) 1021 1022 1023def shorttime(ts): 1024 if isinstance(ts, datetime.datetime): 1025 return ts.strftime("%X") 1026 if ts is not None: 1027 return time.strftime("%X", time.localtime(ts)) 1028 return time.strftime("%X", time.localtime(0)) 1029 1030 1031def shortdate(ts): 1032 if isinstance(ts, datetime.datetime): 1033 return ts.strftime("%F") 1034 if ts is not None: 1035 return time.strftime("%F", time.localtime(ts)) 1036 return time.strftime("%F", time.localtime(0)) 1037 1038 1039def sort_by_mtime(l): 1040 'Sort a (small) list of files by time mod.' 1041 l2 = [(os.stat(x).st_mtime, x) for x in l] 1042 l2.sort() 1043 return [x[1] for x in l2] 1044 1045 1046def file_find_by_name(root, filename): 1047 'Find a file within a tree matching fname' 1048 assert root 1049 assert filename 1050 for root, dirnames, filenames in os.walk(root): 1051 for filename in fnmatch.filter(filenames, filename): 1052 return os.path.join(root, filename) 1053 return None 1054 1055 1056def convert2ints(l): 1057 """ 1058 Convert a list of strings (or a string) to a list of ints. 1059 All strings must be ints, otherwise conversion fails and None 1060 is returned! 1061 """ 1062 try: 1063 if isinstance(l, (tuple, list)): 1064 return [int(x) for x in l] 1065 # it's a string then 1066 return int(l) 1067 except ValueError: 1068 return None 1069 1070 1071def is_int(s): 1072 'Check if the string can be converted to an integer.' 1073 try: 1074 int(s) 1075 return True 1076 except ValueError: 1077 return False 1078 1079 1080def is_process(s): 1081 """ 1082 Returns true if argument is the name of a running process. 1083 1084 s: process name 1085 returns Boolean 1086 """ 1087 from os.path import join, basename 1088 # find pids of running processes 1089 pids = [pid for pid in os.listdir('/proc') if pid.isdigit()] 1090 for pid in pids: 1091 try: 1092 cmdline = open(join('/proc', pid, 'cmdline'), 'rb').read() 1093 procname = basename(to_ascii(cmdline).replace('\x00', ' ').split(' ')[0]) 1094 if procname == s: 1095 return True 1096 except EnvironmentError: 1097 # a process may have died since we got the list of pids 1098 pass 1099 return False 1100 1101 1102def print_stacktrace(): 1103 """ 1104 Print the stack at the site of call 1105 """ 1106 import traceback 1107 import inspect 1108 sf = inspect.currentframe().f_back.f_back 1109 traceback.print_stack(sf) 1110 1111 1112def edit_file(fname): 1113 'Edit a file.' 1114 if not fname: 1115 return 1116 if not config.core.editor: 1117 return 1118 return ext_cmd_nosudo("%s %s" % (config.core.editor, fname)) 1119 1120 1121def edit_file_ext(fname, template=''): 1122 ''' 1123 Edit a file via a temporary file. 1124 Raises IOError on any error. 1125 ''' 1126 if not os.path.isfile(fname): 1127 s = template 1128 else: 1129 s = open(fname).read() 1130 filehash = hash(s) 1131 tmpfile = str2tmp(s) 1132 try: 1133 try: 1134 if edit_file(tmpfile) != 0: 1135 return 1136 s = open(tmpfile, 'r').read() 1137 if hash(s) == filehash: # file unchanged 1138 return 1139 f2 = open(fname, 'w') 1140 f2.write(s) 1141 f2.close() 1142 finally: 1143 os.unlink(tmpfile) 1144 except OSError as e: 1145 raise IOError(e) 1146 1147 1148def need_pager(s, w, h): 1149 from math import ceil 1150 cnt = 0 1151 for l in s.split('\n'): 1152 # need to remove color codes 1153 l = re.sub(r'\${\w+}', '', l) 1154 cnt += int(ceil((len(l) + 0.5) / w)) 1155 if cnt >= h: 1156 return True 1157 return False 1158 1159 1160def term_render(s): 1161 'Render for TERM.' 1162 try: 1163 return term.render(s) 1164 except: 1165 return s 1166 1167 1168def get_pager_cmd(*extra_opts): 1169 'returns a commandline which calls the configured pager' 1170 cmdline = [config.core.pager] 1171 if os.path.basename(config.core.pager) == "less": 1172 cmdline.append('-R') 1173 cmdline.extend(extra_opts) 1174 return ' '.join(cmdline) 1175 1176 1177def page_string(s): 1178 'Page string rendered for TERM.' 1179 if not s: 1180 return 1181 constants.need_reset = True 1182 w, h = get_winsize() 1183 if not need_pager(s, w, h): 1184 print(term_render(s)) 1185 elif not config.core.pager or not can_ask() or options.batch: 1186 print(term_render(s)) 1187 else: 1188 pipe_string(get_pager_cmd(), term_render(s).encode('utf-8')) 1189 constants.need_reset = False 1190 1191 1192def page_gen(g): 1193 'Page lines generated by generator g' 1194 w, h = get_winsize() 1195 if not config.core.pager or not can_ask() or options.batch: 1196 for line in g: 1197 sys.stdout.write(term_render(line)) 1198 else: 1199 pipe_string(get_pager_cmd(), term_render("".join(g))) 1200 1201 1202def page_file(filename): 1203 'Open file in pager' 1204 if not os.path.isfile(filename): 1205 return 1206 return ext_cmd_nosudo(get_pager_cmd(filename), shell=True) 1207 1208 1209def get_winsize(): 1210 try: 1211 import curses 1212 curses.setupterm() 1213 w = curses.tigetnum('cols') 1214 h = curses.tigetnum('lines') 1215 except: 1216 try: 1217 w = os.environ['COLS'] 1218 h = os.environ['LINES'] 1219 except KeyError: 1220 w = 80 1221 h = 25 1222 return w, h 1223 1224 1225def multicolumn(l): 1226 ''' 1227 A ls-like representation of a list of strings. 1228 A naive approach. 1229 ''' 1230 min_gap = 2 1231 w, _ = get_winsize() 1232 max_len = 8 1233 for s in l: 1234 if len(s) > max_len: 1235 max_len = len(s) 1236 cols = w // (max_len + min_gap) # approx. 1237 if not cols: 1238 cols = 1 1239 col_len = w // cols 1240 for i in range(len(l) // cols + 1): 1241 s = '' 1242 for j in range(i * cols, (i + 1) * cols): 1243 if not j < len(l): 1244 break 1245 if not s: 1246 s = "%-*s" % (col_len, l[j]) 1247 elif (j + 1) % cols == 0: 1248 s = "%s%s" % (s, l[j]) 1249 else: 1250 s = "%s%-*s" % (s, col_len, l[j]) 1251 if s: 1252 print(s) 1253 1254 1255def find_value(pl, name): 1256 for n, v in pl: 1257 if n == name: 1258 return v 1259 return None 1260 1261 1262def cli_replace_attr(pl, name, new_val): 1263 for i, attr in enumerate(pl): 1264 if attr[0] == name: 1265 attr[1] = new_val 1266 return 1267 1268 1269def cli_append_attr(pl, name, val): 1270 pl.append([name, val]) 1271 1272 1273def lines2cli(s): 1274 ''' 1275 Convert a string into a list of lines. Replace continuation 1276 characters. Strip white space, left and right. Drop empty lines. 1277 ''' 1278 cl = [] 1279 l = s.split('\n') 1280 cum = [] 1281 for p in l: 1282 p = p.strip() 1283 if p.endswith('\\'): 1284 p = p.rstrip('\\') 1285 cum.append(p) 1286 else: 1287 cum.append(p) 1288 cl.append(''.join(cum).strip()) 1289 cum = [] 1290 if cum: # in case s ends with backslash 1291 cl.append(''.join(cum)) 1292 return [x for x in cl if x] 1293 1294 1295def datetime_is_aware(dt): 1296 """ 1297 Determines if a given datetime.datetime is aware. 1298 1299 The logic is described in Python's docs: 1300 http://docs.python.org/library/datetime.html#datetime.tzinfo 1301 """ 1302 return dt and dt.tzinfo is not None and dt.tzinfo.utcoffset(dt) is not None 1303 1304 1305def make_datetime_naive(dt): 1306 """ 1307 Ensures that the datetime is not time zone-aware: 1308 1309 The returned datetime object is a naive time in UTC. 1310 """ 1311 if dt and datetime_is_aware(dt): 1312 return dt.replace(tzinfo=None) - dt.utcoffset() 1313 return dt 1314 1315 1316def total_seconds(td): 1317 """ 1318 Backwards compatible implementation of timedelta.total_seconds() 1319 """ 1320 if hasattr(datetime.timedelta, 'total_seconds'): 1321 return td.total_seconds() 1322 return (td.microseconds + (td.seconds + td.days * 24 * 3600) * 10**6) // 10**6 1323 1324 1325def datetime_to_timestamp(dt): 1326 """ 1327 Convert a datetime object into a floating-point second value 1328 """ 1329 try: 1330 return total_seconds(make_datetime_naive(dt) - datetime.datetime(1970, 1, 1)) 1331 except Exception as e: 1332 common_err("datetime_to_timestamp error: %s" % (e)) 1333 return None 1334 1335 1336def timestamp_to_datetime(ts): 1337 """ 1338 Convert a timestamp into a naive datetime object 1339 """ 1340 import dateutil 1341 import dateutil.tz 1342 return make_datetime_naive(datetime.datetime.fromtimestamp(ts).replace(tzinfo=dateutil.tz.tzlocal())) 1343 1344 1345def parse_time(t): 1346 ''' 1347 Try to make sense of the user provided time spec. 1348 Use dateutil if available, otherwise strptime. 1349 Return the datetime value. 1350 1351 Also does time zone elimination by passing the datetime 1352 through a timestamp conversion if necessary 1353 1354 TODO: dateutil is very slow, avoid it if possible 1355 ''' 1356 try: 1357 from dateutil import parser, tz 1358 dt = parser.parse(t) 1359 1360 if datetime_is_aware(dt): 1361 ts = datetime_to_timestamp(dt) 1362 if ts is None: 1363 return None 1364 dt = datetime.datetime.fromtimestamp(ts) 1365 else: 1366 # convert to UTC from local time 1367 dt = dt - tz.tzlocal().utcoffset(dt) 1368 except ValueError as msg: 1369 common_err("parse_time %s: %s" % (t, msg)) 1370 return None 1371 except ImportError as msg: 1372 try: 1373 tm = time.strptime(t) 1374 dt = datetime.datetime(*tm[0:7]) 1375 except ValueError as msg: 1376 common_err("no dateutil, please provide times as printed by date(1)") 1377 return None 1378 return dt 1379 1380 1381def parse_to_timestamp(t): 1382 ''' 1383 Read a string and convert it into a UNIX timestamp. 1384 Added as an optimization of parse_time to avoid 1385 extra conversion steps when result would be converted 1386 into a timestamp anyway 1387 ''' 1388 try: 1389 from dateutil import parser, tz 1390 dt = parser.parse(t) 1391 1392 if datetime_is_aware(dt): 1393 return datetime_to_timestamp(dt) 1394 # convert to UTC from local time 1395 return total_seconds(dt - tz.tzlocal().utcoffset(dt) - datetime.datetime(1970, 1, 1)) 1396 except ValueError as msg: 1397 common_err("parse_time %s: %s" % (t, msg)) 1398 return None 1399 except ImportError as msg: 1400 try: 1401 tm = time.strptime(t) 1402 dt = datetime.datetime(*tm[0:7]) 1403 return datetime_to_timestamp(dt) 1404 except ValueError as msg: 1405 common_err("no dateutil, please provide times as printed by date(1)") 1406 return None 1407 1408 1409def save_graphviz_file(ini_f, attr_d): 1410 ''' 1411 Save graphviz settings to an ini file, if it does not exist. 1412 ''' 1413 if os.path.isfile(ini_f): 1414 common_err("%s exists, please remove it first" % ini_f) 1415 return False 1416 try: 1417 f = open(ini_f, "wb") 1418 except IOError as msg: 1419 common_err(msg) 1420 return False 1421 import configparser 1422 p = configparser.ConfigParser() 1423 for section, sect_d in attr_d.items(): 1424 p.add_section(section) 1425 for n, v in sect_d.items(): 1426 p.set(section, n, v) 1427 try: 1428 p.write(f) 1429 except IOError as msg: 1430 common_err(msg) 1431 return False 1432 f.close() 1433 common_info("graphviz attributes saved to %s" % ini_f) 1434 return True 1435 1436 1437def load_graphviz_file(ini_f): 1438 ''' 1439 Load graphviz ini file, if it exists. 1440 ''' 1441 if not os.path.isfile(ini_f): 1442 return True, None 1443 import configparser 1444 p = configparser.ConfigParser() 1445 try: 1446 p.read(ini_f) 1447 except Exception as msg: 1448 common_err(msg) 1449 return False, None 1450 _graph_d = {} 1451 for section in p.sections(): 1452 d = {} 1453 for n, v in p.items(section): 1454 d[n] = v 1455 _graph_d[section] = d 1456 return True, _graph_d 1457 1458 1459def get_pcmk_version(dflt): 1460 version = dflt 1461 1462 crmd = pacemaker_controld() 1463 if crmd: 1464 cmd = crmd 1465 else: 1466 return version 1467 1468 try: 1469 rc, s, err = get_stdout_stderr("%s version" % (cmd)) 1470 if rc != 0: 1471 common_err("%s exited with %d [err: %s][out: %s]" % (cmd, rc, err, s)) 1472 else: 1473 common_debug("pacemaker version: [err: %s][out: %s]" % (err, s)) 1474 if err.startswith("CRM Version:"): 1475 version = s.split()[0] 1476 else: 1477 version = s.split()[2] 1478 common_debug("found pacemaker version: %s" % version) 1479 except Exception as msg: 1480 common_warn("could not get the pacemaker version, bad installation?") 1481 common_warn(msg) 1482 return version 1483 1484 1485def get_cib_property(cib_f, attr, dflt): 1486 """A poor man's get attribute procedure. 1487 We don't want heavy parsing, this needs to be relatively 1488 fast. 1489 """ 1490 open_t = "<cluster_property_set" 1491 close_t = "</cluster_property_set" 1492 attr_s = 'name="%s"' % attr 1493 ver_patt = re.compile('value="([^"]+)"') 1494 ver = dflt # return some version in any case 1495 try: 1496 f = open(cib_f, "r") 1497 except IOError as msg: 1498 common_err(msg) 1499 return ver 1500 state = 0 1501 for s in f: 1502 if state == 0: 1503 if open_t in s: 1504 state += 1 1505 elif state == 1: 1506 if close_t in s: 1507 break 1508 if attr_s in s: 1509 r = ver_patt.search(s) 1510 if r: 1511 ver = r.group(1) 1512 break 1513 f.close() 1514 return ver 1515 1516 1517def get_cib_attributes(cib_f, tag, attr_l, dflt_l): 1518 """A poor man's get attribute procedure. 1519 We don't want heavy parsing, this needs to be relatively 1520 fast. 1521 """ 1522 open_t = "<%s " % tag 1523 val_patt_l = [re.compile('%s="([^"]+)"' % x) for x in attr_l] 1524 val_l = [] 1525 try: 1526 f = open(cib_f, "rb").read() 1527 except IOError as msg: 1528 common_err(msg) 1529 return dflt_l 1530 if os.path.splitext(cib_f)[-1] == '.bz2': 1531 cib_bits = bz2.decompress(f) 1532 else: 1533 cib_bits = f 1534 cib_s = to_ascii(cib_bits) 1535 for s in cib_s.split('\n'): 1536 if s.startswith(open_t): 1537 i = 0 1538 for patt in val_patt_l: 1539 r = patt.search(s) 1540 val_l.append(r and r.group(1) or dflt_l[i]) 1541 i += 1 1542 break 1543 return val_l 1544 1545 1546def is_min_pcmk_ver(min_ver, cib_f=None): 1547 if not constants.pcmk_version: 1548 if cib_f: 1549 constants.pcmk_version = get_cib_property(cib_f, "dc-version", "1.1.11") 1550 common_debug("found pacemaker version: %s in cib: %s" % 1551 (constants.pcmk_version, cib_f)) 1552 else: 1553 constants.pcmk_version = get_pcmk_version("1.1.11") 1554 from distutils.version import LooseVersion 1555 return LooseVersion(constants.pcmk_version) >= LooseVersion(min_ver) 1556 1557 1558def is_pcmk_118(cib_f=None): 1559 return is_min_pcmk_ver("1.1.8", cib_f=cib_f) 1560 1561 1562@memoize 1563def cibadmin_features(): 1564 ''' 1565 # usage example: 1566 if 'corosync-plugin' in cibadmin_features() 1567 ''' 1568 rc, outp = get_stdout(['cibadmin', '-!'], shell=False) 1569 if rc == 0: 1570 m = re.match(r'Pacemaker\s(\S+)\s\(Build: ([^\)]+)\):\s(.*)', outp.strip()) 1571 if m and len(m.groups()) > 2: 1572 return m.group(3).split() 1573 return [] 1574 1575 1576@memoize 1577def cibadmin_can_patch(): 1578 # cibadmin -P doesn't handle comments in <1.1.11 (unless patched) 1579 return is_min_pcmk_ver("1.1.11") 1580 1581 1582# quote function from python module shlex.py in python 3.3 1583 1584_find_unsafe = re.compile(r'[^\w@%+=:,./-]').search 1585 1586 1587def quote(s): 1588 """Return a shell-escaped version of the string *s*.""" 1589 if not s: 1590 return "''" 1591 if _find_unsafe(s) is None: 1592 return s 1593 1594 # use single quotes, and put single quotes into double quotes 1595 # the string $'b is then quoted as '$'"'"'b' 1596 return "'" + s.replace("'", "'\"'\"'") + "'" 1597 1598 1599def doublequote(s): 1600 """Return a shell-escaped version of the string *s*.""" 1601 if not s: 1602 return '""' 1603 if _find_unsafe(s) is None: 1604 return s 1605 1606 # use double quotes 1607 return '"' + s.replace('"', "\\\"") + '"' 1608 1609 1610def fetch_opts(args, opt_l): 1611 ''' 1612 Get and remove option keywords from args. 1613 They are always listed last, at the end of the line. 1614 Return a list of options found. The caller can do 1615 if keyw in optlist: ... 1616 ''' 1617 re_opt = None 1618 if opt_l[0].startswith("@"): 1619 re_opt = re.compile("^%s$" % opt_l[0][1:]) 1620 del opt_l[0] 1621 l = [] 1622 for i in reversed(list(range(len(args)))): 1623 if (args[i] in opt_l) or (re_opt and re_opt.search(args[i])): 1624 l.append(args.pop()) 1625 else: 1626 break 1627 return l 1628 1629 1630_LIFETIME = ["reboot", "forever"] 1631_ISO8601_RE = re.compile("(PT?[0-9]|[0-9]+.*[:-])") 1632 1633 1634def fetch_lifetime_opt(args, iso8601=True): 1635 ''' 1636 Get and remove a lifetime option from args. It can be one of 1637 lifetime_options or an ISO 8601 formatted period/time. There 1638 is apparently no good support in python for this format, so 1639 we cheat a bit. 1640 ''' 1641 if args: 1642 opt = args[-1] 1643 if opt in _LIFETIME or (iso8601 and _ISO8601_RE.match(opt)): 1644 return args.pop() 1645 return None 1646 1647 1648def resolve_hostnames(hostnames): 1649 ''' 1650 Tries to resolve the given list of hostnames. 1651 returns (ok, failed-hostname) 1652 ok: True if all hostnames resolved 1653 failed-hostname: First failed hostname resolution 1654 ''' 1655 import socket 1656 for node in hostnames: 1657 try: 1658 socket.gethostbyname(node) 1659 except socket.error: 1660 return False, node 1661 return True, None 1662 1663 1664def list_corosync_node_names(): 1665 ''' 1666 Returns list of nodes configured 1667 in corosync.conf 1668 ''' 1669 try: 1670 cfg = os.getenv('COROSYNC_MAIN_CONFIG_FILE', '/usr/local/etc/corosync/corosync.conf') 1671 lines = open(cfg).read().split('\n') 1672 name_re = re.compile(r'\s*name:\s+(.*)') 1673 names = [] 1674 for line in lines: 1675 name = name_re.match(line) 1676 if name: 1677 names.append(name.group(1)) 1678 return names 1679 except Exception: 1680 return [] 1681 1682 1683def list_corosync_nodes(): 1684 ''' 1685 Returns list of nodes configured 1686 in corosync.conf 1687 ''' 1688 try: 1689 cfg = os.getenv('COROSYNC_MAIN_CONFIG_FILE', '/usr/local/etc/corosync/corosync.conf') 1690 lines = open(cfg).read().split('\n') 1691 addr_re = re.compile(r'\s*ring0_addr:\s+(.*)') 1692 nodes = [] 1693 for line in lines: 1694 addr = addr_re.match(line) 1695 if addr: 1696 nodes.append(addr.group(1)) 1697 return nodes 1698 except Exception: 1699 return [] 1700 1701 1702def print_cluster_nodes(): 1703 """ 1704 Print the output of crm_node -l 1705 """ 1706 rc, out, _ = get_stdout_stderr("crm_node -l") 1707 if rc == 0 and out: 1708 print("{}\n".format(out)) 1709 1710 1711def list_cluster_nodes(): 1712 ''' 1713 Returns a list of nodes in the cluster. 1714 ''' 1715 def getname(toks): 1716 if toks and len(toks) >= 2: 1717 return toks[1] 1718 return None 1719 1720 try: 1721 # when pacemaker running 1722 rc, outp = stdout2list(['crm_node', '-l'], stderr_on=False, shell=False) 1723 if rc == 0: 1724 return [x for x in [getname(line.split()) for line in outp] if x and x != '(null)'] 1725 1726 # when corosync running 1727 ip_list = get_member_iplist() 1728 if ip_list: 1729 return ip_list 1730 1731 # static situation 1732 cib_path = os.getenv('CIB_file', '/var/lib/pacemaker/cib/cib.xml') 1733 if not os.path.isfile(cib_path): 1734 return None 1735 from . import xmlutil 1736 node_list = [] 1737 cib = xmlutil.file2cib_elem(cib_path) 1738 if cib is None: 1739 return None 1740 for node in cib.xpath('/cib/configuration/nodes/node'): 1741 name = node.get('uname') or node.get('id') 1742 if node.get('type') == 'remote': 1743 srv = cib.xpath("//primitive[@id='%s']/instance_attributes/nvpair[@name='server']" % (name)) 1744 if srv: 1745 continue 1746 node_list.append(name) 1747 return node_list 1748 except OSError as msg: 1749 raise ValueError("Error listing cluster nodes: %s" % (msg)) 1750 1751 1752def cluster_run_cmd(cmd): 1753 """ 1754 Run cmd in cluster nodes 1755 """ 1756 node_list = list_cluster_nodes() 1757 if not node_list: 1758 raise ValueError("Failed to get node list from cluster") 1759 parallax.parallax_call(node_list, cmd) 1760 1761 1762def list_cluster_nodes_except_me(): 1763 """ 1764 Get cluster node list and filter out self 1765 """ 1766 node_list = list_cluster_nodes() 1767 if not node_list: 1768 raise ValueError("Failed to get node list from cluster") 1769 me = this_node() 1770 if me in node_list: 1771 node_list.remove(me) 1772 return node_list 1773 1774 1775def service_info(name): 1776 p = is_program('systemctl') 1777 if p: 1778 rc, outp = get_stdout([p, 'show', 1779 '-p', 'UnitFileState', 1780 '-p', 'ActiveState', 1781 '-p', 'SubState', 1782 name + '.service'], shell=False) 1783 if rc == 0: 1784 info = [] 1785 for line in outp.split('\n'): 1786 data = line.split('=', 1) 1787 if len(data) == 2: 1788 info.append(data[1].strip()) 1789 return '/'.join(info) 1790 return None 1791 1792 1793def running_on(resource): 1794 "returns list of node names where the given resource is running" 1795 rsc_locate = "crm_resource --resource '%s' --locate" 1796 rc, out, err = get_stdout_stderr(rsc_locate % (resource)) 1797 if rc != 0: 1798 return [] 1799 nodes = [] 1800 head = "resource %s is running on: " % (resource) 1801 for line in out.split('\n'): 1802 if line.strip().startswith(head): 1803 w = line[len(head):].split() 1804 if w: 1805 nodes.append(w[0]) 1806 common_debug("%s running on: %s" % (resource, nodes)) 1807 return nodes 1808 1809 1810# This RE matches nvpair values that can 1811# be left unquoted 1812_NOQUOTES_RE = re.compile(r'^[\w\.-]+$') 1813 1814 1815def noquotes(v): 1816 return _NOQUOTES_RE.match(v) is not None 1817 1818 1819def unquote(s): 1820 """ 1821 Reverse shell-quoting a string, so the string '"a b c"' 1822 becomes 'a b c' 1823 """ 1824 sp = shlex.split(s) 1825 if sp: 1826 return sp[0] 1827 return "" 1828 1829 1830def parse_sysconfig(sysconfig_file): 1831 """ 1832 Reads a sysconfig file into a dict 1833 """ 1834 ret = {} 1835 if os.path.isfile(sysconfig_file): 1836 for line in open(sysconfig_file).readlines(): 1837 if line.lstrip().startswith('#'): 1838 continue 1839 try: 1840 key, val = line.split("=", 1) 1841 ret[key] = unquote(val) 1842 except ValueError: 1843 pass 1844 return ret 1845 1846 1847def sysconfig_set(sysconfig_file, **values): 1848 """ 1849 Set the values in the sysconfig file, updating the variables 1850 if they exist already, appending them if not. 1851 """ 1852 outp = "" 1853 if os.path.isfile(sysconfig_file): 1854 for line in open(sysconfig_file).readlines(): 1855 if line.lstrip().startswith('#'): 1856 outp += line 1857 else: 1858 matched = False 1859 try: 1860 key, _ = line.split("=", 1) 1861 for k, v in values.items(): 1862 if k == key: 1863 matched = True 1864 outp += '%s=%s\n' % (k, doublequote(v)) 1865 del values[k] 1866 break 1867 if not matched: 1868 outp += line 1869 except ValueError: 1870 outp += line 1871 1872 for k, v in values.items(): 1873 outp += '%s=%s\n' % (k, doublequote(v)) 1874 str2file(outp, sysconfig_file) 1875 1876 1877def remote_diff_slurp(nodes, filename): 1878 try: 1879 import parallax 1880 except ImportError: 1881 raise ValueError("Parallax is required to diff") 1882 from . import tmpfiles 1883 1884 tmpdir = tmpfiles.create_dir() 1885 opts = parallax.Options() 1886 opts.localdir = tmpdir 1887 dst = os.path.basename(filename) 1888 return list(parallax.slurp(nodes, filename, dst, opts).items()) 1889 1890 1891def remote_diff_this(local_path, nodes, this_node): 1892 try: 1893 import parallax 1894 except ImportError: 1895 raise ValueError("Parallax is required to diff") 1896 1897 by_host = remote_diff_slurp(nodes, local_path) 1898 for host, result in by_host: 1899 if isinstance(result, parallax.Error): 1900 raise ValueError("Failed on %s: %s" % (host, str(result))) 1901 _, _, _, path = result 1902 _, s = get_stdout("diff -U 0 -d -b --label %s --label %s %s %s" % 1903 (host, this_node, path, local_path)) 1904 page_string(s) 1905 1906 1907def remote_diff(local_path, nodes): 1908 try: 1909 import parallax 1910 except ImportError: 1911 raise ValueError("parallax is required to diff") 1912 1913 by_host = remote_diff_slurp(nodes, local_path) 1914 for host, result in by_host: 1915 if isinstance(result, parallax.Error): 1916 raise ValueError("Failed on %s: %s" % (host, str(result))) 1917 h1, r1 = by_host[0] 1918 h2, r2 = by_host[1] 1919 _, s = get_stdout("diff -U 0 -d -b --label %s --label %s %s %s" % 1920 (h1, h2, r1[3], r2[3])) 1921 page_string(s) 1922 1923 1924def remote_checksum(local_path, nodes, this_node): 1925 try: 1926 import parallax 1927 except ImportError: 1928 raise ValueError("Parallax is required to diff") 1929 import hashlib 1930 1931 by_host = remote_diff_slurp(nodes, local_path) 1932 for host, result in by_host: 1933 if isinstance(result, parallax.Error): 1934 raise ValueError(str(result)) 1935 1936 print("%-16s SHA1 checksum of %s" % ('Host', local_path)) 1937 if this_node not in nodes: 1938 print("%-16s: %s" % (this_node, hashlib.sha1(open(local_path).read()).hexdigest())) 1939 for host, result in by_host: 1940 _, _, _, path = result 1941 print("%-16s: %s" % (host, hashlib.sha1(open(path).read()).hexdigest())) 1942 1943 1944def cluster_copy_file(local_path, nodes=None): 1945 """ 1946 Copies given file to all other cluster nodes. 1947 """ 1948 try: 1949 import parallax 1950 except ImportError: 1951 raise ValueError("parallax is required to copy cluster files") 1952 if not nodes: 1953 nodes = list_cluster_nodes() 1954 nodes.remove(this_node()) 1955 opts = parallax.Options() 1956 opts.timeout = 60 1957 opts.ssh_options += ['ControlPersist=no'] 1958 ok = True 1959 for host, result in parallax.copy(nodes, 1960 local_path, 1961 local_path, opts).items(): 1962 if isinstance(result, parallax.Error): 1963 err_buf.error("Failed to push %s to %s: %s" % (local_path, host, result)) 1964 ok = False 1965 else: 1966 err_buf.ok(host) 1967 return ok 1968 1969 1970# a set of fnmatch patterns to match attributes whose values 1971# should be obscured as a sequence of **** when printed 1972_obscured_nvpairs = [] 1973 1974 1975def obscured(key, value): 1976 if key is not None and value is not None: 1977 for o in _obscured_nvpairs: 1978 if fnmatch.fnmatch(key, o): 1979 return '*' * 6 1980 return value 1981 1982 1983@contextmanager 1984def obscure(obscure_list): 1985 global _obscured_nvpairs 1986 prev = _obscured_nvpairs 1987 _obscured_nvpairs = obscure_list 1988 try: 1989 yield 1990 finally: 1991 _obscured_nvpairs = prev 1992 1993 1994def gen_nodeid_from_ipv6(addr): 1995 return int(ipaddress.ip_address(addr)) % 1000000000 1996 1997 1998# Set by detect_cloud() 1999# to avoid multiple requests 2000_ip_for_cloud = None 2001 2002 2003def _cloud_metadata_request(uri, headers={}): 2004 try: 2005 import urllib2 as urllib 2006 except ImportError: 2007 import urllib.request as urllib 2008 req = urllib.Request(uri) 2009 for header, value in headers.items(): 2010 req.add_header(header, value) 2011 try: 2012 resp = urllib.urlopen(req, timeout=5) 2013 content = resp.read() 2014 if type(content) != str: 2015 return content.decode('utf-8').strip() 2016 return content.strip() 2017 except urllib.URLError: 2018 return None 2019 2020 2021@memoize 2022def detect_cloud(): 2023 """ 2024 Tries to determine which (if any) cloud environment 2025 the cluster is running on. 2026 2027 This is mainly done using dmidecode. 2028 2029 If the host cannot be determined, this function 2030 returns None. Otherwise, it returns a string 2031 identifying the platform. 2032 2033 These are the currently known platforms: 2034 2035 * amazon-web-services 2036 * microsoft-azure 2037 * google-cloud-platform 2038 2039 """ 2040 global _ip_for_cloud 2041 2042 if not is_program("dmidecode"): 2043 return None 2044 rc, system_version = get_stdout("dmidecode -s system-version") 2045 if re.search(r".*amazon.*", system_version) is not None: 2046 return "amazon-web-services" 2047 if rc != 0: 2048 return None 2049 rc, system_manufacturer = get_stdout("dmidecode -s system-manufacturer") 2050 if rc == 0 and "microsoft corporation" in system_manufacturer.lower(): 2051 # To detect azure we also need to make an API request 2052 result = _cloud_metadata_request( 2053 "http://169.254.169.254/metadata/instance/network/interface/0/ipv4/ipAddress/0/privateIpAddress?api-version=2017-08-01&format=text", 2054 headers={"Metadata": "true"}) 2055 if result: 2056 _ip_for_cloud = result 2057 return "microsoft-azure" 2058 rc, bios_vendor = get_stdout("dmidecode -s bios-vendor") 2059 if rc == 0 and "Google" in bios_vendor: 2060 # To detect GCP we also need to make an API request 2061 result = _cloud_metadata_request( 2062 "http://metadata.google.internal/computeMetadata/v1/instance/network-interfaces/0/ip", 2063 headers={"Metadata-Flavor": "Google"}) 2064 if result: 2065 _ip_for_cloud = result 2066 return "google-cloud-platform" 2067 return None 2068 2069 2070def debug_timestamp(): 2071 return datetime.datetime.now().strftime('%Y/%m/%d %H:%M:%S') 2072 2073 2074def get_member_iplist(): 2075 rc, out, err= get_stdout_stderr("corosync-cmapctl -b runtime.totem.pg.mrp.srp.members") 2076 if rc != 0: 2077 common_debug(err) 2078 return None 2079 2080 ip_list = [] 2081 for line in out.split('\n'): 2082 match = re.search(r'ip\((.*?)\)', line) 2083 if match: 2084 ip_list.append(match.group(1)) 2085 return ip_list 2086 2087 2088def get_iplist_corosync_using(): 2089 """ 2090 Get ip list used by corosync 2091 """ 2092 rc, out, err = get_stdout_stderr("corosync-cfgtool -s") 2093 if rc != 0: 2094 raise ValueError(err) 2095 return re.findall(r'id\s*=\s*(.*)', out) 2096 2097 2098def check_ssh_passwd_need(host, user="root"): 2099 """ 2100 Check whether access to host need password 2101 """ 2102 ssh_options = "-o StrictHostKeyChecking=no -o EscapeChar=none -o ConnectTimeout=15" 2103 ssh_cmd = "ssh {} -T -o Batchmode=yes {} true".format(ssh_options, host) 2104 ssh_cmd = add_su(ssh_cmd, user) 2105 rc, _, _ = get_stdout_stderr(ssh_cmd) 2106 return rc != 0 2107 2108 2109def check_port_open(ip, port): 2110 import socket 2111 2112 family = socket.AF_INET6 if IP.is_ipv6(ip) else socket.AF_INET 2113 with closing(socket.socket(family, socket.SOCK_STREAM)) as sock: 2114 if sock.connect_ex((ip, port)) == 0: 2115 return True 2116 else: 2117 return False 2118 2119 2120def valid_port(port): 2121 return int(port) >= 1024 and int(port) <= 65535 2122 2123 2124def is_qdevice_configured(): 2125 from . import corosync 2126 return corosync.get_value("quorum.device.model") == "net" 2127 2128 2129def is_qdevice_tls_on(): 2130 from . import corosync 2131 return corosync.get_value("quorum.device.net.tls") == "on" 2132 2133 2134def get_nodeinfo_from_cmaptool(): 2135 nodeid_ip_dict = {} 2136 rc, out = get_stdout("corosync-cmapctl -b runtime.totem.pg.mrp.srp.members") 2137 if rc != 0: 2138 return nodeid_ip_dict 2139 2140 for line in out.split('\n'): 2141 match = re.search(r'members\.(.*)\.ip', line) 2142 if match: 2143 node_id = match.group(1) 2144 iplist = re.findall(r'[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}', line) 2145 nodeid_ip_dict[node_id] = iplist 2146 return nodeid_ip_dict 2147 2148 2149def get_iplist_from_name(name): 2150 """ 2151 Given node host name, return this host's ip list in corosync cmap 2152 """ 2153 ip_list = [] 2154 nodeid = get_nodeid_from_name(name) 2155 if not nodeid: 2156 return ip_list 2157 nodeinfo = {} 2158 nodeinfo = get_nodeinfo_from_cmaptool() 2159 if not nodeinfo: 2160 return ip_list 2161 return nodeinfo[nodeid] 2162 2163 2164def valid_nodeid(nodeid): 2165 from . import bootstrap 2166 if not service_is_active('corosync.service'): 2167 return False 2168 2169 for _id, _ in get_nodeinfo_from_cmaptool().items(): 2170 if _id == nodeid: 2171 return True 2172 return False 2173 2174 2175def get_nodeid_from_name(name): 2176 rc, out = get_stdout('crm_node -l') 2177 if rc != 0: 2178 return None 2179 res = re.search(r'^([0-9]+) {} '.format(name), out, re.M) 2180 if res: 2181 return res.group(1) 2182 else: 2183 return None 2184 2185 2186def check_space_option_value(options): 2187 if not isinstance(options, argparse.Namespace): 2188 raise ValueError("Expected type of \"options\" is \"argparse.Namespace\", not \"{}\"".format(type(options))) 2189 2190 for opt in vars(options): 2191 value = getattr(options, opt) 2192 if isinstance(value, str) and len(value.strip()) == 0: 2193 raise ValueError("Space value not allowed for dest \"{}\"".format(opt)) 2194 2195 2196def interface_choice(): 2197 _, out = get_stdout("ip a") 2198 # should consider interface format like "ethx@xxx" 2199 interface_list = re.findall(r'(?:[0-9]+:) (.*?)(?=: |@.*?: )', out) 2200 return [nic for nic in interface_list if nic != "lo"] 2201 2202 2203class IP(object): 2204 """ 2205 Class to get some properties of IP address 2206 """ 2207 2208 def __init__(self, addr): 2209 """ 2210 Init function 2211 """ 2212 self.addr = addr 2213 2214 @property 2215 def ip_address(self): 2216 """ 2217 Create ipaddress instance 2218 """ 2219 return ipaddress.ip_address(self.addr) 2220 2221 @property 2222 def version(self): 2223 """ 2224 Get IP address version 2225 """ 2226 return self.ip_address.version 2227 2228 @classmethod 2229 def is_mcast(cls, addr): 2230 """ 2231 Check whether the address is multicast address 2232 """ 2233 cls_inst = cls(addr) 2234 return cls_inst.ip_address.is_multicast 2235 2236 @classmethod 2237 def is_ipv6(cls, addr): 2238 """ 2239 Check whether the address is IPV6 address 2240 """ 2241 return cls(addr).version == 6 2242 2243 @classmethod 2244 def is_valid_ip(cls, addr): 2245 """ 2246 Check whether the address is valid IP address 2247 """ 2248 cls_inst = cls(addr) 2249 try: 2250 cls_inst.ip_address 2251 except ValueError: 2252 return False 2253 else: 2254 return True 2255 2256 @property 2257 def is_loopback(self): 2258 """ 2259 Check whether the address is loopback address 2260 """ 2261 return self.ip_address.is_loopback 2262 2263 @property 2264 def is_link_local(self): 2265 """ 2266 Check whether the address is link-local address 2267 """ 2268 return self.ip_address.is_link_local 2269 2270 2271class Interface(IP): 2272 """ 2273 Class to get information from one interface 2274 """ 2275 2276 def __init__(self, ip_with_mask): 2277 """ 2278 Init function 2279 """ 2280 self.ip, self.mask = ip_with_mask.split('/') 2281 super(__class__, self).__init__(self.ip) 2282 2283 @property 2284 def ip_with_mask(self): 2285 """ 2286 Get ip with netmask 2287 """ 2288 return '{}/{}'.format(self.ip, self.mask) 2289 2290 @property 2291 def ip_interface(self): 2292 """ 2293 Create ip_interface instance 2294 """ 2295 return ipaddress.ip_interface(self.ip_with_mask) 2296 2297 @property 2298 def network(self): 2299 """ 2300 Get network address 2301 """ 2302 return str(self.ip_interface.network.network_address) 2303 2304 def ip_in_network(self, addr): 2305 """ 2306 Check whether the addr in the network 2307 """ 2308 return IP(addr).ip_address in self.ip_interface.network 2309 2310 2311class InterfacesInfo(object): 2312 """ 2313 Class to collect interfaces information on local node 2314 """ 2315 2316 def __init__(self, ipv6=False, second_heartbeat=False, custom_nic_list=[]): 2317 """ 2318 Init function 2319 2320 On init process, 2321 "ipv6" is provided by -I option 2322 "second_heartbeat" is provided by -M option 2323 "custom_nic_list" is provided by -i option 2324 """ 2325 self.ip_version = 6 if ipv6 else 4 2326 self.second_heartbeat = second_heartbeat 2327 self._default_nic_list = custom_nic_list 2328 self._nic_info_dict = {} 2329 2330 def get_interfaces_info(self): 2331 """ 2332 Try to get interfaces info dictionary via "ip" command 2333 2334 IMPORTANT: This is the method that populates the data, should always be called after initialize 2335 """ 2336 cmd = "ip -{} -o addr show".format(self.ip_version) 2337 rc, out, err = get_stdout_stderr(cmd) 2338 if rc != 0: 2339 raise ValueError(err) 2340 2341 # format on each line will like: 2342 # 2: enp1s0 inet 192.168.122.241/24 brd 192.168.122.255 scope global enp1s0\ valid_lft forever preferred_lft forever 2343 for line in out.splitlines(): 2344 _, nic, _, ip_with_mask, *_ = line.split() 2345 # maybe from tun interface 2346 if not '/' in ip_with_mask: 2347 continue 2348 interface_inst = Interface(ip_with_mask) 2349 if interface_inst.is_loopback: 2350 continue 2351 # one nic might configured multi IP addresses 2352 if nic not in self._nic_info_dict: 2353 self._nic_info_dict[nic] = [] 2354 self._nic_info_dict[nic].append(interface_inst) 2355 2356 if not self._nic_info_dict: 2357 raise ValueError("No address configured") 2358 if self.second_heartbeat and len(self._nic_info_dict) == 1: 2359 raise ValueError("Cannot configure second heartbeat, since only one address is available") 2360 2361 @property 2362 def nic_list(self): 2363 """ 2364 Get interfaces name list 2365 """ 2366 return list(self._nic_info_dict.keys()) 2367 2368 @property 2369 def interface_list(self): 2370 """ 2371 Get instance list of class Interface 2372 """ 2373 _interface_list = [] 2374 for interface in self._nic_info_dict.values(): 2375 _interface_list.extend(interface) 2376 return _interface_list 2377 2378 @property 2379 def ip_list(self): 2380 """ 2381 Get IP address list 2382 """ 2383 return [interface.ip for interface in self.interface_list] 2384 2385 @classmethod 2386 def get_local_ip_list(cls, is_ipv6): 2387 """ 2388 Get IP address list 2389 """ 2390 cls_inst = cls(is_ipv6) 2391 cls_inst.get_interfaces_info() 2392 return cls_inst.ip_list 2393 2394 @classmethod 2395 def ip_in_local(cls, addr): 2396 """ 2397 Check whether given address was in one of local address 2398 """ 2399 cls_inst = cls(IP.is_ipv6(addr)) 2400 cls_inst.get_interfaces_info() 2401 return addr in cls_inst.ip_list 2402 2403 @property 2404 def network_list(self): 2405 """ 2406 Get network list 2407 """ 2408 return list(set([interface.network for interface in self.interface_list])) 2409 2410 def _nic_first_ip(self, nic): 2411 """ 2412 Get the first IP of specific nic 2413 """ 2414 return self._nic_info_dict[nic][0].ip 2415 2416 def get_default_nic_list_from_route(self): 2417 """ 2418 Get default nic list from route 2419 """ 2420 if self._default_nic_list: 2421 return self._default_nic_list 2422 2423 #TODO what if user only has ipv6 route? 2424 cmd = "ip -o route show" 2425 rc, out, err = get_stdout_stderr(cmd) 2426 if rc != 0: 2427 raise ValueError(err) 2428 res = re.search(r'^default via .* dev (.*?) ', out) 2429 if res: 2430 self._default_nic_list = [res.group(1)] 2431 else: 2432 if not self.nic_list: 2433 self.get_interfaces_info() 2434 common_warn("No default route configured. Using the first found nic") 2435 self._default_nic_list = [self.nic_list[0]] 2436 return self._default_nic_list 2437 2438 def get_default_ip_list(self): 2439 """ 2440 Get default IP list will be used by corosync 2441 """ 2442 if not self._default_nic_list: 2443 self.get_default_nic_list_from_route() 2444 if not self.nic_list: 2445 self.get_interfaces_info() 2446 2447 _ip_list = [] 2448 for nic in self._default_nic_list: 2449 # in case given interface not exist 2450 if nic not in self.nic_list: 2451 raise ValueError("Failed to detect IP address for {}".format(nic)) 2452 _ip_list.append(self._nic_first_ip(nic)) 2453 # in case -M specified but given one interface via -i 2454 if self.second_heartbeat and len(self._default_nic_list) == 1: 2455 for nic in self.nic_list: 2456 if nic not in self._default_nic_list: 2457 _ip_list.append(self._nic_first_ip(nic)) 2458 break 2459 return _ip_list 2460 2461 @classmethod 2462 def ip_in_network(cls, addr): 2463 """ 2464 Check whether given address was in one of local networks 2465 """ 2466 cls_inst = cls(IP.is_ipv6(addr)) 2467 cls_inst.get_interfaces_info() 2468 for interface_inst in cls_inst.interface_list: 2469 if interface_inst.ip_in_network(addr): 2470 return True 2471 return False 2472 2473 2474def check_file_content_included(source_file, target_file): 2475 """ 2476 Check whether target_file includes contents of source_file 2477 """ 2478 if not os.path.exists(source_file): 2479 raise ValueError("File {} not exist".format(source_file)) 2480 if not os.path.exists(target_file): 2481 return False 2482 2483 with open(target_file, 'r') as target_fd: 2484 target_data = target_fd.read() 2485 with open(source_file, 'r') as source_fd: 2486 source_data = source_fd.read() 2487 return source_data in target_data 2488 2489 2490class ServiceManager(object): 2491 """ 2492 Class to manage systemctl services 2493 """ 2494 ACTION_MAP = { 2495 "enable": "enable", 2496 "disable": "disable", 2497 "start": "start", 2498 "stop": "stop", 2499 "is_enabled": "is-enabled", 2500 "is_active": "is-active", 2501 "is_available": "list-unit-files" 2502 } 2503 2504 def __init__(self, service_name, remote_addr=None): 2505 """ 2506 Init function 2507 """ 2508 self.service_name = service_name 2509 self.remote_addr = remote_addr 2510 2511 def _do_action(self, action_type): 2512 """ 2513 Actual do actions to manage service 2514 """ 2515 if action_type not in self.ACTION_MAP.values(): 2516 raise ValueError("status_type should be {}".format('/'.join(list(self.ACTION_MAP.values())))) 2517 2518 cmd = "systemctl {} {}".format(action_type, self.service_name) 2519 if self.remote_addr: 2520 prompt_msg = "Run \"{}\" on {}".format(cmd, self.remote_addr) 2521 rc, output, err = run_cmd_on_remote(cmd, self.remote_addr, prompt_msg) 2522 else: 2523 rc, output, err = get_stdout_stderr(cmd) 2524 if rc != 0 and err: 2525 raise ValueError("Run \"{}\" error: {}".format(cmd, err)) 2526 return rc == 0, output 2527 2528 @property 2529 def is_available(self): 2530 return self.service_name in self._do_action(self.ACTION_MAP["is_available"])[1] 2531 2532 @property 2533 def is_enabled(self): 2534 return self._do_action(self.ACTION_MAP["is_enabled"])[0] 2535 2536 @property 2537 def is_active(self): 2538 return self._do_action(self.ACTION_MAP["is_active"])[0] 2539 2540 def start(self): 2541 self._do_action(self.ACTION_MAP["start"]) 2542 2543 def stop(self): 2544 self._do_action(self.ACTION_MAP["stop"]) 2545 2546 def enable(self): 2547 self._do_action(self.ACTION_MAP["enable"]) 2548 2549 def disable(self): 2550 self._do_action(self.ACTION_MAP["disable"]) 2551 2552 @classmethod 2553 def service_is_available(cls, name, remote_addr=None): 2554 """ 2555 Check whether service is available 2556 """ 2557 inst = cls(name, remote_addr) 2558 return inst.is_available 2559 2560 @classmethod 2561 def service_is_enabled(cls, name, remote_addr=None): 2562 """ 2563 Check whether service is enabled 2564 """ 2565 inst = cls(name, remote_addr) 2566 return inst.is_enabled 2567 2568 @classmethod 2569 def service_is_active(cls, name, remote_addr=None): 2570 """ 2571 Check whether service is active 2572 """ 2573 inst = cls(name, remote_addr) 2574 return inst.is_active 2575 2576 @classmethod 2577 def start_service(cls, name, enable=False, remote_addr=None): 2578 """ 2579 Start service 2580 """ 2581 inst = cls(name, remote_addr) 2582 if enable: 2583 inst.enable() 2584 inst.start() 2585 2586 @classmethod 2587 def stop_service(cls, name, disable=False, remote_addr=None): 2588 """ 2589 Stop service 2590 """ 2591 inst = cls(name, remote_addr) 2592 if disable: 2593 inst.disable() 2594 inst.stop() 2595 2596 @classmethod 2597 def enable_service(cls, name, remote_addr=None): 2598 """ 2599 Enable service 2600 """ 2601 inst = cls(name, remote_addr) 2602 if inst.is_available and not inst.is_enabled: 2603 inst.enable() 2604 2605 @classmethod 2606 def disable_service(cls, name, remote_addr=None): 2607 """ 2608 Disable service 2609 """ 2610 inst = cls(name, remote_addr) 2611 if inst.is_available and inst.is_enabled: 2612 inst.disable() 2613 2614 2615service_is_available = ServiceManager.service_is_available 2616service_is_enabled = ServiceManager.service_is_enabled 2617service_is_active = ServiceManager.service_is_active 2618start_service = ServiceManager.start_service 2619stop_service = ServiceManager.stop_service 2620enable_service = ServiceManager.enable_service 2621disable_service = ServiceManager.disable_service 2622 2623 2624def package_is_installed(pkg, remote_addr=None): 2625 """ 2626 Check if package is installed 2627 """ 2628 cmd = "rpm -q --quiet {}".format(pkg) 2629 if remote_addr: 2630 # check on remote 2631 prompt_msg = "Check whether {} is installed on {}".format(pkg, remote_addr) 2632 rc, _, _ = run_cmd_on_remote(cmd, remote_addr, prompt_msg) 2633 else: 2634 # check on local 2635 rc, _ = get_stdout(cmd) 2636 return rc == 0 2637 2638 2639def ping_node(node): 2640 """ 2641 Check if the remote node is reachable 2642 """ 2643 rc, _, err = get_stdout_stderr("ping -c 1 {}".format(node)) 2644 if rc != 0: 2645 raise ValueError("host \"{}\" is unreachable: {}".format(node, err)) 2646 2647 2648def is_quorate(expected_votes, actual_votes): 2649 """ 2650 Given expected votes and actual votes, calculate if is quorated 2651 """ 2652 return int(actual_votes)/int(expected_votes) > 0.5 2653 2654 2655def get_stdout_or_raise_error(cmd, remote=None, success_val=0): 2656 """ 2657 Common function to get stdout from cmd or raise exception 2658 """ 2659 if remote: 2660 cmd = "ssh {} root@{} \"{}\"".format(SSH_OPTION, remote, cmd) 2661 rc, out, err = get_stdout_stderr(cmd) 2662 if rc != success_val: 2663 raise ValueError("Failed to run \"{}\": {}".format(cmd, err)) 2664 return out 2665 2666 2667def get_quorum_votes_dict(remote=None): 2668 """ 2669 Return a dictionary which contain expect votes and total votes 2670 """ 2671 out = get_stdout_or_raise_error("corosync-quorumtool -s", remote=remote) 2672 return dict(re.findall("(Expected|Total) votes:\s+(\d+)", out)) 2673 2674 2675def has_resource_running(ra_type=None): 2676 """ 2677 Check if any RA is running 2678 """ 2679 out = get_stdout_or_raise_error("crm_mon -1") 2680 if ra_type: 2681 return re.search("{}.*Started".format(ra_type), out) is not None 2682 else: 2683 return re.search("No active resources", out) is None 2684 2685 2686def has_resource_configured(ra_type, peer=None): 2687 """ 2688 Check if the RA configured 2689 """ 2690 out = get_stdout_or_raise_error("crm_mon -1rR", remote=peer) 2691 return re.search(ra_type, out) is not None 2692 2693 2694def check_all_nodes_reachable(): 2695 """ 2696 Check if all cluster nodes are reachable 2697 """ 2698 out = get_stdout_or_raise_error("crm_node -l") 2699 for node in re.findall("\d+ (.*) \w+", out): 2700 ping_node(node) 2701 2702 2703def re_split_string(reg, string): 2704 """ 2705 Split a string by a regrex, filter out empty items 2706 """ 2707 return [x for x in re.split(reg, string) if x] 2708 2709 2710def is_block_device(dev): 2711 """ 2712 Check if dev is a block device 2713 """ 2714 try: 2715 rc = S_ISBLK(os.stat(dev).st_mode) 2716 except OSError: 2717 return False 2718 return rc 2719 2720 2721def has_stonith_running(): 2722 """ 2723 Check if any stonith device registered 2724 """ 2725 out = get_stdout_or_raise_error("stonith_admin -L") 2726 return re.search("[1-9]+ fence device[s]* found", out) is not None 2727 2728 2729def parse_append_action_argument(input_list, parse_re="[; ]"): 2730 """ 2731 Parse append action argument into a list, like: 2732 -s "/dev/sdb1;/dev/sdb2" 2733 -s /dev/sdb1 -s /dev/sbd2 2734 2735 Both return ["/dev/sdb1", "/dev/sdb2"] 2736 """ 2737 result_list = [] 2738 for item in input_list: 2739 result_list += re_split_string(parse_re, item) 2740 return result_list 2741 2742 2743def has_disk_mounted(dev): 2744 """ 2745 Check if device already mounted 2746 """ 2747 out = get_stdout_or_raise_error("mount") 2748 return re.search("\n{} on ".format(dev), out) is not None 2749 2750 2751def has_mount_point_used(directory): 2752 """ 2753 Check if mount directory already mounted 2754 """ 2755 out = get_stdout_or_raise_error("mount") 2756 return re.search(" on {}".format(directory), out) is not None 2757 2758 2759def all_exist_id(): 2760 """ 2761 Get current exist id list 2762 """ 2763 from .cibconfig import cib_factory 2764 cib_factory.refresh() 2765 return cib_factory.id_list() 2766 2767 2768def randomword(length=6): 2769 """ 2770 Generate random word 2771 """ 2772 letters = string.ascii_lowercase 2773 return ''.join(random.choice(letters) for i in range(length)) 2774 2775 2776def gen_unused_id(exist_id_list, prefix="", length=6): 2777 """ 2778 Generate unused id 2779 """ 2780 unused_id = prefix or randomword(length) 2781 while unused_id in exist_id_list: 2782 unused_id = re.sub("$", "-{}".format(randomword(length)), unused_id) 2783 return unused_id 2784 2785 2786def get_all_vg_name(): 2787 """ 2788 Get all available VGs 2789 """ 2790 out = get_stdout_or_raise_error("vgdisplay") 2791 return re.findall("VG Name\s+(.*)", out) 2792 2793 2794def get_pe_number(vg_id): 2795 """ 2796 Get pe number 2797 """ 2798 output = get_stdout_or_raise_error("vgdisplay {}".format(vg_id)) 2799 res = re.search("Total PE\s+(\d+)", output) 2800 if not res: 2801 raise ValueError("Cannot find PE on VG({})".format(vg_id)) 2802 return int(res.group(1)) 2803 2804 2805def has_dev_partitioned(dev, peer=None): 2806 """ 2807 Check if device has partitions 2808 """ 2809 return len(get_dev_info(dev, "NAME", peer=peer).splitlines()) > 1 2810 2811 2812def get_dev_uuid(dev, peer=None): 2813 """ 2814 Get UUID of device on local or peer node 2815 """ 2816 out = get_dev_info(dev, "UUID", peer=peer).splitlines() 2817 return out[0] if out else get_dev_uuid_2(dev, peer) 2818 2819 2820def get_dev_uuid_2(dev, peer=None): 2821 """ 2822 Get UUID of device using blkid 2823 """ 2824 out = get_stdout_or_raise_error("blkid {}".format(dev), remote=peer) 2825 res = re.search("UUID=\"(.*?)\"", out) 2826 return res.group(1) if res else None 2827 2828 2829def get_dev_fs_type(dev, peer=None): 2830 """ 2831 Get filesystem type of device 2832 """ 2833 return get_dev_info(dev, "FSTYPE", peer=peer) 2834 2835 2836def get_dev_info(dev, *_type, peer=None): 2837 """ 2838 Get device info using lsblk 2839 """ 2840 cmd = "lsblk -fno {} {}".format(','.join(_type), dev) 2841 return get_stdout_or_raise_error(cmd, remote=peer) 2842 2843 2844def is_dev_used_for_lvm(dev, peer=None): 2845 """ 2846 Check if device is LV 2847 """ 2848 return get_dev_info(dev, "TYPE", peer=peer) == "lvm" 2849 2850 2851def compare_uuid_with_peer_dev(dev_list, peer): 2852 """ 2853 Check if device UUID is the same with peer's device 2854 """ 2855 for dev in dev_list: 2856 local_uuid = get_dev_uuid(dev) 2857 if not local_uuid: 2858 raise ValueError("Cannot find UUID for {} on local".format(dev)) 2859 peer_uuid = get_dev_uuid(dev, peer) 2860 if not peer_uuid: 2861 raise ValueError("Cannot find UUID for {} on {}".format(dev, peer)) 2862 if local_uuid != peer_uuid: 2863 raise ValueError("UUID of {} not same with peer {}".format(dev, peer)) 2864 2865 2866def append_res_to_group(group_id, res_id): 2867 """ 2868 Append resource to exist group 2869 """ 2870 cmd = "crm configure modgroup {} add {}".format(group_id, res_id) 2871 get_stdout_or_raise_error(cmd) 2872 2873 2874def get_qdevice_sync_timeout(): 2875 """ 2876 Get qdevice sync_timeout 2877 """ 2878 out = get_stdout_or_raise_error("crm corosync status qdevice") 2879 res = re.search("Sync HB interval:\s+(\d+)ms", out) 2880 if not res: 2881 raise ValueError("Cannot find qdevice sync timeout") 2882 return int(int(res.group(1))/1000) 2883# vim:ts=4:sw=4:et: 2884