1#!/usr/bin/env python
2
3r"""
4Define variable manipulation functions.
5"""
6
7import os
8import re
9
10try:
11    from robot.utils import DotDict
12except ImportError:
13    pass
14
15import collections
16
17import gen_print as gp
18import gen_misc as gm
19import func_args as fa
20
21
22def create_var_dict(*args):
23    r"""
24    Create a dictionary whose keys/values are the arg names/arg values passed
25    to it and return it to the caller.
26
27    Note: The resulting dictionary will be ordered.
28
29    Description of argument(s):
30    *args  An unlimited number of arguments to be processed.
31
32    Example use:
33
34    first_name = 'Steve'
35    last_name = 'Smith'
36    var_dict = create_var_dict(first_name, last_name)
37
38    gp.print_var(var_dict)
39
40    The print-out of the resulting var dictionary is:
41    var_dict:
42      var_dict[first_name]:                           Steve
43      var_dict[last_name]:                            Smith
44    """
45
46    try:
47        result_dict = collections.OrderedDict()
48    except AttributeError:
49        result_dict = DotDict()
50
51    arg_num = 1
52    for arg in args:
53        arg_name = gp.get_arg_name(None, arg_num, stack_frame_ix=2)
54        result_dict[arg_name] = arg
55        arg_num += 1
56
57    return result_dict
58
59
60default_record_delim = ':'
61default_key_val_delim = '.'
62
63
64def join_dict(dict,
65              record_delim=default_record_delim,
66              key_val_delim=default_key_val_delim):
67    r"""
68    Join a dictionary's keys and values into a string and return the string.
69
70    Description of argument(s):
71    dict                            The dictionary whose keys and values are
72                                    to be joined.
73    record_delim                    The delimiter to be used to separate
74                                    dictionary pairs in the resulting string.
75    key_val_delim                   The delimiter to be used to separate keys
76                                    from values in the resulting string.
77
78    Example use:
79
80    gp.print_var(var_dict)
81    str1 = join_dict(var_dict)
82    gp.print_var(str1)
83
84    Program output.
85    var_dict:
86      var_dict[first_name]:                           Steve
87      var_dict[last_name]:                            Smith
88    str1:
89    first_name.Steve:last_name.Smith
90    """
91
92    format_str = '%s' + key_val_delim + '%s'
93    return record_delim.join([format_str % (key, value) for (key, value) in
94                              dict.items()])
95
96
97def split_to_dict(string,
98                  record_delim=default_record_delim,
99                  key_val_delim=default_key_val_delim):
100    r"""
101    Split a string into a dictionary and return it.
102
103    This function is the complement to join_dict.
104
105    Description of argument(s):
106    string                          The string to be split into a dictionary.
107                                    The string must have the proper delimiters
108                                    in it.  A string created by join_dict
109                                    would qualify.
110    record_delim                    The delimiter to be used to separate
111                                    dictionary pairs in the input string.
112    key_val_delim                   The delimiter to be used to separate
113                                    keys/values in the input string.
114
115    Example use:
116
117    gp.print_var(str1)
118    new_dict = split_to_dict(str1)
119    gp.print_var(new_dict)
120
121
122    Program output.
123    str1:
124    first_name.Steve:last_name.Smith
125    new_dict:
126      new_dict[first_name]:                           Steve
127      new_dict[last_name]:                            Smith
128    """
129
130    try:
131        result_dict = collections.OrderedDict()
132    except AttributeError:
133        result_dict = DotDict()
134
135    raw_keys_values = string.split(record_delim)
136    for key_value in raw_keys_values:
137        key_value_list = key_value.split(key_val_delim)
138        try:
139            result_dict[key_value_list[0]] = key_value_list[1]
140        except IndexError:
141            result_dict[key_value_list[0]] = ""
142
143    return result_dict
144
145
146def create_file_path(file_name_dict,
147                     dir_path="/tmp/",
148                     file_suffix=""):
149    r"""
150    Create a file path using the given parameters and return it.
151
152    Description of argument(s):
153    file_name_dict                  A dictionary with keys/values which are to
154                                    appear as part of the file name.
155    dir_path                        The dir_path that is to appear as part of
156                                    the file name.
157    file_suffix                     A suffix to be included as part of the
158                                    file name.
159    """
160
161    dir_path = gm.add_trailing_slash(dir_path)
162    return dir_path + join_dict(file_name_dict) + file_suffix
163
164
165def parse_file_path(file_path):
166    r"""
167    Parse a file path created by create_file_path and return the result as a
168    dictionary.
169
170    This function is the complement to create_file_path.
171
172    Description of argument(s):
173    file_path                       The file_path.
174
175    Example use:
176    gp.print_var(boot_results_file_path)
177    file_path_data = parse_file_path(boot_results_file_path)
178    gp.print_var(file_path_data)
179
180    Program output.
181
182    boot_results_file_path:
183    /tmp/pgm_name.obmc_boot_test:openbmc_nickname.beye6:master_pid.2039:boot_re
184    sults
185    file_path_data:
186      file_path_data[dir_path]:                       /tmp/
187      file_path_data[pgm_name]:                       obmc_boot_test
188      file_path_data[openbmc_nickname]:               beye6
189      file_path_data[master_pid]:                     2039
190      file_path_data[boot_results]:
191    """
192
193    try:
194        result_dict = collections.OrderedDict()
195    except AttributeError:
196        result_dict = DotDict()
197
198    dir_path = os.path.dirname(file_path) + os.sep
199    file_path = os.path.basename(file_path)
200
201    result_dict['dir_path'] = dir_path
202
203    result_dict.update(split_to_dict(file_path))
204
205    return result_dict
206
207
208def parse_key_value(string,
209                    delim=":",
210                    strip=" ",
211                    to_lower=1,
212                    underscores=1):
213    r"""
214    Parse a key/value string and return as a key/value tuple.
215
216    This function is useful for parsing a line of program output or data that
217    is in the following form:
218    <key or variable name><delimiter><value>
219
220    An example of a key/value string would be as follows:
221
222    Current Limit State: No Active Power Limit
223
224    In the example shown, the delimiter is ":".  The resulting key would be as
225    follows:
226    Current Limit State
227
228    Note: If one were to take the default values of to_lower=1 and
229    underscores=1, the resulting key would be as follows:
230    current_limit_state
231
232    The to_lower and underscores arguments are provided for those who wish to
233    have their key names have the look and feel of python variable names.
234
235    The resulting value for the example above would be as follows:
236    No Active Power Limit
237
238    Another example:
239    name=Mike
240
241    In this case, the delim would be "=", the key is "name" and the value is
242    "Mike".
243
244    Description of argument(s):
245    string                          The string to be parsed.
246    delim                           The delimiter which separates the key from
247                                    the value.
248    strip                           The characters (if any) to strip from the
249                                    beginning and end of both the key and the
250                                    value.
251    to_lower                        Change the key name to lower case.
252    underscores                     Change any blanks found in the key name to
253                                    underscores.
254    """
255
256    pair = string.split(delim)
257
258    key = pair[0].strip(strip)
259    if len(pair) == 0:
260        value = ""
261    else:
262        value = delim.join(pair[1:]).strip(strip)
263
264    if to_lower:
265        key = key.lower()
266    if underscores:
267        key = re.sub(r" ", "_", key)
268
269    return key, value
270
271
272def key_value_list_to_dict(list,
273                           process_indent=0,
274                           **args):
275    r"""
276    Convert a list containing key/value strings or tuples to a dictionary and
277    return it.
278
279    See docstring of parse_key_value function for details on key/value strings.
280
281    Example usage:
282
283    For the following value of list:
284
285    list:
286      list[0]:          Current Limit State: No Active Power Limit
287      list[1]:          Exception actions:   Hard Power Off & Log Event to SEL
288      list[2]:          Power Limit:         0 Watts
289      list[3]:          Correction time:     0 milliseconds
290      list[4]:          Sampling period:     0 seconds
291
292    And the following call in python:
293
294    power_limit = key_value_outbuf_to_dict(list)
295
296    The resulting power_limit directory would look like this:
297
298    power_limit:
299      [current_limit_state]:        No Active Power Limit
300      [exception_actions]:          Hard Power Off & Log Event to SEL
301      [power_limit]:                0 Watts
302      [correction_time]:            0 milliseconds
303      [sampling_period]:            0 seconds
304
305    For the following list:
306
307    headers:
308      headers[0]:
309        headers[0][0]:           content-length
310        headers[0][1]:           559
311      headers[1]:
312        headers[1][0]:           x-xss-protection
313        headers[1][1]:           1; mode=block
314
315    And the following call in python:
316
317    headers_dict = key_value_list_to_dict(headers)
318
319    The resulting headers_dict would look like this:
320
321    headers_dict:
322      [content-length]:          559
323      [x-xss-protection]:        1; mode=block
324
325    Another example containing a sub-list (see process_indent description
326    below):
327
328    Provides Device SDRs      : yes
329    Additional Device Support :
330        Sensor Device
331        SEL Device
332        FRU Inventory Device
333        Chassis Device
334
335    Note that the 2 qualifications for containing a sub-list are met: 1)
336    'Additional Device Support' has no value and 2) The entries below it are
337    indented.  In this case those entries contain no delimiters (":") so they
338    will be processed as a list rather than as a dictionary.  The result would
339    be as follows:
340
341    mc_info:
342      mc_info[provides_device_sdrs]:            yes
343      mc_info[additional_device_support]:
344        mc_info[additional_device_support][0]:  Sensor Device
345        mc_info[additional_device_support][1]:  SEL Device
346        mc_info[additional_device_support][2]:  FRU Inventory Device
347        mc_info[additional_device_support][3]:  Chassis Device
348
349    Description of argument(s):
350    list                            A list of key/value strings.  (See
351                                    docstring of parse_key_value function for
352                                    details).
353    process_indent                  This indicates that indented
354                                    sub-dictionaries and sub-lists are to be
355                                    processed as such.  An entry may have a
356                                    sub-dict or sub-list if 1) It has no value
357                                    other than blank 2) There are entries
358                                    below it that are indented.  Note that
359                                    process_indent is not allowed for a list
360                                    of tuples (vs. a list of key/value
361                                    strings).
362    **args                          Arguments to be interpreted by
363                                    parse_key_value.  (See docstring of
364                                    parse_key_value function for details).
365    """
366
367    try:
368        result_dict = collections.OrderedDict()
369    except AttributeError:
370        result_dict = DotDict()
371
372    if not process_indent:
373        for entry in list:
374            if type(entry) is tuple:
375                key, value = entry
376            else:
377                key, value = parse_key_value(entry, **args)
378            result_dict[key] = value
379        return result_dict
380
381    # Process list while paying heed to indentation.
382    delim = args.get("delim", ":")
383    # Initialize "parent_" indentation level variables.
384    parent_indent = len(list[0]) - len(list[0].lstrip())
385    sub_list = []
386    for entry in list:
387        key, value = parse_key_value(entry, **args)
388
389        indent = len(entry) - len(entry.lstrip())
390
391        if indent > parent_indent and parent_value == "":
392            # This line is indented compared to the parent entry and the
393            # parent entry has no value.
394            # Append the entry to sub_list for later processing.
395            sub_list.append(str(entry))
396            continue
397
398        # Process any outstanding sub_list and add it to
399        # result_dict[parent_key].
400        if len(sub_list) > 0:
401            if any(delim in word for word in sub_list):
402                # If delim is found anywhere in the sub_list, we'll process
403                # as a sub-dictionary.
404                result_dict[parent_key] = key_value_list_to_dict(sub_list,
405                                                                 **args)
406            else:
407                result_dict[parent_key] = map(str.strip, sub_list)
408            del sub_list[:]
409
410        result_dict[key] = value
411
412        parent_key = key
413        parent_value = value
414        parent_indent = indent
415
416    # Any outstanding sub_list to be processed?
417    if len(sub_list) > 0:
418        if any(delim in word for word in sub_list):
419            # If delim is found anywhere in the sub_list, we'll process as a
420            # sub-dictionary.
421            result_dict[parent_key] = key_value_list_to_dict(sub_list, **args)
422        else:
423            result_dict[parent_key] = map(str.strip, sub_list)
424
425    return result_dict
426
427
428def key_value_outbuf_to_dict(out_buf,
429                             **args):
430    r"""
431    Convert a buffer with a key/value string on each line to a dictionary and
432    return it.
433
434    Each line in the out_buf should end with a \n.
435
436    See docstring of parse_key_value function for details on key/value strings.
437
438    Example usage:
439
440    For the following value of out_buf:
441
442    Current Limit State: No Active Power Limit
443    Exception actions:   Hard Power Off & Log Event to SEL
444    Power Limit:         0 Watts
445    Correction time:     0 milliseconds
446    Sampling period:     0 seconds
447
448    And the following call in python:
449
450    power_limit = key_value_outbuf_to_dict(out_buf)
451
452    The resulting power_limit directory would look like this:
453
454    power_limit:
455      [current_limit_state]:        No Active Power Limit
456      [exception_actions]:          Hard Power Off & Log Event to SEL
457      [power_limit]:                0 Watts
458      [correction_time]:            0 milliseconds
459      [sampling_period]:            0 seconds
460
461    Description of argument(s):
462    out_buf                         A buffer with a key/value string on each
463                                    line. (See docstring of parse_key_value
464                                    function for details).
465    **args                          Arguments to be interpreted by
466                                    parse_key_value.  (See docstring of
467                                    parse_key_value function for details).
468    """
469
470    # Create key_var_list and remove null entries.
471    key_var_list = list(filter(None, out_buf.split("\n")))
472    return key_value_list_to_dict(key_var_list, **args)
473
474
475def create_field_desc_regex(line):
476
477    r"""
478    Create a field descriptor regular expression based on the input line and
479    return it.
480
481    This function is designed for use by the list_to_report function (defined
482    below).
483
484    Example:
485
486    Given the following input line:
487
488    --------   ------------ ------------------ ------------------------
489
490    This function will return this regular expression:
491
492    (.{8})   (.{12}) (.{18}) (.{24})
493
494    This means that other report lines interpreted using the regular
495    expression are expected to have:
496    - An 8 character field
497    - 3 spaces
498    - A 12 character field
499    - One space
500    - An 18 character field
501    - One space
502    - A 24 character field
503
504    Description of argument(s):
505    line                            A line consisting of dashes to represent
506                                    fields and spaces to delimit fields.
507    """
508
509    # Split the line into a descriptors list.  Example:
510    # descriptors:
511    #  descriptors[0]:            --------
512    #  descriptors[1]:
513    #  descriptors[2]:
514    #  descriptors[3]:            ------------
515    #  descriptors[4]:            ------------------
516    #  descriptors[5]:            ------------------------
517    descriptors = line.split(" ")
518
519    # Create regexes list.  Example:
520    # regexes:
521    #  regexes[0]:                (.{8})
522    #  regexes[1]:
523    #  regexes[2]:
524    #  regexes[3]:                (.{12})
525    #  regexes[4]:                (.{18})
526    #  regexes[5]:                (.{24})
527    regexes = []
528    for descriptor in descriptors:
529        if descriptor == "":
530            regexes.append("")
531        else:
532            regexes.append("(.{" + str(len(descriptor)) + "})")
533
534    # Join the regexes list into a regex string.
535    field_desc_regex = ' '.join(regexes)
536
537    return field_desc_regex
538
539
540def list_to_report(report_list,
541                   to_lower=1,
542                   field_delim=None):
543    r"""
544    Convert a list containing report text lines to a report "object" and
545    return it.
546
547    The first entry in report_list must be a header line consisting of column
548    names delimited by white space.  No column name may contain white space.
549    The remaining report_list entries should contain tabular data which
550    corresponds to the column names.
551
552    A report object is a list where each entry is a dictionary whose keys are
553    the field names from the first entry in report_list.
554
555    Example:
556    Given the following report_list as input:
557
558    rl:
559      rl[0]: Filesystem           1K-blocks      Used Available Use% Mounted on
560      rl[1]: dev                     247120         0    247120   0% /dev
561      rl[2]: tmpfs                   248408     79792    168616  32% /run
562
563    This function will return a list of dictionaries as shown below:
564
565    df_report:
566      df_report[0]:
567        [filesystem]:                  dev
568        [1k-blocks]:                   247120
569        [used]:                        0
570        [available]:                   247120
571        [use%]:                        0%
572        [mounted]:                     /dev
573      df_report[1]:
574        [filesystem]:                  dev
575        [1k-blocks]:                   247120
576        [used]:                        0
577        [available]:                   247120
578        [use%]:                        0%
579        [mounted]:                     /dev
580
581    Notice that because "Mounted on" contains a space, "on" would be
582    considered the 7th field.  In this case, there is never any data in field
583    7 so things work out nicely.  A caller could do some pre-processing if
584    desired (e.g. change "Mounted on" to "Mounted_on").
585
586    Example 2:
587
588    If the 2nd line of report data is a series of dashes and spaces as in the
589    following example, that line will serve to delineate columns.
590
591    The 2nd line of data is like this:
592    ID                              status       size
593                                    tool,clientid,userid
594    -------- ------------ ------------------ ------------------------
595    20000001 in progress  0x7D0              ,,
596
597    Description of argument(s):
598    report_list                     A list where each entry is one line of
599                                    output from a report.  The first entry
600                                    must be a header line which contains
601                                    column names.  Column names may not
602                                    contain spaces.
603    to_lower                        Change the resulting key names to lower
604                                    case.
605    field_delim                     Indicates that there are field delimiters
606                                    in report_list entries (which should be
607                                    removed).
608    """
609
610    if len(report_list) <= 1:
611        # If we don't have at least a descriptor line and one line of data,
612        # return an empty array.
613        return []
614
615    if field_delim is not None:
616        report_list = [re.sub("\\|", "", line) for line in report_list]
617
618    header_line = report_list[0]
619    if to_lower:
620        header_line = header_line.lower()
621
622    field_desc_regex = ""
623    if re.match(r"^-[ -]*$", report_list[1]):
624        # We have a field descriptor line (as shown in example 2 above).
625        field_desc_regex = create_field_desc_regex(report_list[1])
626        field_desc_len = len(report_list[1])
627        pad_format_string = "%-" + str(field_desc_len) + "s"
628        # The field descriptor line has served its purpose.  Deleting it.
629        del report_list[1]
630
631    # Process the header line by creating a list of column names.
632    if field_desc_regex == "":
633        columns = header_line.split()
634    else:
635        # Pad the line with spaces on the right to facilitate processing with
636        # field_desc_regex.
637        header_line = pad_format_string % header_line
638        columns = map(str.strip, re.findall(field_desc_regex, header_line)[0])
639
640    report_obj = []
641    for report_line in report_list[1:]:
642        if field_desc_regex == "":
643            line = report_line.split()
644        else:
645            # Pad the line with spaces on the right to facilitate processing
646            # with field_desc_regex.
647            report_line = pad_format_string % report_line
648            line = map(str.strip, re.findall(field_desc_regex, report_line)[0])
649        try:
650            line_dict = collections.OrderedDict(zip(columns, line))
651        except AttributeError:
652            line_dict = DotDict(zip(columns, line))
653        report_obj.append(line_dict)
654
655    return report_obj
656
657
658def outbuf_to_report(out_buf,
659                     **args):
660    r"""
661    Convert a text buffer containing report lines to a report "object" and
662    return it.
663
664    Refer to list_to_report (above) for more details.
665
666    Example:
667
668    Given the following out_buf:
669
670    Filesystem                      1K-blocks      Used Available Use% Mounted
671                                    on
672    dev                             247120         0    247120   0% /dev
673    tmpfs                           248408     79792    168616  32% /run
674
675    This function will return a list of dictionaries as shown below:
676
677    df_report:
678      df_report[0]:
679        [filesystem]:                  dev
680        [1k-blocks]:                   247120
681        [used]:                        0
682        [available]:                   247120
683        [use%]:                        0%
684        [mounted]:                     /dev
685      df_report[1]:
686        [filesystem]:                  dev
687        [1k-blocks]:                   247120
688        [used]:                        0
689        [available]:                   247120
690        [use%]:                        0%
691        [mounted]:                     /dev
692
693    Other possible uses:
694    - Process the output of a ps command.
695    - Process the output of an ls command (the caller would need to supply
696      column names)
697
698    Description of argument(s):
699    out_buf                         A text report.  The first line must be a
700                                    header line which contains column names.
701                                    Column names may not contain spaces.
702    **args                          Arguments to be interpreted by
703                                    list_to_report.  (See docstring of
704                                    list_to_report function for details).
705    """
706
707    report_list = list(filter(None, out_buf.split("\n")))
708    return list_to_report(report_list, **args)
709
710
711def nested_get(key, structure):
712    r"""
713    Return a list of all values from the nested structure with the given key.
714
715    The structure can be a dictionary or a list of dictionaries.
716
717    Example:
718
719    Given a dictionary structure named personnel with the following contents:
720
721    personnel:
722      [manager]:
723        [last_name]:             Doe
724        [first_name]:            John
725      [accountant]:
726        [last_name]:             Smith
727        [first_name]:            Will
728
729    The following code...
730
731    last_names = nested_get('last_name', personnel)
732    print_var(last_names)
733
734    Would result in the following data:
735
736    last_names:
737      last_names[0]:             Doe
738      last_names[1]:             Smith
739
740    Description of argument(s):
741    key                             The key value.
742    structure                       The nested structure.
743    """
744
745    result = []
746    if type(structure) is list:
747        for entry in structure:
748            result += nested_get(key, entry)
749        return result
750    # Structure must be a dictionary.
751    for k, v in structure.items():
752        if isinstance(v, dict):
753            result += nested_get(key, v)
754        if k == key:
755            result.append(v)
756
757    return result
758
759
760def filter_struct(structure, filter_dict):
761    r"""
762    Filter the structure by removing any entries that do NOT contain the
763    keys/values specified in filter_dict and return the result.
764
765    Example:
766
767    Given a dictionary named "properties" that has the following structure:
768
769    properties:
770      [/redfish/v1/Systems/system/Processors]:
771        [Members]:
772          [0]:
773            [@odata.id]:
774            /redfish/v1/Systems/system/Processors/cpu0
775          [1]:
776            [@odata.id]:
777            /redfish/v1/Systems/system/Processors/cpu1
778      [/redfish/v1/Systems/system/Processors/cpu0]:
779        [Status]:
780          [State]:                                    Enabled
781          [Health]:                                   OK
782      [/redfish/v1/Systems/system/Processors/cpu1]:
783        [Status]:
784          [State]:                                    Enabled
785          [Health]:                                   Bad
786
787    The following call:
788
789    properties = filter_struct(properties, "[('Health', 'OK')]")
790
791    Would return a new properties dictionary that looks like this:
792
793    properties:
794      [/redfish/v1/Systems/system/Processors/cpu0]:
795        [Status]:
796          [State]:                                    Enabled
797          [Health]:                                   OK
798
799    Note that the first item in the original properties directory had no key
800    anywhere in the structure named "Health".  Therefore, that item failed to
801    make the cut.  The next item did have a key named "Health" whose value was
802    "OK" so it was included in the new structure.  The third item had a key
803    named "Health" but its value was not "OK" so it also failed to make the
804    cut.
805
806    Description of argument(s):
807    structure                       Either a list or a dictionary.  The
808                                    structure is expected to contain
809                                    sub-dictionaries.
810    filter_dict                     A dictionary containing one or more
811                                    key/value pairs.  For each key value pair,
812                                    each entry in the structure must contain
813                                    the same key/value pair.
814    """
815
816    # Convert filter_dict from a string containing a python object definition
817    # to an actual python object (if warranted).
818    filter_dict = fa.source_to_object(filter_dict)
819
820    if type(structure) is list:
821        result = []
822        for entry in structure:
823            valid = True
824            # The entry must pass all tests to be included in new structure.
825            for filter_key, filter_value in filter_dict.items():
826                # Process one filter key/value pair.
827                actual_value = nested_get(filter_key, entry)
828                if len(actual_value) == 0 or actual_value[0] != filter_value:
829                    valid = False
830                    break
831            if valid:
832                result.append(entry)
833    else:
834        # Assume structure is a dictionary.
835        try:
836            result = collections.OrderedDict()
837        except AttributeError:
838            result = DotDict()
839        for struct_key, struct_value in structure.items():
840            valid = True
841            # The entry must pass all tests to be included in new structure.
842            for filter_key, filter_value in filter_dict.items():
843                # Process one filter key/value pair.
844                actual_value = nested_get(filter_key, struct_value)
845                if len(actual_value) == 0 or actual_value[0] != filter_value:
846                    valid = False
847                    break
848            if valid:
849                result[struct_key] = struct_value
850
851    return result
852