mirror of
https://github.com/ansible/awx.git
synced 2026-01-11 10:00:01 -03:30
Share inventory plugin auth code with modules
refactor shared auth option definitions to repeat less
This commit is contained in:
parent
ec2c121762
commit
e3814c6f0f
@ -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')
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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, '
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user