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