mirror of
https://github.com/ansible/awx.git
synced 2026-05-07 17:37:37 -02:30
@@ -91,9 +91,9 @@ The following notes are changes that may require changes to playbooks:
|
||||
- Specified `tower_config` file used to handle `k=v` pairs on a single line; this is no longer supported. Please use a file formatted as `yaml`, `json` or `ini` only.
|
||||
- Some return values (e.g., `credential_type`) have been removed. Use of `id` is recommended.
|
||||
- `tower_job_template` no longer supports the deprecated `extra_vars_path` parameter, please use `extra_vars` with the lookup plugin to replace this functionality.
|
||||
- The `notification_configuration` parameter of `tower_notification` has changed from a string to a dict. Please use the `lookup` plugin to read an existing file into a dict.
|
||||
- The `notification_configuration` parameter of `tower_notification_template` has changed from a string to a dict. Please use the `lookup` plugin to read an existing file into a dict.
|
||||
- `tower_credential` no longer supports passing a file name to ssh_key_data.
|
||||
- The HipChat `notification_type` has been removed and can no longer be created using the `tower_notification` module.
|
||||
- The HipChat `notification_type` has been removed and can no longer be created using the `tower_notification_template` module.
|
||||
|
||||
## Running Unit Tests
|
||||
|
||||
|
||||
@@ -13,3 +13,5 @@ plugin_routing:
|
||||
deprecation:
|
||||
removal_date: TBD
|
||||
warning_text: see plugin documentation for details
|
||||
tower_notification:
|
||||
redirect: tower_notification_template
|
||||
|
||||
@@ -72,7 +72,7 @@ from ansible.errors import AnsibleParserError, AnsibleOptionsError
|
||||
from ansible.plugins.inventory import BaseInventoryPlugin
|
||||
from ansible.config.manager import ensure_type
|
||||
|
||||
from ..module_utils.tower_api import TowerModule
|
||||
from ..module_utils.tower_api import TowerAPIModule
|
||||
|
||||
|
||||
def handle_error(**kwargs):
|
||||
@@ -104,12 +104,12 @@ class InventoryModule(BaseInventoryPlugin):
|
||||
|
||||
# Defer processing of params to logic shared with the modules
|
||||
module_params = {}
|
||||
for plugin_param, module_param in TowerModule.short_params.items():
|
||||
for plugin_param, module_param in TowerAPIModule.short_params.items():
|
||||
opt_val = self.get_option(plugin_param)
|
||||
if opt_val is not None:
|
||||
module_params[module_param] = opt_val
|
||||
|
||||
module = TowerModule(
|
||||
module = TowerAPIModule(
|
||||
argument_spec={}, direct_params=module_params,
|
||||
error_callback=handle_error, warn_callback=self.warn_callback
|
||||
)
|
||||
|
||||
@@ -115,7 +115,7 @@ from ansible.plugins.lookup import LookupBase
|
||||
from ansible.errors import AnsibleError
|
||||
from ansible.module_utils._text import to_native
|
||||
from ansible.utils.display import Display
|
||||
from ..module_utils.tower_api import TowerModule
|
||||
from ..module_utils.tower_api import TowerAPIModule
|
||||
|
||||
|
||||
class LookupModule(LookupBase):
|
||||
@@ -133,13 +133,13 @@ class LookupModule(LookupBase):
|
||||
|
||||
# Defer processing of params to logic shared with the modules
|
||||
module_params = {}
|
||||
for plugin_param, module_param in TowerModule.short_params.items():
|
||||
for plugin_param, module_param in TowerAPIModule.short_params.items():
|
||||
opt_val = self.get_option(plugin_param)
|
||||
if opt_val is not None:
|
||||
module_params[module_param] = opt_val
|
||||
|
||||
# Create our module
|
||||
module = TowerModule(
|
||||
module = TowerAPIModule(
|
||||
argument_spec={}, direct_params=module_params,
|
||||
error_callback=self.handle_error, warn_callback=self.warn_callback
|
||||
)
|
||||
|
||||
@@ -1,37 +1,19 @@
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule, env_fallback
|
||||
from . tower_module import TowerModule
|
||||
from ansible.module_utils.urls import Request, SSLValidationError, ConnectionError
|
||||
from ansible.module_utils.six import PY2, string_types
|
||||
from ansible.module_utils.six.moves import StringIO
|
||||
from ansible.module_utils.six.moves.urllib.parse import urlparse, urlencode
|
||||
from ansible.module_utils.six import PY2
|
||||
from ansible.module_utils.six.moves.urllib.parse import urlencode
|
||||
from ansible.module_utils.six.moves.urllib.error import HTTPError
|
||||
from ansible.module_utils.six.moves.http_cookiejar import CookieJar
|
||||
from ansible.module_utils.six.moves.configparser import ConfigParser, NoOptionError
|
||||
from socket import gethostbyname
|
||||
import time
|
||||
import re
|
||||
from json import loads, dumps
|
||||
from os.path import isfile, expanduser, split, join, exists, isdir
|
||||
from os import access, R_OK, getcwd
|
||||
from distutils.util import strtobool
|
||||
|
||||
try:
|
||||
import yaml
|
||||
HAS_YAML = True
|
||||
except ImportError:
|
||||
HAS_YAML = False
|
||||
|
||||
|
||||
class ConfigFileException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ItemNotDefined(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class TowerModule(AnsibleModule):
|
||||
class TowerAPIModule(TowerModule):
|
||||
# TODO: Move the collection version check into tower_module.py
|
||||
# This gets set by the make process so whatever is in here is irrelevant
|
||||
_COLLECTION_VERSION = "0.0.1-devel"
|
||||
_COLLECTION_TYPE = "awx"
|
||||
@@ -41,197 +23,16 @@ class TowerModule(AnsibleModule):
|
||||
'awx': 'AWX',
|
||||
'tower': 'Red Hat Ansible Tower',
|
||||
}
|
||||
url = None
|
||||
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='raw', 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
|
||||
verify_ssl = True
|
||||
oauth_token = None
|
||||
oauth_token_id = None
|
||||
session = None
|
||||
cookie_jar = CookieJar()
|
||||
authenticated = False
|
||||
config_name = 'tower_cli.cfg'
|
||||
ENCRYPTED_STRING = "$encrypted$"
|
||||
version_checked = False
|
||||
error_callback = None
|
||||
warn_callback = None
|
||||
|
||||
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}
|
||||
|
||||
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
|
||||
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 magic depending on whether tower_oauthtoken is a string or a dict
|
||||
if self.params.get('tower_oauthtoken'):
|
||||
token_param = self.params.get('tower_oauthtoken')
|
||||
if type(token_param) is dict:
|
||||
if 'token' in token_param:
|
||||
self.oauth_token = self.params.get('tower_oauthtoken')['token']
|
||||
else:
|
||||
self.fail_json(msg="The provided dict in tower_oauthtoken did not properly contain the token entry")
|
||||
elif isinstance(token_param, string_types):
|
||||
self.oauth_token = self.params.get('tower_oauthtoken')
|
||||
else:
|
||||
error_msg = "The provided tower_oauthtoken type was not valid ({0}). Valid options are str or dict.".format(type(token_param).__name__)
|
||||
self.fail_json(msg=error_msg)
|
||||
|
||||
# Perform some basic validation
|
||||
if not re.match('^https{0,1}://', self.host):
|
||||
self.host = "https://{0}".format(self.host)
|
||||
|
||||
# Try to parse the hostname as a url
|
||||
try:
|
||||
self.url = urlparse(self.host)
|
||||
except Exception as e:
|
||||
self.fail_json(msg="Unable to parse tower_host as a URL ({1}): {0}".format(self.host, e))
|
||||
|
||||
# Try to resolve the hostname
|
||||
hostname = self.url.netloc.split(':')[0]
|
||||
try:
|
||||
gethostbyname(hostname)
|
||||
except Exception as e:
|
||||
self.fail_json(msg="Unable to resolve tower_host ({1}): {0}".format(hostname, e))
|
||||
|
||||
super(TowerAPIModule, self).__init__(argument_spec=argument_spec, direct_params=direct_params,
|
||||
error_callback=error_callback, warn_callback=warn_callback, **kwargs)
|
||||
self.session = Request(cookies=CookieJar(), validate_certs=self.verify_ssl)
|
||||
|
||||
def load_config_files(self):
|
||||
# Load configs like TowerCLI would have from least import to most
|
||||
config_files = ['/etc/tower/tower_cli.cfg', join(expanduser("~"), ".{0}".format(self.config_name))]
|
||||
local_dir = getcwd()
|
||||
config_files.append(join(local_dir, self.config_name))
|
||||
while split(local_dir)[1]:
|
||||
local_dir = split(local_dir)[0]
|
||||
config_files.insert(2, join(local_dir, ".{0}".format(self.config_name)))
|
||||
|
||||
# If we have a specified tower config, load it
|
||||
if self.params.get('tower_config_file'):
|
||||
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. '
|
||||
'Precedence may be unstable, we suggest either using config file or params.'
|
||||
).format(', '.join(duplicated_params)))
|
||||
try:
|
||||
# TODO: warn if there are conflicts with other params
|
||||
self.load_config(self.params.get('tower_config_file'))
|
||||
except ConfigFileException as cfe:
|
||||
# Since we were told specifically to load this we want it to fail if we have an error
|
||||
self.fail_json(msg=cfe)
|
||||
else:
|
||||
for config_file in config_files:
|
||||
if exists(config_file) and not isdir(config_file):
|
||||
# Only throw a formatting error if the file exists and is not a directory
|
||||
try:
|
||||
self.load_config(config_file)
|
||||
except ConfigFileException:
|
||||
self.fail_json(msg='The config file {0} is not properly formatted'.format(config_file))
|
||||
|
||||
def load_config(self, config_path):
|
||||
# Validate the config file is an actual file
|
||||
if not isfile(config_path):
|
||||
raise ConfigFileException('The specified config file does not exist')
|
||||
|
||||
if not access(config_path, R_OK):
|
||||
raise ConfigFileException("The specified config file cannot be read")
|
||||
|
||||
# Read in the file contents:
|
||||
with open(config_path, 'r') as f:
|
||||
config_string = f.read()
|
||||
|
||||
# First try to yaml load the content (which will also load json)
|
||||
try:
|
||||
try_config_parsing = True
|
||||
if HAS_YAML:
|
||||
try:
|
||||
config_data = yaml.load(config_string, Loader=yaml.SafeLoader)
|
||||
# If this is an actual ini file, yaml will return the whole thing as a string instead of a dict
|
||||
if type(config_data) is not dict:
|
||||
raise AssertionError("The yaml config file is not properly formatted as a dict.")
|
||||
try_config_parsing = False
|
||||
|
||||
except(AttributeError, yaml.YAMLError, AssertionError):
|
||||
try_config_parsing = True
|
||||
|
||||
if try_config_parsing:
|
||||
# TowerCLI used to support a config file with a missing [general] section by prepending it if missing
|
||||
if '[general]' not in config_string:
|
||||
config_string = '[general]\n{0}'.format(config_string)
|
||||
|
||||
config = ConfigParser()
|
||||
|
||||
try:
|
||||
placeholder_file = StringIO(config_string)
|
||||
# py2 ConfigParser has readfp, that has been deprecated in favor of read_file in py3
|
||||
# This "if" removes the deprecation warning
|
||||
if hasattr(config, 'read_file'):
|
||||
config.read_file(placeholder_file)
|
||||
else:
|
||||
config.readfp(placeholder_file)
|
||||
|
||||
# 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.short_params:
|
||||
try:
|
||||
config_data[honorred_setting] = config.get('general', honorred_setting)
|
||||
except NoOptionError:
|
||||
pass
|
||||
|
||||
except Exception as e:
|
||||
raise ConfigFileException("An unknown exception occured trying to ini load config file: {0}".format(e))
|
||||
|
||||
except Exception as 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
|
||||
for honorred_setting in self.short_params:
|
||||
if honorred_setting in config_data:
|
||||
# Veriffy SSL must be a boolean
|
||||
if honorred_setting == 'verify_ssl':
|
||||
if type(config_data[honorred_setting]) is str:
|
||||
setattr(self, honorred_setting, strtobool(config_data[honorred_setting]))
|
||||
else:
|
||||
setattr(self, honorred_setting, bool(config_data[honorred_setting]))
|
||||
else:
|
||||
setattr(self, honorred_setting, config_data[honorred_setting])
|
||||
|
||||
@staticmethod
|
||||
def param_to_endpoint(name):
|
||||
exceptions = {
|
||||
@@ -305,32 +106,39 @@ class TowerModule(AnsibleModule):
|
||||
|
||||
return response['json']['results'][0]
|
||||
|
||||
def resolve_name_to_id(self, endpoint, name_or_id):
|
||||
# Try to resolve the object by name
|
||||
def get_one_by_name_or_id(self, endpoint, name_or_id):
|
||||
name_field = 'name'
|
||||
if endpoint == 'users':
|
||||
name_field = 'username'
|
||||
|
||||
response = self.get_endpoint(endpoint, **{'data': {name_field: name_or_id}})
|
||||
if response['status_code'] == 400:
|
||||
self.fail_json(msg="Unable to try and resolve {0} for {1} : {2}".format(endpoint, name_or_id, response['json']['detail']))
|
||||
query_params = {'or__{0}'.format(name_field): name_or_id}
|
||||
try:
|
||||
query_params['or__id'] = int(name_or_id)
|
||||
except ValueError:
|
||||
# If we get a value error, then we didn't have an integer so we can just pass and fall down to the fail
|
||||
pass
|
||||
|
||||
response = self.get_endpoint(endpoint, **{'data': query_params})
|
||||
if response['status_code'] != 200:
|
||||
self.fail_json(
|
||||
msg="Failed to query endpoint {0} for {1} {2} ({3}), see results".format(endpoint, name_field, name_or_id, response['status_code']),
|
||||
resuls=response
|
||||
)
|
||||
|
||||
if response['json']['count'] == 1:
|
||||
return response['json']['results'][0]['id']
|
||||
return response['json']['results'][0]
|
||||
elif response['json']['count'] > 1:
|
||||
for tower_object in response['json']['results']:
|
||||
# ID takes priority, so we match on that first
|
||||
if str(tower_object['id']) == name_or_id:
|
||||
return tower_object
|
||||
# We didn't match on an ID but we found more than 1 object, therefore the results are ambiguous
|
||||
self.fail_json(msg="The requested name or id was ambiguous and resulted in too many items")
|
||||
elif response['json']['count'] == 0:
|
||||
try:
|
||||
int(name_or_id)
|
||||
# If we got 0 items by name, maybe they gave us an ID, let's try looking it up by ID
|
||||
response = self.head_endpoint("{0}/{1}".format(endpoint, name_or_id), **{'return_none_on_404': True})
|
||||
if response is not None:
|
||||
return name_or_id
|
||||
except ValueError:
|
||||
# If we got a value error than we didn't have an integer so we can just pass and fall down to the fail
|
||||
pass
|
||||
|
||||
self.fail_json(msg="The {0} {1} was not found on the Tower server".format(endpoint, name_or_id))
|
||||
else:
|
||||
self.fail_json(msg="Found too many names {0} at endpoint {1} try using an ID instead of a name".format(name_or_id, endpoint))
|
||||
|
||||
def resolve_name_to_id(self, endpoint, name_or_id):
|
||||
return self.get_one_by_name_or_id(endpoint, name_or_id)['id']
|
||||
|
||||
def make_request(self, method, endpoint, *args, **kwargs):
|
||||
# In case someone is calling us directly; make sure we were given a method, let's not just assume a GET
|
||||
@@ -650,13 +458,13 @@ class TowerModule(AnsibleModule):
|
||||
"""
|
||||
if isinstance(obj, dict):
|
||||
for val in obj.values():
|
||||
if TowerModule.has_encrypted_values(val):
|
||||
if TowerAPIModule.has_encrypted_values(val):
|
||||
return True
|
||||
elif isinstance(obj, list):
|
||||
for val in obj:
|
||||
if TowerModule.has_encrypted_values(val):
|
||||
if TowerAPIModule.has_encrypted_values(val):
|
||||
return True
|
||||
elif obj == TowerModule.ENCRYPTED_STRING:
|
||||
elif obj == TowerAPIModule.ENCRYPTED_STRING:
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -678,10 +486,9 @@ class TowerModule(AnsibleModule):
|
||||
# This will exit from the module on its own
|
||||
# If the method successfully updates an item and on_update param is defined,
|
||||
# the on_update parameter will be called as a method pasing in this object and the json from the response
|
||||
# This will return one of three things:
|
||||
# This will return one of two things:
|
||||
# 1. None if the existing_item does not need to be updated
|
||||
# 2. The response from Tower from patching to the endpoint. It's up to you to process the response and exit from the module.
|
||||
# 3. An ItemNotDefined exception, if the existing_item does not exist
|
||||
# Note: common error codes from the Tower API can cause the module to fail
|
||||
response = None
|
||||
if existing_item:
|
||||
@@ -745,7 +552,7 @@ class TowerModule(AnsibleModule):
|
||||
return self.create_if_needed(existing_item, new_item, endpoint, on_create=on_create, item_type=item_type, associations=associations)
|
||||
|
||||
def logout(self):
|
||||
if self.authenticated:
|
||||
if self.authenticated and self.oauth_token_id:
|
||||
# Attempt to delete our current token from /api/v2/tokens/
|
||||
# Post to the tokens endpoint with baisc auth to try and get a token
|
||||
api_token_url = (
|
||||
@@ -777,27 +584,47 @@ class TowerModule(AnsibleModule):
|
||||
# Sanity check: Did the server send back some kind of internal error?
|
||||
self.warn('Failed to release tower token {0}: {1}'.format(self.oauth_token_id, e))
|
||||
|
||||
def fail_json(self, **kwargs):
|
||||
# Try to log out if we are authenticated
|
||||
self.logout()
|
||||
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
|
||||
else:
|
||||
return True
|
||||
|
||||
def wait_on_url(self, url, object_name, object_type, timeout=30, interval=10):
|
||||
# Grab our start time to compare against for the timeout
|
||||
start = time.time()
|
||||
result = self.get_endpoint(url)
|
||||
while not result['json']['finished']:
|
||||
# If we are past our time out fail with a message
|
||||
if timeout and timeout < time.time() - start:
|
||||
# Account for Legacy messages
|
||||
if object_type is 'legacy_job_wait':
|
||||
self.json_output['msg'] = 'Monitoring of Job - {0} aborted due to timeout'.format(object_name)
|
||||
else:
|
||||
self.json_output['msg'] = 'Monitoring of {0} - {1} aborted due to timeout'.format(object_type, object_name)
|
||||
self.wait_output(result)
|
||||
self.fail_json(**self.json_output)
|
||||
|
||||
# Put the process to sleep for our interval
|
||||
time.sleep(interval)
|
||||
|
||||
result = self.get_endpoint(url)
|
||||
self.json_output['status'] = result['json']['status']
|
||||
|
||||
# If the job has failed, we want to raise a task failure for that so we get a non-zero response.
|
||||
if result['json']['failed']:
|
||||
# Account for Legacy messages
|
||||
if object_type is 'legacy_job_wait':
|
||||
self.json_output['msg'] = 'Job with id {0} failed'.format(object_name)
|
||||
else:
|
||||
self.json_output['msg'] = 'The {0} - {1}, failed'.format(object_type, object_name)
|
||||
self.wait_output(result)
|
||||
self.fail_json(**self.json_output)
|
||||
|
||||
self.wait_output(result)
|
||||
|
||||
return result
|
||||
|
||||
def wait_output(self, response):
|
||||
for k in ('id', 'status', 'elapsed', 'started', 'finished'):
|
||||
self.json_output[k] = response['json'].get(k)
|
||||
|
||||
53
awx_collection/plugins/module_utils/tower_awxkit.py
Normal file
53
awx_collection/plugins/module_utils/tower_awxkit.py
Normal file
@@ -0,0 +1,53 @@
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
from . tower_module import TowerModule
|
||||
from ansible.module_utils.basic import missing_required_lib
|
||||
|
||||
try:
|
||||
from awxkit.api.client import Connection
|
||||
from awxkit.api.pages.api import ApiV2
|
||||
from awxkit.api import get_registered_page
|
||||
HAS_AWX_KIT = True
|
||||
except ImportError:
|
||||
HAS_AWX_KIT = False
|
||||
|
||||
|
||||
class TowerAWXKitModule(TowerModule):
|
||||
connection = None
|
||||
apiV2Ref = None
|
||||
|
||||
def __init__(self, argument_spec, **kwargs):
|
||||
kwargs['supports_check_mode'] = False
|
||||
|
||||
super(TowerAWXKitModule, self).__init__(argument_spec=argument_spec, **kwargs)
|
||||
|
||||
# Die if we don't have AWX_KIT installed
|
||||
if not HAS_AWX_KIT:
|
||||
self.exit_json(msg=missing_required_lib('awxkit'))
|
||||
|
||||
# Establish our conneciton object
|
||||
self.connection = Connection(self.host, verify=self.verify_ssl)
|
||||
|
||||
def authenticate(self):
|
||||
try:
|
||||
if self.oauth_token:
|
||||
self.connection.login(None, None, token=self.oauth_token)
|
||||
self.authenticated = True
|
||||
elif self.username:
|
||||
self.connection.login(username=self.username, password=self.password)
|
||||
self.authenticated = True
|
||||
except Exception:
|
||||
self.exit_json("Failed to authenticate")
|
||||
|
||||
def get_api_v2_object(self):
|
||||
if not self.apiV2Ref:
|
||||
if not self.authenticated:
|
||||
self.authenticate()
|
||||
v2_index = get_registered_page('/api/v2/')(self.connection).get()
|
||||
self.api_ref = ApiV2(connection=self.connection, **{'json': v2_index})
|
||||
return self.api_ref
|
||||
|
||||
def logout(self):
|
||||
if self.authenticated:
|
||||
self.connection.logout()
|
||||
@@ -91,7 +91,7 @@ def tower_check_mode(module):
|
||||
module.fail_json(changed=False, msg='Failed check mode: {0}'.format(excinfo))
|
||||
|
||||
|
||||
class TowerModule(AnsibleModule):
|
||||
class TowerLegacyModule(AnsibleModule):
|
||||
def __init__(self, argument_spec, **kwargs):
|
||||
args = dict(
|
||||
tower_host=dict(),
|
||||
@@ -110,7 +110,7 @@ class TowerModule(AnsibleModule):
|
||||
('tower_config_file', 'validate_certs'),
|
||||
))
|
||||
|
||||
super(TowerModule, self).__init__(argument_spec=args, **kwargs)
|
||||
super(TowerLegacyModule, self).__init__(argument_spec=args, **kwargs)
|
||||
|
||||
if not HAS_TOWER_CLI:
|
||||
self.fail_json(msg=missing_required_lib('ansible-tower-cli'),
|
||||
239
awx_collection/plugins/module_utils/tower_module.py
Normal file
239
awx_collection/plugins/module_utils/tower_module.py
Normal file
@@ -0,0 +1,239 @@
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule, env_fallback
|
||||
from ansible.module_utils.six import PY2, string_types
|
||||
from ansible.module_utils.six.moves import StringIO
|
||||
from ansible.module_utils.six.moves.urllib.parse import urlparse
|
||||
from ansible.module_utils.six.moves.configparser import ConfigParser, NoOptionError
|
||||
from socket import gethostbyname
|
||||
import re
|
||||
from os.path import isfile, expanduser, split, join, exists, isdir
|
||||
from os import access, R_OK, getcwd
|
||||
from distutils.util import strtobool
|
||||
|
||||
try:
|
||||
import yaml
|
||||
HAS_YAML = True
|
||||
except ImportError:
|
||||
HAS_YAML = False
|
||||
|
||||
|
||||
class ConfigFileException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ItemNotDefined(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class TowerModule(AnsibleModule):
|
||||
url = None
|
||||
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='raw', 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
|
||||
verify_ssl = True
|
||||
oauth_token = None
|
||||
oauth_token_id = None
|
||||
authenticated = False
|
||||
config_name = 'tower_cli.cfg'
|
||||
ENCRYPTED_STRING = "$encrypted$"
|
||||
version_checked = False
|
||||
error_callback = None
|
||||
warn_callback = None
|
||||
|
||||
def __init__(self, argument_spec=None, 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}
|
||||
|
||||
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
|
||||
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 magic depending on whether tower_oauthtoken is a string or a dict
|
||||
if self.params.get('tower_oauthtoken'):
|
||||
token_param = self.params.get('tower_oauthtoken')
|
||||
if type(token_param) is dict:
|
||||
if 'token' in token_param:
|
||||
self.oauth_token = self.params.get('tower_oauthtoken')['token']
|
||||
else:
|
||||
self.fail_json(msg="The provided dict in tower_oauthtoken did not properly contain the token entry")
|
||||
elif isinstance(token_param, string_types):
|
||||
self.oauth_token = self.params.get('tower_oauthtoken')
|
||||
else:
|
||||
error_msg = "The provided tower_oauthtoken type was not valid ({0}). Valid options are str or dict.".format(type(token_param).__name__)
|
||||
self.fail_json(msg=error_msg)
|
||||
|
||||
# Perform some basic validation
|
||||
if not re.match('^https{0,1}://', self.host):
|
||||
self.host = "https://{0}".format(self.host)
|
||||
|
||||
# Try to parse the hostname as a url
|
||||
try:
|
||||
self.url = urlparse(self.host)
|
||||
except Exception as e:
|
||||
self.fail_json(msg="Unable to parse tower_host as a URL ({1}): {0}".format(self.host, e))
|
||||
|
||||
# Try to resolve the hostname
|
||||
hostname = self.url.netloc.split(':')[0]
|
||||
try:
|
||||
gethostbyname(hostname)
|
||||
except Exception as e:
|
||||
self.fail_json(msg="Unable to resolve tower_host ({1}): {0}".format(hostname, e))
|
||||
|
||||
def load_config_files(self):
|
||||
# Load configs like TowerCLI would have from least import to most
|
||||
config_files = ['/etc/tower/tower_cli.cfg', join(expanduser("~"), ".{0}".format(self.config_name))]
|
||||
local_dir = getcwd()
|
||||
config_files.append(join(local_dir, self.config_name))
|
||||
while split(local_dir)[1]:
|
||||
local_dir = split(local_dir)[0]
|
||||
config_files.insert(2, join(local_dir, ".{0}".format(self.config_name)))
|
||||
|
||||
# If we have a specified tower config, load it
|
||||
if self.params.get('tower_config_file'):
|
||||
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. '
|
||||
'Precedence may be unstable, we suggest either using config file or params.'
|
||||
).format(', '.join(duplicated_params)))
|
||||
try:
|
||||
# TODO: warn if there are conflicts with other params
|
||||
self.load_config(self.params.get('tower_config_file'))
|
||||
except ConfigFileException as cfe:
|
||||
# Since we were told specifically to load this we want it to fail if we have an error
|
||||
self.fail_json(msg=cfe)
|
||||
else:
|
||||
for config_file in config_files:
|
||||
if exists(config_file) and not isdir(config_file):
|
||||
# Only throw a formatting error if the file exists and is not a directory
|
||||
try:
|
||||
self.load_config(config_file)
|
||||
except ConfigFileException:
|
||||
self.fail_json(msg='The config file {0} is not properly formatted'.format(config_file))
|
||||
|
||||
def load_config(self, config_path):
|
||||
# Validate the config file is an actual file
|
||||
if not isfile(config_path):
|
||||
raise ConfigFileException('The specified config file does not exist')
|
||||
|
||||
if not access(config_path, R_OK):
|
||||
raise ConfigFileException("The specified config file cannot be read")
|
||||
|
||||
# Read in the file contents:
|
||||
with open(config_path, 'r') as f:
|
||||
config_string = f.read()
|
||||
|
||||
# First try to yaml load the content (which will also load json)
|
||||
try:
|
||||
try_config_parsing = True
|
||||
if HAS_YAML:
|
||||
try:
|
||||
config_data = yaml.load(config_string, Loader=yaml.SafeLoader)
|
||||
# If this is an actual ini file, yaml will return the whole thing as a string instead of a dict
|
||||
if type(config_data) is not dict:
|
||||
raise AssertionError("The yaml config file is not properly formatted as a dict.")
|
||||
try_config_parsing = False
|
||||
|
||||
except(AttributeError, yaml.YAMLError, AssertionError):
|
||||
try_config_parsing = True
|
||||
|
||||
if try_config_parsing:
|
||||
# TowerCLI used to support a config file with a missing [general] section by prepending it if missing
|
||||
if '[general]' not in config_string:
|
||||
config_string = '[general]\n{0}'.format(config_string)
|
||||
|
||||
config = ConfigParser()
|
||||
|
||||
try:
|
||||
placeholder_file = StringIO(config_string)
|
||||
# py2 ConfigParser has readfp, that has been deprecated in favor of read_file in py3
|
||||
# This "if" removes the deprecation warning
|
||||
if hasattr(config, 'read_file'):
|
||||
config.read_file(placeholder_file)
|
||||
else:
|
||||
config.readfp(placeholder_file)
|
||||
|
||||
# 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.short_params:
|
||||
try:
|
||||
config_data[honorred_setting] = config.get('general', honorred_setting)
|
||||
except NoOptionError:
|
||||
pass
|
||||
|
||||
except Exception as e:
|
||||
raise ConfigFileException("An unknown exception occured trying to ini load config file: {0}".format(e))
|
||||
|
||||
except Exception as 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
|
||||
for honorred_setting in self.short_params:
|
||||
if honorred_setting in config_data:
|
||||
# Veriffy SSL must be a boolean
|
||||
if honorred_setting == 'verify_ssl':
|
||||
if type(config_data[honorred_setting]) is str:
|
||||
setattr(self, honorred_setting, strtobool(config_data[honorred_setting]))
|
||||
else:
|
||||
setattr(self, honorred_setting, bool(config_data[honorred_setting]))
|
||||
else:
|
||||
setattr(self, honorred_setting, config_data[honorred_setting])
|
||||
|
||||
def logout(self):
|
||||
# This method is intended to be overridden
|
||||
pass
|
||||
|
||||
def fail_json(self, **kwargs):
|
||||
# Try to log out if we are authenticated
|
||||
self.logout()
|
||||
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)
|
||||
@@ -269,7 +269,7 @@ EXAMPLES = '''
|
||||
|
||||
'''
|
||||
|
||||
from ..module_utils.tower_api import TowerModule
|
||||
from ..module_utils.tower_api import TowerAPIModule
|
||||
|
||||
KIND_CHOICES = {
|
||||
'ssh': 'Machine',
|
||||
@@ -336,7 +336,7 @@ def main():
|
||||
)
|
||||
|
||||
# Create a module for ourselves
|
||||
module = TowerModule(argument_spec=argument_spec, required_one_of=[['kind', 'credential_type']])
|
||||
module = TowerAPIModule(argument_spec=argument_spec, required_one_of=[['kind', 'credential_type']])
|
||||
|
||||
# Extract our parameters
|
||||
name = module.params.get('name')
|
||||
|
||||
@@ -70,7 +70,7 @@ EXAMPLES = '''
|
||||
|
||||
'''
|
||||
|
||||
from ..module_utils.tower_api import TowerModule
|
||||
from ..module_utils.tower_api import TowerAPIModule
|
||||
|
||||
|
||||
def main():
|
||||
@@ -85,7 +85,7 @@ def main():
|
||||
)
|
||||
|
||||
# Create a module for ourselves
|
||||
module = TowerModule(argument_spec=argument_spec)
|
||||
module = TowerAPIModule(argument_spec=argument_spec)
|
||||
|
||||
# Extract our parameters
|
||||
description = module.params.get('description')
|
||||
|
||||
@@ -81,7 +81,7 @@ EXAMPLES = '''
|
||||
RETURN = ''' # '''
|
||||
|
||||
|
||||
from ..module_utils.tower_api import TowerModule
|
||||
from ..module_utils.tower_api import TowerAPIModule
|
||||
|
||||
KIND_CHOICES = {
|
||||
'ssh': 'Machine',
|
||||
@@ -105,7 +105,7 @@ def main():
|
||||
)
|
||||
|
||||
# Create a module for ourselves
|
||||
module = TowerModule(argument_spec=argument_spec)
|
||||
module = TowerAPIModule(argument_spec=argument_spec)
|
||||
|
||||
# Extract our parameters
|
||||
name = module.params.get('name')
|
||||
|
||||
166
awx_collection/plugins/modules/tower_export.py
Normal file
166
awx_collection/plugins/modules/tower_export.py
Normal file
@@ -0,0 +1,166 @@
|
||||
#!/usr/bin/python
|
||||
# coding: utf-8 -*-
|
||||
|
||||
# (c) 2017, John Westcott IV <john.westcott.iv@redhat.com>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: tower_export
|
||||
author: "John Westcott IV (@john-westcott-iv)"
|
||||
version_added: "3.7"
|
||||
short_description: export resources from Ansible Tower.
|
||||
description:
|
||||
- Export assets from Ansible Tower.
|
||||
options:
|
||||
all:
|
||||
description:
|
||||
- Export all assets
|
||||
type: bool
|
||||
default: 'False'
|
||||
organizations:
|
||||
description:
|
||||
- organization name to export
|
||||
type: str
|
||||
users:
|
||||
description:
|
||||
- user name to export
|
||||
type: str
|
||||
teams:
|
||||
description:
|
||||
- team name to export
|
||||
type: str
|
||||
credential_types:
|
||||
description:
|
||||
- credential type name to export
|
||||
type: str
|
||||
credentials:
|
||||
description:
|
||||
- credential name to export
|
||||
type: str
|
||||
notification_templates:
|
||||
description:
|
||||
- notification template name to export
|
||||
type: str
|
||||
inventory_sources:
|
||||
description:
|
||||
- inventory soruce to export
|
||||
type: str
|
||||
inventory:
|
||||
description:
|
||||
- inventory name to export
|
||||
type: str
|
||||
projects:
|
||||
description:
|
||||
- project name to export
|
||||
type: str
|
||||
job_templates:
|
||||
description:
|
||||
- job template name to export
|
||||
type: str
|
||||
workflow_job_templates:
|
||||
description:
|
||||
- workflow name to export
|
||||
type: str
|
||||
requirements:
|
||||
- "awxkit >= 9.3.0"
|
||||
notes:
|
||||
- Specifying a name of "all" for any asset type will export all items of that asset type.
|
||||
extends_documentation_fragment: awx.awx.auth
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: Export all tower assets
|
||||
tower_export:
|
||||
all: True
|
||||
- name: Export all inventories
|
||||
tower_export:
|
||||
inventory: 'all'
|
||||
- name: Export a job template named "My Template" and all Credentials
|
||||
tower_export:
|
||||
job_template: "My Template"
|
||||
credential: 'all'
|
||||
'''
|
||||
|
||||
from os import environ
|
||||
import logging
|
||||
from ansible.module_utils.six.moves import StringIO
|
||||
from ..module_utils.tower_awxkit import TowerAWXKitModule
|
||||
|
||||
try:
|
||||
from awxkit.api.pages.api import EXPORTABLE_RESOURCES
|
||||
HAS_EXPORTABLE_RESOURCES = True
|
||||
except ImportError:
|
||||
HAS_EXPORTABLE_RESOURCES = False
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = dict(
|
||||
all=dict(type='bool', default=False),
|
||||
)
|
||||
|
||||
# We are not going to raise an error here because the __init__ method of TowerAWXKitModule will do that for us
|
||||
if HAS_EXPORTABLE_RESOURCES:
|
||||
for resource in EXPORTABLE_RESOURCES:
|
||||
argument_spec[resource] = dict(type='str')
|
||||
|
||||
module = TowerAWXKitModule(argument_spec=argument_spec)
|
||||
|
||||
if not HAS_EXPORTABLE_RESOURCES:
|
||||
module.fail_json(msg="Your version of awxkit does not have import/export")
|
||||
|
||||
# The export process will never change a Tower system
|
||||
module.json_output['changed'] = False
|
||||
|
||||
# The exporter code currently works like the following:
|
||||
# Empty string == all assets of that type
|
||||
# Non-Empty string = just one asset of that type (by name or ID)
|
||||
# Asset type not present or None = skip asset type (unless everything is None, then export all)
|
||||
# Here we are going to setup a dict of values to export
|
||||
export_args = {}
|
||||
for resource in EXPORTABLE_RESOURCES:
|
||||
if module.params.get('all') or module.params.get(resource) == 'all':
|
||||
# If we are exporting everything or we got the keyword "all" we pass in an empty string for this asset type
|
||||
export_args[resource] = ''
|
||||
else:
|
||||
# Otherwise we take either the string or None (if the parameter was not passed) to get one or no items
|
||||
export_args[resource] = module.params.get(resource)
|
||||
|
||||
# Currently the import process does not return anything on error
|
||||
# It simply just logs to pythons logger
|
||||
# Setup a log gobbler to get error messages from import_assets
|
||||
log_capture_string = StringIO()
|
||||
ch = logging.StreamHandler(log_capture_string)
|
||||
for logger_name in ['awxkit.api.pages.api', 'awxkit.api.pages.page']:
|
||||
logger = logging.getLogger(logger_name)
|
||||
logger.setLevel(logging.WARNING)
|
||||
ch.setLevel(logging.WARNING)
|
||||
|
||||
logger.addHandler(ch)
|
||||
log_contents = ''
|
||||
|
||||
# Run the import process
|
||||
try:
|
||||
module.json_output['assets'] = module.get_api_v2_object().export_assets(**export_args)
|
||||
module.exit_json(**module.json_output)
|
||||
except Exception as e:
|
||||
module.fail_json(msg="Failed to export assets {0}".format(e))
|
||||
finally:
|
||||
# Finally consume the logs incase there were any errors and die if there were
|
||||
log_contents = log_capture_string.getvalue()
|
||||
log_capture_string.close()
|
||||
if log_contents != '':
|
||||
module.fail_json(msg=log_contents)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -76,7 +76,7 @@ EXAMPLES = '''
|
||||
tower_config_file: "~/tower_cli.cfg"
|
||||
'''
|
||||
|
||||
from ..module_utils.tower_api import TowerModule
|
||||
from ..module_utils.tower_api import TowerAPIModule
|
||||
import json
|
||||
|
||||
|
||||
@@ -94,7 +94,7 @@ def main():
|
||||
)
|
||||
|
||||
# Create a module for ourselves
|
||||
module = TowerModule(argument_spec=argument_spec)
|
||||
module = TowerAPIModule(argument_spec=argument_spec)
|
||||
|
||||
# Extract our parameters
|
||||
name = module.params.get('name')
|
||||
|
||||
@@ -72,7 +72,7 @@ EXAMPLES = '''
|
||||
'''
|
||||
|
||||
|
||||
from ..module_utils.tower_api import TowerModule
|
||||
from ..module_utils.tower_api import TowerAPIModule
|
||||
import json
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ def main():
|
||||
)
|
||||
|
||||
# Create a module for ourselves
|
||||
module = TowerModule(argument_spec=argument_spec)
|
||||
module = TowerAPIModule(argument_spec=argument_spec)
|
||||
|
||||
# Extract our parameters
|
||||
name = module.params.get('name')
|
||||
|
||||
105
awx_collection/plugins/modules/tower_import.py
Normal file
105
awx_collection/plugins/modules/tower_import.py
Normal file
@@ -0,0 +1,105 @@
|
||||
#!/usr/bin/python
|
||||
# coding: utf-8 -*-
|
||||
|
||||
# (c) 2017, John Westcott IV <john.westcott.iv@redhat.com>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: tower_import
|
||||
author: "John Westcott (@john-westcott-iv)"
|
||||
version_added: "3.7"
|
||||
short_description: import resources into Ansible Tower.
|
||||
description:
|
||||
- Import assets into Ansible Tower. See
|
||||
U(https://www.ansible.com/tower) for an overview.
|
||||
options:
|
||||
assets:
|
||||
description:
|
||||
- The assets to import.
|
||||
- This can be the output of tower_export or loaded from a file
|
||||
required: True
|
||||
type: dict
|
||||
requirements:
|
||||
- "awxkit >= 9.3.0"
|
||||
extends_documentation_fragment: awx.awx.auth
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: Export all assets
|
||||
tower_export:
|
||||
all: True
|
||||
registeR: export_output
|
||||
|
||||
- name: Import all tower assets from our export
|
||||
tower_import:
|
||||
assets: "{{ export_output.assets }}"
|
||||
|
||||
- name: Load data from a json file created by a command like awx export --organization Default
|
||||
tower_import:
|
||||
assets: "{{ lookup('file', 'org.json') | from_json() }}"
|
||||
'''
|
||||
|
||||
from ..module_utils.tower_awxkit import TowerAWXKitModule
|
||||
|
||||
# These two lines are not needed if awxkit changes to do progamatic notifications on issues
|
||||
from ansible.module_utils.six.moves import StringIO
|
||||
import logging
|
||||
|
||||
# In this module we don't use EXPORTABLE_RESOURCES, we just want to validate that our installed awxkit has import/export
|
||||
try:
|
||||
from awxkit.api.pages.api import EXPORTABLE_RESOURCES
|
||||
HAS_EXPORTABLE_RESOURCES = True
|
||||
except ImportError:
|
||||
HAS_EXPORTABLE_RESOURCES = False
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = dict(
|
||||
assets=dict(type='dict', required=True)
|
||||
)
|
||||
|
||||
module = TowerAWXKitModule(argument_spec=argument_spec, supports_check_mode=False)
|
||||
|
||||
assets = module.params.get('assets')
|
||||
|
||||
if not HAS_EXPORTABLE_RESOURCES:
|
||||
module.fail_json(msg="Your version of awxkit does not appear to have import/export")
|
||||
|
||||
# Currently the import process does not return anything on error
|
||||
# It simply just logs to pythons logger
|
||||
# Setup a log gobbler to get error messages from import_assets
|
||||
logger = logging.getLogger('awxkit.api.pages.api')
|
||||
logger.setLevel(logging.WARNING)
|
||||
log_capture_string = StringIO()
|
||||
ch = logging.StreamHandler(log_capture_string)
|
||||
ch.setLevel(logging.WARNING)
|
||||
logger.addHandler(ch)
|
||||
log_contents = ''
|
||||
|
||||
# Run the import process
|
||||
try:
|
||||
module.json_output['changed'] = module.get_api_v2_object().import_assets(assets)
|
||||
except Exception as e:
|
||||
module.fail_json(msg="Failed to import assets {0}".format(e))
|
||||
finally:
|
||||
# Finally consume the logs incase there were any errors and die if there were
|
||||
log_contents = log_capture_string.getvalue()
|
||||
log_capture_string.close()
|
||||
if log_contents != '':
|
||||
module.fail_json(msg=log_contents)
|
||||
|
||||
module.exit_json(**module.json_output)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -48,7 +48,11 @@ options:
|
||||
type: str
|
||||
host_filter:
|
||||
description:
|
||||
- The host_filter field. Only useful when C(kind=smart).
|
||||
- The host_filter field. Only useful when C(kind=smart).
|
||||
type: str
|
||||
insights_credential:
|
||||
description:
|
||||
- Credentials to be used by hosts belonging to this inventory when accessing Red Hat Insights API.
|
||||
type: str
|
||||
state:
|
||||
description:
|
||||
@@ -71,7 +75,7 @@ EXAMPLES = '''
|
||||
'''
|
||||
|
||||
|
||||
from ..module_utils.tower_api import TowerModule
|
||||
from ..module_utils.tower_api import TowerAPIModule
|
||||
import json
|
||||
|
||||
|
||||
@@ -84,11 +88,12 @@ def main():
|
||||
variables=dict(type='dict'),
|
||||
kind=dict(choices=['', 'smart'], default=''),
|
||||
host_filter=dict(),
|
||||
insights_credential=dict(),
|
||||
state=dict(choices=['present', 'absent'], default='present'),
|
||||
)
|
||||
|
||||
# Create a module for ourselves
|
||||
module = TowerModule(argument_spec=argument_spec)
|
||||
module = TowerAPIModule(argument_spec=argument_spec)
|
||||
|
||||
# Extract our parameters
|
||||
name = module.params.get('name')
|
||||
@@ -98,6 +103,7 @@ def main():
|
||||
state = module.params.get('state')
|
||||
kind = module.params.get('kind')
|
||||
host_filter = module.params.get('host_filter')
|
||||
insights_credential = module.params.get('insights_credential')
|
||||
|
||||
# Attempt to look up the related items the user specified (these will fail the module if not found)
|
||||
org_id = module.resolve_name_to_id('organizations', organization)
|
||||
@@ -125,6 +131,8 @@ def main():
|
||||
inventory_fields['description'] = description
|
||||
if variables is not None:
|
||||
inventory_fields['variables'] = json.dumps(variables)
|
||||
if insights_credential is not None:
|
||||
inventory_fields['insights_credential'] = module.resolve_name_to_id('credentials', insights_credential)
|
||||
|
||||
# We need to perform a check to make sure you are not trying to convert a regular inventory into a smart one.
|
||||
if inventory and inventory['kind'] == '' and inventory_fields['kind'] == 'smart':
|
||||
|
||||
@@ -57,22 +57,22 @@ options:
|
||||
description:
|
||||
- The variables or environment fields to apply to this source type.
|
||||
type: dict
|
||||
enabled_var:
|
||||
description:
|
||||
- The variable to use to determine enabled state e.g., "status.power_state"
|
||||
type: str
|
||||
enabled_value:
|
||||
description:
|
||||
- Value when the host is considered enabled, e.g., "powered_on"
|
||||
type: str
|
||||
host_filter:
|
||||
description:
|
||||
- If specified, AWX will only import hosts that match this regular expression.
|
||||
type: str
|
||||
credential:
|
||||
description:
|
||||
- Credential to use for the source.
|
||||
type: str
|
||||
source_regions:
|
||||
description:
|
||||
- Regions for cloud provider.
|
||||
type: str
|
||||
instance_filters:
|
||||
description:
|
||||
- Comma-separated list of filter expressions for matching hosts.
|
||||
type: str
|
||||
group_by:
|
||||
description:
|
||||
- Limit groups automatically created from inventory source.
|
||||
type: str
|
||||
overwrite:
|
||||
description:
|
||||
- Delete child groups and hosts not found in source.
|
||||
@@ -144,7 +144,7 @@ EXAMPLES = '''
|
||||
private: false
|
||||
'''
|
||||
|
||||
from ..module_utils.tower_api import TowerModule
|
||||
from ..module_utils.tower_api import TowerAPIModule
|
||||
from json import dumps
|
||||
|
||||
|
||||
@@ -164,10 +164,10 @@ def main():
|
||||
source_path=dict(),
|
||||
source_script=dict(),
|
||||
source_vars=dict(type='dict'),
|
||||
enabled_var=dict(),
|
||||
enabled_value=dict(),
|
||||
host_filter=dict(),
|
||||
credential=dict(),
|
||||
source_regions=dict(),
|
||||
instance_filters=dict(),
|
||||
group_by=dict(),
|
||||
overwrite=dict(type='bool'),
|
||||
overwrite_vars=dict(type='bool'),
|
||||
custom_virtualenv=dict(),
|
||||
@@ -184,7 +184,7 @@ def main():
|
||||
)
|
||||
|
||||
# Create a module for ourselves
|
||||
module = TowerModule(argument_spec=argument_spec)
|
||||
module = TowerAPIModule(argument_spec=argument_spec)
|
||||
|
||||
# Extract our parameters
|
||||
name = module.params.get('name')
|
||||
@@ -245,10 +245,9 @@ def main():
|
||||
|
||||
OPTIONAL_VARS = (
|
||||
'description', 'source', 'source_path', 'source_vars',
|
||||
'source_regions', 'instance_filters', 'group_by',
|
||||
'overwrite', 'overwrite_vars', 'custom_virtualenv',
|
||||
'timeout', 'verbosity', 'update_on_launch', 'update_cache_timeout',
|
||||
'update_on_project_update'
|
||||
'update_on_project_update', 'enabled_var', 'enabled_value', 'host_filter',
|
||||
)
|
||||
|
||||
# Layer in all remaining optional information
|
||||
|
||||
@@ -50,7 +50,7 @@ id:
|
||||
'''
|
||||
|
||||
|
||||
from ..module_utils.tower_api import TowerModule
|
||||
from ..module_utils.tower_api import TowerAPIModule
|
||||
|
||||
|
||||
def main():
|
||||
@@ -61,7 +61,7 @@ def main():
|
||||
)
|
||||
|
||||
# Create a module for ourselves
|
||||
module = TowerModule(argument_spec=argument_spec)
|
||||
module = TowerAPIModule(argument_spec=argument_spec)
|
||||
|
||||
# Extract our parameters
|
||||
job_id = module.params.get('job_id')
|
||||
|
||||
@@ -81,6 +81,22 @@ options:
|
||||
description:
|
||||
- Passwords for credentials which are set to prompt on launch
|
||||
type: dict
|
||||
wait:
|
||||
description:
|
||||
- Wait for the job to complete.
|
||||
default: False
|
||||
type: bool
|
||||
interval:
|
||||
description:
|
||||
- The interval to request an update from Tower.
|
||||
required: False
|
||||
default: 1
|
||||
type: float
|
||||
timeout:
|
||||
description:
|
||||
- If waiting for the job to complete this will abort after this
|
||||
amount of seconds
|
||||
type: int
|
||||
extends_documentation_fragment: awx.awx.auth
|
||||
'''
|
||||
|
||||
@@ -124,7 +140,7 @@ status:
|
||||
sample: pending
|
||||
'''
|
||||
|
||||
from ..module_utils.tower_api import TowerModule
|
||||
from ..module_utils.tower_api import TowerAPIModule
|
||||
|
||||
|
||||
def main():
|
||||
@@ -143,10 +159,13 @@ def main():
|
||||
verbosity=dict(type='int', choices=[0, 1, 2, 3, 4, 5]),
|
||||
diff_mode=dict(type='bool'),
|
||||
credential_passwords=dict(type='dict'),
|
||||
wait=dict(default=False, type='bool'),
|
||||
interval=dict(default=1.0, type='float'),
|
||||
timeout=dict(default=None, type='int'),
|
||||
)
|
||||
|
||||
# Create a module for ourselves
|
||||
module = TowerModule(argument_spec=argument_spec)
|
||||
module = TowerAPIModule(argument_spec=argument_spec)
|
||||
|
||||
optional_args = {}
|
||||
# Extract our parameters
|
||||
@@ -162,6 +181,9 @@ def main():
|
||||
optional_args['verbosity'] = module.params.get('verbosity')
|
||||
optional_args['diff_mode'] = module.params.get('diff_mode')
|
||||
optional_args['credential_passwords'] = module.params.get('credential_passwords')
|
||||
wait = module.params.get('wait')
|
||||
interval = module.params.get('interval')
|
||||
timeout = module.params.get('timeout')
|
||||
|
||||
# Create a datastructure to pass into our job launch
|
||||
post_data = {}
|
||||
@@ -216,6 +238,21 @@ def main():
|
||||
if results['status_code'] != 201:
|
||||
module.fail_json(msg="Failed to launch job, see response for details", **{'response': results})
|
||||
|
||||
if not wait:
|
||||
module.exit_json(**{
|
||||
'changed': True,
|
||||
'id': results['json']['id'],
|
||||
'status': results['json']['status'],
|
||||
})
|
||||
|
||||
# Invoke wait function
|
||||
results = module.wait_on_url(
|
||||
url=results['json']['url'],
|
||||
object_name=name,
|
||||
object_type='Job',
|
||||
timeout=timeout, interval=interval
|
||||
)
|
||||
|
||||
module.exit_json(**{
|
||||
'changed': True,
|
||||
'id': results['json']['id'],
|
||||
|
||||
@@ -80,7 +80,7 @@ results:
|
||||
'''
|
||||
|
||||
|
||||
from ..module_utils.tower_api import TowerModule
|
||||
from ..module_utils.tower_api import TowerAPIModule
|
||||
|
||||
|
||||
def main():
|
||||
@@ -93,7 +93,7 @@ def main():
|
||||
)
|
||||
|
||||
# Create a module for ourselves
|
||||
module = TowerModule(
|
||||
module = TowerAPIModule(
|
||||
argument_spec=argument_spec,
|
||||
mutually_exclusive=[
|
||||
('page', 'all_pages'),
|
||||
|
||||
@@ -317,7 +317,7 @@ EXAMPLES = '''
|
||||
|
||||
'''
|
||||
|
||||
from ..module_utils.tower_api import TowerModule
|
||||
from ..module_utils.tower_api import TowerAPIModule
|
||||
import json
|
||||
|
||||
|
||||
@@ -388,7 +388,7 @@ def main():
|
||||
)
|
||||
|
||||
# Create a module for ourselves
|
||||
module = TowerModule(argument_spec=argument_spec)
|
||||
module = TowerAPIModule(argument_spec=argument_spec)
|
||||
|
||||
# Extract our parameters
|
||||
name = module.params.get('name')
|
||||
|
||||
@@ -55,7 +55,7 @@ EXAMPLES = '''
|
||||
- name: Launch a job
|
||||
tower_job_launch:
|
||||
job_template: "My Job Template"
|
||||
register: job
|
||||
register: job
|
||||
|
||||
- name: Wait for job max 120s
|
||||
tower_job_wait:
|
||||
@@ -92,21 +92,7 @@ status:
|
||||
'''
|
||||
|
||||
|
||||
from ..module_utils.tower_api import TowerModule
|
||||
import time
|
||||
|
||||
|
||||
def check_job(module, job_url):
|
||||
response = module.get_endpoint(job_url)
|
||||
if response['status_code'] != 200:
|
||||
module.fail_json(msg="Unable to read job from Tower {0}: {1}".format(response['status_code'], module.extract_errors_from_response(response)))
|
||||
|
||||
# Since we were successful, extract the fields we want to return
|
||||
for k in ('id', 'status', 'elapsed', 'started', 'finished'):
|
||||
module.json_output[k] = response['json'].get(k)
|
||||
|
||||
# And finally return the payload
|
||||
return response['json']
|
||||
from ..module_utils.tower_api import TowerAPIModule
|
||||
|
||||
|
||||
def main():
|
||||
@@ -120,7 +106,7 @@ def main():
|
||||
)
|
||||
|
||||
# Create a module for ourselves
|
||||
module = TowerModule(argument_spec=argument_spec)
|
||||
module = TowerAPIModule(argument_spec=argument_spec)
|
||||
|
||||
# Extract our parameters
|
||||
job_id = module.params.get('job_id')
|
||||
@@ -153,31 +139,13 @@ def main():
|
||||
if job is None:
|
||||
module.fail_json(msg='Unable to wait on job {0}; that ID does not exist in Tower.'.format(job_id))
|
||||
|
||||
job_url = job['url']
|
||||
|
||||
# Grab our start time to compare against for the timeout
|
||||
start = time.time()
|
||||
|
||||
# Get the initial job status from Tower, this will exit if there are any issues with the HTTP call
|
||||
result = check_job(module, job_url)
|
||||
|
||||
# Loop while the job is not yet completed
|
||||
while not result['finished']:
|
||||
# If we are past our time out fail with a message
|
||||
if timeout and timeout < time.time() - start:
|
||||
module.json_output['msg'] = "Monitoring aborted due to timeout"
|
||||
module.fail_json(**module.json_output)
|
||||
|
||||
# Put the process to sleep for our interval
|
||||
time.sleep(interval)
|
||||
|
||||
# Check the job again
|
||||
result = check_job(module, job_url)
|
||||
|
||||
# If the job has failed, we want to raise an Exception for that so we get a non-zero response.
|
||||
if result['failed']:
|
||||
module.json_output['msg'] = 'Job with id {0} failed'.format(job_id)
|
||||
module.fail_json(**module.json_output)
|
||||
# Invoke wait function
|
||||
result = module.wait_on_url(
|
||||
url=job['url'],
|
||||
object_name=job_id,
|
||||
object_type='legacy_job_wait',
|
||||
timeout=timeout, interval=interval
|
||||
)
|
||||
|
||||
module.exit_json(**module.json_output)
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ EXAMPLES = '''
|
||||
organization: My Organization
|
||||
'''
|
||||
|
||||
from ..module_utils.tower_api import TowerModule
|
||||
from ..module_utils.tower_api import TowerAPIModule
|
||||
|
||||
|
||||
def main():
|
||||
@@ -67,7 +67,7 @@ def main():
|
||||
)
|
||||
|
||||
# Create a module for ourselves
|
||||
module = TowerModule(argument_spec=argument_spec)
|
||||
module = TowerAPIModule(argument_spec=argument_spec)
|
||||
|
||||
# Extract our parameters
|
||||
name = module.params.get('name')
|
||||
|
||||
@@ -43,12 +43,12 @@ EXAMPLES = '''
|
||||
eula_accepted: True
|
||||
'''
|
||||
|
||||
from ..module_utils.tower_api import TowerModule
|
||||
from ..module_utils.tower_api import TowerAPIModule
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
module = TowerModule(
|
||||
module = TowerAPIModule(
|
||||
argument_spec=dict(
|
||||
data=dict(type='dict', required=True),
|
||||
eula_accepted=dict(type='bool', required=True),
|
||||
|
||||
@@ -62,11 +62,11 @@ EXAMPLES = '''
|
||||
'''
|
||||
|
||||
|
||||
from ..module_utils.tower_api import TowerModule
|
||||
from ..module_utils.tower_api import TowerAPIModule
|
||||
|
||||
|
||||
def main():
|
||||
module = TowerModule(argument_spec={})
|
||||
module = TowerAPIModule(argument_spec={})
|
||||
namespace = {
|
||||
'awx': 'awx',
|
||||
'tower': 'ansible'
|
||||
|
||||
@@ -15,7 +15,7 @@ ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: tower_notification
|
||||
module: tower_notification_template
|
||||
author: "Samuel Carpentier (@samcarpentier)"
|
||||
short_description: create, update, or destroy Ansible Tower notification.
|
||||
description:
|
||||
@@ -203,7 +203,7 @@ extends_documentation_fragment: awx.awx.auth
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: Add Slack notification with custom messages
|
||||
tower_notification:
|
||||
tower_notification_template:
|
||||
name: slack notification
|
||||
organization: Default
|
||||
notification_type: slack
|
||||
@@ -222,7 +222,7 @@ EXAMPLES = '''
|
||||
tower_config_file: "~/tower_cli.cfg"
|
||||
|
||||
- name: Add webhook notification
|
||||
tower_notification:
|
||||
tower_notification_template:
|
||||
name: webhook notification
|
||||
notification_type: webhook
|
||||
notification_configuration:
|
||||
@@ -233,7 +233,7 @@ EXAMPLES = '''
|
||||
tower_config_file: "~/tower_cli.cfg"
|
||||
|
||||
- name: Add email notification
|
||||
tower_notification:
|
||||
tower_notification_template:
|
||||
name: email notification
|
||||
notification_type: email
|
||||
notification_configuration:
|
||||
@@ -250,7 +250,7 @@ EXAMPLES = '''
|
||||
tower_config_file: "~/tower_cli.cfg"
|
||||
|
||||
- name: Add twilio notification
|
||||
tower_notification:
|
||||
tower_notification_template:
|
||||
name: twilio notification
|
||||
notification_type: twilio
|
||||
notification_configuration:
|
||||
@@ -263,7 +263,7 @@ EXAMPLES = '''
|
||||
tower_config_file: "~/tower_cli.cfg"
|
||||
|
||||
- name: Add PagerDuty notification
|
||||
tower_notification:
|
||||
tower_notification_template:
|
||||
name: pagerduty notification
|
||||
notification_type: pagerduty
|
||||
notification_configuration:
|
||||
@@ -275,7 +275,7 @@ EXAMPLES = '''
|
||||
tower_config_file: "~/tower_cli.cfg"
|
||||
|
||||
- name: Add IRC notification
|
||||
tower_notification:
|
||||
tower_notification_template:
|
||||
name: irc notification
|
||||
notification_type: irc
|
||||
notification_configuration:
|
||||
@@ -290,7 +290,7 @@ EXAMPLES = '''
|
||||
tower_config_file: "~/tower_cli.cfg"
|
||||
|
||||
- name: Delete notification
|
||||
tower_notification:
|
||||
tower_notification_template:
|
||||
name: old notification
|
||||
state: absent
|
||||
tower_config_file: "~/tower_cli.cfg"
|
||||
@@ -300,7 +300,7 @@ EXAMPLES = '''
|
||||
RETURN = ''' # '''
|
||||
|
||||
|
||||
from ..module_utils.tower_api import TowerModule
|
||||
from ..module_utils.tower_api import TowerAPIModule
|
||||
|
||||
OLD_INPUT_NAMES = (
|
||||
'username', 'sender', 'recipients', 'use_tls',
|
||||
@@ -355,7 +355,7 @@ def main():
|
||||
)
|
||||
|
||||
# Create a module for ourselves
|
||||
module = TowerModule(argument_spec=argument_spec)
|
||||
module = TowerAPIModule(argument_spec=argument_spec)
|
||||
|
||||
# Extract our parameters
|
||||
name = module.params.get('name')
|
||||
@@ -88,7 +88,7 @@ EXAMPLES = '''
|
||||
tower_config_file: "~/tower_cli.cfg"
|
||||
'''
|
||||
|
||||
from ..module_utils.tower_api import TowerModule
|
||||
from ..module_utils.tower_api import TowerAPIModule
|
||||
|
||||
|
||||
def main():
|
||||
@@ -106,7 +106,7 @@ def main():
|
||||
)
|
||||
|
||||
# Create a module for ourselves
|
||||
module = TowerModule(argument_spec=argument_spec)
|
||||
module = TowerAPIModule(argument_spec=argument_spec)
|
||||
|
||||
# Extract our parameters
|
||||
name = module.params.get('name')
|
||||
|
||||
@@ -55,10 +55,12 @@ options:
|
||||
- The refspec to use for the SCM resource.
|
||||
type: str
|
||||
default: ''
|
||||
scm_credential:
|
||||
credential:
|
||||
description:
|
||||
- Name of the credential to use with this SCM resource.
|
||||
type: str
|
||||
aliases:
|
||||
- scm_credential
|
||||
scm_clean:
|
||||
description:
|
||||
- Remove local modifications before updating.
|
||||
@@ -86,11 +88,13 @@ options:
|
||||
type: bool
|
||||
aliases:
|
||||
- scm_allow_override
|
||||
job_timeout:
|
||||
timeout:
|
||||
description:
|
||||
- The amount of time (in seconds) to run before the SCM Update is canceled. A value of 0 means no timeout.
|
||||
default: 0
|
||||
type: int
|
||||
aliases:
|
||||
- job_timeout
|
||||
custom_virtualenv:
|
||||
description:
|
||||
- Local absolute file path containing a custom Python virtualenv to use
|
||||
@@ -157,7 +161,7 @@ EXAMPLES = '''
|
||||
|
||||
import time
|
||||
|
||||
from ..module_utils.tower_api import TowerModule
|
||||
from ..module_utils.tower_api import TowerAPIModule
|
||||
|
||||
|
||||
def wait_for_project_update(module, last_request):
|
||||
@@ -188,13 +192,13 @@ def main():
|
||||
local_path=dict(),
|
||||
scm_branch=dict(default=''),
|
||||
scm_refspec=dict(default=''),
|
||||
scm_credential=dict(),
|
||||
credential=dict(aliases=['scm_credential']),
|
||||
scm_clean=dict(type='bool', default=False),
|
||||
scm_delete_on_update=dict(type='bool', default=False),
|
||||
scm_update_on_launch=dict(type='bool', default=False),
|
||||
scm_update_cache_timeout=dict(type='int', default=0),
|
||||
allow_override=dict(type='bool', aliases=['scm_allow_override']),
|
||||
job_timeout=dict(type='int', default=0),
|
||||
timeout=dict(type='int', default=0, aliases=['job_timeout']),
|
||||
custom_virtualenv=dict(),
|
||||
organization=dict(required=True),
|
||||
notification_templates_started=dict(type="list", elements='str'),
|
||||
@@ -205,7 +209,7 @@ def main():
|
||||
)
|
||||
|
||||
# Create a module for ourselves
|
||||
module = TowerModule(argument_spec=argument_spec)
|
||||
module = TowerAPIModule(argument_spec=argument_spec)
|
||||
|
||||
# Extract our parameters
|
||||
name = module.params.get('name')
|
||||
@@ -217,13 +221,13 @@ def main():
|
||||
local_path = module.params.get('local_path')
|
||||
scm_branch = module.params.get('scm_branch')
|
||||
scm_refspec = module.params.get('scm_refspec')
|
||||
scm_credential = module.params.get('scm_credential')
|
||||
credential = module.params.get('credential')
|
||||
scm_clean = module.params.get('scm_clean')
|
||||
scm_delete_on_update = module.params.get('scm_delete_on_update')
|
||||
scm_update_on_launch = module.params.get('scm_update_on_launch')
|
||||
scm_update_cache_timeout = module.params.get('scm_update_cache_timeout')
|
||||
allow_override = module.params.get('allow_override')
|
||||
job_timeout = module.params.get('job_timeout')
|
||||
timeout = module.params.get('timeout')
|
||||
custom_virtualenv = module.params.get('custom_virtualenv')
|
||||
organization = module.params.get('organization')
|
||||
state = module.params.get('state')
|
||||
@@ -231,8 +235,8 @@ def main():
|
||||
|
||||
# Attempt to look up the related items the user specified (these will fail the module if not found)
|
||||
org_id = module.resolve_name_to_id('organizations', organization)
|
||||
if scm_credential is not None:
|
||||
scm_credential_id = module.resolve_name_to_id('credentials', scm_credential)
|
||||
if credential is not None:
|
||||
credential = module.resolve_name_to_id('credentials', credential)
|
||||
|
||||
# Attempt to look up project based on the provided name and org ID
|
||||
project = module.get_one('projects', **{
|
||||
@@ -276,7 +280,7 @@ def main():
|
||||
'scm_refspec': scm_refspec,
|
||||
'scm_clean': scm_clean,
|
||||
'scm_delete_on_update': scm_delete_on_update,
|
||||
'timeout': job_timeout,
|
||||
'timeout': timeout,
|
||||
'organization': org_id,
|
||||
'scm_update_on_launch': scm_update_on_launch,
|
||||
'scm_update_cache_timeout': scm_update_cache_timeout,
|
||||
@@ -284,8 +288,8 @@ def main():
|
||||
}
|
||||
if description is not None:
|
||||
project_fields['description'] = description
|
||||
if scm_credential is not None:
|
||||
project_fields['credential'] = scm_credential_id
|
||||
if credential is not None:
|
||||
project_fields['credential'] = credential
|
||||
if allow_override is not None:
|
||||
project_fields['allow_override'] = allow_override
|
||||
if scm_type == '':
|
||||
|
||||
@@ -134,7 +134,7 @@ assets:
|
||||
sample: [ {}, {} ]
|
||||
'''
|
||||
|
||||
from ..module_utils.ansible_tower import TowerModule, tower_auth_config, HAS_TOWER_CLI
|
||||
from ..module_utils.tower_legacy import TowerLegacyModule, tower_auth_config, HAS_TOWER_CLI
|
||||
|
||||
try:
|
||||
from tower_cli.cli.transfer.receive import Receiver
|
||||
@@ -163,7 +163,7 @@ def main():
|
||||
workflow=dict(type='list', default=[], elements='str'),
|
||||
)
|
||||
|
||||
module = TowerModule(argument_spec=argument_spec, supports_check_mode=False)
|
||||
module = TowerLegacyModule(argument_spec=argument_spec, supports_check_mode=False)
|
||||
|
||||
module.deprecate(msg="This module is deprecated and will be replaced by the AWX CLI export command.", version="awx.awx:14.0.0")
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ EXAMPLES = '''
|
||||
state: present
|
||||
'''
|
||||
|
||||
from ..module_utils.tower_api import TowerModule
|
||||
from ..module_utils.tower_api import TowerAPIModule
|
||||
|
||||
|
||||
def main():
|
||||
@@ -109,7 +109,7 @@ def main():
|
||||
state=dict(choices=['present', 'absent'], default='present'),
|
||||
)
|
||||
|
||||
module = TowerModule(argument_spec=argument_spec)
|
||||
module = TowerAPIModule(argument_spec=argument_spec)
|
||||
|
||||
role_type = module.params.pop('role')
|
||||
role_field = role_type + '_role'
|
||||
@@ -126,11 +126,10 @@ def main():
|
||||
resource_data = {}
|
||||
for param in resource_param_keys:
|
||||
endpoint = module.param_to_endpoint(param)
|
||||
name_field = 'username' if param == 'user' else 'name'
|
||||
|
||||
resource_name = params.get(param)
|
||||
if resource_name:
|
||||
resource = module.get_one(endpoint, **{'data': {name_field: resource_name}})
|
||||
resource = module.get_one_by_name_or_id(module.param_to_endpoint(param), resource_name)
|
||||
if not resource:
|
||||
module.fail_json(
|
||||
msg='Failed to update role, {0} not found in {1}'.format(param, endpoint),
|
||||
@@ -170,14 +169,14 @@ def main():
|
||||
if response['status_code'] == 204:
|
||||
module.json_output['changed'] = True
|
||||
else:
|
||||
module.fail_json(msg="Failed to grant role {0}".format(response['json']['detail']))
|
||||
module.fail_json(msg="Failed to grant role. {0}".format(response['json'].get('detail', response['json'].get('msg', 'unknown'))))
|
||||
else:
|
||||
for an_id in list(set(existing_associated_ids) & set(new_association_list)):
|
||||
response = module.post_endpoint(association_endpoint, **{'data': {'id': int(an_id), 'disassociate': True}})
|
||||
if response['status_code'] == 204:
|
||||
module.json_output['changed'] = True
|
||||
else:
|
||||
module.fail_json(msg="Failed to revoke role {0}".format(response['json']['detail']))
|
||||
module.fail_json(msg="Failed to revoke role. {0}".format(response['json'].get('detail', response['json'].get('msg', 'unknown'))))
|
||||
|
||||
module.exit_json(**module.json_output)
|
||||
|
||||
|
||||
@@ -136,7 +136,7 @@ EXAMPLES = '''
|
||||
register: result
|
||||
'''
|
||||
|
||||
from ..module_utils.tower_api import TowerModule
|
||||
from ..module_utils.tower_api import TowerAPIModule
|
||||
|
||||
|
||||
def main():
|
||||
@@ -161,7 +161,7 @@ def main():
|
||||
)
|
||||
|
||||
# Create a module for ourselves
|
||||
module = TowerModule(argument_spec=argument_spec)
|
||||
module = TowerAPIModule(argument_spec=argument_spec)
|
||||
|
||||
# Extract our parameters
|
||||
rrule = module.params.get('rrule')
|
||||
|
||||
@@ -81,7 +81,7 @@ import os
|
||||
import sys
|
||||
|
||||
from ansible.module_utils.six.moves import StringIO
|
||||
from ..module_utils.ansible_tower import TowerModule, tower_auth_config, HAS_TOWER_CLI
|
||||
from ..module_utils.tower_legacy import TowerLegacyModule, tower_auth_config, HAS_TOWER_CLI
|
||||
|
||||
from tempfile import mkstemp
|
||||
|
||||
@@ -103,7 +103,7 @@ def main():
|
||||
password_management=dict(default='default', choices=['default', 'random']),
|
||||
)
|
||||
|
||||
module = TowerModule(argument_spec=argument_spec, supports_check_mode=False)
|
||||
module = TowerLegacyModule(argument_spec=argument_spec, supports_check_mode=False)
|
||||
|
||||
module.deprecate(msg="This module is deprecated and will be replaced by the AWX CLI import command", version="awx.awx:14.0.0")
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ EXAMPLES = '''
|
||||
last_name: "surname"
|
||||
'''
|
||||
|
||||
from ..module_utils.tower_api import TowerModule
|
||||
from ..module_utils.tower_api import TowerAPIModule
|
||||
|
||||
try:
|
||||
import yaml
|
||||
@@ -111,7 +111,7 @@ def main():
|
||||
)
|
||||
|
||||
# Create a module for ourselves
|
||||
module = TowerModule(
|
||||
module = TowerAPIModule(
|
||||
argument_spec=argument_spec,
|
||||
required_one_of=[['name', 'settings']],
|
||||
mutually_exclusive=[['name', 'settings']],
|
||||
|
||||
@@ -60,7 +60,7 @@ EXAMPLES = '''
|
||||
tower_config_file: "~/tower_cli.cfg"
|
||||
'''
|
||||
|
||||
from ..module_utils.tower_api import TowerModule
|
||||
from ..module_utils.tower_api import TowerAPIModule
|
||||
|
||||
|
||||
def main():
|
||||
@@ -74,7 +74,7 @@ def main():
|
||||
)
|
||||
|
||||
# Create a module for ourselves
|
||||
module = TowerModule(argument_spec=argument_spec)
|
||||
module = TowerAPIModule(argument_spec=argument_spec)
|
||||
|
||||
# Extract our parameters
|
||||
name = module.params.get('name')
|
||||
|
||||
@@ -117,7 +117,7 @@ tower_token:
|
||||
returned: on successful create
|
||||
'''
|
||||
|
||||
from ..module_utils.tower_api import TowerModule
|
||||
from ..module_utils.tower_api import TowerAPIModule
|
||||
|
||||
|
||||
def return_token(module, last_response):
|
||||
@@ -143,7 +143,7 @@ def main():
|
||||
)
|
||||
|
||||
# Create a module for ourselves
|
||||
module = TowerModule(
|
||||
module = TowerAPIModule(
|
||||
argument_spec=argument_spec,
|
||||
mutually_exclusive=[
|
||||
('existing_token', 'existing_token_id'),
|
||||
|
||||
@@ -102,7 +102,7 @@ EXAMPLES = '''
|
||||
tower_config_file: "~/tower_cli.cfg"
|
||||
'''
|
||||
|
||||
from ..module_utils.tower_api import TowerModule
|
||||
from ..module_utils.tower_api import TowerAPIModule
|
||||
|
||||
|
||||
def main():
|
||||
@@ -119,7 +119,7 @@ def main():
|
||||
)
|
||||
|
||||
# Create a module for ourselves
|
||||
module = TowerModule(argument_spec=argument_spec)
|
||||
module = TowerAPIModule(argument_spec=argument_spec)
|
||||
|
||||
# Extract our parameters
|
||||
username = module.params.get('username')
|
||||
|
||||
@@ -137,7 +137,7 @@ EXAMPLES = '''
|
||||
organization: Default
|
||||
'''
|
||||
|
||||
from ..module_utils.tower_api import TowerModule
|
||||
from ..module_utils.tower_api import TowerAPIModule
|
||||
|
||||
import json
|
||||
|
||||
@@ -176,7 +176,7 @@ def main():
|
||||
)
|
||||
|
||||
# Create a module for ourselves
|
||||
module = TowerModule(argument_spec=argument_spec)
|
||||
module = TowerAPIModule(argument_spec=argument_spec)
|
||||
|
||||
# Extract our parameters
|
||||
name = module.params.get('name')
|
||||
|
||||
@@ -157,7 +157,7 @@ EXAMPLES = '''
|
||||
- my-first-node
|
||||
'''
|
||||
|
||||
from ..module_utils.tower_api import TowerModule
|
||||
from ..module_utils.tower_api import TowerAPIModule
|
||||
|
||||
|
||||
def main():
|
||||
@@ -185,7 +185,7 @@ def main():
|
||||
)
|
||||
|
||||
# Create a module for ourselves
|
||||
module = TowerModule(argument_spec=argument_spec)
|
||||
module = TowerAPIModule(argument_spec=argument_spec)
|
||||
|
||||
# Extract our parameters
|
||||
identifier = module.params.get('identifier')
|
||||
|
||||
@@ -91,9 +91,8 @@ EXAMPLES = '''
|
||||
wait: False
|
||||
'''
|
||||
|
||||
from ..module_utils.tower_api import TowerModule
|
||||
from ..module_utils.tower_api import TowerAPIModule
|
||||
import json
|
||||
import time
|
||||
|
||||
|
||||
def main():
|
||||
@@ -111,7 +110,7 @@ def main():
|
||||
)
|
||||
|
||||
# Create a module for ourselves
|
||||
module = TowerModule(argument_spec=argument_spec)
|
||||
module = TowerAPIModule(argument_spec=argument_spec)
|
||||
|
||||
optional_args = {}
|
||||
# Extract our parameters
|
||||
@@ -178,26 +177,13 @@ def main():
|
||||
if not wait:
|
||||
module.exit_json(**module.json_output)
|
||||
|
||||
# Grab our start time to compare against for the timeout
|
||||
start = time.time()
|
||||
|
||||
job_url = result['json']['url']
|
||||
while not result['json']['finished']:
|
||||
# If we are past our time out fail with a message
|
||||
if timeout and timeout < time.time() - start:
|
||||
module.json_output['msg'] = "Monitoring aborted due to timeout"
|
||||
module.fail_json(**module.json_output)
|
||||
|
||||
# Put the process to sleep for our interval
|
||||
time.sleep(interval)
|
||||
|
||||
result = module.get_endpoint(job_url)
|
||||
module.json_output['status'] = result['json']['status']
|
||||
|
||||
# If the job has failed, we want to raise a task failure for that so we get a non-zero response.
|
||||
if result['json']['failed']:
|
||||
module.json_output['msg'] = 'The workflow "{0}" failed'.format(name)
|
||||
module.fail_json(**module.json_output)
|
||||
# Invoke wait function
|
||||
module.wait_on_url(
|
||||
url=result['json']['url'],
|
||||
object_name=name,
|
||||
object_type='Workflow Job',
|
||||
timeout=timeout, interval=interval
|
||||
)
|
||||
|
||||
module.exit_json(**module.json_output)
|
||||
|
||||
|
||||
@@ -108,8 +108,8 @@ EXAMPLES = '''
|
||||
RETURN = ''' # '''
|
||||
|
||||
|
||||
from ..module_utils.ansible_tower import (
|
||||
TowerModule,
|
||||
from ..module_utils.tower_legacy import (
|
||||
TowerLegacyModule,
|
||||
tower_auth_config,
|
||||
tower_check_mode
|
||||
)
|
||||
@@ -140,7 +140,7 @@ def main():
|
||||
state=dict(choices=['present', 'absent'], default='present'),
|
||||
)
|
||||
|
||||
module = TowerModule(
|
||||
module = TowerLegacyModule(
|
||||
argument_spec=argument_spec,
|
||||
supports_check_mode=False
|
||||
)
|
||||
|
||||
@@ -10,19 +10,29 @@ from contextlib import redirect_stdout, suppress
|
||||
from unittest import mock
|
||||
import logging
|
||||
|
||||
from requests.models import Response
|
||||
from requests.models import Response, PreparedRequest
|
||||
|
||||
import pytest
|
||||
|
||||
from awx.main.tests.functional.conftest import _request
|
||||
from awx.main.models import Organization, Project, Inventory, JobTemplate, Credential, CredentialType
|
||||
|
||||
from django.db import transaction
|
||||
|
||||
try:
|
||||
import tower_cli # noqa
|
||||
HAS_TOWER_CLI = True
|
||||
except ImportError:
|
||||
HAS_TOWER_CLI = False
|
||||
|
||||
try:
|
||||
# Because awxkit will be a directory at the root of this makefile and we are using python3, import awxkit will work even if its not installed.
|
||||
# However, awxkit will not contain api whih causes a stack failure down on line 170 when we try to mock it.
|
||||
# So here we are importing awxkit.api to prevent that. Then you only get an error on tests for awxkit functionality.
|
||||
import awxkit.api
|
||||
HAS_AWX_KIT = True
|
||||
except ImportError:
|
||||
HAS_AWX_KIT = False
|
||||
|
||||
logger = logging.getLogger('awx.main.tests')
|
||||
|
||||
@@ -90,7 +100,8 @@ def run_module(request, collection_import):
|
||||
if 'params' in kwargs and method == 'GET':
|
||||
# query params for GET are handled a bit differently by
|
||||
# tower-cli and python requests as opposed to REST framework APIRequestFactory
|
||||
kwargs_copy.setdefault('data', {})
|
||||
if not kwargs_copy.get('data'):
|
||||
kwargs_copy['data'] = {}
|
||||
if isinstance(kwargs['params'], dict):
|
||||
kwargs_copy['data'].update(kwargs['params'])
|
||||
elif isinstance(kwargs['params'], list):
|
||||
@@ -98,8 +109,9 @@ def run_module(request, collection_import):
|
||||
kwargs_copy['data'][k] = v
|
||||
|
||||
# make request
|
||||
rf = _request(method.lower())
|
||||
django_response = rf(url, user=request_user, expect=None, **kwargs_copy)
|
||||
with transaction.atomic():
|
||||
rf = _request(method.lower())
|
||||
django_response = rf(url, user=request_user, expect=None, **kwargs_copy)
|
||||
|
||||
# requests library response object is different from the Django response, but they are the same concept
|
||||
# this converts the Django response object into a requests response object for consumption
|
||||
@@ -117,6 +129,8 @@ def run_module(request, collection_import):
|
||||
request_user.username, resp.status_code
|
||||
)
|
||||
|
||||
resp.request = PreparedRequest()
|
||||
resp.request.prepare(method=method, url=url)
|
||||
return resp
|
||||
|
||||
def new_open(self, method, url, **kwargs):
|
||||
@@ -142,11 +156,22 @@ def run_module(request, collection_import):
|
||||
def mock_load_params(self):
|
||||
self.params = module_params
|
||||
|
||||
with mock.patch.object(resource_module.TowerModule, '_load_params', new=mock_load_params):
|
||||
if getattr(resource_module, 'TowerAWXKitModule', None):
|
||||
resource_class = resource_module.TowerAWXKitModule
|
||||
elif getattr(resource_module, 'TowerAPIModule', None):
|
||||
resource_class = resource_module.TowerAPIModule
|
||||
elif getattr(resource_module, 'TowerLegacyModule', None):
|
||||
resource_class = resource_module.TowerLegacyModule
|
||||
else:
|
||||
raise("The module has neither a TowerLegacyModule, TowerAWXKitModule or a TowerAPIModule")
|
||||
|
||||
with mock.patch.object(resource_class, '_load_params', new=mock_load_params):
|
||||
# Call the test utility (like a mock server) instead of issuing HTTP requests
|
||||
with mock.patch('ansible.module_utils.urls.Request.open', new=new_open):
|
||||
if HAS_TOWER_CLI:
|
||||
tower_cli_mgr = mock.patch('tower_cli.api.Session.request', new=new_request)
|
||||
elif HAS_AWX_KIT:
|
||||
tower_cli_mgr = mock.patch('awxkit.api.client.requests.Session.request', new=new_request)
|
||||
else:
|
||||
tower_cli_mgr = suppress()
|
||||
with tower_cli_mgr:
|
||||
|
||||
267
awx_collection/test/awx/test_completeness.py
Normal file
267
awx_collection/test/awx/test_completeness.py
Normal file
@@ -0,0 +1,267 @@
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import pytest
|
||||
from awx.main.tests.functional.conftest import _request
|
||||
from ansible.module_utils.six import PY2, string_types
|
||||
import yaml
|
||||
import os
|
||||
import re
|
||||
|
||||
# Analysis variables
|
||||
# -----------------------------------------------------------------------------------------------------------
|
||||
|
||||
# Read-only endpoints are dynamically created by an options page with no POST section.
|
||||
# Normally a read-only endpoint should not have a module (i.e. /api/v2/me) but sometimes we reuse a name
|
||||
# For example, we have a tower_role module but /api/v2/roles is a read only endpoint.
|
||||
# This list indicates which read-only endpoints have associated modules with them.
|
||||
read_only_endpoints_with_modules = ['tower_settings', 'tower_role']
|
||||
|
||||
# If a module should not be created for an endpoint and the endpoint is not read-only add it here
|
||||
# THINK HARD ABOUT DOING THIS
|
||||
no_module_for_endpoint = []
|
||||
|
||||
# Some modules work on the related fields of an endpoint. These modules will not have an auto-associated endpoint
|
||||
no_endpoint_for_module = [
|
||||
'tower_import', 'tower_meta', 'tower_export', 'tower_job_launch', 'tower_job_wait', 'tower_job_list',
|
||||
'tower_license', 'tower_ping', 'tower_receive', 'tower_send', 'tower_workflow_launch', 'tower_job_cancel',
|
||||
'tower_workflow_template',
|
||||
]
|
||||
|
||||
# Global module parameters we can ignore
|
||||
ignore_parameters = [
|
||||
'state', 'new_name',
|
||||
]
|
||||
|
||||
# Some modules take additional parameters that do not appear in the API
|
||||
# Add the module name as the key with the value being the list of params to ignore
|
||||
no_api_parameter_ok = {
|
||||
# The wait is for whether or not to wait for a project update on change
|
||||
'tower_project': ['wait'],
|
||||
# Existing_token and id are for working with an existing tokens
|
||||
'tower_token': ['existing_token', 'existing_token_id'],
|
||||
# /survey spec is now how we handle associations
|
||||
# We take an organization here to help with the lookups only
|
||||
'tower_job_template': ['survey_spec', 'organization'],
|
||||
# Organization is how we looking job templates
|
||||
'tower_workflow_job_template_node': ['organization'],
|
||||
# Survey is how we handle associations
|
||||
'tower_workflow_job_template': ['survey'],
|
||||
}
|
||||
|
||||
# When this tool was created we were not feature complete. Adding something in here indicates a module
|
||||
# that needs to be developed. If the module is found on the file system it will auto-detect that the
|
||||
# work is being done and will bypass this check. At some point this module should be removed from this list.
|
||||
needs_development = [
|
||||
'tower_ad_hoc_command', 'tower_application', 'tower_instance_group', 'tower_inventory_script',
|
||||
'tower_workflow_approval'
|
||||
]
|
||||
needs_param_development = {
|
||||
'tower_host': ['instance_id'],
|
||||
}
|
||||
# -----------------------------------------------------------------------------------------------------------
|
||||
|
||||
return_value = 0
|
||||
read_only_endpoint = []
|
||||
|
||||
|
||||
def cause_error(msg):
|
||||
global return_value
|
||||
return_value = 255
|
||||
return msg
|
||||
|
||||
|
||||
def determine_state(module_id, endpoint, module, parameter, api_option, module_option):
|
||||
# This is a hierarchical list of things that are ok/failures based on conditions
|
||||
|
||||
# If we know this module needs development this is a non-blocking failure
|
||||
if module_id in needs_development and module == 'N/A':
|
||||
return "Failed (non-blocking), module needs development"
|
||||
|
||||
# If the module is a read only endpoint:
|
||||
# If it has no module on disk that is ok.
|
||||
# If it has a module on disk but its listed in read_only_endpoints_with_modules that is ok
|
||||
# Else we have a module for a read only endpoint that should not exit
|
||||
if module_id in read_only_endpoint:
|
||||
if module == 'N/A':
|
||||
# There may be some cases where a read only endpoint has a module
|
||||
return "OK, this endpoint is read-only and should not have a module"
|
||||
elif module_id in read_only_endpoints_with_modules:
|
||||
return "OK, module params can not be checked to read-only"
|
||||
else:
|
||||
return cause_error("Failed, read-only endpoint should not have an associated module")
|
||||
|
||||
# If the endpoint is listed as not needing a module and we don't have one we are ok
|
||||
if module_id in no_module_for_endpoint and module == 'N/A':
|
||||
return "OK, this endpoint should not have a module"
|
||||
|
||||
# If module is listed as not needing an endpoint and we don't have one we are ok
|
||||
if module_id in no_endpoint_for_module and endpoint == 'N/A':
|
||||
return "OK, this module does not require an endpoint"
|
||||
|
||||
# All of the end/point module conditionals are done so if we don't have a module or endpoint we have a problem
|
||||
if module == 'N/A':
|
||||
return cause_error('Failed, missing module')
|
||||
if endpoint == 'N/A':
|
||||
return cause_error('Failed, why does this module have no endpoint')
|
||||
|
||||
# Now perform parameter checks
|
||||
|
||||
# First, if the parameter is in the ignore_parameters list we are ok
|
||||
if parameter in ignore_parameters:
|
||||
return "OK, globally ignored parameter"
|
||||
|
||||
# If both the api option and the module option are both either objects or none
|
||||
if (api_option is None) ^ (module_option is None):
|
||||
# If the API option is node and the parameter is in the no_api_parameter list we are ok
|
||||
if api_option is None and parameter in no_api_parameter_ok.get(module, {}):
|
||||
return 'OK, no api parameter is ok'
|
||||
# If we know this parameter needs development and we don't have a module option we are non-blocking
|
||||
if module_option is None and parameter in needs_param_development.get(module_id, {}):
|
||||
return "Failed (non-blocking), parameter needs development"
|
||||
# Check for deprecated in the node, if its deprecated and has no api option we are ok, otherwise we have a problem
|
||||
if module_option and module_option.get('description'):
|
||||
description = ''
|
||||
if isinstance(module_option.get('description'), string_types):
|
||||
description = module_option.get('description')
|
||||
else:
|
||||
description = " ".join(module_option.get('description'))
|
||||
|
||||
if 'deprecated' in description.lower():
|
||||
if api_option is None:
|
||||
return 'OK, deprecated module option'
|
||||
else:
|
||||
return cause_error('Failed, module marks option as deprecated but option still exists in API')
|
||||
# If we don't have a corresponding API option but we are a list then we are likely a relation
|
||||
if not api_option and module_option and module_option.get('type', 'str') == 'list':
|
||||
return "OK, Field appears to be relation"
|
||||
# TODO, at some point try and check the object model to confirm its actually a relation
|
||||
return cause_error('Failed, option mismatch')
|
||||
|
||||
# We made it through all of the checks so we are ok
|
||||
return 'OK'
|
||||
|
||||
|
||||
def test_completeness(collection_import, request, admin_user, job_template):
|
||||
option_comparison = {}
|
||||
# Load a list of existing module files from disk
|
||||
base_folder = os.path.abspath(
|
||||
os.path.join(os.path.dirname(__file__), os.pardir, os.pardir)
|
||||
)
|
||||
module_directory = os.path.join(base_folder, 'plugins', 'modules')
|
||||
for root, dirs, files in os.walk(module_directory):
|
||||
if root == module_directory:
|
||||
for filename in files:
|
||||
if re.match('^tower_.*.py$', filename):
|
||||
module_name = filename[:-3]
|
||||
option_comparison[module_name] = {
|
||||
'endpoint': 'N/A',
|
||||
'api_options': {},
|
||||
'module_options': {},
|
||||
'module_name': module_name,
|
||||
}
|
||||
resource_module = collection_import('plugins.modules.{0}'.format(module_name))
|
||||
option_comparison[module_name]['module_options'] = yaml.load(
|
||||
resource_module.DOCUMENTATION,
|
||||
Loader=yaml.SafeLoader
|
||||
)['options']
|
||||
|
||||
endpoint_response = _request('get')(
|
||||
url='/api/v2/',
|
||||
user=admin_user,
|
||||
expect=None,
|
||||
)
|
||||
for endpoint in endpoint_response.data.keys():
|
||||
# Module names are singular and endpoints are plural so we need to convert to singular
|
||||
singular_endpoint = '{0}'.format(endpoint)
|
||||
if singular_endpoint.endswith('ies'):
|
||||
singular_endpoint = singular_endpoint[:-3]
|
||||
if singular_endpoint != 'settings' and singular_endpoint.endswith('s'):
|
||||
singular_endpoint = singular_endpoint[:-1]
|
||||
module_name = 'tower_{0}'.format(singular_endpoint)
|
||||
|
||||
endpoint_url = endpoint_response.data.get(endpoint)
|
||||
|
||||
# If we don't have a module for this endpoint then we can create an empty one
|
||||
if module_name not in option_comparison:
|
||||
option_comparison[module_name] = {}
|
||||
option_comparison[module_name]['module_name'] = 'N/A'
|
||||
option_comparison[module_name]['module_options'] = {}
|
||||
|
||||
# Add in our endpoint and an empty api_options
|
||||
option_comparison[module_name]['endpoint'] = endpoint_url
|
||||
option_comparison[module_name]['api_options'] = {}
|
||||
|
||||
# Get out the endpoint, load and parse its options page
|
||||
options_response = _request('options')(
|
||||
url=endpoint_url,
|
||||
user=admin_user,
|
||||
expect=None,
|
||||
)
|
||||
if 'POST' in options_response.data.get('actions', {}):
|
||||
option_comparison[module_name]['api_options'] = options_response.data.get('actions').get('POST')
|
||||
else:
|
||||
read_only_endpoint.append(module_name)
|
||||
|
||||
# Parse through our data to get string lengths to make a pretty report
|
||||
longest_module_name = 0
|
||||
longest_option_name = 0
|
||||
longest_endpoint = 0
|
||||
for module in option_comparison:
|
||||
if len(option_comparison[module]['module_name']) > longest_module_name:
|
||||
longest_module_name = len(option_comparison[module]['module_name'])
|
||||
if len(option_comparison[module]['endpoint']) > longest_endpoint:
|
||||
longest_endpoint = len(option_comparison[module]['endpoint'])
|
||||
for option in option_comparison[module]['api_options'], option_comparison[module]['module_options']:
|
||||
if len(option) > longest_option_name:
|
||||
longest_option_name = len(option)
|
||||
|
||||
# Print out some headers
|
||||
print("".join([
|
||||
"End Point", " " * (longest_endpoint - len("End Point")),
|
||||
" | Module Name", " " * (longest_module_name - len("Module Name")),
|
||||
" | Option", " " * (longest_option_name - len("Option")),
|
||||
" | API | Module | State",
|
||||
]))
|
||||
print("-|-".join([
|
||||
"-" * longest_endpoint,
|
||||
"-" * longest_module_name,
|
||||
"-" * longest_option_name,
|
||||
"---",
|
||||
"------",
|
||||
"---------------------------------------------",
|
||||
]))
|
||||
|
||||
# Print out all of our data
|
||||
for module in sorted(option_comparison):
|
||||
module_data = option_comparison[module]
|
||||
all_param_names = list(set(module_data['api_options']) | set(module_data['module_options']))
|
||||
for parameter in sorted(all_param_names):
|
||||
print("".join([
|
||||
module_data['endpoint'], " " * (longest_endpoint - len(module_data['endpoint'])), " | ",
|
||||
module_data['module_name'], " " * (longest_module_name - len(module_data['module_name'])), " | ",
|
||||
parameter, " " * (longest_option_name - len(parameter)), " | ",
|
||||
" X " if (parameter in module_data['api_options']) else ' ', " | ",
|
||||
' X ' if (parameter in module_data['module_options']) else ' ', " | ",
|
||||
determine_state(
|
||||
module,
|
||||
module_data['endpoint'],
|
||||
module_data['module_name'],
|
||||
parameter,
|
||||
module_data['api_options'][parameter] if (parameter in module_data['api_options']) else None,
|
||||
module_data['module_options'][parameter] if (parameter in module_data['module_options']) else None,
|
||||
),
|
||||
]))
|
||||
# This handles cases were we got no params from the options page nor from the modules
|
||||
if len(all_param_names) == 0:
|
||||
print("".join([
|
||||
module_data['endpoint'], " " * (longest_endpoint - len(module_data['endpoint'])), " | ",
|
||||
module_data['module_name'], " " * (longest_module_name - len(module_data['module_name'])), " | ",
|
||||
"N/A", " " * (longest_option_name - len("N/A")), " | ",
|
||||
' ', " | ",
|
||||
' ', " | ",
|
||||
determine_state(module, module_data['endpoint'], module_data['module_name'], 'N/A', None, None),
|
||||
]))
|
||||
|
||||
if return_value != 0:
|
||||
raise Exception("One or more failures caused issues")
|
||||
@@ -3,20 +3,26 @@ __metaclass__ = type
|
||||
|
||||
import pytest
|
||||
|
||||
from awx.main.models import Inventory
|
||||
from awx.main.models import Inventory, Credential
|
||||
from awx.main.tests.functional.conftest import insights_credential, credentialtype_insights
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_inventory_create(run_module, admin_user, organization):
|
||||
def test_inventory_create(run_module, admin_user, organization, insights_credential):
|
||||
# Create an insights credential
|
||||
|
||||
result = run_module('tower_inventory', {
|
||||
'name': 'foo-inventory',
|
||||
'organization': organization.name,
|
||||
'variables': {'foo': 'bar', 'another-foo': {'barz': 'bar2'}},
|
||||
'insights_credential': insights_credential.name,
|
||||
'state': 'present'
|
||||
}, admin_user)
|
||||
assert not result.get('failed', False), result.get('msg', result)
|
||||
|
||||
inv = Inventory.objects.get(name='foo-inventory')
|
||||
assert inv.variables == '{"foo": "bar", "another-foo": {"barz": "bar2"}}'
|
||||
assert inv.insights_credential.name == insights_credential.name
|
||||
|
||||
result.pop('module_args', None)
|
||||
result.pop('invocation', None)
|
||||
|
||||
@@ -190,9 +190,6 @@ def test_falsy_value(run_module, admin_user, base_inventory):
|
||||
# overwrite_vars ? ? o o o o o o o o o o o
|
||||
# update_on_launch ? ? o o o o o o o o o o o
|
||||
# UoPL ? ? o - - - - - - - - - -
|
||||
# source_regions ? ? - o o o - - - - - - -
|
||||
# instance_filters ? ? - o - - o - - - - o -
|
||||
# group_by ? ? - o - - o - - - - - -
|
||||
# source_vars* ? ? - o - o o o o o - - -
|
||||
# environmet vars* ? ? o - - - - - - - - - o
|
||||
# source_script ? ? - - - - - - - - - - r
|
||||
|
||||
@@ -4,6 +4,7 @@ __metaclass__ = type
|
||||
import json
|
||||
import sys
|
||||
|
||||
from awx.main.models import Organization, Team
|
||||
from requests.models import Response
|
||||
from unittest import mock
|
||||
|
||||
@@ -30,12 +31,12 @@ def mock_ping_response(self, method, url, **kwargs):
|
||||
|
||||
|
||||
def test_version_warning(collection_import, silence_warning):
|
||||
TowerModule = collection_import('plugins.module_utils.tower_api').TowerModule
|
||||
TowerAPIModule = collection_import('plugins.module_utils.tower_api').TowerAPIModule
|
||||
cli_data = {'ANSIBLE_MODULE_ARGS': {}}
|
||||
testargs = ['module_file2.py', json.dumps(cli_data)]
|
||||
with mock.patch.object(sys, 'argv', testargs):
|
||||
with mock.patch('ansible.module_utils.urls.Request.open', new=mock_ping_response):
|
||||
my_module = TowerModule(argument_spec=dict())
|
||||
my_module = TowerAPIModule(argument_spec=dict())
|
||||
my_module._COLLECTION_VERSION = "1.0.0"
|
||||
my_module._COLLECTION_TYPE = "not-junk"
|
||||
my_module.collection_to_version['not-junk'] = 'not-junk'
|
||||
@@ -46,12 +47,12 @@ def test_version_warning(collection_import, silence_warning):
|
||||
|
||||
|
||||
def test_type_warning(collection_import, silence_warning):
|
||||
TowerModule = collection_import('plugins.module_utils.tower_api').TowerModule
|
||||
TowerAPIModule = collection_import('plugins.module_utils.tower_api').TowerAPIModule
|
||||
cli_data = {'ANSIBLE_MODULE_ARGS': {}}
|
||||
testargs = ['module_file2.py', json.dumps(cli_data)]
|
||||
with mock.patch.object(sys, 'argv', testargs):
|
||||
with mock.patch('ansible.module_utils.urls.Request.open', new=mock_ping_response):
|
||||
my_module = TowerModule(argument_spec={})
|
||||
my_module = TowerAPIModule(argument_spec={})
|
||||
my_module._COLLECTION_VERSION = "1.2.3"
|
||||
my_module._COLLECTION_TYPE = "junk"
|
||||
my_module.collection_to_version['junk'] = 'junk'
|
||||
@@ -63,7 +64,7 @@ def test_type_warning(collection_import, silence_warning):
|
||||
|
||||
def test_duplicate_config(collection_import, silence_warning):
|
||||
# imports done here because of PATH issues unique to this test suite
|
||||
TowerModule = collection_import('plugins.module_utils.tower_api').TowerModule
|
||||
TowerAPIModule = collection_import('plugins.module_utils.tower_api').TowerAPIModule
|
||||
data = {
|
||||
'name': 'zigzoom',
|
||||
'zig': 'zoom',
|
||||
@@ -71,12 +72,12 @@ def test_duplicate_config(collection_import, silence_warning):
|
||||
'tower_config_file': 'my_config'
|
||||
}
|
||||
|
||||
with mock.patch.object(TowerModule, 'load_config') as mock_load:
|
||||
with mock.patch.object(TowerAPIModule, 'load_config') as mock_load:
|
||||
argument_spec = dict(
|
||||
name=dict(required=True),
|
||||
zig=dict(type='str'),
|
||||
)
|
||||
TowerModule(argument_spec=argument_spec, direct_params=data)
|
||||
TowerAPIModule(argument_spec=argument_spec, direct_params=data)
|
||||
assert mock_load.mock_calls[-1] == mock.call('my_config')
|
||||
|
||||
silence_warning.assert_called_once_with(
|
||||
@@ -92,8 +93,8 @@ def test_no_templated_values(collection_import):
|
||||
Those replacements should happen at build time, so they should not be
|
||||
checked into source.
|
||||
"""
|
||||
TowerModule = collection_import('plugins.module_utils.tower_api').TowerModule
|
||||
assert TowerModule._COLLECTION_VERSION == "0.0.1-devel", (
|
||||
TowerAPIModule = collection_import('plugins.module_utils.tower_api').TowerAPIModule
|
||||
assert TowerAPIModule._COLLECTION_VERSION == "0.0.1-devel", (
|
||||
'The collection version is templated when the collection is built '
|
||||
'and the code should retain the placeholder of "0.0.1-devel".'
|
||||
)
|
||||
@@ -102,3 +103,25 @@ def test_no_templated_values(collection_import):
|
||||
'The inventory plugin FQCN is templated when the collection is built '
|
||||
'and the code should retain the default of awx.awx.'
|
||||
)
|
||||
|
||||
|
||||
def test_conflicting_name_and_id(run_module, admin_user):
|
||||
"""In the event that 2 related items match our search criteria in this way:
|
||||
one item has an id that matches input
|
||||
one item has a name that matches input
|
||||
We should preference the id over the name.
|
||||
Otherwise, the universality of the tower_api lookup plugin is compromised.
|
||||
"""
|
||||
org_by_id = Organization.objects.create(name='foo')
|
||||
slug = str(org_by_id.id)
|
||||
org_by_name = Organization.objects.create(name=slug)
|
||||
result = run_module('tower_team', {
|
||||
'name': 'foo_team', 'description': 'fooin around',
|
||||
'organization': slug
|
||||
}, admin_user)
|
||||
assert not result.get('failed', False), result.get('msg', result)
|
||||
team = Team.objects.filter(name='foo_team').first()
|
||||
assert str(team.organization_id) == slug, (
|
||||
'Lookup by id should be preferenced over name in cases of conflict.'
|
||||
)
|
||||
assert team.organization.name == 'foo'
|
||||
|
||||
@@ -34,7 +34,7 @@ def test_create_modify_notification_template(run_module, admin_user, organizatio
|
||||
'use_tls': False, 'use_ssl': False,
|
||||
'timeout': 4
|
||||
}
|
||||
result = run_module('tower_notification', dict(
|
||||
result = run_module('tower_notification_template', dict(
|
||||
name='foo-notification-template',
|
||||
organization=organization.name,
|
||||
notification_type='email',
|
||||
@@ -49,7 +49,7 @@ def test_create_modify_notification_template(run_module, admin_user, organizatio
|
||||
|
||||
# Test no-op, this is impossible if the notification_configuration is given
|
||||
# because we cannot determine if password fields changed
|
||||
result = run_module('tower_notification', dict(
|
||||
result = run_module('tower_notification_template', dict(
|
||||
name='foo-notification-template',
|
||||
organization=organization.name,
|
||||
notification_type='email',
|
||||
@@ -59,7 +59,7 @@ def test_create_modify_notification_template(run_module, admin_user, organizatio
|
||||
|
||||
# Test a change in the configuration
|
||||
nt_config['timeout'] = 12
|
||||
result = run_module('tower_notification', dict(
|
||||
result = run_module('tower_notification_template', dict(
|
||||
name='foo-notification-template',
|
||||
organization=organization.name,
|
||||
notification_type='email',
|
||||
@@ -74,7 +74,7 @@ def test_create_modify_notification_template(run_module, admin_user, organizatio
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_invalid_notification_configuration(run_module, admin_user, organization):
|
||||
result = run_module('tower_notification', dict(
|
||||
result = run_module('tower_notification_template', dict(
|
||||
name='foo-notification-template',
|
||||
organization=organization.name,
|
||||
notification_type='email',
|
||||
@@ -92,7 +92,7 @@ def test_deprecated_to_modern_no_op(run_module, admin_user, organization):
|
||||
'X-Custom-Header': 'value123'
|
||||
}
|
||||
}
|
||||
result = run_module('tower_notification', dict(
|
||||
result = run_module('tower_notification_template', dict(
|
||||
name='foo-notification-template',
|
||||
organization=organization.name,
|
||||
notification_type='webhook',
|
||||
@@ -101,7 +101,7 @@ def test_deprecated_to_modern_no_op(run_module, admin_user, organization):
|
||||
assert not result.get('failed', False), result.get('msg', result)
|
||||
assert result.pop('changed', None), result
|
||||
|
||||
result = run_module('tower_notification', dict(
|
||||
result = run_module('tower_notification_template', dict(
|
||||
name='foo-notification-template',
|
||||
organization=organization.name,
|
||||
notification_type='webhook',
|
||||
@@ -0,0 +1 @@
|
||||
skip/python2
|
||||
@@ -0,0 +1,77 @@
|
||||
---
|
||||
- name: Generate a random string for test
|
||||
set_fact:
|
||||
test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}"
|
||||
when: test_id is not defined
|
||||
|
||||
- name: Generate names
|
||||
set_fact:
|
||||
org_name1: "AWX-Collection-tests-tower_export-organization-{{ test_id }}"
|
||||
org_name2: "AWX-Collection-tests-tower_export-organization2-{{ test_id }}"
|
||||
inventory_name1: "AWX-Collection-tests-tower_export-inv1-{{ test_id }}"
|
||||
|
||||
- block:
|
||||
- name: Create some organizations
|
||||
tower_organization:
|
||||
name: "{{ item }}"
|
||||
loop:
|
||||
- "{{ org_name1 }}"
|
||||
- "{{ org_name2 }}"
|
||||
|
||||
- name: Create an inventory
|
||||
tower_inventory:
|
||||
name: "{{ inventory_name1 }}"
|
||||
organization: "{{ org_name1 }}"
|
||||
|
||||
- name: Export all tower assets
|
||||
tower_export:
|
||||
all: true
|
||||
register: all_assets
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- all_assets is not changed
|
||||
- all_assets is successful
|
||||
- all_assets['assets']['organizations'] | length() >= 2
|
||||
|
||||
- name: Export all inventories
|
||||
tower_export:
|
||||
inventory: 'all'
|
||||
register: inventory_export
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- inventory_export is successful
|
||||
- inventory_export is not changed
|
||||
- inventory_export['assets']['inventory'] | length() >= 1
|
||||
- "'organizations' not in inventory_export['assets']"
|
||||
|
||||
# This mimics the example in the module
|
||||
- name: Export an all and a specific
|
||||
tower_export:
|
||||
inventory: 'all'
|
||||
organizations: "{{ org_name1 }}"
|
||||
register: mixed_export
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- mixed_export is successful
|
||||
- mixed_export is not changed
|
||||
- mixed_export['assets']['inventory'] | length() >= 1
|
||||
- mixed_export['assets']['organizations'] | length() == 1
|
||||
- "'workflow_job_templates' not in mixed_export['assets']"
|
||||
|
||||
always:
|
||||
- name: Remove our inventory
|
||||
tower_inventory:
|
||||
name: "{{ inventory_name1 }}"
|
||||
organization: "{{ org_name1 }}"
|
||||
state: absent
|
||||
|
||||
- name: Remove test organizations
|
||||
tower_organization:
|
||||
name: "{{ item }}"
|
||||
state: absent
|
||||
loop:
|
||||
- "{{ org_name1 }}"
|
||||
- "{{ org_name2 }}"
|
||||
@@ -0,0 +1 @@
|
||||
skip/python2
|
||||
@@ -0,0 +1,108 @@
|
||||
---
|
||||
- name: Generate a random string for test
|
||||
set_fact:
|
||||
test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}"
|
||||
when: test_id is not defined
|
||||
|
||||
- name: Generate names
|
||||
set_fact:
|
||||
org_name1: "AWX-Collection-tests-tower_import-organization-{{ test_id }}"
|
||||
org_name2: "AWX-Collection-tests-tower_import-organization2-{{ test_id }}"
|
||||
|
||||
- block:
|
||||
- name: "Import something"
|
||||
tower_import:
|
||||
assets:
|
||||
organizations:
|
||||
- name: "{{ org_name1 }}"
|
||||
description: ""
|
||||
max_hosts: 0
|
||||
custom_virtualenv: null
|
||||
related:
|
||||
notification_templates: []
|
||||
notification_templates_started: []
|
||||
notification_templates_success: []
|
||||
notification_templates_error: []
|
||||
notification_templates_approvals: []
|
||||
natural_key:
|
||||
name: "Default"
|
||||
type: "organization"
|
||||
register: import_output
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- import_output is changed
|
||||
|
||||
- name: "Import something again (awxkit is not idempotent, this tests a failure)"
|
||||
tower_import:
|
||||
assets:
|
||||
organizations:
|
||||
- name: "{{ org_name1 }}"
|
||||
description: ""
|
||||
max_hosts: 0
|
||||
custom_virtualenv: null
|
||||
related:
|
||||
notification_templates: []
|
||||
notification_templates_started: []
|
||||
notification_templates_success: []
|
||||
notification_templates_error: []
|
||||
notification_templates_approvals: []
|
||||
natural_key:
|
||||
name: "Default"
|
||||
type: "organization"
|
||||
register: import_output
|
||||
ignore_errors: true
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- import_output is failed
|
||||
- "'Organization with this Name already exists' in import_output.msg"
|
||||
|
||||
- name: "Write out a json file"
|
||||
copy:
|
||||
content: |
|
||||
{
|
||||
"organizations": [
|
||||
{
|
||||
"name": "{{ org_name2 }}",
|
||||
"description": "",
|
||||
"max_hosts": 0,
|
||||
"custom_virtualenv": null,
|
||||
"related": {
|
||||
"notification_templates": [],
|
||||
"notification_templates_started": [],
|
||||
"notification_templates_success": [],
|
||||
"notification_templates_error": [],
|
||||
"notification_templates_approvals": []
|
||||
},
|
||||
"natural_key": {
|
||||
"name": "Default",
|
||||
"type": "organization"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
dest: ./org.json
|
||||
|
||||
- name: "Load assets from a file"
|
||||
tower_import:
|
||||
assets: "{{ lookup('file', 'org.json') | from_json() }}"
|
||||
register: import_output
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- import_output is changed
|
||||
|
||||
always:
|
||||
- name: Remove organizations
|
||||
tower_organization:
|
||||
name: "{{ item }}"
|
||||
state: absent
|
||||
loop:
|
||||
- "{{ org_name1 }}"
|
||||
- "{{ org_name2 }}"
|
||||
|
||||
- name: Delete org.json
|
||||
file:
|
||||
path: ./org.json
|
||||
state: absent
|
||||
@@ -1,101 +1,140 @@
|
||||
---
|
||||
- name: Generate a test ID
|
||||
set_fact:
|
||||
test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}"
|
||||
|
||||
- name: Generate names
|
||||
set_fact:
|
||||
inv_name1: "AWX-Collection-tests-tower_inventory-inv1-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}"
|
||||
inv_name2: "AWX-Collection-tests-tower_inventory-inv2-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}"
|
||||
inv_name1: "AWX-Collection-tests-tower_inventory-inv1-{{ test_id }}"
|
||||
inv_name2: "AWX-Collection-tests-tower_inventory-inv2-{{ test_id }}"
|
||||
cred_name1: "AWX-Collection-tests-tower_inventory-cred1-{{ test_id }}"
|
||||
|
||||
- name: Create an Inventory
|
||||
tower_inventory:
|
||||
name: "{{ inv_name1 }}"
|
||||
organization: Default
|
||||
state: present
|
||||
register: result
|
||||
- block:
|
||||
- name: Create an Insights Credential
|
||||
tower_credential:
|
||||
name: "{{ cred_name1 }}"
|
||||
organization: Default
|
||||
kind: insights
|
||||
inputs:
|
||||
username: joe
|
||||
password: secret
|
||||
state: present
|
||||
register: result
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- "result is changed"
|
||||
- assert:
|
||||
that:
|
||||
- "result is changed"
|
||||
|
||||
- name: Test Inventory module idempotency
|
||||
tower_inventory:
|
||||
name: "{{ inv_name1 }}"
|
||||
organization: Default
|
||||
state: present
|
||||
register: result
|
||||
- name: Create an Inventory
|
||||
tower_inventory:
|
||||
name: "{{ inv_name1 }}"
|
||||
organization: Default
|
||||
insights_credential: "{{ cred_name1 }}"
|
||||
state: present
|
||||
register: result
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- "result is not changed"
|
||||
- assert:
|
||||
that:
|
||||
- "result is changed"
|
||||
|
||||
- name: Fail Change Regular to Smart
|
||||
tower_inventory:
|
||||
name: "{{ inv_name1 }}"
|
||||
organization: Default
|
||||
kind: smart
|
||||
register: result
|
||||
ignore_errors: true
|
||||
- name: Test Inventory module idempotency
|
||||
tower_inventory:
|
||||
name: "{{ inv_name1 }}"
|
||||
organization: Default
|
||||
insights_credential: "{{ cred_name1 }}"
|
||||
state: present
|
||||
register: result
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- "result is failed"
|
||||
- assert:
|
||||
that:
|
||||
- "result is not changed"
|
||||
|
||||
- name: Create a smart inventory
|
||||
tower_inventory:
|
||||
name: "{{ inv_name2 }}"
|
||||
organization: Default
|
||||
kind: smart
|
||||
host_filter: name=foo
|
||||
register: result
|
||||
- name: Fail Change Regular to Smart
|
||||
tower_inventory:
|
||||
name: "{{ inv_name1 }}"
|
||||
organization: Default
|
||||
kind: smart
|
||||
register: result
|
||||
ignore_errors: true
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- "result is changed"
|
||||
- assert:
|
||||
that:
|
||||
- "result is failed"
|
||||
|
||||
- name: Delete a smart inventory
|
||||
tower_inventory:
|
||||
name: "{{ inv_name2 }}"
|
||||
organization: Default
|
||||
kind: smart
|
||||
host_filter: name=foo
|
||||
state: absent
|
||||
register: result
|
||||
- name: Create a smart inventory
|
||||
tower_inventory:
|
||||
name: "{{ inv_name2 }}"
|
||||
organization: Default
|
||||
kind: smart
|
||||
host_filter: name=foo
|
||||
register: result
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- "result is changed"
|
||||
- assert:
|
||||
that:
|
||||
- "result is changed"
|
||||
|
||||
- name: Delete an Inventory
|
||||
tower_inventory:
|
||||
name: "{{ inv_name1 }}"
|
||||
organization: Default
|
||||
state: absent
|
||||
register: result
|
||||
- name: Delete a smart inventory
|
||||
tower_inventory:
|
||||
name: "{{ inv_name2 }}"
|
||||
organization: Default
|
||||
kind: smart
|
||||
host_filter: name=foo
|
||||
state: absent
|
||||
register: result
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- "result is changed"
|
||||
- assert:
|
||||
that:
|
||||
- "result is changed"
|
||||
|
||||
- name: Delete a Non-Existent Inventory
|
||||
tower_inventory:
|
||||
name: "{{ inv_name1 }}"
|
||||
organization: Default
|
||||
state: absent
|
||||
register: result
|
||||
- name: Delete an Inventory
|
||||
tower_inventory:
|
||||
name: "{{ inv_name1 }}"
|
||||
organization: Default
|
||||
state: absent
|
||||
register: result
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- "result is not changed"
|
||||
- assert:
|
||||
that:
|
||||
- "result is changed"
|
||||
|
||||
- name: Check module fails with correct msg
|
||||
tower_inventory:
|
||||
name: test-inventory
|
||||
description: Inventory Description
|
||||
organization: test-non-existing-org
|
||||
state: present
|
||||
register: result
|
||||
ignore_errors: true
|
||||
- name: Delete a Non-Existent Inventory
|
||||
tower_inventory:
|
||||
name: "{{ inv_name1 }}"
|
||||
organization: Default
|
||||
state: absent
|
||||
register: result
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- "result is not changed"
|
||||
- "result.msg =='Failed to update inventory, organization not found: The requested object could not be found.'
|
||||
or result.msg =='The organizations test-non-existing-org was not found on the Tower server'"
|
||||
- assert:
|
||||
that:
|
||||
- "result is not changed"
|
||||
|
||||
- name: Check module fails with correct msg
|
||||
tower_inventory:
|
||||
name: test-inventory
|
||||
description: Inventory Description
|
||||
organization: test-non-existing-org
|
||||
state: present
|
||||
register: result
|
||||
ignore_errors: true
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- "result is not changed"
|
||||
- "result.msg =='Failed to update inventory, organization not found: The requested object could not be found.'
|
||||
or result.msg =='The organizations test-non-existing-org was not found on the Tower server'"
|
||||
always:
|
||||
- name: Delete Inventories
|
||||
tower_inventory:
|
||||
name: "{{ item }}"
|
||||
organization: Default
|
||||
state: absent
|
||||
loop:
|
||||
- "{{ inv_name1 }}"
|
||||
- "{{ inv_name2 }}"
|
||||
|
||||
- name: Delete Insights Credential
|
||||
tower_credential:
|
||||
name: "{{ cred_name1 }}"
|
||||
organization: "Default"
|
||||
kind: insights
|
||||
state: absent
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
jt2: "AWX-Collection-tests-tower_job_template-jt2-{{ test_id }}"
|
||||
lab1: "AWX-Collection-tests-tower_job_template-lab1-{{ test_id }}"
|
||||
email_not: "AWX-Collection-tests-tower_job_template-email-not-{{ test_id }}"
|
||||
webhook_not: "AWX-Collection-tests-tower_notification-wehbook-not-{{ test_id }}"
|
||||
webhook_not: "AWX-Collection-tests-tower_notification_template-wehbook-not-{{ test_id }}"
|
||||
|
||||
- name: Create a Demo Project
|
||||
tower_project:
|
||||
@@ -49,7 +49,7 @@
|
||||
organization: Default
|
||||
|
||||
- name: Add email notification
|
||||
tower_notification:
|
||||
tower_notification_template:
|
||||
name: "{{ email_not }}"
|
||||
organization: Default
|
||||
notification_type: email
|
||||
@@ -65,7 +65,7 @@
|
||||
state: present
|
||||
|
||||
- name: Add webhook notification
|
||||
tower_notification:
|
||||
tower_notification_template:
|
||||
name: "{{ webhook_not }}"
|
||||
organization: Default
|
||||
notification_type: webhook
|
||||
@@ -366,13 +366,13 @@
|
||||
# You can't delete a label directly so no cleanup needed
|
||||
|
||||
- name: Delete email notification
|
||||
tower_notification:
|
||||
tower_notification_template:
|
||||
name: "{{ email_not }}"
|
||||
organization: Default
|
||||
state: absent
|
||||
|
||||
- name: Delete webhook notification
|
||||
tower_notification:
|
||||
tower_notification_template:
|
||||
name: "{{ webhook_not }}"
|
||||
organization: Default
|
||||
state: absent
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
---
|
||||
- name: Generate names
|
||||
set_fact:
|
||||
slack_not: "AWX-Collection-tests-tower_notification-slack-not-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}"
|
||||
webhook_not: "AWX-Collection-tests-tower_notification-wehbook-not-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}"
|
||||
email_not: "AWX-Collection-tests-tower_notification-email-not-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}"
|
||||
twillo_not: "AWX-Collection-tests-tower_notification-twillo-not-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}"
|
||||
pd_not: "AWX-Collection-tests-tower_notification-pd-not-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}"
|
||||
irc_not: "AWX-Collection-tests-tower_notification-irc-not-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}"
|
||||
slack_not: "AWX-Collection-tests-tower_notification_template-slack-not-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}"
|
||||
webhook_not: "AWX-Collection-tests-tower_notification_template-wehbook-not-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}"
|
||||
email_not: "AWX-Collection-tests-tower_notification_template-email-not-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}"
|
||||
twillo_not: "AWX-Collection-tests-tower_notification_template-twillo-not-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}"
|
||||
pd_not: "AWX-Collection-tests-tower_notification_template-pd-not-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}"
|
||||
irc_not: "AWX-Collection-tests-tower_notification_template-irc-not-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}"
|
||||
|
||||
- name: Test deprecation warnings
|
||||
tower_notification:
|
||||
- name: Test deprecation warnings with legacy name
|
||||
tower_notification_template:
|
||||
name: "{{ slack_not }}"
|
||||
organization: Default
|
||||
notification_type: slack
|
||||
@@ -54,7 +54,7 @@
|
||||
- result['deprecations'] | length() == 25
|
||||
|
||||
- name: Create Slack notification with custom messages
|
||||
tower_notification:
|
||||
tower_notification_template:
|
||||
name: "{{ slack_not }}"
|
||||
organization: Default
|
||||
notification_type: slack
|
||||
@@ -76,7 +76,7 @@
|
||||
- result is changed
|
||||
|
||||
- name: Delete Slack notification
|
||||
tower_notification:
|
||||
tower_notification_template:
|
||||
name: "{{ slack_not }}"
|
||||
organization: Default
|
||||
state: absent
|
||||
@@ -87,7 +87,7 @@
|
||||
- result is changed
|
||||
|
||||
- name: Add webhook notification
|
||||
tower_notification:
|
||||
tower_notification_template:
|
||||
name: "{{ webhook_not }}"
|
||||
organization: Default
|
||||
notification_type: webhook
|
||||
@@ -102,7 +102,7 @@
|
||||
- result is changed
|
||||
|
||||
- name: Delete webhook notification
|
||||
tower_notification:
|
||||
tower_notification_template:
|
||||
name: "{{ webhook_not }}"
|
||||
organization: Default
|
||||
state: absent
|
||||
@@ -113,7 +113,7 @@
|
||||
- result is changed
|
||||
|
||||
- name: Add email notification
|
||||
tower_notification:
|
||||
tower_notification_template:
|
||||
name: "{{ email_not }}"
|
||||
organization: Default
|
||||
notification_type: email
|
||||
@@ -134,7 +134,7 @@
|
||||
- result is changed
|
||||
|
||||
- name: Delete email notification
|
||||
tower_notification:
|
||||
tower_notification_template:
|
||||
name: "{{ email_not }}"
|
||||
organization: Default
|
||||
state: absent
|
||||
@@ -145,7 +145,7 @@
|
||||
- result is changed
|
||||
|
||||
- name: Add twilio notification
|
||||
tower_notification:
|
||||
tower_notification_template:
|
||||
name: "{{ twillo_not }}"
|
||||
organization: Default
|
||||
notification_type: twilio
|
||||
@@ -162,7 +162,7 @@
|
||||
- result is changed
|
||||
|
||||
- name: Delete twilio notification
|
||||
tower_notification:
|
||||
tower_notification_template:
|
||||
name: "{{ twillo_not }}"
|
||||
organization: Default
|
||||
state: absent
|
||||
@@ -173,7 +173,7 @@
|
||||
- result is changed
|
||||
|
||||
- name: Add PagerDuty notification
|
||||
tower_notification:
|
||||
tower_notification_template:
|
||||
name: "{{ pd_not }}"
|
||||
organization: Default
|
||||
notification_type: pagerduty
|
||||
@@ -189,7 +189,7 @@
|
||||
- result is changed
|
||||
|
||||
- name: Delete PagerDuty notification
|
||||
tower_notification:
|
||||
tower_notification_template:
|
||||
name: "{{ pd_not }}"
|
||||
organization: Default
|
||||
state: absent
|
||||
@@ -200,7 +200,7 @@
|
||||
- result is changed
|
||||
|
||||
- name: Add IRC notification
|
||||
tower_notification:
|
||||
tower_notification_template:
|
||||
name: "{{ irc_not }}"
|
||||
organization: Default
|
||||
notification_type: irc
|
||||
@@ -219,7 +219,7 @@
|
||||
- result is changed
|
||||
|
||||
- name: Delete IRC notification
|
||||
tower_notification:
|
||||
tower_notification_template:
|
||||
name: "{{ irc_not }}"
|
||||
organization: Default
|
||||
state: absent
|
||||
@@ -1,74 +1,112 @@
|
||||
---
|
||||
- name: Generate a test id
|
||||
set_fact:
|
||||
test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}"
|
||||
|
||||
- name: Generate names
|
||||
set_fact:
|
||||
username: "AWX-Collection-tests-tower_role-user-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}"
|
||||
username: "AWX-Collection-tests-tower_role-user-{{ test_id }}"
|
||||
project_name: "AWX-Collection-tests-tower_role-project-{{ test_id }}"
|
||||
|
||||
- name: Create a User
|
||||
tower_user:
|
||||
first_name: Joe
|
||||
last_name: User
|
||||
username: "{{ username }}"
|
||||
password: "{{ 65535 | random | to_uuid }}"
|
||||
email: joe@example.org
|
||||
state: present
|
||||
register: result
|
||||
- block:
|
||||
- name: Create a User
|
||||
tower_user:
|
||||
first_name: Joe
|
||||
last_name: User
|
||||
username: "{{ username }}"
|
||||
password: "{{ 65535 | random | to_uuid }}"
|
||||
email: joe@example.org
|
||||
state: present
|
||||
register: result
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- "result is changed"
|
||||
- assert:
|
||||
that:
|
||||
- "result is changed"
|
||||
|
||||
- name: Add Joe to the update role of the default Project
|
||||
tower_role:
|
||||
user: "{{ username }}"
|
||||
role: update
|
||||
project: Demo Project
|
||||
state: "{{ item }}"
|
||||
register: result
|
||||
with_items:
|
||||
- "present"
|
||||
- "absent"
|
||||
- name: Create a project
|
||||
tower_project:
|
||||
name: "{{ project_name }}"
|
||||
organization: Default
|
||||
scm_type: git
|
||||
scm_url: https://github.com/ansible/test-playbooks
|
||||
wait: false
|
||||
register: project_info
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- "result is changed"
|
||||
- assert:
|
||||
that:
|
||||
- project_info is changed
|
||||
|
||||
- name: Create a workflow
|
||||
tower_workflow_job_template:
|
||||
name: test-role-workflow
|
||||
organization: Default
|
||||
state: present
|
||||
- name: Add Joe to the update role of the default Project
|
||||
tower_role:
|
||||
user: "{{ username }}"
|
||||
role: update
|
||||
project: "Demo Project"
|
||||
state: "{{ item }}"
|
||||
register: result
|
||||
with_items:
|
||||
- "present"
|
||||
- "absent"
|
||||
|
||||
- name: Add Joe to workflow execute role
|
||||
tower_role:
|
||||
user: "{{ username }}"
|
||||
role: execute
|
||||
workflow: test-role-workflow
|
||||
state: present
|
||||
register: result
|
||||
- assert:
|
||||
that:
|
||||
- "result is changed"
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- "result is changed"
|
||||
- name: Add Joe to the new project by ID
|
||||
tower_role:
|
||||
user: "{{ username }}"
|
||||
role: update
|
||||
project: "{{ project_info['id'] }}"
|
||||
state: "{{ item }}"
|
||||
register: result
|
||||
with_items:
|
||||
- "present"
|
||||
- "absent"
|
||||
|
||||
- name: Add Joe to workflow execute role, no-op
|
||||
tower_role:
|
||||
user: "{{ username }}"
|
||||
role: execute
|
||||
workflow: test-role-workflow
|
||||
state: present
|
||||
register: result
|
||||
- assert:
|
||||
that:
|
||||
- "result is changed"
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- "result is not changed"
|
||||
- name: Create a workflow
|
||||
tower_workflow_job_template:
|
||||
name: test-role-workflow
|
||||
organization: Default
|
||||
state: present
|
||||
|
||||
- name: Delete a User
|
||||
tower_user:
|
||||
username: "{{ username }}"
|
||||
email: joe@example.org
|
||||
state: absent
|
||||
register: result
|
||||
- name: Add Joe to workflow execute role
|
||||
tower_role:
|
||||
user: "{{ username }}"
|
||||
role: execute
|
||||
workflow: test-role-workflow
|
||||
state: present
|
||||
register: result
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- "result is changed"
|
||||
- assert:
|
||||
that:
|
||||
- "result is changed"
|
||||
|
||||
- name: Add Joe to workflow execute role, no-op
|
||||
tower_role:
|
||||
user: "{{ username }}"
|
||||
role: execute
|
||||
workflow: test-role-workflow
|
||||
state: present
|
||||
register: result
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- "result is not changed"
|
||||
|
||||
always:
|
||||
- name: Delete a User
|
||||
tower_user:
|
||||
username: "{{ username }}"
|
||||
email: joe@example.org
|
||||
state: absent
|
||||
register: result
|
||||
|
||||
- name: Delete the project
|
||||
tower_project:
|
||||
name: "{{ project_name }}"
|
||||
organization: Default
|
||||
state: absent
|
||||
register: result
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
jt2_name: "AWX-Collection-tests-tower_workflow_job_template-jt2-{{ test_id }}"
|
||||
wfjt_name: "AWX-Collection-tests-tower_workflow_job_template-wfjt-{{ test_id }}"
|
||||
email_not: "AWX-Collection-tests-tower_job_template-email-not-{{ test_id }}"
|
||||
webhook_not: "AWX-Collection-tests-tower_notification-wehbook-not-{{ test_id }}"
|
||||
webhook_not: "AWX-Collection-tests-tower_notification_template-wehbook-not-{{ test_id }}"
|
||||
|
||||
- name: Create an SCM Credential
|
||||
tower_credential:
|
||||
@@ -25,7 +25,7 @@
|
||||
- "result is changed"
|
||||
|
||||
- name: Add email notification
|
||||
tower_notification:
|
||||
tower_notification_template:
|
||||
name: "{{ email_not }}"
|
||||
organization: Default
|
||||
notification_type: email
|
||||
@@ -41,7 +41,7 @@
|
||||
state: present
|
||||
|
||||
- name: Add webhook notification
|
||||
tower_notification:
|
||||
tower_notification_template:
|
||||
name: "{{ webhook_not }}"
|
||||
organization: Default
|
||||
notification_type: webhook
|
||||
@@ -264,13 +264,13 @@
|
||||
- "result is changed"
|
||||
|
||||
- name: Delete email notification
|
||||
tower_notification:
|
||||
tower_notification_template:
|
||||
name: "{{ email_not }}"
|
||||
organization: Default
|
||||
state: absent
|
||||
|
||||
- name: Delete webhook notification
|
||||
tower_notification:
|
||||
tower_notification_template:
|
||||
name: "{{ webhook_not }}"
|
||||
organization: Default
|
||||
state: absent
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
- assert:
|
||||
that:
|
||||
- result is failed
|
||||
- "'Monitoring aborted due to timeout' in result.msg"
|
||||
- "'Monitoring of Workflow Job - {{ wfjt_name1 }} aborted due to timeout' in result.msg"
|
||||
|
||||
- name: Kick off a workflow and wait for it
|
||||
tower_workflow_launch:
|
||||
|
||||
@@ -3,4 +3,13 @@ plugins/modules/tower_send.py validate-modules:deprecation-mismatch
|
||||
plugins/modules/tower_workflow_template.py validate-modules:deprecation-mismatch
|
||||
plugins/modules/tower_credential.py pylint:wrong-collection-deprecated-version-tag
|
||||
plugins/modules/tower_job_wait.py pylint:wrong-collection-deprecated-version-tag
|
||||
plugins/modules/tower_notification.py pylint:wrong-collection-deprecated-version-tag
|
||||
plugins/modules/tower_notification_template.py pylint:wrong-collection-deprecated-version-tag
|
||||
plugins/inventory/tower.py pylint:raise-missing-from
|
||||
plugins/inventory/tower.py pylint:super-with-arguments
|
||||
plugins/lookup/tower_schedule_rrule.py pylint:raise-missing-from
|
||||
plugins/module_utils/tower_api.py pylint:super-with-arguments
|
||||
plugins/module_utils/tower_awxkit.py pylint:super-with-arguments
|
||||
plugins/module_utils/tower_legacy.py pylint:super-with-arguments
|
||||
plugins/module_utils/tower_module.py pylint:super-with-arguments
|
||||
plugins/module_utils/tower_module.py pylint:raise-missing-from
|
||||
test/awx/conftest.py pylint:raise-missing-from
|
||||
|
||||
@@ -4,3 +4,12 @@ plugins/modules/tower_send.py validate-modules:deprecation-mismatch
|
||||
plugins/modules/tower_send.py validate-modules:invalid-documentation
|
||||
plugins/modules/tower_workflow_template.py validate-modules:deprecation-mismatch
|
||||
plugins/modules/tower_workflow_template.py validate-modules:invalid-documentation
|
||||
plugins/inventory/tower.py pylint:raise-missing-from
|
||||
plugins/inventory/tower.py pylint:super-with-arguments
|
||||
plugins/lookup/tower_schedule_rrule.py pylint:raise-missing-from
|
||||
plugins/module_utils/tower_api.py pylint:super-with-arguments
|
||||
plugins/module_utils/tower_awxkit.py pylint:super-with-arguments
|
||||
plugins/module_utils/tower_legacy.py pylint:super-with-arguments
|
||||
plugins/module_utils/tower_module.py pylint:super-with-arguments
|
||||
plugins/module_utils/tower_module.py pylint:raise-missing-from
|
||||
test/awx/conftest.py pylint:raise-missing-from
|
||||
|
||||
@@ -96,7 +96,7 @@ EXAMPLES = '''
|
||||
{% endif %}
|
||||
'''
|
||||
|
||||
from ..module_utils.tower_api import TowerModule
|
||||
from ..module_utils.tower_api import TowerAPIModule
|
||||
|
||||
|
||||
def main():
|
||||
@@ -142,7 +142,7 @@ def main():
|
||||
)
|
||||
|
||||
# Create a module for ourselves
|
||||
module = TowerModule(argument_spec=argument_spec)
|
||||
module = TowerAPIModule(argument_spec=argument_spec)
|
||||
|
||||
# Extract our parameters
|
||||
{% for option in item['json']['actions']['POST'] %}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
- name: Set the collection version in the tower_api.py file
|
||||
replace:
|
||||
path: "{{ collection_path }}/plugins/module_utils/tower_api.py"
|
||||
regexp: '^ _COLLECTION_VERSION = "devel"'
|
||||
regexp: '^ _COLLECTION_VERSION = "0.0.1-devel"'
|
||||
replace: ' _COLLECTION_VERSION = "{{ collection_version }}"'
|
||||
when:
|
||||
- "awx_template_version | default(True)"
|
||||
|
||||
@@ -80,6 +80,7 @@ Notable releases of the `{{ collection_namespace }}.{{ collection_package }}` co
|
||||
|
||||
The following notes are changes that may require changes to playbooks:
|
||||
|
||||
- The module tower_notification was renamed tower_notification_template. In ansible >= 2.10 there is a seemless redirect. Ansible 2.9 does not respect the redirect.
|
||||
- When a project is created, it will wait for the update/sync to finish by default; this can be turned off with the `wait` parameter, if desired.
|
||||
- Creating a "scan" type job template is no longer supported.
|
||||
- Specifying a custom certificate via the `TOWER_CERTIFICATE` environment variable no longer works.
|
||||
@@ -100,9 +101,9 @@ The following notes are changes that may require changes to playbooks:
|
||||
- Specified `tower_config` file used to handle `k=v` pairs on a single line; this is no longer supported. Please use a file formatted as `yaml`, `json` or `ini` only.
|
||||
- Some return values (e.g., `credential_type`) have been removed. Use of `id` is recommended.
|
||||
- `tower_job_template` no longer supports the deprecated `extra_vars_path` parameter, please use `extra_vars` with the lookup plugin to replace this functionality.
|
||||
- The `notification_configuration` parameter of `tower_notification` has changed from a string to a dict. Please use the `lookup` plugin to read an existing file into a dict.
|
||||
- The `notification_configuration` parameter of `tower_notification_template` has changed from a string to a dict. Please use the `lookup` plugin to read an existing file into a dict.
|
||||
- `tower_credential` no longer supports passing a file name to ssh_key_data.
|
||||
- The HipChat `notification_type` has been removed and can no longer be created using the `tower_notification` module.
|
||||
- The HipChat `notification_type` has been removed and can no longer be created using the `tower_notification_template` module.
|
||||
|
||||
{% if collection_package | lower() == "awx" %}
|
||||
## Running Unit Tests
|
||||
|
||||
Reference in New Issue
Block a user