blob: d7a6373e5277b17d17de26d3b2d4cdc84946b0f5 [file] [log] [blame]
#!/usr/bin/env python3
r"""
This module provides many valuable functions such as my_parm_file.
"""
import collections
import errno
import inspect
import json
import os
import random
import shutil
# sys and os are needed to get the program dir path and program name.
import sys
import time
try:
import ConfigParser
except ImportError:
import configparser
try:
import StringIO
except ImportError:
import io
import re
import socket
import tempfile
try:
import psutil
psutil_imported = True
except ImportError:
psutil_imported = False
import gen_cmd as gc
import gen_print as gp
robot_env = gp.robot_env
if robot_env:
from robot.libraries.BuiltIn import BuiltIn
from robot.utils import DotDict
def add_trailing_slash(dir_path):
r"""
Add a trailing slash to the directory path if it doesn't already have one
and return it.
Description of arguments:
dir_path A directory path.
"""
return os.path.normpath(dir_path) + os.path.sep
def makedirs(path, mode=0o777, quiet=None):
r"""
Call os.makedirs with the caller's arguments.
This function offers 2 advantages over the base os.makedirs function:
1) It will not fail if the directory already exists.
2) It will print an "Issuing: os.makedirs" message.
Description of argument(s):
path The path containing the directories to be created.
mode The mode or permissions to be granted to the created directories.
quiet Indicates whether this function should run the print_issuing() function.
"""
quiet = int(dft(quiet, gp.get_stack_var("quiet", 0)))
gp.qprint_issuing("os.makedirs('" + path + "', mode=" + oct(mode) + ")")
try:
os.makedirs(path, mode)
except OSError:
pass
def rmtree(path, ignore_errors=False, onerror=None, quiet=None):
r"""
Call shutil.rmtree with the caller's arguments.
This function offers this advantage over the base function:
- It will print an "Issuing: shutil.rmtree" message.
Description of argument(s):
(All parms are passed directly to shutil.rmtree. See its prolog for details)
quiet Indicates whether this function should run the print_issuing() function.
"""
quiet = int(dft(quiet, gp.get_stack_var("quiet", 0)))
print_string = gp.sprint_executing(max_width=2000)
print_string = re.sub(
r"Executing: ", "Issuing: shutil.", print_string.rstrip("\n")
)
gp.qprintn(re.sub(r", quiet[ ]?=.*", ")", print_string))
shutil.rmtree(path, ignore_errors, onerror)
def chdir(path, quiet=None):
r"""
Call os.chdir with the caller's arguments.
This function offers this advantage over the base os.chdir function:
- It will print an "Issuing: os.chdir" message.
Description of argument(s):
path The path of the directory to change to.
quiet Indicates whether this function should run the print_issuing() function.
"""
quiet = int(dft(quiet, gp.get_stack_var("quiet", 0)))
gp.qprint_issuing("os.chdir('" + path + "')")
os.chdir(path)
def which(file_path):
r"""
Find the full path of an executable file and return it.
The PATH environment variable dictates the results of this function.
Description of arguments:
file_path The relative file path (e.g. "my_file" or "lib/my_file").
"""
shell_rc, out_buf = gc.cmd_fnc_u(
"which " + file_path, quiet=1, print_output=0, show_err=0
)
if shell_rc != 0:
error_message = (
'Failed to find complete path for file "' + file_path + '".\n'
)
error_message += gp.sprint_var(shell_rc, gp.hexa())
error_message += out_buf
if robot_env:
BuiltIn().fail(gp.sprint_error(error_message))
else:
gp.print_error_report(error_message)
return False
file_path = out_buf.rstrip("\n")
return file_path
def add_path(new_path, path, position=0):
r"""
Add new_path to path, provided that path doesn't already contain new_path, and return the result.
Example:
If PATH has a value of "/bin/user:/lib/user". The following code:
PATH = add_path("/tmp/new_path", PATH)
will change PATH to "/tmp/new_path:/bin/user:/lib/user".
Description of argument(s):
new_path The path to be added. This function will strip the trailing slash.
path The path value to which the new_path should be added.
position The position in path where the new_path should be added. 0 means it
should be added to the beginning, 1 means add it as the 2nd item, etc.
sys.maxsize means it should be added to the end.
"""
path_list = list(filter(None, path.split(":")))
new_path = new_path.rstrip("/")
if new_path not in path_list:
path_list.insert(int(position), new_path)
return ":".join(path_list)
def dft(value, default):
r"""
Return default if value is None. Otherwise, return value.
This is really just shorthand as shown below.
dft(value, default)
vs
default if value is None else value
Description of arguments:
value The value to be returned.
default The default value to return if value is None.
"""
return default if value is None else value
def get_mod_global(var_name, default=None, mod_name="__main__"):
r"""
Get module global variable value and return it.
If we are running in a robot environment, the behavior will default to
calling get_variable_value.
Description of arguments:
var_name The name of the variable whose value is sought.
default The value to return if the global does not exist.
mod_name The name of the module containing the global variable.
"""
if robot_env:
return BuiltIn().get_variable_value("${" + var_name + "}", default)
try:
module = sys.modules[mod_name]
except KeyError:
gp.print_error_report(
"Programmer error - The mod_name passed to"
+ " this function is invalid:\n"
+ gp.sprint_var(mod_name)
)
raise ValueError("Programmer error.")
if default is None:
return getattr(module, var_name)
else:
return getattr(module, var_name, default)
def global_default(var_value, default=0):
r"""
If var_value is not None, return it. Otherwise, return the global
variable of the same name, if it exists. If not, return default.
This is meant for use by functions needing help assigning dynamic default
values to their parms. Example:
def func1(parm1=None):
parm1 = global_default(parm1, 0)
Description of arguments:
var_value The value being evaluated.
default The value to be returned if var_value is None AND the global variable of
the same name does not exist.
"""
var_name = gp.get_arg_name(0, 1, stack_frame_ix=2)
return dft(var_value, get_mod_global(var_name, 0))
def set_mod_global(var_value, mod_name="__main__", var_name=None):
r"""
Set a global variable for a given module.
Description of arguments:
var_value The value to set in the variable.
mod_name The name of the module whose variable is to be set.
var_name The name of the variable to set. This defaults to the name of the
variable used for var_value when calling this function.
"""
try:
module = sys.modules[mod_name]
except KeyError:
gp.print_error_report(
"Programmer error - The mod_name passed to"
+ " this function is invalid:\n"
+ gp.sprint_var(mod_name)
)
raise ValueError("Programmer error.")
if var_name is None:
var_name = gp.get_arg_name(None, 1, 2)
setattr(module, var_name, var_value)
def my_parm_file(prop_file_path):
r"""
Read a properties file, put the keys/values into a dictionary and return the dictionary.
The properties file must have the following format:
var_name<= or :>var_value
Comment lines (those beginning with a "#") and blank lines are allowed and will be ignored. Leading and
trailing single or double quotes will be stripped from the value. E.g.
var1="This one"
Quotes are stripped so the resulting value for var1 is:
This one
Description of arguments:
prop_file_path The caller should pass the path to the properties file.
"""
# ConfigParser expects at least one section header in the file (or you get
# ConfigParser.MissingSectionHeaderError). Properties files don't need those so I'll write a dummy
# section header.
try:
string_file = StringIO.StringIO()
except NameError:
string_file = io.StringIO()
# Write the dummy section header to the string file.
string_file.write("[dummysection]\n")
# Write the entire contents of the properties file to the string file.
string_file.write(open(prop_file_path).read())
# Rewind the string file.
string_file.seek(0, os.SEEK_SET)
# Create the ConfigParser object.
try:
config_parser = ConfigParser.ConfigParser()
except NameError:
config_parser = configparser.ConfigParser(strict=False)
# Make the property names case-sensitive.
config_parser.optionxform = str
# Read the properties from the string file.
config_parser.readfp(string_file)
# Return the properties as a dictionary.
if robot_env:
return DotDict(config_parser.items("dummysection"))
else:
return collections.OrderedDict(config_parser.items("dummysection"))
def file_to_list(file_path, newlines=0, comments=1, trim=0):
r"""
Return the contents of a file as a list. Each element of the resulting
list is one line from the file.
Description of arguments:
file_path The path to the file (relative or absolute).
newlines Include newlines from the file in the results.
comments Include comment lines and blank lines in the results. Comment lines are
any that begin with 0 or more spaces followed by the pound sign ("#").
trim Trim white space from the beginning and end of each line.
"""
lines = []
file = open(file_path)
for line in file:
if not comments:
if re.match(r"[ ]*#|^$", line):
continue
if not newlines:
line = line.rstrip("\n")
if trim:
line = line.strip()
lines.append(line)
file.close()
return lines
def file_to_str(*args, **kwargs):
r"""
Return the contents of a file as a string.
Description of arguments:
See file_to_list defined above for description of arguments.
"""
return "\n".join(file_to_list(*args, **kwargs))
def append_file(file_path, buffer):
r"""
Append the data in buffer to the file named in file_path.
Description of argument(s):
file_path The path to a file (e.g. "/tmp/root/file1").
buffer The buffer of data to be written to the file (e.g. "this and that").
"""
with open(file_path, "a") as file:
file.write(buffer)
def return_path_list():
r"""
This function will split the PATH environment variable into a PATH_LIST and return it. Each element in
the list will be normalized and have a trailing slash added.
"""
PATH_LIST = os.environ["PATH"].split(":")
PATH_LIST = [os.path.normpath(path) + os.sep for path in PATH_LIST]
return PATH_LIST
def escape_bash_quotes(buffer):
r"""
Escape quotes in string and return it.
The escape style implemented will be for use on the bash command line.
Example:
That's all.
Result:
That'\''s all.
The result may then be single quoted on a bash command. Example:
echo 'That'\''s all.'
Description of argument(s):
buffer The string whose quotes are to be escaped.
"""
return re.sub("'", "'\\''", buffer)
def quote_bash_parm(parm):
r"""
Return the bash command line parm with single quotes if they are needed.
Description of arguments:
parm The string to be quoted.
"""
# If any of these characters are found in the parm string, then the string should be quoted. This list
# is by no means complete and should be expanded as needed by the developer of this function.
# Spaces
# Single or double quotes.
# Bash variables (therefore, any string with a "$" may need quoting).
# Glob characters: *, ?, []
# Extended Glob characters: +, @, !
# Bash brace expansion: {}
# Tilde expansion: ~
# Piped commands: |
# Bash re-direction: >, <
bash_special_chars = set(" '\"$*?[]+@!{}~|><")
if any((char in bash_special_chars) for char in parm):
return "'" + escape_bash_quotes(parm) + "'"
if parm == "":
parm = "''"
return parm
def get_host_name_ip(host=None, short_name=0):
r"""
Get the host name and the IP address for the given host and return them as a tuple.
Description of argument(s):
host The host name or IP address to be obtained.
short_name Include the short host name in the returned tuple, i.e. return host, ip
and short_host.
"""
host = dft(host, socket.gethostname())
host_name = socket.getfqdn(host)
try:
host_ip = socket.gethostbyname(host)
except socket.gaierror as my_gaierror:
message = (
"Unable to obtain the host name for the following host:"
+ "\n"
+ gp.sprint_var(host)
)
gp.print_error_report(message)
raise my_gaierror
if short_name:
host_short_name = host_name.split(".")[0]
return host_name, host_ip, host_short_name
else:
return host_name, host_ip
def pid_active(pid):
r"""
Return true if pid represents an active pid and false otherwise.
Description of argument(s):
pid The pid whose status is being sought.
"""
try:
os.kill(int(pid), 0)
except OSError as err:
if err.errno == errno.ESRCH:
# ESRCH == No such process
return False
elif err.errno == errno.EPERM:
# EPERM clearly means there's a process to deny access to
return True
else:
# According to "man 2 kill" possible error values are
# (EINVAL, EPERM, ESRCH)
raise
return True
def to_signed(number, bit_width=None):
r"""
Convert number to a signed number and return the result.
Examples:
With the following code:
var1 = 0xfffffffffffffff1
print_var(var1)
print_var(var1, hexa())
var1 = to_signed(var1)
print_var(var1)
print_var(var1, hexa())
The following is written to stdout:
var1: 18446744073709551601
var1: 0x00000000fffffffffffffff1
var1: -15
var1: 0xfffffffffffffff1
The same code but with var1 set to 0x000000000000007f produces the following:
var1: 127
var1: 0x000000000000007f
var1: 127
var1: 0x000000000000007f
Description of argument(s):
number The number to be converted.
bit_width The number of bits that defines a complete hex value. Typically, this
would be a multiple of 32.
"""
if bit_width is None:
try:
bit_width = gp.bit_length(long(sys.maxsize)) + 1
except NameError:
bit_width = gp.bit_length(int(sys.maxsize)) + 1
if number < 0:
return number
neg_bit_mask = 2 ** (bit_width - 1)
if number & neg_bit_mask:
return ((2**bit_width) - number) * -1
else:
return number
def get_child_pids(quiet=1):
r"""
Get and return a list of pids representing all first-generation processes that are the children of the
current process.
Example:
children = get_child_pids()
print_var(children)
Output:
children:
children[0]: 9123
Description of argument(s):
quiet Display output to stdout detailing how this child pids are obtained.
"""
if psutil_imported:
# If "import psutil" worked, find child pids using psutil.
current_process = psutil.Process()
return [x.pid for x in current_process.children(recursive=False)]
else:
# Otherwise, find child pids using shell commands.
print_output = not quiet
ps_cmd_buf = (
"ps --no-headers --ppid " + str(os.getpid()) + " -o pid,args"
)
# Route the output of ps to a temporary file for later grepping. Avoid using " | grep" in the ps
# command string because it creates yet another process which is of no interest to the caller.
temp = tempfile.NamedTemporaryFile()
temp_file_path = temp.name
gc.shell_cmd(
ps_cmd_buf + " > " + temp_file_path, print_output=print_output
)
# Sample contents of the temporary file:
# 30703 sleep 2
# 30795 /bin/bash -c ps --no-headers --ppid 30672 -o pid,args > /tmp/tmpqqorWY
# Use egrep to exclude the "ps" process itself from the results collected with the prior shell_cmd
# invocation. Only the other children are of interest to the caller. Use cut on the grep results to
# obtain only the pid column.
rc, output = gc.shell_cmd(
"egrep -v '"
+ re.escape(ps_cmd_buf)
+ "' "
+ temp_file_path
+ " | cut -c1-5",
print_output=print_output,
)
# Split the output buffer by line into a list. Strip each element of extra spaces and convert each
# element to an integer.
return map(int, map(str.strip, filter(None, output.split("\n"))))
def json_loads_multiple(buffer):
r"""
Convert the contents of the buffer to a JSON array, run json.loads() on it and return the result.
The buffer is expected to contain one or more JSON objects.
Description of argument(s):
buffer A string containing several JSON objects.
"""
# Any line consisting of just "}", which indicates the end of an object, should have a comma appended.
regex = "([\\r\\n])[\\}]([\\r\\n])"
buffer = re.sub(regex, "\\1},\\2", buffer, 1)
# Remove the comma from after the final object and place the whole buffer inside square brackets.
buffer = "[" + re.sub(",([\r\n])$", "\\1}", buffer, 1) + "]"
if gp.robot_env:
return json.loads(buffer, object_pairs_hook=DotDict)
else:
return json.loads(buffer, object_pairs_hook=collections.OrderedDict)
def file_date_time_stamp():
r"""
Return a date/time stamp in the following format: yymmdd.HHMMSS
This value is suitable for including in file names. Example file1.181001.171716.status
"""
return time.strftime("%y%m%d.%H%M%S", time.localtime(time.time()))
def get_function_stack():
r"""
Return a list of all the function names currently in the call stack.
This function's name will be at offset 0. This function's caller's name will be at offset 1 and so on.
"""
return [str(stack_frame[3]) for stack_frame in inspect.stack()]
def username():
r"""
Return the username for the current process.
"""
username = os.environ.get("USER", "")
if username != "":
return username
user_num = str(os.geteuid())
try:
username = os.getlogin()
except OSError:
if user_num == "0":
username = "root"
else:
username = "?"
return username
def version_tuple(version):
r"""
Convert the version string to a tuple and return it.
Description of argument(s):
version A version string whose format is "n[.n]" (e.g. "3.6.3", "3", etc.).
"""
return tuple(map(int, (version.split("."))))
def get_python_version():
r"""
Get and return the python version.
"""
sys_version = sys.version
# Strip out any revision code data (e.g. "3.6.3rc1" will become "3.6.3").
sys_version = re.sub("rc[^ ]+", "", sys_version).split(" ")[0]
# Remove any non-numerics, etc. (e.g. "2.7.15+" becomes ""2.7.15").
return re.sub("[^0-9\\.]", "", sys_version)
python_version = version_tuple(get_python_version())
ordered_dict_version = version_tuple("3.6")
def create_temp_file_path(delim=":", suffix=""):
r"""
Create a temporary file path and return it.
This function is appropriate for users who with to create a temporary file and:
1) Have control over when and whether the file is deleted.
2) Have the name of the file indicate information such as program name, function name, line, pid, etc.
This can be an aid in debugging, cleanup, etc.
The dir path portion of the file path will be /tmp/<username>/. This function will create this directory
if it doesn't already exist.
This function will NOT create the file. The file will NOT automatically get deleted. It is the
responsibility of the caller to dispose of it.
Example:
pgm123.py is run by user 'joe'. It calls func1 which contains this code:
temp_file_path = create_temp_file_path(suffix='suffix1')
print_var(temp_file_path)
Output:
temp_file_path: /tmp/joe/pgm123.py:func1:line_55:pid_8199:831848:suffix1
Description of argument(s):
delim A delimiter to be used to separate the sub-components of the file name.
suffix A suffix to include as the last sub-component of the file name.
"""
temp_dir_path = "/tmp/" + username() + "/"
try:
os.mkdir(temp_dir_path)
except FileExistsError:
pass
callers_stack_frame = inspect.stack()[1]
file_name_elements = [
gp.pgm_name,
callers_stack_frame.function,
"line_" + str(callers_stack_frame.lineno),
"pid_" + str(os.getpid()),
str(random.randint(0, 1000000)),
suffix,
]
temp_file_name = delim.join(file_name_elements)
temp_file_path = temp_dir_path + temp_file_name
return temp_file_path
def pause(message="Hit enter to continue..."):
r"""
Print the message, with time stamp, and pause until the user hits enter.
Description of argument(s):
message The message to be printed to stdout.
"""
gp.print_time(message)
try:
input()
except SyntaxError:
pass
return