1#!/usr/bin/env python 2 3r""" 4This module provides many valuable functions such as my_parm_file. 5""" 6 7# sys and os are needed to get the program dir path and program name. 8import sys 9import errno 10import os 11import collections 12import json 13import time 14import inspect 15try: 16 import ConfigParser 17except ImportError: 18 import configparser 19try: 20 import StringIO 21except ImportError: 22 import io 23import re 24import socket 25import tempfile 26try: 27 import psutil 28 psutil_imported = True 29except ImportError: 30 psutil_imported = False 31 32import gen_print as gp 33import gen_cmd as gc 34 35robot_env = gp.robot_env 36if robot_env: 37 from robot.libraries.BuiltIn import BuiltIn 38 from robot.utils import DotDict 39 40 41def add_trailing_slash(dir_path): 42 r""" 43 Add a trailing slash to the directory path if it doesn't already have one 44 and return it. 45 46 Description of arguments: 47 dir_path A directory path. 48 """ 49 50 return os.path.normpath(dir_path) + os.path.sep 51 52 53def which(file_path): 54 r""" 55 Find the full path of an executable file and return it. 56 57 The PATH environment variable dictates the results of this function. 58 59 Description of arguments: 60 file_path The relative file path (e.g. "my_file" or "lib/my_file"). 61 """ 62 63 shell_rc, out_buf = gc.cmd_fnc_u("which " + file_path, quiet=1, 64 print_output=0, show_err=0) 65 if shell_rc != 0: 66 error_message = "Failed to find complete path for file \"" +\ 67 file_path + "\".\n" 68 error_message += gp.sprint_var(shell_rc, gp.hexa()) 69 error_message += out_buf 70 if robot_env: 71 BuiltIn().fail(gp.sprint_error(error_message)) 72 else: 73 gp.print_error_report(error_message) 74 return False 75 76 file_path = out_buf.rstrip("\n") 77 78 return file_path 79 80 81def add_path(new_path, 82 path, 83 position=0): 84 r""" 85 Add new_path to path, provided that path doesn't already contain new_path, 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 strip the trailing slash. 96 path The path value to which the new_path should be added. 97 position The position in path where the new_path should be added. 0 means it 98 should be added to the beginning, 1 means add it as the 2nd item, etc. 99 sys.maxsize means it should be added to the end. 100 """ 101 102 path_list = list(filter(None, path.split(":"))) 103 new_path = new_path.rstrip("/") 104 if new_path not in path_list: 105 path_list.insert(int(position), new_path) 106 return ":".join(path_list) 107 108 109def dft(value, default): 110 r""" 111 Return default if value is None. Otherwise, return value. 112 113 This is really just shorthand as shown below. 114 115 dft(value, default) 116 117 vs 118 119 default if value is None else value 120 121 Description of arguments: 122 value The value to be returned. 123 default The default value to return if value is None. 124 """ 125 126 return default if value is None else value 127 128 129def get_mod_global(var_name, 130 default=None, 131 mod_name="__main__"): 132 r""" 133 Get module global variable value and return it. 134 135 If we are running in a robot environment, the behavior will default to 136 calling get_variable_value. 137 138 Description of arguments: 139 var_name The name of the variable whose value is sought. 140 default The value to return if the global does not exist. 141 mod_name The name of the module containing the global variable. 142 """ 143 144 if robot_env: 145 return BuiltIn().get_variable_value("${" + var_name + "}", default) 146 147 try: 148 module = sys.modules[mod_name] 149 except KeyError: 150 gp.print_error_report("Programmer error - The mod_name passed to" 151 + " this function is invalid:\n" 152 + gp.sprint_var(mod_name)) 153 raise ValueError('Programmer error.') 154 155 if default is None: 156 return getattr(module, var_name) 157 else: 158 return getattr(module, var_name, default) 159 160 161def global_default(var_value, 162 default=0): 163 r""" 164 If var_value is not None, return it. Otherwise, return the global 165 variable of the same name, if it exists. If not, return default. 166 167 This is meant for use by functions needing help assigning dynamic default 168 values to their parms. Example: 169 170 def func1(parm1=None): 171 172 parm1 = global_default(parm1, 0) 173 174 Description of arguments: 175 var_value The value being evaluated. 176 default The value to be returned if var_value is None AND the global variable of 177 the same name does not exist. 178 """ 179 180 var_name = gp.get_arg_name(0, 1, stack_frame_ix=2) 181 182 return dft(var_value, get_mod_global(var_name, 0)) 183 184 185def set_mod_global(var_value, 186 mod_name="__main__", 187 var_name=None): 188 r""" 189 Set a global variable for a given module. 190 191 Description of arguments: 192 var_value The value to set in the variable. 193 mod_name The name of the module whose variable is to be set. 194 var_name The name of the variable to set. This defaults to the name of the 195 variable used for var_value when calling this function. 196 """ 197 198 try: 199 module = sys.modules[mod_name] 200 except KeyError: 201 gp.print_error_report("Programmer error - The mod_name passed to" 202 + " this function is invalid:\n" 203 + gp.sprint_var(mod_name)) 204 raise ValueError('Programmer error.') 205 206 if var_name is None: 207 var_name = gp.get_arg_name(None, 1, 2) 208 209 setattr(module, var_name, var_value) 210 211 212def my_parm_file(prop_file_path): 213 r""" 214 Read a properties file, put the keys/values into a dictionary and return the dictionary. 215 216 The properties file must have the following format: 217 var_name<= or :>var_value 218 Comment lines (those beginning with a "#") and blank lines are allowed and will be ignored. Leading and 219 trailing single or double quotes will be stripped from the value. E.g. 220 var1="This one" 221 Quotes are stripped so the resulting value for var1 is: 222 This one 223 224 Description of arguments: 225 prop_file_path The caller should pass the path to the properties file. 226 """ 227 228 # ConfigParser expects at least one section header in the file (or you get 229 # ConfigParser.MissingSectionHeaderError). Properties files don't need those so I'll write a dummy 230 # section header. 231 232 try: 233 string_file = StringIO.StringIO() 234 except NameError: 235 string_file = io.StringIO() 236 237 # Write the dummy section header to the string file. 238 string_file.write('[dummysection]\n') 239 # Write the entire contents of the properties file to the string file. 240 string_file.write(open(prop_file_path).read()) 241 # Rewind the string file. 242 string_file.seek(0, os.SEEK_SET) 243 244 # Create the ConfigParser object. 245 try: 246 config_parser = ConfigParser.ConfigParser() 247 except NameError: 248 config_parser = configparser.ConfigParser(strict=False) 249 # Make the property names case-sensitive. 250 config_parser.optionxform = str 251 # Read the properties from the string file. 252 config_parser.readfp(string_file) 253 # Return the properties as a dictionary. 254 if robot_env: 255 return DotDict(config_parser.items('dummysection')) 256 else: 257 return collections.OrderedDict(config_parser.items('dummysection')) 258 259 260def file_to_list(file_path, 261 newlines=0, 262 comments=1, 263 trim=0): 264 r""" 265 Return the contents of a file as a list. Each element of the resulting 266 list is one line from the file. 267 268 Description of arguments: 269 file_path The path to the file (relative or absolute). 270 newlines Include newlines from the file in the results. 271 comments Include comment lines and blank lines in the results. Comment lines are 272 any that begin with 0 or more spaces followed by the pound sign ("#"). 273 trim Trim white space from the beginning and end of each line. 274 """ 275 276 lines = [] 277 file = open(file_path) 278 for line in file: 279 if not comments: 280 if re.match(r"[ ]*#|^$", line): 281 continue 282 if not newlines: 283 line = line.rstrip("\n") 284 if trim: 285 line = line.strip() 286 lines.append(line) 287 file.close() 288 289 return lines 290 291 292def file_to_str(*args, **kwargs): 293 r""" 294 Return the contents of a file as a string. 295 296 Description of arguments: 297 See file_to_list defined above for description of arguments. 298 """ 299 300 return '\n'.join(file_to_list(*args, **kwargs)) 301 302 303def return_path_list(): 304 r""" 305 This function will split the PATH environment variable into a PATH_LIST and return it. Each element in 306 the list will be normalized and have a trailing slash added. 307 """ 308 309 PATH_LIST = os.environ['PATH'].split(":") 310 PATH_LIST = [os.path.normpath(path) + os.sep for path in PATH_LIST] 311 312 return PATH_LIST 313 314 315def escape_bash_quotes(buffer): 316 r""" 317 Escape quotes in string and return it. 318 319 The escape style implemented will be for use on the bash command line. 320 321 Example: 322 That's all. 323 324 Result: 325 That'\''s all. 326 327 The result may then be single quoted on a bash command. Example: 328 329 echo 'That'\''s all.' 330 331 Description of argument(s): 332 buffer The string whose quotes are to be escaped. 333 """ 334 335 return re.sub("\'", "\'\\\'\'", buffer) 336 337 338def quote_bash_parm(parm): 339 r""" 340 Return the bash command line parm with single quotes if they are needed. 341 342 Description of arguments: 343 parm The string to be quoted. 344 """ 345 346 # If any of these characters are found in the parm string, then the string should be quoted. This list 347 # is by no means complete and should be expanded as needed by the developer of this function. 348 # Spaces 349 # Single or double quotes. 350 # Bash variables (therefore, any string with a "$" may need quoting). 351 # Glob characters: *, ?, [] 352 # Extended Glob characters: +, @, ! 353 # Bash brace expansion: {} 354 # Tilde expansion: ~ 355 # Piped commands: | 356 # Bash re-direction: >, < 357 bash_special_chars = set(' \'"$*?[]+@!{}~|><') 358 359 if any((char in bash_special_chars) for char in parm): 360 return "'" + escape_bash_quotes(parm) + "'" 361 362 if parm == '': 363 parm = "''" 364 365 return parm 366 367 368def get_host_name_ip(host=None, 369 short_name=0): 370 r""" 371 Get the host name and the IP address for the given host and return them as a tuple. 372 373 Description of argument(s): 374 host The host name or IP address to be obtained. 375 short_name Include the short host name in the returned tuple, i.e. return host, ip 376 and short_host. 377 """ 378 379 host = dft(host, socket.gethostname()) 380 host_name = socket.getfqdn(host) 381 try: 382 host_ip = socket.gethostbyname(host) 383 except socket.gaierror as my_gaierror: 384 message = "Unable to obtain the host name for the following host:" +\ 385 "\n" + gp.sprint_var(host) 386 gp.print_error_report(message) 387 raise my_gaierror 388 389 if short_name: 390 host_short_name = host_name.split(".")[0] 391 return host_name, host_ip, host_short_name 392 else: 393 return host_name, host_ip 394 395 396def pid_active(pid): 397 r""" 398 Return true if pid represents an active pid and false otherwise. 399 400 Description of argument(s): 401 pid The pid whose status is being sought. 402 """ 403 404 try: 405 os.kill(int(pid), 0) 406 except OSError as err: 407 if err.errno == errno.ESRCH: 408 # ESRCH == No such process 409 return False 410 elif err.errno == errno.EPERM: 411 # EPERM clearly means there's a process to deny access to 412 return True 413 else: 414 # According to "man 2 kill" possible error values are 415 # (EINVAL, EPERM, ESRCH) 416 raise 417 418 return True 419 420 421def to_signed(number, 422 bit_width=None): 423 r""" 424 Convert number to a signed number and return the result. 425 426 Examples: 427 428 With the following code: 429 430 var1 = 0xfffffffffffffff1 431 print_var(var1) 432 print_var(var1, hexa()) 433 var1 = to_signed(var1) 434 print_var(var1) 435 print_var(var1, hexa()) 436 437 The following is written to stdout: 438 var1: 18446744073709551601 439 var1: 0x00000000fffffffffffffff1 440 var1: -15 441 var1: 0xfffffffffffffff1 442 443 The same code but with var1 set to 0x000000000000007f produces the 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 hex value. Typically, this 452 would be a multiple of 32. 453 """ 454 455 if bit_width is None: 456 try: 457 bit_width = gp.bit_length(long(sys.maxsize)) + 1 458 except NameError: 459 bit_width = gp.bit_length(int(sys.maxsize)) + 1 460 461 if number < 0: 462 return number 463 neg_bit_mask = 2**(bit_width - 1) 464 if number & neg_bit_mask: 465 return ((2**bit_width) - number) * -1 466 else: 467 return number 468 469 470def get_child_pids(quiet=1): 471 472 r""" 473 Get and return a list of pids representing all first-generation processes that are the children of the 474 current process. 475 476 Example: 477 478 children = get_child_pids() 479 print_var(children) 480 481 Output: 482 children: 483 children[0]: 9123 484 485 Description of argument(s): 486 quiet Display output to stdout detailing how this child pids are obtained. 487 """ 488 489 if psutil_imported: 490 # If "import psutil" worked, find child pids using psutil. 491 current_process = psutil.Process() 492 return [x.pid for x in current_process.children(recursive=False)] 493 else: 494 # Otherwise, find child pids using shell commands. 495 print_output = not quiet 496 497 ps_cmd_buf = "ps --no-headers --ppid " + str(os.getpid()) +\ 498 " -o pid,args" 499 # Route the output of ps to a temporary file for later grepping. Avoid using " | grep" in the ps 500 # command string because it creates yet another process which is of no interest to the caller. 501 temp = tempfile.NamedTemporaryFile() 502 temp_file_path = temp.name 503 gc.shell_cmd(ps_cmd_buf + " > " + temp_file_path, 504 print_output=print_output) 505 # Sample contents of the temporary file: 506 # 30703 sleep 2 507 # 30795 /bin/bash -c ps --no-headers --ppid 30672 -o pid,args > /tmp/tmpqqorWY 508 # Use egrep to exclude the "ps" process itself from the results collected with the prior shell_cmd 509 # invocation. Only the other children are of interest to the caller. Use cut on the grep results to 510 # obtain only the pid column. 511 rc, output = \ 512 gc.shell_cmd("egrep -v '" + re.escape(ps_cmd_buf) + "' " 513 + temp_file_path + " | cut -c1-5", 514 print_output=print_output) 515 # Split the output buffer by line into a list. Strip each element of extra spaces and convert each 516 # element to an integer. 517 return map(int, map(str.strip, filter(None, output.split("\n")))) 518 519 520def json_loads_multiple(buffer): 521 r""" 522 Convert the contents of the buffer to a JSON array, run json.loads() on it and return the result. 523 524 The buffer is expected to contain one or more JSON objects. 525 526 Description of argument(s): 527 buffer A string containing several JSON objects. 528 """ 529 530 # Any line consisting of just "}", which indicates the end of an object, should have a comma appended. 531 regex = "([\\r\\n])[\\}]([\\r\\n])" 532 buffer = re.sub(regex, "\\1},\\2", buffer, 1) 533 # Remove the comma from after the final object and place the whole buffer inside square brackets. 534 buffer = "[" + re.sub(",([\r\n])$", "\\1}", buffer, 1) + "]" 535 if gp.robot_env: 536 return json.loads(buffer, object_pairs_hook=DotDict) 537 else: 538 return json.loads(buffer, object_pairs_hook=collections.OrderedDict) 539 540 541def file_date_time_stamp(): 542 r""" 543 Return a date/time stamp in the following format: yymmdd.HHMMSS 544 545 This value is suitable for including in file names. Example file1.181001.171716.status 546 """ 547 548 return time.strftime("%y%m%d.%H%M%S", time.localtime(time.time())) 549 550 551def get_function_stack(): 552 r""" 553 Return a list of all the function names currently in the call stack. 554 555 This function's name will be at offset 0. This function's caller's name will be at offset 1 and so on. 556 """ 557 558 return [str(stack_frame[3]) for stack_frame in inspect.stack()] 559 560 561def username(): 562 r""" 563 Return the username for the current process. 564 """ 565 566 username = os.environ.get("USER", "") 567 if username != "": 568 return username 569 user_num = str(os.geteuid()) 570 try: 571 username = os.getlogin() 572 except OSError: 573 if user_num == "0": 574 username = "root" 575 else: 576 username = "?" 577 578 return username 579 580 581def version_tuple(version): 582 r""" 583 Convert the version string to a tuple and return it. 584 585 Description of argument(s): 586 version A version string whose format is "n[.n]" (e.g. "3.6.3", "3", etc.). 587 """ 588 589 return tuple(map(int, (version.split(".")))) 590 591 592def get_python_version(): 593 r""" 594 Get and return the python version. 595 """ 596 597 sys_version = sys.version 598 # Strip out any revision code data (e.g. "3.6.3rc1" will become "3.6.3"). 599 sys_version = re.sub("rc[^ ]+", "", sys_version).split(" ")[0] 600 # Remove any non-numerics, etc. (e.g. "2.7.15+" becomes ""2.7.15"). 601 return re.sub("[^0-9\\.]", "", sys_version) 602 603 604python_version = \ 605 version_tuple(get_python_version()) 606ordered_dict_version = version_tuple("3.6") 607