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