blob: 2598bf6fb0ea658eedc71dc1612ba4d3f68bb11c [file] [log] [blame]
Michael Walshced4eb02017-09-19 16:49:13 -05001#!/usr/bin/env python
2
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
19
20
21def create_var_dict(*args):
Michael Walshced4eb02017-09-19 16:49:13 -050022 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):
Michael Walshced4eb02017-09-19 16:49:13 -050066 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
Gunnar Mills096cd562018-03-26 10:19:12 -050093 dict.items()])
Michael Walshced4eb02017-09-19 16:49:13 -050094
95
96def split_to_dict(string,
97 record_delim=default_record_delim,
98 key_val_delim=default_key_val_delim):
Michael Walshced4eb02017-09-19 16:49:13 -050099 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=""):
Michael Walshced4eb02017-09-19 16:49:13 -0500148 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):
Michael Walshced4eb02017-09-19 16:49:13 -0500165 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
Michael Walsh05c68d92017-09-20 16:36:37 -0500205
206
207def parse_key_value(string,
208 delim=":",
209 strip=" ",
210 to_lower=1,
211 underscores=1):
Michael Walsh05c68d92017-09-20 16:36:37 -0500212 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:
MICHAEL J. WALSH9509a0f2018-02-08 11:08:52 -0600261 value = delim.join(pair[1:]).strip(strip)
Michael Walsh05c68d92017-09-20 16:36:37 -0500262
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,
Michael Walshcad07132018-02-19 17:28:01 -0600272 process_indent=0,
Michael Walsh05c68d92017-09-20 16:36:37 -0500273 **args):
Michael Walsh05c68d92017-09-20 16:36:37 -0500274 r"""
Michael Walsh1db86872019-04-16 11:48:25 -0500275 Convert a list containing key/value strings or tuples to a dictionary and
276 return it.
Michael Walsh05c68d92017-09-20 16:36:37 -0500277
278 See docstring of parse_key_value function for details on key/value strings.
279
280 Example usage:
281
282 For the following value of list:
283
284 list:
285 list[0]: Current Limit State: No Active Power Limit
286 list[1]: Exception actions: Hard Power Off & Log Event to SEL
287 list[2]: Power Limit: 0 Watts
288 list[3]: Correction time: 0 milliseconds
289 list[4]: Sampling period: 0 seconds
290
291 And the following call in python:
292
293 power_limit = key_value_outbuf_to_dict(list)
294
295 The resulting power_limit directory would look like this:
296
297 power_limit:
298 [current_limit_state]: No Active Power Limit
299 [exception_actions]: Hard Power Off & Log Event to SEL
300 [power_limit]: 0 Watts
301 [correction_time]: 0 milliseconds
302 [sampling_period]: 0 seconds
303
Michael Walsh1db86872019-04-16 11:48:25 -0500304 For the following list:
305
306 headers:
307 headers[0]:
308 headers[0][0]: content-length
309 headers[0][1]: 559
310 headers[1]:
311 headers[1][0]: x-xss-protection
312 headers[1][1]: 1; mode=block
313
314 And the following call in python:
315
316 headers_dict = key_value_list_to_dict(headers)
317
318 The resulting headers_dict would look like this:
319
320 headers_dict:
321 [content-length]: 559
322 [x-xss-protection]: 1; mode=block
323
Michael Walshcad07132018-02-19 17:28:01 -0600324 Another example containing a sub-list (see process_indent description
325 below):
326
327 Provides Device SDRs : yes
328 Additional Device Support :
329 Sensor Device
330 SEL Device
331 FRU Inventory Device
332 Chassis Device
333
334 Note that the 2 qualifications for containing a sub-list are met: 1)
335 'Additional Device Support' has no value and 2) The entries below it are
336 indented. In this case those entries contain no delimiters (":") so they
337 will be processed as a list rather than as a dictionary. The result would
338 be as follows:
339
340 mc_info:
341 mc_info[provides_device_sdrs]: yes
342 mc_info[additional_device_support]:
343 mc_info[additional_device_support][0]: Sensor Device
344 mc_info[additional_device_support][1]: SEL Device
345 mc_info[additional_device_support][2]: FRU Inventory Device
346 mc_info[additional_device_support][3]: Chassis Device
347
Michael Walsh05c68d92017-09-20 16:36:37 -0500348 Description of argument(s):
349 list A list of key/value strings. (See
350 docstring of parse_key_value function for
351 details).
Michael Walshcad07132018-02-19 17:28:01 -0600352 process_indent This indicates that indented
353 sub-dictionaries and sub-lists are to be
354 processed as such. An entry may have a
355 sub-dict or sub-list if 1) It has no value
356 other than blank 2) There are entries
Michael Walsh1db86872019-04-16 11:48:25 -0500357 below it that are indented. Note that
358 process_indent is not allowed for a list
359 of tuples (vs. a list of key/value
360 strings).
Michael Walsh05c68d92017-09-20 16:36:37 -0500361 **args Arguments to be interpreted by
362 parse_key_value. (See docstring of
363 parse_key_value function for details).
364 """
365
366 try:
367 result_dict = collections.OrderedDict()
368 except AttributeError:
369 result_dict = DotDict()
370
Michael Walshcad07132018-02-19 17:28:01 -0600371 if not process_indent:
372 for entry in list:
Michael Walsh1db86872019-04-16 11:48:25 -0500373 if type(entry) is tuple:
374 key, value = entry
375 else:
376 key, value = parse_key_value(entry, **args)
Michael Walshcad07132018-02-19 17:28:01 -0600377 result_dict[key] = value
378 return result_dict
379
380 # Process list while paying heed to indentation.
381 delim = args.get("delim", ":")
382 # Initialize "parent_" indentation level variables.
383 parent_indent = len(list[0]) - len(list[0].lstrip())
384 sub_list = []
Michael Walsh05c68d92017-09-20 16:36:37 -0500385 for entry in list:
Michael Walshc1dfc782017-09-26 16:08:51 -0500386 key, value = parse_key_value(entry, **args)
Michael Walshcad07132018-02-19 17:28:01 -0600387
388 indent = len(entry) - len(entry.lstrip())
389
390 if indent > parent_indent and parent_value == "":
391 # This line is indented compared to the parent entry and the
392 # parent entry has no value.
393 # Append the entry to sub_list for later processing.
394 sub_list.append(str(entry))
395 continue
396
397 # Process any outstanding sub_list and add it to
398 # result_dict[parent_key].
399 if len(sub_list) > 0:
400 if any(delim in word for word in sub_list):
401 # If delim is found anywhere in the sub_list, we'll process
402 # as a sub-dictionary.
403 result_dict[parent_key] = key_value_list_to_dict(sub_list,
404 **args)
405 else:
406 result_dict[parent_key] = map(str.strip, sub_list)
407 del sub_list[:]
408
Michael Walsh05c68d92017-09-20 16:36:37 -0500409 result_dict[key] = value
410
Michael Walshcad07132018-02-19 17:28:01 -0600411 parent_key = key
412 parent_value = value
413 parent_indent = indent
414
415 # Any outstanding sub_list to be processed?
416 if len(sub_list) > 0:
417 if any(delim in word for word in sub_list):
418 # If delim is found anywhere in the sub_list, we'll process as a
419 # sub-dictionary.
420 result_dict[parent_key] = key_value_list_to_dict(sub_list, **args)
421 else:
422 result_dict[parent_key] = map(str.strip, sub_list)
423
Michael Walsh05c68d92017-09-20 16:36:37 -0500424 return result_dict
425
426
427def key_value_outbuf_to_dict(out_buf,
428 **args):
Michael Walsh05c68d92017-09-20 16:36:37 -0500429 r"""
430 Convert a buffer with a key/value string on each line to a dictionary and
431 return it.
432
433 Each line in the out_buf should end with a \n.
434
435 See docstring of parse_key_value function for details on key/value strings.
436
437 Example usage:
438
439 For the following value of out_buf:
440
441 Current Limit State: No Active Power Limit
442 Exception actions: Hard Power Off & Log Event to SEL
443 Power Limit: 0 Watts
444 Correction time: 0 milliseconds
445 Sampling period: 0 seconds
446
447 And the following call in python:
448
449 power_limit = key_value_outbuf_to_dict(out_buf)
450
451 The resulting power_limit directory would look like this:
452
453 power_limit:
454 [current_limit_state]: No Active Power Limit
455 [exception_actions]: Hard Power Off & Log Event to SEL
456 [power_limit]: 0 Watts
457 [correction_time]: 0 milliseconds
458 [sampling_period]: 0 seconds
459
460 Description of argument(s):
461 out_buf A buffer with a key/value string on each
462 line. (See docstring of parse_key_value
463 function for details).
464 **args Arguments to be interpreted by
465 parse_key_value. (See docstring of
466 parse_key_value function for details).
467 """
468
469 # Create key_var_list and remove null entries.
470 key_var_list = list(filter(None, out_buf.split("\n")))
Michael Walshc1dfc782017-09-26 16:08:51 -0500471 return key_value_list_to_dict(key_var_list, **args)
Michael Walshdb560d42017-11-20 16:42:49 -0600472
473
Michael Walshdc978822018-07-12 15:34:13 -0500474def create_field_desc_regex(line):
475
476 r"""
477 Create a field descriptor regular expression based on the input line and
478 return it.
479
480 This function is designed for use by the list_to_report function (defined
481 below).
482
483 Example:
484
485 Given the following input line:
486
487 -------- ------------ ------------------ ------------------------
488
489 This function will return this regular expression:
490
491 (.{8}) (.{12}) (.{18}) (.{24})
492
493 This means that other report lines interpreted using the regular
494 expression are expected to have:
495 - An 8 character field
496 - 3 spaces
497 - A 12 character field
498 - One space
499 - An 18 character field
500 - One space
501 - A 24 character field
502
503 Description of argument(s):
504 line A line consisting of dashes to represent
505 fields and spaces to delimit fields.
506 """
507
508 # Split the line into a descriptors list. Example:
509 # descriptors:
510 # descriptors[0]: --------
511 # descriptors[1]:
512 # descriptors[2]:
513 # descriptors[3]: ------------
514 # descriptors[4]: ------------------
515 # descriptors[5]: ------------------------
516 descriptors = line.split(" ")
517
518 # Create regexes list. Example:
519 # regexes:
520 # regexes[0]: (.{8})
521 # regexes[1]:
522 # regexes[2]:
523 # regexes[3]: (.{12})
524 # regexes[4]: (.{18})
525 # regexes[5]: (.{24})
526 regexes = []
527 for descriptor in descriptors:
528 if descriptor == "":
529 regexes.append("")
530 else:
531 regexes.append("(.{" + str(len(descriptor)) + "})")
532
533 # Join the regexes list into a regex string.
534 field_desc_regex = ' '.join(regexes)
535
536 return field_desc_regex
537
538
Michael Walshdb560d42017-11-20 16:42:49 -0600539def list_to_report(report_list,
Michael Walsh64043d52018-09-21 16:40:44 -0500540 to_lower=1,
541 field_delim=None):
Michael Walshdb560d42017-11-20 16:42:49 -0600542 r"""
543 Convert a list containing report text lines to a report "object" and
544 return it.
545
546 The first entry in report_list must be a header line consisting of column
547 names delimited by white space. No column name may contain white space.
548 The remaining report_list entries should contain tabular data which
549 corresponds to the column names.
550
551 A report object is a list where each entry is a dictionary whose keys are
552 the field names from the first entry in report_list.
553
554 Example:
555 Given the following report_list as input:
556
557 rl:
558 rl[0]: Filesystem 1K-blocks Used Available Use% Mounted on
559 rl[1]: dev 247120 0 247120 0% /dev
560 rl[2]: tmpfs 248408 79792 168616 32% /run
561
562 This function will return a list of dictionaries as shown below:
563
564 df_report:
565 df_report[0]:
566 [filesystem]: dev
567 [1k-blocks]: 247120
568 [used]: 0
569 [available]: 247120
570 [use%]: 0%
571 [mounted]: /dev
572 df_report[1]:
573 [filesystem]: dev
574 [1k-blocks]: 247120
575 [used]: 0
576 [available]: 247120
577 [use%]: 0%
578 [mounted]: /dev
579
580 Notice that because "Mounted on" contains a space, "on" would be
581 considered the 7th field. In this case, there is never any data in field
582 7 so things work out nicely. A caller could do some pre-processing if
583 desired (e.g. change "Mounted on" to "Mounted_on").
584
Michael Walshdc978822018-07-12 15:34:13 -0500585 Example 2:
586
587 If the 2nd line of report data is a series of dashes and spaces as in the
588 following example, that line will serve to delineate columns.
589
590 The 2nd line of data is like this:
591 ID status size
592 tool,clientid,userid
593 -------- ------------ ------------------ ------------------------
594 20000001 in progress 0x7D0 ,,
595
Michael Walshdb560d42017-11-20 16:42:49 -0600596 Description of argument(s):
597 report_list A list where each entry is one line of
598 output from a report. The first entry
599 must be a header line which contains
600 column names. Column names may not
601 contain spaces.
602 to_lower Change the resulting key names to lower
603 case.
Michael Walsh64043d52018-09-21 16:40:44 -0500604 field_delim Indicates that there are field delimiters
605 in report_list entries (which should be
606 removed).
Michael Walshdb560d42017-11-20 16:42:49 -0600607 """
608
Michael Walshdc978822018-07-12 15:34:13 -0500609 if len(report_list) <= 1:
610 # If we don't have at least a descriptor line and one line of data,
611 # return an empty array.
612 return []
613
Michael Walsh64043d52018-09-21 16:40:44 -0500614 if field_delim is not None:
615 report_list = [re.sub("\\|", "", line) for line in report_list]
616
Michael Walshdb560d42017-11-20 16:42:49 -0600617 header_line = report_list[0]
618 if to_lower:
619 header_line = header_line.lower()
Michael Walshdc978822018-07-12 15:34:13 -0500620
621 field_desc_regex = ""
622 if re.match(r"^-[ -]*$", report_list[1]):
623 # We have a field descriptor line (as shown in example 2 above).
624 field_desc_regex = create_field_desc_regex(report_list[1])
625 field_desc_len = len(report_list[1])
626 pad_format_string = "%-" + str(field_desc_len) + "s"
627 # The field descriptor line has served its purpose. Deleting it.
628 del report_list[1]
629
630 # Process the header line by creating a list of column names.
631 if field_desc_regex == "":
632 columns = header_line.split()
633 else:
634 # Pad the line with spaces on the right to facilitate processing with
635 # field_desc_regex.
636 header_line = pad_format_string % header_line
637 columns = map(str.strip, re.findall(field_desc_regex, header_line)[0])
Michael Walshdb560d42017-11-20 16:42:49 -0600638
639 report_obj = []
640 for report_line in report_list[1:]:
Michael Walshdc978822018-07-12 15:34:13 -0500641 if field_desc_regex == "":
642 line = report_line.split()
643 else:
644 # Pad the line with spaces on the right to facilitate processing
645 # with field_desc_regex.
646 report_line = pad_format_string % report_line
647 line = map(str.strip, re.findall(field_desc_regex, report_line)[0])
Michael Walshdb560d42017-11-20 16:42:49 -0600648 try:
649 line_dict = collections.OrderedDict(zip(columns, line))
650 except AttributeError:
651 line_dict = DotDict(zip(columns, line))
652 report_obj.append(line_dict)
653
654 return report_obj
655
656
657def outbuf_to_report(out_buf,
658 **args):
Michael Walshdb560d42017-11-20 16:42:49 -0600659 r"""
660 Convert a text buffer containing report lines to a report "object" and
661 return it.
662
663 Refer to list_to_report (above) for more details.
664
665 Example:
666
667 Given the following out_buf:
668
Michael Walshdc978822018-07-12 15:34:13 -0500669 Filesystem 1K-blocks Used Available Use% Mounted
670 on
671 dev 247120 0 247120 0% /dev
672 tmpfs 248408 79792 168616 32% /run
Michael Walshdb560d42017-11-20 16:42:49 -0600673
674 This function will return a list of dictionaries as shown below:
675
676 df_report:
677 df_report[0]:
678 [filesystem]: dev
679 [1k-blocks]: 247120
680 [used]: 0
681 [available]: 247120
682 [use%]: 0%
683 [mounted]: /dev
684 df_report[1]:
685 [filesystem]: dev
686 [1k-blocks]: 247120
687 [used]: 0
688 [available]: 247120
689 [use%]: 0%
690 [mounted]: /dev
691
692 Other possible uses:
693 - Process the output of a ps command.
694 - Process the output of an ls command (the caller would need to supply
Michael Walshdc978822018-07-12 15:34:13 -0500695 column names)
Michael Walshdb560d42017-11-20 16:42:49 -0600696
697 Description of argument(s):
Michael Walshdc978822018-07-12 15:34:13 -0500698 out_buf A text report. The first line must be a
Michael Walshdb560d42017-11-20 16:42:49 -0600699 header line which contains column names.
700 Column names may not contain spaces.
701 **args Arguments to be interpreted by
702 list_to_report. (See docstring of
703 list_to_report function for details).
704 """
705
Michael Walsh255181c2018-08-07 15:06:23 -0500706 report_list = list(filter(None, out_buf.split("\n")))
Michael Walshdb560d42017-11-20 16:42:49 -0600707 return list_to_report(report_list, **args)
Michael Walsh7822b9e2019-03-12 16:34:38 -0500708
709
710def nested_get(key, dictionary):
711 r"""
712 Return a list of all values from the nested dictionary with the given key.
713
714 Example:
715
716 Given a dictionary named personnel with the following contents:
717
718 personnel:
719 [manager]:
720 [last_name]: Doe
721 [first_name]: John
722 [accountant]:
723 [last_name]: Smith
724 [first_name]: Will
725
726 The following code...
727
728 last_names = nested_get('last_name', personnel)
729 print_var(last_names)
730
731 Would result in the following data:
732
733 last_names:
734 last_names[0]: Doe
735 last_names[1]: Smith
736
737 Description of argument(s):
738 key The key value.
739 dictionary The nested dictionary.
740 """
741
742 result = []
743 for k, v in dictionary.items():
744 if isinstance(v, dict):
745 result += nested_get(key, v)
746 if k == key:
747 result.append(v)
748
749 return result