Share inventory plugin auth code with modules

refactor shared auth option definitions to repeat less
This commit is contained in:
AlanCoding 2020-03-29 21:39:51 -04:00
parent ec2c121762
commit e3814c6f0f
No known key found for this signature in database
GPG Key ID: FD2C3C012A72926B
4 changed files with 85 additions and 115 deletions

View File

@ -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')

View File

@ -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

View File

@ -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

View File

@ -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, '