From 40f67414741b44a8c76c104825362aded869898b Mon Sep 17 00:00:00 2001 From: John Westcott IV Date: Mon, 3 Aug 2020 12:09:00 -0400 Subject: [PATCH 01/17] Adding import/export awx kit features Changed library structure Origional TowerModule becomes TowerLegacyModule TowerModule from tower_api becomes TowerAPIModule A real base TowerModule is created in tower_module.py A new TowerAWXKitModule is created in tower_awxkit TowerAWXKitModule and TowerAPIModule are child classes of TowerModule --- awx_collection/plugins/inventory/tower.py | 6 +- awx_collection/plugins/lookup/tower_api.py | 6 +- .../plugins/module_utils/tower_api.py | 242 +----------------- .../plugins/module_utils/tower_awxkit.py | 50 ++++ .../{ansible_tower.py => tower_legacy.py} | 4 +- .../plugins/module_utils/tower_module.py | 240 +++++++++++++++++ .../plugins/modules/tower_credential.py | 4 +- .../modules/tower_credential_input_source.py | 4 +- .../plugins/modules/tower_credential_type.py | 4 +- .../plugins/modules/tower_export.py | 155 +++++++++++ awx_collection/plugins/modules/tower_group.py | 4 +- awx_collection/plugins/modules/tower_host.py | 4 +- .../plugins/modules/tower_import.py | 95 +++++++ .../plugins/modules/tower_inventory.py | 4 +- .../plugins/modules/tower_inventory_source.py | 4 +- .../plugins/modules/tower_job_cancel.py | 4 +- .../plugins/modules/tower_job_launch.py | 4 +- .../plugins/modules/tower_job_list.py | 4 +- .../plugins/modules/tower_job_template.py | 4 +- .../plugins/modules/tower_job_wait.py | 4 +- awx_collection/plugins/modules/tower_label.py | 4 +- .../plugins/modules/tower_license.py | 4 +- awx_collection/plugins/modules/tower_meta.py | 4 +- .../plugins/modules/tower_notification.py | 4 +- .../plugins/modules/tower_organization.py | 4 +- .../plugins/modules/tower_project.py | 4 +- .../plugins/modules/tower_receive.py | 4 +- awx_collection/plugins/modules/tower_role.py | 4 +- .../plugins/modules/tower_schedule.py | 4 +- awx_collection/plugins/modules/tower_send.py | 4 +- .../plugins/modules/tower_settings.py | 4 +- awx_collection/plugins/modules/tower_team.py | 4 +- awx_collection/plugins/modules/tower_token.py | 4 +- awx_collection/plugins/modules/tower_user.py | 4 +- .../modules/tower_workflow_job_template.py | 4 +- .../tower_workflow_job_template_node.py | 4 +- .../plugins/modules/tower_workflow_launch.py | 4 +- .../modules/tower_workflow_template.py | 6 +- awx_collection/test/awx/conftest.py | 25 +- awx_collection/test/awx/test_module_utils.py | 18 +- .../roles/generate/templates/tower_module.j2 | 4 +- 41 files changed, 652 insertions(+), 315 deletions(-) create mode 100644 awx_collection/plugins/module_utils/tower_awxkit.py rename awx_collection/plugins/module_utils/{ansible_tower.py => tower_legacy.py} (97%) create mode 100644 awx_collection/plugins/module_utils/tower_module.py create mode 100644 awx_collection/plugins/modules/tower_export.py create mode 100644 awx_collection/plugins/modules/tower_import.py diff --git a/awx_collection/plugins/inventory/tower.py b/awx_collection/plugins/inventory/tower.py index 872e2a332897..7dc4aaa1a3fa 100644 --- a/awx_collection/plugins/inventory/tower.py +++ b/awx_collection/plugins/inventory/tower.py @@ -72,7 +72,7 @@ from ansible.plugins.inventory import BaseInventoryPlugin from ansible.config.manager import ensure_type -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule def handle_error(**kwargs): @@ -104,12 +104,12 @@ def parse(self, inventory, loader, path, cache=True): # Defer processing of params to logic shared with the modules module_params = {} - for plugin_param, module_param in TowerModule.short_params.items(): + for plugin_param, module_param in TowerAPIModule.short_params.items(): opt_val = self.get_option(plugin_param) if opt_val is not None: module_params[module_param] = opt_val - module = TowerModule( + module = TowerAPIModule( argument_spec={}, direct_params=module_params, error_callback=handle_error, warn_callback=self.warn_callback ) diff --git a/awx_collection/plugins/lookup/tower_api.py b/awx_collection/plugins/lookup/tower_api.py index 98295071258e..76b32be60a89 100644 --- a/awx_collection/plugins/lookup/tower_api.py +++ b/awx_collection/plugins/lookup/tower_api.py @@ -115,7 +115,7 @@ from ansible.errors import AnsibleError from ansible.module_utils._text import to_native from ansible.utils.display import Display -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule class LookupModule(LookupBase): @@ -133,13 +133,13 @@ def run(self, terms, variables=None, **kwargs): # Defer processing of params to logic shared with the modules module_params = {} - for plugin_param, module_param in TowerModule.short_params.items(): + for plugin_param, module_param in TowerAPIModule.short_params.items(): opt_val = self.get_option(plugin_param) if opt_val is not None: module_params[module_param] = opt_val # Create our module - module = TowerModule( + module = TowerAPIModule( argument_spec={}, direct_params=module_params, error_callback=self.handle_error, warn_callback=self.warn_callback ) diff --git a/awx_collection/plugins/module_utils/tower_api.py b/awx_collection/plugins/module_utils/tower_api.py index d0120ec00394..089273bff112 100644 --- a/awx_collection/plugins/module_utils/tower_api.py +++ b/awx_collection/plugins/module_utils/tower_api.py @@ -1,37 +1,17 @@ from __future__ import absolute_import, division, print_function __metaclass__ = type -from ansible.module_utils.basic import AnsibleModule, env_fallback +from . tower_module import TowerModule from ansible.module_utils.urls import Request, SSLValidationError, ConnectionError -from ansible.module_utils.six import PY2, string_types -from ansible.module_utils.six.moves import StringIO -from ansible.module_utils.six.moves.urllib.parse import urlparse, urlencode +from ansible.module_utils.six import PY2 +from ansible.module_utils.six.moves.urllib.parse import urlencode from ansible.module_utils.six.moves.urllib.error import HTTPError from ansible.module_utils.six.moves.http_cookiejar import CookieJar -from ansible.module_utils.six.moves.configparser import ConfigParser, NoOptionError -from socket import gethostbyname import re from json import loads, dumps -from os.path import isfile, expanduser, split, join, exists, isdir -from os import access, R_OK, getcwd -from distutils.util import strtobool -try: - import yaml - HAS_YAML = True -except ImportError: - HAS_YAML = False - - -class ConfigFileException(Exception): - pass - - -class ItemNotDefined(Exception): - pass - - -class TowerModule(AnsibleModule): +class TowerAPIModule(TowerModule): + # TODO: Move the collection version check into tower_module.py # This gets set by the make process so whatever is in here is irrelevant _COLLECTION_VERSION = "0.0.1-devel" _COLLECTION_TYPE = "awx" @@ -41,197 +21,15 @@ class TowerModule(AnsibleModule): 'awx': 'AWX', 'tower': 'Red Hat Ansible Tower', } - url = None - AUTH_ARGSPEC = dict( - tower_host=dict(required=False, fallback=(env_fallback, ['TOWER_HOST'])), - tower_username=dict(required=False, fallback=(env_fallback, ['TOWER_USERNAME'])), - tower_password=dict(no_log=True, required=False, fallback=(env_fallback, ['TOWER_PASSWORD'])), - validate_certs=dict(type='bool', aliases=['tower_verify_ssl'], required=False, fallback=(env_fallback, ['TOWER_VERIFY_SSL'])), - tower_oauthtoken=dict(type='raw', no_log=True, required=False, fallback=(env_fallback, ['TOWER_OAUTH_TOKEN'])), - tower_config_file=dict(type='path', required=False, default=None), - ) - short_params = { - 'host': 'tower_host', - 'username': 'tower_username', - 'password': 'tower_password', - 'verify_ssl': 'validate_certs', - 'oauth_token': 'tower_oauthtoken', - } - host = '127.0.0.1' - username = None - password = None - verify_ssl = True - oauth_token = None - oauth_token_id = None session = None cookie_jar = CookieJar() - authenticated = False - config_name = 'tower_cli.cfg' - ENCRYPTED_STRING = "$encrypted$" - version_checked = False - error_callback = None - warn_callback = None def __init__(self, argument_spec, direct_params=None, error_callback=None, warn_callback=None, **kwargs): - full_argspec = {} - full_argspec.update(TowerModule.AUTH_ARGSPEC) - full_argspec.update(argument_spec) kwargs['supports_check_mode'] = True - self.error_callback = error_callback - self.warn_callback = warn_callback - - self.json_output = {'changed': False} - - if direct_params is not None: - self.params = direct_params - else: - super(TowerModule, self).__init__(argument_spec=full_argspec, **kwargs) - - self.load_config_files() - - # Parameters specified on command line will override settings in any config - for short_param, long_param in self.short_params.items(): - direct_value = self.params.get(long_param) - if direct_value is not None: - setattr(self, short_param, direct_value) - - # Perform magic depending on whether tower_oauthtoken is a string or a dict - if self.params.get('tower_oauthtoken'): - token_param = self.params.get('tower_oauthtoken') - if type(token_param) is dict: - if 'token' in token_param: - self.oauth_token = self.params.get('tower_oauthtoken')['token'] - else: - self.fail_json(msg="The provided dict in tower_oauthtoken did not properly contain the token entry") - elif isinstance(token_param, string_types): - self.oauth_token = self.params.get('tower_oauthtoken') - else: - error_msg = "The provided tower_oauthtoken type was not valid ({0}). Valid options are str or dict.".format(type(token_param).__name__) - self.fail_json(msg=error_msg) - - # Perform some basic validation - if not re.match('^https{0,1}://', self.host): - self.host = "https://{0}".format(self.host) - - # Try to parse the hostname as a url - try: - self.url = urlparse(self.host) - except Exception as e: - self.fail_json(msg="Unable to parse tower_host as a URL ({1}): {0}".format(self.host, e)) - - # Try to resolve the hostname - hostname = self.url.netloc.split(':')[0] - try: - gethostbyname(hostname) - except Exception as e: - self.fail_json(msg="Unable to resolve tower_host ({1}): {0}".format(hostname, e)) - + super(TowerAPIModule, self).__init__(argument_spec=argument_spec, direct_params=direct_params, error_callback=error_callback, warn_callback=warn_callback, **kwargs) self.session = Request(cookies=CookieJar(), validate_certs=self.verify_ssl) - def load_config_files(self): - # Load configs like TowerCLI would have from least import to most - config_files = ['/etc/tower/tower_cli.cfg', join(expanduser("~"), ".{0}".format(self.config_name))] - local_dir = getcwd() - config_files.append(join(local_dir, self.config_name)) - while split(local_dir)[1]: - local_dir = split(local_dir)[0] - config_files.insert(2, join(local_dir, ".{0}".format(self.config_name))) - - # If we have a specified tower config, load it - if self.params.get('tower_config_file'): - duplicated_params = [ - fn for fn in self.AUTH_ARGSPEC - if fn != 'tower_config_file' and self.params.get(fn) is not None - ] - if duplicated_params: - self.warn(( - 'The parameter(s) {0} were provided at the same time as tower_config_file. ' - 'Precedence may be unstable, we suggest either using config file or params.' - ).format(', '.join(duplicated_params))) - try: - # TODO: warn if there are conflicts with other params - self.load_config(self.params.get('tower_config_file')) - except ConfigFileException as cfe: - # Since we were told specifically to load this we want it to fail if we have an error - self.fail_json(msg=cfe) - else: - for config_file in config_files: - if exists(config_file) and not isdir(config_file): - # Only throw a formatting error if the file exists and is not a directory - try: - self.load_config(config_file) - except ConfigFileException: - self.fail_json(msg='The config file {0} is not properly formatted'.format(config_file)) - - def load_config(self, config_path): - # Validate the config file is an actual file - if not isfile(config_path): - raise ConfigFileException('The specified config file does not exist') - - if not access(config_path, R_OK): - raise ConfigFileException("The specified config file cannot be read") - - # Read in the file contents: - with open(config_path, 'r') as f: - config_string = f.read() - - # First try to yaml load the content (which will also load json) - try: - try_config_parsing = True - if HAS_YAML: - try: - config_data = yaml.load(config_string, Loader=yaml.SafeLoader) - # If this is an actual ini file, yaml will return the whole thing as a string instead of a dict - if type(config_data) is not dict: - raise AssertionError("The yaml config file is not properly formatted as a dict.") - try_config_parsing = False - - except(AttributeError, yaml.YAMLError, AssertionError): - try_config_parsing = True - - if try_config_parsing: - # TowerCLI used to support a config file with a missing [general] section by prepending it if missing - if '[general]' not in config_string: - config_string = '[general]\n{0}'.format(config_string) - - config = ConfigParser() - - try: - placeholder_file = StringIO(config_string) - # py2 ConfigParser has readfp, that has been deprecated in favor of read_file in py3 - # This "if" removes the deprecation warning - if hasattr(config, 'read_file'): - config.read_file(placeholder_file) - else: - config.readfp(placeholder_file) - - # If we made it here then we have values from reading the ini file, so let's pull them out into a dict - config_data = {} - for honorred_setting in self.short_params: - try: - config_data[honorred_setting] = config.get('general', honorred_setting) - except NoOptionError: - pass - - except Exception as e: - raise ConfigFileException("An unknown exception occured trying to ini load config file: {0}".format(e)) - - except Exception as e: - raise ConfigFileException("An unknown exception occured trying to load config file: {0}".format(e)) - - # If we made it here, we have a dict which has values in it from our config, any final settings logic can be performed here - for honorred_setting in self.short_params: - if honorred_setting in config_data: - # Veriffy SSL must be a boolean - if honorred_setting == 'verify_ssl': - if type(config_data[honorred_setting]) is str: - setattr(self, honorred_setting, strtobool(config_data[honorred_setting])) - else: - setattr(self, honorred_setting, bool(config_data[honorred_setting])) - else: - setattr(self, honorred_setting, config_data[honorred_setting]) - @staticmethod def param_to_endpoint(name): exceptions = { @@ -650,13 +448,13 @@ def has_encrypted_values(obj): """ if isinstance(obj, dict): for val in obj.values(): - if TowerModule.has_encrypted_values(val): + if TowerAPIModule.has_encrypted_values(val): return True elif isinstance(obj, list): for val in obj: - if TowerModule.has_encrypted_values(val): + if TowerAPIModule.has_encrypted_values(val): return True - elif obj == TowerModule.ENCRYPTED_STRING: + elif obj == TowerAPIModule.ENCRYPTED_STRING: return True return False @@ -678,10 +476,9 @@ def update_if_needed(self, existing_item, new_item, on_update=None, associations # This will exit from the module on its own # If the method successfully updates an item and on_update param is defined, # the on_update parameter will be called as a method pasing in this object and the json from the response - # This will return one of three things: + # This will return one of two things: # 1. None if the existing_item does not need to be updated # 2. The response from Tower from patching to the endpoint. It's up to you to process the response and exit from the module. - # 3. An ItemNotDefined exception, if the existing_item does not exist # Note: common error codes from the Tower API can cause the module to fail response = None if existing_item: @@ -777,25 +574,6 @@ def logout(self): # Sanity check: Did the server send back some kind of internal error? self.warn('Failed to release tower token {0}: {1}'.format(self.oauth_token_id, e)) - def fail_json(self, **kwargs): - # Try to log out if we are authenticated - self.logout() - if self.error_callback: - self.error_callback(**kwargs) - else: - super(TowerModule, self).fail_json(**kwargs) - - def exit_json(self, **kwargs): - # Try to log out if we are authenticated - self.logout() - super(TowerModule, self).exit_json(**kwargs) - - def warn(self, warning): - if self.warn_callback is not None: - self.warn_callback(warning) - else: - super(TowerModule, self).warn(warning) - def is_job_done(self, job_status): if job_status in ['new', 'pending', 'waiting', 'running']: return False diff --git a/awx_collection/plugins/module_utils/tower_awxkit.py b/awx_collection/plugins/module_utils/tower_awxkit.py new file mode 100644 index 000000000000..eedf3a738785 --- /dev/null +++ b/awx_collection/plugins/module_utils/tower_awxkit.py @@ -0,0 +1,50 @@ +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +from . tower_module import TowerModule +from ansible.module_utils.basic import missing_required_lib + +try: + from awxkit.api.client import Connection + from awxkit.api.pages.api import ApiV2 + from awxkit.api import get_registered_page + HAS_AWX_KIT = True +except ImportError: + HAS_AWX_KIT = False + + +class TowerAWXKitModule(TowerModule): + connection = None + apiV2Ref = None + + def __init__(self, argument_spec, **kwargs): + kwargs['supports_check_mode'] = False + + super(TowerAWXKitModule, self).__init__(argument_spec=argument_spec, **kwargs) + + # Die if we don't have AWX_KIT installed + if not HAS_AWX_KIT: + self.exit_module(msg=missing_required_lib('awxkit')) + + # Establish our conneciton object + self.connection = Connection(self.host, verify=self.verify_ssl) + + def authenticate(self): + try: + self.connection.login(username=self.username, password=self.password, token=self.oauth_token) + # If we have neither of these, then we can try un-authenticated access + self.authenticated = True + except Exception: + self.exit_module("Failed to authenticate") + + def get_api_v2_object(self): + if not self.apiV2Ref: + if not self.authenticated: + self.authenticate() + v2_index = get_registered_page('/api/v2/')(self.connection).get() + self.api_ref = ApiV2(connection=self.connection, **{'json': v2_index}) + return self.api_ref + + def logout(self): + if self.authenticated: + self.connection.logout() diff --git a/awx_collection/plugins/module_utils/ansible_tower.py b/awx_collection/plugins/module_utils/tower_legacy.py similarity index 97% rename from awx_collection/plugins/module_utils/ansible_tower.py rename to awx_collection/plugins/module_utils/tower_legacy.py index 17d6a386808d..3c8408610d1a 100644 --- a/awx_collection/plugins/module_utils/ansible_tower.py +++ b/awx_collection/plugins/module_utils/tower_legacy.py @@ -91,7 +91,7 @@ def tower_check_mode(module): module.fail_json(changed=False, msg='Failed check mode: {0}'.format(excinfo)) -class TowerModule(AnsibleModule): +class TowerLegacyModule(AnsibleModule): def __init__(self, argument_spec, **kwargs): args = dict( tower_host=dict(), @@ -110,7 +110,7 @@ def __init__(self, argument_spec, **kwargs): ('tower_config_file', 'validate_certs'), )) - super(TowerModule, self).__init__(argument_spec=args, **kwargs) + super(TowerLegacyModule, self).__init__(argument_spec=args, **kwargs) if not HAS_TOWER_CLI: self.fail_json(msg=missing_required_lib('ansible-tower-cli'), diff --git a/awx_collection/plugins/module_utils/tower_module.py b/awx_collection/plugins/module_utils/tower_module.py new file mode 100644 index 000000000000..5d5b4d4239c5 --- /dev/null +++ b/awx_collection/plugins/module_utils/tower_module.py @@ -0,0 +1,240 @@ +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +from ansible.module_utils.basic import AnsibleModule, env_fallback +from ansible.module_utils.six import PY2, string_types +from ansible.module_utils.six.moves import StringIO +from ansible.module_utils.six.moves.urllib.parse import urlparse +from ansible.module_utils.six.moves.configparser import ConfigParser, NoOptionError +from socket import gethostbyname +import re +from os.path import isfile, expanduser, split, join, exists, isdir +from os import access, R_OK, getcwd +from distutils.util import strtobool + +try: + import yaml + HAS_YAML = True +except ImportError: + HAS_YAML = False + + +class ConfigFileException(Exception): + pass + + +class ItemNotDefined(Exception): + pass + + +class TowerModule(AnsibleModule): + url = None + AUTH_ARGSPEC = dict( + tower_host=dict(required=False, fallback=(env_fallback, ['TOWER_HOST'])), + tower_username=dict(required=False, fallback=(env_fallback, ['TOWER_USERNAME'])), + tower_password=dict(no_log=True, required=False, fallback=(env_fallback, ['TOWER_PASSWORD'])), + validate_certs=dict(type='bool', aliases=['tower_verify_ssl'], required=False, fallback=(env_fallback, ['TOWER_VERIFY_SSL'])), + tower_oauthtoken=dict(type='raw', no_log=True, required=False, fallback=(env_fallback, ['TOWER_OAUTH_TOKEN'])), + tower_config_file=dict(type='path', required=False, default=None), + ) + short_params = { + 'host': 'tower_host', + 'username': 'tower_username', + 'password': 'tower_password', + 'verify_ssl': 'validate_certs', + 'oauth_token': 'tower_oauthtoken', + } + host = '127.0.0.1' + username = None + password = None + verify_ssl = True + oauth_token = None + oauth_token_id = None + authenticated = False + config_name = 'tower_cli.cfg' + ENCRYPTED_STRING = "$encrypted$" + version_checked = False + error_callback = None + warn_callback = None + + def __init__(self, argument_spec=None, direct_params=None, error_callback=None, warn_callback=None, **kwargs): + full_argspec = {} + full_argspec.update(TowerModule.AUTH_ARGSPEC) + full_argspec.update(argument_spec) + kwargs['supports_check_mode'] = True + + self.error_callback = error_callback + self.warn_callback = warn_callback + + self.json_output = {'changed': False} + + if direct_params is not None: + self.params = direct_params + else: + super(TowerModule, self).__init__(argument_spec=full_argspec, **kwargs) + + self.load_config_files() + + # Parameters specified on command line will override settings in any config + for short_param, long_param in self.short_params.items(): + direct_value = self.params.get(long_param) + if direct_value is not None: + setattr(self, short_param, direct_value) + + # Perform magic depending on whether tower_oauthtoken is a string or a dict + if self.params.get('tower_oauthtoken'): + token_param = self.params.get('tower_oauthtoken') + if type(token_param) is dict: + if 'token' in token_param: + self.oauth_token = self.params.get('tower_oauthtoken')['token'] + else: + self.fail_json(msg="The provided dict in tower_oauthtoken did not properly contain the token entry") + elif isinstance(token_param, string_types): + self.oauth_token = self.params.get('tower_oauthtoken') + else: + error_msg = "The provided tower_oauthtoken type was not valid ({0}). Valid options are str or dict.".format(type(token_param).__name__) + self.fail_json(msg=error_msg) + + # Perform some basic validation + if not re.match('^https{0,1}://', self.host): + self.host = "https://{0}".format(self.host) + + # Try to parse the hostname as a url + try: + self.url = urlparse(self.host) + except Exception as e: + self.fail_json(msg="Unable to parse tower_host as a URL ({1}): {0}".format(self.host, e)) + + # Try to resolve the hostname + hostname = self.url.netloc.split(':')[0] + try: + gethostbyname(hostname) + except Exception as e: + self.fail_json(msg="Unable to resolve tower_host ({1}): {0}".format(hostname, e)) + + def load_config_files(self): + # Load configs like TowerCLI would have from least import to most + config_files = ['/etc/tower/tower_cli.cfg', join(expanduser("~"), ".{0}".format(self.config_name))] + local_dir = getcwd() + config_files.append(join(local_dir, self.config_name)) + while split(local_dir)[1]: + local_dir = split(local_dir)[0] + config_files.insert(2, join(local_dir, ".{0}".format(self.config_name))) + + # If we have a specified tower config, load it + if self.params.get('tower_config_file'): + duplicated_params = [ + fn for fn in self.AUTH_ARGSPEC + if fn != 'tower_config_file' and self.params.get(fn) is not None + ] + if duplicated_params: + self.warn(( + 'The parameter(s) {0} were provided at the same time as tower_config_file. ' + 'Precedence may be unstable, we suggest either using config file or params.' + ).format(', '.join(duplicated_params))) + try: + # TODO: warn if there are conflicts with other params + self.load_config(self.params.get('tower_config_file')) + except ConfigFileException as cfe: + # Since we were told specifically to load this we want it to fail if we have an error + self.fail_json(msg=cfe) + else: + for config_file in config_files: + if exists(config_file) and not isdir(config_file): + # Only throw a formatting error if the file exists and is not a directory + try: + self.load_config(config_file) + except ConfigFileException: + self.fail_json(msg='The config file {0} is not properly formatted'.format(config_file)) + + def load_config(self, config_path): + # Validate the config file is an actual file + if not isfile(config_path): + raise ConfigFileException('The specified config file does not exist') + + if not access(config_path, R_OK): + raise ConfigFileException("The specified config file cannot be read") + + # Read in the file contents: + with open(config_path, 'r') as f: + config_string = f.read() + + # First try to yaml load the content (which will also load json) + try: + try_config_parsing = True + if HAS_YAML: + try: + config_data = yaml.load(config_string, Loader=yaml.SafeLoader) + # If this is an actual ini file, yaml will return the whole thing as a string instead of a dict + if type(config_data) is not dict: + raise AssertionError("The yaml config file is not properly formatted as a dict.") + try_config_parsing = False + + except(AttributeError, yaml.YAMLError, AssertionError): + try_config_parsing = True + + if try_config_parsing: + # TowerCLI used to support a config file with a missing [general] section by prepending it if missing + if '[general]' not in config_string: + config_string = '[general]\n{0}'.format(config_string) + + config = ConfigParser() + + try: + placeholder_file = StringIO(config_string) + # py2 ConfigParser has readfp, that has been deprecated in favor of read_file in py3 + # This "if" removes the deprecation warning + if hasattr(config, 'read_file'): + config.read_file(placeholder_file) + else: + config.readfp(placeholder_file) + + # If we made it here then we have values from reading the ini file, so let's pull them out into a dict + config_data = {} + for honorred_setting in self.short_params: + try: + config_data[honorred_setting] = config.get('general', honorred_setting) + except NoOptionError: + pass + + except Exception as e: + raise ConfigFileException("An unknown exception occured trying to ini load config file: {0}".format(e)) + + except Exception as e: + raise ConfigFileException("An unknown exception occured trying to load config file: {0}".format(e)) + + # If we made it here, we have a dict which has values in it from our config, any final settings logic can be performed here + for honorred_setting in self.short_params: + if honorred_setting in config_data: + # Veriffy SSL must be a boolean + if honorred_setting == 'verify_ssl': + if type(config_data[honorred_setting]) is str: + setattr(self, honorred_setting, strtobool(config_data[honorred_setting])) + else: + setattr(self, honorred_setting, bool(config_data[honorred_setting])) + else: + setattr(self, honorred_setting, config_data[honorred_setting]) + + + def logout(self): + # This method is intended to be overridden + pass + + def fail_json(self, **kwargs): + # Try to log out if we are authenticated + self.logout() + if self.error_callback: + self.error_callback(**kwargs) + else: + super(TowerModule, self).fail_json(**kwargs) + + def exit_json(self, **kwargs): + # Try to log out if we are authenticated + self.logout() + super(TowerModule, self).exit_json(**kwargs) + + def warn(self, warning): + if self.warn_callback is not None: + self.warn_callback(warning) + else: + super(TowerModule, self).warn(warning) diff --git a/awx_collection/plugins/modules/tower_credential.py b/awx_collection/plugins/modules/tower_credential.py index 97a801aa8ff0..2d4cbd33313b 100644 --- a/awx_collection/plugins/modules/tower_credential.py +++ b/awx_collection/plugins/modules/tower_credential.py @@ -269,7 +269,7 @@ ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule KIND_CHOICES = { 'ssh': 'Machine', @@ -336,7 +336,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec, required_one_of=[['kind', 'credential_type']]) + module = TowerAPIModule(argument_spec=argument_spec, required_one_of=[['kind', 'credential_type']]) # Extract our parameters name = module.params.get('name') diff --git a/awx_collection/plugins/modules/tower_credential_input_source.py b/awx_collection/plugins/modules/tower_credential_input_source.py index bc2cb855790d..cdc55cb1f04b 100644 --- a/awx_collection/plugins/modules/tower_credential_input_source.py +++ b/awx_collection/plugins/modules/tower_credential_input_source.py @@ -70,7 +70,7 @@ ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule def main(): @@ -85,7 +85,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec) + module = TowerAPIModule(argument_spec=argument_spec) # Extract our parameters description = module.params.get('description') diff --git a/awx_collection/plugins/modules/tower_credential_type.py b/awx_collection/plugins/modules/tower_credential_type.py index 561ae78f5a60..53f0cc45c9be 100644 --- a/awx_collection/plugins/modules/tower_credential_type.py +++ b/awx_collection/plugins/modules/tower_credential_type.py @@ -81,7 +81,7 @@ RETURN = ''' # ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule KIND_CHOICES = { 'ssh': 'Machine', @@ -105,7 +105,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec) + module = TowerAPIModule(argument_spec=argument_spec) # Extract our parameters name = module.params.get('name') diff --git a/awx_collection/plugins/modules/tower_export.py b/awx_collection/plugins/modules/tower_export.py new file mode 100644 index 000000000000..84c08fe4545b --- /dev/null +++ b/awx_collection/plugins/modules/tower_export.py @@ -0,0 +1,155 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# (c) 2017, John Westcott IV +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + + +DOCUMENTATION = ''' +--- +module: tower_export +author: "John Westcott IV (@john-westcott-iv)" +version_added: "3.7" +short_description: export resources from Ansible Tower. +description: + - Export assets from Ansible Tower. +options: + all: + description: + - Export all assets + type: bool + default: 'False' + organizations: + description: + - organization name to export + default: '' + type: str + user: + description: + - user name to export + default: '' + type: str + team: + description: + - team name to export + default: '' + type: str + credential_type: + description: + - credential type name to export + default: '' + type: str + credential: + description: + - credential name to export + default: '' + type: str + notification_template: + description: + - notification template name to export + default: '' + type: str + inventory_script: + description: + - inventory script name to export + default: '' + type: str + inventory: + description: + - inventory name to export + default: '' + type: str + project: + description: + - project name to export + default: '' + type: str + job_template: + description: + - job template name to export + default: '' + type: str + workflow: + description: + - workflow name to export + default: '' + type: str +requirements: + - "awxkit >= 9.3.0" +notes: + - Specifying a name of "all" for any asset type will export all items of that asset type. +extends_documentation_fragment: awx.awx.auth +''' + +EXAMPLES = ''' +- name: Export all tower assets + tower_export: + all: True +- name: Export all inventories + tower_export: + inventory: 'all' +- name: Export a job template named "My Template" and all Credentials + tower_export: + job_template: "My Template" + credential: 'all' +''' + +from os import environ + +from ..module_utils.tower_awxkit import TowerAWXKitModule + +try: + from awxkit.api.pages.api import EXPORTABLE_RESOURCES + HAS_EXPORTABLE_RESOURCES=True +except ImportError: + HAS_EXPORTABLE_RESOURCES=False + + +def main(): + argument_spec = dict( + all=dict(type='bool', default=False), + ) + + # We are not going to raise an error here because the __init__ method of TowerAWXKitModule will do that for us + if HAS_EXPORTABLE_RESOURCES: + for resource in EXPORTABLE_RESOURCES: + argument_spec[resource] = dict() + + module = TowerAWXKitModule(argument_spec=argument_spec) + + if not HAS_EXPORTABLE_RESOURCES: + module.fail_json(msg="Your version of awxkit does not have import/export") + + # The export process will never change a Tower system + module.json_output['changed'] = False + + # The exporter code currently works like the following: + # Empty list == all assets of that type + # string = just one asset of that type (by name) + # None = skip asset type + # Here we are going to setup a dict of values to export + export_args = {} + for resource in EXPORTABLE_RESOURCES: + if module.params.get('all') or module.params.get(resource) == 'all': + # If we are exporting everything or we got the keyword "all" we pass in an empty list for this asset type + export_args[resource] = [] + else: + # Otherwise we take either the string or None (if the parameter was not passed) to get one or no items + export_args[resource] = module.params.get(resource) + + # Run the export process + module.json_output['assets'] = module.get_api_v2_object().export_assets(**export_args) + + module.exit_json(**module.json_output) + + +if __name__ == '__main__': + main() diff --git a/awx_collection/plugins/modules/tower_group.py b/awx_collection/plugins/modules/tower_group.py index a64826eb889f..9e2eaf4e7e87 100644 --- a/awx_collection/plugins/modules/tower_group.py +++ b/awx_collection/plugins/modules/tower_group.py @@ -76,7 +76,7 @@ tower_config_file: "~/tower_cli.cfg" ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule import json @@ -94,7 +94,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec) + module = TowerAPIModule(argument_spec=argument_spec) # Extract our parameters name = module.params.get('name') diff --git a/awx_collection/plugins/modules/tower_host.py b/awx_collection/plugins/modules/tower_host.py index cb4712a27ca7..f6bfe554043a 100644 --- a/awx_collection/plugins/modules/tower_host.py +++ b/awx_collection/plugins/modules/tower_host.py @@ -72,7 +72,7 @@ ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule import json @@ -89,7 +89,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec) + module = TowerAPIModule(argument_spec=argument_spec) # Extract our parameters name = module.params.get('name') diff --git a/awx_collection/plugins/modules/tower_import.py b/awx_collection/plugins/modules/tower_import.py new file mode 100644 index 000000000000..0bb5dec75a42 --- /dev/null +++ b/awx_collection/plugins/modules/tower_import.py @@ -0,0 +1,95 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# (c) 2017, John Westcott IV +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + + +DOCUMENTATION = ''' +--- +module: tower_import +author: "John Westcott (@john-westcott-iv)" +version_added: "3.7" +short_description: import resources into Ansible Tower. +description: + - Import assets into Ansible Tower. See + U(https://www.ansible.com/tower) for an overview. +options: + assets: + description: + - The assets to import. + - This can be the output of tower_export or loaded from a file + required: True + type: dict +requirements: + - "awxkit >= 9.3.0" +extends_documentation_fragment: awx.awx.auth +''' + +EXAMPLES = ''' +- name: Import all tower assets + tower_import: + assets: "{{ export_output.assets }}" +''' + +from ..module_utils.tower_awxkit import TowerAWXKitModule + +# These two lines are not needed if awxkit changes to do progamatic notifications on issues +from ansible.module_utils.six.moves import StringIO +import logging + +# In this module we don't use EXPORTABLE_RESOURCES, we just want to validate that our installed awxkit has import/export +try: + from awxkit.api.pages.api import EXPORTABLE_RESOURCES + HAS_EXPORTABLE_RESOURCES=True +except ImportError: + HAS_EXPORTABLE_RESOURCES=False + +def main(): + argument_spec = dict( + assets=dict(type='dict', required=True) + ) + + module = TowerAWXKitModule(argument_spec=argument_spec, supports_check_mode=False) + + assets = module.params.get('assets') + + if not HAS_EXPORTABLE_RESOURCES: + module.fail_json(msg="Your version of awxkit does not appear to have import/export") + + # Currently the import process does not return anything on error + # It simply just logs to pythons logger + # Setup a log gobbler to get error messages from import_assets + logger = logging.getLogger('awxkit.api.pages.api') + logger.setLevel(logging.WARNING) + log_capture_string = StringIO() + ch = logging.StreamHandler(log_capture_string) + ch.setLevel(logging.WARNING) + logger.addHandler(ch) + log_contents = '' + + # Run the import process + try: + module.json_output['changed'] = module.get_api_v2_object().import_assets(assets) + except Exception as e: + module.fail_json(msg="Failed to import assets {0}".format(e)) + finally: + # Finally consume the logs incase there were any errors and die if there were + log_contents = log_capture_string.getvalue() + log_capture_string.close() + if log_contents != '': + module.fail_json(msg=log_contents) + + module.exit_json(**module.json_output) + + +if __name__ == '__main__': + main() diff --git a/awx_collection/plugins/modules/tower_inventory.py b/awx_collection/plugins/modules/tower_inventory.py index d0ced630481e..7f03645e016a 100644 --- a/awx_collection/plugins/modules/tower_inventory.py +++ b/awx_collection/plugins/modules/tower_inventory.py @@ -71,7 +71,7 @@ ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule import json @@ -88,7 +88,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec) + module = TowerAPIModule(argument_spec=argument_spec) # Extract our parameters name = module.params.get('name') diff --git a/awx_collection/plugins/modules/tower_inventory_source.py b/awx_collection/plugins/modules/tower_inventory_source.py index afa6c229e24a..5b0e2961dffa 100644 --- a/awx_collection/plugins/modules/tower_inventory_source.py +++ b/awx_collection/plugins/modules/tower_inventory_source.py @@ -144,7 +144,7 @@ private: false ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule from json import dumps @@ -184,7 +184,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec) + module = TowerAPIModule(argument_spec=argument_spec) # Extract our parameters name = module.params.get('name') diff --git a/awx_collection/plugins/modules/tower_job_cancel.py b/awx_collection/plugins/modules/tower_job_cancel.py index 5e82834f6c94..7404d452a477 100644 --- a/awx_collection/plugins/modules/tower_job_cancel.py +++ b/awx_collection/plugins/modules/tower_job_cancel.py @@ -50,7 +50,7 @@ ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule def main(): @@ -61,7 +61,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec) + module = TowerAPIModule(argument_spec=argument_spec) # Extract our parameters job_id = module.params.get('job_id') diff --git a/awx_collection/plugins/modules/tower_job_launch.py b/awx_collection/plugins/modules/tower_job_launch.py index f3447bf24c60..25a1c52fdfc4 100644 --- a/awx_collection/plugins/modules/tower_job_launch.py +++ b/awx_collection/plugins/modules/tower_job_launch.py @@ -124,7 +124,7 @@ sample: pending ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule def main(): @@ -146,7 +146,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec) + module = TowerAPIModule(argument_spec=argument_spec) optional_args = {} # Extract our parameters diff --git a/awx_collection/plugins/modules/tower_job_list.py b/awx_collection/plugins/modules/tower_job_list.py index 2ecfd9d98af8..642a48b03b15 100644 --- a/awx_collection/plugins/modules/tower_job_list.py +++ b/awx_collection/plugins/modules/tower_job_list.py @@ -80,7 +80,7 @@ ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule def main(): @@ -93,7 +93,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule( + module = TowerAPIModule( argument_spec=argument_spec, mutually_exclusive=[ ('page', 'all_pages'), diff --git a/awx_collection/plugins/modules/tower_job_template.py b/awx_collection/plugins/modules/tower_job_template.py index 1f1d776f2892..1f12d5aaddf2 100644 --- a/awx_collection/plugins/modules/tower_job_template.py +++ b/awx_collection/plugins/modules/tower_job_template.py @@ -317,7 +317,7 @@ ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule import json @@ -388,7 +388,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec) + module = TowerAPIModule(argument_spec=argument_spec) # Extract our parameters name = module.params.get('name') diff --git a/awx_collection/plugins/modules/tower_job_wait.py b/awx_collection/plugins/modules/tower_job_wait.py index 77a6977c5cbd..5e5801b25fbb 100644 --- a/awx_collection/plugins/modules/tower_job_wait.py +++ b/awx_collection/plugins/modules/tower_job_wait.py @@ -92,7 +92,7 @@ ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule import time @@ -120,7 +120,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec) + module = TowerAPIModule(argument_spec=argument_spec) # Extract our parameters job_id = module.params.get('job_id') diff --git a/awx_collection/plugins/modules/tower_label.py b/awx_collection/plugins/modules/tower_label.py index d0820d93a80a..6a3a8288a2eb 100644 --- a/awx_collection/plugins/modules/tower_label.py +++ b/awx_collection/plugins/modules/tower_label.py @@ -54,7 +54,7 @@ organization: My Organization ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule def main(): @@ -67,7 +67,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec) + module = TowerAPIModule(argument_spec=argument_spec) # Extract our parameters name = module.params.get('name') diff --git a/awx_collection/plugins/modules/tower_license.py b/awx_collection/plugins/modules/tower_license.py index a1d9840d50e8..25d337cc24e7 100644 --- a/awx_collection/plugins/modules/tower_license.py +++ b/awx_collection/plugins/modules/tower_license.py @@ -43,12 +43,12 @@ eula_accepted: True ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule def main(): - module = TowerModule( + module = TowerAPIModule( argument_spec=dict( data=dict(type='dict', required=True), eula_accepted=dict(type='bool', required=True), diff --git a/awx_collection/plugins/modules/tower_meta.py b/awx_collection/plugins/modules/tower_meta.py index 6d5c801adedc..9455bdf0f4fb 100644 --- a/awx_collection/plugins/modules/tower_meta.py +++ b/awx_collection/plugins/modules/tower_meta.py @@ -62,11 +62,11 @@ ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule def main(): - module = TowerModule(argument_spec={}) + module = TowerAPIModule(argument_spec={}) namespace = { 'awx': 'awx', 'tower': 'ansible' diff --git a/awx_collection/plugins/modules/tower_notification.py b/awx_collection/plugins/modules/tower_notification.py index bfe672a50e91..12dd28ef3194 100644 --- a/awx_collection/plugins/modules/tower_notification.py +++ b/awx_collection/plugins/modules/tower_notification.py @@ -300,7 +300,7 @@ RETURN = ''' # ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule OLD_INPUT_NAMES = ( 'username', 'sender', 'recipients', 'use_tls', @@ -355,7 +355,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec) + module = TowerAPIModule(argument_spec=argument_spec) # Extract our parameters name = module.params.get('name') diff --git a/awx_collection/plugins/modules/tower_organization.py b/awx_collection/plugins/modules/tower_organization.py index fbbbf2885c23..163782814904 100644 --- a/awx_collection/plugins/modules/tower_organization.py +++ b/awx_collection/plugins/modules/tower_organization.py @@ -88,7 +88,7 @@ tower_config_file: "~/tower_cli.cfg" ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule def main(): @@ -106,7 +106,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec) + module = TowerAPIModule(argument_spec=argument_spec) # Extract our parameters name = module.params.get('name') diff --git a/awx_collection/plugins/modules/tower_project.py b/awx_collection/plugins/modules/tower_project.py index 12f9e2809c97..36a4f8666a06 100644 --- a/awx_collection/plugins/modules/tower_project.py +++ b/awx_collection/plugins/modules/tower_project.py @@ -157,7 +157,7 @@ import time -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule def wait_for_project_update(module, last_request): @@ -205,7 +205,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec) + module = TowerAPIModule(argument_spec=argument_spec) # Extract our parameters name = module.params.get('name') diff --git a/awx_collection/plugins/modules/tower_receive.py b/awx_collection/plugins/modules/tower_receive.py index b673e9b81dfd..bd08682503b3 100644 --- a/awx_collection/plugins/modules/tower_receive.py +++ b/awx_collection/plugins/modules/tower_receive.py @@ -134,7 +134,7 @@ sample: [ {}, {} ] ''' -from ..module_utils.ansible_tower import TowerModule, tower_auth_config, HAS_TOWER_CLI +from ..module_utils.tower_legacy import TowerLegacyModule, tower_auth_config, HAS_TOWER_CLI try: from tower_cli.cli.transfer.receive import Receiver @@ -163,7 +163,7 @@ def main(): workflow=dict(type='list', default=[], elements='str'), ) - module = TowerModule(argument_spec=argument_spec, supports_check_mode=False) + module = TowerLegacyModule(argument_spec=argument_spec, supports_check_mode=False) module.deprecate(msg="This module is deprecated and will be replaced by the AWX CLI export command.", version="awx.awx:14.0.0") diff --git a/awx_collection/plugins/modules/tower_role.py b/awx_collection/plugins/modules/tower_role.py index 4cba215b0a86..d0d010a0a786 100644 --- a/awx_collection/plugins/modules/tower_role.py +++ b/awx_collection/plugins/modules/tower_role.py @@ -89,7 +89,7 @@ state: present ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule def main(): @@ -109,7 +109,7 @@ def main(): state=dict(choices=['present', 'absent'], default='present'), ) - module = TowerModule(argument_spec=argument_spec) + module = TowerAPIModule(argument_spec=argument_spec) role_type = module.params.pop('role') role_field = role_type + '_role' diff --git a/awx_collection/plugins/modules/tower_schedule.py b/awx_collection/plugins/modules/tower_schedule.py index 24f8468e4a66..4922aaa688e9 100644 --- a/awx_collection/plugins/modules/tower_schedule.py +++ b/awx_collection/plugins/modules/tower_schedule.py @@ -136,7 +136,7 @@ register: result ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule def main(): @@ -161,7 +161,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec) + module = TowerAPIModule(argument_spec=argument_spec) # Extract our parameters rrule = module.params.get('rrule') diff --git a/awx_collection/plugins/modules/tower_send.py b/awx_collection/plugins/modules/tower_send.py index 7ac60ece595f..772b2b67ec36 100644 --- a/awx_collection/plugins/modules/tower_send.py +++ b/awx_collection/plugins/modules/tower_send.py @@ -81,7 +81,7 @@ import sys from ansible.module_utils.six.moves import StringIO -from ..module_utils.ansible_tower import TowerModule, tower_auth_config, HAS_TOWER_CLI +from ..module_utils.tower_legacy import TowerLegacyModule, tower_auth_config, HAS_TOWER_CLI from tempfile import mkstemp @@ -103,7 +103,7 @@ def main(): password_management=dict(default='default', choices=['default', 'random']), ) - module = TowerModule(argument_spec=argument_spec, supports_check_mode=False) + module = TowerLegacyModule(argument_spec=argument_spec, supports_check_mode=False) module.deprecate(msg="This module is deprecated and will be replaced by the AWX CLI import command", version="awx.awx:14.0.0") diff --git a/awx_collection/plugins/modules/tower_settings.py b/awx_collection/plugins/modules/tower_settings.py index 9db41d9975e3..c2e8ed1ae5ed 100644 --- a/awx_collection/plugins/modules/tower_settings.py +++ b/awx_collection/plugins/modules/tower_settings.py @@ -70,7 +70,7 @@ last_name: "surname" ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule try: import yaml @@ -111,7 +111,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule( + module = TowerAPIModule( argument_spec=argument_spec, required_one_of=[['name', 'settings']], mutually_exclusive=[['name', 'settings']], diff --git a/awx_collection/plugins/modules/tower_team.py b/awx_collection/plugins/modules/tower_team.py index e1506b2425cd..8ed56e48dc89 100644 --- a/awx_collection/plugins/modules/tower_team.py +++ b/awx_collection/plugins/modules/tower_team.py @@ -60,7 +60,7 @@ tower_config_file: "~/tower_cli.cfg" ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule def main(): @@ -74,7 +74,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec) + module = TowerAPIModule(argument_spec=argument_spec) # Extract our parameters name = module.params.get('name') diff --git a/awx_collection/plugins/modules/tower_token.py b/awx_collection/plugins/modules/tower_token.py index 165590520d9b..ee6fd5c2008b 100644 --- a/awx_collection/plugins/modules/tower_token.py +++ b/awx_collection/plugins/modules/tower_token.py @@ -117,7 +117,7 @@ returned: on successful create ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule def return_token(module, last_response): @@ -143,7 +143,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule( + module = TowerAPIModule( argument_spec=argument_spec, mutually_exclusive=[ ('existing_token', 'existing_token_id'), diff --git a/awx_collection/plugins/modules/tower_user.py b/awx_collection/plugins/modules/tower_user.py index 7d049de016a2..15c41cb081fe 100644 --- a/awx_collection/plugins/modules/tower_user.py +++ b/awx_collection/plugins/modules/tower_user.py @@ -102,7 +102,7 @@ tower_config_file: "~/tower_cli.cfg" ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule def main(): @@ -119,7 +119,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec) + module = TowerAPIModule(argument_spec=argument_spec) # Extract our parameters username = module.params.get('username') diff --git a/awx_collection/plugins/modules/tower_workflow_job_template.py b/awx_collection/plugins/modules/tower_workflow_job_template.py index c3ad692af420..8fb350b919a9 100644 --- a/awx_collection/plugins/modules/tower_workflow_job_template.py +++ b/awx_collection/plugins/modules/tower_workflow_job_template.py @@ -137,7 +137,7 @@ organization: Default ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule import json @@ -176,7 +176,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec) + module = TowerAPIModule(argument_spec=argument_spec) # Extract our parameters name = module.params.get('name') diff --git a/awx_collection/plugins/modules/tower_workflow_job_template_node.py b/awx_collection/plugins/modules/tower_workflow_job_template_node.py index 16902e942171..7ef9e1461978 100644 --- a/awx_collection/plugins/modules/tower_workflow_job_template_node.py +++ b/awx_collection/plugins/modules/tower_workflow_job_template_node.py @@ -157,7 +157,7 @@ - my-first-node ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule def main(): @@ -185,7 +185,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec) + module = TowerAPIModule(argument_spec=argument_spec) # Extract our parameters identifier = module.params.get('identifier') diff --git a/awx_collection/plugins/modules/tower_workflow_launch.py b/awx_collection/plugins/modules/tower_workflow_launch.py index 8ef73d82fcf5..249feeed35c8 100644 --- a/awx_collection/plugins/modules/tower_workflow_launch.py +++ b/awx_collection/plugins/modules/tower_workflow_launch.py @@ -91,7 +91,7 @@ wait: False ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule import json import time @@ -111,7 +111,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec) + module = TowerAPIModule(argument_spec=argument_spec) optional_args = {} # Extract our parameters diff --git a/awx_collection/plugins/modules/tower_workflow_template.py b/awx_collection/plugins/modules/tower_workflow_template.py index 9a652a437332..a8557b2ad219 100644 --- a/awx_collection/plugins/modules/tower_workflow_template.py +++ b/awx_collection/plugins/modules/tower_workflow_template.py @@ -108,8 +108,8 @@ RETURN = ''' # ''' -from ..module_utils.ansible_tower import ( - TowerModule, +from ..module_utils.tower_legacy import ( + TowerLegacyModule, tower_auth_config, tower_check_mode ) @@ -140,7 +140,7 @@ def main(): state=dict(choices=['present', 'absent'], default='present'), ) - module = TowerModule( + module = TowerLegacyModule( argument_spec=argument_spec, supports_check_mode=False ) diff --git a/awx_collection/test/awx/conftest.py b/awx_collection/test/awx/conftest.py index 025b8d033d78..53270d0e7f50 100644 --- a/awx_collection/test/awx/conftest.py +++ b/awx_collection/test/awx/conftest.py @@ -10,7 +10,7 @@ from unittest import mock import logging -from requests.models import Response +from requests.models import Response, PreparedRequest import pytest @@ -23,6 +23,11 @@ except ImportError: HAS_TOWER_CLI = False +try: + import awxkit + HAS_AWX_KIT = True +except ImportError: + HAS_AWX_KIT = False logger = logging.getLogger('awx.main.tests') @@ -90,7 +95,8 @@ def new_request(self, method, url, **kwargs): if 'params' in kwargs and method == 'GET': # query params for GET are handled a bit differently by # tower-cli and python requests as opposed to REST framework APIRequestFactory - kwargs_copy.setdefault('data', {}) + if not kwargs_copy.get('data'): + kwargs_copy['data'] = {} if isinstance(kwargs['params'], dict): kwargs_copy['data'].update(kwargs['params']) elif isinstance(kwargs['params'], list): @@ -117,6 +123,8 @@ def new_request(self, method, url, **kwargs): request_user.username, resp.status_code ) + resp.request = PreparedRequest() + resp.request.prepare(method=method, url=url) return resp def new_open(self, method, url, **kwargs): @@ -142,11 +150,22 @@ def new_open(self, method, url, **kwargs): def mock_load_params(self): self.params = module_params - with mock.patch.object(resource_module.TowerModule, '_load_params', new=mock_load_params): + if getattr(resource_module, 'TowerAWXKitModule', None): + resource_class = resource_module.TowerAWXKitModule + elif getattr(resource_module, 'TowerAPIModule', None): + resource_class = resource_module.TowerAPIModule + elif getattr(resource_module, 'TowerLegacyModule', None): + resource_class = resource_module.TowerLegacyModule + else: + raise("The module has neither a TowerLegacyModule, TowerAWXKitModule or a TowerAPIModule") + + with mock.patch.object(resource_class, '_load_params', new=mock_load_params): # Call the test utility (like a mock server) instead of issuing HTTP requests with mock.patch('ansible.module_utils.urls.Request.open', new=new_open): if HAS_TOWER_CLI: tower_cli_mgr = mock.patch('tower_cli.api.Session.request', new=new_request) + elif HAS_AWX_KIT: + tower_cli_mgr = mock.patch('awxkit.api.client.requests.Session.request', new=new_request) else: tower_cli_mgr = suppress() with tower_cli_mgr: diff --git a/awx_collection/test/awx/test_module_utils.py b/awx_collection/test/awx/test_module_utils.py index 0f443890e88b..e93b6ee93921 100644 --- a/awx_collection/test/awx/test_module_utils.py +++ b/awx_collection/test/awx/test_module_utils.py @@ -30,12 +30,12 @@ def mock_ping_response(self, method, url, **kwargs): def test_version_warning(collection_import, silence_warning): - TowerModule = collection_import('plugins.module_utils.tower_api').TowerModule + TowerAPIModule = collection_import('plugins.module_utils.tower_api').TowerAPIModule cli_data = {'ANSIBLE_MODULE_ARGS': {}} testargs = ['module_file2.py', json.dumps(cli_data)] with mock.patch.object(sys, 'argv', testargs): with mock.patch('ansible.module_utils.urls.Request.open', new=mock_ping_response): - my_module = TowerModule(argument_spec=dict()) + my_module = TowerAPIModule(argument_spec=dict()) my_module._COLLECTION_VERSION = "1.0.0" my_module._COLLECTION_TYPE = "not-junk" my_module.collection_to_version['not-junk'] = 'not-junk' @@ -46,12 +46,12 @@ def test_version_warning(collection_import, silence_warning): def test_type_warning(collection_import, silence_warning): - TowerModule = collection_import('plugins.module_utils.tower_api').TowerModule + TowerAPIModule = collection_import('plugins.module_utils.tower_api').TowerAPIModule cli_data = {'ANSIBLE_MODULE_ARGS': {}} testargs = ['module_file2.py', json.dumps(cli_data)] with mock.patch.object(sys, 'argv', testargs): with mock.patch('ansible.module_utils.urls.Request.open', new=mock_ping_response): - my_module = TowerModule(argument_spec={}) + my_module = TowerAPIModule(argument_spec={}) my_module._COLLECTION_VERSION = "1.2.3" my_module._COLLECTION_TYPE = "junk" my_module.collection_to_version['junk'] = 'junk' @@ -63,7 +63,7 @@ def test_type_warning(collection_import, silence_warning): def test_duplicate_config(collection_import, silence_warning): # imports done here because of PATH issues unique to this test suite - TowerModule = collection_import('plugins.module_utils.tower_api').TowerModule + TowerAPIModule = collection_import('plugins.module_utils.tower_api').TowerAPIModule data = { 'name': 'zigzoom', 'zig': 'zoom', @@ -71,12 +71,12 @@ def test_duplicate_config(collection_import, silence_warning): 'tower_config_file': 'my_config' } - with mock.patch.object(TowerModule, 'load_config') as mock_load: + with mock.patch.object(TowerAPIModule, 'load_config') as mock_load: argument_spec = dict( name=dict(required=True), zig=dict(type='str'), ) - TowerModule(argument_spec=argument_spec, direct_params=data) + TowerAPIModule(argument_spec=argument_spec, direct_params=data) assert mock_load.mock_calls[-1] == mock.call('my_config') silence_warning.assert_called_once_with( @@ -92,8 +92,8 @@ def test_no_templated_values(collection_import): Those replacements should happen at build time, so they should not be checked into source. """ - TowerModule = collection_import('plugins.module_utils.tower_api').TowerModule - assert TowerModule._COLLECTION_VERSION == "0.0.1-devel", ( + TowerAPIModule = collection_import('plugins.module_utils.tower_api').TowerAPIModule + assert TowerAPIModule._COLLECTION_VERSION == "0.0.1-devel", ( 'The collection version is templated when the collection is built ' 'and the code should retain the placeholder of "0.0.1-devel".' ) diff --git a/awx_collection/tools/roles/generate/templates/tower_module.j2 b/awx_collection/tools/roles/generate/templates/tower_module.j2 index a9834db28de1..3606cff54767 100644 --- a/awx_collection/tools/roles/generate/templates/tower_module.j2 +++ b/awx_collection/tools/roles/generate/templates/tower_module.j2 @@ -96,7 +96,7 @@ EXAMPLES = ''' {% endif %} ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule def main(): @@ -142,7 +142,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec) + module = TowerAPIModule(argument_spec=argument_spec) # Extract our parameters {% for option in item['json']['actions']['POST'] %} From 08e5dd87e6c0771162fb0ab246c53117b94fa29e Mon Sep 17 00:00:00 2001 From: John Westcott IV Date: Mon, 3 Aug 2020 14:14:40 -0400 Subject: [PATCH 02/17] Adding integration tests and example in import --- .../plugins/modules/tower_import.py | 4 + .../targets/tower_export/tasks/main.yml | 77 +++++++++++++ .../targets/tower_import/tasks/main.yml | 108 ++++++++++++++++++ 3 files changed, 189 insertions(+) create mode 100644 awx_collection/tests/integration/targets/tower_export/tasks/main.yml create mode 100644 awx_collection/tests/integration/targets/tower_import/tasks/main.yml diff --git a/awx_collection/plugins/modules/tower_import.py b/awx_collection/plugins/modules/tower_import.py index 0bb5dec75a42..eeea7b35a3a3 100644 --- a/awx_collection/plugins/modules/tower_import.py +++ b/awx_collection/plugins/modules/tower_import.py @@ -38,6 +38,10 @@ - name: Import all tower assets tower_import: assets: "{{ export_output.assets }}" + +- name: Import orgs from a json file + tower_import: + assets: "{{ lookup('file', 'org.json') | from_json() }}" ''' from ..module_utils.tower_awxkit import TowerAWXKitModule diff --git a/awx_collection/tests/integration/targets/tower_export/tasks/main.yml b/awx_collection/tests/integration/targets/tower_export/tasks/main.yml new file mode 100644 index 000000000000..ce33f50019fb --- /dev/null +++ b/awx_collection/tests/integration/targets/tower_export/tasks/main.yml @@ -0,0 +1,77 @@ +--- +- name: Generate a random string for test + set_fact: + test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + when: test_id is not defined + +- name: Generate names + set_fact: + org_name1: "AWX-Collection-tests-tower_export-organization-{{ test_id }}" + org_name2: "AWX-Collection-tests-tower_export-organization2-{{ test_id }}" + inventory_name1: "AWX-Collection-tests-tower_export-inv1-{{ test_id }}" + +- block: + - name: Create some organizations + tower_organization: + name: "{{ item }}" + loop: + - "{{ org_name1 }}" + - "{{ org_name2 }}" + + - name: Create an inventory + tower_inventory: + name: "{{ inventory_name1 }}" + organization: "{{ org_name1 }}" + + - name: Export all tower assets + tower_export: + all: True + register: all_assets + + - assert: + that: + - all_assets is not changed + - all_assets is successful + - all_assets['assets']['organizations'] | length() >= 2 + + - name: Export all inventories + tower_export: + inventory: 'all' + register: inventory_export + + - assert: + that: + - inventory_export is successful + - inventory_export is not changed + - inventory_export['assets']['inventory'] | length() >= 1 + - "'organizations' not in inventory_export['assets']" + + # This mimics the example in the module + - name: Export an all and a specific + tower_export: + inventory: 'all' + organizations: "{{ org_name1 }}" + register: mixed_export + + - assert: + that: + - mixed_export is successful + - mixed_export is not changed + - mixed_export['assets']['inventory'] | length() >= 1 + - mixed_export['assets']['organizations'] | length() == 1 + - "'workflow_job_templates' not in mixed_export['assets']" + + always: + - name: Remove our inventory + tower_inventory: + name: "{{ inventory_name1 }}" + organization: "{{ org_name1 }}" + state: absent + + - name: Remove test organizations + tower_organization: + name: "{{ item }}" + state: absent + loop: + - "{{ org_name1 }}" + - "{{ org_name2 }}" diff --git a/awx_collection/tests/integration/targets/tower_import/tasks/main.yml b/awx_collection/tests/integration/targets/tower_import/tasks/main.yml new file mode 100644 index 000000000000..09a91c85b0a7 --- /dev/null +++ b/awx_collection/tests/integration/targets/tower_import/tasks/main.yml @@ -0,0 +1,108 @@ +--- +- name: Generate a random string for test + set_fact: + test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + when: test_id is not defined + +- name: Generate names + set_fact: + org_name1: "AWX-Collection-tests-tower_import-organization-{{ test_id }}" + org_name2: "AWX-Collection-tests-tower_import-organization2-{{ test_id }}" + +- block: + - name: "Import something" + tower_import: + assets: + organizations: + - name: "{{ org_name1 }}" + description: "" + max_hosts: 0 + custom_virtualenv: null + related: + notification_templates: [] + notification_templates_started: [] + notification_templates_success: [] + notification_templates_error: [] + notification_templates_approvals: [] + natural_key: + name: "Default" + type: "organization" + register: import_output + + - assert: + that: + - import_output is changed + + - name: "Import something again (awxkit is not idempotent, this tests a filure)" + tower_import: + assets: + organizations: + - name: "{{ org_name1 }}" + description: "" + max_hosts: 0 + custom_virtualenv: null + related: + notification_templates: [] + notification_templates_started: [] + notification_templates_success: [] + notification_templates_error: [] + notification_templates_approvals: [] + natural_key: + name: "Default" + type: "organization" + register: import_output + ignore_errors: True + + - assert: + that: + - import_output is failed + - "'Organization with this Name already exists' in import_output.msg" + + - name: "Write out a json file" + copy: + content: | + { + "organizations": [ + { + "name": "{{ org_name2 }}", + "description": "", + "max_hosts": 0, + "custom_virtualenv": null, + "related": { + "notification_templates": [], + "notification_templates_started": [], + "notification_templates_success": [], + "notification_templates_error": [], + "notification_templates_approvals": [] + }, + "natural_key": { + "name": "Default", + "type": "organization" + } + } + ] + } + dest: ./org.json + + - name: "Load assets from a file" + tower_import: + assets: "{{ lookup('file', 'org.json') | from_json() }}" + register: import_output + + - assert: + that: + - import_output is changed + + always: + - name: Remove organizations + tower_organization: + name: "{{ item }}" + state: absent + loop: + - "{{ org_name1 }}" + - "{{ org_name2 }}" + + - name: Delete org.json + file: + path: ./org.json + state: absent From 748bdbd2dd7e31cf858dcd8093c715bee196b3d7 Mon Sep 17 00:00:00 2001 From: John Westcott IV Date: Tue, 4 Aug 2020 11:21:18 -0400 Subject: [PATCH 03/17] Fix python3 Zuul error with awxkit --- awx_collection/test/awx/conftest.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/awx_collection/test/awx/conftest.py b/awx_collection/test/awx/conftest.py index 53270d0e7f50..5db00d632531 100644 --- a/awx_collection/test/awx/conftest.py +++ b/awx_collection/test/awx/conftest.py @@ -24,7 +24,10 @@ HAS_TOWER_CLI = False try: - import awxkit + # Because awxkit will be a directory at the root of this makefile and we are using python3, import awxkit will work even if its not installed. + # However, awxkit will not contain api whih causes a stack failure down on line 170 when we try to mock it. + # So here we are importing awxkit.api to prevent that. Then you only get an error on tests for awxkit functionality. + import awxkit.api HAS_AWX_KIT = True except ImportError: HAS_AWX_KIT = False From 8a0cd747e11bc72aa2c7a2bd508e95dde078b00e Mon Sep 17 00:00:00 2001 From: John Westcott IV Date: Tue, 4 Aug 2020 11:33:29 -0400 Subject: [PATCH 04/17] Fixing truthy linting issues --- .../tests/integration/targets/tower_export/tasks/main.yml | 2 +- .../tests/integration/targets/tower_import/tasks/main.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/awx_collection/tests/integration/targets/tower_export/tasks/main.yml b/awx_collection/tests/integration/targets/tower_export/tasks/main.yml index ce33f50019fb..7ffbc15820b2 100644 --- a/awx_collection/tests/integration/targets/tower_export/tasks/main.yml +++ b/awx_collection/tests/integration/targets/tower_export/tasks/main.yml @@ -25,7 +25,7 @@ - name: Export all tower assets tower_export: - all: True + all: true register: all_assets - assert: diff --git a/awx_collection/tests/integration/targets/tower_import/tasks/main.yml b/awx_collection/tests/integration/targets/tower_import/tasks/main.yml index 09a91c85b0a7..ea18bb584f5b 100644 --- a/awx_collection/tests/integration/targets/tower_import/tasks/main.yml +++ b/awx_collection/tests/integration/targets/tower_import/tasks/main.yml @@ -33,7 +33,7 @@ that: - import_output is changed - - name: "Import something again (awxkit is not idempotent, this tests a filure)" + - name: "Import something again (awxkit is not idempotent, this tests a failure)" tower_import: assets: organizations: @@ -51,7 +51,7 @@ name: "Default" type: "organization" register: import_output - ignore_errors: True + ignore_errors: true - assert: that: From f2b9bdd5529256c8154c04408b1bcd0af622286c Mon Sep 17 00:00:00 2001 From: John Westcott IV Date: Tue, 4 Aug 2020 13:06:13 -0400 Subject: [PATCH 05/17] Removed default: '' and updated [] to '' per specification --- .../plugins/modules/tower_export.py | 21 +++++-------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/awx_collection/plugins/modules/tower_export.py b/awx_collection/plugins/modules/tower_export.py index 84c08fe4545b..6dabf17f39da 100644 --- a/awx_collection/plugins/modules/tower_export.py +++ b/awx_collection/plugins/modules/tower_export.py @@ -30,57 +30,46 @@ organizations: description: - organization name to export - default: '' type: str user: description: - user name to export - default: '' type: str team: description: - team name to export - default: '' type: str credential_type: description: - credential type name to export - default: '' type: str credential: description: - credential name to export - default: '' type: str notification_template: description: - notification template name to export - default: '' type: str inventory_script: description: - inventory script name to export - default: '' type: str inventory: description: - inventory name to export - default: '' type: str project: description: - project name to export - default: '' type: str job_template: description: - job template name to export - default: '' type: str workflow: description: - workflow name to export - default: '' type: str requirements: - "awxkit >= 9.3.0" @@ -132,15 +121,15 @@ def main(): module.json_output['changed'] = False # The exporter code currently works like the following: - # Empty list == all assets of that type - # string = just one asset of that type (by name) - # None = skip asset type + # Empty string == all assets of that type + # Non-Empty string = just one asset of that type (by name or ID) + # Asset type not present or None = skip asset type (unless everything is None, then export all) # Here we are going to setup a dict of values to export export_args = {} for resource in EXPORTABLE_RESOURCES: if module.params.get('all') or module.params.get(resource) == 'all': - # If we are exporting everything or we got the keyword "all" we pass in an empty list for this asset type - export_args[resource] = [] + # If we are exporting everything or we got the keyword "all" we pass in an empty string for this asset type + export_args[resource] = '' else: # Otherwise we take either the string or None (if the parameter was not passed) to get one or no items export_args[resource] = module.params.get(resource) From 9bf19daa5e544487ddc715c9e7791325836ad4b2 Mon Sep 17 00:00:00 2001 From: John Westcott IV Date: Tue, 4 Aug 2020 13:25:05 -0400 Subject: [PATCH 06/17] Another linting issue --- .../tests/integration/targets/tower_import/tasks/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx_collection/tests/integration/targets/tower_import/tasks/main.yml b/awx_collection/tests/integration/targets/tower_import/tasks/main.yml index ea18bb584f5b..9835ff89a5c2 100644 --- a/awx_collection/tests/integration/targets/tower_import/tasks/main.yml +++ b/awx_collection/tests/integration/targets/tower_import/tasks/main.yml @@ -52,7 +52,7 @@ type: "organization" register: import_output ignore_errors: true - + - assert: that: - import_output is failed From 5107f164a23dcb88836aba646a8b50308cc154fa Mon Sep 17 00:00:00 2001 From: John Westcott IV Date: Thu, 6 Aug 2020 10:32:46 -0400 Subject: [PATCH 07/17] Expanding examples --- awx_collection/plugins/modules/tower_import.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/awx_collection/plugins/modules/tower_import.py b/awx_collection/plugins/modules/tower_import.py index eeea7b35a3a3..0dddeb64a0ac 100644 --- a/awx_collection/plugins/modules/tower_import.py +++ b/awx_collection/plugins/modules/tower_import.py @@ -35,11 +35,16 @@ ''' EXAMPLES = ''' -- name: Import all tower assets +- name: Export all assets + tower_export: + all: True + registeR: export_output + +- name: Import all tower assets from our export tower_import: assets: "{{ export_output.assets }}" -- name: Import orgs from a json file +- name: Load data from a json file created by a command like awx export --organization Default tower_import: assets: "{{ lookup('file', 'org.json') | from_json() }}" ''' From 3fe61cfa4fa7ed38cb4bd3275d81ee110c2b4241 Mon Sep 17 00:00:00 2001 From: John Westcott IV Date: Thu, 6 Aug 2020 14:48:01 -0400 Subject: [PATCH 08/17] Fixing linting issues --- awx_collection/plugins/modules/tower_export.py | 4 ++-- awx_collection/plugins/modules/tower_import.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/awx_collection/plugins/modules/tower_export.py b/awx_collection/plugins/modules/tower_export.py index 6dabf17f39da..ad8da8f3ceb7 100644 --- a/awx_collection/plugins/modules/tower_export.py +++ b/awx_collection/plugins/modules/tower_export.py @@ -97,9 +97,9 @@ try: from awxkit.api.pages.api import EXPORTABLE_RESOURCES - HAS_EXPORTABLE_RESOURCES=True + HAS_EXPORTABLE_RESOURCES = True except ImportError: - HAS_EXPORTABLE_RESOURCES=False + HAS_EXPORTABLE_RESOURCES = False def main(): diff --git a/awx_collection/plugins/modules/tower_import.py b/awx_collection/plugins/modules/tower_import.py index 0dddeb64a0ac..37de035123b0 100644 --- a/awx_collection/plugins/modules/tower_import.py +++ b/awx_collection/plugins/modules/tower_import.py @@ -58,9 +58,9 @@ # In this module we don't use EXPORTABLE_RESOURCES, we just want to validate that our installed awxkit has import/export try: from awxkit.api.pages.api import EXPORTABLE_RESOURCES - HAS_EXPORTABLE_RESOURCES=True + HAS_EXPORTABLE_RESOURCES = True except ImportError: - HAS_EXPORTABLE_RESOURCES=False + HAS_EXPORTABLE_RESOURCES = False def main(): argument_spec = dict( From 8688740e933e5875d6e9298636f406db9caf2f42 Mon Sep 17 00:00:00 2001 From: John Westcott IV Date: Thu, 6 Aug 2020 14:57:30 -0400 Subject: [PATCH 09/17] Fixing ansible pep8 issues --- awx_collection/plugins/module_utils/tower_api.py | 4 +++- awx_collection/plugins/module_utils/tower_module.py | 1 - awx_collection/plugins/modules/tower_import.py | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/awx_collection/plugins/module_utils/tower_api.py b/awx_collection/plugins/module_utils/tower_api.py index 089273bff112..6b5ed8753116 100644 --- a/awx_collection/plugins/module_utils/tower_api.py +++ b/awx_collection/plugins/module_utils/tower_api.py @@ -10,6 +10,7 @@ import re from json import loads, dumps + class TowerAPIModule(TowerModule): # TODO: Move the collection version check into tower_module.py # This gets set by the make process so whatever is in here is irrelevant @@ -27,7 +28,8 @@ class TowerAPIModule(TowerModule): def __init__(self, argument_spec, direct_params=None, error_callback=None, warn_callback=None, **kwargs): kwargs['supports_check_mode'] = True - super(TowerAPIModule, self).__init__(argument_spec=argument_spec, direct_params=direct_params, error_callback=error_callback, warn_callback=warn_callback, **kwargs) + super(TowerAPIModule, self).__init__(argument_spec=argument_spec, direct_params=direct_params, + error_callback=error_callback, warn_callback=warn_callback, **kwargs) self.session = Request(cookies=CookieJar(), validate_certs=self.verify_ssl) @staticmethod diff --git a/awx_collection/plugins/module_utils/tower_module.py b/awx_collection/plugins/module_utils/tower_module.py index 5d5b4d4239c5..553a35248c2a 100644 --- a/awx_collection/plugins/module_utils/tower_module.py +++ b/awx_collection/plugins/module_utils/tower_module.py @@ -215,7 +215,6 @@ def load_config(self, config_path): else: setattr(self, honorred_setting, config_data[honorred_setting]) - def logout(self): # This method is intended to be overridden pass diff --git a/awx_collection/plugins/modules/tower_import.py b/awx_collection/plugins/modules/tower_import.py index 37de035123b0..a39a98a5e37a 100644 --- a/awx_collection/plugins/modules/tower_import.py +++ b/awx_collection/plugins/modules/tower_import.py @@ -62,6 +62,7 @@ except ImportError: HAS_EXPORTABLE_RESOURCES = False + def main(): argument_spec = dict( assets=dict(type='dict', required=True) From c2e0c0655ba2b2ec63bcabfd03e026e1991827fa Mon Sep 17 00:00:00 2001 From: John Westcott IV Date: Thu, 6 Aug 2020 15:04:09 -0400 Subject: [PATCH 10/17] Fixing validate-module errors --- .../plugins/modules/tower_export.py | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/awx_collection/plugins/modules/tower_export.py b/awx_collection/plugins/modules/tower_export.py index ad8da8f3ceb7..9f8f479b7163 100644 --- a/awx_collection/plugins/modules/tower_export.py +++ b/awx_collection/plugins/modules/tower_export.py @@ -31,43 +31,43 @@ description: - organization name to export type: str - user: + users: description: - user name to export type: str - team: + teams: description: - team name to export type: str - credential_type: + credential_types: description: - credential type name to export type: str - credential: + credentials: description: - credential name to export type: str - notification_template: + notification_templates: description: - notification template name to export type: str - inventory_script: + inventory_sources: description: - - inventory script name to export + - inventory soruce to export type: str inventory: description: - inventory name to export type: str - project: + projects: description: - project name to export type: str - job_template: + job_templates: description: - job template name to export type: str - workflow: + workflow_job_templates: description: - workflow name to export type: str @@ -110,7 +110,7 @@ def main(): # We are not going to raise an error here because the __init__ method of TowerAWXKitModule will do that for us if HAS_EXPORTABLE_RESOURCES: for resource in EXPORTABLE_RESOURCES: - argument_spec[resource] = dict() + argument_spec[resource] = dict(type='str') module = TowerAWXKitModule(argument_spec=argument_spec) From 01e08ba0e14eaf3444396610ac96cf57e3592257 Mon Sep 17 00:00:00 2001 From: John Westcott IV Date: Thu, 6 Aug 2020 15:07:23 -0400 Subject: [PATCH 11/17] Fixing exit_module -> exit_json --- awx_collection/plugins/module_utils/tower_awxkit.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/awx_collection/plugins/module_utils/tower_awxkit.py b/awx_collection/plugins/module_utils/tower_awxkit.py index eedf3a738785..24a83b3c0d73 100644 --- a/awx_collection/plugins/module_utils/tower_awxkit.py +++ b/awx_collection/plugins/module_utils/tower_awxkit.py @@ -24,7 +24,7 @@ def __init__(self, argument_spec, **kwargs): # Die if we don't have AWX_KIT installed if not HAS_AWX_KIT: - self.exit_module(msg=missing_required_lib('awxkit')) + self.exit_json(msg=missing_required_lib('awxkit')) # Establish our conneciton object self.connection = Connection(self.host, verify=self.verify_ssl) @@ -35,7 +35,7 @@ def authenticate(self): # If we have neither of these, then we can try un-authenticated access self.authenticated = True except Exception: - self.exit_module("Failed to authenticate") + self.exit_json("Failed to authenticate") def get_api_v2_object(self): if not self.apiV2Ref: @@ -48,3 +48,5 @@ def get_api_v2_object(self): def logout(self): if self.authenticated: self.connection.logout() + + From 76f08744f6a89bd97ad0105969b43f8e88915695 Mon Sep 17 00:00:00 2001 From: beeankha Date: Tue, 18 Aug 2020 15:02:17 -0400 Subject: [PATCH 12/17] Fix linter whitespace error --- awx_collection/plugins/module_utils/tower_awxkit.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/awx_collection/plugins/module_utils/tower_awxkit.py b/awx_collection/plugins/module_utils/tower_awxkit.py index 24a83b3c0d73..ddd2d190c798 100644 --- a/awx_collection/plugins/module_utils/tower_awxkit.py +++ b/awx_collection/plugins/module_utils/tower_awxkit.py @@ -48,5 +48,3 @@ def get_api_v2_object(self): def logout(self): if self.authenticated: self.connection.logout() - - From a2eab45d61fd7f097c11f2e9200cef0a46f07dd2 Mon Sep 17 00:00:00 2001 From: John Westcott IV Date: Wed, 19 Aug 2020 12:12:30 -0400 Subject: [PATCH 13/17] Trying to gobble up logs incase there are errors --- .../plugins/modules/tower_export.py | 33 +++++++++++++++---- 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/awx_collection/plugins/modules/tower_export.py b/awx_collection/plugins/modules/tower_export.py index 9f8f479b7163..e7a788559cce 100644 --- a/awx_collection/plugins/modules/tower_export.py +++ b/awx_collection/plugins/modules/tower_export.py @@ -92,7 +92,8 @@ ''' from os import environ - +import logging +from ansible.module_utils.six.moves import StringIO from ..module_utils.tower_awxkit import TowerAWXKitModule try: @@ -134,11 +135,31 @@ def main(): # Otherwise we take either the string or None (if the parameter was not passed) to get one or no items export_args[resource] = module.params.get(resource) - # Run the export process - module.json_output['assets'] = module.get_api_v2_object().export_assets(**export_args) - - module.exit_json(**module.json_output) - + # Currently the import process does not return anything on error + # It simply just logs to pythons logger + # Setup a log gobbler to get error messages from import_assets + log_capture_string = StringIO() + ch = logging.StreamHandler(log_capture_string) + for logger_name in ['awxkit.api.pages.api', 'awxkit.api.pages.page']: + logger = logging.getLogger(logger_name) + logger.setLevel(logging.WARNING) + ch.setLevel(logging.WARNING) + + logger.addHandler(ch) + log_contents = '' + + # Run the import process + try: + module.json_output['assets'] = module.get_api_v2_object().export_assets(**export_args) + module.exit_json(**module.json_output) + except Exception as e: + module.fail_json(msg="Failed to export assets {0}".format(e)) + finally: + # Finally consume the logs incase there were any errors and die if there were + log_contents = log_capture_string.getvalue() + log_capture_string.close() + if log_contents != '': + module.fail_json(msg=log_contents) if __name__ == '__main__': main() From 3abd77c4c02a29f68d2148a317bb0c8cf6c99a23 Mon Sep 17 00:00:00 2001 From: John Westcott IV Date: Wed, 19 Aug 2020 12:18:07 -0400 Subject: [PATCH 14/17] Fixing oauth token login and making module respect token over username/password --- awx_collection/plugins/module_utils/tower_awxkit.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/awx_collection/plugins/module_utils/tower_awxkit.py b/awx_collection/plugins/module_utils/tower_awxkit.py index ddd2d190c798..506b64fb2067 100644 --- a/awx_collection/plugins/module_utils/tower_awxkit.py +++ b/awx_collection/plugins/module_utils/tower_awxkit.py @@ -31,9 +31,12 @@ def __init__(self, argument_spec, **kwargs): def authenticate(self): try: - self.connection.login(username=self.username, password=self.password, token=self.oauth_token) - # If we have neither of these, then we can try un-authenticated access - self.authenticated = True + if self.oauth_token: + self.connection.login(None, None, token=self.oauth_token, auth_type='Bearer') + self.authenticated = True + elif self.username: + self.connection.login(username=self.username, password=self.password) + self.authenticated = True except Exception: self.exit_json("Failed to authenticate") From 2c8c1ff595b89c63c55c160f218cd186d94f4255 Mon Sep 17 00:00:00 2001 From: John Westcott IV Date: Wed, 19 Aug 2020 14:14:49 -0400 Subject: [PATCH 15/17] Fixing sanity error --- awx_collection/plugins/modules/tower_export.py | 1 + 1 file changed, 1 insertion(+) diff --git a/awx_collection/plugins/modules/tower_export.py b/awx_collection/plugins/modules/tower_export.py index e7a788559cce..bd951d1744d4 100644 --- a/awx_collection/plugins/modules/tower_export.py +++ b/awx_collection/plugins/modules/tower_export.py @@ -161,5 +161,6 @@ def main(): if log_contents != '': module.fail_json(msg=log_contents) + if __name__ == '__main__': main() From b93319e3591ae925dc95110581afc0713e3105cf Mon Sep 17 00:00:00 2001 From: John Westcott IV Date: Wed, 19 Aug 2020 14:27:02 -0400 Subject: [PATCH 16/17] Updating to remove auth_type since its not longer required --- awx_collection/plugins/module_utils/tower_awxkit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx_collection/plugins/module_utils/tower_awxkit.py b/awx_collection/plugins/module_utils/tower_awxkit.py index 506b64fb2067..fc4e232f1b39 100644 --- a/awx_collection/plugins/module_utils/tower_awxkit.py +++ b/awx_collection/plugins/module_utils/tower_awxkit.py @@ -32,7 +32,7 @@ def __init__(self, argument_spec, **kwargs): def authenticate(self): try: if self.oauth_token: - self.connection.login(None, None, token=self.oauth_token, auth_type='Bearer') + self.connection.login(None, None, token=self.oauth_token) self.authenticated = True elif self.username: self.connection.login(username=self.username, password=self.password) From a5afe0214a50f96bc5b074aded25a40a433a9001 Mon Sep 17 00:00:00 2001 From: John Westcott IV Date: Wed, 19 Aug 2020 14:29:42 -0400 Subject: [PATCH 17/17] Trying to make AWXKIT tests not run on python2 --- awx_collection/tests/integration/targets/tower_export/aliases | 1 + awx_collection/tests/integration/targets/tower_import/aliases | 1 + 2 files changed, 2 insertions(+) create mode 100644 awx_collection/tests/integration/targets/tower_export/aliases create mode 100644 awx_collection/tests/integration/targets/tower_import/aliases diff --git a/awx_collection/tests/integration/targets/tower_export/aliases b/awx_collection/tests/integration/targets/tower_export/aliases new file mode 100644 index 000000000000..527d07c3cbb8 --- /dev/null +++ b/awx_collection/tests/integration/targets/tower_export/aliases @@ -0,0 +1 @@ +skip/python2 diff --git a/awx_collection/tests/integration/targets/tower_import/aliases b/awx_collection/tests/integration/targets/tower_import/aliases new file mode 100644 index 000000000000..527d07c3cbb8 --- /dev/null +++ b/awx_collection/tests/integration/targets/tower_import/aliases @@ -0,0 +1 @@ +skip/python2