blob: 02aee8460feaa63d3ad6b80def522cca72248989 [file] [log] [blame]
Michael Walshde791732016-09-06 14:25:24 -05001#!/usr/bin/env python
2
Michael Walsh7423c012016-10-04 10:27:21 -05003r"""
4This module provides many valuable functions such as my_parm_file.
5"""
Michael Walshde791732016-09-06 14:25:24 -05006
7# sys and os are needed to get the program dir path and program name.
8import sys
Michael Walsheaa16852017-09-19 16:30:43 -05009import errno
Michael Walshde791732016-09-06 14:25:24 -050010import os
Michael Walshdece16c2018-08-07 15:01:05 -050011import collections
Michael Walsh6aa69802018-09-21 16:38:34 -050012import json
Michael Walshdece16c2018-08-07 15:01:05 -050013try:
14 import ConfigParser
15except ImportError:
16 import configparser
George Keishing3b7115a2018-08-02 10:48:17 -050017try:
Michael Walsh61f5e8f2018-08-03 11:16:00 -050018 import StringIO
George Keishing3b7115a2018-08-02 10:48:17 -050019except ImportError:
Michael Walshdece16c2018-08-07 15:01:05 -050020 import io
Michael Walsh0f2ea5f2017-02-20 15:55:00 -060021import re
Michael Walsh1c85bab2017-05-04 14:29:24 -050022import socket
Michael Walsh3b621fe2018-07-24 16:27:53 -050023import tempfile
24try:
25 import psutil
26 psutil_imported = True
27except ImportError:
28 psutil_imported = False
Michael Walshde791732016-09-06 14:25:24 -050029
Michael Walsh7423c012016-10-04 10:27:21 -050030import gen_print as gp
Michael Walsh0f2ea5f2017-02-20 15:55:00 -060031import gen_cmd as gc
32
Michael Walsh93a09f22017-11-13 15:34:46 -060033robot_env = gp.robot_env
34if robot_env:
Michael Walsh0f2ea5f2017-02-20 15:55:00 -060035 from robot.libraries.BuiltIn import BuiltIn
Michael Walshdece16c2018-08-07 15:01:05 -050036 from robot.utils import DotDict
Michael Walsh7423c012016-10-04 10:27:21 -050037
Michael Walshde791732016-09-06 14:25:24 -050038
Michael Walsh0f2ea5f2017-02-20 15:55:00 -060039def add_trailing_slash(dir_path):
Michael Walsh7db77942017-01-10 11:37:06 -060040 r"""
Michael Walsh0f2ea5f2017-02-20 15:55:00 -060041 Add a trailing slash to the directory path if it doesn't already have one
42 and return it.
Michael Walsh7db77942017-01-10 11:37:06 -060043
Michael Walsh0f2ea5f2017-02-20 15:55:00 -060044 Description of arguments:
45 dir_path A directory path.
Michael Walsh7db77942017-01-10 11:37:06 -060046 """
47
Michael Walsh0f2ea5f2017-02-20 15:55:00 -060048 return os.path.normpath(dir_path) + os.path.sep
49
Michael Walsh0f2ea5f2017-02-20 15:55:00 -060050
Michael Walsh0f2ea5f2017-02-20 15:55:00 -060051def which(file_path):
Michael Walsh0f2ea5f2017-02-20 15:55:00 -060052 r"""
53 Find the full path of an executable file and return it.
54
55 The PATH environment variable dictates the results of this function.
56
57 Description of arguments:
58 file_path The relative file path (e.g. "my_file" or
59 "lib/my_file").
60 """
61
62 shell_rc, out_buf = gc.cmd_fnc_u("which " + file_path, quiet=1,
63 print_output=0, show_err=0)
64 if shell_rc != 0:
65 error_message = "Failed to find complete path for file \"" +\
66 file_path + "\".\n"
67 error_message += gp.sprint_var(shell_rc, 1)
68 error_message += out_buf
69 if robot_env:
70 BuiltIn().fail(gp.sprint_error(error_message))
71 else:
72 gp.print_error_report(error_message)
73 return False
74
75 file_path = out_buf.rstrip("\n")
76
77 return file_path
78
Michael Walsh0f2ea5f2017-02-20 15:55:00 -060079
Michael Walshf7400f32018-09-26 17:13:43 -050080def add_path(new_path,
81 path,
82 position=0):
83 r"""
84 Add new_path to path, provided that path doesn't already contain new_path,
85 and return the result.
86
87 Example:
88 If PATH has a value of "/bin/user:/lib/user". The following code:
89
90 PATH = add_path("/tmp/new_path", PATH)
91
92 will change PATH to "/tmp/new_path:/bin/user:/lib/user".
93
94 Description of argument(s):
95 new_path The path to be added. This function will
96 strip the trailing slash.
97 path The path value to which the new_path
98 should be added.
99 position The position in path where the new_path
100 should be added. 0 means it should be
101 added to the beginning, 1 means add it as
102 the 2nd item, etc. sys.maxsize means it
103 should be added to the end.
104 """
105
106 path_list = list(filter(None, path.split(":")))
107 new_path = new_path.rstrip("/")
108 if new_path not in path_list:
109 path_list.insert(int(position), new_path)
110 return ":".join(path_list)
111
112
Michael Walsh0f2ea5f2017-02-20 15:55:00 -0600113def dft(value, default):
Michael Walsh0f2ea5f2017-02-20 15:55:00 -0600114 r"""
115 Return default if value is None. Otherwise, return value.
116
117 This is really just shorthand as shown below.
118
119 dft(value, default)
120
121 vs
122
123 default if value is None else value
124
125 Description of arguments:
126 value The value to be returned.
127 default The default value to return if value is
128 None.
129 """
130
131 return default if value is None else value
132
Michael Walsh0f2ea5f2017-02-20 15:55:00 -0600133
Michael Walsh0f2ea5f2017-02-20 15:55:00 -0600134def get_mod_global(var_name,
135 default=None,
136 mod_name="__main__"):
Michael Walsh0f2ea5f2017-02-20 15:55:00 -0600137 r"""
138 Get module global variable value and return it.
139
140 If we are running in a robot environment, the behavior will default to
141 calling get_variable_value.
142
143 Description of arguments:
144 var_name The name of the variable whose value is
145 sought.
146 default The value to return if the global does not
147 exist.
148 mod_name The name of the module containing the
149 global variable.
150 """
151
152 if robot_env:
153 return BuiltIn().get_variable_value("${" + var_name + "}", default)
154
155 try:
156 module = sys.modules[mod_name]
157 except KeyError:
Joy Onyerikwu004ad3c2018-06-11 16:29:56 -0500158 gp.print_error_report("Programmer error - The mod_name passed to"
159 + " this function is invalid:\n"
160 + gp.sprint_var(mod_name))
Michael Walsh0f2ea5f2017-02-20 15:55:00 -0600161 raise ValueError('Programmer error.')
162
163 if default is None:
164 return getattr(module, var_name)
165 else:
166 return getattr(module, var_name, default)
167
Michael Walsh0f2ea5f2017-02-20 15:55:00 -0600168
Michael Walsh0f2ea5f2017-02-20 15:55:00 -0600169def global_default(var_value,
170 default=0):
Michael Walsh0f2ea5f2017-02-20 15:55:00 -0600171 r"""
172 If var_value is not None, return it. Otherwise, return the global
173 variable of the same name, if it exists. If not, return default.
174
175 This is meant for use by functions needing help assigning dynamic default
176 values to their parms. Example:
177
178 def func1(parm1=None):
179
180 parm1 = global_default(parm1, 0)
181
182 Description of arguments:
183 var_value The value being evaluated.
184 default The value to be returned if var_value is
185 None AND the global variable of the same
186 name does not exist.
187 """
188
189 var_name = gp.get_arg_name(0, 1, stack_frame_ix=2)
190
191 return dft(var_value, get_mod_global(var_name, 0))
Michael Walsh7db77942017-01-10 11:37:06 -0600192
Michael Walsh7db77942017-01-10 11:37:06 -0600193
Michael Walsh7db77942017-01-10 11:37:06 -0600194def set_mod_global(var_value,
195 mod_name="__main__",
196 var_name=None):
Michael Walsh7db77942017-01-10 11:37:06 -0600197 r"""
198 Set a global variable for a given module.
199
200 Description of arguments:
201 var_value The value to set in the variable.
202 mod_name The name of the module whose variable is
203 to be set.
204 var_name The name of the variable to set. This
205 defaults to the name of the variable used
206 for var_value when calling this function.
207 """
208
209 try:
210 module = sys.modules[mod_name]
211 except KeyError:
Joy Onyerikwu004ad3c2018-06-11 16:29:56 -0500212 gp.print_error_report("Programmer error - The mod_name passed to"
213 + " this function is invalid:\n"
214 + gp.sprint_var(mod_name))
Michael Walsh7db77942017-01-10 11:37:06 -0600215 raise ValueError('Programmer error.')
216
217 if var_name is None:
218 var_name = gp.get_arg_name(None, 1, 2)
219
220 setattr(module, var_name, var_value)
221
Michael Walsh7db77942017-01-10 11:37:06 -0600222
Michael Walshde791732016-09-06 14:25:24 -0500223def my_parm_file(prop_file_path):
Michael Walshde791732016-09-06 14:25:24 -0500224 r"""
225 Read a properties file, put the keys/values into a dictionary and return
226 the dictionary.
227
228 The properties file must have the following format:
229 var_name<= or :>var_value
230 Comment lines (those beginning with a "#") and blank lines are allowed and
231 will be ignored. Leading and trailing single or double quotes will be
232 stripped from the value. E.g.
233 var1="This one"
234 Quotes are stripped so the resulting value for var1 is:
235 This one
236
237 Description of arguments:
Michael Walsh7423c012016-10-04 10:27:21 -0500238 prop_file_path The caller should pass the path to the
239 properties file.
Michael Walshde791732016-09-06 14:25:24 -0500240 """
241
242 # ConfigParser expects at least one section header in the file (or you
243 # get ConfigParser.MissingSectionHeaderError). Properties files don't
244 # need those so I'll write a dummy section header.
245
Michael Walshdece16c2018-08-07 15:01:05 -0500246 try:
247 string_file = StringIO.StringIO()
248 except NameError:
249 string_file = io.StringIO()
250
Michael Walshde791732016-09-06 14:25:24 -0500251 # Write the dummy section header to the string file.
252 string_file.write('[dummysection]\n')
253 # Write the entire contents of the properties file to the string file.
254 string_file.write(open(prop_file_path).read())
255 # Rewind the string file.
256 string_file.seek(0, os.SEEK_SET)
257
258 # Create the ConfigParser object.
Michael Walshdece16c2018-08-07 15:01:05 -0500259 try:
260 config_parser = ConfigParser.ConfigParser()
261 except NameError:
262 config_parser = configparser.ConfigParser()
Michael Walshde791732016-09-06 14:25:24 -0500263 # Make the property names case-sensitive.
264 config_parser.optionxform = str
265 # Read the properties from the string file.
266 config_parser.readfp(string_file)
267 # Return the properties as a dictionary.
Michael Walshdece16c2018-08-07 15:01:05 -0500268 if robot_env:
269 return DotDict(config_parser.items('dummysection'))
270 else:
271 return collections.OrderedDict(config_parser.items('dummysection'))
Michael Walshde791732016-09-06 14:25:24 -0500272
Michael Walsh7423c012016-10-04 10:27:21 -0500273
Michael Walsh0f2ea5f2017-02-20 15:55:00 -0600274def file_to_list(file_path,
275 newlines=0,
276 comments=1,
277 trim=0):
Michael Walsh0f2ea5f2017-02-20 15:55:00 -0600278 r"""
279 Return the contents of a file as a list. Each element of the resulting
280 list is one line from the file.
281
282 Description of arguments:
283 file_path The path to the file (relative or
284 absolute).
285 newlines Include newlines from the file in the
286 results.
287 comments Include comment lines and blank lines in
288 the results. Comment lines are any that
289 begin with 0 or more spaces followed by
290 the pound sign ("#").
291 trim Trim white space from the beginning and
292 end of each line.
293 """
294
295 lines = []
296 file = open(file_path)
297 for line in file:
298 if not comments:
299 if re.match(r"[ ]*#|^$", line):
300 continue
301 if not newlines:
302 line = line.rstrip("\n")
303 if trim:
304 line = line.strip()
305 lines.append(line)
306
307 return lines
308
Michael Walsh0f2ea5f2017-02-20 15:55:00 -0600309
Michael Walsh7423c012016-10-04 10:27:21 -0500310def return_path_list():
Michael Walsh7423c012016-10-04 10:27:21 -0500311 r"""
312 This function will split the PATH environment variable into a PATH_LIST
313 and return it. Each element in the list will be normalized and have a
314 trailing slash added.
315 """
316
317 PATH_LIST = os.environ['PATH'].split(":")
318 PATH_LIST = [os.path.normpath(path) + os.sep for path in PATH_LIST]
319
320 return PATH_LIST
321
Michael Walsh7db77942017-01-10 11:37:06 -0600322
Michael Walsh9fac55c2017-09-29 16:53:56 -0500323def escape_bash_quotes(buffer):
Michael Walsh9fac55c2017-09-29 16:53:56 -0500324 r"""
325 Escape quotes in string and return it.
326
327 The escape style implemented will be for use on the bash command line.
328
329 Example:
330 That's all.
331
332 Result:
333 That'\''s all.
334
335 The result may then be single quoted on a bash command. Example:
336
337 echo 'That'\''s all.'
338
339 Description of argument(s):
340 buffer The string whose quotes are to be escaped.
341 """
342
343 return re.sub("\'", "\'\\\'\'", buffer)
344
345
Michael Walsh7db77942017-01-10 11:37:06 -0600346def quote_bash_parm(parm):
Michael Walsh7db77942017-01-10 11:37:06 -0600347 r"""
348 Return the bash command line parm with single quotes if they are needed.
349
350 Description of arguments:
351 parm The string to be quoted.
352 """
353
354 # If any of these characters are found in the parm string, then the
355 # string should be quoted. This list is by no means complete and should
356 # be expanded as needed by the developer of this function.
357 bash_special_chars = set(' $')
358
359 if any((char in bash_special_chars) for char in parm):
360 return "'" + parm + "'"
361
362 return parm
363
Michael Walsh1c85bab2017-05-04 14:29:24 -0500364
Michael Walsh74427232018-08-31 12:54:24 -0500365def get_host_name_ip(host=None,
Michael Walshf74b3e42018-01-10 11:11:54 -0600366 short_name=0):
Michael Walsh1c85bab2017-05-04 14:29:24 -0500367 r"""
368 Get the host name and the IP address for the given host and return them as
369 a tuple.
370
371 Description of argument(s):
Michael Walshd1b6c702017-05-30 17:54:30 -0500372 host The host name or IP address to be obtained.
Michael Walshf74b3e42018-01-10 11:11:54 -0600373 short_name Include the short host name in the
374 returned tuple, i.e. return host, ip and
375 short_host.
Michael Walsh1c85bab2017-05-04 14:29:24 -0500376 """
377
Michael Walsh74427232018-08-31 12:54:24 -0500378 host = dft(host, socket.gethostname())
Michael Walshf74b3e42018-01-10 11:11:54 -0600379 host_name = socket.getfqdn(host)
Michael Walshd1b6c702017-05-30 17:54:30 -0500380 try:
381 host_ip = socket.gethostbyname(host)
382 except socket.gaierror as my_gaierror:
383 message = "Unable to obtain the host name for the following host:" +\
384 "\n" + gp.sprint_var(host)
385 gp.print_error_report(message)
386 raise my_gaierror
Michael Walsh1c85bab2017-05-04 14:29:24 -0500387
Michael Walshf74b3e42018-01-10 11:11:54 -0600388 if short_name:
389 host_short_name = host_name.split(".")[0]
390 return host_name, host_ip, host_short_name
391 else:
392 return host_name, host_ip
Michael Walsh1c85bab2017-05-04 14:29:24 -0500393
Michael Walsheaa16852017-09-19 16:30:43 -0500394
395def pid_active(pid):
Michael Walsheaa16852017-09-19 16:30:43 -0500396 r"""
397 Return true if pid represents an active pid and false otherwise.
398
399 Description of argument(s):
400 pid The pid whose status is being sought.
401 """
402
403 try:
404 os.kill(int(pid), 0)
405 except OSError as err:
406 if err.errno == errno.ESRCH:
407 # ESRCH == No such process
408 return False
409 elif err.errno == errno.EPERM:
410 # EPERM clearly means there's a process to deny access to
411 return True
412 else:
413 # According to "man 2 kill" possible error values are
414 # (EINVAL, EPERM, ESRCH)
415 raise
416
417 return True
Michael Walsh112c3592018-06-01 14:15:58 -0500418
419
420def to_signed(number,
Michael Walshdece16c2018-08-07 15:01:05 -0500421 bit_width=None):
Michael Walsh112c3592018-06-01 14:15:58 -0500422 r"""
423 Convert number to a signed number and return the result.
424
425 Examples:
426
427 With the following code:
428
429 var1 = 0xfffffffffffffff1
430 print_var(var1)
431 print_var(var1, 1)
432 var1 = to_signed(var1)
433 print_var(var1)
434 print_var(var1, 1)
435
436 The following is written to stdout:
437 var1: 18446744073709551601
438 var1: 0x00000000fffffffffffffff1
439 var1: -15
440 var1: 0xfffffffffffffff1
441
442 The same code but with var1 set to 0x000000000000007f produces the
443 following:
444 var1: 127
445 var1: 0x000000000000007f
446 var1: 127
447 var1: 0x000000000000007f
448
449 Description of argument(s):
450 number The number to be converted.
451 bit_width The number of bits that defines a complete
452 hex value. Typically, this would be a
453 multiple of 32.
454 """
455
Michael Walshdece16c2018-08-07 15:01:05 -0500456 if bit_width is None:
457 try:
458 bit_width = gp.bit_length(long(sys.maxsize)) + 1
459 except NameError:
460 bit_width = gp.bit_length(int(sys.maxsize)) + 1
461
Michael Walsh112c3592018-06-01 14:15:58 -0500462 if number < 0:
463 return number
464 neg_bit_mask = 2**(bit_width - 1)
465 if number & neg_bit_mask:
466 return ((2**bit_width) - number) * -1
467 else:
468 return number
Michael Walsh3b621fe2018-07-24 16:27:53 -0500469
470
471def get_child_pids(quiet=1):
472
473 r"""
474 Get and return a list of pids representing all first-generation processes
475 that are the children of the current process.
476
477 Example:
478
479 children = get_child_pids()
480 print_var(children)
481
482 Output:
483 children:
484 children[0]: 9123
485
486 Description of argument(s):
487 quiet Display output to stdout detailing how
488 this child pids are obtained.
489 """
490
491 if psutil_imported:
492 # If "import psutil" worked, find child pids using psutil.
493 current_process = psutil.Process()
494 return [x.pid for x in current_process.children(recursive=False)]
495 else:
496 # Otherwise, find child pids using shell commands.
497 print_output = not quiet
498
499 ps_cmd_buf = "ps --no-headers --ppid " + str(os.getpid()) +\
500 " -o pid,args"
501 # Route the output of ps to a temporary file for later grepping.
502 # Avoid using " | grep" in the ps command string because it creates
503 # yet another process which is of no interest to the caller.
504 temp = tempfile.NamedTemporaryFile()
505 temp_file_path = temp.name
506 gc.shell_cmd(ps_cmd_buf + " > " + temp_file_path,
507 print_output=print_output)
508 # Sample contents of the temporary file:
509 # 30703 sleep 2
510 # 30795 /bin/bash -c ps --no-headers --ppid 30672 -o pid,args >
511 # /tmp/tmpqqorWY
512 # Use egrep to exclude the "ps" process itself from the results
513 # collected with the prior shell_cmd invocation. Only the other
514 # children are of interest to the caller. Use cut on the grep results
515 # to obtain only the pid column.
516 rc, output = \
517 gc.shell_cmd("egrep -v '" + re.escape(ps_cmd_buf) + "' "
518 + temp_file_path + " | cut -c1-5",
519 print_output=print_output)
520 # Split the output buffer by line into a list. Strip each element of
521 # extra spaces and convert each element to an integer.
522 return map(int, map(str.strip, filter(None, output.split("\n"))))
Michael Walsh6aa69802018-09-21 16:38:34 -0500523
524
525def json_loads_multiple(buffer):
526 r"""
527 Convert the contents of the buffer to a JSON array, run json.loads() on it
528 and return the result.
529
530 The buffer is expected to contain one or more JSON objects.
531
532 Description of argument(s):
533 buffer A string containing several JSON objects.
534 """
535
536 # Any line consisting of just "}", which indicates the end of an object,
537 # should have a comma appended.
538 regex = "([\\r\\n])[\\}]([\\r\\n])"
539 buffer = re.sub(regex, "\\1},\\2", buffer, 1)
540 # Remove the comma from after the final object and place the whole buffer
541 # inside square brackets.
542 buffer = "[" + re.sub(",([\r\n])$", "\\1}", buffer, 1) + "]"
543 if gp.robot_env:
544 return json.loads(buffer, object_pairs_hook=DotDict)
545 else:
546 return json.loads(buffer, object_pairs_hook=collections.OrderedDict)