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