blob: fdde68ee40605e2ce07dc0b08ebab0a4bda11b64 [file] [log] [blame]
George Keishinge7e91712021-09-03 11:28:44 -05001#!/usr/bin/env python3
Michael Walshced4eb02017-09-19 16:49:13 -05002
3r"""
4Define variable manipulation functions.
5"""
6
7import os
Michael Walsh05c68d92017-09-20 16:36:37 -05008import re
Michael Walshced4eb02017-09-19 16:49:13 -05009
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
Michael Walsh074b7652019-05-22 16:25:38 -050019import func_args as fa
Michael Walshced4eb02017-09-19 16:49:13 -050020
21
22def create_var_dict(*args):
Michael Walshced4eb02017-09-19 16:49:13 -050023 r"""
Michael Walsh410b1782019-10-22 15:56:18 -050024 Create a dictionary whose keys/values are the arg names/arg values passed to it and return it to the
25 caller.
Michael Walshced4eb02017-09-19 16:49:13 -050026
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):
Michael Walshced4eb02017-09-19 16:49:13 -050067 r"""
68 Join a dictionary's keys and values into a string and return the string.
69
70 Description of argument(s):
Michael Walsh410b1782019-10-22 15:56:18 -050071 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.
Michael Walshced4eb02017-09-19 16:49:13 -050076
77 Example use:
78
79 gp.print_var(var_dict)
80 str1 = join_dict(var_dict)
Michael Walshc2762f62019-05-17 15:21:35 -050081 gp.print_var(str1)
Michael Walshced4eb02017-09-19 16:49:13 -050082
83 Program output.
84 var_dict:
85 var_dict[first_name]: Steve
86 var_dict[last_name]: Smith
Michael Walsh410b1782019-10-22 15:56:18 -050087 str1: first_name.Steve:last_name.Smith
Michael Walshced4eb02017-09-19 16:49:13 -050088 """
89
90 format_str = '%s' + key_val_delim + '%s'
91 return record_delim.join([format_str % (key, value) for (key, value) in
Gunnar Mills096cd562018-03-26 10:19:12 -050092 dict.items()])
Michael Walshced4eb02017-09-19 16:49:13 -050093
94
95def split_to_dict(string,
96 record_delim=default_record_delim,
97 key_val_delim=default_key_val_delim):
Michael Walshced4eb02017-09-19 16:49:13 -050098 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):
Michael Walsh410b1782019-10-22 15:56:18 -0500104 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.
Michael Walshced4eb02017-09-19 16:49:13 -0500108
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.
Michael Walsh410b1782019-10-22 15:56:18 -0500117 str1: first_name.Steve:last_name.Smith
Michael Walshced4eb02017-09-19 16:49:13 -0500118 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=""):
Michael Walshced4eb02017-09-19 16:49:13 -0500142 r"""
143 Create a file path using the given parameters and return it.
144
145 Description of argument(s):
Michael Walsh410b1782019-10-22 15:56:18 -0500146 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.
Michael Walshced4eb02017-09-19 16:49:13 -0500150 """
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):
Michael Walshced4eb02017-09-19 16:49:13 -0500157 r"""
Michael Walsh410b1782019-10-22 15:56:18 -0500158 Parse a file path created by create_file_path and return the result as a dictionary.
Michael Walshced4eb02017-09-19 16:49:13 -0500159
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:
Michael Walshc2762f62019-05-17 15:21:35 -0500166 gp.print_var(boot_results_file_path)
Michael Walshced4eb02017-09-19 16:49:13 -0500167 file_path_data = parse_file_path(boot_results_file_path)
Michael Walshc2762f62019-05-17 15:21:35 -0500168 gp.print_var(file_path_data)
Michael Walshced4eb02017-09-19 16:49:13 -0500169
170 Program output.
171
172 boot_results_file_path:
Michael Walsh410b1782019-10-22 15:56:18 -0500173 /tmp/pgm_name.obmc_boot_test:openbmc_nickname.beye6:master_pid.2039:boot_results
Michael Walshced4eb02017-09-19 16:49:13 -0500174 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
Michael Walsh05c68d92017-09-20 16:36:37 -0500195
196
197def parse_key_value(string,
198 delim=":",
199 strip=" ",
200 to_lower=1,
201 underscores=1):
Michael Walsh05c68d92017-09-20 16:36:37 -0500202 r"""
203 Parse a key/value string and return as a key/value tuple.
204
Michael Walsh410b1782019-10-22 15:56:18 -0500205 This function is useful for parsing a line of program output or data that is in the following form:
Michael Walsh05c68d92017-09-20 16:36:37 -0500206 <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
Michael Walsh410b1782019-10-22 15:56:18 -0500212 In the example shown, the delimiter is ":". The resulting key would be as follows:
Michael Walsh05c68d92017-09-20 16:36:37 -0500213 Current Limit State
214
Michael Walsh410b1782019-10-22 15:56:18 -0500215 Note: If one were to take the default values of to_lower=1 and underscores=1, the resulting key would be
216 as follows:
Michael Walsh05c68d92017-09-20 16:36:37 -0500217 current_limit_state
218
Michael Walsh410b1782019-10-22 15:56:18 -0500219 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.
Michael Walsh05c68d92017-09-20 16:36:37 -0500221
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
Michael Walsh410b1782019-10-22 15:56:18 -0500228 In this case, the delim would be "=", the key is "name" and the value is "Mike".
Michael Walsh05c68d92017-09-20 16:36:37 -0500229
230 Description of argument(s):
231 string The string to be parsed.
Michael Walsh410b1782019-10-22 15:56:18 -0500232 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.
Michael Walsh05c68d92017-09-20 16:36:37 -0500235 to_lower Change the key name to lower case.
Michael Walsh410b1782019-10-22 15:56:18 -0500236 underscores Change any blanks found in the key name to underscores.
Michael Walsh05c68d92017-09-20 16:36:37 -0500237 """
238
239 pair = string.split(delim)
240
241 key = pair[0].strip(strip)
242 if len(pair) == 0:
243 value = ""
244 else:
MICHAEL J. WALSH9509a0f2018-02-08 11:08:52 -0600245 value = delim.join(pair[1:]).strip(strip)
Michael Walsh05c68d92017-09-20 16:36:37 -0500246
247 if to_lower:
248 key = key.lower()
249 if underscores:
250 key = re.sub(r" ", "_", key)
251
252 return key, value
253
254
Michael Walsh19df7aa2019-12-16 17:17:39 -0600255def key_value_list_to_dict(key_value_list,
Michael Walshcad07132018-02-19 17:28:01 -0600256 process_indent=0,
Michael Walsh05c68d92017-09-20 16:36:37 -0500257 **args):
Michael Walsh05c68d92017-09-20 16:36:37 -0500258 r"""
Michael Walsh410b1782019-10-22 15:56:18 -0500259 Convert a list containing key/value strings or tuples to a dictionary and return it.
Michael Walsh05c68d92017-09-20 16:36:37 -0500260
261 See docstring of parse_key_value function for details on key/value strings.
262
263 Example usage:
264
Michael Walsh19df7aa2019-12-16 17:17:39 -0600265 For the following value of key_value_list:
Michael Walsh05c68d92017-09-20 16:36:37 -0500266
Michael Walsh19df7aa2019-12-16 17:17:39 -0600267 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
Michael Walsh05c68d92017-09-20 16:36:37 -0500273
274 And the following call in python:
275
Michael Walsh19df7aa2019-12-16 17:17:39 -0600276 power_limit = key_value_outbuf_to_dict(key_value_list)
Michael Walsh05c68d92017-09-20 16:36:37 -0500277
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
Michael Walsh1db86872019-04-16 11:48:25 -0500287 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
Michael Walsh410b1782019-10-22 15:56:18 -0500307 Another example containing a sub-list (see process_indent description below):
Michael Walshcad07132018-02-19 17:28:01 -0600308
309 Provides Device SDRs : yes
310 Additional Device Support :
311 Sensor Device
312 SEL Device
313 FRU Inventory Device
314 Chassis Device
315
Michael Walsh410b1782019-10-22 15:56:18 -0500316 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:
Michael Walshcad07132018-02-19 17:28:01 -0600319
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
Michael Walsh05c68d92017-09-20 16:36:37 -0500328 Description of argument(s):
Michael Walsh19df7aa2019-12-16 17:17:39 -0600329 key_value_list A list of key/value strings. (See docstring of parse_key_value function
Michael Walsh410b1782019-10-22 15:56:18 -0500330 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
Michael Walsh05c68d92017-09-20 16:36:37 -0500337 parse_key_value function for details).
338 """
339
340 try:
341 result_dict = collections.OrderedDict()
342 except AttributeError:
343 result_dict = DotDict()
344
Michael Walshcad07132018-02-19 17:28:01 -0600345 if not process_indent:
Michael Walsh19df7aa2019-12-16 17:17:39 -0600346 for entry in key_value_list:
Michael Walsh1db86872019-04-16 11:48:25 -0500347 if type(entry) is tuple:
348 key, value = entry
349 else:
350 key, value = parse_key_value(entry, **args)
Michael Walshcad07132018-02-19 17:28:01 -0600351 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.
Michael Walsh19df7aa2019-12-16 17:17:39 -0600357 parent_indent = len(key_value_list[0]) - len(key_value_list[0].lstrip())
Michael Walshcad07132018-02-19 17:28:01 -0600358 sub_list = []
Michael Walsh19df7aa2019-12-16 17:17:39 -0600359 for entry in key_value_list:
Michael Walshc1dfc782017-09-26 16:08:51 -0500360 key, value = parse_key_value(entry, **args)
Michael Walshcad07132018-02-19 17:28:01 -0600361
362 indent = len(entry) - len(entry.lstrip())
363
364 if indent > parent_indent and parent_value == "":
Michael Walsh410b1782019-10-22 15:56:18 -0500365 # This line is indented compared to the parent entry and the parent entry has no value.
Michael Walshcad07132018-02-19 17:28:01 -0600366 # Append the entry to sub_list for later processing.
367 sub_list.append(str(entry))
368 continue
369
Michael Walsh410b1782019-10-22 15:56:18 -0500370 # Process any outstanding sub_list and add it to result_dict[parent_key].
Michael Walshcad07132018-02-19 17:28:01 -0600371 if len(sub_list) > 0:
372 if any(delim in word for word in sub_list):
Michael Walsh410b1782019-10-22 15:56:18 -0500373 # If delim is found anywhere in the sub_list, we'll process as a sub-dictionary.
Michael Walshcad07132018-02-19 17:28:01 -0600374 result_dict[parent_key] = key_value_list_to_dict(sub_list,
375 **args)
376 else:
Michael Walsh19df7aa2019-12-16 17:17:39 -0600377 result_dict[parent_key] = list(map(str.strip, sub_list))
Michael Walshcad07132018-02-19 17:28:01 -0600378 del sub_list[:]
379
Michael Walsh05c68d92017-09-20 16:36:37 -0500380 result_dict[key] = value
381
Michael Walshcad07132018-02-19 17:28:01 -0600382 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):
Michael Walsh410b1782019-10-22 15:56:18 -0500389 # If delim is found anywhere in the sub_list, we'll process as a sub-dictionary.
Michael Walshcad07132018-02-19 17:28:01 -0600390 result_dict[parent_key] = key_value_list_to_dict(sub_list, **args)
391 else:
Michael Walsh19df7aa2019-12-16 17:17:39 -0600392 result_dict[parent_key] = list(map(str.strip, sub_list))
Michael Walshcad07132018-02-19 17:28:01 -0600393
Michael Walsh05c68d92017-09-20 16:36:37 -0500394 return result_dict
395
396
397def key_value_outbuf_to_dict(out_buf,
398 **args):
Michael Walsh05c68d92017-09-20 16:36:37 -0500399 r"""
Michael Walsh410b1782019-10-22 15:56:18 -0500400 Convert a buffer with a key/value string on each line to a dictionary and return it.
Michael Walsh05c68d92017-09-20 16:36:37 -0500401
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):
Michael Walsh410b1782019-10-22 15:56:18 -0500430 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
Michael Walsh05c68d92017-09-20 16:36:37 -0500433 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")))
Michael Walshc1dfc782017-09-26 16:08:51 -0500438 return key_value_list_to_dict(key_var_list, **args)
Michael Walshdb560d42017-11-20 16:42:49 -0600439
440
Michael Walsh79af4392019-12-09 11:42:48 -0600441def 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
Michael Walshdc978822018-07-12 15:34:13 -0500513def create_field_desc_regex(line):
514
515 r"""
Michael Walsh410b1782019-10-22 15:56:18 -0500516 Create a field descriptor regular expression based on the input line and return it.
Michael Walshdc978822018-07-12 15:34:13 -0500517
Michael Walsh410b1782019-10-22 15:56:18 -0500518 This function is designed for use by the list_to_report function (defined below).
Michael Walshdc978822018-07-12 15:34:13 -0500519
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
Michael Walsh410b1782019-10-22 15:56:18 -0500530 This means that other report lines interpreted using the regular expression are expected to have:
Michael Walshdc978822018-07-12 15:34:13 -0500531 - 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):
Michael Walsh410b1782019-10-22 15:56:18 -0500540 line A line consisting of dashes to represent fields and spaces to delimit
541 fields.
Michael Walshdc978822018-07-12 15:34:13 -0500542 """
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
Michael Walshdb560d42017-11-20 16:42:49 -0600575def list_to_report(report_list,
Michael Walsh64043d52018-09-21 16:40:44 -0500576 to_lower=1,
577 field_delim=None):
Michael Walshdb560d42017-11-20 16:42:49 -0600578 r"""
Michael Walsh410b1782019-10-22 15:56:18 -0500579 Convert a list containing report text lines to a report "object" and return it.
Michael Walshdb560d42017-11-20 16:42:49 -0600580
Michael Walsh410b1782019-10-22 15:56:18 -0500581 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.
Michael Walshdb560d42017-11-20 16:42:49 -0600584
Michael Walsh410b1782019-10-22 15:56:18 -0500585 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.
Michael Walshdb560d42017-11-20 16:42:49 -0600587
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
Michael Walsh410b1782019-10-22 15:56:18 -0500614 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
Michael Walshdb560d42017-11-20 16:42:49 -0600616 desired (e.g. change "Mounted on" to "Mounted_on").
617
Michael Walshdc978822018-07-12 15:34:13 -0500618 Example 2:
619
Michael Walsh410b1782019-10-22 15:56:18 -0500620 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.
Michael Walshdc978822018-07-12 15:34:13 -0500622
623 The 2nd line of data is like this:
Michael Walsh410b1782019-10-22 15:56:18 -0500624 ID status size tool,clientid,userid
Michael Walshdc978822018-07-12 15:34:13 -0500625 -------- ------------ ------------------ ------------------------
626 20000001 in progress 0x7D0 ,,
627
Michael Walshdb560d42017-11-20 16:42:49 -0600628 Description of argument(s):
Michael Walsh410b1782019-10-22 15:56:18 -0500629 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).
Michael Walshdb560d42017-11-20 16:42:49 -0600635 """
636
Michael Walshdc978822018-07-12 15:34:13 -0500637 if len(report_list) <= 1:
Michael Walsh410b1782019-10-22 15:56:18 -0500638 # If we don't have at least a descriptor line and one line of data, return an empty array.
Michael Walshdc978822018-07-12 15:34:13 -0500639 return []
640
Michael Walsh64043d52018-09-21 16:40:44 -0500641 if field_delim is not None:
642 report_list = [re.sub("\\|", "", line) for line in report_list]
643
Michael Walshdb560d42017-11-20 16:42:49 -0600644 header_line = report_list[0]
645 if to_lower:
646 header_line = header_line.lower()
Michael Walshdc978822018-07-12 15:34:13 -0500647
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:
Michael Walsh410b1782019-10-22 15:56:18 -0500661 # Pad the line with spaces on the right to facilitate processing with field_desc_regex.
Michael Walshdc978822018-07-12 15:34:13 -0500662 header_line = pad_format_string % header_line
Michael Walsh0a124e82019-10-21 15:38:44 -0500663 columns = list(map(str.strip,
664 re.findall(field_desc_regex, header_line)[0]))
Michael Walshdb560d42017-11-20 16:42:49 -0600665
666 report_obj = []
667 for report_line in report_list[1:]:
Michael Walshdc978822018-07-12 15:34:13 -0500668 if field_desc_regex == "":
669 line = report_line.split()
670 else:
Michael Walsh410b1782019-10-22 15:56:18 -0500671 # Pad the line with spaces on the right to facilitate processing with field_desc_regex.
Michael Walshdc978822018-07-12 15:34:13 -0500672 report_line = pad_format_string % report_line
Michael Walsh0a124e82019-10-21 15:38:44 -0500673 line = list(map(str.strip,
674 re.findall(field_desc_regex, report_line)[0]))
Michael Walshdb560d42017-11-20 16:42:49 -0600675 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):
Michael Walshdb560d42017-11-20 16:42:49 -0600686 r"""
Michael Walsh410b1782019-10-22 15:56:18 -0500687 Convert a text buffer containing report lines to a report "object" and return it.
Michael Walshdb560d42017-11-20 16:42:49 -0600688
689 Refer to list_to_report (above) for more details.
690
691 Example:
692
693 Given the following out_buf:
694
Michael Walsh410b1782019-10-22 15:56:18 -0500695 Filesystem 1K-blocks Used Available Use% Mounted on
Michael Walshdc978822018-07-12 15:34:13 -0500696 dev 247120 0 247120 0% /dev
697 tmpfs 248408 79792 168616 32% /run
Michael Walshdb560d42017-11-20 16:42:49 -0600698
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.
Michael Walsh410b1782019-10-22 15:56:18 -0500719 - Process the output of an ls command (the caller would need to supply column names)
Michael Walshdb560d42017-11-20 16:42:49 -0600720
721 Description of argument(s):
Michael Walsh410b1782019-10-22 15:56:18 -0500722 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
Michael Walshdb560d42017-11-20 16:42:49 -0600725 list_to_report function for details).
726 """
727
Michael Walsh255181c2018-08-07 15:06:23 -0500728 report_list = list(filter(None, out_buf.split("\n")))
Michael Walshdb560d42017-11-20 16:42:49 -0600729 return list_to_report(report_list, **args)
Michael Walsh7822b9e2019-03-12 16:34:38 -0500730
731
Michael Walsh46ef0a22019-06-11 15:44:49 -0500732def nested_get(key_name, structure):
Michael Walsh7822b9e2019-03-12 16:34:38 -0500733 r"""
Michael Walsh410b1782019-10-22 15:56:18 -0500734 Return a list of all values from the nested structure that have the given key name.
Michael Walsh7822b9e2019-03-12 16:34:38 -0500735
736 Example:
737
Michael Walsh46ef0a22019-06-11 15:44:49 -0500738 Given a dictionary structure named "personnel" with the following contents:
Michael Walsh7822b9e2019-03-12 16:34:38 -0500739
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
Michael Walsh46ef0a22019-06-11 15:44:49 -0500753 Would result in the following data returned:
Michael Walsh7822b9e2019-03-12 16:34:38 -0500754
755 last_names:
756 last_names[0]: Doe
757 last_names[1]: Smith
758
759 Description of argument(s):
Michael Walsh46ef0a22019-06-11 15:44:49 -0500760 key_name The key name (e.g. 'last_name').
Michael Walsh410b1782019-10-22 15:56:18 -0500761 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.
Michael Walsh7822b9e2019-03-12 16:34:38 -0500765 """
766
767 result = []
Michael Walshd882cdc2019-04-24 16:46:34 -0500768 if type(structure) is list:
769 for entry in structure:
Michael Walsh46ef0a22019-06-11 15:44:49 -0500770 result += nested_get(key_name, entry)
Michael Walshd882cdc2019-04-24 16:46:34 -0500771 return result
Michael Walsh46ef0a22019-06-11 15:44:49 -0500772 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)
Michael Walsh7822b9e2019-03-12 16:34:38 -0500777
778 return result
Michael Walsh074b7652019-05-22 16:25:38 -0500779
780
Michael Walsh46ef0a22019-06-11 15:44:49 -0500781def match_struct(structure, match_dict, regex=False):
782 r"""
Michael Walsh410b1782019-10-22 15:56:18 -0500783 Return True or False to indicate whether the structure matches the match dictionary.
Michael Walsh46ef0a22019-06-11 15:44:49 -0500784
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):
Michael Walsh410b1782019-10-22 15:56:18 -0500806 structure Any nested combination of lists or dictionaries. See the prolog of
Michael Walsh46ef0a22019-06-11 15:44:49 -0500807 get_nested() for details.
Michael Walsh410b1782019-10-22 15:56:18 -0500808 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
Michael Walsh46ef0a22019-06-11 15:44:49 -0500813 regular expressions.
814 """
815
Michael Walsh410b1782019-10-22 15:56:18 -0500816 # 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.
Michael Walsh46ef0a22019-06-11 15:44:49 -0500818 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
Michael Walsh399df5a2019-06-21 11:23:54 -0500838def filter_struct(structure, filter_dict, regex=False, invert=False):
Michael Walsh074b7652019-05-22 16:25:38 -0500839 r"""
Michael Walsh410b1782019-10-22 15:56:18 -0500840 Filter the structure by removing any entries that do NOT contain the keys/values specified in filter_dict
841 and return the result.
Michael Walsh074b7652019-05-22 16:25:38 -0500842
Michael Walsh410b1782019-10-22 15:56:18 -0500843 The selection process is directed only at the first-level entries of the structure.
Michael Walsh46ef0a22019-06-11 15:44:49 -0500844
Michael Walsh074b7652019-05-22 16:25:38 -0500845 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]:
Michael Walsh410b1782019-10-22 15:56:18 -0500853 [@odata.id]: /redfish/v1/Systems/system/Processors/cpu0
Michael Walsh074b7652019-05-22 16:25:38 -0500854 [1]:
Michael Walsh410b1782019-10-22 15:56:18 -0500855 [@odata.id]: /redfish/v1/Systems/system/Processors/cpu1
Michael Walsh074b7652019-05-22 16:25:38 -0500856 [/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
Michael Walsh410b1782019-10-22 15:56:18 -0500877 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.
Michael Walsh074b7652019-05-22 16:25:38 -0500881
882 Description of argument(s):
Michael Walsh410b1782019-10-22 15:56:18 -0500883 structure Any nested combination of lists or dictionaries. See the prolog of
Michael Walsh46ef0a22019-06-11 15:44:49 -0500884 get_nested() for details.
Michael Walsh410b1782019-10-22 15:56:18 -0500885 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
Michael Walsh46ef0a22019-06-11 15:44:49 -0500889 that have no State key at all.
Michael Walsh410b1782019-10-22 15:56:18 -0500890 regex Indicates whether the values in the filter_dict should be interpreted as
Michael Walsh46ef0a22019-06-11 15:44:49 -0500891 regular expressions.
Michael Walsh410b1782019-10-22 15:56:18 -0500892 invert Invert the results. Instead of including only matching entries in the
893 results, include only NON-matching entries in the results.
Michael Walsh074b7652019-05-22 16:25:38 -0500894 """
895
Michael Walsh410b1782019-10-22 15:56:18 -0500896 # Convert filter_dict from a string containing a python object definition to an actual python object (if
897 # warranted).
Michael Walsh074b7652019-05-22 16:25:38 -0500898 filter_dict = fa.source_to_object(filter_dict)
899
Michael Walsh410b1782019-10-22 15:56:18 -0500900 # 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.
Michael Walsh074b7652019-05-22 16:25:38 -0500902 if type(structure) is list:
903 result = []
Michael Walsh46ef0a22019-06-11 15:44:49 -0500904 for element in structure:
Michael Walsh399df5a2019-06-21 11:23:54 -0500905 if match_struct(element, filter_dict, regex) != invert:
Michael Walsh46ef0a22019-06-11 15:44:49 -0500906 result.append(element)
Michael Walsh074b7652019-05-22 16:25:38 -0500907 else:
Michael Walsh074b7652019-05-22 16:25:38 -0500908 try:
909 result = collections.OrderedDict()
910 except AttributeError:
911 result = DotDict()
912 for struct_key, struct_value in structure.items():
Michael Walsh399df5a2019-06-21 11:23:54 -0500913 if match_struct(struct_value, filter_dict, regex) != invert:
Michael Walsh074b7652019-05-22 16:25:38 -0500914 result[struct_key] = struct_value
915
916 return result
Michael Walshf3a8ae12020-02-10 17:06:16 -0600917
918
919def split_dict_on_key(split_key, dictionary):
920 r"""
921 Split a dictionary into two dictionaries based on the first occurrence of the split key and return the
922 resulting sub-dictionaries.
923
924 Example:
925 dictionary = {'one': 1, 'two': 2, 'three':3, 'four':4}
926 dict1, dict2 = split_dict_on_key('three', dictionary)
927 pvars(dictionary, dict1, dict2)
928
929 Output:
930 dictionary:
931 [one]: 1
932 [two]: 2
933 [three]: 3
934 [four]: 4
935 dict1:
936 [one]: 1
937 [two]: 2
938 dict2:
939 [three]: 3
940 [four]: 4
941
942 Description of argument(s):
943 split_key The key value to be used to determine where the dictionary should be
944 split.
945 dictionary The dictionary to be split.
946 """
947 dict1 = {}
948 dict2 = {}
949 found_split_key = False
950 for key in list(dictionary.keys()):
951 if key == split_key:
952 found_split_key = True
953 if found_split_key:
954 dict2[key] = dictionary[key]
955 else:
956 dict1[key] = dictionary[key]
957 return dict1, dict2