diff --git a/utils/ansible/library/env.py b/utils/ansible/library/env.py new file mode 100644 index 00000000000..ec75090bdc3 --- /dev/null +++ b/utils/ansible/library/env.py @@ -0,0 +1,331 @@ +#!/usr/bin/python +# +# SPDX-License-Identifier: BSD-3-Clause +# Copyright 2019-2023, Intel Corporation +# + +import os +from ansible.module_utils.basic import AnsibleModule + +DOCUMENTATION = ''' +--- +module: env +short_description: Module for managing environmental variables on remote hosts +description: + - "Module for set and unset environmental variables on remote hosts. + Setting is done by writing export definitions to the file* (this should help with keeping persistence of values + between reboots) and then execute 'source' command (in order to feed current session with exported variables). + - ---- + - * file: the file under the care of this module, specified by 'path' parameter. By default it is a file in + /etc/profile.d/" +options: + name: + description: + - This is the name of the variable to manage + required: true + value: + description: + - This is the value of the variable to manage. If 'state'='present', this option is required. + required: false + state: + description: + - If 'present', ensure that given variable is set to given value. This is the default. + required: false + default: present + choices: [ present ] + path: + description: + - This is the path of a file where export definitions will be written. Usually you don't want to change it. + required: false + default: /etc/profile.d/ansible_env_module_definitions.sh +author: + - dpysx +''' + +EXAMPLES = ''' +# Set http_proxy: +- name: set http proxy + env: + name: http_proxy + value: http://example.com:80 +''' + +RETURN = ''' +old_value: + description: Value of variable, before the module was called. + type: str +''' + + +# ####################################################################################### +# ########################## General helpers ########################################## +# ####################################################################################### + + +def check_prerequisites(module, result): + """Check if passed module and result are valid types from AnsibleModule.""" + if not isinstance(module, AnsibleModule): + raise TypeError("Given module is not valid!") + + if not isinstance(result, dict): + raise TypeError("Given result is not valid!") + + +# ####################################################################################### +# ################### Helpers for dealing with system commands ######################## +# ####################################################################################### + + +def escape_quotes(string): + """Escape every double quote char with a backslash.""" + return string.replace('"', '\\"') + + +def run_command(module, result, command): + """Execute command and return its stdout; when status not zero, fail module execution.""" + check_prerequisites(module, result) + + command = 'bash -lc "' + escape_quotes(command) + '"' + + rc, stdout, stderr = module.run_command(command) + + if rc != 0: + fail_message = "Command '" + command + "' failed with status code " + str( + rc) + ", stdout: '" + stdout + "', stderr: '" + stderr + "'." + module.fail_json(msg=fail_message, **result) + + return stdout + + +# ####################################################################################### +# ###################### Helpers for handling checking env vars ####################### +# ####################################################################################### + + +def get_env_value_or_empty(module, result, name): + """Obtain value of variable and return it; if not present, return empty string.""" + check_prerequisites(module, result) + + print_variable = 'echo "$' + name + '"' + variable_value = run_command(module, result, print_variable) + + return variable_value.replace('\n', '') + + +def is_env_present(module, result, name): + """Check if variable with given name is present in system.""" + check_prerequisites(module, result) + + env_value = get_env_value_or_empty(module, result, name) + if env_value == "": + return False + else: + return True + + +# ####################################################################################### +# ########################## Helpers for file handling ################################ +# ####################################################################################### + + +valid_shebang = "#!/usr/bin/env bash\n" + + +def read_file_lines(module, result, filename): + """Read file and return its lines in a form of list.""" + content = [] + try: + file = open(filename, "r") + content = file.readlines() + file.close() + except IOError: + fail_message = "Cannot open file '" + filename + "' for reading." + module.fail_json(msg=fail_message, **result) + + return content + + +def is_valid_shebang(module, result, filename): + """Check if file has valid, bash shebang line.""" + check_prerequisites(module, result) + + file_lines = read_file_lines(module, result, filename) + + global valid_shebang + if len(file_lines) == 0 or file_lines[0] != valid_shebang: + return False + else: + return True + + +def write_file_lines(module, result, filename, lines): + """Write all lines to the file.""" + check_prerequisites(module, result) + + changed = False + if not os.access(filename, os.F_OK): + changed = True + + try: + file = open(filename, 'w') + except IOError: + fail_message = "Cannot open file '" + filename + "' for write." + module.fail_json(msg=fail_message, **result) + + try: + content = [] + for line in lines: + if not line.endswith('\n'): + s = line + "\n" + else: + s = line + content.append(s) + + file.writelines(content) + changed = True + except: + fail_message = "Cannot write to file '" + filename + "'." + module.fail_json(msg=fail_message, **result) + finally: + file.close() + + result['changed'] = changed + + +def write_variable_to_file(module, result, name, value, filename): + """Write export definition of the variable to the file. Removes possible duplicates.""" + check_prerequisites(module, result) + global valid_shebang + + content = [] + env_entry_start = "export " + name + "=" + env_entry = env_entry_start + '"' + escape_quotes(value) + '"\n' + + if os.access(filename, os.F_OK): + content = read_file_lines(module, result, filename) + + if not is_valid_shebang(module, result, filename): + content.insert(0, valid_shebang) + + # remove all entries for current variable: + filtered_content = [] + for line in content: + if not line.startswith(env_entry_start): + filtered_content.append(line) + + content = filtered_content + content.append(env_entry) + + write_file_lines(module, result, filename, content) + result['changed'] = True + + else: + content.append(valid_shebang) + content.append(env_entry) + write_file_lines(module, result, filename, content) + try: + os.chmod(filename, 0o755) + result['changed'] = True + except: + fail_message = "Cannot change file '" + filename + "' permissions to 755." + module.fail_json(msg=fail_message, **result) + + +# ####################################################################################### +# ############### Helpers for exporting / unset vars to current session ############### +# ####################################################################################### + + +def import_variables_from_file(module, result, filename): + """Execute 'source' command with file parameter in order to import definitions to the current session.""" + check_prerequisites(module, result) + + result['changed'] = True + + load_variables_from_file = 'source "' + filename + '"' + run_command(module, result, load_variables_from_file) + + +def export_variable(module, result, name, value): + """Execute 'export' command in order to add exported definition.""" + check_prerequisites(module, result) + + result['changed'] = True + export_var = "export " + name + '="' + escape_quotes(value) + '"' + run_command(module, result, export_var) + + +# ####################################################################################### +# ########################## Main module functions #################################### +# ####################################################################################### + + +def present_flow(module, result, name, value, filename): + """Execute module flow for 'present' module mode.""" + check_prerequisites(module, result) + + current_value = get_env_value_or_empty(module, result, name) + result['old_value'] = current_value + + if value is not None: + + if is_env_present(module, result, name) and current_value == value: + module.exit_json(**result) + + result['changed'] = True + + if not module.check_mode: + write_variable_to_file(module, result, name, value, filename) + import_variables_from_file(module, result, filename) + export_variable(module, result, name, value) + + module.exit_json(**result) + + else: + if is_env_present(module, result, name): + module.exit_json(**result) + else: + module.fail_json("variable is not set!", **result) + + +def run_module(): + """Prepare parameter and return value object, select and execute proper module flow.""" + + module_args = dict( + state=dict(type='str', default='present', choices=['present']), + path=dict(type='str', default='/etc/profile.d/ansible_env_module_definitions.sh'), + name=dict(type='str', required=True), + value=dict(type='str'), + ) + + result = dict( + changed=False, + old_value='' + ) + + module = AnsibleModule( + argument_spec=module_args, + supports_check_mode=True + ) + + # state = module.params['state'] + name = module.params['name'] + value = module.params['value'] + path = module.params['path'] + + # arguments' reasonableness check + if value is None: + module.fail_json(msg="There should be 'value' set as well!", **result) + + present_flow(module, result, name, value, path) + + # flow functions should end module's execution; when otherwise, there is some ancient evil in here: + module.fail_json(msg="Unexpected execution path, this is a bug in this module!", **result) + + +def main(): + run_module() + + +if __name__ == '__main__': + main()