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