summaryrefslogtreecommitdiffstats
path: root/awx_collection
diff options
context:
space:
mode:
authorAlanCoding <arominge@redhat.com>2020-03-30 03:39:51 +0200
committerAlanCoding <arominge@redhat.com>2020-06-03 03:17:12 +0200
commite3814c6f0fa1a2ab91ff828e0c9c95df7654d070 (patch)
tree6763f10f850915ba6ad9dcc62068844cd58f07ad /awx_collection
parentMerge pull request #7219 from AlanCoding/config_bug (diff)
downloadawx-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.py78
-rw-r--r--awx_collection/plugins/module_utils/ansible_tower.py30
-rw-r--r--awx_collection/plugins/module_utils/tower_api.py77
-rw-r--r--awx_collection/test/awx/test_module_utils.py15
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, '