diff options
author | AlanCoding <arominge@redhat.com> | 2020-03-30 03:39:51 +0200 |
---|---|---|
committer | AlanCoding <arominge@redhat.com> | 2020-06-03 03:17:12 +0200 |
commit | e3814c6f0fa1a2ab91ff828e0c9c95df7654d070 (patch) | |
tree | 6763f10f850915ba6ad9dcc62068844cd58f07ad /awx_collection | |
parent | Merge pull request #7219 from AlanCoding/config_bug (diff) | |
download | awx-e3814c6f0fa1a2ab91ff828e0c9c95df7654d070.tar.xz awx-e3814c6f0fa1a2ab91ff828e0c9c95df7654d070.zip |
Share inventory plugin auth code with modules
refactor shared auth option definitions to repeat less
Diffstat (limited to 'awx_collection')
-rw-r--r-- | awx_collection/plugins/inventory/tower.py | 78 | ||||
-rw-r--r-- | awx_collection/plugins/module_utils/ansible_tower.py | 30 | ||||
-rw-r--r-- | awx_collection/plugins/module_utils/tower_api.py | 77 | ||||
-rw-r--r-- | awx_collection/test/awx/test_module_utils.py | 15 |
4 files changed, 85 insertions, 115 deletions
diff --git a/awx_collection/plugins/inventory/tower.py b/awx_collection/plugins/inventory/tower.py index 7ed641eefc..0aca4af4a8 100644 --- a/awx_collection/plugins/inventory/tower.py +++ b/awx_collection/plugins/inventory/tower.py @@ -21,31 +21,23 @@ DOCUMENTATION = ''' are missing, this plugin will try to fill in missing arguments by reading from environment variables. - If reading configurations from environment variables, the path in the command must be @tower_inventory. options: - plugin: - description: the name of this plugin, it should always be set to 'tower' - for this plugin to recognize it as it's own. - env: - - name: ANSIBLE_INVENTORY_ENABLED - required: True - choices: ['tower'] host: description: The network address of your Ansible Tower host. - type: string env: - name: TOWER_HOST - required: True username: description: The user that you plan to use to access inventories on Ansible Tower. - type: string env: - name: TOWER_USERNAME - required: True password: description: The password for your Ansible Tower user. - type: string env: - name: TOWER_PASSWORD - required: True + oauth_token: + description: + - The Tower OAuth token to use. + env: + - name: TOWER_OAUTH_TOKEN inventory_id: description: - The ID of the Ansible Tower inventory that you wish to import. @@ -56,14 +48,14 @@ DOCUMENTATION = ''' env: - name: TOWER_INVENTORY required: True - validate_certs: - description: Specify whether Ansible should verify the SSL certificate of Ansible Tower host. + verify_ssl: + description: + - Specify whether Ansible should verify the SSL certificate of Ansible Tower host. + - Defaults to True, but this is handled by the shared module_utils code type: bool - default: True env: - name: TOWER_VERIFY_SSL - required: False - aliases: [ verify_ssl ] + aliases: [ validate_certs ] include_metadata: description: Make extra requests to provide all group vars with metadata about the source Ansible Tower host. type: bool @@ -99,7 +91,6 @@ inventory_id: the_ID_of_targeted_ansible_tower_inventory ''' import os -import re from ansible.module_utils import six from ansible.module_utils._text import to_text, to_native @@ -107,13 +98,11 @@ from ansible.errors import AnsibleParserError, AnsibleOptionsError from ansible.plugins.inventory import BaseInventoryPlugin from ansible.config.manager import ensure_type -from ..module_utils.ansible_tower import make_request, CollectionsParserError, Request +from ..module_utils.tower_api import TowerModule + -# Python 2/3 Compatibility -try: - from urlparse import urljoin -except ImportError: - from urllib.parse import urljoin +def handle_error(**kwargs): + raise AnsibleParserError(to_native(kwargs.get('msg'))) class InventoryModule(BaseInventoryPlugin): @@ -131,20 +120,25 @@ class InventoryModule(BaseInventoryPlugin): else: return False + def warn_callback(self, warning): + self.display.warning(warning) + def parse(self, inventory, loader, path, cache=True): super(InventoryModule, self).parse(inventory, loader, path) if not self.no_config_file_supplied and os.path.isfile(path): self._read_config_data(path) - # Read inventory from tower server. - # Note the environment variables will be handled automatically by InventoryManager. - tower_host = self.get_option('host') - if not re.match('(?:http|https)://', tower_host): - tower_host = 'https://{tower_host}'.format(tower_host=tower_host) - request_handler = Request(url_username=self.get_option('username'), - url_password=self.get_option('password'), - force_basic_auth=True, - validate_certs=self.get_option('validate_certs')) + # Defer processing of params to logic shared with the modules + module_params = {} + for plugin_param, module_param in TowerModule.short_params.items(): + opt_val = self.get_option(plugin_param) + if opt_val is not None: + module_params[module_param] = opt_val + + module = TowerModule( + argument_spec={}, direct_params=module_params, + error_callback=handle_error, warn_callback=self.warn_callback + ) # validate type of inventory_id because we allow two types as special case inventory_id = self.get_option('inventory_id') @@ -159,13 +153,11 @@ class InventoryModule(BaseInventoryPlugin): 'not integer, and cannot convert to string: {err}'.format(err=to_native(e)) ) inventory_id = inventory_id.replace('/', '') - inventory_url = '/api/v2/inventories/{inv_id}/script/?hostvars=1&towervars=1&all=1'.format(inv_id=inventory_id) - inventory_url = urljoin(tower_host, inventory_url) + inventory_url = '/api/v2/inventories/{inv_id}/script/'.format(inv_id=inventory_id) - try: - inventory = make_request(request_handler, inventory_url) - except CollectionsParserError as e: - raise AnsibleParserError(to_native(e)) + inventory = module.get_endpoint( + inventory_url, data={'hostvars': '1', 'towervars': '1', 'all': '1'} + )['json'] # To start with, create all the groups. for group_name in inventory: @@ -195,12 +187,8 @@ class InventoryModule(BaseInventoryPlugin): # Fetch extra variables if told to do so if self.get_option('include_metadata'): - config_url = urljoin(tower_host, '/api/v2/config/') - try: - config_data = make_request(request_handler, config_url) - except CollectionsParserError as e: - raise AnsibleParserError(to_native(e)) + config_data = module.get_endpoint('/api/v2/config/')['json'] server_data = {} server_data['license_type'] = config_data.get('license_info', {}).get('license_type', 'unknown') diff --git a/awx_collection/plugins/module_utils/ansible_tower.py b/awx_collection/plugins/module_utils/ansible_tower.py index c51b410127..17d6a38680 100644 --- a/awx_collection/plugins/module_utils/ansible_tower.py +++ b/awx_collection/plugins/module_utils/ansible_tower.py @@ -29,14 +29,9 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type -import json import os import traceback -from ansible.module_utils._text import to_native -from ansible.module_utils.urls import urllib_error, ConnectionError, socket, httplib -from ansible.module_utils.urls import Request # noqa - TOWER_CLI_IMP_ERR = None try: import tower_cli.utils.exceptions as exc @@ -51,31 +46,6 @@ except ImportError: from ansible.module_utils.basic import AnsibleModule, missing_required_lib -class CollectionsParserError(Exception): - pass - - -def make_request(request_handler, tower_url): - ''' - Makes the request to given URL, handles errors, returns JSON - ''' - try: - response = request_handler.get(tower_url) - except (ConnectionError, urllib_error.URLError, socket.error, httplib.HTTPException) as e: - n_error_msg = 'Connection to remote host failed: {err}'.format(err=to_native(e)) - # If Tower gives a readable error message, display that message to the user. - if callable(getattr(e, 'read', None)): - n_error_msg += ' with message: {err_msg}'.format(err_msg=to_native(e.read())) - raise CollectionsParserError(n_error_msg) - - # Attempt to parse JSON. - try: - return json.loads(response.read()) - except (ValueError, TypeError) as e: - # If the JSON parse fails, print the ValueError - raise CollectionsParserError('Failed to parse json from host: {err}'.format(err=to_native(e))) - - def tower_auth_config(module): ''' `tower_auth_config` attempts to load the tower-cli.cfg file diff --git a/awx_collection/plugins/module_utils/tower_api.py b/awx_collection/plugins/module_utils/tower_api.py index f22f6ed511..5d4f221f4e 100644 --- a/awx_collection/plugins/module_utils/tower_api.py +++ b/awx_collection/plugins/module_utils/tower_api.py @@ -42,7 +42,21 @@ class TowerModule(AnsibleModule): 'tower': 'Red Hat Ansible Tower', } url = None - honorred_settings = ('host', 'username', 'password', 'verify_ssl', 'oauth_token') + 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='str', 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 @@ -55,36 +69,32 @@ class TowerModule(AnsibleModule): config_name = 'tower_cli.cfg' ENCRYPTED_STRING = "$encrypted$" version_checked = False + error_callback = None + warn_callback = None - def __init__(self, argument_spec, **kwargs): - args = 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='str', no_log=True, required=False, fallback=(env_fallback, ['TOWER_OAUTH_TOKEN'])), - tower_config_file=dict(type='path', required=False, default=None), - ) - args.update(argument_spec) + 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} - super(TowerModule, self).__init__(argument_spec=args, **kwargs) + 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 - if self.params.get('tower_host'): - self.host = self.params.get('tower_host') - if self.params.get('tower_username'): - self.username = self.params.get('tower_username') - if self.params.get('tower_password'): - self.password = self.params.get('tower_password') - if self.params.get('validate_certs') is not None: - self.verify_ssl = self.params.get('validate_certs') - if self.params.get('tower_oauthtoken'): - self.oauth_token = self.params.get('tower_oauthtoken') + 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 some basic validation if not re.match('^https{0,1}://', self.host): @@ -116,10 +126,10 @@ class TowerModule(AnsibleModule): # If we have a specified tower config, load it if self.params.get('tower_config_file'): - duplicated_params = [] - for direct_field in ('tower_host', 'tower_username', 'tower_password', 'validate_certs', 'tower_oauthtoken'): - if self.params.get(direct_field): - duplicated_params.append(direct_field) + 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. ' @@ -184,7 +194,7 @@ class TowerModule(AnsibleModule): # 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.honorred_settings: + for honorred_setting in self.short_params: try: config_data[honorred_setting] = config.get('general', honorred_setting) except NoOptionError: @@ -197,7 +207,7 @@ class TowerModule(AnsibleModule): 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.honorred_settings: + for honorred_setting in self.short_params: if honorred_setting in config_data: # Veriffy SSL must be a boolean if honorred_setting == 'verify_ssl': @@ -748,13 +758,22 @@ class TowerModule(AnsibleModule): def fail_json(self, **kwargs): # Try to log out if we are authenticated self.logout() - super(TowerModule, self).fail_json(**kwargs) + 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/test/awx/test_module_utils.py b/awx_collection/test/awx/test_module_utils.py index 3c3cdf61c8..c7238d8c98 100644 --- a/awx_collection/test/awx/test_module_utils.py +++ b/awx_collection/test/awx/test_module_utils.py @@ -71,21 +71,14 @@ def test_duplicate_config(collection_import, silence_warning): 'tower_config_file': 'my_config' } - class DuplicateTestTowerModule(TowerModule): - def load_config(self, config_path): - assert config_path == 'my_config' - - def _load_params(self): - self.params = data - - cli_data = {'ANSIBLE_MODULE_ARGS': data} - testargs = ['module_file.py', json.dumps(cli_data)] - with mock.patch.object(sys, 'argv', testargs): + with mock.patch.object(TowerModule, 'load_config') as mock_load: argument_spec = dict( name=dict(required=True), zig=dict(type='str'), ) - DuplicateTestTowerModule(argument_spec=argument_spec) + TowerModule(argument_spec=argument_spec, direct_params=data) + assert mock_load.mock_calls[-1] == mock.call('my_config') + silence_warning.assert_called_once_with( 'The parameter(s) tower_username were provided at the same time as ' 'tower_config_file. Precedence may be unstable, ' |