xref: /openbmc/openbmc-test-automation/lib/gen_misc.py (revision b5856d0d71461b3ed4fbf6a7253208bd7d5e270b)
1#!/usr/bin/env python
2
3r"""
4This module provides many valuable functions such as my_parm_file.
5"""
6
7# sys and os are needed to get the program dir path and program name.
8import sys
9import errno
10import os
11import collections
12import json
13try:
14    import ConfigParser
15except ImportError:
16    import configparser
17try:
18    import StringIO
19except ImportError:
20    import io
21import re
22import socket
23import tempfile
24try:
25    import psutil
26    psutil_imported = True
27except ImportError:
28    psutil_imported = False
29
30import gen_print as gp
31import gen_cmd as gc
32
33robot_env = gp.robot_env
34if robot_env:
35    from robot.libraries.BuiltIn import BuiltIn
36    from robot.utils import DotDict
37
38
39def add_trailing_slash(dir_path):
40    r"""
41    Add a trailing slash to the directory path if it doesn't already have one
42    and return it.
43
44    Description of arguments:
45    dir_path                        A directory path.
46    """
47
48    return os.path.normpath(dir_path) + os.path.sep
49
50
51def which(file_path):
52    r"""
53    Find the full path of an executable file and return it.
54
55    The PATH environment variable dictates the results of this function.
56
57    Description of arguments:
58    file_path                       The relative file path (e.g. "my_file" or
59                                    "lib/my_file").
60    """
61
62    shell_rc, out_buf = gc.cmd_fnc_u("which " + file_path, quiet=1,
63                                     print_output=0, show_err=0)
64    if shell_rc != 0:
65        error_message = "Failed to find complete path for file \"" +\
66                        file_path + "\".\n"
67        error_message += gp.sprint_var(shell_rc, 1)
68        error_message += out_buf
69        if robot_env:
70            BuiltIn().fail(gp.sprint_error(error_message))
71        else:
72            gp.print_error_report(error_message)
73            return False
74
75    file_path = out_buf.rstrip("\n")
76
77    return file_path
78
79
80def add_path(new_path,
81             path,
82             position=0):
83    r"""
84    Add new_path to path, provided that path doesn't already contain new_path,
85    and return the result.
86
87    Example:
88    If PATH has a value of "/bin/user:/lib/user".  The following code:
89
90    PATH = add_path("/tmp/new_path", PATH)
91
92    will change PATH to "/tmp/new_path:/bin/user:/lib/user".
93
94    Description of argument(s):
95    new_path                        The path to be added.  This function will
96                                    strip the trailing slash.
97    path                            The path value to which the new_path
98                                    should be added.
99    position                        The position in path where the new_path
100                                    should be added.  0 means it should be
101                                    added to the beginning, 1 means add it as
102                                    the 2nd item, etc.  sys.maxsize means it
103                                    should be added to the end.
104    """
105
106    path_list = list(filter(None, path.split(":")))
107    new_path = new_path.rstrip("/")
108    if new_path not in path_list:
109        path_list.insert(int(position), new_path)
110    return ":".join(path_list)
111
112
113def dft(value, default):
114    r"""
115    Return default if value is None.  Otherwise, return value.
116
117    This is really just shorthand as shown below.
118
119    dft(value, default)
120
121    vs
122
123    default if value is None else value
124
125    Description of arguments:
126    value                           The value to be returned.
127    default                         The default value to return if value is
128                                    None.
129    """
130
131    return default if value is None else value
132
133
134def get_mod_global(var_name,
135                   default=None,
136                   mod_name="__main__"):
137    r"""
138    Get module global variable value and return it.
139
140    If we are running in a robot environment, the behavior will default to
141    calling get_variable_value.
142
143    Description of arguments:
144    var_name                        The name of the variable whose value is
145                                    sought.
146    default                         The value to return if the global does not
147                                    exist.
148    mod_name                        The name of the module containing the
149                                    global variable.
150    """
151
152    if robot_env:
153        return BuiltIn().get_variable_value("${" + var_name + "}", default)
154
155    try:
156        module = sys.modules[mod_name]
157    except KeyError:
158        gp.print_error_report("Programmer error - The mod_name passed to"
159                              + " this function is invalid:\n"
160                              + gp.sprint_var(mod_name))
161        raise ValueError('Programmer error.')
162
163    if default is None:
164        return getattr(module, var_name)
165    else:
166        return getattr(module, var_name, default)
167
168
169def global_default(var_value,
170                   default=0):
171    r"""
172    If var_value is not None, return it.  Otherwise, return the global
173    variable of the same name, if it exists.  If not, return default.
174
175    This is meant for use by functions needing help assigning dynamic default
176    values to their parms.  Example:
177
178    def func1(parm1=None):
179
180        parm1 = global_default(parm1, 0)
181
182    Description of arguments:
183    var_value                       The value being evaluated.
184    default                         The value to be returned if var_value is
185                                    None AND the global variable of the same
186                                    name does not exist.
187    """
188
189    var_name = gp.get_arg_name(0, 1, stack_frame_ix=2)
190
191    return dft(var_value, get_mod_global(var_name, 0))
192
193
194def set_mod_global(var_value,
195                   mod_name="__main__",
196                   var_name=None):
197    r"""
198    Set a global variable for a given module.
199
200    Description of arguments:
201    var_value                       The value to set in the variable.
202    mod_name                        The name of the module whose variable is
203                                    to be set.
204    var_name                        The name of the variable to set.  This
205                                    defaults to the name of the variable used
206                                    for var_value when calling this function.
207    """
208
209    try:
210        module = sys.modules[mod_name]
211    except KeyError:
212        gp.print_error_report("Programmer error - The mod_name passed to"
213                              + " this function is invalid:\n"
214                              + gp.sprint_var(mod_name))
215        raise ValueError('Programmer error.')
216
217    if var_name is None:
218        var_name = gp.get_arg_name(None, 1, 2)
219
220    setattr(module, var_name, var_value)
221
222
223def my_parm_file(prop_file_path):
224    r"""
225    Read a properties file, put the keys/values into a dictionary and return
226    the dictionary.
227
228    The properties file must have the following format:
229    var_name<= or :>var_value
230    Comment lines (those beginning with a "#") and blank lines are allowed and
231    will be ignored.  Leading and trailing single or double quotes will be
232    stripped from the value.  E.g.
233    var1="This one"
234    Quotes are stripped so the resulting value for var1 is:
235    This one
236
237    Description of arguments:
238    prop_file_path                  The caller should pass the path to the
239                                    properties file.
240    """
241
242    # ConfigParser expects at least one section header in the file (or you
243    # get ConfigParser.MissingSectionHeaderError).  Properties files don't
244    # need those so I'll write a dummy section header.
245
246    try:
247        string_file = StringIO.StringIO()
248    except NameError:
249        string_file = io.StringIO()
250
251    # Write the dummy section header to the string file.
252    string_file.write('[dummysection]\n')
253    # Write the entire contents of the properties file to the string file.
254    string_file.write(open(prop_file_path).read())
255    # Rewind the string file.
256    string_file.seek(0, os.SEEK_SET)
257
258    # Create the ConfigParser object.
259    try:
260        config_parser = ConfigParser.ConfigParser()
261    except NameError:
262        config_parser = configparser.ConfigParser()
263    # Make the property names case-sensitive.
264    config_parser.optionxform = str
265    # Read the properties from the string file.
266    config_parser.readfp(string_file)
267    # Return the properties as a dictionary.
268    if robot_env:
269        return DotDict(config_parser.items('dummysection'))
270    else:
271        return collections.OrderedDict(config_parser.items('dummysection'))
272
273
274def file_to_list(file_path,
275                 newlines=0,
276                 comments=1,
277                 trim=0):
278    r"""
279    Return the contents of a file as a list.  Each element of the resulting
280    list is one line from the file.
281
282    Description of arguments:
283    file_path                       The path to the file (relative or
284                                    absolute).
285    newlines                        Include newlines from the file in the
286                                    results.
287    comments                        Include comment lines and blank lines in
288                                    the results.  Comment lines are any that
289                                    begin with 0 or more spaces followed by
290                                    the pound sign ("#").
291    trim                            Trim white space from the beginning and
292                                    end of each line.
293    """
294
295    lines = []
296    file = open(file_path)
297    for line in file:
298        if not comments:
299            if re.match(r"[ ]*#|^$", line):
300                continue
301        if not newlines:
302            line = line.rstrip("\n")
303        if trim:
304            line = line.strip()
305        lines.append(line)
306    file.close()
307
308    return lines
309
310
311def file_to_str(*args, **kwargs):
312    r"""
313    Return the contents of a file as a string.
314
315    Description of arguments:
316    See file_to_list defined above for description of arguments.
317    """
318
319    return '\n'.join(file_to_list(*args, **kwargs))
320
321
322def return_path_list():
323    r"""
324    This function will split the PATH environment variable into a PATH_LIST
325    and return it.  Each element in the list will be normalized and have a
326    trailing slash added.
327    """
328
329    PATH_LIST = os.environ['PATH'].split(":")
330    PATH_LIST = [os.path.normpath(path) + os.sep for path in PATH_LIST]
331
332    return PATH_LIST
333
334
335def escape_bash_quotes(buffer):
336    r"""
337    Escape quotes in string and return it.
338
339    The escape style implemented will be for use on the bash command line.
340
341    Example:
342    That's all.
343
344    Result:
345    That'\''s all.
346
347    The result may then be single quoted on a bash command.  Example:
348
349    echo 'That'\''s all.'
350
351    Description of argument(s):
352    buffer                          The string whose quotes are to be escaped.
353    """
354
355    return re.sub("\'", "\'\\\'\'", buffer)
356
357
358def quote_bash_parm(parm):
359    r"""
360    Return the bash command line parm with single quotes if they are needed.
361
362    Description of arguments:
363    parm                            The string to be quoted.
364    """
365
366    # If any of these characters are found in the parm string, then the
367    # string should be quoted.  This list is by no means complete and should
368    # be expanded as needed by the developer of this function.
369    bash_special_chars = set(' $')
370
371    if any((char in bash_special_chars) for char in parm):
372        return "'" + parm + "'"
373
374    return parm
375
376
377def get_host_name_ip(host=None,
378                     short_name=0):
379    r"""
380    Get the host name and the IP address for the given host and return them as
381    a tuple.
382
383    Description of argument(s):
384    host                            The host name or IP address to be obtained.
385    short_name                      Include the short host name in the
386                                    returned tuple, i.e. return host, ip and
387                                    short_host.
388    """
389
390    host = dft(host, socket.gethostname())
391    host_name = socket.getfqdn(host)
392    try:
393        host_ip = socket.gethostbyname(host)
394    except socket.gaierror as my_gaierror:
395        message = "Unable to obtain the host name for the following host:" +\
396                  "\n" + gp.sprint_var(host)
397        gp.print_error_report(message)
398        raise my_gaierror
399
400    if short_name:
401        host_short_name = host_name.split(".")[0]
402        return host_name, host_ip, host_short_name
403    else:
404        return host_name, host_ip
405
406
407def pid_active(pid):
408    r"""
409    Return true if pid represents an active pid and false otherwise.
410
411    Description of argument(s):
412    pid                             The pid whose status is being sought.
413    """
414
415    try:
416        os.kill(int(pid), 0)
417    except OSError as err:
418        if err.errno == errno.ESRCH:
419            # ESRCH == No such process
420            return False
421        elif err.errno == errno.EPERM:
422            # EPERM clearly means there's a process to deny access to
423            return True
424        else:
425            # According to "man 2 kill" possible error values are
426            # (EINVAL, EPERM, ESRCH)
427            raise
428
429    return True
430
431
432def to_signed(number,
433              bit_width=None):
434    r"""
435    Convert number to a signed number and return the result.
436
437    Examples:
438
439    With the following code:
440
441    var1 = 0xfffffffffffffff1
442    print_var(var1)
443    print_var(var1, 1)
444    var1 = to_signed(var1)
445    print_var(var1)
446    print_var(var1, 1)
447
448    The following is written to stdout:
449    var1:  18446744073709551601
450    var1:  0x00000000fffffffffffffff1
451    var1:  -15
452    var1:  0xfffffffffffffff1
453
454    The same code but with var1 set to 0x000000000000007f produces the
455    following:
456    var1:  127
457    var1:  0x000000000000007f
458    var1:  127
459    var1:  0x000000000000007f
460
461    Description of argument(s):
462    number                          The number to be converted.
463    bit_width                       The number of bits that defines a complete
464                                    hex value.  Typically, this would be a
465                                    multiple of 32.
466    """
467
468    if bit_width is None:
469        try:
470            bit_width = gp.bit_length(long(sys.maxsize)) + 1
471        except NameError:
472            bit_width = gp.bit_length(int(sys.maxsize)) + 1
473
474    if number < 0:
475        return number
476    neg_bit_mask = 2**(bit_width - 1)
477    if number & neg_bit_mask:
478        return ((2**bit_width) - number) * -1
479    else:
480        return number
481
482
483def get_child_pids(quiet=1):
484
485    r"""
486    Get and return a list of pids representing all first-generation processes
487    that are the children of the current process.
488
489    Example:
490
491    children = get_child_pids()
492    print_var(children)
493
494    Output:
495    children:
496      children[0]:           9123
497
498    Description of argument(s):
499    quiet                           Display output to stdout detailing how
500                                    this child pids are obtained.
501    """
502
503    if psutil_imported:
504        # If "import psutil" worked, find child pids using psutil.
505        current_process = psutil.Process()
506        return [x.pid for x in current_process.children(recursive=False)]
507    else:
508        # Otherwise, find child pids using shell commands.
509        print_output = not quiet
510
511        ps_cmd_buf = "ps --no-headers --ppid " + str(os.getpid()) +\
512            " -o pid,args"
513        # Route the output of ps to a temporary file for later grepping.
514        # Avoid using " | grep" in the ps command string because it creates
515        # yet another process which is of no interest to the caller.
516        temp = tempfile.NamedTemporaryFile()
517        temp_file_path = temp.name
518        gc.shell_cmd(ps_cmd_buf + " > " + temp_file_path,
519                     print_output=print_output)
520        # Sample contents of the temporary file:
521        # 30703 sleep 2
522        # 30795 /bin/bash -c ps --no-headers --ppid 30672 -o pid,args >
523        # /tmp/tmpqqorWY
524        # Use egrep to exclude the "ps" process itself from the results
525        # collected with the prior shell_cmd invocation.  Only the other
526        # children are of interest to the caller.  Use cut on the grep results
527        # to obtain only the pid column.
528        rc, output = \
529            gc.shell_cmd("egrep -v '" + re.escape(ps_cmd_buf) + "' "
530                         + temp_file_path + " | cut -c1-5",
531                         print_output=print_output)
532        # Split the output buffer by line into a list.  Strip each element of
533        # extra spaces and convert each element to an integer.
534        return map(int, map(str.strip, filter(None, output.split("\n"))))
535
536
537def json_loads_multiple(buffer):
538    r"""
539    Convert the contents of the buffer to a JSON array, run json.loads() on it
540    and return the result.
541
542    The buffer is expected to contain one or more JSON objects.
543
544    Description of argument(s):
545    buffer                          A string containing several JSON objects.
546    """
547
548    # Any line consisting of just "}", which indicates the end of an object,
549    # should have a comma appended.
550    regex = "([\\r\\n])[\\}]([\\r\\n])"
551    buffer = re.sub(regex, "\\1},\\2", buffer, 1)
552    # Remove the comma from after the final object and place the whole buffer
553    # inside square brackets.
554    buffer = "[" + re.sub(",([\r\n])$", "\\1}", buffer, 1) + "]"
555    if gp.robot_env:
556        return json.loads(buffer, object_pairs_hook=DotDict)
557    else:
558        return json.loads(buffer, object_pairs_hook=collections.OrderedDict)
559