Merge pull request #7216 from AlanCoding/inventory_token

Sync inventory param processing with modules (token and config file support)

Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
This commit is contained in:
softwarefactory-project-zuul[bot]
2020-06-05 15:20:01 +00:00
committed by GitHub
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. 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. - If reading configurations from environment variables, the path in the command must be @tower_inventory.
options: 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: host:
description: The network address of your Ansible Tower host. description: The network address of your Ansible Tower host.
type: string
env: env:
- name: TOWER_HOST - name: TOWER_HOST
required: True
username: username:
description: The user that you plan to use to access inventories on Ansible Tower. description: The user that you plan to use to access inventories on Ansible Tower.
type: string
env: env:
- name: TOWER_USERNAME - name: TOWER_USERNAME
required: True
password: password:
description: The password for your Ansible Tower user. description: The password for your Ansible Tower user.
type: string
env: env:
- name: TOWER_PASSWORD - name: TOWER_PASSWORD
required: True oauth_token:
description:
- The Tower OAuth token to use.
env:
- name: TOWER_OAUTH_TOKEN
inventory_id: inventory_id:
description: description:
- The ID of the Ansible Tower inventory that you wish to import. - The ID of the Ansible Tower inventory that you wish to import.
@@ -56,14 +48,14 @@ DOCUMENTATION = '''
env: env:
- name: TOWER_INVENTORY - name: TOWER_INVENTORY
required: True required: True
validate_certs: verify_ssl:
description: Specify whether Ansible should verify the SSL certificate of Ansible Tower host. 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 type: bool
default: True
env: env:
- name: TOWER_VERIFY_SSL - name: TOWER_VERIFY_SSL
required: False aliases: [ validate_certs ]
aliases: [ verify_ssl ]
include_metadata: include_metadata:
description: Make extra requests to provide all group vars with metadata about the source Ansible Tower host. description: Make extra requests to provide all group vars with metadata about the source Ansible Tower host.
type: bool type: bool
@@ -99,7 +91,6 @@ inventory_id: the_ID_of_targeted_ansible_tower_inventory
''' '''
import os import os
import re
from ansible.module_utils import six from ansible.module_utils import six
from ansible.module_utils._text import to_text, to_native 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.plugins.inventory import BaseInventoryPlugin
from ansible.config.manager import ensure_type 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: def handle_error(**kwargs):
from urlparse import urljoin raise AnsibleParserError(to_native(kwargs.get('msg')))
except ImportError:
from urllib.parse import urljoin
class InventoryModule(BaseInventoryPlugin): class InventoryModule(BaseInventoryPlugin):
@@ -131,20 +120,25 @@ class InventoryModule(BaseInventoryPlugin):
else: else:
return False return False
def warn_callback(self, warning):
self.display.warning(warning)
def parse(self, inventory, loader, path, cache=True): def parse(self, inventory, loader, path, cache=True):
super(InventoryModule, self).parse(inventory, loader, path) super(InventoryModule, self).parse(inventory, loader, path)
if not self.no_config_file_supplied and os.path.isfile(path): if not self.no_config_file_supplied and os.path.isfile(path):
self._read_config_data(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'), # Defer processing of params to logic shared with the modules
url_password=self.get_option('password'), module_params = {}
force_basic_auth=True, for plugin_param, module_param in TowerModule.short_params.items():
validate_certs=self.get_option('validate_certs')) 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 # validate type of inventory_id because we allow two types as special case
inventory_id = self.get_option('inventory_id') 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)) 'not integer, and cannot convert to string: {err}'.format(err=to_native(e))
) )
inventory_id = inventory_id.replace('/', '') 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 = '/api/v2/inventories/{inv_id}/script/'.format(inv_id=inventory_id)
inventory_url = urljoin(tower_host, inventory_url)
try: inventory = module.get_endpoint(
inventory = make_request(request_handler, inventory_url) inventory_url, data={'hostvars': '1', 'towervars': '1', 'all': '1'}
except CollectionsParserError as e: )['json']
raise AnsibleParserError(to_native(e))
# To start with, create all the groups. # To start with, create all the groups.
for group_name in inventory: for group_name in inventory:
@@ -195,12 +187,8 @@ class InventoryModule(BaseInventoryPlugin):
# Fetch extra variables if told to do so # Fetch extra variables if told to do so
if self.get_option('include_metadata'): if self.get_option('include_metadata'):
config_url = urljoin(tower_host, '/api/v2/config/')
try: config_data = module.get_endpoint('/api/v2/config/')['json']
config_data = make_request(request_handler, config_url)
except CollectionsParserError as e:
raise AnsibleParserError(to_native(e))
server_data = {} server_data = {}
server_data['license_type'] = config_data.get('license_info', {}).get('license_type', 'unknown') 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) from __future__ import (absolute_import, division, print_function)
__metaclass__ = type __metaclass__ = type
import json
import os import os
import traceback 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 TOWER_CLI_IMP_ERR = None
try: try:
import tower_cli.utils.exceptions as exc import tower_cli.utils.exceptions as exc
@@ -51,31 +46,6 @@ except ImportError:
from ansible.module_utils.basic import AnsibleModule, missing_required_lib 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): def tower_auth_config(module):
''' '''
`tower_auth_config` attempts to load the tower-cli.cfg file `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', 'tower': 'Red Hat Ansible Tower',
} }
url = None 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' host = '127.0.0.1'
username = None username = None
password = None password = None
@@ -55,36 +69,32 @@ class TowerModule(AnsibleModule):
config_name = 'tower_cli.cfg' config_name = 'tower_cli.cfg'
ENCRYPTED_STRING = "$encrypted$" ENCRYPTED_STRING = "$encrypted$"
version_checked = False version_checked = False
error_callback = None
warn_callback = None
def __init__(self, argument_spec, **kwargs): def __init__(self, argument_spec, direct_params=None, error_callback=None, warn_callback=None, **kwargs):
args = dict( full_argspec = {}
tower_host=dict(required=False, fallback=(env_fallback, ['TOWER_HOST'])), full_argspec.update(TowerModule.AUTH_ARGSPEC)
tower_username=dict(required=False, fallback=(env_fallback, ['TOWER_USERNAME'])), full_argspec.update(argument_spec)
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)
kwargs['supports_check_mode'] = True kwargs['supports_check_mode'] = True
self.error_callback = error_callback
self.warn_callback = warn_callback
self.json_output = {'changed': False} 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() self.load_config_files()
# Parameters specified on command line will override settings in any config # Parameters specified on command line will override settings in any config
if self.params.get('tower_host'): for short_param, long_param in self.short_params.items():
self.host = self.params.get('tower_host') direct_value = self.params.get(long_param)
if self.params.get('tower_username'): if direct_value is not None:
self.username = self.params.get('tower_username') setattr(self, short_param, direct_value)
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')
# Perform some basic validation # Perform some basic validation
if not re.match('^https{0,1}://', self.host): 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 we have a specified tower config, load it
if self.params.get('tower_config_file'): if self.params.get('tower_config_file'):
duplicated_params = [] duplicated_params = [
for direct_field in ('tower_host', 'tower_username', 'tower_password', 'validate_certs', 'tower_oauthtoken'): fn for fn in self.AUTH_ARGSPEC
if self.params.get(direct_field): if fn != 'tower_config_file' and self.params.get(fn) is not None
duplicated_params.append(direct_field) ]
if duplicated_params: if duplicated_params:
self.warn(( self.warn((
'The parameter(s) {0} were provided at the same time as tower_config_file. ' '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 # 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 = {} config_data = {}
for honorred_setting in self.honorred_settings: for honorred_setting in self.short_params:
try: try:
config_data[honorred_setting] = config.get('general', honorred_setting) config_data[honorred_setting] = config.get('general', honorred_setting)
except NoOptionError: except NoOptionError:
@@ -197,7 +207,7 @@ class TowerModule(AnsibleModule):
raise ConfigFileException("An unknown exception occured trying to load config file: {0}".format(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 # 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: if honorred_setting in config_data:
# Veriffy SSL must be a boolean # Veriffy SSL must be a boolean
if honorred_setting == 'verify_ssl': if honorred_setting == 'verify_ssl':
@@ -748,13 +758,22 @@ class TowerModule(AnsibleModule):
def fail_json(self, **kwargs): def fail_json(self, **kwargs):
# Try to log out if we are authenticated # Try to log out if we are authenticated
self.logout() 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): def exit_json(self, **kwargs):
# Try to log out if we are authenticated # Try to log out if we are authenticated
self.logout() self.logout()
super(TowerModule, self).exit_json(**kwargs) 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): def is_job_done(self, job_status):
if job_status in ['new', 'pending', 'waiting', 'running']: if job_status in ['new', 'pending', 'waiting', 'running']:
return False return False

View File

@@ -71,21 +71,14 @@ def test_duplicate_config(collection_import, silence_warning):
'tower_config_file': 'my_config' 'tower_config_file': 'my_config'
} }
class DuplicateTestTowerModule(TowerModule): with mock.patch.object(TowerModule, 'load_config') as mock_load:
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):
argument_spec = dict( argument_spec = dict(
name=dict(required=True), name=dict(required=True),
zig=dict(type='str'), 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( silence_warning.assert_called_once_with(
'The parameter(s) tower_username were provided at the same time as ' 'The parameter(s) tower_username were provided at the same time as '
'tower_config_file. Precedence may be unstable, ' 'tower_config_file. Precedence may be unstable, '